@gjsify/sqlite 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,148 @@
1
+ // Parameter binding helpers for SQLite statements via Gda
2
+ // Reference: Node.js lib/sqlite.js
3
+ // Reimplemented for GJS using Gda-6.0
4
+
5
+ import Gda from '@girs/gda-6.0';
6
+ import GObject from '@girs/gobject-2.0';
7
+ import { InvalidArgTypeError, InvalidArgValueError, InvalidStateError, SqliteError } from './errors.ts';
8
+ import type { SQLiteValue } from './types.ts';
9
+
10
+ const MAX_INT64 = 9223372036854775807n;
11
+ const MIN_INT64 = -9223372036854775808n;
12
+
13
+ function validateBindValue(value: unknown, paramIndex: number): void {
14
+ if (value === null || value === undefined) return;
15
+ const t = typeof value;
16
+ if (t === 'number' || t === 'bigint' || t === 'string' || t === 'boolean') return;
17
+ if (value instanceof Uint8Array || value instanceof ArrayBuffer) return;
18
+ if (ArrayBuffer.isView(value)) return;
19
+ throw new InvalidArgTypeError(
20
+ `Provided value cannot be bound to SQLite parameter ${paramIndex}.`
21
+ );
22
+ }
23
+
24
+ function setHolderValue(holder: Gda.Holder, value: unknown): void {
25
+ if (value === null || value === undefined) {
26
+ holder.set_value(null);
27
+ return;
28
+ }
29
+ if (typeof value === 'number') {
30
+ holder.set_value(value as any);
31
+ return;
32
+ }
33
+ if (typeof value === 'bigint') {
34
+ if (value > MAX_INT64 || value < MIN_INT64) {
35
+ throw new InvalidArgValueError(
36
+ `BigInt value is too large to bind.`
37
+ );
38
+ }
39
+ holder.set_value(Number(value) as any);
40
+ return;
41
+ }
42
+ if (typeof value === 'string') {
43
+ holder.set_value(value as any);
44
+ return;
45
+ }
46
+ if (typeof value === 'boolean') {
47
+ holder.set_value((value ? 1 : 0) as any);
48
+ return;
49
+ }
50
+ if (value instanceof Uint8Array || ArrayBuffer.isView(value)) {
51
+ const bytes = value instanceof Uint8Array ? value : new Uint8Array((value as ArrayBufferView).buffer);
52
+ holder.set_value(bytes as any);
53
+ return;
54
+ }
55
+ holder.set_value(value as any);
56
+ }
57
+
58
+ export interface BindingContext {
59
+ allowBareNamedParameters: boolean;
60
+ allowUnknownNamedParameters: boolean;
61
+ }
62
+
63
+ export function bindParameters(
64
+ paramSet: Gda.Set | null,
65
+ anonymousArgs: unknown[],
66
+ namedArgs: Record<string, unknown> | null,
67
+ ctx: BindingContext,
68
+ ): void {
69
+ if (!paramSet) {
70
+ if (anonymousArgs.length > 0) {
71
+ throw new SqliteError('column index out of range', 25, 'column index out of range');
72
+ }
73
+ return;
74
+ }
75
+
76
+ const holders = paramSet.get_holders();
77
+
78
+ if (namedArgs) {
79
+ // Named parameter binding
80
+ const usedKeys = new Set<string>();
81
+
82
+ for (const holder of holders) {
83
+ const id = holder.get_id();
84
+ let value: unknown = undefined;
85
+ let found = false;
86
+
87
+ // Try exact match (with prefix): $name, :name, @name
88
+ if (id in namedArgs) {
89
+ value = namedArgs[id];
90
+ usedKeys.add(id);
91
+ found = true;
92
+ }
93
+
94
+ // Try bare name (without prefix)
95
+ if (!found && ctx.allowBareNamedParameters) {
96
+ const bareName = id.replace(/^[\$:@]/, '');
97
+ if (bareName in namedArgs) {
98
+ value = namedArgs[bareName];
99
+ usedKeys.add(bareName);
100
+ found = true;
101
+ }
102
+ }
103
+
104
+ if (!found && !ctx.allowBareNamedParameters) {
105
+ // Check if user passed bare name — error
106
+ const bareName = id.replace(/^[\$:@]/, '');
107
+ if (bareName in namedArgs) {
108
+ throw new InvalidStateError(`Unknown named parameter '${bareName}'`);
109
+ }
110
+ }
111
+
112
+ if (found) {
113
+ const paramIdx = holders.indexOf(holder) + 1;
114
+ validateBindValue(value, paramIdx);
115
+ setHolderValue(holder, value);
116
+ } else {
117
+ setHolderValue(holder, null);
118
+ }
119
+ }
120
+
121
+ // Check for unknown named parameters
122
+ if (!ctx.allowUnknownNamedParameters) {
123
+ for (const key of Object.keys(namedArgs)) {
124
+ if (!usedKeys.has(key)) {
125
+ // Check if any holder matches this key with prefix
126
+ const matchesHolder = holders.some(h => {
127
+ const id = h.get_id();
128
+ return id === key || id.replace(/^[\$:@]/, '') === key;
129
+ });
130
+ if (!matchesHolder) {
131
+ throw new InvalidStateError(`Unknown named parameter '${key}'`);
132
+ }
133
+ }
134
+ }
135
+ }
136
+ } else {
137
+ // Positional parameter binding
138
+ if (anonymousArgs.length > holders.length) {
139
+ throw new SqliteError('column index out of range', 25, 'column index out of range');
140
+ }
141
+
142
+ for (let i = 0; i < holders.length; i++) {
143
+ const value = i < anonymousArgs.length ? anonymousArgs[i] : undefined;
144
+ validateBindValue(value, i + 1);
145
+ setHolderValue(holders[i], value ?? null);
146
+ }
147
+ }
148
+ }
@@ -0,0 +1,192 @@
1
+ // Ported from refs/node-test/parallel/test-sqlite-statement-sync.js
2
+ // Original: MIT license, Node.js contributors
3
+
4
+ import { describe, it, expect } from '@gjsify/unit';
5
+ import { join } from 'node:path';
6
+ import { tmpdir } from 'node:os';
7
+ import { mkdirSync, rmSync } from 'node:fs';
8
+ import { DatabaseSync } from 'node:sqlite';
9
+
10
+ let cnt = 0;
11
+ const testDir = join(tmpdir(), 'gjsify-sqlite-stmt-test-' + Date.now());
12
+
13
+ function setup() {
14
+ try { mkdirSync(testDir, { recursive: true }); } catch {}
15
+ }
16
+
17
+ function cleanup() {
18
+ try { rmSync(testDir, { recursive: true, force: true }); } catch {}
19
+ }
20
+
21
+ function nextDb(): string {
22
+ return join(testDir, `database-${cnt++}.db`);
23
+ }
24
+
25
+ export default async () => {
26
+ setup();
27
+
28
+ await describe('StatementSync.prototype.get()', async () => {
29
+ await it('returns undefined on no results', async () => {
30
+ const db = new DatabaseSync(nextDb());
31
+ let stmt = db.prepare('CREATE TABLE storage(key TEXT, val TEXT)');
32
+ expect(stmt.get()).toBe(undefined);
33
+ stmt = db.prepare('SELECT * FROM storage');
34
+ expect(stmt.get()).toBe(undefined);
35
+ db.close();
36
+ });
37
+
38
+ await it('returns the first result', async () => {
39
+ const db = new DatabaseSync(nextDb());
40
+ db.exec('CREATE TABLE storage(key TEXT, val TEXT)');
41
+ const insert = db.prepare('INSERT INTO storage (key, val) VALUES (?, ?)');
42
+ insert.run('key1', 'val1');
43
+ insert.run('key2', 'val2');
44
+ const stmt = db.prepare('SELECT * FROM storage ORDER BY key');
45
+ const row = stmt.get() as Record<string, unknown>;
46
+ expect(row).toBeDefined();
47
+ expect(row.key).toBe('key1');
48
+ expect(row.val).toBe('val1');
49
+ db.close();
50
+ });
51
+ });
52
+
53
+ await describe('StatementSync.prototype.all()', async () => {
54
+ await it('returns an empty array on no results', async () => {
55
+ const db = new DatabaseSync(nextDb());
56
+ const stmt = db.prepare('CREATE TABLE storage(key TEXT, val TEXT)');
57
+ const rows = stmt.all();
58
+ expect(Array.isArray(rows)).toBe(true);
59
+ expect(rows.length).toBe(0);
60
+ db.close();
61
+ });
62
+
63
+ await it('returns all results', async () => {
64
+ const db = new DatabaseSync(nextDb());
65
+ db.exec('CREATE TABLE storage(key TEXT, val TEXT)');
66
+ const insert = db.prepare('INSERT INTO storage (key, val) VALUES (?, ?)');
67
+ insert.run('key1', 'val1');
68
+ insert.run('key2', 'val2');
69
+ const stmt = db.prepare('SELECT * FROM storage ORDER BY key');
70
+ const rows = stmt.all() as Record<string, unknown>[];
71
+ expect(rows.length).toBe(2);
72
+ expect(rows[0].key).toBe('key1');
73
+ expect(rows[1].key).toBe('key2');
74
+ db.close();
75
+ });
76
+ });
77
+
78
+ await describe('StatementSync.prototype.run()', async () => {
79
+ await it('returns change metadata', async () => {
80
+ const db = new DatabaseSync(nextDb());
81
+ db.exec(`
82
+ CREATE TABLE storage(key TEXT, val TEXT);
83
+ INSERT INTO storage (key, val) VALUES ('foo', 'bar');
84
+ `);
85
+ const stmt = db.prepare('SELECT * FROM storage');
86
+ const result = stmt.run();
87
+ expect(result).toBeDefined();
88
+ expect(result.changes).toBeDefined();
89
+ expect(result.lastInsertRowid).toBeDefined();
90
+ db.close();
91
+ });
92
+
93
+ await it('returns correct changes and lastInsertRowid', async () => {
94
+ const db = new DatabaseSync(nextDb());
95
+ db.exec('CREATE TABLE storage(key TEXT, val TEXT)');
96
+ const insert = db.prepare('INSERT INTO storage (key, val) VALUES (?, ?)');
97
+ const result = insert.run('key1', 'val1');
98
+ expect(result.changes).toBe(1);
99
+ expect(result.lastInsertRowid).toBe(1);
100
+ const result2 = insert.run('key2', 'val2');
101
+ expect(result2.changes).toBe(1);
102
+ expect(result2.lastInsertRowid).toBe(2);
103
+ db.close();
104
+ });
105
+
106
+ await it('throws when binding too many parameters', async () => {
107
+ const db = new DatabaseSync(nextDb());
108
+ db.exec('CREATE TABLE data(key INTEGER PRIMARY KEY, val INTEGER) STRICT;');
109
+ const stmt = db.prepare('INSERT INTO data (key, val) VALUES (?, ?)');
110
+ expect(() => {
111
+ stmt.run(1, 2, 3);
112
+ }).toThrow();
113
+ db.close();
114
+ });
115
+ });
116
+
117
+ await describe('StatementSync.prototype.sourceSQL', async () => {
118
+ await it('equals input SQL', async () => {
119
+ const db = new DatabaseSync(nextDb());
120
+ db.exec('CREATE TABLE types(key INTEGER PRIMARY KEY, val INTEGER) STRICT;');
121
+ const sql = 'INSERT INTO types (key, val) VALUES ($k, $v)';
122
+ const stmt = db.prepare(sql);
123
+ expect(stmt.sourceSQL).toBe(sql);
124
+ db.close();
125
+ });
126
+ });
127
+
128
+ await describe('StatementSync.prototype.setReadBigInts()', async () => {
129
+ await it('BigInts support can be toggled', async () => {
130
+ const db = new DatabaseSync(nextDb());
131
+ db.exec(`
132
+ CREATE TABLE data(key INTEGER PRIMARY KEY, val INTEGER) STRICT;
133
+ INSERT INTO data (key, val) VALUES (1, 42);
134
+ `);
135
+ const query = db.prepare('SELECT val FROM data');
136
+ let row = query.get() as Record<string, unknown>;
137
+ expect(row.val).toBe(42);
138
+
139
+ query.setReadBigInts(true);
140
+ row = query.get() as Record<string, unknown>;
141
+ expect(row.val).toBe(42n);
142
+
143
+ query.setReadBigInts(false);
144
+ row = query.get() as Record<string, unknown>;
145
+ expect(row.val).toBe(42);
146
+ db.close();
147
+ });
148
+
149
+ await it('throws when input is not a boolean', async () => {
150
+ const db = new DatabaseSync(nextDb());
151
+ db.exec('CREATE TABLE types(key INTEGER PRIMARY KEY, val INTEGER) STRICT;');
152
+ const stmt = db.prepare('INSERT INTO types (key, val) VALUES ($k, $v)');
153
+ expect(() => {
154
+ (stmt as any).setReadBigInts();
155
+ }).toThrow();
156
+ db.close();
157
+ });
158
+ });
159
+
160
+ await describe('StatementSync.prototype.setReturnArrays()', async () => {
161
+ await it('returns array row when enabled', async () => {
162
+ const db = new DatabaseSync(nextDb());
163
+ db.exec(`
164
+ CREATE TABLE data(key INTEGER PRIMARY KEY, val TEXT) STRICT;
165
+ INSERT INTO data (key, val) VALUES (1, 'one');
166
+ `);
167
+ const query = db.prepare('SELECT key, val FROM data WHERE key = 1');
168
+ let row = query.get() as Record<string, unknown>;
169
+ expect(row.key).toBe(1);
170
+ expect(row.val).toBe('one');
171
+
172
+ query.setReturnArrays(true);
173
+ const arrRow = query.get() as unknown[];
174
+ expect(Array.isArray(arrRow)).toBe(true);
175
+ expect(arrRow[0]).toBe(1);
176
+ expect(arrRow[1]).toBe('one');
177
+ db.close();
178
+ });
179
+
180
+ await it('throws when input is not a boolean', async () => {
181
+ const db = new DatabaseSync(nextDb());
182
+ db.exec('CREATE TABLE data(key INTEGER PRIMARY KEY, val INTEGER) STRICT;');
183
+ const stmt = db.prepare('SELECT key, val FROM data');
184
+ expect(() => {
185
+ (stmt as any).setReturnArrays();
186
+ }).toThrow();
187
+ db.close();
188
+ });
189
+ });
190
+
191
+ cleanup();
192
+ };
@@ -0,0 +1,343 @@
1
+ // StatementSync class for node:sqlite
2
+ // Reference: Node.js lib/sqlite.js
3
+ // Reimplemented for GJS using Gda-6.0
4
+
5
+ import Gda from '@girs/gda-6.0';
6
+ import { IllegalConstructorError, InvalidArgTypeError, InvalidArgValueError, SqliteError } from './errors.ts';
7
+ import { readAllRows, readFirstRow, type ReadOptions } from './data-model-reader.ts';
8
+ import type { RunResult, StatementSyncOptions } from './types.ts';
9
+ import type { ParamInfo } from './database-sync.ts';
10
+
11
+ const MAX_INT64 = 9223372036854775807n;
12
+ const MIN_INT64 = -9223372036854775808n;
13
+
14
+ // Sentinel to prevent direct construction
15
+ const INTERNAL = Symbol('StatementSync.internal');
16
+
17
+ function validateBindValue(value: unknown, paramIndex: number): void {
18
+ if (value === null) return;
19
+ const t = typeof value;
20
+ if (t === 'number' || t === 'bigint' || t === 'string' || t === 'boolean') return;
21
+ if (value instanceof Uint8Array || value instanceof ArrayBuffer) return;
22
+ if (ArrayBuffer.isView(value)) return;
23
+ throw new InvalidArgTypeError(
24
+ `Provided value cannot be bound to SQLite parameter ${paramIndex}.`
25
+ );
26
+ }
27
+
28
+ // Escape a JS value for inline SQL. This is safe because only values are
29
+ // interpolated — the SQL structure comes from the user's original prepare() call.
30
+ function sqlEscapeValue(value: unknown): string {
31
+ if (value === null) return 'NULL';
32
+ if (value === undefined) return 'NULL';
33
+ if (typeof value === 'number') return String(value);
34
+ if (typeof value === 'boolean') return value ? '1' : '0';
35
+ if (typeof value === 'bigint') {
36
+ if (value > MAX_INT64 || value < MIN_INT64) {
37
+ throw new InvalidArgValueError('BigInt value is too large to bind.');
38
+ }
39
+ return String(value);
40
+ }
41
+ if (typeof value === 'string') return "'" + value.replace(/'/g, "''") + "'";
42
+ if (value instanceof Uint8Array) {
43
+ // SQLite BLOB literal: X'hex'
44
+ let hex = '';
45
+ for (let i = 0; i < value.length; i++) {
46
+ hex += value[i].toString(16).padStart(2, '0');
47
+ }
48
+ return "X'" + hex + "'";
49
+ }
50
+ if (ArrayBuffer.isView(value)) {
51
+ return sqlEscapeValue(new Uint8Array((value as ArrayBufferView).buffer));
52
+ }
53
+ return 'NULL';
54
+ }
55
+
56
+ export class StatementSync {
57
+ #connection: Gda.Connection;
58
+ #sql: string;
59
+ #paramMap: ParamInfo[];
60
+ #readBigInts: boolean;
61
+ #returnArrays: boolean;
62
+ #allowBareNamedParameters: boolean;
63
+ #allowUnknownNamedParameters: boolean;
64
+ #parser: Gda.SqlParser;
65
+
66
+ constructor(sentinel: symbol, connection: Gda.Connection, sql: string, options: StatementSyncOptions, paramMap: ParamInfo[], parser: Gda.SqlParser) {
67
+ if (sentinel !== INTERNAL) {
68
+ throw new IllegalConstructorError();
69
+ }
70
+ this.#connection = connection;
71
+ this.#sql = sql;
72
+ this.#paramMap = paramMap;
73
+ this.#readBigInts = options.readBigInts ?? false;
74
+ this.#returnArrays = options.returnArrays ?? false;
75
+ this.#allowBareNamedParameters = options.allowBareNamedParameters ?? true;
76
+ this.#allowUnknownNamedParameters = options.allowUnknownNamedParameters ?? false;
77
+ this.#parser = parser;
78
+ }
79
+
80
+ /** @internal */
81
+ static _create(connection: Gda.Connection, sql: string, options: StatementSyncOptions, paramMap: ParamInfo[], parser: Gda.SqlParser): StatementSync {
82
+ return new StatementSync(INTERNAL, connection, sql, options, paramMap, parser);
83
+ }
84
+
85
+ get sourceSQL(): string {
86
+ return this.#sql;
87
+ }
88
+
89
+ get expandedSQL(): string {
90
+ return this.#sql;
91
+ }
92
+
93
+ #getReadOptions(): ReadOptions {
94
+ return {
95
+ readBigInts: this.#readBigInts,
96
+ returnArrays: this.#returnArrays,
97
+ };
98
+ }
99
+
100
+ // Build the final SQL with parameter values substituted inline.
101
+ // Returns the SQL string ready for execution.
102
+ #buildSql(args: unknown[]): string {
103
+ if (this.#paramMap.length === 0) {
104
+ if (args.length > 0) {
105
+ const hasNamedArg = args.length > 0 && args[0] !== null && typeof args[0] === 'object' && !(args[0] instanceof Uint8Array) && !ArrayBuffer.isView(args[0]);
106
+ if (!hasNamedArg) {
107
+ throw new SqliteError('column index out of range', 25, 'column index out of range');
108
+ }
109
+ }
110
+ return this.#sql;
111
+ }
112
+
113
+ // Determine if first arg is a named params object
114
+ let namedArgs: Record<string, unknown> | null = null;
115
+ let positionalArgs: unknown[] = args;
116
+
117
+ if (args.length > 0 && args[0] !== null && typeof args[0] === 'object' && !(args[0] instanceof Uint8Array) && !ArrayBuffer.isView(args[0])) {
118
+ namedArgs = args[0] as Record<string, unknown>;
119
+ positionalArgs = args.slice(1);
120
+ }
121
+
122
+ // Build maps: named params use Map<originalName, escaped>, positional use index-keyed object
123
+ const values = new Map<string, string>();
124
+ const positionalValues: Record<number, string> = {};
125
+
126
+ if (namedArgs) {
127
+ for (const param of this.#paramMap) {
128
+ if (param.position >= 0) continue; // Handle positional separately
129
+ const origName = param.originalName;
130
+ let value: unknown = undefined;
131
+ let found = false;
132
+
133
+ // Exact match with prefix ($name, :name, @name)
134
+ if (origName in namedArgs) {
135
+ value = namedArgs[origName];
136
+ found = true;
137
+ }
138
+
139
+ // Bare name
140
+ if (!found && this.#allowBareNamedParameters) {
141
+ const bareName = origName.replace(/^[\$:@]/, '');
142
+ if (bareName in namedArgs) {
143
+ value = namedArgs[bareName];
144
+ found = true;
145
+ }
146
+ }
147
+
148
+ if (!found && !this.#allowBareNamedParameters) {
149
+ const bareName = origName.replace(/^[\$:@]/, '');
150
+ if (bareName in namedArgs) {
151
+ throw new SqliteError(`Unknown named parameter '${bareName}'`, 0, `Unknown named parameter '${bareName}'`);
152
+ }
153
+ }
154
+
155
+ if (found) {
156
+ validateBindValue(value, this.#paramMap.indexOf(param) + 1);
157
+ values.set(param.originalName, sqlEscapeValue(value));
158
+ } else {
159
+ values.set(param.originalName, 'NULL');
160
+ }
161
+ }
162
+
163
+ // Handle positional after named
164
+ const positionalParams = this.#paramMap.filter(p => p.position >= 0);
165
+ for (let i = 0; i < positionalArgs.length; i++) {
166
+ if (i >= positionalParams.length) {
167
+ throw new SqliteError('column index out of range', 25, 'column index out of range');
168
+ }
169
+ validateBindValue(positionalArgs[i], i + 1);
170
+ positionalValues[positionalParams[i].position] = sqlEscapeValue(positionalArgs[i]);
171
+ }
172
+ } else {
173
+ // Pure positional binding
174
+ const positionalParams = this.#paramMap.filter(p => p.position >= 0);
175
+ if (positionalArgs.length > positionalParams.length && positionalParams.length > 0) {
176
+ throw new SqliteError('column index out of range', 25, 'column index out of range');
177
+ }
178
+
179
+ for (let i = 0; i < positionalParams.length; i++) {
180
+ if (i < positionalArgs.length) {
181
+ validateBindValue(positionalArgs[i], i + 1);
182
+ positionalValues[positionalParams[i].position] = sqlEscapeValue(positionalArgs[i]);
183
+ } else {
184
+ // Not provided — bind as NULL (not an error)
185
+ positionalValues[positionalParams[i].position] = 'NULL';
186
+ }
187
+ }
188
+ }
189
+
190
+ // Substitute parameters in SQL
191
+ let result = this.#sql;
192
+
193
+ // Replace named params ($name, :name, @name) — longest first to avoid partial matches
194
+ const namedParams = this.#paramMap.filter(p => p.position < 0).sort((a, b) => b.originalName.length - a.originalName.length);
195
+ for (const param of namedParams) {
196
+ const escaped = values.get(param.originalName) ?? 'NULL';
197
+ result = result.split(param.originalName).join(escaped);
198
+ }
199
+
200
+ // Replace positional params (? or ?NNN) — scan left-to-right
201
+ if (Object.keys(positionalValues).length > 0) {
202
+ let out = '';
203
+ let pIdx = 0;
204
+ let i = 0;
205
+ while (i < result.length) {
206
+ if (result[i] === "'") {
207
+ const start = i;
208
+ i++;
209
+ while (i < result.length && result[i] !== "'") {
210
+ if (result[i] === "'" && result[i + 1] === "'") { i += 2; continue; }
211
+ i++;
212
+ }
213
+ if (i < result.length) i++;
214
+ out += result.substring(start, i);
215
+ continue;
216
+ }
217
+ if (result[i] === '?') {
218
+ i++;
219
+ let numStr = '';
220
+ while (i < result.length && result[i] >= '0' && result[i] <= '9') {
221
+ numStr += result[i];
222
+ i++;
223
+ }
224
+ const pos = numStr ? parseInt(numStr, 10) - 1 : pIdx;
225
+ pIdx = numStr ? pIdx : pIdx + 1;
226
+ out += (pos in positionalValues) ? positionalValues[pos] : 'NULL';
227
+ continue;
228
+ }
229
+ out += result[i];
230
+ i++;
231
+ }
232
+ result = out;
233
+ }
234
+
235
+ return result;
236
+ }
237
+
238
+ #executeSql(sql: string): { model: Gda.DataModel | null; isSelect: boolean } {
239
+ const [stmt] = this.#parser.parse_string(sql);
240
+ if (!stmt) {
241
+ throw new SqliteError('Failed to parse SQL statement');
242
+ }
243
+ const stmtType = stmt.get_statement_type();
244
+ if (stmtType === Gda.SqlStatementType.SELECT) {
245
+ return { model: this.#connection.statement_execute_select(stmt, null), isSelect: true };
246
+ }
247
+ try {
248
+ this.#connection.statement_execute_non_select(stmt, null);
249
+ return { model: null, isSelect: false };
250
+ } catch {
251
+ // Might be PRAGMA or similar — try as select
252
+ const model = this.#connection.statement_execute_select(stmt, null);
253
+ return { model, isSelect: true };
254
+ }
255
+ }
256
+
257
+ run(...args: unknown[]): RunResult {
258
+ const sql = this.#buildSql(args);
259
+ this.#executeSql(sql);
260
+
261
+ let changes: number | bigint = 0;
262
+ let lastInsertRowid: number | bigint = 0;
263
+
264
+ try {
265
+ const chModel = this.#connection.execute_select_command('SELECT changes()');
266
+ if (chModel && chModel.get_n_rows() > 0) {
267
+ changes = chModel.get_value_at(0, 0) as unknown as number;
268
+ }
269
+ } catch { /* ignore */ }
270
+
271
+ try {
272
+ const ridModel = this.#connection.execute_select_command('SELECT last_insert_rowid()');
273
+ if (ridModel && ridModel.get_n_rows() > 0) {
274
+ lastInsertRowid = ridModel.get_value_at(0, 0) as unknown as number;
275
+ }
276
+ } catch { /* ignore */ }
277
+
278
+ if (this.#readBigInts) {
279
+ changes = BigInt(changes);
280
+ lastInsertRowid = BigInt(lastInsertRowid);
281
+ }
282
+
283
+ return { changes, lastInsertRowid };
284
+ }
285
+
286
+ get(...args: unknown[]): Record<string, unknown> | unknown[] | undefined {
287
+ const sql = this.#buildSql(args);
288
+ try {
289
+ const { model } = this.#executeSql(sql);
290
+ if (!model || model.get_n_rows() === 0) {
291
+ return undefined;
292
+ }
293
+ return readFirstRow(model, this.#getReadOptions());
294
+ } catch {
295
+ return undefined;
296
+ }
297
+ }
298
+
299
+ all(...args: unknown[]): (Record<string, unknown> | unknown[])[] {
300
+ const sql = this.#buildSql(args);
301
+ try {
302
+ const { model } = this.#executeSql(sql);
303
+ if (!model) {
304
+ return [];
305
+ }
306
+ return readAllRows(model, this.#getReadOptions());
307
+ } catch {
308
+ return [];
309
+ }
310
+ }
311
+
312
+ setReadBigInts(enabled: unknown): undefined {
313
+ if (typeof enabled !== 'boolean') {
314
+ throw new InvalidArgTypeError('The "readBigInts" argument must be a boolean.');
315
+ }
316
+ this.#readBigInts = enabled;
317
+ return undefined;
318
+ }
319
+
320
+ setReturnArrays(enabled: unknown): undefined {
321
+ if (typeof enabled !== 'boolean') {
322
+ throw new InvalidArgTypeError('The "returnArrays" argument must be a boolean.');
323
+ }
324
+ this.#returnArrays = enabled;
325
+ return undefined;
326
+ }
327
+
328
+ setAllowBareNamedParameters(enabled: unknown): undefined {
329
+ if (typeof enabled !== 'boolean') {
330
+ throw new InvalidArgTypeError('The "allowBareNamedParameters" argument must be a boolean.');
331
+ }
332
+ this.#allowBareNamedParameters = enabled;
333
+ return undefined;
334
+ }
335
+
336
+ setAllowUnknownNamedParameters(enabled: unknown): undefined {
337
+ if (typeof enabled !== 'boolean') {
338
+ throw new InvalidArgTypeError('The "allowUnknownNamedParameters" argument must be a boolean.');
339
+ }
340
+ this.#allowUnknownNamedParameters = enabled;
341
+ return undefined;
342
+ }
343
+ }
package/src/test.mts ADDED
@@ -0,0 +1,8 @@
1
+
2
+ import { run } from '@gjsify/unit';
3
+
4
+ import testSuiteDatabaseSync from './database-sync.spec.js';
5
+ import testSuiteStatementSync from './statement-sync.spec.js';
6
+ import testSuiteDataTypes from './data-types.spec.js';
7
+
8
+ run({ testSuiteDatabaseSync, testSuiteStatementSync, testSuiteDataTypes });