@pineliner/odb-client 1.1.4 → 1.2.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 +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-socket.d.ts +52 -0
- package/dist/database/adapters/odblite-socket.d.ts.map +1 -0
- package/dist/database/adapters/odblite.d.ts.map +1 -1
- package/dist/database/adapters/socket/client.d.ts +28 -0
- package/dist/database/adapters/socket/client.d.ts.map +1 -0
- package/dist/database/adapters/socket/protocol.d.ts +129 -0
- package/dist/database/adapters/socket/protocol.d.ts.map +1 -0
- package/dist/database/manager.d.ts.map +1 -1
- package/dist/database/sql-parser.d.ts.map +1 -1
- package/dist/database/types.d.ts +6 -1
- package/dist/database/types.d.ts.map +1 -1
- package/dist/index.cjs +513 -30
- package/dist/index.js +515 -30
- package/package.json +2 -2
- package/src/core/client.ts +14 -8
- package/src/core/http-client.ts +22 -7
- package/src/database/adapters/odblite-socket.ts +255 -0
- package/src/database/adapters/odblite.ts +97 -26
- package/src/database/adapters/socket/client.ts +78 -0
- package/src/database/adapters/socket/protocol.ts +147 -0
- package/src/database/manager.ts +19 -1
- package/src/database/sql-parser.ts +23 -3
- package/src/database/types.ts +8 -1
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)=>{
|
|
@@ -1793,6 +1805,25 @@ class LibSQLConnection {
|
|
|
1793
1805
|
return createORM(this);
|
|
1794
1806
|
}
|
|
1795
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
|
+
}
|
|
1796
1827
|
function odblite_parseJsonColumns(rows, jsonColumns) {
|
|
1797
1828
|
if (!jsonColumns || 0 === jsonColumns.length) return rows;
|
|
1798
1829
|
return rows.map((row)=>{
|
|
@@ -1867,7 +1898,8 @@ class ODBLiteAdapter {
|
|
|
1867
1898
|
class ODBLiteConnection {
|
|
1868
1899
|
client;
|
|
1869
1900
|
serviceClient;
|
|
1870
|
-
|
|
1901
|
+
txStorage = AsyncLocalStorageCtor ? new AsyncLocalStorageCtor() : null;
|
|
1902
|
+
fallbackTxId;
|
|
1871
1903
|
databaseName;
|
|
1872
1904
|
databaseHash;
|
|
1873
1905
|
sql;
|
|
@@ -1902,11 +1934,22 @@ class ODBLiteConnection {
|
|
|
1902
1934
|
async query(sql, params = [], options) {
|
|
1903
1935
|
return this.execute(sql, params, options);
|
|
1904
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
|
+
}
|
|
1905
1947
|
async execute(sql, params = [], options) {
|
|
1906
1948
|
try {
|
|
1907
1949
|
let rows;
|
|
1908
1950
|
let rowsAffected;
|
|
1909
1951
|
let lastInsertRowid;
|
|
1952
|
+
const reqOpts = this.requestOpts();
|
|
1910
1953
|
if ('object' == typeof sql) {
|
|
1911
1954
|
if (process.env.DEBUG_SQL) {
|
|
1912
1955
|
console.log('[ODBLite] Executing SQL:', sql.sql);
|
|
@@ -1914,7 +1957,7 @@ class ODBLiteConnection {
|
|
|
1914
1957
|
}
|
|
1915
1958
|
let args = sql.args || [];
|
|
1916
1959
|
if (options?.stringifyParams) args = odblite_stringifyJsonParams(args, options.stringifyParams);
|
|
1917
|
-
const result = await this.client.sql.execute(sql.sql, args);
|
|
1960
|
+
const result = await this.client.sql.execute(sql.sql, args, reqOpts);
|
|
1918
1961
|
rows = result.rows;
|
|
1919
1962
|
rowsAffected = result.rowsAffected || 0;
|
|
1920
1963
|
lastInsertRowid = result.lastInsertRowid;
|
|
@@ -1925,7 +1968,7 @@ class ODBLiteConnection {
|
|
|
1925
1968
|
}
|
|
1926
1969
|
let args = params;
|
|
1927
1970
|
if (options?.stringifyParams) args = odblite_stringifyJsonParams(args, options.stringifyParams);
|
|
1928
|
-
const result = await this.client.sql.execute(sql, args);
|
|
1971
|
+
const result = await this.client.sql.execute(sql, args, reqOpts);
|
|
1929
1972
|
rows = result.rows;
|
|
1930
1973
|
rowsAffected = result.rowsAffected || 0;
|
|
1931
1974
|
lastInsertRowid = result.lastInsertRowid;
|
|
@@ -1954,27 +1997,34 @@ class ODBLiteConnection {
|
|
|
1954
1997
|
};
|
|
1955
1998
|
}
|
|
1956
1999
|
async transaction(fn) {
|
|
1957
|
-
if (this.
|
|
1958
|
-
|
|
1959
|
-
|
|
1960
|
-
|
|
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;
|
|
1961
2005
|
try {
|
|
1962
|
-
|
|
1963
|
-
|
|
1964
|
-
|
|
1965
|
-
|
|
1966
|
-
|
|
1967
|
-
|
|
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;
|
|
1968
2017
|
}
|
|
1969
|
-
}
|
|
1970
|
-
|
|
1971
|
-
|
|
2018
|
+
};
|
|
2019
|
+
return this.txStorage ? this.txStorage.run({
|
|
2020
|
+
txId
|
|
2021
|
+
}, run) : run();
|
|
1972
2022
|
}
|
|
1973
2023
|
async begin(fn) {
|
|
1974
2024
|
return this.transaction(fn);
|
|
1975
2025
|
}
|
|
1976
2026
|
async savepoint(fn) {
|
|
1977
|
-
if (!this.
|
|
2027
|
+
if (!this.activeTxId()) return this.transaction(fn);
|
|
1978
2028
|
const name = `sp_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
|
|
1979
2029
|
try {
|
|
1980
2030
|
await this.execute(`SAVEPOINT ${name}`);
|
|
@@ -1998,6 +2048,417 @@ class ODBLiteConnection {
|
|
|
1998
2048
|
return this.client.httpClient.getNamespaces();
|
|
1999
2049
|
}
|
|
2000
2050
|
}
|
|
2051
|
+
function toHrana(v) {
|
|
2052
|
+
if (null == v) return {
|
|
2053
|
+
type: 'null'
|
|
2054
|
+
};
|
|
2055
|
+
if ('bigint' == typeof v) return {
|
|
2056
|
+
type: 'integer',
|
|
2057
|
+
value: v.toString()
|
|
2058
|
+
};
|
|
2059
|
+
if ('number' == typeof v) return Number.isInteger(v) ? {
|
|
2060
|
+
type: 'integer',
|
|
2061
|
+
value: String(v)
|
|
2062
|
+
} : {
|
|
2063
|
+
type: 'float',
|
|
2064
|
+
value: v
|
|
2065
|
+
};
|
|
2066
|
+
if ('string' == typeof v) return {
|
|
2067
|
+
type: 'text',
|
|
2068
|
+
value: v
|
|
2069
|
+
};
|
|
2070
|
+
if (v instanceof Uint8Array) return {
|
|
2071
|
+
type: 'blob',
|
|
2072
|
+
base64: Buffer.from(v).toString('base64')
|
|
2073
|
+
};
|
|
2074
|
+
if ('boolean' == typeof v) return {
|
|
2075
|
+
type: 'integer',
|
|
2076
|
+
value: v ? '1' : '0'
|
|
2077
|
+
};
|
|
2078
|
+
return {
|
|
2079
|
+
type: 'text',
|
|
2080
|
+
value: String(v)
|
|
2081
|
+
};
|
|
2082
|
+
}
|
|
2083
|
+
function fromHrana(v) {
|
|
2084
|
+
switch(v.type){
|
|
2085
|
+
case 'null':
|
|
2086
|
+
return null;
|
|
2087
|
+
case 'integer':
|
|
2088
|
+
{
|
|
2089
|
+
const n = Number(v.value);
|
|
2090
|
+
return Number.isSafeInteger(n) ? n : BigInt(v.value);
|
|
2091
|
+
}
|
|
2092
|
+
case 'float':
|
|
2093
|
+
return v.value;
|
|
2094
|
+
case 'text':
|
|
2095
|
+
return v.value;
|
|
2096
|
+
case 'blob':
|
|
2097
|
+
return new Uint8Array(Buffer.from(v.base64, 'base64'));
|
|
2098
|
+
}
|
|
2099
|
+
}
|
|
2100
|
+
function toBatch(steps) {
|
|
2101
|
+
return {
|
|
2102
|
+
steps: steps.map((s, i)=>({
|
|
2103
|
+
condition: 0 === i ? void 0 : {
|
|
2104
|
+
type: 'ok',
|
|
2105
|
+
step: i - 1
|
|
2106
|
+
},
|
|
2107
|
+
stmt: {
|
|
2108
|
+
sql: s.sql,
|
|
2109
|
+
args: (s.params ?? []).map(toHrana)
|
|
2110
|
+
},
|
|
2111
|
+
expect: s.expect ? {
|
|
2112
|
+
affected: s.expect
|
|
2113
|
+
} : void 0
|
|
2114
|
+
}))
|
|
2115
|
+
};
|
|
2116
|
+
}
|
|
2117
|
+
class FrameWriter {
|
|
2118
|
+
socket;
|
|
2119
|
+
pending = Buffer.alloc(0);
|
|
2120
|
+
constructor(socket){
|
|
2121
|
+
this.socket = socket;
|
|
2122
|
+
}
|
|
2123
|
+
send(payload) {
|
|
2124
|
+
const header = Buffer.allocUnsafe(4);
|
|
2125
|
+
header.writeUInt32LE(payload.length, 0);
|
|
2126
|
+
const frame = Buffer.concat([
|
|
2127
|
+
header,
|
|
2128
|
+
payload
|
|
2129
|
+
]);
|
|
2130
|
+
this.pending = this.pending.length ? Buffer.concat([
|
|
2131
|
+
this.pending,
|
|
2132
|
+
frame
|
|
2133
|
+
]) : frame;
|
|
2134
|
+
this.flush();
|
|
2135
|
+
}
|
|
2136
|
+
flush() {
|
|
2137
|
+
while(this.pending.length > 0){
|
|
2138
|
+
const n = this.socket.write(this.pending);
|
|
2139
|
+
if (n <= 0) return;
|
|
2140
|
+
this.pending = n < this.pending.length ? this.pending.subarray(n) : Buffer.alloc(0);
|
|
2141
|
+
}
|
|
2142
|
+
}
|
|
2143
|
+
}
|
|
2144
|
+
class FrameReader {
|
|
2145
|
+
buf = Buffer.alloc(0);
|
|
2146
|
+
push(chunk) {
|
|
2147
|
+
this.buf = this.buf.length ? Buffer.concat([
|
|
2148
|
+
this.buf,
|
|
2149
|
+
chunk
|
|
2150
|
+
]) : chunk;
|
|
2151
|
+
const out = [];
|
|
2152
|
+
while(this.buf.length >= 4){
|
|
2153
|
+
const len = this.buf.readUInt32LE(0);
|
|
2154
|
+
if (this.buf.length < 4 + len) break;
|
|
2155
|
+
out.push(this.buf.subarray(4, 4 + len));
|
|
2156
|
+
this.buf = this.buf.subarray(4 + len);
|
|
2157
|
+
}
|
|
2158
|
+
return out;
|
|
2159
|
+
}
|
|
2160
|
+
}
|
|
2161
|
+
class SocketClient {
|
|
2162
|
+
target;
|
|
2163
|
+
socket;
|
|
2164
|
+
reader = new FrameReader();
|
|
2165
|
+
writer;
|
|
2166
|
+
pending = new Map();
|
|
2167
|
+
nextId = 1;
|
|
2168
|
+
ready;
|
|
2169
|
+
constructor(target){
|
|
2170
|
+
this.target = target;
|
|
2171
|
+
this.ready = this.connect();
|
|
2172
|
+
this.ready.catch(()=>{});
|
|
2173
|
+
}
|
|
2174
|
+
async connect() {
|
|
2175
|
+
const Bun = globalThis.Bun;
|
|
2176
|
+
if (!Bun?.connect) throw new Error('odblite-socket backend requires Bun (Bun.connect)');
|
|
2177
|
+
const opts = this.target.unix ? {
|
|
2178
|
+
unix: this.target.unix
|
|
2179
|
+
} : {
|
|
2180
|
+
hostname: this.target.hostname ?? '127.0.0.1',
|
|
2181
|
+
port: this.target.port ?? 8787
|
|
2182
|
+
};
|
|
2183
|
+
this.socket = await Bun.connect({
|
|
2184
|
+
...opts,
|
|
2185
|
+
socket: {
|
|
2186
|
+
data: (_s, chunk)=>{
|
|
2187
|
+
for (const frame of this.reader.push(chunk)){
|
|
2188
|
+
const msg = JSON.parse(frame.toString('utf8'));
|
|
2189
|
+
const resolve = this.pending.get(msg.id);
|
|
2190
|
+
if (resolve) {
|
|
2191
|
+
this.pending.delete(msg.id);
|
|
2192
|
+
resolve(msg.results);
|
|
2193
|
+
}
|
|
2194
|
+
}
|
|
2195
|
+
},
|
|
2196
|
+
drain: (_s)=>this.writer.flush(),
|
|
2197
|
+
close: ()=>{
|
|
2198
|
+
this.socket = void 0;
|
|
2199
|
+
}
|
|
2200
|
+
}
|
|
2201
|
+
});
|
|
2202
|
+
this.writer = new FrameWriter(this.socket);
|
|
2203
|
+
}
|
|
2204
|
+
async pipeline(tenant, requests) {
|
|
2205
|
+
if (!this.socket) {
|
|
2206
|
+
this.ready = this.connect();
|
|
2207
|
+
this.ready.catch(()=>{});
|
|
2208
|
+
}
|
|
2209
|
+
await this.ready;
|
|
2210
|
+
const id = this.nextId++;
|
|
2211
|
+
return new Promise((resolve)=>{
|
|
2212
|
+
this.pending.set(id, resolve);
|
|
2213
|
+
this.writer.send(Buffer.from(JSON.stringify({
|
|
2214
|
+
id,
|
|
2215
|
+
tenant,
|
|
2216
|
+
requests
|
|
2217
|
+
}), 'utf8'));
|
|
2218
|
+
});
|
|
2219
|
+
}
|
|
2220
|
+
close() {
|
|
2221
|
+
this.socket?.end?.();
|
|
2222
|
+
this.socket = void 0;
|
|
2223
|
+
}
|
|
2224
|
+
}
|
|
2225
|
+
function toQueryResult(res) {
|
|
2226
|
+
if ('error' === res.type) throw new Error(res.error.message);
|
|
2227
|
+
const r = res.response.result;
|
|
2228
|
+
const rows = r.rows.map((row)=>{
|
|
2229
|
+
const obj = {};
|
|
2230
|
+
r.cols.forEach((c, i)=>{
|
|
2231
|
+
obj[c.name ?? `c${i}`] = fromHrana(row[i] ?? {
|
|
2232
|
+
type: 'null'
|
|
2233
|
+
});
|
|
2234
|
+
});
|
|
2235
|
+
return obj;
|
|
2236
|
+
});
|
|
2237
|
+
return {
|
|
2238
|
+
rows,
|
|
2239
|
+
rowsAffected: r.affected_row_count,
|
|
2240
|
+
lastInsertRowid: r.last_insert_rowid ? Number(r.last_insert_rowid) : void 0
|
|
2241
|
+
};
|
|
2242
|
+
}
|
|
2243
|
+
class OdbliteSocketConnection {
|
|
2244
|
+
client;
|
|
2245
|
+
tenant;
|
|
2246
|
+
databaseName;
|
|
2247
|
+
databaseHash;
|
|
2248
|
+
sql;
|
|
2249
|
+
buffer;
|
|
2250
|
+
constructor(client, tenant, buffer){
|
|
2251
|
+
this.client = client;
|
|
2252
|
+
this.tenant = tenant;
|
|
2253
|
+
this.databaseName = tenant;
|
|
2254
|
+
this.databaseHash = tenant;
|
|
2255
|
+
this.buffer = buffer;
|
|
2256
|
+
const self = this;
|
|
2257
|
+
this.sql = function(strings, ...values) {
|
|
2258
|
+
if (strings && 'object' == typeof strings && 'raw' in strings) {
|
|
2259
|
+
const text = strings.reduce((a, s, i)=>a + s + (i < values.length ? '?' : ''), '');
|
|
2260
|
+
const returns = /^\s*SELECT/i.test(text) || /\bRETURNING\b/i.test(text);
|
|
2261
|
+
return (returns ? self.query(text, values) : self.execute(text, values)).then((r)=>r.rows);
|
|
2262
|
+
}
|
|
2263
|
+
throw new Error('sql() fragment helper not implemented in odblite-socket');
|
|
2264
|
+
};
|
|
2265
|
+
}
|
|
2266
|
+
async one(requests) {
|
|
2267
|
+
const [res] = await this.client.pipeline(this.tenant, requests);
|
|
2268
|
+
if (!res) throw new Error('odblite-socket: empty pipeline response');
|
|
2269
|
+
return res;
|
|
2270
|
+
}
|
|
2271
|
+
async query(sql, params = []) {
|
|
2272
|
+
return toQueryResult(await this.one([
|
|
2273
|
+
{
|
|
2274
|
+
type: 'execute',
|
|
2275
|
+
stmt: {
|
|
2276
|
+
sql,
|
|
2277
|
+
args: params.map(toHrana)
|
|
2278
|
+
}
|
|
2279
|
+
}
|
|
2280
|
+
]));
|
|
2281
|
+
}
|
|
2282
|
+
async execute(sql, params = []) {
|
|
2283
|
+
const s = 'string' == typeof sql ? sql : sql.sql;
|
|
2284
|
+
const p = 'string' == typeof sql ? params : sql.args ?? [];
|
|
2285
|
+
if (this.buffer) {
|
|
2286
|
+
this.buffer.push({
|
|
2287
|
+
sql: s,
|
|
2288
|
+
params: p
|
|
2289
|
+
});
|
|
2290
|
+
return {
|
|
2291
|
+
rows: [],
|
|
2292
|
+
rowsAffected: 0
|
|
2293
|
+
};
|
|
2294
|
+
}
|
|
2295
|
+
return toQueryResult(await this.one([
|
|
2296
|
+
{
|
|
2297
|
+
type: 'execute',
|
|
2298
|
+
stmt: {
|
|
2299
|
+
sql: s,
|
|
2300
|
+
args: p.map(toHrana)
|
|
2301
|
+
}
|
|
2302
|
+
}
|
|
2303
|
+
]));
|
|
2304
|
+
}
|
|
2305
|
+
async atomic(steps) {
|
|
2306
|
+
const res = await this.one([
|
|
2307
|
+
{
|
|
2308
|
+
type: 'batch',
|
|
2309
|
+
batch: toBatch(steps)
|
|
2310
|
+
}
|
|
2311
|
+
]);
|
|
2312
|
+
if ('error' === res.type) throw new Error(res.error.message);
|
|
2313
|
+
return res.response.result;
|
|
2314
|
+
}
|
|
2315
|
+
prepare(sql) {
|
|
2316
|
+
return {
|
|
2317
|
+
execute: async (params = [])=>this.execute(sql, params),
|
|
2318
|
+
all: async (params = [])=>(await this.query(sql, params)).rows,
|
|
2319
|
+
get: async (params = [])=>(await this.query(sql, params)).rows[0] ?? null
|
|
2320
|
+
};
|
|
2321
|
+
}
|
|
2322
|
+
async transaction(fn) {
|
|
2323
|
+
if (this.buffer) return fn(this);
|
|
2324
|
+
const buffer = [];
|
|
2325
|
+
const out = await fn(new OdbliteSocketConnection(this.client, this.tenant, buffer));
|
|
2326
|
+
if (buffer.length) {
|
|
2327
|
+
const res = await this.atomic(buffer.map((b)=>({
|
|
2328
|
+
sql: b.sql,
|
|
2329
|
+
params: b.params
|
|
2330
|
+
})));
|
|
2331
|
+
const err = res.step_errors?.find((e)=>e);
|
|
2332
|
+
if (err) throw new Error(`transaction batch failed: ${err.message}`);
|
|
2333
|
+
}
|
|
2334
|
+
return out;
|
|
2335
|
+
}
|
|
2336
|
+
begin(fn) {
|
|
2337
|
+
return this.transaction(fn);
|
|
2338
|
+
}
|
|
2339
|
+
savepoint(fn) {
|
|
2340
|
+
return this.transaction(fn);
|
|
2341
|
+
}
|
|
2342
|
+
async ensureNamespaces(namespaces) {
|
|
2343
|
+
return {
|
|
2344
|
+
success: true,
|
|
2345
|
+
databaseName: this.tenant,
|
|
2346
|
+
namespaces
|
|
2347
|
+
};
|
|
2348
|
+
}
|
|
2349
|
+
async getNamespaces() {
|
|
2350
|
+
const ns = (await this.query("SELECT name FROM pragma_database_list WHERE name != 'main'")).rows.map((r)=>r.name);
|
|
2351
|
+
return {
|
|
2352
|
+
success: true,
|
|
2353
|
+
databaseName: this.tenant,
|
|
2354
|
+
exists: true,
|
|
2355
|
+
namespaces: ns,
|
|
2356
|
+
registered: true,
|
|
2357
|
+
registeredNamespaces: ns
|
|
2358
|
+
};
|
|
2359
|
+
}
|
|
2360
|
+
async close() {}
|
|
2361
|
+
}
|
|
2362
|
+
class OdbliteSocketAdapter {
|
|
2363
|
+
type = 'odblite-socket';
|
|
2364
|
+
client;
|
|
2365
|
+
constructor(config){
|
|
2366
|
+
this.client = new SocketClient(config);
|
|
2367
|
+
}
|
|
2368
|
+
async connect(config) {
|
|
2369
|
+
const tenant = config?.databaseHash ?? config?.databaseName;
|
|
2370
|
+
if (!tenant) throw new Error('odblite-socket: databaseHash (or databaseName) required');
|
|
2371
|
+
return new OdbliteSocketConnection(this.client, String(tenant));
|
|
2372
|
+
}
|
|
2373
|
+
async disconnect() {
|
|
2374
|
+
this.client.close();
|
|
2375
|
+
}
|
|
2376
|
+
async isHealthy() {
|
|
2377
|
+
try {
|
|
2378
|
+
await Promise.race([
|
|
2379
|
+
this.client.pipeline('_health', [
|
|
2380
|
+
{
|
|
2381
|
+
type: 'get_autocommit'
|
|
2382
|
+
}
|
|
2383
|
+
]),
|
|
2384
|
+
new Promise((_, rej)=>setTimeout(()=>rej(new Error('probe timeout')), 1500))
|
|
2385
|
+
]);
|
|
2386
|
+
return true;
|
|
2387
|
+
} catch {
|
|
2388
|
+
return false;
|
|
2389
|
+
}
|
|
2390
|
+
}
|
|
2391
|
+
}
|
|
2392
|
+
const DEFAULT_UNIX_SOCKET = '/tmp/odblite.sock';
|
|
2393
|
+
function parseSocketTarget(s) {
|
|
2394
|
+
const v = (s ?? '').trim();
|
|
2395
|
+
if (!v) return {
|
|
2396
|
+
unix: DEFAULT_UNIX_SOCKET
|
|
2397
|
+
};
|
|
2398
|
+
if (v.includes('/')) return {
|
|
2399
|
+
unix: v
|
|
2400
|
+
};
|
|
2401
|
+
const m = v.match(/^(?:([^:]+):)?(\d+)$/);
|
|
2402
|
+
if (m) return {
|
|
2403
|
+
hostname: m[1] || '127.0.0.1',
|
|
2404
|
+
port: Number(m[2])
|
|
2405
|
+
};
|
|
2406
|
+
return {
|
|
2407
|
+
unix: v
|
|
2408
|
+
};
|
|
2409
|
+
}
|
|
2410
|
+
class OdbliteAutoAdapter {
|
|
2411
|
+
type = 'odblite';
|
|
2412
|
+
chosen;
|
|
2413
|
+
constructor(opts){
|
|
2414
|
+
this.chosen = this.pick(opts);
|
|
2415
|
+
}
|
|
2416
|
+
async pick(opts) {
|
|
2417
|
+
const hasBun = void 0 !== globalThis.Bun && !!globalThis.Bun.connect;
|
|
2418
|
+
if (hasBun) for (const target of this.candidates(opts))try {
|
|
2419
|
+
const socket = new OdbliteSocketAdapter(target);
|
|
2420
|
+
if (await socket.isHealthy()) {
|
|
2421
|
+
console.log(`[odb-client] transport → SOCKET (router_v2 @ ${target.unix ?? `${target.hostname}:${target.port}`})`);
|
|
2422
|
+
return socket;
|
|
2423
|
+
}
|
|
2424
|
+
await socket.disconnect();
|
|
2425
|
+
} catch {}
|
|
2426
|
+
console.log('[odb-client] transport → HTTP (no router_v2 socket reachable)');
|
|
2427
|
+
return new ODBLiteAdapter(opts.http);
|
|
2428
|
+
}
|
|
2429
|
+
candidates(opts) {
|
|
2430
|
+
if (opts.socket?.unix || opts.socket?.port) return [
|
|
2431
|
+
opts.socket
|
|
2432
|
+
];
|
|
2433
|
+
const env = globalThis.process?.env?.ODB_SOCKET;
|
|
2434
|
+
if (env) return [
|
|
2435
|
+
parseSocketTarget(env)
|
|
2436
|
+
];
|
|
2437
|
+
const out = [
|
|
2438
|
+
{
|
|
2439
|
+
unix: DEFAULT_UNIX_SOCKET
|
|
2440
|
+
}
|
|
2441
|
+
];
|
|
2442
|
+
try {
|
|
2443
|
+
const host = new URL(opts.http?.serviceUrl).hostname;
|
|
2444
|
+
const port = Number(globalThis.process?.env?.ODB_SOCKET_PORT ?? 8787);
|
|
2445
|
+
if (host) out.push({
|
|
2446
|
+
hostname: host,
|
|
2447
|
+
port
|
|
2448
|
+
});
|
|
2449
|
+
} catch {}
|
|
2450
|
+
return out;
|
|
2451
|
+
}
|
|
2452
|
+
async connect(config) {
|
|
2453
|
+
return (await this.chosen).connect(config);
|
|
2454
|
+
}
|
|
2455
|
+
async disconnect(tenantId) {
|
|
2456
|
+
return (await this.chosen).disconnect(tenantId);
|
|
2457
|
+
}
|
|
2458
|
+
async isHealthy() {
|
|
2459
|
+
return (await this.chosen).isHealthy();
|
|
2460
|
+
}
|
|
2461
|
+
}
|
|
2001
2462
|
function parseSQL(sqlContent, options = {}) {
|
|
2002
2463
|
const separatePragma = options.separatePragma ?? true;
|
|
2003
2464
|
const statements = splitStatements(sqlContent);
|
|
@@ -2023,6 +2484,8 @@ function splitStatements(sqlContent) {
|
|
|
2023
2484
|
let inQuote = false;
|
|
2024
2485
|
let quoteChar = '';
|
|
2025
2486
|
let beginEndDepth = 0;
|
|
2487
|
+
let inCte = false;
|
|
2488
|
+
let cteParenDepth = 0;
|
|
2026
2489
|
const lines = sqlContent.split('\n');
|
|
2027
2490
|
for (const line of lines){
|
|
2028
2491
|
let processedLine = '';
|
|
@@ -2036,6 +2499,15 @@ function splitStatements(sqlContent) {
|
|
|
2036
2499
|
if (beginEndDepth < 0) beginEndDepth = 0;
|
|
2037
2500
|
}
|
|
2038
2501
|
}
|
|
2502
|
+
if (!inQuote && lineWithoutComments.trim()) {
|
|
2503
|
+
const withMatch = lineWithoutComments.match(/\bWITH\b.*\bAS\b/i);
|
|
2504
|
+
if (withMatch) inCte = true;
|
|
2505
|
+
if (inCte) for (const char of lineWithoutComments){
|
|
2506
|
+
if ('(' === char) cteParenDepth++;
|
|
2507
|
+
if (')' === char) cteParenDepth--;
|
|
2508
|
+
if (0 === cteParenDepth && ')' === char) inCte = false;
|
|
2509
|
+
}
|
|
2510
|
+
}
|
|
2039
2511
|
for(let i = 0; i < line.length; i++){
|
|
2040
2512
|
const char = line[i];
|
|
2041
2513
|
const prevChar = i > 0 ? line[i - 1] : '';
|
|
@@ -2051,7 +2523,7 @@ function splitStatements(sqlContent) {
|
|
|
2051
2523
|
quoteChar = char;
|
|
2052
2524
|
}
|
|
2053
2525
|
processedLine += char;
|
|
2054
|
-
if (';' === char && !inQuote && 0 === beginEndDepth) {
|
|
2526
|
+
if (';' === char && !inQuote && 0 === beginEndDepth && !inCte) {
|
|
2055
2527
|
currentStatement += processedLine;
|
|
2056
2528
|
const stmt = currentStatement.trim();
|
|
2057
2529
|
if (stmt && !stmt.startsWith('--')) statements.push(stmt);
|
|
@@ -2096,7 +2568,14 @@ class DatabaseManager {
|
|
|
2096
2568
|
break;
|
|
2097
2569
|
case 'odblite':
|
|
2098
2570
|
if (!config.odblite) throw new Error('odblite config required for odblite backend');
|
|
2099
|
-
this.adapter = new
|
|
2571
|
+
this.adapter = new OdbliteAutoAdapter({
|
|
2572
|
+
socket: config.odbliteSocket,
|
|
2573
|
+
http: config.odblite
|
|
2574
|
+
});
|
|
2575
|
+
break;
|
|
2576
|
+
case 'odblite-socket':
|
|
2577
|
+
if (!config.odbliteSocket) throw new Error('odbliteSocket config required for odblite-socket backend');
|
|
2578
|
+
this.adapter = new OdbliteSocketAdapter(config.odbliteSocket);
|
|
2100
2579
|
break;
|
|
2101
2580
|
default:
|
|
2102
2581
|
throw new Error(`Unknown backend type: ${config.backend}`);
|
|
@@ -2214,6 +2693,12 @@ class DatabaseManager {
|
|
|
2214
2693
|
databaseName: `${this.config.databasePath}_${name}`,
|
|
2215
2694
|
...this.config.odblite
|
|
2216
2695
|
};
|
|
2696
|
+
case 'odblite-socket':
|
|
2697
|
+
return {
|
|
2698
|
+
databaseName: name,
|
|
2699
|
+
databaseHash: name,
|
|
2700
|
+
...this.config.odbliteSocket
|
|
2701
|
+
};
|
|
2217
2702
|
default:
|
|
2218
2703
|
throw new Error(`Unknown backend: ${this.config.backend}`);
|
|
2219
2704
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pineliner/odb-client",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.2.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",
|
|
@@ -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": {
|