Skip to content

Commit

Permalink
feat: add parse trigger (#80)
Browse files Browse the repository at this point in the history
* update package-lock.json

* export cursor class & add node method

* add parse trigger

* remove comment
  • Loading branch information
thormengkheang authored Apr 8, 2024
1 parent f5cd43f commit 6d767a4
Show file tree
Hide file tree
Showing 4 changed files with 302 additions and 3 deletions.
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 5 additions & 1 deletion src/lib/sql-parse-table.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import {
SqlOrder,
} from "@/drivers/base-driver";

class Cursor {
export class Cursor {
protected ptr: SyntaxNode | null;
protected sql: string = "";

Expand Down Expand Up @@ -90,6 +90,10 @@ class Cursor {
return "";
}

node(): SyntaxNode | undefined {
return this.ptr?.node;
}

type(): string | undefined {
return this.ptr?.type.name;
}
Expand Down
183 changes: 183 additions & 0 deletions src/lib/sql-parse-trigger.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,183 @@
import { parseCreateTriggerScript } from "./sql-parse-trigger";

function generateSql({
name,
when,
operation,
columnNames,
tableName,
statement,
}: Record<string, string>) {
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,
});
});
});
112 changes: 112 additions & 0 deletions src/lib/sql-parse-trigger.ts
Original file line number Diff line number Diff line change
@@ -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,
};
}

0 comments on commit 6d767a4

Please sign in to comment.