admindb 1.2.1 → 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (208) hide show
  1. package/README.md +262 -329
  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 -146
  56. package/dist/db/database.js +208 -461
  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 +74 -11
  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 +55 -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 +185 -52
  96. package/dist/public/js/seed.js +865 -152
  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 +157 -64
  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 +7 -3
  131. package/dist/sql/generator.js +144 -7
  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 +40 -11
  159. package/dist/views/pages/databases.hbs +182 -74
  160. package/dist/views/pages/designer.hbs +29 -11
  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 +8 -6
  166. package/dist/views/pages/schema.hbs +41 -13
  167. package/dist/views/pages/seed.hbs +200 -54
  168. package/dist/views/pages/table.hbs +152 -72
  169. package/dist/views/partials/icon.hbs +2 -0
  170. package/dist/views/partials/navbar.hbs +69 -25
  171. package/dist/views/partials/sidebar.hbs +108 -94
  172. package/docs/API.md +144 -0
  173. package/docs/EXAMPLES.md +576 -0
  174. package/docs/SECURITY.md +265 -0
  175. package/package.json +15 -4
  176. package/dist/args.d.ts +0 -28
  177. package/dist/args.js +0 -187
  178. package/dist/args.js.map +0 -1
  179. package/dist/config.d.ts +0 -17
  180. package/dist/config.js +0 -23
  181. package/dist/config.js.map +0 -1
  182. package/dist/csv.js.map +0 -1
  183. package/dist/data/generator.d.ts +0 -86
  184. package/dist/data/generator.js +0 -1031
  185. package/dist/data/generator.js.map +0 -1
  186. package/dist/logger.js +0 -25
  187. package/dist/logger.js.map +0 -1
  188. package/dist/routes/api.d.ts +0 -9
  189. package/dist/routes/api.js +0 -848
  190. package/dist/routes/api.js.map +0 -1
  191. package/dist/test/classifier.test.d.ts +0 -1
  192. package/dist/test/classifier.test.js +0 -44
  193. package/dist/test/classifier.test.js.map +0 -1
  194. package/dist/test/csv.test.d.ts +0 -1
  195. package/dist/test/csv.test.js +0 -64
  196. package/dist/test/csv.test.js.map +0 -1
  197. package/dist/test/database.test.d.ts +0 -1
  198. package/dist/test/database.test.js +0 -402
  199. package/dist/test/database.test.js.map +0 -1
  200. package/dist/test/generator.test.d.ts +0 -1
  201. package/dist/test/generator.test.js +0 -132
  202. package/dist/test/generator.test.js.map +0 -1
  203. package/dist/test/manager.test.d.ts +0 -1
  204. package/dist/test/manager.test.js +0 -146
  205. package/dist/test/manager.test.js.map +0 -1
  206. package/dist/util.js.map +0 -1
  207. /package/dist/{csv.d.ts → utils/csv.d.ts} +0 -0
  208. /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,89 +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
- // Indexes — the data generator needs them to detect UNIQUE columns and
280
- // JSON-expression indexes (which throw "malformed JSON" at insert time
281
- // if the column is filled with non-JSON text).
282
- const indexRows = this.db
283
- .prepare(`PRAGMA index_list(${(0, generator_1.quoteIdentifier)(table)})`)
284
- .all();
285
- const indexes = indexRows.map((ix) => {
286
- const ixCols = this.db
287
- .prepare(`PRAGMA index_info(${(0, generator_1.quoteIdentifier)(ix.name)})`)
288
- .all().map((c) => c.name);
289
- const sqlRow = this.db
290
- .prepare("SELECT sql FROM sqlite_master WHERE type = 'index' AND name = ?")
291
- .get(ix.name);
292
- return {
293
- name: ix.name,
294
- unique: !!ix.unique,
295
- partial: ix.partial,
296
- origin: ix.origin,
297
- columns: ixCols,
298
- sql: sqlRow?.sql ?? null,
299
- };
300
- });
301
- const ddlRow = this.db
302
- .prepare("SELECT sql FROM sqlite_master WHERE type = 'table' AND name = ?")
303
- .get(table);
304
- return {
305
- table,
306
- columns: cols.map((c) => ({
307
- cid: c.cid,
308
- name: c.name,
309
- type: c.type,
310
- notnull: c.notnull,
311
- dflt_value: c.dflt_value,
312
- pk: c.pk,
313
- })),
314
- foreignKeys: fks.map((f) => ({
315
- id: f.id,
316
- seq: f.seq,
317
- table: f.table,
318
- from: f.from,
319
- to: f.to,
320
- on_update: f.on_update,
321
- on_delete: f.on_delete,
322
- })),
323
- primaryKey,
324
- indexes,
325
- sql: ddlRow?.sql ?? null,
326
- };
327
- });
176
+ return this.tryRun(() => (0, introspection_1.getTableInfoSync)(this.db, table));
328
177
  }
329
178
  async getRowCount(table, filters) {
330
179
  return this.tryRun(() => {
331
180
  const info = this.getTableInfoSync(table);
332
- 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) ?? []);
333
182
  const row = this.db
334
183
  .prepare(`SELECT COUNT(*) AS c FROM ${(0, generator_1.quoteIdentifier)(table)}${where}`)
335
184
  .get(...params);
@@ -345,8 +194,8 @@ class SqliteDatabase {
345
194
  const limit = opts.limit && opts.limit > 0 ? opts.limit : 200;
346
195
  const page = opts.page && opts.page > 0 ? opts.page : 1;
347
196
  const offset = (page - 1) * limit;
348
- const { where, params } = buildFilterClause(opts.filters, cols.map((c) => c.name));
349
- 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}`;
350
199
  if (orderCol)
351
200
  sql += ` ORDER BY ${(0, generator_1.quoteIdentifier)(orderCol)} ${orderDir}`;
352
201
  sql += ` LIMIT ${limit} OFFSET ${offset}`;
@@ -354,12 +203,12 @@ class SqliteDatabase {
354
203
  });
355
204
  }
356
205
  async getAllRows(table) {
357
- 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());
358
207
  }
359
208
  async getRow(table, where) {
360
209
  return this.tryRun(() => {
361
210
  const conds = where.map((w) => `${(0, generator_1.quoteIdentifier)(w.column)} = ?`);
362
- 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`;
363
212
  const row = this.db.prepare(sql).get(...where.map((w) => this.toBind(w.value)));
364
213
  return row ? row : null;
365
214
  });
@@ -369,34 +218,19 @@ class SqliteDatabase {
369
218
  const sql = `INSERT INTO ${(0, generator_1.quoteIdentifier)(table)} (${cols.join(', ')}) VALUES (${fields.map(() => '?').join(', ')})`;
370
219
  return this.run(sql, fields.map((f) => this.toBind(f.value)));
371
220
  }
372
- /** Insert many rows (used by CSV import) inside a single transaction. */
221
+ /** Insert many rows inside a single atomic transaction. */
373
222
  async insertRows(table, rows) {
374
- if (this.isReadOnly)
375
- return this.readonlyBlocked();
376
- return this.tryRun(() => {
377
- this.db.exec('BEGIN');
378
- try {
379
- let inserted = 0;
380
- for (const fields of rows) {
381
- if (!fields.length)
382
- continue;
383
- const cols = fields.map((f) => (0, generator_1.quoteIdentifier)(f.column));
384
- const sql = `INSERT INTO ${(0, generator_1.quoteIdentifier)(table)} (${cols.join(', ')}) VALUES (${fields.map(() => '?').join(', ')})`;
385
- this.db.prepare(sql).run(...fields.map((f) => this.toBind(f.value)));
386
- inserted += 1;
387
- }
388
- this.db.exec('COMMIT');
389
- return { inserted, skipped: rows.length - inserted };
390
- }
391
- catch (err) {
392
- try {
393
- this.db.exec('ROLLBACK');
394
- }
395
- catch {
396
- /* ignore */
397
- }
398
- 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;
399
232
  }
233
+ return { inserted, skipped: rows.length - inserted };
400
234
  });
401
235
  }
402
236
  async updateRow(table, fields, where) {
@@ -405,39 +239,20 @@ class SqliteDatabase {
405
239
  const sql = `UPDATE ${(0, generator_1.quoteIdentifier)(table)} SET ${sets.join(', ')} WHERE ${conds.join(' AND ')}`;
406
240
  return this.run(sql, [...fields.map((f) => this.toBind(f.value)), ...where.map((w) => this.toBind(w.value))]);
407
241
  }
408
- /**
409
- * Apply many row updates (each with its own SET fields + primary-key WHERE
410
- * clause) inside a single transaction. Used by the bulk "apply changes"
411
- * flow of inline grid editing. Rolls everything back if any row fails.
412
- */
242
+ /** Apply many row updates inside a single atomic transaction. */
413
243
  async updateRows(table, rows) {
414
- if (this.isReadOnly)
415
- return this.readonlyBlocked();
416
- return this.tryRun(() => {
417
- this.db.exec('BEGIN');
418
- try {
419
- let updated = 0;
420
- for (const { fields, where } of rows) {
421
- if (!fields.length || !where.length)
422
- continue;
423
- const sets = fields.map((f) => `${(0, generator_1.quoteIdentifier)(f.column)} = ?`);
424
- const conds = where.map((w) => `${(0, generator_1.quoteIdentifier)(w.column)} = ?`);
425
- const sql = `UPDATE ${(0, generator_1.quoteIdentifier)(table)} SET ${sets.join(', ')} WHERE ${conds.join(' AND ')}`;
426
- const info = this.db.prepare(sql).run(...[...fields.map((f) => this.toBind(f.value)), ...where.map((w) => this.toBind(w.value))]);
427
- updated += Number(info.changes);
428
- }
429
- this.db.exec('COMMIT');
430
- return { updated };
431
- }
432
- catch (err) {
433
- try {
434
- this.db.exec('ROLLBACK');
435
- }
436
- catch {
437
- /* ignore */
438
- }
439
- 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);
440
254
  }
255
+ return { updated };
441
256
  });
442
257
  }
443
258
  async deleteRow(table, where) {
@@ -445,43 +260,21 @@ class SqliteDatabase {
445
260
  const sql = `DELETE FROM ${(0, generator_1.quoteIdentifier)(table)} WHERE ${conds.join(' AND ')}`;
446
261
  return this.run(sql, where.map((w) => this.toBind(w.value)));
447
262
  }
448
- /**
449
- * Delete many rows (each identified by its primary-key WHERE clause) inside a
450
- * single transaction. Used by bulk delete.
451
- */
263
+ /** Delete many rows inside a single atomic transaction. */
452
264
  async deleteRows(table, rows) {
453
- if (this.isReadOnly)
454
- return this.readonlyBlocked();
455
- return this.tryRun(() => {
456
- this.db.exec('BEGIN');
457
- try {
458
- let deleted = 0;
459
- for (const where of rows) {
460
- if (!where.length)
461
- continue;
462
- const conds = where.map((w) => `${(0, generator_1.quoteIdentifier)(w.column)} = ?`);
463
- const sql = `DELETE FROM ${(0, generator_1.quoteIdentifier)(table)} WHERE ${conds.join(' AND ')}`;
464
- const info = this.db.prepare(sql).run(...where.map((w) => this.toBind(w.value)));
465
- deleted += Number(info.changes);
466
- }
467
- this.db.exec('COMMIT');
468
- return { deleted };
469
- }
470
- catch (err) {
471
- try {
472
- this.db.exec('ROLLBACK');
473
- }
474
- catch {
475
- /* ignore */
476
- }
477
- 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);
478
274
  }
275
+ return { deleted };
479
276
  });
480
277
  }
481
- /**
482
- * Fetch the rows matching the given primary-key WHERE clauses in one query
483
- * (`(a = ? AND b = ?) OR …`). Used by bulk export and FK-impact checks.
484
- */
485
278
  async getRowsByPks(table, pks) {
486
279
  return this.tryRun(() => {
487
280
  const groups = [];
@@ -494,7 +287,7 @@ class SqliteDatabase {
494
287
  }
495
288
  if (!groups.length)
496
289
  return [];
497
- 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 ')}`;
498
291
  return this.db.prepare(sql).all(...params);
499
292
  });
500
293
  }
@@ -507,173 +300,20 @@ class SqliteDatabase {
507
300
  });
508
301
  }
509
302
  async listSavedQueries() {
510
- 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`);
511
304
  }
512
305
  async saveQuery(name, sql) {
513
- 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]);
514
307
  }
515
308
  async deleteSavedQuery(id) {
516
- 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)]);
517
310
  }
518
- // ---- Schema introspection & ALTER -------------------------------------
519
- /**
520
- * Detailed schema for a table: columns with constraint info, indexes,
521
- * foreign keys, and which other tables reference it.
522
- */
523
311
  async getSchema(table) {
524
- return this.tryRun(() => {
525
- const info = this.getTableInfoSync(table);
526
- if (!info || info.columns.length === 0)
527
- throw new Error(`Table "${table}" does not exist.`);
528
- // Foreign keys declared on this table (child FKs).
529
- const fkRows = this.db
530
- .prepare(`PRAGMA foreign_key_list(${(0, generator_1.quoteIdentifier)(table)})`)
531
- .all();
532
- const foreignKeys = fkRows.map((f) => ({
533
- id: f.id,
534
- seq: f.seq,
535
- table: f.table,
536
- from: f.from,
537
- to: f.to,
538
- on_update: f.on_update,
539
- on_delete: f.on_delete,
540
- }));
541
- const indexRows = this.db
542
- .prepare(`PRAGMA index_list(${(0, generator_1.quoteIdentifier)(table)})`)
543
- .all();
544
- const indexes = indexRows.map((ix) => {
545
- const cols = this.db
546
- .prepare(`PRAGMA index_info(${(0, generator_1.quoteIdentifier)(ix.name)})`)
547
- .all().map((c) => c.name);
548
- const sqlRow = this.db
549
- .prepare("SELECT sql FROM sqlite_master WHERE type = 'index' AND name = ?")
550
- .get(ix.name);
551
- return { name: ix.name, unique: !!ix.unique, partial: ix.partial, origin: ix.origin, columns: cols, sql: sqlRow?.sql ?? null };
552
- });
553
- const uniqueCols = new Set();
554
- const indexedCols = new Set();
555
- const originByName = new Map(indexRows.map((ix) => [ix.name, ix.origin]));
556
- for (const ix of indexes) {
557
- if (ix.unique)
558
- ix.columns.forEach((c) => uniqueCols.add(c));
559
- // 'c' = explicit CREATE INDEX; these block DROP COLUMN.
560
- if (originByName.get(ix.name) === 'c')
561
- ix.columns.forEach((c) => indexedCols.add(c));
562
- }
563
- // Foreign keys in OTHER tables that point at this table.
564
- const refs = [];
565
- const allTables = this.db
566
- .prepare("SELECT name FROM sqlite_master WHERE type = 'table' AND name NOT LIKE 'sqlite_%'")
567
- .all().map((r) => r.name);
568
- for (const t of allTables) {
569
- if (t === table)
570
- continue;
571
- try {
572
- const fks = this.db
573
- .prepare(`PRAGMA foreign_key_list(${(0, generator_1.quoteIdentifier)(t)})`)
574
- .all();
575
- for (const fk of fks) {
576
- if (fk.table === table)
577
- refs.push({ table: t, from: fk.from, to: fk.to ?? '' });
578
- }
579
- }
580
- catch {
581
- /* ignore unreadable tables */
582
- }
583
- }
584
- const refsByTo = new Map();
585
- for (const r of refs) {
586
- const arr = refsByTo.get(r.to) ?? [];
587
- arr.push({ table: r.table, from: r.from });
588
- refsByTo.set(r.to, arr);
589
- }
590
- const fkByFrom = new Map();
591
- for (const fk of foreignKeys)
592
- fkByFrom.set(fk.from, fk);
593
- const columns = info.columns.map((c) => {
594
- const unique = c.pk > 0 || uniqueCols.has(c.name);
595
- const indexed = indexedCols.has(c.name);
596
- const referencedBy = refsByTo.get(c.name) ?? [];
597
- const fk = fkByFrom.get(c.name) ?? null;
598
- const dropBlockers = [];
599
- if (c.pk > 0)
600
- dropBlockers.push('Is part of the primary key');
601
- if (unique && c.pk === 0)
602
- dropBlockers.push('Has a UNIQUE constraint');
603
- if (indexed)
604
- dropBlockers.push('Is used by an index');
605
- if (referencedBy.length)
606
- dropBlockers.push(`Referenced by a foreign key in "${referencedBy[0].table}"`);
607
- if (fk)
608
- dropBlockers.push('Is part of a foreign key');
609
- return {
610
- ...c,
611
- unique,
612
- indexed,
613
- referencedBy,
614
- fk,
615
- canDrop: dropBlockers.length === 0,
616
- dropBlockers,
617
- };
618
- });
619
- return {
620
- table,
621
- columns,
622
- foreignKeys,
623
- indexes,
624
- references: refs,
625
- };
626
- });
312
+ return this.tryRun(() => (0, introspection_1.getSchemaSync)(this.db, table));
627
313
  }
628
- /**
629
- * Other tables with foreign keys pointing at `table`, grouped by table, each
630
- * with the FK column mapping and a count of rows that actually reference it.
631
- */
632
314
  async getReferencingTables(table) {
633
- return this.tryRun(() => {
634
- const allTables = this.db
635
- .prepare("SELECT name FROM sqlite_master WHERE type = 'table' AND name NOT LIKE 'sqlite_%'")
636
- .all().map((r) => r.name);
637
- const byTable = new Map();
638
- for (const t of allTables) {
639
- if (t === table)
640
- continue;
641
- try {
642
- const fks = this.db.prepare(`PRAGMA foreign_key_list(${(0, generator_1.quoteIdentifier)(t)})`).all();
643
- for (const fk of fks) {
644
- if (fk.table === table) {
645
- const arr = byTable.get(t) ?? [];
646
- arr.push({ from: fk.from, to: fk.to ?? '' });
647
- byTable.set(t, arr);
648
- }
649
- }
650
- }
651
- catch {
652
- /* ignore unreadable tables */
653
- }
654
- }
655
- const result = [];
656
- for (const [t, refs] of byTable) {
657
- let refCount = 0;
658
- try {
659
- const conds = refs.map((r) => `${(0, generator_1.quoteIdentifier)(r.from)} IS NOT NULL`);
660
- const row = this.db.prepare(`SELECT COUNT(*) AS c FROM ${(0, generator_1.quoteIdentifier)(t)} WHERE ${conds.join(' OR ')}`).get();
661
- refCount = Number(row.c);
662
- }
663
- catch {
664
- /* count is best-effort */
665
- }
666
- result.push({ table: t, refs, refCount });
667
- }
668
- result.sort((a, b) => a.table.localeCompare(b.table));
669
- return result;
670
- });
315
+ return this.tryRun(() => (0, introspection_1.getReferencingTablesSync)(this.db, table));
671
316
  }
672
- /**
673
- * Rows in `table` whose `column` equals `value` — used to show which rows in
674
- * a referencing table point at a specific row via a foreign key — with the
675
- * total matching count.
676
- */
677
317
  async getRowsByFk(table, column, value, limit = 50) {
678
318
  return this.tryRun(() => {
679
319
  const n = Math.max(1, Math.min(500, Number(limit) || 50));
@@ -690,10 +330,117 @@ class SqliteDatabase {
690
330
  return this.execResult((0, generator_1.generateRenameTable)(oldName, newName));
691
331
  }
692
332
  async addColumn(table, col) {
693
- 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
+ });
694
422
  }
695
423
  async renameColumn(table, oldName, newName) {
696
- 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
+ });
697
444
  }
698
445
  async dropColumn(table, column) {
699
446
  return this.execResult((0, generator_1.generateDropColumn)(table, column));