admindb 1.2.0 → 2.0.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 +250 -352
- package/dist/app.d.ts +25 -4
- package/dist/app.js +79 -15
- package/dist/app.js.map +1 -1
- package/dist/auth/config.d.ts +19 -0
- package/dist/auth/config.js +147 -0
- package/dist/auth/config.js.map +1 -0
- package/dist/auth/crypto.d.ts +27 -0
- package/dist/auth/crypto.js +133 -0
- package/dist/auth/crypto.js.map +1 -0
- package/dist/auth/index.d.ts +5 -0
- package/dist/auth/index.js +22 -0
- package/dist/auth/index.js.map +1 -0
- package/dist/auth/middleware.d.ts +14 -0
- package/dist/auth/middleware.js +100 -0
- package/dist/auth/middleware.js.map +1 -0
- package/dist/auth/routes.d.ts +7 -0
- package/dist/auth/routes.js +78 -0
- package/dist/auth/routes.js.map +1 -0
- package/dist/auth/types.d.ts +35 -0
- package/dist/auth/types.js +11 -0
- package/dist/auth/types.js.map +1 -0
- package/dist/cli/args.d.ts +45 -0
- package/dist/cli/args.js +328 -0
- package/dist/cli/args.js.map +1 -0
- package/dist/cli/config.d.ts +56 -0
- package/dist/cli/config.js +133 -0
- package/dist/cli/config.js.map +1 -0
- package/dist/cli/index.d.ts +4 -0
- package/dist/cli/index.js +21 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/cli/runner.d.ts +1 -0
- package/dist/cli/runner.js +222 -0
- package/dist/cli/runner.js.map +1 -0
- package/dist/cli.js +2 -127
- package/dist/cli.js.map +1 -1
- package/dist/data/datasets.d.ts +17 -0
- package/dist/data/datasets.js +133 -0
- package/dist/data/datasets.js.map +1 -0
- package/dist/data/detector.d.ts +30 -0
- package/dist/data/detector.js +472 -0
- package/dist/data/detector.js.map +1 -0
- package/dist/data/engine.d.ts +10 -0
- package/dist/data/engine.js +211 -0
- package/dist/data/engine.js.map +1 -0
- package/dist/data/index.d.ts +5 -0
- package/dist/data/index.js +22 -0
- package/dist/data/index.js.map +1 -0
- package/dist/data/strategies.d.ts +30 -0
- package/dist/data/strategies.js +343 -0
- package/dist/data/strategies.js.map +1 -0
- package/dist/data/types.d.ts +73 -0
- package/dist/data/types.js +8 -0
- package/dist/data/types.js.map +1 -0
- package/dist/db/database.d.ts +14 -142
- package/dist/db/database.js +209 -435
- package/dist/db/database.js.map +1 -1
- package/dist/db/export.d.ts +2 -2
- package/dist/db/export.js +7 -2
- package/dist/db/export.js.map +1 -1
- package/dist/db/filters.d.ts +22 -0
- package/dist/db/filters.js +158 -0
- package/dist/db/filters.js.map +1 -0
- package/dist/db/index.d.ts +8 -0
- package/dist/db/index.js +25 -0
- package/dist/db/index.js.map +1 -0
- package/dist/db/introspection.d.ts +6 -0
- package/dist/db/introspection.js +172 -0
- package/dist/db/introspection.js.map +1 -0
- package/dist/db/manager.d.ts +32 -19
- package/dist/db/manager.js +203 -44
- package/dist/db/manager.js.map +1 -1
- package/dist/db/migrations.d.ts +5 -0
- package/dist/db/migrations.js +110 -0
- package/dist/db/migrations.js.map +1 -0
- package/dist/db/postgres.d.ts +85 -0
- package/dist/db/postgres.js +745 -0
- package/dist/db/postgres.js.map +1 -0
- package/dist/db/types.d.ts +206 -0
- package/dist/db/types.js +7 -0
- package/dist/db/types.js.map +1 -0
- package/dist/index.d.ts +9 -2
- package/dist/index.js +46 -4
- package/dist/index.js.map +1 -1
- package/dist/public/css/app.css +1 -1
- package/dist/public/css/input.css +83 -13
- package/dist/public/js/app.js +142 -0
- package/dist/public/js/browse.js +591 -104
- package/dist/public/js/databases.js +105 -13
- package/dist/public/js/designer.js +40 -2
- package/dist/public/js/forms.js +610 -28
- package/dist/public/js/icons.js +51 -0
- package/dist/public/js/inspector.js +638 -0
- package/dist/public/js/query.js +93 -3
- package/dist/public/js/schema.js +170 -52
- package/dist/public/js/seed.js +1090 -0
- package/dist/routes/api/helpers.d.ts +42 -0
- package/dist/routes/api/helpers.js +133 -0
- package/dist/routes/api/helpers.js.map +1 -0
- package/dist/routes/api/import-export.d.ts +3 -0
- package/dist/routes/api/import-export.js +85 -0
- package/dist/routes/api/import-export.js.map +1 -0
- package/dist/routes/api/index.d.ts +4 -0
- package/dist/routes/api/index.js +37 -0
- package/dist/routes/api/index.js.map +1 -0
- package/dist/routes/api/query.d.ts +3 -0
- package/dist/routes/api/query.js +76 -0
- package/dist/routes/api/query.js.map +1 -0
- package/dist/routes/api/rows.d.ts +3 -0
- package/dist/routes/api/rows.js +373 -0
- package/dist/routes/api/rows.js.map +1 -0
- package/dist/routes/api/seed.d.ts +3 -0
- package/dist/routes/api/seed.js +82 -0
- package/dist/routes/api/seed.js.map +1 -0
- package/dist/routes/api/tables.d.ts +3 -0
- package/dist/routes/api/tables.js +160 -0
- package/dist/routes/api/tables.js.map +1 -0
- package/dist/routes/databases.d.ts +8 -11
- package/dist/routes/databases.js +100 -66
- package/dist/routes/databases.js.map +1 -1
- package/dist/routes/index.d.ts +3 -0
- package/dist/routes/index.js +20 -0
- package/dist/routes/index.js.map +1 -0
- package/dist/routes/pages.d.ts +3 -3
- package/dist/routes/pages.js +163 -54
- package/dist/routes/pages.js.map +1 -1
- package/dist/serverless.d.ts +40 -0
- package/dist/serverless.js +210 -0
- package/dist/serverless.js.map +1 -0
- package/dist/sql/generator.d.ts +4 -3
- package/dist/sql/generator.js +103 -6
- package/dist/sql/generator.js.map +1 -1
- package/dist/sql/index.d.ts +2 -0
- package/dist/sql/index.js +19 -0
- package/dist/sql/index.js.map +1 -0
- package/dist/types/api.d.ts +412 -0
- package/dist/types/api.js +9 -0
- package/dist/types/api.js.map +1 -0
- package/dist/utils/colors.d.ts +40 -0
- package/dist/utils/colors.js +64 -0
- package/dist/utils/colors.js.map +1 -0
- package/dist/{util.d.ts → utils/common.d.ts} +10 -6
- package/dist/{util.js → utils/common.js} +80 -13
- package/dist/utils/common.js.map +1 -0
- package/dist/utils/csv.js.map +1 -0
- package/dist/utils/datatype.d.ts +49 -0
- package/dist/utils/datatype.js +222 -0
- package/dist/utils/datatype.js.map +1 -0
- package/dist/utils/icons.d.ts +11 -0
- package/dist/utils/icons.js +72 -0
- package/dist/utils/icons.js.map +1 -0
- package/dist/utils/index.d.ts +4 -0
- package/dist/utils/index.js +21 -0
- package/dist/utils/index.js.map +1 -0
- package/dist/{logger.d.ts → utils/logger.d.ts} +1 -1
- package/dist/utils/logger.js +40 -0
- package/dist/utils/logger.js.map +1 -0
- package/dist/views/layouts/main.hbs +33 -11
- package/dist/views/pages/databases.hbs +182 -74
- package/dist/views/pages/designer.hbs +9 -6
- package/dist/views/pages/error.hbs +3 -3
- package/dist/views/pages/form.hbs +5 -3
- package/dist/views/pages/home.hbs +22 -13
- package/dist/views/pages/login.hbs +111 -0
- package/dist/views/pages/query.hbs +10 -8
- package/dist/views/pages/schema.hbs +41 -13
- package/dist/views/pages/seed.hbs +232 -0
- package/dist/views/pages/table.hbs +155 -70
- package/dist/views/partials/icon.hbs +2 -0
- package/dist/views/partials/navbar.hbs +70 -31
- package/dist/views/partials/sidebar.hbs +108 -99
- package/package.json +12 -5
- package/dist/args.d.ts +0 -28
- package/dist/args.js +0 -187
- package/dist/args.js.map +0 -1
- package/dist/config.d.ts +0 -17
- package/dist/config.js +0 -23
- package/dist/config.js.map +0 -1
- package/dist/csv.js.map +0 -1
- package/dist/logger.js +0 -25
- package/dist/logger.js.map +0 -1
- package/dist/routes/api.d.ts +0 -9
- package/dist/routes/api.js +0 -786
- package/dist/routes/api.js.map +0 -1
- package/dist/test/classifier.test.d.ts +0 -1
- package/dist/test/classifier.test.js +0 -44
- package/dist/test/classifier.test.js.map +0 -1
- package/dist/test/csv.test.d.ts +0 -1
- package/dist/test/csv.test.js +0 -64
- package/dist/test/csv.test.js.map +0 -1
- package/dist/test/database.test.d.ts +0 -1
- package/dist/test/database.test.js +0 -402
- package/dist/test/database.test.js.map +0 -1
- package/dist/test/generator.test.d.ts +0 -1
- package/dist/test/generator.test.js +0 -132
- package/dist/test/generator.test.js.map +0 -1
- package/dist/test/manager.test.d.ts +0 -1
- package/dist/test/manager.test.js +0 -146
- package/dist/test/manager.test.js.map +0 -1
- package/dist/util.js.map +0 -1
- /package/dist/{csv.d.ts → utils/csv.d.ts} +0 -0
- /package/dist/{csv.js → utils/csv.js} +0 -0
package/dist/db/database.js
CHANGED
|
@@ -1,153 +1,50 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
14
|
+
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
15
|
+
};
|
|
2
16
|
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
17
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
18
|
};
|
|
5
19
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
-
exports.SqliteDatabase =
|
|
7
|
-
exports.buildFilterClause = buildFilterClause;
|
|
20
|
+
exports.SqliteDatabase = void 0;
|
|
8
21
|
const better_sqlite3_1 = __importDefault(require("better-sqlite3"));
|
|
9
22
|
const generator_1 = require("../sql/generator");
|
|
10
|
-
const
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
}
|
|
23
|
+
const common_1 = require("../utils/common");
|
|
24
|
+
const types_1 = require("./types");
|
|
25
|
+
const filters_1 = require("./filters");
|
|
26
|
+
const introspection_1 = require("./introspection");
|
|
27
|
+
const migrations_1 = require("./migrations");
|
|
28
|
+
__exportStar(require("./types"), exports);
|
|
29
|
+
__exportStar(require("./filters"), exports);
|
|
18
30
|
/**
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
* - the legacy string syntax: `=value` exact, `!=value`, `>value`, `>=value`,
|
|
23
|
-
* `<value`, `<=value`, `value*` prefix, anything else → substring match; or
|
|
24
|
-
* - a structured `FilterCondition` (or array of them) produced by the
|
|
25
|
-
* type-aware filter form (FK dropdowns, boolean toggles, date/number ranges).
|
|
31
|
+
* Promise-style wrapper around the better-sqlite3 driver.
|
|
32
|
+
* Every call returns a consistent `{ success, data?, error? }` shape and
|
|
33
|
+
* performs one-time, idempotent schema initialization.
|
|
26
34
|
*/
|
|
27
|
-
function pushFilterCondition(conds, params, quotedCol, c) {
|
|
28
|
-
const value = String(c.value ?? '');
|
|
29
|
-
switch (c.op) {
|
|
30
|
-
case 'eq':
|
|
31
|
-
conds.push(`${quotedCol} = ?`);
|
|
32
|
-
params.push(value);
|
|
33
|
-
break;
|
|
34
|
-
case 'neq':
|
|
35
|
-
conds.push(`${quotedCol} != ?`);
|
|
36
|
-
params.push(value);
|
|
37
|
-
break;
|
|
38
|
-
case 'gt':
|
|
39
|
-
conds.push(`${quotedCol} > ?`);
|
|
40
|
-
params.push(value);
|
|
41
|
-
break;
|
|
42
|
-
case 'gte':
|
|
43
|
-
conds.push(`${quotedCol} >= ?`);
|
|
44
|
-
params.push(value);
|
|
45
|
-
break;
|
|
46
|
-
case 'lt':
|
|
47
|
-
conds.push(`${quotedCol} < ?`);
|
|
48
|
-
params.push(value);
|
|
49
|
-
break;
|
|
50
|
-
case 'lte':
|
|
51
|
-
conds.push(`${quotedCol} <= ?`);
|
|
52
|
-
params.push(value);
|
|
53
|
-
break;
|
|
54
|
-
case 'like':
|
|
55
|
-
conds.push(`${quotedCol} LIKE ? ESCAPE '\\'`);
|
|
56
|
-
params.push('%' + escapeLike(value) + '%');
|
|
57
|
-
break;
|
|
58
|
-
case 'prefix':
|
|
59
|
-
conds.push(`${quotedCol} LIKE ? ESCAPE '\\'`);
|
|
60
|
-
params.push(escapeLike(value) + '%');
|
|
61
|
-
break;
|
|
62
|
-
case 'between':
|
|
63
|
-
conds.push(`${quotedCol} >= ?`);
|
|
64
|
-
params.push(value);
|
|
65
|
-
conds.push(`${quotedCol} <= ?`);
|
|
66
|
-
params.push(String(c.max ?? ''));
|
|
67
|
-
break;
|
|
68
|
-
case 'null':
|
|
69
|
-
conds.push(`${quotedCol} IS NULL`);
|
|
70
|
-
break;
|
|
71
|
-
case 'notnull':
|
|
72
|
-
conds.push(`${quotedCol} IS NOT NULL`);
|
|
73
|
-
break;
|
|
74
|
-
default:
|
|
75
|
-
break;
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
function buildFilterClause(filters, availableCols) {
|
|
79
|
-
const conds = [];
|
|
80
|
-
const params = [];
|
|
81
|
-
const colSet = new Set(availableCols);
|
|
82
|
-
for (const [col, raw] of Object.entries(filters ?? {})) {
|
|
83
|
-
if (!colSet.has(col))
|
|
84
|
-
continue;
|
|
85
|
-
const q = (0, generator_1.quoteIdentifier)(col);
|
|
86
|
-
if (Array.isArray(raw)) {
|
|
87
|
-
for (const c of raw) {
|
|
88
|
-
if (c && typeof c === 'object')
|
|
89
|
-
pushFilterCondition(conds, params, q, c);
|
|
90
|
-
}
|
|
91
|
-
continue;
|
|
92
|
-
}
|
|
93
|
-
if (raw && typeof raw === 'object') {
|
|
94
|
-
pushFilterCondition(conds, params, q, raw);
|
|
95
|
-
continue;
|
|
96
|
-
}
|
|
97
|
-
// Legacy string syntax.
|
|
98
|
-
const value = String(raw ?? '').trim();
|
|
99
|
-
if (value === '')
|
|
100
|
-
continue;
|
|
101
|
-
if (value.startsWith('>=')) {
|
|
102
|
-
conds.push(`${q} >= ?`);
|
|
103
|
-
params.push(value.slice(2).trim());
|
|
104
|
-
}
|
|
105
|
-
else if (value.startsWith('<=')) {
|
|
106
|
-
conds.push(`${q} <= ?`);
|
|
107
|
-
params.push(value.slice(2).trim());
|
|
108
|
-
}
|
|
109
|
-
else if (value.startsWith('!=')) {
|
|
110
|
-
conds.push(`${q} != ?`);
|
|
111
|
-
params.push(value.slice(2).trim());
|
|
112
|
-
}
|
|
113
|
-
else if (value.startsWith('=')) {
|
|
114
|
-
conds.push(`${q} = ?`);
|
|
115
|
-
params.push(value.slice(1).trim());
|
|
116
|
-
}
|
|
117
|
-
else if (value.startsWith('>')) {
|
|
118
|
-
conds.push(`${q} > ?`);
|
|
119
|
-
params.push(value.slice(1).trim());
|
|
120
|
-
}
|
|
121
|
-
else if (value.startsWith('<')) {
|
|
122
|
-
conds.push(`${q} < ?`);
|
|
123
|
-
params.push(value.slice(1).trim());
|
|
124
|
-
}
|
|
125
|
-
else if (value.endsWith('*')) {
|
|
126
|
-
conds.push(`${q} LIKE ? ESCAPE '\\'`);
|
|
127
|
-
params.push(escapeLike(value.slice(0, -1)) + '%');
|
|
128
|
-
}
|
|
129
|
-
else {
|
|
130
|
-
conds.push(`${q} LIKE ? ESCAPE '\\'`);
|
|
131
|
-
params.push('%' + escapeLike(value) + '%');
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
|
-
return { where: conds.length ? ` WHERE ${conds.join(' AND ')}` : '', params };
|
|
135
|
-
}
|
|
136
35
|
class SqliteDatabase {
|
|
137
36
|
db;
|
|
138
37
|
logger;
|
|
139
38
|
path;
|
|
140
|
-
/** True when this database was opened in read-only mode. */
|
|
141
39
|
isReadOnly;
|
|
40
|
+
dialect = 'sqlite';
|
|
142
41
|
constructor(dbPath, logger, options = {}) {
|
|
143
42
|
this.path = dbPath;
|
|
144
43
|
this.logger = logger;
|
|
145
44
|
this.isReadOnly = !!options.readonly;
|
|
146
|
-
// Opening read-only also refuses to create the file when it does not exist.
|
|
147
45
|
this.db = new better_sqlite3_1.default(dbPath, { readonly: this.isReadOnly });
|
|
148
46
|
this.db.exec('PRAGMA foreign_keys = ON;');
|
|
149
47
|
if (this.isReadOnly) {
|
|
150
|
-
// Belt-and-suspenders: even a direct write statement fails at the SQLite layer.
|
|
151
48
|
this.db.exec('PRAGMA query_only = ON;');
|
|
152
49
|
}
|
|
153
50
|
else {
|
|
@@ -156,7 +53,6 @@ class SqliteDatabase {
|
|
|
156
53
|
}
|
|
157
54
|
this.logger.info(`Opened SQLite database at ${dbPath}${this.isReadOnly ? ' (read-only)' : ''}`);
|
|
158
55
|
}
|
|
159
|
-
/** Standard error returned for every write attempt in read-only mode. */
|
|
160
56
|
readonlyBlocked() {
|
|
161
57
|
return { success: false, error: 'Database is open in read-only mode — write operations are disabled.' };
|
|
162
58
|
}
|
|
@@ -165,12 +61,12 @@ class SqliteDatabase {
|
|
|
165
61
|
this.db.close();
|
|
166
62
|
}
|
|
167
63
|
catch (err) {
|
|
168
|
-
this.logger.warn(`Failed to close database: ${(0,
|
|
64
|
+
this.logger.warn(`Failed to close database: ${(0, common_1.errorMessage)(err)}`);
|
|
169
65
|
}
|
|
170
66
|
}
|
|
171
67
|
initSchema() {
|
|
172
68
|
this.db.exec(`
|
|
173
|
-
CREATE TABLE IF NOT EXISTS ${(0, generator_1.quoteIdentifier)(
|
|
69
|
+
CREATE TABLE IF NOT EXISTS ${(0, generator_1.quoteIdentifier)(types_1.INTERNAL_TABLES.savedQueries)} (
|
|
174
70
|
${(0, generator_1.quoteIdentifier)('id')} INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
175
71
|
${(0, generator_1.quoteIdentifier)('name')} TEXT NOT NULL UNIQUE,
|
|
176
72
|
${(0, generator_1.quoteIdentifier)('sql')} TEXT NOT NULL,
|
|
@@ -184,11 +80,31 @@ class SqliteDatabase {
|
|
|
184
80
|
return { success: true, data };
|
|
185
81
|
}
|
|
186
82
|
catch (err) {
|
|
187
|
-
const msg = (0,
|
|
83
|
+
const msg = (0, common_1.errorMessage)(err);
|
|
188
84
|
this.logger.error(msg);
|
|
189
85
|
return { success: false, error: msg };
|
|
190
86
|
}
|
|
191
87
|
}
|
|
88
|
+
/** Run an atomic transaction. Reverts on any error and enforces read-only mode. */
|
|
89
|
+
transaction(fn) {
|
|
90
|
+
if (this.isReadOnly)
|
|
91
|
+
return Promise.resolve(this.readonlyBlocked());
|
|
92
|
+
return this.tryRun(() => {
|
|
93
|
+
this.db.exec('BEGIN');
|
|
94
|
+
try {
|
|
95
|
+
const res = fn();
|
|
96
|
+
this.db.exec('COMMIT');
|
|
97
|
+
return res;
|
|
98
|
+
}
|
|
99
|
+
catch (err) {
|
|
100
|
+
try {
|
|
101
|
+
this.db.exec('ROLLBACK');
|
|
102
|
+
}
|
|
103
|
+
catch { /* ignore */ }
|
|
104
|
+
throw err;
|
|
105
|
+
}
|
|
106
|
+
});
|
|
107
|
+
}
|
|
192
108
|
toBind(value) {
|
|
193
109
|
if (value === undefined)
|
|
194
110
|
return null;
|
|
@@ -220,11 +136,6 @@ class SqliteDatabase {
|
|
|
220
136
|
return { changes: undefined };
|
|
221
137
|
});
|
|
222
138
|
}
|
|
223
|
-
/**
|
|
224
|
-
* True when `sql` contains more than one statement. Detection is delegated to
|
|
225
|
-
* SQLite itself via `prepare()` (which refuses multi-statement strings), so
|
|
226
|
-
* comments, string literals and quoted identifiers are handled correctly.
|
|
227
|
-
*/
|
|
228
139
|
hasMultipleStatements(sql) {
|
|
229
140
|
const s = String(sql ?? '').trim();
|
|
230
141
|
if (!s)
|
|
@@ -234,10 +145,9 @@ class SqliteDatabase {
|
|
|
234
145
|
return false;
|
|
235
146
|
}
|
|
236
147
|
catch (err) {
|
|
237
|
-
return /more than one statement/i.test((0,
|
|
148
|
+
return /more than one statement/i.test((0, common_1.errorMessage)(err));
|
|
238
149
|
}
|
|
239
150
|
}
|
|
240
|
-
/** Run a write statement, reporting row changes when it is a single statement. */
|
|
241
151
|
async runWrite(sql) {
|
|
242
152
|
if (this.isReadOnly)
|
|
243
153
|
return this.readonlyBlocked();
|
|
@@ -247,62 +157,28 @@ class SqliteDatabase {
|
|
|
247
157
|
const info = stmt.run();
|
|
248
158
|
return { success: true, data: { changes: Number(info.changes) } };
|
|
249
159
|
}
|
|
250
|
-
catch
|
|
251
|
-
// Possibly multiple statements — fall back to exec.
|
|
160
|
+
catch {
|
|
252
161
|
try {
|
|
253
162
|
this.db.exec(String(sql));
|
|
254
163
|
return { success: true, data: { changes: undefined } };
|
|
255
164
|
}
|
|
256
165
|
catch (err2) {
|
|
257
|
-
const msg = (0,
|
|
166
|
+
const msg = (0, common_1.errorMessage)(err2);
|
|
258
167
|
this.logger.error(msg);
|
|
259
168
|
return { success: false, error: msg };
|
|
260
169
|
}
|
|
261
170
|
}
|
|
262
171
|
}
|
|
263
172
|
async listTables() {
|
|
264
|
-
return this.tryRun(() =>
|
|
265
|
-
const rows = this.db
|
|
266
|
-
.prepare("SELECT name FROM sqlite_master WHERE type = 'table' AND name NOT LIKE 'sqlite_%' ORDER BY name")
|
|
267
|
-
.all();
|
|
268
|
-
return rows.map((r) => ({ name: r.name }));
|
|
269
|
-
});
|
|
173
|
+
return this.tryRun(() => (0, introspection_1.listTablesSync)(this.db));
|
|
270
174
|
}
|
|
271
175
|
async getTableInfo(table) {
|
|
272
|
-
return this.tryRun(() =>
|
|
273
|
-
const cols = this.db.prepare(`PRAGMA table_info(${(0, generator_1.quoteIdentifier)(table)})`).all();
|
|
274
|
-
const fks = this.db.prepare(`PRAGMA foreign_key_list(${(0, generator_1.quoteIdentifier)(table)})`).all();
|
|
275
|
-
const primaryKey = cols
|
|
276
|
-
.filter((c) => c.pk > 0)
|
|
277
|
-
.sort((a, b) => a.pk - b.pk)
|
|
278
|
-
.map((c) => c.name);
|
|
279
|
-
return {
|
|
280
|
-
table,
|
|
281
|
-
columns: cols.map((c) => ({
|
|
282
|
-
cid: c.cid,
|
|
283
|
-
name: c.name,
|
|
284
|
-
type: c.type,
|
|
285
|
-
notnull: c.notnull,
|
|
286
|
-
dflt_value: c.dflt_value,
|
|
287
|
-
pk: c.pk,
|
|
288
|
-
})),
|
|
289
|
-
foreignKeys: fks.map((f) => ({
|
|
290
|
-
id: f.id,
|
|
291
|
-
seq: f.seq,
|
|
292
|
-
table: f.table,
|
|
293
|
-
from: f.from,
|
|
294
|
-
to: f.to,
|
|
295
|
-
on_update: f.on_update,
|
|
296
|
-
on_delete: f.on_delete,
|
|
297
|
-
})),
|
|
298
|
-
primaryKey,
|
|
299
|
-
};
|
|
300
|
-
});
|
|
176
|
+
return this.tryRun(() => (0, introspection_1.getTableInfoSync)(this.db, table));
|
|
301
177
|
}
|
|
302
178
|
async getRowCount(table, filters) {
|
|
303
179
|
return this.tryRun(() => {
|
|
304
180
|
const info = this.getTableInfoSync(table);
|
|
305
|
-
const { where, params } = buildFilterClause(filters, info?.columns.map((c) => c.name) ?? []);
|
|
181
|
+
const { where, params } = (0, filters_1.buildFilterClause)(filters, info?.columns.map((c) => c.name) ?? []);
|
|
306
182
|
const row = this.db
|
|
307
183
|
.prepare(`SELECT COUNT(*) AS c FROM ${(0, generator_1.quoteIdentifier)(table)}${where}`)
|
|
308
184
|
.get(...params);
|
|
@@ -318,8 +194,8 @@ class SqliteDatabase {
|
|
|
318
194
|
const limit = opts.limit && opts.limit > 0 ? opts.limit : 200;
|
|
319
195
|
const page = opts.page && opts.page > 0 ? opts.page : 1;
|
|
320
196
|
const offset = (page - 1) * limit;
|
|
321
|
-
const { where, params } = buildFilterClause(opts.filters, cols.map((c) => c.name));
|
|
322
|
-
let sql = `SELECT * FROM ${(0, generator_1.quoteIdentifier)(table)}${where}`;
|
|
197
|
+
const { where, params } = (0, filters_1.buildFilterClause)(opts.filters, cols.map((c) => c.name));
|
|
198
|
+
let sql = `SELECT rowid AS _rowid_, * FROM ${(0, generator_1.quoteIdentifier)(table)}${where}`;
|
|
323
199
|
if (orderCol)
|
|
324
200
|
sql += ` ORDER BY ${(0, generator_1.quoteIdentifier)(orderCol)} ${orderDir}`;
|
|
325
201
|
sql += ` LIMIT ${limit} OFFSET ${offset}`;
|
|
@@ -327,12 +203,12 @@ class SqliteDatabase {
|
|
|
327
203
|
});
|
|
328
204
|
}
|
|
329
205
|
async getAllRows(table) {
|
|
330
|
-
return this.tryRun(() => this.db.prepare(`SELECT * FROM ${(0, generator_1.quoteIdentifier)(table)}`).all());
|
|
206
|
+
return this.tryRun(() => this.db.prepare(`SELECT rowid AS _rowid_, * FROM ${(0, generator_1.quoteIdentifier)(table)}`).all());
|
|
331
207
|
}
|
|
332
208
|
async getRow(table, where) {
|
|
333
209
|
return this.tryRun(() => {
|
|
334
210
|
const conds = where.map((w) => `${(0, generator_1.quoteIdentifier)(w.column)} = ?`);
|
|
335
|
-
const sql = `SELECT * FROM ${(0, generator_1.quoteIdentifier)(table)} WHERE ${conds.join(' AND ')} LIMIT 1`;
|
|
211
|
+
const sql = `SELECT rowid AS _rowid_, * FROM ${(0, generator_1.quoteIdentifier)(table)} WHERE ${conds.join(' AND ')} LIMIT 1`;
|
|
336
212
|
const row = this.db.prepare(sql).get(...where.map((w) => this.toBind(w.value)));
|
|
337
213
|
return row ? row : null;
|
|
338
214
|
});
|
|
@@ -342,34 +218,19 @@ class SqliteDatabase {
|
|
|
342
218
|
const sql = `INSERT INTO ${(0, generator_1.quoteIdentifier)(table)} (${cols.join(', ')}) VALUES (${fields.map(() => '?').join(', ')})`;
|
|
343
219
|
return this.run(sql, fields.map((f) => this.toBind(f.value)));
|
|
344
220
|
}
|
|
345
|
-
/** Insert many rows
|
|
221
|
+
/** Insert many rows inside a single atomic transaction. */
|
|
346
222
|
async insertRows(table, rows) {
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
const cols = fields.map((f) => (0, generator_1.quoteIdentifier)(f.column));
|
|
357
|
-
const sql = `INSERT INTO ${(0, generator_1.quoteIdentifier)(table)} (${cols.join(', ')}) VALUES (${fields.map(() => '?').join(', ')})`;
|
|
358
|
-
this.db.prepare(sql).run(...fields.map((f) => this.toBind(f.value)));
|
|
359
|
-
inserted += 1;
|
|
360
|
-
}
|
|
361
|
-
this.db.exec('COMMIT');
|
|
362
|
-
return { inserted, skipped: rows.length - inserted };
|
|
363
|
-
}
|
|
364
|
-
catch (err) {
|
|
365
|
-
try {
|
|
366
|
-
this.db.exec('ROLLBACK');
|
|
367
|
-
}
|
|
368
|
-
catch {
|
|
369
|
-
/* ignore */
|
|
370
|
-
}
|
|
371
|
-
throw err;
|
|
223
|
+
return this.transaction(() => {
|
|
224
|
+
let inserted = 0;
|
|
225
|
+
for (const fields of rows) {
|
|
226
|
+
if (!fields.length)
|
|
227
|
+
continue;
|
|
228
|
+
const cols = fields.map((f) => (0, generator_1.quoteIdentifier)(f.column));
|
|
229
|
+
const sql = `INSERT INTO ${(0, generator_1.quoteIdentifier)(table)} (${cols.join(', ')}) VALUES (${fields.map(() => '?').join(', ')})`;
|
|
230
|
+
this.db.prepare(sql).run(...fields.map((f) => this.toBind(f.value)));
|
|
231
|
+
inserted += 1;
|
|
372
232
|
}
|
|
233
|
+
return { inserted, skipped: rows.length - inserted };
|
|
373
234
|
});
|
|
374
235
|
}
|
|
375
236
|
async updateRow(table, fields, where) {
|
|
@@ -378,39 +239,20 @@ class SqliteDatabase {
|
|
|
378
239
|
const sql = `UPDATE ${(0, generator_1.quoteIdentifier)(table)} SET ${sets.join(', ')} WHERE ${conds.join(' AND ')}`;
|
|
379
240
|
return this.run(sql, [...fields.map((f) => this.toBind(f.value)), ...where.map((w) => this.toBind(w.value))]);
|
|
380
241
|
}
|
|
381
|
-
/**
|
|
382
|
-
* Apply many row updates (each with its own SET fields + primary-key WHERE
|
|
383
|
-
* clause) inside a single transaction. Used by the bulk "apply changes"
|
|
384
|
-
* flow of inline grid editing. Rolls everything back if any row fails.
|
|
385
|
-
*/
|
|
242
|
+
/** Apply many row updates inside a single atomic transaction. */
|
|
386
243
|
async updateRows(table, rows) {
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
const conds = where.map((w) => `${(0, generator_1.quoteIdentifier)(w.column)} = ?`);
|
|
398
|
-
const sql = `UPDATE ${(0, generator_1.quoteIdentifier)(table)} SET ${sets.join(', ')} WHERE ${conds.join(' AND ')}`;
|
|
399
|
-
const info = this.db.prepare(sql).run(...[...fields.map((f) => this.toBind(f.value)), ...where.map((w) => this.toBind(w.value))]);
|
|
400
|
-
updated += Number(info.changes);
|
|
401
|
-
}
|
|
402
|
-
this.db.exec('COMMIT');
|
|
403
|
-
return { updated };
|
|
404
|
-
}
|
|
405
|
-
catch (err) {
|
|
406
|
-
try {
|
|
407
|
-
this.db.exec('ROLLBACK');
|
|
408
|
-
}
|
|
409
|
-
catch {
|
|
410
|
-
/* ignore */
|
|
411
|
-
}
|
|
412
|
-
throw err;
|
|
244
|
+
return this.transaction(() => {
|
|
245
|
+
let updated = 0;
|
|
246
|
+
for (const { fields, where } of rows) {
|
|
247
|
+
if (!fields.length || !where.length)
|
|
248
|
+
continue;
|
|
249
|
+
const sets = fields.map((f) => `${(0, generator_1.quoteIdentifier)(f.column)} = ?`);
|
|
250
|
+
const conds = where.map((w) => `${(0, generator_1.quoteIdentifier)(w.column)} = ?`);
|
|
251
|
+
const sql = `UPDATE ${(0, generator_1.quoteIdentifier)(table)} SET ${sets.join(', ')} WHERE ${conds.join(' AND ')}`;
|
|
252
|
+
const info = this.db.prepare(sql).run(...[...fields.map((f) => this.toBind(f.value)), ...where.map((w) => this.toBind(w.value))]);
|
|
253
|
+
updated += Number(info.changes);
|
|
413
254
|
}
|
|
255
|
+
return { updated };
|
|
414
256
|
});
|
|
415
257
|
}
|
|
416
258
|
async deleteRow(table, where) {
|
|
@@ -418,43 +260,21 @@ class SqliteDatabase {
|
|
|
418
260
|
const sql = `DELETE FROM ${(0, generator_1.quoteIdentifier)(table)} WHERE ${conds.join(' AND ')}`;
|
|
419
261
|
return this.run(sql, where.map((w) => this.toBind(w.value)));
|
|
420
262
|
}
|
|
421
|
-
/**
|
|
422
|
-
* Delete many rows (each identified by its primary-key WHERE clause) inside a
|
|
423
|
-
* single transaction. Used by bulk delete.
|
|
424
|
-
*/
|
|
263
|
+
/** Delete many rows inside a single atomic transaction. */
|
|
425
264
|
async deleteRows(table, rows) {
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
const conds = where.map((w) => `${(0, generator_1.quoteIdentifier)(w.column)} = ?`);
|
|
436
|
-
const sql = `DELETE FROM ${(0, generator_1.quoteIdentifier)(table)} WHERE ${conds.join(' AND ')}`;
|
|
437
|
-
const info = this.db.prepare(sql).run(...where.map((w) => this.toBind(w.value)));
|
|
438
|
-
deleted += Number(info.changes);
|
|
439
|
-
}
|
|
440
|
-
this.db.exec('COMMIT');
|
|
441
|
-
return { deleted };
|
|
442
|
-
}
|
|
443
|
-
catch (err) {
|
|
444
|
-
try {
|
|
445
|
-
this.db.exec('ROLLBACK');
|
|
446
|
-
}
|
|
447
|
-
catch {
|
|
448
|
-
/* ignore */
|
|
449
|
-
}
|
|
450
|
-
throw err;
|
|
265
|
+
return this.transaction(() => {
|
|
266
|
+
let deleted = 0;
|
|
267
|
+
for (const where of rows) {
|
|
268
|
+
if (!where.length)
|
|
269
|
+
continue;
|
|
270
|
+
const conds = where.map((w) => `${(0, generator_1.quoteIdentifier)(w.column)} = ?`);
|
|
271
|
+
const sql = `DELETE FROM ${(0, generator_1.quoteIdentifier)(table)} WHERE ${conds.join(' AND ')}`;
|
|
272
|
+
const info = this.db.prepare(sql).run(...where.map((w) => this.toBind(w.value)));
|
|
273
|
+
deleted += Number(info.changes);
|
|
451
274
|
}
|
|
275
|
+
return { deleted };
|
|
452
276
|
});
|
|
453
277
|
}
|
|
454
|
-
/**
|
|
455
|
-
* Fetch the rows matching the given primary-key WHERE clauses in one query
|
|
456
|
-
* (`(a = ? AND b = ?) OR …`). Used by bulk export and FK-impact checks.
|
|
457
|
-
*/
|
|
458
278
|
async getRowsByPks(table, pks) {
|
|
459
279
|
return this.tryRun(() => {
|
|
460
280
|
const groups = [];
|
|
@@ -467,7 +287,7 @@ class SqliteDatabase {
|
|
|
467
287
|
}
|
|
468
288
|
if (!groups.length)
|
|
469
289
|
return [];
|
|
470
|
-
const sql = `SELECT * FROM ${(0, generator_1.quoteIdentifier)(table)} WHERE ${groups.map((g) => `(${g})`).join(' OR ')}`;
|
|
290
|
+
const sql = `SELECT rowid AS _rowid_, * FROM ${(0, generator_1.quoteIdentifier)(table)} WHERE ${groups.map((g) => `(${g})`).join(' OR ')}`;
|
|
471
291
|
return this.db.prepare(sql).all(...params);
|
|
472
292
|
});
|
|
473
293
|
}
|
|
@@ -480,173 +300,20 @@ class SqliteDatabase {
|
|
|
480
300
|
});
|
|
481
301
|
}
|
|
482
302
|
async listSavedQueries() {
|
|
483
|
-
return this.all(`SELECT id, name, sql, created_at FROM ${(0, generator_1.quoteIdentifier)(
|
|
303
|
+
return this.all(`SELECT id, name, sql, created_at FROM ${(0, generator_1.quoteIdentifier)(types_1.INTERNAL_TABLES.savedQueries)} ORDER BY name COLLATE NOCASE`);
|
|
484
304
|
}
|
|
485
305
|
async saveQuery(name, sql) {
|
|
486
|
-
return this.run(`INSERT INTO ${(0, generator_1.quoteIdentifier)(
|
|
306
|
+
return this.run(`INSERT INTO ${(0, generator_1.quoteIdentifier)(types_1.INTERNAL_TABLES.savedQueries)} (name, sql) VALUES (?, ?)`, [name, sql]);
|
|
487
307
|
}
|
|
488
308
|
async deleteSavedQuery(id) {
|
|
489
|
-
return this.run(`DELETE FROM ${(0, generator_1.quoteIdentifier)(
|
|
309
|
+
return this.run(`DELETE FROM ${(0, generator_1.quoteIdentifier)(types_1.INTERNAL_TABLES.savedQueries)} WHERE id = ?`, [Number(id)]);
|
|
490
310
|
}
|
|
491
|
-
// ---- Schema introspection & ALTER -------------------------------------
|
|
492
|
-
/**
|
|
493
|
-
* Detailed schema for a table: columns with constraint info, indexes,
|
|
494
|
-
* foreign keys, and which other tables reference it.
|
|
495
|
-
*/
|
|
496
311
|
async getSchema(table) {
|
|
497
|
-
return this.tryRun(() =>
|
|
498
|
-
const info = this.getTableInfoSync(table);
|
|
499
|
-
if (!info || info.columns.length === 0)
|
|
500
|
-
throw new Error(`Table "${table}" does not exist.`);
|
|
501
|
-
// Foreign keys declared on this table (child FKs).
|
|
502
|
-
const fkRows = this.db
|
|
503
|
-
.prepare(`PRAGMA foreign_key_list(${(0, generator_1.quoteIdentifier)(table)})`)
|
|
504
|
-
.all();
|
|
505
|
-
const foreignKeys = fkRows.map((f) => ({
|
|
506
|
-
id: f.id,
|
|
507
|
-
seq: f.seq,
|
|
508
|
-
table: f.table,
|
|
509
|
-
from: f.from,
|
|
510
|
-
to: f.to,
|
|
511
|
-
on_update: f.on_update,
|
|
512
|
-
on_delete: f.on_delete,
|
|
513
|
-
}));
|
|
514
|
-
const indexRows = this.db
|
|
515
|
-
.prepare(`PRAGMA index_list(${(0, generator_1.quoteIdentifier)(table)})`)
|
|
516
|
-
.all();
|
|
517
|
-
const indexes = indexRows.map((ix) => {
|
|
518
|
-
const cols = this.db
|
|
519
|
-
.prepare(`PRAGMA index_info(${(0, generator_1.quoteIdentifier)(ix.name)})`)
|
|
520
|
-
.all().map((c) => c.name);
|
|
521
|
-
const sqlRow = this.db
|
|
522
|
-
.prepare("SELECT sql FROM sqlite_master WHERE type = 'index' AND name = ?")
|
|
523
|
-
.get(ix.name);
|
|
524
|
-
return { name: ix.name, unique: !!ix.unique, partial: ix.partial, origin: ix.origin, columns: cols, sql: sqlRow?.sql ?? null };
|
|
525
|
-
});
|
|
526
|
-
const uniqueCols = new Set();
|
|
527
|
-
const indexedCols = new Set();
|
|
528
|
-
const originByName = new Map(indexRows.map((ix) => [ix.name, ix.origin]));
|
|
529
|
-
for (const ix of indexes) {
|
|
530
|
-
if (ix.unique)
|
|
531
|
-
ix.columns.forEach((c) => uniqueCols.add(c));
|
|
532
|
-
// 'c' = explicit CREATE INDEX; these block DROP COLUMN.
|
|
533
|
-
if (originByName.get(ix.name) === 'c')
|
|
534
|
-
ix.columns.forEach((c) => indexedCols.add(c));
|
|
535
|
-
}
|
|
536
|
-
// Foreign keys in OTHER tables that point at this table.
|
|
537
|
-
const refs = [];
|
|
538
|
-
const allTables = this.db
|
|
539
|
-
.prepare("SELECT name FROM sqlite_master WHERE type = 'table' AND name NOT LIKE 'sqlite_%'")
|
|
540
|
-
.all().map((r) => r.name);
|
|
541
|
-
for (const t of allTables) {
|
|
542
|
-
if (t === table)
|
|
543
|
-
continue;
|
|
544
|
-
try {
|
|
545
|
-
const fks = this.db
|
|
546
|
-
.prepare(`PRAGMA foreign_key_list(${(0, generator_1.quoteIdentifier)(t)})`)
|
|
547
|
-
.all();
|
|
548
|
-
for (const fk of fks) {
|
|
549
|
-
if (fk.table === table)
|
|
550
|
-
refs.push({ table: t, from: fk.from, to: fk.to ?? '' });
|
|
551
|
-
}
|
|
552
|
-
}
|
|
553
|
-
catch {
|
|
554
|
-
/* ignore unreadable tables */
|
|
555
|
-
}
|
|
556
|
-
}
|
|
557
|
-
const refsByTo = new Map();
|
|
558
|
-
for (const r of refs) {
|
|
559
|
-
const arr = refsByTo.get(r.to) ?? [];
|
|
560
|
-
arr.push({ table: r.table, from: r.from });
|
|
561
|
-
refsByTo.set(r.to, arr);
|
|
562
|
-
}
|
|
563
|
-
const fkByFrom = new Map();
|
|
564
|
-
for (const fk of foreignKeys)
|
|
565
|
-
fkByFrom.set(fk.from, fk);
|
|
566
|
-
const columns = info.columns.map((c) => {
|
|
567
|
-
const unique = c.pk > 0 || uniqueCols.has(c.name);
|
|
568
|
-
const indexed = indexedCols.has(c.name);
|
|
569
|
-
const referencedBy = refsByTo.get(c.name) ?? [];
|
|
570
|
-
const fk = fkByFrom.get(c.name) ?? null;
|
|
571
|
-
const dropBlockers = [];
|
|
572
|
-
if (c.pk > 0)
|
|
573
|
-
dropBlockers.push('Is part of the primary key');
|
|
574
|
-
if (unique && c.pk === 0)
|
|
575
|
-
dropBlockers.push('Has a UNIQUE constraint');
|
|
576
|
-
if (indexed)
|
|
577
|
-
dropBlockers.push('Is used by an index');
|
|
578
|
-
if (referencedBy.length)
|
|
579
|
-
dropBlockers.push(`Referenced by a foreign key in "${referencedBy[0].table}"`);
|
|
580
|
-
if (fk)
|
|
581
|
-
dropBlockers.push('Is part of a foreign key');
|
|
582
|
-
return {
|
|
583
|
-
...c,
|
|
584
|
-
unique,
|
|
585
|
-
indexed,
|
|
586
|
-
referencedBy,
|
|
587
|
-
fk,
|
|
588
|
-
canDrop: dropBlockers.length === 0,
|
|
589
|
-
dropBlockers,
|
|
590
|
-
};
|
|
591
|
-
});
|
|
592
|
-
return {
|
|
593
|
-
table,
|
|
594
|
-
columns,
|
|
595
|
-
foreignKeys,
|
|
596
|
-
indexes,
|
|
597
|
-
references: refs,
|
|
598
|
-
};
|
|
599
|
-
});
|
|
312
|
+
return this.tryRun(() => (0, introspection_1.getSchemaSync)(this.db, table));
|
|
600
313
|
}
|
|
601
|
-
/**
|
|
602
|
-
* Other tables with foreign keys pointing at `table`, grouped by table, each
|
|
603
|
-
* with the FK column mapping and a count of rows that actually reference it.
|
|
604
|
-
*/
|
|
605
314
|
async getReferencingTables(table) {
|
|
606
|
-
return this.tryRun(() =>
|
|
607
|
-
const allTables = this.db
|
|
608
|
-
.prepare("SELECT name FROM sqlite_master WHERE type = 'table' AND name NOT LIKE 'sqlite_%'")
|
|
609
|
-
.all().map((r) => r.name);
|
|
610
|
-
const byTable = new Map();
|
|
611
|
-
for (const t of allTables) {
|
|
612
|
-
if (t === table)
|
|
613
|
-
continue;
|
|
614
|
-
try {
|
|
615
|
-
const fks = this.db.prepare(`PRAGMA foreign_key_list(${(0, generator_1.quoteIdentifier)(t)})`).all();
|
|
616
|
-
for (const fk of fks) {
|
|
617
|
-
if (fk.table === table) {
|
|
618
|
-
const arr = byTable.get(t) ?? [];
|
|
619
|
-
arr.push({ from: fk.from, to: fk.to ?? '' });
|
|
620
|
-
byTable.set(t, arr);
|
|
621
|
-
}
|
|
622
|
-
}
|
|
623
|
-
}
|
|
624
|
-
catch {
|
|
625
|
-
/* ignore unreadable tables */
|
|
626
|
-
}
|
|
627
|
-
}
|
|
628
|
-
const result = [];
|
|
629
|
-
for (const [t, refs] of byTable) {
|
|
630
|
-
let refCount = 0;
|
|
631
|
-
try {
|
|
632
|
-
const conds = refs.map((r) => `${(0, generator_1.quoteIdentifier)(r.from)} IS NOT NULL`);
|
|
633
|
-
const row = this.db.prepare(`SELECT COUNT(*) AS c FROM ${(0, generator_1.quoteIdentifier)(t)} WHERE ${conds.join(' OR ')}`).get();
|
|
634
|
-
refCount = Number(row.c);
|
|
635
|
-
}
|
|
636
|
-
catch {
|
|
637
|
-
/* count is best-effort */
|
|
638
|
-
}
|
|
639
|
-
result.push({ table: t, refs, refCount });
|
|
640
|
-
}
|
|
641
|
-
result.sort((a, b) => a.table.localeCompare(b.table));
|
|
642
|
-
return result;
|
|
643
|
-
});
|
|
315
|
+
return this.tryRun(() => (0, introspection_1.getReferencingTablesSync)(this.db, table));
|
|
644
316
|
}
|
|
645
|
-
/**
|
|
646
|
-
* Rows in `table` whose `column` equals `value` — used to show which rows in
|
|
647
|
-
* a referencing table point at a specific row via a foreign key — with the
|
|
648
|
-
* total matching count.
|
|
649
|
-
*/
|
|
650
317
|
async getRowsByFk(table, column, value, limit = 50) {
|
|
651
318
|
return this.tryRun(() => {
|
|
652
319
|
const n = Math.max(1, Math.min(500, Number(limit) || 50));
|
|
@@ -663,10 +330,117 @@ class SqliteDatabase {
|
|
|
663
330
|
return this.execResult((0, generator_1.generateRenameTable)(oldName, newName));
|
|
664
331
|
}
|
|
665
332
|
async addColumn(table, col) {
|
|
666
|
-
|
|
333
|
+
if (this.isReadOnly)
|
|
334
|
+
return this.readonlyBlocked();
|
|
335
|
+
return this.tryRun(() => {
|
|
336
|
+
if (col.primaryKey) {
|
|
337
|
+
return (0, migrations_1.modifyTableStructureSync)(this.db, table, (existingCols) => [...existingCols, col]);
|
|
338
|
+
}
|
|
339
|
+
try {
|
|
340
|
+
const colCopy = { ...col, unique: false };
|
|
341
|
+
const sql = (0, generator_1.generateAddColumn)(table, colCopy);
|
|
342
|
+
this.db.exec(sql);
|
|
343
|
+
if (col.unique) {
|
|
344
|
+
const idxName = `idx_${table}_${col.name}_unique`;
|
|
345
|
+
this.db.exec(`CREATE UNIQUE INDEX IF NOT EXISTS ${(0, generator_1.quoteIdentifier)(idxName)} ON ${(0, generator_1.quoteIdentifier)(table)} (${(0, generator_1.quoteIdentifier)(col.name)});`);
|
|
346
|
+
}
|
|
347
|
+
return { changes: 1 };
|
|
348
|
+
}
|
|
349
|
+
catch {
|
|
350
|
+
return (0, migrations_1.modifyTableStructureSync)(this.db, table, (existingCols) => [...existingCols, col]);
|
|
351
|
+
}
|
|
352
|
+
});
|
|
353
|
+
}
|
|
354
|
+
async modifyColumn(table, oldColName, newColDef) {
|
|
355
|
+
if (this.isReadOnly)
|
|
356
|
+
return this.readonlyBlocked();
|
|
357
|
+
return this.tryRun(() => {
|
|
358
|
+
const newName = String(newColDef.name || oldColName).trim();
|
|
359
|
+
const rawCols = this.db.prepare(`PRAGMA table_info(${(0, generator_1.quoteIdentifier)(table)})`).all();
|
|
360
|
+
const existing = rawCols.find((c) => c.name === oldColName);
|
|
361
|
+
if (!existing)
|
|
362
|
+
throw new Error(`Column "${oldColName}" does not exist in table "${table}".`);
|
|
363
|
+
const fks = this.db.prepare(`PRAGMA foreign_key_list(${(0, generator_1.quoteIdentifier)(table)})`).all();
|
|
364
|
+
const existingFk = fks.find((f) => f.from === oldColName);
|
|
365
|
+
const indexRows = this.db.prepare(`PRAGMA index_list(${(0, generator_1.quoteIdentifier)(table)})`).all();
|
|
366
|
+
let existingIsUnique = false;
|
|
367
|
+
for (const ix of indexRows) {
|
|
368
|
+
if (ix.unique) {
|
|
369
|
+
const ixCols = this.db.prepare(`PRAGMA index_info(${(0, generator_1.quoteIdentifier)(ix.name)})`).all().map((c) => c.name);
|
|
370
|
+
if (ixCols.length === 1 && ixCols[0] === oldColName) {
|
|
371
|
+
existingIsUnique = true;
|
|
372
|
+
break;
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
const typeMatches = !newColDef.type || newColDef.type.toUpperCase() === existing.type.toUpperCase();
|
|
377
|
+
const pkMatches = newColDef.primaryKey === undefined || newColDef.primaryKey === (existing.pk > 0);
|
|
378
|
+
const notNullMatches = newColDef.notNull === undefined || newColDef.notNull === (!!existing.notnull);
|
|
379
|
+
const uniqueMatches = newColDef.unique === undefined || newColDef.unique === existingIsUnique;
|
|
380
|
+
const defaultMatches = newColDef.defaultValue === undefined || String(newColDef.defaultValue ?? '').trim() === String(existing.dflt_value ?? '').trim();
|
|
381
|
+
const fkMatches = newColDef.foreignKey === undefined || ((!newColDef.foreignKey && !existingFk) ||
|
|
382
|
+
(newColDef.foreignKey && existingFk && newColDef.foreignKey.table === existingFk.table && newColDef.foreignKey.column === existingFk.to));
|
|
383
|
+
// Fast path: if ONLY the column name changed, use native RENAME COLUMN.
|
|
384
|
+
if (oldColName !== newName && typeMatches && pkMatches && notNullMatches && uniqueMatches && defaultMatches && fkMatches) {
|
|
385
|
+
this.db.exec((0, generator_1.generateRenameColumn)(table, oldColName, newName));
|
|
386
|
+
const existingIdxNames = new Set(indexRows.map((i) => i.name));
|
|
387
|
+
for (const suffix of ['', '_unique']) {
|
|
388
|
+
const defaultOldIdx = `idx_${table}_${oldColName}${suffix}`;
|
|
389
|
+
if (existingIdxNames.has(defaultOldIdx)) {
|
|
390
|
+
const newIdxName = `idx_${table}_${newName}${suffix}`;
|
|
391
|
+
const uniqueKeyword = suffix === '_unique' ? 'UNIQUE ' : '';
|
|
392
|
+
try {
|
|
393
|
+
this.db.exec(`DROP INDEX ${(0, generator_1.quoteIdentifier)(defaultOldIdx)};`);
|
|
394
|
+
this.db.exec(`CREATE ${uniqueKeyword}INDEX IF NOT EXISTS ${(0, generator_1.quoteIdentifier)(newIdxName)} ON ${(0, generator_1.quoteIdentifier)(table)} (${(0, generator_1.quoteIdentifier)(newName)});`);
|
|
395
|
+
}
|
|
396
|
+
catch { /* ignore */ }
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
return { changes: 1 };
|
|
400
|
+
}
|
|
401
|
+
if (oldColName === newName && typeMatches && pkMatches && notNullMatches && uniqueMatches && defaultMatches && fkMatches) {
|
|
402
|
+
return { changes: 0 };
|
|
403
|
+
}
|
|
404
|
+
return (0, migrations_1.modifyTableStructureSync)(this.db, table, (cols) => {
|
|
405
|
+
const idx = cols.findIndex((c) => c.name === oldColName);
|
|
406
|
+
if (idx === -1)
|
|
407
|
+
throw new Error(`Column "${oldColName}" does not exist in table "${table}".`);
|
|
408
|
+
const updated = [...cols];
|
|
409
|
+
updated[idx] = {
|
|
410
|
+
...updated[idx],
|
|
411
|
+
name: newName,
|
|
412
|
+
type: newColDef.type || updated[idx].type,
|
|
413
|
+
primaryKey: newColDef.primaryKey !== undefined ? newColDef.primaryKey : updated[idx].primaryKey,
|
|
414
|
+
notNull: newColDef.notNull !== undefined ? newColDef.notNull : updated[idx].notNull,
|
|
415
|
+
unique: newColDef.unique !== undefined ? newColDef.unique : updated[idx].unique,
|
|
416
|
+
defaultValue: newColDef.defaultValue !== undefined ? newColDef.defaultValue : updated[idx].defaultValue,
|
|
417
|
+
foreignKey: newColDef.foreignKey !== undefined ? newColDef.foreignKey : updated[idx].foreignKey,
|
|
418
|
+
};
|
|
419
|
+
return updated;
|
|
420
|
+
}, oldColName, newName);
|
|
421
|
+
});
|
|
667
422
|
}
|
|
668
423
|
async renameColumn(table, oldName, newName) {
|
|
669
|
-
|
|
424
|
+
if (this.isReadOnly)
|
|
425
|
+
return this.readonlyBlocked();
|
|
426
|
+
return this.tryRun(() => {
|
|
427
|
+
this.db.exec((0, generator_1.generateRenameColumn)(table, oldName, newName));
|
|
428
|
+
const indexRows = this.db.prepare(`PRAGMA index_list(${(0, generator_1.quoteIdentifier)(table)})`).all();
|
|
429
|
+
const existingIdxNames = new Set(indexRows.map((i) => i.name));
|
|
430
|
+
for (const suffix of ['', '_unique']) {
|
|
431
|
+
const defaultOldIdx = `idx_${table}_${oldName}${suffix}`;
|
|
432
|
+
if (existingIdxNames.has(defaultOldIdx)) {
|
|
433
|
+
const newIdxName = `idx_${table}_${newName}${suffix}`;
|
|
434
|
+
const uniqueKeyword = suffix === '_unique' ? 'UNIQUE ' : '';
|
|
435
|
+
try {
|
|
436
|
+
this.db.exec(`DROP INDEX ${(0, generator_1.quoteIdentifier)(defaultOldIdx)};`);
|
|
437
|
+
this.db.exec(`CREATE ${uniqueKeyword}INDEX IF NOT EXISTS ${(0, generator_1.quoteIdentifier)(newIdxName)} ON ${(0, generator_1.quoteIdentifier)(table)} (${(0, generator_1.quoteIdentifier)(newName)});`);
|
|
438
|
+
}
|
|
439
|
+
catch { /* ignore */ }
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
return { changes: 1 };
|
|
443
|
+
});
|
|
670
444
|
}
|
|
671
445
|
async dropColumn(table, column) {
|
|
672
446
|
return this.execResult((0, generator_1.generateDropColumn)(table, column));
|
|
@@ -686,7 +460,7 @@ class SqliteDatabase {
|
|
|
686
460
|
if (!cols.length)
|
|
687
461
|
return null;
|
|
688
462
|
const primaryKey = cols.filter((c) => c.pk > 0).sort((a, b) => a.pk - b.pk).map((c) => c.name);
|
|
689
|
-
return { table, columns: cols, foreignKeys: [], primaryKey };
|
|
463
|
+
return { table, columns: cols, foreignKeys: [], primaryKey, indexes: [], sql: null };
|
|
690
464
|
}
|
|
691
465
|
catch {
|
|
692
466
|
return null;
|