@profullstack/libsql-pg 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +128 -0
- package/bin/libsql-pg.js +11 -0
- package/index.js +8 -0
- package/package.json +73 -0
- package/src/bind.js +141 -0
- package/src/cli.js +123 -0
- package/src/client.js +449 -0
- package/src/copy.js +614 -0
- package/src/errors.js +111 -0
- package/src/result.js +87 -0
- package/src/rewrite.js +549 -0
- package/src/schema.js +549 -0
- package/src/sqlparse.js +283 -0
package/src/client.js
ADDED
|
@@ -0,0 +1,449 @@
|
|
|
1
|
+
import pg from 'pg';
|
|
2
|
+
|
|
3
|
+
import { prepare } from './bind.js';
|
|
4
|
+
import { translateError } from './errors.js';
|
|
5
|
+
import { emptyResultSet, toResultSet } from './result.js';
|
|
6
|
+
import { createRewriter } from './rewrite.js';
|
|
7
|
+
import { convertDdl } from './schema.js';
|
|
8
|
+
import { codeMask, splitStatements } from './sqlparse.js';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Postgres behind the @libsql/client surface.
|
|
12
|
+
*
|
|
13
|
+
* `execute`, `batch`, `transaction`, `executeMultiple`, `close`, `sync` and
|
|
14
|
+
* the ResultSet shape are libSQL's; the SQL is rewritten from SQLite's
|
|
15
|
+
* dialect on the way through (see ./rewrite.js) and `?` / `:name`
|
|
16
|
+
* placeholders become `$n`. The approach is the one rssamplifier.com shipped
|
|
17
|
+
* on 2026-09-25, generalised.
|
|
18
|
+
*
|
|
19
|
+
* @typedef {'sqlite' | 'postgres'} Dialect
|
|
20
|
+
* @typedef {'write' | 'read' | 'deferred'} TransactionMode
|
|
21
|
+
* @typedef {{
|
|
22
|
+
* url: string,
|
|
23
|
+
* authToken?: string,
|
|
24
|
+
* syncUrl?: string,
|
|
25
|
+
* dialect?: Dialect,
|
|
26
|
+
* pool?: { max?: number, idleTimeoutMillis?: number, connectionTimeoutMillis?: number },
|
|
27
|
+
* intMode?: 'number' | 'bigint' | 'string',
|
|
28
|
+
* timestamps?: 'iso' | 'date',
|
|
29
|
+
* nativeBooleans?: boolean,
|
|
30
|
+
* statementTimeoutMs?: number,
|
|
31
|
+
* applicationName?: string,
|
|
32
|
+
* ssl?: boolean | object,
|
|
33
|
+
* onWarning?: (message: string, sql: string) => void,
|
|
34
|
+
* }} Config
|
|
35
|
+
*/
|
|
36
|
+
|
|
37
|
+
const { Pool, types } = pg;
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Read `sslmode=` out of the URL. pg lets the parameter override its `ssl`
|
|
41
|
+
* option and its `require` verifies the certificate, which a self-signed
|
|
42
|
+
* cert on your own box fails; libpq's `require` never verified. So the
|
|
43
|
+
* parameter is honoured here and removed from the string.
|
|
44
|
+
*
|
|
45
|
+
* @param {string} url
|
|
46
|
+
* @param {boolean | object | undefined} explicit
|
|
47
|
+
*/
|
|
48
|
+
export function connectionSettings(url, explicit) {
|
|
49
|
+
const parsed = new URL(url);
|
|
50
|
+
const mode = parsed.searchParams.get('sslmode');
|
|
51
|
+
parsed.searchParams.delete('sslmode');
|
|
52
|
+
let ssl = explicit;
|
|
53
|
+
if (ssl === undefined) {
|
|
54
|
+
if (mode === 'require' || mode === 'prefer') ssl = { rejectUnauthorized: false };
|
|
55
|
+
else if (mode === 'verify-ca' || mode === 'verify-full') ssl = true;
|
|
56
|
+
else ssl = undefined;
|
|
57
|
+
}
|
|
58
|
+
return { connectionString: parsed.toString(), ssl };
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* @param {string} url
|
|
63
|
+
*/
|
|
64
|
+
function assertPostgresUrl(url) {
|
|
65
|
+
if (typeof url !== 'string' || !url) throw new TypeError('createClient: `url` is required (postgres://user:pass@host:5432/db)');
|
|
66
|
+
const scheme = url.split(':')[0].toLowerCase();
|
|
67
|
+
if (scheme === 'postgres' || scheme === 'postgresql') return;
|
|
68
|
+
const hint =
|
|
69
|
+
scheme === 'libsql' || scheme === 'file' || scheme === 'http' || scheme === 'https' || scheme === 'ws' || scheme === 'wss'
|
|
70
|
+
? ' This client speaks Postgres only; move the data first with `libsql-pg copy --from <that url> --to postgres://...` and point `url` at Postgres.'
|
|
71
|
+
: '';
|
|
72
|
+
throw new Error(`createClient: url must be postgres:// or postgresql://, got "${scheme}:".${hint}`);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Per-client type parsing: int8 as number (libSQL's default), or bigint or
|
|
77
|
+
* string via `intMode`; timestamps as ISO text (what a SQLite app stored and
|
|
78
|
+
* compared) unless `timestamps: 'date'`.
|
|
79
|
+
*
|
|
80
|
+
* @param {Config} config
|
|
81
|
+
*/
|
|
82
|
+
function typeParsers(config) {
|
|
83
|
+
const intMode = config.intMode ?? 'number';
|
|
84
|
+
const int8 =
|
|
85
|
+
intMode === 'bigint' ? (v) => (v === null ? null : BigInt(v)) : intMode === 'string' ? (v) => v : (v) => (v === null ? null : Number(v));
|
|
86
|
+
const numeric = intMode === 'string' ? (v) => v : (v) => (v === null ? null : Number(v));
|
|
87
|
+
const iso = (v) => {
|
|
88
|
+
if (v === null) return null;
|
|
89
|
+
const d = new Date(v.includes('+') || v.endsWith('Z') || /[+-]\d\d(:\d\d)?$/.test(v) ? v : `${v}Z`);
|
|
90
|
+
return Number.isNaN(d.getTime()) ? v : d.toISOString();
|
|
91
|
+
};
|
|
92
|
+
const overrides = new Map([
|
|
93
|
+
[20, int8],
|
|
94
|
+
[1700, numeric],
|
|
95
|
+
]);
|
|
96
|
+
if ((config.timestamps ?? 'iso') === 'iso') {
|
|
97
|
+
overrides.set(1184, iso);
|
|
98
|
+
overrides.set(1114, iso);
|
|
99
|
+
overrides.set(1082, (v) => v); // date: keep 'YYYY-MM-DD'
|
|
100
|
+
}
|
|
101
|
+
return {
|
|
102
|
+
getTypeParser(oid, format) {
|
|
103
|
+
if (format !== 'binary' && overrides.has(oid)) return overrides.get(oid);
|
|
104
|
+
return types.getTypeParser(oid, format);
|
|
105
|
+
},
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* @param {string} sql
|
|
111
|
+
* @returns {string | null} the INSERT's target table, lower-cased, when the
|
|
112
|
+
* statement is a plain INSERT without RETURNING
|
|
113
|
+
*/
|
|
114
|
+
function insertWithoutReturning(sql) {
|
|
115
|
+
const mask = codeMask(sql);
|
|
116
|
+
const m = /^\s*insert\s+into\s+("[^"]+"|[A-Za-z_][A-Za-z0-9_.]*)/i.exec(mask);
|
|
117
|
+
if (!m) return null;
|
|
118
|
+
if (/\breturning\b/i.test(mask)) return null;
|
|
119
|
+
const raw = sql.slice(m.index + m[0].length - m[1].length, m.index + m[0].length);
|
|
120
|
+
return raw.startsWith('"') ? raw.slice(1, -1) : raw.toLowerCase();
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* @param {Config} config
|
|
125
|
+
*/
|
|
126
|
+
export function createClient(config) {
|
|
127
|
+
if (!config || typeof config !== 'object') throw new TypeError('createClient(config): config object required');
|
|
128
|
+
assertPostgresUrl(config.url);
|
|
129
|
+
const dialect = config.dialect ?? 'sqlite';
|
|
130
|
+
if (dialect !== 'sqlite' && dialect !== 'postgres') throw new RangeError(`dialect must be 'sqlite' or 'postgres', got ${dialect}`);
|
|
131
|
+
const { connectionString, ssl } = connectionSettings(config.url, config.ssl);
|
|
132
|
+
const pool = new Pool({
|
|
133
|
+
connectionString,
|
|
134
|
+
ssl,
|
|
135
|
+
max: config.pool?.max ?? 10,
|
|
136
|
+
idleTimeoutMillis: config.pool?.idleTimeoutMillis,
|
|
137
|
+
connectionTimeoutMillis: config.pool?.connectionTimeoutMillis,
|
|
138
|
+
application_name: config.applicationName ?? 'libsql-pg',
|
|
139
|
+
statement_timeout: config.statementTimeoutMs,
|
|
140
|
+
allowExitOnIdle: true,
|
|
141
|
+
types: typeParsers(config),
|
|
142
|
+
});
|
|
143
|
+
pool.on('error', () => {
|
|
144
|
+
/* an idle connection dropped by the server; the next query reconnects */
|
|
145
|
+
});
|
|
146
|
+
const bindOpts = { nativeBooleans: config.nativeBooleans ?? false };
|
|
147
|
+
|
|
148
|
+
/** @type {Map<string, import('./rewrite.js').TableKeys | undefined>} */
|
|
149
|
+
const keysCache = new Map();
|
|
150
|
+
/** @type {Map<string, string | null>} table -> identity/serial pk column */
|
|
151
|
+
const pkCache = new Map();
|
|
152
|
+
/** @type {Map<string, string[]>} constraint -> columns */
|
|
153
|
+
const constraintCache = new Map();
|
|
154
|
+
const warned = new Set();
|
|
155
|
+
let pgcryptoEnsured = false;
|
|
156
|
+
|
|
157
|
+
const rewriter = createRewriter({
|
|
158
|
+
keys: (table) => keysCache.get(table),
|
|
159
|
+
ddl: (sql) => convertDdl(sql),
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
/** Primary key, unique indexes and columns of a table, cached. */
|
|
163
|
+
async function loadKeys(table) {
|
|
164
|
+
if (keysCache.has(table)) return keysCache.get(table);
|
|
165
|
+
let info;
|
|
166
|
+
try {
|
|
167
|
+
const { rows } = await pool.query(
|
|
168
|
+
`select i.indisprimary as pk, array_agg(a.attname::text order by x.ord) as cols
|
|
169
|
+
from pg_index i
|
|
170
|
+
join lateral unnest(i.indkey) with ordinality as x(attnum, ord) on true
|
|
171
|
+
join pg_attribute a on a.attrelid = i.indrelid and a.attnum = x.attnum
|
|
172
|
+
where i.indrelid = to_regclass($1) and (i.indisprimary or i.indisunique) and i.indpred is null
|
|
173
|
+
group by i.indexrelid, i.indisprimary
|
|
174
|
+
order by i.indisprimary desc, min(x.ord)`,
|
|
175
|
+
[table.includes('"') || /^[a-z_][a-z0-9_.]*$/.test(table) ? table : `"${table}"`],
|
|
176
|
+
);
|
|
177
|
+
const cols = await pool.query(
|
|
178
|
+
`select attname from pg_attribute where attrelid = to_regclass($1) and attnum > 0 and not attisdropped order by attnum`,
|
|
179
|
+
[table.includes('"') || /^[a-z_][a-z0-9_.]*$/.test(table) ? table : `"${table}"`],
|
|
180
|
+
);
|
|
181
|
+
const pk = rows.find((r) => r.pk)?.cols ?? [];
|
|
182
|
+
const unique = rows.filter((r) => !r.pk).map((r) => r.cols);
|
|
183
|
+
info = { pk, unique, columns: cols.rows.map((r) => r.attname) };
|
|
184
|
+
} catch {
|
|
185
|
+
info = undefined;
|
|
186
|
+
}
|
|
187
|
+
keysCache.set(table, info);
|
|
188
|
+
return info;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/** The identity or serial primary-key column of a table, if it has one. */
|
|
192
|
+
async function loadPk(table) {
|
|
193
|
+
if (pkCache.has(table)) return pkCache.get(table);
|
|
194
|
+
let col = null;
|
|
195
|
+
try {
|
|
196
|
+
const { rows } = await pool.query(
|
|
197
|
+
`select a.attname
|
|
198
|
+
from pg_index i
|
|
199
|
+
join pg_attribute a on a.attrelid = i.indrelid and a.attnum = any(i.indkey)
|
|
200
|
+
left join pg_attrdef d on d.adrelid = a.attrelid and d.adnum = a.attnum
|
|
201
|
+
where i.indrelid = to_regclass($1) and i.indisprimary and array_length(i.indkey, 1) = 1
|
|
202
|
+
and (a.attidentity <> '' or pg_get_expr(d.adbin, d.adrelid) like 'nextval(%')`,
|
|
203
|
+
[/^[a-z_][a-z0-9_.]*$/.test(table) ? table : `"${table}"`],
|
|
204
|
+
);
|
|
205
|
+
col = rows[0]?.attname ?? null;
|
|
206
|
+
} catch {
|
|
207
|
+
col = null;
|
|
208
|
+
}
|
|
209
|
+
pkCache.set(table, col);
|
|
210
|
+
return col;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/** Columns of a unique constraint or index, for the SQLite-style message. */
|
|
214
|
+
async function constraintColumns(name) {
|
|
215
|
+
if (!name) return undefined;
|
|
216
|
+
if (constraintCache.has(name)) return constraintCache.get(name);
|
|
217
|
+
let cols;
|
|
218
|
+
try {
|
|
219
|
+
const { rows } = await pool.query(
|
|
220
|
+
`select a.attname
|
|
221
|
+
from pg_class c
|
|
222
|
+
join pg_index i on i.indexrelid = c.oid
|
|
223
|
+
join lateral unnest(i.indkey) with ordinality as x(attnum, ord) on true
|
|
224
|
+
join pg_attribute a on a.attrelid = i.indrelid and a.attnum = x.attnum
|
|
225
|
+
where c.relname = $1
|
|
226
|
+
order by x.ord`,
|
|
227
|
+
[name],
|
|
228
|
+
);
|
|
229
|
+
cols = rows.map((r) => r.attname);
|
|
230
|
+
} catch {
|
|
231
|
+
cols = undefined;
|
|
232
|
+
}
|
|
233
|
+
constraintCache.set(name, cols);
|
|
234
|
+
return cols;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
async function translate(err) {
|
|
238
|
+
if (err?.code === '23505') {
|
|
239
|
+
const columns = await constraintColumns(err.constraint);
|
|
240
|
+
return translateError(err, { columns });
|
|
241
|
+
}
|
|
242
|
+
return translateError(err);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* @param {string | { sql: string, args?: unknown[] | Record<string, unknown> }} statement
|
|
247
|
+
* @param {unknown[] | Record<string, unknown>} [args]
|
|
248
|
+
*/
|
|
249
|
+
function normalize(statement, args) {
|
|
250
|
+
if (typeof statement === 'string') return { sql: statement, args };
|
|
251
|
+
if (statement && typeof statement === 'object' && typeof statement.sql === 'string') return { sql: statement.sql, args: statement.args };
|
|
252
|
+
throw new TypeError('execute() takes a SQL string or { sql, args }');
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* Rewrite (resolving any table keys the rewrite needs) and bind.
|
|
257
|
+
* @param {{ sql: string, args?: unknown[] | Record<string, unknown> }} st
|
|
258
|
+
*/
|
|
259
|
+
async function compile(st) {
|
|
260
|
+
let rewritten;
|
|
261
|
+
if (dialect === 'sqlite') {
|
|
262
|
+
const need = rewriter.needsKeys(st.sql);
|
|
263
|
+
if (need) await loadKeys(need);
|
|
264
|
+
rewritten = rewriter.rewrite(st.sql);
|
|
265
|
+
if (config.onWarning && rewritten.warnings.length && !warned.has(st.sql)) {
|
|
266
|
+
warned.add(st.sql);
|
|
267
|
+
for (const w of rewritten.warnings) config.onWarning(w, st.sql);
|
|
268
|
+
}
|
|
269
|
+
} else {
|
|
270
|
+
rewritten = { sql: st.sql, noop: false, warnings: [], kind: 'dml' };
|
|
271
|
+
}
|
|
272
|
+
if (rewritten.noop) return { noop: true };
|
|
273
|
+
let text = rewritten.sql;
|
|
274
|
+
// randomblob() became gen_random_bytes(), which lives in pgcrypto. Ask
|
|
275
|
+
// for the extension once; if the role may not, the real error follows.
|
|
276
|
+
if (!pgcryptoEnsured && /\bgen_random_bytes\s*\(/i.test(text)) {
|
|
277
|
+
pgcryptoEnsured = true;
|
|
278
|
+
await pool.query('create extension if not exists pgcrypto').catch(() => {});
|
|
279
|
+
}
|
|
280
|
+
// lastInsertRowid: ask for the identity/serial primary key when the
|
|
281
|
+
// statement is an INSERT without its own RETURNING.
|
|
282
|
+
let rowidColumn = null;
|
|
283
|
+
const table = insertWithoutReturning(text);
|
|
284
|
+
if (table) {
|
|
285
|
+
rowidColumn = await loadPk(table);
|
|
286
|
+
if (rowidColumn) text = `${text.replace(/;\s*$/, '')} RETURNING "${rowidColumn}"`;
|
|
287
|
+
}
|
|
288
|
+
const prepared = prepare(text, st.args, bindOpts);
|
|
289
|
+
return { noop: false, text: prepared.text, values: prepared.values, rowidColumn };
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/**
|
|
293
|
+
* Run one statement on a pool or a checked-out connection.
|
|
294
|
+
* @param {{ query: Function }} target
|
|
295
|
+
* @param {{ sql: string, args?: unknown[] | Record<string, unknown> }} st
|
|
296
|
+
*/
|
|
297
|
+
async function run(target, st) {
|
|
298
|
+
const c = await compile(st);
|
|
299
|
+
if (c.noop) return emptyResultSet();
|
|
300
|
+
let res;
|
|
301
|
+
try {
|
|
302
|
+
res = await target.query({ text: c.text, values: c.values, rowMode: 'array' });
|
|
303
|
+
} catch (err) {
|
|
304
|
+
throw await translate(err);
|
|
305
|
+
}
|
|
306
|
+
if (c.rowidColumn) {
|
|
307
|
+
const value = res.rows?.[0]?.[0];
|
|
308
|
+
const lastInsertRowid = value === undefined || value === null ? undefined : BigInt(value);
|
|
309
|
+
return toResultSet(res, { lastInsertRowid, rowsAffected: res.rowCount ?? 0, hideRows: true });
|
|
310
|
+
}
|
|
311
|
+
return toResultSet(res);
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
function beginSql(mode) {
|
|
315
|
+
if (mode === 'read') return 'begin read only';
|
|
316
|
+
if (mode === 'write' || mode === 'deferred' || mode === undefined) return 'begin';
|
|
317
|
+
throw new RangeError('Unknown transaction mode, supported values are "write", "read" and "deferred"');
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
/** Statements a pooled client cannot honour: each execute may land on another connection. */
|
|
321
|
+
function rejectBareTransactionControl(sql) {
|
|
322
|
+
if (/^\s*(begin|commit|rollback|end)\b/i.test(codeMask(sql))) {
|
|
323
|
+
throw new Error(`"${sql.trim().split(/\s+/)[0].toUpperCase()}" through execute() runs on one pooled connection and the next statement on another; use client.transaction() or client.batch() instead`);
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
const client = {
|
|
328
|
+
/** @type {'postgres'} */
|
|
329
|
+
protocol: 'postgres',
|
|
330
|
+
closed: false,
|
|
331
|
+
|
|
332
|
+
async execute(statement, args) {
|
|
333
|
+
const st = normalize(statement, args);
|
|
334
|
+
rejectBareTransactionControl(st.sql);
|
|
335
|
+
return run(pool, st);
|
|
336
|
+
},
|
|
337
|
+
|
|
338
|
+
/**
|
|
339
|
+
* One transaction on one connection, one result per statement, in order.
|
|
340
|
+
* @param {Array<string | { sql: string, args?: unknown[] | Record<string, unknown> }>} statements
|
|
341
|
+
* @param {TransactionMode} [mode]
|
|
342
|
+
*/
|
|
343
|
+
async batch(statements, mode = 'deferred') {
|
|
344
|
+
const begin = beginSql(mode);
|
|
345
|
+
const conn = await pool.connect();
|
|
346
|
+
try {
|
|
347
|
+
await conn.query(begin);
|
|
348
|
+
const results = [];
|
|
349
|
+
for (const s of statements) results.push(await run(conn, normalize(s)));
|
|
350
|
+
await conn.query('commit');
|
|
351
|
+
return results;
|
|
352
|
+
} catch (err) {
|
|
353
|
+
try {
|
|
354
|
+
await conn.query('rollback');
|
|
355
|
+
} catch {
|
|
356
|
+
/* connection already gone */
|
|
357
|
+
}
|
|
358
|
+
throw err;
|
|
359
|
+
} finally {
|
|
360
|
+
conn.release();
|
|
361
|
+
}
|
|
362
|
+
},
|
|
363
|
+
|
|
364
|
+
/**
|
|
365
|
+
* An interactive transaction held across awaits; libSQL's Transaction surface.
|
|
366
|
+
* @param {TransactionMode} [mode]
|
|
367
|
+
*/
|
|
368
|
+
async transaction(mode = 'deferred') {
|
|
369
|
+
const begin = beginSql(mode);
|
|
370
|
+
const conn = await pool.connect();
|
|
371
|
+
let open = true;
|
|
372
|
+
try {
|
|
373
|
+
await conn.query(begin);
|
|
374
|
+
} catch (err) {
|
|
375
|
+
conn.release();
|
|
376
|
+
throw await translate(err);
|
|
377
|
+
}
|
|
378
|
+
const finish = async (verb) => {
|
|
379
|
+
if (!open) return;
|
|
380
|
+
open = false;
|
|
381
|
+
try {
|
|
382
|
+
await conn.query(verb);
|
|
383
|
+
} catch (err) {
|
|
384
|
+
throw await translate(err);
|
|
385
|
+
} finally {
|
|
386
|
+
conn.release();
|
|
387
|
+
}
|
|
388
|
+
};
|
|
389
|
+
return {
|
|
390
|
+
execute: (statement, args) => run(conn, normalize(statement, args)),
|
|
391
|
+
async batch(statements) {
|
|
392
|
+
const out = [];
|
|
393
|
+
for (const s of statements) out.push(await run(conn, normalize(s)));
|
|
394
|
+
return out;
|
|
395
|
+
},
|
|
396
|
+
async executeMultiple(sql) {
|
|
397
|
+
for (const s of splitStatements(sql)) await run(conn, { sql: s });
|
|
398
|
+
},
|
|
399
|
+
commit: () => finish('commit'),
|
|
400
|
+
rollback: () => finish('rollback'),
|
|
401
|
+
close: () => finish('rollback'),
|
|
402
|
+
get closed() {
|
|
403
|
+
return !open;
|
|
404
|
+
},
|
|
405
|
+
};
|
|
406
|
+
},
|
|
407
|
+
|
|
408
|
+
/**
|
|
409
|
+
* Several statements in one string, run in order on one connection with
|
|
410
|
+
* no transaction around them (libSQL's behaviour). PRAGMAs are skipped
|
|
411
|
+
* and DDL goes through the schema converter.
|
|
412
|
+
* @param {string} sql
|
|
413
|
+
*/
|
|
414
|
+
async executeMultiple(sql) {
|
|
415
|
+
const conn = await pool.connect();
|
|
416
|
+
try {
|
|
417
|
+
for (const s of splitStatements(sql)) {
|
|
418
|
+
rejectBareTransactionControl(s);
|
|
419
|
+
await run(conn, { sql: s });
|
|
420
|
+
}
|
|
421
|
+
} finally {
|
|
422
|
+
conn.release();
|
|
423
|
+
}
|
|
424
|
+
},
|
|
425
|
+
|
|
426
|
+
/** Embedded-replica sync has no meaning here. */
|
|
427
|
+
async sync() {},
|
|
428
|
+
|
|
429
|
+
close() {
|
|
430
|
+
if (client.closed) return Promise.resolve();
|
|
431
|
+
client.closed = true;
|
|
432
|
+
return pool.end();
|
|
433
|
+
},
|
|
434
|
+
|
|
435
|
+
/**
|
|
436
|
+
* Rewrite a statement without running it: what Postgres will be sent.
|
|
437
|
+
* @param {string} sql
|
|
438
|
+
*/
|
|
439
|
+
async explainRewrite(sql) {
|
|
440
|
+
const need = rewriter.needsKeys(sql);
|
|
441
|
+
if (need) await loadKeys(need);
|
|
442
|
+
return rewriter.rewrite(sql);
|
|
443
|
+
},
|
|
444
|
+
|
|
445
|
+
/** The pg pool, for COPY, LISTEN/NOTIFY and anything else libSQL never had. */
|
|
446
|
+
pool,
|
|
447
|
+
};
|
|
448
|
+
return client;
|
|
449
|
+
}
|