sqlite-hub 2.0.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 (85) hide show
  1. package/README.md +14 -9
  2. package/docs/MCP.md +8 -0
  3. package/docs/changelog.md +10 -0
  4. package/docs/guidelines/DESIGN.md +64 -0
  5. package/frontend/assets/mockups/backups_1_1920.webp +0 -0
  6. package/frontend/assets/mockups/backups_2_compare_drawer_1920.webp +0 -0
  7. package/frontend/assets/mockups/backups_3_create_backup_modal_1920.webp +0 -0
  8. package/frontend/assets/mockups/backups_4_edit_backup_modal_1920.webp +0 -0
  9. package/frontend/assets/mockups/backups_5_restore_backup_modal_1920.webp +0 -0
  10. package/frontend/assets/mockups/backups_6_delete_backup_modal_1920.webp +0 -0
  11. package/frontend/assets/mockups/charts_1_1920.webp +0 -0
  12. package/frontend/assets/mockups/charts_2_query_detail_1920.webp +0 -0
  13. package/frontend/assets/mockups/charts_3_create_query_chart_modal_1920.webp +0 -0
  14. package/frontend/assets/mockups/charts_4_edit_query_chart_modal_1920.webp +0 -0
  15. package/frontend/assets/mockups/charts_5_delete_query_chart_modal_1920.webp +0 -0
  16. package/frontend/assets/mockups/charts_6_copy_column_modal_1920.webp +0 -0
  17. package/frontend/assets/mockups/connections_1_1920.webp +0 -0
  18. package/frontend/assets/mockups/connections_2_create_connection_modal_1920.webp +0 -0
  19. package/frontend/assets/mockups/connections_3_open_connection_modal_1920.webp +0 -0
  20. package/frontend/assets/mockups/data_1_1920.webp +0 -0
  21. package/frontend/assets/mockups/data_2_roweditor_1920.webp +0 -0
  22. package/frontend/assets/mockups/data_3_generate_data_modal_1920.webp +0 -0
  23. package/frontend/assets/mockups/data_4_data_export_modal_1920.webp +0 -0
  24. package/frontend/assets/mockups/documents_1_1920.webp +0 -0
  25. package/frontend/assets/mockups/documents_2_document_insert_table_modal_1920.webp +0 -0
  26. package/frontend/assets/mockups/documents_3_document_insert_note_modal_1920.webp +0 -0
  27. package/frontend/assets/mockups/logs_1_1920.webp +0 -0
  28. package/frontend/assets/mockups/media_tagging_queue_1_1920.webp +0 -0
  29. package/frontend/assets/mockups/media_tagging_setup_1_1920.webp +0 -0
  30. package/frontend/assets/mockups/media_tagging_setup_2_create_media_tagging_tag_table_modal_1920.webp +0 -0
  31. package/frontend/assets/mockups/media_tagging_setup_3_create_media_tagging_mapping_table_modal_1920.webp +0 -0
  32. package/frontend/assets/mockups/overview_1_1920.webp +0 -0
  33. package/frontend/assets/mockups/settings_1_1920.webp +0 -0
  34. package/frontend/assets/mockups/sql_editor_1_1920.webp +0 -0
  35. package/frontend/assets/mockups/sql_editor_2_query_detail_1920.webp +0 -0
  36. package/frontend/assets/mockups/sql_editor_3_query_export_modal_1920.webp +0 -0
  37. package/frontend/assets/mockups/structure_1_1920.webp +0 -0
  38. package/frontend/assets/mockups/structure_2_generate_types_modal_1920.webp +0 -0
  39. package/frontend/assets/mockups/structure_3_generate_types_modal_1920.webp +0 -0
  40. package/frontend/assets/mockups/table_designer_1_1920.webp +0 -0
  41. package/frontend/assets/mockups/table_designer_2_table_designer_constraints_modal_1920.webp +0 -0
  42. package/frontend/js/api.js +11 -0
  43. package/frontend/js/app.js +238 -8
  44. package/frontend/js/components/badges.js +16 -0
  45. package/frontend/js/components/connectionCard.js +35 -0
  46. package/frontend/js/components/modal.js +116 -245
  47. package/frontend/js/components/queryHistoryDetail.js +25 -45
  48. package/frontend/js/components/rowEditorPanel.js +7 -60
  49. package/frontend/js/components/structureGraph.js +292 -13
  50. package/frontend/js/components/tableDesignerConstraintsDrawer.js +400 -0
  51. package/frontend/js/components/tableDesignerEditor.js +51 -16
  52. package/frontend/js/components/tableDesignerSidebar.js +9 -5
  53. package/frontend/js/store.js +551 -7
  54. package/frontend/js/utils/connectionRegistry.js +76 -0
  55. package/frontend/js/utils/tableDesigner.js +431 -62
  56. package/frontend/js/views/backups.js +2 -5
  57. package/frontend/js/views/charts.js +20 -10
  58. package/frontend/js/views/connections.js +103 -3
  59. package/frontend/js/views/data.js +19 -5
  60. package/frontend/js/views/structure.js +28 -5
  61. package/frontend/js/views/tableAdvisor.js +14 -6
  62. package/frontend/js/views/tableDesigner.js +44 -9
  63. package/frontend/styles/components.css +676 -79
  64. package/frontend/styles/structure-graph.css +49 -0
  65. package/frontend/styles/tailwind.generated.css +1 -1
  66. package/package.json +1 -1
  67. package/server/routes/connections.js +12 -0
  68. package/server/routes/data.js +1 -1
  69. package/server/routes/tableDesigner.js +15 -1
  70. package/server/services/mcpToolService.js +28 -0
  71. package/server/services/sqlite/connectionManager.js +23 -2
  72. package/server/services/sqlite/dataBrowserService.js +21 -0
  73. package/server/services/sqlite/introspection.js +39 -1
  74. package/server/services/sqlite/structureService.js +66 -11
  75. package/server/services/sqlite/tableAdvisor.js +6 -0
  76. package/server/services/sqlite/tableDesigner/schemaMapping.js +7 -1
  77. package/server/services/sqlite/tableDesigner/sql.js +31 -2
  78. package/server/services/sqlite/tableDesigner/validation.js +69 -9
  79. package/server/services/sqlite/tableDesignerService.js +110 -5
  80. package/server/services/storage/appStateStore.js +303 -4
  81. package/frontend/assets/mockups/backups_2_create_backup_modal_1920.webp +0 -0
  82. package/frontend/assets/mockups/backups_3_edit_backup_modal_1920.webp +0 -0
  83. package/frontend/assets/mockups/backups_4_restore_backup_modal_1920.webp +0 -0
  84. package/frontend/assets/mockups/backups_5_delete_backup_modal_1920.webp +0 -0
  85. package/frontend/assets/mockups/data_3_data_export_modal_1920.webp +0 -0
@@ -41,6 +41,16 @@ function normalizeIdentifierKey(value) {
41
41
  return normalizeTrimmed(value).toLowerCase();
42
42
  }
43
43
 
44
+ function normalizeImportFormat(value, { allowEmpty = false } = {}) {
45
+ const normalized = normalizeIdentifierKey(value);
46
+
47
+ if (["csv", "tsv", "json"].includes(normalized)) {
48
+ return normalized;
49
+ }
50
+
51
+ return allowEmpty ? "" : "csv";
52
+ }
53
+
44
54
  function normalizeDesignerType(value) {
45
55
  return normalizeTrimmed(value).toUpperCase() || "TEXT";
46
56
  }
@@ -61,6 +71,30 @@ function createColumnId() {
61
71
  return `column_${Date.now()}_${Math.random().toString(16).slice(2)}`;
62
72
  }
63
73
 
74
+ function normalizeImportedCellValue(value) {
75
+ if (value === null || value === undefined) {
76
+ return null;
77
+ }
78
+
79
+ if (["boolean", "number", "string"].includes(typeof value)) {
80
+ return value;
81
+ }
82
+
83
+ try {
84
+ return JSON.stringify(value);
85
+ } catch {
86
+ return String(value);
87
+ }
88
+ }
89
+
90
+ function normalizeImportedRows(rows) {
91
+ return Array.isArray(rows)
92
+ ? rows.map((row) =>
93
+ Array.isArray(row) ? row.map((cell) => normalizeImportedCellValue(cell)) : []
94
+ )
95
+ : [];
96
+ }
97
+
64
98
  export function createEmptyTableDesignerColumn(seed = {}) {
65
99
  const type = normalizeDesignerType(seed.type ?? "TEXT");
66
100
  const defaultValue = normalizeText(seed.defaultValue ?? "");
@@ -96,6 +130,16 @@ export function createEmptyTableDesignerColumn(seed = {}) {
96
130
  }
97
131
 
98
132
  function normalizeDraft(rawDraft = {}) {
133
+ const importRows = normalizeImportedRows(rawDraft.importRows ?? rawDraft.importedCsvRows);
134
+ const importSourceFileName = normalizeText(
135
+ rawDraft.importSourceFileName ?? rawDraft.importedCsvFileName ?? ""
136
+ );
137
+ const importDelimiter = normalizeText(rawDraft.importDelimiter ?? rawDraft.importedCsvDelimiter ?? "");
138
+ const importFormat = normalizeImportFormat(
139
+ rawDraft.importFormat ?? (importSourceFileName ? "csv" : ""),
140
+ { allowEmpty: true }
141
+ );
142
+
99
143
  return {
100
144
  mode: String(rawDraft.mode ?? "create").trim() === "edit" ? "edit" : "create",
101
145
  originalTableName: normalizeText(rawDraft.originalTableName ?? ""),
@@ -132,6 +176,7 @@ function normalizeDraft(rawDraft = {}) {
132
176
  id: normalizeText(constraint.id ?? `check:${index}`),
133
177
  name: normalizeText(constraint.name ?? `CHECK ${index + 1}`),
134
178
  originalName: normalizeText(constraint.originalName ?? constraint.name ?? `CHECK ${index + 1}`),
179
+ deleted: normalizeBoolean(constraint.deleted),
135
180
  columns: Array.isArray(constraint.columns)
136
181
  ? constraint.columns
137
182
  .map((column) => ({
@@ -146,18 +191,30 @@ function normalizeDraft(rawDraft = {}) {
146
191
  originalExpression: normalizeText(constraint.originalExpression ?? constraint.expression ?? ""),
147
192
  editable: normalizeBoolean(constraint.editable),
148
193
  preserved: constraint.preserved === undefined ? true : normalizeBoolean(constraint.preserved),
194
+ columnId: normalizeText(constraint.columnId ?? ""),
195
+ source: normalizeText(constraint.source ?? (constraint.originalExpression ? "detected" : "user")),
196
+ presetId: normalizeText(constraint.presetId ?? ""),
197
+ presetFields:
198
+ constraint.presetFields && typeof constraint.presetFields === "object"
199
+ ? Object.fromEntries(
200
+ Object.entries(constraint.presetFields).map(([key, value]) => [
201
+ key,
202
+ normalizeText(value),
203
+ ])
204
+ )
205
+ : {},
149
206
  }))
150
207
  : [],
151
208
  designerVersion: Number(rawDraft.designerVersion) || 1,
152
209
  schemaWarnings: Array.isArray(rawDraft.schemaWarnings) ? rawDraft.schemaWarnings : [],
153
210
  fillImportedRows: normalizeBoolean(rawDraft.fillImportedRows),
154
- importedCsvFileName: normalizeText(rawDraft.importedCsvFileName ?? ""),
155
- importedCsvDelimiter: normalizeText(rawDraft.importedCsvDelimiter ?? ""),
156
- importedCsvRows: Array.isArray(rawDraft.importedCsvRows)
157
- ? rawDraft.importedCsvRows.map((row) =>
158
- Array.isArray(row) ? row.map((cell) => normalizeText(cell)) : []
159
- )
160
- : [],
211
+ importFormat,
212
+ importSourceFileName,
213
+ importDelimiter,
214
+ importRows,
215
+ importedCsvFileName: importSourceFileName,
216
+ importedCsvDelimiter: importDelimiter,
217
+ importedCsvRows: importRows,
161
218
  };
162
219
  }
163
220
 
@@ -421,13 +478,40 @@ function buildColumnDefinition(column) {
421
478
  return parts.join(" ");
422
479
  }
423
480
 
481
+ function normalizeCheckExpressionSql(expression) {
482
+ const normalized = normalizeTrimmed(expression);
483
+
484
+ if (!normalized) {
485
+ return "";
486
+ }
487
+
488
+ if (/^CHECK\s*\(/i.test(normalized)) {
489
+ return normalized;
490
+ }
491
+
492
+ return `CHECK (${normalized})`;
493
+ }
494
+
495
+ function buildCheckConstraintSql(constraint) {
496
+ if (constraint.deleted) {
497
+ return "";
498
+ }
499
+
500
+ return normalizeCheckExpressionSql(constraint.expression);
501
+ }
502
+
424
503
  function buildCreateTableSql(draft) {
425
504
  const columnSql = draft.columns
426
505
  .filter((column) => !column.deleted)
427
- .map((column) => ` ${buildColumnDefinition(column)}`)
506
+ .map((column) => ` ${buildColumnDefinition(column)}`);
507
+ const checkSql = (draft.checkConstraints ?? [])
508
+ .map(buildCheckConstraintSql)
509
+ .filter(Boolean)
510
+ .map((constraintSql) => ` ${constraintSql}`);
511
+ const definitionSql = [...columnSql, ...checkSql]
428
512
  .join(",\n");
429
513
 
430
- return `CREATE TABLE ${quoteIdentifier(draft.tableName)} (\n${columnSql}\n);`;
514
+ return `CREATE TABLE ${quoteIdentifier(draft.tableName)} (\n${definitionSql}\n);`;
431
515
  }
432
516
 
433
517
  function getUniqueConstraintExpression(constraint) {
@@ -807,20 +891,30 @@ function escapeSqlLiteral(value) {
807
891
  }
808
892
 
809
893
  function formatImportedCellValueForSql(column, value) {
810
- const textValue = normalizeText(value);
811
- const trimmedValue = normalizeTrimmed(value);
894
+ if (value === null || value === undefined) {
895
+ return "NULL";
896
+ }
897
+
812
898
  const type = normalizeDesignerType(column.type);
899
+ const normalizedValue =
900
+ typeof value === "object" ? normalizeImportedCellValue(value) : value;
901
+ const textValue = normalizeText(normalizedValue);
902
+ const trimmedValue = normalizeTrimmed(normalizedValue);
813
903
 
814
904
  if (!trimmedValue) {
815
905
  return type === "TEXT" || type === "DATE" || type === "DATETIME" ? "''" : "NULL";
816
906
  }
817
907
 
908
+ if (typeof normalizedValue === "boolean") {
909
+ return normalizedValue ? "1" : "0";
910
+ }
911
+
818
912
  if (type === "BOOLEAN") {
819
- if (["true", "yes", "1"].includes(normalizeIdentifierKey(value))) {
913
+ if (["true", "yes", "1"].includes(normalizeIdentifierKey(normalizedValue))) {
820
914
  return "1";
821
915
  }
822
916
 
823
- if (["false", "no", "0"].includes(normalizeIdentifierKey(value))) {
917
+ if (["false", "no", "0"].includes(normalizeIdentifierKey(normalizedValue))) {
824
918
  return "0";
825
919
  }
826
920
  }
@@ -841,8 +935,16 @@ function getImportedFillColumns(draft) {
841
935
  );
842
936
  }
843
937
 
938
+ function getImportedRows(draft) {
939
+ return Array.isArray(draft.importRows) && draft.importRows.length
940
+ ? draft.importRows
941
+ : draft.importedCsvRows ?? [];
942
+ }
943
+
844
944
  function buildImportedInsertPreviewSql(draft, maxRows = 3) {
845
- if (!draft.fillImportedRows || !draft.importedCsvRows.length) {
945
+ const importRows = getImportedRows(draft);
946
+
947
+ if (!draft.fillImportedRows || !importRows.length) {
846
948
  return [];
847
949
  }
848
950
 
@@ -860,18 +962,18 @@ function buildImportedInsertPreviewSql(draft, maxRows = 3) {
860
962
  columnSql,
861
963
  ") VALUES",
862
964
  ].join(" ");
863
- const previewStatements = draft.importedCsvRows.slice(0, maxRows).map((row) => {
965
+ const previewStatements = importRows.slice(0, maxRows).map((row) => {
864
966
  const valueSql = importedColumns
865
- .map((column) => formatImportedCellValueForSql(column, row[column.importedValueIndex] ?? ""))
967
+ .map((column) => formatImportedCellValueForSql(column, row[column.importedValueIndex]))
866
968
  .join(", ");
867
969
 
868
970
  return [insertPrefix, "(", valueSql, ");"].join(" ");
869
971
  });
870
972
 
871
- if (draft.importedCsvRows.length > maxRows) {
973
+ if (importRows.length > maxRows) {
872
974
  previewStatements.push(
873
- `-- ... ${draft.importedCsvRows.length - maxRows} more imported row${
874
- draft.importedCsvRows.length - maxRows === 1 ? "" : "s"
975
+ `-- ... ${importRows.length - maxRows} more imported row${
976
+ importRows.length - maxRows === 1 ? "" : "s"
875
977
  }`
876
978
  );
877
979
  }
@@ -960,6 +1062,10 @@ function parseCsvRows(text, delimiter = ",", maxRows = 16) {
960
1062
  cell += character;
961
1063
  }
962
1064
 
1065
+ if (inQuotes) {
1066
+ throw new Error("Delimited text contains an unterminated quoted value.");
1067
+ }
1068
+
963
1069
  if (cell.length || row.length) {
964
1070
  pushRow();
965
1071
  }
@@ -1039,6 +1145,16 @@ function suggestImportedTableName(fileName, catalogTables = []) {
1039
1145
  return buildUniqueName(normalizeImportedTableName(fileName), existingNames, "imported_table");
1040
1146
  }
1041
1147
 
1148
+ function inferImportFormatFromFileName(fileName) {
1149
+ const extension = normalizeIdentifierKey(String(fileName ?? "").split(".").pop());
1150
+
1151
+ if (["csv", "tsv", "json"].includes(extension)) {
1152
+ return extension;
1153
+ }
1154
+
1155
+ return "csv";
1156
+ }
1157
+
1042
1158
  function isBooleanSample(value) {
1043
1159
  return ["true", "false", "yes", "no"].includes(normalizeIdentifierKey(value));
1044
1160
  }
@@ -1076,8 +1192,26 @@ function isDateTimeSample(value) {
1076
1192
  return !Number.isNaN(Date.parse(normalized.replace(" ", "T")));
1077
1193
  }
1078
1194
 
1079
- function inferImportedColumnType(columnName, sampleValues) {
1080
- const nonEmptyValues = sampleValues.map(normalizeTrimmed).filter(Boolean);
1195
+ function isNumericImportValue(value) {
1196
+ return typeof value === "number" && Number.isFinite(value);
1197
+ }
1198
+
1199
+ function isIntegerImportValue(value) {
1200
+ return isNumericImportValue(value) && Number.isInteger(value);
1201
+ }
1202
+
1203
+ export function inferSQLiteType(values = [], { columnName = "" } = {}) {
1204
+ const nonEmptyValues = values.filter((value) => {
1205
+ if (value === null || value === undefined) {
1206
+ return false;
1207
+ }
1208
+
1209
+ if (typeof value === "string") {
1210
+ return Boolean(normalizeTrimmed(value));
1211
+ }
1212
+
1213
+ return true;
1214
+ });
1081
1215
  const normalizedColumnName = normalizeIdentifierKey(columnName);
1082
1216
 
1083
1217
  if (normalizedColumnName === "id") {
@@ -1088,95 +1222,258 @@ function inferImportedColumnType(columnName, sampleValues) {
1088
1222
  return normalizedColumnName.endsWith("_id") ? "INTEGER" : "TEXT";
1089
1223
  }
1090
1224
 
1225
+ if (nonEmptyValues.some((value) => typeof value === "object")) {
1226
+ return "TEXT";
1227
+ }
1228
+
1229
+ if (nonEmptyValues.every((value) => typeof value === "boolean")) {
1230
+ return "INTEGER";
1231
+ }
1232
+
1233
+ if (nonEmptyValues.every(isIntegerImportValue)) {
1234
+ return "INTEGER";
1235
+ }
1236
+
1237
+ if (nonEmptyValues.every(isNumericImportValue)) {
1238
+ return "REAL";
1239
+ }
1240
+
1241
+ const stringValues = nonEmptyValues.map(normalizeTrimmed);
1242
+
1091
1243
  if (nonEmptyValues.every(isDateTimeSample)) {
1092
1244
  return "DATETIME";
1093
1245
  }
1094
1246
 
1095
- if (nonEmptyValues.every(isDateSample)) {
1247
+ if (stringValues.every(isDateSample)) {
1096
1248
  return "DATE";
1097
1249
  }
1098
1250
 
1099
- if (nonEmptyValues.every(isBooleanSample)) {
1251
+ if (stringValues.every(isBooleanSample)) {
1100
1252
  return "BOOLEAN";
1101
1253
  }
1102
1254
 
1103
- if (nonEmptyValues.every(isIntegerSample)) {
1255
+ if (stringValues.every(isIntegerSample)) {
1104
1256
  return "INTEGER";
1105
1257
  }
1106
1258
 
1107
- if (nonEmptyValues.every((value) => isIntegerSample(value) || isRealSample(value))) {
1259
+ if (stringValues.every((value) => isIntegerSample(value) || isRealSample(value))) {
1108
1260
  return "REAL";
1109
1261
  }
1110
1262
 
1111
1263
  return "TEXT";
1112
1264
  }
1113
1265
 
1114
- export function createTableDesignerDraftFromCsvImport(
1115
- { fileName = "", csvText = "" },
1116
- { catalogTables = [], supportedTypes = DEFAULT_TABLE_DESIGNER_TYPES, readOnly = false } = {}
1117
- ) {
1118
- const delimiter = detectCsvDelimiter(csvText);
1119
- const rows = parseCsvRows(csvText, delimiter, Number.POSITIVE_INFINITY).filter(
1120
- (row) => !isCsvRowEmpty(row)
1121
- );
1266
+ function isPlainImportObject(value) {
1267
+ return Boolean(value) && typeof value === "object" && !Array.isArray(value);
1268
+ }
1122
1269
 
1123
- if (!rows.length) {
1124
- throw new Error("The selected CSV file is empty.");
1270
+ function normalizeJsonImportValue(value) {
1271
+ if (value === null || value === undefined) {
1272
+ return null;
1125
1273
  }
1126
1274
 
1127
- const headerRow = rows[0].map((cell) => normalizeImportedColumnName(cell));
1275
+ if (typeof value === "boolean") {
1276
+ return value ? 1 : 0;
1277
+ }
1128
1278
 
1129
- if (!headerRow.some((cell) => normalizeTrimmed(cell))) {
1130
- throw new Error("The CSV header row is empty.");
1279
+ if (typeof value === "number") {
1280
+ return Number.isFinite(value) ? value : String(value);
1281
+ }
1282
+
1283
+ if (typeof value === "string") {
1284
+ return value;
1131
1285
  }
1132
1286
 
1133
- const sampleRows = rows.slice(1).filter((row) => !isCsvRowEmpty(row)).slice(0, 3);
1287
+ return normalizeImportedCellValue(value);
1288
+ }
1289
+
1290
+ function buildImportColumns(sourceNames, importRows) {
1134
1291
  const usedColumnNames = new Set();
1135
1292
  let primaryKeyAssigned = false;
1136
1293
 
1137
- const columns = headerRow.map((headerCell, index) => {
1294
+ return sourceNames.map((sourceName, index) => {
1138
1295
  const fallbackName = `column_${index + 1}`;
1139
- const columnName = buildUniqueName(
1140
- normalizeImportedColumnName(headerCell) || fallbackName,
1296
+ const targetName = buildUniqueName(
1297
+ normalizeImportedColumnName(sourceName) || fallbackName,
1141
1298
  usedColumnNames,
1142
1299
  fallbackName
1143
1300
  );
1144
- const sampleValues = sampleRows.map((row) => row[index] ?? "");
1145
- const originalHeaderName = normalizeIdentifierKey(headerCell);
1146
- const isPrimaryKey = !primaryKeyAssigned && originalHeaderName === "id";
1301
+ const values = importRows.map((row) => row[index]);
1302
+ const sourceKey = normalizeIdentifierKey(sourceName);
1303
+ const isPrimaryKey = !primaryKeyAssigned && sourceKey === "id";
1147
1304
 
1148
1305
  if (isPrimaryKey) {
1149
1306
  primaryKeyAssigned = true;
1150
1307
  }
1151
1308
 
1152
- return createEmptyTableDesignerColumn({
1153
- name: columnName,
1154
- type: isPrimaryKey ? "INTEGER" : inferImportedColumnType(columnName, sampleValues),
1309
+ return {
1310
+ sourceName,
1311
+ targetName,
1312
+ inferredType: isPrimaryKey ? "INTEGER" : inferSQLiteType(values, { columnName: targetName }),
1313
+ nullable: values.some((value) => value === null || value === undefined || normalizeTrimmed(value) === ""),
1155
1314
  primaryKey: isPrimaryKey,
1156
- notNull: isPrimaryKey,
1157
- importedValueIndex: index,
1315
+ };
1316
+ });
1317
+ }
1318
+
1319
+ function createDelimitedImportDataset({ fileName = "", text = "", format = "csv" } = {}) {
1320
+ const importFormat = normalizeImportFormat(format);
1321
+ const delimiter = importFormat === "tsv" ? "\t" : detectCsvDelimiter(text);
1322
+ const rows = parseCsvRows(text, delimiter, Number.POSITIVE_INFINITY).filter(
1323
+ (row) => !isCsvRowEmpty(row)
1324
+ );
1325
+
1326
+ if (!rows.length) {
1327
+ throw new Error(`The selected ${importFormat.toUpperCase()} file is empty.`);
1328
+ }
1329
+
1330
+ const headerRow = rows[0].map((cell) => normalizeImportedColumnName(cell));
1331
+
1332
+ if (!headerRow.some((cell) => normalizeTrimmed(cell))) {
1333
+ throw new Error(`The ${importFormat.toUpperCase()} header row is empty.`);
1334
+ }
1335
+
1336
+ const dataRows = rows.slice(1).filter((row) => !isCsvRowEmpty(row));
1337
+ const headerWidth = headerRow.length;
1338
+ const overflowRow = dataRows.find((row) => row.length > headerWidth);
1339
+
1340
+ if (overflowRow) {
1341
+ throw new Error(
1342
+ `${importFormat.toUpperCase()} row has more values than the header row.`
1343
+ );
1344
+ }
1345
+
1346
+ const importRows = dataRows.map((row) => headerRow.map((_, index) => row[index] ?? ""));
1347
+ const sourceNames = headerRow.map((headerCell, index) => headerCell || `column_${index + 1}`);
1348
+ const columns = buildImportColumns(sourceNames, importRows);
1349
+
1350
+ return {
1351
+ format: importFormat,
1352
+ sourceFileName: normalizeText(fileName),
1353
+ suggestedTableName: normalizeImportedTableName(fileName),
1354
+ delimiter,
1355
+ columns,
1356
+ previewRows: importRows.slice(0, 16),
1357
+ rows: importRows,
1358
+ rowCount: importRows.length,
1359
+ };
1360
+ }
1361
+
1362
+ function createJsonImportDataset({ fileName = "", text = "" } = {}) {
1363
+ const sourceText = normalizeText(text).replace(/^\uFEFF/, "").trim();
1364
+
1365
+ if (!sourceText) {
1366
+ throw new Error("The selected JSON file is empty.");
1367
+ }
1368
+
1369
+ let parsed;
1370
+
1371
+ try {
1372
+ parsed = JSON.parse(sourceText);
1373
+ } catch (error) {
1374
+ throw new Error(`INVALID_JSON: ${error.message}`);
1375
+ }
1376
+
1377
+ let records;
1378
+
1379
+ if (Array.isArray(parsed)) {
1380
+ if (!parsed.length) {
1381
+ throw new Error("EMPTY_JSON_ARRAY_IS_NOT_SUPPORTED");
1382
+ }
1383
+
1384
+ if (!parsed.every(isPlainImportObject)) {
1385
+ throw new Error("MIXED_ARRAY_VALUES_ARE_NOT_SUPPORTED");
1386
+ }
1387
+
1388
+ records = parsed;
1389
+ } else if (isPlainImportObject(parsed)) {
1390
+ records = [parsed];
1391
+ } else {
1392
+ throw new Error("JSON_ROOT_VALUE_IS_NOT_SUPPORTED");
1393
+ }
1394
+
1395
+ const sourceNames = [];
1396
+ const seenKeys = new Set();
1397
+
1398
+ records.forEach((record) => {
1399
+ Object.keys(record).forEach((key) => {
1400
+ if (seenKeys.has(key)) {
1401
+ return;
1402
+ }
1403
+
1404
+ seenKeys.add(key);
1405
+ sourceNames.push(key);
1158
1406
  });
1159
1407
  });
1160
1408
 
1161
- const importedCsvRows = rows
1162
- .slice(1)
1163
- .filter((row) => !isCsvRowEmpty(row))
1164
- .map((row) => headerRow.map((_, index) => row[index] ?? ""));
1409
+ if (!sourceNames.length) {
1410
+ throw new Error("JSON records do not contain importable properties.");
1411
+ }
1412
+
1413
+ const importRows = records.map((record) =>
1414
+ sourceNames.map((sourceName) =>
1415
+ Object.prototype.hasOwnProperty.call(record, sourceName)
1416
+ ? normalizeJsonImportValue(record[sourceName])
1417
+ : null
1418
+ )
1419
+ );
1420
+ const columns = buildImportColumns(sourceNames, importRows);
1421
+
1422
+ return {
1423
+ format: "json",
1424
+ sourceFileName: normalizeText(fileName),
1425
+ suggestedTableName: normalizeImportedTableName(fileName),
1426
+ delimiter: "",
1427
+ columns,
1428
+ previewRows: importRows.slice(0, 16),
1429
+ rows: importRows,
1430
+ rowCount: importRows.length,
1431
+ };
1432
+ }
1433
+
1434
+ export function createImportDatasetFromText({ fileName = "", text = "", format = "" } = {}) {
1435
+ const importFormat = normalizeImportFormat(format || inferImportFormatFromFileName(fileName));
1436
+
1437
+ if (importFormat === "json") {
1438
+ return createJsonImportDataset({ fileName, text });
1439
+ }
1440
+
1441
+ return createDelimitedImportDataset({ fileName, text, format: importFormat });
1442
+ }
1443
+
1444
+ export function createTableDesignerDraftFromImport(
1445
+ { fileName = "", text = "", format = "" },
1446
+ { catalogTables = [], supportedTypes = DEFAULT_TABLE_DESIGNER_TYPES, readOnly = false } = {}
1447
+ ) {
1448
+ const dataset = createImportDatasetFromText({ fileName, text, format });
1449
+ const columns = dataset.columns.map((column, index) =>
1450
+ createEmptyTableDesignerColumn({
1451
+ name: column.targetName,
1452
+ type: column.inferredType,
1453
+ primaryKey: column.primaryKey,
1454
+ notNull: column.primaryKey,
1455
+ importedValueIndex: index,
1456
+ })
1457
+ );
1165
1458
 
1166
1459
  const draft = recalculateTableDesignerDraft(
1167
1460
  {
1168
1461
  mode: "create",
1169
1462
  originalTableName: "",
1170
- tableName: suggestImportedTableName(fileName, catalogTables),
1463
+ tableName: suggestImportedTableName(dataset.sourceFileName, catalogTables),
1171
1464
  columns,
1172
1465
  uniqueConstraints: [],
1173
1466
  checkConstraints: [],
1174
1467
  designerVersion: 2,
1175
1468
  schemaWarnings: [],
1176
- fillImportedRows: importedCsvRows.length > 0,
1177
- importedCsvFileName: normalizeText(fileName),
1178
- importedCsvDelimiter: delimiter,
1179
- importedCsvRows,
1469
+ fillImportedRows: dataset.rows.length > 0,
1470
+ importFormat: dataset.format,
1471
+ importSourceFileName: dataset.sourceFileName,
1472
+ importDelimiter: dataset.delimiter,
1473
+ importRows: dataset.rows,
1474
+ importedCsvFileName: dataset.sourceFileName,
1475
+ importedCsvDelimiter: dataset.delimiter,
1476
+ importedCsvRows: dataset.rows,
1180
1477
  },
1181
1478
  { catalogTables, supportedTypes, readOnly }
1182
1479
  );
@@ -1184,12 +1481,24 @@ export function createTableDesignerDraftFromCsvImport(
1184
1481
  return {
1185
1482
  draft,
1186
1483
  columnCount: columns.length,
1187
- delimiter,
1188
- importedRowCount: importedCsvRows.length,
1189
- sampleRowCount: sampleRows.length,
1484
+ delimiter: dataset.delimiter,
1485
+ format: dataset.format,
1486
+ importedRowCount: dataset.rowCount,
1487
+ dataset,
1488
+ sampleRowCount: Math.min(dataset.rowCount, 16),
1190
1489
  };
1191
1490
  }
1192
1491
 
1492
+ export function createTableDesignerDraftFromCsvImport(
1493
+ { fileName = "", csvText = "" },
1494
+ context = {}
1495
+ ) {
1496
+ return createTableDesignerDraftFromImport(
1497
+ { fileName, text: csvText, format: "csv" },
1498
+ context
1499
+ );
1500
+ }
1501
+
1193
1502
  export function createNewTableDesignerDraft() {
1194
1503
  return {
1195
1504
  mode: "create",
@@ -1202,6 +1511,10 @@ export function createNewTableDesignerDraft() {
1202
1511
  dirty: false,
1203
1512
  schemaWarnings: [],
1204
1513
  fillImportedRows: false,
1514
+ importFormat: "",
1515
+ importSourceFileName: "",
1516
+ importDelimiter: "",
1517
+ importRows: [],
1205
1518
  importedCsvFileName: "",
1206
1519
  importedCsvDelimiter: "",
1207
1520
  importedCsvRows: [],
@@ -1323,6 +1636,62 @@ export function updateTableDesignerConstraintField(
1323
1636
  );
1324
1637
  }
1325
1638
 
1639
+ export function addTableDesignerCheckConstraint(draft, seed = {}, context = {}) {
1640
+ const nextIndex = (draft.checkConstraints ?? []).length + 1;
1641
+ const checkConstraint = {
1642
+ id: normalizeText(seed.id ?? `check:new:${createColumnId()}`),
1643
+ name: normalizeText(seed.name ?? `CHECK ${nextIndex}`),
1644
+ originalName: "",
1645
+ deleted: false,
1646
+ columns: Array.isArray(seed.columns) ? seed.columns : [],
1647
+ expression: normalizeText(seed.expression ?? ""),
1648
+ originalExpression: "",
1649
+ editable: true,
1650
+ preserved: false,
1651
+ columnId: normalizeText(seed.columnId ?? ""),
1652
+ source: normalizeText(seed.source ?? "user"),
1653
+ presetId: normalizeText(seed.presetId ?? ""),
1654
+ presetFields:
1655
+ seed.presetFields && typeof seed.presetFields === "object"
1656
+ ? Object.fromEntries(
1657
+ Object.entries(seed.presetFields).map(([key, value]) => [key, normalizeText(value)])
1658
+ )
1659
+ : {},
1660
+ };
1661
+
1662
+ return recalculateTableDesignerDraft(
1663
+ {
1664
+ ...draft,
1665
+ checkConstraints: [...(draft.checkConstraints ?? []), checkConstraint],
1666
+ },
1667
+ context
1668
+ );
1669
+ }
1670
+
1671
+ export function removeTableDesignerCheckConstraint(draft, constraintId, context = {}) {
1672
+ const targetConstraint = (draft.checkConstraints ?? []).find(
1673
+ (constraint) => constraint.id === constraintId
1674
+ );
1675
+
1676
+ if (!targetConstraint) {
1677
+ return recalculateTableDesignerDraft(draft, context);
1678
+ }
1679
+
1680
+ const nextCheckConstraints = targetConstraint.originalExpression
1681
+ ? (draft.checkConstraints ?? []).map((constraint) =>
1682
+ constraint.id === constraintId ? { ...constraint, deleted: true } : constraint
1683
+ )
1684
+ : (draft.checkConstraints ?? []).filter((constraint) => constraint.id !== constraintId);
1685
+
1686
+ return recalculateTableDesignerDraft(
1687
+ {
1688
+ ...draft,
1689
+ checkConstraints: nextCheckConstraints,
1690
+ },
1691
+ context
1692
+ );
1693
+ }
1694
+
1326
1695
  export function addTableDesignerColumn(draft, context = {}) {
1327
1696
  return recalculateTableDesignerDraft(
1328
1697
  {