From 53b06c98cbd936cfb16cc96017cf540d615e4312 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lo=C3=AFc=20Knuchel?= Date: Wed, 30 Oct 2024 23:18:44 +0100 Subject: [PATCH] Add alter column --- libs/parser-sql/src/postgresAst.ts | 10 +- libs/parser-sql/src/postgresBuilder.test.ts | 6 +- libs/parser-sql/src/postgresBuilder.ts | 113 +++++++++++--------- libs/parser-sql/src/postgresParser.test.ts | 76 +++++++++---- libs/parser-sql/src/postgresParser.ts | 56 ++++++---- 5 files changed, 164 insertions(+), 97 deletions(-) diff --git a/libs/parser-sql/src/postgresAst.ts b/libs/parser-sql/src/postgresAst.ts index c8cc478a0..158bb37a7 100644 --- a/libs/parser-sql/src/postgresAst.ts +++ b/libs/parser-sql/src/postgresAst.ts @@ -20,7 +20,7 @@ export type StatementAst = { meta: TokenInfo } & (AlterSchemaStatementAst | Alte InsertIntoStatementAst | SelectStatementAst | SetStatementAst | ShowStatementAst | UpdateStatementAst) export type AlterSchemaStatementAst = { kind: 'AlterSchema', token: TokenInfo, schema: IdentifierAst, action: { kind: 'Rename', token: TokenInfo, schema: IdentifierAst } | { kind: 'Owner', token: TokenInfo, role: SchemaRoleAst } } export type AlterSequenceStatementAst = { kind: 'AlterSequence', token: TokenInfo, ifExists?: TokenInfo, 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?: TokenInfo, only?: TokenInfo, schema?: IdentifierAst, table: IdentifierAst, action: AlterTableActionAst } +export type AlterTableStatementAst = { kind: 'AlterTable', token: TokenInfo, ifExists?: TokenInfo, only?: TokenInfo, schema?: IdentifierAst, table: IdentifierAst, actions: AlterTableActionAst[] } export type BeginStatementAst = { kind: 'Begin', token: TokenInfo, object?: {kind: 'Work' | 'Transaction', token: TokenInfo}, modes?: TransactionModeAst[] } export type CommentOnStatementAst = { kind: 'CommentOn', token: TokenInfo, object: { token: TokenInfo, kind: CommentObject }, schema?: IdentifierAst, parent?: IdentifierAst, entity: IdentifierAst, comment: StringAst | NullAst } export type CommitStatementAst = { kind: 'Commit', token: TokenInfo, object?: { kind: 'Work' | 'Transaction', token: TokenInfo }, chain?: { token: TokenInfo, no?: TokenInfo } } @@ -68,11 +68,15 @@ export type OrderByClauseAst = { token: TokenInfo, expressions: (ExpressionAst & 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 } } -export type AlterTableActionAst = AddColumnAst | AddConstraintAst | DropColumnAst | DropConstraintAst +export type AlterTableActionAst = AddColumnAst | AlterColumnAst | DropColumnAst | AddConstraintAst | DropConstraintAst export type AddColumnAst = { kind: 'AddColumn', token: TokenInfo, ifNotExists?: TokenInfo, column: TableColumnAst } -export type AddConstraintAst = { kind: 'AddConstraint', token: TokenInfo, constraint: TableConstraintAst } export type DropColumnAst = { kind: 'DropColumn', token: TokenInfo, ifExists?: TokenInfo, column: IdentifierAst } +export type AlterColumnAst = { kind: 'AlterColumn', token: TokenInfo, column: IdentifierAst, action: AlterColumnActionAst } +export type AddConstraintAst = { kind: 'AddConstraint', token: TokenInfo, constraint: TableConstraintAst, notValid?: TokenInfo } export type DropConstraintAst = { kind: 'DropConstraint', token: TokenInfo, ifExists?: TokenInfo, constraint: IdentifierAst } +export type AlterColumnActionAst = AlterColumnDefaultAst | AlterColumnNotNullAst +export type AlterColumnDefaultAst = { kind: 'Default', action: { kind: 'Set' | 'Drop', token: TokenInfo }, token: TokenInfo, expression?: ExpressionAst } +export type AlterColumnNotNullAst = { kind: 'NotNull', action: { kind: 'Set' | 'Drop', token: TokenInfo }, token: TokenInfo } export type TypeColumnAst = { name: IdentifierAst, type: ColumnTypeAst, collation?: { token: TokenInfo, name: IdentifierAst } } export type IndexColumnAst = ExpressionAst & { collation?: { token: TokenInfo, name: IdentifierAst }, order?: SortOrderAst, nulls?: SortNullsAst } export type TableColumnAst = { name: IdentifierAst, type: ColumnTypeAst, constraints?: TableColumnConstraintAst[] } diff --git a/libs/parser-sql/src/postgresBuilder.test.ts b/libs/parser-sql/src/postgresBuilder.test.ts index c0f95f76c..9f807afb8 100644 --- a/libs/parser-sql/src/postgresBuilder.test.ts +++ b/libs/parser-sql/src/postgresBuilder.test.ts @@ -21,6 +21,7 @@ CREATE TABLE users ( 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'; @@ -34,6 +35,7 @@ CREATE TABLE cms.posts ( ); 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'; @@ -48,7 +50,7 @@ COMMENT ON TYPE bug_status IS 'bug status'; attrs: [ {name: 'id', type: 'int'}, {name: 'name', type: 'varchar', default: 'anon', doc: 'user name'}, - {name: 'role', type: 'varchar'}, + {name: 'role', type: 'varchar', default: 'guest'}, {name: 'created_at', type: 'timestamp', default: '`now()`'}, ], pk: {attrs: [['id']]}, @@ -61,7 +63,7 @@ COMMENT ON TYPE bug_status IS 'bug status'; name: 'posts', attrs: [ {name: 'id', type: 'int'}, - {name: 'title', type: 'varchar', null: true}, + {name: 'title', type: 'varchar'}, {name: 'author', type: 'int', null: true}, ], pk: {attrs: [['id']]}, diff --git a/libs/parser-sql/src/postgresBuilder.ts b/libs/parser-sql/src/postgresBuilder.ts index 511d78492..aefd4fcb4 100644 --- a/libs/parser-sql/src/postgresBuilder.ts +++ b/libs/parser-sql/src/postgresBuilder.ts @@ -261,58 +261,71 @@ function createIndex(index: number, stmt: CreateIndexStatementAst, entities: Ent 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) { - const action = stmt.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 === '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}, - })) + 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 { - isNever(constraint) + isNever(action) } - } 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 { - isNever(action) - } + }) } } diff --git a/libs/parser-sql/src/postgresParser.test.ts b/libs/parser-sql/src/postgresParser.test.ts index 2099ce506..20fb20c45 100644 --- a/libs/parser-sql/src/postgresParser.test.ts +++ b/libs/parser-sql/src/postgresParser.test.ts @@ -66,40 +66,43 @@ describe('postgresParser', () => { // 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: 1169) + // TODO:326 `SELECT ... FROM (VALUES ($2, ($3)::text), ($4, ($5)::text)) as v(key, val)` (also: 1168) }) test.skip('plausible', () => { const sql = fs.readFileSync('./resources/plausible.sql', 'utf8') const parsed = parsePostgresAst(sql, {strict: true}) expect(parsed.errors || []).toEqual([]) - // TODO:73 `CREATE FUNCTION` - // TODO:246 `features character varying(255)[] DEFAULT ARRAY['props'::character varying, 'stats_api'::character varying] NOT NULL` - // TODO:542 `recipients public.citext[] DEFAULT ARRAY[]::public.citext[] NOT NULL` - // TODO:575 `errors jsonb[] DEFAULT ARRAY[]::jsonb[] NOT NULL,` + // TODO:246 `features character varying(255)[] DEFAULT ARRAY['props'::character varying, 'stats_api'::character varying] NOT NULL,` + // TODO:542 `recipients public.citext[] DEFAULT ARRAY[]::public.citext[] NOT NULL,` + // TODO:575 `errors jsonb[] DEFAULT ARRAY[]::jsonb[] NOT NULL,` + // TODO:585 `tags character varying(255)[] DEFAULT ARRAY[]::character varying[],` + // TODO:1357 `recipients public.citext[] DEFAULT ARRAY[]::public.citext[] NOT NULL,` + // TODO:1357 `recipients public.citext[] DEFAULT ARRAY[]::public.citext[] NOT NULL` + // TODO:2246 `CREATE TRIGGER ...` }) 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', // fail#246: `DEFAULT ARRAY['props'::character varying, 'stats_api'::character varying]` // 'https://raw.githubusercontent.com/inaturalist/inaturalist/refs/heads/main/db/structure.sql', // fail#44: column type: `numeric[]` // 'https://raw.githubusercontent.com/cardstack/cardstack/refs/heads/main/packages/hub/config/structure.sql', // fail#52: `ALTER TYPE` & 1986: `COPY` - // 'https://raw.githubusercontent.com/gocardless/draupnir/refs/heads/master/structure.sql', // fail#118: `ALTER TABLE ... ALTER COLUMN id SET DEFAULT ...` TODO // 'https://raw.githubusercontent.com/drenther/Empirical-Core/refs/heads/develop/db/structure.sql', // fail#688: `ARRAY[]` // 'https://raw.githubusercontent.com/spuddybike/archivist/refs/heads/develop/db/structure.sql', // fail#849: parenthesis in FROM clause // 'https://raw.githubusercontent.com/sathreddy/bety/refs/heads/master/db/structure.sql', // fail#274: LexingError: unexpected character: ->\\<- // 'https://raw.githubusercontent.com/dhbtk/achabus/refs/heads/master/db/structure.sql', // fail#293: column type: `geography(Point,4326)` // 'https://raw.githubusercontent.com/mveytsman/community/refs/heads/master/db/structure.sql', // fail#299: JOIN (`SELECT * FROM users, events;`) - // 'https://raw.githubusercontent.com/yazilimcilarinmolayeri/pixels-clone/refs/heads/master/Structure.sql', // fail#27: CREATE SEQUENCE param order (START at the end) TODO - // 'https://raw.githubusercontent.com/henry2992/aprendemy/refs/heads/master/db/structure.sql', // fail#1291: `ALTER TABLE ... ALTER COLUMN id SET DEFAULT ...` TODO - // 'https://raw.githubusercontent.com/LuisMDeveloper/travis-core/refs/heads/master/db/structure.sql', // fail#832: `ALTER TABLE ... ALTER COLUMN id SET DEFAULT ...` TODO + // 'https://raw.githubusercontent.com/yazilimcilarinmolayeri/pixels-clone/refs/heads/master/Structure.sql', // fail#68: column `GENERATED ALWAYS AS IDENTITY` + // 'https://raw.githubusercontent.com/henry2992/aprendemy/refs/heads/master/db/structure.sql', // fail#1961: `CREATE TRIGGER` // '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#2: `CREATE OR REPLACE 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;` - // 'https://raw.githubusercontent.com/TechnoDann/PPC-board-2.0/refs/heads/master/db/structure.sql', // fail#254: `ALTER TABLE ... ALTER COLUMN id SET DEFAULT ...` TODO + // '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/bocoup/devstats/refs/heads/master/structure.sql', // fail#57: `ALTER FUNCTION current_state.label_prefix(some_label text) OWNER TO devstats_team;` - // 'https://raw.githubusercontent.com/gustavodiel/monet-backend/refs/heads/main/db/structure.sql', // fail#199: `ALTER TABLE ... ALTER COLUMN id SET DEFAULT ...` TODO - // 'https://raw.githubusercontent.com/Leonardo-Zappani/bd2/refs/heads/main/db/structure.sql', // fail#250: `ALTER TABLE ... ALTER COLUMN id SET DEFAULT ...` TODO + // 'https://raw.githubusercontent.com/Leonardo-Zappani/bd2/refs/heads/main/db/structure.sql', // fail#390: `CREATE TRIGGER` // 'https://raw.githubusercontent.com/Style12341/UTN-gestor-aulas-backend/refs/heads/main/db/structure.sql', // fail#25: `CREATE TYPE public.timerange AS RANGE` ] await Promise.all(structures.map(async url => { @@ -159,35 +162,68 @@ describe('postgresParser', () => { only: token(22, 25), schema: identifier('public', 27), table: identifier('users', 34), - action: {...kind('AddConstraint', 40, 42), constraint: {constraint: {token: token(44, 53), name: identifier('users_pk', 55)}, ...kind('PrimaryKey', 64, 74), columns: [identifier('id', 77)]}}, + 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), - action: {...kind('AddColumn', 18, 20), column: {name: identifier('author', 22), type: {name: {value: 'int', token: token(29, 31)}, token: token(29, 31)}}}, + actions: [{...kind('AddColumn', 18, 20), column: {name: identifier('author', 22), type: {name: {value: 'int', token: token(29, 31)}, 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), - action: {...kind('DropColumn', 18, 21), column: identifier('author', 23)}, + 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(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: {value: 'regclass', token: token(89, 96)}, 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(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(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);')).toEqual({result: {statements: [{ - ...stmt('AlterTable', 0, 10, 38), + expect(parsePostgresAst('ALTER TABLE users ADD PRIMARY KEY (id) NOT VALID;')).toEqual({result: {statements: [{ + ...stmt('AlterTable', 0, 10, 48), table: identifier('users', 12), - action: {...kind('AddConstraint', 18, 20), constraint: {...kind('PrimaryKey', 22, 32), columns: [identifier('id', 35)]}}, + actions: [{...kind('AddConstraint', 18, 20), constraint: {...kind('PrimaryKey', 22, 32), columns: [identifier('id', 35)]}, notValid: 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), - action: {...kind('DropConstraint', 18, 32), constraint: identifier('users_pk', 34)}, + actions: [{...kind('DropConstraint', 18, 32), constraint: identifier('users_pk', 34)}], }]}}) }) }) @@ -1167,7 +1203,7 @@ describe('postgresParser', () => { expect(parseRule(p => p.identifierRule(), '"an id with \\""')).toEqual({result: {...identifier('an id with "', 0, 14), quoted: true}}) }) test('not empty', () => { - const specials = ['Add', 'Commit', 'Data', 'Database', 'Deferrable', 'Increment', 'Index', 'Input', 'Nulls', 'Rows', 'Schema', 'Start', 'Temporary', 'Type', 'Version'] + const specials = ['Add', 'Commit', 'Data', 'Database', 'Deferrable', 'Domain', 'Increment', 'Index', 'Input', 'Nulls', 'Rows', 'Schema', 'Start', 'Temporary', '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}}} diff --git a/libs/parser-sql/src/postgresParser.ts b/libs/parser-sql/src/postgresParser.ts index 7fb3842c6..842593b80 100644 --- a/libs/parser-sql/src/postgresParser.ts +++ b/libs/parser-sql/src/postgresParser.ts @@ -13,6 +13,7 @@ import { AliasAst, AlterSchemaStatementAst, AlterSequenceStatementAst, + AlterTableActionAst, AlterTableStatementAst, BeginStatementAst, BooleanAst, @@ -239,8 +240,6 @@ const Sequence = createToken({name: 'Sequence', pattern: /\bSEQUENCE\b/i, longer 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 SetDefault = createToken({name: 'SetDefault', pattern: /\bSET\s+DEFAULT\b/i}) -const SetNull = createToken({name: 'SetNull', pattern: /\bSET\s+NULL\b/i}) 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}) @@ -260,6 +259,7 @@ const Unique = createToken({name: 'Unique', pattern: /\bUNIQUE\b/i, longer_alt: 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}) @@ -279,9 +279,8 @@ const keywordTokens: TokenType[] = [ Null, Nulls, Offset, On, Only, Or, OrderBy, Out, Outer, Over, OwnedBy, OwnerTo, PartitionBy, PrimaryKey, ReadCommitted, ReadOnly, ReadUncommitted, ReadWrite, Recursive, References, RenameTo, RepeatableRead, Replace, Restrict, Return, Returning, Returns, Right, Row, Rows, Schema, Select, Sequence, Serializable, Session, - SessionUser, SetDefault, SetNull, Set, SetOf, Show, Table, Stable, Start, Strict, Temp, Temporary, Ties, To, - Transaction, True, Type, Union, Unique, Unlogged, Update, Using, Values, Version, View, Volatile, Where, Window, - With, Work + SessionUser, Set, SetOf, Show, Table, Stable, Start, Strict, Temp, Temporary, Ties, To, Transaction, True, Type, + Union, Unique, Unlogged, Update, Using, Valid, Values, Version, View, Volatile, Where, Window, With, Work ] const Amp = createToken({name: 'Amp', pattern: /&/}) @@ -446,16 +445,24 @@ class PostgresParser extends EmbeddedActionsParser { const ifExists = $.SUBRULE(ifExistsRule) const only = $.OPTION(() => tokenInfo($.CONSUME(Only))) const object = $.SUBRULE($.objectNameRule) - const action = $.OR([ + const actions: AlterTableActionAst[] = [] + $.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: 'AddConstraint' as const, token: tokenInfo($.CONSUME2(Add)), constraint: $.SUBRULE($.tableConstraintRule)})}, {ALT: () => removeUndefined({kind: 'DropColumn' as const, token: tokenInfo2($.CONSUME(Drop), $.OPTION3(() => $.CONSUME2(Column))), ifExists: $.SUBRULE2(ifExistsRule), column: $.SUBRULE($.identifierRule)})}, - {ALT: () => removeUndefined({kind: 'DropConstraint' as const, token: tokenInfo2($.CONSUME2(Drop), $.CONSUME(Constraint)), ifExists: $.SUBRULE3(ifExistsRule), constraint: $.SUBRULE2($.identifierRule)})}, - // TODO: ALTER COLUMN - ]) + {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(() => tokenInfo2($.CONSUME2(Not), $.CONSUME(Valid)))})}, + {ALT: () => removeUndefined({kind: 'DropConstraint' as const, token: tokenInfo2($.CONSUME2(Drop), $.CONSUME(Constraint)), ifExists: $.SUBRULE3(ifExistsRule), constraint: $.SUBRULE3($.identifierRule)})}, + ]))}) const end = $.CONSUME(Semicolon) - return removeUndefined({kind: 'AlterTable' as const, meta: tokenInfo2(start, end), token, ifExists, only, schema: object.schema, table: object.name, action}) + 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 @@ -706,16 +713,20 @@ class PostgresParser extends EmbeddedActionsParser { const token = tokenInfo2(begin, $.CONSUME(Sequence)) const ifNotExists = $.OPTION2(() => $.SUBRULE(ifNotExistsRule)) 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)) + + 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(begin, end), token, mode, ifNotExists, ...object, as, start, increment, minValue, maxValue, cache, ownedBy}) + return removeUndefined({kind: 'CreateSequence' as const, meta: tokenInfo2(begin, 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)})) @@ -1356,8 +1367,8 @@ class PostgresParser extends EmbeddedActionsParser { {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: tokenInfo($.CONSUME(SetNull))})}, - {ALT: () => ({kind: 'SetDefault' as const, token: tokenInfo($.CONSUME(SetDefault))})}, + {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}) @@ -1644,6 +1655,7 @@ class PostgresParser extends EmbeddedActionsParser { {ALT: () => toIdentifier($.CONSUME(Data))}, {ALT: () => toIdentifier($.CONSUME(Database))}, {ALT: () => toIdentifier($.CONSUME(Deferrable))}, + {ALT: () => toIdentifier($.CONSUME(Domain))}, {ALT: () => toIdentifier($.CONSUME(Increment))}, {ALT: () => toIdentifier($.CONSUME(Index))}, {ALT: () => toIdentifier($.CONSUME(Input))},