sqlite-hub 0.1.3

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 (76) hide show
  1. package/.npmingnore +4 -0
  2. package/README.md +46 -0
  3. package/assets/images/logo.webp +0 -0
  4. package/assets/images/logo_extrasmall.webp +0 -0
  5. package/assets/images/logo_raw.png +0 -0
  6. package/assets/images/logo_small.webp +0 -0
  7. package/assets/mockups/connections.png +0 -0
  8. package/assets/mockups/data.png +0 -0
  9. package/assets/mockups/data_edit.png +0 -0
  10. package/assets/mockups/home.png +0 -0
  11. package/assets/mockups/overview.png +0 -0
  12. package/assets/mockups/sql_editor.png +0 -0
  13. package/assets/mockups/structure.png +0 -0
  14. package/bin/sqlite-hub.js +116 -0
  15. package/changelog.md +3 -0
  16. package/data/.gitkeep +0 -0
  17. package/index.html +100 -0
  18. package/js/api.js +193 -0
  19. package/js/app.js +520 -0
  20. package/js/components/actionBar.js +8 -0
  21. package/js/components/appShell.js +17 -0
  22. package/js/components/badges.js +5 -0
  23. package/js/components/bottomTabs.js +37 -0
  24. package/js/components/connectionCard.js +106 -0
  25. package/js/components/dataGrid.js +47 -0
  26. package/js/components/emptyState.js +159 -0
  27. package/js/components/metricCard.js +32 -0
  28. package/js/components/modal.js +317 -0
  29. package/js/components/pageHeader.js +33 -0
  30. package/js/components/queryEditor.js +121 -0
  31. package/js/components/queryResults.js +107 -0
  32. package/js/components/rowEditorPanel.js +164 -0
  33. package/js/components/sidebar.js +57 -0
  34. package/js/components/statusBar.js +39 -0
  35. package/js/components/toast.js +39 -0
  36. package/js/components/topNav.js +27 -0
  37. package/js/router.js +66 -0
  38. package/js/store.js +1092 -0
  39. package/js/utils/format.js +179 -0
  40. package/js/views/connections.js +133 -0
  41. package/js/views/data.js +400 -0
  42. package/js/views/editor.js +259 -0
  43. package/js/views/landing.js +11 -0
  44. package/js/views/overview.js +220 -0
  45. package/js/views/settings.js +109 -0
  46. package/js/views/structure.js +242 -0
  47. package/package.json +18 -0
  48. package/publish_brew.sh +444 -0
  49. package/publish_npm.sh +241 -0
  50. package/server/routes/connections.js +146 -0
  51. package/server/routes/data.js +59 -0
  52. package/server/routes/export.js +25 -0
  53. package/server/routes/overview.js +39 -0
  54. package/server/routes/settings.js +50 -0
  55. package/server/routes/sql.js +50 -0
  56. package/server/routes/structure.js +38 -0
  57. package/server/server.js +136 -0
  58. package/server/services/sqlite/connectionManager.js +306 -0
  59. package/server/services/sqlite/dataBrowserService.js +255 -0
  60. package/server/services/sqlite/exportService.js +34 -0
  61. package/server/services/sqlite/importService.js +111 -0
  62. package/server/services/sqlite/introspection.js +302 -0
  63. package/server/services/sqlite/overviewService.js +109 -0
  64. package/server/services/sqlite/sqlExecutor.js +434 -0
  65. package/server/services/sqlite/structureService.js +60 -0
  66. package/server/services/storage/appStateStore.js +530 -0
  67. package/server/utils/csv.js +34 -0
  68. package/server/utils/errors.js +175 -0
  69. package/server/utils/fileValidation.js +135 -0
  70. package/server/utils/identifier.js +38 -0
  71. package/server/utils/sqliteTypes.js +112 -0
  72. package/styles/base.css +176 -0
  73. package/styles/components.css +323 -0
  74. package/styles/layout.css +101 -0
  75. package/styles/tokens.css +49 -0
  76. package/styles/views.css +84 -0
@@ -0,0 +1,306 @@
1
+ const path = require("node:path");
2
+ const crypto = require("node:crypto");
3
+ const Database = require("better-sqlite3");
4
+ const {
5
+ ConflictError,
6
+ DatabaseRequiredError,
7
+ NotFoundError,
8
+ ReadOnlyError,
9
+ ValidationError,
10
+ mapSqliteError,
11
+ } = require("../../utils/errors");
12
+ const {
13
+ ensureFileDoesNotExist,
14
+ ensureParentDirectory,
15
+ getFileMetadata,
16
+ isWritable,
17
+ resolveUserPath,
18
+ validateSqlitePath,
19
+ } = require("../../utils/fileValidation");
20
+
21
+ class ConnectionManager {
22
+ constructor({ appStateStore }) {
23
+ this.appStateStore = appStateStore;
24
+ this.current = null;
25
+ }
26
+
27
+ initialize() {
28
+ const activeId = this.appStateStore.getActiveConnectionId();
29
+
30
+ if (!activeId) {
31
+ return null;
32
+ }
33
+
34
+ const recent = this.appStateStore
35
+ .getRecentConnections()
36
+ .find((connection) => connection.id === activeId);
37
+
38
+ if (!recent) {
39
+ return null;
40
+ }
41
+
42
+ try {
43
+ return this.openConnection({
44
+ filePath: recent.path,
45
+ label: recent.label,
46
+ id: recent.id,
47
+ makeActive: true,
48
+ });
49
+ } catch (error) {
50
+ this.appStateStore.setActiveConnectionId(null);
51
+ return null;
52
+ }
53
+ }
54
+
55
+ buildConnectionRecord(filePath, options = {}) {
56
+ const metadata = getFileMetadata(filePath);
57
+
58
+ return {
59
+ id:
60
+ options.id ??
61
+ `conn_${crypto.createHash("sha1").update(filePath).digest("hex").slice(0, 16)}`,
62
+ label: options.label?.trim() || path.basename(filePath),
63
+ path: filePath,
64
+ lastOpenedAt: new Date().toISOString(),
65
+ lastModifiedAt: metadata.lastModifiedAt,
66
+ sizeBytes: metadata.sizeBytes,
67
+ readOnly: options.readOnly ?? !isWritable(filePath),
68
+ };
69
+ }
70
+
71
+ openRawDatabase(filePath, options = {}) {
72
+ const db = new Database(filePath, {
73
+ readonly: Boolean(options.readOnly),
74
+ fileMustExist: Boolean(options.fileMustExist),
75
+ timeout: this.appStateStore.getSettings().busyTimeoutMs,
76
+ });
77
+
78
+ try {
79
+ db.pragma(`busy_timeout = ${this.appStateStore.getSettings().busyTimeoutMs}`);
80
+ db.pragma("foreign_keys");
81
+ db.prepare("SELECT sqlite_version() AS version").get();
82
+ return db;
83
+ } catch (error) {
84
+ db.close();
85
+ throw mapSqliteError(error);
86
+ }
87
+ }
88
+
89
+ closeCurrent() {
90
+ if (this.current?.db) {
91
+ this.current.db.close();
92
+ }
93
+
94
+ this.current = null;
95
+ }
96
+
97
+ openConnection({ filePath, label, id, makeActive = true, readOnly = false }) {
98
+ const resolvedPath = validateSqlitePath(filePath, { mustExist: true });
99
+ const db = this.openRawDatabase(resolvedPath, {
100
+ fileMustExist: true,
101
+ readOnly,
102
+ });
103
+
104
+ this.closeCurrent();
105
+
106
+ const connection = this.buildConnectionRecord(resolvedPath, {
107
+ id,
108
+ label,
109
+ readOnly,
110
+ });
111
+
112
+ this.current = {
113
+ ...connection,
114
+ db,
115
+ };
116
+
117
+ if (makeActive) {
118
+ this.appStateStore.upsertRecentConnection(connection);
119
+ }
120
+
121
+ return this.getActiveConnection();
122
+ }
123
+
124
+ createConnection({ filePath, label }) {
125
+ const resolvedPath = resolveUserPath(filePath);
126
+ ensureFileDoesNotExist(resolvedPath, "SQLite database");
127
+ ensureParentDirectory(resolvedPath);
128
+
129
+ const extension = path.extname(resolvedPath).toLowerCase();
130
+
131
+ if (![".db", ".sqlite", ".sqlite3"].includes(extension)) {
132
+ throw new ValidationError(
133
+ "SQLite database must use one of: .db, .sqlite, .sqlite3"
134
+ );
135
+ }
136
+
137
+ const db = this.openRawDatabase(resolvedPath, {
138
+ fileMustExist: false,
139
+ readOnly: false,
140
+ });
141
+
142
+ try {
143
+ db.exec("VACUUM;");
144
+ } catch (error) {
145
+ db.close();
146
+ throw mapSqliteError(error);
147
+ }
148
+
149
+ db.close();
150
+
151
+ return this.openConnection({
152
+ filePath: resolvedPath,
153
+ label,
154
+ makeActive: true,
155
+ readOnly: false,
156
+ });
157
+ }
158
+
159
+ selectActiveConnection(id) {
160
+ const recent = this.appStateStore
161
+ .getRecentConnections()
162
+ .find((connection) => connection.id === id);
163
+
164
+ if (!recent) {
165
+ throw new NotFoundError(`Recent connection not found: ${id}`);
166
+ }
167
+
168
+ return this.openConnection({
169
+ filePath: recent.path,
170
+ label: recent.label,
171
+ id: recent.id,
172
+ makeActive: true,
173
+ readOnly: recent.readOnly,
174
+ });
175
+ }
176
+
177
+ removeRecentConnection(id) {
178
+ const state = this.appStateStore.removeRecentConnection(id);
179
+
180
+ if (this.current?.id === id) {
181
+ this.closeCurrent();
182
+ }
183
+
184
+ return state.recentConnections;
185
+ }
186
+
187
+ updateRecentConnection(id, { filePath, label, readOnly = false }) {
188
+ const recentConnections = this.appStateStore.getRecentConnections();
189
+ const existing = recentConnections.find((connection) => connection.id === id);
190
+
191
+ if (!existing) {
192
+ throw new NotFoundError(`Recent connection not found: ${id}`);
193
+ }
194
+
195
+ const resolvedPath = validateSqlitePath(filePath, { mustExist: true });
196
+ const duplicateConnection = recentConnections.find(
197
+ (connection) => connection.id !== id && connection.path === resolvedPath
198
+ );
199
+
200
+ if (duplicateConnection) {
201
+ throw new ConflictError(`A saved connection already targets: ${resolvedPath}`);
202
+ }
203
+
204
+ const normalizedLabel = label?.trim() || path.basename(resolvedPath);
205
+ const normalizedReadOnly = Boolean(readOnly);
206
+
207
+ if (this.current?.id === id) {
208
+ return this.openConnection({
209
+ filePath: resolvedPath,
210
+ label: normalizedLabel,
211
+ id,
212
+ makeActive: true,
213
+ readOnly: normalizedReadOnly,
214
+ });
215
+ }
216
+
217
+ const db = this.openRawDatabase(resolvedPath, {
218
+ fileMustExist: true,
219
+ readOnly: normalizedReadOnly,
220
+ });
221
+
222
+ db.close();
223
+
224
+ const metadata = getFileMetadata(resolvedPath);
225
+ const nextConnection = {
226
+ ...existing,
227
+ label: normalizedLabel,
228
+ path: resolvedPath,
229
+ lastModifiedAt: metadata.lastModifiedAt,
230
+ sizeBytes: metadata.sizeBytes,
231
+ readOnly: normalizedReadOnly,
232
+ };
233
+
234
+ this.appStateStore.updateRecentConnection(id, () => nextConnection);
235
+
236
+ return {
237
+ ...nextConnection,
238
+ isActive: false,
239
+ };
240
+ }
241
+
242
+ getActiveConnection() {
243
+ if (!this.current) {
244
+ return null;
245
+ }
246
+
247
+ const { db, ...connection } = this.current;
248
+ return {
249
+ ...connection,
250
+ isActive: true,
251
+ };
252
+ }
253
+
254
+ getActiveDatabase() {
255
+ if (!this.current?.db) {
256
+ throw new DatabaseRequiredError();
257
+ }
258
+
259
+ return this.current.db;
260
+ }
261
+
262
+ assertWritable() {
263
+ const connection = this.getActiveConnection();
264
+
265
+ if (!connection) {
266
+ throw new DatabaseRequiredError();
267
+ }
268
+
269
+ if (connection.readOnly) {
270
+ throw new ReadOnlyError(
271
+ `Database is opened in read-only mode: ${connection.path}`
272
+ );
273
+ }
274
+
275
+ return connection;
276
+ }
277
+
278
+ listRecentConnections() {
279
+ const activeId = this.current?.id ?? this.appStateStore.getActiveConnectionId();
280
+
281
+ return this.appStateStore.getRecentConnections().map((connection) => ({
282
+ ...connection,
283
+ isActive: connection.id === activeId,
284
+ }));
285
+ }
286
+
287
+ getStatus() {
288
+ const active = this.getActiveConnection();
289
+
290
+ if (!active) {
291
+ return {
292
+ connected: false,
293
+ activeConnection: null,
294
+ };
295
+ }
296
+
297
+ return {
298
+ connected: true,
299
+ activeConnection: active,
300
+ };
301
+ }
302
+ }
303
+
304
+ module.exports = {
305
+ ConnectionManager,
306
+ };
@@ -0,0 +1,255 @@
1
+ const { NotFoundError, ValidationError } = require("../../utils/errors");
2
+ const { quoteIdentifier } = require("../../utils/identifier");
3
+ const {
4
+ deserializeSqliteValue,
5
+ serializeRow,
6
+ serializeRows,
7
+ } = require("../../utils/sqliteTypes");
8
+ const { getRawStructureEntries, getTableDetail } = require("./introspection");
9
+
10
+ const DEFAULT_LIMIT = 50;
11
+ const MAX_LIMIT = 100;
12
+
13
+ function buildRowIdentity(tableDetail, row) {
14
+ if (tableDetail.identityStrategy?.type === "rowid") {
15
+ return {
16
+ kind: "rowid",
17
+ values: {
18
+ rowid: row.__rowid__,
19
+ },
20
+ };
21
+ }
22
+
23
+ if (tableDetail.identityStrategy?.type === "primaryKey") {
24
+ return {
25
+ kind: "primaryKey",
26
+ columns: tableDetail.identityStrategy.columns,
27
+ values: Object.fromEntries(
28
+ tableDetail.identityStrategy.columns.map((columnName) => [columnName, row[columnName]])
29
+ ),
30
+ };
31
+ }
32
+
33
+ return null;
34
+ }
35
+
36
+ function normalizePaginationOptions(options = {}) {
37
+ const limit = Number(options.limit ?? DEFAULT_LIMIT);
38
+ const offset = Number(options.offset ?? 0);
39
+
40
+ if (!Number.isInteger(limit) || limit < 1 || limit > MAX_LIMIT) {
41
+ throw new ValidationError(`limit must be an integer between 1 and ${MAX_LIMIT}.`);
42
+ }
43
+
44
+ if (!Number.isInteger(offset) || offset < 0) {
45
+ throw new ValidationError("offset must be a non-negative integer.");
46
+ }
47
+
48
+ return {
49
+ limit,
50
+ offset,
51
+ };
52
+ }
53
+
54
+ class DataBrowserService {
55
+ constructor({ connectionManager }) {
56
+ this.connectionManager = connectionManager;
57
+ }
58
+
59
+ listTables() {
60
+ const db = this.connectionManager.getActiveDatabase();
61
+
62
+ return getRawStructureEntries(db)
63
+ .filter((entry) => entry.type === "table")
64
+ .map((entry) => ({
65
+ name: entry.name,
66
+ }));
67
+ }
68
+
69
+ getTableData(tableName, options = {}) {
70
+ const db = this.connectionManager.getActiveDatabase();
71
+ const tableDetail = getTableDetail(db, tableName);
72
+ const { limit, offset } = normalizePaginationOptions(options);
73
+ const selectExpression =
74
+ tableDetail.identityStrategy?.type === "rowid" ? "rowid AS __rowid__, *" : "*";
75
+ const orderClause = this.buildOrderClause(tableDetail);
76
+ const statement = db.prepare(
77
+ [
78
+ `SELECT ${selectExpression} FROM ${quoteIdentifier(tableName)}`,
79
+ orderClause ? `ORDER BY ${orderClause}` : "",
80
+ "LIMIT ? OFFSET ?",
81
+ ]
82
+ .filter(Boolean)
83
+ .join(" ")
84
+ );
85
+ const rawRows = statement.all(limit, offset);
86
+ const columns = statement
87
+ .columns()
88
+ .map((column) => column.name)
89
+ .filter((columnName) => columnName !== "__rowid__");
90
+
91
+ const rows = serializeRows(rawRows).map((row) => {
92
+ const identity = buildRowIdentity(tableDetail, row);
93
+
94
+ if (Object.prototype.hasOwnProperty.call(row, "__rowid__")) {
95
+ delete row.__rowid__;
96
+ }
97
+
98
+ return {
99
+ ...row,
100
+ __identity: identity,
101
+ };
102
+ });
103
+
104
+ return {
105
+ name: tableDetail.name,
106
+ type: tableDetail.type,
107
+ rowCount: tableDetail.rowCount ?? rows.length,
108
+ limit,
109
+ offset,
110
+ page: Math.floor(offset / limit) + 1,
111
+ pageCount: Math.max(1, Math.ceil((tableDetail.rowCount ?? rows.length) / limit)),
112
+ columns,
113
+ columnMeta: tableDetail.columns,
114
+ rows,
115
+ identityStrategy: tableDetail.identityStrategy,
116
+ notSafelyUpdatable: tableDetail.notSafelyUpdatable,
117
+ };
118
+ }
119
+
120
+ updateTableRow(tableName, payload = {}) {
121
+ this.connectionManager.assertWritable();
122
+
123
+ const db = this.connectionManager.getActiveDatabase();
124
+ const tableDetail = getTableDetail(db, tableName, { includeRowCount: false });
125
+ const values = payload.values ?? {};
126
+ const identity = payload.identity ?? null;
127
+
128
+ if (tableDetail.notSafelyUpdatable) {
129
+ throw new ValidationError(
130
+ `Table ${tableName} cannot be safely updated because it has no stable row identity.`
131
+ );
132
+ }
133
+
134
+ const identityColumnSet = new Set(
135
+ tableDetail.identityStrategy?.type === "primaryKey"
136
+ ? tableDetail.identityStrategy.columns
137
+ : []
138
+ );
139
+ const editableColumns = tableDetail.columns.filter(
140
+ (column) => column.visible && !column.generated && !identityColumnSet.has(column.name)
141
+ );
142
+ const columnsToUpdate = editableColumns.filter((column) =>
143
+ Object.prototype.hasOwnProperty.call(values, column.name)
144
+ );
145
+
146
+ if (!columnsToUpdate.length) {
147
+ throw new ValidationError("No editable column values were provided.");
148
+ }
149
+
150
+ const where = this.buildWhereClause(tableDetail, identity);
151
+ const setClause = columnsToUpdate
152
+ .map((column) => `${quoteIdentifier(column.name)} = ?`)
153
+ .join(", ");
154
+ const setParams = columnsToUpdate.map((column) =>
155
+ deserializeSqliteValue(values[column.name])
156
+ );
157
+
158
+ db.prepare(
159
+ `UPDATE ${quoteIdentifier(tableName)} SET ${setClause} WHERE ${where.clause}`
160
+ ).run(...setParams, ...where.params);
161
+
162
+ const updatedRow = this.getRowByIdentity(db, tableDetail, where);
163
+
164
+ if (!updatedRow) {
165
+ throw new NotFoundError(`Row not found in table: ${tableName}`);
166
+ }
167
+
168
+ return {
169
+ tableName,
170
+ row: updatedRow,
171
+ };
172
+ }
173
+
174
+ buildWhereClause(tableDetail, identity) {
175
+ if (tableDetail.identityStrategy?.type === "rowid") {
176
+ const rowid = identity?.values?.rowid;
177
+
178
+ if (rowid === undefined) {
179
+ throw new ValidationError("rowid is required to update this row.");
180
+ }
181
+
182
+ return {
183
+ clause: "rowid IS ?",
184
+ params: [deserializeSqliteValue(rowid)],
185
+ };
186
+ }
187
+
188
+ if (tableDetail.identityStrategy?.type === "primaryKey") {
189
+ const columns = tableDetail.identityStrategy.columns ?? [];
190
+
191
+ if (!columns.length) {
192
+ throw new ValidationError("Primary key columns are required to update this row.");
193
+ }
194
+
195
+ return {
196
+ clause: columns.map((columnName) => `${quoteIdentifier(columnName)} IS ?`).join(" AND "),
197
+ params: columns.map((columnName) => {
198
+ if (!Object.prototype.hasOwnProperty.call(identity?.values ?? {}, columnName)) {
199
+ throw new ValidationError(`Missing primary key value for ${columnName}.`);
200
+ }
201
+
202
+ return deserializeSqliteValue(identity.values[columnName]);
203
+ }),
204
+ };
205
+ }
206
+
207
+ throw new ValidationError(
208
+ `Table ${tableDetail.name} cannot be updated because it has no stable row identity.`
209
+ );
210
+ }
211
+
212
+ buildOrderClause(tableDetail) {
213
+ if (tableDetail.identityStrategy?.type === "rowid") {
214
+ return "rowid ASC";
215
+ }
216
+
217
+ if (tableDetail.identityStrategy?.type === "primaryKey") {
218
+ return tableDetail.identityStrategy.columns
219
+ .map((columnName) => `${quoteIdentifier(columnName)} ASC`)
220
+ .join(", ");
221
+ }
222
+
223
+ return "";
224
+ }
225
+
226
+ getRowByIdentity(db, tableDetail, where) {
227
+ const selectExpression =
228
+ tableDetail.identityStrategy?.type === "rowid" ? "rowid AS __rowid__, *" : "*";
229
+ const row = db
230
+ .prepare(
231
+ `SELECT ${selectExpression} FROM ${quoteIdentifier(tableDetail.name)} WHERE ${where.clause}`
232
+ )
233
+ .get(...where.params);
234
+
235
+ if (!row) {
236
+ return null;
237
+ }
238
+
239
+ const serialized = serializeRow(row);
240
+ const identity = buildRowIdentity(tableDetail, serialized);
241
+
242
+ if (Object.prototype.hasOwnProperty.call(serialized, "__rowid__")) {
243
+ delete serialized.__rowid__;
244
+ }
245
+
246
+ return {
247
+ ...serialized,
248
+ __identity: identity,
249
+ };
250
+ }
251
+ }
252
+
253
+ module.exports = {
254
+ DataBrowserService,
255
+ };
@@ -0,0 +1,34 @@
1
+ const { rowsToCsv } = require("../../utils/csv");
2
+
3
+ class ExportService {
4
+ constructor({ appStateStore, sqlExecutor }) {
5
+ this.appStateStore = appStateStore;
6
+ this.sqlExecutor = sqlExecutor;
7
+ }
8
+
9
+ getDelimiter() {
10
+ return this.appStateStore.getSettings().csvDelimiter || ",";
11
+ }
12
+
13
+ exportQuery(sql) {
14
+ const result = this.sqlExecutor.execute(sql, {
15
+ persistHistory: false,
16
+ requireReader: true,
17
+ });
18
+
19
+ return {
20
+ filename: "query-results.csv",
21
+ csv: rowsToCsv({
22
+ columns: result.columns,
23
+ rows: result.rows,
24
+ delimiter: this.getDelimiter(),
25
+ }),
26
+ columns: result.columns,
27
+ rowCount: result.rows.length,
28
+ };
29
+ }
30
+ }
31
+
32
+ module.exports = {
33
+ ExportService,
34
+ };
@@ -0,0 +1,111 @@
1
+ const fs = require("node:fs");
2
+ const { ReadOnlyError, ValidationError, mapSqliteError } = require("../../utils/errors");
3
+ const { validateSqlDumpPath } = require("../../utils/fileValidation");
4
+ const { splitSqlStatements } = require("./sqlExecutor");
5
+
6
+ class ImportService {
7
+ constructor({ connectionManager }) {
8
+ this.connectionManager = connectionManager;
9
+ }
10
+
11
+ containsExplicitTransaction(sql) {
12
+ return /\b(BEGIN|COMMIT|ROLLBACK|SAVEPOINT|RELEASE)\b/i.test(sql);
13
+ }
14
+
15
+ importSql({
16
+ sqlFilePath,
17
+ targetPath,
18
+ targetConnectionId,
19
+ createNew = false,
20
+ label,
21
+ }) {
22
+ const dumpPath = validateSqlDumpPath(sqlFilePath);
23
+ const sql = fs.readFileSync(dumpPath, "utf8");
24
+ let createdNewDatabase = false;
25
+
26
+ if (!sql.trim()) {
27
+ throw new ValidationError("SQL dump is empty.");
28
+ }
29
+
30
+ let activeConnection = null;
31
+
32
+ if (createNew) {
33
+ if (!targetPath) {
34
+ throw new ValidationError("targetPath is required when createNew is true.");
35
+ }
36
+
37
+ activeConnection = this.connectionManager.createConnection({
38
+ filePath: targetPath,
39
+ label,
40
+ });
41
+ createdNewDatabase = true;
42
+ } else if (targetConnectionId) {
43
+ activeConnection = this.connectionManager.selectActiveConnection(targetConnectionId);
44
+ } else if (targetPath) {
45
+ activeConnection = this.connectionManager.openConnection({
46
+ filePath: targetPath,
47
+ label,
48
+ makeActive: true,
49
+ });
50
+ } else {
51
+ activeConnection = this.connectionManager.getActiveConnection();
52
+ }
53
+
54
+ if (!activeConnection) {
55
+ throw new ValidationError(
56
+ "An active SQLite database or explicit target is required for import."
57
+ );
58
+ }
59
+
60
+ if (activeConnection.readOnly) {
61
+ throw new ReadOnlyError(
62
+ `Cannot import SQL into a read-only database: ${activeConnection.path}`
63
+ );
64
+ }
65
+
66
+ const db = this.connectionManager.getActiveDatabase();
67
+ const warnings = [];
68
+ const startedAt = Date.now();
69
+ const statementCount = splitSqlStatements(sql).length;
70
+
71
+ try {
72
+ if (this.containsExplicitTransaction(sql)) {
73
+ warnings.push(
74
+ "SQL dump contains explicit transaction control. Import executed without an additional wrapper transaction."
75
+ );
76
+ db.exec(sql);
77
+ } else {
78
+ db.exec("BEGIN");
79
+ try {
80
+ db.exec(sql);
81
+ db.exec("COMMIT");
82
+ } catch (error) {
83
+ db.exec("ROLLBACK");
84
+ throw error;
85
+ }
86
+ }
87
+ } catch (error) {
88
+ if (createdNewDatabase) {
89
+ try {
90
+ this.connectionManager.closeCurrent();
91
+ fs.rmSync(activeConnection.path, { force: true });
92
+ } catch (cleanupError) {
93
+ // Keep original SQLite error surface.
94
+ }
95
+ }
96
+ throw mapSqliteError(error);
97
+ }
98
+
99
+ return {
100
+ importedInto: activeConnection,
101
+ sourceDumpPath: dumpPath,
102
+ statementCount,
103
+ timingMs: Date.now() - startedAt,
104
+ warnings,
105
+ };
106
+ }
107
+ }
108
+
109
+ module.exports = {
110
+ ImportService,
111
+ };