diff --git a/src/ast-mapper.ts b/src/ast-mapper.ts index 4a38d11..128d34b 100644 --- a/src/ast-mapper.ts +++ b/src/ast-mapper.ts @@ -52,7 +52,7 @@ export interface IAstPartialMapper { from?: (from: a.From) => a.From | nil fromCall?: (from: a.FromCall) => a.From | nil fromStatement?: (from: a.FromStatement) => a.From | nil - fromValues?: (from: a.FromValues) => a.From | nil; + values?: (from: a.ValuesStatement) => a.SelectStatement | nil; fromTable?: (from: a.FromTable) => a.From | nil selectionColumn?: (val: a.SelectedColumn) => a.SelectedColumn | nil expr?: (val: a.Expr) => a.Expr | nil @@ -68,6 +68,7 @@ export interface IAstPartialMapper { callOverlay?: (val: a.ExprOverlay) => a.Expr | nil array?: (val: a.ExprList) => a.Expr | nil constant?: (value: a.ExprLiteral) => a.Expr | nil + default?: (value: a.ExprDefault) => a.Expr | nil; ref?: (val: a.ExprRef) => a.Expr | nil unary?: (val: a.ExprUnary) => a.Expr | nil binary?: (val: a.ExprBinary) => a.Expr | nil @@ -272,6 +273,8 @@ export class AstDefaultMapper implements IAstMapper { return this.do(val); case 'create function': return this.createFunction(val); + case 'values': + return this.values(val); default: throw NotSupported.never(val); } @@ -432,18 +435,10 @@ export class AstDefaultMapper implements IAstMapper { if (!into) { return null; // nowhere to insert into } - const values = arrayNilMap(val.values, valSet => { - return arrayNilMap(valSet, v => { - if (v === 'default') { - return v; - } - return this.expr(v); - }); - }); - const select = val.select && this.select(val.select); + const select = val.insert && this.select(val.insert); - if (!values?.length && !select) { + if (!select) { // nothing to insert return null; } @@ -462,8 +457,7 @@ export class AstDefaultMapper implements IAstMapper { return assignChanged(val, { into, - values, - select, + insert: select, returning, onConflict: !ocdo ? val.onConflict : assignChanged(val.onConflict, { do: ocdo, @@ -811,6 +805,8 @@ export class AstDefaultMapper implements IAstMapper { return this.union(val); case 'with': return this.with(val); + case 'values': + return this.values(val); default: throw NotSupported.never(val); } @@ -894,8 +890,6 @@ export class AstDefaultMapper implements IAstMapper { return this.fromTable(from); case 'statement': return this.fromStatement(from); - case 'values': - return this.fromValues(from); case 'call': return this.fromCall(from); default: @@ -924,7 +918,7 @@ export class AstDefaultMapper implements IAstMapper { }) } - fromValues(from: a.FromValues): a.From | nil { + values(from: a.ValuesStatement): a.SelectStatement | nil { const values = arrayNilMap(from.values, x => arrayNilMap(x, y => this.expr(y))); if (!values?.length) { return null; // nothing to select from @@ -945,13 +939,13 @@ export class AstDefaultMapper implements IAstMapper { } fromTable(from: a.FromTable): a.From | nil { - const nfrom = this.tableRef(from); + const nfrom = this.tableRef(from.name); if (!nfrom) { return null; // nothing to select from } const join = from.join && this.join(from.join); return assignChanged(from, { - ...nfrom, + name: nfrom, join, }) } @@ -1021,6 +1015,10 @@ export class AstDefaultMapper implements IAstMapper { return this.callOverlay(val); case 'substring': return this.callSubstring(val); + case 'values': + return this.values(val); + case 'default': + return this.default(val); default: throw NotSupported.never(val); } @@ -1166,6 +1164,9 @@ export class AstDefaultMapper implements IAstMapper { return value; } + default(value: a.ExprDefault): a.Expr | nil { + return value; + } /** Called when a reference is used */ ref(val: a.ExprRef): a.Expr | nil { diff --git a/src/syntax/ast.ts b/src/syntax/ast.ts index 1db5a83..0d79926 100644 --- a/src/syntax/ast.ts +++ b/src/syntax/ast.ts @@ -37,6 +37,7 @@ export type Statement = SelectStatement | CommentStatement | CreateSchemaStatement | RaiseStatement + | ValuesStatement | CreateFunctionStatement | DoStatement | BeginStatement @@ -212,10 +213,7 @@ export interface InsertStatement extends PGNode { returning?: SelectedColumn[] | nil; columns?: Name[] | nil; overriding?: 'system' | 'user'; - /** Insert values */ - values?: (Expr | 'default')[][] | nil; - /** Insert into select */ - select?: SelectStatement | nil; + insert: SelectStatement; onConflict?: OnConflictAction | nil; } @@ -512,6 +510,7 @@ export interface WithStatement extends PGNode { export type SelectStatement = SelectFromStatement | SelectFromUnion + | ValuesStatement | WithStatement; export interface SelectFromStatement extends PGNode { @@ -560,7 +559,9 @@ export interface SelectedColumn extends PGNode { alias?: Name; } -export type From = FromTable | FromStatement | FromValues | FromCall +export type From = FromTable + | FromStatement + | FromCall export interface FromCall extends ExprCall, PGNode { @@ -570,28 +571,32 @@ export interface FromCall extends ExprCall, PGNode { -export interface FromValues { +export interface ValuesStatement extends PGNode { type: 'values'; - alias: Name; values: Expr[][]; - columnNames?: Name[] | nil; - join?: JoinClause | nil; } + export interface QNameAliased extends QName, PGNode { alias?: string; } -export interface FromTable extends QNameAliased, PGNode { +export interface QNameMapped extends QNameAliased { + columnNames?: Name[] | nil; +} + +export interface FromTable extends PGNode { type: 'table', + name: QNameMapped; join?: JoinClause | nil; } export interface FromStatement extends PGNode { type: 'statement'; statement: SelectStatement; - alias: Name; + alias: string; + columnNames?: Name[] | nil; db?: null | nil; join?: JoinClause | nil; } @@ -614,6 +619,7 @@ export type Expr = ExprRef | ExprNull | ExprExtract | ExprInteger + | ExprDefault | ExprMember | ExprValueKeyword | ExprArrayIndex @@ -804,6 +810,10 @@ export interface ExprInteger extends PGNode { value: number; } +export interface ExprDefault extends PGNode { + type: 'default'; +} + export interface ExprNumeric extends PGNode { type: 'numeric'; value: number; diff --git a/src/syntax/create-view.spec.ts b/src/syntax/create-view.spec.ts index 00b3550..ed0ba6b 100644 --- a/src/syntax/create-view.spec.ts +++ b/src/syntax/create-view.spec.ts @@ -1,6 +1,6 @@ import 'mocha'; import 'chai'; -import { checkInvalid, checkStatement, columns } from './spec-utils'; +import { checkInvalid, checkStatement, columns, tbl } from './spec-utils'; describe('Create view statements', () => { @@ -10,7 +10,7 @@ describe('Create view statements', () => { query: { type: 'select', columns: columns('*'), - from: [{ type: 'table', name: 'tbl' }], + from: [tbl('tbl')], }, }); @@ -23,7 +23,7 @@ describe('Create view statements', () => { query: { type: 'select', columns: columns('*'), - from: [{ type: 'table', name: 'tbl' }], + from: [tbl('tbl')], }, }); @@ -36,7 +36,7 @@ describe('Create view statements', () => { query: { type: 'select', columns: columns('*'), - from: [{ type: 'table', name: 'tbl' }], + from: [tbl('tbl')], }, }); @@ -48,7 +48,7 @@ describe('Create view statements', () => { query: { type: 'select', columns: columns('*'), - from: [{ type: 'table', name: 'tbl' }], + from: [tbl('tbl')], }, }); @@ -60,7 +60,7 @@ describe('Create view statements', () => { query: { type: 'select', columns: columns('*'), - from: [{ type: 'table', name: 'tbl' }], + from: [tbl('tbl')], }, }); @@ -71,7 +71,7 @@ describe('Create view statements', () => { query: { type: 'select', columns: columns('*'), - from: [{ type: 'table', name: 'tbl' }], + from: [tbl('tbl')], }, }); @@ -82,7 +82,7 @@ describe('Create view statements', () => { query: { type: 'select', columns: columns('*'), - from: [{ type: 'table', name: 'tbl' }], + from: [tbl('tbl')], }, }); @@ -94,7 +94,7 @@ describe('Create view statements', () => { query: { type: 'select', columns: columns('*'), - from: [{ type: 'table', name: 'tbl' }], + from: [tbl('tbl')], }, }); @@ -107,7 +107,7 @@ describe('Create view statements', () => { query: { type: 'select', columns: columns('*'), - from: [{ type: 'table', name: 'tbl' }], + from: [tbl('tbl')], }, }); @@ -117,7 +117,7 @@ describe('Create view statements', () => { query: { type: 'select', columns: columns('*'), - from: [{ type: 'table', name: 'tbl' }], + from: [tbl('tbl')], }, }); @@ -128,7 +128,7 @@ describe('Create view statements', () => { query: { type: 'select', columns: columns('*'), - from: [{ type: 'table', name: 'tbl' }], + from: [tbl('tbl')], }, }); @@ -139,7 +139,7 @@ describe('Create view statements', () => { query: { type: 'select', columns: columns('*'), - from: [{ type: 'table', name: 'tbl' }], + from: [tbl('tbl')], }, }); @@ -151,7 +151,7 @@ describe('Create view statements', () => { query: { type: 'select', columns: columns('*'), - from: [{ type: 'table', name: 'tbl' }], + from: [tbl('tbl')], }, withData: true, }); @@ -163,7 +163,7 @@ describe('Create view statements', () => { query: { type: 'select', columns: columns('*'), - from: [{ type: 'table', name: 'tbl' }], + from: [tbl('tbl')], }, withData: false, }); @@ -178,7 +178,7 @@ describe('Create view statements', () => { query: { type: 'select', columns: columns('*'), - from: [{ type: 'table', name: 'tbl' }], + from: [tbl('tbl')], }, }); diff --git a/src/syntax/expr.ne b/src/syntax/expr.ne index 56699c1..2cc3b63 100644 --- a/src/syntax/expr.ne +++ b/src/syntax/expr.ne @@ -227,6 +227,7 @@ expr_primary | %kw_null {% x => track(x, { type: 'null' }) %} | value_keyword {% x => track(x, {type: 'keyword', keyword: toStr(x) }) %} | %qparam {% x => track(x, { type: 'parameter', name: toStr(x[0]) }) %} + | %kw_default {% x => track(x, { type: 'default'}) %} # LIKE-kind operators diff --git a/src/syntax/expr.spec.ts b/src/syntax/expr.spec.ts index 3f998fa..155c87c 100644 --- a/src/syntax/expr.spec.ts +++ b/src/syntax/expr.spec.ts @@ -1,6 +1,6 @@ import 'mocha'; import 'chai'; -import { checkTreeExpr, checkInvalidExpr, checkInvalid, checkTreeExprLoc, starCol, star, col, ref } from './spec-utils'; +import { checkTreeExpr, checkInvalidExpr, checkInvalid, checkTreeExprLoc, starCol, star, col, ref, tbl, name } from './spec-utils'; import { toSql } from '../to-sql'; import { expect } from 'chai'; import { parse } from '../parser'; @@ -1332,7 +1332,11 @@ line`, }], from: [{ _location: { start: 22, end: 25 }, - type: 'table', name: 'tbl' + type: 'table', + name: { + _location: { start: 22, end: 25 }, + name: 'tbl' + }, }], }] } @@ -1346,7 +1350,7 @@ line`, right: { type: 'select', columns: [starCol], - from: [{ type: 'table', name: 'tb' }], + from: [tbl('tb')], } }); diff --git a/src/syntax/insert.ne b/src/syntax/insert.ne index 24d5696..2cc9416 100644 --- a/src/syntax/insert.ne +++ b/src/syntax/insert.ne @@ -7,24 +7,21 @@ insert_statement -> (kw_insert %kw_into) table_ref_aliased collist_paren:? (kw_overriding (kw_system | %kw_user) kw_value {% get(1) %}):? - (kw_values insert_values {% last %}):? (selection | selection_paren):? (%kw_on kw_conflict insert_on_conflict {% last %}):? (%kw_returning select_expr_list_aliased {% last %}):? {% x => { const columns = x[2] && x[2].map(asName); const overriding = toStr(x[3]); - const values = x[4]; - const select = unwrap(x[5]); - const onConflict = x[6]; - const returning = x[7]; + const insert = unwrap(x[4]); + const onConflict = x[5]; + const returning = x[6]; return track(x, { type: 'insert', into: unwrap(x[1]), + insert, ...overriding && { overriding }, ...columns && { columns }, - ...values && { values }, - ...select && { select }, ...returning && { returning }, ...onConflict && { onConflict }, }) @@ -38,8 +35,7 @@ insert_values -> insert_value (comma insert_value {% last %}):* {% ([head, tail] insert_value -> lparen insert_expr_list_raw rparen {% get(1) %} -insert_single_value -> (expr_or_select | %kw_default {% () => 'default' %}) {% unwrap %} -insert_expr_list_raw -> insert_single_value (comma insert_single_value {% last %}):* {% ([head, tail]) => { +insert_expr_list_raw -> expr_or_select (comma expr_or_select {% last %}):* {% ([head, tail]) => { return [head, ...(tail || [])]; } %} diff --git a/src/syntax/insert.spec.ts b/src/syntax/insert.spec.ts index b2a4f5f..e2ad095 100644 --- a/src/syntax/insert.spec.ts +++ b/src/syntax/insert.spec.ts @@ -1,6 +1,6 @@ import 'mocha'; import 'chai'; -import { checkInsert, checkInsertLoc } from './spec-utils'; +import { checkInsert, checkInsertLoc, tbl } from './spec-utils'; describe('Insert', () => { @@ -9,13 +9,16 @@ describe('Insert', () => { type: 'insert', into: { name: 'test' }, columns: [{ name: 'a' }, { name: 'b' }], - values: [[{ - type: 'integer', - value: 1, - }, { - type: 'string', - value: 'x', - }]], + insert: { + type: 'values', + values: [[{ + type: 'integer', + value: 1, + }, { + type: 'string', + value: 'x', + }]] + }, }); checkInsertLoc([`insert into test(a) values (1)`], { @@ -29,21 +32,28 @@ describe('Insert', () => { _location: { start: 17, end: 18 }, name: 'a' }], - values: [[{ - _location: { start: 28, end: 29 }, - type: 'integer', - value: 1, - },]], + insert: { + _location: { start: 20, end: 29 }, + type: 'values', + values: [[{ + _location: { start: 28, end: 29 }, + type: 'integer', + value: 1, + },]] + }, }); checkInsert([`insert into test(a) values (1) on conflict do nothing`], { type: 'insert', into: { name: 'test' }, columns: [{ name: 'a' }], - values: [[{ - type: 'integer', - value: 1, - },]], + insert: { + type: 'values', + values: [[{ + type: 'integer', + value: 1, + },]] + }, onConflict: { do: 'do nothing', }, @@ -53,10 +63,13 @@ describe('Insert', () => { type: 'insert', into: { name: 'test' }, columns: [{ name: 'a' }], - values: [[{ - type: 'integer', - value: 1, - },]], + insert: { + type: 'values', + values: [[{ + type: 'integer', + value: 1, + },]] + }, onConflict: { do: 'do nothing', on: [ @@ -70,10 +83,13 @@ describe('Insert', () => { type: 'insert', into: { name: 'test' }, columns: [{ name: 'a' }], - values: [[{ - type: 'integer', - value: 1, - },]], + insert: { + type: 'values', + values: [[{ + type: 'integer', + value: 1, + },]] + }, onConflict: { do: { sets: [{ @@ -88,42 +104,54 @@ describe('Insert', () => { type: 'insert', into: { name: 'test' }, returning: [{ expr: { type: 'ref', name: 'id' } }], - values: [[{ - type: 'integer', - value: 1, - },]], + insert: { + type: 'values', + values: [[{ + type: 'integer', + value: 1, + },]] + }, }); checkInsert([`insert into test values (1) returning "id" as x;`], { type: 'insert', into: { name: 'test' }, returning: [{ expr: { type: 'ref', name: 'id' }, alias: { name: 'x' } }], - values: [[{ - type: 'integer', - value: 1, - },]], + insert: { + type: 'values', + values: [[{ + type: 'integer', + value: 1, + },]] + }, }); checkInsert([`insert into test values (1) returning "id", val;`], { type: 'insert', into: { name: 'test' }, returning: [{ expr: { type: 'ref', name: 'id' } }, { expr: { type: 'ref', name: 'val' } }], - values: [[{ - type: 'integer', - value: 1, - },]], + insert: { + type: 'values', + values: [[{ + type: 'integer', + value: 1, + },]] + }, }); checkInsert([`insert into db . test(a, b) values (1, 'x')`, `INSERT INTO"db"."test"(a,"b")VALUES(1,'x')`], { type: 'insert', into: { name: 'test', schema: 'db' }, columns: [{ name: 'a' }, { name: 'b' }], - values: [[{ - type: 'integer', - value: 1, - }, { - type: 'string', - value: 'x', - }]] + insert: { + type: 'values', + values: [[{ + type: 'integer', + value: 1, + }, { + type: 'string', + value: 'x', + }]] + } }); @@ -132,12 +160,14 @@ describe('Insert', () => { type: 'insert', into: { name: 'test', schema: 'db' }, columns: [{ name: 'a' }, { name: 'b' }], - select: { + insert: { type: 'select', from: [{ type: 'table', - name: 'test', - schema: 'x' + name: { + name: 'test', + schema: 'x', + }, }], columns: [{ expr: { @@ -156,12 +186,9 @@ describe('Insert', () => { checkInsert([`insert into "test" select * FROM test`, `insert into test(select * FROM test)`], { type: 'insert', into: { name: 'test' }, - select: { + insert: { type: 'select', - from: [{ - type: 'table', - name: 'test' - }], + from: [tbl('test')], columns: [{ expr: { type: 'ref', @@ -176,11 +203,14 @@ describe('Insert', () => { type: 'insert', into: { name: 'test' }, columns: [{ name: 'a' }, { name: 'b' }], - values: [[{ - type: 'integer', - value: 1, + insert: { + type: 'values', + values: [[{ + type: 'integer', + value: 1, + } + , { type: 'default' }]] } - , 'default']] }); checkInsert([`insert into test(a, b) overriding system value values (1, default)`], { @@ -188,11 +218,14 @@ describe('Insert', () => { into: { name: 'test' }, columns: [{ name: 'a' }, { name: 'b' }], overriding: 'system', - values: [[{ - type: 'integer', - value: 1, + insert: { + type: 'values', + values: [[{ + type: 'integer', + value: 1, + } + , { type: 'default' }]] } - , 'default']] }); @@ -201,10 +234,13 @@ describe('Insert', () => { into: { name: 'test' }, columns: [{ name: 'a' }, { name: 'b' }], overriding: 'user', - values: [[{ - type: 'integer', - value: 1, + insert: { + type: 'values', + values: [[{ + type: 'integer', + value: 1, + } + , { type: 'default' }]] } - , 'default']] }); }); \ No newline at end of file diff --git a/src/syntax/main.ne b/src/syntax/main.ne index 4baf809..e24c346 100644 --- a/src/syntax/main.ne +++ b/src/syntax/main.ne @@ -62,6 +62,7 @@ statement_noprep selection -> select_statement {% unwrap %} + | select_values {% unwrap %} | with_statement {% unwrap %} | union_statement {% unwrap %} diff --git a/src/syntax/prepare.spec.ts b/src/syntax/prepare.spec.ts index f014c14..b33b105 100644 --- a/src/syntax/prepare.spec.ts +++ b/src/syntax/prepare.spec.ts @@ -1,6 +1,6 @@ import 'mocha'; import 'chai'; -import { checkStatement } from './spec-utils'; +import { checkStatement, tbl } from './spec-utils'; describe('Prepare', () => { @@ -9,7 +9,7 @@ describe('Prepare', () => { name: { name: 'st' }, statement: { type: 'select', - from: [{ type: 'table', name: 'data' }], + from: [tbl('data')], columns: [{ expr: { type: 'ref', name: 'c' } }], }, }); @@ -21,7 +21,7 @@ describe('Prepare', () => { statement: { type: 'select', columns: [{ expr: { type: 'ref', name: 'c' } }], - from: [{ type: 'table', name: 'data' }], + from: [tbl('data')], where: { type: 'binary', op: '=', @@ -37,7 +37,7 @@ describe('Prepare', () => { args: [{ name: 'text' }, { name: 'int' }], statement: { type: 'select', - from: [{ type: 'table', name: 'data' }], + from: [tbl('data')], columns: [{ expr: { type: 'ref', name: 'c' } }], }, }); diff --git a/src/syntax/select.ne b/src/syntax/select.ne index 7fcde2c..8c54431 100644 --- a/src/syntax/select.ne +++ b/src/syntax/select.ne @@ -42,12 +42,45 @@ select_subject_joins -> select_table_base select_table_join:+ {% ([head, tail]) # [tableName] or [select x, y from z] select_table_base - -> table_ref_aliased {% x => { - return track(x, { type: 'table', ...x[0]}); + -> stb_table {% unwrap %} + | stb_statement {% unwrap %} + | stb_call {% unwrap %} + + +stb_opts + -> ident_aliased collist_paren:? {% x => track(x, { + alias: toStr(x[0]), + ...x[1] && {columnNames: unbox(x[1]).map(asName)}, + }) %} + + +# Selects on tables CAN have an alias +stb_table -> table_ref stb_opts:? {% x => { + return track(x, { + type: 'table', + name: track(x, { + ...x[0], + ...x[1], + }, + }); } %} - | select_subject_select_statement {% unwrap %} - | select_subject_select_values {% unwrap %} - | expr_call (%kw_as:? ident {% last %}):? {% x => + + +# Selects on subselects MUST have an alias +stb_statement -> selection_paren stb_opts {% x => track(x, { + type: 'statement', + statement: unwrap(x[0]), + ...x[1], +}) %} + + +select_values -> kw_values insert_values {% x => track(x, { + type: 'values', + values: x[1], +}) %} + + +stb_call -> expr_call (%kw_as:? ident {% last %}):? {% x => !x[1] ? x[0] : track(x, { @@ -55,6 +88,7 @@ select_table_base alias: asName(x[1]), }) %} + # [, othertable] or [join expression] # select_table_joined # -> comma select_table_base {% last %} @@ -82,22 +116,8 @@ select_join_op | (%kw_right %kw_outer:? {% x => box(x, 'RIGHT JOIN') %}) | (%kw_full %kw_outer:? {% x => box(x, 'FULL JOIN') %}) -# Selects on subselects MUST have an alias -select_subject_select_statement -> selection_paren ident_aliased {% x => track(x, { - type: 'statement', - statement: unwrap(x[0]), - alias: asName(x[1]) -}) %} -# Select values: select * from (values (1, 'one'), (2, 'two')) as vals (num, letter) -select_subject_select_values -> lparen kw_values insert_values rparen %kw_as ident collist_paren:? {% x => track(x, { - type: 'values', - alias: asName(x[5]), - values: x[2], - ...x[6] && {columnNames: unbox(x[6]).map(asName)}, -}) %} - # 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 38cca43..32eefe3 100644 --- a/src/syntax/select.spec.ts +++ b/src/syntax/select.spec.ts @@ -1,6 +1,6 @@ import 'mocha'; import 'chai'; -import { checkSelect, checkInvalid, columns, ref, star } from './spec-utils'; +import { checkSelect, checkInvalid, columns, ref, star, tbl, name, qname, checkStatement, int, binary } from './spec-utils'; import { JoinType, SelectStatement } from './ast'; describe('Select statements', () => { @@ -59,7 +59,7 @@ describe('Select statements', () => { checkSelect(['select * from test', 'select*from"test"', 'select* from"test"', 'select *from"test"', 'select*from "test"', 'select * from "test"'], { type: 'select', - from: [{ type: 'table', name: 'test' }], + from: [tbl('test')], columns: columns({ type: 'ref', name: '*' }) }); @@ -71,7 +71,7 @@ describe('Select statements', () => { checkSelect(['select a as a1, b as b1 from test', 'select a a1,b b1 from test', 'select a a1 ,b b1 from test'], { type: 'select', - from: [{ type: 'table', name: 'test' }], + from: [tbl('test')], columns: [{ expr: { type: 'ref', name: 'a' }, alias: { name: 'a1' }, @@ -83,14 +83,14 @@ describe('Select statements', () => { checkSelect(['select * from db.test'], { type: 'select', - from: [{ type: 'table', name: 'test', schema: 'db' }], + from: [{ type: 'table', name: qname('test', 'db') }], columns: columns({ type: 'ref', name: '*' }), }); checkSelect(['select * from test limit 5', 'select * from test fetch first 5', 'select * from test fetch next 5 rows'], { type: 'select', - from: [{ type: 'table', name: 'test' }], + from: [tbl('test')], columns: columns({ type: 'ref', name: '*' }), limit: { limit: { type: 'integer', value: 5 } @@ -99,7 +99,7 @@ describe('Select statements', () => { checkSelect(['select * from test limit 0'], { type: 'select', - from: [{ type: 'table', name: 'test' }], + from: [tbl('test')], columns: columns({ type: 'ref', name: '*' }), limit: { limit: { type: 'integer', value: 0 } @@ -108,7 +108,7 @@ describe('Select statements', () => { checkSelect(['select * from test limit 5 offset 3', 'select * from test offset 3 rows fetch first 5'], { type: 'select', - from: [{ type: 'table', name: 'test' }], + from: [tbl('test')], columns: columns({ type: 'ref', name: '*' }), limit: { limit: { type: 'integer', value: 5 } @@ -119,7 +119,7 @@ describe('Select statements', () => { checkSelect(['select * from test limit $1 offset $2'], { type: 'select', - from: [{ type: 'table', name: 'test' }], + from: [tbl('test')], columns: columns({ type: 'ref', name: '*' }), limit: { limit: { type: 'parameter', name: '$1' }, @@ -129,7 +129,7 @@ describe('Select statements', () => { checkSelect(['select * from test offset 3', 'select * from test offset 3 rows'], { type: 'select', - from: [{ type: 'table', name: 'test' }], + from: [tbl('test')], columns: columns({ type: 'ref', name: '*' }), limit: { offset: { type: 'integer', value: 3 }, @@ -139,7 +139,7 @@ describe('Select statements', () => { checkSelect(['select * from test order by a asc limit 3'], { type: 'select', - from: [{ type: 'table', name: 'test' }], + from: [tbl('test')], columns: columns({ type: 'ref', name: '*' }), limit: { limit: { type: 'integer', value: 3 } @@ -152,7 +152,7 @@ describe('Select statements', () => { checkSelect(['select * from test order by a limit 3'], { type: 'select', - from: [{ type: 'table', name: 'test' }], + from: [tbl('test')], columns: columns({ type: 'ref', name: '*' }), limit: { limit: { type: 'integer', value: 3 } @@ -165,7 +165,7 @@ describe('Select statements', () => { checkSelect(['select * from test order by a asc, b desc'], { type: 'select', - from: [{ type: 'table', name: 'test' }], + from: [tbl('test')], columns: columns({ type: 'ref', name: '*' }), orderBy: [{ by: { type: 'ref', name: 'a' }, @@ -202,7 +202,7 @@ describe('Select statements', () => { , 'select*from test as"a"where a.b > 42' , 'select*from test as a where a.b > 42'], { type: 'select', - from: [{ type: 'table', name: 'test', alias: 'a' }], + from: [{ type: 'table', name: { name: 'test', alias: 'a' } }], columns: columns({ type: 'ref', name: '*' }), where: { type: 'binary', @@ -232,24 +232,24 @@ describe('Select statements', () => { type: 'statement', statement: { type: 'select', - from: [{ type: 'table', name: 'test' }], + from: [tbl('test')], columns: columns({ type: 'ref', name: 'id' }), }, - alias: { name: 'd' }, + alias: 'd', }] }) checkSelect(['select * from test group by grp', 'select * from test group by (grp)'], { type: 'select', columns: columns({ type: 'ref', name: '*' }), - from: [{ type: 'table', name: 'test' }], + from: [tbl('test')], groupBy: [{ type: 'ref', name: 'grp' }] }) checkSelect(['select * from test group by a,b', 'select * from test group by (a,b)'], { type: 'select', columns: columns({ type: 'ref', name: '*' }), - from: [{ type: 'table', name: 'test' }], + from: [tbl('test')], groupBy: [ { type: 'ref', name: 'a' }, { type: 'ref', name: 'b' } @@ -261,12 +261,9 @@ describe('Select statements', () => { return { type: 'select', columns: columns({ type: 'ref', name: '*' }), - from: [{ + from: [tbl('ta'), { type: 'table', - name: 'ta' - }, { - type: 'table', - name: 'tb', + name: name('tb'), join: { type: t, on: { @@ -316,19 +313,15 @@ describe('Select statements', () => { USING("studentId")`, { type: 'select', columns: [{ expr: star }], - from: [ - { - type: 'table', - name: 'stud_ass_progress', - }, - { - type: 'table', - name: 'accuracy', - join: { - type: 'LEFT JOIN', - using: [{ name: 'studentId' }], - } + from: [tbl('stud_ass_progress'), + { + type: 'table', + name: name('accuracy'), + join: { + type: 'LEFT JOIN', + using: [{ name: 'studentId' }], } + } ] }); @@ -387,21 +380,21 @@ describe('Select statements', () => { checkSelect(['select distinct a from test'], { type: 'select', - from: [{ type: 'table', name: 'test' }], + from: [tbl('test')], distinct: 'distinct', columns: columns({ type: 'ref', name: 'a' }), }); checkSelect(['select distinct on (a) a from test'], { type: 'select', - from: [{ type: 'table', name: 'test' }], + from: [tbl('test')], distinct: [{ type: 'ref', name: 'a' }], columns: columns({ type: 'ref', name: 'a' }), }); checkSelect(['select distinct on (a, b) a from test'], { type: 'select', - from: [{ type: 'table', name: 'test' }], + from: [tbl('test')], distinct: [{ type: 'ref', name: 'a' }, { type: 'ref', name: 'b' }], columns: columns({ type: 'ref', name: 'a' }), }); @@ -409,7 +402,7 @@ describe('Select statements', () => { checkSelect(['select count(distinct("userId")) from photo'], { type: 'select', - from: [{ type: 'table', name: 'photo' }], + from: [tbl('photo')], columns: columns({ type: 'call', function: { name: 'count' }, @@ -420,7 +413,7 @@ describe('Select statements', () => { checkSelect(['select max(distinct("userId")) from photo'], { type: 'select', - from: [{ type: 'table', name: 'photo' }], + from: [tbl('photo')], columns: columns({ type: 'call', function: { name: 'max' }, @@ -431,22 +424,33 @@ describe('Select statements', () => { checkSelect(['select all a from test'], { type: 'select', - from: [{ type: 'table', name: 'test' }], + from: [tbl('test')], distinct: 'all', columns: columns({ type: 'ref', name: 'a' }), }); + checkStatement(`VALUES (1, 1+1), (3, 4)`, { + type: 'values', + values: [ + [int(1), binary(int(1), '+', int(1))], + [int(3), int(4)], + ] + }) + checkSelect([`select * from (values (1, 'one'), (2, 'two')) as vals (num, letter)`], { type: 'select', from: [{ - type: 'values', - alias: { name: 'vals' }, + type: 'statement', + statement: { + type: 'values', + values: [ + [{ type: 'integer', value: 1 }, { type: 'string', value: 'one' }], + [{ type: 'integer', value: 2 }, { type: 'string', value: 'two' }], + ], + }, + alias: 'vals', columnNames: [{ name: 'num' }, { name: 'letter' }], - values: [ - [{ type: 'integer', value: 1 }, { type: 'string', value: 'one' }], - [{ type: 'integer', value: 2 }, { type: 'string', value: 'two' }], - ], }], columns: columns({ type: 'ref', name: '*' }) }); @@ -454,12 +458,15 @@ describe('Select statements', () => { checkSelect([`select * from (values (1, 'one'), (2, 'two')) as vals`], { type: 'select', from: [{ - type: 'values', - alias: { name: 'vals' }, - values: [ - [{ type: 'integer', value: 1 }, { type: 'string', value: 'one' }], - [{ type: 'integer', value: 2 }, { type: 'string', value: 'two' }], - ], + type: 'statement', + statement: { + type: 'values', + values: [ + [{ type: 'integer', value: 1 }, { type: 'string', value: 'one' }], + [{ type: 'integer', value: 2 }, { type: 'string', value: 'two' }], + ], + }, + alias: 'vals', }], columns: columns({ type: 'ref', name: '*' }) }); @@ -473,12 +480,16 @@ describe('Select statements', () => { }), from: [{ type: 'table', - name: 'ta', - alias: 't1', + name: { + name: 'ta', + alias: 't1', + }, }, { type: 'table', - name: 'tb', - alias: 't2', + name: { + name: 'tb', + alias: 't2', + }, join: { type: 'INNER JOIN', on: { @@ -588,7 +599,7 @@ describe('Select statements', () => { statement: { type: 'select', columns: [{ expr: { type: 'ref', name: 'val' } }], - from: [{ type: 'table', name: 'example' }], + from: [tbl('example')], }, }], in: { @@ -600,7 +611,7 @@ describe('Select statements', () => { args: [{ type: 'ref', name: 'val' }], } }], - from: [{ type: 'table', name: 'x' }], + from: [tbl('x')], } }), }); diff --git a/src/syntax/simple-statements.spec.ts b/src/syntax/simple-statements.spec.ts index 2f37d62..43ed9a5 100644 --- a/src/syntax/simple-statements.spec.ts +++ b/src/syntax/simple-statements.spec.ts @@ -1,6 +1,6 @@ import 'mocha'; import 'chai'; -import { checkStatement } from './spec-utils'; +import { binary, checkStatement, int, name, ref, tbl } from './spec-utils'; import { parseWithComments } from '../parser'; import { assert, expect } from 'chai'; @@ -213,7 +213,7 @@ describe('Simple statements', () => { assert.deepEqual(ast, [{ type: 'select', columns: [{ expr: { type: 'ref', name: '*' } }], - from: [{ type: 'table', name: 'tbl' }], + from: [tbl('tbl')], }]); assert.deepEqual(comments.map(c => c.comment), ['/* comment a */ ', '/* comment b */ ']) }); @@ -235,4 +235,48 @@ describe('Simple statements', () => { deferrable: false, }) + + checkStatement(`select * from (select a from mytable) myalias(col_renamed)`, { + type: 'select', + columns: [{ expr: { type: 'ref', name: '*' } }], + from: [{ + type: 'statement', + statement: { + type: 'select', + columns: [{ expr: ref('a') }], + from: [tbl('mytable')], + }, + alias: 'myalias', + columnNames: [name('col_renamed')], + }] + }); + + checkStatement(`select * from mytable "myAlias"(a)`, { + type: 'select', + columns: [{ expr: { type: 'ref', name: '*' } }], + from: [{ + type: 'table', + name: { + name: 'mytable', + alias: 'myAlias', + columnNames: [name('a')], + }, + }] + }); + + checkStatement(`select * from (select a,b from mytable) "myAlias"(x,y)`, { + type: 'select', + columns: [{ expr: { type: 'ref', name: '*' } }], + from: [{ + type: 'statement', + statement: { + type: 'select', + columns: [{ expr: ref('a') }, { expr: ref('b') }], + from: [tbl('mytable')], + }, + alias:'myAlias', + columnNames: [name('x'), name('y')], + }] + }); + }); diff --git a/src/syntax/spec-utils.ts b/src/syntax/spec-utils.ts index f9b080e..d938a4c 100644 --- a/src/syntax/spec-utils.ts +++ b/src/syntax/spec-utils.ts @@ -2,7 +2,7 @@ import { Parser, Grammar } from 'nearley'; import { expect, assert } from 'chai'; import grammar from '../syntax/main.ne'; import { trimNullish } from '../utils'; -import { Expr, SelectStatement, CreateTableStatement, CreateIndexStatement, Statement, InsertStatement, UpdateStatement, AlterTableStatement, DeleteStatement, CreateExtensionStatement, CreateSequenceStatement, AlterSequenceStatement, SelectedColumn, Interval } from './ast'; +import { Expr, SelectStatement, CreateTableStatement, CreateIndexStatement, Statement, InsertStatement, UpdateStatement, AlterTableStatement, DeleteStatement, CreateExtensionStatement, CreateSequenceStatement, AlterSequenceStatement, SelectedColumn, Interval, BinaryOperator, ExprBinary, Name, ExprInteger, FromTable, QName } from './ast'; import { astMapper, IAstMapper } from '../ast-mapper'; import { toSql, IAstToSql } from '../to-sql'; import { parseIntervalLiteral } from '../parser'; @@ -215,4 +215,22 @@ export function col(name: string, alias?: string): SelectedColumn { } export function ref(name: string): Expr { return { type: 'ref', name }; +} +export function binary(left: Expr, op: BinaryOperator, right: Expr): ExprBinary { + return { type: 'binary', left, op, right }; +} +export function name(name: string): Name { + return { name }; +} +export function qname(name: string, schema?: string): QName { + return { name, schema }; +} +export function int(value: number): ExprInteger { + return { type: 'integer', value }; +} +export function tbl(nm: string): FromTable { + return { + type: 'table', + name: name(nm), + }; } \ No newline at end of file diff --git a/src/syntax/union.spec.ts b/src/syntax/union.spec.ts index b2b8d05..6dd1ace 100644 --- a/src/syntax/union.spec.ts +++ b/src/syntax/union.spec.ts @@ -1,6 +1,6 @@ import 'mocha'; import 'chai'; -import { checkSelect, columns } from './spec-utils'; +import { checkSelect, columns, tbl } from './spec-utils'; import { SelectedColumn } from './ast'; describe('Union statement', () => { @@ -10,11 +10,14 @@ describe('Union statement', () => { left: { type: 'select', from: [{ - type: 'values', - alias: { name: 'fst' }, - values: [ - [{ type: 'integer', value: 1 }, { type: 'string', value: 'one' }], - ], + type: 'statement', + statement: { + type: 'values', + values: [ + [{ type: 'integer', value: 1 }, { type: 'string', value: 'one' }], + ], + }, + alias: 'fst', }], columns: columns({ type: 'ref', name: '*' }) }, @@ -22,11 +25,14 @@ describe('Union statement', () => { type: 'select', from: [{ - type: 'values', - alias: { name: 'snd' }, - values: [ - [{ type: 'integer', value: 2 }, { type: 'string', value: 'two' }], - ], + type: 'statement', + statement: { + type: 'values', + values: [ + [{ type: 'integer', value: 2 }, { type: 'string', value: 'two' }], + ], + }, + alias: 'snd', }], columns: columns({ type: 'ref', name: '*' }) } @@ -41,19 +47,19 @@ describe('Union statement', () => { left: { type: 'select', columns: columns({ type: 'ref', name: '*' }), - from: [{ type: 'table', name: 'ta' }], + from: [tbl('ta')], }, right: { type: 'union', left: { type: 'select', columns: columns({ type: 'ref', name: '*' }), - from: [{ type: 'table', name: 'tb' }], + from: [tbl('tb')], }, right: { type: 'select', columns: columns({ type: 'ref', name: '*' }), - from: [{ type: 'table', name: 'tc' }], + from: [tbl('tc')], } } }); @@ -98,7 +104,7 @@ describe('Union statement', () => { )`, { type: 'select', columns: [star], - from: [{ type: 'table', name: 'tbl' }], + from: [tbl('tbl')], where: { type: 'binary', op: 'IN', diff --git a/src/syntax/with.spec.ts b/src/syntax/with.spec.ts index e468b04..aee3bd8 100644 --- a/src/syntax/with.spec.ts +++ b/src/syntax/with.spec.ts @@ -1,6 +1,6 @@ import 'mocha'; import 'chai'; -import { checkStatement, checkInvalidExpr } from './spec-utils'; +import { checkStatement, checkInvalidExpr, tbl } from './spec-utils'; import { expect } from 'chai'; import { SelectedColumn } from './ast'; @@ -14,14 +14,14 @@ describe('With clause', () => { alias: { name: 'sel' }, statement: { type: 'select', - from: [{ type: 'table', name: 'data' }], + from: [tbl('data')], columns: [{ expr: { type: 'ref', name: 'v' } }], } } ], in: { type: 'select', - from: [{ type: 'table', name: 'sel' }], + from: [tbl('sel')], columns: [{ expr: { type: 'ref', name: 'v' } }], } }); @@ -33,21 +33,21 @@ describe('With clause', () => { alias: { name: 'sel1' }, statement: { type: 'select', - from: [{ type: 'table', name: 'data' }], + from: [tbl('data')], columns: [{ expr: { type: 'ref', name: 'v' } }], } }, { alias: { name: 'sel2' }, statement: { type: 'select', - from: [{ type: 'table', name: 'data' }], + from: [tbl('data')], columns: [{ expr: { type: 'ref', name: 'v' } }], } } ], in: { type: 'select', - from: [{ type: 'table', name: 'sel' }], + from: [tbl('sel')], columns: [{ expr: { type: 'ref', name: 'v' } }], } }); @@ -67,7 +67,7 @@ describe('With clause', () => { type: 'union', left: { type: 'select', - from: [{ type: 'table', name: 'sel', alias: 's' }], + from: [{ type: 'table', name: { name: 'sel', alias: 's' }, }], columns: [{ expr: { type: 'ref', name: '*' } }], }, right: { @@ -88,7 +88,7 @@ describe('With clause', () => { in: { type: 'select', columns: [star], - from: [{ type: 'table', name: 'a' }] + from: [tbl('a')] }, bind: [{ alias: { name: 'a' }, @@ -109,7 +109,7 @@ describe('With clause', () => { in: { type: 'select', columns: [star], - from: [{ type: 'table', name: 'b' }] + from: [tbl('b')] }, } }], diff --git a/src/to-sql.ts b/src/to-sql.ts index 087ad2e..b95dd90 100644 --- a/src/to-sql.ts +++ b/src/to-sql.ts @@ -903,31 +903,35 @@ const visitor = astVisitor(m => ({ m.select(s.statement); ret.push(') '); if (s.alias) { - ret.push(' AS ', name(s.alias), ' '); + ret.push(' AS ', ident(s.alias)); + if (s.columnNames) { + list(s.columnNames, c => ret.push(name(c)), true); + } + ret.push(' '); } }); ret.push(' '); }, - fromValues: s => { - join(m, s.join, () => { - ret.push('(VALUES '); - list(s.values, vlist => { - list(vlist, e => { - m.expr(e); - }, true); - }, false); - ret.push(') AS ', name(s.alias)); - if (s.columnNames) { - list(s.columnNames, s => ret.push(name(s)), true); - } - }); + values: s => { + ret.push('VALUES '); + list(s.values, vlist => { + list(vlist, e => { + m.expr(e); + }, true); + }, false); }, fromTable: s => { join(m, s.join, () => { - m.tableRef(s); + m.tableRef(s.name); + if (s.name.columnNames) { + if(!s.name.alias) { + throw new Error('Cannot specify aliased column names without an alias'); + } + list(s.name.columnNames, c => ret.push(name(c)), true); + } }); }, @@ -951,25 +955,8 @@ const visitor = astVisitor(m => ({ ret.push('OVERRIDING ', i.overriding.toUpperCase(), ' VALUE '); } - // insert values - if (i.values) { - ret.push('VALUES '); - list(i.values, vlist => { - list(vlist, e => { - if (e === 'default') { - ret.push('default'); - } else { - m.expr(e); - } - }, true); - }, false); - ret.push(' '); - } - - if (i.select) { - m.select(i.select); - ret.push(' '); - } + m.select(i.insert); + ret.push(' '); if (i.onConflict) { ret.push('ON CONFLICT '); @@ -1012,6 +999,10 @@ const visitor = astVisitor(m => ({ ret.push(' '); }, + default: () => { + ret.push(' DEFAULT '); + }, + member: e => { m.expr(e.operand); ret.push(e.op);