kibana/test/api_integration/apis/esql/errors.ts
Alejandro Fernández Haro 52ab19db2d
Upgrade ES client to 9.0.0-alpha.3 (#208776)
## Summary

Updating the ES client to 9.0. 

Resolves #116102

## What changes?

**Breaking change**: `body` has been removed.

Most of the changes are about bringing all the content inside the body
as a root attribute to the API params:

```diff
const response = await client.search({
  index: 'test',
-  body: {
    query: {
      match_all: {}
    }
-  }
})
```

For this reason, enabling the "Hide whitespace changes" option when
reviewing is recommended.

Some exceptions to this rule:

* Bulk APIs replace the `body` array with `operations` array (direct
replacement)
* Index Put Settings API replace `body` array with `settings` (direct
replacement)
* Msearch replaces the `body` array with `searches` array (direct
replacement)
* Document Index API replaces `body` with `document` (direct
replacement)
* Create Repository replaces `body` with `repository` (direct
replacement)

Because of a known issue in the client
(https://github.com/elastic/elasticsearch-js/issues/2584), there's still
an escape hatch to send data in the body in case the specific use case
requires it via `// @ts-expect-error elasticsearch@9.0.0
https://github.com/elastic/elasticsearch-js/issues/2584`, but it
shouldn't be abused because we lose types. In this PR we've used it in
those scenarios where we reuse the response of a GET as the body of a
PUT/POST.

### Other changes

* `estypes` can be imported from the root of the library as `import type
{ estypes } from '@elastic/elasticsearch';`
* `estypesWithBody` have been removed
* `requestTimeout`'s 30s default has been removed in the client. This PR
explicitly adds the setting in all client usages.


### Identify risks

- [x] The client places unknown properties as querystring, risking body
params leaking there, and causing 400 errors from ES => Solved by
forcing `body` usage there via `// @ts-expect-error elasticsearch@9.0.0
https://github.com/elastic/elasticsearch-js/issues/2584`. The next
version of the client will address this.
- [x] We need to run the MKI tests to make sure that we're not breaking
anything there =>
https://elastic.slack.com/archives/C04HT4P1YS3/p1739528112482629?thread_ts=1739480136.231439&cid=C04HT4P1YS3

---------

Co-authored-by: Gloria Hornero <gloria.hornero@elastic.co>
2025-02-25 14:37:23 +00:00

273 lines
9.2 KiB
TypeScript

/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the "Elastic License
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
* Public License v 1"; you may not use this file except in compliance with, at
* your election, the "Elastic License 2.0", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/
import Fs from 'fs';
import Path from 'path';
import expect from '@kbn/expect';
import { MappingProperty } from '@elastic/elasticsearch/lib/api/types';
import { REPO_ROOT } from '@kbn/repo-info';
import uniqBy from 'lodash/uniqBy';
import { groupBy, mapValues } from 'lodash';
import { FtrProviderContext } from '../../ftr_provider_context';
function getConfigPath() {
return Path.resolve(
REPO_ROOT,
'src/platform/packages/shared/kbn-esql-validation-autocomplete/src/validation'
);
}
function getSetupPath() {
return Path.resolve(getConfigPath(), 'esql_validation_meta_tests.json');
}
function getMissmatchedPath() {
return Path.resolve(getConfigPath(), 'esql_validation_missmatches.json');
}
function readSetupFromESQLPackage() {
const esqlPackagePath = getSetupPath();
const json = Fs.readFileSync(esqlPackagePath, 'utf8');
const esqlPackage = JSON.parse(json);
return esqlPackage;
}
function createIndexRequest(
index: string,
fields: Array<{ name: string; type: string }>,
stringType: 'text' | 'keyword',
numberType: 'integer' | 'double' | 'long' | 'unsigned_long'
) {
return {
index,
mappings: {
properties: fields.reduce(
(memo: Record<string, MappingProperty>, { name, type }: { name: string; type: string }) => {
let esType = type;
if (type === 'string') {
esType = stringType;
}
if (type === 'number') {
esType = numberType;
}
if (type === 'cartesian_point') {
esType = 'point';
}
if (type === 'cartesian_shape') {
esType = 'shape';
}
if (type === 'unsupported' || type === 'function_named_parameters') {
esType = 'integer_range';
}
memo[name] = { type: esType } as MappingProperty;
return memo;
},
{}
),
},
};
}
interface JSONConfig {
testCases: Array<{ query: string; error: string[] }>;
indexes: string[];
policies: Array<{
name: string;
sourceIndices: string[];
matchField: string;
enrichFields: string[];
}>;
unsupported_field: Array<{ name: string; type: string }>;
fields: Array<{ name: string; type: string }>;
enrichFields: Array<{ name: string; type: string }>;
}
export interface EsqlResultColumn {
name: string;
type: string;
}
export type EsqlResultRow = Array<string | null>;
export interface EsqlTable {
columns: EsqlResultColumn[];
values: EsqlResultRow[];
}
function parseConfig(config: JSONConfig) {
return {
queryToErrors: config.testCases,
indexes: config.indexes,
policies: config.policies.map(({ name }: { name: string }) => name),
};
}
export default function ({ getService }: FtrProviderContext) {
const es = getService('es');
const log = getService('log');
// Send raw ES|QL query directly to ES endpoint bypassing Kibana
// as we do not need more overhead here
async function sendESQLQuery(query: string): Promise<{
resp: EsqlTable | undefined;
error: { message: string } | undefined;
}> {
try {
const resp = await es.transport.request<EsqlTable>({
method: 'POST',
path: '/_query',
body: {
query,
},
});
return { resp, error: undefined };
} catch (e) {
return { resp: undefined, error: { message: e.meta.body.error.root_cause[0].reason } };
}
}
describe('error messages', () => {
const config = readSetupFromESQLPackage();
const { queryToErrors, indexes, policies } = parseConfig(config);
const missmatches: Array<{ query: string; error: string }> = [];
// Swap these for DEBUG/further investigation on ES bugs
const stringVariants = ['text', 'keyword'] as const;
const numberVariants = ['integer', 'long', 'double', 'long'] as const;
async function cleanup() {
// clean it up all indexes and policies
log.info(`cleaning up all indexes: ${indexes.join(', ')}`);
await es.indices.delete({ index: indexes, ignore_unavailable: true }, { ignore: [404] });
await es.indices.delete(
{ index: config.policies[0].sourceIndices[0], ignore_unavailable: true },
{ ignore: [404] }
);
for (const policy of policies) {
log.info(`deleting policy "${policy}"...`);
// TODO: Maybe `policy` -> `policy.name`?
await es.enrich.deletePolicy({ name: policy }, { ignore: [404] });
}
}
after(async () => {
if (missmatches.length) {
const distinctMissmatches = uniqBy(
missmatches,
(missmatch) => missmatch.query + missmatch.error
);
const missmatchesGrouped = mapValues(
groupBy(distinctMissmatches, (missmatch) => missmatch.error),
(list) => list.map(({ query }) => query)
);
log.info(`writing ${Object.keys(missmatchesGrouped).length} missmatches to file...`);
Fs.writeFileSync(getMissmatchedPath(), JSON.stringify(missmatchesGrouped, null, 2));
}
});
for (const stringFieldType of stringVariants) {
for (const numberFieldType of numberVariants) {
describe(`Using string field type: ${stringFieldType} and number field type: ${numberFieldType}`, () => {
before(async () => {
await cleanup();
log.info(`creating ${indexes.length} indexes...`);
for (const index of indexes) {
// setup all indexes, mappings and policies here
log.info(
`creating a index "${index}" with mapping...\n${JSON.stringify(config.fields)}`
);
const fieldsExcludingCounterType = config.fields.filter(
// ES|QL supports counter_integer, counter_long, counter_double, date_period, etc.
// but they are not types suitable for Elasticsearch indices
(c: { type: string }) =>
!c.type.startsWith('counter_') &&
c.type !== 'date_period' &&
c.type !== 'time_duration' &&
c.type !== 'null' &&
c.type !== 'time_literal'
);
await es.indices.create(
createIndexRequest(
index,
/unsupported/.test(index) ? config.unsupported_field : fieldsExcludingCounterType,
stringFieldType,
numberFieldType
),
{ ignore: [409] }
);
}
for (const { sourceIndices, matchField } of config.policies.slice(0, 1)) {
const enrichFields = [{ name: matchField, type: 'string' }].concat(
config.enrichFields
);
log.info(`creating a index "${sourceIndices[0]}" for policy with mapping...`);
await es.indices.create(
createIndexRequest(
sourceIndices[0],
enrichFields,
stringFieldType,
numberFieldType
),
{
ignore: [409],
}
);
}
log.info(`creating ${policies.length} policies...`);
for (const { name, sourceIndices, matchField, enrichFields } of config.policies) {
log.info(`creating a policy "${name}"...`);
await es.enrich.putPolicy(
{
name,
match: {
indices: sourceIndices,
match_field: matchField,
enrich_fields: enrichFields,
},
},
{ ignore: [409] }
);
log.info(`executing policy "${name}"...`);
await es.enrich.executePolicy({ name });
}
});
after(async () => {
await cleanup();
});
it(`Checking error messages`, async () => {
for (const { query, error } of queryToErrors) {
const jsonBody = await sendESQLQuery(query);
const clientSideHasError = Boolean(error.length);
const serverSideHasError = Boolean(jsonBody.error);
if (clientSideHasError !== serverSideHasError) {
if (clientSideHasError) {
// in this case it's a problem, so fail the test
expect().fail(`Client side errored but ES server did not: ${query}`);
}
if (serverSideHasError) {
// in this case client side validator can improve, but it's not hard failure
// rather log it as it can be a useful to investigate a bug on the ES implementation side for some type combination
missmatches.push({ query, error: jsonBody.error!.message });
}
}
}
});
});
}
}
});
}