diff --git a/dev-packages/node-integration-tests/suites/tracing/prisma-orm-v5/test.ts b/dev-packages/node-integration-tests/suites/tracing/prisma-orm-v5/test.ts index b5a3ef7dbe7c..33b8c89d0ec3 100644 --- a/dev-packages/node-integration-tests/suites/tracing/prisma-orm-v5/test.ts +++ b/dev-packages/node-integration-tests/suites/tracing/prisma-orm-v5/test.ts @@ -85,7 +85,7 @@ function expectPrismaV5Spans(transaction: TransactionEvent): void { expect.objectContaining({ data: { 'db.statement': expect.stringContaining('SELECT'), - 'db.query.summary': 'SELECT "public"', + 'db.query.summary': 'SELECT "public"."User"', 'db.system': 'postgresql', 'sentry.kind': 'client', 'sentry.op': 'db', @@ -157,10 +157,10 @@ describeWithDockerCompose('Prisma ORM v5', { workingDirectory: [__dirname] }, () })), ).toEqual([ { name: 'INSERT "public"."User"', summary: 'INSERT "public"."User"' }, - { name: 'SELECT "public"', summary: 'SELECT "public"' }, + { name: 'SELECT "public"."User"', summary: 'SELECT "public"."User"' }, { name: 'BEGIN', summary: 'BEGIN' }, { name: 'INSERT "public"."User"', summary: 'INSERT "public"."User"' }, - { name: 'SELECT "public"', summary: 'SELECT "public"' }, + { name: 'SELECT "public"."User"', summary: 'SELECT "public"."User"' }, { name: 'COMMIT', summary: 'COMMIT' }, { name: 'DELETE "public"."User"', summary: 'DELETE "public"."User"' }, ]); diff --git a/dev-packages/node-integration-tests/suites/tracing/prisma-orm-v6/test.ts b/dev-packages/node-integration-tests/suites/tracing/prisma-orm-v6/test.ts index 0b01d4cbaa3e..542c482a0fa1 100644 --- a/dev-packages/node-integration-tests/suites/tracing/prisma-orm-v6/test.ts +++ b/dev-packages/node-integration-tests/suites/tracing/prisma-orm-v6/test.ts @@ -88,7 +88,7 @@ describeWithDockerCompose('Prisma ORM v6 Tests', { workingDirectory: [__dirname] 'sentry.op': 'db', 'db.query.text': 'SELECT "public"."User"."id", "public"."User"."createdAt", "public"."User"."email", "public"."User"."name" FROM "public"."User" WHERE 1=1 OFFSET $1', - 'db.query.summary': 'SELECT "public"', + 'db.query.summary': 'SELECT "public"."User"', 'db.system': 'postgresql', 'sentry.kind': 'client', }, @@ -136,8 +136,6 @@ describeWithDockerCompose('Prisma ORM v6 Tests', { workingDirectory: [__dirname] span: container => { const querySpans = container.items.filter(item => item.attributes['db.query.text']); - // `SELECT "public"` is what the core query-summary helper derives from a schema-qualified, - // quoted table (it stops at the first quoted identifier). expect( querySpans.map(span => ({ name: span.name, @@ -145,7 +143,7 @@ describeWithDockerCompose('Prisma ORM v6 Tests', { workingDirectory: [__dirname] })), ).toEqual([ { name: 'INSERT "public"."User"', summary: 'INSERT "public"."User"' }, - { name: 'SELECT "public"', summary: 'SELECT "public"' }, + { name: 'SELECT "public"."User"', summary: 'SELECT "public"."User"' }, { name: 'DELETE "public"."User"', summary: 'DELETE "public"."User"' }, ]); diff --git a/dev-packages/node-integration-tests/suites/tracing/prisma-orm-v7/test.ts b/dev-packages/node-integration-tests/suites/tracing/prisma-orm-v7/test.ts index b436b0f96035..b9fd94230350 100644 --- a/dev-packages/node-integration-tests/suites/tracing/prisma-orm-v7/test.ts +++ b/dev-packages/node-integration-tests/suites/tracing/prisma-orm-v7/test.ts @@ -112,8 +112,6 @@ describe('Prisma ORM v7 Tests', () => { item.attributes['sentry.origin']?.value === 'auto.db.prisma' && item.attributes['db.query.text'], ); - // `SELECT "public"` is what the core query-summary helper derives from a schema-qualified, - // quoted table (it stops at the first quoted identifier). expect( querySpans.map(span => ({ name: span.name, @@ -121,7 +119,7 @@ describe('Prisma ORM v7 Tests', () => { })), ).toEqual([ { name: 'INSERT "public"."User"', summary: 'INSERT "public"."User"' }, - { name: 'SELECT "public"', summary: 'SELECT "public"' }, + { name: 'SELECT "public"."User"', summary: 'SELECT "public"."User"' }, { name: 'DELETE "public"."User"', summary: 'DELETE "public"."User"' }, ]); diff --git a/packages/core/src/utils/sql.ts b/packages/core/src/utils/sql.ts index cc6b5a672a8a..f9f2c02600f1 100644 --- a/packages/core/src/utils/sql.ts +++ b/packages/core/src/utils/sql.ts @@ -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*(?(?:CREATE|DROP)\\s+(?:TABLE|INDEX)|ALTER\\s+TABLE)(?:\\s+IF\\s+(?:NOT\\s+)?EXISTS)?\\s+(?${TABLE_NAME})`, @@ -27,8 +33,8 @@ const SELECT_RE = /^\s*\(?\s*(?SELECT)\b/i; const PRAGMA_RE = /^\s*(?PRAGMA)\s+(?\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; /** @@ -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]); diff --git a/packages/core/test/lib/utils/sql.test.ts b/packages/core/test/lib/utils/sql.test.ts index b41463645870..3b33a2400c0c 100644 --- a/packages/core/test/lib/utils/sql.test.ts +++ b/packages/core/test/lib/utils/sql.test.ts @@ -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);