Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 11 additions & 5 deletions packages/core/src/utils/sql.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,13 @@
const MAX_SUMMARY_LENGTH = 255;

const TABLE_NAME_CHARS = /[^\s(,;)]+/;
const TABLE_NAME = TABLE_NAME_CHARS.source;
// A single identifier: quoted (`"..."`, `'...'`, or MySQL backticks) or bare. The quoted forms have
// to be matched as a unit, otherwise an identifier containing a space or a dot is cut in half.
const IDENTIFIER = '(?:"[^"]*"|\'[^\']*\'|`[^`]*`|[^\\s(,;).\'"`]+)';

// A table reference can be schema-qualified (`"public"."User"`, `db.schema.table`), with each part
// quoted independently. The whole qualified name is the summary target, since the schema is what
// distinguishes two same-named tables.
const TABLE_NAME = `${IDENTIFIER}(?:\\.${IDENTIFIER})*`;

const DDL_RE = new RegExp(
`^\\s*(?<operation>(?:CREATE|DROP)\\s+(?:TABLE|INDEX)|ALTER\\s+TABLE)(?:\\s+IF\\s+(?:NOT\\s+)?EXISTS)?\\s+(?<table>${TABLE_NAME})`,
Expand All @@ -27,8 +33,8 @@ const SELECT_RE = /^\s*\(?\s*(?<operation>SELECT)\b/i;
const PRAGMA_RE = /^\s*(?<operation>PRAGMA)\s+(?<command>\S+)/i;

const TOKEN_RE = /\b(?:FROM|JOIN)\s+|\(\s*(SELECT)\b|\b(?:UNION|INTERSECT|EXCEPT|MINUS)\s+(?:ALL\s+)?(SELECT)\b/gi;
const QUOTED_OR_PLAIN_TABLE_RE = /^(?:"[^"]*"|'[^']*'|[^\s(,;)]+)/;
const COMMA_TABLE_RE = /^\s*,\s*((?:"[^"]*"|'[^']*'|[^\s(,;)]+))/;
const TABLE_REF_RE = new RegExp(`^${TABLE_NAME}`);
const COMMA_TABLE_RE = new RegExp(`^\\s*,\\s*(${TABLE_NAME})`);
const SUBQUERY_SELECT_RE = /^\(\s*(SELECT)\b/i;

/**
Expand Down Expand Up @@ -117,7 +123,7 @@ function extractTableNames(sql: string): string[] {
continue;
}

const tableMatch = QUOTED_OR_PLAIN_TABLE_RE.exec(rest);
const tableMatch = TABLE_REF_RE.exec(rest);
if (!tableMatch) continue;
tables.push(tableMatch[0]);

Expand Down
34 changes: 34 additions & 0 deletions packages/core/test/lib/utils/sql.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -213,6 +213,40 @@ describe('getSqlQuerySummary', () => {
});
});

describe('quoted and schema-qualified table names', () => {
it.each([
['SELECT * FROM "public"."User"', 'SELECT "public"."User"'],
['DELETE FROM "public"."User"', 'DELETE "public"."User"'],
['INSERT INTO "public"."User" (name) VALUES (?)', 'INSERT "public"."User"'],
['UPDATE "public"."User" SET name = ?', 'UPDATE "public"."User"'],
['CREATE TABLE "public"."User" (id INTEGER)', 'CREATE TABLE "public"."User"'],
['SELECT * FROM public.User', 'SELECT public.User'],
['SELECT * FROM `mydb`.`users`', 'SELECT `mydb`.`users`'],
['SELECT * FROM "catalog"."public"."User"', 'SELECT "catalog"."public"."User"'],
['SELECT * FROM "public".User', 'SELECT "public".User'],
['SELECT * FROM public."User"', 'SELECT public."User"'],
])('keeps the whole qualified name: %j => %j', (input, expected) => {
expect(getSqlQuerySummary(input)).toBe(expected);
});

it('keeps schema-qualified JOIN targets distinguishable', () => {
expect(getSqlQuerySummary('SELECT * FROM "public"."A" JOIN "public"."B" ON "A".id = "B"."a_id"')).toBe(
'SELECT "public"."A" "public"."B"',
);
});

it.each([
['SELECT * FROM "my table"', 'SELECT "my table"'],
['INSERT INTO "my table" (id) VALUES (?)', 'INSERT "my table"'],
['UPDATE "my table" SET id = ?', 'UPDATE "my table"'],
['DELETE FROM "my table"', 'DELETE "my table"'],
['CREATE TABLE "my table" (id INTEGER)', 'CREATE TABLE "my table"'],
['SELECT * FROM "my schema"."my table"', 'SELECT "my schema"."my table"'],
])('does not split identifiers containing spaces: %j => %j', (input, expected) => {
expect(getSqlQuerySummary(input)).toBe(expected);
});
});

describe('truncation', () => {
it('truncates at 255 characters on a word boundary', () => {
const longTable = 'a'.repeat(300);
Expand Down
Loading