diff --git a/backend/config/config.exs b/backend/config/config.exs index be9cb9abb..29c1df3c2 100644 --- a/backend/config/config.exs +++ b/backend/config/config.exs @@ -28,8 +28,8 @@ config :azimutt, azimutt_github_issues_new: "https://github.com/azimuttapp/azimutt/issues/new", environment: config_env(), # TODO: find an automated process to build it - version: "2.1.11", - version_date: "2024-10-15T00:00:00.000Z", + version: "2.1.12", + version_date: "2024-11-09T00:00:00.000Z", commit_hash: System.cmd("git", ["log", "-1", "--pretty=format:%h"]) |> elem(0) |> String.trim(), commit_message: System.cmd("git", ["log", "-1", "--pretty=format:%s"]) |> elem(0) |> String.trim(), commit_date: System.cmd("git", ["log", "-1", "--pretty=format:%aI"]) |> elem(0) |> String.trim(), diff --git a/backend/lib/azimutt.ex b/backend/lib/azimutt.ex index 1aaceb191..c6182b36a 100644 --- a/backend/lib/azimutt.ex +++ b/backend/lib/azimutt.ex @@ -329,7 +329,7 @@ defmodule Azimutt do %{id: "aml", name: "AML", parse: true, generate: true}, %{id: "dbml", name: "DBML", parse: false, generate: false}, %{id: "json", name: "JSON", parse: true, generate: true}, - %{id: "postgres", name: "PostgreSQL", parse: false, generate: true}, + %{id: "postgres", name: "PostgreSQL", parse: true, generate: true}, %{id: "mysql", name: "MySQL", parse: false, generate: false}, %{id: "oracle", name: "Oracle", parse: false, generate: false}, %{id: "sqlserver", name: "SQL Server", parse: false, generate: false}, diff --git a/backend/lib/azimutt_web/controllers/api/clever_cloud_controller.ex b/backend/lib/azimutt_web/controllers/api/clever_cloud_controller.ex index 16997454b..ca0c0aa3e 100644 --- a/backend/lib/azimutt_web/controllers/api/clever_cloud_controller.ex +++ b/backend/lib/azimutt_web/controllers/api/clever_cloud_controller.ex @@ -37,7 +37,7 @@ defmodule AzimuttWeb.Api.CleverCloudController do end end - def migrations(conn, params) do + def migrations(_conn, params) do Logger.info("Api.CleverCloudController.migrations: #{inspect(params)}") :ok end diff --git a/backend/lib/azimutt_web/templates/website/converters/_editors-script.html.heex b/backend/lib/azimutt_web/templates/website/converters/_editors-script.html.heex index 1372a32d0..0de4712f6 100644 --- a/backend/lib/azimutt_web/templates/website/converters/_editors-script.html.heex +++ b/backend/lib/azimutt_web/templates/website/converters/_editors-script.html.heex @@ -1,5 +1,5 @@ - + @@ -95,6 +95,7 @@ if (lang === 'aml') return aml.parseAml(content) if (lang === 'amlv1') return aml.parseAml(content).mapError(errs => errs.filter(e => e.kind !== 'LegacySyntax')) if (lang === 'json') return aml.parseJsonDatabase(content) + if (lang === 'postgres') return sql.parseSql(content, 'postgres') return {errors: [{message: 'Unsupported source dialect: ' + lang, kind: 'UnsupportedDialect', level: 'error', offset: {start: 0, end: 100}, position: {start: {line: 1, column: 1}, end: {line: 10, column: 10}}}]} } catch (e) { return {errors: [{message: 'Failed to parse ' + lang + (e && e.message ? ': ' + e.message : ''), kind: 'DialectError', level: 'error', offset: {start: 0, end: 100}, position: {start: {line: 1, column: 1}, end: {line: 10, column: 10}}}]} diff --git a/libs/aml/resources/full.aml b/libs/aml/resources/full.aml index ceff76306..f9a682d6f 100644 --- a/libs/aml/resources/full.aml +++ b/libs/aml/resources/full.aml @@ -79,7 +79,7 @@ type slug | anonymous type type uid int {tags: [generic]} # alias type type cms.post_status (draft, published, archived) # enum type type position {x int, y int} # struct type -type box `(INTERNALLENGTH = 16, INPUT = lower, OUTPUT = lower)` # custom type +type box `(INPUT = lower, OUTPUT = lower, INTERNALLENGTH = 16)` # custom type namespace social. diff --git a/libs/aml/resources/full.json b/libs/aml/resources/full.json index afc941a27..39a9e049a 100644 --- a/libs/aml/resources/full.json +++ b/libs/aml/resources/full.json @@ -224,7 +224,7 @@ {"name": "uid", "alias": "int", "extra": {"line": 79, "statement": 15, "tags": ["generic"], "comment": "alias type"}}, {"schema": "cms", "name": "post_status", "values": ["draft", "published", "archived"], "extra": {"line": 80, "statement": 16, "comment": "enum type"}}, {"name": "position", "attrs": [{"name": "x", "type": "int"}, {"name": "y", "type": "int"}], "extra": {"line": 81, "statement": 17, "comment": "struct type"}}, - {"name": "box", "definition": "(INTERNALLENGTH = 16, INPUT = lower, OUTPUT = lower)", "extra": {"line": 82, "statement": 18, "comment": "custom type"}} + {"name": "box", "definition": "(INPUT = lower, OUTPUT = lower, INTERNALLENGTH = 16)", "extra": {"line": 82, "statement": 18, "comment": "custom type"}} ], "extra": { "comments": [{"line": 1, "comment": ""}, {"line": 2, "comment": "Full Schema AML"}, {"line": 3, "comment": ""}], diff --git a/libs/aml/resources/full.md b/libs/aml/resources/full.md index 3d63d6ca5..d938d54ca 100644 --- a/libs/aml/resources/full.md +++ b/libs/aml/resources/full.md @@ -165,7 +165,7 @@ STRUCT: ### box -EXPRESSION: (INTERNALLENGTH = 16, INPUT = lower, OUTPUT = lower) +EXPRESSION: (INPUT = lower, OUTPUT = lower, INTERNALLENGTH = 16) ## Diagram diff --git a/libs/aml/src/amlParser.ts b/libs/aml/src/amlParser.ts index d0ee4250e..40e2f6a5d 100644 --- a/libs/aml/src/amlParser.ts +++ b/libs/aml/src/amlParser.ts @@ -667,7 +667,7 @@ function tokenPosition(token: IToken): TokenPosition { } function pos(value: number | undefined): number { - return value !== undefined && !isNaN(value) ? value : defaultPos + return value !== undefined && !Number.isNaN(value) ? value : defaultPos } // utils functions diff --git a/libs/models/src/parserResult.ts b/libs/models/src/parserResult.ts index 9d66d8b27..0123ae3e1 100644 --- a/libs/models/src/parserResult.ts +++ b/libs/models/src/parserResult.ts @@ -61,7 +61,7 @@ export const tokenPosition = (offsetStart: number, offsetEnd: number, positionSt ({offset: {start: offsetStart, end: offsetEnd}, position: {start: {line: positionStartLine, column: positionStartColumn}, end: {line: positionEndLine, column: positionEndColumn}}}) export const mergePositions = (positions: (TokenPosition | undefined)[]): TokenPosition => { - const pos: TokenPosition[] = positions.filter(isNotUndefined) + const pos: TokenPosition[] = positions.filter(isNotUndefined).filter(p => !!p.offset) return ({ offset: {start: posStart(pos.map(p => p.offset.start)), end: posEnd(pos.map(p => p.offset.end))}, position: { @@ -84,11 +84,11 @@ export const positionEndAdd = (pos: T, value: number): }) const posStart = (values: number[]): number => { - const valid = values.filter(n => n >= 0 && !isNaN(n) && isFinite(n)) + const valid = values.filter(n => n >= 0 && !Number.isNaN(n) && Number.isFinite(n)) return valid.length > 0 ? Math.min(...valid) : 0 } const posEnd = (values: number[]): number => { - const valid = values.filter(n => n >= 0 && !isNaN(n) && isFinite(n)) + const valid = values.filter(n => n >= 0 && !Number.isNaN(n) && Number.isFinite(n)) return valid.length > 0 ? Math.max(...valid) : 0 } diff --git a/libs/parser-sql/package.json b/libs/parser-sql/package.json index 3dbb2efe0..39f099d13 100644 --- a/libs/parser-sql/package.json +++ b/libs/parser-sql/package.json @@ -1,6 +1,6 @@ { "name": "@azimutt/parser-sql", - "version": "0.1.1", + "version": "0.1.2", "description": "Parse and Generate SQL.", "keywords": [], "homepage": "https://azimutt.app", diff --git a/libs/parser-sql/resources/complex.postgres.sql b/libs/parser-sql/resources/complex.postgres.sql new file mode 100644 index 000000000..43954ccf0 --- /dev/null +++ b/libs/parser-sql/resources/complex.postgres.sql @@ -0,0 +1,28 @@ +-- +-- A file with several statements more or less complex to check the parser is works fine. +-- They don't make sense for a particular db ^^ +-- + +/* + * first query + * Author: Loïc + */ +select id /* primary key */, name +from users -- table +where role = 'admin'; + +DROP TABLE IF EXISTS users, public.posts CASCADE; + +CREATE TABLE users ( + id int PRIMARY KEY, + first_name varchar NOT NULL, + last_name varchar NOT NULL, + CONSTRAINT name_uniq UNIQUE (first_name, last_name), + email varchar UNIQUE CHECK ( email LIKE '%@%' ), + role varchar DEFAULT 'guest' +); + +CREATE TABLE public.posts ( + id int PRIMARY KEY, + author int CONSTRAINT posts_author_fk REFERENCES users(id) +); diff --git a/libs/parser-sql/resources/full.postgres.sql b/libs/parser-sql/resources/full.postgres.sql index b90ad32ce..617584e99 100644 --- a/libs/parser-sql/resources/full.postgres.sql +++ b/libs/parser-sql/resources/full.postgres.sql @@ -16,7 +16,7 @@ COMMENT ON TYPE slug IS 'anonymous type'; -- CREATE TYPE uid AS int; -- type alias not supported on PostgreSQL CREATE TYPE cms.post_status AS ENUM ('draft', 'published', 'archived'); CREATE TYPE position AS (x int, y int); -CREATE TYPE box (INTERNALLENGTH = 16, INPUT = lower, OUTPUT = lower); +CREATE TYPE box (INPUT = lower, OUTPUT = lower, INTERNALLENGTH = 16); -- -- Full Schema AML diff --git a/libs/parser-sql/src/errors.ts b/libs/parser-sql/src/errors.ts new file mode 100644 index 000000000..9e9f9106c --- /dev/null +++ b/libs/parser-sql/src/errors.ts @@ -0,0 +1,4 @@ +import {ParserError, ParserErrorLevel, TokenPosition} from "@azimutt/models"; + +export const duplicated = (name: string, definedAtLine: number | undefined, position: TokenPosition): ParserError => + ({message: `${name} already defined${definedAtLine !== undefined ? ` at line ${definedAtLine}` : ''}`, kind: 'Duplicated', level: ParserErrorLevel.enum.warning, offset: position.offset, position: position.position}) diff --git a/libs/parser-sql/src/postgresAst.ts b/libs/parser-sql/src/postgresAst.ts new file mode 100644 index 000000000..5857978cd --- /dev/null +++ b/libs/parser-sql/src/postgresAst.ts @@ -0,0 +1,196 @@ +import {ParserErrorLevel, TokenPosition} from "@azimutt/models"; + +/** + * Conventions: + * - use `kind` attribute for discriminated unions or enum values + * - use `value` attribute for the actual source value + * - keep all token positions + * - statement positions are start/end, all other positions are the specific token + */ + +// statements +export type PostgresAst = StatementsAst & { comments?: CommentAst[] } +export type PostgresStatementAst = StatementAst & { comments?: CommentAst[] } +export type StatementsAst = { statements: StatementAst[] } +export type StatementAst = { meta: TokenInfo } & (AlterSchemaStatementAst | AlterSequenceStatementAst | + AlterTableStatementAst | BeginStatementAst | CommentOnStatementAst | CommitStatementAst | + CreateExtensionStatementAst | CreateFunctionStatementAst | CreateIndexStatementAst | + CreateMaterializedViewStatementAst | CreateSchemaStatementAst | CreateSequenceStatementAst | + CreateTableStatementAst | CreateTriggerStatementAst | CreateTypeStatementAst | CreateViewStatementAst | DeleteStatementAst | DropStatementAst | + InsertIntoStatementAst | SelectStatementAst | SetStatementAst | ShowStatementAst | UpdateStatementAst) +export type AlterSchemaStatementAst = { kind: 'AlterSchema', token: TokenInfo, schema: IdentifierAst, action: SchemaAlterActionAst } +export type AlterSequenceStatementAst = { kind: 'AlterSequence', token: TokenInfo, ifExists?: TokenAst, schema?: IdentifierAst, name: IdentifierAst, as?: SequenceTypeAst, start?: SequenceParamAst, increment?: SequenceParamAst, minValue?: SequenceParamOptAst, maxValue?: SequenceParamOptAst, cache?: SequenceParamAst, ownedBy?: SequenceOwnedByAst } +export type AlterTableStatementAst = { kind: 'AlterTable', token: TokenInfo, ifExists?: TokenAst, only?: TokenAst, schema?: IdentifierAst, table: IdentifierAst, actions: TableAlterActionAst[] } +export type BeginStatementAst = { kind: 'Begin', token: TokenInfo, object?: TransactionObjectAst, modes?: TransactionModeAst[] } +export type CommentOnStatementAst = { kind: 'CommentOn', token: TokenInfo, object: CommentObjectAst, schema?: IdentifierAst, parent?: IdentifierAst, entity: IdentifierAst, comment: StringAst | NullAst } +export type CommitStatementAst = { kind: 'Commit', token: TokenInfo, object?: TransactionObjectAst, chain?: TransactionChainAst } +export type CreateExtensionStatementAst = { kind: 'CreateExtension', token: TokenInfo, ifNotExists?: TokenAst, name: IdentifierAst, with?: TokenAst, schema?: NameAst, version?: ExtensionVersionAst, cascade?: TokenAst } +export type CreateFunctionStatementAst = { kind: 'CreateFunction', token: TokenInfo, replace?: TokenAst, schema?: IdentifierAst, name: IdentifierAst, args: FunctionArgumentAst[], returns?: FunctionReturnsAst, language?: NameAst, behavior?: FunctionBehaviorAst, nullBehavior?: FunctionNullBehaviorAst, definition?: FunctionDefinitionAst, return?: FunctionReturnAst } +export type CreateIndexStatementAst = { kind: 'CreateIndex', token: TokenInfo, unique?: TokenAst, concurrently?: TokenAst, ifNotExists?: TokenAst, name?: IdentifierAst, only?: TokenAst, schema?: IdentifierAst, table: IdentifierAst, using?: IndexUsingAst, columns: IndexColumnAst[], include?: IndexIncludeAst, where?: WhereClauseAst } +export type CreateMaterializedViewStatementAst = { kind: 'CreateMaterializedView', token: TokenInfo, ifNotExists?: TokenAst, schema?: IdentifierAst, name: IdentifierAst, columns?: IdentifierAst[], query: SelectInnerAst, withData?: ViewMaterializedDataAst } +export type CreateSchemaStatementAst = { kind: 'CreateSchema', token: TokenInfo, ifNotExists?: TokenAst, schema?: IdentifierAst, authorization?: SchemaAuthorizationAst } +export type CreateSequenceStatementAst = { kind: 'CreateSequence', token: TokenInfo, mode?: SequenceModeAst, ifNotExists?: TokenAst, schema?: IdentifierAst, name: IdentifierAst, as?: SequenceTypeAst, start?: SequenceParamAst, increment?: SequenceParamAst, minValue?: SequenceParamOptAst, maxValue?: SequenceParamOptAst, cache?: SequenceParamAst, ownedBy?: SequenceOwnedByAst } +export type CreateTableStatementAst = { kind: 'CreateTable', token: TokenInfo, mode?: TableCreateModeAst, ifNotExists?: TokenAst, schema?: IdentifierAst, name: IdentifierAst, columns?: TableColumnAst[], constraints?: TableConstraintAst[] } +export type CreateTriggerStatementAst = { kind: 'CreateTrigger', token: TokenInfo, replace?: TokenAst, constraint?: TokenAst, name: IdentifierAst, timing: TriggerTimingAst, events?: TriggerEventAst[], schema?: IdentifierAst, table: IdentifierAst, from?: TriggerFromAst, deferrable?: TriggerDeferrableAst, referencing?: TriggerReferencingAst, target?: TriggerTargetAst, when?: FilterAst, execute: TriggerExecuteAst } +export type CreateTypeStatementAst = { kind: 'CreateType', token: TokenInfo, schema?: IdentifierAst, name: IdentifierAst, struct?: TypeStructAst, enum?: TypeEnumAst, base?: TypeBaseAttrAst[] } +export type CreateViewStatementAst = { kind: 'CreateView', token: TokenInfo, replace?: TokenAst, temporary?: TokenAst, recursive?: TokenAst, schema?: IdentifierAst, name: IdentifierAst, columns?: IdentifierAst[], query: SelectInnerAst } +export type DeleteStatementAst = { kind: 'Delete', token: TokenInfo, only?: TokenAst, schema?: IdentifierAst, table: IdentifierAst, descendants?: TokenAst, alias?: AliasAst, using?: FromClauseItemAst & TokenAst, where?: WhereClauseAst, returning?: SelectClauseAst } +export type DropStatementAst = { kind: 'Drop', token: TokenInfo, object: DropObject, entities: ObjectNameAst[], concurrently?: TokenAst, ifExists?: TokenAst, mode?: DropModeAst } +export type InsertIntoStatementAst = { kind: 'InsertInto', token: TokenInfo, schema?: IdentifierAst, table: IdentifierAst, columns?: IdentifierAst[], values: InsertValueAst[][], onConflict?: OnConflictClauseAst, returning?: SelectClauseAst } +export type SelectStatementAst = { kind: 'Select' } & SelectInnerAst +export type SetStatementAst = { kind: 'Set', token: TokenInfo, scope?: SetModeAst, parameter: IdentifierAst, equal?: SetAssignAst, value: SetValueAst } +export type ShowStatementAst = { kind: 'Show', token: TokenInfo, name: IdentifierAst } +export type UpdateStatementAst = { kind: 'Update', token: TokenInfo, only?: TokenAst, schema?: IdentifierAst, table: IdentifierAst, descendants?: TokenAst, alias?: AliasAst, columns: ColumnUpdateAst[], where?: WhereClauseAst, returning?: SelectClauseAst } + +// select clauses +export type SelectInnerAst = SelectMainAst & SelectResultAst +export type SelectMainAst = SelectClauseAst & { from?: FromClauseAst, where?: WhereClauseAst, groupBy?: GroupByClauseAst, having?: HavingClauseAst, window?: WindowClauseAst[] } +export type SelectResultAst = { union?: UnionClauseAst, orderBy?: OrderByClauseAst, limit?: LimitClauseAst, offset?: OffsetClauseAst, fetch?: FetchClauseAst } +export type SelectClauseAst = { token: TokenInfo, distinct?: { token: TokenInfo, on?: { token: TokenInfo, columns: ExpressionAst[] } }, columns: SelectClauseColumnAst[] } +export type SelectClauseColumnAst = ExpressionAst & { filter?: { token: TokenInfo, where: WhereClauseAst }, over?: TokenAst & ({ name: IdentifierAst } | WindowClauseContentAst), alias?: AliasAst } +export type FromClauseAst = FromClauseItemAst & { token: TokenInfo, joins?: FromClauseJoinAst[] } +export type FromClauseItemAst = (FromClauseTableAst | FromClauseQueryAst) & { alias?: AliasAst } +export type FromClauseTableAst = { kind: 'Table', schema?: IdentifierAst, table: IdentifierAst } +export type FromClauseQueryAst = { kind: 'Select', select: SelectInnerAst } +export type FromClauseJoinAst = { kind: JoinKind, token: TokenInfo, from: FromClauseItemAst, on: JoinOnAst | JoinUsingAst | JoinNaturalAst, alias?: AliasAst } +export type JoinOnAst = { kind: 'On', token: TokenInfo, predicate: ExpressionAst } +export type JoinUsingAst = { kind: 'Using', token: TokenInfo, columns: IdentifierAst[] } +export type JoinNaturalAst = { kind: 'Natural', token: TokenInfo } +export type WhereClauseAst = { token: TokenInfo, predicate: ExpressionAst } +export type GroupByClauseAst = { token: TokenInfo, mode: { kind: 'All' | 'Distinct', token: TokenInfo }, expressions: ExpressionAst[] } +export type HavingClauseAst = { token: TokenInfo, predicate: ExpressionAst } +export type WindowClauseAst = NameAst & WindowClauseContentAst +export type WindowClauseContentAst = { partitionBy?: { token: TokenInfo, columns: ExpressionAst[] }, orderBy?: OrderByClauseAst } +export type UnionClauseAst = { kind: 'Union' | 'Intersect' | 'Except', token: TokenInfo, mode: { kind: 'All' | 'Distinct', token: TokenInfo }, select: SelectInnerAst } // TODO: VALUES also allowed +export type OrderByClauseAst = { token: TokenInfo, expressions: (ExpressionAst & { order?: SortOrderAst, nulls?: SortNullsAst })[] } +export type LimitClauseAst = { token: TokenInfo, value: IntegerAst | ParameterAst | ({ kind: 'All', token: TokenInfo }) } +export type OffsetClauseAst = { token: TokenInfo, value: IntegerAst | ParameterAst, rows?: { kind: 'Rows' | 'Row', token: TokenInfo } } +export type FetchClauseAst = { token: TokenInfo, first: { kind: 'First' | 'Next', token: TokenInfo }, value: IntegerAst | ParameterAst, rows: { kind: 'Rows' | 'Row', token: TokenInfo }, mode: { kind: 'Only' | 'WithTies', token: TokenInfo } } + +// other clauses +export type ColumnAlterActionAst = ColumnAlterDefaultAst | ColumnAlterNotNullAst +export type ColumnAlterDefaultAst = { kind: 'Default', action: { kind: 'Set' | 'Drop', token: TokenInfo }, token: TokenInfo, expression?: ExpressionAst } +export type ColumnAlterNotNullAst = { kind: 'NotNull', action: { kind: 'Set' | 'Drop', token: TokenInfo }, token: TokenInfo } +export type ColumnTypeAst = { token: TokenInfo, schema?: IdentifierAst, name: IdentifierAst, args?: IntegerAst[], array?: TokenAst } +export type ColumnUpdateAst = { column: IdentifierAst, value: InsertValueAst } +export type CommentObjectAst = { kind: CommentObject, token: TokenInfo } +export type DropModeAst = { kind: DropMode, token: TokenInfo } +export type ExtensionVersionAst = { token: TokenInfo, number: StringAst | IdentifierAst } +export type ForeignKeyActionAst = { action: { kind: ForeignKeyAction, token: TokenInfo }, columns?: IdentifierAst[] } +export type FunctionArgumentAst = { mode?: { kind: 'In' | 'Out' | 'InOut' | 'Variadic', token: TokenInfo }, name?: IdentifierAst, type: ColumnTypeAst } +export type FunctionBehaviorAst = { kind: 'Immutable' | 'Stable' | 'Volatile', token: TokenInfo } +export type FunctionDefinitionAst = { token: TokenInfo, value: StringAst } +export type FunctionNullBehaviorAst = { kind: 'Called' | 'ReturnsNull' | 'Strict', token: TokenInfo } +export type FunctionReturnAst = { token: TokenInfo, expression: ExpressionAst } +export type FunctionReturnsAst = { kind: 'Type', token: TokenInfo, setOf?: TokenAst, type: ColumnTypeAst } | { kind: 'Table', token: TokenInfo, columns: { name: IdentifierAst, type: ColumnTypeAst }[] } +export type IndexColumnAst = ExpressionAst & { collation?: NameAst, order?: SortOrderAst, nulls?: SortNullsAst } +export type IndexIncludeAst = { token: TokenInfo, columns: IdentifierAst[] } +export type IndexUsingAst = { token: TokenInfo, method: IdentifierAst } +export type InsertValueAst = ExpressionAst | { kind: 'Default', token: TokenInfo } +export type OnConflictClauseAst = { token: TokenInfo, target?: OnConflictColumnsAst | OnConflictConstraintAst, action: OnConflictNothingAst | OnConflictUpdateAst } +export type OnConflictColumnsAst = { kind: 'Columns', columns: IdentifierAst[], where?: WhereClauseAst } +export type OnConflictConstraintAst = { kind: 'Constraint', token: TokenInfo, name: IdentifierAst } +export type OnConflictNothingAst = { kind: 'Nothing', token: TokenInfo } +export type OnConflictUpdateAst = { kind: 'Update', columns: ColumnUpdateAst[], where?: WhereClauseAst } +export type SchemaAlterActionAst = { kind: 'Rename', token: TokenInfo, schema: IdentifierAst } | { kind: 'Owner', token: TokenInfo, owner: OwnerAst } +export type SchemaAuthorizationAst = { token: TokenInfo, owner: OwnerAst } +export type SequenceModeAst = { kind: 'Unlogged' | 'Temporary', token: TokenInfo } +export type SequenceOwnedByAst = { token: TokenInfo, owner: { kind: 'None', token: TokenInfo } | { kind: 'Column', schema?: IdentifierAst, table: IdentifierAst, column: IdentifierAst } } +export type SequenceParamAst = { token: TokenInfo, value: IntegerAst } +export type SequenceParamOptAst = { token: TokenInfo, value?: IntegerAst } +export type SequenceTypeAst = { token: TokenInfo, type: IdentifierAst } +export type SetAssignAst = { kind: SetAssign, token: TokenInfo } +export type SetModeAst = { kind: SetScope, token: TokenInfo } +export type SetValueAst = IdentifierAst | LiteralAst | (IdentifierAst | LiteralAst)[] | { kind: 'Default', token: TokenInfo } +export type TableAlterActionAst = TableAddColumnAst | TableAddConstraintAst | TableAlterColumnAst | TableDropColumnAst | TableDropConstraintAst | TableSetOwnerAst +export type TableAddColumnAst = { kind: 'AddColumn', token: TokenInfo, ifNotExists?: TokenAst, column: TableColumnAst } +export type TableAddConstraintAst = { kind: 'AddConstraint', token: TokenInfo, constraint: TableConstraintAst, notValid?: TokenAst } +export type TableAlterColumnAst = { kind: 'AlterColumn', token: TokenInfo, column: IdentifierAst, action: ColumnAlterActionAst } +export type TableDropColumnAst = { kind: 'DropColumn', token: TokenInfo, ifExists?: TokenAst, column: IdentifierAst } +export type TableDropConstraintAst = { kind: 'DropConstraint', token: TokenInfo, ifExists?: TokenAst, constraint: IdentifierAst } +export type TableSetOwnerAst = { kind: 'SetOwner', token: TokenInfo, owner: OwnerAst } +export type TableColumnAst = { name: IdentifierAst, type: ColumnTypeAst, constraints?: TableColumnConstraintAst[] } +export type TableColumnConstraintAst = TableColumnNullableAst | TableColumnDefaultAst | TableColumnPkAst | TableColumnUniqueAst | TableColumnCheckAst | TableColumnFkAst +export type TableColumnNullableAst = { kind: 'Nullable', value: boolean } & TableConstraintCommonAst +export type TableColumnDefaultAst = { kind: 'Default', expression: ExpressionAst } & TableConstraintCommonAst +export type TableColumnPkAst = { kind: 'PrimaryKey' } & TableConstraintCommonAst +export type TableColumnUniqueAst = { kind: 'Unique' } & TableConstraintCommonAst +export type TableColumnCheckAst = { kind: 'Check', predicate: ExpressionAst } & TableConstraintCommonAst +export type TableColumnFkAst = { kind: 'ForeignKey', schema?: IdentifierAst, table: IdentifierAst, column?: IdentifierAst, onUpdate?: ForeignKeyActionAst & {token: TokenInfo}, onDelete?: ForeignKeyActionAst & {token: TokenInfo} } & TableConstraintCommonAst +export type TableConstraintAst = TablePkAst | TableUniqueAst | TableCheckAst | TableFkAst +export type TablePkAst = { kind: 'PrimaryKey', columns: IdentifierAst[] } & TableConstraintCommonAst +export type TableUniqueAst = { kind: 'Unique', columns: IdentifierAst[] } & TableConstraintCommonAst +export type TableCheckAst = { kind: 'Check', predicate: ExpressionAst } & TableConstraintCommonAst +export type TableFkAst = { kind: 'ForeignKey', columns: IdentifierAst[], ref: { token: TokenInfo, schema?: IdentifierAst, table: IdentifierAst, columns?: IdentifierAst[] }, onUpdate?: ForeignKeyActionAst & {token: TokenInfo}, onDelete?: ForeignKeyActionAst & {token: TokenInfo} } & TableConstraintCommonAst +export type TableConstraintCommonAst = { token: TokenInfo, constraint?: NameAst } +export type TableCreateModeAst = ({ kind: 'Unlogged', token: TokenInfo }) | ({ kind: 'Temporary', token: TokenInfo, scope?: { kind: 'Local' | 'Global', token: TokenInfo } }) +export type TransactionChainAst = { token: TokenInfo, no?: TokenAst } +export type TransactionModeAst = { kind: 'IsolationLevel', token: TokenInfo, level: { kind: 'Serializable' | 'RepeatableRead' | 'ReadCommitted' | 'ReadUncommitted', token: TokenInfo } } | { kind: 'ReadWrite' | 'ReadOnly', token: TokenInfo } | { kind: 'Deferrable', token: TokenInfo, not?: TokenAst } +export type TransactionObjectAst = { kind: 'Work' | 'Transaction', token: TokenInfo } +export type TriggerDeferrableAst = { kind: 'Deferrable' | 'NotDeferrable', token: TokenInfo, initially?: { kind: 'Immediate' | 'Deferred', token: TokenInfo } } +export type TriggerEventAst = { kind: 'Insert' | 'Update' | 'Delete' | 'Truncate', token: TokenInfo, columns?: IdentifierAst } +export type TriggerExecuteAst = { token: TokenInfo, schema?: IdentifierAst, function: IdentifierAst, arguments: ExpressionAst[] } +export type TriggerFromAst = { token: TokenInfo, schema?: IdentifierAst, table: IdentifierAst } +export type TriggerReferencingAst = { token: TokenInfo, old?: NameAst, new?: NameAst } +export type TriggerTargetAst = { kind: 'Row' | 'Statement', token: TokenInfo } +export type TriggerTimingAst = { kind: 'Before' | 'After' | 'InsteadOf', token: TokenInfo } +export type TypeBaseAttrAst = { name: IdentifierAst, value: ExpressionAst } +export type TypeColumnAst = { name: IdentifierAst, type: ColumnTypeAst, collation?: NameAst } +export type TypeEnumAst = { token: TokenInfo, values: StringAst[] } +export type TypeStructAst = { token: TokenInfo, attrs: TypeColumnAst[] } +export type ViewMaterializedDataAst = { token: TokenInfo, no?: TokenAst } + +// basic parts +export type AliasAst = { token?: TokenInfo, name: IdentifierAst } +export type NameAst = { token: TokenInfo, name: IdentifierAst } +export type FilterAst = { token: TokenInfo, condition: ExpressionAst } +export type ObjectNameAst = { schema?: IdentifierAst, name: IdentifierAst } +export type OwnerAst = { kind: 'User', name: IdentifierAst } | { kind: 'CurrentRole' | 'CurrentUser' | 'SessionUser', token: TokenInfo } +export type ExpressionAst = (LiteralAst | ParameterAst | ColumnAst | WildcardAst | FunctionAst | GroupAst | OperationAst | OperationLeftAst | OperationRightAst | ArrayAst | ListAst) & { cast?: { token: TokenInfo, type: ColumnTypeAst } } +export type LiteralAst = StringAst | IntegerAst | DecimalAst | BooleanAst | NullAst +export type ColumnAst = { kind: 'Column', schema?: IdentifierAst, table?: IdentifierAst, column: IdentifierAst, json?: ColumnJsonAst[] } +export type ColumnJsonAst = { kind: JsonOp, token: TokenInfo, field: StringAst | ParameterAst } +export type FunctionAst = { kind: 'Function', schema?: IdentifierAst, function: IdentifierAst, distinct?: TokenAst, parameters: ExpressionAst[] } +export type GroupAst = { kind: 'Group', expression: ExpressionAst } +export type WildcardAst = { kind: 'Wildcard', token: TokenInfo, schema?: IdentifierAst, table?: IdentifierAst } +export type OperationAst = { kind: 'Operation', left: ExpressionAst, op: OperatorAst, right: ExpressionAst } +export type OperatorAst = { kind: Operator, token: TokenInfo } +export type OperationLeftAst = { kind: 'OperationLeft', op: OperatorLeftAst, right: ExpressionAst } +export type OperatorLeftAst = { kind: OperatorLeft, token: TokenInfo } +export type OperationRightAst = { kind: 'OperationRight', left: ExpressionAst, op: OperatorRightAst } +export type OperatorRightAst = { kind: OperatorRight, token: TokenInfo } +export type ArrayAst = { kind: 'Array', token: TokenInfo, items: ExpressionAst[] } +export type ListAst = { kind: 'List', items: LiteralAst[] } +export type SortOrderAst = { kind: SortOrder, token: TokenInfo } +export type SortNullsAst = { kind: SortNulls, token: TokenInfo } + +// elements +export type ParameterAst = { kind: 'Parameter', token: TokenInfo, value: string, index?: number } +export type IdentifierAst = { kind: 'Identifier', token: TokenInfo, value: string, quoted?: boolean } +export type StringAst = { kind: 'String', token: TokenInfo, value: string, escaped?: boolean, dollar?: string } +export type DecimalAst = { kind: 'Decimal', token: TokenInfo, value: number } +export type IntegerAst = { kind: 'Integer', token: TokenInfo, value: number } +export type BooleanAst = { kind: 'Boolean', token: TokenInfo, value: boolean } +export type NullAst = { kind: 'Null', token: TokenInfo } +export type CommentAst = { kind: CommentKind, token: TokenInfo, value: string } // special case +export type TokenAst = { token: TokenInfo } + +// enums +export type Operator = '+' | '-' | '*' | '/' | '%' | '^' | '&' | '|' | '#' | '<<' | '>>' | '=' | '<' | '>' | '<=' | '>=' | '<>' | '!=' | '||' | '~' | '~*' | '!~' | '!~*' | 'Is' | 'IsNot' | 'Like' | 'NotLike' | 'In' | 'NotIn' | 'DistinctFrom' | 'NotDistinctFrom' | 'Or' | 'And' +export type OperatorLeft = 'Not' | 'Interval' | '~' +export type OperatorRight = 'IsNull' | 'NotNull' +export type JsonOp = '->' | '->>' | '#>' | '#>>' +export type ForeignKeyAction = 'NoAction' | 'Restrict' | 'Cascade' | 'SetNull' | 'SetDefault' +export type DropObject = 'Index' | 'MaterializedView' | 'Sequence' | 'Table' | 'Type' | 'View' +export type DropMode = 'Cascade' | 'Restrict' +export type CommentObject = 'Column' | 'Constraint' | 'Database' | 'Extension' | 'Index' | 'MaterializedView' | 'Schema' | 'Table' | 'Type' | 'View' +export type JoinKind = 'Inner' | 'Left' | 'Right' | 'Full' | 'Cross' +export type SortOrder = 'Asc' | 'Desc' +export type SortNulls = 'First' | 'Last' +export type SetScope = 'Session' | 'Local' +export type SetAssign = '=' | 'To' +export type CommentKind = 'line' | 'block' | 'doc' + +// helpers +export type TokenInfo = TokenPosition & { issues?: TokenIssue[] } +export type TokenIssue = { message: string, kind: string, level: ParserErrorLevel } diff --git a/libs/parser-sql/src/postgresBuilder.test.ts b/libs/parser-sql/src/postgresBuilder.test.ts new file mode 100644 index 000000000..596352c54 --- /dev/null +++ b/libs/parser-sql/src/postgresBuilder.test.ts @@ -0,0 +1,212 @@ +import * as fs from "fs"; +import {describe, expect, test} from "@jest/globals"; +import {removeFieldsDeep} from "@azimutt/utils"; +import {Database, Entity, parseJsonDatabase, ParserError} from "@azimutt/models"; +import {SelectInnerAst} from "./postgresAst"; +import {parsePostgresAst} from "./postgresParser"; +import {buildPostgresDatabase, SelectEntities, selectEntities} from "./postgresBuilder"; + +describe('postgresBuilder', () => { + test('empty', () => { + expect(parse('')).toEqual({db: {}, errors: []}) + }) + test('complex', () => { + const input = ` +CREATE TABLE users ( + id int PRIMARY KEY, + name varchar NOT NULL DEFAULT 'anon' CONSTRAINT users_name_uniq UNIQUE, + role varchar NOT NULL, + CHECK ( length(name) >= 4 ) +); +CREATE INDEX ON users (role); +ALTER TABLE users ADD COLUMN created_at timestamp NOT NULL DEFAULT now(); +ALTER TABLE users DROP CONSTRAINT users_name_uniq; +ALTER TABLE users ALTER COLUMN role SET DEFAULT 'guest'; +COMMENT ON TABLE users IS 'List users'; +COMMENT ON COLUMN users.name IS 'user name'; +COMMENT ON COLUMN users.role IS 'user role'; +COMMENT ON COLUMN users.role IS NULL; + +CREATE TABLE cms.posts ( + id int PRIMARY KEY, + title varchar CHECK ( length(title) > 10 ), + author int CONSTRAINT posts_author_fk REFERENCES users(id), + created_at timestamp NOT NULL DEFAULT now() +); +ALTER TABLE cms.posts DROP COLUMN created_at; +ALTER TABLE cms.posts ADD UNIQUE (title); +ALTER TABLE cms.posts ALTER COLUMN title SET NOT NULL; +COMMENT ON CONSTRAINT posts_author_fk ON cms.posts IS 'posts fk'; + +CREATE VIEW admins AS SELECT id, name FROM users WHERE role='admin'; + +CREATE TYPE bug_status AS ENUM ('open', 'closed'); +COMMENT ON TYPE bug_status IS 'bug status'; +` + const db: Database = { + entities: [ + { + name: 'users', + attrs: [ + {name: 'id', type: 'int'}, + {name: 'name', type: 'varchar', default: 'anon', doc: 'user name'}, + {name: 'role', type: 'varchar', default: 'guest'}, + {name: 'created_at', type: 'timestamp', default: '`now()`'}, + ], + pk: {attrs: [['id']]}, + indexes: [{attrs: [['role']]}], + checks: [{attrs: [['name']], predicate: 'length(name) >= 4'}], + doc: 'List users' + }, + { + schema: 'cms', + name: 'posts', + attrs: [ + {name: 'id', type: 'int'}, + {name: 'title', type: 'varchar'}, + {name: 'author', type: 'int', null: true}, + ], + pk: {attrs: [['id']]}, + indexes: [{attrs: [['title']], unique: true}], + checks: [{attrs: [['title']], predicate: 'length(title) > 10'}] + }, + { + name: 'admins', + kind: 'view', + def: "SELECT id, name FROM users WHERE role = 'admin'", + attrs: [ + {name: 'id', type: 'int'}, + {name: 'name', type: 'varchar'}, + ] + } + ], + relations: [ + {name: 'posts_author_fk', src: {schema: 'cms', entity: 'posts', attrs: [['author']]}, ref: {entity: 'users', attrs: [['id']]}, doc: 'posts fk'}, + ], + types: [ + {name: 'bug_status', values: ['open', 'closed'], doc: 'bug status'}, + ], + } + expect(parse(input)).toEqual({db, errors: []}) + }) + test.skip('full', () => { // won't work, some concepts can't be handled in SQL, yet, worth checking the diff ^^ + const json = parseJsonDatabase(fs.readFileSync('../aml/resources/full.json', 'utf8')) + const db: Database = json.result || {} + const sql = fs.readFileSync('./resources/full.postgres.sql', 'utf8') + const parsed = parse(sql) + expect(parsed.errors).toEqual([]) + expect(removeFieldsDeep(parsed.db, ['extra'])).toEqual(removeFieldsDeep(db, ['extra'])) + }) + describe('selectEntities', () => { + const users: Entity = {schema: 'public', name: 'users', attrs: [{name: 'id', type: 'int'}, {name: 'name', type: 'varchar'}]} + const events: Entity = {schema: 'public', name: 'events', attrs: [{name: 'id', type: 'int'}, {name: 'name', type: 'varchar'}, {name: 'created_by', type: 'int'}]} + test('simple', () => { + expect(extract('SELECT id, name FROM users;', [])).toEqual({ + columns: [ + {name: 'id', sources: [{table: 'users', column: 'id'}]}, + {name: 'name', sources: [{table: 'users', column: 'name'}]} + ], + sources: [{name: 'users', from: {kind: 'Table', table: 'users'}}] + }) + expect(extract('SELECT id, name FROM users;', [users])).toEqual({ + columns: [ + {name: 'id', type: 'int', sources: [{schema: 'public', table: 'users', column: 'id', type: 'int'}]}, + {name: 'name', type: 'varchar', sources: [{schema: 'public', table: 'users', column: 'name', type: 'varchar'}]} + ], + sources: [{name: 'users', from: {kind: 'Table', schema: 'public', table: 'users', columns: [{name: 'id', type: 'int'}, {name: 'name', type: 'varchar'}]}}] + }) + }) + test('wildcard', () => { + expect(extract('SELECT * FROM users;', [])).toEqual({ + columns: [{name: '*', sources: []}], + sources: [{name: 'users', from: {kind: 'Table', table: 'users'}}] + }) + expect(extract('SELECT * FROM users;', [users])).toEqual({ + columns: [ + {name: 'id', type: 'int', sources: [{schema: 'public', table: 'users', column: 'id', type: 'int'}]}, + {name: 'name', type: 'varchar', sources: [{schema: 'public', table: 'users', column: 'name', type: 'varchar'}]} + ], + sources: [{name: 'users', from: {kind: 'Table', schema: 'public', table: 'users', columns: [{name: 'id', type: 'int'}, {name: 'name', type: 'varchar'}]}}] + }) + }) + test('no from', () => { + expect(extract('SELECT 1;', [])).toEqual({ + columns: [{name: 'col_1', sources: []}], + sources: [] + }) + }) + test('function', () => { + expect(extract('SELECT first_name || last_name FROM users;', [])).toEqual({ + columns: [{name: 'col_1', sources: [{table: 'users', column: 'first_name'}, {table: 'users', column: 'last_name'}]}], + sources: [{name: 'users', from: {kind: 'Table', table: 'users'}}] + }) + expect(extract('SELECT lower(name) FROM users;', [])).toEqual({ + columns: [{name: 'lower', sources: [{table: 'users', column: 'name'}]}], + sources: [{name: 'users', from: {kind: 'Table', table: 'users'}}] + }) + expect(extract('SELECT count(*) FROM users;', [])).toEqual({ + columns: [{name: 'count', sources: []}], + sources: [{name: 'users', from: {kind: 'Table', table: 'users'}}] + }) + }) + test('join', () => { + expect(extract('SELECT u.name, e.* FROM events e JOIN users u ON e.created_by = u.id;', [users, events])).toEqual({ + columns: [ + {table: 'u', name: 'name', type: 'varchar', sources: [{schema: 'public', table: 'users', column: 'name', type: 'varchar'}]}, + {table: 'e', name: 'id', type: 'int', sources: [{schema: 'public', table: 'events', column: 'id', type: 'int'}]}, + {table: 'e', name: 'name', type: 'varchar', sources: [{schema: 'public', table: 'events', column: 'name', type: 'varchar'}]}, + {table: 'e', name: 'created_by', type: 'int', sources: [{schema: 'public', table: 'events', column: 'created_by', type: 'int'}]}, + ], + sources: [ + {name: 'e', from: {kind: 'Table', schema: 'public', table: 'events', columns: [{name: 'id', type: 'int'}, {name: 'name', type: 'varchar'}, {name: 'created_by', type: 'int'}]}}, + {name: 'u', from: {kind: 'Table', schema: 'public', table: 'users', columns: [{name: 'id', type: 'int'}, {name: 'name', type: 'varchar'}]}}, + ] + }) + expect(extract('SELECT u.name, created_by FROM events e JOIN users u ON e.created_by = u.id;', [users, events])).toEqual({ + columns: [ + {table: 'u', name: 'name', type: 'varchar', sources: [{schema: 'public', table: 'users', column: 'name', type: 'varchar'}]}, + {name: 'created_by', type: 'int', sources: [{schema: 'public', table: 'events', column: 'created_by', type: 'int'}]}, + ], + sources: [ + {name: 'e', from: {kind: 'Table', schema: 'public', table: 'events', columns: [{name: 'id', type: 'int'}, {name: 'name', type: 'varchar'}, {name: 'created_by', type: 'int'}]}}, + {name: 'u', from: {kind: 'Table', schema: 'public', table: 'users', columns: [{name: 'id', type: 'int'}, {name: 'name', type: 'varchar'}]}}, + ] + }) + }) + test('subquery', () => { + expect(extract("SELECT a.* FROM (SELECT id FROM users WHERE role = 'admin') a;", [])).toEqual({ + columns: [{table: 'a', name: 'id', sources: [{table: 'users', column: 'id'}]}], + sources: [{name: 'a', from: { + kind: 'Select', + columns: [{name: 'id', sources: [{table: 'users', column: 'id'}]}], + sources: [{name: 'users', from: {kind: 'Table', table: 'users'}}]} + }] + }) + expect(extract("SELECT a.* FROM (SELECT id FROM users WHERE role = 'admin') a;", [users])).toEqual({ + columns: [{table: 'a', name: 'id', type: 'int', sources: [{schema: 'public', table: 'users', column: 'id', type: 'int'}]}], + sources: [{name: 'a', from: { + kind: 'Select', + columns: [{name: 'id', type: 'int', sources: [{schema: 'public', table: 'users', column: 'id', type: 'int'}]}], + sources: [{name: 'users', from: {kind: 'Table', schema: 'public', table: 'users', columns: [{name: 'id', type: 'int'}, {name: 'name', type: 'varchar'}]}}]} + }] + }) + }) + }) +}) + +function parse(sql: string): {db: Database, errors: ParserError[]} { + try { + const start = Date.now() + const res = parsePostgresAst(sql) + .flatMap(ast => buildPostgresDatabase(ast, start, Date.now())) + .map(({extra: {source, createdAt, creationTimeMs, parsingTimeMs, formattingTimeMs, ...extra} = {}, ...db}) => ({...db, extra})) + return {db: removeFieldsDeep(res.result || {}, ['extra']), errors: res.errors || []} + } catch (e) { + console.error(e) // print stack trace + throw new Error(`Can't parse '${sql}'${typeof e === 'object' && e !== null && 'message' in e ? ': ' + e.message : ''}`) + } +} + +function extract(sql: string, entities: Entity[]): SelectEntities { + return removeFieldsDeep(selectEntities(parsePostgresAst(sql).result?.statements?.[0] as SelectInnerAst, entities), ['token']) +} diff --git a/libs/parser-sql/src/postgresBuilder.ts b/libs/parser-sql/src/postgresBuilder.ts new file mode 100644 index 000000000..22331de52 --- /dev/null +++ b/libs/parser-sql/src/postgresBuilder.ts @@ -0,0 +1,630 @@ +import {distinctBy, isNever, isNotUndefined, removeEmpty, removeUndefined} from "@azimutt/utils"; +import { + Attribute, + AttributePath, + attributePathSame, + AttributeValue, + Check, + Database, + Entity, + entityRefSame, + entityToId, + entityToRef, + Index, + mergeEntity, + mergePositions, + mergeType, + ParserError, + ParserResult, + PrimaryKey, + Relation, + TokenPosition, + Type, + typeRefSame, + typeToId, + typeToRef +} from "@azimutt/models"; +import packageJson from "../package.json"; +import { + AliasAst, + AlterTableStatementAst, + ColumnAst, + CommentObject, + CommentOnStatementAst, + CreateIndexStatementAst, + CreateMaterializedViewStatementAst, + CreateTableStatementAst, + CreateTypeStatementAst, + CreateViewStatementAst, + ExpressionAst, + FromClauseAst, + FromClauseItemAst, + FunctionAst, + IdentifierAst, + Operator, + OperatorLeft, + OperatorRight, + PostgresAst, + SelectClauseColumnAst, + SelectInnerAst, + StatementAst, + TableColumnAst, + TokenInfo +} from "./postgresAst"; +import {duplicated} from "./errors"; + +export function buildPostgresDatabase(ast: PostgresAst, start: number, parsed: number): ParserResult { + const db: Database = {entities: [], relations: [], types: []} + const errors: ParserError[] = [] + ast.statements.forEach((stmt, i) => evolvePostgres(db, errors, i + 1, stmt)) + const done = Date.now() + const extra = removeEmpty({ + source: `PostgreSQL parser <${packageJson.version}>`, + createdAt: new Date().toISOString(), + creationTimeMs: done - start, + parsingTimeMs: parsed - start, + formattingTimeMs: done - parsed, + comments: ast.comments?.map(c => ({line: c.token.position.start.line, comment: c.value})) || [], + }) + return new ParserResult(removeEmpty({...db, extra}), errors) +} + +export function evolvePostgres(db: Database, errors: ParserError[], index: number, stmt: StatementAst): void { + if (stmt.kind === 'AlterTable') { + alterTable(index, stmt, db) + } else if (stmt.kind === 'CommentOn') { + commentOn(index, stmt, db) + } else if (stmt.kind === 'CreateIndex') { + if (!db.entities) db.entities = [] + createIndex(index, stmt, db.entities) + } else if (stmt.kind === 'CreateMaterializedView') { + if (!db.entities) db.entities = [] + const entity = createMaterializedView(index, stmt, db.entities) + addEntity(db.entities, errors, entity, mergePositions([stmt.schema, stmt.name].map(v => v?.token))) + } else if (stmt.kind === 'CreateTable') { + if (!db.entities) db.entities = [] + const res = createTable(index, stmt) + addEntity(db.entities, errors, res.entity, mergePositions([stmt.schema, stmt.name].map(v => v?.token))) + res.relations.forEach(r => db.relations?.push(r)) + } else if (stmt.kind === 'CreateType') { + if (!db.types) db.types = [] + const type = createType(index, stmt) + addType(db.types, errors, type, mergePositions([stmt.schema, stmt.name].map(v => v?.token))) + } else if (stmt.kind === 'CreateView') { + if (!db.entities) db.entities = [] + const entity = createView(index, stmt, db.entities) + addEntity(db.entities, errors, entity, mergePositions([stmt.schema, stmt.name].map(v => v?.token))) + } else if (stmt.kind === 'AlterSchema') { // nothing + } else if (stmt.kind === 'AlterSequence') { // nothing + } else if (stmt.kind === 'Begin') { // nothing + } else if (stmt.kind === 'Commit') { // nothing + } else if (stmt.kind === 'CreateExtension') { // nothing + } else if (stmt.kind === 'CreateFunction') { // nothing + } else if (stmt.kind === 'CreateTrigger') { // nothing + } else if (stmt.kind === 'CreateSchema') { // nothing + } else if (stmt.kind === 'CreateSequence') { // nothing + } else if (stmt.kind === 'Delete') { // nothing + } else if (stmt.kind === 'Drop') { // nothing + } else if (stmt.kind === 'InsertInto') { // nothing + } else if (stmt.kind === 'Select') { // nothing + } else if (stmt.kind === 'Set') { // nothing + } else if (stmt.kind === 'Show') { // nothing + } else if (stmt.kind === 'Update') { // nothing + } else { + isNever(stmt) + } +} + +function addEntity(entities: Entity[], errors: ParserError[], entity: Entity, pos: TokenPosition): void { + const ref = entityToRef(entity) + const prevIndex = entities.findIndex(e => entityRefSame(entityToRef(e), ref)) + if (prevIndex !== -1) { + const prev = entities[prevIndex] + errors.push(duplicated(`Entity ${entityToId(entity)}`, prev.extra?.line ? prev.extra.line : undefined, pos)) + entities[prevIndex] = mergeEntity(prev, entity) + } else { + entities.push(entity) + } +} + +function addType(types: Type[], errors: ParserError[], type: Type, pos: TokenPosition): void { + const ref = typeToRef(type) + const prevIndex = types.findIndex(t => typeRefSame(typeToRef(t), ref)) + if (prevIndex !== -1) { + const prev = types[prevIndex] + errors.push(duplicated(`Type ${typeToId(type)}`, prev.extra?.line ? prev.extra.line : undefined, pos)) + types[prevIndex] = mergeType(prev, type) + } else { + types.push(type) + } +} + +function createTable(index: number, stmt: CreateTableStatementAst): { entity: Entity, relations: Relation[] } { + const colPk: PrimaryKey[] = stmt.columns?.flatMap(col => col.constraints?.flatMap(c => c.kind === 'PrimaryKey' ? [removeUndefined({attrs: [[col.name.value]], name: c.constraint?.name.value, extra: {line: c.token.position.start.line, statement: index}})] : []) || []) || [] + const tablePk: PrimaryKey[] = stmt.constraints?.flatMap(c => c.kind === 'PrimaryKey' ? [removeUndefined({attrs: c.columns.map(col => [col.value]), name: c.constraint?.name.value, extra: {line: c.token.position.start.line, statement: index}})] : []) || [] + const pk: PrimaryKey[] = colPk.concat(tablePk) + const colIndexes: Index[] = stmt.columns?.flatMap(col => col.constraints?.flatMap(c => c.kind === 'Unique' ? [removeUndefined({attrs: [[col.name.value]], unique: true, name: c.constraint?.name.value, extra: {line: c.token.position.start.line, statement: index}})] : []) || []) || [] + const tableIndexes: Index[] = stmt.constraints?.flatMap(c => c.kind === 'Unique' ? [removeUndefined({attrs: c.columns.map(col => [col.value]), unique: true, name: c.constraint?.name.value, extra: {line: c.token.position.start.line, statement: index}})] : []) || [] + const indexes: Index[] = colIndexes.concat(tableIndexes) + const colChecks: Check[] = stmt.columns?.flatMap(col => col.constraints?.flatMap(c => c.kind === 'Check' ? [removeUndefined({attrs: [[col.name.value]], predicate: expressionToString(c.predicate), name: c.constraint?.name.value, extra: {line: c.token.position.start.line, statement: index}})] : []) || []) || [] + const tableChecks: Check[] = stmt.constraints?.flatMap(c => c.kind === 'Check' ? [removeUndefined({attrs: expressionAttrs(c.predicate), predicate: expressionToString(c.predicate), name: c.constraint?.name.value, extra: {line: c.token.position.start.line, statement: index}})] : []) || [] + const checks: Check[] = colChecks.concat(tableChecks) + const colRels: Relation[] = stmt.columns?.flatMap(col => col.constraints?.flatMap(c => c.kind === 'ForeignKey' ? [removeUndefined({ + name: c.constraint?.name.value, + src: removeUndefined({schema: stmt.schema?.value, entity: stmt.name.value, attrs: [[col.name.value]]}), + ref: removeUndefined({schema: c.schema?.value, entity: c.table.value, attrs: [c.column ? [c.column.value] : []]}), + extra: {line: c.token.position.start.line, statement: index}, + })] : []) || []) || [] + const tableRels: Relation[] = stmt.constraints?.flatMap(c => c.kind === 'ForeignKey' ? [removeUndefined({ + name: c.constraint?.name.value, + src: removeUndefined({schema: stmt.schema?.value, entity: stmt.name.value, attrs: c.columns.map(col => [col.value])}), + ref: removeUndefined({schema: c.ref.schema?.value, entity: c.ref.table.value, attrs: c.ref.columns?.map(col => [col.value]) || []}), + extra: {line: c.token.position.start.line, statement: index}, + })] : []) || [] + const relations: Relation[] = colRels.concat(tableRels) + return {entity: removeEmpty({ + schema: stmt.schema?.value, + name: stmt.name.value, + kind: undefined, + def: undefined, + attrs: (stmt.columns || []).map(c => buildTableAttr(index, c, !!pk.find(pk => pk.attrs.some(a => attributePathSame(a, [c.name.value]))))), + pk: pk.length > 0 ? pk[0] : undefined, + indexes, + checks, + // doc: z.string().optional(), // not defined in CREATE TABLE + // stats: EntityStats.optional(), // no stats in SQL + extra: {line: stmt.token.position.start.line, statement: index}, + }), relations} +} + +function buildTableAttr(index: number, c: TableColumnAst, notNull?: boolean): Attribute { + return removeUndefined({ + name: c.name.value, + type: c.type.name.value, + null: (c.constraints?.find(c => c.kind === 'Nullable' ? !c.value : false) || notNull) ? undefined : true, + // gen: z.boolean().optional(), // not handled for now + default: (c.constraints || []).flatMap(c => c.kind === 'Default' ? [expressionToValue(c.expression)] : [])[0], + // attrs: z.lazy(() => Attribute.array().optional()), // no nested attrs from SQL + // doc: z.string().optional(), // not defined in CREATE TABLE + // stats: AttributeStats.optional(), // no stats in SQL + extra: {line: c.name.token.position.start.line, statement: index}, + }) +} + +function createView(index: number, stmt: CreateViewStatementAst, entities: Entity[]): Entity { + return removeEmpty({ + schema: stmt.schema?.value, + name: stmt.name.value, + kind: 'view' as const, + def: selectInnerToString(stmt.query), + attrs: buildViewAttrs(index, stmt.query, stmt.columns, entities), + // pk: PrimaryKey.optional(), // not in VIEW + // indexes: Index.array().optional(), // not in VIEW + // checks: Check.array().optional(), // not in VIEW + // doc: z.string().optional(), // not in VIEW + // stats: EntityStats.optional(), // no stats in SQL + extra: {line: stmt.token.position.start.line, statement: index}, + }) +} + +function createMaterializedView(index: number, stmt: CreateMaterializedViewStatementAst, entities: Entity[]): Entity { + return removeEmpty({ + schema: stmt.schema?.value, + name: stmt.name.value, + kind: 'materialized view' as const, + def: selectInnerToString(stmt.query), + attrs: buildViewAttrs(index, stmt.query, stmt.columns, entities), + // pk: PrimaryKey.optional(), // not in VIEW + // indexes: Index.array().optional(), // not in VIEW + // checks: Check.array().optional(), // not in VIEW + // doc: z.string().optional(), // not in VIEW + // stats: EntityStats.optional(), // no stats in SQL + extra: {line: stmt.token.position.start.line, statement: index}, + }) +} + +function buildViewAttrs(index: number, query: SelectInnerAst, columns: IdentifierAst[] | undefined, entities: Entity[]): Attribute[] { + const attrs = selectEntities(query, entities).columns.map(c => removeUndefined({ + name: c.name, + type: c.type || 'unknown', + // null: z.boolean().optional(), // not in VIEW + // gen: z.boolean().optional(), // not handled for now + // default: AttributeValue.optional(), // now in VIEW + // attrs: z.lazy(() => Attribute.array().optional()), // no nested attrs from SQL + // doc: z.string().optional(), // not defined in CREATE TABLE + // stats: AttributeStats.optional(), // no stats in SQL + extra: {line: c.token.position.start.line, statement: index}, + })) + return columns ? columns.map(c => attrs.find(a => a.name === c.value) || {name: c.value, type: 'unknown'}) : attrs +} + +function createIndex(index: number, stmt: CreateIndexStatementAst, entities: Entity[]): void { + const entity = entities.find(e => e.schema === stmt.schema?.value && e.name === stmt.table?.value) + if (entity) { + if (!entity.indexes) entity.indexes = [] + entity.indexes.push(removeUndefined({ + name: stmt.name?.value, + attrs: stmt.columns.map(c => { + if (c.kind === 'Column') { + return [c.column.value] // TODO: improve + } + }).filter(isNotUndefined), + unique: stmt.unique ? true : undefined, + partial: stmt.where ? expressionToString(stmt.where.predicate) : undefined, + // TODO: definition: z.string().optional(), + // doc: z.string().optional(), + // stats: IndexStats.optional(), + extra: {line: stmt.token.position.start.line, statement: index}, + })) + } +} + +function alterTable(index: number, stmt: AlterTableStatementAst, db: Database): void { + const entity = db.entities?.find(e => e.schema === stmt.schema?.value && e.name === stmt.table?.value) + if (entity) { + stmt.actions.forEach(action => { + if (action.kind === 'AddColumn') { + if (!entity.attrs) entity.attrs = [] + const exists = entity.attrs.find(a => a.name === action.column.name.value) + if (!exists) entity.attrs.push(buildTableAttr(index, action.column)) + } else if (action.kind === 'DropColumn') { + const attrIndex = entity.attrs?.findIndex(a => a.name === action.column.value) + if (attrIndex !== undefined && attrIndex !== -1) entity.attrs?.splice(attrIndex, 1) + // TODO: remove constraints depending on this column + } else if (action.kind === 'AlterColumn') { + const attr = entity.attrs?.find(a => a.name === action.column.value) + if (attr) { + const aa = action.action + if (aa.kind === 'Default') { + attr.default = aa.action.kind === 'Set' && aa.expression ? expressionToValue(aa.expression) : undefined + } else if (aa.kind === 'NotNull') { + attr.null = aa.action.kind === 'Set' ? undefined : true + } else { + isNever(aa) + } + } + } else if (action.kind === 'AddConstraint') { + const constraint = action.constraint + if (constraint.kind === 'PrimaryKey') { + entity.pk = removeUndefined({name: constraint.constraint?.name.value, attrs: constraint.columns.map(c => [c.value]), extra: {line: stmt.token.position.start.line, statement: index}}) + } else if (constraint.kind === 'Unique') { + if (!entity.indexes) entity.indexes = [] + entity.indexes.push(removeUndefined({ + name: constraint.constraint?.name.value, + attrs: constraint.columns.map(c => [c.value]), + unique: true, + extra: {line: stmt.token.position.start.line, statement: index}, + })) + } else if (constraint.kind === 'Check') { + if (!entity.checks) entity.checks = [] + entity.checks.push(removeUndefined({ + name: constraint.constraint?.name.value, + attrs: expressionAttrs(constraint.predicate), + predicate: expressionToString(constraint.predicate), + extra: {line: stmt.token.position.start.line, statement: index}, + })) + } else if (constraint.kind === 'ForeignKey') { + if (!db.relations) db.relations = [] + db.relations.push(removeUndefined({ + name: constraint.constraint?.name.value, + src: removeUndefined({schema: stmt.schema?.value, entity: stmt.table?.value, attrs: constraint.columns.map(c => [c.value])}), + ref: removeUndefined({schema: constraint.ref.schema?.value, entity: constraint.ref.table.value, attrs: constraint.ref.columns?.map(c => [c.value]) || []}), + extra: {line: stmt.token.position.start.line, statement: index}, + })) + } else { + isNever(constraint) + } + } else if (action.kind === 'DropConstraint') { + if (entity.pk?.name === action.constraint.value) entity.pk = undefined + const idxIndex = entity.indexes?.findIndex(a => a.name === action.constraint.value) + if (idxIndex !== undefined && idxIndex !== -1) entity.indexes?.splice(idxIndex, 1) + const chkIndex = entity.checks?.findIndex(c => c.name === action.constraint.value) + if (chkIndex !== undefined && chkIndex !== -1) entity.checks?.splice(chkIndex, 1) + const relIndex = db.relations?.findIndex(r => r.name === action.constraint.value && r.src.schema === stmt.schema?.value && r.src.entity === stmt.table?.value) + if (relIndex !== undefined && relIndex !== -1) db.relations?.splice(relIndex, 1) + // TODO: also NOT NULL & DEFAULT constraints... + } else if (action.kind === 'SetOwner') { // nothing + } else { + isNever(action) + } + }) + } +} + +function createType(index: number, stmt: CreateTypeStatementAst): Type { + return removeUndefined({ + schema: stmt.schema?.value, + name: stmt.name.value, + // alias: z.string().optional(), // does not exist in PostgreSQL + values: stmt.enum?.values.map(v => v.value), + attrs: stmt.struct?.attrs.map(a => ({ + name: a.name.value, + type: a.type.name.value, + })), + definition: stmt.base ? '(' + stmt.base.map(p => `${p.name.value} = ${expressionToString(p.value)}`).join(', ') + ')' : undefined, + // doc: z.string().optional(), // not defined in CREATE TYPE + extra: {line: stmt.token.position.start.line, statement: index}, + }) +} + +function commentOn(index: number, stmt: CommentOnStatementAst, db: Database): void { + // TODO: store comment statement infos? where? + const object: CommentObject = stmt.object.kind + if (object === 'Column') { + const entity = db.entities?.find(e => e.schema === stmt.schema?.value && e.name === stmt.parent?.value) + const attr = entity?.attrs?.find(a => a.name === stmt.entity.value) + if (attr) attr.doc = stmt.comment.kind === 'String' ? stmt.comment.value : undefined + } else if (object === 'Constraint') { + const entity = db.entities?.find(e => e.name === stmt.parent?.value && e.schema === stmt.schema?.value) + if (entity) { + if (entity.pk?.name === stmt.entity.value) entity.pk.doc = stmt.comment.kind === 'String' ? stmt.comment.value : undefined + const index = entity.indexes?.find(i => i.name === stmt.entity.value) + if (index) index.doc = stmt.comment.kind === 'String' ? stmt.comment.value : undefined + const check = entity.checks?.find(c => c.name === stmt.entity.value) + if (check) check.doc = stmt.comment.kind === 'String' ? stmt.comment.value : undefined + } + const rel = db.relations?.find(r => r.name === stmt.entity.value && r.src.entity === stmt.parent?.value && r.src.schema === stmt.schema?.value) + if (rel) rel.doc = stmt.comment.kind === 'String' ? stmt.comment.value : undefined + } else if (object === 'Database') { + if (!db.extra) db.extra = {} + db.extra.doc = stmt.comment.kind === 'String' ? stmt.comment.value : undefined + } else if (object === 'Extension') { + // not stored + } else if (object === 'Index') { + const index = db.entities?.flatMap(e => e.schema === stmt.schema?.value && e.name === stmt.parent?.value ? e.indexes?.filter(i => i.name === stmt.entity.value) : [])?.[0] + if (index) index.doc = stmt.comment.kind === 'String' ? stmt.comment.value : undefined + } else if (object === 'Schema') { + // not stored + } else if (object === 'Table' || object === 'View' || object === 'MaterializedView') { + const entity = db.entities?.find(e => e.schema === stmt.schema?.value && e.name === stmt.entity.value) + if (entity) entity.doc = stmt.comment.kind === 'String' ? stmt.comment.value : undefined + } else if (object === 'Type') { + const type = db.types?.find(e => e.schema === stmt.schema?.value && e.name === stmt.entity.value) + if (type) type.doc = stmt.comment.kind === 'String' ? stmt.comment.value : undefined + } else { + isNever(object) + } +} + +function selectInnerToString(s: SelectInnerAst): string { + const select = 'SELECT ' + s.columns.map(c => expressionToString(c) + (c.alias ? ' ' + aliasToString(c.alias) : '')).join(', ') + const from = s.from ? fromToString(s.from) : '' + const where = s.where ? ' WHERE ' + expressionToString(s.where.predicate) : '' + return select + from + where +} + +function fromToString(f: FromClauseAst): string { + if (f.kind === 'Table') return ' FROM ' + (f.schema ? f.schema.value + '.' : '') + f.table.value + (f.alias ? ' ' + aliasToString(f.alias) : '') + if (f.kind === 'Select') return ' FROM (' + selectInnerToString(f.select) + ')' + (f.alias ? ' ' + aliasToString(f.alias) : '') + return isNever(f) +} + +function aliasToString(a: AliasAst): string { + return (a.token ? 'AS ' : '') + a.name.value +} + +function expressionToString(e: ExpressionAst): string { + if (e.kind === 'String') return "'" + e.value + "'" + if (e.kind === 'Integer') return e.value.toString() + if (e.kind === 'Decimal') return e.value.toString() + if (e.kind === 'Boolean') return e.value.toString() + if (e.kind === 'Null') return 'null' + if (e.kind === 'Parameter') return e.value + if (e.kind === 'Column') return columnToString(e) + if (e.kind === 'Wildcard') return (e.schema ? e.schema.value + '.' : '') + (e.table ? e.table.value + '.' : '') + '*' + if (e.kind === 'Function') return functionToString(e) + if (e.kind === 'Group') return '(' + expressionToString(e.expression) + ')' + if (e.kind === 'Operation') return expressionToString(e.left) + ' ' + operatorToString(e.op.kind) + ' ' + expressionToString(e.right) + if (e.kind === 'OperationLeft') return operatorLeftToString(e.op.kind) + ' ' + expressionToString(e.right) + if (e.kind === 'OperationRight') return expressionToString(e.left) + ' ' + operatorRightToString(e.op.kind) + if (e.kind === 'Array') return '[' + e.items.map(expressionToString).join(', ') + ']' + if (e.kind === 'List') return '(' + e.items.map(expressionToString).join(', ') + ')' + return isNever(e) +} + +function expressionToValue(e: ExpressionAst): AttributeValue { + if (e.kind === 'String') return e.value + if (e.kind === 'Integer') return e.value + if (e.kind === 'Decimal') return e.value + if (e.kind === 'Boolean') return e.value + if (e.kind === 'Null') return null + if (e.kind === 'Parameter') return e.value + if (e.kind === 'Column') return columnToString(e) + if (e.kind === 'Wildcard') return (e.schema ? e.schema.value + '.' : '') + (e.table ? e.table.value + '.' : '') + '*' + if (e.kind === 'Function') return '`' + functionToString(e) + '`' + if (e.kind === 'Group') return expressionToValue(e.expression) + if (e.kind === 'Operation') return '`' + expressionToString(e) + '`' + if (e.kind === 'OperationLeft') return '`' + expressionToString(e) + '`' + if (e.kind === 'OperationRight') return '`' + expressionToString(e) + '`' + if (e.kind === 'Array') return e.items.map(expressionToValue) + if (e.kind === 'List') return e.items.map(expressionToValue) + return isNever(e) +} + +function columnToString(c: ColumnAst): string { + const schema = c.schema ? c.schema.value + '.' : '' + const table = c.table ? c.table.value + '.' : '' + const json = c.json ? c.json.map(j => j.kind + j.field.value).join('') : '' + return schema + table + c.column.value + json +} + +function functionToString(f: FunctionAst): string { + const schema = f.schema ? f.schema.value + '.' : '' + const distinct = f.distinct ? 'distinct ' : '' + const params = f.parameters.map(expressionToString).join(', ') + return schema + f.function.value + '(' + distinct + params + ')' +} + +function operatorToString(o: Operator): string { + if (o === 'Or') return 'OR' + if (o === 'And') return 'AND' + if (o === 'Is') return 'IS' + if (o === 'In') return 'IN' + if (o === 'NotIn') return 'NOT IN' + if (o === 'Like') return 'LIKE' + if (o === 'NotLike') return 'NOT LIKE' + return o +} + +function operatorLeftToString(o: OperatorLeft): string { + if (o === 'Not') return 'NOT' + if (o === 'Interval') return 'INTERVAL' + return o +} + +function operatorRightToString(o: OperatorRight): string { + if (o === 'IsNull') return 'IS NULL' + if (o === 'NotNull') return 'IS NOT NULL' + return o +} + +function expressionAttrs(e: ExpressionAst): AttributePath[] { + if (e.kind === 'String') return [] + if (e.kind === 'Integer') return [] + if (e.kind === 'Decimal') return [] + if (e.kind === 'Boolean') return [] + if (e.kind === 'Null') return [] + if (e.kind === 'Parameter') return [] + if (e.kind === 'Column') return [[e.column.value]] + if (e.kind === 'Wildcard') return [] + if (e.kind === 'Function') return distinctBy(e.parameters.flatMap(expressionAttrs), p => p.join('.')) + if (e.kind === 'Group') return expressionAttrs(e.expression) + if (e.kind === 'Operation') return distinctBy(expressionAttrs(e.left).concat(expressionAttrs(e.right)), p => p.join('.')) + if (e.kind === 'OperationLeft') return expressionAttrs(e.right) + if (e.kind === 'OperationRight') return expressionAttrs(e.left) + if (e.kind === 'Array') return [] + if (e.kind === 'List') return [] + return isNever(e) +} + +export type SelectEntities = { columns: SelectColumn[], sources: SelectSource[] } +export type SelectColumn = { schema?: string, table?: string, name: string, type?: string, sources: SelectColumnSource[], token: TokenInfo } +export type SelectColumnSource = { schema?: string, table: string, column: string, type?: string } +export type SelectSource = { name: string, from: SelectSourceFrom } +export type SelectSourceFrom = SelectSourceTable | SelectSourceSelect +export type SelectSourceTable = { kind: 'Table', schema?: string, table: string, columns?: { name: string, type: string }[] } +export type SelectSourceSelect = { kind: 'Select' } & SelectEntities + +// TODO: also extract entities in clauses such as WHERE, HAVING... (know all use involved tables & columns, wherever they are used) +export function selectEntities(s: SelectInnerAst, entities: Entity[]): SelectEntities { + const sources = s.from ? selectTables(s.from, entities) : [] + const columns: SelectColumn[] = s.columns.flatMap((c, i) => selectColumn(c, i, sources)) + return {columns, sources} +} +function selectTables(f: FromClauseAst, entities: Entity[]): SelectSource[] { + const joins = f.joins?.map(j => fromTables(j.from, entities)) || [] + return [fromTables(f, entities), ...joins] +} +function fromTables(i: FromClauseItemAst, entities: Entity[]): SelectSource { + if (i.kind === 'Table') { + const entity = findEntity(entities, i.table.value, i.schema?.value) + if (entity) { + return {name: i.alias?.name.value || i.table.value, from: removeEmpty({kind: 'Table' as const, schema: entity.schema, table: entity.name, columns: entity.attrs?.map(a => ({name: a.name, type: a.type})) || []})} + } else { + return {name: i.alias?.name.value || i.table.value, from: removeUndefined({kind: 'Table' as const, schema: i.schema?.value, table: i.table.value})} + } + } else if (i.kind === 'Select') { + return {name: i.alias?.name.value || '', from: {kind: 'Select', ...selectEntities(i.select, entities)}} + } else { + return isNever(i) + } +} +function selectColumn(c: SelectClauseColumnAst, i: number, sources: SelectSource[]): SelectColumn[] { + if(c.kind === 'Column') { + const ref = removeUndefined({schema: c.schema?.value, table: c.table?.value, name: c.alias ? c.alias.name.value : c.column.value, token: c.column.token}) + const source = findSource(sources, c.schema?.value, c.table?.value, c.column.value)?.from + if (source?.kind === 'Table') { + const col = source.columns?.find(col => col.name === c.column.value) + return [removeUndefined({...ref, type: col?.type, sources: [removeUndefined({schema: source.schema, table: source.table, column: c.column.value, type: col?.type})]})] + } else if (source?.kind === 'Select') { + const col = source.columns.find(col => col.name === c.column.value) + return [col ? col : {...ref, sources: []}] // `col` should always exist in correct queries + } else { + return [{...ref, sources: []}] // `source` should always exist in correct queries + } + } else if (c.kind === 'Wildcard') { + const ref = removeUndefined({schema: c.schema?.value, table: c.table?.value, name: c.alias ? c.alias.name.value : '*', token: c.token}) + const source = findSource(sources, c.schema?.value, c.table?.value, undefined)?.from + if (source?.kind === 'Table') { + const cols = source.columns || [] + if(cols.length > 0) { + return cols.map(a => ({...ref, name: a.name, type: a.type, sources: [removeUndefined({schema: source.schema, table: source.table, column: a.name, type: a.type})]})) + } else { + return [{...ref, sources: []}] // Wildcard from a table with no columns :/ (happen when missing entities) + } + } else if (source?.kind === 'Select') { + return source.columns.map(col => removeUndefined({...col, schema: c.schema?.value, table: c.table?.value})) + } else { + return [{...ref, sources: []}] // `source` should always exist in correct queries + } + } else if (c.kind === 'Function') { + return [{name: c.alias ? c.alias.name.value : c.function.value, sources: columnSources(c, sources), token: c.function.token}] + } else { + return [{name: c.alias ? c.alias.name.value : `col_${i + 1}`, sources: columnSources(c, sources), token: expressionToken(c)}] + } +} +function findEntity(entities: Entity[], entity: string, schema: string | undefined): Entity | undefined { + const candidates = entities.filter(e => e.name === entity) + if (schema !== undefined) return candidates.find(e => e.schema === schema) + if (candidates.length === 1) return candidates[0] + return candidates.find(e => e.schema === undefined) || candidates.find(e => e.schema === '') +} +function findSource(sources: SelectSource[], schema: string | undefined, table: string | undefined, column: string | undefined): SelectSource | undefined { + // not sure what to do with `schema` :/ + if (table) { + return sources.find(s => s.name === table) + } + if (column) { + const candidates = sources.filter(s => { + if (s.from.kind === 'Table') { + return !!s.from.columns?.find(a => a.name === column) + } else if (s.from.kind === 'Select') { + return !!s.from.columns.find(c => c.name === column) + } else { + return isNever(s.from) + } + }) + return candidates.length === 1 ? candidates[0] : sources.length === 1 ? sources[0] : undefined + } + return sources.length === 1 ? sources[0] : undefined +} +function columnSources(c: ExpressionAst, sources: SelectSource[]): SelectColumnSource[] { + if (c.kind === 'Column') { + const source = c.table ? sources.find(s => s.name === c.table?.value) : sources.length === 1 ? sources[0] : undefined + if (source && source.from.kind === 'Table') { + return [removeUndefined({schema: source.from.schema, table: source.from.table, column: c.column.value})] + } + } + if (c.kind === 'Wildcard') return [] + if (c.kind === 'Function') return c.parameters.flatMap(p => columnSources(p, sources)) + if (c.kind === 'Group') return columnSources(c.expression, sources) + if (c.kind === 'Operation') return columnSources(c.left, sources).concat(columnSources(c.right, sources)) + if (c.kind === 'OperationLeft') return columnSources(c.right, sources) + if (c.kind === 'OperationRight') return columnSources(c.left, sources) + return [] +} +function expressionToken(e: ExpressionAst): TokenPosition { + if (e.kind === 'Parameter' || e.kind === 'String' || e.kind === 'Decimal' || e.kind === 'Integer' || e.kind === 'Boolean' || e.kind === 'Null') { + return e.token + } else if (e.kind === 'Column') { + return e.column.token + } else if (e.kind === 'Wildcard') { + return e.token + } else if (e.kind === 'Function') { + return e.function.token + } else if (e.kind === 'Group') { + return expressionToken(e.expression) + } else if (e.kind === 'Operation') { + return mergePositions([expressionToken(e.left), expressionToken(e.right)]) + } else if (e.kind === 'OperationLeft') { + return mergePositions([e.op.token, expressionToken(e.right)]) + } else if (e.kind === 'OperationRight') { + return mergePositions([expressionToken(e.left), e.op.token]) + } else if (e.kind === 'Array') { + return mergePositions([e.token, ...e.items.map(expressionToken)]) + } else if (e.kind === 'List') { + return mergePositions(e.items.map(expressionToken)) + } else { + return isNever(e) + } +} diff --git a/libs/parser-sql/src/postgresParser.test.ts b/libs/parser-sql/src/postgresParser.test.ts new file mode 100644 index 000000000..5bb18b25d --- /dev/null +++ b/libs/parser-sql/src/postgresParser.test.ts @@ -0,0 +1,1418 @@ +import * as fs from "fs"; +import {describe, expect, test} from "@jest/globals"; +import {removeEmpty, removeFieldsDeep, removeUndefined} from "@azimutt/utils"; +import type { + AliasAst, + BooleanAst, + ColumnAst, + DecimalAst, + ExpressionAst, + FunctionAst, + GroupAst, + IdentifierAst, + IntegerAst, + ListAst, + LiteralAst, + NullAst, + OperationAst, + OperationLeftAst, + OperationRightAst, + Operator, + OperatorAst, + OperatorLeft, + OperatorLeftAst, + OperatorRight, + OperatorRightAst, + ParameterAst, + StringAst, + TokenInfo, + TokenIssue, + WildcardAst +} from "./postgresAst"; +import {parsePostgresAst, parseRule} from "./postgresParser"; + +describe('postgresParser', () => { + test('empty', () => { + expect(parsePostgresAst('')).toEqual({result: {statements: []}}) + }) + test('complex', () => { + const sql = fs.readFileSync('./resources/complex.postgres.sql', 'utf8') + const parsed = parsePostgresAst(sql, {strict: true}) + expect(parsed.errors || []).toEqual([]) + }) + test('structure', () => { + const sql = fs.readFileSync('../../backend/priv/repo/structure.sql', 'utf8') + const parsed = parsePostgresAst(sql, {strict: true}) + expect(parsed.errors || []).toEqual([]) + }) + test('full', () => { + const sql = fs.readFileSync('./resources/full.postgres.sql', 'utf8') + const parsed = parsePostgresAst(sql, {strict: true}) + expect(parsed.errors || []).toEqual([]) + }) + test.skip('statements', () => { + const sql = fs.readFileSync('./resources/pg_statements.sql', 'utf8') + const parsed = parsePostgresAst(sql, {strict: true}) + expect(parsed.errors || []).toEqual([]) + // medium: + // TODO:1362 `(u0."created_at" > $3::timestamp + (-($4)::numeric * interval $5))` + // TODO:2155 `ARRAY(SELECT a.atttypid)` + // TODO:1639 `SELECT CASE con.confupdtype WHEN $3 THEN $4 ELSE $15 END AS UPDATE_RULE` + // TODO:2010 `SELECT CASE n.nspname ~ $2 OR n.nspname = $3 WHEN $4 THEN CASE ELSE $51 END AS TABLE_TYPE` + // TODO:1862 `SELECT ("public"."Event"."details" #>> array [$1]::text[])::text AS "details → source"` (also: 1962) + // TODO:1415 `SET SESSION CHARACTERISTICS AS TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;` (also: 1934) + // TODO:321 `SHOW TRANSACTION ISOLATION LEVEL;` (also: 1694) + // hard: + // TODO:12 `(information_schema._pg_expandarray(i.indkey)).n` + // TODO:847 `select (current_schemas($3))[s.r] as nspname` (also: 1057) + // TODO:326 `SELECT ... FROM (VALUES ($2, ($3)::text), ($4, ($5)::text)) as v(key, val)` (also: 1168) + }) + test.skip('other structures', async () => { + // cf https://github.com/search?q=path%3A**%2Fstructure.sql&type=code + const structures = [ + 'https://raw.githubusercontent.com/ChanAlex2357/gestion_analytique_S5/refs/heads/main/Structure.sql', + 'https://raw.githubusercontent.com/gocardless/draupnir/refs/heads/master/structure.sql', + 'https://raw.githubusercontent.com/LuisMDeveloper/travis-core/refs/heads/master/db/structure.sql', + 'https://raw.githubusercontent.com/gustavodiel/monet-backend/refs/heads/main/db/structure.sql', + 'https://raw.githubusercontent.com/plausible/analytics/refs/heads/master/priv/repo/structure.sql', + 'https://raw.githubusercontent.com/henry2992/aprendemy/refs/heads/master/db/structure.sql', + 'https://raw.githubusercontent.com/Leonardo-Zappani/bd2/refs/heads/main/db/structure.sql', + 'https://raw.githubusercontent.com/wikimedia/mediawiki/refs/heads/master/maintenance/postgres/tables-generated.sql', + 'https://raw.githubusercontent.com/Unleash/unleash/refs/heads/main/website/docs/migrations/unleash_dump.sql', + // 'https://raw.githubusercontent.com/gitlabhq/gitlabhq/refs/heads/master/db/init_structure.sql', // fail#209: `SECURITY DEFINER` in `CREATE FUNCTION` + // 'https://raw.githubusercontent.com/gitlabhq/gitlabhq/refs/heads/master/db/structure.sql', // fail#195: `STABLE COST 1 PARALLEL SAFE` in `CREATE FUNCTION` + // 'https://raw.githubusercontent.com/Rotabot-io/rotabot/refs/heads/main/assets/structure.sql', // fail#57: `ALTER FUNCTION public.generate_uid(size integer) OWNER TO rotabot;` TODO + // 'https://raw.githubusercontent.com/bocoup/devstats/refs/heads/master/structure.sql', // fail#57: `ALTER FUNCTION current_state.label_prefix(some_label text) OWNER TO devstats_team;` TODO + // 'https://raw.githubusercontent.com/yazilimcilarinmolayeri/pixels-clone/refs/heads/master/Structure.sql', // fail#68: column `GENERATED ALWAYS AS IDENTITY` TODO + // 'https://raw.githubusercontent.com/metabase/metabase/refs/heads/master/resources/migrations/initialization/metabase_postgres.sql', // fail#29: `ALTER TABLE public.activity ALTER COLUMN id ADD GENERATED BY DEFAULT AS IDENTITY` + // 'https://raw.githubusercontent.com/knadh/listmonk/refs/heads/master/schema.sql', // fail#58: `INTEGER REFERENCES subscribers(id) ON DELETE CASCADE ON UPDATE CASCADE` + // 'https://raw.githubusercontent.com/drenther/Empirical-Core/refs/heads/develop/db/structure.sql', // fail#2894: `CREATE INDEX email_idx ON users USING gin (email gin_trgm_ops);` + // 'https://raw.githubusercontent.com/TechnoDann/PPC-board-2.0/refs/heads/master/db/structure.sql', // fail#350: `CREATE INDEX index_posts_on_ancestry ON public.posts USING btree (ancestry text_pattern_ops NULLS FIRST);` + // 'https://raw.githubusercontent.com/mattermost/mattermost/refs/heads/master/server/scripts/mattermost-postgresql-6.0.0.sql', // fail#2300: `CREATE INDEX idx_users_email_lower_textpattern ON public.users USING btree (lower((email)::text) text_pattern_ops);` + // 'https://raw.githubusercontent.com/dokuwiki/dokuwiki/refs/heads/master/lib/plugins/authpdo/_test/pgsql/django.sql', // fail#1080: `CREATE INDEX auth_group_name_a6ea08ec_like ON auth_group USING btree (name varchar_pattern_ops);` + // 'https://raw.githubusercontent.com/goksan/statusnook/refs/heads/main/schema.sql', // fail#65: `destination text not null collate nocase` + // 'https://raw.githubusercontent.com/Style12341/UTN-gestor-aulas-backend/refs/heads/main/db/structure.sql', // fail#25: `CREATE TYPE public.timerange AS RANGE` + // 'https://raw.githubusercontent.com/inaturalist/inaturalist/refs/heads/main/db/structure.sql', // fail#44: `CREATE FUNCTION` with unnamed parameter + // 'https://raw.githubusercontent.com/cardstack/cardstack/refs/heads/main/packages/hub/config/structure.sql', // fail#52: `ALTER TYPE` & 1986: `COPY` + // 'https://raw.githubusercontent.com/CLOSER-Cohorts/archivist/refs/heads/develop/db/structure.sql', // fail#849: parenthesis in FROM clause + // 'https://raw.githubusercontent.com/PecanProject/bety/refs/heads/master/db/structure.sql', // fail#274: LexingError: unexpected character: ->\\<- + // 'https://raw.githubusercontent.com/recursecenter/community/refs/heads/master/db/structure.sql', // fail#299: JOIN (`SELECT * FROM users, events;`) + // 'https://raw.githubusercontent.com/dhbtk/achabus/refs/heads/master/db/structure.sql', // fail#293: column type: `geography(Point,4326)` + // 'https://raw.githubusercontent.com/ppawel/openstreetmap-watch-list/refs/heads/master/db/structure.sql', // fail#92: `CREATE TYPE change AS (geom geometry(Geometry,4326))` + // 'https://raw.githubusercontent.com/OPG-813/electronic-queue-server/refs/heads/master/src/db/structure.sql', // fail#110: `DEFAULT CURRENT_DATE AT TIME ZONE( SYSTEM_TIMEZONE() )` + // 'https://raw.githubusercontent.com/yourselfhosted/slash/refs/heads/main/store/migration/postgres/prod/LATEST.sql', // fail#4: `EXTRACT(EPOCH FROM NOW())` + ] + await Promise.all(structures.map(async url => { + const sql = await fetch(url).then(res => res.text()) + const parsed = parsePostgresAst(sql, {strict: true}) + if ((parsed.errors || []).length > 0) { + console.log(`Error in ${url}`) + expect(parsed.errors || []).toEqual([]) + } + })) + }) + describe('alterSchema', () => { + test('rename', () => { + expect(parsePostgresAst('ALTER SCHEMA cms RENAME TO marketing;')).toEqual({result: {statements: [{ + ...stmt('AlterSchema', 0, 11, 36), + schema: identifier('cms', 13), + action: {kind: 'Rename', token: token(17, 25), schema: identifier('marketing', 27)} + }]}}) + }) + test('owner', () => { + expect(parsePostgresAst('ALTER SCHEMA cms OWNER TO CURRENT_USER;')).toEqual({result: {statements: [{ + ...stmt('AlterSchema', 0, 11, 38), + schema: identifier('cms', 13), + action: {kind: 'Owner', token: token(17, 24), owner: kind('CurrentUser', 26, 37)} + }]}}) + }) + }) + describe('alterSequence', () => { + test('simple', () => { + expect(parsePostgresAst('ALTER SEQUENCE users_id_seq OWNED BY users.id;')).toEqual({result: {statements: [{ + ...stmt('AlterSequence', 0, 13, 45), + name: identifier('users_id_seq', 15), + ownedBy: {token: token(28, 35), owner: {kind: 'Column', table: identifier('users', 37), column: identifier('id', 43)}} + }]}}) + }) + test('complex', () => { + expect(parsePostgresAst('ALTER SEQUENCE IF EXISTS public.users_id_seq AS integer START WITH 1 INCREMENT BY 1 MINVALUE 1 NO MAXVALUE CACHE 1 OWNED BY users.id;')).toEqual({result: {statements: [{ + ...stmt('AlterSequence', 0, 13, 132), + ifExists: {token: token(15, 23)}, + schema: identifier('public', 25), + name: identifier('users_id_seq', 32), + as: {token: token(45, 46), type: identifier('integer', 48)}, + start: {token: token(56, 65), value: integer(1, 67)}, + increment: {token: token(69, 80), value: integer(1, 82)}, + minValue: {token: token(84, 91), value: integer(1, 93)}, + maxValue: {token: token(95, 105)}, + cache: {token: token(107, 111), value: integer(1, 113)}, + ownedBy: {token: token(115, 122), owner: {kind: 'Column', table: identifier('users', 124), column: identifier('id', 130)}} + }]}}) + }) + }) + describe('alterTable', () => { + test('full', () => { + expect(parsePostgresAst('ALTER TABLE IF EXISTS ONLY public.users ADD CONSTRAINT users_pk PRIMARY KEY (id);')).toEqual({result: {statements: [{ + ...stmt('AlterTable', 0, 10, 80), + ifExists: {token: token(12, 20)}, + only: {token: token(22, 25)}, + schema: identifier('public', 27), + table: identifier('users', 34), + actions: [{...kind('AddConstraint', 40, 42), constraint: {constraint: {token: token(44, 53), name: identifier('users_pk', 55)}, ...kind('PrimaryKey', 64, 74), columns: [identifier('id', 77)]}}], + }]}}) + }) + test('add column', () => { + expect(parsePostgresAst('ALTER TABLE users ADD author int;')).toEqual({result: {statements: [{ + ...stmt('AlterTable', 0, 10, 32), + table: identifier('users', 12), + actions: [{...kind('AddColumn', 18, 20), column: {name: identifier('author', 22), type: {name: identifier('int', 29), token: token(29, 31)}}}], + }]}}) + }) + test('drop column', () => { + expect(parsePostgresAst('ALTER TABLE users DROP author;')).toEqual({result: {statements: [{ + ...stmt('AlterTable', 0, 10, 29), + table: identifier('users', 12), + actions: [{...kind('DropColumn', 18, 21), column: identifier('author', 23)}], + }]}}) + }) + test('alter column default', () => { + expect(parsePostgresAst("ALTER TABLE ONLY public.posts ALTER COLUMN id SET DEFAULT nextval('public.posts_id_seq'::regclass);")).toEqual({result: {statements: [{ + ...stmt('AlterTable', 0, 10, 98), + only: {token: token(12, 15)}, + schema: identifier('public', 17), + table: identifier('posts', 24), + actions: [{ + ...kind('AlterColumn', 30, 41), + column: identifier('id', 43), + action: { + ...kind('Default', 50, 56), + action: kind('Set', 46), + expression: function_('nextval', 58, [{...string('public.posts_id_seq', 66), cast: {token: token(87, 88), type: {name: identifier('regclass', 89), token: token(89, 96)}}}]) + } + }] + }]}}) + expect(parsePostgresAst("ALTER TABLE ONLY public.posts ALTER COLUMN id DROP DEFAULT;")).toEqual({result: {statements: [{ + ...stmt('AlterTable', 0, 10, 58), + only: {token: token(12, 15)}, + schema: identifier('public', 17), + table: identifier('posts', 24), + actions: [{...kind('AlterColumn', 30, 41), column: identifier('id', 43), action: {...kind('Default', 51, 57), action: kind('Drop', 46)}}] + }]}}) + }) + test('alter column not null', () => { + expect(parsePostgresAst("ALTER TABLE ONLY public.posts ALTER COLUMN id DROP NOT NULL;")).toEqual({result: {statements: [{ + ...stmt('AlterTable', 0, 10, 59), + only: {token: token(12, 15)}, + schema: identifier('public', 17), + table: identifier('posts', 24), + actions: [{...kind('AlterColumn', 30, 41), column: identifier('id', 43), action: {...kind('NotNull', 51, 58), action: kind('Drop', 46)}}] + }]}}) + }) + test('add primaryKey', () => { + expect(parsePostgresAst('ALTER TABLE users ADD PRIMARY KEY (id) NOT VALID;')).toEqual({result: {statements: [{ + ...stmt('AlterTable', 0, 10, 48), + table: identifier('users', 12), + actions: [{...kind('AddConstraint', 18, 20), constraint: {...kind('PrimaryKey', 22, 32), columns: [identifier('id', 35)]}, notValid: {token: token(39, 47)}}], + }]}}) + }) + test('drop primaryKey', () => { + expect(parsePostgresAst('ALTER TABLE users DROP CONSTRAINT users_pk;')).toEqual({result: {statements: [{ + ...stmt('AlterTable', 0, 10, 42), + table: identifier('users', 12), + actions: [{...kind('DropConstraint', 18, 32), constraint: identifier('users_pk', 34)}], + }]}}) + }) + test('change owner', () => { + expect(parsePostgresAst('ALTER TABLE users OWNER TO admin;')).toEqual({result: {statements: [{ + ...stmt('AlterTable', 0, 10, 32), + table: identifier('users', 12), + actions: [{...kind('SetOwner', 18), owner: {kind: 'User', name: identifier('admin', 27)}}], + }]}}) + }) + }) + describe('begin', () => { + test('simple', () => { + expect(parsePostgresAst('BEGIN;')).toEqual({result: {statements: [stmt('Begin', 0, 4, 5)]}}) + }) + test('complex', () => { + expect(parsePostgresAst('BEGIN TRANSACTION ISOLATION LEVEL READ COMMITTED, READ ONLY, NOT DEFERRABLE;')).toEqual({result: {statements: [{ + ...stmt('Begin', 0, 4, 75), + object: kind('Transaction', 6), + modes: [ + {...kind('IsolationLevel', 18, 32), level: kind('ReadCommitted', 34, 47)}, + kind('ReadOnly', 50, 58), + {not: {token: token(61, 63)}, ...kind('Deferrable', 65, 74)}, + ] + }]}}) + }) + }) + describe('commentOn', () => { + test('schema', () => { + expect(parsePostgresAst("COMMENT ON SCHEMA public IS 'Main schema';")).toEqual({result: {statements: [{ + ...stmt('CommentOn', 0, 9, 41), + object: kind('Schema', 11), + entity: identifier('public', 18), + comment: string('Main schema', 28), + }]}}) + }) + test('table', () => { + expect(parsePostgresAst("COMMENT ON TABLE public.users IS 'List users';")).toEqual({result: {statements: [{ + ...stmt('CommentOn', 0, 9, 45), + object: kind('Table', 11), + schema: identifier('public', 17), + entity: identifier('users', 24), + comment: string('List users', 33), + }]}}) + }) + test('column', () => { + expect(parsePostgresAst("COMMENT ON COLUMN public.users.name IS 'user name';")).toEqual({result: {statements: [{ + ...stmt('CommentOn', 0, 9, 50), + object: kind('Column', 11), + schema: identifier('public', 18), + parent: identifier('users', 25), + entity: identifier('name', 31), + comment: string('user name', 39), + }]}}) + }) + test('constraint', () => { + expect(parsePostgresAst("COMMENT ON CONSTRAINT users_pk ON public.users IS 'users pk';")).toEqual({result: {statements: [{ + ...stmt('CommentOn', 0, 9, 60), + object: kind('Constraint', 11), + entity: identifier('users_pk', 22), + schema: identifier('public', 34), + parent: identifier('users', 41), + comment: string('users pk', 50), + }]}}) + }) + }) + describe('commit', () => { + test('simple', () => { + expect(parsePostgresAst('COMMIT;')).toEqual({result: {statements: [stmt('Commit', 0, 5, 6)]}}) + }) + test('complex', () => { + expect(parsePostgresAst('COMMIT WORK AND NO CHAIN;')).toEqual({result: {statements: [{ + ...stmt('Commit', 0, 5, 24), + object: kind('Work', 7), + chain: {token: token(12, 23), no: {token: token(16, 17)}}, + }]}}) + }) + }) + describe('createExtension', () => { + test('simple', () => { + expect(parsePostgresAst('CREATE EXTENSION citext;')).toEqual({result: {statements: [{ + ...stmt('CreateExtension', 0, 15, 23), + name: identifier('citext', 17), + }]}}) + }) + test('full', () => { + expect(parsePostgresAst("CREATE EXTENSION IF NOT EXISTS citext WITH SCHEMA public VERSION '1.0' CASCADE;")).toEqual({result: {statements: [{ + ...stmt('CreateExtension', 0, 15, 78), + ifNotExists: {token: token(17, 29)}, + name: identifier('citext', 31), + with: {token: token(38, 41)}, + schema: {token: token(43, 48), name: identifier('public', 50)}, + version: {token: token(57, 63), number: string('1.0', 65)}, + cascade: {token: token(71, 77)}, + }]}}) + }) + }) + describe('createFunction', () => { + test('sample', () => { + expect(parsePostgresAst("CREATE FUNCTION add(IN a integer, IN b integer) RETURNS integer LANGUAGE SQL IMMUTABLE AS $$ BEGIN RETURN a + b; END; $$ RETURNS NULL ON NULL INPUT;")).toEqual({result: {statements: [{ + ...stmt('CreateFunction', 0, 14, 147), + name: identifier('add', 16), + args: [ + {mode: kind('In', 20), name: identifier('a', 23), type: {name: identifier('integer', 25), token: token(25, 31)}}, + {mode: kind('In', 34), name: identifier('b', 37), type: {name: identifier('integer', 39), token: token(39, 45)}}, + ], + returns: {kind: 'Type', token: token(48, 54), type: {name: identifier('integer', 56), token: token(56, 62)}}, + language: {token: token(64, 71), name: identifier('SQL', 73)}, + behavior: kind('Immutable', 77), + definition: {token: token(87, 88), value: {...string('BEGIN RETURN a + b; END;', 90, 119), dollar: '$$'}}, + nullBehavior: kind('ReturnsNull', 121, 146), + }]}}) + }) + test('using SQL', () => { + expect(parsePostgresAst('CREATE FUNCTION add(a integer, b integer) RETURNS integer LANGUAGE SQL IMMUTABLE RETURNS NULL ON NULL INPUT RETURN a + b;')).toEqual({result: {statements: [{ + ...stmt('CreateFunction', 0, 14, 120), + name: identifier('add', 16), + args: [ + {name: identifier('a', 20), type: {name: identifier('integer', 22), token: token(22, 28)}}, + {name: identifier('b', 31), type: {name: identifier('integer', 33), token: token(33, 39)}}, + ], + returns: {kind: 'Type', token: token(42, 48), type: {name: identifier('integer', 50), token: token(50, 56)}}, + language: {token: token(58, 65), name: identifier('SQL', 67)}, + behavior: kind('Immutable', 71), + nullBehavior: kind('ReturnsNull', 81, 106), + return: {token: token(108, 113), expression: operation(column('a', 115), op('+', 117), column('b', 119))} + }]}}) + }) + test('using plSQL', () => { + expect(parsePostgresAst('CREATE FUNCTION increment(i integer) RETURNS integer AS $$ BEGIN RETURN i + 1; END; $$ LANGUAGE plpgsql;')).toEqual({result: {statements: [{ + ...stmt('CreateFunction', 0, 14, 103), + name: identifier('increment', 16), + args: [{name: identifier('i', 26), type: {name: identifier('integer', 28), token: token(28, 34)}}], + returns: {kind: 'Type', token: token(37, 43), type: {name: identifier('integer', 45), token: token(45, 51)}}, + definition: {token: token(53, 54), value: {...string('BEGIN RETURN i + 1; END;', 56, 85), dollar: '$$'}}, + language: {token: token(87, 94), name: identifier('plpgsql', 96)}, + }]}}) + }) + test.skip('using SQL query', () => { + // FIXME: bad argument parsing when no name + expect(parsePostgresAst("CREATE OR REPLACE FUNCTION public.add(integer, integer) RETURNS integer AS 'select $1 + $2;' LANGUAGE SQL IMMUTABLE RETURNS NULL ON NULL INPUT;")).toEqual({result: {statements: [{ + ...stmt('CreateFunction', 0, 14, 28), + schema: identifier('public', 16), + name: identifier('add', 23), + args: [ + {type: {name: identifier('integer', 25), token: token(25, 31)}}, + {type: {name: identifier('integer', 39), token: token(39, 45)}}, + ], + returns: {kind: 'Type', token: token(48, 54), type: {name: identifier('integer', 56), token: token(56, 62)}}, + definition: {token: token(87, 88), value: string('select $1 + $2;', 90, 119)}, + language: {token: token(64, 71), name: identifier('SQL', 73)}, + behavior: kind('Immutable', 77), + nullBehavior: kind('ReturnsNull', 121, 146), + }]}}) + }) + }) + describe('createIndex', () => { + test('simple', () => { + expect(parsePostgresAst('CREATE INDEX ON users (name);')).toEqual({result: {statements: [{ + ...stmt('CreateIndex', 0, 11, 28), + table: identifier('users', 16), + columns: [column('name', 23)], + }]}}) + }) + test('full', () => { + expect(parsePostgresAst('CREATE UNIQUE INDEX CONCURRENTLY IF NOT EXISTS users_name_idx ON ONLY public.users USING btree' + + ' ((lower(first_name)) COLLATE "de_DE" ASC NULLS LAST)' + + // TODO: ' INCLUDE (email) NULLS NOT DISTINCT WITH (fastupdate = off) TABLESPACE indexspace WHERE deleted_at IS NULL' + + ' INCLUDE (email);' + )).toEqual({result: {statements: [{ + ...stmt('CreateIndex', 0, 18, 163), + unique: {token: token(7, 12)}, + concurrently: {token: token(20, 31)}, + ifNotExists: {token: token(33, 45)}, + index: identifier('users_name_idx', 47), + only: {token: token(65, 68)}, + schema: identifier('public', 70), + table: identifier('users', 77), + using: {token: token(83, 87), method: identifier('btree', 89)}, + columns: [{ + kind: 'Group', + expression: function_('lower', 97, [column('first_name', 103)]), + collation: {token: token(116, 122), name: {...identifier('de_DE', 124, 130), quoted: true}}, + order: kind('Asc', 132), + nulls: kind('Last', 136, 145) + }], + include: {token: token(148, 154), columns: [identifier('email', 157)]}, + // where: {token: token(0, 0), predicate: {???}} + }]}}) + }) + }) + describe('createMaterializedView', () => { + test('simple', () => { + expect(parsePostgresAst("CREATE MATERIALIZED VIEW admins AS SELECT * FROM users WHERE role='admin';")).toEqual({result: {statements: [{ + ...stmt('CreateMaterializedView', 0, 23, 73), + name: identifier('admins', 25), + query: { + token: token(35, 40), + columns: [wildcard(42)], + from: {...kind('Table', 44, 47), table: identifier('users', 49)}, + where: {token: token(55, 59), predicate: operation(column('role', 61), op('=', 65), string('admin', 66))}, + } + }]}}) + }) + }) + describe('createSchema', () => { + test('simple', () => { + expect(parsePostgresAst('CREATE SCHEMA cms;')).toEqual({result: {statements: [{ + ...stmt('CreateSchema', 0, 12, 17), + schema: identifier('cms', 14), + }]}}) + }) + test('complex', () => { + expect(parsePostgresAst('CREATE SCHEMA IF NOT EXISTS cms AUTHORIZATION CURRENT_USER;')).toEqual({result: {statements: [{ + ...stmt('CreateSchema', 0, 12, 58), + ifNotExists: {token: token(14, 26)}, + schema: identifier('cms', 28), + authorization: {token: token(32, 44), owner: kind('CurrentUser', 46, 57)}, + }]}}) + }) + }) + describe('createSequence', () => { + test('simple', () => { + expect(parsePostgresAst('CREATE SEQUENCE users_id_seq;')).toEqual({result: {statements: [{ + ...stmt('CreateSequence', 0, 14, 28), + name: identifier('users_id_seq', 16) + }]}}) + }) + test('complex', () => { + expect(parsePostgresAst('CREATE UNLOGGED SEQUENCE IF NOT EXISTS public.users_id_seq AS integer START WITH 1 INCREMENT BY 1 MINVALUE 1 NO MAXVALUE CACHE 1 OWNED BY users.id;')).toEqual({result: {statements: [{ + ...stmt('CreateSequence', 0, 23, 146), + mode: kind('Unlogged', 7), + ifNotExists: {token: token(25, 37)}, + schema: identifier('public', 39), + name: identifier('users_id_seq', 46), + as: {token: token(59, 60), type: identifier('integer', 62)}, + start: {token: token(70, 79), value: integer(1, 81)}, + increment: {token: token(83, 94), value: integer(1, 96)}, + minValue: {token: token(98, 105), value: integer(1, 107)}, + maxValue: {token: token(109, 119)}, + cache: {token: token(121, 125), value: integer(1, 127)}, + ownedBy: {token: token(129, 136), owner: {kind: 'Column', table: identifier('users', 138), column: identifier('id', 144)}} + }]}}) + }) + }) + describe('createTable', () => { + test('simple', () => { + expect(parsePostgresAst('CREATE TABLE users (id int PRIMARY KEY, name VARCHAR);')).toEqual({result: {statements: [{ + ...stmt('CreateTable', 0, 11, 53), + name: identifier('users', 13), + columns: [ + {name: identifier('id', 20), type: {name: identifier('int', 23), token: token(23, 25)}, constraints: [kind('PrimaryKey', 27, 37)]}, + {name: identifier('name', 40), type: {name: identifier('VARCHAR', 45), token: token(45, 51)}}, + ], + }]}}) + }) + test('with constraints', () => { + expect(parsePostgresAst('CREATE TABLE users (id int, role VARCHAR, CONSTRAINT users_pk PRIMARY KEY (id), FOREIGN KEY (role) REFERENCES roles (name));')).toEqual({result: {statements: [{ + ...stmt('CreateTable', 0, 11, 123), + name: identifier('users', 13), + columns: [ + {name: identifier('id', 20), type: {name: identifier('int', 23), token: token(23, 25)}}, + {name: identifier('role', 28), type: {name: identifier('VARCHAR', 33), token: token(33, 39)}}, + ], + constraints: [ + {constraint: {token: token(42, 51), name: identifier('users_pk', 53)}, ...kind('PrimaryKey', 62, 72), columns: [identifier('id', 75)]}, + {...kind('ForeignKey', 80, 90), columns: [identifier('role', 93)], ref: {token: token(99, 108), table: identifier('roles', 110), columns: [identifier('name', 117)]}} + ], + }]}}) + }) + }) + describe('createTrigger', () => { + test('simple', () => { + expect(parsePostgresAst('CREATE TRIGGER check_update BEFORE UPDATE ON accounts EXECUTE FUNCTION check_account_update();')).toEqual({result: {statements: [{ + ...stmt('CreateTrigger', 0, 13, 93), + name: identifier('check_update', 15), + timing: kind('Before', 28), + events: [kind('Update', 35)], + table: identifier('accounts', 45), + execute: {token: token(54, 69), function: identifier('check_account_update', 71), arguments: []} + }]}}) + }) + test('complex', () => { + expect(parsePostgresAst('CREATE OR REPLACE CONSTRAINT TRIGGER check_delete INSTEAD OF DELETE OR TRUNCATE OR UPDATE OF email, password ON public.accounts ' + + 'FROM public.admins DEFERRABLE INITIALLY DEFERRED ' + + 'REFERENCING OLD TABLE AS oldtab NEW TABLE newtab ' + + 'FOR EACH ROW ' + + 'WHEN (OLD.* IS DISTINCT FROM NEW.*) ' + + 'EXECUTE PROCEDURE public.check_account_delete(\'check\', 1);')).toEqual({result: {statements: [{ + ...stmt('CreateTrigger', 0, 35, 332), + replace: {token: token(7, 16)}, + constraint: {token: token(18, 27)}, + name: identifier('check_delete', 37), + timing: kind('InsteadOf', 50, 59), + events: [kind('Delete', 61), kind('Truncate', 71), {...kind('Update', 83), columns: [identifier('email', 93), identifier('password', 100)]}], + schema: identifier('public', 112), + table: identifier('accounts', 119), + from: {token: token(128, 131), schema: identifier('public', 133), table: identifier('admins', 140)}, + deferrable: {...kind('Deferrable', 147), initially: kind('Deferred', 158, 175)}, + referencing: {token: token(177, 187), old: {token: token(189, 200), name: identifier('oldtab', 202)}, new: {token: token(209, 217), name: identifier('newtab', 219)}}, + target: kind('Row', 226, 237), + when: {token: token(239, 242), condition: operation(wildcard(245, 'OLD'), op('DistinctFrom', 251, 266), wildcard(268, 'NEW'))}, + execute: {token: token(275, 291), schema: identifier('public', 293), function: identifier('check_account_delete', 300), arguments: [string('check', 321), integer(1, 330)]} + }]}}) + }) + }) + describe('createType', () => { + test('simple', () => { + expect(parsePostgresAst('CREATE TYPE position;')).toEqual({result: {statements: [{ + ...stmt('CreateType', 0, 10, 20), + name: identifier('position', 12), + }]}}) + }) + test('struct', () => { + expect(parsePostgresAst('CREATE TYPE layout_position AS (x int, y int COLLATE "fr_FR");')).toEqual({result: {statements: [{ + ...stmt('CreateType', 0, 10, 61), + name: identifier('layout_position', 12), + struct: {token: token(28, 29), attrs: [ + {name: identifier('x', 32), type: {name: identifier('int', 34), token: token(34, 36)}}, + {name: identifier('y', 39), type: {name: identifier('int', 41), token: token(41, 43)}, collation: {token: token(45, 51), name: {...identifier('fr_FR', 53, 59), quoted: true}}} + ]}, + }]}}) + }) + test('enum', () => { + expect(parsePostgresAst("CREATE TYPE public.bug_status AS ENUM ('open', 'closed');")).toEqual({result: {statements: [{ + ...stmt('CreateType', 0, 10, 56), + schema: identifier('public', 12), + name: identifier('bug_status', 19), + enum: {token: token(30, 36), values: [string('open', 39), string('closed', 47)]}, + }]}}) + }) + // TODO: range + test('base', () => { + expect(parsePostgresAst("CREATE TYPE box (INPUT = my_box_in_function, OUTPUT = my_box_out_function, INTERNALLENGTH = 16);")).toEqual({result: {statements: [{ + ...stmt('CreateType', 0, 10, 95), + name: identifier('box', 12), + base: [ + // FIXME: expressions should be different here (identifier instead of column), or better, each parameter should have its own kind of value... + {name: identifier('INPUT', 17), value: column('my_box_in_function', 25)}, + {name: identifier('OUTPUT', 45), value: column('my_box_out_function', 54)}, + {name: identifier('INTERNALLENGTH', 75), value: integer(16, 92)}, + ], + }]}}) + }) + }) + describe('createView', () => { + test('simple', () => { + expect(parsePostgresAst("CREATE VIEW admins AS SELECT * FROM users WHERE role = 'admin';")).toEqual({result: {statements: [{ + ...stmt('CreateView', 0, 10, 62), + name: identifier('admins', 12), + query: { + token: token(22, 27), + columns: [wildcard(29)], + from: {token: token(31, 34), kind: 'Table', table: identifier('users', 36)}, + where: {token: token(42, 46), predicate: operation(column('role', 48), op('=', 53), string('admin', 55))}, + }, + }]}}) + }) + test('full', () => { + expect(parsePostgresAst("CREATE OR REPLACE TEMP RECURSIVE VIEW admins (id, name) AS SELECT * FROM users WHERE role = 'admin';")).toEqual({result: {statements: [{ + ...stmt('CreateView', 0, 36, 99), + replace: {token: token(7, 16)}, + temporary: {token: token(18, 21)}, + recursive: {token: token(23, 31)}, + name: identifier('admins', 38), + columns: [identifier('id', 46), identifier('name', 50)], + query: { + token: token(59, 64), + columns: [wildcard(66)], + from: {token: token(68, 71), kind: 'Table', table: identifier('users', 73)}, + where: {token: token(79, 83), predicate: operation(column('role', 85), op('=', 90), string('admin', 92))}, + }, + }]}}) + }) + }) + describe('delete', () => { + test('simple', () => { + expect(parsePostgresAst('DELETE FROM films;')).toEqual({result: {statements: [{ + ...stmt('Delete', 0, 10, 17), + table: identifier('films', 12), + }]}}) + }) + test('complex', () => { + expect(parsePostgresAst("DELETE FROM ONLY public.tasks * t WHERE t.status = 'DONE' RETURNING *;")).toEqual({result: {statements: [{ + ...stmt('Delete', 0, 10, 69), + only: {token: token(12, 15)}, + schema: identifier('public', 17), + table: identifier('tasks', 24), + descendants: {token: token(30, 30)}, + alias: alias('t', 32), + where: {token: token(34, 38), predicate: operation(column('status', 40, 't'), op('=', 49), string('DONE', 51))}, + returning: {token: token(58, 66), columns: [wildcard(68)]} + }]}}) + }) + test('using', () => { + expect(parsePostgresAst("DELETE FROM films USING producers WHERE producer_id = producers.id AND producers.name = 'foo';")).toEqual({result: {statements: [{ + ...stmt('Delete', 0, 10, 93), + table: identifier('films', 12), + using: {token: token(18, 22), kind: 'Table', table: identifier('producers', 24)}, + where: {token: token(34, 38), predicate: operation( + operation(column('producer_id', 40), op('=', 52), column('id', 54, 'producers')), + op('And', 67), + operation(column('name', 71, 'producers'), op('=', 86), string('foo', 88)) + )} + }]}}) + }) + }) + describe('drop', () => { + test('simple', () => { + expect(parsePostgresAst('DROP TABLE users;')).toEqual({result: {statements: [{ + ...stmt('Drop', 0, 9, 16), + object: 'Table', + entities: [{name: identifier('users', 11)}], + }]}}) + }) + test('complex', () => { + expect(parsePostgresAst('DROP INDEX CONCURRENTLY IF EXISTS users_idx, posts_idx CASCADE;')).toEqual({result: {statements: [{ + ...stmt('Drop', 0, 9, 62), + object: 'Index', + concurrently: {token: token(11, 22)}, + ifExists: {token: token(24, 32)}, + entities: [{name: identifier('users_idx', 34)}, {name: identifier('posts_idx', 45)}], + mode: kind('Cascade', 55), + }]}}) + }) + test('kind', () => { + expect(parsePostgresAst('DROP TABLE users;').errors || []).toEqual([]) + expect(parsePostgresAst('DROP VIEW users;').errors || []).toEqual([]) + expect(parsePostgresAst('DROP MATERIALIZED VIEW users;').errors || []).toEqual([]) + expect(parsePostgresAst('DROP INDEX users_idx;').errors || []).toEqual([]) + expect(parsePostgresAst('DROP TYPE users;').errors || []).toEqual([]) + }) + }) + describe('insertInto', () => { + test('simple', () => { + expect(parsePostgresAst("INSERT INTO users VALUES (1, 'loic');")).toEqual({result: {statements: [{ + ...stmt('InsertInto', 0, 10, 36), + table: identifier('users', 12), + values: [[integer(1, 26), string('loic', 29)]], + }]}}) + }) + test('full', () => { + expect(parsePostgresAst("INSERT INTO users (id, name) VALUES (1, 'loic'), (DEFAULT, 'lou') RETURNING id;")).toEqual({result: {statements: [{ + ...stmt('InsertInto', 0, 10, 78), + table: identifier('users', 12), + columns: [identifier('id', 19), identifier('name', 23)], + values: [[integer(1, 37), string('loic', 40)], [kind('Default', 50), string('lou', 59)]], + returning: {token: token(66, 74), columns: [column('id', 76)]}, + }]}}) + }) + test('conflict', () => { + expect(parsePostgresAst("INSERT INTO users VALUES (1, 'loic') ON CONFLICT DO NOTHING;")).toEqual({result: {statements: [{ + ...stmt('InsertInto', 0, 10, 59), + table: identifier('users', 12), + values: [[integer(1, 26), string('loic', 29)]], + onConflict: {token: token(37, 47), action: kind('Nothing', 49, 58)} + }]}}) + expect(parsePostgresAst("INSERT INTO users VALUES (1, 'loic') ON CONFLICT (id) WHERE id > 10 DO UPDATE SET name = EXCLUDED.name WHERE users.role='guest';")).toEqual({result: {statements: [{ + ...stmt('InsertInto', 0, 10, 127), + table: identifier('users', 12), + values: [[integer(1, 26), string('loic', 29)]], + onConflict: { + token: token(37, 47), + target: { + kind: 'Columns', + columns: [identifier('id', 50)], + where: {token: token(54, 58), predicate: operation(column('id', 60), op('>', 63), integer(10, 65))} + }, + action: { + ...kind('Update', 68, 76), + columns: [{column: identifier('name', 82), value: column('name', 89, 'EXCLUDED')}], + where: {token: token(103, 107), predicate: operation(column('role', 109, 'users'), op('=', 119), string('guest', 120))} + } + } + }]}}) + }) + // TODO: `INSERT INTO films SELECT * FROM tmp_films WHERE date_prod < '2004-05-07';` + }) + describe('select', () => { + test('simple', () => { + expect(parsePostgresAst('SELECT name FROM users;')).toEqual({result: {statements: [{ + ...stmt('Select', 0, 5, 22), + columns: [column('name', 7)], + from: {token: token(12, 15), kind: 'Table', table: identifier('users', 17)}, + }]}}) + }) + test('complex', () => { + const id = (value: string) => ({kind: 'Identifier', value}) + const int = (value: number) => ({kind: 'Integer', value}) + const op = (left: any, op: Operator, right: any) => ({kind: 'Operation', left, op: {kind: op}, right}) + expect(removeTokens(parsePostgresAst('SELECT u.id, first_name AS name FROM public.users u WHERE u.id = 1 LIMIT 10 OFFSET 20;'))).toEqual({result: {statements: [{ + kind: 'Select', + columns: [ + {kind: 'Column', table: id('u'), column: id('id')}, + {kind: 'Column', column: id('first_name'), alias: {name: id('name')}} + ], + from: {kind: 'Table', schema: id('public'), table: id('users'), alias: {name: id('u')}}, + where: {predicate: op({kind: 'Column', table: id('u'), column: id('id')}, '=', int(1))}, + limit: {value: int(10)}, + offset: {value: int(20)}, + }]}}) + }) + test('select only', () => { + expect(parsePostgresAst("SELECT pg_catalog.set_config('search_path', '', false);")).toEqual({result: {statements: [{ + ...stmt('Select', 0, 5, 54), + columns: [function_('set_config', 7, [string('search_path', 29), string('', 44), boolean(false, 48)], 'pg_catalog')], + }]}}) + }) + test('join', () => { + expect(parsePostgresAst( + 'SELECT * FROM events e JOIN users u ON e.created_by=u.id' + + ' LEFT OUTER JOIN public.projects AS p USING (project_id) AS jp' + + ' NATURAL CROSS JOIN demo;' + )).toEqual({result: {statements: [{ + ...stmt('Select', 0, 5, 142), + columns: [wildcard(7)], + from: {token: token(9, 12), kind: 'Table', table: identifier('events', 14), alias: alias('e', 21), joins: [{ + ...kind('Inner', 23, 26), + from: {kind: 'Table', table: identifier('users', 28), alias: alias('u', 34)}, + on: {...kind('On', 36), predicate: operation(column('created_by', 39, 'e'), op('=', 51), column('id', 52, 'u'))} + }, { + ...kind('Left', 57, 71), + from: {kind: 'Table', schema: identifier('public', 73), table: identifier('projects', 80), alias: alias('p', 92, 89)}, + on: {...kind('Using', 94), columns: [identifier('project_id', 101)]}, + alias: alias('jp', 116, 113) + }, { + ...kind('Cross', 127, 136), + from: {kind: 'Table', table: identifier('demo', 138)}, + on: kind('Natural', 119) + }]} + }]}}) + }) + test('group by', () => { + // TODO interval: expect(parsePostgresAst("SELECT kind, sum(len) AS total FROM films GROUP BY kind HAVING sum(len) < interval '5 hours' ORDER BY kind;")).toEqual({result: {statements: [{ + expect(parsePostgresAst("SELECT kind, sum(len) AS total FROM films GROUP BY kind HAVING sum(len) < '5 hours' ORDER BY kind DESC;")).toEqual({result: {statements: [{ + ...stmt('Select', 0, 5, 111), + columns: [column('kind', 7), {...function_('sum', 13, [column('len', 17)]), alias: alias('total', 25, 22)}], + from: {token: token(31, 34), kind: 'Table', table: identifier('films', 36)}, + groupBy: {token: token(42, 49), expressions: [column('kind', 51)]}, + having: {token: token(56, 61), predicate: operation(function_('sum', 63, [column('len', 67)]), op('<', 72), string('5 hours', 83))}, + orderBy: {token: token(93, 100), expressions: [{...column('kind', 102), order: kind('Desc', 107)}]} + }]}}) + }) + test('distinct', () => { + expect(parsePostgresAst('SELECT DISTINCT name, email FROM users;')).toEqual({result: {statements: [{ + ...stmt('Select', 0, 5, 38), + distinct: {token: token(7, 14)}, + columns: [column('name', 16), column('email', 22)], + from: {token: token(28, 31), kind: 'Table', table: identifier('users', 33)} + }]}}) + expect(parsePostgresAst('SELECT DISTINCT ON (email) name, email FROM users;')).toEqual({result: {statements: [{ + ...stmt('Select', 0, 5, 49), + distinct: {token: token(7, 14), on: {token: token(16, 17), columns: [column('email', 20)]}}, + columns: [column('name', 27), column('email', 33)], + from: {token: token(39, 42), kind: 'Table', table: identifier('users', 44)} + }]}}) + }) + test('from select', () => { + expect(parsePostgresAst('SELECT * FROM (SELECT * FROM users) u WHERE id = 1;')).toEqual({result: {statements: [{ + ...stmt('Select', 0, 5, 50), + columns: [wildcard(7)], + from: { + token: token(9, 12), + kind: 'Select', + select: { + token: token(15, 20), + columns: [wildcard(22)], + from: {token: token(24, 27), kind: 'Table', table: identifier('users', 29)}, + }, + alias: alias('u', 36) + }, + where: {token: token(38, 42), predicate: operation(column('id', 44), op('=', 47), integer(1, 49))} + }]}}) + }) + test('union', () => { + expect(parsePostgresAst('SELECT name FROM users UNION ALL SELECT name FROM organizations;')).toEqual({result: {statements: [{ + ...stmt('Select', 0, 5, 63), + columns: [column('name', 7)], + from: {token: token(12, 15), kind: 'Table', table: identifier('users', 17)}, + union: {...kind('Union', 23), mode: kind('All', 29), select: { + token: token(33, 38), + columns: [column('name', 40)], + from: {token: token(45, 48), kind: 'Table', table: identifier('organizations', 50)}, + }} + }]}}) + }) + test('filter', () => { + expect(parsePostgresAst('SELECT count(*) AS user_count, count(*) FILTER (WHERE spend > 5000) AS top_user_count FROM users;')).toEqual({result: {statements: [{ + ...stmt('Select', 0, 5, 96), + columns: [{...function_('count', 7, [wildcard(13)]), alias: alias('user_count', 19, 16)}, { + ...function_('count', 31, [wildcard(37)]), + filter: {token: token(40, 45), where: {token: token(48, 52), predicate: operation(column('spend', 54), op('>', 60), integer(5000, 62))}}, + alias: alias('top_user_count', 71, 68) + }], + from: {token: token(86, 89), kind: 'Table', table: identifier('users', 91)} + }]}}) + }) + test('window', () => { + expect(parsePostgresAst('SELECT org, salary, avg(salary) OVER (PARTITION BY org ORDER BY salary DESC) FROM users;')).toEqual({result: {statements: [{ + ...stmt('Select', 0, 5, 87), + columns: [column('org', 7), column('salary', 12), {...function_('avg', 20, [column('salary', 24)]), over: { + token: token(32, 35), + partitionBy: {token: token(38, 49), columns: [column('org', 51)]}, + orderBy: {token: token(55, 62), expressions: [{...column('salary', 64), order: kind('Desc', 71)}]} + }}], + from: {token: token(77, 80), kind: 'Table', table: identifier('users', 82)} + }]}}) + expect(parsePostgresAst('SELECT org, salary, avg(salary) OVER org_window FROM users WINDOW org_window AS (PARTITION BY org ORDER BY salary DESC);')).toEqual({result: {statements: [{ + ...stmt('Select', 0, 5, 119), + columns: [column('org', 7), column('salary', 12), {...function_('avg', 20, [column('salary', 24)]), over: {token: token(32, 35), name: identifier('org_window', 37)}}], + from: {token: token(48, 51), kind: 'Table', table: identifier('users', 53)}, + window: [{ + token: token(59, 64), + name: identifier('org_window', 66), + partitionBy: {token: token(81, 92), columns: [column('org', 94)]}, + orderBy: {token: token(98, 105), expressions: [{...column('salary', 107), order: kind('Desc', 114)}]} + }] + }]}}) + }) + test('flexible parenthesis', () => { + expect(parsePostgresAst('(SELECT * FROM ((SELECT name FROM users) UNION (SELECT name FROM organizations)));')).toEqual({result: {statements: [{ + ...stmt('Select', 1, 6, 81), + columns: [wildcard(8)], + from: {token: token(10, 13), kind: 'Select', select: { + token: token(17, 22), + columns: [column('name', 24)], + from: {token: token(29, 32), kind: 'Table', table: identifier('users', 34)}, + union: {...kind('Union', 41), select: { + token: token(48, 53), + columns: [column('name', 55)], + from: {token: token(60, 63), kind: 'Table', table: identifier('organizations', 65)}, + }} + }} + }]}}) + expect(parsePostgresAst('(SELECT * FROM ((SELECT name FROM users LIMIT 1) UNION (SELECT name FROM organizations LIMIT 1)));').errors).toEqual(undefined) + expect(parsePostgresAst('(SELECT * FROM users WHERE role=0 ORDER BY id LIMIT 1 OFFSET 0 FETCH FIRST 1 ROW ONLY);').errors).toEqual(undefined) + // expect(parsePostgresAst('(SELECT * FROM users WHERE role=0 ORDER BY id LIMIT 1 OFFSET 0) FETCH FIRST 1 ROW ONLY;').errors).toEqual(undefined) + // expect(parsePostgresAst('(SELECT * FROM users WHERE role=0 ORDER BY id LIMIT 1) OFFSET 0 FETCH FIRST 1 ROW ONLY;').errors).toEqual(undefined) + // expect(parsePostgresAst('(SELECT * FROM users WHERE role=0 ORDER BY id) LIMIT 1 OFFSET 0 FETCH FIRST 1 ROW ONLY;').errors).toEqual(undefined) + // expect(parsePostgresAst('(SELECT * FROM users WHERE role=0) ORDER BY id LIMIT 1 OFFSET 0 FETCH FIRST 1 ROW ONLY;').errors).toEqual(undefined) + }) + test.skip('common table expression', () => { + expect(parsePostgresAst('WITH t AS (SELECT random() as x FROM generate_series(1, 3)) SELECT * FROM t UNION ALL SELECT * FROM t;')).toEqual({result: {statements: []}}) + expect(removeTokens(parsePostgresAst("WITH RECURSIVE employee_recursive(distance, employee_name, manager_name) AS (\n" + + " SELECT 1, employee_name, manager_name FROM employee WHERE manager_name = 'Mary'\n" + + " UNION ALL\n" + + " SELECT er.distance + 1, e.employee_name, e.manager_name FROM employee_recursive er, employee e WHERE er.employee_name = e.manager_name\n" + + ")\n" + + "SELECT distance, employee_name FROM employee_recursive;"))).toEqual({result: {statements: []}}) + }) + }) + describe('set', () => { + test('simple', () => { + expect(parsePostgresAst('SET lock_timeout = 0;')).toEqual({result: {statements: [ + {...stmt('Set', 0, 2, 20), parameter: identifier('lock_timeout', 4), equal: kind('=', 17), value: integer(0, 19)} + ]}}) + }) + test('complex', () => { + expect(parsePostgresAst('SET SESSION search_path TO my_schema, public;')).toEqual({result: {statements: [ + {...stmt('Set', 0, 2, 44), scope: kind('Session', 4), parameter: identifier('search_path', 12), equal: kind('To', 24), value: [identifier('my_schema', 27), identifier('public', 38)]} + ]}}) + }) + test('no equal', () => { + expect(parsePostgresAst("SET ROLE 'admin';")).toEqual({result: {statements: [ + {...stmt('Set', 0, 2, 16), parameter: identifier('ROLE', 4), value: string('admin', 9)} + ]}}) + }) + test('on', () => { + expect(parsePostgresAst('SET standard_conforming_strings = on;')).toEqual({result: {statements: [ + {...stmt('Set', 0, 2, 36), parameter: identifier('standard_conforming_strings', 4), equal: kind('=', 32), value: identifier('on', 34)} + ]}}) + }) + }) + describe('show', () => { + test('simple', () => { + expect(parsePostgresAst('SHOW block_size;')).toEqual({result: {statements: [{ + ...stmt('Show', 0, 3, 15), + name: identifier('block_size', 5) + }]}}) + }) + test.skip('complex', () => { + expect(parsePostgresAst('SHOW TRANSACTION ISOLATION LEVEL;')).toEqual({result: {statements: [{ + ...stmt('Show', 0, 3, 32), + name: identifier('TRANSACTION ISOLATION LEVEL', 5) + }]}}) + }) + }) + describe('update', () => { + test('simple', () => { + expect(parsePostgresAst("UPDATE films SET kind = 'Dramatic' WHERE kind = 'Drama';")).toEqual({result: {statements: [{ + ...stmt('Update', 0, 5, 55), + table: identifier('films', 7), + columns: [ { column: identifier('kind', 17), value: string('Dramatic', 24) } ], + where: {token: token(35, 39), predicate: {kind: 'Operation', left: column('kind', 41), op: op('=', 46), right: string('Drama', 48)}} + }]}}) + // UPDATE weather SET temp_lo = temp_lo + 1, prcp = DEFAULT WHERE city = 'San Francisco' RETURNING temp_lo; + // UPDATE weather SET (temp_lo, prcp) = (temp_lo+1, DEFAULT) WHERE city = 'San Francisco'; + // UPDATE employees SET sales_count = sales_count + 1 FROM accounts WHERE accounts.name = 'Acme' AND employees.id = accounts.sales_person; + // UPDATE employees SET sales_count = sales_count + 1 WHERE id = (SELECT sales_person FROM accounts WHERE name = 'Acme'); + // UPDATE accounts SET (first_name, last_name) = (SELECT first_name, last_name FROM employees WHERE employees.id = accounts.sales_person); + }) + }) + describe('clauses', () => { + describe('selectClause', () => { + test('simple', () => { + expect(parseRule(p => p.selectClauseRule(), 'SELECT name')).toEqual({result: { + token: token(0, 5), + columns: [column('name', 7)], + }}) + }) + test('complex', () => { + expect(parseRule(p => p.selectClauseRule(), 'SELECT e.*, u.name AS user_name, lower(u.email), "public"."Event"."id"')).toEqual({result: {token: token(0, 5), columns: [ + {table: identifier('e', 7), ...wildcard(9)}, + {...column('name', 12, 'u'), alias: alias('user_name', 22, 19)}, + function_('lower', 33, [column('email', 39, 'u')]), + {kind: 'Column', schema: {...identifier('public', 49, 56), quoted: true}, table: {...identifier('Event', 58, 64), quoted: true}, column: {...identifier('id', 66, 69), quoted: true}} + ]}}) + }) + // TODO: SELECT count(*), count(distinct e.created_by) FILTER (WHERE u.created_at + interval '#{period}' < e.created_at) AS not_new_users + }) + describe('fromClause', () => { + test('simple', () => { + expect(parseRule(p => p.fromClauseRule(), 'FROM users')).toEqual({result: {token: token(0, 3), kind: 'Table', table: identifier('users', 5)}}) + }) + test('table', () => { + expect(parseRule(p => p.fromClauseRule(), 'FROM "users" as u')).toEqual({result: { + token: token(0, 3), + kind: 'Table', + table: {...identifier('users', 5, 11), quoted: true}, + alias: alias('u', 16, 13), + }}) + }) + // TODO: FROM (SELECT * FROM ...) + }) + describe('whereClause', () => { + test('simple', () => { + expect(parseRule(p => p.whereClauseRule(), 'WHERE id = 1')).toEqual({result: { + token: token(0, 4), + predicate: operation(column('id', 6), op('=', 9), integer(1, 11)), + }}) + }) + test('complex', () => { + expect(parseRule(p => p.whereClauseRule(), "WHERE \"id\" = $1 OR (email LIKE '%@azimutt.app' AND role = 'admin')")).toEqual({result: {token: token(0, 4), predicate: operation( + operation({kind: 'Column', column: {...identifier('id', 6, 9), quoted: true}}, op('=', 11), parameter(1, 13)), + op('Or', 16), + group(operation( + operation(column('email', 20), op('Like', 26), string('%@azimutt.app', 31)), + op('And', 47), + operation(column('role', 51), op('=', 56), string('admin', 58)) + )) + )}}) + }) + }) + describe('tableColumnRule', () => { + test('simple', () => { + expect(parseRule(p => p.tableColumnRule(), 'id int')).toEqual({result: {name: identifier('id', 0), type: {name: identifier('int', 3), token: token(3, 5)}}}) + }) + test('not null & default', () => { + expect(parseRule(p => p.tableColumnRule(), "role varchar NOT NULL DEFAULT 'guest'")).toEqual({result: { + name: identifier('role', 0), + type: {name: identifier('varchar', 5), token: token(5, 11)}, + constraints: [{...kind('Nullable', 13, 20), value: false}, {...kind('Default', 22), expression: string('guest', 30)}], + }}) + expect(parseRule(p => p.tableColumnRule(), "role int DEFAULT 0 NOT NULL")).toEqual({result: { + name: identifier('role', 0), + type: {name: identifier('int', 5), token: token(5, 7)}, + constraints: [{...kind('Default', 9), expression: integer(0, 17)}, {...kind('Nullable', 19, 26), value: false}], + }}) + expect(parseRule(p => p.tableColumnRule(), "role varchar DEFAULT 'guest'::character varying")).toEqual({result: { + name: identifier('role', 0), + type: {name: identifier('varchar', 5), token: token(5, 11)}, + constraints: [{...kind('Default', 13), expression: {...string('guest', 21), cast: {token: token(28, 29), type: {name: identifier('character varying', 30), token: token(30, 46)}}}}], + }}) + }) + test('primaryKey', () => { + expect(parseRule(p => p.tableColumnRule(), 'id int PRIMARY KEY')).toEqual({result: {name: identifier('id', 0), type: {name: identifier('int', 3), token: token(3, 5)}, constraints: [ + kind('PrimaryKey', 7, 17) + ]}}) + }) + test('unique', () => { + expect(parseRule(p => p.tableColumnRule(), "email varchar UNIQUE")).toEqual({result: {name: identifier('email', 0), type: {name: identifier('varchar', 6), token: token(6, 12)}, constraints: [ + kind('Unique', 14) + ]}}) + }) + test('check', () => { + expect(parseRule(p => p.tableColumnRule(), "email varchar CHECK (email LIKE '%@%')")).toEqual({result: {name: identifier('email', 0), type: {name: identifier('varchar', 6), token: token(6, 12)}, constraints: [ + {...kind('Check', 14), predicate: operation(column('email', 21), op('Like', 27), string('%@%', 32))} + ]}}) + }) + test('foreignKey', () => { + expect(parseRule(p => p.tableColumnRule(), "author uuid REFERENCES users(id) ON DELETE SET NULL (id)")).toEqual({result: {name: identifier('author', 0), type: {name: identifier('uuid', 7), token: token(7, 10)}, constraints: [{ + ...kind('ForeignKey', 12, 21), + table: identifier('users', 23), + column: identifier('id', 29), + onDelete: {token: token(33, 41), action: kind('SetNull', 43, 50), columns: [identifier('id', 53)]} + }]}}) + }) + test('full', () => { + expect(parseRule(p => p.tableColumnRule(), "email varchar " + + "CONSTRAINT users_email_nn NOT NULL " + + "CONSTRAINT users_email_def DEFAULT 'anon@mail.com' " + + "CONSTRAINT users_pk PRIMARY KEY " + + "CONSTRAINT users_email_uniq UNIQUE " + + "CONSTRAINT users_email_chk CHECK (email LIKE '%@%') " + + "CONSTRAINT users_email_fk REFERENCES public.emails(id)")).toEqual({result: { + name: identifier('email', 0), + type: {name: identifier('varchar', 6), token: token(6, 12)}, + constraints: [ + {constraint: {token: token(14, 23), name: identifier('users_email_nn', 25)}, ...kind('Nullable', 40, 47), value: false}, + {constraint: {token: token(49, 58), name: identifier('users_email_def', 60)}, ...kind('Default', 76), expression: string('anon@mail.com', 84)}, + {constraint: {token: token(100, 109), name: identifier('users_pk', 111)}, ...kind('PrimaryKey', 120, 130)}, + {constraint: {token: token(132, 141), name: identifier('users_email_uniq', 143)}, ...kind('Unique', 160, 165)}, + {constraint: {token: token(167, 176), name: identifier('users_email_chk', 178)}, ...kind('Check', 194), predicate: operation(column('email', 201), op('Like', 207), string('%@%', 212))}, + {constraint: {token: token(219, 228), name: identifier('users_email_fk', 230)}, ...kind('ForeignKey', 245, 254), schema: identifier('public', 256), table: identifier('emails', 263), column: identifier('id', 270)}, + ] + }}) + }) + }) + describe('tableConstraintRule', () => { + test('primaryKey', () => { + expect(parseRule(p => p.tableConstraintRule(), 'PRIMARY KEY (id)')).toEqual({result: {...kind('PrimaryKey', 0, 10), columns: [identifier('id', 13)]}}) + }) + test('unique', () => { + expect(parseRule(p => p.tableConstraintRule(), 'UNIQUE (first_name, last_name)')).toEqual({result: {...kind('Unique', 0), columns: [identifier('first_name', 8), identifier('last_name', 20)]}}) + }) + // check is the same as the column + test('foreignKey', () => { + expect(parseRule(p => p.tableConstraintRule(), "FOREIGN KEY (author) REFERENCES users(id) ON DELETE SET NULL (author)")).toEqual({result: { + ...kind('ForeignKey', 0, 10), + columns: [identifier('author', 13)], + ref: { + token: token(21, 30), + table: identifier('users', 32), + columns: [identifier('id', 38)], + }, + onDelete: {token: token(42, 50), action: kind('SetNull', 52, 59), columns: [identifier('author', 62)]} + }}) + }) + }) + }) + describe('basic parts', () => { + describe('expressionRule', () => { + test('literal', () => { + expect(parseRule(p => p.expressionRule(), "'str'")).toEqual({result: string('str', 0)}) + expect(parseRule(p => p.expressionRule(), '1')).toEqual({result: integer(1, 0)}) + expect(parseRule(p => p.expressionRule(), '1.2')).toEqual({result: decimal(1.2, 0)}) + expect(parseRule(p => p.expressionRule(), 'true')).toEqual({result: boolean(true, 0)}) + expect(parseRule(p => p.expressionRule(), 'null')).toEqual({result: null_(0)}) + }) + test('column', () => { + expect(parseRule(p => p.expressionRule(), 'id')).toEqual({result: column('id', 0)}) + expect(parseRule(p => p.expressionRule(), 'users.id')).toEqual({result: column('id', 0, 'users')}) + expect(parseRule(p => p.expressionRule(), 'public.users.id')).toEqual({result: column('id', 0, 'users', 'public')}) + expect(parseRule(p => p.expressionRule(), "settings->'category'->>'id'")).toEqual({result: {...column('settings', 0), json: [ + {...kind('->', 8), field: string('category', 10)}, + {...kind('->>', 20), field: string('id', 23)}, + ]}}) + }) + test('wildcard', () => { + expect(parseRule(p => p.expressionRule(), '*')).toEqual({result: wildcard(0)}) + expect(parseRule(p => p.expressionRule(), 'users.*')).toEqual({result: {table: identifier('users', 0), ...wildcard(6)}}) + expect(parseRule(p => p.expressionRule(), 'public.users.*')).toEqual({result: {schema: identifier('public', 0), table: identifier('users', 7), ...wildcard(13)}}) + }) + test('function', () => { + expect(parseRule(p => p.expressionRule(), 'max(price)')).toEqual({result: function_('max', 0, [column('price', 4)])}) + expect(parseRule(p => p.expressionRule(), "pg_catalog.set_config('search_path', '', false)")) + .toEqual({result: function_('set_config', 0, [string('search_path', 22), string('', 37), boolean(false, 41)], 'pg_catalog')}) + }) + test('parameter', () => { + expect(parseRule(p => p.expressionRule(), '?')).toEqual({result: parameter(0, 0)}) + expect(parseRule(p => p.expressionRule(), '$1')).toEqual({result: parameter(1, 0)}) + }) + test('group', () => { + expect(parseRule(p => p.expressionRule(), '(1)')).toEqual({result: group(integer(1, 1))}) + }) + test('operation', () => { + expect(parseRule(p => p.expressionRule(), '1 + 1')).toEqual({result: operation(integer(1, 0), op('+', 2), integer(1, 4))}) + expect(parseRule(p => p.expressionRule(), '1 - 1')).toEqual({result: operation(integer(1, 0), op('-', 2), integer(1, 4))}) + expect(parseRule(p => p.expressionRule(), '1 * 1')).toEqual({result: operation(integer(1, 0), op('*', 2), integer(1, 4))}) + expect(parseRule(p => p.expressionRule(), '1 / 1')).toEqual({result: operation(integer(1, 0), op('/', 2), integer(1, 4))}) + expect(parseRule(p => p.expressionRule(), '1 % 1')).toEqual({result: operation(integer(1, 0), op('%', 2), integer(1, 4))}) + expect(parseRule(p => p.expressionRule(), '1 ^ 1')).toEqual({result: operation(integer(1, 0), op('^', 2), integer(1, 4))}) + expect(parseRule(p => p.expressionRule(), '1 & 1')).toEqual({result: operation(integer(1, 0), op('&', 2), integer(1, 4))}) + expect(parseRule(p => p.expressionRule(), '1 | 1')).toEqual({result: operation(integer(1, 0), op('|', 2), integer(1, 4))}) + expect(parseRule(p => p.expressionRule(), '1 # 1')).toEqual({result: operation(integer(1, 0), op('#', 2), integer(1, 4))}) + expect(parseRule(p => p.expressionRule(), '1 << 1')).toEqual({result: operation(integer(1, 0), op('<<', 2), integer(1, 5))}) + expect(parseRule(p => p.expressionRule(), '1 >> 1')).toEqual({result: operation(integer(1, 0), op('>>', 2), integer(1, 5))}) + expect(parseRule(p => p.expressionRule(), 'id = 1')).toEqual({result: operation(column('id', 0), op('=', 3), integer(1, 5))}) + expect(parseRule(p => p.expressionRule(), 'id < 1')).toEqual({result: operation(column('id', 0), op('<', 3), integer(1, 5))}) + expect(parseRule(p => p.expressionRule(), 'id > 1')).toEqual({result: operation(column('id', 0), op('>', 3), integer(1, 5))}) + expect(parseRule(p => p.expressionRule(), 'id <= 1')).toEqual({result: operation(column('id', 0), op('<=', 3), integer(1, 6))}) + expect(parseRule(p => p.expressionRule(), 'id >= 1')).toEqual({result: operation(column('id', 0), op('>=', 3), integer(1, 6))}) + expect(parseRule(p => p.expressionRule(), 'id <> 1')).toEqual({result: operation(column('id', 0), op('<>', 3), integer(1, 6))}) + expect(parseRule(p => p.expressionRule(), 'id != 1')).toEqual({result: operation(column('id', 0), op('!=', 3), integer(1, 6))}) + expect(parseRule(p => p.expressionRule(), "'a' || 'b'")).toEqual({result: operation(string('a', 0), op('||', 4), string('b', 7))}) + expect(parseRule(p => p.expressionRule(), "'a' ~ 'b'")).toEqual({result: operation(string('a', 0), op('~', 4), string('b', 6))}) + expect(parseRule(p => p.expressionRule(), "'a' ~* 'b'")).toEqual({result: operation(string('a', 0), op('~*', 4), string('b', 7))}) + expect(parseRule(p => p.expressionRule(), "'a' !~ 'b'")).toEqual({result: operation(string('a', 0), op('!~', 4), string('b', 7))}) + expect(parseRule(p => p.expressionRule(), "'a' !~* 'b'")).toEqual({result: operation(string('a', 0), op('!~*', 4), string('b', 8))}) + expect(parseRule(p => p.expressionRule(), "name IS NULL")).toEqual({result: operation(column('name', 0), op('Is', 5), null_(8))}) + expect(parseRule(p => p.expressionRule(), "name LIKE 'a_%'")).toEqual({result: operation(column('name', 0), op('Like', 5), string('a_%', 10))}) + expect(parseRule(p => p.expressionRule(), "name NOT LIKE 'a_%'")).toEqual({result: operation(column('name', 0), op('NotLike', 5, 12), string('a_%', 14))}) + expect(parseRule(p => p.expressionRule(), "old.name IS DISTINCT FROM new.name")).toEqual({result: operation(column('name', 0, 'old'), op('DistinctFrom', 9, 24), column('name', 26, 'new'))}) + expect(parseRule(p => p.expressionRule(), "old.name IS NOT DISTINCT FROM new.name")).toEqual({result: operation(column('name', 0, 'old'), op('NotDistinctFrom', 9, 28), column('name', 30, 'new'))}) + expect(parseRule(p => p.expressionRule(), "role IN ('author', 'editor')")).toEqual({result: operation(column('role', 0), op('In', 5), list([string('author', 9), string('editor', 19)]))}) + expect(parseRule(p => p.expressionRule(), "role NOT IN ('author', 'editor')")).toEqual({result: operation(column('role', 0), op('NotIn', 5, 10), list([string('author', 13), string('editor', 23)]))}) + // TODO: expect(parseRule(p => p.expressionRule(), "role IN (SELECT id FROM roles)")).toEqual({result: operation(column('role', 0), op('In', 5), list([string('author', 9), string('editor', 19)]))}) + expect(parseRule(p => p.expressionRule(), 'true OR true')).toEqual({result: operation(boolean(true, 0), op('Or', 5), boolean(true, 8))}) + expect(parseRule(p => p.expressionRule(), 'true AND true')).toEqual({result: operation(boolean(true, 0), op('And', 5), boolean(true, 9))}) + // TODO: and many more... ^^ + }) + test('left operation', () => { + expect(parseRule(p => p.expressionRule(), 'NOT NULL')).toEqual({result: operationLeft(opLeft('Not', 0), null_(4))}) + expect(parseRule(p => p.expressionRule(), '~1')).toEqual({result: operationLeft(opLeft('~', 0), integer(1, 1))}) + }) + test('right operation', () => { + expect(parseRule(p => p.expressionRule(), 'id ISNULL')).toEqual({result: operationRight(column('id', 0), opRight('IsNull', 3))}) + expect(parseRule(p => p.expressionRule(), 'id NOTNULL')).toEqual({result: operationRight(column('id', 0), opRight('NotNull', 3))}) + }) + test('mixed operation', () => { + expect(parseRule(p => p.expressionRule(), 'name IS NOT NULL')).toEqual({result: operation(column('name', 0), op('IsNot', 5, 10), null_(12))}) + }) + test('cast', () => { + expect(parseRule(p => p.expressionRule(), "'owner'::character varying")) + .toEqual({result: {...string('owner', 0), cast: {token: token(7, 8), type: {name: identifier('character varying', 9), token: token(9, 25)}}}}) + }) + test('complex', () => { + const i = (value: string) => ({kind: 'Identifier', value}) + const n = (value: number) => ({kind: 'Integer', value}) + const c = (column: string, table?: string) => removeUndefined({kind: 'Column', table: table ? i(table) : undefined, column: i(column)}) + const f = (name: string, parameters: any[]) => ({kind: 'Function', function: i(name), parameters}) + const p = (value: string) => ({kind: 'Parameter', value, index: value === '?' ? undefined : parseInt(value.slice(1))}) + const o = (left: any, kind: string, right: any) => ({kind: 'Operation', left, op: {kind}, right}) + const u = (kind: string, right: any) => ({kind: 'OperationLeft', op: {kind}, right}) + const g = (expression: any) => ({kind: 'Group', expression}) + const l = (items: any) => ({kind: 'List', items}) + const parse = (input: string) => removeTokens(parseRule(p => p.expressionRule(), input)) + expect(parse('id')).toEqual({result: c('id')}) + expect(parse('id = 0')).toEqual({result: o(c('id'), '=', n(0))}) + expect(parse('id = 0 OR id = ?')).toEqual({result: o(o(c('id'), '=', n(0)), 'Or', o(c('id'), '=', p('?')))}) + expect(parse('(id = 0) OR (id = ?)')).toEqual({result: o(g(o(c('id'), '=', n(0))), 'Or', g(o(c('id'), '=', p('?'))))}) + expect(parse('type IN ($2, $3) AND name NOT IN ($4, $5)')).toEqual({result: o(o(c('type'), 'In', l([p('$2'), p('$3')])), 'And', o(c('name'), 'NotIn', l([p('$4'), p('$5')])))}) + expect(parse('split_part(details ->> $1, $2, $3)')).toEqual({result: f('split_part', [{...c('details'), json: [{kind: '->>', field: p('$1')}]}, p('$2'), p('$3')])}) + expect(parse('$1 || queryid')).toEqual({result: o(p('$1'), '||', c('queryid'))}) + expect(parse('$1 || queryid ~ $2')).toEqual({result: o(o(p('$1'), '||', c('queryid')), '~', p('$2'))}) + expect(parse('count(distinct to_char(e.created_at, $2))')).toEqual({result: {...f('count', [f('to_char', [c('created_at', 'e'), p('$2')])]), distinct: {}}}) + expect(parse('e.created_at > NOW() - INTERVAL $3')).toEqual({result: o(o(c('created_at', 'e'), '>', f('NOW', [])), '-', u('Interval', p('$3')))}) + }) + }) + describe('objectNameRule', () => { + test('object only', () => { + expect(parseRule(p => p.objectNameRule(), 'users')).toEqual({result: {name: identifier('users', 0)}}) + }) + test('object and schema', () => { + expect(parseRule(p => p.objectNameRule(), 'public.users')).toEqual({result: {schema: identifier('public', 0), name: identifier('users', 7)}}) + }) + }) + describe('columnTypeRule', () => { + test('simple', () => { + expect(parseRule(p => p.columnTypeRule(), 'int')).toEqual({result: {name: identifier('int', 0), token: token(0, 2)}}) + }) + test('with space', () => { + expect(parseRule(p => p.columnTypeRule(), 'character varying')).toEqual({result: {name: identifier('character varying', 0), token: token(0, 16)}}) + }) + test('with args', () => { + expect(parseRule(p => p.columnTypeRule(), 'character(255)')).toEqual({result: {name: identifier('character(255)', 0), args: [integer(255, 10)], token: token(0, 13)}}) + expect(parseRule(p => p.columnTypeRule(), 'NUMERIC(2, -3)')).toEqual({result: {name: identifier('NUMERIC(2, -3)', 0), args: [integer(2, 8), integer(-3, 11)], token: token(0, 13)}}) + }) + test('array', () => { + expect(parseRule(p => p.columnTypeRule(), 'int[]')).toEqual({result: {name: identifier('int[]', 0), array: {token: token(3, 4)}, token: token(0, 4)}}) + }) + test('with time zone', () => { + expect(parseRule(p => p.columnTypeRule(), 'timestamp with time zone')).toEqual({result: {name: identifier('timestamp with time zone', 0), token: token(0, 23)}}) + expect(parseRule(p => p.columnTypeRule(), 'timestamp without time zone')).toEqual({result: {name: identifier('timestamp without time zone', 0), token: token(0, 26)}}) + }) + test('with time zone and args', () => { + expect(parseRule(p => p.columnTypeRule(), 'timestamp(0) without time zone')) + .toEqual({result: {name: identifier('timestamp(0) without time zone', 0), args: [integer(0, 10)], token: token(0, 29)}}) + }) + test('with schema', () => { + expect(parseRule(p => p.columnTypeRule(), 'public.citext')).toEqual({result: {schema: identifier('public', 0), name: identifier('citext', 7), token: token(0, 12)}}) + }) + // TODO: intervals + }) + describe('literalRule', () => { + test('string', () => { + expect(parseRule(p => p.literalRule(), "'id'")).toEqual({result: {...kind('String', 0, 3), value: 'id'}}) + }) + test('decimal', () => { + expect(parseRule(p => p.literalRule(), '3.14')).toEqual({result: {...kind('Decimal', 0, 3), value: 3.14}}) + expect(parseRule(p => p.literalRule(), '-3.14')).toEqual({result: {...kind('Decimal', 0, 4), value: -3.14}}) + }) + test('integer', () => { + expect(parseRule(p => p.literalRule(), '3')).toEqual({result: {...kind('Integer', 0, 0), value: 3}}) + expect(parseRule(p => p.literalRule(), '-3')).toEqual({result: {...kind('Integer', 0, 1), value: -3}}) + }) + test('boolean', () => { + expect(parseRule(p => p.literalRule(), 'true')).toEqual({result: {...kind('Boolean', 0, 3), value: true}}) + }) + }) + }) + describe('elements', () => { + describe('parameterRule', () => { + test('anonymous', () => { + expect(parseRule(p => p.parameterRule(), '?')).toEqual({result: {...kind('Parameter', 0, 0), value: '?'}}) + }) + test('indexed', () => { + expect(parseRule(p => p.parameterRule(), '$1')).toEqual({result: {...kind('Parameter', 0, 1), value: '$1', index: 1}}) + }) + }) + describe('identifierRule', () => { + test('basic', () => { + expect(parseRule(p => p.identifierRule(), 'id')).toEqual({result: identifier('id', 0)}) + }) + test('quoted', () => { + expect(parseRule(p => p.identifierRule(), '"an id"')).toEqual({result: {...identifier('an id', 0, 6), quoted: true}}) + }) + test('with quote', () => { + expect(parseRule(p => p.identifierRule(), '"an id with \\""')).toEqual({result: {...identifier('an id with "', 0, 14), quoted: true}}) + }) + test('not empty', () => { + const specials = ['Add', 'Comment', 'Commit', 'Data', 'Database', 'Deferrable', 'Domain', 'Extension', 'Increment', 'Index', 'Input', 'New', 'Nulls', 'Old', 'Rows', 'Schema', 'Session', 'Start', 'Temporary', 'Trigger', 'Type', 'Version'] + expect(parseRule(p => p.identifierRule(), '""')).toEqual({errors: [ + {kind: 'LexingError', level: 'error', message: 'unexpected character: ->"<- at offset: 0, skipped 2 characters.', ...token(0, 2)}, + {kind: 'NoViableAltException', level: 'error', message: `Expecting: one of these possible Token sequences:\n 1. [Identifier]${specials.map((n, i) => `\n ${i + 2}. [${n}]`).join('')}\nbut found: ''`, offset: {start: -1, end: -1}, position: {start: {line: -1, column: -1}, end: {line: -1, column: -1}}} + ]}) + }) + test('special', () => { + expect(parseRule(p => p.identifierRule(), 'database')).toEqual({result: identifier('database', 0)}) + expect(parseRule(p => p.identifierRule(), 'index')).toEqual({result: identifier('index', 0)}) + expect(parseRule(p => p.identifierRule(), 'nulls')).toEqual({result: identifier('nulls', 0)}) + expect(parseRule(p => p.identifierRule(), 'rows')).toEqual({result: identifier('rows', 0)}) + expect(parseRule(p => p.identifierRule(), 'schema')).toEqual({result: identifier('schema', 0)}) + expect(parseRule(p => p.identifierRule(), 'type')).toEqual({result: identifier('type', 0)}) + expect(parseRule(p => p.identifierRule(), 'version')).toEqual({result: identifier('version', 0)}) + }) + }) + describe('stringRule', () => { + test('basic', () => { + expect(parseRule(p => p.stringRule(), "'value'")).toEqual({result: string('value', 0)}) + }) + test('empty', () => { + expect(parseRule(p => p.stringRule(), "''")).toEqual({result: string('', 0)}) + }) + test('escape quote', () => { + expect(parseRule(p => p.stringRule(), "'l''id'")).toEqual({result: string("l'id", 0, 6)}) + }) + test('escape quote start & end', () => { + expect(parseRule(p => p.stringRule(), "'''id'''")).toEqual({result: string("'id'", 0, 7)}) + }) + test('escape quote quote', () => { + expect(parseRule(p => p.stringRule(), "''''")).toEqual({result: string("'", 0, 3)}) + }) + test('escaped', () => { + expect(parseRule(p => p.stringRule(), "E'value\\nmulti\\nline'")).toEqual({result: {...string('value\\nmulti\\nline', 0, 20), escaped: true}}) + }) + test('dollar', () => { + expect(parseRule(p => p.stringRule(), "$tag$\nmulti\nline\nvalue\n$tag$")).toEqual({result: {value: 'multi\nline\nvalue', dollar: '$tag$', kind: 'String', token: {offset: {start: 0, end: 27}, position: {start: {line: 1, column: 1}, end: {line: 5, column: 5}}}}}) + }) + }) + describe('integerRule', () => { + test('0', () => { + expect(parseRule(p => p.integerRule(), '0')).toEqual({result: integer(0, 0)}) + }) + test('number', () => { + expect(parseRule(p => p.integerRule(), '12')).toEqual({result: integer(12, 0)}) + }) + }) + describe('decimalRule', () => { + test('0', () => { + expect(parseRule(p => p.decimalRule(), '0.0')).toEqual({result: decimal(0, 0, 2)}) + }) + test('number', () => { + expect(parseRule(p => p.decimalRule(), '3.14')).toEqual({result: decimal(3.14, 0)}) + }) + }) + describe('booleanRule', () => { + test('true', () => { + expect(parseRule(p => p.booleanRule(), 'true')).toEqual({result: boolean(true, 0)}) + }) + test('false', () => { + expect(parseRule(p => p.booleanRule(), 'false')).toEqual({result: boolean(false, 0)}) + }) + }) + }) +}) + +function op(kind: Operator, start: number, end?: number): OperatorAst { + return {kind, token: token(start, end === undefined ? start + kind.length - 1 : end)} +} + +function operation(left: ExpressionAst, op: OperatorAst, right: ExpressionAst): OperationAst { + return {kind: 'Operation', left, op, right} +} + +function opLeft(kind: OperatorLeft, start: number, end?: number): OperatorLeftAst { + return {kind, token: token(start, end === undefined ? start + kind.length - 1 : end)} +} + +function operationLeft(op: OperatorLeftAst, right: ExpressionAst): OperationLeftAst { + return {kind: 'OperationLeft', op, right} +} + +function opRight(kind: OperatorRight, start: number, end?: number): OperatorRightAst { + return {kind, token: token(start, end === undefined ? start + kind.length - 1 : end)} +} + +function operationRight(left: ExpressionAst, op: OperatorRightAst): OperationRightAst { + return {kind: 'OperationRight', left, op} +} + +function group(expression: ExpressionAst): GroupAst { + return {kind: 'Group', expression} +} + +function wildcard(start: number, table?: string, schema?: string): WildcardAst { + if (schema && table) { + const offset = start + schema.length + table.length + 2 + return {kind: 'Wildcard', token: token(offset, offset), schema: identifier(schema, start), table: identifier(table, start + schema.length + 1)} + } else if (table) { + const offset = start + table.length + 1 + return {kind: 'Wildcard', token: token(offset, offset), table: identifier(table, start)} + } else { + return {kind: 'Wildcard', token: token(start, start)} + } +} + +function column(name: string, start: number, table?: string, schema?: string): ColumnAst { + if (schema && table) { + return {kind: 'Column', schema: identifier(schema, start), table: identifier(table, start + schema.length + 1), column: identifier(name, start + schema.length + table.length + 2)} + } else if (table) { + return {kind: 'Column', table: identifier(table, start), column: identifier(name, start + table.length + 1)} + } else { + return {kind: 'Column', column: identifier(name, start)} + } +} + +function function_(name: string, start: number, parameters: ExpressionAst[], schema?: string): FunctionAst { + if (schema) { + return {kind: 'Function', schema: identifier(schema, start), function: identifier(name, start + schema.length + 1), parameters} + } else { + return {kind: 'Function', function: identifier(name, start), parameters} + } +} + +function alias(name: string, start: number, tokenStart?: number): AliasAst { + return tokenStart ? {token: token(tokenStart, tokenStart + 1), name: identifier(name, start)} : {name: identifier(name, start)} +} + +function identifier(value: string, start: number, end?: number): IdentifierAst { // needs `end` for quoted identifiers: `"id"` + return {kind: 'Identifier', value, token: token(start, end === undefined ? start + value.length - 1 : end)} +} + +function string(value: string, start: number, end?: number): StringAst { // needs `end` for escaped strings: `E'str'` + return {kind: 'String', value, token: token(start, end === undefined ? start + value.length + 1 : end)} +} + +function integer(value: number, start: number): IntegerAst { + return {kind: 'Integer', value, token: token(start, start + value.toString().length - 1)} +} + +function decimal(value: number, start: number, end?: number): DecimalAst { // needs `end` for 0 decimal: `0.0` + return {kind: 'Decimal', value, token: token(start, end == undefined ? start + value.toString().length - 1 : end)} +} + +function boolean(value: boolean, start: number): BooleanAst { + return {kind: 'Boolean', value, token: token(start, start + value.toString().length - 1)} +} + +function null_(start: number): NullAst { + return {kind: 'Null', token: token(start, start + 3)} +} + +function parameter(index: number, start: number): ParameterAst { + return {kind: 'Parameter', value: index ? `$${index}` : '?', index: index ? index : undefined, token: token(start, index ? start + index.toString().length : start)} +} + +function list(items: LiteralAst[]): ListAst { + return {kind: 'List', items} +} + +function stmt(kind: string, start: number, tokenEnd: number, statementEnd: number): {kind: string, meta: TokenInfo, token: TokenInfo} { + return {kind, meta: token(start, statementEnd), token: token(start, tokenEnd)} +} + +function kind(kind: string, start: number, end?: number): {kind: string, token: TokenInfo} { + return {kind, token: token(start, end === undefined ? start + kind.length - 1 : end)} +} + +function token(start: number, end: number, issues?: TokenIssue[]): TokenInfo { + return removeEmpty({offset: {start, end}, position: {start: {line: 1, column: start + 1}, end: {line: 1, column: end + 1}}, issues}) +} + +function removeTokens(ast: T): T { + return removeFieldsDeep(ast, ['meta', 'token']) +} diff --git a/libs/parser-sql/src/postgresParser.ts b/libs/parser-sql/src/postgresParser.ts new file mode 100644 index 000000000..755eed313 --- /dev/null +++ b/libs/parser-sql/src/postgresParser.ts @@ -0,0 +1,1936 @@ +import { + createToken, + EmbeddedActionsParser, + ILexingError, + IRecognitionException, + IToken, + Lexer, + TokenType +} from "chevrotain"; +import {isNotUndefined, removeEmpty, removeUndefined} from "@azimutt/utils"; +import {mergePositions, ParserError, ParserErrorLevel, ParserResult, TokenPosition} from "@azimutt/models"; +import { + AliasAst, + AlterSchemaStatementAst, + AlterSequenceStatementAst, + AlterTableStatementAst, + BeginStatementAst, + BooleanAst, + ColumnJsonAst, + ColumnTypeAst, + ColumnUpdateAst, + CommentAst, + CommentOnStatementAst, + CommitStatementAst, + CreateExtensionStatementAst, + CreateFunctionStatementAst, + CreateIndexStatementAst, + CreateMaterializedViewStatementAst, + CreateSchemaStatementAst, + CreateSequenceStatementAst, + CreateTableStatementAst, + CreateTriggerStatementAst, + CreateTypeStatementAst, + CreateViewStatementAst, + DecimalAst, + DeleteStatementAst, + DropStatementAst, + ExpressionAst, + FetchClauseAst, + ForeignKeyActionAst, + FromClauseAst, + FromClauseItemAst, + FromClauseJoinAst, + FromClauseQueryAst, + FromClauseTableAst, + FunctionArgumentAst, + FunctionReturnsAst, + GroupAst, + GroupByClauseAst, + HavingClauseAst, + IdentifierAst, + IndexColumnAst, + InsertIntoStatementAst, + IntegerAst, + JsonOp, + LimitClauseAst, + ListAst, + LiteralAst, + NameAst, + NullAst, + ObjectNameAst, + OffsetClauseAst, + OnConflictClauseAst, + OperatorAst, + OperatorLeftAst, + OperatorRightAst, + OrderByClauseAst, + OwnerAst, + ParameterAst, + PostgresAst, + PostgresStatementAst, + SelectClauseAst, + SelectClauseColumnAst, + SelectInnerAst, + SelectMainAst, + SelectResultAst, + SelectStatementAst, + SequenceOwnedByAst, + SequenceParamAst, + SequenceParamOptAst, + SequenceTypeAst, + SetStatementAst, + ShowStatementAst, + SortNullsAst, + SortOrderAst, + StatementAst, + StatementsAst, + StringAst, + TableAlterActionAst, + TableColumnAst, + TableColumnCheckAst, + TableColumnConstraintAst, + TableColumnDefaultAst, + TableColumnFkAst, + TableColumnNullableAst, + TableColumnPkAst, + TableColumnUniqueAst, + TableConstraintAst, + TableFkAst, + TablePkAst, + TableUniqueAst, + TokenInfo, + TokenIssue, + TransactionModeAst, + TriggerDeferrableAst, + TriggerEventAst, + TriggerReferencingAst, + TypeColumnAst, + UnionClauseAst, + UpdateStatementAst, + WhereClauseAst, + WindowClauseAst, + WindowClauseContentAst +} from "./postgresAst"; + +const LineComment = createToken({name: 'LineComment', pattern: /--.*/, group: 'comments'}) +const BlockComment = createToken({name: 'BlockComment', pattern: /\/\*[\s\S]*?\*\//, line_breaks: true, group: 'comments'}) +const WhiteSpace = createToken({name: 'WhiteSpace', pattern: /\s+/, group: Lexer.SKIPPED}) + +const Identifier = createToken({name: 'Identifier', pattern: /\b[a-zA-Z_]\w*\b|"([^\\"]|\\\\|\\")+"/}) +const String = createToken({name: 'String', pattern: /E?'([^']|'')*'/i}) +const StringDollar = createToken({name: 'StringDollar', pattern: /\$(\w*)\$[\s\S]*?\$\1\$/i}) +const Decimal = createToken({name: 'Decimal', pattern: /\d+\.\d+/}) +const Integer = createToken({name: 'Integer', pattern: /0|[1-9]\d*/, longer_alt: Decimal}) +const valueTokens: TokenType[] = [Integer, Decimal, String, StringDollar, Identifier, LineComment, BlockComment] + +const Add = createToken({name: 'Add', pattern: /\bADD\b/i, longer_alt: Identifier}) +const After = createToken({name: 'After', pattern: /\bAFTER\b/i, longer_alt: Identifier}) +const All = createToken({name: 'All', pattern: /\bALL\b/i, longer_alt: Identifier}) +const Alter = createToken({name: 'Alter', pattern: /\bALTER\b/i, longer_alt: Identifier}) +const And = createToken({name: 'And', pattern: /\bAND\b/i, longer_alt: Identifier}) +const Array = createToken({name: 'Array', pattern: /\bARRAY\b/i, longer_alt: Identifier}) +const As = createToken({name: 'As', pattern: /\bAS\b/i, longer_alt: Identifier}) +const Asc = createToken({name: 'Asc', pattern: /\bASC\b/i, longer_alt: Identifier}) +const Authorization = createToken({name: 'Authorization', pattern: /\bAUTHORIZATION\b/i, longer_alt: Identifier}) +const Before = createToken({name: 'Before', pattern: /\bBEFORE\b/i, longer_alt: Identifier}) +const Begin = createToken({name: 'Begin', pattern: /\bBEGIN\b/i, longer_alt: Identifier}) +const By = createToken({name: 'By', pattern: /\bBY\b/i, longer_alt: Identifier}) +const Cache = createToken({name: 'Cache', pattern: /\bCACHE\b/i, longer_alt: Identifier}) +const Called = createToken({name: 'Called', pattern: /\bCALLED\b/i, longer_alt: Identifier}) +const Cascade = createToken({name: 'Cascade', pattern: /\bCASCADE\b/i, longer_alt: Identifier}) +const Chain = createToken({name: 'Chain', pattern: /\bCHAIN\b/i, longer_alt: Identifier}) +const Check = createToken({name: 'Check', pattern: /\bCHECK\b/i, longer_alt: Identifier}) +const Collate = createToken({name: 'Collate', pattern: /\bCOLLATE\b/i, longer_alt: Identifier}) +const Column = createToken({name: 'Column', pattern: /\bCOLUMN\b/i, longer_alt: Identifier}) +const Comment = createToken({name: 'Comment', pattern: /\bCOMMENT\b/i, longer_alt: Identifier}) +const Commit = createToken({name: 'Commit', pattern: /\bCOMMIT\b/i, longer_alt: Identifier}) +const Concurrently = createToken({name: 'Concurrently', pattern: /\bCONCURRENTLY\b/i, longer_alt: Identifier}) +const Conflict = createToken({name: 'Conflict', pattern: /\bCONFLICT\b/i, longer_alt: Identifier}) +const Constraint = createToken({name: 'Constraint', pattern: /\bCONSTRAINT\b/i, longer_alt: Identifier}) +const Create = createToken({name: 'Create', pattern: /\bCREATE\b/i, longer_alt: Identifier}) +const Cross = createToken({name: 'Cross', pattern: /\bCROSS\b/i, longer_alt: Identifier}) +const CurrentRole = createToken({name: 'CurrentRole', pattern: /\bCURRENT_ROLE\b/i, longer_alt: Identifier}) +const CurrentUser = createToken({name: 'CurrentUser', pattern: /\bCURRENT_USER\b/i, longer_alt: Identifier}) +const Cycle = createToken({name: 'Cycle', pattern: /\bCYCLE\b/i, longer_alt: Identifier}) +const Data = createToken({name: 'Data', pattern: /\bDATA\b/i, longer_alt: Identifier}) +const Database = createToken({name: 'Database', pattern: /\bDATABASE\b/i, longer_alt: Identifier}) +const Default = createToken({name: 'Default', pattern: /\bDEFAULT\b/i, longer_alt: Identifier}) +const Deferrable = createToken({name: 'Deferrable', pattern: /\bDEFERRABLE\b/i, longer_alt: Identifier}) +const Deferred = createToken({name: 'Deferred', pattern: /\bDEFERRED\b/i, longer_alt: Identifier}) +const Delete = createToken({name: 'Delete', pattern: /\bDELETE\b/i, longer_alt: Identifier}) +const Desc = createToken({name: 'Desc', pattern: /\bDESC\b/i, longer_alt: Identifier}) +const Distinct = createToken({name: 'Distinct', pattern: /\bDISTINCT\b/i, longer_alt: Identifier}) +const Do = createToken({name: 'Do', pattern: /\bDO\b/i, longer_alt: Identifier}) +const Domain = createToken({name: 'Domain', pattern: /\bDOMAIN\b/i, longer_alt: Identifier}) +const Drop = createToken({name: 'Drop', pattern: /\bDROP\b/i, longer_alt: Identifier}) +const Each = createToken({name: 'Each', pattern: /\bEACH\b/i, longer_alt: Identifier}) +const Enum = createToken({name: 'Enum', pattern: /\bENUM\b/i, longer_alt: Identifier}) +const Except = createToken({name: 'Except', pattern: /\bEXCEPT\b/i, longer_alt: Identifier}) +const Execute = createToken({name: 'Execute', pattern: /\bEXECUTE\b/i, longer_alt: Identifier}) +const Exists = createToken({name: 'Exists', pattern: /\bEXISTS\b/i, longer_alt: Identifier}) +const Extension = createToken({name: 'Extension', pattern: /\bEXTENSION\b/i, longer_alt: Identifier}) +const False = createToken({name: 'False', pattern: /\bFALSE\b/i, longer_alt: Identifier}) +const Fetch = createToken({name: 'Fetch', pattern: /\bFETCH\b/i, longer_alt: Identifier}) +const Filter = createToken({name: 'Filter', pattern: /\bFILTER\b/i, longer_alt: Identifier}) +const First = createToken({name: 'First', pattern: /\bFIRST\b/i, longer_alt: Identifier}) +const For = createToken({name: 'For', pattern: /\bFOR\b/i, longer_alt: Identifier}) +const ForeignKey = createToken({name: 'ForeignKey', pattern: /\bFOREIGN\s+KEY\b/i}) +const From = createToken({name: 'From', pattern: /\bFROM\b/i, longer_alt: Identifier}) +const Full = createToken({name: 'Full', pattern: /\bFULL\b/i, longer_alt: Identifier}) +const Function = createToken({name: 'Function', pattern: /\bFUNCTION\b/i, longer_alt: Identifier}) +const Global = createToken({name: 'Global', pattern: /\bGLOBAL\b/i, longer_alt: Identifier}) +const GroupBy = createToken({name: 'GroupBy', pattern: /\bGROUP\s+BY\b/i}) +const Having = createToken({name: 'Having', pattern: /\bHAVING\b/i, longer_alt: Identifier}) +const If = createToken({name: 'If', pattern: /\bIF\b/i, longer_alt: Identifier}) +const Immediate = createToken({name: 'Immediate', pattern: /\bIMMEDIATE\b/i, longer_alt: Identifier}) +const Immutable = createToken({name: 'Immutable', pattern: /\bIMMUTABLE\b/i, longer_alt: Identifier}) +const In = createToken({name: 'In', pattern: /\bIN\b/i, longer_alt: Identifier}) +const Include = createToken({name: 'Include', pattern: /\bINCLUDE\b/i, longer_alt: Identifier}) +const Increment = createToken({name: 'Increment', pattern: /\bINCREMENT\b/i, longer_alt: Identifier}) +const Index = createToken({name: 'Index', pattern: /\bINDEX\b/i, longer_alt: Identifier}) +const Initially = createToken({name: 'Initially', pattern: /\bINITIALLY\b/i, longer_alt: Identifier}) +const Inner = createToken({name: 'Inner', pattern: /\bINNER\b/i, longer_alt: Identifier}) +const InOut = createToken({name: 'InOut', pattern: /\bINOUT\b/i, longer_alt: Identifier}) +const Input = createToken({name: 'Input', pattern: /\bINPUT\b/i, longer_alt: Identifier}) +const Insert = createToken({name: 'Insert', pattern: /\bINSERT\b/i, longer_alt: Identifier}) +const InsteadOf = createToken({name: 'InsteadOf', pattern: /\bINSTEAD\s+OF\b/i}) +const Intersect = createToken({name: 'Intersect', pattern: /\bINTERSECT\b/i, longer_alt: Identifier}) +const Interval = createToken({name: 'Interval', pattern: /\bINTERVAL\b/i, longer_alt: Identifier}) +const Into = createToken({name: 'Into', pattern: /\bINTO\b/i, longer_alt: Identifier}) +const Is = createToken({name: 'Is', pattern: /\bIS\b/i, longer_alt: Identifier}) +const IsNull = createToken({name: 'IsNull', pattern: /\bISNULL\b/i, longer_alt: Identifier}) +const IsolationLevel = createToken({name: 'IsolationLevel', pattern: /\bISOLATION\s+LEVEL\b/i}) +const Join = createToken({name: 'Join', pattern: /\bJOIN\b/i, longer_alt: Identifier}) +const Language = createToken({name: 'Language', pattern: /\bLANGUAGE\b/i, longer_alt: Identifier}) +const Last = createToken({name: 'Last', pattern: /\bLAST\b/i, longer_alt: Identifier}) +const Left = createToken({name: 'Left', pattern: /\bLEFT\b/i, longer_alt: Identifier}) +const Like = createToken({name: 'Like', pattern: /\bLIKE\b/i, longer_alt: Identifier}) +const Limit = createToken({name: 'Limit', pattern: /\bLIMIT\b/i, longer_alt: Identifier}) +const Local = createToken({name: 'Local', pattern: /\bLOCAL\b/i, longer_alt: Identifier}) +const MaterializedView = createToken({name: 'MaterializedView', pattern: /\bMATERIALIZED\s+VIEW\b/i}) +const Maxvalue = createToken({name: 'Maxvalue', pattern: /\bMAXVALUE\b/i, longer_alt: Identifier}) +const Minvalue = createToken({name: 'Minvalue', pattern: /\bMINVALUE\b/i, longer_alt: Identifier}) +const Natural = createToken({name: 'Natural', pattern: /\bNATURAL\b/i, longer_alt: Identifier}) +const New = createToken({name: 'New', pattern: /\bNEW\b/i, longer_alt: Identifier}) +const Next = createToken({name: 'Next', pattern: /\bNEXT\b/i, longer_alt: Identifier}) +const No = createToken({name: 'No', pattern: /\bNO\b/i, longer_alt: Identifier}) +const NoAction = createToken({name: 'NoAction', pattern: /\bNO\s+ACTION\b/i}) +const None = createToken({name: 'None', pattern: /\bNONE\b/i, longer_alt: Identifier}) +const Not = createToken({name: 'Not', pattern: /\bNOT\b/i, longer_alt: Identifier}) +const Nothing = createToken({name: 'Nothing', pattern: /\bNOTHING\b/i, longer_alt: Identifier}) +const NotNull = createToken({name: 'NotNull', pattern: /\bNOTNULL\b/i, longer_alt: Identifier}) +const Null = createToken({name: 'Null', pattern: /\bNULL\b/i, longer_alt: Identifier}) +const Nulls = createToken({name: 'Nulls', pattern: /\bNULLS\b/i, longer_alt: Identifier}) +const Of = createToken({name: 'Of', pattern: /\bOF\b/i, longer_alt: Identifier}) +const Offset = createToken({name: 'Offset', pattern: /\bOFFSET\b/i, longer_alt: Identifier}) +const Old = createToken({name: 'Old', pattern: /\bOLD\b/i, longer_alt: Identifier}) +const On = createToken({name: 'On', pattern: /\bON\b/i, longer_alt: Identifier}) +const Only = createToken({name: 'Only', pattern: /\bONLY\b/i, longer_alt: Identifier}) +const Or = createToken({name: 'Or', pattern: /\bOR\b/i, longer_alt: Identifier}) +const OrderBy = createToken({name: 'OrderBy', pattern: /\bORDER\s+BY\b/i}) +const Out = createToken({name: 'Out', pattern: /\bOUT\b/i, longer_alt: Identifier}) +const Outer = createToken({name: 'Outer', pattern: /\bOUTER\b/i, longer_alt: Identifier}) +const Over = createToken({name: 'Over', pattern: /\bOVER\b/i, longer_alt: Identifier}) +const OwnedBy = createToken({name: 'OwnedBy', pattern: /\bOWNED\s+BY\b/i}) +const OwnerTo = createToken({name: 'OwnerTo', pattern: /\bOWNER\s+TO\b/i}) +const PartitionBy = createToken({name: 'PartitionBy', pattern: /\bPARTITION\s+BY\b/i}) +const PrimaryKey = createToken({name: 'PrimaryKey', pattern: /\bPRIMARY\s+KEY\b/i}) +const Procedure = createToken({name: 'Procedure', pattern: /\bPROCEDURE\b/i, longer_alt: Identifier}) +const ReadCommitted = createToken({name: 'ReadCommitted', pattern: /\bREAD\s+COMMITTED\b/i}) +const ReadOnly = createToken({name: 'ReadOnly', pattern: /\bREAD\s+ONLY\b/i}) +const ReadUncommitted = createToken({name: 'ReadUncommitted', pattern: /\bREAD\s+UNCOMMITTED\b/i}) +const ReadWrite = createToken({name: 'ReadWrite', pattern: /\bREAD\s+WRITE\b/i}) +const Recursive = createToken({name: 'Recursive', pattern: /\bRECURSIVE\b/i, longer_alt: Identifier}) +const References = createToken({name: 'References', pattern: /\bREFERENCES\b/i, longer_alt: Identifier}) +const Referencing = createToken({name: 'Referencing', pattern: /\bREFERENCING\b/i, longer_alt: Identifier}) +const RenameTo = createToken({name: 'RenameTo', pattern: /\bRENAME\s+TO\b/i}) +const RepeatableRead = createToken({name: 'RepeatableRead', pattern: /\bREPEATABLE\s+READ\b/i}) +const Replace = createToken({name: 'Replace', pattern: /\bREPLACE\b/i, longer_alt: Identifier}) +const Restrict = createToken({name: 'Restrict', pattern: /\bRESTRICT\b/i, longer_alt: Identifier}) +const Return = createToken({name: 'Return', pattern: /\bRETURN\b/i, longer_alt: Identifier}) +const Returning = createToken({name: 'Returning', pattern: /\bRETURNING\b/i, longer_alt: Identifier}) +const Returns = createToken({name: 'Returns', pattern: /\bRETURNS\b/i, longer_alt: Identifier}) +const Right = createToken({name: 'Right', pattern: /\bRIGHT\b/i, longer_alt: Identifier}) +const Row = createToken({name: 'Row', pattern: /\bROW\b/i, longer_alt: Identifier}) +const Rows = createToken({name: 'Rows', pattern: /\bROWS\b/i, longer_alt: Identifier}) +const Schema = createToken({name: 'Schema', pattern: /\bSCHEMA\b/i, longer_alt: Identifier}) +const Select = createToken({name: 'Select', pattern: /\bSELECT\b/i, longer_alt: Identifier}) +const Sequence = createToken({name: 'Sequence', pattern: /\bSEQUENCE\b/i, longer_alt: Identifier}) +const Serializable = createToken({name: 'Serializable', pattern: /\bSERIALIZABLE\b/i, longer_alt: Identifier}) +const Session = createToken({name: 'Session', pattern: /\bSESSION\b/i, longer_alt: Identifier}) +const SessionUser = createToken({name: 'SessionUser', pattern: /\bSESSION_USER\b/i, longer_alt: Identifier}) +const Set = createToken({name: 'Set', pattern: /\bSET\b/i, longer_alt: Identifier}) +const SetOf = createToken({name: 'SetOf', pattern: /\bSETOF\b/i, longer_alt: Identifier}) +const Show = createToken({name: 'Show', pattern: /\bSHOW\b/i, longer_alt: Identifier}) +const Stable = createToken({name: 'Stable', pattern: /\bSTABLE\b/i, longer_alt: Identifier}) +const Start = createToken({name: 'Start', pattern: /\bSTART\b/i, longer_alt: Identifier}) +const Statement = createToken({name: 'Statement', pattern: /\bSTATEMENT\b/i, longer_alt: Identifier}) +const Strict = createToken({name: 'Strict', pattern: /\bSTRICT\b/i, longer_alt: Identifier}) +const Table = createToken({name: 'Table', pattern: /\bTABLE\b/i, longer_alt: Identifier}) +const Temp = createToken({name: 'Temp', pattern: /\bTEMP\b/i, longer_alt: Identifier}) +const Temporary = createToken({name: 'Temporary', pattern: /\bTEMPORARY\b/i, longer_alt: Identifier}) +const Ties = createToken({name: 'Ties', pattern: /\bTIES\b/i, longer_alt: Identifier}) +const To = createToken({name: 'To', pattern: /\bTO\b/i, longer_alt: Identifier}) +const Transaction = createToken({name: 'Transaction', pattern: /\bTRANSACTION\b/i, longer_alt: Identifier}) +const Trigger = createToken({name: 'Trigger', pattern: /\bTRIGGER\b/i, longer_alt: Identifier}) +const True = createToken({name: 'True', pattern: /\bTRUE\b/i, longer_alt: Identifier}) +const Truncate = createToken({name: 'Truncate', pattern: /\bTRUNCATE\b/i, longer_alt: Identifier}) +const Type = createToken({name: 'Type', pattern: /\bTYPE\b/i, longer_alt: Identifier}) +const Union = createToken({name: 'Union', pattern: /\bUNION\b/i, longer_alt: Identifier}) +const Unique = createToken({name: 'Unique', pattern: /\bUNIQUE\b/i, longer_alt: Identifier}) +const Unlogged = createToken({name: 'Unlogged', pattern: /\bUNLOGGED\b/i, longer_alt: Identifier}) +const Update = createToken({name: 'Update', pattern: /\bUPDATE\b/i, longer_alt: Identifier}) +const Using = createToken({name: 'Using', pattern: /\bUSING\b/i, longer_alt: Identifier}) +const Valid = createToken({name: 'Valid', pattern: /\bVALID\b/i, longer_alt: Identifier}) +const Values = createToken({name: 'Values', pattern: /\bVALUES\b/i, longer_alt: Identifier}) +const Variadic = createToken({name: 'Variadic', pattern: /\bVARIADIC\b/i, longer_alt: Identifier}) +const Version = createToken({name: 'Version', pattern: /\bVERSION\b/i, longer_alt: Identifier}) +const View = createToken({name: 'View', pattern: /\bVIEW\b/i, longer_alt: Identifier}) +const Volatile = createToken({name: 'Volatile', pattern: /\bVOLATILE\b/i, longer_alt: Identifier}) +const When = createToken({name: 'When', pattern: /\bWHEN\b/i, longer_alt: Identifier}) +const Where = createToken({name: 'Where', pattern: /\bWHERE\b/i, longer_alt: Identifier}) +const Window = createToken({name: 'Window', pattern: /\bWINDOW\b/i, longer_alt: Identifier}) +const With = createToken({name: 'With', pattern: /\bWITH\b/i, longer_alt: Identifier}) +const Work = createToken({name: 'Work', pattern: /\bWORK\b/i, longer_alt: Identifier}) +const keywordTokens: TokenType[] = [ + Add, After, All, Alter, And, Array, As, Asc, Authorization, Before, Begin, By, Cache, Called, Cascade, Chain, Check, + Collate, Column, Comment, Commit, Concurrently, Conflict, Constraint, Create, Cross, CurrentRole, CurrentUser, Cycle, + Data, Database, Default, Deferrable, Deferred, Delete, Desc, Distinct, Do, Domain, Drop, Each, Enum, Except, Execute, + Exists, Extension, False, Fetch, Filter, First, For, ForeignKey, From, Full, Function, Global, GroupBy, Having, If, + Immediate, Immutable, In, Include, Increment, Index, Initially, Inner, InOut, Input, Insert, InsteadOf, Intersect, + Interval, Into, Is, IsNull, IsolationLevel, Join, Language, Last, Left, Like, Limit, Local, MaterializedView, Maxvalue, + Minvalue, Natural, New, Next, No, NoAction, None, Not, Nothing, NotNull, Null, Nulls, Of, Offset, Old, On, Only, Or, + OrderBy, Out, Outer, Over, OwnedBy, OwnerTo, PartitionBy, PrimaryKey, Procedure, ReadCommitted, ReadOnly, ReadUncommitted, + ReadWrite, Recursive, References, Referencing, RenameTo, RepeatableRead, Replace, Restrict, Return, Returning, Returns, + Right, Row, Rows, Schema, Select, Sequence, Serializable, Session, SessionUser, Set, SetOf, Show, Stable, Start, + Statement, Strict, Table, Temp, Temporary, Ties, To, Transaction, Trigger, True, Truncate, Type, Union, Unique, + Unlogged, Update, Using, Valid, Values, Variadic, Version, View, Volatile, When, Where, Window, With, Work +] + +const Amp = createToken({name: 'Amp', pattern: /&/}) +const Asterisk = createToken({name: 'Asterisk', pattern: /\*/}) +const BracketLeft = createToken({name: 'BracketLeft', pattern: /\[/}) +const BracketRight = createToken({name: 'BracketRight', pattern: /]/}) +const Caret = createToken({name: 'Caret', pattern: /\^/}) +const Colon = createToken({name: 'Colon', pattern: /:/}) +const Comma = createToken({name: 'Comma', pattern: /,/}) +const CurlyLeft = createToken({name: 'CurlyLeft', pattern: /\{/}) +const CurlyRight = createToken({name: 'CurlyRight', pattern: /}/}) +const Dash = createToken({name: 'Dash', pattern: /-/, longer_alt: LineComment}) +const Dollar = createToken({name: 'Dollar', pattern: /\$/, longer_alt: StringDollar}) +const Dot = createToken({name: 'Dot', pattern: /\./}) +const Equal = createToken({name: 'Equal', pattern: /=/}) +const Exclamation = createToken({name: 'Exclamation', pattern: /!/}) +const GreaterThan = createToken({name: 'GreaterThan', pattern: />/}) +const Hash = createToken({name: 'Hash', pattern: /#/}) +const LowerThan = createToken({name: 'LowerThan', pattern: / StatementsAst + // statements + statementRule: () => StatementAst + alterSchemaStatementRule: () => AlterSchemaStatementAst + alterSequenceStatementRule: () => AlterSequenceStatementAst + alterTableStatementRule: () => AlterTableStatementAst + beginStatementRule: () => BeginStatementAst + commentOnStatementRule: () => CommentOnStatementAst + commitStatementRule: () => CommitStatementAst + createExtensionStatementRule: (create: IToken) => CreateExtensionStatementAst + createFunctionStatementRule: (create: IToken, replace?: { token: TokenInfo }) => CreateFunctionStatementAst + createIndexStatementRule: (create: IToken) => CreateIndexStatementAst + createMaterializedViewStatementRule: (create: IToken) => CreateMaterializedViewStatementAst + createSchemaStatementRule: (create: IToken) => CreateSchemaStatementAst + createSequenceStatementRule: (create: IToken) => CreateSequenceStatementAst + createTableStatementRule: (create: IToken) => CreateTableStatementAst + createTriggerStatementRule: (create: IToken, replace?: { token: TokenInfo }) => CreateTriggerStatementAst + createTypeStatementRule: (create: IToken) => CreateTypeStatementAst + createViewStatementRule: (create: IToken, replace?: { token: TokenInfo }) => CreateViewStatementAst + deleteStatementRule: () => DeleteStatementAst + dropStatementRule: () => DropStatementAst + insertIntoStatementRule: () => InsertIntoStatementAst + selectStatementRule: () => SelectStatementAst + setStatementRule: () => SetStatementAst + showStatementRule: () => ShowStatementAst + updateStatementRule: () => UpdateStatementAst + // clauses + selectClauseRule: () => SelectClauseAst + fromClauseRule: () => FromClauseAst + whereClauseRule: () => WhereClauseAst + tableColumnRule: () => TableColumnAst + tableConstraintRule: () => TableConstraintAst + // basic parts + aliasRule: () => AliasAst + expressionRule: () => ExpressionAst + objectNameRule: () => ObjectNameAst + columnTypeRule: () => ColumnTypeAst + literalRule: () => LiteralAst + // elements + parameterRule: () => ParameterAst + identifierRule: () => IdentifierAst + stringRule: () => StringAst + integerRule: () => IntegerAst + decimalRule: () => DecimalAst + booleanRule: () => BooleanAst + nullRule: () => NullAst + + constructor(tokens: TokenType[], recovery: boolean) { + super(tokens, {recoveryEnabled: recovery}) + const $ = this + + // statements + + this.statementsRule = $.RULE<() => StatementsAst>('statementsRule', () => { + const statements: StatementAst[] = [] + $.MANY(() => { + const stmt = $.SUBRULE($.statementRule) + stmt && statements.push(stmt) // `stmt` can be undefined on invalid input + }) + return {statements} + }) + + this.statementRule = $.RULE<() => StatementAst>('statementRule', () => $.OR([ + {ALT: () => $.SUBRULE($.alterSchemaStatementRule)}, + {ALT: () => $.SUBRULE($.alterSequenceStatementRule)}, + {ALT: () => $.SUBRULE($.alterTableStatementRule)}, + {ALT: () => $.SUBRULE($.beginStatementRule)}, + {ALT: () => $.SUBRULE($.commentOnStatementRule)}, + {ALT: () => $.SUBRULE($.commitStatementRule)}, + {ALT: () => { + const create = $.CONSUME(Create) + const replace = $.OPTION(() => ({token: tokenInfo2($.CONSUME(Or), $.CONSUME(Replace))})) + return $.OR2([ + {ALT: () => $.SUBRULE($.createExtensionStatementRule, {ARGS: [create]})}, + {ALT: () => $.SUBRULE($.createFunctionStatementRule, {ARGS: [create, replace]})}, + {ALT: () => $.SUBRULE($.createIndexStatementRule, {ARGS: [create]})}, + {ALT: () => $.SUBRULE($.createMaterializedViewStatementRule, {ARGS: [create]})}, + {ALT: () => $.SUBRULE($.createSchemaStatementRule, {ARGS: [create]})}, + {ALT: () => $.SUBRULE($.createSequenceStatementRule, {ARGS: [create]})}, + {ALT: () => $.SUBRULE($.createTableStatementRule, {ARGS: [create]})}, + {ALT: () => $.SUBRULE($.createTriggerStatementRule, {ARGS: [create, replace]})}, + {ALT: () => $.SUBRULE($.createTypeStatementRule, {ARGS: [create]})}, + {ALT: () => $.SUBRULE($.createViewStatementRule, {ARGS: [create, replace]})}, + ]) + }}, + {ALT: () => $.SUBRULE($.deleteStatementRule)}, + {ALT: () => $.SUBRULE($.dropStatementRule)}, + {ALT: () => $.SUBRULE($.insertIntoStatementRule)}, + {ALT: () => $.SUBRULE($.selectStatementRule)}, + {ALT: () => $.SUBRULE($.setStatementRule)}, + {ALT: () => $.SUBRULE($.showStatementRule)}, + {ALT: () => $.SUBRULE($.updateStatementRule)}, + ])) + + this.alterSchemaStatementRule = $.RULE<() => AlterSchemaStatementAst>('alterSchemaStatementRule', () => { + // https://www.postgresql.org/docs/current/sql-alterschema.html + const start = $.CONSUME(Alter) + const token = tokenInfo2(start, $.CONSUME(Schema)) + const schema = $.SUBRULE($.identifierRule) + const action = $.OR([ + {ALT: () => ({kind: 'Rename' as const, token: tokenInfo($.CONSUME(RenameTo)), schema: $.SUBRULE2($.identifierRule)})}, + {ALT: () => ({kind: 'Owner' as const, token: tokenInfo($.CONSUME(OwnerTo)), owner: $.SUBRULE(ownerRule)})}, + ]) + const end = $.CONSUME(Semicolon) + return removeUndefined({kind: 'AlterSchema' as const, meta: tokenInfo2(start, end), token, schema, action}) + }) + + this.alterSequenceStatementRule = $.RULE<() => AlterSequenceStatementAst>('alterSequenceStatementRule', () => { + // https://www.postgresql.org/docs/current/sql-altersequence.html + const begin = $.CONSUME(Alter) + const token = tokenInfo2(begin, $.CONSUME(Sequence)) + const ifExists = $.SUBRULE(ifExistsRule) + const object = $.SUBRULE($.objectNameRule) + const as = $.OPTION3(() => $.SUBRULE(sequenceTypeRule)) + const start = $.OPTION4(() => $.SUBRULE(sequenceStartRule)) + const increment = $.OPTION5(() => $.SUBRULE(sequenceIncrementRule)) + const minValue = $.OPTION6(() => $.SUBRULE(sequenceMinValueRule)) + const maxValue = $.OPTION7(() => $.SUBRULE(sequenceMaxValueRule)) + const cache = $.OPTION8(() => $.SUBRULE(sequenceCacheRule)) + // TODO: CYCLE + const ownedBy = $.OPTION9(() => $.SUBRULE(sequenceOwnedByRule)) + // TODO: RESTART + const end = $.CONSUME(Semicolon) + return removeUndefined({kind: 'AlterSequence' as const, meta: tokenInfo2(begin, end), token, ifExists, ...object, as, start, increment, minValue, maxValue, cache, ownedBy}) + }) + + this.alterTableStatementRule = $.RULE<() => AlterTableStatementAst>('alterTableStatementRule', () => { + // https://www.postgresql.org/docs/current/sql-altertable.html + const start = $.CONSUME(Alter) + const token = tokenInfo2(start, $.CONSUME(Table)) + const ifExists = $.SUBRULE(ifExistsRule) + const only = $.OPTION(() => ({token: tokenInfo($.CONSUME(Only))})) + const object = $.SUBRULE($.objectNameRule) + const actions: TableAlterActionAst[] = [] + $.MANY_SEP({SEP: Comma, DEF: () => actions.push($.OR([ + {ALT: () => removeUndefined({kind: 'AddColumn' as const, token: tokenInfo2($.CONSUME(Add), $.OPTION2(() => $.CONSUME(Column))), ifNotExists: $.SUBRULE(ifNotExistsRule), column: $.SUBRULE($.tableColumnRule)})}, + {ALT: () => removeUndefined({kind: 'DropColumn' as const, token: tokenInfo2($.CONSUME(Drop), $.OPTION3(() => $.CONSUME2(Column))), ifExists: $.SUBRULE2(ifExistsRule), column: $.SUBRULE($.identifierRule)})}, + {ALT: () => removeUndefined({kind: 'AlterColumn' as const, token: tokenInfo2($.CONSUME2(Alter), $.OPTION4(() => $.CONSUME3(Column))), column: $.SUBRULE2($.identifierRule), action: $.OR2([ + {ALT: () => removeUndefined({kind: 'Default' as const, action: $.SUBRULE(constraintActionRule), token: tokenInfo($.CONSUME(Default)), expression: $.OPTION5(() => $.SUBRULE($.expressionRule))})}, + {ALT: () => ({kind: 'NotNull' as const, action: $.SUBRULE2(constraintActionRule), token: tokenInfo2($.CONSUME(Not), $.CONSUME(Null))})}, + ])})}, + {ALT: () => removeUndefined({kind: 'AddConstraint' as const, token: tokenInfo($.CONSUME2(Add)), constraint: $.SUBRULE($.tableConstraintRule), notValid: $.OPTION6(() => ({token: tokenInfo2($.CONSUME2(Not), $.CONSUME(Valid))}))})}, + {ALT: () => removeUndefined({kind: 'DropConstraint' as const, token: tokenInfo2($.CONSUME2(Drop), $.CONSUME(Constraint)), ifExists: $.SUBRULE3(ifExistsRule), constraint: $.SUBRULE3($.identifierRule)})}, + {ALT: () => removeUndefined({kind: 'SetOwner' as const, token: tokenInfo($.CONSUME(OwnerTo)), owner: $.SUBRULE(ownerRule)})}, + ]))}) + const end = $.CONSUME(Semicolon) + return removeUndefined({kind: 'AlterTable' as const, meta: tokenInfo2(start, end), token, ifExists, only, schema: object.schema, table: object.name, actions}) + }) + const constraintActionRule =$.RULE<() => { kind: 'Set' | 'Drop', token: TokenInfo }>('constraintActionRule', () => $.OR([ + {ALT: () => ({kind: 'Set' as const, token: tokenInfo($.CONSUME(Set))})}, + {ALT: () => ({kind: 'Drop' as const, token: tokenInfo($.CONSUME(Drop))})}, + ])) + + this.beginStatementRule = $.RULE<() => BeginStatementAst>('beginStatementRule', () => { + // https://www.postgresql.org/docs/current/sql-begin.html + const start = $.CONSUME(Begin) + const token = tokenInfo(start) + const object = $.OPTION(() => $.OR([ + {ALT: () => ({kind: 'Work' as const, token: tokenInfo($.CONSUME(Work))})}, + {ALT: () => ({kind: 'Transaction' as const, token: tokenInfo($.CONSUME(Transaction))})}, + ])) + const modes: TransactionModeAst[] = [] + $.MANY_SEP({SEP: Comma, DEF: () => modes.push($.OR2([ + {ALT: () => ({kind: 'IsolationLevel' as const, token: tokenInfo($.CONSUME(IsolationLevel)), level: $.OR3([ + {ALT: () => ({kind: 'Serializable' as const, token: tokenInfo($.CONSUME(Serializable))})}, + {ALT: () => ({kind: 'RepeatableRead' as const, token: tokenInfo($.CONSUME(RepeatableRead))})}, + {ALT: () => ({kind: 'ReadCommitted' as const, token: tokenInfo($.CONSUME(ReadCommitted))})}, + {ALT: () => ({kind: 'ReadUncommitted' as const, token: tokenInfo($.CONSUME(ReadUncommitted))})}, + ])})}, + {ALT: () => ({kind: 'ReadOnly' as const, token: tokenInfo($.CONSUME(ReadOnly))})}, + {ALT: () => ({kind: 'ReadWrite' as const, token: tokenInfo($.CONSUME(ReadWrite))})}, + {ALT: () => ({not: $.OPTION2(() => ({token: tokenInfo($.CONSUME(Not))})), kind: 'Deferrable' as const, token: tokenInfo($.CONSUME(Deferrable))})} + ]))}) + const end = $.CONSUME(Semicolon) + return removeEmpty({kind: 'Begin' as const, meta: tokenInfo2(start, end), token, object, modes}) + }) + + this.commentOnStatementRule = $.RULE<() => CommentOnStatementAst>('commentOnStatementRule', () => { + // https://www.postgresql.org/docs/current/sql-comment.html + const start = $.CONSUME(Comment) + const token = tokenInfo2(start, $.CONSUME(On)) + const {object, schema, parent, entity} = $.OR([ + {ALT: () => ({object: {kind: 'Column' as const, token: tokenInfo($.CONSUME(Column))}, ...$.SUBRULE(commentColumnRule)})}, + {ALT: () => ({object: {kind: 'Constraint' as const, token: tokenInfo($.CONSUME(Constraint))}, ...$.SUBRULE(commentConstraintRule)})}, + {ALT: () => ({object: {kind: 'Database' as const, token: tokenInfo($.CONSUME(Database))}, ...$.SUBRULE(commentObjectDefaultRule)})}, + {ALT: () => ({object: {kind: 'Extension' as const, token: tokenInfo($.CONSUME(Extension))}, ...$.SUBRULE2(commentObjectDefaultRule)})}, + {ALT: () => ({object: {kind: 'Index' as const, token: tokenInfo($.CONSUME(Index))}, ...$.SUBRULE3(commentObjectDefaultRule)})}, + {ALT: () => ({object: {kind: 'MaterializedView' as const, token: tokenInfo($.CONSUME(MaterializedView))}, ...$.SUBRULE4(commentObjectDefaultRule)})}, + {ALT: () => ({object: {kind: 'Schema' as const, token: tokenInfo($.CONSUME(Schema))}, ...$.SUBRULE5(commentObjectDefaultRule)})}, + {ALT: () => ({object: {kind: 'Table' as const, token: tokenInfo($.CONSUME(Table))}, ...$.SUBRULE6(commentObjectDefaultRule)})}, + {ALT: () => ({object: {kind: 'Type' as const, token: tokenInfo($.CONSUME(Type))}, ...$.SUBRULE7(commentObjectDefaultRule)})}, + {ALT: () => ({object: {kind: 'View' as const, token: tokenInfo($.CONSUME(View))}, ...$.SUBRULE8(commentObjectDefaultRule)})}, + ]) + $.CONSUME(Is) + const comment = $.OR2([ + {ALT: () => $.SUBRULE($.stringRule)}, + {ALT: () => $.SUBRULE($.nullRule)}, + ]) + const end = $.CONSUME(Semicolon) + return removeUndefined({kind: 'CommentOn' as const, meta: tokenInfo2(start, end), token, object, schema, parent, entity, comment}) + }) + const commentObjectDefaultRule = $.RULE<() => {schema?: IdentifierAst, entity: IdentifierAst}>('commentObjectDefaultRule', () => { + const object = $.SUBRULE($.objectNameRule) + return removeUndefined({schema: object.schema, entity: object.name}) + }) + const commentColumnRule = $.RULE<() => {schema?: IdentifierAst, parent: IdentifierAst, entity: IdentifierAst}>('commentColumnRule', () => { + const first = $.SUBRULE($.identifierRule) + $.CONSUME(Dot) + const second = $.SUBRULE2($.identifierRule) + const third = $.OPTION2(() => { + $.CONSUME2(Dot) + return $.SUBRULE3($.identifierRule) + }) + return third ? {schema: first, parent: second, entity: third} : {parent: first, entity: second} + }) + const commentConstraintRule = $.RULE<() => {schema?: IdentifierAst, parent: IdentifierAst, entity: IdentifierAst}>('commentConstraintRule', () => { + const entity = $.SUBRULE($.identifierRule) + $.CONSUME(On) + $.OPTION(() => $.CONSUME(Domain)) + const object = $.SUBRULE($.objectNameRule) + return {entity, schema: object.schema, parent: object.name} + }) + + this.commitStatementRule = $.RULE<() => CommitStatementAst>('commitStatementRule', () => { + // https://www.postgresql.org/docs/current/sql-commit.html + const start = $.CONSUME(Commit) + const token = tokenInfo(start) + const object = $.OPTION(() => $.OR([ + {ALT: () => ({kind: 'Work' as const, token: tokenInfo($.CONSUME(Work))})}, + {ALT: () => ({kind: 'Transaction' as const, token: tokenInfo($.CONSUME(Transaction))})}, + ])) + const chain = $.OPTION2(() => { + const and = $.CONSUME(And) + const no = $.OPTION3(() => ({token: tokenInfo($.CONSUME(No))})) + const chain = $.CONSUME(Chain) + return {token: tokenInfo2(and, chain), no} + }) + const end = $.CONSUME(Semicolon) + return removeUndefined({kind: 'Commit' as const, meta: tokenInfo2(start, end), token, object, chain}) + }) + + this.createExtensionStatementRule = $.RULE<(create: IToken) => CreateExtensionStatementAst>('createExtensionStatementRule', (create: IToken) => { + // https://www.postgresql.org/docs/current/sql-createextension.html + const token = tokenInfo2(create, $.CONSUME(Extension)) + const ifNotExists = $.SUBRULE(ifNotExistsRule) + const name = $.SUBRULE($.identifierRule) + const with_ = $.OPTION(() => ({token: tokenInfo($.CONSUME(With))})) + const schema = $.OPTION2(() => ({token: tokenInfo($.CONSUME(Schema)), name: $.SUBRULE2($.identifierRule)})) + const version = $.OPTION3(() => ({token: tokenInfo($.CONSUME(Version)), number: $.OR([{ALT: () => $.SUBRULE($.stringRule)}, {ALT: () => $.SUBRULE3($.identifierRule)}])})) + const cascade = $.OPTION4(() => ({token: tokenInfo($.CONSUME(Cascade))})) + const end = $.CONSUME(Semicolon) + return removeUndefined({kind: 'CreateExtension' as const, meta: tokenInfo2(create, end), token, ifNotExists, name, with: with_, schema, version, cascade}) + }) + + this.createFunctionStatementRule = $.RULE<(create: IToken, replace?: { token: TokenInfo }) => CreateFunctionStatementAst>('createFunctionStatementRule', (create: IToken, replace?: { token: TokenInfo }) => { + // https://www.postgresql.org/docs/current/sql-createfunction.html + const token = tokenInfo2(create, $.CONSUME(Function)) + const object = $.SUBRULE($.objectNameRule) + const args = $.SUBRULE(functionArgumentsRule) + const statement: Pick = {} + $.MANY({DEF: () => $.OR([ + {ALT: () => statement.returns = $.SUBRULE(functionReturnsRule)}, + {ALT: () => statement.language = {token: tokenInfo($.CONSUME(Language)), name: $.SUBRULE3($.identifierRule)}}, + {ALT: () => statement.behavior = $.OR2([ + {ALT: () => ({kind: 'Immutable' as const, token: tokenInfo($.CONSUME(Immutable))})}, + {ALT: () => ({kind: 'Stable' as const, token: tokenInfo($.CONSUME(Stable))})}, + {ALT: () => ({kind: 'Volatile' as const, token: tokenInfo($.CONSUME(Volatile))})}, + ])}, + {ALT: () => statement.definition = {token: tokenInfo($.CONSUME(As)), value: $.SUBRULE(this.stringRule)}}, + {ALT: () => statement.nullBehavior = $.OR3([ + {ALT: () => ({kind: 'Called' as const, token: tokenInfoN([$.CONSUME(Called), $.CONSUME(On), $.CONSUME(Null), $.CONSUME(Input)])})}, + {ALT: () => ({kind: 'ReturnsNull' as const, token: tokenInfoN([$.CONSUME(Returns), $.CONSUME2(Null), $.CONSUME2(On), $.CONSUME3(Null), $.CONSUME2(Input)])})}, + {ALT: () => ({kind: 'Strict' as const, token: tokenInfo($.CONSUME(Strict))})}, + ])}, + {ALT: () => statement.return = {token: tokenInfo($.CONSUME(Return)), expression: $.SUBRULE(this.expressionRule)}}, + ])}) + const end = $.CONSUME(Semicolon) + return removeUndefined({kind: 'CreateFunction' as const, meta: tokenInfo2(create, end), token, replace, ...object, args, ...statement}) + }) + const functionArgumentsRule = $.RULE<() => FunctionArgumentAst[]>('functionArgumentsRule', () => { + const args: FunctionArgumentAst[] = [] + $.CONSUME(ParenLeft) + $.MANY_SEP({SEP: Comma, DEF: () => { + const mode = $.OPTION(() => $.OR([ + {ALT: () => ({kind: 'In' as const, token: tokenInfo($.CONSUME(In))})}, + {ALT: () => ({kind: 'Out' as const, token: tokenInfo($.CONSUME(Out))})}, + {ALT: () => ({kind: 'InOut' as const, token: tokenInfo($.CONSUME(InOut))})}, + {ALT: () => ({kind: 'Variadic' as const, token: tokenInfo($.CONSUME(Variadic))})}, + ])) + const name = $.OPTION2(() => $.SUBRULE($.identifierRule)) + const type = $.SUBRULE($.columnTypeRule) + args.push(removeUndefined({mode, name, type})) + }}) + $.CONSUME(ParenRight) + return args + }) + const functionReturnsRule = $.RULE<() => FunctionReturnsAst>('functionReturnsRule', () => { + const ret = $.CONSUME(Returns) + return $.OR([ + {ALT: () => removeUndefined({kind: 'Type' as const, token: tokenInfo(ret), setOf: $.OPTION(() => ({token: tokenInfo($.CONSUME(SetOf))})), type: $.SUBRULE($.columnTypeRule)})}, + {ALT: () => { + const token = tokenInfo2(ret, $.CONSUME(Table)) + $.CONSUME(ParenLeft) + const columns: {name: IdentifierAst, type: ColumnTypeAst}[] = [] + $.MANY_SEP({SEP: Comma, DEF: () => columns.push({name: $.SUBRULE($.identifierRule), type: $.SUBRULE2($.columnTypeRule)})}) + $.CONSUME(ParenRight) + return {kind: 'Table' as const, token, columns} + }}, + ]) + }) + + this.createIndexStatementRule = $.RULE<(create: IToken) => CreateIndexStatementAst>('createIndexStatementRule', (create: IToken) => { + // https://www.postgresql.org/docs/current/sql-createindex.html + const unique = $.OPTION(() => ({token: tokenInfo($.CONSUME(Unique))})) + const token = tokenInfo2(create, $.CONSUME(Index)) + const concurrently = $.OPTION2(() => ({token: tokenInfo($.CONSUME(Concurrently))})) + const name = $.OPTION3(() => ({ifNotExists: $.SUBRULE(ifNotExistsRule), index: $.SUBRULE($.identifierRule)})) + $.CONSUME(On) + const only = $.OPTION4(() => ({token: tokenInfo($.CONSUME(Only))})) + const object = $.SUBRULE($.objectNameRule) + const using = $.OPTION5(() => ({token: tokenInfo($.CONSUME(Using)), method: $.SUBRULE2($.identifierRule)})) + $.CONSUME(ParenLeft) + const columns: IndexColumnAst[] = [] + $.AT_LEAST_ONE_SEP({SEP: Comma, DEF: () => columns.push($.SUBRULE(indexColumnRule))}) + $.CONSUME(ParenRight) + const include = $.OPTION6(() => { + const token = tokenInfo($.CONSUME(Include)) + $.CONSUME2(ParenLeft) + const columns: IdentifierAst[] = [] + $.AT_LEAST_ONE_SEP2({SEP: Comma, DEF: () => columns.push($.SUBRULE3($.identifierRule))}) + $.CONSUME2(ParenRight) + return {token, columns} + }) + // TODO: NULLS [ NOT ] DISTINCT + // TODO: WITH (parameters) + // TODO: TABLESPACE name + const where = $.OPTION7(() => $.SUBRULE($.whereClauseRule)) + const end = $.CONSUME(Semicolon) + return removeUndefined({kind: 'CreateIndex' as const, meta: tokenInfo2(create, end), token, unique, concurrently, ...name, only, schema: object.schema, table: object.name, using, columns, include, where}) + }) + + this.createMaterializedViewStatementRule = $.RULE<(create: IToken) => CreateMaterializedViewStatementAst>('createMaterializedViewStatementRule', (create: IToken) => { + // https://www.postgresql.org/docs/current/sql-creatematerializedview.html + const token = tokenInfo2(create, $.CONSUME(MaterializedView)) + const ifNotExists = $.OPTION(() => $.SUBRULE(ifNotExistsRule)) + const object = $.SUBRULE($.objectNameRule) + const columns: IdentifierAst[] = [] + $.OPTION2(() => { + $.CONSUME(ParenLeft) + $.AT_LEAST_ONE_SEP({SEP: Comma, DEF: () => columns.push($.SUBRULE($.identifierRule))}) + $.CONSUME(ParenRight) + }) + // TODO: USING + // TODO: WITH + // TODO: TABLESPACE + $.CONSUME(As) + const query = $.SUBRULE(selectStatementInnerRule) + const withData = $.OPTION3(() => { + const with_ = $.CONSUME(With) + const no = $.OPTION4(() => ({token: tokenInfo($.CONSUME(No))})) + const data = $.CONSUME(Data) + return {token: tokenInfo2(with_, data), no} + }) + const end = $.CONSUME(Semicolon) + return removeEmpty({kind: 'CreateMaterializedView' as const, meta: tokenInfo2(create, end), token, ifNotExists, ...object, columns, query, withData}) + }) + + this.createSchemaStatementRule = $.RULE<(create: IToken) => CreateSchemaStatementAst>('createSchemaStatementRule', (create: IToken) => { + // https://www.postgresql.org/docs/current/sql-createschema.html + const token = tokenInfo2(create, $.CONSUME(Schema)) + const ifNotExists = $.SUBRULE(ifNotExistsRule) + const schema = $.OPTION2(() => $.SUBRULE($.identifierRule)) + const authorization = $.OPTION3(() => ({token: tokenInfo($.CONSUME(Authorization)), owner: $.SUBRULE(ownerRule)})) + const end = $.CONSUME(Semicolon) + return removeEmpty({kind: 'CreateSchema' as const, meta: tokenInfo2(create, end), token, ifNotExists, schema, authorization}) + }) + const ownerRule = $.RULE<() => OwnerAst>('ownerRule', () => $.OR([ + {ALT: () => ({kind: 'User' as const, name: $.SUBRULE2($.identifierRule)})}, + {ALT: () => ({kind: 'CurrentRole' as const, token: tokenInfo($.CONSUME(CurrentRole))})}, + {ALT: () => ({kind: 'CurrentUser' as const, token: tokenInfo($.CONSUME(CurrentUser))})}, + {ALT: () => ({kind: 'SessionUser' as const, token: tokenInfo($.CONSUME(SessionUser))})}, + ])) + + this.createSequenceStatementRule = $.RULE<(create: IToken) => CreateSequenceStatementAst>('createSequenceStatementRule', (create: IToken) => { + // https://www.postgresql.org/docs/current/sql-createsequence.html + const mode = $.OPTION(() => $.OR([ + {ALT: () => ({kind: 'Unlogged' as const, token: tokenInfo($.CONSUME(Unlogged))})}, + {ALT: () => ({kind: 'Temporary' as const, token: $.OR2([ + {ALT: () => tokenInfo($.CONSUME(Temp))}, + {ALT: () => tokenInfo($.CONSUME(Temporary))} + ])})} + ])) + const token = tokenInfo2(create, $.CONSUME(Sequence)) + const ifNotExists = $.OPTION2(() => $.SUBRULE(ifNotExistsRule)) + const object = $.SUBRULE($.objectNameRule) + + const statement: Pick = {} + $.MANY({DEF: () => $.OR3([ + {ALT: () => statement.as = $.SUBRULE(sequenceTypeRule)}, + {ALT: () => statement.start = $.SUBRULE(sequenceStartRule)}, + {ALT: () => statement.increment = $.SUBRULE(sequenceIncrementRule)}, + {ALT: () => statement.minValue = $.SUBRULE(sequenceMinValueRule)}, + {ALT: () => statement.maxValue = $.SUBRULE(sequenceMaxValueRule)}, + {ALT: () => statement.cache = $.SUBRULE(sequenceCacheRule)}, + // TODO: CYCLE + {ALT: () => statement.ownedBy = $.SUBRULE(sequenceOwnedByRule)}, + ])}) + const end = $.CONSUME(Semicolon) + return removeUndefined({kind: 'CreateSequence' as const, meta: tokenInfo2(create, end), token, mode, ifNotExists, ...object, ...statement}) + }) + const sequenceTypeRule = $.RULE<() => SequenceTypeAst>('sequenceTypeRule', () => ({token: tokenInfo($.CONSUME(As)), type: $.SUBRULE($.identifierRule)})) + const sequenceStartRule = $.RULE<() => SequenceParamAst>('sequenceStartRule', () => ({token: tokenInfo2($.CONSUME(Start), $.OPTION(() => $.CONSUME(With))), value: $.SUBRULE($.integerRule)})) + const sequenceIncrementRule = $.RULE<() => SequenceParamAst>('sequenceIncrementRule', () => ({token: tokenInfo2($.CONSUME(Increment), $.OPTION(() => $.CONSUME(By))), value: $.SUBRULE($.integerRule)})) + const sequenceMinValueRule = $.RULE<() => SequenceParamOptAst>('sequenceMinValueRule', () => $.OR([ + {ALT: () => ({token: tokenInfo2($.CONSUME(No), $.CONSUME(Minvalue))})}, + {ALT: () => ({token: tokenInfo($.CONSUME2(Minvalue)), value: $.SUBRULE($.integerRule)})}, + ])) + const sequenceMaxValueRule = $.RULE<() => SequenceParamOptAst>('sequenceMaxValueRule', () => $.OR([ + {ALT: () => ({token: tokenInfo2($.CONSUME(No), $.CONSUME(Maxvalue))})}, + {ALT: () => ({token: tokenInfo($.CONSUME2(Maxvalue)), value: $.SUBRULE($.integerRule)})}, + ])) + const sequenceCacheRule = $.RULE<() => SequenceParamAst>('sequenceCacheRule', () => ({token: tokenInfo($.CONSUME(Cache)), value: $.SUBRULE5($.integerRule)})) + const sequenceOwnedByRule = $.RULE<() => SequenceOwnedByAst>('sequenceOwnedByRule', () => { + return {token: tokenInfo($.CONSUME(OwnedBy)), owner: $.OR([ + {ALT: () => ({kind: 'None' as const, token: $.CONSUME(None)})}, + {ALT: () => { + const first = $.SUBRULE($.identifierRule) + $.CONSUME(Dot) + const second = $.SUBRULE2($.identifierRule) + const third = $.OPTION(() => { + $.CONSUME2(Dot) + return $.SUBRULE3($.identifierRule) + }) + return third ? {kind: 'Column' as const, schema: first, table: second, column: third} : {kind: 'Column' as const, table: first, column: second} + }}, + ])} + }) + + this.createTableStatementRule = $.RULE<(create: IToken) => CreateTableStatementAst>('createTableStatementRule', (create: IToken) => { + // https://www.postgresql.org/docs/current/sql-createtable.html + const mode = $.OPTION(() => $.OR([ + {ALT: () => ({kind: 'Unlogged' as const, token: tokenInfo($.CONSUME(Unlogged))})}, + {ALT: () => { + const scope = $.OPTION2(() => $.OR2([ + {ALT: () => ({kind: 'Global' as const, token: tokenInfo($.CONSUME(Global))})}, + {ALT: () => ({kind: 'Local' as const, token: tokenInfo($.CONSUME(Local))})}, + ])) + const temporary = $.OR3([{ALT: () => tokenInfo($.CONSUME(Temp))}, {ALT: () => tokenInfo($.CONSUME(Temporary))}]) + return removeUndefined({kind: 'Temporary' as const, ...temporary, scope}) + }} + ])) + const token = tokenInfo2(create, $.CONSUME(Table)) + const ifNotExists = $.OPTION3(() => $.SUBRULE(ifNotExistsRule)) + const object = $.SUBRULE($.objectNameRule) + $.CONSUME(ParenLeft) + const columns: TableColumnAst[] = [] + const constraints: TableConstraintAst[] = [] + $.MANY_SEP({SEP: Comma, DEF: () => $.OR4([ + {ALT: () => columns.push($.SUBRULE($.tableColumnRule))}, + {ALT: () => constraints.push($.SUBRULE($.tableConstraintRule))}, + ])}) + $.CONSUME(ParenRight) + const end = $.CONSUME(Semicolon) + return removeEmpty({kind: 'CreateTable' as const, meta: tokenInfo2(create, end), token, mode, ifNotExists, ...object, columns: columns.filter(isNotUndefined), constraints: constraints.filter(isNotUndefined)}) + }) + + this.createTriggerStatementRule = $.RULE<(create: IToken, replace?: { token: TokenInfo }) => CreateTriggerStatementAst>('createTriggerStatementRule', (create: IToken, replace?: { token: TokenInfo }) => { + // https://www.postgresql.org/docs/current/sql-createtrigger.html + const constraint = $.OPTION(() => ({token: tokenInfo($.CONSUME(Constraint))})) + const token = tokenInfo2(create, $.CONSUME(Trigger)) + const name = $.SUBRULE($.identifierRule) + const timing = $.OR([ + {ALT: () => ({kind: 'Before' as const, token: tokenInfo($.CONSUME(Before))})}, + {ALT: () => ({kind: 'After' as const, token: tokenInfo($.CONSUME(After))})}, + {ALT: () => ({kind: 'InsteadOf' as const, token: tokenInfo($.CONSUME(InsteadOf))})}, + ]) + const events: TriggerEventAst[] = [] + $.AT_LEAST_ONE_SEP({SEP: Or, DEF: () => events.push($.SUBRULE(triggerEventRule))}) + $.CONSUME2(On) + const object = $.SUBRULE($.objectNameRule) + const from = $.OPTION2(() => { + const token = tokenInfo($.CONSUME(From)) + const object = $.SUBRULE2($.objectNameRule) + return removeUndefined({token, schema: object.schema, table: object.name}) + }) + const deferrable = $.OPTION3(() => $.SUBRULE(triggerDeferrableRule)) + const referencing = $.OPTION4(() => $.SUBRULE(triggerReferencingRule)) + const target = $.OPTION5(() => { + const token = $.CONSUME(For) + const each = $.OPTION6(() => $.CONSUME(Each)) + return $.OR2([ + {ALT: () => ({kind: 'Row' as const, token: tokenInfo3(token, each, $.CONSUME(Row))})}, + {ALT: () => ({kind: 'Statement' as const, token: tokenInfo3(token, each, $.CONSUME(Statement))})}, + ]) + }) + const when = $.OPTION7(() => { + const token = tokenInfo($.CONSUME(When)) + $.CONSUME(ParenLeft) + const condition = $.SUBRULE($.expressionRule) + $.CONSUME(ParenRight) + return {token, condition} + }) + const execToken = tokenInfo2($.CONSUME(Execute), $.OR3([{ALT: () => $.CONSUME(Function)}, {ALT: () => $.CONSUME(Procedure)}])) + const func = $.SUBRULE3($.objectNameRule) + $.CONSUME2(ParenLeft) + const execArguments: ExpressionAst[] = [] + const execute = removeUndefined({token: execToken, schema: func.schema, function: func.name, arguments: execArguments}) + $.MANY_SEP({SEP: Comma, DEF: () => execArguments.push($.SUBRULE2($.expressionRule))}) + $.CONSUME2(ParenRight) + const end = $.CONSUME(Semicolon) + return removeUndefined({kind: 'CreateTrigger' as const, meta: tokenInfo2(create, end), token, replace, constraint, name, timing, events, schema: object.schema, table: object.name, from, deferrable, referencing, target, when, execute}) + }) + const triggerEventRule = $.RULE<() => TriggerEventAst>('triggerEventRule', () => $.OR([ + {ALT: () => ({kind: 'Insert' as const, token: tokenInfo($.CONSUME(Insert))})}, + {ALT: () => removeUndefined({kind: 'Update' as const, token: tokenInfo($.CONSUME(Update)), columns: $.OPTION(() => { + $.CONSUME(Of) + const columns: IdentifierAst[] = [] + $.AT_LEAST_ONE_SEP2({SEP: Comma, DEF: () => columns.push($.SUBRULE2($.identifierRule))}) + return columns + })})}, + {ALT: () => ({kind: 'Delete' as const, token: tokenInfo($.CONSUME(Delete))})}, + {ALT: () => ({kind: 'Truncate' as const, token: tokenInfo($.CONSUME(Truncate))})}, + ])) + const triggerDeferrableRule = $.RULE<() => TriggerDeferrableAst>('triggerDeferrableRule', () => { + const not = $.OPTION(() => $.CONSUME(Not)) + const token = tokenInfo2(not, $.CONSUME(Deferrable)) + const initially = $.OPTION2(() => { + const init = $.CONSUME(Initially) + return $.OR([ + {ALT: () => ({kind: 'Immediate' as const, token: tokenInfo2(init, $.CONSUME(Immediate))})}, + {ALT: () => ({kind: 'Deferred' as const, token: tokenInfo2(init, $.CONSUME(Deferred))})}, + ]) + }) + return removeUndefined({ kind: not ? 'NotDeferrable' as const : 'Deferrable' as const, token, initially}) + }) + const triggerReferencingRule = $.RULE<() => TriggerReferencingAst>('triggerReferencingRule', () => { + const res: TriggerReferencingAst = {token: tokenInfo($.CONSUME(Referencing))} + $.AT_LEAST_ONE({DEF: () => { + const kind = $.OR([ + {ALT: () => ({old: true, token: $.CONSUME(Old)})}, + {ALT: () => ({old: false, token: $.CONSUME(New)})}, + ]) + const token = tokenInfo3(kind.token, $.CONSUME(Table), $.OPTION(() => $.CONSUME(As))) + const name = $.SUBRULE($.identifierRule) + if (kind.old) { + res.old = {token, name} + } else { + res.new = {token, name} + } + }}) + return res + }) + + this.createTypeStatementRule = $.RULE<(create: IToken) => CreateTypeStatementAst>('createTypeStatementRule', (create: IToken) => { + // https://www.postgresql.org/docs/current/sql-createtype.html + const token = tokenInfo2(create, $.CONSUME(Type)) + const object = $.SUBRULE($.objectNameRule) + const content = $.OPTION(() => $.OR([ + {ALT: () => ({struct: {token: tokenInfo($.CONSUME(As)), attrs: $.SUBRULE(createTypeStructAttrs)}})}, + {ALT: () => ({enum: {token: tokenInfo2($.CONSUME2(As), $.CONSUME(Enum)), values: $.SUBRULE(createTypeEnumValues)}})}, + // TODO: RANGE + {ALT: () => ({base: $.SUBRULE(createTypeBase)})} + ])) + const end = $.CONSUME(Semicolon) + return removeEmpty({kind: 'CreateType' as const, meta: tokenInfo2(create, end), token, ...object, ...content}) + }) + + this.createViewStatementRule = $.RULE<(create: IToken, replace?: { token: TokenInfo }) => CreateViewStatementAst>('createViewStatementRule', (create: IToken, replace?: { token: TokenInfo }) => { + // https://www.postgresql.org/docs/current/sql-createview.html + const temporary = $.OPTION2(() => ({token: $.OR([{ALT: () => tokenInfo($.CONSUME(Temp))}, {ALT: () => tokenInfo($.CONSUME(Temporary))}])})) + const recursive = $.OPTION3(() => ({token: tokenInfo($.CONSUME(Recursive))})) + const token = tokenInfo2(create, $.CONSUME(View)) + const object = $.SUBRULE($.objectNameRule) + const columns: IdentifierAst[] = [] + $.OPTION4(() => { + $.CONSUME(ParenLeft) + $.AT_LEAST_ONE_SEP({SEP: Comma, DEF: () => columns.push($.SUBRULE($.identifierRule))}) + $.CONSUME(ParenRight) + }) + $.CONSUME(As) + const query = $.SUBRULE(selectStatementInnerRule) + const end = $.CONSUME(Semicolon) + return removeEmpty({kind: 'CreateView' as const, meta: tokenInfo2(create, end), token, replace, temporary, recursive, ...object, columns, query}) + }) + + this.deleteStatementRule = $.RULE<() => DeleteStatementAst>('deleteStatementRule', () => { + const start = $.CONSUME(Delete) + const token = tokenInfo2(start, $.CONSUME(From)) + const only = $.OPTION(() => ({token: tokenInfo($.CONSUME(Only))})) + const object = $.SUBRULE($.objectNameRule) + const descendants = $.OPTION2(() => ({token: tokenInfo($.CONSUME(Asterisk))})) + const alias = $.OPTION3(() => $.SUBRULE($.aliasRule)) + const using = $.OPTION4(() => ({token: tokenInfo($.CONSUME(Using)), ...$.SUBRULE(fromClauseItemRule)})) + const where = $.OPTION5(() => $.SUBRULE($.whereClauseRule)) + const returning = $.OPTION6(() => $.SUBRULE(returningClauseRule)) + const end = $.CONSUME(Semicolon) + return removeUndefined({kind: 'Delete' as const, meta: tokenInfo2(start, end), token, only, schema: object.schema, table: object.name, descendants, alias, using, where, returning}) + }) + + this.dropStatementRule = $.RULE<() => DropStatementAst>('dropStatementRule', () => { + const start = $.CONSUME(Drop) + const {token, object} = $.OR([ + {ALT: () => ({token: tokenInfo2(start, $.CONSUME(Index)), object: 'Index' as const})}, // https://www.postgresql.org/docs/current/sql-dropindex.html + {ALT: () => ({token: tokenInfo2(start, $.CONSUME(MaterializedView)), object: 'MaterializedView' as const})}, // https://www.postgresql.org/docs/current/sql-dropmaterializedview.html + {ALT: () => ({token: tokenInfo2(start, $.CONSUME(Sequence)), object: 'Sequence' as const})}, // https://www.postgresql.org/docs/current/sql-dropsequence.html + {ALT: () => ({token: tokenInfo2(start, $.CONSUME(Table)), object: 'Table' as const})}, // https://www.postgresql.org/docs/current/sql-droptable.html + {ALT: () => ({token: tokenInfo2(start, $.CONSUME(Type)), object: 'Type' as const})}, // https://www.postgresql.org/docs/current/sql-droptype.html + {ALT: () => ({token: tokenInfo2(start, $.CONSUME(View)), object: 'View' as const})}, // https://www.postgresql.org/docs/current/sql-dropview.html + ]) + const concurrently = $.OPTION(() => ({token: tokenInfo($.CONSUME(Concurrently))})) + const ifExists = $.SUBRULE(ifExistsRule) + const objects: ObjectNameAst[] = [] + $.AT_LEAST_ONE_SEP({SEP: Comma, DEF: () => objects.push($.SUBRULE($.objectNameRule))}) + const mode = $.OPTION2(() => $.OR2([ + {ALT: () => ({token: tokenInfo($.CONSUME(Cascade)), kind: 'Cascade' as const})}, + {ALT: () => ({token: tokenInfo($.CONSUME(Restrict)), kind: 'Restrict' as const})}, + ])) + const end = $.CONSUME(Semicolon) + return removeUndefined({kind: 'Drop' as const, meta: tokenInfo2(start, end), token, object, entities: objects.filter(isNotUndefined), concurrently, ifExists, mode}) + }) + + this.insertIntoStatementRule = $.RULE<() => InsertIntoStatementAst>('insertIntoStatementRule', () => { + // https://www.postgresql.org/docs/current/sql-insert.html + const start = $.CONSUME(Insert) + const into = $.CONSUME(Into) + const object = $.SUBRULE($.objectNameRule) + const columns = $.OPTION(() => { + const columns: IdentifierAst[] = [] + $.CONSUME(ParenLeft) + $.AT_LEAST_ONE_SEP({SEP: Comma, DEF: () => columns.push($.SUBRULE($.identifierRule))}) + $.CONSUME(ParenRight) + return columns + }) + $.CONSUME(Values) + const values: (ExpressionAst | { kind: 'Default', token: TokenInfo })[][] = [] + $.AT_LEAST_ONE_SEP2({SEP: Comma, DEF: () => { + const row: ExpressionAst[] = [] + $.CONSUME2(ParenLeft) + $.AT_LEAST_ONE_SEP3({SEP: Comma, DEF: () => row.push($.OR([ + {ALT: () => $.SUBRULE($.expressionRule)}, + {ALT: () => ({kind: 'Default' as const, token: tokenInfo($.CONSUME(Default))})} + ]))}) + $.CONSUME2(ParenRight) + values.push(row) + }}) + const onConflict = $.OPTION2(() => $.SUBRULE(onConflictClauseRule)) + const returning = $.OPTION3(() => $.SUBRULE(returningClauseRule)) + const end = $.CONSUME(Semicolon) + return removeUndefined({kind: 'InsertInto' as const, meta: tokenInfo2(start, end), token: tokenInfo2(start, into), schema: object.schema, table: object.name, columns, values, onConflict, returning}) + }) + const onConflictClauseRule = $.RULE<() => OnConflictClauseAst>('onConflictClauseRule', () => { + const token = tokenInfo2($.CONSUME(On), $.CONSUME(Conflict)) + const target = $.OPTION(() => $.OR([ + {ALT: () => ({kind: 'Constraint' as const, token: tokenInfo2($.CONSUME2(On), $.CONSUME(Constraint)), name: $.SUBRULE($.identifierRule)})}, + {ALT: () => { + $.CONSUME(ParenLeft) + const columns: IdentifierAst[] = [] + $.AT_LEAST_ONE_SEP({SEP: Comma, DEF: () => columns.push($.SUBRULE2($.identifierRule))}) + $.CONSUME(ParenRight) + const where = $.OPTION2(() => $.SUBRULE($.whereClauseRule)) + return {kind: 'Columns' as const, columns, where} + }} + ])) + const do_ = $.CONSUME(Do) + const action = $.OR2([ + {ALT: () => ({kind: 'Nothing' as const, token: tokenInfo2(do_, $.CONSUME(Nothing))})}, + {ALT: () => { + const token = tokenInfo2(do_, $.CONSUME(Update)) + $.CONSUME(Set) + const columns = $.SUBRULE(updateColumnsRule) + const where = $.OPTION3(() => $.SUBRULE2($.whereClauseRule)) + return ({kind: 'Update' as const, token, columns, where}) + }}, + ]) + return {token, target, action} + }) + const returningClauseRule = $.RULE<() => SelectClauseAst>('returningClauseRule', () => { + const token = tokenInfo($.CONSUME(Returning)) + const columns: SelectClauseColumnAst[] = [] + $.AT_LEAST_ONE_SEP4({SEP: Comma, DEF: () => columns.push($.SUBRULE(selectClauseColumnRule))}) + return {token, columns} + }) + + this.selectStatementRule = $.RULE<() => SelectStatementAst>('selectStatementRule', () => { + const select = $.SUBRULE(selectStatementInnerRule) + const end = $.CONSUME(Semicolon) + return removeUndefined({kind: 'Select' as const, meta: mergePositions([select?.token, tokenInfo(end)]), ...select}) + }) + const selectStatementInnerRule = $.RULE<() => SelectInnerAst>('selectStatementInnerRule', (): SelectInnerAst => { + // https://www.postgresql.org/docs/current/sql-select.html + return $.OR([ + {ALT: () => { + const main = $.SUBRULE(selectStatementMainRule) + const result = $.SUBRULE(selectStatementResultRule) + return removeUndefined({...main, ...result}) + }}, + {ALT: () => { // additional parenthesis + $.CONSUME(ParenLeft) + const main = $.SUBRULE2(selectStatementMainRule) + const result = $.SUBRULE2(selectStatementResultRule) + $.CONSUME(ParenRight) + const union = $.OPTION(() => $.SUBRULE(unionClauseRule)) + return removeUndefined({...main, union, ...result}) + }}, + ]) + }) + const selectStatementMainRule = $.RULE<() => SelectMainAst>('selectStatementMainRule', (): SelectInnerAst => { + const select = $.SUBRULE($.selectClauseRule) + const from = $.OPTION(() => $.SUBRULE($.fromClauseRule)) + const where = $.OPTION2(() => $.SUBRULE($.whereClauseRule)) + const groupBy = $.OPTION3(() => $.SUBRULE(groupByClauseRule)) + const having = $.OPTION4(() => $.SUBRULE(havingClauseRule)) + const window: WindowClauseAst[] = [] + $.MANY(() => window.push($.SUBRULE(windowClauseRule))) + return removeEmpty({...select, from, where, groupBy, having, window}) + }) + const selectStatementResultRule = $.RULE<() => SelectResultAst>('selectStatementResultRule', () => { + const union = $.OPTION(() => $.SUBRULE3(unionClauseRule)) + const orderBy = $.OPTION2(() => $.SUBRULE(orderByClauseRule)) + const limit = $.OPTION3(() => $.SUBRULE(limitClauseRule)) + const offset = $.OPTION4(() => $.SUBRULE(offsetClauseRule)) + const fetch = $.OPTION5(() => $.SUBRULE(fetchClauseRule)) + return removeUndefined({union, orderBy, limit, offset, fetch}) + }) + + this.setStatementRule = $.RULE<() => SetStatementAst>('setStatementRule', () => { + // https://www.postgresql.org/docs/current/sql-set.html + const start = $.CONSUME(Set) + const scope = $.OPTION(() => $.OR([ + {ALT: () => ({kind: 'Session' as const, token: tokenInfo($.CONSUME(Session))})}, + {ALT: () => ({kind: 'Local' as const, token: tokenInfo($.CONSUME(Local))})}, + ])) + const parameter = $.SUBRULE($.identifierRule) + const equal = $.OPTION2(() => $.OR2([ + {ALT: () => ({kind: '=' as const, token: tokenInfo($.CONSUME(Equal))})}, + {ALT: () => ({kind: 'To' as const, token: tokenInfo($.CONSUME(To))})}, + ])) + const value = $.OR3([ + {ALT: () => ({kind: 'Default' as const, token: tokenInfo($.CONSUME(Default))})}, + {ALT: () => { + const values: (IdentifierAst | LiteralAst)[] = [] + $.AT_LEAST_ONE_SEP({SEP: Comma, DEF: () => values.push($.OR4([ + {ALT: () => $.SUBRULE2($.literalRule)}, + {ALT: () => $.SUBRULE3($.identifierRule)}, + {ALT: () => toIdentifier($.CONSUME(On))}, // special case, `on` being a valid identifier here + ]))}) + return values.length === 1 ? values[0] : values + }}, + ]) + const end = $.CONSUME(Semicolon) + return removeUndefined({kind: 'Set' as const, meta: tokenInfo2(start, end), token: tokenInfo(start), scope, parameter, equal, value}) + }) + + this.showStatementRule = $.RULE<() => ShowStatementAst>('showStatementRule', () => { + // https://www.postgresql.org/docs/current/sql-show.html + const start = $.CONSUME(Show) + const name = $.SUBRULE($.identifierRule) + const end = $.CONSUME(Semicolon) + return removeUndefined({kind: 'Show' as const, meta: tokenInfo2(start, end), token: tokenInfo(start), name}) + }) + + this.updateStatementRule = $.RULE<() => UpdateStatementAst>('updateStatementRule', () => { + // https://www.postgresql.org/docs/current/sql-update.html + const start = $.CONSUME(Update) + const only = $.OPTION(() => ({token: tokenInfo($.CONSUME(Only))})) + const object = $.SUBRULE($.objectNameRule) + const descendants = $.OPTION2(() => ({token: tokenInfo($.CONSUME(Asterisk))})) + const alias = $.OPTION3(() => $.SUBRULE($.aliasRule)) + $.CONSUME(Set) + const columns = $.SUBRULE(updateColumnsRule) + const where = $.OPTION4(() => $.SUBRULE($.whereClauseRule)) + const returning = $.OPTION5(() => $.SUBRULE(returningClauseRule)) + const end = $.CONSUME(Semicolon) + return removeUndefined({kind: 'Update' as const, meta: tokenInfo2(start, end), token: tokenInfo(start), only, schema: object.schema, table: object.name, descendants, alias, columns, where, returning}) + }) + const updateColumnsRule = $.RULE<() => ColumnUpdateAst[]>('updateColumnsRule', () => { + const columns: ColumnUpdateAst[] = [] + $.AT_LEAST_ONE_SEP({SEP: Comma, DEF: () => { + const column = $.SUBRULE($.identifierRule) + $.CONSUME(Equal) + const value = $.OR([ + {ALT: () => $.SUBRULE($.expressionRule)}, + {ALT: () => ({kind: 'Default' as const, token: tokenInfo($.CONSUME(Default))})} + ]) + columns.push({column, value}) + }}) + return columns + }) + + // clauses + + this.selectClauseRule = $.RULE<() => SelectClauseAst>('selectClauseRule', () => { + const token = tokenInfo($.CONSUME(Select)) + $.OPTION(() => $.CONSUME(All)) // default behavior, not specified most of the time but just in case... + const distinct = $.OPTION2(() => removeUndefined({ + token: tokenInfo($.CONSUME(Distinct)), + on: $.OPTION3(() => { + const token = tokenInfo($.CONSUME(On)) + $.CONSUME(ParenLeft) + const columns: ExpressionAst[] = [] + $.AT_LEAST_ONE_SEP({SEP: Comma, DEF: () => columns.push($.SUBRULE($.expressionRule))}) + $.CONSUME(ParenRight) + return ({token, columns}) + }) + })) + const columns: SelectClauseColumnAst[] = [] + $.AT_LEAST_ONE_SEP2({SEP: Comma, DEF: () => columns.push($.SUBRULE(selectClauseColumnRule))}) + return {token, distinct, columns} + }) + const selectClauseColumnRule = $.RULE<() => SelectClauseColumnAst>('selectClauseColumnRule', () => { + const expression = $.SUBRULE($.expressionRule) + const filter = $.OPTION(() => { + const token = tokenInfo($.CONSUME(Filter)) + $.CONSUME(ParenLeft) + const where = $.SUBRULE($.whereClauseRule) + $.CONSUME(ParenRight) + return {token, where} + }) + const over = $.OPTION2(() => { + const token = tokenInfo($.CONSUME(Over)) + const content = $.OR([ + {ALT: () => ({name: $.SUBRULE($.identifierRule)})}, + {ALT: () => $.SUBRULE(windowClauseContentRule)} + ]) + return {token, ...content} + }) + const alias = $.OPTION3(() => $.SUBRULE($.aliasRule)) + return removeUndefined({...expression, filter, over, alias}) + }) + + this.fromClauseRule = $.RULE<() => FromClauseAst>('fromClauseRule', () => { + const token = tokenInfo($.CONSUME(From)) + const item = $.SUBRULE(fromClauseItemRule) + const joins: FromClauseJoinAst[] = [] + $.MANY({DEF: () => joins.push($.SUBRULE(fromClauseJoinRule))}) + return removeEmpty({token, ...item, joins}) + }) + const fromClauseItemRule = $.RULE<() => FromClauseItemAst>('fromClauseItemRule', () => { + const item = $.OR([ + {ALT: () => $.SUBRULE(fromClauseTableRule)}, + {ALT: () => $.SUBRULE(fromClauseQueryRule)}, + ]) + const alias = $.OPTION(() => $.SUBRULE($.aliasRule)) + return removeUndefined({...item, alias}) + }) + const fromClauseTableRule = $.RULE<() => FromClauseTableAst>('fromClauseTableRule', () => { + const object = $.SUBRULE($.objectNameRule) + return removeUndefined({kind: 'Table' as const, schema: object.schema, table: object.name}) + }) + const fromClauseQueryRule = $.RULE<() => FromClauseQueryAst>('fromClauseQueryRule', () => { + $.CONSUME(ParenLeft) + const select = $.SUBRULE(selectStatementInnerRule) + $.CONSUME(ParenRight) + return {kind: 'Select', select} + }) + const fromClauseJoinRule = $.RULE<() => FromClauseJoinAst>('fromClauseJoinRule', () => { + const natural = $.OPTION(() => ({kind: 'Natural', token: tokenInfo($.CONSUME(Natural))})) + const {kind, token} = $.OR([ + {ALT: () => ({kind: 'Inner' as const, token: tokenInfo2($.OPTION2(() => $.CONSUME(Inner)), $.CONSUME(Join))})}, + {ALT: () => ({kind: 'Left' as const, token: tokenInfo3($.CONSUME(Left), $.OPTION3(() => $.CONSUME(Outer)), $.CONSUME2(Join))})}, + {ALT: () => ({kind: 'Right' as const, token: tokenInfo3($.CONSUME(Right), $.OPTION4(() => $.CONSUME2(Outer)), $.CONSUME3(Join))})}, + {ALT: () => ({kind: 'Full' as const, token: tokenInfo3($.CONSUME(Full), $.OPTION5(() => $.CONSUME3(Outer)), $.CONSUME4(Join))})}, + {ALT: () => ({kind: 'Cross' as const, token: tokenInfo2($.CONSUME(Cross), $.CONSUME5(Join))})}, + ]) + const from = $.SUBRULE(fromClauseItemRule) + const on = $.OPTION6(() => $.OR2([ + {ALT: () => ({kind: 'On', token: tokenInfo($.CONSUME(On)), predicate: $.SUBRULE($.expressionRule)})}, + {ALT: () => { + const token = tokenInfo($.CONSUME(Using)) + const columns: IdentifierAst[] = [] + $.CONSUME(ParenLeft) + $.AT_LEAST_ONE_SEP({SEP: Comma, DEF: () => columns.push($.SUBRULE($.identifierRule))}) + $.CONSUME(ParenRight) + return {kind: 'Using', token, columns: columns.filter(isNotUndefined)} + }}, + ])) || natural + const alias = $.OPTION7(() => $.SUBRULE($.aliasRule)) + return removeUndefined({kind, token, from, on, alias}) + }) + + this.whereClauseRule = $.RULE<() => WhereClauseAst>('whereClauseRule', () => { + const token = tokenInfo($.CONSUME(Where)) + return {token, predicate: $.SUBRULE($.expressionRule)} + }) + const groupByClauseRule = $.RULE<() => GroupByClauseAst>('groupByClauseRule', () => { + const token = tokenInfo($.CONSUME(GroupBy)) + const mode = $.OPTION(() => $.OR([ + {ALT: () => ({kind: 'All' as const, token: tokenInfo($.CONSUME(All))})}, + {ALT: () => ({kind: 'Distinct' as const, token: tokenInfo($.CONSUME(Distinct))})}, + ])) + const expressions: ExpressionAst[] = [] + $.AT_LEAST_ONE_SEP({SEP: Comma, DEF: () => expressions.push($.SUBRULE($.expressionRule))}) + return removeUndefined({token, mode, expressions: expressions.filter(isNotUndefined)}) + }) + const havingClauseRule = $.RULE<() => HavingClauseAst>('havingClauseRule', () => { + const token = tokenInfo($.CONSUME(Having)) + return {token, predicate: $.SUBRULE($.expressionRule)} + }) + const windowClauseRule = $.RULE<() => WindowClauseAst>('windowClauseRule', () => { + const token = tokenInfo($.CONSUME(Window)) + const name = $.SUBRULE($.identifierRule) + $.CONSUME(As) + const content = $.SUBRULE(windowClauseContentRule) + return {token, name, ...content} + }) + const windowClauseContentRule = $.RULE<() => WindowClauseContentAst>('windowClauseContentRule', () => { + $.CONSUME(ParenLeft) + const partitionBy = $.OPTION(() => { + const token = tokenInfo($.CONSUME(PartitionBy)) + const columns: ExpressionAst[] = [] + $.AT_LEAST_ONE_SEP({SEP: Comma, DEF: () => columns.push($.SUBRULE($.expressionRule))}) + return {token, columns} + }) + const orderBy = $.OPTION2(() => $.SUBRULE(orderByClauseRule)) + $.CONSUME(ParenRight) + return removeUndefined({partitionBy, orderBy}) + }) + const unionClauseRule = $.RULE<() => UnionClauseAst>('unionClauseRule', () => { + const {kind, token} = $.OR([ + {ALT: () => ({kind: 'Union' as const, token: tokenInfo($.CONSUME(Union))})}, + {ALT: () => ({kind: 'Intersect' as const, token: tokenInfo($.CONSUME(Intersect))})}, + {ALT: () => ({kind: 'Except' as const, token: tokenInfo($.CONSUME(Except))})}, + ]) + const mode = $.OPTION(() => $.OR2([ + {ALT: () => ({kind: 'All' as const, token: tokenInfo($.CONSUME(All))})}, + {ALT: () => ({kind: 'Distinct' as const, token: tokenInfo($.CONSUME(Distinct))})}, + ])) + const select = $.SUBRULE(selectStatementInnerRule) + return removeUndefined({kind, token, mode, select}) + }) + const orderByClauseRule = $.RULE<() => OrderByClauseAst>('orderByClauseRule', () => { + const token = tokenInfo($.CONSUME(OrderBy)) + const expressions: (ExpressionAst & {order?: SortOrderAst, nulls?: SortNullsAst})[] = [] + $.AT_LEAST_ONE_SEP({SEP: Comma, DEF: () => { + const expr = $.SUBRULE($.expressionRule) + const order = $.OPTION2(() => $.SUBRULE(sortOrderRule)) + const nulls = $.OPTION3(() => $.SUBRULE(sortNullsRule)) + expr && expressions.push(removeUndefined({...expr, order, nulls})) + }}) + return {token, expressions} + }) + const limitClauseRule = $.RULE<() => LimitClauseAst>('limitClauseRule', () => { + const token = tokenInfo($.CONSUME(Limit)) + const value = $.OR([ + {ALT: () => $.SUBRULE($.integerRule)}, + {ALT: () => $.SUBRULE($.parameterRule)}, + {ALT: () => ({kind: 'All' as const, token: tokenInfo($.CONSUME(All))})}, + ]) + return {token, value} + }) + const offsetClauseRule = $.RULE<() => OffsetClauseAst>('offsetClauseRule', () => { + const token = tokenInfo($.CONSUME(Offset)) + const value = $.OR([ + {ALT: () => $.SUBRULE($.integerRule)}, + {ALT: () => $.SUBRULE($.parameterRule)}, + ]) + const rows = $.OPTION(() => $.OR2([ + {ALT: () => ({kind: 'Row' as const, token: tokenInfo($.CONSUME(Row))})}, + {ALT: () => ({kind: 'Rows' as const, token: tokenInfo($.CONSUME(Rows))})}, + ])) + return removeUndefined({token, value, rows}) + }) + const fetchClauseRule = $.RULE<() => FetchClauseAst>('fetchClauseRule', () => { + const token = tokenInfo($.CONSUME(Fetch)) + const first = $.OR([ + {ALT: () => ({kind: 'First' as const, token: tokenInfo($.CONSUME(First))})}, + {ALT: () => ({kind: 'Next' as const, token: tokenInfo($.CONSUME(Next))})}, + ]) + const value = $.OR2([ + {ALT: () => $.SUBRULE($.integerRule)}, + {ALT: () => $.SUBRULE($.parameterRule)}, + ]) + const rows = $.OR3([ + {ALT: () => ({kind: 'Row' as const, token: tokenInfo($.CONSUME(Row))})}, + {ALT: () => ({kind: 'Rows' as const, token: tokenInfo($.CONSUME(Rows))})}, + ]) + const mode = $.OR4([ + {ALT: () => ({kind: 'Only' as const, token: tokenInfo($.CONSUME(Only))})}, + {ALT: () => ({kind: 'WithTies' as const, token: tokenInfo2($.CONSUME(With), $.CONSUME(Ties))})}, + ]) + return {token, first, value, rows, mode} + }) + + const indexColumnRule = $.RULE<() => IndexColumnAst>('indexColumnRule', () => { + const expr = $.SUBRULE($.expressionRule) + const collation = $.OPTION(() => ({token: tokenInfo($.CONSUME(Collate)), name: $.SUBRULE3($.identifierRule)})) + const order = $.OPTION2(() => $.SUBRULE(sortOrderRule)) + const nulls = $.OPTION3(() => $.SUBRULE(sortNullsRule)) + return removeUndefined({...expr, collation, order, nulls}) + }) + const sortOrderRule = $.RULE<() => SortOrderAst>('sortOrderRule', () => { + return $.OR([ + {ALT: () => ({kind: 'Asc' as const, token: tokenInfo($.CONSUME(Asc))})}, + {ALT: () => ({kind: 'Desc' as const, token: tokenInfo($.CONSUME(Desc))})}, + ]) + }) + const sortNullsRule = $.RULE<() => SortNullsAst>('sortNullsRule', () => { + const nulls = $.CONSUME(Nulls) + return $.OR([ + {ALT: () => ({kind: 'First' as const, token: tokenInfo2(nulls, $.CONSUME(First))})}, + {ALT: () => ({kind: 'Last' as const, token: tokenInfo2(nulls, $.CONSUME(Last))})}, + ]) + }) + + this.tableColumnRule = $.RULE<() => TableColumnAst>('tableColumnRule', () => { + const name = $.SUBRULE($.identifierRule) + const type = $.SUBRULE($.columnTypeRule) + const constraints: TableColumnConstraintAst[] = [] + $.MANY(() => constraints.push($.SUBRULE(tableColumnConstraintRule))) + return removeEmpty({name, type, constraints: constraints.filter(isNotUndefined)}) + }) + const tableColumnConstraintRule = $.RULE<() => TableColumnConstraintAst>('tableColumnConstraintRule', () => $.OR([ + {ALT: () => $.SUBRULE(tableColumnNullableRule)}, + {ALT: () => $.SUBRULE(tableColumnDefaultRule)}, + {ALT: () => $.SUBRULE(tableColumnPkRule)}, + {ALT: () => $.SUBRULE(tableColumnUniqueRule)}, + {ALT: () => $.SUBRULE(tableColumnCheckRule)}, + {ALT: () => $.SUBRULE(tableColumnFkRule)}, + ])) + const tableColumnNullableRule = $.RULE<() => TableColumnNullableAst>('tableColumnNullableRule', () => { + const constraint = $.OPTION(() => $.SUBRULE(constraintNameRule)) + const not = $.OPTION2(() => $.CONSUME(Not)) + const nullable = $.CONSUME(Null) + return removeUndefined({kind: 'Nullable' as const, constraint, token: tokenInfo2(not, nullable), value: !not}) + }) + const tableColumnDefaultRule = $.RULE<() => TableColumnDefaultAst>('tableColumnDefaultRule', () => { + const constraint = $.OPTION(() => $.SUBRULE(constraintNameRule)) + const token = tokenInfo($.CONSUME(Default)) + const expression = $.SUBRULE(atomicExpressionRule) + return removeUndefined({kind: 'Default' as const, constraint, token, expression}) + }) + const tableColumnPkRule = $.RULE<() => TableColumnPkAst>('tableColumnPkRule', () => { + const constraint = $.OPTION(() => $.SUBRULE(constraintNameRule)) + const token = tokenInfo($.CONSUME(PrimaryKey)) + return removeUndefined({kind: 'PrimaryKey' as const, constraint, token}) + }) + const tableColumnUniqueRule = $.RULE<() => TableColumnUniqueAst>('tableColumnUniqueRule', () => { + const constraint = $.OPTION(() => $.SUBRULE(constraintNameRule)) + const token = tokenInfo($.CONSUME(Unique)) + return removeUndefined({kind: 'Unique' as const, constraint, token}) + }) + const tableColumnCheckRule = $.RULE<() => TableColumnCheckAst>('tableColumnCheckRule', () => { + const constraint = $.OPTION(() => $.SUBRULE(constraintNameRule)) + const token = tokenInfo($.CONSUME(Check)) + $.CONSUME(ParenLeft) + const predicate = $.SUBRULE($.expressionRule) + $.CONSUME(ParenRight) + return removeUndefined({kind: 'Check' as const, constraint, token, predicate}) + }) + const tableColumnFkRule = $.RULE<() => TableColumnFkAst>('tableColumnFkRule', () => { + const constraint = $.OPTION(() => $.SUBRULE(constraintNameRule)) + const token = tokenInfo($.CONSUME(References)) + const object = $.SUBRULE($.objectNameRule) + const column = $.OPTION2(() => { + $.CONSUME(ParenLeft) + const column = $.SUBRULE($.identifierRule) + $.CONSUME(ParenRight) + return column + }) + const onUpdate = $.OPTION3(() => ({token: tokenInfo2($.CONSUME(On), $.CONSUME(Update)), ...$.SUBRULE(foreignKeyActionsRule)})) + const onDelete = $.OPTION4(() => ({token: tokenInfo2($.CONSUME2(On), $.CONSUME(Delete)), ...$.SUBRULE2(foreignKeyActionsRule)})) + return removeUndefined({kind: 'ForeignKey' as const, constraint, token, schema: object.schema, table: object.name, column, onUpdate, onDelete}) + }) + + this.tableConstraintRule = $.RULE<() => TableConstraintAst>('tableConstraintRule', () => $.OR([ + {ALT: () => $.SUBRULE(tablePkRule)}, + {ALT: () => $.SUBRULE(tableUniqueRule)}, + {ALT: () => $.SUBRULE(tableCheckRule)}, + {ALT: () => $.SUBRULE(tableFkRule)}, + ])) + const tablePkRule = $.RULE<() => TablePkAst>('tablePkRule', () => { + const constraint = $.OPTION(() => $.SUBRULE(constraintNameRule)) + const token = tokenInfo($.CONSUME(PrimaryKey)) + const columns = $.SUBRULE(columnNamesRule) + return removeUndefined({kind: 'PrimaryKey' as const, constraint, token, columns}) + }) + const tableUniqueRule = $.RULE<() => TableUniqueAst>('tableUniqueRule', () => { + const constraint = $.OPTION(() => $.SUBRULE(constraintNameRule)) + const token = tokenInfo($.CONSUME(Unique)) + const columns = $.SUBRULE(columnNamesRule) + return removeUndefined({kind: 'Unique' as const, constraint, token, columns}) + }) + const tableCheckRule = tableColumnCheckRule // exactly the same ^^ + const tableFkRule = $.RULE<() => TableFkAst>('tableFkRule', () => { + const constraint = $.OPTION(() => $.SUBRULE(constraintNameRule)) + const token = tokenInfo($.CONSUME(ForeignKey)) + const columns = $.SUBRULE(columnNamesRule) + const refToken = $.CONSUME(References) + const refTable = $.SUBRULE($.objectNameRule) + const refColumns = $.OPTION2(() => $.SUBRULE2(columnNamesRule)) + const onUpdate = $.OPTION3(() => ({token: tokenInfo2($.CONSUME(On), $.CONSUME(Update)), ...$.SUBRULE(foreignKeyActionsRule)})) + const onDelete = $.OPTION4(() => ({token: tokenInfo2($.CONSUME2(On), $.CONSUME(Delete)), ...$.SUBRULE2(foreignKeyActionsRule)})) + const ref = removeUndefined({token: tokenInfo(refToken), schema: refTable.schema, table: refTable.name, columns: refColumns}) + return removeUndefined({kind: 'ForeignKey' as const, constraint, token, columns, ref, onUpdate, onDelete}) + }) + + const createTypeStructAttrs = $.RULE<() => TypeColumnAst[]>('createTypeStructAttrs', () => { + $.CONSUME(ParenLeft) + const attrs: TypeColumnAst[] = [] + $.AT_LEAST_ONE_SEP({SEP: Comma, DEF: () => attrs.push(removeUndefined({ + name: $.SUBRULE($.identifierRule), + type: $.SUBRULE($.columnTypeRule), + collation: $.OPTION(() => ({token: tokenInfo($.CONSUME(Collate)), name: $.SUBRULE2($.identifierRule)})) + }))}) + $.CONSUME(ParenRight) + return attrs.filter(isNotUndefined) + }) + const createTypeEnumValues = $.RULE<() => StringAst[]>('createTypeEnumValues', () => { + $.CONSUME(ParenLeft) + const values: StringAst[] = [] + $.AT_LEAST_ONE_SEP({SEP: Comma, DEF: () => values.push($.SUBRULE($.stringRule))}) + $.CONSUME(ParenRight) + return values + }) + const createTypeBase = $.RULE<() => {name: IdentifierAst, value: ExpressionAst}[]>('createTypeBase', () => { + $.CONSUME(ParenLeft) + const params: {name: IdentifierAst, value: ExpressionAst}[] = [] + $.AT_LEAST_ONE_SEP({SEP: Comma, DEF: () => { + const name = $.SUBRULE($.identifierRule) + $.CONSUME(Equal) + const value = $.SUBRULE($.expressionRule) + params.push({name, value}) + }}) + $.CONSUME(ParenRight) + return params + }) + + const constraintNameRule = $.RULE<() => NameAst>('constraintNameRule', () => ({token: tokenInfo($.CONSUME(Constraint)), name: $.SUBRULE($.identifierRule)})) + const foreignKeyActionsRule = $.RULE<() => ForeignKeyActionAst>('foreignKeyActionsRule', () => { + const action = $.OR([ + {ALT: () => ({kind: 'NoAction' as const, token: tokenInfo($.CONSUME(NoAction))})}, + {ALT: () => ({kind: 'Restrict' as const, token: tokenInfo($.CONSUME(Restrict))})}, + {ALT: () => ({kind: 'Cascade' as const, token: tokenInfo($.CONSUME(Cascade))})}, + {ALT: () => ({kind: 'SetNull' as const, token: tokenInfo2($.CONSUME(Set), $.CONSUME(Null))})}, + {ALT: () => ({kind: 'SetDefault' as const, token: tokenInfo2($.CONSUME2(Set), $.CONSUME(Default))})}, + ]) + const columns = $.OPTION(() => $.SUBRULE(columnNamesRule)) + return removeEmpty({action, columns}) + }) + const columnNamesRule = $.RULE<() => IdentifierAst[]>('columnNamesRule', () => { + $.CONSUME(ParenLeft) + const columns: IdentifierAst[] = [] + $.AT_LEAST_ONE_SEP({SEP: Comma, DEF: () => columns.push($.SUBRULE($.identifierRule))}) + $.CONSUME(ParenRight) + return columns.filter(isNotUndefined) + }) + + const ifExistsRule = $.RULE<() => {token: TokenInfo} | undefined>('ifExistsRule', () => $.OPTION(() => ({token: tokenInfo2($.CONSUME(If), $.CONSUME(Exists))}))) + const ifNotExistsRule = $.RULE<() => {token: TokenInfo} | undefined>('ifNotExistsRule', () => $.OPTION(() => ({token: tokenInfo3($.CONSUME(If), $.CONSUME(Not), $.CONSUME(Exists))}))) + + // basic parts + + this.aliasRule = $.RULE<() => AliasAst>('aliasRule', () => { + const token = $.OPTION(() => tokenInfo($.CONSUME(As))) + const name = $.SUBRULE($.identifierRule) + return removeUndefined({token, name}) + }) + + this.expressionRule = $.RULE<() => ExpressionAst>('expressionRule', () => $.SUBRULE(orExpressionRule)) // Start with OR expressions (lowest precedence) + const orExpressionRule = $.RULE<() => ExpressionAst>('orExpressionRule', () => { + let expr = $.SUBRULE(andExpressionRule) // AND has higher precedence than OR + $.MANY(() => { + const op = {kind: 'Or' as const, token: tokenInfo($.CONSUME(Or))} + const right = $.SUBRULE2(andExpressionRule) + expr = {kind: 'Operation', left: expr, op, right} + }) + return expr + }) + const andExpressionRule = $.RULE<() => ExpressionAst>('andExpressionRule', () => { + let expr = $.SUBRULE(operatorExpressionRule) // operator has higher precedence than AND + $.MANY(() => { + const op = {kind: 'And' as const, token: tokenInfo($.CONSUME(And))} + const right = $.SUBRULE2(operatorExpressionRule) + expr = {kind: 'Operation', left: expr, op, right} + }) + return expr + }) + const operatorExpressionRule = $.RULE<() => ExpressionAst>('operatorExpressionRule', () => { + let expr = $.SUBRULE(unaryExpressionRule) // unary has higher precedence than other operators + $.MANY(() => { + const op = $.SUBRULE(operatorRule) + if (['In', 'NotIn'].includes(op?.kind)) { + const right = $.SUBRULE(listRule) + expr = {kind: 'Operation', left: expr, op, right} + } else { + const right = $.SUBRULE2(unaryExpressionRule) + expr = {kind: 'Operation', left: expr, op, right} + } + }) + return expr + }) + const unaryExpressionRule = $.RULE<() => ExpressionAst>('unaryExpressionRule', () => $.OR([ + {ALT: () => { + const opLeft = $.SUBRULE(operatorLeftRule) + const expr = $.SUBRULE(atomicExpressionRule) // atomic has the highest precedence + return {kind: 'OperationLeft', op: opLeft, right: expr} + }}, + {ALT: () => { + const expr = $.SUBRULE2(atomicExpressionRule) // atomic has the highest precedence + const opRight = $.OPTION(() => $.SUBRULE(operatorRightRule)) + return opRight ? {kind: 'OperationRight', left: expr, op: opRight} : expr + }}, + ])) + const atomicExpressionRule = $.RULE<() => ExpressionAst>('atomicExpressionRule', () => { + const expr = $.OR([ + {ALT: () => $.SUBRULE(groupRule)}, + {ALT: () => $.SUBRULE($.literalRule)}, + {ALT: () => ({kind: 'Wildcard', token: tokenInfo($.CONSUME(Asterisk))})}, + {ALT: () => { + const token = tokenInfo($.CONSUME(Array)) + $.CONSUME(BracketLeft) + const items: ExpressionAst[] = [] + $.MANY_SEP({SEP: Comma, DEF: () => items.push($.SUBRULE($.expressionRule))}) + $.CONSUME(BracketRight) + return {kind: 'Array' as const, token, items} + }}, + {ALT: () => { + const first = $.SUBRULE($.identifierRule) + const nest = $.OPTION(() => $.OR2([ + {ALT: () => removeUndefined({kind: 'Function', function: first, ...($.SUBRULE(functionParamsRule) || {})})}, + {ALT: () => { + $.CONSUME(Dot) + return $.OR3([ + {ALT: () => ({kind: 'Wildcard', table: first, token: tokenInfo($.CONSUME2(Asterisk))})}, + {ALT: () => { + const second = $.SUBRULE2($.identifierRule) + const nest2 = $.OPTION2(() => $.OR4([ + {ALT: () => removeUndefined({kind: 'Function', schema: first, function: second, ...($.SUBRULE2(functionParamsRule) || {})})}, + {ALT: () => { + $.CONSUME2(Dot) + return $.OR5([ + {ALT: () => ({kind: 'Wildcard', schema: first, table: second, token: tokenInfo($.CONSUME3(Asterisk))})}, + {ALT: () => removeEmpty({kind: 'Column', schema: first, table: second, column: $.SUBRULE3($.identifierRule), json: $.SUBRULE3(columnJsonRule)})}, + ]) + }} + ])) + return nest2 ? nest2 : removeEmpty({kind: 'Column', table: first, column: second, json: $.SUBRULE2(columnJsonRule)}) + }} + ]) + }} + ])) + return nest ? nest : removeEmpty({kind: 'Column', column: first, json: $.SUBRULE(columnJsonRule)}) + }} + ]) + const cast = $.OPTION3(() => ({token: tokenInfo2($.CONSUME(Colon), $.CONSUME2(Colon)), type: $.SUBRULE($.columnTypeRule)})) + return removeUndefined({...expr, cast}) + }) + const operatorRule = $.RULE<() => OperatorAst>('operatorRule', () => $.OR([ + // https://www.postgresql.org/docs/current/functions.html + {ALT: () => ({kind: '+' as const, token: tokenInfo($.CONSUME(Plus))})}, + {ALT: () => ({kind: '-' as const, token: tokenInfo($.CONSUME(Dash))})}, + {ALT: () => ({kind: '*' as const, token: tokenInfo($.CONSUME(Asterisk))})}, + {ALT: () => ({kind: '/' as const, token: tokenInfo($.CONSUME(Slash))})}, + {ALT: () => ({kind: '%' as const, token: tokenInfo($.CONSUME(Percent))})}, + {ALT: () => ({kind: '^' as const, token: tokenInfo($.CONSUME(Caret))})}, + {ALT: () => ({kind: '&' as const, token: tokenInfo($.CONSUME(Amp))})}, + {ALT: () => ({kind: '#' as const, token: tokenInfo($.CONSUME(Hash))})}, + {ALT: () => ({kind: '=' as const, token: tokenInfo($.CONSUME(Equal))})}, + {ALT: () => ({kind: 'In' as const, token: tokenInfo($.CONSUME(In))})}, + {ALT: () => ({kind: 'Like' as const, token: tokenInfo($.CONSUME(Like))})}, + {ALT: () => { + const lt = $.CONSUME(LowerThan) + const res = $.OPTION(() => $.OR2([ + {ALT: () => ({kind: '<=' as const, token: tokenInfo2(lt, $.CONSUME2(Equal))})}, + {ALT: () => ({kind: '<<' as const, token: tokenInfo2(lt, $.CONSUME2(LowerThan))})}, + {ALT: () => ({kind: '<>' as const, token: tokenInfo2(lt, $.CONSUME(GreaterThan))})}, + ])) + return res ? res : {kind: '<' as const, token: tokenInfo(lt)} + }}, + {ALT: () => { + const gt = $.CONSUME2(GreaterThan) + const res = $.OPTION2(() => $.OR3([ + {ALT: () => ({kind: '>=' as const, token: tokenInfo2(gt, $.CONSUME3(Equal))})}, + {ALT: () => ({kind: '>>' as const, token: tokenInfo2(gt, $.CONSUME3(GreaterThan))})}, + ])) + return res ? res : {kind: '>' as const, token: tokenInfo(gt)} + }}, + {ALT: () => { + const pipe = $.CONSUME(Pipe) + const res = $.OPTION3(() => ({kind: '||' as const, token: tokenInfo2(pipe, $.CONSUME2(Pipe))})) + return res ? res : {kind: '|' as const, token: tokenInfo(pipe)} + }}, + {ALT: () => { + const tilde = $.CONSUME(Tilde) + const res = $.OPTION4(() => ({kind: '~*' as const, token: tokenInfo2(tilde, $.CONSUME2(Asterisk))})) + return res ? res : {kind: '~' as const, token: tokenInfo(tilde)} + }}, + {ALT: () => { + const exclamation = $.CONSUME(Exclamation) + return $.OR4([ + {ALT: () => ({kind: '!=' as const, token: tokenInfo2(exclamation, $.CONSUME4(Equal))})}, + {ALT: () => { + const tilde = $.CONSUME2(Tilde) + const res = $.OPTION5(() => ({kind: '!~*' as const, token: tokenInfo3(exclamation, tilde, $.CONSUME3(Asterisk))})) + return res ? res : {kind: '!~' as const, token: tokenInfo2(exclamation, tilde)} + }} + ]) + }}, + {ALT: () => { + const not = $.CONSUME2(Not) + return $.OPTION6(() => $.OR5([ + {ALT: () => ({kind: 'NotLike' as const, token: tokenInfo2(not, $.CONSUME2(Like))})}, + {ALT: () => ({kind: 'NotIn' as const, token: tokenInfo2(not, $.CONSUME2(In))})}, + ])) + }}, + {ALT: () => { + const is = $.CONSUME(Is) + const not = $.OPTION7(() => $.CONSUME3(Not)) + const distinctFrom = $.OPTION8(() => { $.CONSUME(Distinct); return $.CONSUME(From) }) + return distinctFrom ? + (not ? {kind: 'NotDistinctFrom' as const, token: tokenInfo2(is, distinctFrom)} : {kind: 'DistinctFrom' as const, token: tokenInfo2(is, distinctFrom)}) : + (not ? {kind: 'IsNot' as const, token: tokenInfo2(is, not)} : {kind: 'Is' as const, token: tokenInfo(is)}) + }}, + ])) + const operatorLeftRule = $.RULE<() => OperatorLeftAst>('operatorLeftRule', () => $.OR([ + {ALT: () => ({kind: 'Not' as const, token: tokenInfo($.CONSUME(Not))})}, + {ALT: () => ({kind: 'Interval' as const, token: tokenInfo($.CONSUME(Interval))})}, + {ALT: () => ({kind: '~' as const, token: tokenInfo($.CONSUME(Tilde))})}, + ])) + const operatorRightRule = $.RULE<() => OperatorRightAst>('operatorRightRule', () => $.OR([ + {ALT: () => ({kind: 'IsNull' as const, token: tokenInfo($.CONSUME(IsNull))})}, + {ALT: () => ({kind: 'NotNull' as const, token: tokenInfo($.CONSUME(NotNull))})}, + ])) + const groupRule = $.RULE<() => GroupAst>('groupRule', () => { + $.CONSUME(ParenLeft) + const expression = $.SUBRULE($.expressionRule) + $.CONSUME(ParenRight) + return {kind: 'Group', expression} + }) + const listRule = $.RULE<() => ListAst>('listRule', () => { + $.CONSUME(ParenLeft) + const items: LiteralAst[] = [] + $.AT_LEAST_ONE_SEP({SEP: Comma, DEF: () => items.push($.SUBRULE($.literalRule))}) + $.CONSUME(ParenRight) + return {kind: 'List', items} + }) + const columnJsonRule = $.RULE<() => ColumnJsonAst[]>('columnJsonRule', () => { + const res: ColumnJsonAst[] = [] + $.MANY({DEF: () => { + const nest = $.SUBRULE(jsonOpRule) + const field = $.OR([ + {ALT: () => $.SUBRULE($.stringRule)}, + {ALT: () => $.SUBRULE($.parameterRule)} + ]) + res.push({...nest, field}) + }}) + return res + }) + const jsonOpRule = $.RULE<() => { kind: JsonOp, token: TokenInfo }>('jsonOpRule', () => { + // https://www.postgresql.org/docs/current/functions-json.html + const kind = $.OR([ + {ALT: () => ({dash: true, token: $.CONSUME(Dash)})}, + {ALT: () => ({dash: false, token: $.CONSUME(Hash)})}, + ]) + const gt = $.CONSUME(GreaterThan) + const gt2 = $.OPTION(() => $.CONSUME2(GreaterThan)) + return kind.dash ? + (gt2 ? {kind: '->>' as const, token: tokenInfo3(kind.token, gt, gt2)} : {kind: '->' as const, token: tokenInfo2(kind.token, gt)}) : + (gt2 ? {kind: '#>>' as const, token: tokenInfo3(kind.token, gt, gt2)} : {kind: '#>' as const, token: tokenInfo2(kind.token, gt)}) + }) + const functionParamsRule = $.RULE<() => { distinct?: {token: TokenInfo}, parameters: ExpressionAst[] }>('functionParamsRule', () => { + $.CONSUME(ParenLeft) + const distinct = $.OPTION(() => ({token: tokenInfo($.CONSUME(Distinct))})) + const parameters: ExpressionAst[] = [] + $.MANY_SEP({SEP: Comma, DEF: () => parameters.push($.SUBRULE($.expressionRule))}) + $.CONSUME(ParenRight) + return {distinct, parameters: parameters.filter(isNotUndefined)} + }) + + this.objectNameRule = $.RULE<() => ObjectNameAst>('objectNameRule', () => { + const first = $.SUBRULE($.identifierRule) + const second = $.OPTION(() => { + $.CONSUME(Dot) + return $.SUBRULE2($.identifierRule) + }) + return second ? {schema: first, name: second} : {name: first} + }) + + this.columnTypeRule = $.RULE<() => ColumnTypeAst>('columnTypeRule', () => { + const schema = $.OPTION(() => { + const s = $.SUBRULE($.identifierRule) + $.CONSUME(Dot) + return s + }) + const parts: {name: IdentifierAst, args?: IntegerAst[], last?: TokenInfo}[] = [] + $.AT_LEAST_ONE({DEF: () => parts.push($.OR([ + {ALT: () => { + const name = $.SUBRULE2($.identifierRule) + const params = $.OPTION2(() => { + $.CONSUME(ParenLeft) + const values: IntegerAst[] = [] + $.AT_LEAST_ONE_SEP({SEP: Comma, DEF: () => values.push($.SUBRULE($.integerRule))}) + const last = tokenInfo($.CONSUME(ParenRight)) + return {values, last} + }) + return {name, args: params?.values, last: params?.last} + }}, + {ALT: () => ({name: toIdentifier($.CONSUME(With))})}, // needed for `with time zone` + ]))}) + const array = $.OPTION3(() => ({token: tokenInfo2($.CONSUME(BracketLeft), $.CONSUME(BracketRight))})) + const name = { + kind: 'Identifier' as const, + token: mergePositions(parts.flatMap(p => [p.name?.token, p.last]).concat([array?.token])), + value: parts.filter(isNotUndefined).map(p => p.name?.value + (p.args ? `(${p.args.map(v => v.value).join(', ')})` : '')).join(' ') + (array ? '[]' : '') + } + return removeEmpty({schema, name, args: parts.flatMap(p => p.args || []), array, token: mergePositions([schema?.token, name.token])}) + }) + + this.literalRule = $.RULE<() => LiteralAst>('literalRule', () => $.OR([ + {ALT: () => $.SUBRULE($.stringRule)}, + {ALT: () => $.SUBRULE($.decimalRule)}, + {ALT: () => $.SUBRULE($.integerRule)}, + {ALT: () => $.SUBRULE($.booleanRule)}, + {ALT: () => $.SUBRULE($.nullRule)}, + {ALT: () => $.SUBRULE($.parameterRule)}, + ])) + + // elements + + this.parameterRule = $.RULE<() => ParameterAst>('parameterRule', () => $.OR([ + {ALT: () => ({kind: 'Parameter', value: '?', token: tokenInfo($.CONSUME(QuestionMark))})}, + {ALT: () => { + const d = $.CONSUME(Dollar) + const i = $.CONSUME(Integer) + return {kind: 'Parameter', value: `${d.image}${i.image}`, index: parseInt(i.image), token: tokenInfo2(d, i)} + }} + ])) + + this.identifierRule = $.RULE<() => IdentifierAst>('identifierRule', () => $.OR([ + {ALT: () => { + const token = $.CONSUME(Identifier) + if (token.image.startsWith('"') && token.image.endsWith('"')) { + return {kind: 'Identifier', token: tokenInfo(token), value: token.image.slice(1, -1).replaceAll(/\\"/g, '"'), quoted: true} + } else { + return {kind: 'Identifier', token: tokenInfo(token), value: token.image} + } + }}, + // tokens allowed as identifiers: + {ALT: () => toIdentifier($.CONSUME(Add))}, + {ALT: () => toIdentifier($.CONSUME(Comment))}, + {ALT: () => toIdentifier($.CONSUME(Commit))}, + {ALT: () => toIdentifier($.CONSUME(Data))}, + {ALT: () => toIdentifier($.CONSUME(Database))}, + {ALT: () => toIdentifier($.CONSUME(Deferrable))}, + {ALT: () => toIdentifier($.CONSUME(Domain))}, + {ALT: () => toIdentifier($.CONSUME(Extension))}, + {ALT: () => toIdentifier($.CONSUME(Increment))}, + {ALT: () => toIdentifier($.CONSUME(Index))}, + {ALT: () => toIdentifier($.CONSUME(Input))}, + {ALT: () => toIdentifier($.CONSUME(New))}, + {ALT: () => toIdentifier($.CONSUME(Nulls))}, + {ALT: () => toIdentifier($.CONSUME(Old))}, + {ALT: () => toIdentifier($.CONSUME(Rows))}, + {ALT: () => toIdentifier($.CONSUME(Schema))}, + {ALT: () => toIdentifier($.CONSUME(Session))}, + {ALT: () => toIdentifier($.CONSUME(Start))}, + {ALT: () => toIdentifier($.CONSUME(Temporary))}, + {ALT: () => toIdentifier($.CONSUME(Trigger))}, + {ALT: () => toIdentifier($.CONSUME(Type))}, + {ALT: () => toIdentifier($.CONSUME(Version))}, + ])) + + this.stringRule = $.RULE<() => StringAst>('stringRule', () => $.OR([ + {ALT: () => { + const token = $.CONSUME(String) + if (token.image.match(/^E/i)) { + // https://www.postgresql.org/docs/current/sql-syntax-lexical.html + return {kind: 'String', token: tokenInfo(token), value: token.image.slice(2, -1).replaceAll(/''/g, "'"), escaped: true} + } else { + return {kind: 'String', token: tokenInfo(token), value: token.image.slice(1, -1).replaceAll(/''/g, "'")} + } + }}, + {ALT: () => { + // https://www.postgresql.org/docs/current/sql-syntax-lexical.html#SQL-SYNTAX-DOLLAR-QUOTING + const token = $.CONSUME(StringDollar) + const [, dollar] = token.image.match(/^(\$[^$]*\$)/) || [] + const prefix = dollar?.length || 0 + return {kind: 'String', token: tokenInfo(token), value: token.image.slice(prefix, -prefix).trim(), dollar} + }}, + ])) + + this.integerRule = $.RULE<() => IntegerAst>('integerRule', () => { + const neg = $.OPTION(() => $.CONSUME(Dash)) + const token = $.CONSUME(Integer) + return neg ? {kind: 'Integer', token: tokenInfo2(neg, token), value: parseInt(neg.image + token.image)} : + {kind: 'Integer', token: tokenInfo(token), value: parseInt(token.image)} + }) + + this.decimalRule = $.RULE<() => DecimalAst>('decimalRule', () => { + const neg = $.OPTION(() => $.CONSUME(Dash)) + const token = $.CONSUME(Decimal) + return neg ? {kind: 'Decimal', token: tokenInfo2(neg, token), value: parseFloat(neg.image + token.image)} : + {kind: 'Decimal', token: tokenInfo(token), value: parseFloat(token.image)} + }) + + this.booleanRule = $.RULE<() => BooleanAst>('booleanRule', () => $.OR([ + {ALT: () => ({kind: 'Boolean', token: tokenInfo($.CONSUME(True)), value: true})}, + {ALT: () => ({kind: 'Boolean', token: tokenInfo($.CONSUME(False)), value: false})}, + ])) + + this.nullRule = $.RULE<() => NullAst>('nullRule', () => ({kind: 'Null', token: tokenInfo($.CONSUME(Null))})) + + this.performSelfAnalysis() + } +} + +const lexer = new Lexer(allTokens) +const parserStrict = new PostgresParser(allTokens, false) +const parserWithRecovery = new PostgresParser(allTokens, true) + +// exported only for tests, use the `parse` function instead +export function parseRule(parse: (p: PostgresParser) => T, input: string, strict: boolean = false): ParserResult { + const lexingResult = lexer.tokenize(input) + const parser = strict ? parserStrict : parserWithRecovery + parser.input = lexingResult.tokens // "input" is a setter which will reset the parser's state. + const res = parse(parser) + const errors = lexingResult.errors.map(formatLexerError).concat(parser.errors.map(formatParserError)) + const comments = lexingResult.groups.comments.map(buildComment) + return new ParserResult(comments.length > 0 ? {...res, comments} : res, errors) +} + +export function parsePostgresAst(input: string, opts: { strict?: boolean } = {strict: false}): ParserResult { + return parseRule(p => p.statementsRule(), input, opts.strict || false) +} + +export function parsePostgresStatementAst(input: string, opts: { strict?: boolean } = {strict: false}): ParserResult { + return parseRule(p => p.statementRule(), input, opts.strict || false) +} + +function buildComment(token: IToken): CommentAst { + if (token.tokenType.name === 'LineComment') { + return {kind: 'line', token: tokenInfo(token), value: token.image.slice(2).trim()} + } else if (token.image.startsWith('/*\n *') && token.image.endsWith('\n */')) { + return {kind: 'doc', token: tokenInfo(token), value: token.image.slice(3, -4).split('\n').map(line => line.startsWith(' *') ? line.slice(2).trim() : line.trim()).join('\n')} + } else { + return {kind: 'block', token: tokenInfo(token), value: token.image.slice(2, -2).trim()} + } +} + +function formatLexerError(err: ILexingError): ParserError { + return { + message: err.message, + kind: 'LexingError', + level: ParserErrorLevel.enum.error, + offset: {start: err.offset, end: err.offset + err.length}, + position: { + start: {line: err.line || defaultPos, column: err.column || defaultPos}, + end: {line: err.line || defaultPos, column: (err.column || defaultPos) + err.length} + } + } +} + +function formatParserError(err: IRecognitionException): ParserError { + return {message: err.message, kind: err.name, level: ParserErrorLevel.enum.error, ...tokenInfo(err.token)} +} + +function toIdentifier(token: IToken): IdentifierAst { + return {kind: 'Identifier', token: tokenInfo(token), value: token.image} +} + +function tokenInfo(token: IToken, issues?: TokenIssue[]): TokenInfo { + return removeEmpty({...tokenPosition(token), issues}) +} + +function tokenInfo2(start: IToken | undefined, end: IToken | undefined, issues?: TokenIssue[]): TokenInfo { + return removeEmpty({...mergePositions([start, end].map(t => t ? tokenPosition(t) : undefined)), issues}) +} + +function tokenInfo3(start: IToken | undefined, middle: IToken | undefined, end: IToken | undefined, issues?: TokenIssue[]): TokenInfo { + return removeEmpty({...mergePositions([start, middle, end].map(t => t ? tokenPosition(t) : undefined)), issues}) +} + +function tokenInfoN(tokens: (IToken | undefined)[], issues?: TokenIssue[]): TokenInfo { + return removeEmpty({...mergePositions(tokens.map(t => t ? tokenPosition(t) : undefined)), issues}) +} + +function tokenPosition(token: IToken): TokenPosition { + return { + offset: {start: pos(token.startOffset), end: pos(token.endOffset)}, + position: { + start: {line: pos(token.startLine), column: pos(token.startColumn)}, + end: {line: pos(token.endLine), column: pos(token.endColumn)} + } + } +} + +function pos(value: number | undefined): number { + return value !== undefined && !Number.isNaN(value) ? value : defaultPos +} diff --git a/libs/parser-sql/src/sql.test.ts b/libs/parser-sql/src/sql.test.ts index 296b39095..642219d57 100644 --- a/libs/parser-sql/src/sql.test.ts +++ b/libs/parser-sql/src/sql.test.ts @@ -1,6 +1,16 @@ import {describe, test} from "@jest/globals"; import {parseSql} from "./sql"; +/* +SQLite: +https://github.com/duplicati/duplicati/blob/master/Duplicati/Library/Main/Database/Database%20schema/Schema.sql +https://github.com/wikimedia/mediawiki/blob/0c37286a6f418e78ccd896020d9bd1c4d041f5e9/maintenance/tables-generated.sql +Prisma: +https://github.com/formbricks/formbricks/blob/main/packages/database/schema.prisma +https://github.com/useplunk/plunk/blob/main/prisma/schema.prisma +https://github.com/AmruthPillai/Reactive-Resume/blob/main/tools/prisma/schema.prisma +https://github.com/umami-software/umami/blob/master/db/postgresql/schema.prisma + */ describe('sql', () => { describe('select', () => { test.skip('basic', async () => { diff --git a/libs/parser-sql/src/sql.ts b/libs/parser-sql/src/sql.ts index 6651a2773..b5b20c9b0 100644 --- a/libs/parser-sql/src/sql.ts +++ b/libs/parser-sql/src/sql.ts @@ -4,9 +4,13 @@ import * as generator from "./generator"; import {SqlScript} from "./statements"; import {importDatabase} from "./sqlImport"; import {exportDatabase} from "./sqlExport"; +import {parsePostgresAst} from "./postgresParser"; +import {buildPostgresDatabase} from "./postgresBuilder"; import {generatePostgres, generatePostgresDiff} from "./postgresGenerator"; export function parseSql(content: string, dialect: DatabaseKind, opts: { strict?: boolean, context?: Database } = {}): ParserResult { + const start = Date.now() + if (dialect === DatabaseKind.enum.postgres) return parsePostgresAst(content, opts).flatMap(ast => buildPostgresDatabase(ast, start, Date.now())) return parseSqlScript(content).map(importDatabase) } diff --git a/libs/utils/src/array.ts b/libs/utils/src/array.ts index 49636dfba..70573faee 100644 --- a/libs/utils/src/array.ts +++ b/libs/utils/src/array.ts @@ -15,7 +15,23 @@ export const collectOne = (arr: T[], f: (t: T) => U | undefined): U | unde return undefined } -export const distinct = (arr: T[]): T[] => arr.filter((t, i) => arr.indexOf(t) === i) +export const distinct = (arr: T[]): T[] => { + const seen = new Set() + return arr.filter(t => { + if (seen.has(t)) return false + seen.add(t) + return true + }) +} +export const distinctBy = (arr: T[], by: (t: T) => string | number): T[] => { + const seen = new Set() + return arr.filter(t => { + const key = by(t) + if (seen.has(key)) return false + seen.add(key) + return true + }) +} export type Diff = {left: T[], right: T[], both: {left: T, right: T}[]} export const diffBy = (arr1: T[], arr2: T[], f: (t: T, i: number) => string): Diff => {