Skip to content

Commit

Permalink
Add create type statement
Browse files Browse the repository at this point in the history
  • Loading branch information
loicknuchel committed Oct 18, 2024
1 parent 4cbcd39 commit 5cc6cf5
Show file tree
Hide file tree
Showing 3 changed files with 96 additions and 26 deletions.
7 changes: 5 additions & 2 deletions libs/parser-sql/src/postgresAst.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@ export type StatementsAst = { statements: StatementAst[] }
export type StatementAst = CommentStatementAst | CreateExtensionStatementAst | CreateTableStatementAst | DropStatementAst | SelectStatementAst | SetStatementAst
export type CommentStatementAst = { statement: 'Comment', object: { kind: CommentObject } & TokenInfo, schema?: IdentifierAst, parent?: IdentifierAst, entity: IdentifierAst, comment: StringAst | NullAst } & TokenInfo
export type CreateExtensionStatementAst = { statement: 'CreateExtension', ifNotExists?: TokenInfo, name: IdentifierAst, with?: TokenInfo, schema?: {name: IdentifierAst} & TokenInfo, version?: {number: StringAst | IdentifierAst} & TokenInfo, cascade?: TokenInfo } & TokenInfo
export type CreateTableStatementAst = { statement: 'CreateTable', name: TableAst, columns: TableColumnAst[], constraints?: TableConstraintAst[] }
export type CreateTableStatementAst = { statement: 'CreateTable', schema?: IdentifierAst, table: IdentifierAst, columns: TableColumnAst[], constraints?: TableConstraintAst[] } & TokenInfo
export type CreateTypeStatementAst = { statement: 'CreateType', schema?: IdentifierAst, type: IdentifierAst, struct?: {attrs: TypeColumnAst[]} & TokenInfo, enum?: {values: StringAst[]} & TokenInfo } & TokenInfo
export type DropStatementAst = { statement: 'Drop', object: { kind: DropObject } & TokenInfo, entities: TableAst[], concurrently?: TokenInfo, ifExists?: TokenInfo, mode?: { kind: DropMode } & TokenInfo } & TokenInfo
export type SelectStatementAst = { statement: 'Select', select: SelectClauseAst, from?: FromClauseAst, where?: WhereClauseAst } & TokenInfo
export type SetStatementAst = { statement: 'Set', scope?: { kind: SetScope } & TokenInfo, parameter: IdentifierAst, equal: { kind: SetAssign } & TokenInfo, value: SetValueAst } & TokenInfo
Expand All @@ -23,7 +24,8 @@ export type SelectClauseAst = { expressions: SelectClauseExprAst[] } & TokenInfo
export type SelectClauseExprAst = ExpressionAst & { alias?: AliasAst }
export type FromClauseAst = { table: IdentifierAst, alias?: AliasAst } & TokenInfo
export type WhereClauseAst = { condition: ConditionAst } & TokenInfo
export type TableColumnAst = { name: IdentifierAst, type: IdentifierAst, constraints?: TableColumnConstraintAst[] }
export type TypeColumnAst = { name: IdentifierAst, type: ColumnTypeAst, collation?: {name: IdentifierAst} & TokenInfo }
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 } & ConstraintCommonAst
export type TableColumnDefaultAst = { kind: 'Default', expression: ExpressionAst } & ConstraintCommonAst
Expand All @@ -38,6 +40,7 @@ export type TableCheckAst = { kind: 'Check', predicate: ConditionAst } & Constra
export type TableFkAst = { kind: 'ForeignKey', columns: IdentifierAst[], ref: {schema?: IdentifierAst, table: IdentifierAst, columns?: IdentifierAst[]} & TokenInfo, onUpdate?: ForeignKeyActionAst & TokenInfo, onDelete?: ForeignKeyActionAst & TokenInfo } & ConstraintCommonAst
export type ConstraintCommonAst = { constraint?: ConstraintNameAst } & TokenInfo
export type ConstraintNameAst = { name: IdentifierAst } & TokenInfo
export type ColumnTypeAst = IdentifierAst
export type ForeignKeyActionAst = {action: {kind: ForeignKeyAction} & TokenInfo, columns?: IdentifierAst[]}
export type SetValueAst = IdentifierAst | LiteralAst | (IdentifierAst | LiteralAst)[] | { kind: 'Default' } & TokenInfo

Expand Down
35 changes: 32 additions & 3 deletions libs/parser-sql/src/postgresParser.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import {
import {parsePostgresAst, parseRule} from "./postgresParser";

describe('postgresParser', () => {
// CREATE VIEW/MATERIALIZED VIEW/INDEX/TYPE
// CREATE VIEW/MATERIALIZED VIEW/INDEX
// INSERT INTO
// UPDATE
// DELETE
Expand Down Expand Up @@ -104,7 +104,7 @@ describe('postgresParser', () => {
test('simplest', () => {
expect(parsePostgresAst('CREATE TABLE users (id int PRIMARY KEY, name VARCHAR);')).toEqual({result: {statements: [{
statement: 'CreateTable',
name: {table: identifier('users', 13, 17)},
table: identifier('users', 13, 17),
columns: [
{name: identifier('id', 20, 21), type: identifier('int', 23, 25), constraints: [{kind: 'PrimaryKey', ...token(27, 37)}]},
{name: identifier('name', 40, 43), type: identifier('VARCHAR', 45, 51)},
Expand All @@ -115,7 +115,7 @@ describe('postgresParser', () => {
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: [{
statement: 'CreateTable',
name: {table: identifier('users', 13, 17)},
table: identifier('users', 13, 17),
columns: [
{name: identifier('id', 20, 21), type: identifier('int', 23, 25)},
{name: identifier('role', 28, 31), type: identifier('VARCHAR', 33, 39)},
Expand All @@ -130,6 +130,35 @@ describe('postgresParser', () => {
// TODO: CREATE TABLE IF NOT EXISTS users (id int);
// TODO: CREATE UNLOGGED TABLE users (id int);
})
describe('createTypeStatement', () => {
test('simplest', () => {
expect(parsePostgresAst('CREATE TYPE position;')).toEqual({result: {statements: [{
statement: 'CreateType',
type: identifier('position', 12, 19),
...token(0, 20)
}]}})
})
test('struct', () => {
expect(parsePostgresAst('CREATE TYPE layout_position AS (x int, y int COLLATE "fr_FR");')).toEqual({result: {statements: [{
statement: 'CreateType',
type: identifier('layout_position', 12, 26),
struct: {...token(28, 29), attrs: [
{name: identifier('x', 32, 32), type: identifier('int', 34, 36)},
{name: identifier('y', 39, 39), type: identifier('int', 41, 43), collation: {...token(45, 51), name: {...identifier('fr_FR', 53, 59), quoted: true}}}
]},
...token(0, 61)
}]}})
})
test('enum', () => {
expect(parsePostgresAst("CREATE TYPE public.bug_status AS ENUM ('open', 'closed');")).toEqual({result: {statements: [{
statement: 'CreateType',
schema: identifier('public', 12, 17),
type: identifier('bug_status', 19, 28),
enum: {...token(30, 36), values: [string('open', 39, 44), string('closed', 47, 54)]},
...token(0, 56)
}]}})
})
})
describe('dropStatement', () => {
test('simplest', () => {
expect(parsePostgresAst('DROP TABLE users;')).toEqual({result: {statements: [{
Expand Down
80 changes: 59 additions & 21 deletions libs/parser-sql/src/postgresParser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,15 @@ import {
AliasAst,
BooleanAst,
ColumnAst,
ColumnTypeAst,
ColumnWithTableAst,
CommentAst,
CommentStatementAst,
ConditionAst,
ConstraintNameAst,
CreateExtensionStatementAst,
CreateTableStatementAst,
CreateTypeStatementAst,
DecimalAst,
DropStatementAst,
ExpressionAst,
Expand Down Expand Up @@ -53,6 +55,7 @@ import {
TableUniqueAst,
TokenInfo,
TokenIssue,
TypeColumnAst,
WhereClauseAst
} from "./postgresAst";

Expand All @@ -70,6 +73,7 @@ const And = createToken({name: 'And', pattern: /\bAND\b/i, longer_alt: Identifie
const As = createToken({name: 'As', pattern: /\bAS\b/i, longer_alt: Identifier})
const Cascade = createToken({name: 'Cascade', pattern: /\bCASCADE\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 Concurrently = createToken({name: 'Concurrently', pattern: /\bCONCURRENTLY\b/i, longer_alt: Identifier})
Expand All @@ -81,6 +85,7 @@ const Delete = createToken({name: 'Delete', pattern: /\bDELETE\b/i, longer_alt:
const Distinct = createToken({name: 'Distinct', pattern: /\bDISTINCT\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 Enum = createToken({name: 'Enum', pattern: /\bENUM\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})
Expand Down Expand Up @@ -126,8 +131,8 @@ const Where = createToken({name: 'Where', pattern: /\bWHERE\b/i, longer_alt: Ide
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 keywordTokens: TokenType[] = [
And, As, Cascade, Check, Column, Comment, Concurrently, Constraint, Create, Default, Database, Delete, Distinct, Domain, Drop, Exists, Extension, False, Fetch, ForeignKey, From,
GroupBy, Having, If, Is, Index, Join, Like, Limit, Local, MaterializedView, NoAction, Not, Null, Offset, On, Or, OrderBy,
And, As, Cascade, Check, Collate, Column, Comment, Concurrently, Constraint, Create, Default, Database, Delete, Distinct, Domain, Drop, Enum, Exists, Extension,
False, Fetch, ForeignKey, From, GroupBy, Having, If, Is, Index, Join, Like, Limit, Local, MaterializedView, NoAction, Not, Null, Offset, On, Or, OrderBy,
PrimaryKey, References, Restrict, Schema, Select, Session, SetDefault, SetNull, Set, Table, To, True, Type, Union, Unique, Update, Version, View, Where, Window, With
]

Expand Down Expand Up @@ -158,6 +163,7 @@ class PostgresParser extends EmbeddedActionsParser {
commentStatementRule: () => CommentStatementAst
createExtensionStatementRule: () => CreateExtensionStatementAst
createTableStatementRule: () => CreateTableStatementAst
createTypeStatementRule: () => CreateTypeStatementAst
dropStatementRule: () => DropStatementAst
selectStatementRule: () => SelectStatementAst
setStatementRule: () => SetStatementAst
Expand All @@ -176,6 +182,7 @@ class PostgresParser extends EmbeddedActionsParser {
tableRule: () => TableAst
columnRule: () => ColumnAst
columnWithTableRule: () => ColumnWithTableAst
columnTypeRule: () => ColumnTypeAst
literalRule: () => LiteralAst
// elements
identifierRule: () => IdentifierAst
Expand Down Expand Up @@ -204,6 +211,7 @@ class PostgresParser extends EmbeddedActionsParser {
{ ALT: () => $.SUBRULE($.commentStatementRule) },
{ ALT: () => $.SUBRULE($.createExtensionStatementRule) },
{ ALT: () => $.SUBRULE($.createTableStatementRule) },
{ ALT: () => $.SUBRULE($.createTypeStatementRule) },
{ ALT: () => $.SUBRULE($.dropStatementRule) },
{ ALT: () => $.SUBRULE($.selectStatementRule) },
{ ALT: () => $.SUBRULE($.setStatementRule) },
Expand Down Expand Up @@ -267,7 +275,7 @@ class PostgresParser extends EmbeddedActionsParser {
// https://www.postgresql.org/docs/current/sql-createtable.html
const start = $.CONSUME(Create)
$.CONSUME(Table)
const name = $.SUBRULE($.tableRule)
const table = $.SUBRULE($.tableRule)
$.CONSUME(ParenLeft)
const columns: TableColumnAst[] = []
const constraints: TableConstraintAst[] = []
Expand All @@ -277,7 +285,42 @@ class PostgresParser extends EmbeddedActionsParser {
])})
$.CONSUME(ParenRight)
const end = $.CONSUME(Semicolon)
return removeEmpty({statement: 'CreateTable' as const, name, columns: columns.filter(isNotUndefined), constraints: constraints.filter(isNotUndefined), ...tokenInfo2(start, end)})
return removeEmpty({statement: 'CreateTable' as const, ...table, columns: columns.filter(isNotUndefined), constraints: constraints.filter(isNotUndefined), ...tokenInfo2(start, end)})
})

this.createTypeStatementRule = $.RULE<() => CreateTypeStatementAst>('createTypeStatementRule', () => {
// https://www.postgresql.org/docs/current/sql-createtype.html
const start = $.CONSUME(Create)
$.CONSUME(Type)
const table = $.SUBRULE($.tableRule)
const content = $.OPTION(() => {
const as = $.CONSUME(As)
return $.OR([
{ALT: () => {
$.CONSUME(ParenLeft)
const attrs: TypeColumnAst[] = []
$.AT_LEAST_ONE_SEP({SEP: Comma, DEF: () => attrs.push(removeUndefined({
name: $.SUBRULE($.identifierRule),
type: $.SUBRULE($.columnTypeRule),
collation: $.OPTION2(() => ({...tokenInfo($.CONSUME(Collate)), name: $.SUBRULE2($.identifierRule)}))
}))})
$.CONSUME(ParenRight)
return {struct: {...tokenInfo(as), attrs: attrs.filter(isNotUndefined)}}
}},
{ALT: () => {
const token = tokenInfo2(as, $.CONSUME(Enum))
$.CONSUME2(ParenLeft)
const values: StringAst[] = []
$.AT_LEAST_ONE_SEP2({SEP: Comma, DEF: () => values.push($.SUBRULE($.stringRule))})
$.CONSUME2(ParenRight)
return {enum: {...token, values: values.filter(isNotUndefined)}}
}},
// TODO: RANGE
// TODO: function
])
})
const end = $.CONSUME(Semicolon)
return removeEmpty({statement: 'CreateType' as const, schema: table.schema, type: table.table, ...content, ...tokenInfo2(start, end)})
})

this.dropStatementRule = $.RULE<() => DropStatementAst>('dropStatementRule', () => {
Expand All @@ -292,19 +335,13 @@ class PostgresParser extends EmbeddedActionsParser {
const concurrently = $.OPTION(() => tokenInfo($.CONSUME(Concurrently)))
const ifExists = $.OPTION2(() => tokenInfo2($.CONSUME(If), $.CONSUME(Exists)))
const entities: TableAst[] = []
$.AT_LEAST_ONE_SEP({
SEP: Comma,
DEF: () => {
const entity = $.SUBRULE($.tableRule)
entity && entities.push(entity)
}
})
$.AT_LEAST_ONE_SEP({SEP: Comma, DEF: () => entities.push($.SUBRULE($.tableRule))})
const mode = $.OPTION3(() => $.OR2([
{ALT: () => ({kind: 'Cascade' as const, ...tokenInfo($.CONSUME(Cascade))})},
{ALT: () => ({kind: 'Restrict' as const, ...tokenInfo($.CONSUME(Restrict))})},
]))
const end = $.CONSUME(Semicolon)
return removeUndefined({statement: 'Drop' as const, object, entities, concurrently, ifExists, mode, ...tokenInfo2(start, end)})
return removeUndefined({statement: 'Drop' as const, object, entities: entities.filter(isNotUndefined), concurrently, ifExists, mode, ...tokenInfo2(start, end)})
})

this.selectStatementRule = $.RULE<() => SelectStatementAst>('selectStatementRule', () => {
Expand Down Expand Up @@ -349,14 +386,11 @@ class PostgresParser extends EmbeddedActionsParser {
this.selectClauseRule = $.RULE<() => SelectClauseAst>('selectClauseRule', () => {
const token = $.CONSUME(Select)
const expressions: SelectClauseExprAst[] = []
$.AT_LEAST_ONE_SEP({
SEP: Comma,
DEF: () => {
const expression = $.SUBRULE($.expressionRule)
const alias = $.OPTION(() => $.SUBRULE($.aliasRule))
expression && expressions.push(removeUndefined({...expression, alias}))
}
})
$.AT_LEAST_ONE_SEP({SEP: Comma, DEF: () => {
const expression = $.SUBRULE($.expressionRule)
const alias = $.OPTION(() => $.SUBRULE($.aliasRule))
expression && expressions.push(removeUndefined({...expression, alias}))
}})
return {...tokenInfo(token), expressions}
})

Expand All @@ -374,7 +408,7 @@ class PostgresParser extends EmbeddedActionsParser {

this.tableColumnRule = $.RULE<() => TableColumnAst>('tableColumnRule', () => {
const name = $.SUBRULE($.identifierRule)
const type = $.SUBRULE2($.identifierRule) // TODO: handle types with space (timestamp without time zone), numbers (character varying(255)) and schema (public.citext)
const type = $.SUBRULE($.columnTypeRule)
const constraints: TableColumnConstraintAst[] = []
$.MANY(() => constraints.push($.SUBRULE(tableColumnConstraintRule)))
return removeEmpty({name, type, constraints: constraints.filter(isNotUndefined)})
Expand Down Expand Up @@ -577,6 +611,10 @@ class PostgresParser extends EmbeddedActionsParser {
return removeUndefined({schema, table, column})
})

this.columnTypeRule = $.RULE<() => ColumnTypeAst>('columnTypeRule', () => {
return $.SUBRULE($.identifierRule) // TODO: handle types with space (timestamp without time zone), numbers (character varying(255)) and schema (public.citext)
})

this.literalRule = $.RULE<() => LiteralAst>('literalRule', () => $.OR([
{ ALT: () => $.SUBRULE($.stringRule) },
{ ALT: () => $.SUBRULE($.decimalRule) },
Expand Down

0 comments on commit 5cc6cf5

Please sign in to comment.