diff --git a/doc.md b/doc.md index bf14d0e..8fad1b6 100644 --- a/doc.md +++ b/doc.md @@ -429,3 +429,31 @@ const f32FromSqlite = new Float32Array(u8FromSqlite.buffer); // safely convert b Note 3: The `parseJson` option allows you to disable JSON parsing which is enabled by default. + +## Statement Status Counters + +Prepared statements expose SQLite `sqlite3_stmt_status()` values through helper +methods: + +- `statusFullscanStep(reset?: boolean): number` +- `statusSort(reset?: boolean): number` +- `statusAutoindex(reset?: boolean): number` +- `statusVmStep(reset?: boolean): number` +- `statusReprepare(reset?: boolean): number` +- `statusRun(reset?: boolean): number` +- `statusFilterMiss(reset?: boolean): number` +- `statusFilterHit(reset?: boolean): number` +- `statusMemused(): number` + +Passing `true` to `reset` returns the current counter value and resets it to +zero for future reads. + +```ts +const stmt = db.prepare("SELECT text FROM test ORDER BY text"); +stmt.values(); + +const runs = stmt.statusRun(); // > 0 +const vmSteps = stmt.statusVmStep(true); // read and reset +const vmStepsAfterReset = stmt.statusVmStep(); // 0 +const mem = stmt.statusMemused(); // approximate bytes used by statement +``` diff --git a/src/constants.ts b/src/constants.ts index aa67db7..9914b30 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -65,3 +65,14 @@ export const SQLITE_FLOAT = 2; export const SQLITE_TEXT = 3; export const SQLITE_BLOB = 4; export const SQLITE_NULL = 5; + +// Statement Status +export const SQLITE_STMTSTATUS_FULLSCAN_STEP = 1; +export const SQLITE_STMTSTATUS_SORT = 2; +export const SQLITE_STMTSTATUS_AUTOINDEX = 3; +export const SQLITE_STMTSTATUS_VM_STEP = 4; +export const SQLITE_STMTSTATUS_REPREPARE = 5; +export const SQLITE_STMTSTATUS_RUN = 6; +export const SQLITE_STMTSTATUS_FILTER_MISS = 7; +export const SQLITE_STMTSTATUS_FILTER_HIT = 8; +export const SQLITE_STMTSTATUS_MEMUSED = 99; diff --git a/src/ffi.ts b/src/ffi.ts index 80d8ca8..d092489 100644 --- a/src/ffi.ts +++ b/src/ffi.ts @@ -583,6 +583,11 @@ const symbols = { ], result: "pointer", }, + + sqlite3_stmt_status: { + parameters: ["pointer", "i32", "i32"], + result: "i32", + }, } as const satisfies Deno.ForeignLibraryInterface; let lib: Deno.DynamicLibrary["symbols"]; diff --git a/src/statement.ts b/src/statement.ts index e6ff4e3..264dccd 100644 --- a/src/statement.ts +++ b/src/statement.ts @@ -7,6 +7,15 @@ import { SQLITE_BLOB, SQLITE_FLOAT, SQLITE_INTEGER, + SQLITE_STMTSTATUS_AUTOINDEX, + SQLITE_STMTSTATUS_FILTER_HIT, + SQLITE_STMTSTATUS_FILTER_MISS, + SQLITE_STMTSTATUS_FULLSCAN_STEP, + SQLITE_STMTSTATUS_MEMUSED, + SQLITE_STMTSTATUS_REPREPARE, + SQLITE_STMTSTATUS_RUN, + SQLITE_STMTSTATUS_SORT, + SQLITE_STMTSTATUS_VM_STEP, SQLITE_TEXT, } from "./constants.ts"; @@ -39,6 +48,7 @@ const { sqlite3_bind_parameter_name, sqlite3_changes, sqlite3_column_int, + sqlite3_stmt_status, } = ffi; /** Types that can be possibly serialized as SQLite bind values */ @@ -55,6 +65,17 @@ export type BindValue = | BindValue[] | { [key: string]: BindValue }; +type StmtStatusOp = + | typeof SQLITE_STMTSTATUS_FULLSCAN_STEP + | typeof SQLITE_STMTSTATUS_SORT + | typeof SQLITE_STMTSTATUS_AUTOINDEX + | typeof SQLITE_STMTSTATUS_VM_STEP + | typeof SQLITE_STMTSTATUS_REPREPARE + | typeof SQLITE_STMTSTATUS_RUN + | typeof SQLITE_STMTSTATUS_FILTER_MISS + | typeof SQLITE_STMTSTATUS_FILTER_HIT + | typeof SQLITE_STMTSTATUS_MEMUSED; + export type BindParameters = BindValue[] | Record; export type RestBindParameters = BindValue[] | [BindParameters]; @@ -738,6 +759,64 @@ export class Statement> { } } + #status(op: StmtStatusOp, reset?: boolean): number { + return sqlite3_stmt_status(this.#handle, op, reset ? 1 : 0); + } + + /** This is the number of times that SQLite has stepped forward in a table as part of a full table scan. + * Large numbers for this counter may indicate opportunities for performance improvement through careful use of indices. */ + statusFullscanStep(reset?: boolean): number { + return this.#status(SQLITE_STMTSTATUS_FULLSCAN_STEP, reset); + } + + /** This is the number of sort operations that have occurred. + * A non-zero value in this counter may indicate an opportunity to improve performance through careful use of indices. */ + statusSort(reset?: boolean): number { + return this.#status(SQLITE_STMTSTATUS_SORT, reset); + } + + /** This is the number of rows inserted into transient indices that were created automatically in order to help joins run faster. + * A non-zero value in this counter may indicate an opportunity to improve performance by adding permanent indices that do not need to be reinitialized each time the statement is run. */ + statusAutoindex(reset?: boolean): number { + return this.#status(SQLITE_STMTSTATUS_AUTOINDEX, reset); + } + + /** This is the number of virtual machine operations executed by the prepared statement if that number is less than or equal to 2147483647. + * The number of virtual machine operations can be used as a proxy for the total work done by the prepared statement. + * If the number of virtual machine operations exceeds 2147483647 then the value returned by this statement status code is undefined. */ + statusVmStep(reset?: boolean): number { + return this.#status(SQLITE_STMTSTATUS_VM_STEP, reset); + } + + /** This is the number of times that the prepare statement has been automatically regenerated due to schema changes or changes to bound parameters that might affect the query plan. */ + statusReprepare(reset?: boolean): number { + return this.#status(SQLITE_STMTSTATUS_REPREPARE, reset); + } + + /** This is the number of times that the prepared statement has been run. + * A single "run" for the purposes of this counter is one or more calls to sqlite3_step() followed by a call to sqlite3_reset(). + * The counter is incremented on the first sqlite3_step() call of each cycle. */ + statusRun(reset?: boolean): number { + return this.#status(SQLITE_STMTSTATUS_RUN, reset); + } + + /** This is the number of times that a join step was bypassed because a Bloom filter returned not-found. */ + statusFilterMiss(reset?: boolean): number { + return this.#status(SQLITE_STMTSTATUS_FILTER_MISS, reset); + } + + /** The corresponding SQLITE_STMTSTATUS_FILTER_MISS value is the number of times that the Bloom filter returned a find, + * and thus the join step had to be processed as normal. */ + statusFilterHit(reset?: boolean): number { + return this.#status(SQLITE_STMTSTATUS_FILTER_HIT, reset); + } + + /** This is the approximate number of bytes of heap memory used to store the prepared statement. + * This value is not actually a counter. */ + statusMemused(): number { + return this.#status(SQLITE_STMTSTATUS_MEMUSED); + } + /** Free up the statement object. */ finalize(): void { if (!STATEMENTS_TO_DB.has(this.#handle)) return; diff --git a/test/test.ts b/test/test.ts index 4dc1e61..405fde5 100644 --- a/test/test.ts +++ b/test/test.ts @@ -145,6 +145,42 @@ Deno.test("sqlite", async (t) => { assertEquals(db.totalChanges, 12); }); + await t.step("statement status", () => { + const stmt = db.prepare("select text from test order by text"); + + assert(stmt.statusMemused() > 0); + assertEquals(stmt.statusRun(), 0); + assertEquals(stmt.statusVmStep(), 0); + assertEquals(stmt.statusSort(), 0); + + const rows = stmt.values<[string]>(); + assert(rows.length > 0); + + assertEquals(stmt.statusRun(), 1); + assert(stmt.statusVmStep() > 0); + assert(stmt.statusFullscanStep() > 0); + assert(stmt.statusSort() > 0); + + assertEquals(typeof stmt.statusAutoindex(), "number"); + assertEquals(typeof stmt.statusReprepare(), "number"); + assertEquals(typeof stmt.statusFilterMiss(), "number"); + assertEquals(typeof stmt.statusFilterHit(), "number"); + + const sortCount = stmt.statusSort(true); + assert(sortCount > 0); + assertEquals(stmt.statusSort(), 0); + + const runCount = stmt.statusRun(true); + assert(runCount > 0); + assertEquals(stmt.statusRun(), 0); + + const vmStepCount = stmt.statusVmStep(true); + assert(vmStepCount > 0); + assertEquals(stmt.statusVmStep(), 0); + + stmt.finalize(); + }); + await t.step("query array", () => { const row = db.prepare("select * from test where integer = 0").values< [number, string, number, Uint8Array, null]