From 5dd1cd3265e97b2a48454189a9230fe75f501b9c Mon Sep 17 00:00:00 2001 From: Abdelrahman Awad Date: Fri, 7 Nov 2025 13:33:58 +0200 Subject: [PATCH 01/10] feat: implement tracing for db0 --- src/index.ts | 6 + src/tracing.ts | 97 ++++++++++ test/tracing.test.ts | 451 +++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 554 insertions(+) create mode 100644 src/tracing.ts create mode 100644 test/tracing.test.ts diff --git a/src/index.ts b/src/index.ts index 2ce54c1..1261663 100644 --- a/src/index.ts +++ b/src/index.ts @@ -13,3 +13,9 @@ export type { } from "./types.ts"; export type { ConnectorName, ConnectorOptions } from "./_connectors.ts"; + +export { + withTracing, + type TraceContext, + type TracedOperation, +} from "./tracing.ts"; diff --git a/src/tracing.ts b/src/tracing.ts new file mode 100644 index 0000000..e0f5203 --- /dev/null +++ b/src/tracing.ts @@ -0,0 +1,97 @@ +import { type TracingChannel, tracingChannel } from "node:diagnostics_channel"; +import type { Connector, Database } from "./types.ts"; +import { sqlTemplate } from "./template.ts"; + +export type TracedOperation = "query"; + +export interface TraceContext { + query: string; + method: "exec" | "sql" | "prepare.all" | "prepare.run" | "prepare.get"; +} + +const channels: Record = { + query: createChannel("query"), +}; + +/** + * Create a tracing channel for a given operation. + */ +function createChannel(operation: TracedOperation) { + return tracingChannel(`db0.${operation}`); +} + +/** + * Trace a promise with a given operation and data. + */ +async function tracePromise( + operation: TracedOperation, + exec: () => Promise, + data: TraceContext, +): Promise { + const channel = channels[operation]; + + // TODO: Remove this cast once the @types/node types are updated. + // The @types/node types incorrectly mark tracePromise as returning void, + // but according to the JSDoc and actual implementation, it returns the promise. + // This is fixed in later versions of Node.js. + // See: https://nodejs.org/api/diagnostics_channel.html#channelstracepromisefn-context-thisarg-args + return channel.tracePromise(exec, data) as unknown as Promise; +} + +type MaybeTracedDatabase = + Database & { + __traced?: boolean; + }; + +/** + * Wrap a database instance with tracing functionality. + */ +export function withTracing( + db: MaybeTracedDatabase, +): Database { + // Avoids double patching + if (db.__traced) { + return db; + } + + const tracedDb: MaybeTracedDatabase = { ...db, __traced: true }; + + tracedDb.exec = (query) => + tracePromise("query", () => db.exec(query), { query, method: "exec" }); + + tracedDb.sql = (strings, ...values) => + tracePromise("query", () => db.sql(strings, ...values), { + query: sqlTemplate(strings, ...values)[0], + method: "sql", + }); + + /** + * Prepare needs a special treatment because it returns a statement instance that needs to be patched. + */ + tracedDb.prepare = (query) => { + const statement = db.prepare(query); + const tracedStatement = { ...statement }; + + tracedStatement.all = (...params) => + tracePromise("query", () => statement.all(...params), { + query, + method: "prepare.all", + }); + + tracedStatement.run = (...params) => + tracePromise("query", () => statement.run(...params), { + query, + method: "prepare.run", + }); + + tracedStatement.get = (...params) => + tracePromise("query", () => statement.get(...params), { + query, + method: "prepare.get", + }); + + return tracedStatement; + }; + + return tracedDb; +} diff --git a/test/tracing.test.ts b/test/tracing.test.ts new file mode 100644 index 0000000..3d931de --- /dev/null +++ b/test/tracing.test.ts @@ -0,0 +1,451 @@ +import { describe, it, expect, beforeEach, vi } from "vitest"; +import { tracingChannel } from "node:diagnostics_channel"; +import { createDatabase } from "../src/database.ts"; +import { withTracing } from "../src/tracing.ts"; +import type { Database } from "../src/types.ts"; +import type { TracedOperation, TraceContext } from "../src/tracing.ts"; +import connector from "../src/connectors/better-sqlite3.ts"; + +type TracingEvent = { + start?: { data: TraceContext }; + end?: { data: TraceContext }; + asyncStart?: { data: TraceContext }; + asyncEnd?: { data: TraceContext; result?: any; error?: Error }; + error?: { data: TraceContext; error: Error }; +}; + +function createTracingListener(operationName: TracedOperation) { + const events: TracingEvent = {}; + + // Create tracing channel + const channel = tracingChannel(`db0.${operationName}`); + + // Create handlers + const startHandler = vi.fn((message: any) => { + events.start = { data: message }; + }); + + const endHandler = vi.fn((message: any) => { + events.end = { data: message }; + }); + + const asyncStartHandler = vi.fn((message: any) => { + events.asyncStart = { data: message }; + }); + + const asyncEndHandler = vi.fn((message: any) => { + events.asyncEnd = { + data: message, + result: message.result, + error: message.error, + }; + }); + + const errorHandler = vi.fn((message: any) => { + events.error = { data: message, error: message.error }; + }); + + // Subscribe using the subscribe method which listens to all events + channel.subscribe({ + start: startHandler, + end: endHandler, + asyncStart: asyncStartHandler, + asyncEnd: asyncEndHandler, + error: errorHandler, + }); + + return { + events, + handlers: { + start: startHandler, + end: endHandler, + asyncStart: asyncStartHandler, + asyncEnd: asyncEndHandler, + error: errorHandler, + }, + cleanup: () => { + channel.unsubscribe({ + start: startHandler, + end: endHandler, + asyncStart: asyncStartHandler, + asyncEnd: asyncEndHandler, + error: errorHandler, + }); + }, + }; +} + +describe("tracing", () => { + let db: Database; + + beforeEach(async () => { + const plainDb = createDatabase( + connector({ + name: ":memory:", + }), + ); + db = withTracing(plainDb); + + // Create a test table + await db.exec( + `CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT, email TEXT)`, + ); + }); + + describe("opt-in behavior", () => { + it("should not emit tracing events without withTracing wrapper", async () => { + const plainDb = createDatabase( + connector({ + name: ":memory:", + }), + ); + const listener = createTracingListener("query"); + + await plainDb.exec(`CREATE TABLE test (id INTEGER PRIMARY KEY)`); + await plainDb.sql`SELECT * FROM test`; + + // No tracing events should be emitted + expect(listener.handlers.start).not.toHaveBeenCalled(); + expect(listener.handlers.end).not.toHaveBeenCalled(); + expect(listener.handlers.asyncStart).not.toHaveBeenCalled(); + expect(listener.handlers.asyncEnd).not.toHaveBeenCalled(); + expect(listener.handlers.error).not.toHaveBeenCalled(); + + listener.cleanup(); + }); + + it("should prevent double tracing when wrapped multiple times", async () => { + const plainDb = createDatabase( + connector({ + name: ":memory:", + }), + ); + const tracedOnce = withTracing(plainDb); + const tracedTwice = withTracing(tracedOnce); + + const listener = createTracingListener("query"); + + await tracedTwice.exec(`CREATE TABLE test (id INTEGER PRIMARY KEY)`); + + // Should only be called once, not twice + expect(listener.handlers.start).toHaveBeenCalledTimes(1); + expect(listener.handlers.end).toHaveBeenCalledTimes(1); + expect(listener.handlers.asyncStart).toHaveBeenCalledTimes(1); + expect(listener.handlers.asyncEnd).toHaveBeenCalledTimes(1); + + listener.cleanup(); + }); + }); + + describe("exec", () => { + it("should emit correct tracing events on success", async () => { + const listener = createTracingListener("query"); + + const result = await db.exec( + `INSERT INTO users (id, name, email) VALUES (1, 'John Doe', 'john@example.com')`, + ); + + expect(result).toBeDefined(); + expect(listener.handlers.start).toHaveBeenCalledTimes(1); + expect(listener.handlers.end).toHaveBeenCalledTimes(1); + expect(listener.handlers.asyncStart).toHaveBeenCalledTimes(1); + expect(listener.handlers.asyncEnd).toHaveBeenCalledTimes(1); + expect(listener.handlers.error).not.toHaveBeenCalled(); + + expect(listener.events.start?.data.query).toContain("INSERT INTO users"); + expect(listener.events.start?.data.method).toBe("exec"); + + listener.cleanup(); + }); + + it("should emit error event on failure", async () => { + const listener = createTracingListener("query"); + + await expect( + db.exec(`INSERT INTO non_existing_table VALUES (1, 'test')`), + ).rejects.toThrow(); + + expect(listener.handlers.start).toHaveBeenCalledTimes(1); + // asyncStart might not be called if error is thrown synchronously + expect(listener.handlers.asyncStart).not.toHaveBeenCalledTimes(1); + expect(listener.handlers.error).toHaveBeenCalledTimes(1); + expect(listener.events.error?.error).toBeDefined(); + expect(listener.events.error?.data.query).toContain( + "INSERT INTO non_existing_table", + ); + expect(listener.events.error?.data.method).toBe("exec"); + + listener.cleanup(); + }); + }); + + describe("sql", () => { + it("should emit correct tracing events on SELECT success", async () => { + const listener = createTracingListener("query"); + + await db.exec( + `INSERT INTO users (id, name, email) VALUES (1, 'John Doe', 'john@example.com')`, + ); + + const result = await db.sql`SELECT * FROM users WHERE id = ${1}`; + + expect(result.rows).toHaveLength(1); + expect(listener.handlers.start).toHaveBeenCalled(); + expect(listener.handlers.end).toHaveBeenCalled(); + expect(listener.handlers.asyncStart).toHaveBeenCalled(); + expect(listener.handlers.asyncEnd).toHaveBeenCalled(); + expect(listener.handlers.error).not.toHaveBeenCalled(); + + // Find the SELECT query event + const selectCalls = listener.handlers.start.mock.calls.filter((call) => + call[0].query.includes("SELECT"), + ); + expect(selectCalls.length).toBeGreaterThan(0); + expect(selectCalls[0][0].method).toBe("sql"); + expect(selectCalls[0][0].query).toContain("SELECT * FROM users"); + + listener.cleanup(); + }); + + it("should emit correct tracing events on INSERT with RETURNING", async () => { + const listener = createTracingListener("query"); + + const result = + await db.sql`INSERT INTO users (id, name, email) VALUES (${2}, ${"Jane Doe"}, ${"jane@example.com"}) RETURNING *`; + + expect(result.rows).toHaveLength(1); + expect(listener.handlers.start).toHaveBeenCalled(); + expect(listener.handlers.end).toHaveBeenCalled(); + expect(listener.handlers.asyncStart).toHaveBeenCalled(); + expect(listener.handlers.asyncEnd).toHaveBeenCalled(); + expect(listener.handlers.error).not.toHaveBeenCalled(); + + // Find the INSERT query event + const insertCalls = listener.handlers.start.mock.calls.filter((call) => + call[0].query.includes("INSERT"), + ); + expect(insertCalls.length).toBeGreaterThan(0); + expect(insertCalls[0][0].method).toBe("sql"); + expect(insertCalls[0][0].query).toContain("INSERT INTO users"); + expect(insertCalls[0][0].query).toContain("RETURNING"); + + listener.cleanup(); + }); + + it("should emit error event on failure", async () => { + const listener = createTracingListener("query"); + + await expect( + db.sql`SELECT * FROM non_existing_table WHERE id = ${1}`, + ).rejects.toThrow(); + + expect(listener.handlers.start).toHaveBeenCalledTimes(1); + expect(listener.handlers.asyncStart).toHaveBeenCalledTimes(1); + expect(listener.handlers.error).toHaveBeenCalledTimes(1); + expect(listener.events.error?.error).toBeDefined(); + expect(listener.events.error?.data.query).toContain( + "SELECT * FROM non_existing_table", + ); + expect(listener.events.error?.data.method).toBe("sql"); + + listener.cleanup(); + }); + }); + + describe("prepare.all", () => { + it("should emit correct tracing events on success", async () => { + const listener = createTracingListener("query"); + + await db.exec( + `INSERT INTO users (id, name, email) VALUES (1, 'John Doe', 'john@example.com')`, + ); + await db.exec( + `INSERT INTO users (id, name, email) VALUES (2, 'Jane Doe', 'jane@example.com')`, + ); + + const stmt = db.prepare("SELECT * FROM users WHERE id > ?"); + const rows = await stmt.all(0); + + expect(rows).toHaveLength(2); + + // Find the prepare.all query event + const prepareCalls = listener.handlers.start.mock.calls.filter( + (call) => call[0].method === "prepare.all", + ); + expect(prepareCalls.length).toBeGreaterThan(0); + expect(prepareCalls[0][0].query).toContain("SELECT * FROM users"); + expect(prepareCalls[0][0].method).toBe("prepare.all"); + + expect(listener.handlers.error).not.toHaveBeenCalled(); + + listener.cleanup(); + }); + + it("should emit error event on failure", async () => { + const listener = createTracingListener("query"); + + const stmt = db.prepare("SELECT * FROM non_existing_table WHERE id = ?"); + + await expect(stmt.all(1)).rejects.toThrow(); + + const prepareCalls = listener.handlers.error.mock.calls.filter( + (call) => call[0].method === "prepare.all", + ); + expect(prepareCalls.length).toBeGreaterThan(0); + expect(prepareCalls[0][0].error).toBeDefined(); + expect(prepareCalls[0][0].query).toContain( + "SELECT * FROM non_existing_table", + ); + expect(prepareCalls[0][0].method).toBe("prepare.all"); + + listener.cleanup(); + }); + }); + + describe("prepare.run", () => { + it("should emit correct tracing events on success", async () => { + const listener = createTracingListener("query"); + + const stmt = db.prepare( + "INSERT INTO users (id, name, email) VALUES (?, ?, ?)", + ); + const result = await stmt.run(3, "Bob Smith", "bob@example.com"); + + expect(result).toBeDefined(); + + // Find the prepare.run query event + const prepareCalls = listener.handlers.start.mock.calls.filter( + (call) => call[0].method === "prepare.run", + ); + expect(prepareCalls.length).toBeGreaterThan(0); + expect(prepareCalls[0][0].query).toContain("INSERT INTO users"); + expect(prepareCalls[0][0].method).toBe("prepare.run"); + + expect(listener.handlers.error).not.toHaveBeenCalled(); + + listener.cleanup(); + }); + + it("should emit error event on failure", async () => { + const listener = createTracingListener("query"); + + const stmt = db.prepare( + "INSERT INTO non_existing_table (id, name) VALUES (?, ?)", + ); + + await expect(stmt.run(1, "test")).rejects.toThrow(); + + const prepareCalls = listener.handlers.error.mock.calls.filter( + (call) => call[0].method === "prepare.run", + ); + expect(prepareCalls.length).toBeGreaterThan(0); + expect(prepareCalls[0][0].error).toBeDefined(); + expect(prepareCalls[0][0].query).toContain( + "INSERT INTO non_existing_table", + ); + expect(prepareCalls[0][0].method).toBe("prepare.run"); + + listener.cleanup(); + }); + }); + + describe("prepare.get", () => { + it("should emit correct tracing events on success", async () => { + const listener = createTracingListener("query"); + + await db.exec( + `INSERT INTO users (id, name, email) VALUES (1, 'John Doe', 'john@example.com')`, + ); + + const stmt = db.prepare("SELECT * FROM users WHERE id = ?"); + const row = await stmt.get(1); + + expect(row).toBeDefined(); + expect((row as any).name).toBe("John Doe"); + + // Find the prepare.get query event + const prepareCalls = listener.handlers.start.mock.calls.filter( + (call) => call[0].method === "prepare.get", + ); + expect(prepareCalls.length).toBeGreaterThan(0); + expect(prepareCalls[0][0].query).toContain("SELECT * FROM users"); + expect(prepareCalls[0][0].method).toBe("prepare.get"); + + expect(listener.handlers.error).not.toHaveBeenCalled(); + + listener.cleanup(); + }); + + it("should emit error event on failure", async () => { + const listener = createTracingListener("query"); + + const stmt = db.prepare("SELECT * FROM non_existing_table WHERE id = ?"); + + await expect(stmt.get(1)).rejects.toThrow(); + + const prepareCalls = listener.handlers.error.mock.calls.filter( + (call) => call[0].method === "prepare.get", + ); + expect(prepareCalls.length).toBeGreaterThan(0); + expect(prepareCalls[0][0].error).toBeDefined(); + expect(prepareCalls[0][0].query).toContain( + "SELECT * FROM non_existing_table", + ); + expect(prepareCalls[0][0].method).toBe("prepare.get"); + + listener.cleanup(); + }); + }); + + describe("query reconstruction in sql method", () => { + it("should correctly reconstruct query with template literals", async () => { + const listener = createTracingListener("query"); + + const name = "John Doe"; + const email = "john@example.com"; + await db.sql`SELECT * FROM users WHERE name = ${name} AND email = ${email}`; + + const selectCalls = listener.handlers.start.mock.calls.filter( + (call) => call[0].method === "sql" && call[0].query.includes("SELECT"), + ); + expect(selectCalls.length).toBeGreaterThan(0); + + // Query should be reconstructed with placeholders + const query = selectCalls[0][0].query; + expect(query).toContain("SELECT * FROM users"); + expect(query).toContain("WHERE"); + expect(query).toContain("name"); + expect(query).toContain("email"); + + listener.cleanup(); + }); + }); + + describe("multiple operations", () => { + it("should emit separate events for each operation", async () => { + const listener = createTracingListener("query"); + + await db.exec( + `INSERT INTO users (id, name, email) VALUES (1, 'John Doe', 'john@example.com')`, + ); + await db.sql`SELECT * FROM users WHERE id = ${1}`; + const stmt = db.prepare("SELECT * FROM users WHERE id = ?"); + await stmt.all(1); + + // Should have events for: exec, sql (prepare.all internally), and prepare.all + expect(listener.handlers.start.mock.calls.length).toBeGreaterThanOrEqual( + 3, + ); + + // Check that different methods were called + const methods = listener.handlers.start.mock.calls.map( + (call) => call[0].method, + ); + expect(methods).toContain("exec"); + expect(methods).toContain("sql"); + expect(methods).toContain("prepare.all"); + + listener.cleanup(); + }); + }); +}); From 0ae9a84afa835f2dd9a142c22af26b40d3bcfb50 Mon Sep 17 00:00:00 2001 From: Abdelrahman Awad Date: Sun, 9 Nov 2025 00:25:33 +0200 Subject: [PATCH 02/10] feat: add dialect to the context data --- src/tracing.ts | 20 +++++++++++++++----- test/tracing.test.ts | 12 ++++++++++++ 2 files changed, 27 insertions(+), 5 deletions(-) diff --git a/src/tracing.ts b/src/tracing.ts index e0f5203..a171d4c 100644 --- a/src/tracing.ts +++ b/src/tracing.ts @@ -1,5 +1,5 @@ import { type TracingChannel, tracingChannel } from "node:diagnostics_channel"; -import type { Connector, Database } from "./types.ts"; +import type { Connector, Database, SQLDialect } from "./types.ts"; import { sqlTemplate } from "./template.ts"; export type TracedOperation = "query"; @@ -7,6 +7,7 @@ export type TracedOperation = "query"; export interface TraceContext { query: string; method: "exec" | "sql" | "prepare.all" | "prepare.run" | "prepare.get"; + dialect: SQLDialect; } const channels: Record = { @@ -57,12 +58,17 @@ export function withTracing( const tracedDb: MaybeTracedDatabase = { ...db, __traced: true }; tracedDb.exec = (query) => - tracePromise("query", () => db.exec(query), { query, method: "exec" }); + tracePromise("query", () => db.exec(query), { + query, + method: "exec", + dialect: db.dialect, + }); tracedDb.sql = (strings, ...values) => tracePromise("query", () => db.sql(strings, ...values), { query: sqlTemplate(strings, ...values)[0], method: "sql", + dialect: db.dialect, }); /** @@ -71,23 +77,27 @@ export function withTracing( tracedDb.prepare = (query) => { const statement = db.prepare(query); const tracedStatement = { ...statement }; + const partialContext = { + query, + dialect: db.dialect, + }; tracedStatement.all = (...params) => tracePromise("query", () => statement.all(...params), { - query, method: "prepare.all", + ...partialContext, }); tracedStatement.run = (...params) => tracePromise("query", () => statement.run(...params), { - query, method: "prepare.run", + ...partialContext, }); tracedStatement.get = (...params) => tracePromise("query", () => statement.get(...params), { - query, method: "prepare.get", + ...partialContext, }); return tracedStatement; diff --git a/test/tracing.test.ts b/test/tracing.test.ts index 3d931de..6708e19 100644 --- a/test/tracing.test.ts +++ b/test/tracing.test.ts @@ -154,6 +154,7 @@ describe("tracing", () => { expect(listener.events.start?.data.query).toContain("INSERT INTO users"); expect(listener.events.start?.data.method).toBe("exec"); + expect(listener.events.start?.data.dialect).toBe("sqlite"); listener.cleanup(); }); @@ -174,6 +175,7 @@ describe("tracing", () => { "INSERT INTO non_existing_table", ); expect(listener.events.error?.data.method).toBe("exec"); + expect(listener.events.error?.data.dialect).toBe("sqlite"); listener.cleanup(); }); @@ -203,6 +205,7 @@ describe("tracing", () => { expect(selectCalls.length).toBeGreaterThan(0); expect(selectCalls[0][0].method).toBe("sql"); expect(selectCalls[0][0].query).toContain("SELECT * FROM users"); + expect(selectCalls[0][0].dialect).toBe("sqlite"); listener.cleanup(); }); @@ -228,6 +231,7 @@ describe("tracing", () => { expect(insertCalls[0][0].method).toBe("sql"); expect(insertCalls[0][0].query).toContain("INSERT INTO users"); expect(insertCalls[0][0].query).toContain("RETURNING"); + expect(insertCalls[0][0].dialect).toBe("sqlite"); listener.cleanup(); }); @@ -247,6 +251,7 @@ describe("tracing", () => { "SELECT * FROM non_existing_table", ); expect(listener.events.error?.data.method).toBe("sql"); + expect(listener.events.error?.data.dialect).toBe("sqlite"); listener.cleanup(); }); @@ -275,6 +280,7 @@ describe("tracing", () => { expect(prepareCalls.length).toBeGreaterThan(0); expect(prepareCalls[0][0].query).toContain("SELECT * FROM users"); expect(prepareCalls[0][0].method).toBe("prepare.all"); + expect(prepareCalls[0][0].dialect).toBe("sqlite"); expect(listener.handlers.error).not.toHaveBeenCalled(); @@ -297,6 +303,7 @@ describe("tracing", () => { "SELECT * FROM non_existing_table", ); expect(prepareCalls[0][0].method).toBe("prepare.all"); + expect(prepareCalls[0][0].dialect).toBe("sqlite"); listener.cleanup(); }); @@ -320,6 +327,7 @@ describe("tracing", () => { expect(prepareCalls.length).toBeGreaterThan(0); expect(prepareCalls[0][0].query).toContain("INSERT INTO users"); expect(prepareCalls[0][0].method).toBe("prepare.run"); + expect(prepareCalls[0][0].dialect).toBe("sqlite"); expect(listener.handlers.error).not.toHaveBeenCalled(); @@ -344,6 +352,7 @@ describe("tracing", () => { "INSERT INTO non_existing_table", ); expect(prepareCalls[0][0].method).toBe("prepare.run"); + expect(prepareCalls[0][0].dialect).toBe("sqlite"); listener.cleanup(); }); @@ -370,6 +379,7 @@ describe("tracing", () => { expect(prepareCalls.length).toBeGreaterThan(0); expect(prepareCalls[0][0].query).toContain("SELECT * FROM users"); expect(prepareCalls[0][0].method).toBe("prepare.get"); + expect(prepareCalls[0][0].dialect).toBe("sqlite"); expect(listener.handlers.error).not.toHaveBeenCalled(); @@ -392,6 +402,7 @@ describe("tracing", () => { "SELECT * FROM non_existing_table", ); expect(prepareCalls[0][0].method).toBe("prepare.get"); + expect(prepareCalls[0][0].dialect).toBe("sqlite"); listener.cleanup(); }); @@ -416,6 +427,7 @@ describe("tracing", () => { expect(query).toContain("WHERE"); expect(query).toContain("name"); expect(query).toContain("email"); + expect(selectCalls[0][0].dialect).toBe("sqlite"); listener.cleanup(); }); From 3ce185a2d24068f0e79d89221875be0e611a1d2a Mon Sep 17 00:00:00 2001 From: Abdelrahman Awad Date: Thu, 11 Dec 2025 14:31:50 +0200 Subject: [PATCH 03/10] refactor: avoid explicit import of diag channel --- src/tracing.ts | 60 +++++++++++++++++++++----------------------------- 1 file changed, 25 insertions(+), 35 deletions(-) diff --git a/src/tracing.ts b/src/tracing.ts index a171d4c..d8ae256 100644 --- a/src/tracing.ts +++ b/src/tracing.ts @@ -1,4 +1,3 @@ -import { type TracingChannel, tracingChannel } from "node:diagnostics_channel"; import type { Connector, Database, SQLDialect } from "./types.ts"; import { sqlTemplate } from "./template.ts"; @@ -10,35 +9,6 @@ export interface TraceContext { dialect: SQLDialect; } -const channels: Record = { - query: createChannel("query"), -}; - -/** - * Create a tracing channel for a given operation. - */ -function createChannel(operation: TracedOperation) { - return tracingChannel(`db0.${operation}`); -} - -/** - * Trace a promise with a given operation and data. - */ -async function tracePromise( - operation: TracedOperation, - exec: () => Promise, - data: TraceContext, -): Promise { - const channel = channels[operation]; - - // TODO: Remove this cast once the @types/node types are updated. - // The @types/node types incorrectly mark tracePromise as returning void, - // but according to the JSDoc and actual implementation, it returns the promise. - // This is fixed in later versions of Node.js. - // See: https://nodejs.org/api/diagnostics_channel.html#channelstracepromisefn-context-thisarg-args - return channel.tracePromise(exec, data) as unknown as Promise; -} - type MaybeTracedDatabase = Database & { __traced?: boolean; @@ -55,17 +25,37 @@ export function withTracing( return db; } + const { tracingChannel } = + globalThis.process?.getBuiltinModule?.("node:diagnostics_channel") || {}; + if (!tracingChannel) { + return db; + } + + const queryChannel = tracingChannel(`db0.query`); + + async function tracePromise( + exec: () => Promise, + data: TraceContext, + ): Promise { + // TODO: Remove this cast once the @types/node types are updated. + // The @types/node types incorrectly mark tracePromise as returning void, + // but according to the JSDoc and actual implementation, it returns the promise. + // This is fixed in later versions of Node.js. + // See: https://nodejs.org/api/diagnostics_channel.html#channelstracepromisefn-context-thisarg-args + return queryChannel.tracePromise(exec, data) as unknown as Promise; + } + const tracedDb: MaybeTracedDatabase = { ...db, __traced: true }; tracedDb.exec = (query) => - tracePromise("query", () => db.exec(query), { + tracePromise(() => db.exec(query), { query, method: "exec", dialect: db.dialect, }); tracedDb.sql = (strings, ...values) => - tracePromise("query", () => db.sql(strings, ...values), { + tracePromise(() => db.sql(strings, ...values), { query: sqlTemplate(strings, ...values)[0], method: "sql", dialect: db.dialect, @@ -83,19 +73,19 @@ export function withTracing( }; tracedStatement.all = (...params) => - tracePromise("query", () => statement.all(...params), { + tracePromise(() => statement.all(...params), { method: "prepare.all", ...partialContext, }); tracedStatement.run = (...params) => - tracePromise("query", () => statement.run(...params), { + tracePromise(() => statement.run(...params), { method: "prepare.run", ...partialContext, }); tracedStatement.get = (...params) => - tracePromise("query", () => statement.get(...params), { + tracePromise(() => statement.get(...params), { method: "prepare.get", ...partialContext, }); From c0b97d18c762e8a185c07dffb2e92f93f6991598 Mon Sep 17 00:00:00 2001 From: Abdelrahman Awad Date: Thu, 11 Dec 2025 14:32:19 +0200 Subject: [PATCH 04/10] refactor: expose tracing as a subpath export --- build.config.ts | 1 + package.json | 4 ++++ src/index.ts | 6 ------ 3 files changed, 5 insertions(+), 6 deletions(-) diff --git a/build.config.ts b/build.config.ts index 96e5ede..c422c8b 100644 --- a/build.config.ts +++ b/build.config.ts @@ -3,6 +3,7 @@ import { defineBuildConfig } from "obuild/config"; export default defineBuildConfig({ entries: [ { type: "bundle", input: "src/index.ts" }, + { type: "bundle", input: "src/tracing.ts" }, { type: "transform", input: "src/connectors/", outDir: "dist/connectors" }, { type: "transform", diff --git a/package.json b/package.json index af8793b..ab2a224 100644 --- a/package.json +++ b/package.json @@ -11,6 +11,10 @@ "types": "./dist/index.d.mts", "default": "./dist/index.mjs" }, + "./tracing": { + "types": "./dist/tracing.d.mts", + "default": "./dist/tracing.mjs" + }, "./connectors/*": { "types": "./dist/connectors/*.d.ts", "default": "./dist/connectors/*.mjs" diff --git a/src/index.ts b/src/index.ts index 1261663..2ce54c1 100644 --- a/src/index.ts +++ b/src/index.ts @@ -13,9 +13,3 @@ export type { } from "./types.ts"; export type { ConnectorName, ConnectorOptions } from "./_connectors.ts"; - -export { - withTracing, - type TraceContext, - type TracedOperation, -} from "./tracing.ts"; From f858d8921050ec37960db0081371c5c881bca776 Mon Sep 17 00:00:00 2001 From: Abdelrahman Awad Date: Thu, 11 Dec 2025 14:53:23 +0200 Subject: [PATCH 05/10] docs: added info and examples to docs --- docs/1.guide/1.index.md | 2 ++ docs/1.guide/2.tracing.md | 53 +++++++++++++++++++++++++++++++++++ examples/tracing/index.ts | 50 +++++++++++++++++++++++++++++++++ examples/tracing/package.json | 11 ++++++++ 4 files changed, 116 insertions(+) create mode 100644 docs/1.guide/2.tracing.md create mode 100644 examples/tracing/index.ts create mode 100644 examples/tracing/package.json diff --git a/docs/1.guide/1.index.md b/docs/1.guide/1.index.md index 7ff5715..ab87543 100644 --- a/docs/1.guide/1.index.md +++ b/docs/1.guide/1.index.md @@ -57,6 +57,8 @@ console.log(rows); ## Next steps +:read-more{to="/guide/tracing"} + :read-more{to="/connectors"} :read-more{to="/integrations"} diff --git a/docs/1.guide/2.tracing.md b/docs/1.guide/2.tracing.md new file mode 100644 index 0000000..6595fb5 --- /dev/null +++ b/docs/1.guide/2.tracing.md @@ -0,0 +1,53 @@ +--- +icon: ph:chart-bar-horizontal-duotone +--- + +# Tracing + +> db0 provides first-class support for tracing capabilities out of the box. + +## Overview + +db0 uses [tracing channels](https://nodejs.org/api/diagnostics_channel.html) of the diagnostics channel module to emit traceable actions for query operations. + +Common use cases for tracing are: + +- Query performance monitoring and profiling +- Logging +- Query debugging +- Error tracking + +## Query Tracing Channel + +db0 uses `db0.query` for the tracing channel name. + +To set up tracing, you need to subscribe to the tracing channel. Since query operations are asynchronous, you need to subscribe to the `asyncEnd` event as it signals the completion of the operation. + +```ts +import { tracingChannel } from "node:diagnostics_channel"; + +const queryChannel = tracingChannel("db0.query"); + +queryChannel.subscribe({ + start: (data) => { + console.log("start", data.query); + }, + asyncEnd: (data) => { + console.log("end", data.query, data.result); + }, +}); +``` + +The event payload contains several properties that can be used to track the query operation: + +- `query`: The query string. +- `method`: The method used to execute the query (`exec`, `sql`, `prepare.all`, `prepare.run`, `prepare.get`). +- `dialect`: The dialect of the database connection (e.g. `sqlite`, `postgres`, `mysql`). +- `result`: The result of the query (only available for `asyncEnd` event). +- `error`: The error that occurred during the query operation (only available for `error` and `asyncEnd` events). + +## Next steps + +:read-more{to="/connectors"} + +:read-more{to="/integrations"} diff --git a/examples/tracing/index.ts b/examples/tracing/index.ts new file mode 100644 index 0000000..c040c77 --- /dev/null +++ b/examples/tracing/index.ts @@ -0,0 +1,50 @@ +import { createDatabase } from "../../src"; +import { TraceContext, withTracing } from "../../src/tracing"; +import { tracingChannel } from "node:diagnostics_channel"; + +import sqlite from "../../src/connectors/better-sqlite3"; + +async function main() { + const db = withTracing(createDatabase(sqlite({}))); + + // Subscribe to tracing events + subscribeToTracing(); + + await db.sql`create table if not exists users ( + id integer primary key autoincrement, + full_name text + )`; + + const res = await db.sql`insert into users (full_name) values ('John Doe')`; + + console.log({ res }); +} + +function subscribeToTracing() { + const queryChannel = tracingChannel("db0.query"); + + queryChannel.subscribe({ + start: (data) => { + console.log("start", data.query); + }, + end: (message) => { + console.log("end", message.query); + }, + asyncStart: (data) => { + console.log("asyncStart", data.query); + }, + asyncEnd: (data) => { + console.log("asyncEnd", data.query, data.result); + }, + error: (data) => { + console.log("error", data.error); + }, + }); +} + +// eslint-disable-next-line unicorn/prefer-top-level-await +main().catch((error) => { + console.error(error); + // eslint-disable-next-line unicorn/no-process-exit + process.exit(1); +}); diff --git a/examples/tracing/package.json b/examples/tracing/package.json new file mode 100644 index 0000000..b27aff4 --- /dev/null +++ b/examples/tracing/package.json @@ -0,0 +1,11 @@ +{ + "name": "db0-with-tracing", + "private": true, + "scripts": { + "start": "jiti ./index.ts" + }, + "devDependencies": { + "jiti": "^1.21.0", + "db0": "latest" + } +} From d219d5eb46216b722aa952d403ae9a967ffaefb5 Mon Sep 17 00:00:00 2001 From: Abdelrahman Awad Date: Thu, 18 Dec 2025 13:20:20 +0200 Subject: [PATCH 06/10] feat: traced statement class --- src/tracing.ts | 72 ++++++++++++++++++++++++++++++-------------------- 1 file changed, 44 insertions(+), 28 deletions(-) diff --git a/src/tracing.ts b/src/tracing.ts index d8ae256..209df7a 100644 --- a/src/tracing.ts +++ b/src/tracing.ts @@ -1,4 +1,10 @@ -import type { Connector, Database, SQLDialect } from "./types.ts"; +import type { + Connector, + Database, + Primitive, + SQLDialect, + Statement, +} from "./types.ts"; import { sqlTemplate } from "./template.ts"; export type TracedOperation = "query"; @@ -61,37 +67,47 @@ export function withTracing( dialect: db.dialect, }); - /** - * Prepare needs a special treatment because it returns a statement instance that needs to be patched. - */ - tracedDb.prepare = (query) => { - const statement = db.prepare(query); - const tracedStatement = { ...statement }; - const partialContext = { - query, - dialect: db.dialect, - }; - - tracedStatement.all = (...params) => - tracePromise(() => statement.all(...params), { - method: "prepare.all", - ...partialContext, + class TracedStatement implements Statement { + #statement: Statement; + #query: string; + + constructor(statement: Statement, query: string) { + this.#statement = statement; + this.#query = query; + } + + private withTrace( + fn: () => Promise, + method: "prepare.all" | "prepare.run" | "prepare.get", + ) { + return tracePromise(() => fn(), { + method, + query: this.#query, + dialect: db.dialect, }); + } - tracedStatement.run = (...params) => - tracePromise(() => statement.run(...params), { - method: "prepare.run", - ...partialContext, - }); + bind(...args: Primitive[]) { + return this.#statement.bind(...args); + } - tracedStatement.get = (...params) => - tracePromise(() => statement.get(...params), { - method: "prepare.get", - ...partialContext, - }); + all(...args: Primitive[]) { + return this.withTrace(() => this.#statement.all(...args), "prepare.all"); + } - return tracedStatement; - }; + run(...args: Primitive[]) { + return this.withTrace(() => this.#statement.run(...args), "prepare.run"); + } + + get(...args: Primitive[]) { + return this.withTrace(() => this.#statement.get(...args), "prepare.get"); + } + } + + /** + * Prepare needs a special treatment because it returns a statement instance that needs to be patched. + */ + tracedDb.prepare = (query) => new TracedStatement(db.prepare(query), query); return tracedDb; } From b36f56700f87c35058757f3d616023b2c14d67e6 Mon Sep 17 00:00:00 2001 From: Abdelrahman Awad Date: Thu, 18 Dec 2025 14:24:08 +0200 Subject: [PATCH 07/10] fix: use prepared statement as a base instead --- src/tracing.ts | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/src/tracing.ts b/src/tracing.ts index 209df7a..512c358 100644 --- a/src/tracing.ts +++ b/src/tracing.ts @@ -1,9 +1,9 @@ import type { Connector, Database, + PreparedStatement, Primitive, SQLDialect, - Statement, } from "./types.ts"; import { sqlTemplate } from "./template.ts"; @@ -67,11 +67,11 @@ export function withTracing( dialect: db.dialect, }); - class TracedStatement implements Statement { - #statement: Statement; + class TracedStatement implements PreparedStatement { + #statement: PreparedStatement; #query: string; - constructor(statement: Statement, query: string) { + constructor(statement: PreparedStatement, query: string) { this.#statement = statement; this.#query = query; } @@ -88,19 +88,19 @@ export function withTracing( } bind(...args: Primitive[]) { - return this.#statement.bind(...args); + return new TracedStatement(this.#statement.bind(...args), this.#query); } - all(...args: Primitive[]) { - return this.withTrace(() => this.#statement.all(...args), "prepare.all"); + all() { + return this.withTrace(() => this.#statement.all(), "prepare.all"); } - run(...args: Primitive[]) { - return this.withTrace(() => this.#statement.run(...args), "prepare.run"); + run() { + return this.withTrace(() => this.#statement.run(), "prepare.run"); } - get(...args: Primitive[]) { - return this.withTrace(() => this.#statement.get(...args), "prepare.get"); + get() { + return this.withTrace(() => this.#statement.get(), "prepare.get"); } } From 7cdb8959427eea910ca635bff0496ef4cd6edcc2 Mon Sep 17 00:00:00 2001 From: Abdelrahman Awad Date: Thu, 18 Dec 2025 14:29:52 +0200 Subject: [PATCH 08/10] fix: handle nested bind statements --- src/tracing.ts | 20 +++--- test/tracing.test.ts | 146 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 156 insertions(+), 10 deletions(-) diff --git a/src/tracing.ts b/src/tracing.ts index 512c358..5a3f6f0 100644 --- a/src/tracing.ts +++ b/src/tracing.ts @@ -1,9 +1,9 @@ import type { Connector, Database, - PreparedStatement, Primitive, SQLDialect, + Statement, } from "./types.ts"; import { sqlTemplate } from "./template.ts"; @@ -67,11 +67,11 @@ export function withTracing( dialect: db.dialect, }); - class TracedStatement implements PreparedStatement { - #statement: PreparedStatement; + class TracedStatement implements Statement { + #statement: Statement; #query: string; - constructor(statement: PreparedStatement, query: string) { + constructor(statement: Statement, query: string) { this.#statement = statement; this.#query = query; } @@ -91,16 +91,16 @@ export function withTracing( return new TracedStatement(this.#statement.bind(...args), this.#query); } - all() { - return this.withTrace(() => this.#statement.all(), "prepare.all"); + all(...args: Primitive[]) { + return this.withTrace(() => this.#statement.all(...args), "prepare.all"); } - run() { - return this.withTrace(() => this.#statement.run(), "prepare.run"); + run(...args: Primitive[]) { + return this.withTrace(() => this.#statement.run(...args), "prepare.run"); } - get() { - return this.withTrace(() => this.#statement.get(), "prepare.get"); + get(...args: Primitive[]) { + return this.withTrace(() => this.#statement.get(...args), "prepare.get"); } } diff --git a/test/tracing.test.ts b/test/tracing.test.ts index 6708e19..43ca347 100644 --- a/test/tracing.test.ts +++ b/test/tracing.test.ts @@ -460,4 +460,150 @@ describe("tracing", () => { listener.cleanup(); }); }); + + describe("nested bind support", () => { + it("should support chained bind calls with all()", async () => { + const listener = createTracingListener("query"); + + await db.exec( + `INSERT INTO users (id, name, email) VALUES (1, 'John Doe', 'john@example.com')`, + ); + await db.exec( + `INSERT INTO users (id, name, email) VALUES (2, 'Jane Doe', 'jane@example.com')`, + ); + + const stmt = db.prepare("SELECT * FROM users WHERE id > ?"); + const boundStmt = stmt.bind(0); + const rows = await boundStmt.all(); + + expect(rows).toHaveLength(2); + + // Find the prepare.all query event + const prepareCalls = listener.handlers.start.mock.calls.filter( + (call) => call[0].method === "prepare.all", + ); + expect(prepareCalls.length).toBeGreaterThan(0); + expect(prepareCalls[0][0].query).toContain("SELECT * FROM users"); + expect(prepareCalls[0][0].method).toBe("prepare.all"); + expect(prepareCalls[0][0].dialect).toBe("sqlite"); + + expect(listener.handlers.error).not.toHaveBeenCalled(); + + listener.cleanup(); + }); + + it("should support chained bind calls with run()", async () => { + const listener = createTracingListener("query"); + + const stmt = db.prepare( + "INSERT INTO users (id, name, email) VALUES (?, ?, ?)", + ); + const boundStmt = stmt.bind(10, "Alice Smith", "alice@example.com"); + const result = await boundStmt.run(); + + expect(result).toBeDefined(); + + // Find the prepare.run query event + const prepareCalls = listener.handlers.start.mock.calls.filter( + (call) => call[0].method === "prepare.run", + ); + expect(prepareCalls.length).toBeGreaterThan(0); + expect(prepareCalls[0][0].query).toContain("INSERT INTO users"); + expect(prepareCalls[0][0].method).toBe("prepare.run"); + expect(prepareCalls[0][0].dialect).toBe("sqlite"); + + expect(listener.handlers.error).not.toHaveBeenCalled(); + + listener.cleanup(); + }); + + it("should support chained bind calls with get()", async () => { + const listener = createTracingListener("query"); + + await db.exec( + `INSERT INTO users (id, name, email) VALUES (1, 'John Doe', 'john@example.com')`, + ); + + const stmt = db.prepare("SELECT * FROM users WHERE id = ?"); + const boundStmt = stmt.bind(1); + const row = await boundStmt.get(); + + expect(row).toBeDefined(); + expect((row as any).name).toBe("John Doe"); + + // Find the prepare.get query event + const prepareCalls = listener.handlers.start.mock.calls.filter( + (call) => call[0].method === "prepare.get", + ); + expect(prepareCalls.length).toBeGreaterThan(0); + expect(prepareCalls[0][0].query).toContain("SELECT * FROM users"); + expect(prepareCalls[0][0].method).toBe("prepare.get"); + expect(prepareCalls[0][0].dialect).toBe("sqlite"); + + expect(listener.handlers.error).not.toHaveBeenCalled(); + + listener.cleanup(); + }); + + it("should support multiple nested bind calls", async () => { + const listener = createTracingListener("query"); + + await db.exec( + `INSERT INTO users (id, name, email) VALUES (1, 'John Doe', 'john@example.com')`, + ); + + const stmt = db.prepare( + "SELECT * FROM users WHERE id = ? AND name = ? AND email = ?", + ); + const row = await stmt.bind(1, "John Doe", "john@example.com").get(); + + expect(row).toBeDefined(); + expect((row as any).name).toBe("John Doe"); + expect((row as any).email).toBe("john@example.com"); + + // Find the prepare.get query event + const prepareCalls = listener.handlers.start.mock.calls.filter( + (call) => call[0].method === "prepare.get", + ); + expect(prepareCalls.length).toBeGreaterThan(0); + expect(prepareCalls[0][0].query).toContain("SELECT * FROM users"); + expect(prepareCalls[0][0].method).toBe("prepare.get"); + expect(prepareCalls[0][0].dialect).toBe("sqlite"); + + expect(listener.handlers.error).not.toHaveBeenCalled(); + + listener.cleanup(); + }); + + it("should preserve query context through nested binds", async () => { + const listener = createTracingListener("query"); + + const query = "SELECT * FROM users WHERE id = ?"; + const stmt = db.prepare(query); + + await db.exec( + `INSERT INTO users (id, name, email) VALUES (1, 'John Doe', 'john@example.com')`, + ); + + // Bind in multiple steps - each rebind replaces the parameters + const step1 = stmt.bind(999); // This will be replaced + const step2 = step1.bind(1); // This is the final binding + const row = await step2.get(); + + expect(row).toBeDefined(); + expect((row as any).name).toBe("John Doe"); + + // Find the prepare.get query event + const prepareCalls = listener.handlers.start.mock.calls.filter( + (call) => call[0].method === "prepare.get", + ); + expect(prepareCalls.length).toBeGreaterThan(0); + + // The original query should be preserved through all bind operations + expect(prepareCalls[0][0].query).toBe(query); + expect(prepareCalls[0][0].dialect).toBe("sqlite"); + + listener.cleanup(); + }); + }); }); From c335701492524983b2903606bbe3e7c6a711f302 Mon Sep 17 00:00:00 2001 From: Abdelrahman Awad Date: Wed, 24 Dec 2025 15:17:26 +0200 Subject: [PATCH 09/10] fix: make sure to preserver dialect and disposed getters --- src/tracing.ts | 6 ++++- test/tracing.test.ts | 53 +++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 57 insertions(+), 2 deletions(-) diff --git a/src/tracing.ts b/src/tracing.ts index 5a3f6f0..d09c3f0 100644 --- a/src/tracing.ts +++ b/src/tracing.ts @@ -51,7 +51,11 @@ export function withTracing( return queryChannel.tracePromise(exec, data) as unknown as Promise; } - const tracedDb: MaybeTracedDatabase = { ...db, __traced: true }; + // Use Object.create to preserve getter properties like `dialect` and `disposed` + // The spread operator would evaluate getters at spread-time, making `disposed` + // always return the initial value rather than the current state. + const tracedDb = Object.create(db) as MaybeTracedDatabase; + tracedDb.__traced = true; tracedDb.exec = (query) => tracePromise(() => db.exec(query), { diff --git a/test/tracing.test.ts b/test/tracing.test.ts index 43ca347..f28caa0 100644 --- a/test/tracing.test.ts +++ b/test/tracing.test.ts @@ -137,6 +137,57 @@ describe("tracing", () => { }); }); + describe("getter properties", () => { + it("should preserve dialect getter from original database", () => { + const plainDb = createDatabase( + connector({ + name: ":memory:", + }), + ); + const tracedDb = withTracing(plainDb); + + expect(tracedDb.dialect).toBe("sqlite"); + expect(tracedDb.dialect).toBe(plainDb.dialect); + }); + + it("should preserve disposed getter and reflect current state", async () => { + const plainDb = createDatabase( + connector({ + name: ":memory:", + }), + ); + const tracedDb = withTracing(plainDb); + + // Initially not disposed + expect(tracedDb.disposed).toBe(false); + expect(tracedDb.disposed).toBe(plainDb.disposed); + + // Dispose the database + await tracedDb.dispose(); + + // disposed should now be true (testing the getter reflects current state) + expect(tracedDb.disposed).toBe(true); + expect(tracedDb.disposed).toBe(plainDb.disposed); + }); + + it("should reflect disposed state when original db is disposed", async () => { + const plainDb = createDatabase( + connector({ + name: ":memory:", + }), + ); + const tracedDb = withTracing(plainDb); + + expect(tracedDb.disposed).toBe(false); + + // Dispose via the original database + await plainDb.dispose(); + + // tracedDb.disposed should reflect the change + expect(tracedDb.disposed).toBe(true); + }); + }); + describe("exec", () => { it("should emit correct tracing events on success", async () => { const listener = createTracingListener("query"); @@ -168,7 +219,7 @@ describe("tracing", () => { expect(listener.handlers.start).toHaveBeenCalledTimes(1); // asyncStart might not be called if error is thrown synchronously - expect(listener.handlers.asyncStart).not.toHaveBeenCalledTimes(1); + expect(listener.handlers.asyncStart).not.toHaveBeenCalled(); expect(listener.handlers.error).toHaveBeenCalledTimes(1); expect(listener.events.error?.error).toBeDefined(); expect(listener.events.error?.data.query).toContain( From b1244f6f1b076f54e242d939eb04b5d5deb671bd Mon Sep 17 00:00:00 2001 From: Abdelrahman Awad Date: Tue, 30 Dec 2025 16:28:14 +0200 Subject: [PATCH 10/10] chore: update lock --- pnpm-lock.yaml | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index dff25f4..5183ee1 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -106,6 +106,15 @@ importers: specifier: ^1.21.0 version: 1.21.7 + examples/tracing: + devDependencies: + db0: + specifier: latest + version: 0.3.4(@electric-sql/pglite@0.3.14)(@libsql/client@0.15.15)(better-sqlite3@12.4.1)(drizzle-orm@0.44.7(@cloudflare/workers-types@4.20251120.0)(@electric-sql/pglite@0.3.14)(@libsql/client@0.15.15)(@planetscale/database@1.19.0)(@types/better-sqlite3@7.6.13)(@types/pg@8.15.6)(better-sqlite3@12.4.1)(bun-types@1.3.2(@types/react@19.1.13))(mysql2@3.15.3)(pg@8.16.3)(sqlite3@5.1.7))(mysql2@3.15.3)(sqlite3@5.1.7) + jiti: + specifier: ^1.21.0 + version: 1.21.7 + packages: '@babel/generator@7.28.5': @@ -5275,6 +5284,15 @@ snapshots: mysql2: 3.15.3 sqlite3: 5.1.7 + db0@0.3.4(@electric-sql/pglite@0.3.14)(@libsql/client@0.15.15)(better-sqlite3@12.4.1)(drizzle-orm@0.44.7(@cloudflare/workers-types@4.20251120.0)(@electric-sql/pglite@0.3.14)(@libsql/client@0.15.15)(@planetscale/database@1.19.0)(@types/better-sqlite3@7.6.13)(@types/pg@8.15.6)(better-sqlite3@12.4.1)(bun-types@1.3.2(@types/react@19.1.13))(mysql2@3.15.3)(pg@8.16.3)(sqlite3@5.1.7))(mysql2@3.15.3)(sqlite3@5.1.7): + optionalDependencies: + '@electric-sql/pglite': 0.3.14 + '@libsql/client': 0.15.15 + better-sqlite3: 12.4.1 + drizzle-orm: 0.44.7(@cloudflare/workers-types@4.20251120.0)(@electric-sql/pglite@0.3.14)(@libsql/client@0.15.15)(@planetscale/database@1.19.0)(@types/better-sqlite3@7.6.13)(@types/pg@8.15.6)(better-sqlite3@12.4.1)(bun-types@1.3.2(@types/react@19.1.13))(mysql2@3.15.3)(pg@8.16.3)(sqlite3@5.1.7) + mysql2: 3.15.3 + sqlite3: 5.1.7 + debug@4.4.3: dependencies: ms: 2.1.3