-
Notifications
You must be signed in to change notification settings - Fork 8.3k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
[8.x] [ES|QL] Load fields of indices in `JOIN` command (#20…
…7375) (#208160) # Backport This will backport the following commits from `main` to `8.x`: - [[ES|QL] Load fields of indices in `JOIN` command (#207375)](#207375) <!--- Backport version: 9.4.3 --> ### Questions ? Please refer to the [Backport tool documentation](https://github.com/sqren/backport) <!--BACKPORT [{"author":{"name":"Vadim Kibana","email":"[email protected]"},"sourceCommit":{"committedDate":"2025-01-24T10:09:14Z","message":"[ES|QL] Load fields of indices in `JOIN` command (#207375)\n\n## Summary\r\n\r\nCloses https://github.com/elastic/kibana/issues/207171\r\n\r\n### Testing\r\n\r\nFollow [*Testing*\r\ninstructions](#205762 (comment)) to\r\nsetup sample data.\r\n\r\nGo to Discover and enter query:\r\n\r\n```\r\nFROM kibana_sample_data_ecommerce | LOOKUP JOIN lookup_index ON currency | LIMIT 3 | KEEP continenet\r\n```\r\n\r\n<img width=\"1235\" alt=\"image\"\r\nsrc=\"https://github.com/user-attachments/assets/61877a1f-6915-42e5-8f4a-efa0d4d2a0b1\"\r\n/>\r\n\r\nNow the field `continenet` passes validation and is suggested in\r\nautocomplete.\r\n\r\n\r\n### Checklist\r\n\r\n- [x] [Unit or functional\r\ntests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)\r\nwere updated or added to match the most common scenarios\r\n\r\n---------\r\n\r\nCo-authored-by: Stratoula Kalafateli <[email protected]>","sha":"8344ea17f514ab1993b26703e39f4d99f8db95de","branchLabelMapping":{"^v9.0.0$":"main","^v8.18.0$":"8.x","^v(\\d+).(\\d+).\\d+$":"$1.$2"}},"sourcePullRequest":{"labels":["review","release_note:skip","v9.0.0","backport:prev-minor","Feature:ES|QL","Team:ESQL","v8.18.0"],"title":"[ES|QL] Load fields of indices in `JOIN` command","number":207375,"url":"https://github.com/elastic/kibana/pull/207375","mergeCommit":{"message":"[ES|QL] Load fields of indices in `JOIN` command (#207375)\n\n## Summary\r\n\r\nCloses https://github.com/elastic/kibana/issues/207171\r\n\r\n### Testing\r\n\r\nFollow [*Testing*\r\ninstructions](#205762 (comment)) to\r\nsetup sample data.\r\n\r\nGo to Discover and enter query:\r\n\r\n```\r\nFROM kibana_sample_data_ecommerce | LOOKUP JOIN lookup_index ON currency | LIMIT 3 | KEEP continenet\r\n```\r\n\r\n<img width=\"1235\" alt=\"image\"\r\nsrc=\"https://github.com/user-attachments/assets/61877a1f-6915-42e5-8f4a-efa0d4d2a0b1\"\r\n/>\r\n\r\nNow the field `continenet` passes validation and is suggested in\r\nautocomplete.\r\n\r\n\r\n### Checklist\r\n\r\n- [x] [Unit or functional\r\ntests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)\r\nwere updated or added to match the most common scenarios\r\n\r\n---------\r\n\r\nCo-authored-by: Stratoula Kalafateli <[email protected]>","sha":"8344ea17f514ab1993b26703e39f4d99f8db95de"}},"sourceBranch":"main","suggestedTargetBranches":["8.x"],"targetPullRequestStates":[{"branch":"main","label":"v9.0.0","branchLabelMappingKey":"^v9.0.0$","isSourceBranch":true,"state":"MERGED","url":"https://github.com/elastic/kibana/pull/207375","number":207375,"mergeCommit":{"message":"[ES|QL] Load fields of indices in `JOIN` command (#207375)\n\n## Summary\r\n\r\nCloses https://github.com/elastic/kibana/issues/207171\r\n\r\n### Testing\r\n\r\nFollow [*Testing*\r\ninstructions](#205762 (comment)) to\r\nsetup sample data.\r\n\r\nGo to Discover and enter query:\r\n\r\n```\r\nFROM kibana_sample_data_ecommerce | LOOKUP JOIN lookup_index ON currency | LIMIT 3 | KEEP continenet\r\n```\r\n\r\n<img width=\"1235\" alt=\"image\"\r\nsrc=\"https://github.com/user-attachments/assets/61877a1f-6915-42e5-8f4a-efa0d4d2a0b1\"\r\n/>\r\n\r\nNow the field `continenet` passes validation and is suggested in\r\nautocomplete.\r\n\r\n\r\n### Checklist\r\n\r\n- [x] [Unit or functional\r\ntests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)\r\nwere updated or added to match the most common scenarios\r\n\r\n---------\r\n\r\nCo-authored-by: Stratoula Kalafateli <[email protected]>","sha":"8344ea17f514ab1993b26703e39f4d99f8db95de"}},{"branch":"8.x","label":"v8.18.0","branchLabelMappingKey":"^v8.18.0$","isSourceBranch":false,"state":"NOT_CREATED"}]}] BACKPORT--> Co-authored-by: Vadim Kibana <[email protected]>
- Loading branch information
1 parent
ef88984
commit ce6d90e
Showing
8 changed files
with
338 additions
and
4 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
166 changes: 166 additions & 0 deletions
166
src/platform/packages/shared/kbn-esql-ast/src/mutate/commands/join/index.test.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,166 @@ | ||
/* | ||
* 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 * as commands from '..'; | ||
import { EsqlQuery } from '../../../query'; | ||
|
||
describe('commands.where', () => { | ||
describe('.list()', () => { | ||
it('lists all "JOIN" commands', () => { | ||
const src = | ||
'FROM index | LIMIT 1 | JOIN join_index1 ON join_field1 | WHERE b == 2 | JOIN join_index2 ON join_field2 | LIMIT 1'; | ||
const query = EsqlQuery.fromSrc(src); | ||
|
||
const nodes = [...commands.join.list(query.ast)]; | ||
|
||
expect(nodes).toMatchObject([ | ||
{ | ||
type: 'command', | ||
name: 'join', | ||
args: [ | ||
{ | ||
type: 'identifier', | ||
name: 'join_index1', | ||
}, | ||
{}, | ||
], | ||
}, | ||
{ | ||
type: 'command', | ||
name: 'join', | ||
args: [ | ||
{ | ||
type: 'identifier', | ||
name: 'join_index2', | ||
}, | ||
{}, | ||
], | ||
}, | ||
]); | ||
}); | ||
}); | ||
|
||
describe('.byIndex()', () => { | ||
it('retrieves the specific "WHERE" command by index', () => { | ||
const src = | ||
'FROM index | LIMIT 1 | JOIN join_index1 ON join_field1 | WHERE b == 2 | JOIN join_index2 ON join_field2 | LIMIT 1'; | ||
const query = EsqlQuery.fromSrc(src); | ||
|
||
const node1 = commands.join.byIndex(query.ast, 1); | ||
const node2 = commands.join.byIndex(query.ast, 0); | ||
|
||
expect(node1).toMatchObject({ | ||
type: 'command', | ||
name: 'join', | ||
args: [ | ||
{ | ||
type: 'identifier', | ||
name: 'join_index2', | ||
}, | ||
{}, | ||
], | ||
}); | ||
expect(node2).toMatchObject({ | ||
type: 'command', | ||
name: 'join', | ||
args: [ | ||
{ | ||
type: 'identifier', | ||
name: 'join_index1', | ||
}, | ||
{}, | ||
], | ||
}); | ||
}); | ||
}); | ||
|
||
describe('.summarize', () => { | ||
it('returns target index fields', () => { | ||
const src = | ||
'FROM index | LIMIT 1 | JOIN join_index1 ON join_field1 | WHERE b == 2 | JOIN join_index2 ON join_field2 | LIMIT 1'; | ||
const query = EsqlQuery.fromSrc(src); | ||
const summary = commands.join.summarize(query.ast); | ||
|
||
expect(summary).toMatchObject([ | ||
{ | ||
target: { | ||
index: { | ||
type: 'identifier', | ||
name: 'join_index1', | ||
}, | ||
}, | ||
}, | ||
{ | ||
target: { | ||
index: { | ||
type: 'identifier', | ||
name: 'join_index2', | ||
}, | ||
}, | ||
}, | ||
]); | ||
}); | ||
|
||
it('returns target aliases', () => { | ||
const src = | ||
'FROM index | LIMIT 1 | JOIN join_index1 AS a ON join_field1 | WHERE b == 2 | JOIN join_index2 AS b ON join_field2 | LIMIT 1'; | ||
const query = EsqlQuery.fromSrc(src); | ||
const summary = commands.join.summarize(query.ast); | ||
|
||
expect(summary).toMatchObject([ | ||
{ | ||
target: { | ||
alias: { | ||
type: 'identifier', | ||
name: 'a', | ||
}, | ||
}, | ||
}, | ||
{ | ||
target: { | ||
alias: { | ||
type: 'identifier', | ||
name: 'b', | ||
}, | ||
}, | ||
}, | ||
]); | ||
}); | ||
|
||
it('captures join conditions', () => { | ||
const src = | ||
'FROM index | LIMIT 1 | JOIN join_index1 AS a ON join_field1 | WHERE b == 2 | JOIN join_index2 AS b ON join_field2, join_field3 | LIMIT 1'; | ||
const query = EsqlQuery.fromSrc(src); | ||
const summary = commands.join.summarize(query.ast); | ||
|
||
expect(summary).toMatchObject([ | ||
{ | ||
conditions: [ | ||
{ | ||
type: 'column', | ||
name: 'join_field1', | ||
}, | ||
], | ||
}, | ||
{ | ||
conditions: [ | ||
{ | ||
type: 'column', | ||
name: 'join_field2', | ||
}, | ||
{ | ||
type: 'column', | ||
name: 'join_field3', | ||
}, | ||
], | ||
}, | ||
]); | ||
}); | ||
}); | ||
}); |
101 changes: 101 additions & 0 deletions
101
src/platform/packages/shared/kbn-esql-ast/src/mutate/commands/join/index.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,101 @@ | ||
/* | ||
* 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 type { WalkerAstNode } from '../../../walker/walker'; | ||
import { isAsExpression } from '../../../ast/helpers'; | ||
import { Walker } from '../../../walker'; | ||
import type { | ||
ESQLAstExpression, | ||
ESQLAstJoinCommand, | ||
ESQLAstQueryExpression, | ||
ESQLCommand, | ||
ESQLIdentifier, | ||
} from '../../../types'; | ||
import * as generic from '../../generic'; | ||
|
||
/** | ||
* Lists all "JOIN" commands in the query AST. | ||
* | ||
* @param ast The root AST node to search for "JOIN" commands. | ||
* @returns A collection of "JOIN" commands. | ||
*/ | ||
export const list = (ast: ESQLAstQueryExpression): IterableIterator<ESQLAstJoinCommand> => { | ||
return generic.commands.list( | ||
ast, | ||
(cmd) => cmd.name === 'join' | ||
) as IterableIterator<ESQLAstJoinCommand>; | ||
}; | ||
|
||
/** | ||
* Retrieves the "JOIN" command at the specified index in order of appearance. | ||
* | ||
* @param ast The root AST node to search for "JOIN" commands. | ||
* @param index The index of the "JOIN" command to retrieve. | ||
* @returns The "JOIN" command at the specified index, if any. | ||
*/ | ||
export const byIndex = (ast: ESQLAstQueryExpression, index: number): ESQLCommand | undefined => { | ||
return [...list(ast)][index]; | ||
}; | ||
|
||
const getIdentifier = (node: WalkerAstNode): ESQLIdentifier => | ||
Walker.match(node, { | ||
type: 'identifier', | ||
}) as ESQLIdentifier; | ||
|
||
/** | ||
* Summarizes all JOIN commands in the query. | ||
* | ||
* @param query Query to summarize. | ||
* @returns Returns a list of summaries for all JOIN commands in the query in | ||
* order of appearance. | ||
*/ | ||
export const summarize = (query: ESQLAstQueryExpression): JoinCommandSummary[] => { | ||
const summaries: JoinCommandSummary[] = []; | ||
|
||
for (const command of list(query)) { | ||
const firstArg = command.args[0]; | ||
let index: ESQLIdentifier | undefined; | ||
let alias: ESQLIdentifier | undefined; | ||
const conditions: ESQLAstExpression[] = []; | ||
|
||
if (isAsExpression(firstArg)) { | ||
index = getIdentifier(firstArg.args[0]); | ||
alias = getIdentifier(firstArg.args[1]); | ||
} else { | ||
index = getIdentifier(firstArg); | ||
} | ||
|
||
const on = generic.commands.options.find(command, ({ name }) => name === 'on'); | ||
|
||
conditions.push(...((on?.args || []) as ESQLAstExpression[])); | ||
|
||
const target: JoinCommandTarget = { | ||
index: index!, | ||
alias, | ||
}; | ||
const summary: JoinCommandSummary = { | ||
target, | ||
conditions, | ||
}; | ||
|
||
summaries.push(summary); | ||
} | ||
|
||
return summaries; | ||
}; | ||
|
||
export interface JoinCommandSummary { | ||
target: JoinCommandTarget; | ||
conditions: ESQLAstExpression[]; | ||
} | ||
|
||
export interface JoinCommandTarget { | ||
index: ESQLIdentifier; | ||
alias?: ESQLIdentifier; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.