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: /})
+const ParenLeft = createToken({name: 'ParenLeft', pattern: /\(/})
+const ParenRight = createToken({name: 'ParenRight', pattern: /\)/})
+const Percent = createToken({name: 'Percent', pattern: /%/})
+const Pipe = createToken({name: 'Pipe', pattern: /\|/})
+const Plus = createToken({name: 'Plus', pattern: /\+/})
+const QuestionMark = createToken({name: 'QuestionMark', pattern: /\?/})
+const Semicolon = createToken({name: 'Semicolon', pattern: /;/})
+const Slash = createToken({name: 'Slash', pattern: /\//, longer_alt: BlockComment})
+const Tilde = createToken({name: 'Tilde', pattern: /~/})
+const charTokens: TokenType[] = [
+ Amp, Asterisk, BracketLeft, BracketRight, Caret, Colon, Comma, CurlyLeft, CurlyRight, Dash, Dollar, Dot, Equal, Exclamation,
+ GreaterThan, Hash, LowerThan, ParenLeft, ParenRight, Percent, Pipe, Plus, QuestionMark, Semicolon, Slash, Tilde
+]
+
+const allTokens: TokenType[] = [WhiteSpace, ...keywordTokens, ...charTokens, ...valueTokens]
+
+const defaultPos: number = -1 // used when error position is undefined
+
+class PostgresParser extends EmbeddedActionsParser {
+ // top level
+ statementsRule: () => 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 => {