pgkiln 0.45.0 → 0.47.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 (93) hide show
  1. package/db/migrations/106_form_designer.sql +102 -0
  2. package/db/migrations/107_app_version.sql +9 -0
  3. package/db/migrations/108_sql_history.sql +25 -0
  4. package/docs/apex-feature-parity.md +33 -4
  5. package/docs/guide/03-builder.md +123 -28
  6. package/docs/guide/04-pages-and-regions.md +74 -2
  7. package/docs/guide/05-items.md +14 -2
  8. package/docs/guide/08-security.md +1 -1
  9. package/docs/guide/09-reference.md +1 -1
  10. package/docs/guide/10-tutorial.md +1 -1
  11. package/docs/guide/11-from-apex.md +2 -1
  12. package/docs/guide/12-development.md +26 -3
  13. package/docs/guide/16-files.md +2 -2
  14. package/examples/hr/hr_61_form_designer.sql +80 -0
  15. package/package.json +1 -1
  16. package/public/app.css +62 -2
  17. package/public/app.js +244 -35
  18. package/public/builder.css +21 -1
  19. package/public/builder.js +67 -0
  20. package/public/sw.js +2 -1
  21. package/public/worksheet.css +244 -0
  22. package/public/worksheet.js +665 -0
  23. package/src/app.ts +9 -1
  24. package/src/builder/ai-builder.ts +3 -3
  25. package/src/builder/appwizard.ts +22 -6
  26. package/src/builder/arrange.ts +33 -12
  27. package/src/builder/code-editor.ts +1 -1
  28. package/src/builder/components.ts +1 -1
  29. package/src/builder/designer.ts +44 -17
  30. package/src/builder/errors.ts +65 -0
  31. package/src/builder/form-designer.ts +229 -0
  32. package/src/builder/locks.ts +6 -0
  33. package/src/builder/newapp.ts +2 -1
  34. package/src/builder/object-actions.ts +166 -0
  35. package/src/builder/object-browser.ts +111 -33
  36. package/src/builder/querybuilder.ts +1 -1
  37. package/src/builder/region-help.ts +2 -0
  38. package/src/builder/region-settings.ts +2 -0
  39. package/src/builder/reporter.ts +1 -1
  40. package/src/builder/routes.ts +7 -5
  41. package/src/builder/sampledata.ts +2 -2
  42. package/src/builder/scripts.ts +3 -3
  43. package/src/builder/search.ts +3 -3
  44. package/src/builder/shared.ts +2 -2
  45. package/src/builder/sql.ts +15 -73
  46. package/src/builder/table-editor.ts +219 -0
  47. package/src/builder/ui.ts +5 -3
  48. package/src/builder/unload.ts +8 -4
  49. package/src/builder/worksheet.ts +569 -0
  50. package/src/builder/workspaces.ts +1 -1
  51. package/src/cli/replace.ts +10 -0
  52. package/src/i18n/ar.ts +62 -0
  53. package/src/i18n/cs.ts +62 -0
  54. package/src/i18n/da.ts +62 -0
  55. package/src/i18n/de.ts +62 -0
  56. package/src/i18n/el.ts +62 -0
  57. package/src/i18n/es.ts +62 -0
  58. package/src/i18n/fi.ts +62 -0
  59. package/src/i18n/fr.ts +62 -0
  60. package/src/i18n/he.ts +62 -0
  61. package/src/i18n/it.ts +62 -0
  62. package/src/i18n/ja.ts +62 -0
  63. package/src/i18n/ko.ts +62 -0
  64. package/src/i18n/nb.ts +62 -0
  65. package/src/i18n/pl.ts +62 -0
  66. package/src/i18n/pt.ts +62 -0
  67. package/src/i18n/ru.ts +62 -0
  68. package/src/i18n/sv.ts +62 -0
  69. package/src/i18n/tr.ts +62 -0
  70. package/src/i18n/uk.ts +62 -0
  71. package/src/i18n/zh.ts +62 -0
  72. package/src/i18n.ts +124 -0
  73. package/src/metadata.ts +4 -2
  74. package/src/runtime/devbar.ts +1 -1
  75. package/src/runtime/facet-state.ts +10 -5
  76. package/src/runtime/facets.ts +29 -5
  77. package/src/runtime/form-designer.ts +676 -0
  78. package/src/runtime/grid.ts +15 -8
  79. package/src/runtime/items.ts +33 -21
  80. package/src/runtime/lists.ts +1 -1
  81. package/src/runtime/pdf.ts +2 -1
  82. package/src/runtime/regions.ts +4 -0
  83. package/src/runtime/render.ts +7 -3
  84. package/src/runtime/report.ts +2 -2
  85. package/src/runtime/routes.ts +3 -1
  86. package/src/runtime/smart-filters.ts +2 -1
  87. package/src/runtime/tasks.ts +6 -3
  88. package/src/sqlformat.ts +208 -0
  89. package/src/sqlplan.ts +133 -0
  90. package/src/sqlscript.ts +6 -2
  91. package/src/sqlworksheet.ts +278 -0
  92. package/src/tabledef.ts +226 -0
  93. package/src/unload.ts +14 -3
@@ -0,0 +1,226 @@
1
+ import pg from 'pg';
2
+
3
+ // (sprint 95, Database Actions DA-8: the Table Properties dialog) A table as a definition the builder's form edits:
4
+ // columns (type, not null, default, identity, comment), primary key, unique keys, foreign keys, checks, indexes, the
5
+ // table's comment, row level security and, for a new table, partitioning. readTable() reads one from the catalog;
6
+ // tableSql() gives the CREATE statements for a new table or the ALTER statements that turn the old definition into
7
+ // the new one; checkTable() says what is wrong with a definition before any SQL is made. Pure apart from readTable.
8
+
9
+ export interface ColumnDef {
10
+ name: string;
11
+ /** the column's name in the table as read (empty: a new column) */
12
+ orig: string;
13
+ type: string;
14
+ notNull: boolean;
15
+ default: string;
16
+ identity: '' | 'always' | 'by default';
17
+ /** a stored generated column's expression (read only here) */
18
+ generated: string;
19
+ comment: string;
20
+ pk: boolean;
21
+ }
22
+ export interface KeyDef { name: string; columns: string }
23
+ export interface ForeignKeyDef { name: string; columns: string; refTable: string; refColumns: string; onDelete: string }
24
+ export interface CheckDef { name: string; expr: string }
25
+ export interface IndexDef { name: string; columns: string; unique: boolean; /** an index the form can't express (another method, a WHERE): kept as it is */ fixed?: string }
26
+ export interface TableDef {
27
+ schema: string;
28
+ name: string;
29
+ comment: string;
30
+ columns: ColumnDef[];
31
+ pkName: string;
32
+ uniques: KeyDef[];
33
+ fks: ForeignKeyDef[];
34
+ checks: CheckDef[];
35
+ indexes: IndexDef[];
36
+ rls: boolean;
37
+ forceRls: boolean;
38
+ /** new tables only: range | list | hash and the key */
39
+ partitionBy: '' | 'range' | 'list' | 'hash';
40
+ partitionKey: string;
41
+ }
42
+
43
+ export const ON_DELETE = ['no action', 'restrict', 'cascade', 'set null', 'set default'] as const;
44
+ export const NAME_RE = /^[A-Za-z_][A-Za-z0-9_$]{0,62}$/;
45
+ /** A type: a name (a few words allowed: double precision, timestamp with time zone), a size, array brackets, a schema. */
46
+ export const TYPE_RE = /^([a-z_][a-z0-9_]*\.)?[a-z_][a-z0-9_ ]{0,40}(\(\d{1,4}(,\s?\d{1,4})?\))?( with(out)? time zone)?(\[\])*$/i;
47
+
48
+ const ident = (n: string) => (/^[a-z_][a-z0-9_$]*$/.test(n) && !RESERVED.has(n) ? n : pg.escapeIdentifier(n));
49
+ const RESERVED = new Set(['all', 'analyse', 'analyze', 'and', 'any', 'array', 'as', 'asc', 'case', 'cast', 'check', 'collate', 'column', 'constraint', 'create', 'default', 'desc', 'distinct', 'do', 'else', 'end', 'except', 'false', 'for', 'foreign', 'from', 'grant', 'group', 'having', 'in', 'into', 'limit', 'not', 'null', 'offset', 'on', 'only', 'or', 'order', 'primary', 'references', 'select', 'table', 'then', 'to', 'true', 'union', 'unique', 'user', 'using', 'when', 'where', 'with']);
50
+ const lit = (s: string) => `'${s.replace(/'/g, "''")}'`;
51
+ /** "a, b" → the quoted names */
52
+ const names = (list: string) => list.split(',').map((x) => x.trim()).filter(Boolean);
53
+ const quoteList = (list: string) => names(list).map(ident).join(', ');
54
+
55
+ export const emptyTable = (schema: string): TableDef => ({
56
+ schema, name: '', comment: '', columns: [], pkName: '', uniques: [], fks: [], checks: [], indexes: [], rls: false, forceRls: false, partitionBy: '', partitionKey: '',
57
+ });
58
+
59
+ type Q = { query: <T extends pg.QueryResultRow = any>(sql: string, params?: unknown[]) => Promise<pg.QueryResult<T>> };
60
+
61
+ /** A table as the form shows it; null when `ref` is no table. */
62
+ export async function readTable(db: Q, ref: string): Promise<TableDef | null> {
63
+ const t = (await db.query<{ oid: number; schema: string; name: string; comment: string | null; rls: boolean; force: boolean }>(
64
+ `select c.oid, n.nspname as schema, c.relname as name, obj_description(c.oid, 'pg_class') as comment, c.relrowsecurity as rls, c.relforcerowsecurity as force
65
+ from pg_class c join pg_namespace n on n.oid = c.relnamespace where c.oid = to_regclass($1) and c.relkind in ('r', 'p')`, [ref])).rows[0];
66
+ if (!t) return null;
67
+ const cols = (await db.query<{ name: string; type: string; notnull: boolean; def: string | null; identity: string; generated: string; comment: string | null }>(
68
+ `select a.attname as name, format_type(a.atttypid, a.atttypmod) as type, a.attnotnull as notnull, pg_get_expr(d.adbin, d.adrelid) as def,
69
+ a.attidentity as identity, a.attgenerated as generated, col_description(a.attrelid, a.attnum) as comment
70
+ from pg_attribute a left join pg_attrdef d on d.adrelid = a.attrelid and d.adnum = a.attnum
71
+ where a.attrelid = $1 and a.attnum > 0 and not a.attisdropped order by a.attnum`, [t.oid])).rows;
72
+ const cons = (await db.query<{ name: string; type: string; cols: string[]; ref: string | null; refcols: string[] | null; deltype: string; def: string }>(
73
+ `select k.conname as name, k.contype as type,
74
+ array(select a.attname from unnest(k.conkey) with ordinality u(n, i) join pg_attribute a on a.attrelid = k.conrelid and a.attnum = u.n order by u.i)::text[] as cols,
75
+ k.confrelid::regclass::text as ref,
76
+ array(select a.attname from unnest(k.confkey) with ordinality u(n, i) join pg_attribute a on a.attrelid = k.confrelid and a.attnum = u.n order by u.i)::text[] as refcols,
77
+ k.confdeltype as deltype, pg_get_constraintdef(k.oid) as def
78
+ from pg_constraint k where k.conrelid = $1 and k.contype in ('p', 'u', 'f', 'c') order by k.conname`, [t.oid])).rows;
79
+ const idx = (await db.query<{ name: string; def: string; unique: boolean }>(
80
+ `select i.relname as name, pg_get_indexdef(x.indexrelid) as def, x.indisunique as unique
81
+ from pg_index x join pg_class i on i.oid = x.indexrelid
82
+ where x.indrelid = $1 and not exists (select 1 from pg_constraint k where k.conindid = x.indexrelid) order by i.relname`, [t.oid])).rows;
83
+ const pk = cons.find((k) => k.type === 'p');
84
+ const DEL: Record<string, string> = { a: 'no action', r: 'restrict', c: 'cascade', n: 'set null', d: 'set default' };
85
+ return {
86
+ schema: t.schema,
87
+ name: t.name,
88
+ comment: t.comment ?? '',
89
+ columns: cols.map((c) => ({
90
+ name: c.name, orig: c.name, type: c.type, notNull: c.notnull, default: c.generated === 's' || c.identity ? '' : (c.def ?? ''),
91
+ identity: c.identity === 'a' ? 'always' : c.identity === 'd' ? 'by default' : '', generated: c.generated === 's' ? (c.def ?? '') : '',
92
+ comment: c.comment ?? '', pk: !!pk?.cols.includes(c.name),
93
+ })),
94
+ pkName: pk?.name ?? '',
95
+ uniques: cons.filter((k) => k.type === 'u').map((k) => ({ name: k.name, columns: k.cols.join(', ') })),
96
+ fks: cons.filter((k) => k.type === 'f').map((k) => ({ name: k.name, columns: k.cols.join(', '), refTable: k.ref ?? '', refColumns: (k.refcols ?? []).join(', '), onDelete: DEL[k.deltype] ?? 'no action' })),
97
+ checks: cons.filter((k) => k.type === 'c').map((k) => ({ name: k.name, expr: /^CHECK \(([\s\S]*)\)( NOT VALID)?$/.exec(k.def)?.[1] ?? k.def })),
98
+ indexes: idx.map((i) => {
99
+ const m = /^CREATE (UNIQUE )?INDEX \S+ ON \S+ USING btree \(([\s\S]*)\)$/.exec(i.def);
100
+ return m ? { name: i.name, columns: m[2], unique: !!m[1] } : { name: i.name, columns: '', unique: i.unique, fixed: i.def };
101
+ }),
102
+ rls: t.rls,
103
+ forceRls: t.force,
104
+ partitionBy: '',
105
+ partitionKey: '',
106
+ };
107
+ }
108
+
109
+ /** An expression the form takes (a default, a check, index columns, a partition key): one expression, no statements. */
110
+ const expressionOk = (e: string, max = 500) => e.length <= max && !/;|--|\/\*/.test(e);
111
+
112
+ /** What is wrong with a definition (empty: nothing). */
113
+ export function checkTable(def: TableDef, old: TableDef | null): string[] {
114
+ const errors: string[] = [];
115
+ if (!NAME_RE.test(def.name)) errors.push('The table needs a name: a letter or _ followed by letters, digits, _ or $.');
116
+ if (!def.columns.length) errors.push('A table needs at least one column.');
117
+ const seen = new Set<string>();
118
+ for (const c of def.columns) {
119
+ if (!NAME_RE.test(c.name)) errors.push(`Column “${c.name}”: a name is a letter or _ followed by letters, digits, _ or $.`);
120
+ else if (seen.has(c.name)) errors.push(`Column ${c.name} is there twice.`);
121
+ seen.add(c.name);
122
+ if (!TYPE_RE.test(c.type)) errors.push(`Column ${c.name}: a type is a name with an optional size, e.g. text, numeric(9,2), varchar(40), int[].`);
123
+ if (c.default && !expressionOk(c.default)) errors.push(`Column ${c.name}: a default is one expression (at most 500 characters, without ; or comments).`);
124
+ if (c.default && c.identity) errors.push(`Column ${c.name}: an identity column has no default.`);
125
+ if (c.comment.length > 2000) errors.push(`Column ${c.name}: a comment has at most 2,000 characters.`);
126
+ const was = old?.columns.find((o) => o.orig && o.orig === c.orig);
127
+ if (was && was.generated !== c.generated) errors.push(`Column ${c.name}: a generated column's expression can't be changed here.`);
128
+ }
129
+ const has = (list: string) => names(list).every((n) => seen.has(n)) && names(list).length > 0;
130
+ for (const k of def.uniques) if (!NAME_RE.test(k.name) || !has(k.columns)) errors.push(`Unique key ${k.name || '(no name)'}: a name and columns of the table.`);
131
+ for (const f of def.fks) {
132
+ if (!NAME_RE.test(f.name) || !has(f.columns)) errors.push(`Foreign key ${f.name || '(no name)'}: a name and columns of the table.`);
133
+ if (!f.refTable) errors.push(`Foreign key ${f.name}: choose the table it refers to.`);
134
+ if (f.refColumns && !names(f.refColumns).every((n) => NAME_RE.test(n))) errors.push(`Foreign key ${f.name}: the columns it refers to are names.`);
135
+ if (!(ON_DELETE as readonly string[]).includes(f.onDelete)) errors.push(`Foreign key ${f.name}: an unknown ON DELETE.`);
136
+ }
137
+ for (const k of def.checks) if (!NAME_RE.test(k.name) || !k.expr || !expressionOk(k.expr)) errors.push(`Check ${k.name || '(no name)'}: a name and one expression (without ; or comments).`);
138
+ for (const i of def.indexes) if (!i.fixed && (!NAME_RE.test(i.name) || !i.columns || !expressionOk(i.columns))) errors.push(`Index ${i.name || '(no name)'}: a name and its columns or expressions (without ; or comments).`);
139
+ if (def.partitionBy && (!def.partitionKey || !expressionOk(def.partitionKey, 200))) errors.push('Partitioning needs a key: columns or an expression.');
140
+ if (def.comment.length > 2000) errors.push('A comment has at most 2,000 characters.');
141
+ return errors;
142
+ }
143
+
144
+ const columnSql = (c: ColumnDef) =>
145
+ `${ident(c.name)} ${c.type}${c.identity ? ` generated ${c.identity} as identity` : ''}${c.generated ? ` generated always as (${c.generated}) stored` : ''}${c.default ? ` default ${c.default}` : ''}${c.notNull && !c.pk ? ' not null' : ''}`;
146
+
147
+ /** The statements that make `def`: CREATE for a new table (old null), else ALTER from old to def. */
148
+ export function tableSql(def: TableDef, old: TableDef | null): string[] {
149
+ const q = `${ident(def.schema)}.${ident(def.name)}`;
150
+ const out: string[] = [];
151
+ const pkCols = def.columns.filter((c) => c.pk).map((c) => ident(c.name));
152
+ const fkSql = (f: ForeignKeyDef) => `constraint ${ident(f.name)} foreign key (${quoteList(f.columns)}) references ${f.refTable}${f.refColumns ? ` (${quoteList(f.refColumns)})` : ''}${f.onDelete !== 'no action' ? ` on delete ${f.onDelete}` : ''}`;
153
+ const indexSql = (i: IndexDef) => `create ${i.unique ? 'unique ' : ''}index ${ident(i.name)} on ${q} (${i.columns});`;
154
+ const comments = (from: TableDef | null) => {
155
+ if ((from?.comment ?? '') !== def.comment) out.push(`comment on table ${q} is ${def.comment ? lit(def.comment) : 'null'};`);
156
+ for (const c of def.columns) {
157
+ const was = from?.columns.find((o) => o.orig && o.orig === c.orig);
158
+ if ((was?.comment ?? '') !== c.comment) out.push(`comment on column ${q}.${ident(c.name)} is ${c.comment ? lit(c.comment) : 'null'};`);
159
+ }
160
+ };
161
+ const rls = (from: TableDef | null) => {
162
+ if ((from?.rls ?? false) !== def.rls) out.push(`alter table ${q} ${def.rls ? 'enable' : 'disable'} row level security;`);
163
+ if ((from?.forceRls ?? false) !== def.forceRls) out.push(`alter table ${q} ${def.forceRls ? '' : 'no '}force row level security;`);
164
+ };
165
+
166
+ if (!old) {
167
+ const lines = [
168
+ ...def.columns.map(columnSql),
169
+ ...(pkCols.length ? [`${def.pkName ? `constraint ${ident(def.pkName)} ` : ''}primary key (${pkCols.join(', ')})`] : []),
170
+ ...def.uniques.map((k) => `constraint ${ident(k.name)} unique (${quoteList(k.columns)})`),
171
+ ...def.checks.map((k) => `constraint ${ident(k.name)} check (${k.expr})`),
172
+ ...def.fks.map(fkSql),
173
+ ];
174
+ out.push(`create table ${q} (\n ${lines.join(',\n ')}\n)${def.partitionBy ? ` partition by ${def.partitionBy} (${def.partitionKey})` : ''};`);
175
+ for (const i of def.indexes) if (!i.fixed) out.push(indexSql(i));
176
+ comments(null);
177
+ rls(null);
178
+ return out;
179
+ }
180
+
181
+ const oq = `${ident(old.schema)}.${ident(old.name)}`;
182
+ if (old.name !== def.name) out.push(`alter table ${oq} rename to ${ident(def.name)};`);
183
+ const same = <T extends { name: string }>(a: T, b: T | undefined, key: (x: T) => string) => !!b && key(a) === key(b);
184
+ // constraints and indexes that go or change first, then the columns, then what is added
185
+ const keyOf = (k: KeyDef) => names(k.columns).join(',');
186
+ const fkOf = (f: ForeignKeyDef) => [names(f.columns).join(','), f.refTable, names(f.refColumns).join(','), f.onDelete].join('|');
187
+ const ixOf = (i: IndexDef) => `${i.unique}|${i.columns.replace(/\s+/g, ' ').trim()}`;
188
+ const oldPk = old.columns.filter((c) => c.pk).map((c) => c.orig);
189
+ const newPk = def.columns.filter((c) => c.pk).map((c) => c.orig || c.name);
190
+ const pkChanged = oldPk.join(',') !== newPk.join(',') || def.columns.some((c) => c.pk && !c.orig);
191
+ if (pkChanged && old.pkName) out.push(`alter table ${q} drop constraint ${ident(old.pkName)};`);
192
+ for (const k of old.uniques) if (!same(k, def.uniques.find((x) => x.name === k.name), keyOf)) out.push(`alter table ${q} drop constraint ${ident(k.name)};`);
193
+ for (const k of old.checks) if (!same(k, def.checks.find((x) => x.name === k.name), (x) => x.expr.trim())) out.push(`alter table ${q} drop constraint ${ident(k.name)};`);
194
+ for (const f of old.fks) if (!same(f, def.fks.find((x) => x.name === f.name), fkOf)) out.push(`alter table ${q} drop constraint ${ident(f.name)};`);
195
+ for (const i of old.indexes) if (!i.fixed && !same(i, def.indexes.find((x) => x.name === i.name), ixOf)) out.push(`drop index ${ident(def.schema)}.${ident(i.name)};`);
196
+ for (const c of old.columns) if (!def.columns.some((n) => n.orig === c.orig)) out.push(`alter table ${q} drop column ${ident(c.orig)};`);
197
+ for (const c of def.columns) {
198
+ const was = old.columns.find((o) => o.orig && o.orig === c.orig);
199
+ if (!was) {
200
+ out.push(`alter table ${q} add column ${columnSql(c)};`);
201
+ continue;
202
+ }
203
+ const col = ident(c.name);
204
+ if (was.name !== c.name) out.push(`alter table ${q} rename column ${ident(was.name)} to ${col};`);
205
+ const parts: string[] = [];
206
+ if (was.type !== c.type) parts.push(`alter column ${col} type ${c.type} using ${col}::${c.type}`);
207
+ if (was.default !== c.default) parts.push(c.default ? `alter column ${col} set default ${c.default}` : `alter column ${col} drop default`);
208
+ if (was.identity !== c.identity) {
209
+ if (!c.identity) parts.push(`alter column ${col} drop identity`);
210
+ else if (!was.identity) parts.push(`alter column ${col} add generated ${c.identity} as identity`);
211
+ else parts.push(`alter column ${col} set generated ${c.identity}`);
212
+ }
213
+ // a primary key's columns are not null whatever the box says
214
+ const wantNotNull = c.notNull || c.pk;
215
+ if (wantNotNull !== was.notNull) parts.push(`alter column ${col} ${wantNotNull ? 'set' : 'drop'} not null`);
216
+ if (parts.length) out.push(`alter table ${q}\n ${parts.join(',\n ')};`);
217
+ }
218
+ if (pkChanged && newPk.length) out.push(`alter table ${q} add ${def.pkName && def.pkName !== old.pkName ? `constraint ${ident(def.pkName)} ` : old.pkName ? `constraint ${ident(old.pkName)} ` : ''}primary key (${def.columns.filter((c) => c.pk).map((c) => ident(c.name)).join(', ')});`);
219
+ for (const k of def.uniques) if (!same(k, old.uniques.find((x) => x.name === k.name), keyOf)) out.push(`alter table ${q} add constraint ${ident(k.name)} unique (${quoteList(k.columns)});`);
220
+ for (const k of def.checks) if (!same(k, old.checks.find((x) => x.name === k.name), (x) => x.expr.trim())) out.push(`alter table ${q} add constraint ${ident(k.name)} check (${k.expr});`);
221
+ for (const f of def.fks) if (!same(f, old.fks.find((x) => x.name === f.name), fkOf)) out.push(`alter table ${q} add ${fkSql(f)};`);
222
+ for (const i of def.indexes) if (!i.fixed && !same(i, old.indexes.find((x) => x.name === i.name), ixOf)) out.push(indexSql(i));
223
+ comments(old);
224
+ rls(old);
225
+ return out;
226
+ }
package/src/unload.ts CHANGED
@@ -13,8 +13,8 @@ import { esc, XLSX_TYPE, XlsxWriter, xlsxWidths, type XlsxCell } from './xlsx.ts
13
13
  // bytea are exact. The builder runs it in a read-only transaction with a
14
14
  // statement timeout (builder/unload.ts).
15
15
 
16
- export type UnloadFormat = 'csv' | 'json' | 'xlsx' | 'xml';
17
- export const UNLOAD_FORMATS: UnloadFormat[] = ['csv', 'json', 'xlsx', 'xml'];
16
+ export type UnloadFormat = 'csv' | 'json' | 'xlsx' | 'xml' | 'sql';
17
+ export const UNLOAD_FORMATS: UnloadFormat[] = ['csv', 'json', 'xlsx', 'xml', 'sql'];
18
18
 
19
19
  export interface UnloadOptions {
20
20
  format: UnloadFormat;
@@ -27,7 +27,7 @@ export interface UnloadOptions {
27
27
  /** XML: the root element and the element of each row */
28
28
  rootTag: string;
29
29
  rowTag: string;
30
- /** Excel: the sheet name */
30
+ /** Excel: the sheet name; SQL inserts: the table name */
31
31
  sheet: string;
32
32
  }
33
33
 
@@ -41,6 +41,7 @@ export const UNLOAD_TYPES: Record<UnloadFormat, string> = {
41
41
  json: 'application/json; charset=utf-8',
42
42
  xlsx: XLSX_TYPE,
43
43
  xml: 'application/xml; charset=utf-8',
44
+ sql: 'application/sql; charset=utf-8',
44
45
  };
45
46
 
46
47
  export class UnloadError extends Error {}
@@ -139,6 +140,16 @@ function encoder(o: UnloadOptions, fields: pg.FieldDef[]): Encoder {
139
140
  end: () => `</${o.rootTag}>\n`,
140
141
  };
141
142
  }
143
+ case 'sql': {
144
+ // (sprint 92) INSERT statements: numbers and booleans as they are, other values as quoted literals that
145
+ // PostgreSQL casts to the column's type
146
+ const ident = (n: string) => `"${n.replace(/"/g, '""')}"`;
147
+ const head = `insert into ${ident(o.sheet)} (${names.map(ident).join(', ')}) values (`;
148
+ const literal = (v: string | null, i: number) =>
149
+ v === null ? 'null' : oids[i] === BOOL ? (v === 't' ? 'true' : 'false') : isNumeric(oids[i]) && JSON_NUMBER.test(v) ? v : `'${v.replace(/'/g, "''")}'`;
150
+ const rows = (rows: Row[]) => rows.map((r) => `${head}${r.map(literal).join(', ')});\n`).join('');
151
+ return { start: rows, rows, end: () => '' };
152
+ }
142
153
  case 'xlsx': {
143
154
  let pending: Uint8Array[] = [];
144
155
  let xlsx: XlsxWriter;