diff --git a/src/ast-mapper.ts b/src/ast-mapper.ts index 8a22930..6139891 100644 --- a/src/ast-mapper.ts +++ b/src/ast-mapper.ts @@ -20,6 +20,7 @@ export interface IAstPartialMapper { drop?: (val: a.DropStatement) => a.Statement | nil show?: (val: a.ShowStatement) => a.Statement | nil createTable?: (val: a.CreateTableStatement) => a.Statement | nil + createTableAs?: (val: a.CreateTableAsStatement) => a.Statement | nil truncateTable?: (val: a.TruncateTableStatement) => a.Statement | nil createExtension?: (val: a.CreateExtensionStatement) => a.Statement | nil set?: (st: a.SetStatement) => a.SetStatement | nil @@ -229,6 +230,8 @@ export class AstDefaultMapper implements IAstMapper { return this.createIndex(val); case 'create table': return this.createTable(val); + case 'create table as': + return this.createTableAs(val); case 'truncate table': return this.truncateTable(val); case 'delete': @@ -585,6 +588,21 @@ export class AstDefaultMapper implements IAstMapper { }); } + createTableAs(val: a.CreateTableAsStatement): a.Statement | nil { + const query = this.select(val.query); + if (!query) { + return null; + } + const ref = this.tableRef(val.name); + if (!ref) { + return null; + } + return assignChanged(val, { + query, + name: ref, + }); + } + likeTable(col: a.CreateColumnsLikeTable): a.CreateColumnDef | a.CreateColumnsLikeTable | nil { const like = this.tableRef(col.like); if (!like) { diff --git a/src/syntax/ast.ts b/src/syntax/ast.ts index 96ffbe1..14549bd 100644 --- a/src/syntax/ast.ts +++ b/src/syntax/ast.ts @@ -11,6 +11,7 @@ export function locationOf(node: PGNode): NodeLocation { export type Statement = SelectStatement | CreateTableStatement + | CreateTableAsStatement | CreateSequenceStatement | CreateIndexStatement | CreateExtensionStatement @@ -505,6 +506,18 @@ export interface CreateTableStatement extends PGNode { inherits?: QName[]; } +export interface CreateTableAsStatement extends PGNode { + type: 'create table as'; + name: QName; + temporary?: boolean; + unlogged?: boolean; + locality?: 'global' | 'local'; + ifNotExists?: true; + columnNames?: Name[]; + query: SelectStatement; + withData?: boolean; +} + export interface CreateColumnsLikeTable extends PGNode { kind: 'like table'; like: QName; diff --git a/src/syntax/create-table.ne b/src/syntax/create-table.ne index 441b422..b9b92fe 100644 --- a/src/syntax/create-table.ne +++ b/src/syntax/create-table.ne @@ -33,6 +33,29 @@ createtable_statement -> %kw_create } %} +# https://www.postgresql.org/docs/current/sql-createtableas.html +createtableas_statement -> %kw_create + createtable_modifiers:? + %kw_table + kw_ifnotexists:? + qname + (lparen array_of[ident] rparen {% get(1) %}):? + %kw_as + selection + (%kw_with kw_no:? kw_data):? + {% x => { + + return track(x, { + type: 'create table as', + ... !!x[3] ? { ifNotExists: true } : {}, + name: x[4], + ... x[5] && { columnNames: x[5].map(asName) }, + ...unwrap(x[1]), + query: x[7], + ... x[8] && { withData: toStr(x[8][1]) !== 'no' }, + }); + } %} + createtable_modifiers -> kw_unlogged {% x => x[0] ? { unlogged: true } : {} %} | m_locglob diff --git a/src/syntax/create-table.spec.ts b/src/syntax/create-table.spec.ts index 5b07a41..434b173 100644 --- a/src/syntax/create-table.spec.ts +++ b/src/syntax/create-table.spec.ts @@ -1,6 +1,6 @@ import 'mocha'; import 'chai'; -import { checkCreateTable, checkCreateTableLoc, checkInvalid } from './spec-utils'; +import { checkCreateTable, checkCreateTableLoc, checkCreateTableAs, checkCreateTableAsLoc, checkInvalid } from './spec-utils'; describe('Create table', () => { @@ -758,4 +758,93 @@ describe('Create table', () => { }, ] }) + + checkCreateTableAs(['create table foo as select * from bar', 'CREATE TABLE foo AS SELECT * FROM bar'], { + type: 'create table as', + name: { name: 'foo' }, + query: { + type: 'select', + columns: [{ expr: { type: 'ref', name: '*' } }], + from: [{ type: 'table', name: { name: 'bar' } }], + }, + }); + + checkCreateTableAsLoc(['CREATE TABLE foo AS SELECT * FROM bar'], { + _location: { start: 0, end: 37 }, + type: 'create table as', + name: { + _location: { start: 13, end: 16 }, + name: 'foo', + }, + query: { + _location: { start: 20, end: 37 }, + type: 'select', + columns: [{ + _location: { start: 27, end: 28 }, + expr: { + _location: { start: 27, end: 28 }, + type: 'ref', + name: '*', + }, + }], + from: [{ + _location: { start: 34, end: 37 }, + type: 'table', + name: { + _location: { start: 34, end: 37 }, + name: 'bar', + }, + }], + }, + }); + + checkCreateTableAs(['CREATE TABLE public.foo AS SELECT * FROM bar'], { + type: 'create table as', + name: { name: 'foo', schema: 'public' }, + query: { + type: 'select', + columns: [{ expr: { type: 'ref', name: '*' } }], + from: [{ type: 'table', name: { name: 'bar' } }], + }, + }); + + // typed column definitions are not valid in a "CREATE TABLE ... AS" column list + // (this also guards against the grammar becoming ambiguous between "CREATE TABLE" and "CREATE TABLE AS") + checkInvalid('CREATE TABLE foo (a text, b text) AS SELECT 1'); + + checkCreateTableAs(['CREATE TABLE IF NOT EXISTS foo (a, b) AS SELECT x, y FROM bar WITH NO DATA'], { + type: 'create table as', + ifNotExists: true, + name: { name: 'foo' }, + columnNames: [{ name: 'a' }, { name: 'b' }], + query: { + type: 'select', + columns: [{ expr: { type: 'ref', name: 'x' } }, { expr: { type: 'ref', name: 'y' } }], + from: [{ type: 'table', name: { name: 'bar' } }], + }, + withData: false, + }); + + checkCreateTableAs(['CREATE TEMPORARY TABLE foo AS SELECT * FROM bar WITH DATA'], { + type: 'create table as', + temporary: true, + name: { name: 'foo' }, + query: { + type: 'select', + columns: [{ expr: { type: 'ref', name: '*' } }], + from: [{ type: 'table', name: { name: 'bar' } }], + }, + withData: true, + }); + + // ensure that statement is still parsed as a regular "CREATE TABLE" when there is no "as" + checkCreateTable(['CREATE TABLE foo (a text)'], { + type: 'create table', + name: { name: 'foo' }, + columns: [{ + kind: 'column', + name: { name: 'a' }, + dataType: { name: 'text' }, + }], + }); }); \ No newline at end of file diff --git a/src/syntax/main.ne b/src/syntax/main.ne index 611df40..6c2c835 100644 --- a/src/syntax/main.ne +++ b/src/syntax/main.ne @@ -47,6 +47,7 @@ statement -> statement_noprep | prepare | deallocate statement_noprep -> selection | createtable_statement + | createtableas_statement | createextension_statement | createindex_statement | simplestatements_all diff --git a/src/syntax/spec-utils.ts b/src/syntax/spec-utils.ts index 4dc800a..7f92ee6 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, BinaryOperator, ExprBinary, Name, ExprInteger, FromTable, QName, AlterIndexStatement, ExprNumeric } from './ast'; +import { Expr, SelectStatement, CreateTableStatement, CreateTableAsStatement, CreateIndexStatement, Statement, InsertStatement, UpdateStatement, AlterTableStatement, DeleteStatement, CreateExtensionStatement, CreateSequenceStatement, AlterSequenceStatement, SelectedColumn, Interval, BinaryOperator, ExprBinary, Name, ExprInteger, FromTable, QName, AlterIndexStatement, ExprNumeric } from './ast'; import { astMapper, IAstMapper } from '../ast-mapper'; import { toSql, IAstToSql } from '../to-sql'; import { parseIntervalLiteral } from '../parser'; @@ -23,6 +23,12 @@ export function checkCreateTable(value: string | string[], expected: CreateTable export function checkCreateTableLoc(value: string | string[], expected: CreateTableStatement) { checkTree(value, expected, (p, m) => m.statement(p), undefined, true); } +export function checkCreateTableAs(value: string | string[], expected: CreateTableAsStatement) { + checkTree(value, expected, (p, m) => m.statement(p)); +} +export function checkCreateTableAsLoc(value: string | string[], expected: CreateTableAsStatement) { + checkTree(value, expected, (p, m) => m.statement(p), undefined, true); +} export function checkCreateIndex(value: string | string[], expected: CreateIndexStatement) { checkTree(value, expected, (p, m) => m.statement(p)); } diff --git a/src/to-sql.ts b/src/to-sql.ts index 181023b..387e778 100644 --- a/src/to-sql.ts +++ b/src/to-sql.ts @@ -998,6 +998,29 @@ const visitor = astVisitor(m => ({ } }, + createTableAs: t => { + ret.push('CREATE '); + if (t.locality) { + ret.push(t.locality.toUpperCase(), ' '); + } + if (t.temporary) { + ret.push('TEMPORARY '); + } + if (t.unlogged) { + ret.push('UNLOGGED '); + } + ret.push(t.ifNotExists ? 'TABLE IF NOT EXISTS ' : 'TABLE '); + m.tableRef(t.name); + if (t.columnNames) { + list(t.columnNames, c => ret.push(name(c)), true); + } + ret.push(' AS '); + m.select(t.query); + if (t.withData !== undefined) { + ret.push(t.withData ? ' WITH DATA' : ' WITH NO DATA'); + } + }, + likeTable: l => { ret.push(' LIKE '); m.tableRef(l.like);