admindb 2.1.1 → 2.3.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/README.md +368 -335
- package/dist/app.js +227 -1
- package/dist/auth/config.d.ts +1 -1
- package/dist/auth/config.js +109 -1
- package/dist/auth/crypto.js +98 -1
- package/dist/auth/index.js +21 -1
- package/dist/auth/middleware.js +78 -1
- package/dist/auth/routes.js +64 -1
- package/dist/auth/types.js +7 -1
- package/dist/cli/args.d.ts +1 -1
- package/dist/cli/args.js +309 -1
- package/dist/cli/config.js +118 -1
- package/dist/cli/index.js +20 -1
- package/dist/cli/runner.js +208 -1
- package/dist/cli.js +4 -1
- package/dist/data/chain.d.ts +1 -0
- package/dist/data/chain.js +130 -0
- package/dist/data/datasets.d.ts +1 -1
- package/dist/data/datasets.js +171 -1
- package/dist/data/detector.d.ts +1 -1
- package/dist/data/detector.js +535 -1
- package/dist/data/engine.d.ts +1 -1
- package/dist/data/engine.js +586 -1
- package/dist/data/index.d.ts +1 -1
- package/dist/data/index.js +30 -1
- package/dist/data/prng.d.ts +1 -0
- package/dist/data/prng.js +73 -0
- package/dist/data/registry.d.ts +1 -0
- package/dist/data/registry.js +805 -0
- package/dist/data/schema-graph.d.ts +1 -0
- package/dist/data/schema-graph.js +210 -0
- package/dist/data/strategies.d.ts +1 -1
- package/dist/data/strategies.js +100 -1
- package/dist/data/templates.d.ts +1 -0
- package/dist/data/templates.js +134 -0
- package/dist/data/types.d.ts +1 -1
- package/dist/data/types.js +5 -1
- package/dist/data/validator.d.ts +1 -0
- package/dist/data/validator.js +160 -0
- package/dist/db/database.d.ts +1 -1
- package/dist/db/database.js +472 -1
- package/dist/db/export.d.ts +1 -1
- package/dist/db/export.js +67 -1
- package/dist/db/filters.d.ts +1 -1
- package/dist/db/filters.js +119 -1
- package/dist/db/index.js +24 -1
- package/dist/db/introspection.js +164 -1
- package/dist/db/manager.js +327 -1
- package/dist/db/migrations.js +100 -1
- package/dist/db/postgres.d.ts +1 -1
- package/dist/db/postgres.js +687 -1
- package/dist/db/types.d.ts +1 -1
- package/dist/db/types.js +6 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +52 -1
- package/dist/public/css/app.css +1 -1
- package/dist/public/js/api.js +1 -1
- package/dist/public/js/browse.js +1 -1
- package/dist/public/js/common.js +1 -0
- package/dist/public/js/databases.js +1 -1
- package/dist/public/js/designer.js +1 -1
- package/dist/public/js/forms.js +1 -1
- package/dist/public/js/home.js +1 -0
- package/dist/public/js/inspector.js +1 -1
- package/dist/public/js/query.js +1 -1
- package/dist/public/js/schema.js +1 -1
- package/dist/public/js/seed.js +1 -1
- package/dist/routes/api/erd.d.ts +1 -0
- package/dist/routes/api/erd.js +45 -0
- package/dist/routes/api/helpers.d.ts +1 -1
- package/dist/routes/api/helpers.js +213 -1
- package/dist/routes/api/import-export.js +82 -1
- package/dist/routes/api/index.js +48 -1
- package/dist/routes/api/query.js +74 -1
- package/dist/routes/api/rows.js +358 -1
- package/dist/routes/api/seed.js +167 -1
- package/dist/routes/api/tables.js +216 -1
- package/dist/routes/databases.js +210 -1
- package/dist/routes/index.js +19 -1
- package/dist/routes/pages.js +446 -1
- package/dist/serverless.js +149 -1
- package/dist/sql/classifier.js +24 -1
- package/dist/sql/error-analyzer.d.ts +1 -0
- package/dist/sql/error-analyzer.js +550 -0
- package/dist/sql/generator.d.ts +1 -1
- package/dist/sql/generator.js +245 -1
- package/dist/sql/index.js +18 -1
- package/dist/types/api.d.ts +1 -1
- package/dist/types/api.js +2 -1
- package/dist/utils/colors.js +54 -1
- package/dist/utils/common.d.ts +1 -1
- package/dist/utils/common.js +185 -1
- package/dist/utils/csv.d.ts +1 -1
- package/dist/utils/csv.js +61 -1
- package/dist/utils/datatype.js +167 -1
- package/dist/utils/icons.js +81 -1
- package/dist/utils/index.js +20 -1
- package/dist/utils/logger.js +36 -1
- package/dist/views/layouts/main.hbs +1 -74
- package/dist/views/pages/databases.hbs +1 -233
- package/dist/views/pages/designer.hbs +1 -89
- package/dist/views/pages/erd.hbs +1 -0
- package/dist/views/pages/error.hbs +1 -10
- package/dist/views/pages/form.hbs +1 -59
- package/dist/views/pages/home.hbs +1 -124
- package/dist/views/pages/info.hbs +1 -0
- package/dist/views/pages/login.hbs +1 -98
- package/dist/views/pages/query.hbs +1 -87
- package/dist/views/pages/schema.hbs +1 -298
- package/dist/views/pages/seed-select.hbs +251 -0
- package/dist/views/pages/seed.hbs +273 -167
- package/dist/views/pages/table.hbs +1 -327
- package/dist/views/partials/navbar.hbs +1 -83
- package/dist/views/partials/sidebar.hbs +1 -117
- package/docs/API.md +149 -144
- package/docs/EXAMPLES.md +579 -576
- package/docs/SECURITY.md +265 -265
- package/package.json +79 -78
- package/dist/public/js/icons.js +0 -1
|
@@ -0,0 +1,550 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.buildErrorSnippet = buildErrorSnippet;
|
|
4
|
+
exports.offsetToLineCol = offsetToLineCol;
|
|
5
|
+
exports.maskCommentsAndStrings = maskCommentsAndStrings;
|
|
6
|
+
exports.findTokenPosition = findTokenPosition;
|
|
7
|
+
exports.analyzeSqlError = analyzeSqlError;
|
|
8
|
+
function levenshteinDistance(a, b) {
|
|
9
|
+
const an = a.length;
|
|
10
|
+
const bn = b.length;
|
|
11
|
+
if (an === 0)
|
|
12
|
+
return bn;
|
|
13
|
+
if (bn === 0)
|
|
14
|
+
return an;
|
|
15
|
+
const matrix = [];
|
|
16
|
+
for (let i = 0; i <= bn; i++)
|
|
17
|
+
matrix[i] = [i];
|
|
18
|
+
for (let j = 0; j <= an; j++)
|
|
19
|
+
matrix[0][j] = j;
|
|
20
|
+
for (let i = 1; i <= bn; i++) {
|
|
21
|
+
for (let j = 1; j <= an; j++) {
|
|
22
|
+
if (b.charAt(i - 1).toLowerCase() === a.charAt(j - 1).toLowerCase()) {
|
|
23
|
+
matrix[i][j] = matrix[i - 1][j - 1];
|
|
24
|
+
}
|
|
25
|
+
else {
|
|
26
|
+
matrix[i][j] = Math.min(matrix[i - 1][j - 1] + 1, matrix[i][j - 1] + 1, matrix[i - 1][j] + 1);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
return matrix[bn][an];
|
|
31
|
+
}
|
|
32
|
+
function findClosestMatch(target, candidates, maxDist = 3) {
|
|
33
|
+
if (!candidates.length || !target)
|
|
34
|
+
return null;
|
|
35
|
+
let best = null;
|
|
36
|
+
let bestDist = maxDist + 1;
|
|
37
|
+
for (const c of candidates) {
|
|
38
|
+
if (c.toLowerCase() === target.toLowerCase())
|
|
39
|
+
continue;
|
|
40
|
+
const dist = levenshteinDistance(target, c);
|
|
41
|
+
if (dist < bestDist) {
|
|
42
|
+
bestDist = dist;
|
|
43
|
+
best = c;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return bestDist <= maxDist ? best : null;
|
|
47
|
+
}
|
|
48
|
+
function buildErrorSnippet(sql, line, column) {
|
|
49
|
+
const lines = sql.split(/\r?\n/);
|
|
50
|
+
const targetLineIdx = Math.max(0, Math.min(lines.length - 1, line - 1));
|
|
51
|
+
const startIdx = Math.max(0, targetLineIdx - 1);
|
|
52
|
+
const endIdx = Math.min(lines.length - 1, targetLineIdx + 1);
|
|
53
|
+
const maxLineNumLen = Math.max(2, String(endIdx + 1).length);
|
|
54
|
+
const out = [];
|
|
55
|
+
for (let i = startIdx; i <= endIdx; i++) {
|
|
56
|
+
const lineNum = String(i + 1).padStart(maxLineNumLen, ' ');
|
|
57
|
+
const isError = i === targetLineIdx;
|
|
58
|
+
const prefix = isError ? `> ${lineNum} | ` : ` ${lineNum} | `;
|
|
59
|
+
out.push(`${prefix}${lines[i]}`);
|
|
60
|
+
if (isError) {
|
|
61
|
+
const colPad = Math.max(0, column - 1);
|
|
62
|
+
const indent = ' '.repeat(maxLineNumLen + 5 + colPad);
|
|
63
|
+
out.push(`${indent}^`);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
return out.join('\n');
|
|
67
|
+
}
|
|
68
|
+
function offsetToLineCol(sql, offset) {
|
|
69
|
+
let line = 1;
|
|
70
|
+
let col = 1;
|
|
71
|
+
const target = Math.max(1, Math.min(sql.length, offset));
|
|
72
|
+
for (let i = 0; i < target - 1; i++) {
|
|
73
|
+
if (sql[i] === '\n') {
|
|
74
|
+
line++;
|
|
75
|
+
col = 1;
|
|
76
|
+
}
|
|
77
|
+
else {
|
|
78
|
+
col++;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
return { line, column: col };
|
|
82
|
+
}
|
|
83
|
+
function maskCommentsAndStrings(sql) {
|
|
84
|
+
let out = '';
|
|
85
|
+
let inSingleQuote = false;
|
|
86
|
+
let inDoubleQuote = false;
|
|
87
|
+
let inLineComment = false;
|
|
88
|
+
let inBlockComment = false;
|
|
89
|
+
for (let i = 0; i < sql.length; i++) {
|
|
90
|
+
const ch = sql[i];
|
|
91
|
+
const next = sql[i + 1];
|
|
92
|
+
if (inLineComment) {
|
|
93
|
+
if (ch === '\n') {
|
|
94
|
+
inLineComment = false;
|
|
95
|
+
out += '\n';
|
|
96
|
+
}
|
|
97
|
+
else {
|
|
98
|
+
out += ' ';
|
|
99
|
+
}
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
if (inBlockComment) {
|
|
103
|
+
if (ch === '*' && next === '/') {
|
|
104
|
+
inBlockComment = false;
|
|
105
|
+
out += ' ';
|
|
106
|
+
i++;
|
|
107
|
+
}
|
|
108
|
+
else if (ch === '\n') {
|
|
109
|
+
out += '\n';
|
|
110
|
+
}
|
|
111
|
+
else {
|
|
112
|
+
out += ' ';
|
|
113
|
+
}
|
|
114
|
+
continue;
|
|
115
|
+
}
|
|
116
|
+
if (inSingleQuote) {
|
|
117
|
+
if (ch === "'") {
|
|
118
|
+
if (next === "'") {
|
|
119
|
+
out += ' ';
|
|
120
|
+
i++;
|
|
121
|
+
}
|
|
122
|
+
else {
|
|
123
|
+
inSingleQuote = false;
|
|
124
|
+
out += ' ';
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
else if (ch === '\n') {
|
|
128
|
+
out += '\n';
|
|
129
|
+
}
|
|
130
|
+
else {
|
|
131
|
+
out += ' ';
|
|
132
|
+
}
|
|
133
|
+
continue;
|
|
134
|
+
}
|
|
135
|
+
if (inDoubleQuote) {
|
|
136
|
+
if (ch === '"') {
|
|
137
|
+
if (next === '"') {
|
|
138
|
+
out += ' ';
|
|
139
|
+
i++;
|
|
140
|
+
}
|
|
141
|
+
else {
|
|
142
|
+
inDoubleQuote = false;
|
|
143
|
+
out += ' ';
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
else if (ch === '\n') {
|
|
147
|
+
out += '\n';
|
|
148
|
+
}
|
|
149
|
+
else {
|
|
150
|
+
out += ' ';
|
|
151
|
+
}
|
|
152
|
+
continue;
|
|
153
|
+
}
|
|
154
|
+
if (ch === '-' && next === '-') {
|
|
155
|
+
inLineComment = true;
|
|
156
|
+
out += ' ';
|
|
157
|
+
i++;
|
|
158
|
+
continue;
|
|
159
|
+
}
|
|
160
|
+
if (ch === '/' && next === '*') {
|
|
161
|
+
inBlockComment = true;
|
|
162
|
+
out += ' ';
|
|
163
|
+
i++;
|
|
164
|
+
continue;
|
|
165
|
+
}
|
|
166
|
+
if (ch === "'") {
|
|
167
|
+
inSingleQuote = true;
|
|
168
|
+
out += ' ';
|
|
169
|
+
continue;
|
|
170
|
+
}
|
|
171
|
+
if (ch === '"') {
|
|
172
|
+
inDoubleQuote = true;
|
|
173
|
+
out += ' ';
|
|
174
|
+
continue;
|
|
175
|
+
}
|
|
176
|
+
out += ch;
|
|
177
|
+
}
|
|
178
|
+
return out;
|
|
179
|
+
}
|
|
180
|
+
function findTokenPosition(sql, token) {
|
|
181
|
+
if (!token)
|
|
182
|
+
return null;
|
|
183
|
+
const masked = maskCommentsAndStrings(sql);
|
|
184
|
+
const maskedLines = masked.split(/\r?\n/);
|
|
185
|
+
const originalLines = sql.split(/\r?\n/);
|
|
186
|
+
const isWordToken = /^[A-Za-z0-9_$]+$/.test(token);
|
|
187
|
+
if (token === '(') {
|
|
188
|
+
for (let l = 0; l < maskedLines.length; l++) {
|
|
189
|
+
const lineText = maskedLines[l];
|
|
190
|
+
const dfMatch = lineText.match(/DEFAULT\s+[a-zA-Z0-9_]+\s*(\()/i);
|
|
191
|
+
if (dfMatch && dfMatch.index != null) {
|
|
192
|
+
const parenCol = lineText.indexOf('(', dfMatch.index + 7);
|
|
193
|
+
if (parenCol !== -1)
|
|
194
|
+
return { line: l + 1, column: parenCol + 1 };
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
for (let l = 0; l < maskedLines.length; l++) {
|
|
198
|
+
const lineText = maskedLines[l];
|
|
199
|
+
if (/\bFROM\s+\(/i.test(lineText) || /,\s*\(/i.test(lineText)) {
|
|
200
|
+
const parenCol = lineText.indexOf('(');
|
|
201
|
+
if (parenCol !== -1)
|
|
202
|
+
return { line: l + 1, column: parenCol + 1 };
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
if (isWordToken) {
|
|
207
|
+
const wordRegex = new RegExp(`\\b${token}\\b`, 'i');
|
|
208
|
+
for (let l = 0; l < maskedLines.length; l++) {
|
|
209
|
+
const m = maskedLines[l].match(wordRegex);
|
|
210
|
+
if (m && m.index != null) {
|
|
211
|
+
return { line: l + 1, column: m.index + 1 };
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
for (let l = 0; l < maskedLines.length; l++) {
|
|
216
|
+
const colIdx = maskedLines[l].toLowerCase().indexOf(token.toLowerCase());
|
|
217
|
+
if (colIdx !== -1) {
|
|
218
|
+
return { line: l + 1, column: colIdx + 1 };
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
for (let l = 0; l < originalLines.length; l++) {
|
|
222
|
+
const colIdx = originalLines[l].toLowerCase().indexOf(token.toLowerCase());
|
|
223
|
+
if (colIdx !== -1) {
|
|
224
|
+
return { line: l + 1, column: colIdx + 1 };
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
return null;
|
|
228
|
+
}
|
|
229
|
+
async function analyzeSqlError(sql, rawError, db) {
|
|
230
|
+
const rawMsg = rawError instanceof Error ? rawError.message : String(rawError ?? 'Unknown error');
|
|
231
|
+
const details = {
|
|
232
|
+
message: rawMsg,
|
|
233
|
+
raw: rawMsg,
|
|
234
|
+
};
|
|
235
|
+
const cleanSql = sql.trim();
|
|
236
|
+
const lines = sql.split(/\r?\n/);
|
|
237
|
+
const pgPos = rawError?.position;
|
|
238
|
+
if (pgPos != null) {
|
|
239
|
+
const numPos = Number(pgPos);
|
|
240
|
+
if (Number.isFinite(numPos) && numPos > 0) {
|
|
241
|
+
const pos = offsetToLineCol(sql, numPos);
|
|
242
|
+
details.line = pos.line;
|
|
243
|
+
details.column = pos.column;
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
let openParens = 0;
|
|
247
|
+
let closeParens = 0;
|
|
248
|
+
let singleQuotes = 0;
|
|
249
|
+
let doubleQuotes = 0;
|
|
250
|
+
let inSingleQuote = false;
|
|
251
|
+
let inDoubleQuote = false;
|
|
252
|
+
let inComment = false;
|
|
253
|
+
for (let i = 0; i < sql.length; i++) {
|
|
254
|
+
const ch = sql[i];
|
|
255
|
+
const next = sql[i + 1];
|
|
256
|
+
if (!inSingleQuote && !inDoubleQuote) {
|
|
257
|
+
if (ch === '-' && next === '-') {
|
|
258
|
+
while (i < sql.length && sql[i] !== '\n')
|
|
259
|
+
i++;
|
|
260
|
+
continue;
|
|
261
|
+
}
|
|
262
|
+
if (ch === '/' && next === '*') {
|
|
263
|
+
i += 2;
|
|
264
|
+
while (i < sql.length - 1 && !(sql[i] === '*' && sql[i + 1] === '/'))
|
|
265
|
+
i++;
|
|
266
|
+
i++;
|
|
267
|
+
continue;
|
|
268
|
+
}
|
|
269
|
+
if (ch === '(')
|
|
270
|
+
openParens++;
|
|
271
|
+
if (ch === ')')
|
|
272
|
+
closeParens++;
|
|
273
|
+
}
|
|
274
|
+
if (ch === "'" && !inDoubleQuote) {
|
|
275
|
+
singleQuotes++;
|
|
276
|
+
inSingleQuote = !inSingleQuote;
|
|
277
|
+
}
|
|
278
|
+
if (ch === '"' && !inSingleQuote) {
|
|
279
|
+
doubleQuotes++;
|
|
280
|
+
inDoubleQuote = !inDoubleQuote;
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
const nearMatch = rawMsg.match(/near\s+"([^"]+)":\s*syntax error/i) || rawMsg.match(/syntax error at or near\s+"?([^"\s]+)"?/i);
|
|
284
|
+
if (nearMatch) {
|
|
285
|
+
const token = nearMatch[1];
|
|
286
|
+
details.token = token;
|
|
287
|
+
if (!details.line) {
|
|
288
|
+
const pos = findTokenPosition(sql, token);
|
|
289
|
+
if (pos) {
|
|
290
|
+
details.line = pos.line;
|
|
291
|
+
details.column = pos.column;
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
if (token === '(') {
|
|
295
|
+
const defaultFnMatch = sql.match(/DEFAULT\s+([a-zA-Z0-9_]+)\s*\(/i);
|
|
296
|
+
if (defaultFnMatch) {
|
|
297
|
+
const fnName = defaultFnMatch[1];
|
|
298
|
+
if (fnName.toLowerCase() === 'gen_random_uuid') {
|
|
299
|
+
details.cause = `In SQLite, default expressions calling functions must be enclosed in parentheses "DEFAULT (expr)". Additionally, "gen_random_uuid()" is a PostgreSQL function not built into SQLite.`;
|
|
300
|
+
details.suggestion = `For SQLite, use "DEFAULT (lower(hex(randomblob(16))))" or assign UUIDs in your application. Also replace "NOW()" with "CURRENT_TIMESTAMP".`;
|
|
301
|
+
}
|
|
302
|
+
else if (fnName.toLowerCase() === 'now') {
|
|
303
|
+
details.cause = `In SQLite, "NOW()" is not a built-in function (PostgreSQL only), and function default expressions must be enclosed in parentheses.`;
|
|
304
|
+
details.suggestion = `Replace "DEFAULT NOW()" with "DEFAULT CURRENT_TIMESTAMP" or "DEFAULT (datetime('now'))".`;
|
|
305
|
+
}
|
|
306
|
+
else {
|
|
307
|
+
details.cause = `In SQLite, function calls and expressions in a DEFAULT constraint must be enclosed in parentheses, e.g. "DEFAULT (${fnName}(...))" instead of "DEFAULT ${fnName}(...)".`;
|
|
308
|
+
details.suggestion = `Wrap the default function expression in parentheses: DEFAULT (${fnName}(...))`;
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
else if (/\bfrom\s+\(/i.test(sql) && !/\bfrom\s+\([^)]+\)\s+as\s+[a-z0-9_]+/i.test(sql) && !/\bfrom\s+\([^)]+\)\s+[a-z0-9_]+/i.test(sql)) {
|
|
312
|
+
details.cause = 'Subqueries in the FROM clause require a table alias.';
|
|
313
|
+
details.suggestion = 'Add a subquery alias after the closing parenthesis, e.g. FROM (SELECT ...) AS sub_table';
|
|
314
|
+
}
|
|
315
|
+
else if (openParens !== closeParens) {
|
|
316
|
+
details.cause = `Mismatched parentheses: Found ${openParens} opening '(' and ${closeParens} closing ')'.`;
|
|
317
|
+
details.suggestion = openParens > closeParens
|
|
318
|
+
? `Add ${openParens - closeParens} closing ')' to balance the expression.`
|
|
319
|
+
: `Remove ${closeParens - openParens} extra closing ')' from the query.`;
|
|
320
|
+
}
|
|
321
|
+
else if (/,\s*\(/i.test(sql) && !/\bvalues\s*\(/i.test(sql)) {
|
|
322
|
+
details.cause = 'Unexpected parenthesis following a comma in the expression list.';
|
|
323
|
+
details.suggestion = 'Check for extra commas or ensure the parenthesized expression is part of a valid function or subquery.';
|
|
324
|
+
}
|
|
325
|
+
else {
|
|
326
|
+
details.cause = 'An unexpected opening parenthesis "(" was found where an identifier or expression was expected.';
|
|
327
|
+
details.suggestion = 'Check for missing column/table names before "(", or verify subquery syntax.';
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
else if (token === ')') {
|
|
331
|
+
if (openParens !== closeParens) {
|
|
332
|
+
details.cause = `Mismatched parentheses: Found ${openParens} opening '(' and ${closeParens} closing ')'.`;
|
|
333
|
+
details.suggestion = `Remove the extra closing parenthesis ')' or add a matching opening '('.`;
|
|
334
|
+
}
|
|
335
|
+
else if (/,\s*\)/i.test(sql)) {
|
|
336
|
+
details.cause = 'Trailing comma detected before the closing parenthesis ")".';
|
|
337
|
+
details.suggestion = 'Remove the trailing comma before ")", e.g. (col1, col2) instead of (col1, col2,).';
|
|
338
|
+
}
|
|
339
|
+
else {
|
|
340
|
+
details.cause = 'An unexpected closing parenthesis ")" was encountered.';
|
|
341
|
+
details.suggestion = 'Verify that every opening parenthesis has a corresponding closing parenthesis and valid enclosed expressions.';
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
else if (token === ',') {
|
|
345
|
+
details.cause = 'Unexpected comma in SQL statement.';
|
|
346
|
+
details.suggestion = 'Remove duplicate or misplaced commas (e.g. trailing commas before FROM, WHERE, or clauses).';
|
|
347
|
+
}
|
|
348
|
+
else if (token === ';') {
|
|
349
|
+
details.cause = 'Premature semicolon or empty statement.';
|
|
350
|
+
details.suggestion = 'Check if a clause before the semicolon is incomplete, or remove extra semicolons.';
|
|
351
|
+
}
|
|
352
|
+
else if (token.toUpperCase() === 'FROM' && /,\s*FROM/i.test(sql)) {
|
|
353
|
+
details.cause = 'Trailing comma before the FROM keyword.';
|
|
354
|
+
details.suggestion = 'Remove the trailing comma right before FROM in your SELECT column list.';
|
|
355
|
+
}
|
|
356
|
+
else if (token.toUpperCase() === 'WHERE' && /,\s*WHERE/i.test(sql)) {
|
|
357
|
+
details.cause = 'Trailing comma before the WHERE keyword.';
|
|
358
|
+
details.suggestion = 'Remove the trailing comma before WHERE (e.g. in UPDATE ... SET col = val, WHERE).';
|
|
359
|
+
}
|
|
360
|
+
else {
|
|
361
|
+
details.cause = `Syntax error near keyword or identifier "${token}".`;
|
|
362
|
+
details.suggestion = `Check the syntax immediately preceding "${token}" for missing keywords, operators, or commas.`;
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
const noTableMatch = rawMsg.match(/no such table:\s*([^\s:]+)/i) || rawMsg.match(/relation "([^"]+)" does not exist/i);
|
|
366
|
+
if (noTableMatch) {
|
|
367
|
+
const tableName = noTableMatch[1].replace(/["']/g, '');
|
|
368
|
+
details.token = tableName;
|
|
369
|
+
if (!details.line) {
|
|
370
|
+
const pos = findTokenPosition(sql, tableName);
|
|
371
|
+
if (pos) {
|
|
372
|
+
details.line = pos.line;
|
|
373
|
+
details.column = pos.column;
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
details.cause = `Table "${tableName}" does not exist in the database.`;
|
|
377
|
+
let suggestion = `Verify the table name spelling or create the table first.`;
|
|
378
|
+
if (db) {
|
|
379
|
+
try {
|
|
380
|
+
const tablesResult = await db.listTables();
|
|
381
|
+
if (tablesResult.success && tablesResult.data) {
|
|
382
|
+
const knownTables = tablesResult.data.map((t) => t.name);
|
|
383
|
+
const match = findClosestMatch(tableName, knownTables);
|
|
384
|
+
if (match) {
|
|
385
|
+
suggestion = `Did you mean table "${match}"?`;
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
catch { }
|
|
390
|
+
}
|
|
391
|
+
details.suggestion = suggestion;
|
|
392
|
+
}
|
|
393
|
+
const tableExistsMatch = rawMsg.match(/table\s+([^\s:]+)\s+already exists/i) || rawMsg.match(/relation "([^"]+)" already exists/i);
|
|
394
|
+
if (tableExistsMatch) {
|
|
395
|
+
const tableName = tableExistsMatch[1].replace(/["']/g, '');
|
|
396
|
+
details.token = tableName;
|
|
397
|
+
for (let l = 0; l < lines.length; l++) {
|
|
398
|
+
const lineText = lines[l];
|
|
399
|
+
const reg = new RegExp(`CREATE\\s+TABLE\\s+(?:IF\\s+NOT\\s+EXISTS\\s+)?["'\`]?${tableName}["'\`]?`, 'i');
|
|
400
|
+
if (reg.test(lineText)) {
|
|
401
|
+
details.line = l + 1;
|
|
402
|
+
details.column = lineText.toLowerCase().indexOf(tableName.toLowerCase()) + 1;
|
|
403
|
+
break;
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
if (!details.line) {
|
|
407
|
+
const pos = findTokenPosition(sql, tableName);
|
|
408
|
+
if (pos) {
|
|
409
|
+
details.line = pos.line;
|
|
410
|
+
details.column = pos.column;
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
details.cause = `A table named "${tableName}" already exists in this database.`;
|
|
414
|
+
details.suggestion = `Use "CREATE TABLE IF NOT EXISTS ${tableName} ..." or drop the existing table first using "DROP TABLE IF EXISTS ${tableName};".`;
|
|
415
|
+
}
|
|
416
|
+
const indexExistsMatch = rawMsg.match(/index\s+([^\s:]+)\s+already exists/i);
|
|
417
|
+
if (indexExistsMatch) {
|
|
418
|
+
const indexName = indexExistsMatch[1].replace(/["']/g, '');
|
|
419
|
+
details.token = indexName;
|
|
420
|
+
const pos = findTokenPosition(sql, indexName);
|
|
421
|
+
if (pos) {
|
|
422
|
+
details.line = pos.line;
|
|
423
|
+
details.column = pos.column;
|
|
424
|
+
}
|
|
425
|
+
details.cause = `An index named "${indexName}" already exists in the database.`;
|
|
426
|
+
details.suggestion = `Use "CREATE INDEX IF NOT EXISTS ${indexName} ..." or drop the existing index with "DROP INDEX IF EXISTS ${indexName};".`;
|
|
427
|
+
}
|
|
428
|
+
if (/non-deterministic functions prohibited in generated columns/i.test(rawMsg)) {
|
|
429
|
+
for (let l = 0; l < lines.length; l++) {
|
|
430
|
+
const lineText = lines[l];
|
|
431
|
+
if (/GENERATED\s+ALWAYS\s+AS/i.test(lineText) && /CURRENT_DATE|CURRENT_TIMESTAMP|datetime|random|now/i.test(lineText)) {
|
|
432
|
+
details.line = l + 1;
|
|
433
|
+
details.column = lineText.search(/CURRENT_DATE|CURRENT_TIMESTAMP|datetime|random|now/i) + 1;
|
|
434
|
+
break;
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
details.cause = `SQLite does not permit non-deterministic expressions (like CURRENT_DATE, CURRENT_TIMESTAMP, or datetime('now')) in GENERATED (computed) column definitions because their output varies over time.`;
|
|
438
|
+
details.suggestion = `Remove CURRENT_DATE from the GENERATED ALWAYS AS clause, or evaluate active status dynamically in queries (e.g. SELECT (end_date IS NULL OR end_date > CURRENT_DATE) AS is_active).`;
|
|
439
|
+
}
|
|
440
|
+
const noColMatch = rawMsg.match(/no such column:\s*([^\s:]+)/i) || rawMsg.match(/column "([^"]+)" does not exist/i);
|
|
441
|
+
if (noColMatch) {
|
|
442
|
+
const colName = noColMatch[1].replace(/["']/g, '');
|
|
443
|
+
details.token = colName;
|
|
444
|
+
if (!details.line) {
|
|
445
|
+
const pos = findTokenPosition(sql, colName);
|
|
446
|
+
if (pos) {
|
|
447
|
+
details.line = pos.line;
|
|
448
|
+
details.column = pos.column;
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
details.cause = `Column "${colName}" was not found in the referenced tables.`;
|
|
452
|
+
details.suggestion = `Check for typos in column name "${colName}", or use single quotes '${colName}' if this was intended as a text string.`;
|
|
453
|
+
}
|
|
454
|
+
const tableHasNoColMatch = rawMsg.match(/table\s+([^\s]+)\s+has no column named\s+([^\s:]+)/i);
|
|
455
|
+
if (tableHasNoColMatch) {
|
|
456
|
+
const targetTable = tableHasNoColMatch[1];
|
|
457
|
+
const targetCol = tableHasNoColMatch[2];
|
|
458
|
+
details.token = targetCol;
|
|
459
|
+
if (!details.line) {
|
|
460
|
+
const pos = findTokenPosition(sql, targetCol);
|
|
461
|
+
if (pos) {
|
|
462
|
+
details.line = pos.line;
|
|
463
|
+
details.column = pos.column;
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
details.cause = `Table "${targetTable}" has no column named "${targetCol}".`;
|
|
467
|
+
details.suggestion = `Check the column names defined on "${targetTable}" or alter the table to add "${targetCol}".`;
|
|
468
|
+
}
|
|
469
|
+
if (/incomplete input/i.test(rawMsg)) {
|
|
470
|
+
details.line = lines.length;
|
|
471
|
+
details.column = lines[lines.length - 1].length + 1;
|
|
472
|
+
if (inSingleQuote) {
|
|
473
|
+
details.cause = 'Unclosed single quote string literal.';
|
|
474
|
+
details.suggestion = 'Add a closing single quote (\') to terminate the string literal.';
|
|
475
|
+
}
|
|
476
|
+
else if (openParens > closeParens) {
|
|
477
|
+
details.cause = `Unclosed parenthesis: Found ${openParens} opening '(' but only ${closeParens} closing ')'.`;
|
|
478
|
+
details.suggestion = `Add ${openParens - closeParens} closing ')' at the end of the statement.`;
|
|
479
|
+
}
|
|
480
|
+
else {
|
|
481
|
+
details.cause = 'The SQL statement is incomplete and ended prematurely.';
|
|
482
|
+
details.suggestion = 'Complete the statement by providing the required clause arguments or closing brackets/quotes.';
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
const uniqueMatch = rawMsg.match(/UNIQUE constraint failed:\s*(.+)/i) || rawMsg.match(/duplicate key value violates unique constraint\s*"?([^"\s]+)?"?/i);
|
|
486
|
+
if (uniqueMatch) {
|
|
487
|
+
const rawTarget = (uniqueMatch[1] || '').trim();
|
|
488
|
+
const parts = rawTarget.split(/[,\s]+/).map((s) => s.trim()).filter(Boolean);
|
|
489
|
+
const firstPart = parts[0] || '';
|
|
490
|
+
const colName = firstPart.includes('.') ? firstPart.split('.').pop() : firstPart;
|
|
491
|
+
const tableName = firstPart.includes('.') ? firstPart.split('.')[0] : '';
|
|
492
|
+
if (colName && !details.line) {
|
|
493
|
+
const pos = findTokenPosition(sql, colName);
|
|
494
|
+
if (pos) {
|
|
495
|
+
details.line = pos.line;
|
|
496
|
+
details.column = pos.column;
|
|
497
|
+
details.token = colName;
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
if (tableName && colName) {
|
|
501
|
+
details.cause = `A UNIQUE constraint on column "${colName}" in table "${tableName}" was violated because one of the rows you are inserting has an email/value that already exists in the database (or is duplicated in your batch).`;
|
|
502
|
+
}
|
|
503
|
+
else {
|
|
504
|
+
details.cause = `A unique constraint was violated (${rawTarget || 'duplicate key'}). A row with these values already exists in the database.`;
|
|
505
|
+
}
|
|
506
|
+
details.suggestion = `• To skip duplicate rows and insert only new records, use "INSERT OR IGNORE INTO ${tableName || 'table'} ..."\n• To overwrite existing records, use "INSERT OR REPLACE INTO ${tableName || 'table'} ..."\n• Or use DELETE FROM ${tableName || 'table'}; to clear previous data before re-inserting.`;
|
|
507
|
+
}
|
|
508
|
+
const fkMatch = rawMsg.match(/FOREIGN KEY constraint failed/i) || rawMsg.match(/violates foreign key constraint/i);
|
|
509
|
+
if (fkMatch) {
|
|
510
|
+
details.cause = 'Foreign key constraint violation: A foreign key column references a parent ID that does not exist in the referenced table.';
|
|
511
|
+
details.suggestion = '• Ensure referenced parent records exist before inserting child records.\n• If you deleted and re-inserted rows in SQLite, AUTOINCREMENT assigns new IDs (e.g. 17+ instead of 1). Either supply explicit IDs in your INSERTs (e.g. INSERT INTO users (id, ...)), or reset sequence counters with "DELETE FROM sqlite_sequence;" before re-seeding.';
|
|
512
|
+
}
|
|
513
|
+
const notNullMatch = rawMsg.match(/NOT NULL constraint failed:\s*(.+)/i) || rawMsg.match(/null value in column "([^"]+)" violates not-null constraint/i);
|
|
514
|
+
if (notNullMatch) {
|
|
515
|
+
const col = notNullMatch[1];
|
|
516
|
+
details.cause = `Column "${col}" has a NOT NULL constraint and cannot be NULL.`;
|
|
517
|
+
details.suggestion = `Provide a non-NULL value for "${col}" or define a DEFAULT value on the column.`;
|
|
518
|
+
}
|
|
519
|
+
const checkMatch = rawMsg.match(/CHECK constraint failed:\s*(.+)/i) || rawMsg.match(/violates check constraint/i);
|
|
520
|
+
if (checkMatch) {
|
|
521
|
+
details.cause = `CHECK constraint condition failed (${checkMatch[1] || 'condition not satisfied'}).`;
|
|
522
|
+
details.suggestion = 'Ensure the inserted/updated values satisfy all CHECK constraint expressions on the table.';
|
|
523
|
+
}
|
|
524
|
+
const ambigMatch = rawMsg.match(/ambiguous column name:\s*(.+)/i);
|
|
525
|
+
if (ambigMatch) {
|
|
526
|
+
const col = ambigMatch[1];
|
|
527
|
+
details.token = col;
|
|
528
|
+
if (!details.line) {
|
|
529
|
+
const pos = findTokenPosition(sql, col);
|
|
530
|
+
if (pos) {
|
|
531
|
+
details.line = pos.line;
|
|
532
|
+
details.column = pos.column;
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
details.cause = `Column name "${col}" is ambiguous because it exists in multiple joined tables.`;
|
|
536
|
+
details.suggestion = `Qualify the column with its table or alias name (e.g. "table_name.${col}").`;
|
|
537
|
+
}
|
|
538
|
+
if (!details.line) {
|
|
539
|
+
details.line = 1;
|
|
540
|
+
details.column = 1;
|
|
541
|
+
}
|
|
542
|
+
if (details.line && details.column) {
|
|
543
|
+
details.snippet = buildErrorSnippet(sql, details.line, details.column);
|
|
544
|
+
}
|
|
545
|
+
if (!details.cause) {
|
|
546
|
+
details.cause = 'SQL execution error.';
|
|
547
|
+
details.suggestion = 'Review the SQL syntax and ensure all table names, column names, and clauses are valid.';
|
|
548
|
+
}
|
|
549
|
+
return details;
|
|
550
|
+
}
|
package/dist/sql/generator.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const SQLITE_DESIGNER_TYPES:readonly ["TEXT","INTEGER","REAL","BLOB","BOOLEAN","DATE","DATETIME","NUMERIC","VARCHAR(255)","JSON"];export declare const POSTGRES_DESIGNER_TYPES:readonly ["TEXT","VARCHAR(255)","INTEGER","BIGINT","SMALLINT","SERIAL","BIGSERIAL","REAL","DECIMAL(10,2)","NUMERIC","BOOLEAN","DATE","DATETIME","TIMESTAMP","TIMESTAMPTZ","TIME","INTERVAL","JSON","JSONB","UUID","BYTEA","INET"];export declare const DESIGNER_TYPES:readonly ["TEXT","VARCHAR(255)","INTEGER","BIGINT","SMALLINT","SERIAL","BIGSERIAL","REAL","DECIMAL(10,2)","NUMERIC","BOOLEAN","DATE","DATETIME","TIMESTAMP","TIMESTAMPTZ","TIME","INTERVAL","JSON","JSONB","UUID","BYTEA","BLOB","INET"];export declare
|
|
1
|
+
export declare const SQLITE_DESIGNER_TYPES:readonly ["TEXT","INTEGER","REAL","BLOB","BOOLEAN","DATE","DATETIME","NUMERIC","VARCHAR(255)","JSON"];export declare const POSTGRES_DESIGNER_TYPES:readonly ["TEXT","VARCHAR(255)","INTEGER","BIGINT","SMALLINT","SERIAL","BIGSERIAL","REAL","DECIMAL(10,2)","NUMERIC","BOOLEAN","DATE","DATETIME","TIMESTAMP","TIMESTAMPTZ","TIME","INTERVAL","JSON","JSONB","UUID","BYTEA","INET"];export declare const DESIGNER_TYPES:readonly ["TEXT","VARCHAR(255)","INTEGER","BIGINT","SMALLINT","SERIAL","BIGSERIAL","REAL","DECIMAL(10,2)","NUMERIC","BOOLEAN","DATE","DATETIME","TIMESTAMP","TIMESTAMPTZ","TIME","INTERVAL","JSON","JSONB","UUID","BYTEA","BLOB","INET"];export declare const getDesignerTypes:(dialect?:"sqlite"|"postgres")=>readonly string[];export type DesignerColumnType=( typeof DESIGNER_TYPES)[number];export interface ColumnDef{name:string;type:string;primaryKey?:boolean;notNull?:boolean;unique?:boolean;defaultValue?:string|null;foreignKey?:{table:string;column:string;}|null;}export interface FieldValue{column:string;value:unknown;}export declare const quoteIdentifier:(identifier:string)=>string;export declare const escapeString:(value:string)=>string;export declare function sqlValue(value:unknown):string;export interface MappedType{sqlType:string;defaultAuto?:string;}export declare function mapColumnType(type:string):MappedType;export interface RenderOptions{forAddColumn?:boolean;}export declare function formatSqlDefault(raw:string,colType?:string):string;export declare function renderColumnDef(col:ColumnDef,opts?:RenderOptions):string;export declare function generateCreateTable(tableName:string,columns:ColumnDef[]):string;export declare function generateAddColumn(tableName:string,col:ColumnDef):string;export declare function generateRenameTable(tableName:string,newName:string):string;export declare function generateRenameColumn(tableName:string,oldName:string,newName:string):string;export declare function generateDropColumn(tableName:string,column:string):string;export declare function generateDropTable(tableName:string):string;export interface IndexDef{name?:string;columns:string[];unique?:boolean;}export declare function generateCreateIndex(tableName:string,index:IndexDef):string;export declare function generateDropIndex(indexName:string):string;export declare function generateInsert(table:string,fields:FieldValue[]):string;export declare function generateUpdate(table:string,fields:FieldValue[],where:FieldValue[]):string;
|