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
@@ -1,4 +1,10 @@
1
- import { escapeHtml, highlightSql, truncateMiddle } from "../utils/format.js";
1
+ import { escapeHtml, formatNumber, highlightSql, truncateMiddle } from "../utils/format.js";
2
+ import {
3
+ buildCopyColumnPreviewText,
4
+ getCopyColumnActionLabel,
5
+ getCopyColumnExportMetadata,
6
+ isMarkdownTodoCopyColumnMode,
7
+ } from "../utils/copyColumnExport.js";
2
8
  import { renderConnectionLogo } from "./connectionLogo.js";
3
9
  import {
4
10
  analyzeQueryChartResult,
@@ -800,6 +806,498 @@ function renderDeleteQueryHistoryForm(modal) {
800
806
  ].join("");
801
807
  }
802
808
 
809
+ function renderQueryExportPreview(lines = []) {
810
+ return lines.map((line) => `<span class="block whitespace-pre">${escapeHtml(line)}</span>`).join("");
811
+ }
812
+
813
+ function getExportOptions() {
814
+ return [
815
+ { label: "CSV", format: "csv", icon: "table_rows", meta: ".csv", preview: ["id,name", "1,Acme"] },
816
+ { label: "TSV", format: "tsv", icon: "view_column", meta: ".tsv", preview: ["id\tname", "1\tAcme"] },
817
+ {
818
+ label: "Markdown",
819
+ format: "md",
820
+ icon: "article",
821
+ meta: ".md",
822
+ preview: ["| id | name |", "| --- | --- |"],
823
+ },
824
+ {
825
+ label: "Duplicate",
826
+ format: "table",
827
+ icon: "table_chart",
828
+ meta: "as table",
829
+ preview: ["columns inferred", "rows prefilled"],
830
+ },
831
+ ];
832
+ }
833
+
834
+ function renderTextExportModal(modal, action) {
835
+ const disabledAttribute = modal.submitting ? 'disabled aria-disabled="true"' : "";
836
+
837
+ return `
838
+ <div class="space-y-5" data-export-modal>
839
+ <label class="block space-y-2">
840
+ <span class="text-[10px] font-mono uppercase tracking-[0.22em] text-on-surface-variant/60">
841
+ Filename
842
+ </span>
843
+ <input
844
+ autocomplete="off"
845
+ class="control-input w-full border border-outline-variant/20 bg-surface-container-lowest text-sm text-on-surface outline-none transition-colors focus:border-primary-container"
846
+ name="filename"
847
+ spellcheck="false"
848
+ type="text"
849
+ value="${escapeHtml(modal.filename ?? "")}"
850
+ ${disabledAttribute}
851
+ />
852
+ </label>
853
+ <div class="grid grid-cols-1 gap-4 sm:grid-cols-2">
854
+ ${getExportOptions()
855
+ .map(
856
+ (option) => `
857
+ <button
858
+ class="group flex min-h-36 flex-col justify-between border border-outline-variant/20 bg-surface-container-low px-5 py-5 text-left text-on-surface transition-colors hover:border-primary-container/45 hover:bg-surface-container-high focus-visible:outline focus-visible:outline-2 focus-visible:outline-primary-container disabled:cursor-wait disabled:opacity-60"
859
+ data-action="${escapeHtml(action)}"
860
+ data-export-format="${escapeHtml(option.format)}"
861
+ type="button"
862
+ ${disabledAttribute}
863
+ >
864
+ <span class="flex w-full items-start justify-between gap-4">
865
+ <span class="flex min-w-0 items-center gap-3">
866
+ <span class="flex h-10 w-10 shrink-0 items-center justify-center border border-outline-variant/20 bg-surface-container-highest text-primary-container group-hover:border-primary-container/35">
867
+ <span class="material-symbols-outlined text-xl">${escapeHtml(option.icon)}</span>
868
+ </span>
869
+ <span class="min-w-0">
870
+ <span class="block truncate font-headline text-lg font-black uppercase tracking-normal text-primary-container">
871
+ ${escapeHtml(option.label)}
872
+ </span>
873
+ <span class="mt-1 block font-mono text-[10px] uppercase tracking-[0.18em] text-on-surface-variant/55">
874
+ ${escapeHtml(option.meta)}
875
+ </span>
876
+ </span>
877
+ </span>
878
+ <span class="material-symbols-outlined mt-1 text-base text-on-surface-variant/35 group-hover:text-primary-container">
879
+ arrow_forward
880
+ </span>
881
+ </span>
882
+ <span class="mt-5 block w-full overflow-hidden border border-outline-variant/10 bg-surface-container-lowest px-3 py-3 font-mono text-[11px] leading-5 text-on-surface-variant/70">
883
+ ${renderQueryExportPreview(option.preview)}
884
+ </span>
885
+ </button>
886
+ `,
887
+ )
888
+ .join("")}
889
+ </div>
890
+ ${renderError(modal.error)}
891
+ <div class="flex justify-end">
892
+ <button class="standard-button" data-action="close-modal" type="button">
893
+ Cancel
894
+ </button>
895
+ </div>
896
+ </div>
897
+ `;
898
+ }
899
+
900
+ function renderQueryExportModal(modal) {
901
+ return renderTextExportModal(modal, "export-query-format");
902
+ }
903
+
904
+ function renderDataExportModal(modal) {
905
+ return renderTextExportModal(modal, "export-data-format");
906
+ }
907
+
908
+ function getCopyColumnResult(state, modal) {
909
+ return modal.scope === "charts" ? state.charts.result : state.editor.result;
910
+ }
911
+
912
+ function renderCopyColumnPreview(modal, state) {
913
+ const result = getCopyColumnResult(state, modal);
914
+ const separator = Boolean(modal.lineBreaks) && !isMarkdownTodoCopyColumnMode(modal.copyMode)
915
+ ? "\n"
916
+ : String(modal.separator ?? ",");
917
+ const wrapper = String(modal.wrapper ?? '"');
918
+ const preview = buildCopyColumnPreviewText({
919
+ result,
920
+ columnName: modal.columnName,
921
+ copyMode: modal.copyMode,
922
+ separator,
923
+ wrapper,
924
+ maxRows: isMarkdownTodoCopyColumnMode(modal.copyMode) ? 10 : 4,
925
+ });
926
+
927
+ if (!preview) {
928
+ return "";
929
+ }
930
+
931
+ return `
932
+ <div class="space-y-2">
933
+ <div class="text-[10px] font-mono uppercase tracking-[0.22em] text-on-surface-variant/60">
934
+ Preview
935
+ </div>
936
+ <pre class="copy-column-preview custom-scrollbar">${escapeHtml(preview)}</pre>
937
+ </div>
938
+ `;
939
+ }
940
+
941
+ function renderCopyColumnLineBreaksField({ checked = false, disabled = false } = {}) {
942
+ return `
943
+ <label class="block space-y-2">
944
+ <span class="text-[10px] font-mono uppercase tracking-[0.22em] text-on-surface-variant/60">
945
+ Format
946
+ </span>
947
+ <span class="standard-checkbox ${disabled ? "is-disabled" : ""}">
948
+ <input
949
+ ${checked ? "checked" : ""}
950
+ ${disabled ? "disabled" : ""}
951
+ data-bind="copy-column-format-field"
952
+ data-field="lineBreaks"
953
+ name="lineBreaks"
954
+ type="checkbox"
955
+ />
956
+ <span>Line breaks</span>
957
+ </span>
958
+ </label>
959
+ `;
960
+ }
961
+
962
+ function renderCopyColumnFormatField({ label, name, value = "" }) {
963
+ return `
964
+ <label class="block space-y-2">
965
+ <span class="text-[10px] font-mono uppercase tracking-[0.22em] text-on-surface-variant/60">
966
+ ${escapeHtml(label)}
967
+ </span>
968
+ <input
969
+ class="control-input w-full border border-outline-variant/20 bg-surface-container-lowest text-sm text-on-surface outline-none transition-colors focus:border-primary-container"
970
+ data-bind="copy-column-format-field"
971
+ data-field="${escapeHtml(name)}"
972
+ name="${escapeHtml(name)}"
973
+ type="text"
974
+ value="${escapeHtml(value)}"
975
+ />
976
+ </label>
977
+ `;
978
+ }
979
+
980
+ function renderCopyColumnModal(modal, state) {
981
+ const result = getCopyColumnResult(state, modal);
982
+ const rows = result?.rows ?? [];
983
+ const valueCount = modal.copyMode === "first-10" ? Math.min(rows.length, 10) : rows.length;
984
+ const disabledAttribute = modal.submitting ? 'disabled aria-disabled="true"' : "";
985
+ const exportMetadata = getCopyColumnExportMetadata(modal.copyMode);
986
+ const isMarkdownTodo = isMarkdownTodoCopyColumnMode(modal.copyMode);
987
+ const lineBreaks = isMarkdownTodo || Boolean(modal.lineBreaks);
988
+ const formatFieldsMarkup = isMarkdownTodo
989
+ ? renderCopyColumnLineBreaksField({
990
+ checked: true,
991
+ disabled: true,
992
+ })
993
+ : `
994
+ <div class="grid grid-cols-1 gap-4 sm:grid-cols-3">
995
+ ${renderCopyColumnFormatField({
996
+ label: "Separator",
997
+ name: "separator",
998
+ value: modal.separator ?? ",",
999
+ })}
1000
+ ${renderCopyColumnFormatField({
1001
+ label: "Wrapper",
1002
+ name: "wrapper",
1003
+ value: modal.wrapper ?? '"',
1004
+ })}
1005
+ ${renderCopyColumnLineBreaksField({
1006
+ checked: lineBreaks,
1007
+ })}
1008
+ </div>
1009
+ `;
1010
+
1011
+ return `
1012
+ <form class="space-y-5" data-form="copy-column">
1013
+ <input name="scope" type="hidden" value="${escapeHtml(modal.scope ?? "editor")}" />
1014
+ <input name="columnName" type="hidden" value="${escapeHtml(modal.columnName ?? "")}" />
1015
+ <input name="copyMode" type="hidden" value="${escapeHtml(modal.copyMode ?? "column")}" />
1016
+ <div class="border border-outline-variant/10 bg-surface-container-lowest px-4 py-3">
1017
+ <div class="text-[10px] font-mono uppercase tracking-[0.22em] text-on-surface-variant/60">
1018
+ Column
1019
+ </div>
1020
+ <div class="mt-2 flex min-w-0 items-center justify-between gap-4">
1021
+ <code class="min-w-0 truncate font-mono text-sm text-primary-container" title="${escapeHtml(
1022
+ modal.columnName ?? ""
1023
+ )}">${escapeHtml(modal.columnName ?? "")}</code>
1024
+ <span class="shrink-0 font-mono text-[10px] uppercase tracking-[0.18em] text-on-surface-variant/50">
1025
+ ${escapeHtml(getCopyColumnActionLabel(modal.copyMode))} · ${formatNumber(valueCount)}
1026
+ </span>
1027
+ </div>
1028
+ </div>
1029
+ ${formatFieldsMarkup}
1030
+ ${renderCopyColumnPreview(modal, state)}
1031
+ ${renderError(modal.error)}
1032
+ <div class="flex flex-wrap items-center justify-end gap-3 pt-2">
1033
+ <button
1034
+ class="standard-button"
1035
+ data-action="close-modal"
1036
+ type="button"
1037
+ ${disabledAttribute}
1038
+ >
1039
+ Cancel
1040
+ </button>
1041
+ <button
1042
+ class="standard-button"
1043
+ name="intent"
1044
+ type="submit"
1045
+ value="export"
1046
+ ${disabledAttribute}
1047
+ >
1048
+ ${modal.submitting ? "Working..." : `Export as ${exportMetadata.extension.toUpperCase()}`}
1049
+ </button>
1050
+ <button
1051
+ class="signature-button"
1052
+ name="intent"
1053
+ type="submit"
1054
+ value="copy"
1055
+ ${disabledAttribute}
1056
+ >
1057
+ ${modal.submitting ? "Working..." : "Copy"}
1058
+ </button>
1059
+ </div>
1060
+ </form>
1061
+ `;
1062
+ }
1063
+
1064
+ function getTableDesignerUniqueConstraintTypeLabel(constraint) {
1065
+ if (constraint.partial) {
1066
+ return "Partial unique index";
1067
+ }
1068
+
1069
+ if ((constraint.columns?.length ?? 0) > 1) {
1070
+ return "Multi-column unique";
1071
+ }
1072
+
1073
+ return "Unique constraint";
1074
+ }
1075
+
1076
+ function renderTableDesignerUniqueConstraintExpression(constraint) {
1077
+ const expression = String(constraint.expression || constraint.sql || "").trim();
1078
+
1079
+ if (expression) {
1080
+ return expression;
1081
+ }
1082
+
1083
+ const columns = (constraint.columns ?? []).map((column) => column.name).filter(Boolean);
1084
+ return columns.length ? `UNIQUE (${columns.join(", ")})` : "UNIQUE constraint";
1085
+ }
1086
+
1087
+ function renderTableDesignerCheckConstraintExpression(constraint) {
1088
+ return String(constraint.expression || "").trim() || "CHECK constraint";
1089
+ }
1090
+
1091
+ function normalizeTableDesignerConstraintColumnName(name) {
1092
+ return String(name ?? "").trim().toLowerCase();
1093
+ }
1094
+
1095
+ function tableDesignerConstraintIncludesColumn(constraint, columnName) {
1096
+ const normalizedColumn = normalizeTableDesignerConstraintColumnName(columnName);
1097
+
1098
+ if (!normalizedColumn) {
1099
+ return false;
1100
+ }
1101
+
1102
+ return (constraint.columns ?? []).some(
1103
+ (constraintColumn) =>
1104
+ normalizeTableDesignerConstraintColumnName(constraintColumn.name) === normalizedColumn
1105
+ );
1106
+ }
1107
+
1108
+ function renderTableDesignerUniqueConstraintEditor(constraint) {
1109
+ const columns = (constraint.columns ?? []).map((column) => column.name).filter(Boolean);
1110
+
1111
+ return `
1112
+ <article class="table-designer-constraint-editor">
1113
+ <div class="table-designer-constraint-editor__header">
1114
+ <div class="min-w-0">
1115
+ <div class="table-designer-constraint-editor__type">
1116
+ ${escapeHtml(getTableDesignerUniqueConstraintTypeLabel(constraint))}
1117
+ ${constraint.origin ? ` // origin ${escapeHtml(constraint.origin)}` : ""}
1118
+ </div>
1119
+ <input
1120
+ class="table-designer-field table-designer-constraint-editor__name"
1121
+ data-bind="table-designer-constraint-field"
1122
+ data-constraint-id="${escapeHtml(constraint.id)}"
1123
+ data-constraint-kind="unique"
1124
+ data-field="name"
1125
+ spellcheck="false"
1126
+ type="text"
1127
+ value="${escapeHtml(constraint.name || "UNIQUE constraint")}"
1128
+ />
1129
+ </div>
1130
+ <div class="status-badge status-badge--muted">Rebuild</div>
1131
+ </div>
1132
+ ${
1133
+ columns.length
1134
+ ? `
1135
+ <div class="table-designer-constraint__columns">
1136
+ ${columns.map((column) => `<span>${escapeHtml(column)}</span>`).join("")}
1137
+ </div>
1138
+ `
1139
+ : ""
1140
+ }
1141
+ <textarea
1142
+ class="table-designer-constraint-editor__sql custom-scrollbar"
1143
+ data-bind="table-designer-constraint-field"
1144
+ data-constraint-id="${escapeHtml(constraint.id)}"
1145
+ data-constraint-kind="unique"
1146
+ data-field="expression"
1147
+ spellcheck="false"
1148
+ >${escapeHtml(renderTableDesignerUniqueConstraintExpression(constraint))}</textarea>
1149
+ </article>
1150
+ `;
1151
+ }
1152
+
1153
+ function renderTableDesignerCheckConstraintEditor(constraint) {
1154
+ const columns = constraint.columns ?? [];
1155
+ const allowedValues = columns.flatMap((column) =>
1156
+ (column.allowedValues ?? []).map((value) => ({
1157
+ columnName: column.name,
1158
+ value,
1159
+ }))
1160
+ );
1161
+
1162
+ return `
1163
+ <article class="table-designer-constraint-editor">
1164
+ <div class="table-designer-constraint-editor__header">
1165
+ <div class="min-w-0">
1166
+ <div class="table-designer-constraint-editor__type">
1167
+ ${columns.length ? escapeHtml(columns.map((column) => column.name).join(", ")) : "Table check"}
1168
+ </div>
1169
+ <input
1170
+ class="table-designer-field table-designer-constraint-editor__name"
1171
+ data-bind="table-designer-constraint-field"
1172
+ data-constraint-id="${escapeHtml(constraint.id)}"
1173
+ data-constraint-kind="check"
1174
+ data-field="name"
1175
+ spellcheck="false"
1176
+ type="text"
1177
+ value="${escapeHtml(constraint.name || "CHECK constraint")}"
1178
+ />
1179
+ </div>
1180
+ <div class="status-badge status-badge--muted">Rebuild</div>
1181
+ </div>
1182
+ ${
1183
+ allowedValues.length
1184
+ ? `
1185
+ <div class="table-designer-constraint__values custom-scrollbar">
1186
+ ${allowedValues
1187
+ .map(
1188
+ (entry) => `
1189
+ <span title="${escapeHtml(entry.columnName)}">${escapeHtml(entry.value)}</span>
1190
+ `
1191
+ )
1192
+ .join("")}
1193
+ </div>
1194
+ `
1195
+ : ""
1196
+ }
1197
+ <textarea
1198
+ class="table-designer-constraint-editor__sql custom-scrollbar"
1199
+ data-bind="table-designer-constraint-field"
1200
+ data-constraint-id="${escapeHtml(constraint.id)}"
1201
+ data-constraint-kind="check"
1202
+ data-field="expression"
1203
+ spellcheck="false"
1204
+ >${escapeHtml(renderTableDesignerCheckConstraintExpression(constraint))}</textarea>
1205
+ </article>
1206
+ `;
1207
+ }
1208
+
1209
+ function renderTableDesignerConstraintSection({ title, count, emptyText, body }) {
1210
+ return `
1211
+ <section class="table-designer-constraints-modal__section">
1212
+ <div class="table-designer-constraints-modal__section-header">
1213
+ <div class="table-designer-constraints-modal__section-title">${escapeHtml(title)}</div>
1214
+ <div class="status-badge status-badge--muted">${formatNumber(count)}</div>
1215
+ </div>
1216
+ ${
1217
+ count
1218
+ ? `<div class="table-designer-constraints__list">${body}</div>`
1219
+ : `<div class="table-designer-constraints-modal__empty">${escapeHtml(emptyText)}</div>`
1220
+ }
1221
+ </section>
1222
+ `;
1223
+ }
1224
+
1225
+ function renderTableDesignerConstraintsModal(modal, state) {
1226
+ const draft = state.tableDesigner?.draft;
1227
+
1228
+ if (!draft) {
1229
+ return `
1230
+ <div class="table-designer-constraints-modal__empty">
1231
+ No active table designer draft.
1232
+ </div>
1233
+ <div class="flex items-center justify-end gap-3 pt-2">
1234
+ <button class="standard-button" data-action="close-modal" type="button">Close</button>
1235
+ </div>
1236
+ `;
1237
+ }
1238
+
1239
+ const selectedColumn = modal.columnId
1240
+ ? (draft.columns ?? []).find((column) => column.id === modal.columnId)
1241
+ : null;
1242
+ const selectedColumnName = String(modal.columnName ?? selectedColumn?.name ?? "").trim();
1243
+ const hasSelectedColumn = Boolean(modal.columnId || selectedColumnName);
1244
+ const selectedColumnLabel = selectedColumnName || "Unnamed column";
1245
+ const allUniqueConstraints = draft.mode === "edit" ? draft.uniqueConstraints ?? [] : [];
1246
+ const allCheckConstraints = draft.mode === "edit" ? draft.checkConstraints ?? [] : [];
1247
+ const uniqueConstraints = hasSelectedColumn
1248
+ ? allUniqueConstraints.filter((constraint) =>
1249
+ tableDesignerConstraintIncludesColumn(constraint, selectedColumnName)
1250
+ )
1251
+ : allUniqueConstraints;
1252
+ const checkConstraints = hasSelectedColumn
1253
+ ? allCheckConstraints.filter((constraint) =>
1254
+ tableDesignerConstraintIncludesColumn(constraint, selectedColumnName)
1255
+ )
1256
+ : allCheckConstraints;
1257
+ const totalCount = uniqueConstraints.length + checkConstraints.length;
1258
+
1259
+ return `
1260
+ <div class="table-designer-constraints-modal">
1261
+ <div class="table-designer-constraints-modal__summary">
1262
+ <div class="min-w-0">
1263
+ <div class="table-designer-constraints-modal__label">Table</div>
1264
+ <code title="${escapeHtml(draft.tableName ?? "")}">${escapeHtml(draft.tableName ?? "")}</code>
1265
+ </div>
1266
+ ${
1267
+ hasSelectedColumn
1268
+ ? `
1269
+ <div class="min-w-0">
1270
+ <div class="table-designer-constraints-modal__label">Column</div>
1271
+ <code title="${escapeHtml(selectedColumnLabel)}">${escapeHtml(selectedColumnLabel)}</code>
1272
+ </div>
1273
+ `
1274
+ : ""
1275
+ }
1276
+ <div class="status-badge status-badge--primary">V2 · ${formatNumber(totalCount)}</div>
1277
+ </div>
1278
+ ${renderTableDesignerConstraintSection({
1279
+ title: "Check constraints",
1280
+ count: checkConstraints.length,
1281
+ emptyText: hasSelectedColumn
1282
+ ? "No check constraints detected for this column."
1283
+ : "No check constraints detected.",
1284
+ body: checkConstraints.map(renderTableDesignerCheckConstraintEditor).join(""),
1285
+ })}
1286
+ ${renderTableDesignerConstraintSection({
1287
+ title: hasSelectedColumn ? "Related unique constraints" : "Unique constraints",
1288
+ count: uniqueConstraints.length,
1289
+ emptyText: hasSelectedColumn
1290
+ ? "No related multi-column or partial unique constraints detected."
1291
+ : "No multi-column or partial unique constraints detected.",
1292
+ body: uniqueConstraints.map(renderTableDesignerUniqueConstraintEditor).join(""),
1293
+ })}
1294
+ <div class="flex items-center justify-end gap-3 pt-2">
1295
+ <button class="standard-button" data-action="close-modal" type="button">Close</button>
1296
+ </div>
1297
+ </div>
1298
+ `;
1299
+ }
1300
+
803
1301
  function renderCreateMediaTaggingMappingTableForm(modal, state) {
804
1302
  const mappingExists = hasDefaultMediaTaggingMappingTable(state.mediaTagging.schemaTables ?? []);
805
1303
  const readOnly = Boolean(state.mediaTagging.connection?.readOnly);
@@ -981,6 +1479,26 @@ export function renderModal(state) {
981
1479
  title: "Delete Query",
982
1480
  body: renderDeleteQueryHistoryForm(modal),
983
1481
  },
1482
+ "query-export": {
1483
+ eyebrow: "SQL Editor // Export query result",
1484
+ title: "Export Query",
1485
+ body: renderQueryExportModal(modal),
1486
+ },
1487
+ "data-export": {
1488
+ eyebrow: "Data Browser // Export table data",
1489
+ title: "Export Table",
1490
+ body: renderDataExportModal(modal),
1491
+ },
1492
+ "copy-column": {
1493
+ eyebrow: "Results // Copy or export column values",
1494
+ title: isMarkdownTodoCopyColumnMode(modal.copyMode) ? "Export Markdown Todo" : "Copy column",
1495
+ body: renderCopyColumnModal(modal, state),
1496
+ },
1497
+ "table-designer-constraints": {
1498
+ eyebrow: "Table Designer // Constraints",
1499
+ title: "Checks",
1500
+ body: renderTableDesignerConstraintsModal(modal, state),
1501
+ },
984
1502
  "create-media-tagging-tag-table": {
985
1503
  eyebrow: "Media Tagging // Create default tag table",
986
1504
  title: "Create Tag Table",
@@ -1001,7 +1519,13 @@ export function renderModal(state) {
1001
1519
 
1002
1520
  return `
1003
1521
  <div class="fixed inset-0 z-50 flex items-center justify-center bg-background/85 px-4 backdrop-blur-sm">
1004
- <div class="w-full ${modal.kind === "chart-editor" || modal.kind === "row-update-preview" ? "max-w-3xl" : "max-w-xl"} border border-outline-variant/20 bg-surface-container shadow-[0_24px_80px_rgba(0,0,0,0.45)]">
1522
+ <div class="w-full ${
1523
+ modal.kind === "chart-editor" ||
1524
+ modal.kind === "row-update-preview" ||
1525
+ modal.kind === "table-designer-constraints"
1526
+ ? "max-w-3xl"
1527
+ : "max-w-xl"
1528
+ } border border-outline-variant/20 bg-surface-container shadow-[0_24px_80px_rgba(0,0,0,0.45)]">
1005
1529
  <div class="flex items-start justify-between gap-4 border-b border-outline-variant/10 bg-surface-container-low px-6 py-5">
1006
1530
  <div>
1007
1531
  <div class="text-[10px] font-mono uppercase tracking-[0.26em] text-primary-container/70">
@@ -69,9 +69,17 @@ export function renderQueryEditor({
69
69
  data-next-value="${historyVisible ? "false" : "true"}"
70
70
  type="button"
71
71
  >
72
- <span class="material-symbols-outlined text-sm">${historyVisible ? "visibility_off" : "history"}</span>
72
+ <span class="material-symbols-outlined text-sm">${historyVisible ? "visibility_off" : "visibility"}</span>
73
73
  ${historyVisible ? "Hide History" : "Show History"}
74
74
  </button>
75
+ <button
76
+ class="${secondaryButtonClass}"
77
+ data-action="format-current-query"
78
+ type="button"
79
+ >
80
+ <span class="material-symbols-outlined text-sm">format_align_left</span>
81
+ Format
82
+ </button>
75
83
  <button
76
84
  class="${secondaryButtonClass}"
77
85
  data-action="clear-query"
@@ -81,10 +89,11 @@ export function renderQueryEditor({
81
89
  </button>
82
90
  <button
83
91
  class="${secondaryButtonClass}"
84
- data-action="export-query-csv"
92
+ data-action="open-query-export-modal"
85
93
  type="button"
86
94
  >
87
- ${exporting ? "Exporting..." : "Export CSV"}
95
+ <span class="material-symbols-outlined text-sm">download</span>
96
+ ${exporting ? "Exporting..." : "Export"}
88
97
  </button>
89
98
  <button
90
99
  class="signature-button"