@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
@@ -29,6 +29,7 @@ import { RecordView } from '../grid/RecordView.tsx';
29
29
  import { ValueDock } from '../grid/ValueDock.tsx';
30
30
  import { valueOfCell, type EditValue, type RowChange, type TableStructure, type Value } from '../protocol/index.ts';
31
31
  import type { SqlTarget } from '../sql.ts';
32
+ import { parseStoredSize } from '../stored-size.ts';
32
33
  import { useSchemaChange } from '../use-schema-change.ts';
33
34
  import { useStableCallback } from '../use-stable-callback.ts';
34
35
  import {
@@ -78,15 +79,6 @@ import { useTableTransfer, type TransferNotice } from './useTableTransfer.ts';
78
79
  export const RECORD_WIDTH_KEY = 'database:record-view';
79
80
  const RECORD_WIDTH = 360;
80
81
 
81
- const parseRecordWidth = (raw: string | null | undefined): number | null => {
82
- try {
83
- const value: unknown = JSON.parse(raw ?? 'null');
84
- return typeof value === 'number' && Number.isFinite(value) && value > 0 ? Math.round(value) : null;
85
- } catch {
86
- return null;
87
- }
88
- };
89
-
90
82
  export interface TableViewProps {
91
83
  connection: Connection;
92
84
  schema: string;
@@ -113,12 +105,15 @@ const readOnlyReasonOf = (connection: Connection, structure: TableStructure | nu
113
105
  if (connection.config.readOnly === true) {
114
106
  return t('table.readOnly.connection');
115
107
  }
108
+
116
109
  if (structure?.kind === 'view') {
117
110
  return t('table.readOnly.view');
118
111
  }
112
+
119
113
  if (structure !== null && structure.rowKey === null) {
120
114
  return t('table.readOnly.noKey');
121
115
  }
116
+
122
117
  return null;
123
118
  };
124
119
 
@@ -178,6 +173,7 @@ function TableBody({ connection, schema, table, toolbarStart, defaultWhere, defa
178
173
  const counter = useRef<AbortController | null>(null);
179
174
  const insertedBefore = useRef(0);
180
175
  const engine = connection.config.engine;
176
+
181
177
  const [chips, setChips] = useState<Chip[]>(() =>
182
178
  chipsFromQuery(engine, {
183
179
  where: defaultWhere ?? stored?.where ?? '',
@@ -185,6 +181,7 @@ function TableBody({ connection, schema, table, toolbarStart, defaultWhere, defa
185
181
  remembered: defaultWhere === undefined ? stored?.filters : undefined
186
182
  })
187
183
  );
184
+
188
185
  const [offset, setOffset] = useState(0);
189
186
  const [pageSize, setPageSize] = useState(stored?.pageSize ?? DEFAULT_PAGE_SIZE);
190
187
  const [pending, setPending] = useState<PendingChanges>(emptyPending);
@@ -197,7 +194,7 @@ function TableBody({ connection, schema, table, toolbarStart, defaultWhere, defa
197
194
  const [discard, setDiscard] = useState<{ run(): void } | null>(null);
198
195
  const [focus, setFocus] = useState<FocusedCell | null>(null);
199
196
  const [recordOpen, setRecordOpen] = useState(false);
200
- const [recordWidth, setRecordWidth] = useState(() => parseRecordWidth(storage?.get(RECORD_WIDTH_KEY)));
197
+ const [recordWidth, setRecordWidth] = useState(() => parseStoredSize(storage?.get(RECORD_WIDTH_KEY), Number.MIN_VALUE));
201
198
  const [focusRequest, setFocusRequest] = useState<FocusRequest | null>(null);
202
199
  const [columnRequest, setColumnRequest] = useState<ColumnRequest | null>(null);
203
200
  const [range, setRange] = useState<RangeBlock | null>(null);
@@ -205,6 +202,7 @@ function TableBody({ connection, schema, table, toolbarStart, defaultWhere, defa
205
202
  const applied = useMemo((): Filters => ({ where: chipsToWhere(chips), orderBy: chipsToOrderBy(engine, chips) }), [chips, engine]);
206
203
  const target = `${connection.id}|${schema}|${table}`;
207
204
  const structureLoad = useLoaded((signal) => session.structure(schema, table, { signal }), target);
205
+
208
206
  // The offset travels with the page it belongs to, since the page on screen lags behind the one asked for.
209
207
  const rowsLoad = useLoaded(
210
208
  async (signal) => ({
@@ -218,6 +216,7 @@ function TableBody({ connection, schema, table, toolbarStart, defaultWhere, defa
218
216
  }),
219
217
  JSON.stringify([target, applied, offset, pageSize])
220
218
  );
219
+
221
220
  const structure = structureLoad.value;
222
221
  const loaded = rowsLoad.value?.result ?? null;
223
222
  const loadedOffset = rowsLoad.value?.offset ?? 0;
@@ -226,10 +225,12 @@ function TableBody({ connection, schema, table, toolbarStart, defaultWhere, defa
226
225
  const editable = readOnlyReason === null && structure !== null && loaded !== null && !rowsLoad.loading;
227
226
 
228
227
  const columns = useMemo(() => (loaded === null ? [] : buildGridColumns(loaded, structure)), [loaded, structure]);
228
+
229
229
  const rows = useMemo(
230
230
  () => (loaded === null ? [] : buildGridRows({ structure, loaded, pending, offset: loadedOffset })),
231
231
  [loaded, structure, pending, loadedOffset]
232
232
  );
233
+
233
234
  const transfer = useTableTransfer({
234
235
  client,
235
236
  session,
@@ -257,6 +258,7 @@ function TableBody({ connection, schema, table, toolbarStart, defaultWhere, defa
257
258
  : { tone: 'success', title: transferText(found, t) }
258
259
  )
259
260
  });
261
+
260
262
  const changeCount = pendingCount(pending);
261
263
  const dirty = changeCount > 0;
262
264
  const reportDirty = useRef(onDirtyChange);
@@ -267,6 +269,7 @@ function TableBody({ connection, schema, table, toolbarStart, defaultWhere, defa
267
269
  // The record view shows the row of the focused cell, or the first row before a cell has the focus.
268
270
  const focusedIndex = focus === null ? -1 : rows.findIndex((row) => row.key === focus.rowKey);
269
271
  const recordIndex = focusedIndex >= 0 ? focusedIndex : rows.length > 0 ? 0 : -1;
272
+
270
273
  const selectionFigures = useMemo(
271
274
  () =>
272
275
  range === null || blockSize(range) < 2
@@ -281,6 +284,7 @@ function TableBody({ connection, schema, table, toolbarStart, defaultWhere, defa
281
284
  },
282
285
  [range, rows, columns]
283
286
  );
287
+
284
288
  const sqlTarget = useMemo((): SqlTarget => ({ engine, schema, table }), [engine, schema, table]);
285
289
 
286
290
  useSchemaChange(connection.id, schema, () => {
@@ -307,6 +311,7 @@ function TableBody({ connection, schema, table, toolbarStart, defaultWhere, defa
307
311
  useEffect(() => {
308
312
  const controller = new AbortController();
309
313
  lifetime.current = controller;
314
+
310
315
  return () => {
311
316
  controller.abort();
312
317
  counter.current?.abort();
@@ -317,6 +322,7 @@ function TableBody({ connection, schema, table, toolbarStart, defaultWhere, defa
317
322
  if (pending.inserts.length > insertedBefore.current && grid.current !== null) {
318
323
  grid.current.scrollTop = grid.current.scrollHeight;
319
324
  }
325
+
320
326
  insertedBefore.current = pending.inserts.length;
321
327
  }, [pending.inserts.length]);
322
328
 
@@ -350,11 +356,14 @@ function TableBody({ connection, schema, table, toolbarStart, defaultWhere, defa
350
356
  const where = chipsToWhere(next);
351
357
  const orderBy = chipsToOrderBy(engine, next);
352
358
  const filters = next.filter((chip): chip is FilterChip => chip.kind === 'filter').map(({ text, sql }) => ({ text, sql }));
359
+
353
360
  if (where === applied.where && orderBy === applied.orderBy) {
354
361
  remember({ filters });
355
362
  setChips([...next]);
363
+
356
364
  return;
357
365
  }
366
+
358
367
  guard(() => {
359
368
  remember({ where, orderBy, filters });
360
369
  counter.current?.abort();
@@ -375,11 +384,13 @@ function TableBody({ connection, schema, table, toolbarStart, defaultWhere, defa
375
384
  const row = parseRowKey(cell.rowKey);
376
385
  const column = loaded?.columns[cell.column];
377
386
  const value = loaded === null || row.kind !== 'loaded' ? undefined : valueOfCell(loaded.rows[row.index]?.[cell.column] ?? null);
387
+
378
388
  return column === undefined || value === undefined ? null : cellChip(engine, column.name, value, column.kind, exclude);
379
389
  };
380
390
 
381
391
  const sortByColumn = (cell: FocusedCell): void => {
382
392
  const name = columns[cell.column]?.name;
393
+
383
394
  if (name !== undefined) {
384
395
  applyChips(toggleSort(chips, name));
385
396
  }
@@ -387,6 +398,7 @@ function TableBody({ connection, schema, table, toolbarStart, defaultWhere, defa
387
398
 
388
399
  const jumpToColumn = (name: string): void => {
389
400
  const index = columns.findIndex((column) => column.name === name);
401
+
390
402
  if (index >= 0) {
391
403
  setRecordOpen(false);
392
404
  setColumnRequest({ column: index });
@@ -423,10 +435,13 @@ function TableBody({ connection, schema, table, toolbarStart, defaultWhere, defa
423
435
  counter.current?.abort();
424
436
  counter.current = controller;
425
437
  setCounting(true);
438
+
426
439
  try {
427
440
  const total = await session.count(schema, table, applied.where || undefined, { signal: controller.signal });
441
+
428
442
  if (!controller.signal.aborted) {
429
443
  setCounted(total);
444
+
430
445
  return total;
431
446
  }
432
447
  } catch (error) {
@@ -438,11 +453,13 @@ function TableBody({ connection, schema, table, toolbarStart, defaultWhere, defa
438
453
  setCounting(false);
439
454
  }
440
455
  }
456
+
441
457
  return null;
442
458
  };
443
459
 
444
460
  const goToLast = async (): Promise<void> => {
445
461
  const total = counted ?? (await countRows());
462
+
446
463
  if (total !== null) {
447
464
  goToPage(lastPageOffset(total, pageSize));
448
465
  }
@@ -451,9 +468,11 @@ function TableBody({ connection, schema, table, toolbarStart, defaultWhere, defa
451
468
  const commit = (rowKey: string, column: number, value: EditValue): void => {
452
469
  const name = loaded?.columns[column]?.name;
453
470
  const row = parseRowKey(rowKey);
471
+
454
472
  if (loaded === null || name === undefined) {
455
473
  return;
456
474
  }
475
+
457
476
  if (row.kind === 'inserted') {
458
477
  setPending((now) => setInsertValue(now, row.id, name, value));
459
478
  } else {
@@ -465,13 +484,16 @@ function TableBody({ connection, schema, table, toolbarStart, defaultWhere, defa
465
484
  const row = parseRowKey(rowKey);
466
485
  const name = loaded?.columns[column]?.name;
467
486
  const key = row.kind === 'loaded' && structure !== null && loaded !== null ? rowKeyOf(structure, loaded, row.index) : null;
487
+
468
488
  if (key === null || name === undefined) {
469
489
  return undefined;
470
490
  }
491
+
471
492
  try {
472
493
  return await session.cell(schema, table, key, name, { signal: lifetime.current?.signal });
473
494
  } catch (error) {
474
495
  setFailure(messageOf(error));
496
+
475
497
  return undefined;
476
498
  }
477
499
  });
@@ -479,14 +501,17 @@ function TableBody({ connection, schema, table, toolbarStart, defaultWhere, defa
479
501
  const refsOf = (keys: Iterable<string>): RowSelectionRefs => {
480
502
  const loadedRows: number[] = [];
481
503
  const inserted: number[] = [];
504
+
482
505
  for (const key of keys) {
483
506
  const row = parseRowKey(key);
507
+
484
508
  if (row.kind === 'inserted') {
485
509
  inserted.push(row.id);
486
510
  } else {
487
511
  loadedRows.push(row.index);
488
512
  }
489
513
  }
514
+
490
515
  return { loaded: loadedRows, inserted };
491
516
  };
492
517
 
@@ -494,11 +519,13 @@ function TableBody({ connection, schema, table, toolbarStart, defaultWhere, defa
494
519
  setPending((now) => {
495
520
  const refs = refsOf(keys);
496
521
  const next = refs.inserted.reduce(removeInsert, now);
522
+
497
523
  return markDeleted(
498
524
  next,
499
525
  refs.loaded.filter((index) => loaded !== null && structure !== null && rowKeyOf(structure, loaded, index) !== null)
500
526
  );
501
527
  });
528
+
502
529
  setSelected(NO_KEYS);
503
530
  };
504
531
 
@@ -520,16 +547,19 @@ function TableBody({ connection, schema, table, toolbarStart, defaultWhere, defa
520
547
  const column = columns[cell.column];
521
548
  const key = column === undefined || structure === null ? undefined : foreignKeyOf(structure.foreignKeys, column.name);
522
549
  const row = rowByKey.get(cell.rowKey);
550
+
523
551
  return key === undefined || row === undefined ? null : referenceOf(engine, key, columns, row.cells);
524
552
  };
525
553
 
526
554
  const isReferenceColumn = (column: number): boolean => {
527
555
  const name = columns[column]?.name;
556
+
528
557
  return name !== undefined && structure !== null && foreignKeyOf(structure.foreignKeys, name) !== undefined;
529
558
  };
530
559
 
531
560
  const follow = (cell: FocusedCell): void => {
532
561
  const reference = referenceAt(cell);
562
+
533
563
  if (reference !== null) {
534
564
  onAction?.({
535
565
  kind: 'open-table',
@@ -552,6 +582,7 @@ function TableBody({ connection, schema, table, toolbarStart, defaultWhere, defa
552
582
 
553
583
  const changeRecordIndex = (index: number): void => {
554
584
  const row = rows[index];
585
+
555
586
  if (row !== undefined) {
556
587
  followInGrid({ rowKey: row.key, column: focus?.column ?? 0 });
557
588
  }
@@ -560,11 +591,14 @@ function TableBody({ connection, schema, table, toolbarStart, defaultWhere, defa
560
591
  const queryMenu = (context: GridMenuContext): ReactNode => {
561
592
  const cell = context.cell;
562
593
  const name = cell === null ? undefined : columns[cell.column]?.name;
594
+
563
595
  if (cell === null || name === undefined) {
564
596
  return null;
565
597
  }
598
+
566
599
  const include = cellChipAt(cell, false);
567
600
  const exclude = cellChipAt(cell, true);
601
+
568
602
  return (
569
603
  <>
570
604
  <ContextMenu.Item disabled={include === null} onClick={() => include !== null && applyChips(addChip(chips, include))}>
@@ -591,6 +625,7 @@ function TableBody({ connection, schema, table, toolbarStart, defaultWhere, defa
591
625
 
592
626
  const gridMenu = (context: GridMenuContext): ReactNode => {
593
627
  const count = context.rowKeys.length;
628
+
594
629
  return (
595
630
  <>
596
631
  <ContextMenu.Item disabled={!editable} onClick={() => cloneRows(context.rowKeys)}>
@@ -615,9 +650,11 @@ function TableBody({ connection, schema, table, toolbarStart, defaultWhere, defa
615
650
 
616
651
  const conflictMessage = (error: DatabaseRequestError, changes: readonly RowChange[]): string => {
617
652
  const change = error.change === undefined ? undefined : changes[error.change];
653
+
618
654
  if (error.change === undefined || change === undefined || change.kind === 'insert') {
619
655
  return error.message;
620
656
  }
657
+
621
658
  return t('table.conflict', { number: formatNumber(error.change + 1), row: describeKey(change.key) });
622
659
  };
623
660
 
@@ -625,9 +662,11 @@ function TableBody({ connection, schema, table, toolbarStart, defaultWhere, defa
625
662
  if (structure === null || loaded === null) {
626
663
  return;
627
664
  }
665
+
628
666
  const changes = toRowChanges(structure, loaded, pending);
629
667
  setSubmitting(true);
630
668
  setFailure(null);
669
+
631
670
  try {
632
671
  await session.apply(schema, table, changes);
633
672
  discardPending();
@@ -834,6 +873,7 @@ interface TransferBannerProps {
834
873
 
835
874
  function TransferBanner({ notice, onCancel, onDismiss }: TransferBannerProps) {
836
875
  const { t } = useTranslation('database');
876
+
837
877
  return (
838
878
  <Banner
839
879
  icon={notice.kind === 'failed' ? CircleAlert : notice.kind === 'exporting' ? Download : Check}
@@ -56,6 +56,7 @@ const PATTERN = /^((?:NOT\s+)?(?:LIKE|ILIKE|GLOB|REGEXP|RLIKE))\s+([\s\S]+)$/i;
56
56
  const MEMBER = /^((?:NOT\s+)?IN)\s*(\([\s\S]+\))$/i;
57
57
  const BETWEEN = /^((?:NOT\s+)?BETWEEN)\s+([\s\S]+?)\s+AND\s+([\s\S]+)$/i;
58
58
  const IS_TEST = /^IS\s+(?:NOT\s+)?(?:NULL|TRUE|FALSE)$/i;
59
+
59
60
  /* A name being typed, or one that opened a quote. Whitespace after it means an operator is coming. */
60
61
  const NAME_BEING_TYPED = /^\s*(?:[\w$]*|[`"][^`"]*[`"]?)$/;
61
62
 
@@ -76,9 +77,11 @@ export const chipsToWhere = (chips: readonly Chip[]): string => {
76
77
  .filter(isFilter)
77
78
  .map((chip) => chip.sql.trim())
78
79
  .filter((sql) => sql !== '');
80
+
79
81
  if (conditions.length < 2) {
80
82
  return conditions[0] ?? '';
81
83
  }
84
+
82
85
  return conditions.map((sql) => (OR.test(sql) ? `(${sql})` : sql)).join(' AND ');
83
86
  };
84
87
 
@@ -89,6 +92,7 @@ export const chipsToOrderBy = (engine: Engine, chips: readonly Chip[]): string =
89
92
  if (chip.kind === 'sort') {
90
93
  return [orderByClause(engine, [chip])];
91
94
  }
95
+
92
96
  return chip.kind === 'order' ? [chip.text.trim()] : [];
93
97
  })
94
98
  .filter((part) => part !== '')
@@ -104,6 +108,7 @@ export const toggleSort = (chips: readonly Chip[], column: string): Chip[] => {
104
108
  if (!chips.some((chip) => isSort(chip) && chip.column === column)) {
105
109
  return [...chips, { kind: 'sort', column, direction: 'asc' }];
106
110
  }
111
+
107
112
  return chips.map((chip) => (isSort(chip) && chip.column === column ? { ...chip, direction: chip.direction === 'asc' ? 'desc' : 'asc' } : chip));
108
113
  };
109
114
 
@@ -116,6 +121,7 @@ export const removeChipAt = (chips: readonly Chip[], index: number): Chip[] => c
116
121
  /* Puts the chip where another stood, dropping it if an equal one is already elsewhere in the list. */
117
122
  export const replaceChipAt = (chips: readonly Chip[], index: number, chip: Chip): Chip[] => {
118
123
  const duplicate = chips.some((other, at) => at !== index && chipKey(other) === chipKey(chip));
124
+
119
125
  return duplicate ? removeChipAt(chips, index) : chips.map((other, at) => (at === index ? chip : other));
120
126
  };
121
127
 
@@ -124,20 +130,26 @@ const rawFilter = (text: string): FilterChip => ({ kind: 'filter', text, sql: te
124
130
  /* The WHERE of a layout or of a jump as chips: the remembered ones when they still say that text, else the text as one raw chip. */
125
131
  export const filterChipsOf = (where: string, remembered: readonly StoredFilter[] = []): Chip[] => {
126
132
  const text = where.trim();
133
+
127
134
  if (text === '') {
128
135
  return [];
129
136
  }
137
+
130
138
  const kept = remembered.map((filter): FilterChip => ({ kind: 'filter', text: filter.text, sql: filter.sql }));
139
+
131
140
  return chipsToWhere(kept) === text ? kept : [rawFilter(text)];
132
141
  };
133
142
 
134
143
  /* A plain list of columns becomes sort chips; anything else is one chip that keeps the text. */
135
144
  export const sortChipsOf = (engine: Engine, orderBy: string): Chip[] => {
136
145
  const text = orderBy.trim();
146
+
137
147
  if (text === '') {
138
148
  return [];
139
149
  }
150
+
140
151
  const sorts = parseOrderByLoose(engine, text);
152
+
141
153
  return sorts === null ? [{ kind: 'order', text }] : sorts.map(sortChip);
142
154
  };
143
155
 
@@ -156,6 +168,7 @@ export const chipsFromQuery = (engine: Engine, { where, orderBy, remembered }: C
156
168
  const isClosed = (operand: string): boolean => {
157
169
  let quote: string | null = null;
158
170
  let depth = 0;
171
+
159
172
  for (const character of operand) {
160
173
  if (quote !== null) {
161
174
  quote = character === quote ? null : quote;
@@ -165,11 +178,13 @@ const isClosed = (operand: string): boolean => {
165
178
  depth++;
166
179
  } else if (character === ')') {
167
180
  depth--;
181
+
168
182
  if (depth < 0) {
169
183
  return false;
170
184
  }
171
185
  }
172
186
  }
187
+
173
188
  return quote === null && depth === 0;
174
189
  };
175
190
 
@@ -178,20 +193,27 @@ const upper = (keyword: string): string => keyword.replace(/\s+/g, ' ').toUpperC
178
193
  /* The operator and operands of a condition in the form the chip shows, or `null` when the text does not read as one. */
179
194
  const readOperation = (rest: string): string | null => {
180
195
  const compare = COMPARE.exec(rest);
196
+
181
197
  if (compare !== null) {
182
198
  const operator = compare[1] === '==' ? '=' : compare[1] === '!=' ? '<>' : compare[1];
199
+
183
200
  return isClosed(compare[2]!) ? `${operator} ${compare[2]}` : null;
184
201
  }
202
+
185
203
  for (const form of [PATTERN, MEMBER]) {
186
204
  const found = form.exec(rest);
205
+
187
206
  if (found !== null) {
188
207
  return isClosed(found[2]!) ? `${upper(found[1]!)} ${found[2]}` : null;
189
208
  }
190
209
  }
210
+
191
211
  const between = BETWEEN.exec(rest);
212
+
192
213
  if (between !== null) {
193
214
  return isClosed(between[2]!) && isClosed(between[3]!) ? `${upper(between[1]!)} ${between[2]} AND ${between[3]}` : null;
194
215
  }
216
+
195
217
  return IS_TEST.test(rest) ? upper(rest) : null;
196
218
  };
197
219
 
@@ -201,14 +223,18 @@ const readOperation = (rest: string): string | null => {
201
223
  */
202
224
  export const parseCondition = (engine: Engine, text: string, columns: readonly string[]): FilterChip | null => {
203
225
  const match = WORD.exec(text.trim());
226
+
204
227
  if (match === null) {
205
228
  return null;
206
229
  }
230
+
207
231
  const column = findColumn(engine, match[1]!, columns);
208
232
  const operation = column === undefined ? null : readOperation(match[2]!.trim());
233
+
209
234
  if (column === undefined || operation === null) {
210
235
  return null;
211
236
  }
237
+
212
238
  return { kind: 'filter', text: `${match[1]} ${operation}`, sql: `${quoteIdentifier(engine, column)} ${operation}` };
213
239
  };
214
240
 
@@ -216,6 +242,7 @@ export const parseCondition = (engine: Engine, text: string, columns: readonly s
216
242
  export const cellChip = (engine: Engine, column: string, value: LiteralValue, kind: ValueKind | undefined, exclude: boolean): FilterChip => {
217
243
  const name = identifierText(column);
218
244
  const text = value === null ? `${name} IS ${exclude ? 'NOT ' : ''}NULL` : `${name} ${exclude ? '<>' : '='} ${sqlLiteral(engine, value, kind)}`;
245
+
219
246
  return { kind: 'filter', text, sql: exclude ? notEqualsCondition(engine, column, value, kind) : equalsCondition(engine, column, value, kind) };
220
247
  };
221
248
 
@@ -225,24 +252,30 @@ export const shorten = (text: string, limit: number): string => (text.length > l
225
252
  /* How well a name matches what is typed: 0 is the name itself, then its start, a word of it, anywhere in it; `null` is no match. */
226
253
  const rankOf = (name: string, token: string): number | null => {
227
254
  const lower = name.toLowerCase();
255
+
228
256
  if (lower === token) {
229
257
  return 0;
230
258
  }
259
+
231
260
  if (lower.startsWith(token)) {
232
261
  return 1;
233
262
  }
263
+
234
264
  if (lower.split(/[\s_.-]+/).some((word) => word.startsWith(token))) {
235
265
  return 2;
236
266
  }
267
+
237
268
  return lower.includes(token) ? 3 : null;
238
269
  };
239
270
 
240
271
  /* The columns that match a typed name, the closest first and in table order among equals. */
241
272
  export const matchColumns = (columns: readonly CommandColumn[], typed: string, limit = SUGGESTED_COLUMNS): CommandColumn[] => {
242
273
  const token = typed.trim().replace(/^[`"]/, '').replace(/[`"]$/, '').toLowerCase();
274
+
243
275
  if (token === '') {
244
276
  return [];
245
277
  }
278
+
246
279
  return columns
247
280
  .map((column, order) => ({ column, order, rank: rankOf(column.name, token) }))
248
281
  .filter((entry): entry is { column: CommandColumn; order: number; rank: number } => entry.rank !== null)
@@ -257,24 +290,31 @@ export const matchColumns = (columns: readonly CommandColumn[], typed: string, l
257
290
  */
258
291
  export const suggest = (engine: Engine, text: string, columns: readonly CommandColumn[], limit = SUGGESTED_COLUMNS): Suggestion[] => {
259
292
  const typed = text.trim();
293
+
260
294
  if (typed === '') {
261
295
  return [];
262
296
  }
297
+
263
298
  const suggestions: Suggestion[] = [];
299
+
264
300
  const condition = parseCondition(
265
301
  engine,
266
302
  typed,
267
303
  columns.map((column) => column.name)
268
304
  );
305
+
269
306
  if (condition !== null) {
270
307
  suggestions.push({ kind: 'condition', chip: condition });
271
308
  }
309
+
272
310
  if (NAME_BEING_TYPED.test(text)) {
273
311
  for (const column of matchColumns(columns, typed, limit)) {
274
312
  suggestions.push({ kind: 'filter', column }, { kind: 'sort', column }, { kind: 'jump', column });
275
313
  }
276
314
  }
315
+
277
316
  suggestions.push({ kind: 'sql', chip: rawFilter(typed) });
317
+
278
318
  return suggestions;
279
319
  };
280
320
 
@@ -15,7 +15,9 @@ export const parseRowKey = (key: string): RowRef =>
15
15
 
16
16
  export const buildGridColumns = (loaded: Pick<RowsResult, 'columns'>, structure: TableStructure | null): GridColumn[] => {
17
17
  const keys = { primaryKey: structure?.primaryKey ?? [], foreignKey: structure?.foreignKeys.flatMap((key) => key.columns) ?? [] };
18
+
18
19
  const infoOf = (name: string) => structure?.columns.find((info) => info.name === name);
20
+
19
21
  return loaded.columns.map((column) => gridColumnOf(column, infoOf(column.name), keys));
20
22
  };
21
23
 
@@ -32,14 +34,19 @@ export const buildGridRows = ({ structure, loaded, pending, offset }: GridRowsIn
32
34
  const rows = loaded.rows.map((cells, index): GridRow => {
33
35
  const edits = pending.edits[index];
34
36
  const edited = new Set<number>();
37
+
35
38
  const shown = cells.map((cell, column): Shown => {
36
39
  const edit = edits?.[loaded.columns[column]!.name];
40
+
37
41
  if (edit === undefined) {
38
42
  return cell;
39
43
  }
44
+
40
45
  edited.add(column);
46
+
41
47
  return shownOfEdit(edit);
42
48
  });
49
+
43
50
  return {
44
51
  key: loadedRowKey(index),
45
52
  number: offset + index + 1,
@@ -49,15 +56,18 @@ export const buildGridRows = ({ structure, loaded, pending, offset }: GridRowsIn
49
56
  locked: structure === null || rowKeyOf(structure, loaded, index) === null
50
57
  };
51
58
  });
59
+
52
60
  const inserted = pending.inserts.map((row): GridRow => ({
53
61
  key: insertedRowKey(row.id),
54
62
  number: null,
55
63
  cells: loaded.columns.map((column): Shown => {
56
64
  const value = row.values[column.name];
65
+
57
66
  return value === undefined ? { kind: 'default' } : shownOfEdit(value);
58
67
  }),
59
68
  state: 'inserted'
60
69
  }));
70
+
61
71
  return [...rows, ...inserted];
62
72
  };
63
73
 
@@ -67,11 +77,14 @@ export const buildGridRows = ({ structure, loaded, pending, offset }: GridRowsIn
67
77
  */
68
78
  export const cloneValues = (columns: readonly GridColumn[], cells: readonly Shown[]): Record<string, EditValue> => {
69
79
  const values: Record<string, EditValue> = {};
80
+
70
81
  columns.forEach((column, index) => {
71
82
  const value = column.autoIncrement === true || column.readOnly === true ? undefined : wholeValueOf(cells[index] ?? null);
83
+
72
84
  if (value !== undefined) {
73
85
  values[column.name] = value;
74
86
  }
75
87
  });
88
+
76
89
  return values;
77
90
  };
@@ -17,14 +17,19 @@ export const matchColumns = (fileColumns: readonly string[], tableColumns: reado
17
17
  if (!header) {
18
18
  return fileColumns.map((_, index) => tableColumns[index] ?? null);
19
19
  }
20
+
20
21
  const taken = new Set<string>();
22
+
21
23
  return fileColumns.map((name) => {
22
24
  const wanted = name.trim().toLowerCase();
23
25
  const match = tableColumns.find((column) => !taken.has(column) && column.toLowerCase() === wanted);
26
+
24
27
  if (match === undefined) {
25
28
  return null;
26
29
  }
30
+
27
31
  taken.add(match);
32
+
28
33
  return match;
29
34
  });
30
35
  };
@@ -35,16 +35,21 @@ export const parseStoredLayout = (text: string | null): StoredLayout | null => {
35
35
  if (text === null) {
36
36
  return null;
37
37
  }
38
+
38
39
  let data: unknown;
40
+
39
41
  try {
40
42
  data = JSON.parse(text);
41
43
  } catch {
42
44
  return null;
43
45
  }
46
+
44
47
  if (!isRecord(data) || data.version !== LAYOUT_VERSION) {
45
48
  return null;
46
49
  }
50
+
47
51
  const widths: Record<string, number> = {};
52
+
48
53
  if (isRecord(data.widths)) {
49
54
  for (const [name, width] of Object.entries(data.widths)) {
50
55
  if (typeof width === 'number' && Number.isFinite(width) && width > 0) {
@@ -52,7 +57,9 @@ export const parseStoredLayout = (text: string | null): StoredLayout | null => {
52
57
  }
53
58
  }
54
59
  }
60
+
55
61
  const pageSize = PAGE_SIZES.find((size) => size === data.pageSize) ?? null;
62
+
56
63
  return {
57
64
  widths,
58
65
  hidden: isStringList(data.hidden) ? data.hidden : [],
@@ -19,6 +19,7 @@ export interface PageBounds {
19
19
  export const pageBounds = (offset: number, rowCount: number, hasMore: boolean, counted: number | null): PageBounds => {
20
20
  const exact = counted !== null || !hasMore;
21
21
  const total = counted ?? (hasMore ? offset + rowCount + 1 : offset + rowCount);
22
+
22
23
  return {
23
24
  from: rowCount === 0 ? 0 : offset + 1,
24
25
  to: offset + rowCount,