From 727463b3aad2c784aea77668435ef114361e6aa3 Mon Sep 17 00:00:00 2001 From: Harry Brundage Date: Mon, 8 Jan 2024 08:14:54 -0500 Subject: [PATCH] Support parsing and tagging lateral joins (#149) Postgres has a LATERAL keyword that modifies one of the `from_item`s in a selection. It doesn't have to be an explicit join statement but is most often used as such. This adds support for parsing the LATERAL keyword in front of each from_item type that supports it. See https://www.postgresql.org/docs/current/sql-select.html Fixes #122 --- src/syntax/ast.ts | 3 + src/syntax/select.ne | 20 ++++--- src/syntax/select.spec.ts | 117 ++++++++++++++++++++++++++++++++++++++ src/to-sql.ts | 6 ++ 4 files changed, 137 insertions(+), 9 deletions(-) diff --git a/src/syntax/ast.ts b/src/syntax/ast.ts index 6242048..285ae60 100644 --- a/src/syntax/ast.ts +++ b/src/syntax/ast.ts @@ -693,6 +693,7 @@ export type From = FromTable export interface FromCall extends ExprCall, PGNode { alias?: TableAliasName; join?: JoinClause | nil; + lateral?: true; withOrdinality?: boolean; }; @@ -716,6 +717,7 @@ export interface QNameMapped extends QNameAliased { export interface FromTable extends PGNode { type: 'table', name: QNameMapped; + lateral?: true; join?: JoinClause | nil; } @@ -723,6 +725,7 @@ export interface FromStatement extends PGNode { type: 'statement'; statement: SelectStatement; alias: string; + lateral?: true; columnNames?: Name[] | nil; db?: null | nil; join?: JoinClause | nil; diff --git a/src/syntax/select.ne b/src/syntax/select.ne index 653a377..638c29c 100644 --- a/src/syntax/select.ne +++ b/src/syntax/select.ne @@ -81,10 +81,11 @@ stb_table -> table_ref stb_opts:? {% x => { # Selects on subselects MUST have an alias -stb_statement -> selection_paren stb_opts {% x => track(x, { +stb_statement -> %kw_lateral:? selection_paren stb_opts {% x => track(x, { type: 'statement', - statement: unwrap(x[0]), - ...x[1], + statement: unwrap(x[1]), + ...x[0] && { lateral: true }, + ...x[2], }) %} @@ -94,16 +95,18 @@ select_values -> kw_values insert_values {% x => track(x, { }) %} -stb_call -> expr_function_call kw_withordinality:? stb_call_alias:? {% x => { - const withOrdinality = x[1]; - const alias = x[2]; +stb_call -> %kw_lateral:? expr_function_call kw_withordinality:? stb_call_alias:? {% x => { + const lateral = x[0]; + const withOrdinality = x[2]; + const alias = x[3]; if (!withOrdinality && !alias) { - return x[0]; + return x[1]; } return track(x, { - ...x[0], + ...x[1], + ...lateral && { lateral: true }, ... withOrdinality && { withOrdinality: true }, alias: alias ? asNameWithColumns(alias[0], alias[1]) : undefined, }); @@ -140,7 +143,6 @@ select_join_op | (%kw_full %kw_outer:? {% x => box(x, 'FULL JOIN') %}) - # SELECT x,y as YY,z select_what -> %kw_select select_distinct:? select_expr_list_aliased:? {% x => track(x, { columns: x[2], diff --git a/src/syntax/select.spec.ts b/src/syntax/select.spec.ts index 2835559..c7fab3d 100644 --- a/src/syntax/select.spec.ts +++ b/src/syntax/select.spec.ts @@ -583,6 +583,123 @@ describe('Select statements', () => { ] }); + checkSelect(` + select * from test + inner join lateral ( + select * from test2 + where test2.foo = test1.bar + ) test2_inner on true + `, { + type: 'select', + columns: [{ expr: star }], + from: [tbl('test'), + { + alias: "test2_inner", + join: { + on: { + type: "boolean", + value: true + }, + type: "INNER JOIN", + }, + statement: { + columns: [ + { + expr: { + name: "*", + type: "ref" + } + } + ], + from: [ + { + name: { + name: "test2" + }, + type: "table" + } + ], + type: "select", + where: { + left: { + name: "foo", + table: { + name: "test2" + }, + type: "ref" + }, + op: "=", + right: { + name: "bar", + table: { + name: "test1" + }, + type: "ref", + }, + type: "binary" + } + }, + type: "statement", + lateral: true, + } + ] + }); + + checkSelect(` + SELECT m.name AS mname, pname + FROM manufacturers m, LATERAL get_product_names(m.id) pname; + `, { + "columns": [ + { + "expr": { + "type": "ref", + "table": { + "name": "m" + }, + "name": "name" + }, + "alias": { + "name": "mname" + } + }, + { + "expr": { + "type": "ref", + "name": "pname" + } + } + ], + "from": [ + { + "type": "table", + "name": { + "name": "manufacturers", + "alias": "m" + } + }, + { + "type": "call", + "function": { + "name": "get_product_names" + }, + "args": [ + { + "type": "ref", + "table": { + "name": "m" + }, + "name": "id" + } + ], + "lateral": true, + "alias": { + "name": "pname" + } + } + ], + "type": "select" + }); + checkSelect(['select current_schema()'], { type: 'select', columns: [{ diff --git a/src/to-sql.ts b/src/to-sql.ts index 6e562a8..65e87b6 100644 --- a/src/to-sql.ts +++ b/src/to-sql.ts @@ -1047,6 +1047,9 @@ const visitor = astVisitor(m => ({ fromCall: s => { join(m, s.join, () => { + if (s.lateral) { + ret.push("LATERAL ") + } m.call(s); if (s.withOrdinality) { ret.push(' WITH ORDINALITY') @@ -1074,6 +1077,9 @@ const visitor = astVisitor(m => ({ // todo: use 's.db' if defined join(m, s.join, () => { + if (s.lateral) { + ret.push("LATERAL ") + } ret.push('('); m.select(s.statement); ret.push(') ');