sqlite-hub 0.11.1 → 0.16.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 (52) hide show
  1. package/README.md +52 -13
  2. package/database.sqlite +0 -0
  3. package/frontend/js/api.js +64 -12
  4. package/frontend/js/app.js +723 -37
  5. package/frontend/js/components/emptyState.js +42 -46
  6. package/frontend/js/components/modal.js +526 -2
  7. package/frontend/js/components/queryEditor.js +12 -3
  8. package/frontend/js/components/queryResults.js +79 -17
  9. package/frontend/js/components/rowEditorPanel.js +345 -12
  10. package/frontend/js/components/sidebar.js +106 -23
  11. package/frontend/js/components/tableDesignerEditor.js +69 -11
  12. package/frontend/js/store.js +437 -26
  13. package/frontend/js/utils/copyColumnExport.js +117 -0
  14. package/frontend/js/utils/exportFilenames.js +32 -0
  15. package/frontend/js/utils/filePathPreview.js +315 -0
  16. package/frontend/js/utils/format.js +1 -1
  17. package/frontend/js/utils/rowEditorJson.js +65 -0
  18. package/frontend/js/utils/sqlFormatter.js +691 -0
  19. package/frontend/js/utils/tableDesigner.js +178 -6
  20. package/frontend/js/utils/textCellStats.js +20 -0
  21. package/frontend/js/utils/timestampPreview.js +264 -0
  22. package/frontend/js/views/charts.js +3 -1
  23. package/frontend/js/views/data.js +39 -4
  24. package/frontend/js/views/editor.js +50 -1
  25. package/frontend/js/views/settings.js +1 -4
  26. package/frontend/js/views/structure.js +154 -212
  27. package/frontend/styles/base.css +6 -0
  28. package/frontend/styles/components.css +463 -2
  29. package/frontend/styles/structure-graph.css +0 -3
  30. package/frontend/styles/tailwind.generated.css +1 -1
  31. package/frontend/styles/tokens.css +95 -95
  32. package/package.json +2 -3
  33. package/server/routes/export.js +97 -12
  34. package/server/services/sqlite/dataBrowserService.js +2 -68
  35. package/server/services/sqlite/exportService.js +74 -15
  36. package/server/services/sqlite/introspection.js +209 -1
  37. package/server/services/sqlite/sqlExecutor.js +31 -0
  38. package/server/services/sqlite/tableDesigner/changeAnalysis.js +25 -1
  39. package/server/services/sqlite/tableDesigner/schemaMapping.js +105 -10
  40. package/server/services/sqlite/tableDesigner/validation.js +60 -2
  41. package/server/services/sqlite/tableDesignerService.js +1 -1
  42. package/server/services/sqlite/tableFilter.js +75 -0
  43. package/server/utils/csv.js +30 -4
  44. package/tests/check-constraint-options.test.js +90 -0
  45. package/tests/export-filenames.test.js +34 -0
  46. package/tests/file-path-preview.test.js +165 -0
  47. package/tests/row-editor-json.test.js +82 -0
  48. package/tests/row-editor-timestamp-preview.test.js +192 -0
  49. package/tests/sql-formatter.test.js +173 -0
  50. package/tests/sql-highlight.test.js +38 -0
  51. package/tests/table-designer-v2-unique-constraints.test.js +78 -0
  52. package/tests/text-cell-stats.test.js +38 -0
@@ -6,19 +6,11 @@ const {
6
6
  serializeRows,
7
7
  } = require("../../utils/sqliteTypes");
8
8
  const { getRawStructureEntries, getTableDetail } = require("./introspection");
9
+ const { normalizeTableFilter } = require("./tableFilter");
9
10
  const { buildTableOrderClause, normalizeTableSort } = require("./tableSort");
10
11
 
11
12
  const DEFAULT_LIMIT = 50;
12
13
  const MAX_LIMIT = 250;
13
- const FILTER_OPERATORS = new Set(["=", "!=", "<", ">", "<=", ">=", "equals"]);
14
-
15
- function escapeLikePattern(value) {
16
- return String(value).replace(/[\\%_]/g, (character) => `\\${character}`);
17
- }
18
-
19
- function isTextColumn(column) {
20
- return String(column?.affinity ?? "").toUpperCase() === "TEXT";
21
- }
22
14
 
23
15
  function buildRowIdentity(tableDetail, row) {
24
16
  if (tableDetail.identityStrategy?.type === "rowid") {
@@ -61,64 +53,6 @@ function normalizePaginationOptions(options = {}) {
61
53
  };
62
54
  }
63
55
 
64
- function normalizeFilterOptions(tableDetail, options = {}) {
65
- const columnName = String(options.filterColumn ?? "").trim();
66
- const operator = String(options.filterOperator ?? "=").trim();
67
- const value = options.filterValue;
68
-
69
- if (!columnName || value === undefined || value === null || String(value).trim() === "") {
70
- return null;
71
- }
72
-
73
- if (!FILTER_OPERATORS.has(operator)) {
74
- throw new ValidationError(
75
- `filterOperator must be one of: ${Array.from(FILTER_OPERATORS).join(", ")}.`
76
- );
77
- }
78
-
79
- const filterColumn = tableDetail.columns.find(
80
- (column) => column.visible && column.name === columnName
81
- );
82
-
83
- if (!filterColumn) {
84
- throw new ValidationError(`Unknown filter column: ${columnName}.`);
85
- }
86
-
87
- const normalizedValue = String(value);
88
- const quotedColumn = quoteIdentifier(filterColumn.name);
89
-
90
- if (operator === "equals") {
91
- return {
92
- column: filterColumn.name,
93
- operator,
94
- value: normalizedValue,
95
- matchMode: "equals",
96
- clause: `${quotedColumn}${isTextColumn(filterColumn) ? " COLLATE NOCASE" : ""} = ?`,
97
- params: [normalizedValue],
98
- };
99
- }
100
-
101
- if (isTextColumn(filterColumn) && (operator === "=" || operator === "!=")) {
102
- return {
103
- column: filterColumn.name,
104
- operator,
105
- value: normalizedValue,
106
- matchMode: operator === "=" ? "contains" : "notContains",
107
- clause: `${quotedColumn} COLLATE NOCASE ${operator === "=" ? "LIKE" : "NOT LIKE"} ? ESCAPE '\\'`,
108
- params: [`%${escapeLikePattern(normalizedValue)}%`],
109
- };
110
- }
111
-
112
- return {
113
- column: filterColumn.name,
114
- operator,
115
- value: normalizedValue,
116
- matchMode: "comparison",
117
- clause: `${quotedColumn} ${operator} ?`,
118
- params: [normalizedValue],
119
- };
120
- }
121
-
122
56
  function formatPreviewValue(value) {
123
57
  if (value && typeof value === "object" && value.__type === "blob") {
124
58
  return `BLOB ${value.sizeBytes ?? 0} bytes`;
@@ -167,7 +101,7 @@ class DataBrowserService {
167
101
  const tableDetail = getTableDetail(db, tableName);
168
102
  const { limit, offset } = normalizePaginationOptions(options);
169
103
  const sort = normalizeTableSort(tableDetail, options);
170
- const filter = normalizeFilterOptions(tableDetail, options);
104
+ const filter = normalizeTableFilter(tableDetail, options);
171
105
  const selectExpression =
172
106
  tableDetail.identityStrategy?.type === "rowid" ? "rowid AS __rowid__, *" : "*";
173
107
  const orderClause = buildTableOrderClause(tableDetail, sort);
@@ -1,7 +1,12 @@
1
1
  const { quoteIdentifier } = require("../../utils/identifier");
2
2
  const { serializeRows } = require("../../utils/sqliteTypes");
3
- const { rowsToCsv } = require("../../utils/csv");
3
+ const {
4
+ rowsToCsv,
5
+ rowsToDelimitedText,
6
+ rowsToMarkdownTable,
7
+ } = require("../../utils/csv");
4
8
  const { getTableDetail } = require("./introspection");
9
+ const { normalizeTableFilter } = require("./tableFilter");
5
10
  const { buildTableOrderClause, normalizeTableSort } = require("./tableSort");
6
11
  const {
7
12
  buildAutoTitle,
@@ -23,6 +28,43 @@ function sanitizeFilenameBase(value, fallback = "query-results") {
23
28
  return sanitized.slice(0, 120);
24
29
  }
25
30
 
31
+ const EXPORT_FORMATS = {
32
+ csv: {
33
+ extension: "csv",
34
+ mimeType: "text/csv; charset=utf-8",
35
+ },
36
+ tsv: {
37
+ extension: "tsv",
38
+ mimeType: "text/tab-separated-values; charset=utf-8",
39
+ },
40
+ md: {
41
+ extension: "md",
42
+ mimeType: "text/markdown; charset=utf-8",
43
+ },
44
+ };
45
+
46
+ function normalizeExportFormat(format) {
47
+ const normalized = String(format ?? "csv").toLowerCase();
48
+
49
+ if (!EXPORT_FORMATS[normalized]) {
50
+ throw new Error(`Unsupported export format: ${format}`);
51
+ }
52
+
53
+ return normalized;
54
+ }
55
+
56
+ function renderExportContent({ columns, rows, format, csvDelimiter }) {
57
+ if (format === "tsv") {
58
+ return rowsToDelimitedText({ columns, rows, delimiter: "\t" });
59
+ }
60
+
61
+ if (format === "md") {
62
+ return rowsToMarkdownTable({ columns, rows });
63
+ }
64
+
65
+ return rowsToCsv({ columns, rows, delimiter: csvDelimiter });
66
+ }
67
+
26
68
  class ExportService {
27
69
  constructor({ appStateStore, connectionManager, sqlExecutor }) {
28
70
  this.appStateStore = appStateStore;
@@ -34,7 +76,9 @@ class ExportService {
34
76
  return this.appStateStore.getSettings().csvDelimiter || ",";
35
77
  }
36
78
 
37
- exportQuery(sql) {
79
+ exportQuery(sql, options = {}) {
80
+ const format = normalizeExportFormat(options.format);
81
+ const formatConfig = EXPORT_FORMATS[format];
38
82
  const activeConnection = this.connectionManager.getActiveConnection();
39
83
  const historyItem = activeConnection
40
84
  ? this.appStateStore.findQueryHistoryItemBySql(activeConnection.id, sql)
@@ -50,44 +94,59 @@ class ExportService {
50
94
  persistHistory: false,
51
95
  requireReader: true,
52
96
  });
97
+ const content = renderExportContent({
98
+ columns: result.columns,
99
+ rows: result.rows,
100
+ format,
101
+ csvDelimiter: this.getDelimiter(),
102
+ });
53
103
 
54
104
  return {
55
- filename: `${filenameBase}.csv`,
56
- csv: rowsToCsv({
57
- columns: result.columns,
58
- rows: result.rows,
59
- delimiter: this.getDelimiter(),
60
- }),
105
+ filename: `${filenameBase}.${formatConfig.extension}`,
106
+ content,
107
+ csv: format === "csv" ? content : undefined,
108
+ format,
109
+ mimeType: formatConfig.mimeType,
61
110
  columns: result.columns,
62
111
  rowCount: result.rows.length,
63
112
  };
64
113
  }
65
114
 
66
115
  exportTable(tableName, options = {}) {
116
+ const format = normalizeExportFormat(options.format);
117
+ const formatConfig = EXPORT_FORMATS[format];
67
118
  const db = this.connectionManager.getActiveDatabase();
68
119
  const tableDetail = getTableDetail(db, tableName, { includeRowCount: false });
69
120
  const sort = normalizeTableSort(tableDetail, options);
121
+ const filter = normalizeTableFilter(tableDetail, options);
70
122
  const orderClause = buildTableOrderClause(tableDetail, sort);
123
+ const whereClause = filter ? `WHERE ${filter.clause}` : "";
71
124
  const statement = db.prepare(
72
125
  [
73
126
  "SELECT * FROM",
74
127
  quoteIdentifier(tableName),
128
+ whereClause,
75
129
  orderClause ? "ORDER BY" : "",
76
130
  orderClause,
77
131
  ]
78
132
  .filter(Boolean)
79
133
  .join(" ")
80
134
  );
81
- const rows = serializeRows(statement.all());
135
+ const rows = serializeRows(statement.all(...(filter?.params ?? [])));
82
136
  const columns = statement.columns().map((column) => column.name);
137
+ const content = renderExportContent({
138
+ columns,
139
+ rows,
140
+ format,
141
+ csvDelimiter: this.getDelimiter(),
142
+ });
83
143
 
84
144
  return {
85
- filename: `${tableName}.csv`,
86
- csv: rowsToCsv({
87
- columns,
88
- rows,
89
- delimiter: this.getDelimiter(),
90
- }),
145
+ filename: `${tableName}.${formatConfig.extension}`,
146
+ content,
147
+ csv: format === "csv" ? content : undefined,
148
+ format,
149
+ mimeType: formatConfig.mimeType,
91
150
  columns,
92
151
  rowCount: rows.length,
93
152
  };
@@ -64,6 +64,198 @@ function normalizeColumn(column, visibleSet) {
64
64
  };
65
65
  }
66
66
 
67
+ function isIdentifierCharacter(character) {
68
+ return /[A-Za-z0-9_$]/.test(character);
69
+ }
70
+
71
+ function skipQuotedSql(text, index) {
72
+ const quote = text[index];
73
+ let cursor = index + 1;
74
+
75
+ while (cursor < text.length) {
76
+ if (text[cursor] === quote) {
77
+ if (text[cursor + 1] === quote) {
78
+ cursor += 2;
79
+ continue;
80
+ }
81
+
82
+ return cursor + 1;
83
+ }
84
+
85
+ cursor += 1;
86
+ }
87
+
88
+ return text.length;
89
+ }
90
+
91
+ function findMatchingParenthesis(text, openIndex) {
92
+ let depth = 0;
93
+
94
+ for (let index = openIndex; index < text.length; index += 1) {
95
+ const character = text[index];
96
+
97
+ if (character === "'" || character === '"' || character === "`") {
98
+ index = skipQuotedSql(text, index) - 1;
99
+ continue;
100
+ }
101
+
102
+ if (character === "[") {
103
+ const closeIndex = text.indexOf("]", index + 1);
104
+ index = closeIndex === -1 ? text.length : closeIndex;
105
+ continue;
106
+ }
107
+
108
+ if (character === "(") {
109
+ depth += 1;
110
+ continue;
111
+ }
112
+
113
+ if (character === ")") {
114
+ depth -= 1;
115
+
116
+ if (depth === 0) {
117
+ return index;
118
+ }
119
+ }
120
+ }
121
+
122
+ return -1;
123
+ }
124
+
125
+ function extractCheckExpressions(ddl = "") {
126
+ const expressions = [];
127
+ const checkPattern = /\bCHECK\s*\(/gi;
128
+ let match;
129
+
130
+ while ((match = checkPattern.exec(ddl))) {
131
+ const openIndex = ddl.indexOf("(", match.index);
132
+ const closeIndex = findMatchingParenthesis(ddl, openIndex);
133
+
134
+ if (closeIndex === -1) {
135
+ continue;
136
+ }
137
+
138
+ expressions.push(ddl.slice(openIndex + 1, closeIndex));
139
+ checkPattern.lastIndex = closeIndex + 1;
140
+ }
141
+
142
+ return expressions;
143
+ }
144
+
145
+ function normalizeIdentifier(value) {
146
+ const text = String(value ?? "").trim();
147
+
148
+ if (text.startsWith('"') && text.endsWith('"')) {
149
+ return text.slice(1, -1).replace(/""/g, '"').toLowerCase();
150
+ }
151
+
152
+ if (text.startsWith("`") && text.endsWith("`")) {
153
+ return text.slice(1, -1).replace(/``/g, "`").toLowerCase();
154
+ }
155
+
156
+ if (text.startsWith("[") && text.endsWith("]")) {
157
+ return text.slice(1, -1).replace(/\]\]/g, "]").toLowerCase();
158
+ }
159
+
160
+ return text.toLowerCase();
161
+ }
162
+
163
+ function parseSqlStringList(text = "") {
164
+ const values = [];
165
+ let index = 0;
166
+
167
+ while (index < text.length) {
168
+ if (text[index] !== "'") {
169
+ index += 1;
170
+ continue;
171
+ }
172
+
173
+ let value = "";
174
+ index += 1;
175
+
176
+ while (index < text.length) {
177
+ if (text[index] === "'") {
178
+ if (text[index + 1] === "'") {
179
+ value += "'";
180
+ index += 2;
181
+ continue;
182
+ }
183
+
184
+ index += 1;
185
+ values.push(value);
186
+ break;
187
+ }
188
+
189
+ value += text[index];
190
+ index += 1;
191
+ }
192
+ }
193
+
194
+ return values;
195
+ }
196
+
197
+ function findColumnInListExpression(expression, columnName) {
198
+ const normalizedColumnName = normalizeIdentifier(columnName);
199
+ const identifierPattern = /(?:"(?:[^"]|"")+"|`(?:[^`]|``)+`|\[[^\]]+\]|[A-Za-z_][A-Za-z0-9_$]*)\s+IN\s*\(/gi;
200
+ let match;
201
+
202
+ while ((match = identifierPattern.exec(expression))) {
203
+ const matchedIdentifier = match[0].replace(/\s+IN\s*\($/i, "").trim();
204
+
205
+ if (normalizeIdentifier(matchedIdentifier) !== normalizedColumnName) {
206
+ continue;
207
+ }
208
+
209
+ const before = expression[match.index - 1] ?? "";
210
+
211
+ if (before && isIdentifierCharacter(before)) {
212
+ continue;
213
+ }
214
+
215
+ const openIndex = expression.indexOf("(", match.index + matchedIdentifier.length);
216
+ const closeIndex = findMatchingParenthesis(expression, openIndex);
217
+
218
+ if (closeIndex === -1) {
219
+ continue;
220
+ }
221
+
222
+ const values = parseSqlStringList(expression.slice(openIndex + 1, closeIndex));
223
+
224
+ if (values.length) {
225
+ return values;
226
+ }
227
+ }
228
+
229
+ return [];
230
+ }
231
+
232
+ function parseCheckAllowedValues(ddl = "", columns = []) {
233
+ const allowedValuesByColumn = new Map();
234
+ const expressions = extractCheckExpressions(ddl);
235
+
236
+ columns.forEach((column) => {
237
+ const values = [];
238
+ const seen = new Set();
239
+
240
+ expressions.forEach((expression) => {
241
+ findColumnInListExpression(expression, column.name).forEach((value) => {
242
+ if (seen.has(value)) {
243
+ return;
244
+ }
245
+
246
+ seen.add(value);
247
+ values.push(value);
248
+ });
249
+ });
250
+
251
+ if (values.length) {
252
+ allowedValuesByColumn.set(column.name, values);
253
+ }
254
+ });
255
+
256
+ return allowedValuesByColumn;
257
+ }
258
+
67
259
  function groupForeignKeys(rows) {
68
260
  const grouped = new Map();
69
261
 
@@ -140,19 +332,33 @@ function getTableDetail(db, tableName, options = {}) {
140
332
  .all();
141
333
  const visibleSet = new Set(tableInfo.map((column) => column.name));
142
334
 
143
- const columns = extendedInfo
335
+ let columns = extendedInfo
144
336
  .map((column) => normalizeColumn(column, visibleSet))
145
337
  .sort((left, right) => left.cid - right.cid);
338
+ const allowedValuesByColumn = parseCheckAllowedValues(entry.sql, columns);
339
+
340
+ columns = columns.map((column) => ({
341
+ ...column,
342
+ allowedValues: allowedValuesByColumn.get(column.name) ?? [],
343
+ }));
146
344
 
147
345
  const foreignKeys = groupForeignKeys(
148
346
  db.prepare(`PRAGMA foreign_key_list(${quoteIdentifier(tableName)})`).all()
149
347
  );
348
+ const checkConstraints = extractCheckExpressions(entry.sql).map((expression, index) => ({
349
+ id: index,
350
+ expression: expression.trim(),
351
+ }));
150
352
 
151
353
  const indexList = db
152
354
  .prepare(`PRAGMA index_list(${quoteIdentifier(tableName)})`)
153
355
  .all()
154
356
  .map((indexEntry) => {
155
357
  let indexColumns = [];
358
+ const indexSql =
359
+ db
360
+ .prepare("SELECT sql FROM sqlite_master WHERE type = 'index' AND name = ?")
361
+ .get(indexEntry.name)?.sql ?? null;
156
362
 
157
363
  try {
158
364
  indexColumns = db
@@ -182,6 +388,7 @@ function getTableDetail(db, tableName, options = {}) {
182
388
  unique: Boolean(indexEntry.unique),
183
389
  origin: indexEntry.origin,
184
390
  partial: Boolean(indexEntry.partial),
391
+ sql: indexSql,
185
392
  columns: indexColumns,
186
393
  };
187
394
  });
@@ -204,6 +411,7 @@ function getTableDetail(db, tableName, options = {}) {
204
411
  withoutRowId,
205
412
  strict: Boolean(tableListEntry?.strict),
206
413
  columns,
414
+ checkConstraints,
207
415
  foreignKeys,
208
416
  indexes: indexList,
209
417
  indexCount: indexList.length,
@@ -204,6 +204,13 @@ function mapEditableColumns(tableDetail, columnDefinitions) {
204
204
  const tableColumnsByName = new Map(
205
205
  tableDetail.columns.map((column) => [column.name, column])
206
206
  );
207
+ const foreignKeyColumnNames = new Set(
208
+ (tableDetail.foreignKeys ?? []).flatMap((foreignKey) =>
209
+ (foreignKey.mappings ?? [])
210
+ .map((mapping) => String(mapping.from ?? "").trim())
211
+ .filter(Boolean)
212
+ )
213
+ );
207
214
  const identityColumns = new Set(
208
215
  tableDetail.identityStrategy?.type === "primaryKey"
209
216
  ? tableDetail.identityStrategy.columns ?? []
@@ -223,10 +230,31 @@ function mapEditableColumns(tableDetail, columnDefinitions) {
223
230
  visible: isRowId ? true : Boolean(columnMeta?.visible),
224
231
  generated: Boolean(columnMeta?.generated),
225
232
  identity: identityColumns.has(definition.column),
233
+ declaredType: columnMeta?.declaredType ?? "",
234
+ affinity: columnMeta?.affinity ?? "",
235
+ primaryKeyPosition: Number(columnMeta?.primaryKeyPosition ?? 0),
236
+ foreignKey: foreignKeyColumnNames.has(definition.column),
237
+ notNull: Boolean(columnMeta?.notNull),
238
+ allowedValues: columnMeta?.allowedValues ?? [],
226
239
  };
227
240
  });
228
241
  }
229
242
 
243
+ function buildTableMeta(tableDetail) {
244
+ return {
245
+ columns: (tableDetail.columns ?? []).map((column) => ({
246
+ name: column.name,
247
+ declaredType: column.declaredType,
248
+ affinity: column.affinity,
249
+ primaryKeyPosition: Number(column.primaryKeyPosition ?? 0),
250
+ foreignKey: (tableDetail.foreignKeys ?? []).some((foreignKey) =>
251
+ (foreignKey.mappings ?? []).some((mapping) => mapping.from === column.name)
252
+ ),
253
+ })),
254
+ foreignKeys: tableDetail.foreignKeys ?? [],
255
+ };
256
+ }
257
+
230
258
  function hasRequiredIdentityColumns(tableDetail, editableColumns) {
231
259
  const availableSourceColumns = new Set(editableColumns.map((column) => column.sourceColumn));
232
260
 
@@ -299,6 +327,7 @@ function resolveEditableResult(db, columnDefinitions, serializedRows) {
299
327
  reason: getEditableResultReason(tableDetail),
300
328
  columns: editableColumns,
301
329
  identityStrategy: tableDetail.identityStrategy,
330
+ tableMeta: buildTableMeta(tableDetail),
302
331
  };
303
332
  }
304
333
 
@@ -317,6 +346,7 @@ function resolveEditableResult(db, columnDefinitions, serializedRows) {
317
346
  reason: "",
318
347
  columns: editableColumns,
319
348
  identityStrategy: tableDetail.identityStrategy,
349
+ tableMeta: buildTableMeta(tableDetail),
320
350
  rows,
321
351
  };
322
352
  }
@@ -376,6 +406,7 @@ class SqlExecutor {
376
406
  reason: editableResult.reason ?? "",
377
407
  columns: editableResult.columns ?? [],
378
408
  identityStrategy: editableResult.identityStrategy ?? null,
409
+ tableMeta: editableResult.tableMeta ?? null,
379
410
  }
380
411
  : null,
381
412
  };
@@ -26,6 +26,17 @@ function hasColumnChanged(originalColumn, draftColumn) {
26
26
  );
27
27
  }
28
28
 
29
+ function hasConstraintChanged(constraint) {
30
+ return (
31
+ normalizeComparableValue(constraint.name) !==
32
+ normalizeComparableValue(constraint.originalName ?? constraint.name) ||
33
+ normalizeComparableValue(constraint.expression || constraint.sql) !==
34
+ normalizeComparableValue(
35
+ constraint.originalExpression ?? constraint.originalSql ?? constraint.expression ?? constraint.sql
36
+ )
37
+ );
38
+ }
39
+
29
40
  function hasMeaningfulNewDraftContent(draft) {
30
41
  return (
31
42
  Boolean(String(draft.tableName ?? "").trim()) ||
@@ -149,7 +160,7 @@ function analyzeEditDraft(draft, originalDraft) {
149
160
  warnings.push(
150
161
  buildRiskyChangeWarning(
151
162
  "Column Rename Requires Rebuild",
152
- `Renaming column ${originalColumn.name} to ${column.name} is intentionally blocked in Table Designer v1.`
163
+ `Renaming column ${originalColumn.name} to ${column.name} is intentionally blocked in Table Designer v2.`
153
164
  )
154
165
  );
155
166
  }
@@ -265,6 +276,19 @@ function analyzeEditDraft(draft, originalDraft) {
265
276
  );
266
277
  });
267
278
 
279
+ [...(draft.uniqueConstraints ?? []), ...(draft.checkConstraints ?? [])].forEach((constraint) => {
280
+ if (!hasConstraintChanged(constraint)) {
281
+ return;
282
+ }
283
+
284
+ warnings.push(
285
+ buildRiskyChangeWarning(
286
+ "Constraint Change Requires Rebuild",
287
+ `Changing ${constraint.originalName || constraint.name || "a table constraint"} requires a SQLite table rebuild.`
288
+ )
289
+ );
290
+ });
291
+
268
292
  const dirty = Boolean(executableStatements.length || warnings.length);
269
293
 
270
294
  return {