@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
@@ -8,6 +8,7 @@ import { formatNumber } from '@adecore/ui/format';
8
8
  import { isCancelled } from '../client/types.js';
9
9
  import { useDatabaseAction, useDatabaseClient, useDatabaseFiles, useDatabaseNotice, useDatabaseStorage } from '../client-context.js';
10
10
  import { splitStatements, statementAt } from '../sql-split.js';
11
+ import { parseStoredSize } from '../stored-size.js';
11
12
  import { DestructiveDialog } from './DestructiveDialog.js';
12
13
  import { findDestructive } from './destructive.js';
13
14
  import { HistoryPanel } from './HistoryPanel.js';
@@ -28,15 +29,6 @@ export const RESULTS_HEIGHT_KEY = 'database:console-results';
28
29
  /* The results and the editor never shrink below these; the results start at half the console. */
29
30
  const MIN_RESULTS_HEIGHT = 96;
30
31
  const MIN_EDITOR_HEIGHT = 120;
31
- const parseHeight = (raw) => {
32
- try {
33
- const value = JSON.parse(raw ?? 'null');
34
- return typeof value === 'number' && Number.isFinite(value) && value >= MIN_RESULTS_HEIGHT ? Math.round(value) : null;
35
- }
36
- catch {
37
- return null;
38
- }
39
- };
40
32
  /* The app's editor as an element of its own, so it gets its props the way `SqlEditor` does and not during the console's render. */
41
33
  function AppEditor({ render, ...editor }) {
42
34
  return render(editor);
@@ -90,7 +82,7 @@ export function QueryConsole({ connection, schema, onSchemaChange, value, defaul
90
82
  const [notice, setNotice] = useState(null);
91
83
  // Closed until the first run, so the editor has the whole height before anything ran.
92
84
  const [resultsOpen, setResultsOpen] = useState(false);
93
- const [resultsHeight, setResultsHeight] = useState(() => parseHeight(storage?.get(RESULTS_HEIGHT_KEY)));
85
+ const [resultsHeight, setResultsHeight] = useState(() => parseStoredSize(storage?.get(RESULTS_HEIGHT_KEY), MIN_RESULTS_HEIGHT));
94
86
  const { startResize } = useColumnResize(resultsPane, {
95
87
  size: resultsHeight ?? MIN_RESULTS_HEIGHT,
96
88
  min: MIN_RESULTS_HEIGHT,
@@ -1,7 +1,6 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
2
  import { useTranslation } from 'react-i18next';
3
3
  import { Select } from '@adecore/ui';
4
- /* Picks the schema the console's statements run in. */
5
4
  export function SchemaPicker({ schemas, value, onValueChange, disabled, className, ref }) {
6
5
  const { t } = useTranslation('database');
7
6
  return (_jsx(Select, { ref: ref, size: "sm", label: t('console.schema'), placeholder: t('console.schemaPlaceholder'), value: value, disabled: disabled, className: className, items: schemas.map((schema) => ({ value: schema.name, label: schema.name })), onValueChange: onValueChange }));
@@ -4,13 +4,13 @@ import { ArrowUpRight, ChevronLeft, ChevronRight, CircleAlert, Download, PanelRi
4
4
  import { useTranslation } from 'react-i18next';
5
5
  import { Banner, ButtonGroup, ContextMenu, Icon, IconButton, Menu, Spinner } from '@adecore/ui';
6
6
  import { formatDecimal, formatNumber } from '@adecore/ui/format';
7
+ import { EXPORT_FORMATS } from '../export-formats.js';
7
8
  import { DataGrid } from '../grid/DataGrid.js';
8
9
  import { previewValueOf } from '../grid/focused-value.js';
9
10
  import { ValueDock } from '../grid/ValueDock.js';
10
11
  import { ValuePanel } from '../value/ValuePanel.js';
11
12
  /* What "Copy as SQL INSERT" names the table of a result that comes from no table. */
12
13
  const RESULT_TABLE = 'result';
13
- const EXPORT_FORMATS = ['csv', 'tsv', 'json', 'sql'];
14
14
  /* One statement's outcome: a result set in a read-only grid, a count of affected rows, or the error the server gave. */
15
15
  export function StatementResultView({ result, offset = 0, pager, onExport, exporting = false, engine, valuePanelOpen = false, onValuePanelOpenChange, keys = null, onFollowReference, onNotice }) {
16
16
  const { t } = useTranslation('database');
package/dist/ddl/diff.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { sameDefinition } from './column.js';
2
- const sameList = (left, right) => left.length === right.length && left.every((name, at) => name === right[at]);
2
+ import { sameList } from '../same-list.js';
3
3
  /* The positions of a longest run that already is in order: those columns can stay where they are. */
4
4
  const longestRun = (positions) => {
5
5
  const length = [];
@@ -70,7 +70,6 @@ export const hasMenu = (row) => row.kind === 'connection' ||
70
70
  export const isSelectable = (row) => row.kind === 'connection' || row.kind === 'schema' || row.kind === 'table';
71
71
  /* The statement behind New console here on a table: every row, as the engine quotes it. */
72
72
  export const selectAllSql = (engine, ref) => `SELECT * FROM ${qualifiedName({ engine, schema: ref.schema, table: ref.table })}`;
73
- /* The schemas a list shows. */
74
73
  export const visibleSchemas = (schemas, showSystemSchemas) => showSystemSchemas ? schemas : schemas.filter((schema) => !schema.system);
75
74
  /* One schema is no level worth a row: its folders hang straight under the connection. */
76
75
  const soleSchema = (visible) => (visible.length === 1 ? visible[0].name : null);
@@ -0,0 +1,2 @@
1
+ import type { FileFormat } from './protocol/index.ts';
2
+ export declare const EXPORT_FORMATS: readonly FileFormat[];
@@ -0,0 +1 @@
1
+ export const EXPORT_FORMATS = ['csv', 'tsv', 'json', 'sql'];
@@ -3,7 +3,6 @@ import { Braces, Copy, Database, FileSpreadsheet, Table2 } from 'lucide-react';
3
3
  import { useTranslation } from 'react-i18next';
4
4
  import { ContextMenu, Icon } from '@adecore/ui';
5
5
  const ICONS = { tsv: Table2, csv: FileSpreadsheet, json: Braces, sql: Database };
6
- /* The "Copy as" submenu of the grid's context menu. */
7
6
  export function CopyAsMenu({ disabled, formats, onCopy }) {
8
7
  const { t } = useTranslation('database');
9
8
  return (_jsxs(ContextMenu.SubmenuRoot, { children: [_jsxs(ContextMenu.SubmenuTrigger, { disabled: disabled, children: [_jsx(Icon, { icon: Copy, size: 14 }), t('grid.copyAs.label')] }), _jsx(ContextMenu.Popup, { children: formats.map((format) => (_jsxs(ContextMenu.Item, { onClick: () => onCopy(format), children: [_jsx(Icon, { icon: ICONS[format], size: 14 }), t(`grid.copyAs.${format}`)] }, format))) })] }));
@@ -31,7 +31,8 @@ const NO_KEYS = new Set();
31
31
  const NO_SORTS = [];
32
32
  const signatureOf = (columns) => columns.map((column) => column.name).join('\u0000');
33
33
  const textOf = (view) => (view.suffix === undefined ? view.text : `${view.text} ${view.suffix}`);
34
- const estimateWidths = (columns, rows, notation) => columns.map((column, index) => estimateColumnWidth(column.name, rows.slice(0, 50).map((row) => textOf(cellView(row.cells[index] ?? null, column.kind, columnNotation(column, notation)))), column.primaryKey === true));
34
+ const columnTexts = (column, index, rows, notation) => rows.map((row) => textOf(cellView(row.cells[index] ?? null, column.kind, columnNotation(column, notation))));
35
+ const estimateWidths = (columns, rows, notation) => columns.map((column, index) => estimateColumnWidth(column.name, columnTexts(column, index, rows.slice(0, 50), notation), column.primaryKey === true));
35
36
  /*
36
37
  * The table of the table view and of the console, and of any rows an app has on its own: it needs no
37
38
  * `DatabaseProvider`, only the `UIProvider` of `@adecore/ui`. Only the rows in view are drawn, at a fixed
@@ -289,11 +290,11 @@ export function DataGrid({ columns, rows, label, editable = false, selectedKeys,
289
290
  scroller.current?.focus();
290
291
  };
291
292
  const followCell = (cell) => onFollow !== undefined && (canFollow?.(cell) ?? true);
293
+ const menuCell = menuTarget === null || menuTarget.column === null ? null : { row: menuTarget.row, column: menuTarget.column };
292
294
  const commitValue = (value) => {
293
- const position = menuTarget === null || menuTarget.column === null ? null : { row: menuTarget.row, column: menuTarget.column };
294
- const row = position === null ? undefined : rows[position.row];
295
- if (position !== null && row !== undefined && canEdit(position)) {
296
- onCommit?.(row.key, position.column, value);
295
+ const row = menuCell === null ? undefined : rows[menuCell.row];
296
+ if (menuCell !== null && row !== undefined && canEdit(menuCell)) {
297
+ onCommit?.(row.key, menuCell.column, value);
297
298
  }
298
299
  };
299
300
  /* The cells from row `top` to `bottom` in the given columns, with the whole value of every preview `loadValue` can fetch. */
@@ -335,8 +336,8 @@ export function DataGrid({ columns, rows, label, editable = false, selectedKeys,
335
336
  }
336
337
  };
337
338
  const copyCell = () => {
338
- if (menuTarget !== null && menuTarget.column !== null && rows[menuTarget.row] !== undefined) {
339
- void copyOne(menuTarget.row, menuTarget.column);
339
+ if (menuCell !== null && rows[menuCell.row] !== undefined) {
340
+ void copyOne(menuCell.row, menuCell.column);
340
341
  }
341
342
  };
342
343
  const selectAll = () => {
@@ -448,7 +449,7 @@ export function DataGrid({ columns, rows, label, editable = false, selectedKeys,
448
449
  const fitColumn = (index) => {
449
450
  const column = columns[index];
450
451
  if (column !== undefined) {
451
- resizeColumn(index, fitColumnWidth(column.name, rows.map((row) => textOf(cellView(row.cells[index] ?? null, column.kind, columnNotation(column, notation)))), column.primaryKey === true));
452
+ resizeColumn(index, fitColumnWidth(column.name, columnTexts(column, index, rows, notation), column.primaryKey === true));
452
453
  }
453
454
  };
454
455
  const changeView = (change) => {
@@ -513,10 +514,9 @@ export function DataGrid({ columns, rows, label, editable = false, selectedKeys,
513
514
  return (_jsx(GridHeaderCell, { column: column, index: columnIndex, position: position, width: shownWidths[position], sortable: sortable, selected: pickedSet.has(columnIndex), id: headerId(columnIndex), sort: sortable ? sortStateOf(sortList, column.name) : null, multipleSorts: sortList.length > 1, hasSorts: sortList.length > 0, pinned: position < pinnedShown, lastPinned: position === pinnedShown - 1, last: position === order.length - 1, hasHidden: current.view.hidden.size > 0, canHide: order.length > 1, actions: headerActions }, columnIndex));
514
515
  });
515
516
  const menuRow = menuTarget === null ? undefined : rows[menuTarget.row];
516
- const menuCell = menuTarget === null || menuTarget.column === null ? null : { row: menuTarget.row, column: menuTarget.column };
517
517
  const settable = menuCell !== null && canEdit(menuCell);
518
- const nullable = menuCell !== null && columns[menuCell.column]?.nullable !== false;
519
518
  const menuColumn = menuCell === null ? undefined : columns[menuCell.column];
519
+ const nullable = menuColumn?.nullable !== false;
520
520
  const hasDefault = menuColumn !== undefined && ((menuColumn.defaultValue ?? null) !== null || menuColumn.autoIncrement === true);
521
521
  const menuContext = menuRow === undefined || menuTarget === null
522
522
  ? null
@@ -4,7 +4,6 @@ export interface GridLayout {
4
4
  readonly hidden: readonly string[];
5
5
  readonly pinned: readonly string[];
6
6
  }
7
- export declare const NO_GRID_LAYOUT: GridLayout;
8
7
  export interface ColumnState {
9
8
  readonly resized: Readonly<Record<number, number>>;
10
9
  readonly view: ColumnView;
@@ -1,5 +1,5 @@
1
1
  import { MAX_RESIZED_WIDTH, MIN_COLUMN_WIDTH } from './layout.js';
2
- export const NO_GRID_LAYOUT = { widths: {}, hidden: [], pinned: [] };
2
+ import { sameList } from '../same-list.js';
3
3
  /* The state of a grid for columns it has, from a layout. Names it does not have are skipped; a layout that would hide every column hides none. */
4
4
  export const restoreLayout = (columns, layout) => {
5
5
  const resized = {};
@@ -40,7 +40,6 @@ export const captureLayout = (columns, state) => {
40
40
  pinned: namesOf(state.view.pinned)
41
41
  };
42
42
  };
43
- const sameList = (a, b) => a.length === b.length && a.every((item, index) => item === b[index]);
44
43
  export const sameLayout = (a, b) => {
45
44
  const names = Object.keys(a.widths);
46
45
  return (names.length === Object.keys(b.widths).length &&
@@ -1,7 +1,8 @@
1
1
  export const NO_COLUMN_VIEW = { hidden: new Set(), pinned: [] };
2
+ const shownPins = (count, view) => view.pinned.filter((index) => index < count && !view.hidden.has(index));
2
3
  /* The columns to draw as indexes: the pinned ones first in pin order, then the rest as they come, hidden ones left out. */
3
4
  export const displayOrder = (count, view) => {
4
- const pinned = view.pinned.filter((index) => index < count && !view.hidden.has(index));
5
+ const pinned = shownPins(count, view);
5
6
  const rest = [];
6
7
  for (let i = 0; i < count; i++) {
7
8
  if (!view.hidden.has(i) && !pinned.includes(i)) {
@@ -20,4 +21,4 @@ export const togglePin = (view, index) => ({
20
21
  pinned: view.pinned.includes(index) ? view.pinned.filter((pinned) => pinned !== index) : [...view.pinned, index]
21
22
  });
22
23
  /* How many of the first columns of `displayOrder` are pinned. */
23
- export const pinnedCount = (count, view) => view.pinned.filter((index) => index < count && !view.hidden.has(index)).length;
24
+ export const pinnedCount = (count, view) => shownPins(count, view).length;
@@ -83,8 +83,7 @@ export const copyTextOf = (cell) => {
83
83
  /* An edit as a cell, so the grid draws a pending value the way it draws a loaded one. */
84
84
  export const shownOfEdit = (value) => {
85
85
  if (value !== null && typeof value === 'object' && value.kind === 'binary') {
86
- const binary = value;
87
- return { kind: 'binary', hex: binary.hex, length: binary.hex.length / 2 };
86
+ return { kind: 'binary', hex: value.hex, length: value.hex.length / 2 };
88
87
  }
89
88
  return value;
90
89
  };
package/dist/host/host.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { PROTOCOL_VERSION } from '../protocol/index.js';
2
- import { parseRequest } from './validate.js';
2
+ import { isDict, parseRequest } from './validate.js';
3
3
  const DEFAULT_READY_TIMEOUT_MS = 10000;
4
4
  /* How long a release or a dispose waits for a helper to answer its `close` requests. */
5
5
  const CLOSE_GRACE_MS = 2000;
@@ -21,7 +21,6 @@ const parseLine = (line) => {
21
21
  return undefined;
22
22
  }
23
23
  };
24
- const isDict = (value) => typeof value === 'object' && value !== null && !Array.isArray(value);
25
24
  /* Settles when every promise has, or after the grace period; a helper that hangs must not hold up an app that quits. */
26
25
  const settleWithin = async (promises) => {
27
26
  let timer;
@@ -7,4 +7,7 @@ export type ParsedRequest = {
7
7
  readonly id: string;
8
8
  readonly error: DatabaseError;
9
9
  };
10
+ type Dict = Record<string, unknown>;
11
+ export declare const isDict: (value: unknown) => value is Dict;
10
12
  export declare const parseRequest: (input: unknown) => ParsedRequest;
13
+ export {};
@@ -13,7 +13,7 @@ class InvalidRequest extends Error {
13
13
  const fail = (message) => {
14
14
  throw new InvalidRequest(message);
15
15
  };
16
- const isDict = (value) => typeof value === 'object' && value !== null && !Array.isArray(value);
16
+ export const isDict = (value) => typeof value === 'object' && value !== null && !Array.isArray(value);
17
17
  const isRequestId = (value) => typeof value === 'string' && value.length > 0 && value.length <= MAX_ID_LENGTH;
18
18
  /* Rejects a key nobody asked for, so a typo cannot pass for an option the helper ignores. */
19
19
  const shape = (value, label, required, optional = []) => {
@@ -203,6 +203,15 @@ const exportSource = (input, label) => {
203
203
  };
204
204
  const cellLimit = (params) => optional(params, 'cellLimit', 'params', (limit, name) => integer(limit, name, 1, MAX_CELL_LIMIT));
205
205
  const maxBytes = (params) => optional(params, 'maxBytes', 'params', (limit, name) => integer(limit, name, 1, MAX_BYTES));
206
+ const rowLimit = (params) => optional(params, 'limit', 'params', (limit, name) => integer(limit, name, 1, MAX_LIMIT));
207
+ const pageWindow = (params) => {
208
+ integer(params.offset, 'params.offset', 0, Number.MAX_SAFE_INTEGER);
209
+ integer(params.limit, 'params.limit', 1, MAX_LIMIT);
210
+ cellLimit(params);
211
+ maxBytes(params);
212
+ };
213
+ const connectionParams = (input) => connection(shape(input, 'params', ['connection']).connection, 'params.connection');
214
+ const sessionParams = (input) => filledText(shape(input, 'params', ['session']).session, 'params.session');
206
215
  const table = (params) => {
207
216
  filledText(params.session, 'params.session');
208
217
  filledText(params.schema, 'params.schema');
@@ -210,10 +219,10 @@ const table = (params) => {
210
219
  };
211
220
  const TARGET = ['session', 'schema', 'table'];
212
221
  const PARAMS = {
213
- open: (input) => connection(shape(input, 'params', ['connection']).connection, 'params.connection'),
214
- test: (input) => connection(shape(input, 'params', ['connection']).connection, 'params.connection'),
215
- close: (input) => filledText(shape(input, 'params', ['session']).session, 'params.session'),
216
- schemas: (input) => filledText(shape(input, 'params', ['session']).session, 'params.session'),
222
+ open: connectionParams,
223
+ test: connectionParams,
224
+ close: sessionParams,
225
+ schemas: sessionParams,
217
226
  tables: (input) => {
218
227
  const params = shape(input, 'params', ['session', 'schema']);
219
228
  filledText(params.session, 'params.session');
@@ -225,10 +234,7 @@ const PARAMS = {
225
234
  table(params);
226
235
  optional(params, 'where', 'params', anyText);
227
236
  optional(params, 'orderBy', 'params', anyText);
228
- integer(params.offset, 'params.offset', 0, Number.MAX_SAFE_INTEGER);
229
- integer(params.limit, 'params.limit', 1, MAX_LIMIT);
230
- cellLimit(params);
231
- maxBytes(params);
237
+ pageWindow(params);
232
238
  },
233
239
  count: (input) => {
234
240
  const params = shape(input, 'params', TARGET, ['where']);
@@ -255,7 +261,7 @@ const PARAMS = {
255
261
  filledText(params.session, 'params.session');
256
262
  filledText(params.sql, 'params.sql');
257
263
  optional(params, 'schema', 'params', filledText);
258
- optional(params, 'limit', 'params', (limit, name) => integer(limit, name, 1, MAX_LIMIT));
264
+ rowLimit(params);
259
265
  cellLimit(params);
260
266
  maxBytes(params);
261
267
  },
@@ -264,10 +270,7 @@ const PARAMS = {
264
270
  filledText(params.session, 'params.session');
265
271
  filledText(params.sql, 'params.sql');
266
272
  optional(params, 'schema', 'params', filledText);
267
- integer(params.offset, 'params.offset', 0, Number.MAX_SAFE_INTEGER);
268
- integer(params.limit, 'params.limit', 1, MAX_LIMIT);
269
- cellLimit(params);
270
- maxBytes(params);
273
+ pageWindow(params);
271
274
  },
272
275
  transaction: (input) => {
273
276
  const params = shape(input, 'params', ['session', 'action']);
@@ -288,7 +291,7 @@ const PARAMS = {
288
291
  absolutePath(params.path, 'params.path');
289
292
  oneOf(DELIMITED_FORMATS)(params.format, 'params.format');
290
293
  flag(params.header, 'params.header');
291
- optional(params, 'limit', 'params', (limit, name) => integer(limit, name, 1, MAX_LIMIT));
294
+ rowLimit(params);
292
295
  },
293
296
  import: (input) => {
294
297
  const params = shape(input, 'params', [...TARGET, 'path', 'format', 'header', 'columns']);
@@ -0,0 +1 @@
1
+ export declare const sameList: (left: readonly string[], right: readonly string[]) => boolean;
@@ -0,0 +1 @@
1
+ export const sameList = (left, right) => left.length === right.length && left.every((item, index) => item === right[index]);
@@ -0,0 +1 @@
1
+ export declare const parseStoredSize: (raw: string | null | undefined, minimum: number) => number | null;
@@ -0,0 +1,10 @@
1
+ /* A size in pixels a view wrote to the app's storage, or `null` when the text is missing, broken or not at least `minimum`. */
2
+ export const parseStoredSize = (raw, minimum) => {
3
+ try {
4
+ const value = JSON.parse(raw ?? 'null');
5
+ return typeof value === 'number' && Number.isFinite(value) && value >= minimum ? Math.round(value) : null;
6
+ }
7
+ catch {
8
+ return null;
9
+ }
10
+ };
@@ -4,9 +4,9 @@ import { ChevronsLeft, ChevronsRight, CopyPlus, Download, Ellipsis, FileInput, P
4
4
  import { useTranslation } from 'react-i18next';
5
5
  import { Button, ButtonGroup, Icon, IconButton, Menu, Separator } from '@adecore/ui';
6
6
  import { formatNumber } from '@adecore/ui/format';
7
+ import { EXPORT_FORMATS } from '../export-formats.js';
7
8
  import { CommandField } from './CommandField.js';
8
9
  import { PAGE_SIZES } from './paging.js';
9
- const EXPORT_FORMATS = ['csv', 'tsv', 'json', 'sql'];
10
10
  /* The command field for filtering, sorting and jumping to a column, refresh, add row, the submit of pending changes, and a menu of the rest. */
11
11
  export function TableToolbar({ start, refreshing, readOnlyReason, hasSelection, canRevertSelection, recordViewOpen = false, transfer, pendingCount, submitting, engine, chips, columns, pageSize, bounds, commandRef, onChipsChange, onJumpToColumn, onRefresh, onAddRow, onDeleteRows, onCloneRows, onRevertRows, onToggleRecordView, onPageSizeChange, onFirstPage, onLastPage, onSubmit, onRevert, className, ref }) {
12
12
  const { t } = useTranslation('database');
@@ -12,6 +12,7 @@ import { DataGrid } from '../grid/DataGrid.js';
12
12
  import { RecordView } from '../grid/RecordView.js';
13
13
  import { ValueDock } from '../grid/ValueDock.js';
14
14
  import { valueOfCell } from '../protocol/index.js';
15
+ import { parseStoredSize } from '../stored-size.js';
15
16
  import { useSchemaChange } from '../use-schema-change.js';
16
17
  import { useStableCallback } from '../use-stable-callback.js';
17
18
  import { addChip, cellChip, chipsFromQuery, chipsToOrderBy, chipsToWhere, shorten, sortsOf, toggleSort, withSorts } from './command-field.js';
@@ -30,15 +31,6 @@ import { useTableTransfer } from './useTableTransfer.js';
30
31
  /* Where the width of the record view is kept, in pixels, for every table. */
31
32
  export const RECORD_WIDTH_KEY = 'database:record-view';
32
33
  const RECORD_WIDTH = 360;
33
- const parseRecordWidth = (raw) => {
34
- try {
35
- const value = JSON.parse(raw ?? 'null');
36
- return typeof value === 'number' && Number.isFinite(value) && value > 0 ? Math.round(value) : null;
37
- }
38
- catch {
39
- return null;
40
- }
41
- };
42
34
  const NO_KEYS = new Set();
43
35
  const readOnlyReasonOf = (connection, structure, t) => {
44
36
  if (connection.config.readOnly === true) {
@@ -108,7 +100,7 @@ function TableBody({ connection, schema, table, toolbarStart, defaultWhere, defa
108
100
  const [discard, setDiscard] = useState(null);
109
101
  const [focus, setFocus] = useState(null);
110
102
  const [recordOpen, setRecordOpen] = useState(false);
111
- const [recordWidth, setRecordWidth] = useState(() => parseRecordWidth(storage?.get(RECORD_WIDTH_KEY)));
103
+ const [recordWidth, setRecordWidth] = useState(() => parseStoredSize(storage?.get(RECORD_WIDTH_KEY), Number.MIN_VALUE));
112
104
  const [focusRequest, setFocusRequest] = useState(null);
113
105
  const [columnRequest, setColumnRequest] = useState(null);
114
106
  const [range, setRange] = useState(null);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adecore/database",
3
- "version": "0.38.0",
3
+ "version": "0.40.0",
4
4
  "description": "Browse, query and edit SQLite and MySQL databases: React views for the page, a host for the backend and a native helper that talks to the server.",
5
5
  "license": "FSL-1.1-MIT",
6
6
  "author": "Bas Milius",
@@ -58,32 +58,32 @@
58
58
  "dependencies": {
59
59
  "@base-ui/react": "^1.9.0",
60
60
  "clsx": "^2.1.1",
61
- "lucide-react": "^1.47.0"
61
+ "lucide-react": "^1.61.0"
62
62
  },
63
63
  "optionalDependencies": {
64
- "@adecore/database-darwin-arm64": "0.38.0",
65
- "@adecore/database-linux-arm64": "0.38.0",
66
- "@adecore/database-linux-x64": "0.38.0",
67
- "@adecore/database-win32-x64": "0.38.0"
64
+ "@adecore/database-darwin-arm64": "0.40.0",
65
+ "@adecore/database-linux-arm64": "0.40.0",
66
+ "@adecore/database-linux-x64": "0.40.0",
67
+ "@adecore/database-win32-x64": "0.40.0"
68
68
  },
69
69
  "peerDependencies": {
70
- "@adecore/ui": "^0.38.0",
70
+ "@adecore/ui": "^0.40.0",
71
71
  "i18next": "^26.4.2",
72
72
  "react": "^19.3.0",
73
73
  "react-dom": "^19.3.0",
74
74
  "react-i18next": "^17.0.15"
75
75
  },
76
76
  "devDependencies": {
77
- "@adecore/ui": "0.38.0",
78
- "@happy-dom/global-registrator": "^20.14.5",
79
- "@types/bun": "^1.4.2",
80
- "@types/node": "^24.13.6",
77
+ "@adecore/ui": "0.40.0",
78
+ "@happy-dom/global-registrator": "^20.14.6",
79
+ "@types/bun": "^1.4.4",
80
+ "@types/node": "^24.19.2",
81
81
  "@types/react": "^19.3.0",
82
82
  "@types/react-dom": "^19.3.0",
83
- "i18next": "^26.4.2",
84
- "react": "^19.3.0",
85
- "react-dom": "^19.3.0",
86
- "react-i18next": "^17.0.15",
83
+ "i18next": "^26.5.11",
84
+ "react": "^19.3.1",
85
+ "react-dom": "^19.3.1",
86
+ "react-i18next": "^17.1.9",
87
87
  "typescript": "^7.0.2"
88
88
  }
89
89
  }
@@ -34,6 +34,7 @@ export function DatabaseProvider({ client, onAction, onNotice, storage, files, n
34
34
  const ownStorage = storage ?? outer?.storage;
35
35
  const ownFiles = files ?? outer?.files;
36
36
  const ownNotation = numberNotation ?? outer?.numberNotation ?? 'database';
37
+
37
38
  const value = useMemo(
38
39
  () =>
39
40
  ownClient === undefined
@@ -41,6 +42,7 @@ export function DatabaseProvider({ client, onAction, onNotice, storage, files, n
41
42
  : { client: ownClient, onAction: ownAction, onNotice: ownNotice, storage: ownStorage, files: ownFiles, numberNotation: ownNotation },
42
43
  [ownClient, ownAction, ownNotice, ownStorage, ownFiles, ownNotation]
43
44
  );
45
+
44
46
  if (value === null) {
45
47
  throw new Error('A DatabaseProvider needs a client, or a DatabaseProvider above it to take one from.');
46
48
  }
package/src/KeyIcon.tsx CHANGED
@@ -11,5 +11,6 @@ export function KeyIcon({ primaryKey, foreignKey, size }: ColumnKeys & { size: 1
11
11
  if (!primaryKey && !foreignKey) {
12
12
  return null;
13
13
  }
14
+
14
15
  return <Icon icon={KeyRound} size={size} className={clsx('shrink-0', primaryKey ? 'text-(--file-icon-yellow)' : 'text-(--file-icon-blue)')} />;
15
16
  }
@@ -56,11 +56,13 @@ const raceAbort = <T>(promise: Promise<T>, signal: AbortSignal | undefined): Pro
56
56
 
57
57
  if (signal.aborted) {
58
58
  promise.catch(() => {});
59
+
59
60
  return Promise.reject(cancelledError());
60
61
  }
61
62
 
62
63
  return new Promise<T>((resolve, reject) => {
63
64
  const onAbort = (): void => reject(cancelledError());
65
+
64
66
  signal.addEventListener('abort', onAbort, { once: true });
65
67
  promise.then(resolve, reject).finally(() => signal.removeEventListener('abort', onAbort));
66
68
  });
@@ -162,6 +164,7 @@ export const createDatabaseClient = (transport: DatabaseTransport, options: Data
162
164
 
163
165
  track();
164
166
  held.add(created);
167
+
165
168
  opening ??= send('open', { connection: connection.config })
166
169
  .then((result) => (opened = result))
167
170
  .finally(() => {
@@ -247,6 +250,7 @@ export const createDatabaseClient = (transport: DatabaseTransport, options: Data
247
250
  },
248
251
  async count(schema, table, where, requestOptions) {
249
252
  const params = (id: string) => (where === undefined ? { session: id, schema, table } : { session: id, schema, table, where });
253
+
250
254
  return (await request('count', params, requestOptions, true)).count;
251
255
  },
252
256
  async cell(schema, table, key, column, requestOptions) {
@@ -287,6 +291,7 @@ export const createDatabaseClient = (transport: DatabaseTransport, options: Data
287
291
  connection = next;
288
292
  }
289
293
  };
294
+
290
295
  return created;
291
296
  };
292
297
 
@@ -296,6 +301,7 @@ export const createDatabaseClient = (transport: DatabaseTransport, options: Data
296
301
  },
297
302
  async discover(kind, discoverOptions) {
298
303
  const params = discoverOptions?.context === undefined ? { kind } : { kind, context: discoverOptions.context };
304
+
299
305
  return (await send('discover', params, discoverOptions?.signal)).containers;
300
306
  },
301
307
  sample(path, format, header, requestOptions) {
@@ -304,6 +310,7 @@ export const createDatabaseClient = (transport: DatabaseTransport, options: Data
304
310
  notifySchemaChange,
305
311
  onSchemaChange(listener) {
306
312
  schemaListeners.add(listener);
313
+
307
314
  return () => {
308
315
  schemaListeners.delete(listener);
309
316
  };
@@ -313,6 +320,7 @@ export const createDatabaseClient = (transport: DatabaseTransport, options: Data
313
320
 
314
321
  if (existing !== undefined && isEqual(existing.session.connection.config, connection.config)) {
315
322
  existing.update(connection);
323
+
316
324
  return existing.session;
317
325
  }
318
326
 
@@ -320,23 +328,28 @@ export const createDatabaseClient = (transport: DatabaseTransport, options: Data
320
328
 
321
329
  const created = createSession(connection, channel);
322
330
  channelsOf(connection.id).set(channel, created);
331
+
323
332
  return created.session;
324
333
  },
325
334
  async disconnect(connectionId) {
326
335
  const existing = new Set(live.get(connectionId)?.values());
336
+
327
337
  for (const entry of held) {
328
338
  if (entry.session.connection.id === connectionId) {
329
339
  existing.add(entry);
330
340
  }
331
341
  }
342
+
332
343
  live.delete(connectionId);
333
344
  await Promise.all([...existing].map((entry) => entry.close()));
334
345
  },
335
346
  async dispose() {
336
347
  const all = new Set([...live.values()].flatMap((channels) => [...channels.values()]));
348
+
337
349
  for (const entry of held) {
338
350
  all.add(entry);
339
351
  }
352
+
340
353
  live.clear();
341
354
  await Promise.all([...all].map((entry) => entry.close()));
342
355
  }
@@ -16,9 +16,11 @@ export const DatabaseContext = createContext<DatabaseContextValue | null>(null);
16
16
 
17
17
  const useDatabaseContext = (): DatabaseContextValue => {
18
18
  const value = use(DatabaseContext);
19
+
19
20
  if (value === null) {
20
21
  throw new Error('A database view needs a DatabaseProvider above it.');
21
22
  }
23
+
22
24
  return value;
23
25
  };
24
26
 
@@ -53,6 +53,7 @@ export function ConnectionForm({ value, onValueChange, onBrowse, className, ref
53
53
  const { t } = useTranslation('database');
54
54
  const [touched, setTouched] = useState<Touched>(new Set());
55
55
  const config = value.config;
56
+
56
57
  const options: readonly SegmentedOption<Engine>[] = [
57
58
  { id: 'sqlite', label: t('engine.sqlite'), icon: ENGINE_ICONS.sqlite },
58
59
  { id: 'mysql', label: t('engine.mysql'), icon: ENGINE_ICONS.mysql }
@@ -109,9 +110,11 @@ export function ConnectionForm({ value, onValueChange, onBrowse, className, ref
109
110
  /* The sentence for a problem, once the field has something to answer for. */
110
111
  function useProblem(problem: ConfigProblem | undefined, field: string, touched: Touched, filled: boolean): string | undefined {
111
112
  const { t } = useTranslation('database');
113
+
112
114
  if (problem === undefined || !(touched.has(field) || filled)) {
113
115
  return undefined;
114
116
  }
117
+
115
118
  return t(`connections.form.problems.${problem}`);
116
119
  }
117
120
 
@@ -124,10 +127,12 @@ function SqliteFields({ config, onConfigChange, touched, touch, onBrowse }: Fiel
124
127
  const browse = async (): Promise<void> => {
125
128
  await browsing.run(async () => {
126
129
  const picked = await onBrowse?.('database');
130
+
127
131
  if (picked !== null && picked !== undefined) {
128
132
  onConfigChange({ ...config, path: picked });
129
133
  }
130
134
  });
135
+
131
136
  touch('path');
132
137
  };
133
138
 
@@ -239,6 +244,7 @@ function MysqlFields({ config, onConfigChange, touched, touch, onBrowse }: Field
239
244
  const mode = modeOf(config);
240
245
  const tunnel = config.tunnel;
241
246
  const modes: readonly SegmentedOption<ConnectMode>[] = CONNECT_MODES.map((id) => ({ id, label: t(`connections.form.mode.${id}`), icon: MODE_ICONS[id] }));
247
+
242
248
  const tlsItems: SelectItem<MysqlTlsMode>[] = TLS_MODES.map((tls) => ({
243
249
  value: tls,
244
250
  label: t(`connections.form.tls.${tls}`),
@@ -351,11 +357,13 @@ function SshFields({ config, tunnel, onConfigChange, touched, touch, onBrowse }:
351
357
  const problems = configProblems(config);
352
358
  const hostError = useProblem(problems.sshHost, 'sshHost', touched, tunnel.host !== '');
353
359
  const portError = useProblem(problems.sshPort, 'sshPort', touched, tunnel.port !== undefined);
360
+
354
361
  const update = (patch: Partial<SshTunnel>): void => onConfigChange({ ...config, tunnel: { ...tunnel, ...patch } });
355
362
 
356
363
  const browse = async (): Promise<void> => {
357
364
  await browsing.run(async () => {
358
365
  const picked = await onBrowse?.('identity');
366
+
359
367
  if (picked !== null && picked !== undefined) {
360
368
  update({ identityFile: picked });
361
369
  }
@@ -421,6 +429,7 @@ function DockerFields({ config, tunnel, onConfigChange, touched, touch }: Fields
421
429
 
422
430
  const publishedText = (container: DockerContainer, port: number): string => {
423
431
  const host = publishedPortOf(container, port);
432
+
424
433
  return host === null ? t('connections.form.docker.notPublished') : t('connections.form.docker.published', { port: host });
425
434
  };
426
435
 
@@ -429,6 +438,7 @@ function DockerFields({ config, tunnel, onConfigChange, touched, touch }: Fields
429
438
  label: container.name,
430
439
  description: containerDescription(container, publishedText(container, containerPortOf(container)))
431
440
  }));
441
+
432
442
  if (tunnel.container !== '' && picked === undefined) {
433
443
  items.unshift({ value: tunnel.container, label: tunnel.container, description: t('connections.form.docker.notRunning') });
434
444
  }
@@ -448,6 +458,7 @@ function DockerFields({ config, tunnel, onConfigChange, touched, touch }: Fields
448
458
 
449
459
  const pick = (name: string): void => {
450
460
  const container = containers.find((candidate) => candidate.name === name);
461
+
451
462
  if (container !== undefined) {
452
463
  onConfigChange(withContainer(config, container));
453
464
  }