Skip to content

Commit f4b316f

Browse files
authored
feat: expose Statement sqlite3_stmt_status values (#162)
1 parent 2c6d908 commit f4b316f

File tree

5 files changed

+159
-0
lines changed

5 files changed

+159
-0
lines changed

doc.md

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -429,3 +429,31 @@ const f32FromSqlite = new Float32Array(u8FromSqlite.buffer); // safely convert b
429429

430430
Note 3: The `parseJson` option allows you to disable JSON parsing which is
431431
enabled by default.
432+
433+
## Statement Status Counters
434+
435+
Prepared statements expose SQLite `sqlite3_stmt_status()` values through helper
436+
methods:
437+
438+
- `statusFullscanStep(reset?: boolean): number`
439+
- `statusSort(reset?: boolean): number`
440+
- `statusAutoindex(reset?: boolean): number`
441+
- `statusVmStep(reset?: boolean): number`
442+
- `statusReprepare(reset?: boolean): number`
443+
- `statusRun(reset?: boolean): number`
444+
- `statusFilterMiss(reset?: boolean): number`
445+
- `statusFilterHit(reset?: boolean): number`
446+
- `statusMemused(): number`
447+
448+
Passing `true` to `reset` returns the current counter value and resets it to
449+
zero for future reads.
450+
451+
```ts
452+
const stmt = db.prepare("SELECT text FROM test ORDER BY text");
453+
stmt.values();
454+
455+
const runs = stmt.statusRun(); // > 0
456+
const vmSteps = stmt.statusVmStep(true); // read and reset
457+
const vmStepsAfterReset = stmt.statusVmStep(); // 0
458+
const mem = stmt.statusMemused(); // approximate bytes used by statement
459+
```

src/constants.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,3 +65,14 @@ export const SQLITE_FLOAT = 2;
6565
export const SQLITE_TEXT = 3;
6666
export const SQLITE_BLOB = 4;
6767
export const SQLITE_NULL = 5;
68+
69+
// Statement Status
70+
export const SQLITE_STMTSTATUS_FULLSCAN_STEP = 1;
71+
export const SQLITE_STMTSTATUS_SORT = 2;
72+
export const SQLITE_STMTSTATUS_AUTOINDEX = 3;
73+
export const SQLITE_STMTSTATUS_VM_STEP = 4;
74+
export const SQLITE_STMTSTATUS_REPREPARE = 5;
75+
export const SQLITE_STMTSTATUS_RUN = 6;
76+
export const SQLITE_STMTSTATUS_FILTER_MISS = 7;
77+
export const SQLITE_STMTSTATUS_FILTER_HIT = 8;
78+
export const SQLITE_STMTSTATUS_MEMUSED = 99;

src/ffi.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -583,6 +583,11 @@ const symbols = {
583583
],
584584
result: "pointer",
585585
},
586+
587+
sqlite3_stmt_status: {
588+
parameters: ["pointer", "i32", "i32"],
589+
result: "i32",
590+
},
586591
} as const satisfies Deno.ForeignLibraryInterface;
587592

588593
let lib: Deno.DynamicLibrary<typeof symbols>["symbols"];

src/statement.ts

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,15 @@ import {
77
SQLITE_BLOB,
88
SQLITE_FLOAT,
99
SQLITE_INTEGER,
10+
SQLITE_STMTSTATUS_AUTOINDEX,
11+
SQLITE_STMTSTATUS_FILTER_HIT,
12+
SQLITE_STMTSTATUS_FILTER_MISS,
13+
SQLITE_STMTSTATUS_FULLSCAN_STEP,
14+
SQLITE_STMTSTATUS_MEMUSED,
15+
SQLITE_STMTSTATUS_REPREPARE,
16+
SQLITE_STMTSTATUS_RUN,
17+
SQLITE_STMTSTATUS_SORT,
18+
SQLITE_STMTSTATUS_VM_STEP,
1019
SQLITE_TEXT,
1120
} from "./constants.ts";
1221

@@ -39,6 +48,7 @@ const {
3948
sqlite3_bind_parameter_name,
4049
sqlite3_changes,
4150
sqlite3_column_int,
51+
sqlite3_stmt_status,
4252
} = ffi;
4353

4454
/** Types that can be possibly serialized as SQLite bind values */
@@ -55,6 +65,17 @@ export type BindValue =
5565
| BindValue[]
5666
| { [key: string]: BindValue };
5767

68+
type StmtStatusOp =
69+
| typeof SQLITE_STMTSTATUS_FULLSCAN_STEP
70+
| typeof SQLITE_STMTSTATUS_SORT
71+
| typeof SQLITE_STMTSTATUS_AUTOINDEX
72+
| typeof SQLITE_STMTSTATUS_VM_STEP
73+
| typeof SQLITE_STMTSTATUS_REPREPARE
74+
| typeof SQLITE_STMTSTATUS_RUN
75+
| typeof SQLITE_STMTSTATUS_FILTER_MISS
76+
| typeof SQLITE_STMTSTATUS_FILTER_HIT
77+
| typeof SQLITE_STMTSTATUS_MEMUSED;
78+
5879
export type BindParameters = BindValue[] | Record<string, BindValue>;
5980
export type RestBindParameters = BindValue[] | [BindParameters];
6081

@@ -738,6 +759,64 @@ export class Statement<TStatement extends object = Record<string, any>> {
738759
}
739760
}
740761

762+
#status(op: StmtStatusOp, reset?: boolean): number {
763+
return sqlite3_stmt_status(this.#handle, op, reset ? 1 : 0);
764+
}
765+
766+
/** This is the number of times that SQLite has stepped forward in a table as part of a full table scan.
767+
* Large numbers for this counter may indicate opportunities for performance improvement through careful use of indices. */
768+
statusFullscanStep(reset?: boolean): number {
769+
return this.#status(SQLITE_STMTSTATUS_FULLSCAN_STEP, reset);
770+
}
771+
772+
/** This is the number of sort operations that have occurred.
773+
* A non-zero value in this counter may indicate an opportunity to improve performance through careful use of indices. */
774+
statusSort(reset?: boolean): number {
775+
return this.#status(SQLITE_STMTSTATUS_SORT, reset);
776+
}
777+
778+
/** This is the number of rows inserted into transient indices that were created automatically in order to help joins run faster.
779+
* 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. */
780+
statusAutoindex(reset?: boolean): number {
781+
return this.#status(SQLITE_STMTSTATUS_AUTOINDEX, reset);
782+
}
783+
784+
/** This is the number of virtual machine operations executed by the prepared statement if that number is less than or equal to 2147483647.
785+
* The number of virtual machine operations can be used as a proxy for the total work done by the prepared statement.
786+
* If the number of virtual machine operations exceeds 2147483647 then the value returned by this statement status code is undefined. */
787+
statusVmStep(reset?: boolean): number {
788+
return this.#status(SQLITE_STMTSTATUS_VM_STEP, reset);
789+
}
790+
791+
/** 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. */
792+
statusReprepare(reset?: boolean): number {
793+
return this.#status(SQLITE_STMTSTATUS_REPREPARE, reset);
794+
}
795+
796+
/** This is the number of times that the prepared statement has been run.
797+
* A single "run" for the purposes of this counter is one or more calls to sqlite3_step() followed by a call to sqlite3_reset().
798+
* The counter is incremented on the first sqlite3_step() call of each cycle. */
799+
statusRun(reset?: boolean): number {
800+
return this.#status(SQLITE_STMTSTATUS_RUN, reset);
801+
}
802+
803+
/** This is the number of times that a join step was bypassed because a Bloom filter returned not-found. */
804+
statusFilterMiss(reset?: boolean): number {
805+
return this.#status(SQLITE_STMTSTATUS_FILTER_MISS, reset);
806+
}
807+
808+
/** The corresponding SQLITE_STMTSTATUS_FILTER_MISS value is the number of times that the Bloom filter returned a find,
809+
* and thus the join step had to be processed as normal. */
810+
statusFilterHit(reset?: boolean): number {
811+
return this.#status(SQLITE_STMTSTATUS_FILTER_HIT, reset);
812+
}
813+
814+
/** This is the approximate number of bytes of heap memory used to store the prepared statement.
815+
* This value is not actually a counter. */
816+
statusMemused(): number {
817+
return this.#status(SQLITE_STMTSTATUS_MEMUSED);
818+
}
819+
741820
/** Free up the statement object. */
742821
finalize(): void {
743822
if (!STATEMENTS_TO_DB.has(this.#handle)) return;

test/test.ts

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,42 @@ Deno.test("sqlite", async (t) => {
145145
assertEquals(db.totalChanges, 12);
146146
});
147147

148+
await t.step("statement status", () => {
149+
const stmt = db.prepare("select text from test order by text");
150+
151+
assert(stmt.statusMemused() > 0);
152+
assertEquals(stmt.statusRun(), 0);
153+
assertEquals(stmt.statusVmStep(), 0);
154+
assertEquals(stmt.statusSort(), 0);
155+
156+
const rows = stmt.values<[string]>();
157+
assert(rows.length > 0);
158+
159+
assertEquals(stmt.statusRun(), 1);
160+
assert(stmt.statusVmStep() > 0);
161+
assert(stmt.statusFullscanStep() > 0);
162+
assert(stmt.statusSort() > 0);
163+
164+
assertEquals(typeof stmt.statusAutoindex(), "number");
165+
assertEquals(typeof stmt.statusReprepare(), "number");
166+
assertEquals(typeof stmt.statusFilterMiss(), "number");
167+
assertEquals(typeof stmt.statusFilterHit(), "number");
168+
169+
const sortCount = stmt.statusSort(true);
170+
assert(sortCount > 0);
171+
assertEquals(stmt.statusSort(), 0);
172+
173+
const runCount = stmt.statusRun(true);
174+
assert(runCount > 0);
175+
assertEquals(stmt.statusRun(), 0);
176+
177+
const vmStepCount = stmt.statusVmStep(true);
178+
assert(vmStepCount > 0);
179+
assertEquals(stmt.statusVmStep(), 0);
180+
181+
stmt.finalize();
182+
});
183+
148184
await t.step("query array", () => {
149185
const row = db.prepare("select * from test where integer = 0").values<
150186
[number, string, number, Uint8Array, null]

0 commit comments

Comments
 (0)