From 6d767a410532d08b255522b8e54094d96d8aaefb Mon Sep 17 00:00:00 2001 From: Kheang Date: Mon, 8 Apr 2024 20:56:16 +0700 Subject: [PATCH] feat: add parse trigger (#80) * update package-lock.json * export cursor class & add node method * add parse trigger * remove comment --- package-lock.json | 4 +- src/lib/sql-parse-table.ts | 6 +- src/lib/sql-parse-trigger.test.ts | 183 ++++++++++++++++++++++++++++++ src/lib/sql-parse-trigger.ts | 112 ++++++++++++++++++ 4 files changed, 302 insertions(+), 3 deletions(-) create mode 100644 src/lib/sql-parse-trigger.test.ts create mode 100644 src/lib/sql-parse-trigger.ts diff --git a/package-lock.json b/package-lock.json index dcb65b63..2b38e350 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "libsql-studio", - "version": "0.2.4", + "version": "0.3.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "libsql-studio", - "version": "0.2.4", + "version": "0.3.0", "dependencies": { "@aws-sdk/client-s3": "^3.540.0", "@blocknote/core": "^0.12.1", diff --git a/src/lib/sql-parse-table.ts b/src/lib/sql-parse-table.ts index fa1876f1..9e0d090a 100644 --- a/src/lib/sql-parse-table.ts +++ b/src/lib/sql-parse-table.ts @@ -9,7 +9,7 @@ import { SqlOrder, } from "@/drivers/base-driver"; -class Cursor { +export class Cursor { protected ptr: SyntaxNode | null; protected sql: string = ""; @@ -90,6 +90,10 @@ class Cursor { return ""; } + node(): SyntaxNode | undefined { + return this.ptr?.node; + } + type(): string | undefined { return this.ptr?.type.name; } diff --git a/src/lib/sql-parse-trigger.test.ts b/src/lib/sql-parse-trigger.test.ts new file mode 100644 index 00000000..e6e745d3 --- /dev/null +++ b/src/lib/sql-parse-trigger.test.ts @@ -0,0 +1,183 @@ +import { parseCreateTriggerScript } from "./sql-parse-trigger"; + +function generateSql({ + name, + when, + operation, + columnNames, + tableName, + statement, +}: Record) { + let whenString = ""; + if (when) { + whenString = `${when} `; + } + let columnNameString = ""; + if (columnNames) { + columnNameString = ` ${columnNames}`; + } + return ` + CREATE TRIGGER ${name} + ${whenString}${operation}${columnNameString} ON ${tableName} + BEGIN + ${statement}; + END; + `; +} + +describe("parse trigger", () => { + const name = "cust_addr_chng"; + const tableName = "customer_address"; + const statement = `UPDATE customer SET cust_addr=NEW.cust_addr WHERE cust_id=NEW.cust_id;`; + it("when: BEFORE", () => { + const deleteOutput = parseCreateTriggerScript( + generateSql({ name, operation: "DELETE", tableName, statement }) + ); + expect(deleteOutput).toMatchObject({ + name: name, + when: "BEFORE", + operation: "DELETE", + tableName: tableName, + statement: statement, + }); + + const insert = parseCreateTriggerScript( + generateSql({ name, operation: "INSERT", tableName, statement }) + ); + expect(insert).toMatchObject({ + name: name, + when: "BEFORE", + operation: "INSERT", + tableName: tableName, + statement: statement, + }); + + const updateOf = parseCreateTriggerScript( + generateSql({ + name, + operation: "UPDATE OF", + columnNames: "cust_addr", + tableName, + statement, + }) + ); + expect(updateOf).toMatchObject({ + name: name, + when: "BEFORE", + operation: "UPDATE", + columnNames: ["cust_addr"], + tableName: tableName, + statement: statement, + }); + }); + + it("when: AFTER", () => { + const deleteOutput = parseCreateTriggerScript( + generateSql({ + name, + when: "AFTER", + operation: "DELETE", + tableName, + statement, + }) + ); + expect(deleteOutput).toMatchObject({ + name: name, + when: "AFTER", + operation: "DELETE", + tableName: tableName, + statement: statement, + }); + + const insert = parseCreateTriggerScript( + generateSql({ + name, + when: "AFTER", + operation: "INSERT", + tableName, + statement, + }) + ); + expect(insert).toMatchObject({ + name: name, + when: "AFTER", + operation: "INSERT", + tableName: tableName, + statement: statement, + }); + + const updateOf = parseCreateTriggerScript( + generateSql({ + name, + when: "AFTER", + operation: "UPDATE OF", + columnNames: "cust_addr", + tableName, + statement, + }) + ); + expect(updateOf).toMatchObject({ + name: name, + when: "AFTER", + operation: "UPDATE", + columnNames: ["cust_addr"], + tableName: tableName, + statement: statement, + }); + }); + + it("when: INSTEAD OF", () => { + const deleteOutput = parseCreateTriggerScript( + generateSql({ + name, + when: "INSTEAD OF", + operation: "DELETE", + tableName, + statement, + }) + ); + expect(deleteOutput).toMatchObject({ + name: name, + when: "INSTEAD_OF", + operation: "DELETE", + tableName: tableName, + statement: statement, + }); + + const insert = parseCreateTriggerScript( + generateSql({ + name, + when: "INSTEAD OF", + operation: "INSERT", + tableName, + statement, + }) + ); + expect(insert).toMatchObject({ + name: name, + when: "INSTEAD_OF", + operation: "INSERT", + tableName: tableName, + statement: statement, + }); + + const updateOf = parseCreateTriggerScript( + generateSql({ + name, + when: "INSTEAD OF", + operation: "UPDATE OF", + columnNames: "cust_addr", + tableName, + statement, + }) + ); + expect(updateOf).toMatchObject({ + name: name, + when: "INSTEAD_OF", + operation: "UPDATE", + columnNames: ["cust_addr"], + tableName: tableName, + statement: statement, + }); + }); +}); diff --git a/src/lib/sql-parse-trigger.ts b/src/lib/sql-parse-trigger.ts new file mode 100644 index 00000000..e2998f06 --- /dev/null +++ b/src/lib/sql-parse-trigger.ts @@ -0,0 +1,112 @@ +import { SQLite } from "@codemirror/lang-sql"; +import { Cursor, parseColumnList } from "./sql-parse-table"; + +type TriggerWhen = "BEFORE" | "AFTER" | "INSTEAD_OF"; + +type TriggerOperation = "INSERT" | "UPDATE" | "DELETE"; + +export interface DatabaseTriggerSchema { + name: string; + operation: TriggerOperation; + when: TriggerWhen; + tableName: string; + columnNames?: string[]; + whenExpression: string; + statement: string; +} + +export function parseCreateTriggerScript(sql: string): DatabaseTriggerSchema { + const tree = SQLite.language.parser.parse(sql); + const ptr = tree.cursor(); + ptr.firstChild(); + ptr.firstChild(); + const cursor = new Cursor(ptr, sql); + cursor.expectKeyword("CREATE"); + cursor.expectKeywordOptional("TEMP"); + cursor.expectKeywordOptional("TEMPORARY"); + cursor.expectKeyword("TRIGGER"); + cursor.expectKeywordsOptional(["IF", "NOT", "EXIST"]); + const name = cursor.consumeIdentifier(); + + let when: TriggerWhen = "BEFORE"; + + if (cursor.matchKeyword("BEFORE")) { + cursor.next(); + } else if (cursor.matchKeyword("AFTER")) { + when = "AFTER"; + cursor.next(); + } else if (cursor.matchKeywords(["INSTEAD", "OF"])) { + when = "INSTEAD_OF"; + cursor.next(); + cursor.next(); + } + + let operation: TriggerOperation = "INSERT"; + let columnNames; + + if (cursor.matchKeyword("DELETE")) { + operation = "DELETE"; + cursor.next(); + } else if (cursor.matchKeyword("INSERT")) { + operation = "INSERT"; + cursor.next(); + } else if (cursor.matchKeyword("UPDATE")) { + operation = "UPDATE"; + cursor.next(); + if (cursor.matchKeyword("OF")) { + cursor.next(); + columnNames = parseColumnList(cursor); + } + } + + cursor.expectKeyword("ON"); + const tableName = cursor.consumeIdentifier(); + cursor.expectKeywordsOptional(["FOR", "EACH", "ROW"]); + + let whenExpression = ""; + const fromExpression = cursor.node()?.from; + let toExpression; + + if (cursor.matchKeyword("WHEN")) { + // Loop till the end or meet the BEGIN + cursor.next(); + + while (!cursor.end()) { + toExpression = cursor.node()?.to; + if (cursor.matchKeyword("BEGIN")) break; + cursor.next(); + } + } + + if (fromExpression) { + whenExpression = sql.substring(fromExpression, toExpression); + } + + cursor.expectKeyword("BEGIN"); + + let statement = ""; + const fromStatement = cursor.node()?.from; + let toStatement; + + while (!cursor.end()) { + toStatement = cursor.node()?.to; + if (cursor.matchKeyword(";")) { + break; + } + cursor.next(); + } + + if (fromStatement) { + statement = sql.substring(fromStatement, toStatement); + } + + return { + name, + operation, + when, + tableName, + columnNames, + whenExpression, + statement, + }; +}