@adecore/database 0.39.0 → 0.40.1

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
@@ -18,7 +18,9 @@ const isEntry = (value: unknown): value is HistoryEntry => {
18
18
  if (value === null || typeof value !== 'object') {
19
19
  return false;
20
20
  }
21
+
21
22
  const entry = value as Record<string, unknown>;
23
+
22
24
  return (
23
25
  typeof entry.sql === 'string' &&
24
26
  typeof entry.at === 'number' &&
@@ -33,8 +35,10 @@ export const parseHistory = (raw: string | null): HistoryEntry[] => {
33
35
  if (raw === null) {
34
36
  return [];
35
37
  }
38
+
36
39
  try {
37
40
  const parsed: unknown = JSON.parse(raw);
41
+
38
42
  return Array.isArray(parsed) ? parsed.filter(isEntry).slice(0, HISTORY_LIMIT) : [];
39
43
  } catch {
40
44
  return [];
@@ -46,17 +50,21 @@ export const serializeHistory = (entries: readonly HistoryEntry[]): string => JS
46
50
  /* The list with the run in front. A run of the same SQL as the latest one replaces it. */
47
51
  export const addEntry = (entries: readonly HistoryEntry[], entry: HistoryEntry): HistoryEntry[] => {
48
52
  const rest = entries[0]?.sql.trim() === entry.sql.trim() ? entries.slice(1) : entries;
53
+
49
54
  return [entry, ...rest].slice(0, HISTORY_LIMIT);
50
55
  };
51
56
 
52
57
  /* The entries whose SQL holds every word of the query, in any case. */
53
58
  export const searchHistory = (entries: readonly HistoryEntry[], query: string): readonly HistoryEntry[] => {
54
59
  const words = query.toLowerCase().split(/\s+/).filter(Boolean);
60
+
55
61
  if (words.length === 0) {
56
62
  return entries;
57
63
  }
64
+
58
65
  return entries.filter((entry) => {
59
66
  const sql = entry.sql.toLowerCase();
67
+
60
68
  return words.every((word) => sql.includes(word));
61
69
  });
62
70
  };
@@ -3,5 +3,6 @@ const LABEL_LENGTH = 32;
3
3
  /* The tab of a statement: its SQL on one line, cut where it gets long. */
4
4
  export const statementLabel = (sql: string): string => {
5
5
  const line = sql.replace(/\s+/g, ' ').trim();
6
+
6
7
  return line.length > LABEL_LENGTH ? `${line.slice(0, LABEL_LENGTH - 1).trimEnd()}…` : line;
7
8
  };
@@ -10,5 +10,6 @@ export const outcomeOf = (results: readonly StatementResult[]): { readonly ok: b
10
10
  /* Whether the helper can wrap the statement to fetch a page of it: a read that starts with `SELECT` or `WITH`. */
11
11
  export const isPageable = (sql: string, engine: Engine): boolean => {
12
12
  const keyword = firstKeyword(sql, engine);
13
+
13
14
  return keyword === 'SELECT' || keyword === 'WITH';
14
15
  };
@@ -21,11 +21,13 @@ const tableKey = ({ schema, table }: TableName): string => `${schema}\u0000${tab
21
21
  /* The tables the columns of a result come from, each once, in the order they first appear. */
22
22
  export const sourceTablesOf = (columns: readonly ResultColumn[]): TableName[] => {
23
23
  const tables = new Map<string, TableName>();
24
+
24
25
  for (const { source } of columns) {
25
26
  if (source !== undefined && !tables.has(tableKey(source))) {
26
27
  tables.set(tableKey(source), { schema: source.schema, table: source.table });
27
28
  }
28
29
  }
30
+
29
31
  return [...tables.values()];
30
32
  };
31
33
 
@@ -36,18 +38,23 @@ export const sourceTablesOf = (columns: readonly ResultColumn[]): TableName[] =>
36
38
  export const resultKeysOf = (engine: Engine, columns: readonly ResultColumn[], structures: ReadonlyMap<string, TableStructure>): ResultKeys => {
37
39
  const structureOf = (column: ResultColumn | undefined): TableStructure | undefined =>
38
40
  column?.source === undefined ? undefined : structures.get(tableKey(column.source));
41
+
39
42
  const foreignKeyAt = (column: number) => {
40
43
  const source = columns[column]?.source;
41
44
  const structure = structureOf(columns[column]);
45
+
42
46
  return source === undefined || structure === undefined ? undefined : foreignKeyOf(structure.foreignKeys, source.column);
43
47
  };
48
+
44
49
  return {
45
50
  columns: columns.map((column): GridColumn => {
46
51
  const structure = structureOf(column);
47
52
  const name = column.source?.column;
53
+
48
54
  if (structure === undefined || name === undefined) {
49
55
  return column;
50
56
  }
57
+
51
58
  return {
52
59
  ...column,
53
60
  primaryKey: structure.primaryKey.includes(name),
@@ -58,14 +65,17 @@ export const resultKeysOf = (engine: Engine, columns: readonly ResultColumn[], s
58
65
  referenceAt: (column, cells) => {
59
66
  const key = foreignKeyAt(column);
60
67
  const source = columns[column]?.source;
68
+
61
69
  if (key === undefined || source === undefined) {
62
70
  return null;
63
71
  }
72
+
64
73
  // The columns of the result as the columns of that table, so a join names each key column of its own table only.
65
74
  const asTable = columns.map((entry) => ({
66
75
  name: entry.source !== undefined && tableKey(entry.source) === tableKey(source) ? entry.source.column : '',
67
76
  kind: entry.kind
68
77
  }));
78
+
69
79
  return referenceOf(engine, key, asTable, cells);
70
80
  }
71
81
  };
@@ -82,11 +92,15 @@ export function useResultKeys(session: DatabaseSession | null, engine: Engine |
82
92
  if (session === null || result?.kind !== 'rows') {
83
93
  return undefined;
84
94
  }
95
+
85
96
  const tables = sourceTablesOf(result.columns);
97
+
86
98
  if (tables.length === 0) {
87
99
  return undefined;
88
100
  }
101
+
89
102
  const controller = new AbortController();
103
+
90
104
  // A table the server named but cannot describe, such as a derived one, marks no column.
91
105
  const loads = tables.map((table) =>
92
106
  session.structure(table.schema, table.table, { signal: controller.signal }).then(
@@ -94,11 +108,13 @@ export function useResultKeys(session: DatabaseSession | null, engine: Engine |
94
108
  () => null
95
109
  )
96
110
  );
111
+
97
112
  void Promise.all(loads).then((entries) => {
98
113
  if (!controller.signal.aborted) {
99
114
  setLoaded({ result, structures: new Map(entries.filter((entry) => entry !== null)) });
100
115
  }
101
116
  });
117
+
102
118
  return () => controller.abort();
103
119
  }, [session, result]);
104
120
 
@@ -10,8 +10,10 @@ export const runScopeOf = (event: KeyLike, apple: boolean): QueryConsoleRunScope
10
10
  if (matchesShortcut(RUN_ALL_SHORTCUT, event, apple)) {
11
11
  return 'all';
12
12
  }
13
+
13
14
  if (matchesShortcut(RUN_SHORTCUT, event, apple)) {
14
15
  return 'selection-or-statement';
15
16
  }
17
+
16
18
  return null;
17
19
  };
@@ -16,9 +16,11 @@ const withPosition = (clause: string, position: string | null): string => (posit
16
16
  export const mysqlAlterSql = (dialect: Dialect, schema: string, original: TableDraft, draft: TableDraft, diff: TableDiff): string[] => {
17
17
  const early: string[] = [];
18
18
  const late: string[] = [];
19
+
19
20
  const column = (verb: 'CHANGE' | 'MODIFY' | 'ADD', target: ColumnDraft, previous: string | null, position: string | null): string => {
20
21
  const spec = columnSpecSql(dialect, target);
21
22
  const named = verb === 'CHANGE' ? `${quote(dialect, previous!)} ${quote(dialect, target.name)}` : quote(dialect, target.name);
23
+
22
24
  return withPosition(`${verb} COLUMN ${named} ${spec}`, position);
23
25
  };
24
26
 
@@ -29,25 +31,31 @@ export const mysqlAlterSql = (dialect: Dialect, schema: string, original: TableD
29
31
  for (const foreignKey of diff.droppedForeignKeys) {
30
32
  early.push(`DROP FOREIGN KEY ${quote(dialect, foreignKey.name)}`);
31
33
  }
34
+
32
35
  for (const index of diff.droppedIndexes) {
33
36
  early.push(`DROP INDEX ${quote(dialect, index.name)}`);
34
37
  }
38
+
35
39
  for (const { from, to } of diff.renamedIndexes) {
36
40
  early.push(`RENAME INDEX ${quote(dialect, from.name)} TO ${quote(dialect, to.name)}`);
37
41
  }
42
+
38
43
  if (diff.primaryKeyChanged && original.primaryKey.length > 0) {
39
44
  early.push('DROP PRIMARY KEY');
40
45
  }
46
+
41
47
  for (const dropped of diff.droppedColumns) {
42
48
  early.push(`DROP COLUMN ${quote(dialect, dropped.name)}`);
43
49
  }
44
50
 
45
51
  for (const kept of diff.keptColumns) {
46
52
  const verb = kept.renamed ? 'CHANGE' : 'MODIFY';
53
+
47
54
  if (split) {
48
55
  if (kept.renamed || kept.redefined) {
49
56
  early.push(column(verb, kept.column, kept.original.name, null));
50
57
  }
58
+
51
59
  if (kept.moved) {
52
60
  late.push(column('MODIFY', kept.column, null, positionSql(dialect, kept.after)));
53
61
  }
@@ -55,6 +63,7 @@ export const mysqlAlterSql = (dialect: Dialect, schema: string, original: TableD
55
63
  early.push(column(verb, kept.column, kept.original.name, kept.moved ? positionSql(dialect, kept.after) : null));
56
64
  }
57
65
  }
66
+
58
67
  for (const added of diff.addedColumns) {
59
68
  late.push(column('ADD', added.column, null, added.placed ? positionSql(dialect, added.after) : null));
60
69
  }
@@ -62,21 +71,28 @@ export const mysqlAlterSql = (dialect: Dialect, schema: string, original: TableD
62
71
  if (diff.primaryKeyChanged && draft.primaryKey.length > 0) {
63
72
  late.push(`ADD PRIMARY KEY (${columnList(dialect, draft.primaryKey)})`);
64
73
  }
74
+
65
75
  for (const index of diff.addedIndexes) {
66
76
  late.push(`ADD ${inlineIndexSql(dialect, index)}`);
67
77
  }
78
+
68
79
  for (const foreignKey of diff.addedForeignKeys) {
69
80
  late.push(`ADD ${foreignKeySql(dialect, schema, foreignKey)}`);
70
81
  }
82
+
71
83
  const options = diff.optionsChanged ? mysqlOptionsSql(draft.options, original.options).join(' ') : '';
84
+
72
85
  if (options !== '') {
73
86
  late.push(options);
74
87
  }
88
+
75
89
  if (diff.renamed) {
76
90
  late.push(`RENAME TO ${quote(dialect, draft.name)}`);
77
91
  }
78
92
 
79
93
  const target = tableName(dialect, schema, original.name);
94
+
80
95
  const statement = (clauses: readonly string[]): string[] => (clauses.length === 0 ? [] : [`ALTER TABLE ${target}\n ${clauses.join(',\n ')}`]);
96
+
81
97
  return split ? [...statement(early), ...statement(late)] : statement([...early, ...late]);
82
98
  };
@@ -13,12 +13,16 @@ const addable = (column: ColumnDraft): boolean => {
13
13
  if (column.autoIncrement || (column.generated && column.generatedClause !== null && /\bSTORED\b/i.test(column.generatedClause))) {
14
14
  return false;
15
15
  }
16
+
16
17
  const expression = column.defaultValue?.trim() ?? '';
18
+
17
19
  if (expression === '') {
18
20
  return column.nullable;
19
21
  }
22
+
20
23
  // Only a constant may fill existing rows: not an expression, not the clock.
21
24
  const clause = defaultClause(expression);
25
+
22
26
  return !clause.startsWith('(') && !/^CURRENT_|^LOCALTIME|^NOW/i.test(clause) && (column.nullable || !/^NULL$/i.test(clause));
23
27
  };
24
28
 
@@ -26,19 +30,25 @@ const needsRebuild = (dialect: Dialect, original: TableDraft, diff: TableDiff):
26
30
  if (diff.optionsChanged || diff.primaryKeyChanged || diff.droppedForeignKeys.length > 0 || diff.addedForeignKeys.length > 0) {
27
31
  return true;
28
32
  }
33
+
29
34
  if (diff.keptColumns.some((kept) => kept.redefined || kept.moved || (kept.renamed && !atLeast(dialect, ...RENAME_COLUMN)))) {
30
35
  return true;
31
36
  }
37
+
32
38
  if (diff.addedColumns.some((added) => added.placed || !addable(added.column))) {
33
39
  return true;
34
40
  }
41
+
35
42
  if (diff.droppedColumns.length > 0 && !atLeast(dialect, ...DROP_COLUMN)) {
36
43
  return true;
37
44
  }
45
+
38
46
  const autoIndexed = new Set(original.indexes.filter((index) => isAutoIndex(index.name)).flatMap((index) => index.columns));
47
+
39
48
  if (diff.droppedColumns.some((dropped) => autoIndexed.has(dropped.name))) {
40
49
  return true;
41
50
  }
51
+
42
52
  return [...diff.droppedIndexes, ...diff.renamedIndexes.map(({ from }) => from)].some((index) => isAutoIndex(index.name));
43
53
  };
44
54
 
@@ -48,11 +58,13 @@ const rebuildSql = (dialect: Dialect, schema: string, original: TableDraft, draf
48
58
  const { table, indexes } = createTableParts(dialect, schema, draft, temporary);
49
59
  const copied = diff.keptColumns.filter((kept) => !kept.column.generated);
50
60
  const statements = ['PRAGMA foreign_keys=OFF', 'BEGIN', table];
61
+
51
62
  if (copied.length > 0) {
52
63
  const into = copied.map((kept) => quote(dialect, kept.column.name)).join(', ');
53
64
  const from = copied.map((kept) => quote(dialect, kept.original.name)).join(', ');
54
65
  statements.push(`INSERT INTO ${tableName(dialect, schema, temporary)} (${into}) SELECT ${from} FROM ${tableName(dialect, schema, original.name)}`);
55
66
  }
67
+
56
68
  statements.push(
57
69
  `DROP TABLE ${tableName(dialect, schema, original.name)}`,
58
70
  `ALTER TABLE ${tableName(dialect, schema, temporary)} RENAME TO ${quote(dialect, draft.name)}`,
@@ -61,6 +73,7 @@ const rebuildSql = (dialect: Dialect, schema: string, original: TableDraft, draf
61
73
  'COMMIT',
62
74
  'PRAGMA foreign_keys=ON'
63
75
  );
76
+
64
77
  return statements;
65
78
  };
66
79
 
@@ -69,25 +82,33 @@ export const sqliteAlterSql = (dialect: Dialect, schema: string, original: Table
69
82
  if (needsRebuild(dialect, original, diff)) {
70
83
  return rebuildSql(dialect, schema, original, draft, diff);
71
84
  }
85
+
72
86
  const target = tableName(dialect, schema, original.name);
73
87
  const statements: string[] = [];
88
+
74
89
  for (const index of [...diff.droppedIndexes, ...diff.renamedIndexes.map(({ from }) => from)]) {
75
90
  statements.push(dropIndexSql(dialect, schema, original.name, index.name));
76
91
  }
92
+
77
93
  for (const dropped of diff.droppedColumns) {
78
94
  statements.push(`ALTER TABLE ${target} DROP COLUMN ${quote(dialect, dropped.name)}`);
79
95
  }
96
+
80
97
  for (const kept of diff.keptColumns.filter((candidate) => candidate.renamed)) {
81
98
  statements.push(`ALTER TABLE ${target} RENAME COLUMN ${quote(dialect, kept.original.name)} TO ${quote(dialect, kept.column.name)}`);
82
99
  }
100
+
83
101
  for (const added of diff.addedColumns) {
84
102
  statements.push(`ALTER TABLE ${target} ADD COLUMN ${columnSql(dialect, added.column)}`);
85
103
  }
104
+
86
105
  for (const index of [...diff.addedIndexes, ...diff.renamedIndexes.map(({ to }) => to)]) {
87
106
  statements.push(createIndexSql(dialect, schema, original.name, index));
88
107
  }
108
+
89
109
  if (diff.renamed) {
90
110
  statements.push(`ALTER TABLE ${target} RENAME TO ${quote(dialect, draft.name)}`);
91
111
  }
112
+
92
113
  return statements;
93
114
  };
package/src/ddl/alter.ts CHANGED
@@ -22,8 +22,10 @@ const unchanged = (diff: TableDiff): boolean =>
22
22
  export const alterTableSql = (dialect: Dialect, schema: string, structure: TableStructure, draft: TableDraft): string[] => {
23
23
  const original = draftOf(structure);
24
24
  const diff = diffOf(dialect, original, draft);
25
+
25
26
  if (unchanged(diff)) {
26
27
  return [];
27
28
  }
29
+
28
30
  return dialect.engine === 'sqlite' ? sqliteAlterSql(dialect, schema, original, draft, diff) : mysqlAlterSql(dialect, schema, original, draft, diff);
29
31
  };
package/src/ddl/column.ts CHANGED
@@ -11,9 +11,11 @@ const KEYWORD = /^(?:NULL|TRUE|FALSE|CURRENT_DATE|CURRENT_TIME(?:\(\d*\))?|CURRE
11
11
  */
12
12
  export const defaultClause = (expression: string): string => {
13
13
  const text = expression.trim();
14
+
14
15
  if (NUMERIC_TEXT.test(text) || STRING.test(text) || KEYWORD.test(text)) {
15
16
  return text;
16
17
  }
18
+
17
19
  return text.startsWith('(') && text.endsWith(')') ? text : `(${text})`;
18
20
  };
19
21
 
@@ -26,34 +28,43 @@ export interface ColumnSqlOptions {
26
28
  export const columnSpecSql = (dialect: Dialect, column: ColumnDraft, { inlinePrimaryKey = false }: ColumnSqlOptions = {}): string => {
27
29
  const mysql = dialect.engine === 'mysql';
28
30
  const parts: string[] = [];
31
+
29
32
  if (column.type !== '') {
30
33
  parts.push(column.type);
31
34
  }
35
+
32
36
  if (column.generated && column.generatedClause !== null) {
33
37
  parts.push(column.generatedClause);
34
38
  }
39
+
35
40
  if (inlinePrimaryKey) {
36
41
  parts.push(column.autoIncrement ? 'PRIMARY KEY AUTOINCREMENT' : 'PRIMARY KEY');
37
42
  }
43
+
38
44
  // MariaDB takes neither NULL nor NOT NULL after the expression of a computed column.
39
45
  const statesNullability = !(column.generated && dialect.flavor === 'mariadb');
46
+
40
47
  if (statesNullability && !column.nullable) {
41
48
  parts.push('NOT NULL');
42
49
  } else if (statesNullability && mysql) {
43
50
  // MySQL gives a TIMESTAMP column NOT NULL unless it is told otherwise.
44
51
  parts.push('NULL');
45
52
  }
53
+
46
54
  if (!column.generated) {
47
55
  if (column.defaultValue !== null && column.defaultValue.trim() !== '') {
48
56
  parts.push(`DEFAULT ${defaultClause(column.defaultValue)}`);
49
57
  }
58
+
50
59
  if (mysql && column.autoIncrement) {
51
60
  parts.push('AUTO_INCREMENT');
52
61
  }
53
62
  }
63
+
54
64
  if (mysql && column.comment !== '') {
55
65
  parts.push(`COMMENT ${sqlLiteral('mysql', column.comment)}`);
56
66
  }
67
+
57
68
  return parts.join(' ');
58
69
  };
59
70
 
package/src/ddl/create.ts CHANGED
@@ -9,22 +9,27 @@ const INDENT = ' ';
9
9
  /* `CONSTRAINT name FOREIGN KEY (...) REFERENCES t (...) ON UPDATE .. ON DELETE ..`, shared by a create and an `ADD`. */
10
10
  export const foreignKeySql = (dialect: Dialect, schema: string, foreignKey: ForeignKeyDraft): string => {
11
11
  const sqlite = dialect.engine === 'sqlite';
12
+
12
13
  // SQLite cannot name another schema in a reference, and MySQL needs one only for another database.
13
14
  const target =
14
15
  sqlite || foreignKey.referencedSchema === '' || foreignKey.referencedSchema === schema
15
16
  ? quote(dialect, foreignKey.referencedTable)
16
17
  : `${quote(dialect, foreignKey.referencedSchema)}.${quote(dialect, foreignKey.referencedTable)}`;
18
+
17
19
  const parts = [
18
20
  ...(foreignKey.name === '' ? [] : [`CONSTRAINT ${quote(dialect, foreignKey.name)}`]),
19
21
  `FOREIGN KEY (${columnList(dialect, foreignKey.columns)})`,
20
22
  `REFERENCES ${target} (${columnList(dialect, foreignKey.referencedColumns)})`
21
23
  ];
24
+
22
25
  if (foreignKey.onUpdate !== null) {
23
26
  parts.push(`ON UPDATE ${foreignKey.onUpdate}`);
24
27
  }
28
+
25
29
  if (foreignKey.onDelete !== null) {
26
30
  parts.push(`ON DELETE ${foreignKey.onDelete}`);
27
31
  }
32
+
28
33
  return parts.join(' ');
29
34
  };
30
35
 
@@ -33,6 +38,7 @@ export const mysqlOptionsSql = (options: TableOptions, against?: TableOptions):
33
38
  // Only a comment can be emptied; an engine, a character set or a collation has no empty value to ask for.
34
39
  const changed = (field: 'engine' | 'charset' | 'collation' | 'comment'): boolean =>
35
40
  options[field] !== against?.[field] && (options[field] !== '' || (field === 'comment' && against !== undefined));
41
+
36
42
  return [
37
43
  ...(changed('engine') ? [`ENGINE=${options.engine}`] : []),
38
44
  ...(changed('charset') ? [`DEFAULT CHARSET=${options.charset}`] : []),
@@ -59,16 +65,21 @@ export interface CreateParts {
59
65
  /* The `CREATE TABLE` of a draft under a name of the caller's choosing, which a rebuild uses for the table it fills before the swap. */
60
66
  export const createTableParts = (dialect: Dialect, schema: string, draft: TableDraft, name: string): CreateParts => {
61
67
  const sqlite = dialect.engine === 'sqlite';
68
+
62
69
  // SQLite declares AUTOINCREMENT on the column itself, which then is the whole primary key; a WITHOUT ROWID table has no rowid to count.
63
70
  const autoColumn =
64
71
  sqlite && draft.primaryKey.length === 1 && !draft.options.withoutRowid
65
72
  ? draft.columns.find((column) => column.name === draft.primaryKey[0] && column.autoIncrement)?.name
66
73
  : undefined;
74
+
67
75
  const lines = draft.columns.map((column) => columnSql(dialect, column, { inlinePrimaryKey: column.name === autoColumn }));
76
+
68
77
  if (draft.primaryKey.length > 0 && autoColumn === undefined) {
69
78
  lines.push(`PRIMARY KEY (${columnList(dialect, draft.primaryKey)})`);
70
79
  }
80
+
71
81
  const separate: IndexDraft[] = [];
82
+
72
83
  for (const index of draft.indexes) {
73
84
  if (!sqlite) {
74
85
  lines.push(inlineIndexSql(dialect, index));
@@ -78,15 +89,19 @@ export const createTableParts = (dialect: Dialect, schema: string, draft: TableD
78
89
  separate.push(index);
79
90
  }
80
91
  }
92
+
81
93
  for (const foreignKey of draft.foreignKeys) {
82
94
  lines.push(foreignKeySql(dialect, schema, foreignKey));
83
95
  }
96
+
84
97
  const options = sqlite ? sqliteOptionsSql(draft.options).join(', ') : mysqlOptionsSql(draft.options).join(' ');
85
98
  const head = `CREATE TABLE ${tableName(dialect, schema, name)} (\n${lines.map((line) => INDENT + line).join(',\n')}\n)`;
99
+
86
100
  return { table: options === '' ? head : `${head} ${options}`, indexes: separate };
87
101
  };
88
102
 
89
103
  export const createTableSql = (dialect: Dialect, schema: string, draft: TableDraft): string[] => {
90
104
  const { table, indexes } = createTableParts(dialect, schema, draft, draft.name);
105
+
91
106
  return [table, ...indexes.map((index) => createIndexSql(dialect, schema, draft.name, index))];
92
107
  };
@@ -25,12 +25,15 @@ export const atLeast = (dialect: Dialect, ...wanted: number[]): boolean => {
25
25
  if (dialect.version.length === 0) {
26
26
  return true;
27
27
  }
28
+
28
29
  for (let i = 0; i < wanted.length; i++) {
29
30
  const have = dialect.version[i] ?? 0;
31
+
30
32
  if (have !== wanted[i]) {
31
33
  return have > wanted[i]!;
32
34
  }
33
35
  }
36
+
34
37
  return true;
35
38
  };
36
39
 
@@ -47,6 +50,7 @@ export const tableName = (dialect: Dialect, schema: string, table: string): stri
47
50
  /* SQLite puts the schema in front of the name of an index, where MySQL names the table it is on. */
48
51
  export const sqliteSchemaPrefix = (dialect: Dialect, schema: string): string => {
49
52
  const named = schemaOf(dialect, schema);
53
+
50
54
  return named === undefined ? '' : `${quote(dialect, named)}.`;
51
55
  };
52
56
 
package/src/ddl/diff.ts CHANGED
@@ -2,6 +2,7 @@ import { sameDefinition } from './column.ts';
2
2
  import type { Dialect } from './dialect.ts';
3
3
  import type { ColumnDraft, ForeignKeyDraft, IndexDraft, TableDraft } from './draft.ts';
4
4
  import type { TableOptions } from './parse.ts';
5
+ import { sameList } from '../same-list.ts';
5
6
 
6
7
  export interface KeptColumn {
7
8
  readonly original: ColumnDraft;
@@ -41,30 +42,34 @@ export interface TableDiff {
41
42
  readonly optionsChanged: boolean;
42
43
  }
43
44
 
44
- const sameList = (left: readonly string[], right: readonly string[]): boolean => left.length === right.length && left.every((name, at) => name === right[at]);
45
-
46
45
  /* The positions of a longest run that already is in order: those columns can stay where they are. */
47
46
  const longestRun = (positions: readonly number[]): ReadonlySet<number> => {
48
47
  const length: number[] = [];
49
48
  const previous: number[] = [];
50
49
  let best = -1;
50
+
51
51
  for (let i = 0; i < positions.length; i++) {
52
52
  length[i] = 1;
53
53
  previous[i] = -1;
54
+
54
55
  for (let before = 0; before < i; before++) {
55
56
  if (positions[before]! < positions[i]! && length[before]! + 1 > length[i]!) {
56
57
  length[i] = length[before]! + 1;
57
58
  previous[i] = before;
58
59
  }
59
60
  }
61
+
60
62
  if (best < 0 || length[i]! > length[best]!) {
61
63
  best = i;
62
64
  }
63
65
  }
66
+
64
67
  const run = new Set<number>();
68
+
65
69
  for (let at = best; at >= 0; at = previous[at]!) {
66
70
  run.add(at);
67
71
  }
72
+
68
73
  return run;
69
74
  };
70
75
 
@@ -82,17 +87,23 @@ const columnChanges = (dialect: Dialect, original: TableDraft, draft: TableDraft
82
87
  let survivor = 0;
83
88
  const keptColumns: KeptColumn[] = [];
84
89
  const addedColumns: AddedColumn[] = [];
90
+
85
91
  draft.columns.forEach((column, at) => {
86
92
  const after = at === 0 ? null : draft.columns[at - 1]!.name;
93
+
87
94
  if (column.originalName === null) {
88
95
  addedColumns.push({ column, placed: draft.columns.slice(at + 1).some((later) => later.originalName !== null), after });
96
+
89
97
  return;
90
98
  }
99
+
91
100
  const rank = survivor++;
92
101
  const before = original.columns.find((candidate) => candidate.name === column.originalName);
102
+
93
103
  if (before === undefined) {
94
104
  return;
95
105
  }
106
+
96
107
  keptColumns.push({
97
108
  original: before,
98
109
  column,
@@ -104,6 +115,7 @@ const columnChanges = (dialect: Dialect, original: TableDraft, draft: TableDraft
104
115
  });
105
116
 
106
117
  const droppedColumns = original.columns.filter((column) => !survivors.some((kept) => kept.originalName === column.name));
118
+
107
119
  return { droppedColumns, keptColumns, addedColumns };
108
120
  };
109
121
 
@@ -111,8 +123,10 @@ const indexChanges = (original: TableDraft, draft: TableDraft, project: Project)
111
123
  const droppedIndexes: IndexDraft[] = [];
112
124
  const addedIndexes = draft.indexes.filter((index) => index.originalName === null);
113
125
  const renamedIndexes: RenamedIndex[] = [];
126
+
114
127
  for (const before of original.indexes) {
115
128
  const after = draft.indexes.find((index) => matches(before, index));
129
+
116
130
  if (after === undefined) {
117
131
  droppedIndexes.push(before);
118
132
  } else if (!sameList(project(before.columns), after.columns) || before.unique !== after.unique) {
@@ -122,6 +136,7 @@ const indexChanges = (original: TableDraft, draft: TableDraft, project: Project)
122
136
  renamedIndexes.push({ from: before, to: after });
123
137
  }
124
138
  }
139
+
125
140
  return { droppedIndexes, addedIndexes, renamedIndexes };
126
141
  };
127
142
 
@@ -137,21 +152,26 @@ const sameForeignKey = (before: ForeignKeyDraft, after: ForeignKeyDraft, project
137
152
  const foreignKeyChanges = (original: TableDraft, draft: TableDraft, project: Project) => {
138
153
  const droppedForeignKeys: ForeignKeyDraft[] = [];
139
154
  const addedForeignKeys = draft.foreignKeys.filter((key) => key.originalName === null);
155
+
140
156
  for (const before of original.foreignKeys) {
141
157
  const after = draft.foreignKeys.find((key) => matches(before, key));
158
+
142
159
  if (after === undefined || !sameForeignKey(before, after, project)) {
143
160
  droppedForeignKeys.push(before);
161
+
144
162
  if (after !== undefined) {
145
163
  addedForeignKeys.push(after);
146
164
  }
147
165
  }
148
166
  }
167
+
149
168
  return { droppedForeignKeys, addedForeignKeys };
150
169
  };
151
170
 
152
171
  /* What a draft changes in the table it came from, told apart by the names items kept. */
153
172
  export const diffOf = (dialect: Dialect, original: TableDraft, draft: TableDraft): TableDiff => {
154
173
  const renames = new Map(draft.columns.filter((column) => column.originalName !== null).map((column) => [column.originalName!, column.name]));
174
+
155
175
  const project: Project = (names) => names.map((name) => renames.get(name) ?? name);
156
176
 
157
177
  return {
package/src/ddl/draft.ts CHANGED
@@ -105,6 +105,7 @@ const foreignKeyDraftOf = (foreignKey: ForeignKeyInfo, at: number): ForeignKeyDr
105
105
  /* The draft that changes nothing: what a structure already says. */
106
106
  export const draftOf = (structure: TableStructure): TableDraft => {
107
107
  const clauses = generatedClausesOf(structure.ddl);
108
+
108
109
  return {
109
110
  originalName: structure.name,
110
111
  name: structure.name,