sqlite-hub 0.9.12 → 0.10.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.
@@ -52,6 +52,34 @@ function normalizePaginationOptions(options = {}) {
52
52
  };
53
53
  }
54
54
 
55
+ function formatPreviewValue(value) {
56
+ if (value && typeof value === "object" && value.__type === "blob") {
57
+ return `BLOB ${value.sizeBytes ?? 0} bytes`;
58
+ }
59
+
60
+ if (value === null) {
61
+ return "NULL";
62
+ }
63
+
64
+ if (value === undefined) {
65
+ return "UNDEFINED";
66
+ }
67
+
68
+ if (typeof value === "object") {
69
+ return JSON.stringify(value);
70
+ }
71
+
72
+ return String(value);
73
+ }
74
+
75
+ function isUnchangedSubmittedValue(currentValue, submittedValue) {
76
+ if (currentValue === null && submittedValue === "") {
77
+ return true;
78
+ }
79
+
80
+ return formatPreviewValue(currentValue) === formatPreviewValue(submittedValue);
81
+ }
82
+
55
83
  class DataBrowserService {
56
84
  constructor({ connectionManager }) {
57
85
  this.connectionManager = connectionManager;
@@ -153,52 +181,11 @@ class DataBrowserService {
153
181
  this.connectionManager.assertWritable();
154
182
 
155
183
  const db = this.connectionManager.getActiveDatabase();
156
- const tableDetail = getTableDetail(db, tableName, { includeRowCount: false });
157
- const values = payload.values ?? {};
158
- const identity = payload.identity ?? null;
159
-
160
- if (tableDetail.notSafelyUpdatable) {
161
- throw new ValidationError(
162
- `Table ${tableName} cannot be safely updated because it has no stable row identity.`
163
- );
164
- }
165
-
166
- const identityColumnSet = new Set(
167
- tableDetail.identityStrategy?.type === "primaryKey"
168
- ? tableDetail.identityStrategy.columns
169
- : []
170
- );
171
- const editableColumns = tableDetail.columns.filter(
172
- (column) => column.visible && !column.generated && !identityColumnSet.has(column.name)
173
- );
174
- const columnsToUpdate = editableColumns.filter((column) =>
175
- Object.prototype.hasOwnProperty.call(values, column.name)
176
- );
184
+ const updatePlan = this.buildUpdatePlan(db, tableName, payload);
177
185
 
178
- if (!columnsToUpdate.length) {
179
- throw new ValidationError("No editable column values were provided.");
180
- }
186
+ db.prepare(updatePlan.sql).run(...updatePlan.params);
181
187
 
182
- const where = this.buildWhereClause(tableDetail, identity);
183
- const setClause = columnsToUpdate
184
- .map((column) => `${quoteIdentifier(column.name)} = ?`)
185
- .join(", ");
186
- const setParams = columnsToUpdate.map((column) =>
187
- deserializeSqliteValue(values[column.name])
188
- );
189
-
190
- db.prepare(
191
- [
192
- "UPDATE",
193
- quoteIdentifier(tableName),
194
- "SET",
195
- setClause,
196
- "WHERE",
197
- where.clause,
198
- ].join(" ")
199
- ).run(...setParams, ...where.params);
200
-
201
- const updatedRow = this.getRowByIdentity(db, tableDetail, where);
188
+ const updatedRow = this.getRowByIdentity(db, updatePlan.tableDetail, updatePlan.where);
202
189
 
203
190
  if (!updatedRow) {
204
191
  throw new NotFoundError(`Row not found in table: ${tableName}`);
@@ -210,6 +197,24 @@ class DataBrowserService {
210
197
  };
211
198
  }
212
199
 
200
+ previewTableRowUpdate(tableName, payload = {}) {
201
+ this.connectionManager.assertWritable();
202
+
203
+ const db = this.connectionManager.getActiveDatabase();
204
+ const updatePlan = this.buildUpdatePlan(db, tableName, payload);
205
+
206
+ return {
207
+ tableName: updatePlan.tableDetail.name,
208
+ sql: updatePlan.sql,
209
+ params: updatePlan.params.map((value, index) => ({
210
+ index: index + 1,
211
+ value: formatPreviewValue(value),
212
+ })),
213
+ changes: updatePlan.changes,
214
+ warnings: updatePlan.warnings,
215
+ };
216
+ }
217
+
213
218
  deleteTableRow(tableName, payload = {}) {
214
219
  this.connectionManager.assertWritable();
215
220
 
@@ -279,6 +284,81 @@ class DataBrowserService {
279
284
  `Table ${tableDetail.name} cannot be updated because it has no stable row identity.`
280
285
  );
281
286
  }
287
+
288
+ buildUpdatePlan(db, tableName, payload = {}) {
289
+ const tableDetail = getTableDetail(db, tableName, { includeRowCount: false });
290
+ const values = payload.values ?? {};
291
+ const identity = payload.identity ?? null;
292
+
293
+ if (tableDetail.notSafelyUpdatable) {
294
+ throw new ValidationError(
295
+ `Table ${tableName} cannot be safely updated because it has no stable row identity.`
296
+ );
297
+ }
298
+
299
+ const where = this.buildWhereClause(tableDetail, identity);
300
+ const currentRow = this.getRowByIdentity(db, tableDetail, where);
301
+
302
+ if (!currentRow) {
303
+ throw new NotFoundError(`Row not found in table: ${tableName}`);
304
+ }
305
+
306
+ const identityColumnSet = new Set(
307
+ tableDetail.identityStrategy?.type === "primaryKey"
308
+ ? tableDetail.identityStrategy.columns
309
+ : []
310
+ );
311
+ const editableColumns = tableDetail.columns.filter(
312
+ (column) => column.visible && !column.generated && !identityColumnSet.has(column.name)
313
+ );
314
+ const providedColumns = editableColumns.filter((column) =>
315
+ Object.prototype.hasOwnProperty.call(values, column.name)
316
+ );
317
+ const columnsToUpdate = providedColumns.filter(
318
+ (column) => !isUnchangedSubmittedValue(currentRow[column.name], values[column.name])
319
+ );
320
+
321
+ if (!columnsToUpdate.length) {
322
+ throw new ValidationError(
323
+ providedColumns.length
324
+ ? "No row values changed."
325
+ : "No editable column values were provided."
326
+ );
327
+ }
328
+
329
+ const setClause = columnsToUpdate
330
+ .map((column) => `${quoteIdentifier(column.name)} = ?`)
331
+ .join(", ");
332
+ const setParams = columnsToUpdate.map((column) =>
333
+ deserializeSqliteValue(values[column.name])
334
+ );
335
+ const sql = [
336
+ "UPDATE",
337
+ quoteIdentifier(tableDetail.name),
338
+ "SET",
339
+ setClause,
340
+ "WHERE",
341
+ where.clause,
342
+ ].join(" ");
343
+
344
+ return {
345
+ tableDetail,
346
+ where,
347
+ sql,
348
+ params: [...setParams, ...where.params],
349
+ changes: columnsToUpdate.map((column) => ({
350
+ column: column.name,
351
+ oldValue: formatPreviewValue(currentRow[column.name]),
352
+ newValue: formatPreviewValue(values[column.name]),
353
+ })),
354
+ warnings:
355
+ tableDetail.identityStrategy?.type === "primaryKey" &&
356
+ (tableDetail.identityStrategy.columns?.length ?? 0) > 1
357
+ ? ["This update targets a row through a composite primary key."]
358
+ : [],
359
+ };
360
+ }
361
+
282
362
  getRowByIdentity(db, tableDetail, where) {
283
363
  const selectExpression =
284
364
  tableDetail.identityStrategy?.type === "rowid" ? "rowid AS __rowid__, *" : "*";
@@ -8,6 +8,7 @@ test("data browser mutations preserve quoted dynamic identifiers", () => {
8
8
  const db = new Database(":memory:");
9
9
  const tableName = 'items" archived';
10
10
  const valueColumn = 'display"name';
11
+ const noteColumn = 'review"note';
11
12
 
12
13
  try {
13
14
  db.exec(
@@ -16,7 +17,9 @@ test("data browser mutations preserve quoted dynamic identifiers", () => {
16
17
  quoteIdentifier(tableName),
17
18
  "(",
18
19
  quoteIdentifier(valueColumn),
19
- "TEXT, status INTEGER",
20
+ "TEXT, status INTEGER,",
21
+ quoteIdentifier(noteColumn),
22
+ "TEXT",
20
23
  ")",
21
24
  ].join(" ")
22
25
  );
@@ -24,10 +27,10 @@ test("data browser mutations preserve quoted dynamic identifiers", () => {
24
27
  [
25
28
  "INSERT INTO",
26
29
  quoteIdentifier(tableName),
27
- "(" + quoteIdentifier(valueColumn) + ", status)",
28
- "VALUES (?, ?)",
30
+ "(" + quoteIdentifier(valueColumn) + ", status, " + quoteIdentifier(noteColumn) + ")",
31
+ "VALUES (?, ?, ?)",
29
32
  ].join(" ")
30
- ).run("before", 0);
33
+ ).run("before", 0, null);
31
34
 
32
35
  const service = new DataBrowserService({
33
36
  connectionManager: {
@@ -37,19 +40,56 @@ test("data browser mutations preserve quoted dynamic identifiers", () => {
37
40
  });
38
41
  const tableData = service.getTableData(tableName, { limit: 10, offset: 0 });
39
42
  const identity = tableData.rows[0].__identity;
43
+ const preview = service.previewTableRowUpdate(tableName, {
44
+ identity,
45
+ values: {
46
+ [valueColumn]: "after",
47
+ status: 0,
48
+ [noteColumn]: "",
49
+ },
50
+ });
51
+
52
+ assert.equal(
53
+ preview.sql,
54
+ [
55
+ "UPDATE",
56
+ quoteIdentifier(tableName),
57
+ "SET",
58
+ quoteIdentifier(valueColumn) + " = ?",
59
+ "WHERE",
60
+ "rowid IS ?",
61
+ ].join(" ")
62
+ );
63
+ assert.deepEqual(preview.changes, [
64
+ {
65
+ column: valueColumn,
66
+ oldValue: "before",
67
+ newValue: "after",
68
+ },
69
+ ]);
40
70
 
41
71
  const updated = service.updateTableRow(tableName, {
42
72
  identity,
43
73
  values: {
44
74
  [valueColumn]: "after",
75
+ [noteColumn]: "",
45
76
  },
46
77
  });
47
78
 
48
79
  assert.equal(updated.row[valueColumn], "after");
49
- assert.equal(
50
- db.prepare(["SELECT", quoteIdentifier(valueColumn), "FROM", quoteIdentifier(tableName)].join(" ")).get()[valueColumn],
51
- "after"
52
- );
80
+ assert.equal(updated.row[noteColumn], null);
81
+ const persistedRow = db.prepare(
82
+ [
83
+ "SELECT",
84
+ quoteIdentifier(valueColumn) + ",",
85
+ quoteIdentifier(noteColumn),
86
+ "FROM",
87
+ quoteIdentifier(tableName),
88
+ ].join(" ")
89
+ ).get();
90
+
91
+ assert.equal(persistedRow[valueColumn], "after");
92
+ assert.equal(persistedRow[noteColumn], null);
53
93
 
54
94
  const deleted = service.deleteTableRow(tableName, {
55
95
  identity: updated.row.__identity,