Skip to content

Commit

Permalink
Add Create View
Browse files Browse the repository at this point in the history
  • Loading branch information
loicknuchel committed Oct 19, 2024
1 parent 1682f93 commit 1ec2092
Show file tree
Hide file tree
Showing 3 changed files with 86 additions and 21 deletions.
9 changes: 6 additions & 3 deletions libs/parser-sql/src/postgresAst.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,20 +10,23 @@ import {ParserErrorLevel, TokenPosition} from "@azimutt/models";

// statements
export type StatementsAst = { statements: StatementAst[] }
export type StatementAst = AlterTableStatementAst | CommentStatementAst | CreateExtensionStatementAst | CreateIndexStatementAst | CreateTableStatementAst | CreateTypeStatementAst | DropStatementAst | InsertIntoStatementAst | SelectStatementAst | SetStatementAst
export type StatementAst = AlterTableStatementAst | CommentStatementAst | CreateExtensionStatementAst | CreateIndexStatementAst | CreateTableStatementAst
| CreateTypeStatementAst | CreateViewStatementAst | DropStatementAst | InsertIntoStatementAst | SelectStatementAst | SetStatementAst
export type AlterTableStatementAst = { kind: 'AlterTable', ifExists?: TokenInfo, only?: TokenInfo, schema?: IdentifierAst, table: IdentifierAst, action: AlterTableActionAst } & TokenInfo
export type CommentStatementAst = { kind: 'Comment', object: { kind: CommentObject } & TokenInfo, schema?: IdentifierAst, parent?: IdentifierAst, entity: IdentifierAst, comment: StringAst | NullAst } & TokenInfo
export type CreateExtensionStatementAst = { kind: 'CreateExtension', ifNotExists?: TokenInfo, name: IdentifierAst, with?: TokenInfo, schema?: {name: IdentifierAst} & TokenInfo, version?: {number: StringAst | IdentifierAst} & TokenInfo, cascade?: TokenInfo } & TokenInfo
export type CreateIndexStatementAst = { kind: 'CreateIndex', unique?: TokenInfo, concurrently?: TokenInfo, ifNotExists?: TokenInfo, index?: IdentifierAst, only?: TokenInfo, schema?: IdentifierAst, table: IdentifierAst, using?: {method: IdentifierAst} & TokenInfo, columns: IndexColumnAst[], include?: {columns: IdentifierAst[]} & TokenInfo, where?: {predicate: ExpressionAst} & TokenInfo } & TokenInfo
export type CreateTableStatementAst = { kind: 'CreateTable', schema?: IdentifierAst, table: IdentifierAst, columns: TableColumnAst[], constraints?: TableConstraintAst[] } & TokenInfo
export type CreateTypeStatementAst = { kind: 'CreateType', schema?: IdentifierAst, type: IdentifierAst, struct?: {attrs: TypeColumnAst[]} & TokenInfo, enum?: {values: StringAst[]} & TokenInfo, base?: {name: IdentifierAst, value: ExpressionAst}[] } & TokenInfo
export type CreateViewStatementAst = { kind: 'CreateView', replace?: TokenInfo, temporary?: TokenInfo, recursive?: TokenInfo, schema?: IdentifierAst, view: IdentifierAst, columns?: IdentifierAst[], query: SelectStatementInnerAst } & TokenInfo
export type DropStatementAst = { kind: 'Drop', object: { kind: DropObject } & TokenInfo, entities: ObjectNameAst[], concurrently?: TokenInfo, ifExists?: TokenInfo, mode?: { kind: DropMode } & TokenInfo } & TokenInfo
export type InsertIntoStatementAst = { kind: 'InsertInto', schema?: IdentifierAst, table: IdentifierAst, columns?: IdentifierAst[], values: (ExpressionAst | { kind: 'Default' } & TokenInfo)[][], returning?: SelectClauseAst } & TokenInfo
export type SelectStatementAst = { kind: 'Select', select: SelectClauseAst, from?: FromClauseAst, where?: WhereClauseAst } & TokenInfo
export type SelectStatementAst = { kind: 'Select' } & SelectStatementInnerAst & TokenInfo
export type SetStatementAst = { kind: 'Set', scope?: { kind: SetScope } & TokenInfo, parameter: IdentifierAst, equal: { kind: SetAssign } & TokenInfo, value: SetValueAst } & TokenInfo

// clauses
export type SelectClauseAst = { expressions: SelectClauseExprAst[] } & TokenInfo
export type SelectStatementInnerAst = { select: SelectClauseAst, from?: FromClauseAst, where?: WhereClauseAst }
export type SelectClauseAst = { columns: SelectClauseExprAst[] } & TokenInfo
export type SelectClauseExprAst = ExpressionAst & { alias?: AliasAst }
export type FromClauseAst = { table: IdentifierAst, alias?: AliasAst } & TokenInfo
export type WhereClauseAst = { predicate: ExpressionAst } & TokenInfo
Expand Down
46 changes: 38 additions & 8 deletions libs/parser-sql/src/postgresParser.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import {
import {parsePostgresAst, parseRule} from "./postgresParser";

describe('postgresParser', () => {
// CREATE VIEW/MATERIALIZED VIEW
// CREATE MATERIALIZED VIEW
// UPDATE
// DELETE
test('empty', () => {
Expand All @@ -34,7 +34,7 @@ describe('postgresParser', () => {
const parsed = parsePostgresAst(sql, {strict: true})
expect(parsed.errors || []).toEqual([])
})
test.skip('full', () => {
test('full', () => {
const sql = fs.readFileSync('./resources/full.postgres.sql', 'utf8')
const parsed = parsePostgresAst(sql, {strict: true})
expect(parsed.errors || []).toEqual([])
Expand Down Expand Up @@ -257,6 +257,36 @@ describe('postgresParser', () => {
}]}})
})
})
describe('createViewStatementRule', () => {
test('simplest', () => {
expect(parsePostgresAst("CREATE VIEW admins AS SELECT * FROM users WHERE role = 'admin';")).toEqual({result: {statements: [{
kind: 'CreateView',
view: identifier('admins', 12, 17),
query: {
select: {...token(22, 27), columns: [{kind: 'Wildcard', ...token(29, 29)}]},
from: {...token(31, 34), table: identifier('users', 36, 40)},
where: {...token(42, 46), predicate: {kind: 'Operation', left: {kind: 'Column', column: identifier('role', 48, 51)}, op: {kind: '=', ...token(53, 53)}, right: string('admin', 55, 61)}},
},
...token(0, 62)
}]}})
})
test('full', () => {
expect(parsePostgresAst("CREATE OR REPLACE TEMP RECURSIVE VIEW admins (id, name) AS SELECT * FROM users WHERE role = 'admin';")).toEqual({result: {statements: [{
kind: 'CreateView',
replace: token(7, 16),
temporary: token(18, 21),
recursive: token(23, 31),
view: identifier('admins', 38, 43),
columns: [identifier('id', 46, 47), identifier('name', 50, 53)],
query: {
select: {...token(59, 64), columns: [{kind: 'Wildcard', ...token(66, 66)}]},
from: {...token(68, 71), table: identifier('users', 73, 77)},
where: {...token(79, 83), predicate: {kind: 'Operation', left: {kind: 'Column', column: identifier('role', 85, 88)}, op: {kind: '=', ...token(90, 90)}, right: string('admin', 92, 98)}},
},
...token(0, 99)
}]}})
})
})
describe('dropStatement', () => {
test('simplest', () => {
expect(parsePostgresAst('DROP TABLE users;')).toEqual({result: {statements: [{
Expand Down Expand Up @@ -300,7 +330,7 @@ describe('postgresParser', () => {
table: identifier('users', 12, 16),
columns: [identifier('id', 19, 20), identifier('name', 23, 26)],
values: [[integer(1, 37, 37), string('loic', 40, 45)], [{kind: 'Default', ...token(50, 56)}, string('lou', 59, 63)]],
returning: {...token(66, 74), expressions: [{kind: 'Column', column: identifier('id', 76, 77)}]},
returning: {...token(66, 74), columns: [{kind: 'Column', column: identifier('id', 76, 77)}]},
...token(0, 78)
}]}})
})
Expand All @@ -311,15 +341,15 @@ describe('postgresParser', () => {
test('simplest', () => {
expect(parsePostgresAst('SELECT name FROM users;')).toEqual({result: {statements: [{
kind: 'Select',
select: {...token(0, 5), expressions: [{kind: 'Column', column: identifier('name', 7, 10)}]},
select: {...token(0, 5), columns: [{kind: 'Column', column: identifier('name', 7, 10)}]},
from: {...token(12, 15), table: identifier('users', 17, 21)},
...token(0, 22)
}]}})
})
test('complex', () => {
expect(removeTokens(parsePostgresAst('SELECT id, first_name AS name FROM users WHERE id = 1;'))).toEqual({result: {statements: [{
kind: 'Select',
select: {expressions: [
select: {columns: [
{kind: 'Column', column: {kind: 'Identifier', value: 'id'}},
{kind: 'Column', column: {kind: 'Identifier', value: 'first_name'}, alias: {name: {kind: 'Identifier', value: 'name'}}}
]},
Expand All @@ -330,7 +360,7 @@ describe('postgresParser', () => {
test('strange', () => {
expect(parsePostgresAst("SELECT pg_catalog.set_config('search_path', '', false);")).toEqual({result: {statements: [{
kind: 'Select',
select: {...token(0, 5), expressions: [{
select: {...token(0, 5), columns: [{
kind: 'Function',
schema: identifier('pg_catalog', 7, 16),
function: identifier('set_config', 18, 27),
Expand Down Expand Up @@ -362,11 +392,11 @@ describe('postgresParser', () => {
test('simplest', () => {
expect(parseRule(p => p.selectClauseRule(), 'SELECT name')).toEqual({result: {
...token(0, 5),
expressions: [{kind: 'Column', column: identifier('name', 7, 10)}],
columns: [{kind: 'Column', column: identifier('name', 7, 10)}],
}})
})
test('complex', () => {
expect(parseRule(p => p.selectClauseRule(), 'SELECT e.*, u.name AS user_name, lower(u.email), "public"."Event"."id"')).toEqual({result: {...token(0, 5), expressions: [
expect(parseRule(p => p.selectClauseRule(), 'SELECT e.*, u.name AS user_name, lower(u.email), "public"."Event"."id"')).toEqual({result: {...token(0, 5), columns: [
{kind: 'Wildcard', table: identifier('e', 7, 7), ...token(9, 9)},
{kind: 'Column', table: identifier('u', 12, 12), column: identifier('name', 14, 17), alias: {...token(19, 20), name: identifier('user_name', 22, 30)}},
{kind: 'Function', function: identifier('lower', 33, 37), parameters: [{kind: 'Column', table: identifier('u', 39, 39), column: identifier('email', 41, 45)}]},
Expand Down
52 changes: 42 additions & 10 deletions libs/parser-sql/src/postgresParser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import {
CreateIndexStatementAst,
CreateTableStatementAst,
CreateTypeStatementAst,
CreateViewStatementAst,
DecimalAst,
DropStatementAst,
ExpressionAst,
Expand All @@ -42,6 +43,7 @@ import {
SelectClauseAst,
SelectClauseExprAst,
SelectStatementAst,
SelectStatementInnerAst,
SetStatementAst,
StatementAst,
StatementsAst,
Expand Down Expand Up @@ -126,7 +128,9 @@ const Only = createToken({name: 'Only', pattern: /\bONLY\b/i, longer_alt: Identi
const Or = createToken({name: 'Or', pattern: /\bOR\b/i, longer_alt: Identifier})
const OrderBy = createToken({name: 'OrderBy', pattern: /\bORDER\s+BY\b/i})
const PrimaryKey = createToken({name: 'PrimaryKey', pattern: /\bPRIMARY\s+KEY\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 Replace = createToken({name: 'Replace', pattern: /\bREPLACE\b/i, longer_alt: Identifier})
const Restrict = createToken({name: 'Restrict', pattern: /\bRESTRICT\b/i, longer_alt: Identifier})
const Returning = createToken({name: 'Returning', pattern: /\bRETURNING\b/i, longer_alt: Identifier})
const Schema = createToken({name: 'Schema', pattern: /\bSCHEMA\b/i, longer_alt: Identifier})
Expand All @@ -136,6 +140,8 @@ const SetDefault = createToken({name: 'SetDefault', pattern: /\bSET\s+DEFAULT\b/
const SetNull = createToken({name: 'SetNull', pattern: /\bSET\s+NULL\b/i})
const Set = createToken({name: 'Set', pattern: /\bSET\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 To = createToken({name: 'To', pattern: /\bTO\b/i, longer_alt: Identifier})
const True = createToken({name: 'True', pattern: /\bTRUE\b/i, longer_alt: Identifier})
const Type = createToken({name: 'Type', pattern: /\bTYPE\b/i, longer_alt: Identifier})
Expand All @@ -152,8 +158,8 @@ const With = createToken({name: 'With', pattern: /\bWITH\b/i, longer_alt: Identi
const keywordTokens: TokenType[] = [
Add, Alter, And, As, Asc, Cascade, Check, Collate, Column, Comment, Concurrently, Constraint, Create, Default, Database, Delete, Desc, Distinct, Domain, Drop,
Enum, Exists, Extension, False, Fetch, First, ForeignKey, From, GroupBy, Having, If, In, Include, Index, InsertInto, Is, Join, Last, Like, Limit, Local,
MaterializedView, NoAction, Not, Null, Nulls, Offset, On, Only, Or, OrderBy, PrimaryKey, References, Restrict, Returning, Schema, Select, Session, SetDefault, SetNull, Set,
Table, To, True, Type, Union, Unique, Update, Using, Values, Version, View, Where, Window, With
MaterializedView, NoAction, Not, Null, Nulls, Offset, On, Only, Or, OrderBy, PrimaryKey, Recursive, References, Replace, Restrict, Returning,
Schema, Select, Session, SetDefault, SetNull, Set, Table, Temp, Temporary, To, True, Type, Union, Unique, Update, Using, Values, Version, View, Where, Window, With
]

const Amp = createToken({name: 'Amp', pattern: /&/})
Expand Down Expand Up @@ -202,6 +208,7 @@ class PostgresParser extends EmbeddedActionsParser {
createIndexStatementRule: () => CreateIndexStatementAst
createTableStatementRule: () => CreateTableStatementAst
createTypeStatementRule: () => CreateTypeStatementAst
createViewStatementRule: () => CreateViewStatementAst
dropStatementRule: () => DropStatementAst
insertIntoStatementRule: () => InsertIntoStatementAst
selectStatementRule: () => SelectStatementAst
Expand Down Expand Up @@ -249,6 +256,7 @@ class PostgresParser extends EmbeddedActionsParser {
{ALT: () => $.SUBRULE($.createIndexStatementRule)},
{ALT: () => $.SUBRULE($.createTableStatementRule)},
{ALT: () => $.SUBRULE($.createTypeStatementRule)},
{ALT: () => $.SUBRULE($.createViewStatementRule)},
{ALT: () => $.SUBRULE($.dropStatementRule)},
{ALT: () => $.SUBRULE($.insertIntoStatementRule)},
{ALT: () => $.SUBRULE($.selectStatementRule)},
Expand Down Expand Up @@ -395,6 +403,26 @@ class PostgresParser extends EmbeddedActionsParser {
return removeEmpty({kind: 'CreateType' as const, schema: object.schema, type: object.name, ...content, ...tokenInfo2(start, end)})
})

this.createViewStatementRule = $.RULE<() => CreateViewStatementAst>('createViewStatementRule', () => {
// https://www.postgresql.org/docs/current/sql-createview.html
const start = $.CONSUME(Create)
const replace = $.OPTION(() => tokenInfo2($.CONSUME(Or), $.CONSUME(Replace)))
const temporary = $.OPTION2(() => $.OR([{ALT: () => tokenInfo($.CONSUME(Temp))}, {ALT: () => tokenInfo($.CONSUME(Temporary))}]))
const recursive = $.OPTION3(() => tokenInfo($.CONSUME(Recursive)))
$.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, replace, temporary, recursive, schema: object.schema, view: object.name, columns, query, ...tokenInfo2(start, end)})
})

this.dropStatementRule = $.RULE<() => DropStatementAst>('dropStatementRule', () => {
const start = $.CONSUME(Drop)
const object = $.OR([
Expand Down Expand Up @@ -441,21 +469,25 @@ class PostgresParser extends EmbeddedActionsParser {
}})
const returning = $.OPTION2(() => {
const token = $.CONSUME(Returning)
const expressions: SelectClauseExprAst[] = []
$.AT_LEAST_ONE_SEP4({SEP: Comma, DEF: () => expressions.push($.SUBRULE(selectClauseColumnRule))})
return {...tokenInfo(token), expressions}
const columns: SelectClauseExprAst[] = []
$.AT_LEAST_ONE_SEP4({SEP: Comma, DEF: () => columns.push($.SUBRULE(selectClauseColumnRule))})
return {...tokenInfo(token), columns}
})
const end = $.CONSUME(Semicolon)
return removeUndefined({kind: 'InsertInto' as const, schema: object.schema, table: object.name, columns, values, returning, ...tokenInfo2(start, end)})
})

this.selectStatementRule = $.RULE<() => SelectStatementAst>('selectStatementRule', () => {
const inner = $.SUBRULE(selectStatementInnerRule)
const end = $.CONSUME(Semicolon)
return removeUndefined({kind: 'Select' as const, ...inner, ...mergePositions([inner.select, tokenInfo(end)])})
})
const selectStatementInnerRule = $.RULE<() => SelectStatementInnerAst>('selectStatementInnerRule', () => {
// https://www.postgresql.org/docs/current/sql-select.html
const select = $.SUBRULE($.selectClauseRule)
const from = $.OPTION(() => $.SUBRULE($.fromClauseRule))
const where = $.OPTION2(() => $.SUBRULE($.whereClauseRule))
const end = $.CONSUME(Semicolon)
return removeUndefined({kind: 'Select' as const, select, from, where, ...mergePositions([select, tokenInfo(end)])})
return removeUndefined({select, from, where})
})

this.setStatementRule = $.RULE<() => SetStatementAst>('setStatementRule', () => {
Expand Down Expand Up @@ -490,9 +522,9 @@ class PostgresParser extends EmbeddedActionsParser {

this.selectClauseRule = $.RULE<() => SelectClauseAst>('selectClauseRule', () => {
const token = $.CONSUME(Select)
const expressions: SelectClauseExprAst[] = []
$.AT_LEAST_ONE_SEP({SEP: Comma, DEF: () => expressions.push($.SUBRULE(selectClauseColumnRule))})
return {...tokenInfo(token), expressions}
const columns: SelectClauseExprAst[] = []
$.AT_LEAST_ONE_SEP({SEP: Comma, DEF: () => columns.push($.SUBRULE(selectClauseColumnRule))})
return {...tokenInfo(token), columns}
})
const selectClauseColumnRule = $.RULE<() => SelectClauseExprAst>('selectClauseColumnRule', () => {
const expression = $.SUBRULE($.expressionRule)
Expand Down

0 comments on commit 1ec2092

Please sign in to comment.