@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/dist/core/client.d.ts +13 -2
- package/dist/core/client.d.ts.map +1 -1
- package/dist/core/http-client.d.ts +32 -0
- package/dist/core/http-client.d.ts.map +1 -1
- package/dist/core/transaction.d.ts +8 -0
- package/dist/core/transaction.d.ts.map +1 -1
- package/dist/database/adapters/bun-sqlite.d.ts.map +1 -1
- package/dist/database/adapters/libsql.d.ts.map +1 -1
- package/dist/database/adapters/odblite.d.ts.map +1 -1
- package/dist/database/types.d.ts +39 -2
- package/dist/database/types.d.ts.map +1 -1
- package/dist/index.cjs +386 -81
- package/dist/index.js +387 -82
- package/dist/service/service-client.d.ts +18 -2
- package/dist/service/service-client.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/core/client.ts +28 -11
- package/src/core/http-client.ts +141 -0
- package/src/core/transaction.ts +203 -0
- package/src/database/adapters/bun-sqlite.ts +59 -5
- package/src/database/adapters/libsql.ts +66 -6
- package/src/database/adapters/odblite.ts +147 -21
- package/src/database/types.ts +43 -2
- package/src/service/service-client.ts +34 -2
package/package.json
CHANGED
package/src/core/client.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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 =
|
|
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
|
-
*
|
|
172
|
+
* Uses batch-based transaction for true ACID guarantees
|
|
156
173
|
*/
|
|
157
174
|
async begin(): Promise<Transaction> {
|
|
158
|
-
return
|
|
175
|
+
return createBatchTransaction(this.httpClient);
|
|
159
176
|
}
|
|
160
177
|
|
|
161
178
|
/**
|
package/src/core/http-client.ts
CHANGED
|
@@ -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
|
}
|
package/src/core/transaction.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
212
|
+
rows,
|
|
153
213
|
rowsAffected: Number(result.rowsAffected),
|
|
154
214
|
lastInsertRowid: result.lastInsertRowid
|
|
155
215
|
? BigInt(result.lastInsertRowid.toString())
|