@pineliner/odb-client 1.1.3 → 1.1.5
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 +3 -3
- package/dist/core/client.d.ts.map +1 -1
- package/dist/core/http-client.d.ts +8 -1
- package/dist/core/http-client.d.ts.map +1 -1
- package/dist/database/adapters/odblite.d.ts.map +1 -1
- package/dist/database/sql-parser.d.ts.map +1 -1
- package/dist/database/types.d.ts +1 -0
- package/dist/database/types.d.ts.map +1 -1
- package/dist/index.cjs +126 -39
- package/dist/index.js +128 -39
- package/package.json +2 -2
- package/src/core/client.ts +14 -8
- package/src/core/http-client.ts +22 -7
- package/src/database/adapters/bun-sqlite.ts +21 -0
- package/src/database/adapters/libsql.ts +21 -0
- package/src/database/adapters/odblite.ts +118 -33
- package/src/database/sql-parser.ts +23 -3
- package/src/database/types.ts +5 -0
package/dist/core/client.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { ODBLiteConfig, ODBLiteConnection, QueryResult, Transaction, TransactionCallback, TransactionMode, SQLFragment } from '../types.ts';
|
|
2
|
-
import { HTTPClient } from './http-client.ts';
|
|
2
|
+
import { HTTPClient, type QueryRequestOptions } from './http-client.ts';
|
|
3
3
|
interface BatchResult<T = any> {
|
|
4
4
|
results: QueryResult<T>[];
|
|
5
5
|
executionTime: number;
|
|
@@ -10,11 +10,11 @@ interface SQLFunction {
|
|
|
10
10
|
<T = any>(input: any): SQLFragment;
|
|
11
11
|
raw(text: string): string;
|
|
12
12
|
identifier(name: string): string;
|
|
13
|
-
query<T = any>(sql: string, params?: any[]): Promise<QueryResult<T>>;
|
|
13
|
+
query<T = any>(sql: string, params?: any[], opts?: QueryRequestOptions): Promise<QueryResult<T>>;
|
|
14
14
|
execute<T = any>(sql: string | {
|
|
15
15
|
sql: string;
|
|
16
16
|
args?: any[];
|
|
17
|
-
}, args?: any[]): Promise<QueryResult<T>>;
|
|
17
|
+
}, args?: any[], opts?: QueryRequestOptions): Promise<QueryResult<T>>;
|
|
18
18
|
batch<T = any>(statements: Array<{
|
|
19
19
|
sql: string;
|
|
20
20
|
params?: any[];
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../../src/core/client.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,iBAAiB,EAAE,WAAW,EAAE,WAAW,EAAE,mBAAmB,EAAE,eAAe,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AACjJ,OAAO,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;
|
|
1
|
+
{"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../../src/core/client.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,4 +1,11 @@
|
|
|
1
1
|
import type { ODBLiteConfig, QueryResult } from '../types.ts';
|
|
2
|
+
/** Per-request options for a single query. */
|
|
3
|
+
export interface QueryRequestOptions {
|
|
4
|
+
/** Transaction/session token tying this statement to a server-pinned connection. */
|
|
5
|
+
txId?: string;
|
|
6
|
+
/** Override the client's retry count (transaction statements pass 1 = no retry). */
|
|
7
|
+
maxRetries?: number;
|
|
8
|
+
}
|
|
2
9
|
/**
|
|
3
10
|
* HTTP client for communicating with ODBLite service
|
|
4
11
|
*/
|
|
@@ -8,7 +15,7 @@ export declare class HTTPClient {
|
|
|
8
15
|
/**
|
|
9
16
|
* Execute a query against the ODBLite service
|
|
10
17
|
*/
|
|
11
|
-
query<T = any>(sql: string, params?: any[]): Promise<QueryResult<T>>;
|
|
18
|
+
query<T = any>(sql: string, params?: any[], opts?: QueryRequestOptions): Promise<QueryResult<T>>;
|
|
12
19
|
/**
|
|
13
20
|
* Execute multiple statements on the same connection (for transactions)
|
|
14
21
|
* All statements are sent in a single HTTP request and executed atomically on the server
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"http-client.d.ts","sourceRoot":"","sources":["../../src/core/http-client.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAmB,WAAW,EAAE,MAAM,aAAa,CAAA;AAG9E;;GAEG;AACH,qBAAa,UAAU;IACrB,OAAO,CAAC,MAAM,CAAe;gBAEjB,MAAM,EAAE,aAAa;IAajC;;OAEG;IACG,KAAK,CAAC,CAAC,GAAG,GAAG,EAAE,GAAG,EAAE,MAAM,EAAE,MAAM,GAAE,GAAG,EAAO,GAAG,OAAO,CAAC,WAAW,CAAC,CAAC,CAAC,CAAC;
|
|
1
|
+
{"version":3,"file":"http-client.d.ts","sourceRoot":"","sources":["../../src/core/http-client.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAmB,WAAW,EAAE,MAAM,aAAa,CAAA;AAG9E,8CAA8C;AAC9C,MAAM,WAAW,mBAAmB;IAClC,oFAAoF;IACpF,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,oFAAoF;IACpF,UAAU,CAAC,EAAE,MAAM,CAAA;CACpB;AAED;;GAEG;AACH,qBAAa,UAAU;IACrB,OAAO,CAAC,MAAM,CAAe;gBAEjB,MAAM,EAAE,aAAa;IAajC;;OAEG;IACG,KAAK,CAAC,CAAC,GAAG,GAAG,EAAE,GAAG,EAAE,MAAM,EAAE,MAAM,GAAE,GAAG,EAAO,EAAE,IAAI,CAAC,EAAE,mBAAmB,GAAG,OAAO,CAAC,WAAW,CAAC,CAAC,CAAC,CAAC;IAwD1G;;;OAGG;IACG,KAAK,CAAC,CAAC,GAAG,GAAG,EAAE,UAAU,EAAE,KAAK,CAAC;QAAE,GAAG,EAAE,MAAM,CAAC;QAAC,MAAM,CAAC,EAAE,GAAG,EAAE,CAAA;KAAE,CAAC,GAAG,OAAO,CAAC;QAChF,OAAO,EAAE,WAAW,CAAC,CAAC,CAAC,EAAE,CAAC;QAC1B,aAAa,EAAE,MAAM,CAAC;QACtB,kBAAkB,EAAE,MAAM,CAAC;KAC5B,CAAC;IA4CF;;OAEG;IACG,IAAI,IAAI,OAAO,CAAC,OAAO,CAAC;IAqB9B;;OAEG;IACG,eAAe,IAAI,OAAO,CAAC,GAAG,CAAC;IAqBrC;;OAEG;IACH,WAAW,CAAC,UAAU,EAAE,MAAM,GAAG,IAAI;IAIrC;;OAEG;YACW,WAAW;IAoDzB;;OAEG;IACH,SAAS,CAAC,OAAO,EAAE,OAAO,CAAC,aAAa,CAAC,GAAG,UAAU;IAOtD;;OAEG;IACH,SAAS,IAAI,QAAQ,CAAC,aAAa,CAAC;IAIpC;;;OAGG;IACG,gBAAgB,CAAC,UAAU,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC;QACpD,OAAO,EAAE,OAAO,CAAC;QACjB,YAAY,EAAE,MAAM,CAAC;QACrB,UAAU,EAAE,MAAM,EAAE,CAAC;KACtB,CAAC;IAqCF;;OAEG;IACG,aAAa,IAAI,OAAO,CAAC;QAC7B,OAAO,EAAE,OAAO,CAAC;QACjB,YAAY,EAAE,MAAM,CAAC;QACrB,MAAM,EAAE,OAAO,CAAC;QAChB,UAAU,EAAE,MAAM,EAAE,CAAC;QACrB,UAAU,EAAE,OAAO,CAAC;QACpB,oBAAoB,EAAE,MAAM,EAAE,CAAC;KAChC,CAAC;CAiCH"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"odblite.d.ts","sourceRoot":"","sources":["../../../src/database/adapters/odblite.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,8BAA8B,CAAA;AAE5D,OAAO,KAAK,EACV,eAAe,EACf,UAAU,EAGV,aAAa,EAEd,MAAM,UAAU,CAAA;
|
|
1
|
+
{"version":3,"file":"odblite.d.ts","sourceRoot":"","sources":["../../../src/database/adapters/odblite.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,8BAA8B,CAAA;AAE5D,OAAO,KAAK,EACV,eAAe,EACf,UAAU,EAGV,aAAa,EAEd,MAAM,UAAU,CAAA;AA6FjB;;;GAGG;AACH,qBAAa,cAAe,YAAW,eAAe;IACpD,QAAQ,CAAC,IAAI,EAAG,SAAS,CAAS;IAClC,OAAO,CAAC,MAAM,CAAe;IACtB,aAAa,EAAE,aAAa,CAAA;gBAEvB,MAAM,EAAE,aAAa;IAQ3B,OAAO,CAAC,MAAM,EAAE,GAAG,GAAG,OAAO,CAAC,UAAU,CAAC;IAgCzC,UAAU,CAAC,QAAQ,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAa5C,SAAS,IAAI,OAAO,CAAC,OAAO,CAAC;CAQpC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"sql-parser.d.ts","sourceRoot":"","sources":["../../src/database/sql-parser.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,gBAAgB;IAC/B,gBAAgB,EAAE,MAAM,EAAE,CAAA;IAC1B,iBAAiB,EAAE,MAAM,EAAE,CAAA;CAC5B;AAED,MAAM,WAAW,gBAAgB;IAC/B,cAAc,CAAC,EAAE,OAAO,CAAA;CACzB;AAED;;;GAGG;AACH,wBAAgB,QAAQ,CAAC,UAAU,EAAE,MAAM,EAAE,OAAO,GAAE,gBAAqB,GAAG,gBAAgB,CA0B7F;
|
|
1
|
+
{"version":3,"file":"sql-parser.d.ts","sourceRoot":"","sources":["../../src/database/sql-parser.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,gBAAgB;IAC/B,gBAAgB,EAAE,MAAM,EAAE,CAAA;IAC1B,iBAAiB,EAAE,MAAM,EAAE,CAAA;CAC5B;AAED,MAAM,WAAW,gBAAgB;IAC/B,cAAc,CAAC,EAAE,OAAO,CAAA;CACzB;AAED;;;GAGG;AACH,wBAAgB,QAAQ,CAAC,UAAU,EAAE,MAAM,EAAE,OAAO,GAAE,gBAAqB,GAAG,gBAAgB,CA0B7F;AAwGD;;GAEG;AACH,wBAAgB,kBAAkB,CAAC,UAAU,EAAE,MAAM,GAAG,MAAM,EAAE,CAG/D"}
|
package/dist/database/types.d.ts
CHANGED
|
@@ -58,6 +58,7 @@ export interface Connection {
|
|
|
58
58
|
prepare(sql: string): PreparedStatement;
|
|
59
59
|
transaction<T>(fn: (tx: Connection) => Promise<T>): Promise<T>;
|
|
60
60
|
begin<T>(fn: (tx: Connection) => Promise<T>): Promise<T>;
|
|
61
|
+
savepoint<T>(fn: (tx: Connection) => Promise<T>): Promise<T>;
|
|
61
62
|
close(): Promise<void>;
|
|
62
63
|
createORM?(): any;
|
|
63
64
|
databaseHash?: string;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/database/types.ts"],"names":[],"mappings":"AAIA;;GAEG;AACH,MAAM,MAAM,WAAW,GACnB,WAAW,GACX,cAAc,GACd,WAAW,GACX,WAAW,CAAA;AAEf;;GAEG;AACH,MAAM,MAAM,WAAW,GAAG,YAAY,GAAG,QAAQ,GAAG,SAAS,CAAA;AAE7D;;GAEG;AACH,MAAM,WAAW,WAAW,CAAC,CAAC,GAAG,GAAG;IAClC,IAAI,EAAE,CAAC,EAAE,CAAA;IACT,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,eAAe,CAAC,EAAE,MAAM,GAAG,MAAM,CAAA;CAClC;AAED;;GAEG;AACH,MAAM,WAAW,YAAY;IAC3B;;;;;;OAMG;IACH,WAAW,CAAC,EAAE,MAAM,EAAE,CAAA;IAEtB;;;;;;;;;OASG;IACH,eAAe,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;CACzC;AAED;;;;;;;;GAQG;AACH,MAAM,WAAW,UAAU;IAGzB,GAAG,CAAC,CAAC,GAAG,GAAG,EAAE,OAAO,EAAE,oBAAoB,EAAE,GAAG,MAAM,EAAE,GAAG,EAAE,GAAG,OAAO,CAAC,CAAC,EAAE,CAAC,CAAA;IAG3E,GAAG,CAAC,KAAK,EAAE,GAAG,EAAE,GAAG,IAAI,EAAE,MAAM,EAAE,GAAG,GAAG,CAAA;IAGvC,KAAK,CAAC,CAAC,GAAG,GAAG,EAAE,GAAG,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,GAAG,EAAE,EAAE,OAAO,CAAC,EAAE,YAAY,GAAG,OAAO,CAAC,WAAW,CAAC,CAAC,CAAC,CAAC,CAAA;IAC5F,OAAO,CAAC,GAAG,EAAE,MAAM,GAAG;QAAE,GAAG,EAAE,MAAM,CAAC;QAAC,IAAI,CAAC,EAAE,GAAG,EAAE,CAAA;KAAE,EAAE,MAAM,CAAC,EAAE,GAAG,EAAE,EAAE,OAAO,CAAC,EAAE,YAAY,GAAG,OAAO,CAAC,WAAW,CAAC,CAAA;IAClH,OAAO,CAAC,GAAG,EAAE,MAAM,GAAG,iBAAiB,CAAA;IAGvC,WAAW,CAAC,CAAC,EAAE,EAAE,EAAE,CAAC,EAAE,EAAE,UAAU,KAAK,OAAO,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,CAAA;IAC9D,KAAK,CAAC,CAAC,EAAE,EAAE,EAAE,CAAC,EAAE,EAAE,UAAU,KAAK,OAAO,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,CAAA;
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/database/types.ts"],"names":[],"mappings":"AAIA;;GAEG;AACH,MAAM,MAAM,WAAW,GACnB,WAAW,GACX,cAAc,GACd,WAAW,GACX,WAAW,CAAA;AAEf;;GAEG;AACH,MAAM,MAAM,WAAW,GAAG,YAAY,GAAG,QAAQ,GAAG,SAAS,CAAA;AAE7D;;GAEG;AACH,MAAM,WAAW,WAAW,CAAC,CAAC,GAAG,GAAG;IAClC,IAAI,EAAE,CAAC,EAAE,CAAA;IACT,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,eAAe,CAAC,EAAE,MAAM,GAAG,MAAM,CAAA;CAClC;AAED;;GAEG;AACH,MAAM,WAAW,YAAY;IAC3B;;;;;;OAMG;IACH,WAAW,CAAC,EAAE,MAAM,EAAE,CAAA;IAEtB;;;;;;;;;OASG;IACH,eAAe,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;CACzC;AAED;;;;;;;;GAQG;AACH,MAAM,WAAW,UAAU;IAGzB,GAAG,CAAC,CAAC,GAAG,GAAG,EAAE,OAAO,EAAE,oBAAoB,EAAE,GAAG,MAAM,EAAE,GAAG,EAAE,GAAG,OAAO,CAAC,CAAC,EAAE,CAAC,CAAA;IAG3E,GAAG,CAAC,KAAK,EAAE,GAAG,EAAE,GAAG,IAAI,EAAE,MAAM,EAAE,GAAG,GAAG,CAAA;IAGvC,KAAK,CAAC,CAAC,GAAG,GAAG,EAAE,GAAG,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,GAAG,EAAE,EAAE,OAAO,CAAC,EAAE,YAAY,GAAG,OAAO,CAAC,WAAW,CAAC,CAAC,CAAC,CAAC,CAAA;IAC5F,OAAO,CAAC,GAAG,EAAE,MAAM,GAAG;QAAE,GAAG,EAAE,MAAM,CAAC;QAAC,IAAI,CAAC,EAAE,GAAG,EAAE,CAAA;KAAE,EAAE,MAAM,CAAC,EAAE,GAAG,EAAE,EAAE,OAAO,CAAC,EAAE,YAAY,GAAG,OAAO,CAAC,WAAW,CAAC,CAAA;IAClH,OAAO,CAAC,GAAG,EAAE,MAAM,GAAG,iBAAiB,CAAA;IAGvC,WAAW,CAAC,CAAC,EAAE,EAAE,EAAE,CAAC,EAAE,EAAE,UAAU,KAAK,OAAO,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,CAAA;IAC9D,KAAK,CAAC,CAAC,EAAE,EAAE,EAAE,CAAC,EAAE,EAAE,UAAU,KAAK,OAAO,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,CAAA;IAKxD,SAAS,CAAC,CAAC,EAAE,EAAE,EAAE,CAAC,EAAE,EAAE,UAAU,KAAK,OAAO,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,CAAA;IAG5D,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAA;IAGtB,SAAS,CAAC,IAAI,GAAG,CAAA;IAGjB,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,YAAY,CAAC,EAAE,MAAM,CAAA;IAGrB,gBAAgB,CAAC,CAAC,UAAU,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC;QAC/C,OAAO,EAAE,OAAO,CAAC;QACjB,YAAY,EAAE,MAAM,CAAC;QACrB,UAAU,EAAE,MAAM,EAAE,CAAC;KACtB,CAAC,CAAA;IACF,aAAa,CAAC,IAAI,OAAO,CAAC;QACxB,OAAO,EAAE,OAAO,CAAC;QACjB,YAAY,EAAE,MAAM,CAAC;QACrB,MAAM,EAAE,OAAO,CAAC;QAChB,UAAU,EAAE,MAAM,EAAE,CAAC;QACrB,UAAU,EAAE,OAAO,CAAC;QACpB,oBAAoB,EAAE,MAAM,EAAE,CAAC;KAChC,CAAC,CAAA;CACH;AAED;;GAEG;AACH,MAAM,WAAW,iBAAiB;IAChC,OAAO,CAAC,MAAM,CAAC,EAAE,GAAG,EAAE,GAAG,OAAO,CAAC,WAAW,CAAC,CAAA;IAC7C,GAAG,CAAC,MAAM,CAAC,EAAE,GAAG,EAAE,GAAG,OAAO,CAAC,GAAG,EAAE,CAAC,CAAA;IACnC,GAAG,CAAC,MAAM,CAAC,EAAE,GAAG,EAAE,GAAG,OAAO,CAAC,GAAG,GAAG,IAAI,CAAC,CAAA;CACzC;AAED;;GAEG;AACH,MAAM,WAAW,eAAe;IAC9B,QAAQ,CAAC,IAAI,EAAE,WAAW,CAAA;IAG1B,OAAO,CAAC,MAAM,EAAE,GAAG,GAAG,OAAO,CAAC,UAAU,CAAC,CAAA;IACzC,UAAU,CAAC,QAAQ,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;IAG5C,SAAS,IAAI,OAAO,CAAC,OAAO,CAAC,CAAA;CAC9B;AAED;;GAEG;AACH,MAAM,WAAW,eAAe;IAC9B,aAAa,EAAE,MAAM,CAAA;CACtB;AAED;;;GAGG;AACH,MAAM,WAAW,qBAAqB;IAEpC,OAAO,EAAE,WAAW,CAAA;IAGpB,YAAY,EAAE,MAAM,CAAA;IAGpB,MAAM,CAAC,EAAE;QACP,aAAa,CAAC,EAAE,MAAM,CAAA;QACtB,SAAS,CAAC,EAAE,MAAM,CAAA;KACnB,CAAA;IAED,OAAO,CAAC,EAAE;QACR,UAAU,EAAE,MAAM,CAAA;QAClB,MAAM,EAAE,MAAM,CAAA;QACd,MAAM,CAAC,EAAE,MAAM,CAAA;KAChB,CAAA;IAGD,kBAAkB,CAAC,EAAE,MAAM,CAAA;CAC5B;AAED;;GAEG;AACH,MAAM,WAAW,eAAe;IAC9B,YAAY,EAAE,MAAM,CAAA;IACpB,QAAQ,CAAC,EAAE,OAAO,CAAA;IAClB,MAAM,CAAC,EAAE,OAAO,CAAA;CACjB;AAED;;GAEG;AACH,MAAM,WAAW,YAAY;IAC3B,GAAG,EAAE,MAAM,CAAA;IACX,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,aAAa,CAAC,EAAE,MAAM,CAAA;CACvB;AAED;;GAEG;AACH,MAAM,WAAW,aAAa;IAC5B,UAAU,EAAE,MAAM,CAAA;IAClB,MAAM,EAAE,MAAM,CAAA;IACd,MAAM,CAAC,EAAE,MAAM,CAAA;CAChB;AAED;;GAEG;AACH,MAAM,WAAW,UAAU;IACzB,QAAQ,EAAE,MAAM,CAAA;IAChB,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,SAAS,EAAE,IAAI,CAAA;IACf,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAA;CAC/B"}
|
package/dist/index.cjs
CHANGED
|
@@ -412,6 +412,12 @@ var __webpack_modules__ = {
|
|
|
412
412
|
function createORM(db) {
|
|
413
413
|
return new ORM(db);
|
|
414
414
|
}
|
|
415
|
+
},
|
|
416
|
+
async_hooks (module) {
|
|
417
|
+
module.exports = require("async_hooks");
|
|
418
|
+
},
|
|
419
|
+
"node:async_hooks" (module) {
|
|
420
|
+
module.exports = require("node:async_hooks");
|
|
415
421
|
}
|
|
416
422
|
};
|
|
417
423
|
var __webpack_module_cache__ = {};
|
|
@@ -540,12 +546,15 @@ var __webpack_exports__ = {};
|
|
|
540
546
|
};
|
|
541
547
|
if ('string' == typeof this.config.baseUrl) this.config.baseUrl = this.config.baseUrl.replace(/\/$/, '');
|
|
542
548
|
}
|
|
543
|
-
async query(sql, params = []) {
|
|
549
|
+
async query(sql, params = [], opts) {
|
|
544
550
|
if (!this.config.databaseId) throw new ConnectionError('No database ID configured. Use setDatabase() first.');
|
|
545
551
|
const url = `${this.config.baseUrl}/query/${this.config.databaseId}`;
|
|
546
552
|
const body = {
|
|
547
553
|
sql,
|
|
548
|
-
params
|
|
554
|
+
params,
|
|
555
|
+
...opts?.txId ? {
|
|
556
|
+
txId: opts.txId
|
|
557
|
+
} : {}
|
|
549
558
|
};
|
|
550
559
|
try {
|
|
551
560
|
const response = await this.makeRequest(url, {
|
|
@@ -555,7 +564,7 @@ var __webpack_exports__ = {};
|
|
|
555
564
|
Authorization: `Bearer ${this.config.apiKey}`
|
|
556
565
|
},
|
|
557
566
|
body: JSON.stringify(body)
|
|
558
|
-
});
|
|
567
|
+
}, opts?.maxRetries);
|
|
559
568
|
const data = await response.json();
|
|
560
569
|
if (!data.success) throw new types_QueryError(data.error || 'Query failed', sql, params);
|
|
561
570
|
let rows = [];
|
|
@@ -634,9 +643,10 @@ var __webpack_exports__ = {};
|
|
|
634
643
|
setDatabase(databaseId) {
|
|
635
644
|
this.config.databaseId = databaseId;
|
|
636
645
|
}
|
|
637
|
-
async makeRequest(url, options) {
|
|
646
|
+
async makeRequest(url, options, maxRetries) {
|
|
638
647
|
let lastError;
|
|
639
|
-
|
|
648
|
+
const retries = maxRetries ?? this.config.retries ?? 3;
|
|
649
|
+
for(let attempt = 1; attempt <= retries; attempt++)try {
|
|
640
650
|
const controller = new AbortController();
|
|
641
651
|
const timeoutId = setTimeout(()=>controller.abort(), this.config.timeout);
|
|
642
652
|
const response = await fetch(url, {
|
|
@@ -660,12 +670,12 @@ var __webpack_exports__ = {};
|
|
|
660
670
|
} catch (error) {
|
|
661
671
|
lastError = error instanceof Error ? error : new Error('Unknown error');
|
|
662
672
|
if (error instanceof ConnectionError && error.message.includes('HTTP 4')) throw error;
|
|
663
|
-
if (attempt <
|
|
673
|
+
if (attempt < retries) {
|
|
664
674
|
const delay = Math.min(1000 * 2 ** (attempt - 1), 10000);
|
|
665
675
|
await new Promise((resolve)=>setTimeout(resolve, delay));
|
|
666
676
|
}
|
|
667
677
|
}
|
|
668
|
-
throw new ConnectionError(`Failed after ${
|
|
678
|
+
throw new ConnectionError(`Failed after ${retries} attempts: ${lastError?.message}`, lastError);
|
|
669
679
|
}
|
|
670
680
|
configure(updates) {
|
|
671
681
|
return new HTTPClient({
|
|
@@ -1138,10 +1148,10 @@ var __webpack_exports__ = {};
|
|
|
1138
1148
|
};
|
|
1139
1149
|
sqlFunction.raw = (text)=>sql_parser_SQLParser.raw(text);
|
|
1140
1150
|
sqlFunction.identifier = (name)=>sql_parser_SQLParser.identifier(name);
|
|
1141
|
-
sqlFunction.query = async (sql, params = [])=>await this.httpClient.query(sql, params);
|
|
1142
|
-
sqlFunction.execute = async (sql, args)=>{
|
|
1143
|
-
if ('string' == typeof sql) return await this.httpClient.query(sql, args || []);
|
|
1144
|
-
return await this.httpClient.query(sql.sql, sql.args || []);
|
|
1151
|
+
sqlFunction.query = async (sql, params = [], opts)=>await this.httpClient.query(sql, params, opts);
|
|
1152
|
+
sqlFunction.execute = async (sql, args, opts)=>{
|
|
1153
|
+
if ('string' == typeof sql) return await this.httpClient.query(sql, args || [], opts);
|
|
1154
|
+
return await this.httpClient.query(sql.sql, sql.args || [], opts);
|
|
1145
1155
|
};
|
|
1146
1156
|
sqlFunction.batch = async (statements)=>await this.httpClient.batch(statements);
|
|
1147
1157
|
sqlFunction.begin = async (modeOrCallback, callback)=>{
|
|
@@ -1697,6 +1707,19 @@ var __webpack_exports__ = {};
|
|
|
1697
1707
|
async begin(fn) {
|
|
1698
1708
|
return this.transaction(fn);
|
|
1699
1709
|
}
|
|
1710
|
+
async savepoint(fn) {
|
|
1711
|
+
if (!this.inTransaction) return this.transaction(fn);
|
|
1712
|
+
const name = `sp_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
|
|
1713
|
+
try {
|
|
1714
|
+
await this.execute(`SAVEPOINT ${name}`);
|
|
1715
|
+
const result = await fn(this);
|
|
1716
|
+
await this.execute(`RELEASE SAVEPOINT ${name}`);
|
|
1717
|
+
return result;
|
|
1718
|
+
} catch (error) {
|
|
1719
|
+
await this.execute(`ROLLBACK TO SAVEPOINT ${name}`).catch(()=>{});
|
|
1720
|
+
throw error;
|
|
1721
|
+
}
|
|
1722
|
+
}
|
|
1700
1723
|
async close() {
|
|
1701
1724
|
this.db.close();
|
|
1702
1725
|
}
|
|
@@ -1853,12 +1876,44 @@ var __webpack_exports__ = {};
|
|
|
1853
1876
|
async begin(fn) {
|
|
1854
1877
|
return this.transaction(fn);
|
|
1855
1878
|
}
|
|
1879
|
+
async savepoint(fn) {
|
|
1880
|
+
if (!this.txClient) return this.transaction(fn);
|
|
1881
|
+
const name = `sp_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
|
|
1882
|
+
try {
|
|
1883
|
+
await this.execute(`SAVEPOINT ${name}`);
|
|
1884
|
+
const result = await fn(this);
|
|
1885
|
+
await this.execute(`RELEASE SAVEPOINT ${name}`);
|
|
1886
|
+
return result;
|
|
1887
|
+
} catch (error) {
|
|
1888
|
+
await this.execute(`ROLLBACK TO SAVEPOINT ${name}`).catch(()=>{});
|
|
1889
|
+
throw error;
|
|
1890
|
+
}
|
|
1891
|
+
}
|
|
1856
1892
|
async close() {}
|
|
1857
1893
|
createORM() {
|
|
1858
1894
|
const { createORM } = __webpack_require__("./src/orm/index.ts");
|
|
1859
1895
|
return createORM(this);
|
|
1860
1896
|
}
|
|
1861
1897
|
}
|
|
1898
|
+
let AsyncLocalStorageCtor = null;
|
|
1899
|
+
try {
|
|
1900
|
+
AsyncLocalStorageCtor = __webpack_require__("node:async_hooks").AsyncLocalStorage;
|
|
1901
|
+
} catch {
|
|
1902
|
+
try {
|
|
1903
|
+
AsyncLocalStorageCtor = __webpack_require__("async_hooks").AsyncLocalStorage;
|
|
1904
|
+
} catch {
|
|
1905
|
+
AsyncLocalStorageCtor = null;
|
|
1906
|
+
}
|
|
1907
|
+
}
|
|
1908
|
+
let txCounter = 0;
|
|
1909
|
+
function newTxId() {
|
|
1910
|
+
try {
|
|
1911
|
+
const c = globalThis.crypto;
|
|
1912
|
+
if (c?.randomUUID) return `tx_${c.randomUUID()}`;
|
|
1913
|
+
} catch {}
|
|
1914
|
+
txCounter = (txCounter + 1) % Number.MAX_SAFE_INTEGER;
|
|
1915
|
+
return `tx_${Date.now().toString(36)}_${txCounter.toString(36)}`;
|
|
1916
|
+
}
|
|
1862
1917
|
function odblite_parseJsonColumns(rows, jsonColumns) {
|
|
1863
1918
|
if (!jsonColumns || 0 === jsonColumns.length) return rows;
|
|
1864
1919
|
return rows.map((row)=>{
|
|
@@ -1933,7 +1988,8 @@ var __webpack_exports__ = {};
|
|
|
1933
1988
|
class ODBLiteConnection {
|
|
1934
1989
|
client;
|
|
1935
1990
|
serviceClient;
|
|
1936
|
-
|
|
1991
|
+
txStorage = AsyncLocalStorageCtor ? new AsyncLocalStorageCtor() : null;
|
|
1992
|
+
fallbackTxId;
|
|
1937
1993
|
databaseName;
|
|
1938
1994
|
databaseHash;
|
|
1939
1995
|
sql;
|
|
@@ -1968,11 +2024,22 @@ var __webpack_exports__ = {};
|
|
|
1968
2024
|
async query(sql, params = [], options) {
|
|
1969
2025
|
return this.execute(sql, params, options);
|
|
1970
2026
|
}
|
|
2027
|
+
activeTxId() {
|
|
2028
|
+
return this.txStorage ? this.txStorage.getStore()?.txId : this.fallbackTxId;
|
|
2029
|
+
}
|
|
2030
|
+
requestOpts() {
|
|
2031
|
+
const txId = this.activeTxId();
|
|
2032
|
+
return txId ? {
|
|
2033
|
+
txId,
|
|
2034
|
+
maxRetries: 1
|
|
2035
|
+
} : void 0;
|
|
2036
|
+
}
|
|
1971
2037
|
async execute(sql, params = [], options) {
|
|
1972
2038
|
try {
|
|
1973
2039
|
let rows;
|
|
1974
2040
|
let rowsAffected;
|
|
1975
2041
|
let lastInsertRowid;
|
|
2042
|
+
const reqOpts = this.requestOpts();
|
|
1976
2043
|
if ('object' == typeof sql) {
|
|
1977
2044
|
if (process.env.DEBUG_SQL) {
|
|
1978
2045
|
console.log('[ODBLite] Executing SQL:', sql.sql);
|
|
@@ -1980,7 +2047,7 @@ var __webpack_exports__ = {};
|
|
|
1980
2047
|
}
|
|
1981
2048
|
let args = sql.args || [];
|
|
1982
2049
|
if (options?.stringifyParams) args = odblite_stringifyJsonParams(args, options.stringifyParams);
|
|
1983
|
-
const result = await this.client.sql.execute(sql.sql, args);
|
|
2050
|
+
const result = await this.client.sql.execute(sql.sql, args, reqOpts);
|
|
1984
2051
|
rows = result.rows;
|
|
1985
2052
|
rowsAffected = result.rowsAffected || 0;
|
|
1986
2053
|
lastInsertRowid = result.lastInsertRowid;
|
|
@@ -1991,7 +2058,7 @@ var __webpack_exports__ = {};
|
|
|
1991
2058
|
}
|
|
1992
2059
|
let args = params;
|
|
1993
2060
|
if (options?.stringifyParams) args = odblite_stringifyJsonParams(args, options.stringifyParams);
|
|
1994
|
-
const result = await this.client.sql.execute(sql, args);
|
|
2061
|
+
const result = await this.client.sql.execute(sql, args, reqOpts);
|
|
1995
2062
|
rows = result.rows;
|
|
1996
2063
|
rowsAffected = result.rowsAffected || 0;
|
|
1997
2064
|
lastInsertRowid = result.lastInsertRowid;
|
|
@@ -2020,36 +2087,45 @@ var __webpack_exports__ = {};
|
|
|
2020
2087
|
};
|
|
2021
2088
|
}
|
|
2022
2089
|
async transaction(fn) {
|
|
2023
|
-
if (this.
|
|
2024
|
-
|
|
2025
|
-
|
|
2026
|
-
|
|
2027
|
-
|
|
2028
|
-
await this.execute(`RELEASE SAVEPOINT ${savepointName}`);
|
|
2029
|
-
return result;
|
|
2030
|
-
} catch (error) {
|
|
2031
|
-
await this.execute(`ROLLBACK TO SAVEPOINT ${savepointName}`).catch(()=>{});
|
|
2032
|
-
throw error;
|
|
2033
|
-
}
|
|
2034
|
-
}
|
|
2035
|
-
this.inTransaction = true;
|
|
2036
|
-
try {
|
|
2037
|
-
await this.execute('BEGIN');
|
|
2090
|
+
if (this.activeTxId()) return fn(this);
|
|
2091
|
+
const txId = newTxId();
|
|
2092
|
+
const run = async ()=>{
|
|
2093
|
+
const prevFallback = this.fallbackTxId;
|
|
2094
|
+
if (!this.txStorage) this.fallbackTxId = txId;
|
|
2038
2095
|
try {
|
|
2039
|
-
|
|
2040
|
-
|
|
2041
|
-
|
|
2042
|
-
|
|
2043
|
-
|
|
2044
|
-
|
|
2096
|
+
await this.execute('BEGIN');
|
|
2097
|
+
try {
|
|
2098
|
+
const result = await fn(this);
|
|
2099
|
+
await this.execute('COMMIT');
|
|
2100
|
+
return result;
|
|
2101
|
+
} catch (error) {
|
|
2102
|
+
await this.execute('ROLLBACK').catch(()=>{});
|
|
2103
|
+
throw error;
|
|
2104
|
+
}
|
|
2105
|
+
} finally{
|
|
2106
|
+
if (!this.txStorage) this.fallbackTxId = prevFallback;
|
|
2045
2107
|
}
|
|
2046
|
-
}
|
|
2047
|
-
|
|
2048
|
-
|
|
2108
|
+
};
|
|
2109
|
+
return this.txStorage ? this.txStorage.run({
|
|
2110
|
+
txId
|
|
2111
|
+
}, run) : run();
|
|
2049
2112
|
}
|
|
2050
2113
|
async begin(fn) {
|
|
2051
2114
|
return this.transaction(fn);
|
|
2052
2115
|
}
|
|
2116
|
+
async savepoint(fn) {
|
|
2117
|
+
if (!this.activeTxId()) return this.transaction(fn);
|
|
2118
|
+
const name = `sp_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
|
|
2119
|
+
try {
|
|
2120
|
+
await this.execute(`SAVEPOINT ${name}`);
|
|
2121
|
+
const result = await fn(this);
|
|
2122
|
+
await this.execute(`RELEASE SAVEPOINT ${name}`);
|
|
2123
|
+
return result;
|
|
2124
|
+
} catch (error) {
|
|
2125
|
+
await this.execute(`ROLLBACK TO SAVEPOINT ${name}`).catch(()=>{});
|
|
2126
|
+
throw error;
|
|
2127
|
+
}
|
|
2128
|
+
}
|
|
2053
2129
|
async close() {}
|
|
2054
2130
|
createORM() {
|
|
2055
2131
|
const { createORM } = __webpack_require__("./src/orm/index.ts");
|
|
@@ -2087,6 +2163,8 @@ var __webpack_exports__ = {};
|
|
|
2087
2163
|
let inQuote = false;
|
|
2088
2164
|
let quoteChar = '';
|
|
2089
2165
|
let beginEndDepth = 0;
|
|
2166
|
+
let inCte = false;
|
|
2167
|
+
let cteParenDepth = 0;
|
|
2090
2168
|
const lines = sqlContent.split('\n');
|
|
2091
2169
|
for (const line of lines){
|
|
2092
2170
|
let processedLine = '';
|
|
@@ -2100,6 +2178,15 @@ var __webpack_exports__ = {};
|
|
|
2100
2178
|
if (beginEndDepth < 0) beginEndDepth = 0;
|
|
2101
2179
|
}
|
|
2102
2180
|
}
|
|
2181
|
+
if (!inQuote && lineWithoutComments.trim()) {
|
|
2182
|
+
const withMatch = lineWithoutComments.match(/\bWITH\b.*\bAS\b/i);
|
|
2183
|
+
if (withMatch) inCte = true;
|
|
2184
|
+
if (inCte) for (const char of lineWithoutComments){
|
|
2185
|
+
if ('(' === char) cteParenDepth++;
|
|
2186
|
+
if (')' === char) cteParenDepth--;
|
|
2187
|
+
if (0 === cteParenDepth && ')' === char) inCte = false;
|
|
2188
|
+
}
|
|
2189
|
+
}
|
|
2103
2190
|
for(let i = 0; i < line.length; i++){
|
|
2104
2191
|
const char = line[i];
|
|
2105
2192
|
const prevChar = i > 0 ? line[i - 1] : '';
|
|
@@ -2115,7 +2202,7 @@ var __webpack_exports__ = {};
|
|
|
2115
2202
|
quoteChar = char;
|
|
2116
2203
|
}
|
|
2117
2204
|
processedLine += char;
|
|
2118
|
-
if (';' === char && !inQuote && 0 === beginEndDepth) {
|
|
2205
|
+
if (';' === char && !inQuote && 0 === beginEndDepth && !inCte) {
|
|
2119
2206
|
currentStatement += processedLine;
|
|
2120
2207
|
const stmt = currentStatement.trim();
|
|
2121
2208
|
if (stmt && !stmt.startsWith('--')) statements.push(stmt);
|
package/dist/index.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import * as __rspack_external_async_hooks from "async_hooks";
|
|
2
|
+
import * as __rspack_external_node_async_hooks_e65a2d6c from "node:async_hooks";
|
|
1
3
|
import { __webpack_require__ } from "./rslib-runtime.js";
|
|
2
4
|
import { Database } from "bun:sqlite";
|
|
3
5
|
import { createClient } from "@libsql/client";
|
|
@@ -415,6 +417,12 @@ __webpack_require__.add({
|
|
|
415
417
|
function createORM(db) {
|
|
416
418
|
return new ORM(db);
|
|
417
419
|
}
|
|
420
|
+
},
|
|
421
|
+
async_hooks (module) {
|
|
422
|
+
module.exports = __rspack_external_async_hooks;
|
|
423
|
+
},
|
|
424
|
+
"node:async_hooks" (module) {
|
|
425
|
+
module.exports = __rspack_external_node_async_hooks_e65a2d6c;
|
|
418
426
|
}
|
|
419
427
|
});
|
|
420
428
|
class ODBLiteError extends Error {
|
|
@@ -450,12 +458,15 @@ class HTTPClient {
|
|
|
450
458
|
};
|
|
451
459
|
if ('string' == typeof this.config.baseUrl) this.config.baseUrl = this.config.baseUrl.replace(/\/$/, '');
|
|
452
460
|
}
|
|
453
|
-
async query(sql, params = []) {
|
|
461
|
+
async query(sql, params = [], opts) {
|
|
454
462
|
if (!this.config.databaseId) throw new ConnectionError('No database ID configured. Use setDatabase() first.');
|
|
455
463
|
const url = `${this.config.baseUrl}/query/${this.config.databaseId}`;
|
|
456
464
|
const body = {
|
|
457
465
|
sql,
|
|
458
|
-
params
|
|
466
|
+
params,
|
|
467
|
+
...opts?.txId ? {
|
|
468
|
+
txId: opts.txId
|
|
469
|
+
} : {}
|
|
459
470
|
};
|
|
460
471
|
try {
|
|
461
472
|
const response = await this.makeRequest(url, {
|
|
@@ -465,7 +476,7 @@ class HTTPClient {
|
|
|
465
476
|
Authorization: `Bearer ${this.config.apiKey}`
|
|
466
477
|
},
|
|
467
478
|
body: JSON.stringify(body)
|
|
468
|
-
});
|
|
479
|
+
}, opts?.maxRetries);
|
|
469
480
|
const data = await response.json();
|
|
470
481
|
if (!data.success) throw new types_QueryError(data.error || 'Query failed', sql, params);
|
|
471
482
|
let rows = [];
|
|
@@ -544,9 +555,10 @@ class HTTPClient {
|
|
|
544
555
|
setDatabase(databaseId) {
|
|
545
556
|
this.config.databaseId = databaseId;
|
|
546
557
|
}
|
|
547
|
-
async makeRequest(url, options) {
|
|
558
|
+
async makeRequest(url, options, maxRetries) {
|
|
548
559
|
let lastError;
|
|
549
|
-
|
|
560
|
+
const retries = maxRetries ?? this.config.retries ?? 3;
|
|
561
|
+
for(let attempt = 1; attempt <= retries; attempt++)try {
|
|
550
562
|
const controller = new AbortController();
|
|
551
563
|
const timeoutId = setTimeout(()=>controller.abort(), this.config.timeout);
|
|
552
564
|
const response = await fetch(url, {
|
|
@@ -570,12 +582,12 @@ class HTTPClient {
|
|
|
570
582
|
} catch (error) {
|
|
571
583
|
lastError = error instanceof Error ? error : new Error('Unknown error');
|
|
572
584
|
if (error instanceof ConnectionError && error.message.includes('HTTP 4')) throw error;
|
|
573
|
-
if (attempt <
|
|
585
|
+
if (attempt < retries) {
|
|
574
586
|
const delay = Math.min(1000 * 2 ** (attempt - 1), 10000);
|
|
575
587
|
await new Promise((resolve)=>setTimeout(resolve, delay));
|
|
576
588
|
}
|
|
577
589
|
}
|
|
578
|
-
throw new ConnectionError(`Failed after ${
|
|
590
|
+
throw new ConnectionError(`Failed after ${retries} attempts: ${lastError?.message}`, lastError);
|
|
579
591
|
}
|
|
580
592
|
configure(updates) {
|
|
581
593
|
return new HTTPClient({
|
|
@@ -1048,10 +1060,10 @@ class ODBLiteClient {
|
|
|
1048
1060
|
};
|
|
1049
1061
|
sqlFunction.raw = (text)=>sql_parser_SQLParser.raw(text);
|
|
1050
1062
|
sqlFunction.identifier = (name)=>sql_parser_SQLParser.identifier(name);
|
|
1051
|
-
sqlFunction.query = async (sql, params = [])=>await this.httpClient.query(sql, params);
|
|
1052
|
-
sqlFunction.execute = async (sql, args)=>{
|
|
1053
|
-
if ('string' == typeof sql) return await this.httpClient.query(sql, args || []);
|
|
1054
|
-
return await this.httpClient.query(sql.sql, sql.args || []);
|
|
1063
|
+
sqlFunction.query = async (sql, params = [], opts)=>await this.httpClient.query(sql, params, opts);
|
|
1064
|
+
sqlFunction.execute = async (sql, args, opts)=>{
|
|
1065
|
+
if ('string' == typeof sql) return await this.httpClient.query(sql, args || [], opts);
|
|
1066
|
+
return await this.httpClient.query(sql.sql, sql.args || [], opts);
|
|
1055
1067
|
};
|
|
1056
1068
|
sqlFunction.batch = async (statements)=>await this.httpClient.batch(statements);
|
|
1057
1069
|
sqlFunction.begin = async (modeOrCallback, callback)=>{
|
|
@@ -1606,6 +1618,19 @@ class BunSQLiteConnection {
|
|
|
1606
1618
|
async begin(fn) {
|
|
1607
1619
|
return this.transaction(fn);
|
|
1608
1620
|
}
|
|
1621
|
+
async savepoint(fn) {
|
|
1622
|
+
if (!this.inTransaction) return this.transaction(fn);
|
|
1623
|
+
const name = `sp_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
|
|
1624
|
+
try {
|
|
1625
|
+
await this.execute(`SAVEPOINT ${name}`);
|
|
1626
|
+
const result = await fn(this);
|
|
1627
|
+
await this.execute(`RELEASE SAVEPOINT ${name}`);
|
|
1628
|
+
return result;
|
|
1629
|
+
} catch (error) {
|
|
1630
|
+
await this.execute(`ROLLBACK TO SAVEPOINT ${name}`).catch(()=>{});
|
|
1631
|
+
throw error;
|
|
1632
|
+
}
|
|
1633
|
+
}
|
|
1609
1634
|
async close() {
|
|
1610
1635
|
this.db.close();
|
|
1611
1636
|
}
|
|
@@ -1761,12 +1786,44 @@ class LibSQLConnection {
|
|
|
1761
1786
|
async begin(fn) {
|
|
1762
1787
|
return this.transaction(fn);
|
|
1763
1788
|
}
|
|
1789
|
+
async savepoint(fn) {
|
|
1790
|
+
if (!this.txClient) return this.transaction(fn);
|
|
1791
|
+
const name = `sp_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
|
|
1792
|
+
try {
|
|
1793
|
+
await this.execute(`SAVEPOINT ${name}`);
|
|
1794
|
+
const result = await fn(this);
|
|
1795
|
+
await this.execute(`RELEASE SAVEPOINT ${name}`);
|
|
1796
|
+
return result;
|
|
1797
|
+
} catch (error) {
|
|
1798
|
+
await this.execute(`ROLLBACK TO SAVEPOINT ${name}`).catch(()=>{});
|
|
1799
|
+
throw error;
|
|
1800
|
+
}
|
|
1801
|
+
}
|
|
1764
1802
|
async close() {}
|
|
1765
1803
|
createORM() {
|
|
1766
1804
|
const { createORM } = __webpack_require__("./src/orm/index.ts");
|
|
1767
1805
|
return createORM(this);
|
|
1768
1806
|
}
|
|
1769
1807
|
}
|
|
1808
|
+
let AsyncLocalStorageCtor = null;
|
|
1809
|
+
try {
|
|
1810
|
+
AsyncLocalStorageCtor = __webpack_require__("node:async_hooks").AsyncLocalStorage;
|
|
1811
|
+
} catch {
|
|
1812
|
+
try {
|
|
1813
|
+
AsyncLocalStorageCtor = __webpack_require__("async_hooks").AsyncLocalStorage;
|
|
1814
|
+
} catch {
|
|
1815
|
+
AsyncLocalStorageCtor = null;
|
|
1816
|
+
}
|
|
1817
|
+
}
|
|
1818
|
+
let txCounter = 0;
|
|
1819
|
+
function newTxId() {
|
|
1820
|
+
try {
|
|
1821
|
+
const c = globalThis.crypto;
|
|
1822
|
+
if (c?.randomUUID) return `tx_${c.randomUUID()}`;
|
|
1823
|
+
} catch {}
|
|
1824
|
+
txCounter = (txCounter + 1) % Number.MAX_SAFE_INTEGER;
|
|
1825
|
+
return `tx_${Date.now().toString(36)}_${txCounter.toString(36)}`;
|
|
1826
|
+
}
|
|
1770
1827
|
function odblite_parseJsonColumns(rows, jsonColumns) {
|
|
1771
1828
|
if (!jsonColumns || 0 === jsonColumns.length) return rows;
|
|
1772
1829
|
return rows.map((row)=>{
|
|
@@ -1841,7 +1898,8 @@ class ODBLiteAdapter {
|
|
|
1841
1898
|
class ODBLiteConnection {
|
|
1842
1899
|
client;
|
|
1843
1900
|
serviceClient;
|
|
1844
|
-
|
|
1901
|
+
txStorage = AsyncLocalStorageCtor ? new AsyncLocalStorageCtor() : null;
|
|
1902
|
+
fallbackTxId;
|
|
1845
1903
|
databaseName;
|
|
1846
1904
|
databaseHash;
|
|
1847
1905
|
sql;
|
|
@@ -1876,11 +1934,22 @@ class ODBLiteConnection {
|
|
|
1876
1934
|
async query(sql, params = [], options) {
|
|
1877
1935
|
return this.execute(sql, params, options);
|
|
1878
1936
|
}
|
|
1937
|
+
activeTxId() {
|
|
1938
|
+
return this.txStorage ? this.txStorage.getStore()?.txId : this.fallbackTxId;
|
|
1939
|
+
}
|
|
1940
|
+
requestOpts() {
|
|
1941
|
+
const txId = this.activeTxId();
|
|
1942
|
+
return txId ? {
|
|
1943
|
+
txId,
|
|
1944
|
+
maxRetries: 1
|
|
1945
|
+
} : void 0;
|
|
1946
|
+
}
|
|
1879
1947
|
async execute(sql, params = [], options) {
|
|
1880
1948
|
try {
|
|
1881
1949
|
let rows;
|
|
1882
1950
|
let rowsAffected;
|
|
1883
1951
|
let lastInsertRowid;
|
|
1952
|
+
const reqOpts = this.requestOpts();
|
|
1884
1953
|
if ('object' == typeof sql) {
|
|
1885
1954
|
if (process.env.DEBUG_SQL) {
|
|
1886
1955
|
console.log('[ODBLite] Executing SQL:', sql.sql);
|
|
@@ -1888,7 +1957,7 @@ class ODBLiteConnection {
|
|
|
1888
1957
|
}
|
|
1889
1958
|
let args = sql.args || [];
|
|
1890
1959
|
if (options?.stringifyParams) args = odblite_stringifyJsonParams(args, options.stringifyParams);
|
|
1891
|
-
const result = await this.client.sql.execute(sql.sql, args);
|
|
1960
|
+
const result = await this.client.sql.execute(sql.sql, args, reqOpts);
|
|
1892
1961
|
rows = result.rows;
|
|
1893
1962
|
rowsAffected = result.rowsAffected || 0;
|
|
1894
1963
|
lastInsertRowid = result.lastInsertRowid;
|
|
@@ -1899,7 +1968,7 @@ class ODBLiteConnection {
|
|
|
1899
1968
|
}
|
|
1900
1969
|
let args = params;
|
|
1901
1970
|
if (options?.stringifyParams) args = odblite_stringifyJsonParams(args, options.stringifyParams);
|
|
1902
|
-
const result = await this.client.sql.execute(sql, args);
|
|
1971
|
+
const result = await this.client.sql.execute(sql, args, reqOpts);
|
|
1903
1972
|
rows = result.rows;
|
|
1904
1973
|
rowsAffected = result.rowsAffected || 0;
|
|
1905
1974
|
lastInsertRowid = result.lastInsertRowid;
|
|
@@ -1928,36 +1997,45 @@ class ODBLiteConnection {
|
|
|
1928
1997
|
};
|
|
1929
1998
|
}
|
|
1930
1999
|
async transaction(fn) {
|
|
1931
|
-
if (this.
|
|
1932
|
-
|
|
2000
|
+
if (this.activeTxId()) return fn(this);
|
|
2001
|
+
const txId = newTxId();
|
|
2002
|
+
const run = async ()=>{
|
|
2003
|
+
const prevFallback = this.fallbackTxId;
|
|
2004
|
+
if (!this.txStorage) this.fallbackTxId = txId;
|
|
1933
2005
|
try {
|
|
1934
|
-
await this.execute(
|
|
1935
|
-
|
|
1936
|
-
|
|
1937
|
-
|
|
1938
|
-
|
|
1939
|
-
|
|
1940
|
-
|
|
1941
|
-
|
|
1942
|
-
|
|
1943
|
-
|
|
1944
|
-
|
|
1945
|
-
await this.execute('BEGIN');
|
|
1946
|
-
try {
|
|
1947
|
-
const result = await fn(this);
|
|
1948
|
-
await this.execute('COMMIT');
|
|
1949
|
-
return result;
|
|
1950
|
-
} catch (error) {
|
|
1951
|
-
await this.execute('ROLLBACK').catch(()=>{});
|
|
1952
|
-
throw error;
|
|
2006
|
+
await this.execute('BEGIN');
|
|
2007
|
+
try {
|
|
2008
|
+
const result = await fn(this);
|
|
2009
|
+
await this.execute('COMMIT');
|
|
2010
|
+
return result;
|
|
2011
|
+
} catch (error) {
|
|
2012
|
+
await this.execute('ROLLBACK').catch(()=>{});
|
|
2013
|
+
throw error;
|
|
2014
|
+
}
|
|
2015
|
+
} finally{
|
|
2016
|
+
if (!this.txStorage) this.fallbackTxId = prevFallback;
|
|
1953
2017
|
}
|
|
1954
|
-
}
|
|
1955
|
-
|
|
1956
|
-
|
|
2018
|
+
};
|
|
2019
|
+
return this.txStorage ? this.txStorage.run({
|
|
2020
|
+
txId
|
|
2021
|
+
}, run) : run();
|
|
1957
2022
|
}
|
|
1958
2023
|
async begin(fn) {
|
|
1959
2024
|
return this.transaction(fn);
|
|
1960
2025
|
}
|
|
2026
|
+
async savepoint(fn) {
|
|
2027
|
+
if (!this.activeTxId()) return this.transaction(fn);
|
|
2028
|
+
const name = `sp_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
|
|
2029
|
+
try {
|
|
2030
|
+
await this.execute(`SAVEPOINT ${name}`);
|
|
2031
|
+
const result = await fn(this);
|
|
2032
|
+
await this.execute(`RELEASE SAVEPOINT ${name}`);
|
|
2033
|
+
return result;
|
|
2034
|
+
} catch (error) {
|
|
2035
|
+
await this.execute(`ROLLBACK TO SAVEPOINT ${name}`).catch(()=>{});
|
|
2036
|
+
throw error;
|
|
2037
|
+
}
|
|
2038
|
+
}
|
|
1961
2039
|
async close() {}
|
|
1962
2040
|
createORM() {
|
|
1963
2041
|
const { createORM } = __webpack_require__("./src/orm/index.ts");
|
|
@@ -1995,6 +2073,8 @@ function splitStatements(sqlContent) {
|
|
|
1995
2073
|
let inQuote = false;
|
|
1996
2074
|
let quoteChar = '';
|
|
1997
2075
|
let beginEndDepth = 0;
|
|
2076
|
+
let inCte = false;
|
|
2077
|
+
let cteParenDepth = 0;
|
|
1998
2078
|
const lines = sqlContent.split('\n');
|
|
1999
2079
|
for (const line of lines){
|
|
2000
2080
|
let processedLine = '';
|
|
@@ -2008,6 +2088,15 @@ function splitStatements(sqlContent) {
|
|
|
2008
2088
|
if (beginEndDepth < 0) beginEndDepth = 0;
|
|
2009
2089
|
}
|
|
2010
2090
|
}
|
|
2091
|
+
if (!inQuote && lineWithoutComments.trim()) {
|
|
2092
|
+
const withMatch = lineWithoutComments.match(/\bWITH\b.*\bAS\b/i);
|
|
2093
|
+
if (withMatch) inCte = true;
|
|
2094
|
+
if (inCte) for (const char of lineWithoutComments){
|
|
2095
|
+
if ('(' === char) cteParenDepth++;
|
|
2096
|
+
if (')' === char) cteParenDepth--;
|
|
2097
|
+
if (0 === cteParenDepth && ')' === char) inCte = false;
|
|
2098
|
+
}
|
|
2099
|
+
}
|
|
2011
2100
|
for(let i = 0; i < line.length; i++){
|
|
2012
2101
|
const char = line[i];
|
|
2013
2102
|
const prevChar = i > 0 ? line[i - 1] : '';
|
|
@@ -2023,7 +2112,7 @@ function splitStatements(sqlContent) {
|
|
|
2023
2112
|
quoteChar = char;
|
|
2024
2113
|
}
|
|
2025
2114
|
processedLine += char;
|
|
2026
|
-
if (';' === char && !inQuote && 0 === beginEndDepth) {
|
|
2115
|
+
if (';' === char && !inQuote && 0 === beginEndDepth && !inCte) {
|
|
2027
2116
|
currentStatement += processedLine;
|
|
2028
2117
|
const stmt = currentStatement.trim();
|
|
2029
2118
|
if (stmt && !stmt.startsWith('--')) statements.push(stmt);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pineliner/odb-client",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.5",
|
|
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",
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
"deploy": "bun run build && npm publish --access public"
|
|
15
15
|
},
|
|
16
16
|
"dependencies": {
|
|
17
|
-
"@libsql/client": "^0.17.
|
|
17
|
+
"@libsql/client": "^0.17.3",
|
|
18
18
|
"node-sql-parser": "^5.3.12"
|
|
19
19
|
},
|
|
20
20
|
"devDependencies": {
|
package/src/core/client.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { ODBLiteConfig, ODBLiteConnection, QueryResult, Transaction, TransactionCallback, TransactionMode, SQLFragment } from '../types.ts';
|
|
2
|
-
import { HTTPClient } from './http-client.ts';
|
|
2
|
+
import { HTTPClient, type QueryRequestOptions } from './http-client.ts';
|
|
3
3
|
import { SQLParser } from './sql-parser.ts';
|
|
4
4
|
import { createBatchTransaction, createSimpleTransaction, SimpleTransaction } from './transaction.ts';
|
|
5
5
|
import { ConnectionError } from '../types.ts';
|
|
@@ -22,10 +22,10 @@ interface SQLFunction {
|
|
|
22
22
|
identifier(name: string): string;
|
|
23
23
|
|
|
24
24
|
// Additional postgres.js-like methods
|
|
25
|
-
query<T = any>(sql: string, params?: any[]): Promise<QueryResult<T>>;
|
|
25
|
+
query<T = any>(sql: string, params?: any[], opts?: QueryRequestOptions): Promise<QueryResult<T>>;
|
|
26
26
|
|
|
27
27
|
// libsql-compatible execute method (for backward compatibility)
|
|
28
|
-
execute<T = any>(sql: string | { sql: string; args?: any[] }, args?: any[]): Promise<QueryResult<T>>;
|
|
28
|
+
execute<T = any>(sql: string | { sql: string; args?: any[] }, args?: any[], opts?: QueryRequestOptions): Promise<QueryResult<T>>;
|
|
29
29
|
|
|
30
30
|
// Batch execution - runs multiple statements on same connection (for transactions)
|
|
31
31
|
batch<T = any>(statements: Array<{ sql: string; params?: any[] }>): Promise<BatchResult<T>>;
|
|
@@ -59,6 +59,12 @@ export class ODBLiteClient implements ODBLiteConnection {
|
|
|
59
59
|
// Handle template string queries (returns Promise)
|
|
60
60
|
if (Array.isArray(sql) && (('raw' in sql) || (typeof sql[0] === 'string' && values.length >= 0))) {
|
|
61
61
|
const parsed = SQLParser.parse(sql, values);
|
|
62
|
+
// NOTE: this raw template path sends NO transaction token. The router pins
|
|
63
|
+
// a transaction's statements by txId, so do NOT use `client.sql`...`` (or
|
|
64
|
+
// ODBLiteClient.query) for statements inside a router-pinned transaction —
|
|
65
|
+
// they'd be sent tokenless and treated as foreign work (deadlock-until-
|
|
66
|
+
// watchdog). The DB adapter (ODBLiteConnection) goes through sql.execute()
|
|
67
|
+
// with opts, which carries the token; use the connection, not the raw client.
|
|
62
68
|
return this.httpClient.query<T>(parsed.sql, parsed.params);
|
|
63
69
|
}
|
|
64
70
|
|
|
@@ -75,16 +81,16 @@ export class ODBLiteClient implements ODBLiteConnection {
|
|
|
75
81
|
sqlFunction.identifier = (name: string): string => SQLParser.identifier(name);
|
|
76
82
|
|
|
77
83
|
// Attach client methods to the function
|
|
78
|
-
sqlFunction.query = async <T = any>(sql: string, params: any[] = []): Promise<QueryResult<T>> => {
|
|
79
|
-
return await this.httpClient.query<T>(sql, params);
|
|
84
|
+
sqlFunction.query = async <T = any>(sql: string, params: any[] = [], opts?: QueryRequestOptions): Promise<QueryResult<T>> => {
|
|
85
|
+
return await this.httpClient.query<T>(sql, params, opts);
|
|
80
86
|
};
|
|
81
87
|
|
|
82
88
|
// libsql-compatible execute method (for backward compatibility)
|
|
83
|
-
sqlFunction.execute = async <T = any>(sql: string | { sql: string; args?: any[] }, args?: any[]): Promise<QueryResult<T>> => {
|
|
89
|
+
sqlFunction.execute = async <T = any>(sql: string | { sql: string; args?: any[] }, args?: any[], opts?: QueryRequestOptions): Promise<QueryResult<T>> => {
|
|
84
90
|
if (typeof sql === 'string') {
|
|
85
|
-
return await this.httpClient.query<T>(sql, args || []);
|
|
91
|
+
return await this.httpClient.query<T>(sql, args || [], opts);
|
|
86
92
|
} else {
|
|
87
|
-
return await this.httpClient.query<T>(sql.sql, sql.args || []);
|
|
93
|
+
return await this.httpClient.query<T>(sql.sql, sql.args || [], opts);
|
|
88
94
|
}
|
|
89
95
|
};
|
|
90
96
|
|
package/src/core/http-client.ts
CHANGED
|
@@ -1,6 +1,14 @@
|
|
|
1
1
|
import type { ODBLiteConfig, ODBLiteResponse, QueryResult } from '../types.ts'
|
|
2
2
|
import { ConnectionError, QueryError } from '../types.ts'
|
|
3
3
|
|
|
4
|
+
/** Per-request options for a single query. */
|
|
5
|
+
export interface QueryRequestOptions {
|
|
6
|
+
/** Transaction/session token tying this statement to a server-pinned connection. */
|
|
7
|
+
txId?: string
|
|
8
|
+
/** Override the client's retry count (transaction statements pass 1 = no retry). */
|
|
9
|
+
maxRetries?: number
|
|
10
|
+
}
|
|
11
|
+
|
|
4
12
|
/**
|
|
5
13
|
* HTTP client for communicating with ODBLite service
|
|
6
14
|
*/
|
|
@@ -23,7 +31,7 @@ export class HTTPClient {
|
|
|
23
31
|
/**
|
|
24
32
|
* Execute a query against the ODBLite service
|
|
25
33
|
*/
|
|
26
|
-
async query<T = any>(sql: string, params: any[] = []): Promise<QueryResult<T>> {
|
|
34
|
+
async query<T = any>(sql: string, params: any[] = [], opts?: QueryRequestOptions): Promise<QueryResult<T>> {
|
|
27
35
|
if (!this.config.databaseId) {
|
|
28
36
|
throw new ConnectionError('No database ID configured. Use setDatabase() first.')
|
|
29
37
|
}
|
|
@@ -31,7 +39,10 @@ export class HTTPClient {
|
|
|
31
39
|
const url = `${this.config.baseUrl}/query/${this.config.databaseId}`
|
|
32
40
|
const body = {
|
|
33
41
|
sql,
|
|
34
|
-
params
|
|
42
|
+
params,
|
|
43
|
+
// Transaction/session token — present for every statement inside a
|
|
44
|
+
// connection.transaction() so the router pins them to one connection.
|
|
45
|
+
...(opts?.txId ? { txId: opts.txId } : {})
|
|
35
46
|
}
|
|
36
47
|
|
|
37
48
|
try {
|
|
@@ -42,7 +53,7 @@ export class HTTPClient {
|
|
|
42
53
|
Authorization: `Bearer ${this.config.apiKey}`
|
|
43
54
|
},
|
|
44
55
|
body: JSON.stringify(body)
|
|
45
|
-
})
|
|
56
|
+
}, opts?.maxRetries)
|
|
46
57
|
|
|
47
58
|
const data: ODBLiteResponse<T> = await response.json()
|
|
48
59
|
|
|
@@ -186,10 +197,14 @@ export class HTTPClient {
|
|
|
186
197
|
/**
|
|
187
198
|
* Make HTTP request with retry logic
|
|
188
199
|
*/
|
|
189
|
-
private async makeRequest(url: string, options: RequestInit): Promise<Response> {
|
|
200
|
+
private async makeRequest(url: string, options: RequestInit, maxRetries?: number): Promise<Response> {
|
|
190
201
|
let lastError: Error | undefined
|
|
202
|
+
// Transaction-control and in-transaction statements pass maxRetries=1: they
|
|
203
|
+
// are NOT idempotent (a retried COMMIT/INSERT could double-apply or hit a
|
|
204
|
+
// closed transaction), so they must never be auto-retried.
|
|
205
|
+
const retries = maxRetries ?? this.config.retries ?? 3
|
|
191
206
|
|
|
192
|
-
for (let attempt = 1; attempt <=
|
|
207
|
+
for (let attempt = 1; attempt <= retries; attempt++) {
|
|
193
208
|
try {
|
|
194
209
|
const controller = new AbortController()
|
|
195
210
|
const timeoutId = setTimeout(() => controller.abort(), this.config.timeout)
|
|
@@ -224,14 +239,14 @@ export class HTTPClient {
|
|
|
224
239
|
}
|
|
225
240
|
|
|
226
241
|
// Wait before retry (exponential backoff)
|
|
227
|
-
if (attempt <
|
|
242
|
+
if (attempt < retries) {
|
|
228
243
|
const delay = Math.min(1000 * 2 ** (attempt - 1), 10000)
|
|
229
244
|
await new Promise((resolve) => setTimeout(resolve, delay))
|
|
230
245
|
}
|
|
231
246
|
}
|
|
232
247
|
}
|
|
233
248
|
|
|
234
|
-
throw new ConnectionError(`Failed after ${
|
|
249
|
+
throw new ConnectionError(`Failed after ${retries} attempts: ${lastError?.message}`, lastError)
|
|
235
250
|
}
|
|
236
251
|
|
|
237
252
|
/**
|
|
@@ -292,6 +292,27 @@ class BunSQLiteConnection implements Connection {
|
|
|
292
292
|
return this.transaction(fn)
|
|
293
293
|
}
|
|
294
294
|
|
|
295
|
+
/**
|
|
296
|
+
* Execute function within an explicit savepoint for partial rollback isolation.
|
|
297
|
+
* If called outside a transaction, behaves like transaction().
|
|
298
|
+
*/
|
|
299
|
+
async savepoint<T>(fn: (tx: Connection) => Promise<T>): Promise<T> {
|
|
300
|
+
if (!this.inTransaction) {
|
|
301
|
+
return this.transaction(fn)
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
const name = `sp_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`
|
|
305
|
+
try {
|
|
306
|
+
await this.execute(`SAVEPOINT ${name}`)
|
|
307
|
+
const result = await fn(this)
|
|
308
|
+
await this.execute(`RELEASE SAVEPOINT ${name}`)
|
|
309
|
+
return result
|
|
310
|
+
} catch (error) {
|
|
311
|
+
await this.execute(`ROLLBACK TO SAVEPOINT ${name}`).catch(() => {})
|
|
312
|
+
throw error
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
295
316
|
/**
|
|
296
317
|
* Close connection
|
|
297
318
|
*/
|
|
@@ -270,6 +270,27 @@ class LibSQLConnection implements Connection {
|
|
|
270
270
|
return this.transaction(fn)
|
|
271
271
|
}
|
|
272
272
|
|
|
273
|
+
/**
|
|
274
|
+
* Execute function within an explicit savepoint for partial rollback isolation.
|
|
275
|
+
* If called outside a transaction, behaves like transaction().
|
|
276
|
+
*/
|
|
277
|
+
async savepoint<T>(fn: (tx: Connection) => Promise<T>): Promise<T> {
|
|
278
|
+
if (!this.txClient) {
|
|
279
|
+
return this.transaction(fn)
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
const name = `sp_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`
|
|
283
|
+
try {
|
|
284
|
+
await this.execute(`SAVEPOINT ${name}`)
|
|
285
|
+
const result = await fn(this)
|
|
286
|
+
await this.execute(`RELEASE SAVEPOINT ${name}`)
|
|
287
|
+
return result
|
|
288
|
+
} catch (error) {
|
|
289
|
+
await this.execute(`ROLLBACK TO SAVEPOINT ${name}`).catch(() => {})
|
|
290
|
+
throw error
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
273
294
|
/**
|
|
274
295
|
* Close connection
|
|
275
296
|
*/
|
|
@@ -19,6 +19,45 @@ import {
|
|
|
19
19
|
where
|
|
20
20
|
} from '../sql-template'
|
|
21
21
|
|
|
22
|
+
/**
|
|
23
|
+
* AsyncLocalStorage, loaded lazily so the client stays isomorphic. On the server
|
|
24
|
+
* (Node/Bun) it carries the active transaction token across awaits so every
|
|
25
|
+
* statement inside a transaction is tagged with — and only with — its own token,
|
|
26
|
+
* even when other transactions run concurrently on the same shared connection.
|
|
27
|
+
* In browsers/edge (no async_hooks) it's null and we fall back to a per-connection
|
|
28
|
+
* flag, which is sufficient there because that concurrency pattern doesn't occur.
|
|
29
|
+
*/
|
|
30
|
+
type TxStore = { txId: string }
|
|
31
|
+
let AsyncLocalStorageCtor: (new () => AsyncLocalStorageLike<TxStore>) | null = null
|
|
32
|
+
interface AsyncLocalStorageLike<T> {
|
|
33
|
+
getStore(): T | undefined
|
|
34
|
+
run<R>(store: T, fn: () => R): R
|
|
35
|
+
}
|
|
36
|
+
try {
|
|
37
|
+
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
38
|
+
AsyncLocalStorageCtor = require('node:async_hooks').AsyncLocalStorage
|
|
39
|
+
} catch {
|
|
40
|
+
try {
|
|
41
|
+
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
42
|
+
AsyncLocalStorageCtor = require('async_hooks').AsyncLocalStorage
|
|
43
|
+
} catch {
|
|
44
|
+
AsyncLocalStorageCtor = null
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
let txCounter = 0
|
|
49
|
+
/** Generate a process-unique transaction token. */
|
|
50
|
+
function newTxId(): string {
|
|
51
|
+
try {
|
|
52
|
+
const c = (globalThis as any).crypto
|
|
53
|
+
if (c?.randomUUID) return `tx_${c.randomUUID()}`
|
|
54
|
+
} catch {
|
|
55
|
+
// fall through
|
|
56
|
+
}
|
|
57
|
+
txCounter = (txCounter + 1) % Number.MAX_SAFE_INTEGER
|
|
58
|
+
return `tx_${Date.now().toString(36)}_${txCounter.toString(36)}`
|
|
59
|
+
}
|
|
60
|
+
|
|
22
61
|
/**
|
|
23
62
|
* Parse JSON columns in query results
|
|
24
63
|
* Only parses if the value is a string (to avoid double-parsing)
|
|
@@ -139,7 +178,14 @@ export class ODBLiteAdapter implements DatabaseAdapter {
|
|
|
139
178
|
class ODBLiteConnection implements Connection {
|
|
140
179
|
private client: ODBLiteClient
|
|
141
180
|
private serviceClient: ServiceClient
|
|
142
|
-
|
|
181
|
+
// Carries the active transaction token across awaits (server only). Distinguishes
|
|
182
|
+
// a genuinely-nested transaction() call (same async context) from a concurrent
|
|
183
|
+
// top-level one on this shared connection — replacing the old shared
|
|
184
|
+
// `inTransaction` boolean, which leaked between concurrent requests.
|
|
185
|
+
private txStorage: AsyncLocalStorageLike<TxStore> | null =
|
|
186
|
+
AsyncLocalStorageCtor ? new AsyncLocalStorageCtor() : null
|
|
187
|
+
// Browser/edge fallback when AsyncLocalStorage is unavailable.
|
|
188
|
+
private fallbackTxId?: string
|
|
143
189
|
|
|
144
190
|
// Public metadata fields
|
|
145
191
|
public databaseName: string
|
|
@@ -209,6 +255,23 @@ class ODBLiteConnection implements Connection {
|
|
|
209
255
|
return this.execute(sql, params, options) as Promise<QueryResult<T>>
|
|
210
256
|
}
|
|
211
257
|
|
|
258
|
+
/** The token of the transaction this async context belongs to, if any. */
|
|
259
|
+
private activeTxId(): string | undefined {
|
|
260
|
+
return this.txStorage ? this.txStorage.getStore()?.txId : this.fallbackTxId
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* Per-request options for the underlying client. Inside a transaction every
|
|
265
|
+
* statement carries the transaction token (so the router pins it to the
|
|
266
|
+
* transaction's connection) and disables retries (transaction statements are
|
|
267
|
+
* not idempotent — a retried COMMIT/INSERT could double-apply or hit a closed
|
|
268
|
+
* transaction).
|
|
269
|
+
*/
|
|
270
|
+
private requestOpts(): { txId?: string; maxRetries?: number } | undefined {
|
|
271
|
+
const txId = this.activeTxId()
|
|
272
|
+
return txId ? { txId, maxRetries: 1 } : undefined
|
|
273
|
+
}
|
|
274
|
+
|
|
212
275
|
/**
|
|
213
276
|
* Execute SQL with parameters
|
|
214
277
|
*/
|
|
@@ -217,6 +280,7 @@ class ODBLiteConnection implements Connection {
|
|
|
217
280
|
let rows: any[]
|
|
218
281
|
let rowsAffected: number
|
|
219
282
|
let lastInsertRowid: any
|
|
283
|
+
const reqOpts = this.requestOpts()
|
|
220
284
|
|
|
221
285
|
// Handle object format { sql, args }
|
|
222
286
|
if (typeof sql === 'object') {
|
|
@@ -230,7 +294,7 @@ class ODBLiteConnection implements Connection {
|
|
|
230
294
|
if (options?.stringifyParams) {
|
|
231
295
|
args = stringifyJsonParams(args, options.stringifyParams)
|
|
232
296
|
}
|
|
233
|
-
const result = await this.client.sql.execute(sql.sql, args)
|
|
297
|
+
const result = await this.client.sql.execute(sql.sql, args, reqOpts)
|
|
234
298
|
rows = result.rows
|
|
235
299
|
rowsAffected = result.rowsAffected || 0
|
|
236
300
|
lastInsertRowid = (result as any).lastInsertRowid
|
|
@@ -245,7 +309,7 @@ class ODBLiteConnection implements Connection {
|
|
|
245
309
|
if (options?.stringifyParams) {
|
|
246
310
|
args = stringifyJsonParams(args, options.stringifyParams)
|
|
247
311
|
}
|
|
248
|
-
const result = await this.client.sql.execute(sql, args)
|
|
312
|
+
const result = await this.client.sql.execute(sql, args, reqOpts)
|
|
249
313
|
rows = result.rows
|
|
250
314
|
rowsAffected = result.rowsAffected || 0
|
|
251
315
|
lastInsertRowid = (result as any).lastInsertRowid
|
|
@@ -300,42 +364,42 @@ class ODBLiteConnection implements Connection {
|
|
|
300
364
|
* same connection without interleaving from other requests.
|
|
301
365
|
*/
|
|
302
366
|
async transaction<T>(fn: (tx: Connection) => Promise<T>): Promise<T> {
|
|
303
|
-
if (this.
|
|
304
|
-
//
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
await this.execute(`RELEASE SAVEPOINT ${savepointName}`)
|
|
310
|
-
return result
|
|
311
|
-
} catch (error) {
|
|
312
|
-
await this.execute(`ROLLBACK TO SAVEPOINT ${savepointName}`).catch(() => {})
|
|
313
|
-
throw error
|
|
314
|
-
}
|
|
367
|
+
if (this.activeTxId()) {
|
|
368
|
+
// Genuinely nested call: this async context already owns a transaction on
|
|
369
|
+
// this connection — join it (no nested BEGIN), exactly like the previous
|
|
370
|
+
// reentrancy behaviour. Convenience functions that each wrap in
|
|
371
|
+
// db.transaction() compose freely inside an outer transaction.
|
|
372
|
+
return fn(this)
|
|
315
373
|
}
|
|
316
374
|
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
await this.execute('BEGIN')
|
|
375
|
+
// New top-level transaction: mint a token so the router pins every one of
|
|
376
|
+
// this transaction's statements to a single connection and never lets a
|
|
377
|
+
// concurrent transaction's (or autocommit) statements interleave onto it.
|
|
378
|
+
const txId = newTxId()
|
|
322
379
|
|
|
380
|
+
const run = async (): Promise<T> => {
|
|
381
|
+
const prevFallback = this.fallbackTxId
|
|
382
|
+
if (!this.txStorage) this.fallbackTxId = txId
|
|
323
383
|
try {
|
|
324
|
-
//
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
384
|
+
// Start transaction (tagged with txId + no-retry via requestOpts())
|
|
385
|
+
await this.execute('BEGIN')
|
|
386
|
+
try {
|
|
387
|
+
// Execute user function - each statement executes immediately
|
|
388
|
+
const result = await fn(this)
|
|
389
|
+
// Commit transaction
|
|
390
|
+
await this.execute('COMMIT')
|
|
391
|
+
return result
|
|
392
|
+
} catch (error) {
|
|
393
|
+
// Rollback on error
|
|
394
|
+
await this.execute('ROLLBACK').catch(() => {})
|
|
395
|
+
throw error
|
|
396
|
+
}
|
|
397
|
+
} finally {
|
|
398
|
+
if (!this.txStorage) this.fallbackTxId = prevFallback
|
|
335
399
|
}
|
|
336
|
-
} finally {
|
|
337
|
-
this.inTransaction = false
|
|
338
400
|
}
|
|
401
|
+
|
|
402
|
+
return this.txStorage ? this.txStorage.run({ txId }, run) : run()
|
|
339
403
|
}
|
|
340
404
|
|
|
341
405
|
/**
|
|
@@ -345,6 +409,27 @@ class ODBLiteConnection implements Connection {
|
|
|
345
409
|
return this.transaction(fn)
|
|
346
410
|
}
|
|
347
411
|
|
|
412
|
+
/**
|
|
413
|
+
* Execute function within an explicit savepoint for partial rollback isolation.
|
|
414
|
+
* If called outside a transaction, behaves like transaction().
|
|
415
|
+
*/
|
|
416
|
+
async savepoint<T>(fn: (tx: Connection) => Promise<T>): Promise<T> {
|
|
417
|
+
if (!this.activeTxId()) {
|
|
418
|
+
return this.transaction(fn)
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
const name = `sp_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`
|
|
422
|
+
try {
|
|
423
|
+
await this.execute(`SAVEPOINT ${name}`)
|
|
424
|
+
const result = await fn(this)
|
|
425
|
+
await this.execute(`RELEASE SAVEPOINT ${name}`)
|
|
426
|
+
return result
|
|
427
|
+
} catch (error) {
|
|
428
|
+
await this.execute(`ROLLBACK TO SAVEPOINT ${name}`).catch(() => {})
|
|
429
|
+
throw error
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
|
|
348
433
|
/**
|
|
349
434
|
* Close connection
|
|
350
435
|
*/
|
|
@@ -20,7 +20,7 @@ export function parseSQL(sqlContent: string, options: SQLParserOptions = {}): Pa
|
|
|
20
20
|
if (!separatePragma) {
|
|
21
21
|
return {
|
|
22
22
|
pragmaStatements: [],
|
|
23
|
-
regularStatements: statements
|
|
23
|
+
regularStatements: statements
|
|
24
24
|
}
|
|
25
25
|
}
|
|
26
26
|
|
|
@@ -49,6 +49,8 @@ function splitStatements(sqlContent: string): string[] {
|
|
|
49
49
|
let inQuote = false
|
|
50
50
|
let quoteChar = ''
|
|
51
51
|
let beginEndDepth = 0
|
|
52
|
+
let inCte = false
|
|
53
|
+
let cteParenDepth = 0
|
|
52
54
|
|
|
53
55
|
const lines = sqlContent.split('\n')
|
|
54
56
|
|
|
@@ -72,6 +74,24 @@ function splitStatements(sqlContent: string): string[] {
|
|
|
72
74
|
}
|
|
73
75
|
}
|
|
74
76
|
|
|
77
|
+
// Track CTE (WITH ... AS) parenthesis depth
|
|
78
|
+
if (!inQuote && lineWithoutComments.trim()) {
|
|
79
|
+
const withMatch = lineWithoutComments.match(/\bWITH\b.*\bAS\b/i)
|
|
80
|
+
if (withMatch) {
|
|
81
|
+
inCte = true
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if (inCte) {
|
|
85
|
+
for (const char of lineWithoutComments) {
|
|
86
|
+
if (char === '(') cteParenDepth++
|
|
87
|
+
if (char === ')') cteParenDepth--
|
|
88
|
+
if (cteParenDepth === 0 && char === ')') {
|
|
89
|
+
inCte = false
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
75
95
|
for (let i = 0; i < line.length; i++) {
|
|
76
96
|
const char = line[i]
|
|
77
97
|
const prevChar = i > 0 ? line[i - 1] : ''
|
|
@@ -95,8 +115,8 @@ function splitStatements(sqlContent: string): string[] {
|
|
|
95
115
|
|
|
96
116
|
processedLine += char
|
|
97
117
|
|
|
98
|
-
// Split on semicolon when not in quotes AND not inside BEGIN...END
|
|
99
|
-
if (char === ';' && !inQuote && beginEndDepth === 0) {
|
|
118
|
+
// Split on semicolon when not in quotes AND not inside BEGIN...END AND not inside CTE
|
|
119
|
+
if (char === ';' && !inQuote && beginEndDepth === 0 && !inCte) {
|
|
100
120
|
currentStatement += processedLine
|
|
101
121
|
const stmt = currentStatement.trim()
|
|
102
122
|
if (stmt && !stmt.startsWith('--')) {
|
package/src/database/types.ts
CHANGED
|
@@ -77,6 +77,11 @@ export interface Connection {
|
|
|
77
77
|
transaction<T>(fn: (tx: Connection) => Promise<T>): Promise<T>
|
|
78
78
|
begin<T>(fn: (tx: Connection) => Promise<T>): Promise<T>
|
|
79
79
|
|
|
80
|
+
// Savepoint support — explicit partial rollback isolation within a transaction.
|
|
81
|
+
// Use when you need an inner operation to fail without rolling back the outer transaction.
|
|
82
|
+
// If called outside a transaction, behaves like transaction().
|
|
83
|
+
savepoint<T>(fn: (tx: Connection) => Promise<T>): Promise<T>
|
|
84
|
+
|
|
80
85
|
// Connection management
|
|
81
86
|
close(): Promise<void>
|
|
82
87
|
|