@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/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
+ }