@adecore/database 0.39.0 → 0.40.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 (131) hide show
  1. package/dist/console/QueryConsole.js +2 -10
  2. package/dist/console/SchemaPicker.js +0 -1
  3. package/dist/console/StatementResultView.js +1 -1
  4. package/dist/ddl/diff.js +1 -1
  5. package/dist/explorer/tree.js +0 -1
  6. package/dist/export-formats.d.ts +2 -0
  7. package/dist/export-formats.js +1 -0
  8. package/dist/grid/CopyAsMenu.js +0 -1
  9. package/dist/grid/DataGrid.js +10 -10
  10. package/dist/grid/column-layout.d.ts +0 -1
  11. package/dist/grid/column-layout.js +1 -2
  12. package/dist/grid/column-view.js +3 -2
  13. package/dist/grid/display.js +1 -2
  14. package/dist/host/host.js +1 -2
  15. package/dist/host/validate.d.ts +3 -0
  16. package/dist/host/validate.js +18 -15
  17. package/dist/same-list.d.ts +1 -0
  18. package/dist/same-list.js +1 -0
  19. package/dist/stored-size.d.ts +1 -0
  20. package/dist/stored-size.js +10 -0
  21. package/dist/table/TableToolbar.js +1 -1
  22. package/dist/table/TableView.js +2 -10
  23. package/package.json +15 -15
  24. package/src/DatabaseProvider.tsx +2 -0
  25. package/src/KeyIcon.tsx +1 -0
  26. package/src/client/client.ts +13 -0
  27. package/src/client-context.ts +2 -0
  28. package/src/connections/ConnectionForm.tsx +11 -0
  29. package/src/connections/ConnectionManager.tsx +9 -0
  30. package/src/connections/connection-config.ts +21 -0
  31. package/src/connections/docker.ts +2 -0
  32. package/src/connections/use-docker-containers.ts +3 -0
  33. package/src/console/DestructiveDialog.tsx +1 -0
  34. package/src/console/QueryConsole.tsx +38 -10
  35. package/src/console/SchemaPicker.tsx +1 -1
  36. package/src/console/SqlEditor.tsx +10 -0
  37. package/src/console/StatementResultView.tsx +7 -2
  38. package/src/console/TransactionControls.tsx +1 -0
  39. package/src/console/destructive.ts +11 -0
  40. package/src/console/editing.ts +10 -0
  41. package/src/console/history.ts +8 -0
  42. package/src/console/labels.ts +1 -0
  43. package/src/console/outcome.ts +1 -0
  44. package/src/console/result-keys.ts +16 -0
  45. package/src/console/shortcuts.ts +2 -0
  46. package/src/ddl/alter-mysql.ts +16 -0
  47. package/src/ddl/alter-sqlite.ts +21 -0
  48. package/src/ddl/alter.ts +2 -0
  49. package/src/ddl/column.ts +11 -0
  50. package/src/ddl/create.ts +15 -0
  51. package/src/ddl/dialect.ts +4 -0
  52. package/src/ddl/diff.ts +22 -2
  53. package/src/ddl/draft.ts +1 -0
  54. package/src/ddl/parse.ts +31 -0
  55. package/src/ddl/types.ts +3 -0
  56. package/src/ddl/validate.ts +12 -0
  57. package/src/designer/ColumnsEditor.tsx +3 -0
  58. package/src/designer/DesignerView.tsx +2 -0
  59. package/src/designer/ForeignKeysEditor.tsx +4 -0
  60. package/src/designer/IndexesEditor.tsx +1 -0
  61. package/src/designer/NamePicker.tsx +2 -0
  62. package/src/designer/OptionsEditor.tsx +4 -0
  63. package/src/designer/SqlPreview.tsx +2 -0
  64. package/src/designer/TableDesigner.tsx +14 -0
  65. package/src/designer/edit.ts +13 -0
  66. package/src/designer/plan.ts +3 -0
  67. package/src/designer/recover.ts +1 -0
  68. package/src/designer/useReferenceColumns.ts +5 -0
  69. package/src/explorer/DatabaseExplorer.tsx +40 -0
  70. package/src/explorer/RowMenu.tsx +2 -0
  71. package/src/explorer/TableDialog.tsx +6 -0
  72. package/src/explorer/expansion.ts +8 -0
  73. package/src/explorer/tree.ts +73 -2
  74. package/src/explorer/use-explorer-loads.ts +15 -0
  75. package/src/explorer/use-stored-expansion.ts +4 -0
  76. package/src/export-formats.ts +3 -0
  77. package/src/grid/CellEditor.tsx +1 -0
  78. package/src/grid/CopyAsMenu.tsx +1 -1
  79. package/src/grid/DataGrid.tsx +93 -23
  80. package/src/grid/EnumPicker.tsx +6 -0
  81. package/src/grid/GridHeaderCell.tsx +11 -0
  82. package/src/grid/GridRowView.tsx +3 -0
  83. package/src/grid/RecordView.tsx +19 -0
  84. package/src/grid/ValueDock.tsx +2 -0
  85. package/src/grid/aggregates.ts +12 -0
  86. package/src/grid/column-layout.ts +12 -4
  87. package/src/grid/column-selection.ts +9 -0
  88. package/src/grid/column-view.ts +6 -2
  89. package/src/grid/copy-formats.ts +13 -0
  90. package/src/grid/display.ts +17 -3
  91. package/src/grid/edit-value.ts +11 -0
  92. package/src/grid/enum-type.ts +15 -0
  93. package/src/grid/focused-value.ts +4 -0
  94. package/src/grid/layout.ts +12 -0
  95. package/src/grid/navigation.ts +5 -0
  96. package/src/grid/row-selection.ts +7 -0
  97. package/src/grid/sort.ts +1 -0
  98. package/src/grid/whole-cells.ts +8 -0
  99. package/src/host/host.ts +16 -3
  100. package/src/host/spawn.ts +2 -0
  101. package/src/host/validate.ts +37 -15
  102. package/src/same-list.ts +2 -0
  103. package/src/sql-split.ts +28 -0
  104. package/src/sql.ts +20 -0
  105. package/src/stored-size.ts +10 -0
  106. package/src/structure/StructureView.tsx +18 -0
  107. package/src/table/CommandField.tsx +11 -0
  108. package/src/table/ImportDialog.tsx +4 -0
  109. package/src/table/TableStatusBar.tsx +1 -0
  110. package/src/table/TableToolbar.tsx +2 -2
  111. package/src/table/TableView.tsx +50 -10
  112. package/src/table/command-field.ts +40 -0
  113. package/src/table/grid-rows.ts +13 -0
  114. package/src/table/import-mapping.ts +5 -0
  115. package/src/table/layout-store.ts +7 -0
  116. package/src/table/paging.ts +1 -0
  117. package/src/table/pending.ts +18 -0
  118. package/src/table/references.ts +4 -0
  119. package/src/table/useLoaded.ts +3 -0
  120. package/src/table/useStoredLayout.ts +2 -0
  121. package/src/table/useTableTransfer.ts +17 -1
  122. package/src/testing/fake.ts +24 -0
  123. package/src/use-database-resources.ts +1 -0
  124. package/src/use-popup-press.ts +3 -0
  125. package/src/value/ValueEditor.tsx +6 -0
  126. package/src/value/draft.ts +7 -0
  127. package/src/value/hex.ts +8 -0
  128. package/src/value/json.ts +16 -0
  129. package/src/value/views.ts +14 -0
  130. package/src/workbench/DatabaseWorkbench.tsx +22 -0
  131. package/src/workbench/tabs.ts +34 -0
@@ -56,6 +56,7 @@ type MenuProps = Pick<GridHeaderCellProps, 'column' | 'index' | 'sortable' | 'ha
56
56
  /* The items of the menu a header opens, from its button and from a right click. */
57
57
  function HeaderMenuItems({ column, index, sortable, hasSorts, pinned, hasHidden, canHide, actions }: MenuProps) {
58
58
  const { t } = useTranslation('database');
59
+
59
60
  return (
60
61
  <>
61
62
  {sortable && (
@@ -102,6 +103,7 @@ function HeaderMenuItems({ column, index, sortable, hasSorts, pinned, hasHidden,
102
103
  function ColumnFacts({ column }: { column: GridColumn }) {
103
104
  const { t } = useTranslation('database');
104
105
  const keys = { primaryKey: column.primaryKey === true, foreignKey: column.foreignKey === true };
106
+
105
107
  const facts = [
106
108
  keys.primaryKey || keys.foreignKey ? t(keyLabelOf(keys)) : null,
107
109
  column.type === '' ? null : column.type,
@@ -109,6 +111,7 @@ function ColumnFacts({ column }: { column: GridColumn }) {
109
111
  column.defaultValue === undefined || column.defaultValue === null ? null : t('grid.column.default', { value: column.defaultValue }),
110
112
  column.autoIncrement === true ? t('grid.column.autoIncrement') : null
111
113
  ].filter((fact) => fact !== null);
114
+
112
115
  return (
113
116
  <>
114
117
  <span className="block">{column.name}</span>
@@ -141,6 +144,7 @@ export function GridHeaderCell({
141
144
  const lastPress = useRef<number | null>(null);
142
145
  // Set by a press on the resize strip, since the click that ends the drag would otherwise pick the column.
143
146
  const resized = useRef(false);
147
+
144
148
  const { startResize } = useColumnResize(cell, {
145
149
  size: width,
146
150
  min: MIN_COLUMN_WIDTH,
@@ -151,25 +155,32 @@ export function GridHeaderCell({
151
155
 
152
156
  const pressHandle = (event: PointerEvent<HTMLElement>): void => {
153
157
  resized.current = true;
158
+
154
159
  if (isDoubleClick(lastPress.current, event.timeStamp)) {
155
160
  lastPress.current = null;
156
161
  actions.onFit(index);
162
+
157
163
  return;
158
164
  }
165
+
159
166
  lastPress.current = event.timeStamp;
160
167
  startResize(event);
161
168
  };
162
169
 
163
170
  const handleClick = (event: MouseEvent<HTMLDivElement>): void => {
164
171
  const target = event.target as Element;
172
+
165
173
  // A click inside a menu popup reaches here through the React tree, not the DOM.
166
174
  if (!event.currentTarget.contains(target) || target.closest('button') !== null) {
167
175
  return;
168
176
  }
177
+
169
178
  if (resized.current) {
170
179
  resized.current = false;
180
+
171
181
  return;
172
182
  }
183
+
173
184
  actions.onSelect(index, { shiftKey: event.shiftKey, mod: isModHeld(event, isApplePlatform()) });
174
185
  };
175
186
 
@@ -85,6 +85,7 @@ export interface GridRowViewProps {
85
85
  export const GridRowView = memo(function GridRowView({ row, index, selected, focusedColumn, rangeStart, rangeEnd, editing, look, actions }: GridRowViewProps) {
86
86
  const { columns, order, widths, pinnedShown, gutter, notation, enumTypes, picked } = look;
87
87
  const tint = row.state === undefined ? (selected ? 'bg-accent-soft/40' : '') : ROW_STATE[row.state];
88
+
88
89
  const cells = order.map((columnIndex, position) => {
89
90
  const column = columns[columnIndex]!;
90
91
  const cell = row.cells[columnIndex] ?? null;
@@ -96,6 +97,7 @@ export const GridRowView = memo(function GridRowView({ row, index, selected, foc
96
97
  const isEditing = editing !== null && editing.column === columnIndex;
97
98
  const pinned = position < pinnedShown;
98
99
  const enumType = enumTypes[columnIndex] ?? null;
100
+
99
101
  return (
100
102
  <div
101
103
  key={columnIndex}
@@ -154,6 +156,7 @@ export const GridRowView = memo(function GridRowView({ row, index, selected, foc
154
156
  </div>
155
157
  );
156
158
  });
159
+
157
160
  return (
158
161
  <div
159
162
  role="row"
@@ -56,13 +56,16 @@ const textOf = (cell: GridRow['cells'][number], kind: GridColumn['kind'], notati
56
56
  if (isPreview(cell)) {
57
57
  return cellView(cell, kind, notation).text;
58
58
  }
59
+
59
60
  if (cell !== null && typeof cell === 'object' && cell.kind === 'default') {
60
61
  return '';
61
62
  }
63
+
62
64
  // A field that can be edited holds the server's text, which is what an edit starts from.
63
65
  if (readOnly && isNumericKind(kind) && (typeof cell === 'number' || typeof cell === 'string')) {
64
66
  return numeralText(cell, notation);
65
67
  }
68
+
66
69
  return draftOf(cell);
67
70
  };
68
71
 
@@ -116,6 +119,7 @@ export function RecordView({
116
119
  if (editing !== null && !editing.loading && row !== undefined && editing.draft !== editing.initial) {
117
120
  onCommit?.(row.key, editing.column, parseDraft(editing.draft, columns[editing.column]!.kind));
118
121
  }
122
+
119
123
  setEditing(null);
120
124
  };
121
125
 
@@ -127,19 +131,25 @@ export function RecordView({
127
131
 
128
132
  const beginEdit = async (column: number): Promise<void> => {
129
133
  const cell = row?.cells[column] ?? null;
134
+
130
135
  if (row === undefined || !canEdit(column) || !isPreview(cell) || loadValue === undefined) {
131
136
  return;
132
137
  }
138
+
133
139
  setEditing({ column, draft: '', initial: '', loading: true });
134
140
  const value = await loadValue(row.key, column);
141
+
135
142
  setEditing((now) => {
136
143
  if (now === null || !now.loading || now.column !== column) {
137
144
  return now;
138
145
  }
146
+
139
147
  if (value === undefined) {
140
148
  return null;
141
149
  }
150
+
142
151
  const text = draftOf(value);
152
+
143
153
  return { column, draft: text, initial: text, loading: false };
144
154
  });
145
155
  };
@@ -155,9 +165,11 @@ export function RecordView({
155
165
  const toggleOpened = (column: number): void => {
156
166
  setOpened((now) => {
157
167
  const next = new Set(now);
168
+
158
169
  if (!next.delete(column)) {
159
170
  next.add(column);
160
171
  }
172
+
161
173
  return next;
162
174
  });
163
175
  };
@@ -171,10 +183,13 @@ export function RecordView({
171
183
  if (event.nativeEvent.isComposing) {
172
184
  return;
173
185
  }
186
+
174
187
  const mod = isModHeld(event, apple);
188
+
175
189
  if (event.key === 'ArrowUp' || event.key === 'ArrowDown') {
176
190
  event.preventDefault();
177
191
  const direction = event.key === 'ArrowUp' ? -1 : 1;
192
+
178
193
  if (mod) {
179
194
  moveRow(index + direction);
180
195
  } else {
@@ -241,6 +256,7 @@ export function RecordView({
241
256
  const placeholder = cell === null ? 'NULL' : typeof cell === 'object' && cell.kind === 'default' ? 'DEFAULT' : undefined;
242
257
  const expandable = enumType === null && opensUp(cell, column.kind);
243
258
  const expanded = expandable && opened.has(position);
259
+
244
260
  return (
245
261
  <ContextMenu.Root key={position}>
246
262
  <ContextMenu.Trigger
@@ -382,12 +398,15 @@ function WholeValue({
382
398
  if (known !== undefined || loadValue === undefined) {
383
399
  return;
384
400
  }
401
+
385
402
  let live = true;
403
+
386
404
  void loadValue(row.key, column).then((whole) => {
387
405
  if (live) {
388
406
  setFetched({ cell, value: whole });
389
407
  }
390
408
  });
409
+
391
410
  return () => {
392
411
  live = false;
393
412
  };
@@ -25,6 +25,7 @@ export function ValueDock({ open, panel, children, width: given, onWidthChange,
25
25
  const dock = useRef<HTMLDivElement>(null);
26
26
  const [own, setOwn] = useState(VALUE_PANEL_WIDTH);
27
27
  const width = given ?? own;
28
+
28
29
  const { startResize } = useColumnResize(dock, {
29
30
  size: width,
30
31
  min: VALUE_PANEL_MIN_WIDTH,
@@ -34,6 +35,7 @@ export function ValueDock({ open, panel, children, width: given, onWidthChange,
34
35
  if (given === undefined) {
35
36
  setOwn(next);
36
37
  }
38
+
37
39
  onWidthChange?.(next);
38
40
  }
39
41
  });
@@ -32,10 +32,13 @@ const numberOf = (cell: Shown): { value: number; text: string } | null => {
32
32
  if (typeof cell === 'number') {
33
33
  return Number.isFinite(cell) ? { value: cell, text: String(cell) } : null;
34
34
  }
35
+
35
36
  if (typeof cell === 'string') {
36
37
  const text = cell.trim();
38
+
37
39
  return NUMERIC_TEXT.test(text) ? { value: Number(text), text } : null;
38
40
  }
41
+
39
42
  return null;
40
43
  };
41
44
 
@@ -52,31 +55,40 @@ export const aggregateBlock = (rows: readonly (readonly Shown[])[], kinds: reado
52
55
  let sum = 0;
53
56
  let minimum: { value: number; text: string } | null = null;
54
57
  let maximum: { value: number; text: string } | null = null;
58
+
55
59
  for (let row = block.top; row <= block.bottom; row++) {
56
60
  const cells = rows[row];
61
+
57
62
  if (cells === undefined) {
58
63
  continue;
59
64
  }
65
+
60
66
  for (const column of block.columns) {
61
67
  count++;
62
68
  const kind = kinds[column];
63
69
  const found = kind !== undefined && isNumericKind(kind) ? numberOf(cells[column] ?? null) : null;
70
+
64
71
  if (found === null) {
65
72
  continue;
66
73
  }
74
+
67
75
  numbers++;
68
76
  sum += found.value;
77
+
69
78
  if (minimum === null || found.value < minimum.value) {
70
79
  minimum = found;
71
80
  }
81
+
72
82
  if (maximum === null || found.value > maximum.value) {
73
83
  maximum = found;
74
84
  }
75
85
  }
76
86
  }
87
+
77
88
  if (numbers === 0 || minimum === null || maximum === null) {
78
89
  return { count, numeric: null };
79
90
  }
91
+
80
92
  return {
81
93
  count,
82
94
  numeric: {
@@ -1,5 +1,6 @@
1
1
  import type { ColumnView } from './column-view.ts';
2
2
  import { MAX_RESIZED_WIDTH, MIN_COLUMN_WIDTH } from './layout.ts';
3
+ import { sameList } from '../same-list.ts';
3
4
 
4
5
  /* What a person did to the columns of a grid, by column name so it outlives a change of the column order. */
5
6
  export interface GridLayout {
@@ -10,8 +11,6 @@ export interface GridLayout {
10
11
  readonly pinned: readonly string[];
11
12
  }
12
13
 
13
- export const NO_GRID_LAYOUT: GridLayout = { widths: {}, hidden: [], pinned: [] };
14
-
15
14
  export interface ColumnState {
16
15
  readonly resized: Readonly<Record<number, number>>;
17
16
  readonly view: ColumnView;
@@ -22,36 +21,46 @@ export const restoreLayout = (columns: readonly { readonly name: string }[], lay
22
21
  const resized: Record<number, number> = {};
23
22
  const hidden = new Set<number>();
24
23
  const pinned: number[] = [];
24
+
25
25
  if (layout !== undefined) {
26
26
  columns.forEach((column, index) => {
27
27
  const width = layout.widths[column.name];
28
+
28
29
  if (width !== undefined) {
29
30
  resized[index] = Math.max(MIN_COLUMN_WIDTH, Math.min(Math.round(width), MAX_RESIZED_WIDTH));
30
31
  }
32
+
31
33
  if (layout.hidden.includes(column.name)) {
32
34
  hidden.add(index);
33
35
  }
34
36
  });
37
+
35
38
  for (const name of layout.pinned) {
36
39
  const index = columns.findIndex((column) => column.name === name);
40
+
37
41
  if (index >= 0 && !hidden.has(index) && !pinned.includes(index)) {
38
42
  pinned.push(index);
39
43
  }
40
44
  }
41
45
  }
46
+
42
47
  return { resized, view: { hidden: hidden.size >= columns.length ? new Set() : hidden, pinned } };
43
48
  };
44
49
 
45
50
  /* The layout of a grid's state, with the columns by name. */
46
51
  export const captureLayout = (columns: readonly { readonly name: string }[], state: ColumnState): GridLayout => {
47
52
  const widths: Record<string, number> = {};
53
+
48
54
  for (const [index, width] of Object.entries(state.resized)) {
49
55
  const column = columns[Number(index)];
56
+
50
57
  if (column !== undefined) {
51
58
  widths[column.name] = width;
52
59
  }
53
60
  }
61
+
54
62
  const namesOf = (indexes: readonly number[]): string[] => indexes.flatMap((index) => columns[index]?.name ?? []);
63
+
55
64
  return {
56
65
  widths,
57
66
  hidden: namesOf([...state.view.hidden].sort((a, b) => a - b)),
@@ -59,10 +68,9 @@ export const captureLayout = (columns: readonly { readonly name: string }[], sta
59
68
  };
60
69
  };
61
70
 
62
- const sameList = (a: readonly string[], b: readonly string[]): boolean => a.length === b.length && a.every((item, index) => item === b[index]);
63
-
64
71
  export const sameLayout = (a: GridLayout, b: GridLayout): boolean => {
65
72
  const names = Object.keys(a.widths);
73
+
66
74
  return (
67
75
  names.length === Object.keys(b.widths).length &&
68
76
  names.every((name) => a.widths[name] === b.widths[name]) &&
@@ -19,22 +19,31 @@ export const NO_COLUMN_SELECTION: ColumnSelection = { columns: new Set(), anchor
19
19
  */
20
20
  export const selectColumn = (current: ColumnSelection, order: readonly number[], index: number, click: ColumnClick): ColumnSelection => {
21
21
  const at = order.indexOf(index);
22
+
22
23
  if (at < 0) {
23
24
  return current;
24
25
  }
26
+
25
27
  const anchorAt = current.anchor === null ? -1 : order.indexOf(current.anchor);
28
+
26
29
  if (click.shiftKey && anchorAt >= 0) {
27
30
  const range = order.slice(Math.min(anchorAt, at), Math.max(anchorAt, at) + 1);
31
+
28
32
  return { columns: new Set(click.mod ? [...current.columns, ...range] : range), anchor: current.anchor };
29
33
  }
34
+
30
35
  if (click.mod) {
31
36
  const next = new Set(current.columns);
37
+
32
38
  if (next.delete(index)) {
33
39
  return { columns: next, anchor: next.size === 0 ? null : current.anchor };
34
40
  }
41
+
35
42
  next.add(index);
43
+
36
44
  return { columns: next, anchor: index };
37
45
  }
46
+
38
47
  return { columns: new Set([index]), anchor: index };
39
48
  };
40
49
 
@@ -7,15 +7,19 @@ export interface ColumnView {
7
7
 
8
8
  export const NO_COLUMN_VIEW: ColumnView = { hidden: new Set(), pinned: [] };
9
9
 
10
+ const shownPins = (count: number, view: ColumnView): number[] => view.pinned.filter((index) => index < count && !view.hidden.has(index));
11
+
10
12
  /* The columns to draw as indexes: the pinned ones first in pin order, then the rest as they come, hidden ones left out. */
11
13
  export const displayOrder = (count: number, view: ColumnView): number[] => {
12
- const pinned = view.pinned.filter((index) => index < count && !view.hidden.has(index));
14
+ const pinned = shownPins(count, view);
13
15
  const rest: number[] = [];
16
+
14
17
  for (let i = 0; i < count; i++) {
15
18
  if (!view.hidden.has(i) && !pinned.includes(i)) {
16
19
  rest.push(i);
17
20
  }
18
21
  }
22
+
19
23
  return [...pinned, ...rest];
20
24
  };
21
25
 
@@ -32,4 +36,4 @@ export const togglePin = (view: ColumnView, index: number): ColumnView => ({
32
36
  });
33
37
 
34
38
  /* How many of the first columns of `displayOrder` are pinned. */
35
- export const pinnedCount = (count: number, view: ColumnView): number => view.pinned.filter((index) => index < count && !view.hidden.has(index)).length;
39
+ export const pinnedCount = (count: number, view: ColumnView): number => shownPins(count, view).length;
@@ -34,19 +34,24 @@ const jsonOf = (cell: Shown, kind: ValueKind): string => {
34
34
  if (cell === null) {
35
35
  return 'null';
36
36
  }
37
+
37
38
  if (typeof cell === 'boolean') {
38
39
  return String(cell);
39
40
  }
41
+
40
42
  if (typeof cell === 'number') {
41
43
  if (kind === 'boolean' && (cell === 0 || cell === 1)) {
42
44
  return String(cell === 1);
43
45
  }
46
+
44
47
  return Number.isFinite(cell) ? String(cell) : 'null';
45
48
  }
49
+
46
50
  if (typeof cell === 'string') {
47
51
  if (isNumericKind(kind) && JSON_NUMBER.test(cell)) {
48
52
  return cell;
49
53
  }
54
+
50
55
  if (kind === 'json') {
51
56
  try {
52
57
  return JSON.stringify(JSON.parse(cell));
@@ -54,8 +59,10 @@ const jsonOf = (cell: Shown, kind: ValueKind): string => {
54
59
  return JSON.stringify(cell);
55
60
  }
56
61
  }
62
+
57
63
  return JSON.stringify(cell);
58
64
  }
65
+
59
66
  return cell.kind === 'default' ? 'null' : JSON.stringify(copyTextOf(cell));
60
67
  };
61
68
 
@@ -70,13 +77,17 @@ const toJson = ({ columns, rows }: CopyInput): string => {
70
77
  if (rows.length === 0) {
71
78
  return '[]';
72
79
  }
80
+
73
81
  const objects = rows.map((row) => {
74
82
  if (columns.length === 0) {
75
83
  return ' {}';
76
84
  }
85
+
77
86
  const fields = columns.map((column, index) => ` ${JSON.stringify(column.name)}: ${jsonOf(row[index] ?? null, column.kind)}`);
87
+
78
88
  return ` {\n${fields.join(',\n')}\n }`;
79
89
  });
90
+
80
91
  return `[\n${objects.join(',\n')}\n]`;
81
92
  };
82
93
 
@@ -84,7 +95,9 @@ const toSql = ({ columns, rows, target }: CopyInput): string => {
84
95
  if (target === undefined) {
85
96
  throw new Error('Copying as SQL needs the table the rows belong to.');
86
97
  }
98
+
87
99
  const head = `INSERT INTO ${qualifiedName(target)} (${columns.map((column) => quoteIdentifier(target.engine, column.name)).join(', ')}) VALUES`;
100
+
88
101
  return rows.map((row) => `${head} (${columns.map((column, index) => sqlLiteral(target.engine, row[index] ?? null, column.kind)).join(', ')});`).join('\n');
89
102
  };
90
103
 
@@ -1,5 +1,5 @@
1
1
  import { formatBytes, formatNumeral } from '@adecore/ui/format';
2
- import type { BinaryValue, Cell, EditValue, ValueKind } from '../protocol/index.ts';
2
+ import type { Cell, EditValue, ValueKind } from '../protocol/index.ts';
3
3
  import { isNumericKind } from '../sql.ts';
4
4
  import type { GridColumn } from './types.ts';
5
5
 
@@ -21,6 +21,7 @@ export type NumberNotation = 'database' | 'region';
21
21
 
22
22
  /* A cell never draws more than this, however long the text: the DOM stays small and the cell is one line anyway. */
23
23
  export const DISPLAY_LIMIT = 200;
24
+
24
25
  /* How many bytes of a binary value the cell spells out. */
25
26
  export const BINARY_PREVIEW_BYTES = 8;
26
27
 
@@ -33,9 +34,11 @@ export const isPreview = (cell: Shown): boolean => {
33
34
  if (!isObject(cell)) {
34
35
  return false;
35
36
  }
37
+
36
38
  if (cell.kind === 'longText') {
37
39
  return true;
38
40
  }
41
+
39
42
  return cell.kind === 'binary' && cell.hex.length < cell.length * 2;
40
43
  };
41
44
 
@@ -44,13 +47,16 @@ export const numeralText = (numeral: string | number, notation: NumberNotation):
44
47
  if (notation === 'database') {
45
48
  return String(numeral);
46
49
  }
50
+
47
51
  const formatted = formatNumeral(numeral);
52
+
48
53
  return formatted === String(numeral).trim() ? String(numeral) : formatted;
49
54
  };
50
55
 
51
56
  /* Identifiers are labels, even when the database stores their characters as a number. */
52
57
  export function columnNotation(column: GridColumn, notation: NumberNotation): NumberNotation {
53
58
  const identifierName = (name: string): boolean => /^(?:id|.+_id)$/i.test(name) || /(?:Id|ID)$/.test(name);
59
+
54
60
  return column.primaryKey === true ||
55
61
  column.foreignKey === true ||
56
62
  column.autoIncrement === true ||
@@ -65,18 +71,23 @@ const cut = (text: string): string => (text.length > DISPLAY_LIMIT ? `${text.sli
65
71
  export const cellView = (cell: Shown, kind: ValueKind, notation: NumberNotation = 'database'): CellView => {
66
72
  const numeric = isNumericKind(kind);
67
73
  const align = numeric ? 'end' : 'start';
74
+
68
75
  if (cell === null) {
69
76
  return { text: 'NULL', tone: 'null', align };
70
77
  }
78
+
71
79
  if (typeof cell === 'boolean') {
72
80
  return { text: cell ? 'true' : 'false', tone: 'value', align };
73
81
  }
82
+
74
83
  if (typeof cell === 'number') {
75
84
  return { text: numeric ? numeralText(cell, notation) : String(cell), tone: 'value', align };
76
85
  }
86
+
77
87
  if (typeof cell === 'string') {
78
88
  return { text: cut(numeric ? numeralText(cell, notation) : cell), tone: 'value', align };
79
89
  }
90
+
80
91
  switch (cell.kind) {
81
92
  case 'default':
82
93
  return { text: 'DEFAULT', tone: 'default', align };
@@ -85,6 +96,7 @@ export const cellView = (cell: Shown, kind: ValueKind, notation: NumberNotation
85
96
  case 'binary': {
86
97
  const shown = cell.hex.slice(0, BINARY_PREVIEW_BYTES * 2);
87
98
  const truncated = shown.length < cell.length * 2;
99
+
88
100
  return { text: `0x${shown}${truncated ? ELLIPSIS : ''}`, tone: 'value', align: 'start', suffix: formatBytes(cell.length) };
89
101
  }
90
102
  }
@@ -95,9 +107,11 @@ export const copyTextOf = (cell: Shown): string => {
95
107
  if (cell === null) {
96
108
  return 'NULL';
97
109
  }
110
+
98
111
  if (typeof cell !== 'object') {
99
112
  return String(cell);
100
113
  }
114
+
101
115
  switch (cell.kind) {
102
116
  case 'default':
103
117
  return 'DEFAULT';
@@ -111,8 +125,8 @@ export const copyTextOf = (cell: Shown): string => {
111
125
  /* An edit as a cell, so the grid draws a pending value the way it draws a loaded one. */
112
126
  export const shownOfEdit = (value: EditValue): Shown => {
113
127
  if (value !== null && typeof value === 'object' && value.kind === 'binary') {
114
- const binary = value as BinaryValue;
115
- return { kind: 'binary', hex: binary.hex, length: binary.hex.length / 2 };
128
+ return { kind: 'binary', hex: value.hex, length: value.hex.length / 2 };
116
129
  }
130
+
117
131
  return value as Shown;
118
132
  };
@@ -11,42 +11,53 @@ export const draftOf = (value: Cell | Value | { readonly kind: 'default' }): str
11
11
  if (value === null) {
12
12
  return '';
13
13
  }
14
+
14
15
  if (typeof value !== 'object') {
15
16
  return String(value);
16
17
  }
18
+
17
19
  if (value.kind === 'binary') {
18
20
  return `0x${value.hex}`;
19
21
  }
22
+
20
23
  if (value.kind === 'longText') {
21
24
  return value.preview;
22
25
  }
26
+
23
27
  return '';
24
28
  };
25
29
 
26
30
  /* What a person typed, as the value to send. A number the text spells exactly stays a number; anything else goes as text and the server decides. */
27
31
  export const parseDraft = (text: string, kind: ValueKind): EditValue => {
28
32
  const trimmed = text.trim();
33
+
29
34
  if (trimmed === '' && EMPTY_IS_NULL.has(kind)) {
30
35
  return null;
31
36
  }
37
+
32
38
  switch (kind) {
33
39
  case 'integer': {
34
40
  const number = Number(trimmed);
41
+
35
42
  return INTEGER.test(trimmed) && Number.isSafeInteger(number) ? number : trimmed;
36
43
  }
37
44
  case 'float': {
38
45
  const number = Number(trimmed);
46
+
39
47
  return Number.isFinite(number) ? number : trimmed;
40
48
  }
41
49
  case 'boolean': {
42
50
  const lower = trimmed.toLowerCase();
51
+
43
52
  if (lower === 'true' || lower === '1') {
44
53
  return true;
45
54
  }
55
+
46
56
  return lower === 'false' || lower === '0' ? false : trimmed;
47
57
  }
48
58
  case 'binary': {
49
59
  const match = HEX.exec(trimmed);
60
+
50
61
  return match !== null && match[1]!.length % 2 === 0 ? { kind: 'binary', hex: match[1]!.toLowerCase() } : trimmed;
51
62
  }
52
63
  case 'decimal':
@@ -11,44 +11,59 @@ const DECLARATION = /^(enum|set)\s*\(/i;
11
11
  */
12
12
  export const parseEnumType = (type: string): EnumType | null => {
13
13
  const head = DECLARATION.exec(type.trim());
14
+
14
15
  if (head === null) {
15
16
  return null;
16
17
  }
18
+
17
19
  const source = type.trim().slice(head[0].length);
18
20
  const values: string[] = [];
19
21
  let position = 0;
22
+
20
23
  while (position < source.length) {
21
24
  if (source[position] !== "'") {
22
25
  return null;
23
26
  }
27
+
24
28
  position++;
25
29
  let value = '';
30
+
26
31
  for (;;) {
27
32
  if (position >= source.length) {
28
33
  return null;
29
34
  }
35
+
30
36
  if (source[position] === "'") {
31
37
  if (source[position + 1] === "'") {
32
38
  value += "'";
33
39
  position += 2;
40
+
34
41
  continue;
35
42
  }
43
+
36
44
  position++;
45
+
37
46
  break;
38
47
  }
48
+
39
49
  value += source[position];
40
50
  position++;
41
51
  }
52
+
42
53
  values.push(value);
43
54
  const next = source[position];
55
+
44
56
  if (next === ')') {
45
57
  return source.slice(position + 1).trim() === '' ? tidy(head[1]!, values) : null;
46
58
  }
59
+
47
60
  if (next !== ',') {
48
61
  return null;
49
62
  }
63
+
50
64
  position++;
51
65
  }
66
+
52
67
  return null;
53
68
  };
54
69