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.
Files changed (202) hide show
  1. package/README.md +250 -352
  2. package/dist/app.d.ts +25 -4
  3. package/dist/app.js +79 -15
  4. package/dist/app.js.map +1 -1
  5. package/dist/auth/config.d.ts +19 -0
  6. package/dist/auth/config.js +147 -0
  7. package/dist/auth/config.js.map +1 -0
  8. package/dist/auth/crypto.d.ts +27 -0
  9. package/dist/auth/crypto.js +133 -0
  10. package/dist/auth/crypto.js.map +1 -0
  11. package/dist/auth/index.d.ts +5 -0
  12. package/dist/auth/index.js +22 -0
  13. package/dist/auth/index.js.map +1 -0
  14. package/dist/auth/middleware.d.ts +14 -0
  15. package/dist/auth/middleware.js +100 -0
  16. package/dist/auth/middleware.js.map +1 -0
  17. package/dist/auth/routes.d.ts +7 -0
  18. package/dist/auth/routes.js +78 -0
  19. package/dist/auth/routes.js.map +1 -0
  20. package/dist/auth/types.d.ts +35 -0
  21. package/dist/auth/types.js +11 -0
  22. package/dist/auth/types.js.map +1 -0
  23. package/dist/cli/args.d.ts +45 -0
  24. package/dist/cli/args.js +328 -0
  25. package/dist/cli/args.js.map +1 -0
  26. package/dist/cli/config.d.ts +56 -0
  27. package/dist/cli/config.js +133 -0
  28. package/dist/cli/config.js.map +1 -0
  29. package/dist/cli/index.d.ts +4 -0
  30. package/dist/cli/index.js +21 -0
  31. package/dist/cli/index.js.map +1 -0
  32. package/dist/cli/runner.d.ts +1 -0
  33. package/dist/cli/runner.js +222 -0
  34. package/dist/cli/runner.js.map +1 -0
  35. package/dist/cli.js +2 -127
  36. package/dist/cli.js.map +1 -1
  37. package/dist/data/datasets.d.ts +17 -0
  38. package/dist/data/datasets.js +133 -0
  39. package/dist/data/datasets.js.map +1 -0
  40. package/dist/data/detector.d.ts +30 -0
  41. package/dist/data/detector.js +472 -0
  42. package/dist/data/detector.js.map +1 -0
  43. package/dist/data/engine.d.ts +10 -0
  44. package/dist/data/engine.js +211 -0
  45. package/dist/data/engine.js.map +1 -0
  46. package/dist/data/index.d.ts +5 -0
  47. package/dist/data/index.js +22 -0
  48. package/dist/data/index.js.map +1 -0
  49. package/dist/data/strategies.d.ts +30 -0
  50. package/dist/data/strategies.js +343 -0
  51. package/dist/data/strategies.js.map +1 -0
  52. package/dist/data/types.d.ts +73 -0
  53. package/dist/data/types.js +8 -0
  54. package/dist/data/types.js.map +1 -0
  55. package/dist/db/database.d.ts +14 -142
  56. package/dist/db/database.js +209 -435
  57. package/dist/db/database.js.map +1 -1
  58. package/dist/db/export.d.ts +2 -2
  59. package/dist/db/export.js +7 -2
  60. package/dist/db/export.js.map +1 -1
  61. package/dist/db/filters.d.ts +22 -0
  62. package/dist/db/filters.js +158 -0
  63. package/dist/db/filters.js.map +1 -0
  64. package/dist/db/index.d.ts +8 -0
  65. package/dist/db/index.js +25 -0
  66. package/dist/db/index.js.map +1 -0
  67. package/dist/db/introspection.d.ts +6 -0
  68. package/dist/db/introspection.js +172 -0
  69. package/dist/db/introspection.js.map +1 -0
  70. package/dist/db/manager.d.ts +32 -19
  71. package/dist/db/manager.js +203 -44
  72. package/dist/db/manager.js.map +1 -1
  73. package/dist/db/migrations.d.ts +5 -0
  74. package/dist/db/migrations.js +110 -0
  75. package/dist/db/migrations.js.map +1 -0
  76. package/dist/db/postgres.d.ts +85 -0
  77. package/dist/db/postgres.js +745 -0
  78. package/dist/db/postgres.js.map +1 -0
  79. package/dist/db/types.d.ts +206 -0
  80. package/dist/db/types.js +7 -0
  81. package/dist/db/types.js.map +1 -0
  82. package/dist/index.d.ts +9 -2
  83. package/dist/index.js +46 -4
  84. package/dist/index.js.map +1 -1
  85. package/dist/public/css/app.css +1 -1
  86. package/dist/public/css/input.css +83 -13
  87. package/dist/public/js/app.js +142 -0
  88. package/dist/public/js/browse.js +591 -104
  89. package/dist/public/js/databases.js +105 -13
  90. package/dist/public/js/designer.js +40 -2
  91. package/dist/public/js/forms.js +610 -28
  92. package/dist/public/js/icons.js +51 -0
  93. package/dist/public/js/inspector.js +638 -0
  94. package/dist/public/js/query.js +93 -3
  95. package/dist/public/js/schema.js +170 -52
  96. package/dist/public/js/seed.js +1090 -0
  97. package/dist/routes/api/helpers.d.ts +42 -0
  98. package/dist/routes/api/helpers.js +133 -0
  99. package/dist/routes/api/helpers.js.map +1 -0
  100. package/dist/routes/api/import-export.d.ts +3 -0
  101. package/dist/routes/api/import-export.js +85 -0
  102. package/dist/routes/api/import-export.js.map +1 -0
  103. package/dist/routes/api/index.d.ts +4 -0
  104. package/dist/routes/api/index.js +37 -0
  105. package/dist/routes/api/index.js.map +1 -0
  106. package/dist/routes/api/query.d.ts +3 -0
  107. package/dist/routes/api/query.js +76 -0
  108. package/dist/routes/api/query.js.map +1 -0
  109. package/dist/routes/api/rows.d.ts +3 -0
  110. package/dist/routes/api/rows.js +373 -0
  111. package/dist/routes/api/rows.js.map +1 -0
  112. package/dist/routes/api/seed.d.ts +3 -0
  113. package/dist/routes/api/seed.js +82 -0
  114. package/dist/routes/api/seed.js.map +1 -0
  115. package/dist/routes/api/tables.d.ts +3 -0
  116. package/dist/routes/api/tables.js +160 -0
  117. package/dist/routes/api/tables.js.map +1 -0
  118. package/dist/routes/databases.d.ts +8 -11
  119. package/dist/routes/databases.js +100 -66
  120. package/dist/routes/databases.js.map +1 -1
  121. package/dist/routes/index.d.ts +3 -0
  122. package/dist/routes/index.js +20 -0
  123. package/dist/routes/index.js.map +1 -0
  124. package/dist/routes/pages.d.ts +3 -3
  125. package/dist/routes/pages.js +163 -54
  126. package/dist/routes/pages.js.map +1 -1
  127. package/dist/serverless.d.ts +40 -0
  128. package/dist/serverless.js +210 -0
  129. package/dist/serverless.js.map +1 -0
  130. package/dist/sql/generator.d.ts +4 -3
  131. package/dist/sql/generator.js +103 -6
  132. package/dist/sql/generator.js.map +1 -1
  133. package/dist/sql/index.d.ts +2 -0
  134. package/dist/sql/index.js +19 -0
  135. package/dist/sql/index.js.map +1 -0
  136. package/dist/types/api.d.ts +412 -0
  137. package/dist/types/api.js +9 -0
  138. package/dist/types/api.js.map +1 -0
  139. package/dist/utils/colors.d.ts +40 -0
  140. package/dist/utils/colors.js +64 -0
  141. package/dist/utils/colors.js.map +1 -0
  142. package/dist/{util.d.ts → utils/common.d.ts} +10 -6
  143. package/dist/{util.js → utils/common.js} +80 -13
  144. package/dist/utils/common.js.map +1 -0
  145. package/dist/utils/csv.js.map +1 -0
  146. package/dist/utils/datatype.d.ts +49 -0
  147. package/dist/utils/datatype.js +222 -0
  148. package/dist/utils/datatype.js.map +1 -0
  149. package/dist/utils/icons.d.ts +11 -0
  150. package/dist/utils/icons.js +72 -0
  151. package/dist/utils/icons.js.map +1 -0
  152. package/dist/utils/index.d.ts +4 -0
  153. package/dist/utils/index.js +21 -0
  154. package/dist/utils/index.js.map +1 -0
  155. package/dist/{logger.d.ts → utils/logger.d.ts} +1 -1
  156. package/dist/utils/logger.js +40 -0
  157. package/dist/utils/logger.js.map +1 -0
  158. package/dist/views/layouts/main.hbs +33 -11
  159. package/dist/views/pages/databases.hbs +182 -74
  160. package/dist/views/pages/designer.hbs +9 -6
  161. package/dist/views/pages/error.hbs +3 -3
  162. package/dist/views/pages/form.hbs +5 -3
  163. package/dist/views/pages/home.hbs +22 -13
  164. package/dist/views/pages/login.hbs +111 -0
  165. package/dist/views/pages/query.hbs +10 -8
  166. package/dist/views/pages/schema.hbs +41 -13
  167. package/dist/views/pages/seed.hbs +232 -0
  168. package/dist/views/pages/table.hbs +155 -70
  169. package/dist/views/partials/icon.hbs +2 -0
  170. package/dist/views/partials/navbar.hbs +70 -31
  171. package/dist/views/partials/sidebar.hbs +108 -99
  172. package/package.json +12 -5
  173. package/dist/args.d.ts +0 -28
  174. package/dist/args.js +0 -187
  175. package/dist/args.js.map +0 -1
  176. package/dist/config.d.ts +0 -17
  177. package/dist/config.js +0 -23
  178. package/dist/config.js.map +0 -1
  179. package/dist/csv.js.map +0 -1
  180. package/dist/logger.js +0 -25
  181. package/dist/logger.js.map +0 -1
  182. package/dist/routes/api.d.ts +0 -9
  183. package/dist/routes/api.js +0 -786
  184. package/dist/routes/api.js.map +0 -1
  185. package/dist/test/classifier.test.d.ts +0 -1
  186. package/dist/test/classifier.test.js +0 -44
  187. package/dist/test/classifier.test.js.map +0 -1
  188. package/dist/test/csv.test.d.ts +0 -1
  189. package/dist/test/csv.test.js +0 -64
  190. package/dist/test/csv.test.js.map +0 -1
  191. package/dist/test/database.test.d.ts +0 -1
  192. package/dist/test/database.test.js +0 -402
  193. package/dist/test/database.test.js.map +0 -1
  194. package/dist/test/generator.test.d.ts +0 -1
  195. package/dist/test/generator.test.js +0 -132
  196. package/dist/test/generator.test.js.map +0 -1
  197. package/dist/test/manager.test.d.ts +0 -1
  198. package/dist/test/manager.test.js +0 -146
  199. package/dist/test/manager.test.js.map +0 -1
  200. package/dist/util.js.map +0 -1
  201. /package/dist/{csv.d.ts → utils/csv.d.ts} +0 -0
  202. /package/dist/{csv.js → utils/csv.js} +0 -0
@@ -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 = exports.INTERNAL_TABLES = void 0;
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 util_1 = require("../util");
11
- exports.INTERNAL_TABLES = {
12
- savedQueries: '_saved_queries',
13
- };
14
- /** Escape LIKE wildcards so user filter text is matched literally. */
15
- function escapeLike(s) {
16
- return String(s).replace(/[\\%_]/g, (m) => '\\' + m);
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
- * Build a parameterized WHERE clause from per-column filters.
20
- *
21
- * A filter value is either:
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, util_1.errorMessage)(err)}`);
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)(exports.INTERNAL_TABLES.savedQueries)} (
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, util_1.errorMessage)(err);
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, util_1.errorMessage)(err));
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 (err) {
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, util_1.errorMessage)(err2);
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 (used by CSV import) inside a single transaction. */
221
+ /** Insert many rows inside a single atomic transaction. */
346
222
  async insertRows(table, rows) {
347
- if (this.isReadOnly)
348
- return this.readonlyBlocked();
349
- return this.tryRun(() => {
350
- this.db.exec('BEGIN');
351
- try {
352
- let inserted = 0;
353
- for (const fields of rows) {
354
- if (!fields.length)
355
- continue;
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
- if (this.isReadOnly)
388
- return this.readonlyBlocked();
389
- return this.tryRun(() => {
390
- this.db.exec('BEGIN');
391
- try {
392
- let updated = 0;
393
- for (const { fields, where } of rows) {
394
- if (!fields.length || !where.length)
395
- continue;
396
- const sets = fields.map((f) => `${(0, generator_1.quoteIdentifier)(f.column)} = ?`);
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
- if (this.isReadOnly)
427
- return this.readonlyBlocked();
428
- return this.tryRun(() => {
429
- this.db.exec('BEGIN');
430
- try {
431
- let deleted = 0;
432
- for (const where of rows) {
433
- if (!where.length)
434
- continue;
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)(exports.INTERNAL_TABLES.savedQueries)} ORDER BY name COLLATE NOCASE`);
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)(exports.INTERNAL_TABLES.savedQueries)} (name, sql) VALUES (?, ?)`, [name, sql]);
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)(exports.INTERNAL_TABLES.savedQueries)} WHERE id = ?`, [Number(id)]);
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
- return this.execResult((0, generator_1.generateAddColumn)(table, col));
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
- return this.execResult((0, generator_1.generateRenameColumn)(table, oldName, newName));
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;