@pineliner/odb-client 1.0.8 → 1.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pineliner/odb-client",
3
- "version": "1.0.8",
3
+ "version": "1.1.0",
4
4
  "description": "Isomorphic client for ODB-Lite with postgres.js-like template string SQL support",
5
5
  "main": "./dist/index.cjs",
6
6
  "module": "./dist/index.js",
@@ -1,9 +1,16 @@
1
1
  import type { ODBLiteConfig, ODBLiteConnection, QueryResult, Transaction, TransactionCallback, TransactionMode, SQLFragment } from '../types.ts';
2
2
  import { HTTPClient } from './http-client.ts';
3
3
  import { SQLParser } from './sql-parser.ts';
4
- import { createSimpleTransaction, SimpleTransaction } from './transaction.ts';
4
+ import { createBatchTransaction, createSimpleTransaction, SimpleTransaction } from './transaction.ts';
5
5
  import { ConnectionError } from '../types.ts';
6
6
 
7
+ // Batch result type
8
+ interface BatchResult<T = any> {
9
+ results: QueryResult<T>[];
10
+ executionTime: number;
11
+ statementsExecuted: number;
12
+ }
13
+
7
14
  // Define the callable SQL function type
8
15
  interface SQLFunction {
9
16
  // Primary call signatures - context-aware like postgres.js
@@ -20,6 +27,9 @@ interface SQLFunction {
20
27
  // libsql-compatible execute method (for backward compatibility)
21
28
  execute<T = any>(sql: string | { sql: string; args?: any[] }, args?: any[]): Promise<QueryResult<T>>;
22
29
 
30
+ // Batch execution - runs multiple statements on same connection (for transactions)
31
+ batch<T = any>(statements: Array<{ sql: string; params?: any[] }>): Promise<BatchResult<T>>;
32
+
23
33
  // Transaction support - multiple overloads like postgres.js
24
34
  begin(): Promise<Transaction>;
25
35
  begin<T>(callback: TransactionCallback<T>): Promise<T>;
@@ -36,7 +46,7 @@ interface SQLFunction {
36
46
  * Main ODBLite client that provides postgres.js-like interface
37
47
  */
38
48
  export class ODBLiteClient implements ODBLiteConnection {
39
- private httpClient: HTTPClient;
49
+ public httpClient: HTTPClient;
40
50
  public config: ODBLiteConfig;
41
51
  public sql: SQLFunction;
42
52
 
@@ -78,21 +88,27 @@ export class ODBLiteClient implements ODBLiteConnection {
78
88
  }
79
89
  };
80
90
 
91
+ // Batch execution - runs multiple statements on same connection (for transactions)
92
+ sqlFunction.batch = async <T = any>(statements: Array<{ sql: string; params?: any[] }>): Promise<BatchResult<T>> => {
93
+ return await this.httpClient.batch<T>(statements);
94
+ };
95
+
81
96
  // Enhanced begin method with callback support
97
+ // Uses batch-based transactions for true ACID guarantees
82
98
  sqlFunction.begin = async <T = any>(
83
99
  modeOrCallback?: TransactionMode | TransactionCallback<T>,
84
100
  callback?: TransactionCallback<T>
85
101
  ): Promise<Transaction | T> => {
86
102
  // Determine if this is callback-style or traditional
87
103
  if (typeof modeOrCallback === 'function') {
88
- // begin(callback)
104
+ // begin(callback) - execute with batch transaction
89
105
  return this.executeTransactionWithCallback(modeOrCallback);
90
106
  } else if (typeof modeOrCallback === 'string' && callback) {
91
- // begin(mode, callback)
107
+ // begin(mode, callback) - execute with batch transaction
92
108
  return this.executeTransactionWithCallback(callback, modeOrCallback);
93
109
  } else {
94
- // begin() - traditional style
95
- return createSimpleTransaction(this.httpClient);
110
+ // begin() - traditional style, returns batch transaction
111
+ return createBatchTransaction(this.httpClient);
96
112
  }
97
113
  };
98
114
  sqlFunction.ping = async (): Promise<boolean> => {
@@ -119,23 +135,24 @@ export class ODBLiteClient implements ODBLiteConnection {
119
135
 
120
136
  /**
121
137
  * Execute a transaction with callback (postgres.js style)
138
+ * Uses batch-based transaction for true ACID guarantees
122
139
  */
123
140
  private async executeTransactionWithCallback<T>(
124
141
  callback: TransactionCallback<T>,
125
142
  mode?: TransactionMode
126
143
  ): Promise<T> {
127
- const tx = createSimpleTransaction(this.httpClient);
144
+ const tx = createBatchTransaction(this.httpClient);
128
145
 
129
146
  try {
130
147
  // Execute the callback with the transaction
131
148
  const result = await callback(tx);
132
149
 
133
- // Commit the transaction
150
+ // Commit the transaction - sends all queued statements as batch
134
151
  await tx.commit();
135
152
 
136
153
  return result as T;
137
154
  } catch (error) {
138
- // Rollback on any error
155
+ // Rollback on any error - for batch transactions, this just clears the queue
139
156
  await tx.rollback();
140
157
  throw error;
141
158
  }
@@ -152,10 +169,10 @@ export class ODBLiteClient implements ODBLiteConnection {
152
169
 
153
170
  /**
154
171
  * Begin a transaction
155
- * Note: Uses simple transaction model suitable for HTTP-based access
172
+ * Uses batch-based transaction for true ACID guarantees
156
173
  */
157
174
  async begin(): Promise<Transaction> {
158
- return createSimpleTransaction(this.httpClient);
175
+ return createBatchTransaction(this.httpClient);
159
176
  }
160
177
 
161
178
  /**
@@ -76,6 +76,58 @@ export class HTTPClient {
76
76
  }
77
77
  }
78
78
 
79
+ /**
80
+ * Execute multiple statements on the same connection (for transactions)
81
+ * All statements are sent in a single HTTP request and executed atomically on the server
82
+ */
83
+ async batch<T = any>(statements: Array<{ sql: string; params?: any[] }>): Promise<{
84
+ results: QueryResult<T>[];
85
+ executionTime: number;
86
+ statementsExecuted: number;
87
+ }> {
88
+ if (!this.config.databaseId) {
89
+ throw new ConnectionError('No database ID configured. Use setDatabase() first.')
90
+ }
91
+
92
+ const url = `${this.config.baseUrl}/batch/${this.config.databaseId}`
93
+ const body = { statements }
94
+
95
+ try {
96
+ const response = await this.makeRequest(url, {
97
+ method: 'POST',
98
+ headers: {
99
+ 'Content-Type': 'application/json',
100
+ Authorization: `Bearer ${this.config.apiKey}`
101
+ },
102
+ body: JSON.stringify(body)
103
+ })
104
+
105
+ const data = await response.json()
106
+
107
+ if (!data.success) {
108
+ throw new QueryError(data.error || 'Batch failed')
109
+ }
110
+
111
+ const batchData = data.data || data
112
+ return {
113
+ results: batchData.results || [],
114
+ executionTime: batchData.executionTime || 0,
115
+ statementsExecuted: batchData.statementsExecuted || statements.length
116
+ }
117
+ } catch (error) {
118
+ if (error instanceof QueryError || error instanceof ConnectionError) {
119
+ throw error
120
+ }
121
+
122
+ throw new QueryError(
123
+ error instanceof Error ? error.message : 'Unknown error occurred',
124
+ undefined,
125
+ undefined,
126
+ error instanceof Error ? error : undefined
127
+ )
128
+ }
129
+ }
130
+
79
131
  /**
80
132
  * Check database health
81
133
  */
@@ -198,4 +250,93 @@ export class HTTPClient {
198
250
  getConfig(): Readonly<ODBLiteConfig> {
199
251
  return { ...this.config }
200
252
  }
253
+
254
+ /**
255
+ * Ensure namespaces exist for the database
256
+ * Creates the namespace .db files if they don't exist
257
+ */
258
+ async ensureNamespaces(namespaces: string[]): Promise<{
259
+ success: boolean;
260
+ databaseName: string;
261
+ namespaces: string[];
262
+ }> {
263
+ if (!this.config.databaseId) {
264
+ throw new ConnectionError('No database ID configured. Use setDatabase() first.')
265
+ }
266
+
267
+ const url = `${this.config.baseUrl}/namespaces/${this.config.databaseId}`
268
+ const body = { namespaces }
269
+
270
+ try {
271
+ const response = await this.makeRequest(url, {
272
+ method: 'POST',
273
+ headers: {
274
+ 'Content-Type': 'application/json',
275
+ Authorization: `Bearer ${this.config.apiKey}`
276
+ },
277
+ body: JSON.stringify(body)
278
+ })
279
+
280
+ const data = await response.json()
281
+
282
+ if (!data.success) {
283
+ throw new ConnectionError(data.error || 'Failed to ensure namespaces')
284
+ }
285
+
286
+ return data
287
+ } catch (error) {
288
+ if (error instanceof ConnectionError) {
289
+ throw error
290
+ }
291
+
292
+ throw new ConnectionError(
293
+ error instanceof Error ? error.message : 'Unknown error occurred',
294
+ error instanceof Error ? error : undefined
295
+ )
296
+ }
297
+ }
298
+
299
+ /**
300
+ * Get namespace information for the database
301
+ */
302
+ async getNamespaces(): Promise<{
303
+ success: boolean;
304
+ databaseName: string;
305
+ exists: boolean;
306
+ namespaces: string[];
307
+ registered: boolean;
308
+ registeredNamespaces: string[];
309
+ }> {
310
+ if (!this.config.databaseId) {
311
+ throw new ConnectionError('No database ID configured. Use setDatabase() first.')
312
+ }
313
+
314
+ const url = `${this.config.baseUrl}/namespaces/${this.config.databaseId}`
315
+
316
+ try {
317
+ const response = await this.makeRequest(url, {
318
+ method: 'GET',
319
+ headers: {
320
+ Authorization: `Bearer ${this.config.apiKey}`
321
+ }
322
+ })
323
+
324
+ const data = await response.json()
325
+
326
+ if (!data.success) {
327
+ throw new ConnectionError(data.error || 'Failed to get namespaces')
328
+ }
329
+
330
+ return data
331
+ } catch (error) {
332
+ if (error instanceof ConnectionError) {
333
+ throw error
334
+ }
335
+
336
+ throw new ConnectionError(
337
+ error instanceof Error ? error.message : 'Unknown error occurred',
338
+ error instanceof Error ? error : undefined
339
+ )
340
+ }
341
+ }
201
342
  }
@@ -3,6 +3,209 @@ import type { HTTPClient } from './http-client.ts';
3
3
  import { SQLParser } from './sql-parser.ts';
4
4
  import { QueryError } from '../types.ts';
5
5
 
6
+ /**
7
+ * Create a batch-based transaction that executes all statements atomically
8
+ * using the /batch endpoint. This ensures true ACID transactions by:
9
+ * 1. Collecting all write statements during callback execution
10
+ * 2. Sending BEGIN + statements + COMMIT as a single batch request
11
+ * 3. If callback throws, nothing is executed (implicit rollback)
12
+ */
13
+ export function createBatchTransaction(httpClient: HTTPClient): Transaction {
14
+ let isCommitted = false;
15
+ let isRolledBack = false;
16
+ const queuedStatements: Array<{ sql: string; params: any[] }> = [];
17
+
18
+ const checkTransactionState = (): void => {
19
+ if (isCommitted) {
20
+ throw new QueryError('Transaction has already been committed');
21
+ }
22
+ if (isRolledBack) {
23
+ throw new QueryError('Transaction has been rolled back');
24
+ }
25
+ };
26
+
27
+ const isReadQuery = (sql: string): boolean => {
28
+ const trimmed = sql.trim().toUpperCase();
29
+ return trimmed.startsWith('SELECT') ||
30
+ trimmed.startsWith('WITH') ||
31
+ trimmed.startsWith('EXPLAIN') ||
32
+ trimmed.startsWith('PRAGMA');
33
+ };
34
+
35
+ // Create the callable transaction function
36
+ const txFunction = <T = any>(sql: TemplateStringsArray | any, ...values: any[]): Promise<QueryResult<T>> | SQLFragment => {
37
+ checkTransactionState();
38
+
39
+ // Handle template string queries
40
+ if (Array.isArray(sql) && (('raw' in sql) || (typeof sql[0] === 'string' && values.length >= 0))) {
41
+ const parsed = SQLParser.parse(sql, values);
42
+
43
+ // For read queries, execute immediately (outside transaction for now)
44
+ // TODO: Consider queueing reads too for strict isolation
45
+ if (isReadQuery(parsed.sql)) {
46
+ return httpClient.query<T>(parsed.sql, parsed.params);
47
+ }
48
+
49
+ // For write queries, queue them for batch execution
50
+ queuedStatements.push(parsed);
51
+
52
+ // Return a placeholder result
53
+ return Promise.resolve({
54
+ rows: [],
55
+ rowsAffected: 0,
56
+ executionTime: 0
57
+ });
58
+ }
59
+
60
+ // Handle direct object/array inputs
61
+ const parsed = SQLParser.parse(sql, values);
62
+ return {
63
+ text: parsed.sql,
64
+ values: parsed.params
65
+ };
66
+ };
67
+
68
+ // Attach utility methods
69
+ txFunction.raw = (text: string): string => SQLParser.raw(text);
70
+ txFunction.identifier = (name: string): string => SQLParser.identifier(name);
71
+
72
+ // Add execute method for compatibility
73
+ txFunction.execute = async <T = any>(sql: string | { sql: string; args?: any[] }, args?: any[]): Promise<QueryResult<T>> => {
74
+ checkTransactionState();
75
+
76
+ const sqlStr = typeof sql === 'string' ? sql : sql.sql;
77
+ const params = typeof sql === 'string' ? (args || []) : (sql.args || []);
78
+
79
+ // For read queries, execute immediately
80
+ if (isReadQuery(sqlStr)) {
81
+ return await httpClient.query<T>(sqlStr, params);
82
+ }
83
+
84
+ // For write queries, queue them
85
+ queuedStatements.push({ sql: sqlStr, params });
86
+
87
+ return {
88
+ rows: [],
89
+ rowsAffected: 0,
90
+ executionTime: 0
91
+ };
92
+ };
93
+
94
+ // Add query method for compatibility
95
+ txFunction.query = async <T = any>(sql: string, params: any[] = []): Promise<QueryResult<T>> => {
96
+ checkTransactionState();
97
+
98
+ if (isReadQuery(sql)) {
99
+ return await httpClient.query<T>(sql, params);
100
+ }
101
+
102
+ queuedStatements.push({ sql, params });
103
+ return {
104
+ rows: [],
105
+ rowsAffected: 0,
106
+ executionTime: 0
107
+ };
108
+ };
109
+
110
+ txFunction.commit = async (): Promise<void> => {
111
+ checkTransactionState();
112
+
113
+ if (queuedStatements.length === 0) {
114
+ isCommitted = true;
115
+ return;
116
+ }
117
+
118
+ try {
119
+ // Build batch: BEGIN + statements + COMMIT
120
+ const batchStatements = [
121
+ { sql: 'BEGIN', params: [] },
122
+ ...queuedStatements,
123
+ { sql: 'COMMIT', params: [] }
124
+ ];
125
+
126
+ // Execute all statements atomically via batch endpoint
127
+ await httpClient.batch(batchStatements);
128
+
129
+ isCommitted = true;
130
+ } catch (error) {
131
+ isRolledBack = true;
132
+ throw new QueryError(
133
+ `Transaction failed: ${error instanceof Error ? error.message : 'Unknown error'}`,
134
+ undefined,
135
+ undefined,
136
+ error instanceof Error ? error : undefined
137
+ );
138
+ }
139
+ };
140
+
141
+ txFunction.rollback = async (): Promise<void> => {
142
+ checkTransactionState();
143
+ // For batch transactions, rollback is implicit - we just don't commit
144
+ // Clear the queue and mark as rolled back
145
+ queuedStatements.length = 0;
146
+ isRolledBack = true;
147
+ };
148
+
149
+ txFunction.savepoint = async <T = any>(callback: (sql: Transaction) => Promise<T>): Promise<T> => {
150
+ checkTransactionState();
151
+
152
+ // Create a nested transaction that collects its own statements
153
+ const savepointStatements: Array<{ sql: string; params: any[] }> = [];
154
+
155
+ const savepointTx = <U = any>(sql: TemplateStringsArray | any, ...values: any[]): Promise<QueryResult<U>> | SQLFragment => {
156
+ if (isCommitted || isRolledBack) {
157
+ throw new QueryError('Transaction is no longer active');
158
+ }
159
+
160
+ if (Array.isArray(sql) && (('raw' in sql) || (typeof sql[0] === 'string' && values.length >= 0))) {
161
+ const parsed = SQLParser.parse(sql, values);
162
+
163
+ if (isReadQuery(parsed.sql)) {
164
+ return httpClient.query<U>(parsed.sql, parsed.params);
165
+ }
166
+
167
+ savepointStatements.push(parsed);
168
+ return Promise.resolve({
169
+ rows: [],
170
+ rowsAffected: 0,
171
+ executionTime: 0
172
+ });
173
+ }
174
+
175
+ const parsed = SQLParser.parse(sql, values);
176
+ return { text: parsed.sql, values: parsed.params };
177
+ };
178
+
179
+ (savepointTx as any).raw = (text: string): string => SQLParser.raw(text);
180
+ (savepointTx as any).identifier = (name: string): string => SQLParser.identifier(name);
181
+ (savepointTx as any).execute = async <U = any>(sql: string | { sql: string; args?: any[] }, args?: any[]): Promise<QueryResult<U>> => {
182
+ const sqlStr = typeof sql === 'string' ? sql : sql.sql;
183
+ const params = typeof sql === 'string' ? (args || []) : (sql.args || []);
184
+ if (isReadQuery(sqlStr)) {
185
+ return await httpClient.query<U>(sqlStr, params);
186
+ }
187
+ savepointStatements.push({ sql: sqlStr, params });
188
+ return { rows: [], rowsAffected: 0, executionTime: 0 };
189
+ };
190
+ (savepointTx as any).commit = async (): Promise<void> => {};
191
+ (savepointTx as any).rollback = async (): Promise<void> => {
192
+ savepointStatements.length = 0;
193
+ };
194
+
195
+ try {
196
+ const result = await callback(savepointTx as Transaction);
197
+ // On success, add savepoint statements to main transaction
198
+ queuedStatements.push(...savepointStatements);
199
+ return result;
200
+ } catch (error) {
201
+ // On error, savepoint statements are discarded (not added to main)
202
+ throw error;
203
+ }
204
+ };
205
+
206
+ return txFunction as Transaction;
207
+ }
208
+
6
209
  /**
7
210
  * Create a transaction function similar to the main sql function
8
211
  */
@@ -5,6 +5,7 @@ import type {
5
5
  QueryResult,
6
6
  PreparedStatement,
7
7
  BunSQLiteConfig,
8
+ QueryOptions,
8
9
  } from '../types'
9
10
  import {
10
11
  convertTemplateToQuery,
@@ -17,6 +18,48 @@ import {
17
18
  where
18
19
  } from '../sql-template'
19
20
 
21
+ /**
22
+ * Parse JSON columns in query results
23
+ * Only parses if the value is a string (to avoid double-parsing)
24
+ */
25
+ function parseJsonColumns<T = any>(rows: any[], jsonColumns?: string[]): T[] {
26
+ if (!jsonColumns || jsonColumns.length === 0) {
27
+ return rows
28
+ }
29
+
30
+ return rows.map(row => {
31
+ const parsed = { ...row }
32
+ for (const col of jsonColumns) {
33
+ if (col in parsed && typeof parsed[col] === 'string') {
34
+ try {
35
+ parsed[col] = JSON.parse(parsed[col])
36
+ } catch {
37
+ // Keep original value if parsing fails
38
+ }
39
+ }
40
+ }
41
+ return parsed
42
+ })
43
+ }
44
+
45
+ /**
46
+ * Stringify JSON parameters for INSERT/UPDATE queries
47
+ * Only stringifies if the value is an object/array (not already a string)
48
+ */
49
+ function stringifyJsonParams(params: any[], stringifyParams?: Record<string, number>): any[] {
50
+ if (!stringifyParams || Object.keys(stringifyParams).length === 0) {
51
+ return params
52
+ }
53
+
54
+ const result = [...params]
55
+ for (const [_columnName, index] of Object.entries(stringifyParams)) {
56
+ if (index < result.length && result[index] != null && typeof result[index] === 'object') {
57
+ result[index] = JSON.stringify(result[index])
58
+ }
59
+ }
60
+ return result
61
+ }
62
+
20
63
  /**
21
64
  * Bun SQLite adapter for DatabaseManager
22
65
  * Wraps bun:sqlite with Connection interface
@@ -117,14 +160,14 @@ class BunSQLiteConnection implements Connection {
117
160
  /**
118
161
  * Query with SQL string and parameters (alias for execute)
119
162
  */
120
- async query<T = any>(sql: string, params: any[] = []): Promise<QueryResult<T>> {
121
- return this.execute(sql, params) as Promise<QueryResult<T>>
163
+ async query<T = any>(sql: string, params: any[] = [], options?: QueryOptions): Promise<QueryResult<T>> {
164
+ return this.execute(sql, params, options) as Promise<QueryResult<T>>
122
165
  }
123
166
 
124
167
  /**
125
168
  * Execute SQL with parameters
126
169
  */
127
- async execute(sql: string | { sql: string; args?: any[] }, params: any[] = []): Promise<QueryResult> {
170
+ async execute(sql: string | { sql: string; args?: any[] }, params: any[] = [], options?: QueryOptions): Promise<QueryResult> {
128
171
  try {
129
172
  // Handle object format { sql, args }
130
173
  let sqlStr: string
@@ -147,14 +190,25 @@ class BunSQLiteConnection implements Connection {
147
190
  sqlParams = params
148
191
  }
149
192
 
193
+ // Stringify JSON parameters if specified
194
+ if (options?.stringifyParams) {
195
+ sqlParams = stringifyJsonParams(sqlParams, options.stringifyParams)
196
+ }
197
+
150
198
  const stmt = this.db.query(sqlStr)
151
199
  const isSelect = sqlStr.trim().toUpperCase().startsWith('SELECT') ||
152
200
  sqlStr.trim().toUpperCase().includes('RETURNING')
153
201
 
154
202
  if (isSelect) {
155
- const rows = stmt.all(...sqlParams)
203
+ let rows = stmt.all(...sqlParams) as any[]
204
+
205
+ // Parse JSON columns if specified
206
+ if (options?.jsonColumns) {
207
+ rows = parseJsonColumns(rows, options.jsonColumns)
208
+ }
209
+
156
210
  return {
157
- rows: rows as any[],
211
+ rows,
158
212
  rowsAffected: 0,
159
213
  }
160
214
  } else {
@@ -5,6 +5,7 @@ import type {
5
5
  QueryResult,
6
6
  PreparedStatement,
7
7
  LibSQLConfig,
8
+ QueryOptions,
8
9
  } from '../types'
9
10
  import {
10
11
  convertTemplateToQuery,
@@ -17,6 +18,48 @@ import {
17
18
  where
18
19
  } from '../sql-template'
19
20
 
21
+ /**
22
+ * Parse JSON columns in query results
23
+ * Only parses if the value is a string (to avoid double-parsing)
24
+ */
25
+ function parseJsonColumns<T = any>(rows: any[], jsonColumns?: string[]): T[] {
26
+ if (!jsonColumns || jsonColumns.length === 0) {
27
+ return rows
28
+ }
29
+
30
+ return rows.map(row => {
31
+ const parsed = { ...row }
32
+ for (const col of jsonColumns) {
33
+ if (col in parsed && typeof parsed[col] === 'string') {
34
+ try {
35
+ parsed[col] = JSON.parse(parsed[col])
36
+ } catch {
37
+ // Keep original value if parsing fails
38
+ }
39
+ }
40
+ }
41
+ return parsed
42
+ })
43
+ }
44
+
45
+ /**
46
+ * Stringify JSON parameters for INSERT/UPDATE queries
47
+ * Only stringifies if the value is an object/array (not already a string)
48
+ */
49
+ function stringifyJsonParams(params: any[], stringifyParams?: Record<string, number>): any[] {
50
+ if (!stringifyParams || Object.keys(stringifyParams).length === 0) {
51
+ return params
52
+ }
53
+
54
+ const result = [...params]
55
+ for (const [_columnName, index] of Object.entries(stringifyParams)) {
56
+ if (index < result.length && result[index] != null && typeof result[index] === 'object') {
57
+ result[index] = JSON.stringify(result[index])
58
+ }
59
+ }
60
+ return result
61
+ }
62
+
20
63
  /**
21
64
  * LibSQL adapter for DatabaseManager
22
65
  * Wraps @libsql/client with Connection interface
@@ -117,15 +160,15 @@ class LibSQLConnection implements Connection {
117
160
  /**
118
161
  * Query with SQL string and parameters (alias for execute)
119
162
  */
120
- async query<T = any>(sql: string, params: any[] = []): Promise<QueryResult<T>> {
121
- return this.execute(sql, params) as Promise<QueryResult<T>>
163
+ async query<T = any>(sql: string, params: any[] = [], options?: QueryOptions): Promise<QueryResult<T>> {
164
+ return this.execute(sql, params, options) as Promise<QueryResult<T>>
122
165
  }
123
166
 
124
167
  /**
125
168
  * Execute SQL with parameters
126
169
  * Supports both formats: execute(sql, params) and execute({sql, args})
127
170
  */
128
- async execute(sql: string | { sql: string; args?: any[] }, params: any[] = []): Promise<QueryResult> {
171
+ async execute(sql: string | { sql: string; args?: any[] }, params: any[] = [], options?: QueryOptions): Promise<QueryResult> {
129
172
  try {
130
173
  const target = this.txClient || this.client
131
174
 
@@ -137,19 +180,36 @@ class LibSQLConnection implements Connection {
137
180
  console.log('[LibSQL] Executing SQL:', sql)
138
181
  console.log('[LibSQL] With params:', params)
139
182
  }
140
- query = { sql, args: params }
183
+ let args = params
184
+ // Stringify JSON parameters if specified
185
+ if (options?.stringifyParams) {
186
+ args = stringifyJsonParams(args, options.stringifyParams)
187
+ }
188
+ query = { sql, args }
141
189
  } else {
142
190
  if (process.env.DEBUG_SQL) {
143
191
  console.log('[LibSQL] Executing SQL:', sql.sql)
144
192
  console.log('[LibSQL] With args:', sql.args)
145
193
  }
146
- query = { sql: sql.sql, args: sql.args || [] }
194
+ let args = sql.args || []
195
+ // Stringify JSON parameters if specified
196
+ if (options?.stringifyParams) {
197
+ args = stringifyJsonParams(args, options.stringifyParams)
198
+ }
199
+ query = { sql: sql.sql, args }
147
200
  }
148
201
 
149
202
  const result = await target.execute(query)
150
203
 
204
+ let rows = result.rows as any[]
205
+
206
+ // Parse JSON columns if specified
207
+ if (options?.jsonColumns) {
208
+ rows = parseJsonColumns(rows, options.jsonColumns)
209
+ }
210
+
151
211
  return {
152
- rows: result.rows as any[],
212
+ rows,
153
213
  rowsAffected: Number(result.rowsAffected),
154
214
  lastInsertRowid: result.lastInsertRowid
155
215
  ? BigInt(result.lastInsertRowid.toString())