@adecore/database 0.38.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
@@ -100,6 +100,7 @@ export function DatabaseExplorer({
100
100
  versions: loads.versions,
101
101
  folders: (connectionId: string) => folders?.[connectionId] ?? []
102
102
  };
103
+
103
104
  const rows = flattenTree({ ...input, filter });
104
105
  const stop = tabStop(rows, activeKey, selectedKey);
105
106
 
@@ -118,15 +119,19 @@ export function DatabaseExplorer({
118
119
 
119
120
  const setOpen = (key: string, open: boolean): void => {
120
121
  const folder = startsOpen(key);
122
+
121
123
  const toggle = (current: ReadonlySet<string>): ReadonlySet<string> => {
122
124
  const next = new Set(current);
125
+
123
126
  if (open !== folder) {
124
127
  next.add(key);
125
128
  } else {
126
129
  next.delete(key);
127
130
  }
131
+
128
132
  return next;
129
133
  };
134
+
130
135
  (folder ? setCollapsed : setExpanded)(toggle);
131
136
  };
132
137
 
@@ -138,11 +143,13 @@ export function DatabaseExplorer({
138
143
  if (value === undefined) {
139
144
  setOwn(selection);
140
145
  }
146
+
141
147
  onValueChange?.(selection);
142
148
  };
143
149
 
144
150
  const pick = (row: TreeRow): void => {
145
151
  const selection = selectionOf(row);
152
+
146
153
  if (selection !== null) {
147
154
  select(selection);
148
155
  }
@@ -152,12 +159,16 @@ export function DatabaseExplorer({
152
159
  const open = (row: TreeRow, preview = false): void => {
153
160
  if (row.kind === 'appItem') {
154
161
  row.item.onOpen?.({ preview });
162
+
155
163
  return;
156
164
  }
165
+
157
166
  const ref = openedTableOf(row);
167
+
158
168
  if (ref === null) {
159
169
  return;
160
170
  }
171
+
161
172
  pick(row);
162
173
  const tableKind = row.kind === 'table' && row.ref === ref ? row.table.kind : tableKindIn(loads.tables(ref.connectionId, ref.schema), ref.table);
163
174
  act?.({ kind: 'open-table', ref, view: 'data', ...(tableKind === undefined ? {} : { tableKind }), ...(openOnClick ? { preview } : {}) });
@@ -170,6 +181,7 @@ export function DatabaseExplorer({
170
181
  case 'entry':
171
182
  case 'appItem':
172
183
  open(row);
184
+
173
185
  break;
174
186
  case 'connection':
175
187
  case 'schema':
@@ -178,9 +190,11 @@ export function DatabaseExplorer({
178
190
  case 'appFolder':
179
191
  pick(row);
180
192
  toggle(row);
193
+
181
194
  break;
182
195
  case 'error':
183
196
  loads.reset(row.retry);
197
+
184
198
  break;
185
199
  }
186
200
  };
@@ -189,16 +203,21 @@ export function DatabaseExplorer({
189
203
  if ((e.target as HTMLElement).tagName === 'BUTTON') {
190
204
  return;
191
205
  }
206
+
192
207
  const action = navigate(rows, activeKey ?? stop, e.key);
208
+
193
209
  if (action === null) {
194
210
  return;
195
211
  }
212
+
196
213
  e.preventDefault();
214
+
197
215
  if (action.type === 'focus') {
198
216
  focusAfterRender.current = true;
199
217
  setActiveKey(action.key);
200
218
  } else if (action.type === 'activate') {
201
219
  const row = rows.find((candidate) => candidate.key === action.key);
220
+
202
221
  if (row !== undefined) {
203
222
  activate(row);
204
223
  }
@@ -212,14 +231,19 @@ export function DatabaseExplorer({
212
231
  if (pressedInPopup()) {
213
232
  return;
214
233
  }
234
+
215
235
  setActiveKey(row.key);
236
+
216
237
  if (openOnClick && (row.kind === 'table' || row.kind === 'appItem')) {
217
238
  if (count <= 1) {
218
239
  open(row, true);
219
240
  }
241
+
220
242
  return;
221
243
  }
244
+
222
245
  pick(row);
246
+
223
247
  if (row.kind !== 'table' && isExpandable(row)) {
224
248
  toggle(row);
225
249
  } else if (row.kind === 'error') {
@@ -236,6 +260,7 @@ export function DatabaseExplorer({
236
260
  const finish = (done: TableRequest, to: string | undefined): void => {
237
261
  setRequest(null);
238
262
  const { connectionId, schema, table } = done.ref;
263
+
239
264
  if (selected?.connectionId === connectionId && selected.schema === schema && selected.table === table) {
240
265
  select(to === undefined ? { connectionId, schema } : { connectionId, schema, table: to });
241
266
  }
@@ -243,6 +268,7 @@ export function DatabaseExplorer({
243
268
 
244
269
  const connectionOf = (row: TreeRow): Connection | undefined => {
245
270
  const selection = selectionOf(row);
271
+
246
272
  return selection === null ? undefined : connections.find((entry) => entry.id === selection.connectionId);
247
273
  };
248
274
 
@@ -250,14 +276,17 @@ export function DatabaseExplorer({
250
276
 
251
277
  const renderRow = (row: TreeRow, index: number): ReactNode => {
252
278
  const connection = connectionOf(row);
279
+
253
280
  const menu =
254
281
  row.kind === 'appItem' ? (
255
282
  (row.item.menu ?? null)
256
283
  ) : connection !== undefined && hasMenu(row) ? (
257
284
  <RowMenu row={row} connection={connection} onRefresh={loads.reset} onDisconnect={disconnect} onRequest={setRequest} />
258
285
  ) : null;
286
+
259
287
  const selectedRow = isSelected(row);
260
288
  const draggable = row.kind === 'table' && onTableDragStart !== undefined;
289
+
261
290
  const props = {
262
291
  level: row.level,
263
292
  selected: selectedRow,
@@ -288,7 +317,9 @@ export function DatabaseExplorer({
288
317
  }
289
318
  }
290
319
  };
320
+
291
321
  const content = <RowContent row={row} onToggle={isExpandable(row) ? () => toggle(row) : undefined} />;
322
+
292
323
  if (menu === null) {
293
324
  return (
294
325
  <Tree.Row key={row.key} {...props}>
@@ -296,6 +327,7 @@ export function DatabaseExplorer({
296
327
  </Tree.Row>
297
328
  );
298
329
  }
330
+
299
331
  return (
300
332
  <ContextMenu.Root key={row.key}>
301
333
  <ContextMenu.Trigger render={<Tree.Row {...props} />}>{content}</ContextMenu.Trigger>
@@ -339,6 +371,7 @@ export function DatabaseExplorer({
339
371
 
340
372
  function RowContent({ row, onToggle }: { row: TreeRow; onToggle?: () => void }) {
341
373
  const { t } = useTranslation('database');
374
+
342
375
  switch (row.kind) {
343
376
  case 'connection':
344
377
  return (
@@ -446,6 +479,7 @@ const EMPTY_WORDS = { schemas: 'explorer.noSchemas', tables: 'explorer.noTables'
446
479
  function SchemaCount({ shown, total }: { shown: number; total: number }) {
447
480
  const { t } = useTranslation('database');
448
481
  const counts = { shown: formatNumber(shown), total: formatNumber(total) };
482
+
449
483
  return (
450
484
  <Tooltip label={t('explorer.schemaCountHint', { count: total, ...counts })}>
451
485
  <span className="shrink-0 text-text-faint tabular-nums">{t('explorer.schemaCount', counts)}</span>
@@ -458,6 +492,7 @@ const ENTRY_ICON_SLOT = 'inline-flex size-4 shrink-0 items-center justify-center
458
492
 
459
493
  function ColumnIcon(keys: ColumnKeys) {
460
494
  const { t } = useTranslation('database');
495
+
461
496
  if (!keys.primaryKey && !keys.foreignKey) {
462
497
  return (
463
498
  <span className={ENTRY_ICON_SLOT}>
@@ -465,6 +500,7 @@ function ColumnIcon(keys: ColumnKeys) {
465
500
  </span>
466
501
  );
467
502
  }
503
+
468
504
  return (
469
505
  <Tooltip label={t(keyLabelOf(keys))}>
470
506
  <span className={ENTRY_ICON_SLOT}>
@@ -478,11 +514,13 @@ function ColumnIcon(keys: ColumnKeys) {
478
514
  function EntryContent({ row }: { row: EntryRow }) {
479
515
  const { t } = useTranslation('database');
480
516
  const { entry } = row;
517
+
481
518
  const icon = (glyph: LucideIcon, className: string): ReactNode => (
482
519
  <span className={ENTRY_ICON_SLOT}>
483
520
  <Icon icon={glyph} size={14} className={className} />
484
521
  </span>
485
522
  );
523
+
486
524
  switch (entry.type) {
487
525
  case 'key':
488
526
  return (
@@ -496,6 +534,7 @@ function EntryContent({ row }: { row: EntryRow }) {
496
534
  case 'foreignKey': {
497
535
  const { foreignKey } = entry;
498
536
  const columns = foreignKey.columns.join(', ');
537
+
499
538
  return (
500
539
  <>
501
540
  <Tree.ChevronSlot />
@@ -544,6 +583,7 @@ function EntryContent({ row }: { row: EntryRow }) {
544
583
 
545
584
  function ErrorContent({ row }: { row: ErrorRow }) {
546
585
  const { t } = useTranslation('database');
586
+
547
587
  return (
548
588
  <>
549
589
  <Tree.ChevronSlot />
@@ -47,6 +47,7 @@ export function RowMenu({ row, connection, onRefresh, onDisconnect, onRequest }:
47
47
 
48
48
  const copyDdl = async (ref: TableRef): Promise<void> => {
49
49
  const structure = await client.session(connection).structure(ref.schema, ref.table);
50
+
50
51
  if (structure.ddl !== null) {
51
52
  copyText(structure.ddl);
52
53
  }
@@ -64,6 +65,7 @@ export function RowMenu({ row, connection, onRefresh, onDisconnect, onRequest }:
64
65
 
65
66
  const tableGroups = (ref: TableRef, kind: TableKind): ReactNode[][] => {
66
67
  const request = (change: TableRequest['change']) => (): void => onRequest({ change, connection, ref, kind });
68
+
67
69
  return [
68
70
  [
69
71
  actionItem('open-data', Table, t('explorer.openData'), { kind: 'open-table', ref, view: 'data', tableKind: kind }),
@@ -32,13 +32,16 @@ export function TableDialog({ request, onClose, onDone }: TableDialogProps) {
32
32
  const dialect = dialectOf(await session.server());
33
33
  const { results } = await session.execute(statementOf(dialect, change, ref, kind, to));
34
34
  const failed = results.find((result) => result.kind === 'error');
35
+
35
36
  if (failed?.kind === 'error') {
36
37
  throw new Error(failed.error.message);
37
38
  }
39
+
38
40
  // The client tells its listeners after a `RENAME`, a `DROP` and a `TRUNCATE`, but not after a SQLite emptying, which is a `DELETE FROM`.
39
41
  if (change === 'truncate' && dialect.engine === 'sqlite') {
40
42
  client.notifySchemaChange({ connectionId: ref.connectionId, schema: ref.schema });
41
43
  }
44
+
42
45
  onDone(request, to);
43
46
  };
44
47
 
@@ -46,13 +49,16 @@ export function TableDialog({ request, onClose, onDone }: TableDialogProps) {
46
49
  if (change === 'rename') {
47
50
  if (typed === ref.table) {
48
51
  onClose();
52
+
49
53
  return;
50
54
  }
55
+
51
56
  await run(typed);
52
57
  } else if (change === 'drop') {
53
58
  if (typed !== ref.table) {
54
59
  throw new Error(t('explorer.drop.mismatch'));
55
60
  }
61
+
56
62
  await run();
57
63
  } else {
58
64
  await run();
@@ -14,15 +14,20 @@ export const expansionKey = (connectionId: string): string => `database:explorer
14
14
  /* Reads what `serializeExpansion` wrote. Anything that does not fit gives nothing open and no folder closed. */
15
15
  export const parseExpansion = (raw: string | null, connectionId: string): Expansion => {
16
16
  const none: Expansion = { expanded: new Set(), collapsed: new Set() };
17
+
17
18
  if (raw === null) {
18
19
  return none;
19
20
  }
21
+
20
22
  try {
21
23
  const parsed: unknown = JSON.parse(raw);
24
+
22
25
  if (typeof parsed !== 'object' || parsed === null || (parsed as { version?: unknown }).version !== VERSION) {
23
26
  return none;
24
27
  }
28
+
25
29
  const { expanded, collapsed } = parsed as { expanded?: unknown; collapsed?: unknown };
30
+
26
31
  const own = (keys: unknown, folders: boolean): Set<string> =>
27
32
  new Set(
28
33
  Array.isArray(keys)
@@ -32,6 +37,7 @@ export const parseExpansion = (raw: string | null, connectionId: string): Expans
32
37
  )
33
38
  : []
34
39
  );
40
+
35
41
  return { expanded: own(expanded, false), collapsed: own(collapsed, true) };
36
42
  } catch {
37
43
  return none;
@@ -41,7 +47,9 @@ export const parseExpansion = (raw: string | null, connectionId: string): Expans
41
47
  /* What a connection has open as the text to store, or `null` when everything is as it starts, which removes the key. */
42
48
  export const serializeExpansion = (connectionId: string, { expanded, collapsed }: Expansion): string | null => {
43
49
  const own = (keys: ReadonlySet<string>): string[] => [...keys].filter((key) => connectionOfKey(key) === connectionId).sort();
50
+
44
51
  const open = own(expanded);
45
52
  const closed = own(collapsed);
53
+
46
54
  return open.length === 0 && closed.length === 0 ? null : JSON.stringify({ version: VERSION, expanded: open, collapsed: closed });
47
55
  };
@@ -35,31 +35,45 @@ export interface LoadTarget {
35
35
  const SEPARATOR = '\u0000';
36
36
 
37
37
  export const connectionKey = (connectionId: string): string => `c:${connectionId}`;
38
+
38
39
  export const schemaKey = (connectionId: string, schema: string): string => `s:${connectionId}${SEPARATOR}${schema}`;
40
+
39
41
  export const folderKey = (connectionId: string, schema: string, group: TableKind): string => `f:${connectionId}${SEPARATOR}${schema}${SEPARATOR}${group}`;
42
+
40
43
  export const tableKey = (ref: TableRef): string => `t:${ref.connectionId}${SEPARATOR}${ref.schema}${SEPARATOR}${ref.table}`;
44
+
41
45
  /* The folders of a table, in the order they come under it. */
42
46
  export const PARTS = ['columns', 'keys', 'foreignKeys', 'indexes', 'checks', 'triggers'] as const;
47
+
43
48
  export type TablePart = (typeof PARTS)[number];
49
+
44
50
  export const partKey = (ref: TableRef, part: TablePart): string => `p:${ref.connectionId}${SEPARATOR}${ref.schema}${SEPARATOR}${ref.table}${SEPARATOR}${part}`;
51
+
45
52
  /* A folder of a schema, and the columns of a table, are open until the person closes them; everything else is closed until they open it. */
46
53
  export const startsOpen = (key: string): boolean => key.startsWith('f:') || (key.startsWith('p:') && key.endsWith(`${SEPARATOR}columns`));
54
+
47
55
  /* The connection a key of a connection, schema, folder or table belongs to. */
48
56
  export const connectionOfKey = (key: string): string => {
49
57
  const rest = key.slice(2);
50
58
  const end = rest.indexOf(SEPARATOR);
59
+
51
60
  return end === -1 ? rest : rest.slice(0, end);
52
61
  };
62
+
53
63
  export const columnKey = (ref: TableRef, column: string): string => `${tableKey(ref)}${SEPARATOR}${column}`;
64
+
54
65
  /* A folder of the app's own under a connection, and an item in it. */
55
66
  export const appFolderKey = (connectionId: string, folder: string): string => `x:${connectionId}${SEPARATOR}${folder}`;
67
+
56
68
  const appItemKey = (connectionId: string, folder: string, item: string): string => `${appFolderKey(connectionId, folder)}${SEPARATOR}${item}`;
69
+
57
70
  const entryKey = (ref: TableRef, part: TablePart, index: number): string => `${partKey(ref, part)}${SEPARATOR}${index}`;
58
71
 
59
72
  export const loadKey = (target: LoadTarget): string => {
60
73
  if (target.schema === undefined) {
61
74
  return connectionKey(target.connectionId);
62
75
  }
76
+
63
77
  return target.table === undefined
64
78
  ? schemaKey(target.connectionId, target.schema)
65
79
  : tableKey({ connectionId: target.connectionId, schema: target.schema, table: target.table });
@@ -142,7 +156,6 @@ export type TableEntry =
142
156
 
143
157
  export interface EntryRow extends RowBase {
144
158
  readonly kind: 'entry';
145
- /* The table the entry belongs to. */
146
159
  readonly ref: TableRef;
147
160
  readonly entry: TableEntry;
148
161
  }
@@ -262,7 +275,6 @@ export interface TreeInput {
262
275
  folders?(connectionId: string): readonly ExplorerFolder[];
263
276
  }
264
277
 
265
- /* The schemas a list shows. */
266
278
  export const visibleSchemas = (schemas: readonly SchemaInfo[], showSystemSchemas: boolean): readonly SchemaInfo[] =>
267
279
  showSystemSchemas ? schemas : schemas.filter((schema) => !schema.system);
268
280
 
@@ -299,11 +311,14 @@ const matches = (name: string, query: string): boolean => name.toLowerCase().inc
299
311
  /* What each folder of a table besides its columns holds: the primary key and the unique keys, the foreign keys, the other indexes, the checks and the triggers. */
300
312
  export const tableEntries = (structure: TableStructure): Readonly<Record<Exclude<TablePart, 'columns'>, readonly TableEntry[]>> => {
301
313
  const primaryIndex = structure.indexes.find((index) => index.primary);
314
+
302
315
  const primary: TableEntry[] =
303
316
  structure.primaryKey.length === 0 ? [] : [{ type: 'key', name: primaryIndex?.name ?? null, columns: structure.primaryKey, primary: true }];
317
+
304
318
  const unique = structure.indexes
305
319
  .filter((index) => index.unique && !index.primary)
306
320
  .map((index): TableEntry => ({ type: 'key', name: index.name, columns: index.columns, primary: false }));
321
+
307
322
  return {
308
323
  keys: [...primary, ...unique],
309
324
  foreignKeys: structure.foreignKeys.map((foreignKey) => ({ type: 'foreignKey', foreignKey })),
@@ -324,6 +339,7 @@ export const flattenTree = (input: TreeInput): TreeRow[] => {
324
339
  const columnRows = (ref: TableRef, structure: TableStructure, level: number, parent: string): Draft[] => {
325
340
  const primary = new Set(structure.primaryKey);
326
341
  const foreign = new Set(structure.foreignKeys.flatMap((foreignKey) => foreignKey.columns));
342
+
327
343
  return structure.columns.map((column): Draft => ({
328
344
  make: (place) => ({
329
345
  kind: 'column',
@@ -344,24 +360,33 @@ export const flattenTree = (input: TreeInput): TreeRow[] => {
344
360
  /* The folders a table opens into, each only when it holds something. A view has only its columns, straight under it. */
345
361
  const tableChildren = (ref: TableRef, kind: TableKind, level: number, parent: string): Draft[] => {
346
362
  const load = input.structures(ref.connectionId, ref.schema, ref.table);
363
+
347
364
  if (load.status !== 'ready') {
348
365
  return [pendingDraft(load, { connectionId: ref.connectionId, schema: ref.schema, table: ref.table }, level, parent)];
349
366
  }
367
+
350
368
  const structure = load.value;
369
+
351
370
  if (structure.columns.length === 0) {
352
371
  return [statusDraft({ kind: 'empty', of: 'columns' }, level, parent, true)];
353
372
  }
373
+
354
374
  if (kind === 'view') {
355
375
  return columnRows(ref, structure, level, parent);
356
376
  }
377
+
357
378
  const entries = tableEntries(structure);
379
+
358
380
  return PARTS.flatMap((part): Draft[] => {
359
381
  const count = part === 'columns' ? structure.columns.length : entries[part].length;
382
+
360
383
  if (count === 0) {
361
384
  return [];
362
385
  }
386
+
363
387
  const key = partKey(ref, part);
364
388
  const open = startsOpen(key) ? !input.collapsed.has(key) : input.expanded.has(key);
389
+
365
390
  const below = (): Draft[] =>
366
391
  part === 'columns'
367
392
  ? columnRows(ref, structure, level + 1, key)
@@ -378,6 +403,7 @@ export const flattenTree = (input: TreeInput): TreeRow[] => {
378
403
  }),
379
404
  below: []
380
405
  }));
406
+
381
407
  return [
382
408
  {
383
409
  make: (place) => ({ kind: 'part', key, level, parent, focusable: true, ref, part, expanded: open, count, ...place }),
@@ -390,6 +416,7 @@ export const flattenTree = (input: TreeInput): TreeRow[] => {
390
416
  const tableDraft = (ref: TableRef, table: TableInfo, level: number, parent: string): Draft => {
391
417
  const key = tableKey(ref);
392
418
  const open = input.expanded.has(key);
419
+
393
420
  return {
394
421
  make: (place) => ({ kind: 'table', key, level, parent, focusable: true, ref, table, expanded: open, ...place }),
395
422
  below: open ? settle(tableChildren(ref, table.kind, level + 1, key)) : []
@@ -399,19 +426,25 @@ export const flattenTree = (input: TreeInput): TreeRow[] => {
399
426
  /* The folders under a schema (or under a connection with only that schema), and how many tables they hold. */
400
427
  const folderDrafts = (connection: Connection, schema: string, level: number, parent: string): { drafts: Draft[]; tables: number } => {
401
428
  const load = input.tables(connection.id, schema);
429
+
402
430
  if (load.status === 'ready') {
403
431
  const shown = filtering ? load.value.filter((table) => matches(table.name, query)) : load.value;
432
+
404
433
  if (shown.length === 0 && !filtering) {
405
434
  return { drafts: [statusDraft({ kind: 'empty', of: 'tables' }, level, parent, true)], tables: 0 };
406
435
  }
436
+
407
437
  const drafts = GROUPS.flatMap((group): Draft[] => {
408
438
  const members = shown.filter((table) => table.kind === group);
439
+
409
440
  if (members.length === 0) {
410
441
  return [];
411
442
  }
443
+
412
444
  const key = folderKey(connection.id, schema, group);
413
445
  const open = filtering || !input.collapsed.has(key);
414
446
  const tables = members.map((table) => tableDraft({ connectionId: connection.id, schema, table: table.name }, table, level + 1, key));
447
+
415
448
  return [
416
449
  {
417
450
  make: (place) => ({
@@ -431,21 +464,27 @@ export const flattenTree = (input: TreeInput): TreeRow[] => {
431
464
  }
432
465
  ];
433
466
  });
467
+
434
468
  return { drafts, tables: shown.length };
435
469
  }
470
+
436
471
  if (filtering) {
437
472
  return { drafts: [], tables: 0 };
438
473
  }
474
+
439
475
  return { drafts: [pendingDraft(load, { connectionId: connection.id, schema }, level, parent)], tables: 0 };
440
476
  };
441
477
 
442
478
  const schemaDraft = (connection: Connection, schema: string, parent: string): Draft | null => {
443
479
  const key = schemaKey(connection.id, schema);
444
480
  const { drafts, tables } = folderDrafts(connection, schema, 3, key);
481
+
445
482
  if (filtering && tables === 0) {
446
483
  return null;
447
484
  }
485
+
448
486
  const open = filtering || input.expanded.has(key);
487
+
449
488
  return {
450
489
  make: (place) => ({ kind: 'schema', key, level: 2, parent, focusable: true, connection, schema, expanded: open, ...place }),
451
490
  below: open ? settle(drafts) : []
@@ -456,11 +495,14 @@ export const flattenTree = (input: TreeInput): TreeRow[] => {
456
495
  const appFolderDrafts = (connection: Connection, level: number, parent: string): Draft[] =>
457
496
  (input.folders?.(connection.id) ?? []).flatMap((folder): Draft[] => {
458
497
  const items = filtering ? folder.items.filter((item) => matches(item.label, query)) : folder.items;
498
+
459
499
  if (items.length === 0) {
460
500
  return [];
461
501
  }
502
+
462
503
  const key = appFolderKey(connection.id, folder.id);
463
504
  const open = filtering || input.expanded.has(key);
505
+
464
506
  const rows = items.map((item): Draft => ({
465
507
  make: (place) => ({
466
508
  kind: 'appItem',
@@ -474,6 +516,7 @@ export const flattenTree = (input: TreeInput): TreeRow[] => {
474
516
  }),
475
517
  below: []
476
518
  }));
519
+
477
520
  return [
478
521
  {
479
522
  make: (place) => ({
@@ -500,12 +543,15 @@ export const flattenTree = (input: TreeInput): TreeRow[] => {
500
543
  const open = filtering ? load.status === 'ready' || appFolders.length > 0 : input.expanded.has(key);
501
544
  let children: Draft[] = [];
502
545
  let schemaCount: ConnectionRow['schemaCount'] = null;
546
+
503
547
  if (load.status === 'ready') {
504
548
  const visible = visibleSchemas(load.value, input.showSystemSchemas);
505
549
  const sole = soleSchema(visible);
550
+
506
551
  if (visible.length < load.value.length) {
507
552
  schemaCount = { shown: visible.length, total: load.value.length };
508
553
  }
554
+
509
555
  if (sole !== null) {
510
556
  children = open ? folderDrafts(connection, sole, 2, key).drafts : [];
511
557
  } else if (visible.length === 0) {
@@ -516,12 +562,16 @@ export const flattenTree = (input: TreeInput): TreeRow[] => {
516
562
  } else if (open && !filtering) {
517
563
  children = [pendingDraft(load, { connectionId: connection.id }, 2, key)];
518
564
  }
565
+
519
566
  // Behind a status row too: what the app keeps of a connection does not wait on its server.
520
567
  children = [...children, ...(open ? appFolders : [])];
568
+
521
569
  if (filtering && children.length === 0) {
522
570
  return null;
523
571
  }
572
+
524
573
  const version = input.versions(connection.id);
574
+
525
575
  return {
526
576
  make: (place) => ({ kind: 'connection', key, level: 1, parent: null, focusable: true, connection, expanded: open, version, schemaCount, ...place }),
527
577
  below: open ? settle(children) : []
@@ -534,30 +584,41 @@ export const flattenTree = (input: TreeInput): TreeRow[] => {
534
584
  /* The loads an open node is waiting for and nobody has asked for yet. */
535
585
  export const neededLoads = (input: Omit<TreeInput, 'filter'>): LoadTarget[] => {
536
586
  const needed: LoadTarget[] = [];
587
+
537
588
  for (const connection of input.connections) {
538
589
  if (!input.expanded.has(connectionKey(connection.id))) {
539
590
  continue;
540
591
  }
592
+
541
593
  const load = input.schemas(connection.id);
594
+
542
595
  if (load.status === 'idle') {
543
596
  needed.push({ connectionId: connection.id });
597
+
544
598
  continue;
545
599
  }
600
+
546
601
  if (load.status !== 'ready') {
547
602
  continue;
548
603
  }
604
+
549
605
  const visible = visibleSchemas(load.value, input.showSystemSchemas);
550
606
  const sole = soleSchema(visible);
551
607
  const wanted = sole !== null ? [sole] : visible.map((schema) => schema.name).filter((name) => input.expanded.has(schemaKey(connection.id, name)));
608
+
552
609
  for (const schema of wanted) {
553
610
  const tables = input.tables(connection.id, schema);
611
+
554
612
  if (tables.status === 'idle') {
555
613
  needed.push({ connectionId: connection.id, schema });
614
+
556
615
  continue;
557
616
  }
617
+
558
618
  if (tables.status !== 'ready') {
559
619
  continue;
560
620
  }
621
+
561
622
  for (const table of tables.value) {
562
623
  if (
563
624
  input.expanded.has(tableKey({ connectionId: connection.id, schema, table: table.name })) &&
@@ -568,6 +629,7 @@ export const neededLoads = (input: Omit<TreeInput, 'filter'>): LoadTarget[] => {
568
629
  }
569
630
  }
570
631
  }
632
+
571
633
  return needed;
572
634
  };
573
635
 
@@ -581,11 +643,14 @@ export type TreeAction =
581
643
  /* What a key does to the row that has the focus, or `null` when it does nothing there. */
582
644
  export const navigate = (rows: readonly TreeRow[], activeKey: string | null, pressed: string): TreeAction | null => {
583
645
  const stops = rows.filter((row) => row.focusable);
646
+
584
647
  if (stops.length === 0) {
585
648
  return null;
586
649
  }
650
+
587
651
  const at = stops.findIndex((row) => row.key === activeKey);
588
652
  const row = at === -1 ? null : stops[at]!;
653
+
589
654
  switch (pressed) {
590
655
  case 'ArrowDown':
591
656
  return { type: 'focus', key: stops[Math.min(at + 1, stops.length - 1)]!.key };
@@ -599,19 +664,24 @@ export const navigate = (rows: readonly TreeRow[], activeKey: string | null, pre
599
664
  if (row === null || !isExpandable(row)) {
600
665
  return null;
601
666
  }
667
+
602
668
  if (!row.expanded) {
603
669
  return { type: 'expand', key: row.key };
604
670
  }
671
+
605
672
  const child = stops.find((candidate) => candidate.parent === row.key);
673
+
606
674
  return child === undefined ? null : { type: 'focus', key: child.key };
607
675
  }
608
676
  case 'ArrowLeft': {
609
677
  if (row === null) {
610
678
  return null;
611
679
  }
680
+
612
681
  if (isExpandable(row) && row.expanded) {
613
682
  return { type: 'collapse', key: row.key };
614
683
  }
684
+
615
685
  return row.parent === null ? null : { type: 'focus', key: row.parent };
616
686
  }
617
687
  case 'Enter':
@@ -629,5 +699,6 @@ export const tabStop = (rows: readonly TreeRow[], activeKey: string | null, sele
629
699
  return key;
630
700
  }
631
701
  }
702
+
632
703
  return rows.find((row) => row.focusable)?.key ?? null;
633
704
  };