@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/sqlparse.js
ADDED
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Small, dependency-free helpers for looking at SQL text without a parser.
|
|
3
|
+
*
|
|
4
|
+
* Everything here works on a *code mask*: a string the same length as the
|
|
5
|
+
* SQL in which every character inside a string literal, quoted identifier or
|
|
6
|
+
* comment has been replaced by a space. Regexes run on the mask and the
|
|
7
|
+
* indexes they return are valid in the original text, so a rewrite never
|
|
8
|
+
* touches the inside of a literal such as `'$.a[0]'` or `'-- not a comment'`.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* @param {string} sql
|
|
13
|
+
* @returns {string} the same length as `sql`; literals, quoted identifiers
|
|
14
|
+
* and comments blanked to spaces, everything else preserved.
|
|
15
|
+
*/
|
|
16
|
+
export function codeMask(sql) {
|
|
17
|
+
const out = new Array(sql.length);
|
|
18
|
+
const len = sql.length;
|
|
19
|
+
let i = 0;
|
|
20
|
+
const blank = (from, to) => {
|
|
21
|
+
for (let k = from; k < to; k++) out[k] = sql[k] === '\n' ? '\n' : ' ';
|
|
22
|
+
};
|
|
23
|
+
while (i < len) {
|
|
24
|
+
const c = sql[i];
|
|
25
|
+
if (c === "'") {
|
|
26
|
+
let j = i + 1;
|
|
27
|
+
while (j < len) {
|
|
28
|
+
if (sql[j] === "'") {
|
|
29
|
+
if (sql[j + 1] === "'") {
|
|
30
|
+
j += 2;
|
|
31
|
+
continue;
|
|
32
|
+
}
|
|
33
|
+
break;
|
|
34
|
+
}
|
|
35
|
+
j++;
|
|
36
|
+
}
|
|
37
|
+
blank(i, Math.min(j + 1, len));
|
|
38
|
+
i = j + 1;
|
|
39
|
+
} else if (c === '"' || c === '`') {
|
|
40
|
+
// Quoted identifier. Kept visible as the quote character itself so a
|
|
41
|
+
// caller can still find the identifier's extent; the inside is blanked.
|
|
42
|
+
const j = sql.indexOf(c, i + 1);
|
|
43
|
+
const end = j === -1 ? len - 1 : j;
|
|
44
|
+
out[i] = c;
|
|
45
|
+
blank(i + 1, end);
|
|
46
|
+
if (end < len) out[end] = c;
|
|
47
|
+
i = end + 1;
|
|
48
|
+
} else if (c === '[' && /[A-Za-z_]/.test(sql[i + 1] ?? '')) {
|
|
49
|
+
// `[name]` identifier (SQLite accepts the MS Access style).
|
|
50
|
+
const j = sql.indexOf(']', i + 1);
|
|
51
|
+
const end = j === -1 ? len - 1 : j;
|
|
52
|
+
out[i] = '[';
|
|
53
|
+
blank(i + 1, end);
|
|
54
|
+
if (end < len) out[end] = ']';
|
|
55
|
+
i = end + 1;
|
|
56
|
+
} else if (c === '-' && sql[i + 1] === '-') {
|
|
57
|
+
const j = sql.indexOf('\n', i);
|
|
58
|
+
const end = j === -1 ? len : j;
|
|
59
|
+
blank(i, end);
|
|
60
|
+
i = end;
|
|
61
|
+
} else if (c === '/' && sql[i + 1] === '*') {
|
|
62
|
+
const j = sql.indexOf('*/', i + 2);
|
|
63
|
+
const end = j === -1 ? len : j + 2;
|
|
64
|
+
blank(i, end);
|
|
65
|
+
i = end;
|
|
66
|
+
} else {
|
|
67
|
+
out[i] = c;
|
|
68
|
+
i++;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
return out.join('');
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Split a script into statements at `;` outside literals and comments.
|
|
76
|
+
* A `CREATE TRIGGER ... BEGIN ... END;` body, which carries its own
|
|
77
|
+
* semicolons, is kept as one statement.
|
|
78
|
+
*
|
|
79
|
+
* @param {string} sql
|
|
80
|
+
* @returns {string[]} trimmed statements, empty ones dropped
|
|
81
|
+
*/
|
|
82
|
+
export function splitStatements(sql) {
|
|
83
|
+
const mask = codeMask(sql);
|
|
84
|
+
const out = [];
|
|
85
|
+
let start = 0;
|
|
86
|
+
let i = 0;
|
|
87
|
+
const len = sql.length;
|
|
88
|
+
while (i < len) {
|
|
89
|
+
// Skip leading whitespace so we can look at the first keywords.
|
|
90
|
+
while (i < len && /\s/.test(mask[i])) i++;
|
|
91
|
+
start = i;
|
|
92
|
+
const head = mask.slice(i, i + 64);
|
|
93
|
+
const isTrigger = /^create\s+(temp(orary)?\s+)?trigger\b/i.test(head);
|
|
94
|
+
let depth = 0;
|
|
95
|
+
let seenBegin = false;
|
|
96
|
+
while (i < len) {
|
|
97
|
+
if (isTrigger) {
|
|
98
|
+
const word = /^[A-Za-z_]+/.exec(mask.slice(i, i + 12));
|
|
99
|
+
if (word && (i === 0 || !/[A-Za-z0-9_]/.test(mask[i - 1]))) {
|
|
100
|
+
const w = word[0].toLowerCase();
|
|
101
|
+
if (w === 'begin' || w === 'case') {
|
|
102
|
+
depth++;
|
|
103
|
+
seenBegin = true;
|
|
104
|
+
} else if (w === 'end') depth--;
|
|
105
|
+
i += word[0].length;
|
|
106
|
+
continue;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
if (mask[i] === ';' && (!isTrigger || (seenBegin && depth <= 0))) break;
|
|
110
|
+
i++;
|
|
111
|
+
}
|
|
112
|
+
const stmt = sql.slice(start, i).trim();
|
|
113
|
+
if (stmt) out.push(stmt);
|
|
114
|
+
i++;
|
|
115
|
+
}
|
|
116
|
+
return out;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Split text at top-level commas (depth 0 in parentheses, outside literals).
|
|
121
|
+
*
|
|
122
|
+
* @param {string} text
|
|
123
|
+
* @returns {string[]} trimmed parts
|
|
124
|
+
*/
|
|
125
|
+
export function splitTopLevel(text, separator = ',') {
|
|
126
|
+
const mask = codeMask(text);
|
|
127
|
+
const parts = [];
|
|
128
|
+
let depth = 0;
|
|
129
|
+
let last = 0;
|
|
130
|
+
for (let i = 0; i < text.length; i++) {
|
|
131
|
+
const c = mask[i];
|
|
132
|
+
if (c === '(') depth++;
|
|
133
|
+
else if (c === ')') depth--;
|
|
134
|
+
else if (c === separator && depth === 0) {
|
|
135
|
+
parts.push(text.slice(last, i).trim());
|
|
136
|
+
last = i + 1;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
parts.push(text.slice(last).trim());
|
|
140
|
+
return parts.filter((p) => p.length);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Index of the `)` matching the `(` at `open`, or -1.
|
|
145
|
+
*
|
|
146
|
+
* @param {string} mask
|
|
147
|
+
* @param {number} open
|
|
148
|
+
*/
|
|
149
|
+
export function matchParen(mask, open) {
|
|
150
|
+
let depth = 0;
|
|
151
|
+
for (let i = open; i < mask.length; i++) {
|
|
152
|
+
if (mask[i] === '(') depth++;
|
|
153
|
+
else if (mask[i] === ')') {
|
|
154
|
+
depth--;
|
|
155
|
+
if (depth === 0) return i;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
return -1;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Find the next call of a function by name, e.g. `datetime(` at depth
|
|
163
|
+
* anywhere, starting at `from`.
|
|
164
|
+
*
|
|
165
|
+
* @param {string} sql
|
|
166
|
+
* @param {string} mask
|
|
167
|
+
* @param {string} name case-insensitive function name
|
|
168
|
+
* @param {number} [from]
|
|
169
|
+
* @returns {{ start: number, open: number, close: number, args: string[], argsText: string } | null}
|
|
170
|
+
*/
|
|
171
|
+
export function findCall(sql, mask, name, from = 0) {
|
|
172
|
+
const re = new RegExp(`(?<![A-Za-z0-9_."\`])${name}\\s*\\(`, 'ig');
|
|
173
|
+
re.lastIndex = from;
|
|
174
|
+
const m = re.exec(mask);
|
|
175
|
+
if (!m) return null;
|
|
176
|
+
const open = m.index + m[0].length - 1;
|
|
177
|
+
const close = matchParen(mask, open);
|
|
178
|
+
if (close === -1) return null;
|
|
179
|
+
const argsText = sql.slice(open + 1, close);
|
|
180
|
+
return { start: m.index, open, close, args: splitTopLevel(argsText), argsText };
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Rewrite every call of `name`, innermost calls after outer ones have been
|
|
185
|
+
* handled (the text is re-scanned after each edit). `fn` receives the parsed
|
|
186
|
+
* call and returns replacement text, or null to leave that call alone.
|
|
187
|
+
*
|
|
188
|
+
* @param {string} sql
|
|
189
|
+
* @param {string} name
|
|
190
|
+
* @param {(call: { args: string[], argsText: string }) => string | null} fn
|
|
191
|
+
* @returns {string}
|
|
192
|
+
*/
|
|
193
|
+
export function replaceCalls(sql, name, fn) {
|
|
194
|
+
let from = 0;
|
|
195
|
+
for (let guard = 0; guard < 10_000; guard++) {
|
|
196
|
+
const mask = codeMask(sql);
|
|
197
|
+
const call = findCall(sql, mask, name, from);
|
|
198
|
+
if (!call) return sql;
|
|
199
|
+
const out = fn(call);
|
|
200
|
+
if (out == null) {
|
|
201
|
+
from = call.open + 1;
|
|
202
|
+
continue;
|
|
203
|
+
}
|
|
204
|
+
sql = sql.slice(0, call.start) + out + sql.slice(call.close + 1);
|
|
205
|
+
from = call.start;
|
|
206
|
+
}
|
|
207
|
+
return sql;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Replace a regex on the code mask, keeping literals intact. The regex is
|
|
212
|
+
* applied to the mask; the replacement is spliced into the SQL at the same
|
|
213
|
+
* indexes. Groups in `replacement` (via a function) receive the *original*
|
|
214
|
+
* text of each match, not the masked one.
|
|
215
|
+
*
|
|
216
|
+
* @param {string} sql
|
|
217
|
+
* @param {RegExp} re must carry the `g` flag
|
|
218
|
+
* @param {string | ((match: string, ...groups: string[]) => string)} replacement
|
|
219
|
+
*/
|
|
220
|
+
export function replaceCode(sql, re, replacement) {
|
|
221
|
+
const mask = codeMask(sql);
|
|
222
|
+
let out = '';
|
|
223
|
+
let last = 0;
|
|
224
|
+
re.lastIndex = 0;
|
|
225
|
+
for (let m = re.exec(mask); m; m = re.exec(mask)) {
|
|
226
|
+
const original = sql.slice(m.index, m.index + m[0].length);
|
|
227
|
+
out += sql.slice(last, m.index);
|
|
228
|
+
if (typeof replacement === 'function') {
|
|
229
|
+
// Re-run the regex on the original slice so groups carry real text.
|
|
230
|
+
const local = new RegExp(re.source, re.flags.replace('g', ''));
|
|
231
|
+
const lm = local.exec(original);
|
|
232
|
+
out += replacement(original, ...(lm ? lm.slice(1) : []));
|
|
233
|
+
} else {
|
|
234
|
+
out += original.replace(new RegExp(re.source, re.flags.replace('g', '')), replacement);
|
|
235
|
+
}
|
|
236
|
+
last = m.index + m[0].length;
|
|
237
|
+
if (m[0].length === 0) re.lastIndex++;
|
|
238
|
+
}
|
|
239
|
+
return out + sql.slice(last);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* Strip the quotes off an identifier and fold an unquoted one to lower case,
|
|
244
|
+
* the way Postgres will.
|
|
245
|
+
*
|
|
246
|
+
* @param {string} ident
|
|
247
|
+
*/
|
|
248
|
+
export function unquote(ident) {
|
|
249
|
+
const t = ident.trim();
|
|
250
|
+
if (/^".*"$/.test(t)) return t.slice(1, -1).replace(/""/g, '"');
|
|
251
|
+
if (/^`.*`$/.test(t) || /^\[.*\]$/.test(t)) return t.slice(1, -1);
|
|
252
|
+
return t.toLowerCase();
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* Quote an identifier for Postgres.
|
|
257
|
+
*
|
|
258
|
+
* @param {string} name
|
|
259
|
+
*/
|
|
260
|
+
export function quoteIdent(name) {
|
|
261
|
+
return `"${String(name).replace(/"/g, '""')}"`;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* Quote an identifier only when it needs it, keeping schema qualification.
|
|
266
|
+
* @param {string} name possibly `schema.table`, possibly already quoted
|
|
267
|
+
*/
|
|
268
|
+
export function identSql(name) {
|
|
269
|
+
return name
|
|
270
|
+
.split('.')
|
|
271
|
+
.map((part) => (/^[a-z_][a-z0-9_]*$/.test(part) ? part : quoteIdent(part)))
|
|
272
|
+
.join('.');
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/**
|
|
276
|
+
* Is this a plain single-quoted string literal? Returns its value or null.
|
|
277
|
+
* @param {string} text
|
|
278
|
+
*/
|
|
279
|
+
export function literalValue(text) {
|
|
280
|
+
const t = text.trim();
|
|
281
|
+
if (!/^'.*'$/s.test(t)) return null;
|
|
282
|
+
return t.slice(1, -1).replace(/''/g, "'");
|
|
283
|
+
}
|