@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/errors.js
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Postgres errors reshaped so code that matched on SQLite's wording keeps
|
|
3
|
+
* working. The original pg error rides along as `cause`; `code` (the
|
|
4
|
+
* SQLSTATE) is preserved and `sqliteCode` carries the SQLite name that the
|
|
5
|
+
* libSQL client used to expose.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/** @typedef {Error & { code?: string, sqliteCode?: string, cause?: unknown, constraint?: string, table?: string, column?: string, detail?: string }} LibsqlPgError */
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* @param {string} message
|
|
12
|
+
* @param {any} err the pg error
|
|
13
|
+
* @param {string} sqliteCode
|
|
14
|
+
* @returns {LibsqlPgError}
|
|
15
|
+
*/
|
|
16
|
+
function wrap(message, err, sqliteCode) {
|
|
17
|
+
const e = /** @type {LibsqlPgError} */ (new Error(message, { cause: err }));
|
|
18
|
+
e.name = 'LibsqlError';
|
|
19
|
+
e.code = err.code;
|
|
20
|
+
e.sqliteCode = sqliteCode;
|
|
21
|
+
e.constraint = err.constraint;
|
|
22
|
+
e.table = err.table;
|
|
23
|
+
e.column = err.column;
|
|
24
|
+
e.detail = err.detail;
|
|
25
|
+
return e;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Translate a pg error. `columnsOf(constraint)` may be supplied to name the
|
|
30
|
+
* columns of a unique constraint the way SQLite did (`t.a, t.b`); without it,
|
|
31
|
+
* or when it has nothing, the constraint name is used.
|
|
32
|
+
*
|
|
33
|
+
* @param {any} err
|
|
34
|
+
* @param {{ columns?: string[] }} [info] resolved constraint columns, if any
|
|
35
|
+
* @returns {any}
|
|
36
|
+
*/
|
|
37
|
+
export function translateError(err, info = {}) {
|
|
38
|
+
if (!err || typeof err.code !== 'string') return err;
|
|
39
|
+
const table = err.table ?? '';
|
|
40
|
+
switch (err.code) {
|
|
41
|
+
case '23505': {
|
|
42
|
+
const cols = info.columns?.length
|
|
43
|
+
? info.columns.map((c) => (table ? `${table}.${c}` : c)).join(', ')
|
|
44
|
+
: detailColumns(err.detail, table) ?? err.constraint ?? '';
|
|
45
|
+
return wrap(`UNIQUE constraint failed: ${cols}`.trim(), err, 'SQLITE_CONSTRAINT_UNIQUE');
|
|
46
|
+
}
|
|
47
|
+
case '23503':
|
|
48
|
+
return wrap('FOREIGN KEY constraint failed', err, 'SQLITE_CONSTRAINT_FOREIGNKEY');
|
|
49
|
+
case '23502': {
|
|
50
|
+
const col = err.column ? `${table ? `${table}.` : ''}${err.column}` : table;
|
|
51
|
+
return wrap(`NOT NULL constraint failed: ${col}`.trim(), err, 'SQLITE_CONSTRAINT_NOTNULL');
|
|
52
|
+
}
|
|
53
|
+
case '23514':
|
|
54
|
+
return wrap(`CHECK constraint failed: ${err.constraint ?? ''}`.trim(), err, 'SQLITE_CONSTRAINT_CHECK');
|
|
55
|
+
case '42P01':
|
|
56
|
+
return wrap(`no such table: ${tableFromMessage(err.message)}`, err, 'SQLITE_ERROR');
|
|
57
|
+
case '42703':
|
|
58
|
+
return wrap(`no such column: ${columnFromMessage(err.message)}`, err, 'SQLITE_ERROR');
|
|
59
|
+
case '42P07':
|
|
60
|
+
// The migration runners that test for "already exists" get the same words.
|
|
61
|
+
return wrap(err.message, err, 'SQLITE_ERROR');
|
|
62
|
+
default:
|
|
63
|
+
return err;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* pg puts `Key (a, b)=(1, 2) already exists.` in `detail`; the column list
|
|
69
|
+
* inside it is the same information SQLite printed.
|
|
70
|
+
* @param {string | undefined} detail
|
|
71
|
+
* @param {string} table
|
|
72
|
+
*/
|
|
73
|
+
function detailColumns(detail, table) {
|
|
74
|
+
const m = /^Key \((.+?)\)=\(/.exec(detail ?? '');
|
|
75
|
+
if (!m) return null;
|
|
76
|
+
return m[1]
|
|
77
|
+
.split(',')
|
|
78
|
+
.map((c) => c.trim())
|
|
79
|
+
.map((c) => (table ? `${table}.${c}` : c))
|
|
80
|
+
.join(', ');
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function tableFromMessage(message) {
|
|
84
|
+
const m = /relation "([^"]+)" does not exist/.exec(message ?? '');
|
|
85
|
+
return m ? m[1] : '';
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function columnFromMessage(message) {
|
|
89
|
+
const m = /column "?([^"\s]+)"? (?:of relation "[^"]+" )?does not exist/.exec(message ?? '');
|
|
90
|
+
return m ? m[1] : '';
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* The error thrown when a statement runs `MATCH` against an FTS5 table.
|
|
95
|
+
* @param {string} table
|
|
96
|
+
* @param {string} sql
|
|
97
|
+
*/
|
|
98
|
+
export function ftsError(table, sql) {
|
|
99
|
+
const e = /** @type {LibsqlPgError} */ (
|
|
100
|
+
new Error(
|
|
101
|
+
`FTS5 MATCH is not available in Postgres (statement queries "${table}"). ` +
|
|
102
|
+
'Replace the FTS5 table with a tsvector column and query it with ' +
|
|
103
|
+
"`search @@ websearch_to_tsquery('english', ?)`; see the README section " +
|
|
104
|
+
'"Full-text search: FTS5 to tsvector". Statement: ' +
|
|
105
|
+
sql.slice(0, 160),
|
|
106
|
+
)
|
|
107
|
+
);
|
|
108
|
+
e.name = 'LibsqlError';
|
|
109
|
+
e.code = 'FTS5_NOT_SUPPORTED';
|
|
110
|
+
return e;
|
|
111
|
+
}
|
package/src/result.js
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* libSQL's ResultSet shape, built from a pg array-mode result.
|
|
3
|
+
*
|
|
4
|
+
* libSQL rows are objects with an enumerable property per column name AND a
|
|
5
|
+
* non-enumerable numeric index per position, plus a non-enumerable `length`,
|
|
6
|
+
* so both `row.id` and `row[0]` work and `JSON.stringify(row)` shows only
|
|
7
|
+
* the names. The first of two same-named columns wins, as in libSQL.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
/** pg type oids to the declared-type words libSQL reports (SQLite's). */
|
|
11
|
+
const OID_TYPES = new Map([
|
|
12
|
+
[16, 'BOOLEAN'],
|
|
13
|
+
[17, 'BLOB'],
|
|
14
|
+
[20, 'INTEGER'],
|
|
15
|
+
[21, 'INTEGER'],
|
|
16
|
+
[23, 'INTEGER'],
|
|
17
|
+
[25, 'TEXT'],
|
|
18
|
+
[114, 'TEXT'],
|
|
19
|
+
[3802, 'TEXT'],
|
|
20
|
+
[700, 'REAL'],
|
|
21
|
+
[701, 'REAL'],
|
|
22
|
+
[1700, 'REAL'],
|
|
23
|
+
[1042, 'TEXT'],
|
|
24
|
+
[1043, 'TEXT'],
|
|
25
|
+
[1082, 'DATE'],
|
|
26
|
+
[1083, 'TIME'],
|
|
27
|
+
[1114, 'DATETIME'],
|
|
28
|
+
[1184, 'DATETIME'],
|
|
29
|
+
[2950, 'TEXT'],
|
|
30
|
+
]);
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* @param {string[]} names
|
|
34
|
+
* @param {unknown[]} values
|
|
35
|
+
*/
|
|
36
|
+
export function makeRow(names, values) {
|
|
37
|
+
const row = {};
|
|
38
|
+
Object.defineProperty(row, 'length', { value: values.length });
|
|
39
|
+
for (let i = 0; i < values.length; i++) {
|
|
40
|
+
Object.defineProperty(row, i, { value: values[i] });
|
|
41
|
+
const name = names[i];
|
|
42
|
+
if (name !== undefined && !Object.hasOwn(row, name)) {
|
|
43
|
+
Object.defineProperty(row, name, {
|
|
44
|
+
value: values[i],
|
|
45
|
+
enumerable: true,
|
|
46
|
+
configurable: true,
|
|
47
|
+
writable: true,
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
return row;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* @param {import('pg').QueryArrayResult} res result of a `rowMode: 'array'` query
|
|
56
|
+
* @param {{ lastInsertRowid?: bigint, rowsAffected?: number, hideRows?: boolean }} [extra]
|
|
57
|
+
*/
|
|
58
|
+
export function toResultSet(res, extra = {}) {
|
|
59
|
+
const fields = res.fields ?? [];
|
|
60
|
+
const columns = extra.hideRows ? [] : fields.map((f) => f.name);
|
|
61
|
+
const columnTypes = extra.hideRows ? [] : fields.map((f) => OID_TYPES.get(f.dataTypeID) ?? '');
|
|
62
|
+
const raw = extra.hideRows ? [] : (res.rows ?? []);
|
|
63
|
+
const rows = raw.map((values) => makeRow(columns, /** @type {unknown[]} */ (values)));
|
|
64
|
+
const rowsAffected = extra.rowsAffected ?? res.rowCount ?? 0;
|
|
65
|
+
const lastInsertRowid = extra.lastInsertRowid;
|
|
66
|
+
return {
|
|
67
|
+
columns,
|
|
68
|
+
columnTypes,
|
|
69
|
+
rows,
|
|
70
|
+
rowsAffected,
|
|
71
|
+
lastInsertRowid,
|
|
72
|
+
toJSON() {
|
|
73
|
+
return {
|
|
74
|
+
columns,
|
|
75
|
+
columnTypes,
|
|
76
|
+
rows: raw,
|
|
77
|
+
rowsAffected,
|
|
78
|
+
lastInsertRowid: lastInsertRowid === undefined ? undefined : lastInsertRowid.toString(),
|
|
79
|
+
};
|
|
80
|
+
},
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** An empty result, for PRAGMA and other statements Postgres has no use for. */
|
|
85
|
+
export function emptyResultSet() {
|
|
86
|
+
return toResultSet({ fields: [], rows: [], rowCount: 0, command: '', oid: 0 });
|
|
87
|
+
}
|