@rebasepro/studio 0.24.0 → 0.24.1-canary.gcb918fc

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 (48) hide show
  1. package/dist/{ApiExplorer-CCtczj3F.js → ApiExplorer-Bzt7Ye49.js} +5 -3
  2. package/dist/ApiExplorer-Bzt7Ye49.js.map +1 -0
  3. package/dist/{ApiKeysView-B66TdzhH.js → ApiKeysView-Bqf6XYR1.js} +2 -1
  4. package/dist/{ApiKeysView-B66TdzhH.js.map → ApiKeysView-Bqf6XYR1.js.map} +1 -1
  5. package/dist/{BackupsView-DmSMzAbT.js → BackupsView-BhOHB6iP.js} +6 -20
  6. package/dist/BackupsView-BhOHB6iP.js.map +1 -0
  7. package/dist/{BranchesView-DP1a87E-.js → BranchesView-BHfwGTYc.js} +5 -3
  8. package/dist/BranchesView-BHfwGTYc.js.map +1 -0
  9. package/dist/{CronJobsView-TDErHTuK.js → CronJobsView-CMNK7eWO.js} +52 -59
  10. package/dist/CronJobsView-CMNK7eWO.js.map +1 -0
  11. package/dist/{JSEditor-B59pwg4c.js → JSEditor-DIO2Mw8q.js} +115 -96
  12. package/dist/JSEditor-DIO2Mw8q.js.map +1 -0
  13. package/dist/{LogsExplorer-BKM4yfP8.js → LogsExplorer-C-bwDFIl.js} +19 -23
  14. package/dist/LogsExplorer-C-bwDFIl.js.map +1 -0
  15. package/dist/{RLSEditor-BlLLV6xs.js → RLSEditor-jFD_d06f.js} +10 -8
  16. package/dist/RLSEditor-jFD_d06f.js.map +1 -0
  17. package/dist/{SQLEditor-1An7w0j6.js → SQLEditor-D5Ci4pyW.js} +372 -545
  18. package/dist/SQLEditor-D5Ci4pyW.js.map +1 -0
  19. package/dist/{SchemaVisualizer-DCLVfA5t.js → SchemaVisualizer-D-rAqwul.js} +479 -206
  20. package/dist/SchemaVisualizer-D-rAqwul.js.map +1 -0
  21. package/dist/{StorageView-CbsHuCBf.js → StorageView-CPkDKbsX.js} +498 -241
  22. package/dist/StorageView-CPkDKbsX.js.map +1 -0
  23. package/dist/assets/dagre-layout.worker-BFnhAzuR.js.map +1 -0
  24. package/dist/components/SQLEditor/SQLEditor.d.ts +11 -0
  25. package/dist/components/SchemaVisualizer/dagre-layout.d.ts +32 -0
  26. package/dist/components/SchemaVisualizer/dagre-layout.worker.d.ts +13 -0
  27. package/dist/components/SchemaVisualizer/layout-runner.d.ts +23 -0
  28. package/dist/components/SchemaVisualizer/schema-visualizer.utils.d.ts +21 -10
  29. package/dist/components/SchemaVisualizer/useSchemaGraph.d.ts +38 -1
  30. package/dist/components/StorageView/useVisibleFileMetadata.d.ts +46 -0
  31. package/dist/dagre-layout.worker-j9NbH9Gb.js +27 -0
  32. package/dist/dagre-layout.worker-j9NbH9Gb.js.map +1 -0
  33. package/dist/index.es.js +16 -14
  34. package/dist/index.es.js.map +1 -1
  35. package/dist/sql_utils-BZD31xE3.js +478 -0
  36. package/dist/sql_utils-BZD31xE3.js.map +1 -0
  37. package/dist/utils/sql_utils.d.ts +60 -0
  38. package/package.json +9 -9
  39. package/dist/ApiExplorer-CCtczj3F.js.map +0 -1
  40. package/dist/BackupsView-DmSMzAbT.js.map +0 -1
  41. package/dist/BranchesView-DP1a87E-.js.map +0 -1
  42. package/dist/CronJobsView-TDErHTuK.js.map +0 -1
  43. package/dist/JSEditor-B59pwg4c.js.map +0 -1
  44. package/dist/LogsExplorer-BKM4yfP8.js.map +0 -1
  45. package/dist/RLSEditor-BlLLV6xs.js.map +0 -1
  46. package/dist/SQLEditor-1An7w0j6.js.map +0 -1
  47. package/dist/SchemaVisualizer-DCLVfA5t.js.map +0 -1
  48. package/dist/StorageView-CbsHuCBf.js.map +0 -1
@@ -0,0 +1,478 @@
1
+ import { csvRecord, toSnakeCase } from "@rebasepro/utils";
2
+ import { astVisitor, parse } from "pgsql-ast-parser";
3
+ //#region src/utils/sql_utils.ts
4
+ /** The statements in `sqlText`, or `null` when the parser cannot read it. */
5
+ function parseStatements(sqlText) {
6
+ try {
7
+ return parse(sqlText);
8
+ } catch {
9
+ return null;
10
+ }
11
+ }
12
+ /**
13
+ * The `EXPLAIN` for one statement, or `null` when `sqlText` is not exactly one.
14
+ *
15
+ * Never `ANALYZE`: that executes the statement to time it, so explaining a
16
+ * `DELETE` deleted the rows — without the confirmation "Run" asks for.
17
+ *
18
+ * And never more than one statement. Sent as one simple query, `EXPLAIN (…)
19
+ * SELECT 1; DELETE …` explains the SELECT and *runs* the DELETE. Text the
20
+ * parser cannot read is still explained, as long as no `;` could be hiding a
21
+ * second statement in it.
22
+ */
23
+ function buildExplainSql(sqlText) {
24
+ const statement = sqlText.trim().replace(/;+$/, "").trim();
25
+ if (!statement) return null;
26
+ const statements = parseStatements(statement);
27
+ return (statements ? statements.length === 1 : !statement.includes(";")) ? `EXPLAIN (FORMAT JSON) ${statement}` : null;
28
+ }
29
+ /**
30
+ * Whether the console's automatic `LIMIT` may be appended to `sqlText`: one
31
+ * top-level SELECT that has no limit of its own.
32
+ *
33
+ * Decided on the parsed statement, not the text. A search for the word SELECT
34
+ * also found it inside `INSERT INTO … SELECT` and `CREATE TABLE … AS SELECT`,
35
+ * which then copied a thousand rows and reported success, and at the start of
36
+ * a script whose last statement the appended `LIMIT` turned into a syntax
37
+ * error. Text the parser cannot read is left as written.
38
+ */
39
+ function acceptsAutoLimit(sqlText) {
40
+ const statements = parseStatements(sqlText);
41
+ if (!statements || statements.length !== 1) return false;
42
+ const [statement] = statements;
43
+ return statement.type === "select" && !statement.limit;
44
+ }
45
+ /** A destructive command named anywhere in a text, comments included. */
46
+ var DESTRUCTIVE_COMMAND = /\b(DELETE|UPDATE|DROP|TRUNCATE)\b/i;
47
+ function isDestructiveStatement(statement) {
48
+ switch (statement.type) {
49
+ case "delete":
50
+ case "update": return !statement.where;
51
+ case "truncate table":
52
+ case "drop table":
53
+ case "drop sequence":
54
+ case "drop index":
55
+ case "drop type":
56
+ case "drop trigger":
57
+ case "drop function": return true;
58
+ case "alter table": return statement.changes.some((change) => change.type === "drop column" || change.type === "drop constraint");
59
+ case "with": return statement.bind.some((binding) => isDestructiveStatement(binding.statement)) || isDestructiveStatement(statement.in);
60
+ case "with recursive": return isDestructiveStatement(statement.in);
61
+ case "do": return DESTRUCTIVE_COMMAND.test(statement.code);
62
+ default: return false;
63
+ }
64
+ }
65
+ /**
66
+ * Whether "Run" asks before running `sqlText`: a statement in it deletes or
67
+ * updates without a WHERE of its own, drops or truncates.
68
+ *
69
+ * Decided per parsed statement, not on the text. A search of the text for the
70
+ * word WHERE found it in another statement or in a comment, so `DELETE FROM
71
+ * posts; SELECT * FROM posts WHERE id = 1` deleted every post without asking.
72
+ * Text the parser cannot read is confirmed whenever it names a destructive
73
+ * command.
74
+ */
75
+ function needsDestructiveConfirmation(sqlText) {
76
+ const statements = parseStatements(sqlText);
77
+ if (!statements) return DESTRUCTIVE_COMMAND.test(sqlText);
78
+ return statements.some(isDestructiveStatement);
79
+ }
80
+ /**
81
+ * A table as SQL: `"schema"."table"` when a schema is given, a bare `"table"`
82
+ * otherwise.
83
+ */
84
+ function quoteTableName(tableName, schemaName) {
85
+ return schemaName ? `${quoteIdentifier(schemaName)}.${quoteIdentifier(tableName)}` : quoteIdentifier(tableName);
86
+ }
87
+ /** An identifier as SQL, whatever it holds: double-quoted, with `"` doubled. */
88
+ function quoteIdentifier(identifier) {
89
+ return `"${identifier.replace(/"/g, "\"\"")}"`;
90
+ }
91
+ var refuse = (key, params = {}) => ({ refusal: {
92
+ key,
93
+ params
94
+ } });
95
+ /**
96
+ * Which stored cell an edit of the result column `resultColumn` writes, and
97
+ * how its row is found again — or why no edit is safe.
98
+ *
99
+ * Decided on what the database reported, not on the query's text. Each
100
+ * column of the result says which table column it was read from, or that it
101
+ * was computed; a cell is editable only when it was read from a column of a
102
+ * table, every column of that table's primary key was read too, and each of
103
+ * them is the only column of the result with its name — a row holds one value
104
+ * per name.
105
+ *
106
+ * The text was the only evidence before, and it was wrong twice over. For
107
+ * `SELECT p.id, a.name FROM posts p JOIN authors a …` it took the post's `id`
108
+ * for the author's key and updated whichever author shared the post's id.
109
+ * For `SELECT id, lower(email) AS name FROM users` it wrote the edited text
110
+ * to `users.name`.
111
+ *
112
+ * The database reports a table, not which mention of it: in a self-join both
113
+ * sides are `authors`, and a key from one with a value from the other finds
114
+ * the wrong row. So the query is still read, for one thing — the table must
115
+ * be read once, and nothing it is read through may be read twice.
116
+ */
117
+ function resolveCellEdit(sqlText, provenance, resultColumn) {
118
+ const sameName = provenance.columns.filter((column) => column.name === resultColumn);
119
+ if (sameName.length !== 1) return refuse("studio_sql_edit_duplicate_column", { column: resultColumn });
120
+ const source = sameName[0].source;
121
+ if (!source) return refuse("studio_sql_edit_computed_column", { column: resultColumn });
122
+ const tableLabel = `${source.schema}.${source.table}`;
123
+ const table = provenance.tables.find((t) => t.schema === source.schema && t.table === source.table);
124
+ if (!table || table.kind !== "table" && table.kind !== "partitioned table") return refuse("studio_sql_edit_not_a_table", {
125
+ column: resultColumn,
126
+ table: tableLabel
127
+ });
128
+ if (table.hasInheritors) return refuse("studio_sql_edit_inherited_table", { table: tableLabel });
129
+ if (table.primaryKey.length === 0) return refuse("studio_sql_edit_no_primary_key", { table: tableLabel });
130
+ const key = [];
131
+ for (const keyColumn of table.primaryKey) {
132
+ const holders = provenance.columns.filter((column) => column.source?.schema === source.schema && column.source.table === source.table && column.source.column === keyColumn);
133
+ const holder = holders.length === 1 && provenance.columns.filter((column) => column.name === holders[0].name).length === 1 ? holders[0] : void 0;
134
+ if (!holder) return refuse("studio_sql_edit_key_missing", {
135
+ table: tableLabel,
136
+ columns: table.primaryKey.join(", ")
137
+ });
138
+ key.push({
139
+ column: keyColumn,
140
+ resultColumn: holder.name
141
+ });
142
+ }
143
+ const readOnce = tableReadOnce(sqlText, source.table);
144
+ if (readOnce !== true) return readOnce;
145
+ return { target: {
146
+ schema: source.schema,
147
+ table: source.table,
148
+ column: source.column,
149
+ key
150
+ } };
151
+ }
152
+ /**
153
+ * Whether `sqlText` is one SELECT that reads `table` once — counting every
154
+ * mention of a table of that name, in any schema and at any depth, and every
155
+ * mention of a `WITH` query, since one read twice reads its tables twice.
156
+ */
157
+ function tableReadOnce(sqlText, table) {
158
+ const statements = parseStatements(sqlText);
159
+ if (!statements) return refuse("studio_sql_edit_unreadable_query");
160
+ if (statements.length !== 1) return refuse("studio_sql_edit_not_a_select");
161
+ const [statement] = statements;
162
+ if (!(statement.type === "select" || statement.type === "with" && statement.in.type === "select" && statement.bind.every((binding) => binding.statement.type === "select"))) return refuse("studio_sql_edit_not_a_select");
163
+ const mentions = /* @__PURE__ */ new Map();
164
+ const withNames = [];
165
+ astVisitor((visitor) => ({
166
+ fromTable: (from) => {
167
+ const name = from.name.name.toLowerCase();
168
+ mentions.set(name, (mentions.get(name) ?? 0) + 1);
169
+ visitor.super().fromTable(from);
170
+ },
171
+ with: (withStatement) => {
172
+ withNames.push(...withStatement.bind.map((binding) => binding.alias.name.toLowerCase()));
173
+ visitor.super().with(withStatement);
174
+ }
175
+ })).statement(statement);
176
+ return (mentions.get(table.toLowerCase()) ?? 0) !== 1 || withNames.some((name) => (mentions.get(name) ?? 0) > 1) ? refuse("studio_sql_edit_table_read_twice", { table }) : true;
177
+ }
178
+ /**
179
+ * The collections a result's rows can be opened as: those whose table one of
180
+ * the result's columns was read from, with the table's (single-column) primary
181
+ * key read too, under a name no other column of the result has.
182
+ *
183
+ * Matched on the provenance the database reported. A text match took the
184
+ * result's `id` for every table of a join: `SELECT p.id, a.name FROM posts p
185
+ * JOIN authors a …` offered to open the author whose id was the post's.
186
+ */
187
+ function resolveQueryCollections(provenance, collections) {
188
+ const results = [];
189
+ for (const table of provenance.tables) {
190
+ const collection = collections.find((c) => {
191
+ const tableName = ("table" in c ? c.table : void 0) || toSnakeCase(c.slug);
192
+ const schemaName = ("schema" in c ? c.schema : void 0) || "public";
193
+ return tableName === table.table && schemaName === table.schema;
194
+ });
195
+ if (!collection || table.primaryKey.length !== 1) continue;
196
+ const [keyColumn] = table.primaryKey;
197
+ const holders = provenance.columns.filter((column) => column.source?.schema === table.schema && column.source.table === table.table && column.source.column === keyColumn);
198
+ if (holders.length !== 1) continue;
199
+ const pkColumn = holders[0].name;
200
+ if (provenance.columns.filter((column) => column.name === pkColumn).length !== 1) continue;
201
+ results.push({
202
+ tableName: table.table,
203
+ schemaName: table.schema,
204
+ collection,
205
+ pkColumn
206
+ });
207
+ }
208
+ return results;
209
+ }
210
+ /** A character an unquoted identifier or keyword can hold, `$` included. */
211
+ var IDENTIFIER_CHARACTER = /[A-Za-z0-9_$]/;
212
+ /**
213
+ * Cut `sqlText` into code and the things in it whose text is data.
214
+ *
215
+ * A lexer, not a parser: it reads text the parser refuses as well, which is
216
+ * most of what a person types while writing a query.
217
+ */
218
+ function splitSql(sqlText) {
219
+ const segments = [];
220
+ let code = "";
221
+ const push = (text, kind) => {
222
+ if (code) segments.push({
223
+ text: code,
224
+ kind: "code"
225
+ });
226
+ code = "";
227
+ segments.push({
228
+ text,
229
+ kind
230
+ });
231
+ };
232
+ let i = 0;
233
+ while (i < sqlText.length) {
234
+ const ch = sqlText[i];
235
+ const next = sqlText[i + 1];
236
+ if (ch === "-" && next === "-") {
237
+ const newline = sqlText.indexOf("\n", i);
238
+ const end = newline === -1 ? sqlText.length : newline + 1;
239
+ push(sqlText.slice(i, end), "line comment");
240
+ i = end;
241
+ continue;
242
+ }
243
+ if (ch === "/" && next === "*") {
244
+ let depth = 1;
245
+ let j = i + 2;
246
+ while (j < sqlText.length && depth > 0) if (sqlText[j] === "/" && sqlText[j + 1] === "*") {
247
+ depth++;
248
+ j += 2;
249
+ } else if (sqlText[j] === "*" && sqlText[j + 1] === "/") {
250
+ depth--;
251
+ j += 2;
252
+ } else j++;
253
+ push(sqlText.slice(i, j), "block comment");
254
+ i = j;
255
+ continue;
256
+ }
257
+ if (ch === "'" || ch === "\"") {
258
+ const backslashEscapes = ch === "'" && /(^|[^A-Za-z0-9_$])[eE]$/.test(code);
259
+ let j = i + 1;
260
+ while (j < sqlText.length) if (backslashEscapes && sqlText[j] === "\\") j += 2;
261
+ else if (sqlText[j] === ch && sqlText[j + 1] === ch) j += 2;
262
+ else if (sqlText[j] === ch) {
263
+ j++;
264
+ break;
265
+ } else j++;
266
+ push(sqlText.slice(i, j), ch === "'" ? "literal" : "identifier");
267
+ i = j;
268
+ continue;
269
+ }
270
+ if (ch === "$" && !IDENTIFIER_CHARACTER.test(code.slice(-1))) {
271
+ const tag = /^\$(?:[A-Za-z_][A-Za-z0-9_]*)?\$/.exec(sqlText.slice(i));
272
+ if (tag) {
273
+ const close = sqlText.indexOf(tag[0], i + tag[0].length);
274
+ const end = close === -1 ? sqlText.length : close + tag[0].length;
275
+ push(sqlText.slice(i, end), "literal");
276
+ i = end;
277
+ continue;
278
+ }
279
+ }
280
+ code += ch;
281
+ i++;
282
+ }
283
+ if (code) segments.push({
284
+ text: code,
285
+ kind: "code"
286
+ });
287
+ return segments;
288
+ }
289
+ /**
290
+ * The console's "Format SQL": runs of whitespace become one space, a comma is
291
+ * followed by one, and an `=` standing alone gets one on each side — in the
292
+ * code only.
293
+ *
294
+ * A literal, a quoted identifier and a comment are copied as they are: their
295
+ * text is data, and rewriting `'a,b'` to `'a, b'` changes the value the next
296
+ * Run writes. A line comment keeps the line break that ends it, or it would
297
+ * swallow the code after it. `>=`, `<=`, `!=`, `:=` and `=>` are operators of
298
+ * their own and are left whole.
299
+ */
300
+ function formatSql(sqlText) {
301
+ const segments = splitSql(sqlText);
302
+ return segments.map((segment, index) => {
303
+ if (segment.kind !== "code") return segment.text;
304
+ const tidied = segment.text.replace(/\s+/g, " ").replace(/\s*,\s*/g, ", ").replace(/(?<![<>!:=~*+\-/%^&|#@?])\s*=\s*(?![=>~*<])/g, " = ");
305
+ return segments[index - 1]?.kind === "line comment" ? tidied.trimStart() : tidied;
306
+ }).join("").trim();
307
+ }
308
+ /**
309
+ * `sqlText` with the console's automatic `LIMIT` on it, or `null` when it
310
+ * takes none (see {@link acceptsAutoLimit}).
311
+ *
312
+ * The limit goes straight after the statement's last token — before a
313
+ * trailing comment, which would otherwise swallow it, and in place of a
314
+ * trailing `;`. A query that computes an aggregate is limited like any other:
315
+ * the limit applies to the rows it returns, so a `count(*)` still counts every
316
+ * row, and a window `count(*) OVER ()` is no longer a way past the cap.
317
+ */
318
+ function withAutoLimit(sqlText, limit) {
319
+ if (!acceptsAutoLimit(sqlText)) return null;
320
+ const segments = splitSql(sqlText);
321
+ const isComment = (segment) => segment.kind === "line comment" || segment.kind === "block comment";
322
+ const isFiller = (segment) => isComment(segment) || segment.kind === "code" && /^[\s;]*$/.test(segment.text);
323
+ let last = segments.length - 1;
324
+ while (last >= 0 && isFiller(segments[last])) last--;
325
+ if (last < 0) return null;
326
+ const lastSegment = segments[last];
327
+ const head = lastSegment.kind === "code" ? lastSegment.text.replace(/[\s;]+$/, "") : lastSegment.text;
328
+ const tail = (lastSegment.kind === "code" ? lastSegment.text.slice(head.length) : "").replace(/;/g, "") + segments.slice(last + 1).map((segment) => segment.kind === "code" ? segment.text.replace(/;/g, "") : segment.text).join("");
329
+ return `${segments.slice(0, last).map((segment) => segment.text).join("")}${head} LIMIT ${limit};${tail.trimEnd()}`;
330
+ }
331
+ /**
332
+ * Rows as CSV text: a header record of column names, then one record per
333
+ * row, each value under its own column.
334
+ *
335
+ * RFC 4180 with every record ended by CRLF — byte for byte what
336
+ * `POST /api/admin/sql/csv` streams for the same result, so "Export CSV" and
337
+ * "Download full result as CSV" write one format. They used to differ in
338
+ * the line break (LF here), which a diff, a checksum or a strict reader
339
+ * notices.
340
+ */
341
+ function toCsv(columns, rows) {
342
+ return [csvRecord(columns), ...rows.map((row) => csvRecord(columns.map((column) => row[column])))].map((record) => record + "\r\n").join("");
343
+ }
344
+ /**
345
+ * The rows on screen as CSV text — {@link toCsv} over the first row's
346
+ * columns.
347
+ *
348
+ * A value was quoted only when it held a comma, and a quote in it was never
349
+ * doubled nor a line break quoted, so `say "hi"` or a two-line note broke the
350
+ * row it was in.
351
+ */
352
+ function resultsToCsv(rows) {
353
+ if (rows.length === 0) return "";
354
+ return toCsv(Object.keys(rows[0]), rows);
355
+ }
356
+ /**
357
+ * The widths kept, at most `limit` queries' worth, the most recently resized
358
+ * last.
359
+ *
360
+ * They are kept per query text, in `localStorage`, and used to be kept
361
+ * forever: every query anybody ever resized a column of stayed, whole SQL
362
+ * text and all, in an origin's storage — which the console's tabs, snippets
363
+ * and history share, and which a full store refuses to write at all. The
364
+ * oldest go first; resizing a query's column again makes it the newest.
365
+ */
366
+ function rememberColumnWidth(widths, sql, column, width, limit = 50) {
367
+ const { [sql]: current, ...others } = widths;
368
+ return keepLatestColumnWidths({
369
+ ...others,
370
+ [sql]: {
371
+ ...current,
372
+ [column]: width
373
+ }
374
+ }, limit);
375
+ }
376
+ /**
377
+ * The `limit` most recent queries' widths — the last ones in insertion
378
+ * order — and only entries that are a column → width record.
379
+ */
380
+ function keepLatestColumnWidths(widths, limit = 50) {
381
+ const kept = {};
382
+ for (const [sql, value] of Object.entries(widths).slice(-limit)) {
383
+ if (typeof value !== "object" || value === null || Array.isArray(value)) continue;
384
+ const columns = {};
385
+ for (const [column, width] of Object.entries(value)) if (typeof width === "number" && Number.isFinite(width)) columns[column] = width;
386
+ kept[sql] = columns;
387
+ }
388
+ return kept;
389
+ }
390
+ /**
391
+ * The schema browser's tables, by schema, from `information_schema.columns`
392
+ * rows: each table in the order it first appears, its columns in row order.
393
+ *
394
+ * A table is found by its key, not looked up among the schema's tables: that
395
+ * lookup ran once per row over every table so far — 142 ms at 2,000 tables,
396
+ * 923 ms at 5,000, on every open of the console.
397
+ */
398
+ function groupSchemaRows(data) {
399
+ const grouped = {};
400
+ const tables = /* @__PURE__ */ new Map();
401
+ for (const curr of data) {
402
+ const schema = curr.schema || curr.SCHEMA || curr.table_schema || "public";
403
+ const table = curr.table || curr.TABLE || curr.table_name;
404
+ const column = curr.column || curr.COLUMN || curr.column_name;
405
+ const dataType = curr.data_type || curr.DATA_TYPE || "";
406
+ const isPrimaryKey = curr.is_pk === true || curr.is_pk === "true";
407
+ const key = `${schema}\u0000${table}`;
408
+ let tableInfo = tables.get(key);
409
+ if (!tableInfo) {
410
+ tableInfo = {
411
+ schemaName: schema,
412
+ tableName: table,
413
+ columns: []
414
+ };
415
+ tables.set(key, tableInfo);
416
+ (grouped[schema] ??= []).push(tableInfo);
417
+ }
418
+ tableInfo.columns.push({
419
+ name: column,
420
+ dataType,
421
+ isPrimaryKey
422
+ });
423
+ }
424
+ return grouped;
425
+ }
426
+ var INTEGER_TYPES = /* @__PURE__ */ new Set([
427
+ "smallint",
428
+ "integer",
429
+ "bigint"
430
+ ]);
431
+ var FLOAT_TYPES = /* @__PURE__ */ new Set(["real", "double precision"]);
432
+ /** How many significant digits a plain decimal (`-0012.3400`) has. */
433
+ function significantDigits(decimal) {
434
+ return decimal.replace(/^-/, "").replace(".", "").replace(/^0+/, "").replace(/0+$/, "").length;
435
+ }
436
+ /**
437
+ * One value of the console's result, read by its column's type: an integer a
438
+ * double holds exactly, a float, a numeric of at most 15 significant digits,
439
+ * a boolean (`t`/`f`), json. Anything else — and any value that would change
440
+ * on the way (a bigint past 2^53, a 20-digit numeric, `NaN`) — stays the
441
+ * database's text. A value that is not text (a backend that returned typed
442
+ * rows) or a column with no type is left as it came.
443
+ */
444
+ function typedValue(value, type) {
445
+ if (typeof value !== "string" || !type) return value;
446
+ const base = type.replace(/\(.*\)$/, "").trim();
447
+ if (INTEGER_TYPES.has(base)) {
448
+ const number = Number(value);
449
+ return /^-?\d+$/.test(value) && Number.isSafeInteger(number) ? number : value;
450
+ }
451
+ if (FLOAT_TYPES.has(base)) {
452
+ const number = Number(value);
453
+ return value.trim() !== "" && Number.isFinite(number) ? number : value;
454
+ }
455
+ if (base === "numeric") return /^-?\d+(\.\d+)?$/.test(value) && significantDigits(value) <= 15 ? Number(value) : value;
456
+ if (base === "boolean") return value === "t" ? true : value === "f" ? false : value;
457
+ if (base === "json" || base === "jsonb") try {
458
+ return JSON.parse(value);
459
+ } catch {
460
+ return value;
461
+ }
462
+ return value;
463
+ }
464
+ /**
465
+ * The console's rows for a typed JSON export: each value read by the type of
466
+ * the column it is in (see `typedValue`), where "Export JSON" writes the
467
+ * database's text as the console holds it. A row holds the last of two
468
+ * columns that share a name, so that column's type is the one read.
469
+ */
470
+ function typedJsonRows(rows, columns) {
471
+ const typeByName = /* @__PURE__ */ new Map();
472
+ for (const column of columns) typeByName.set(column.name, column.type);
473
+ return rows.map((row) => Object.fromEntries(Object.entries(row).map(([key, value]) => [key, typedValue(value, typeByName.get(key))])));
474
+ }
475
+ //#endregion
476
+ export { needsDestructiveConfirmation as a, rememberColumnWidth as c, resultsToCsv as d, toCsv as f, keepLatestColumnWidths as i, resolveCellEdit as l, withAutoLimit as m, formatSql as n, quoteIdentifier as o, typedJsonRows as p, groupSchemaRows as r, quoteTableName as s, buildExplainSql as t, resolveQueryCollections as u };
477
+
478
+ //# sourceMappingURL=sql_utils-BZD31xE3.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sql_utils-BZD31xE3.js","names":[],"sources":["../src/utils/sql_utils.ts"],"sourcesContent":["import { astVisitor, parse, type Statement } from \"pgsql-ast-parser\";\nimport { csvRecord, toSnakeCase } from \"@rebasepro/utils\";\nimport type { AdminCollection } from \"@rebasepro/cms-types\";\nimport type { SqlScriptResult } from \"@rebasepro/types\";\nimport type { TableInfo } from \"../components/SQLEditor/sql_editor_types\";\n\n/** The statements in `sqlText`, or `null` when the parser cannot read it. */\nfunction parseStatements(sqlText: string): Statement[] | null {\n try {\n return parse(sqlText);\n } catch {\n return null;\n }\n}\n\n/**\n * The `EXPLAIN` for one statement, or `null` when `sqlText` is not exactly one.\n *\n * Never `ANALYZE`: that executes the statement to time it, so explaining a\n * `DELETE` deleted the rows — without the confirmation \"Run\" asks for.\n *\n * And never more than one statement. Sent as one simple query, `EXPLAIN (…)\n * SELECT 1; DELETE …` explains the SELECT and *runs* the DELETE. Text the\n * parser cannot read is still explained, as long as no `;` could be hiding a\n * second statement in it.\n */\nexport function buildExplainSql(sqlText: string): string | null {\n const statement = sqlText.trim().replace(/;+$/, \"\").trim();\n if (!statement) return null;\n const statements = parseStatements(statement);\n const single = statements ? statements.length === 1 : !statement.includes(\";\");\n return single ? `EXPLAIN (FORMAT JSON) ${statement}` : null;\n}\n\n/**\n * Whether the console's automatic `LIMIT` may be appended to `sqlText`: one\n * top-level SELECT that has no limit of its own.\n *\n * Decided on the parsed statement, not the text. A search for the word SELECT\n * also found it inside `INSERT INTO … SELECT` and `CREATE TABLE … AS SELECT`,\n * which then copied a thousand rows and reported success, and at the start of\n * a script whose last statement the appended `LIMIT` turned into a syntax\n * error. Text the parser cannot read is left as written.\n */\nexport function acceptsAutoLimit(sqlText: string): boolean {\n const statements = parseStatements(sqlText);\n if (!statements || statements.length !== 1) return false;\n const [statement] = statements;\n return statement.type === \"select\" && !statement.limit;\n}\n\n/** A destructive command named anywhere in a text, comments included. */\nconst DESTRUCTIVE_COMMAND = /\\b(DELETE|UPDATE|DROP|TRUNCATE)\\b/i;\n\nfunction isDestructiveStatement(statement: Statement): boolean {\n switch (statement.type) {\n case \"delete\":\n case \"update\":\n return !statement.where;\n case \"truncate table\":\n case \"drop table\":\n case \"drop sequence\":\n case \"drop index\":\n case \"drop type\":\n case \"drop trigger\":\n case \"drop function\":\n return true;\n case \"alter table\":\n return statement.changes.some(change => change.type === \"drop column\" || change.type === \"drop constraint\");\n case \"with\":\n return statement.bind.some(binding => isDestructiveStatement(binding.statement)) ||\n isDestructiveStatement(statement.in);\n case \"with recursive\":\n return isDestructiveStatement(statement.in);\n case \"do\":\n return DESTRUCTIVE_COMMAND.test(statement.code);\n default:\n return false;\n }\n}\n\n/**\n * Whether \"Run\" asks before running `sqlText`: a statement in it deletes or\n * updates without a WHERE of its own, drops or truncates.\n *\n * Decided per parsed statement, not on the text. A search of the text for the\n * word WHERE found it in another statement or in a comment, so `DELETE FROM\n * posts; SELECT * FROM posts WHERE id = 1` deleted every post without asking.\n * Text the parser cannot read is confirmed whenever it names a destructive\n * command.\n */\nexport function needsDestructiveConfirmation(sqlText: string): boolean {\n const statements = parseStatements(sqlText);\n if (!statements) return DESTRUCTIVE_COMMAND.test(sqlText);\n return statements.some(isDestructiveStatement);\n}\n\n/**\n * A table as SQL: `\"schema\".\"table\"` when a schema is given, a bare `\"table\"`\n * otherwise.\n */\nexport function quoteTableName(tableName: string, schemaName?: string): string {\n return schemaName ? `${quoteIdentifier(schemaName)}.${quoteIdentifier(tableName)}` : quoteIdentifier(tableName);\n}\n\n/** An identifier as SQL, whatever it holds: double-quoted, with `\"` doubled. */\nexport function quoteIdentifier(identifier: string): string {\n return `\"${identifier.replace(/\"/g, \"\\\"\\\"\")}\"`;\n}\n\n/** What the console knows about a result: its columns, and the tables they came from. */\nexport type ResultProvenance = Pick<SqlScriptResult, \"columns\" | \"tables\">;\n\n/**\n * The stored cell a result cell is, and the key that finds its row.\n */\nexport interface CellEditTarget {\n schema: string;\n table: string;\n /** The table column the edited value is written to. */\n column: string;\n /** Each primary key column of the table, and the result column holding its value for the row. */\n key: { column: string; resultColumn: string }[];\n}\n\n/**\n * Why a cell cannot be edited: a translation key of the console's, and its\n * parameters.\n */\nexport interface CellEditRefusal {\n key:\n | \"studio_sql_edit_unreadable_query\"\n | \"studio_sql_edit_not_a_select\"\n | \"studio_sql_edit_duplicate_column\"\n | \"studio_sql_edit_computed_column\"\n | \"studio_sql_edit_not_a_table\"\n | \"studio_sql_edit_inherited_table\"\n | \"studio_sql_edit_no_primary_key\"\n | \"studio_sql_edit_key_missing\"\n | \"studio_sql_edit_table_read_twice\";\n params: Record<string, string>;\n}\n\nexport type CellEditResolution =\n | { target: CellEditTarget; refusal?: undefined }\n | { target?: undefined; refusal: CellEditRefusal };\n\nconst refuse = (key: CellEditRefusal[\"key\"], params: Record<string, string> = {}): CellEditResolution =>\n ({ refusal: { key, params } });\n\n/**\n * Which stored cell an edit of the result column `resultColumn` writes, and\n * how its row is found again — or why no edit is safe.\n *\n * Decided on what the database reported, not on the query's text. Each\n * column of the result says which table column it was read from, or that it\n * was computed; a cell is editable only when it was read from a column of a\n * table, every column of that table's primary key was read too, and each of\n * them is the only column of the result with its name — a row holds one value\n * per name.\n *\n * The text was the only evidence before, and it was wrong twice over. For\n * `SELECT p.id, a.name FROM posts p JOIN authors a …` it took the post's `id`\n * for the author's key and updated whichever author shared the post's id.\n * For `SELECT id, lower(email) AS name FROM users` it wrote the edited text\n * to `users.name`.\n *\n * The database reports a table, not which mention of it: in a self-join both\n * sides are `authors`, and a key from one with a value from the other finds\n * the wrong row. So the query is still read, for one thing — the table must\n * be read once, and nothing it is read through may be read twice.\n */\nexport function resolveCellEdit(sqlText: string, provenance: ResultProvenance, resultColumn: string): CellEditResolution {\n const sameName = provenance.columns.filter(column => column.name === resultColumn);\n if (sameName.length !== 1) return refuse(\"studio_sql_edit_duplicate_column\", { column: resultColumn });\n const source = sameName[0].source;\n if (!source) return refuse(\"studio_sql_edit_computed_column\", { column: resultColumn });\n\n const tableLabel = `${source.schema}.${source.table}`;\n const table = provenance.tables.find(t => t.schema === source.schema && t.table === source.table);\n if (!table || (table.kind !== \"table\" && table.kind !== \"partitioned table\")) {\n return refuse(\"studio_sql_edit_not_a_table\", { column: resultColumn, table: tableLabel });\n }\n if (table.hasInheritors) return refuse(\"studio_sql_edit_inherited_table\", { table: tableLabel });\n if (table.primaryKey.length === 0) return refuse(\"studio_sql_edit_no_primary_key\", { table: tableLabel });\n\n const key: CellEditTarget[\"key\"] = [];\n for (const keyColumn of table.primaryKey) {\n const holders = provenance.columns.filter(column =>\n column.source?.schema === source.schema &&\n column.source.table === source.table &&\n column.source.column === keyColumn);\n const holder = holders.length === 1 && provenance.columns.filter(column => column.name === holders[0].name).length === 1\n ? holders[0]\n : undefined;\n if (!holder) return refuse(\"studio_sql_edit_key_missing\", { table: tableLabel, columns: table.primaryKey.join(\", \") });\n key.push({ column: keyColumn, resultColumn: holder.name });\n }\n\n const readOnce = tableReadOnce(sqlText, source.table);\n if (readOnce !== true) return readOnce;\n\n return { target: { schema: source.schema, table: source.table, column: source.column, key } };\n}\n\n/**\n * Whether `sqlText` is one SELECT that reads `table` once — counting every\n * mention of a table of that name, in any schema and at any depth, and every\n * mention of a `WITH` query, since one read twice reads its tables twice.\n */\nfunction tableReadOnce(sqlText: string, table: string): true | CellEditResolution {\n const statements = parseStatements(sqlText);\n if (!statements) return refuse(\"studio_sql_edit_unreadable_query\");\n if (statements.length !== 1) return refuse(\"studio_sql_edit_not_a_select\");\n const [statement] = statements;\n const isRead = statement.type === \"select\" ||\n (statement.type === \"with\" &&\n statement.in.type === \"select\" &&\n statement.bind.every(binding => binding.statement.type === \"select\"));\n if (!isRead) return refuse(\"studio_sql_edit_not_a_select\");\n\n const mentions = new Map<string, number>();\n const withNames: string[] = [];\n astVisitor(visitor => ({\n fromTable: from => {\n const name = from.name.name.toLowerCase();\n mentions.set(name, (mentions.get(name) ?? 0) + 1);\n visitor.super().fromTable(from);\n },\n with: withStatement => {\n withNames.push(...withStatement.bind.map(binding => binding.alias.name.toLowerCase()));\n visitor.super().with(withStatement);\n }\n })).statement(statement);\n\n const readTwice = (mentions.get(table.toLowerCase()) ?? 0) !== 1 ||\n withNames.some(name => (mentions.get(name) ?? 0) > 1);\n return readTwice ? refuse(\"studio_sql_edit_table_read_twice\", { table }) : true;\n}\n\n/**\n * A collection whose records a query's rows can be opened as.\n */\nexport interface ResolvedQueryCollection {\n /** The table, as the database named it. */\n tableName: string;\n schemaName: string;\n /** The matched collection */\n collection: AdminCollection;\n /** The result column that holds the table's primary key. */\n pkColumn: string;\n}\n\n/**\n * The collections a result's rows can be opened as: those whose table one of\n * the result's columns was read from, with the table's (single-column) primary\n * key read too, under a name no other column of the result has.\n *\n * Matched on the provenance the database reported. A text match took the\n * result's `id` for every table of a join: `SELECT p.id, a.name FROM posts p\n * JOIN authors a …` offered to open the author whose id was the post's.\n */\nexport function resolveQueryCollections(provenance: ResultProvenance, collections: AdminCollection[]): ResolvedQueryCollection[] {\n const results: ResolvedQueryCollection[] = [];\n for (const table of provenance.tables) {\n const collection = collections.find(c => {\n const tableName = (\"table\" in c ? c.table : undefined) || toSnakeCase(c.slug);\n const schemaName = (\"schema\" in c ? c.schema : undefined) || \"public\";\n return tableName === table.table && schemaName === table.schema;\n });\n if (!collection || table.primaryKey.length !== 1) continue;\n const [keyColumn] = table.primaryKey;\n const holders = provenance.columns.filter(column =>\n column.source?.schema === table.schema &&\n column.source.table === table.table &&\n column.source.column === keyColumn);\n if (holders.length !== 1) continue;\n const pkColumn = holders[0].name;\n if (provenance.columns.filter(column => column.name === pkColumn).length !== 1) continue;\n results.push({ tableName: table.table, schemaName: table.schema, collection, pkColumn });\n }\n return results;\n}\n\n/**\n * A stretch of SQL text, by what it is to the database: code, or text that is\n * data and must not be rewritten — a string literal (quoted, `E'…'` or\n * dollar-quoted), a quoted identifier, or a comment.\n */\ninterface SqlSegment {\n text: string;\n kind: \"code\" | \"literal\" | \"identifier\" | \"line comment\" | \"block comment\";\n}\n\n/** A character an unquoted identifier or keyword can hold, `$` included. */\nconst IDENTIFIER_CHARACTER = /[A-Za-z0-9_$]/;\n\n/**\n * Cut `sqlText` into code and the things in it whose text is data.\n *\n * A lexer, not a parser: it reads text the parser refuses as well, which is\n * most of what a person types while writing a query.\n */\nfunction splitSql(sqlText: string): SqlSegment[] {\n const segments: SqlSegment[] = [];\n let code = \"\";\n const push = (text: string, kind: SqlSegment[\"kind\"]) => {\n if (code) segments.push({ text: code, kind: \"code\" });\n code = \"\";\n segments.push({ text, kind });\n };\n let i = 0;\n while (i < sqlText.length) {\n const ch = sqlText[i];\n const next = sqlText[i + 1];\n if (ch === \"-\" && next === \"-\") {\n const newline = sqlText.indexOf(\"\\n\", i);\n const end = newline === -1 ? sqlText.length : newline + 1;\n push(sqlText.slice(i, end), \"line comment\");\n i = end;\n continue;\n }\n if (ch === \"/\" && next === \"*\") {\n // Block comments nest in Postgres.\n let depth = 1;\n let j = i + 2;\n while (j < sqlText.length && depth > 0) {\n if (sqlText[j] === \"/\" && sqlText[j + 1] === \"*\") {\n depth++;\n j += 2;\n } else if (sqlText[j] === \"*\" && sqlText[j + 1] === \"/\") {\n depth--;\n j += 2;\n } else {\n j++;\n }\n }\n push(sqlText.slice(i, j), \"block comment\");\n i = j;\n continue;\n }\n if (ch === \"'\" || ch === \"\\\"\") {\n // `E'…'` takes backslash escapes; every quoted text takes its\n // quote doubled.\n const backslashEscapes = ch === \"'\" && /(^|[^A-Za-z0-9_$])[eE]$/.test(code);\n let j = i + 1;\n while (j < sqlText.length) {\n if (backslashEscapes && sqlText[j] === \"\\\\\") {\n j += 2;\n } else if (sqlText[j] === ch && sqlText[j + 1] === ch) {\n j += 2;\n } else if (sqlText[j] === ch) {\n j++;\n break;\n } else {\n j++;\n }\n }\n push(sqlText.slice(i, j), ch === \"'\" ? \"literal\" : \"identifier\");\n i = j;\n continue;\n }\n if (ch === \"$\" && !IDENTIFIER_CHARACTER.test(code.slice(-1))) {\n // `$tag$…$tag$` — not `$1`, a parameter, whose tag would start\n // with a digit.\n const tag = /^\\$(?:[A-Za-z_][A-Za-z0-9_]*)?\\$/.exec(sqlText.slice(i));\n if (tag) {\n const close = sqlText.indexOf(tag[0], i + tag[0].length);\n const end = close === -1 ? sqlText.length : close + tag[0].length;\n push(sqlText.slice(i, end), \"literal\");\n i = end;\n continue;\n }\n }\n code += ch;\n i++;\n }\n if (code) segments.push({ text: code, kind: \"code\" });\n return segments;\n}\n\n/**\n * The console's \"Format SQL\": runs of whitespace become one space, a comma is\n * followed by one, and an `=` standing alone gets one on each side — in the\n * code only.\n *\n * A literal, a quoted identifier and a comment are copied as they are: their\n * text is data, and rewriting `'a,b'` to `'a, b'` changes the value the next\n * Run writes. A line comment keeps the line break that ends it, or it would\n * swallow the code after it. `>=`, `<=`, `!=`, `:=` and `=>` are operators of\n * their own and are left whole.\n */\nexport function formatSql(sqlText: string): string {\n const segments = splitSql(sqlText);\n return segments.map((segment, index) => {\n if (segment.kind !== \"code\") return segment.text;\n const tidied = segment.text\n .replace(/\\s+/g, \" \")\n .replace(/\\s*,\\s*/g, \", \")\n .replace(/(?<![<>!:=~*+\\-/%^&|#@?])\\s*=\\s*(?![=>~*<])/g, \" = \");\n return segments[index - 1]?.kind === \"line comment\" ? tidied.trimStart() : tidied;\n }).join(\"\").trim();\n}\n\n/**\n * `sqlText` with the console's automatic `LIMIT` on it, or `null` when it\n * takes none (see {@link acceptsAutoLimit}).\n *\n * The limit goes straight after the statement's last token — before a\n * trailing comment, which would otherwise swallow it, and in place of a\n * trailing `;`. A query that computes an aggregate is limited like any other:\n * the limit applies to the rows it returns, so a `count(*)` still counts every\n * row, and a window `count(*) OVER ()` is no longer a way past the cap.\n */\nexport function withAutoLimit(sqlText: string, limit: number): string | null {\n if (!acceptsAutoLimit(sqlText)) return null;\n const segments = splitSql(sqlText);\n const isComment = (segment: SqlSegment) => segment.kind === \"line comment\" || segment.kind === \"block comment\";\n const isFiller = (segment: SqlSegment) => isComment(segment) || (segment.kind === \"code\" && /^[\\s;]*$/.test(segment.text));\n let last = segments.length - 1;\n while (last >= 0 && isFiller(segments[last])) last--;\n if (last < 0) return null;\n\n const lastSegment = segments[last];\n const head = lastSegment.kind === \"code\" ? lastSegment.text.replace(/[\\s;]+$/, \"\") : lastSegment.text;\n const after = lastSegment.kind === \"code\" ? lastSegment.text.slice(head.length) : \"\";\n // What follows the statement is comments and the space between them; its\n // `;` are dropped, the limit brings its own.\n const tail = after.replace(/;/g, \"\") + segments.slice(last + 1)\n .map(segment => segment.kind === \"code\" ? segment.text.replace(/;/g, \"\") : segment.text)\n .join(\"\");\n const before = segments.slice(0, last).map(segment => segment.text).join(\"\");\n return `${before}${head} LIMIT ${limit};${tail.trimEnd()}`;\n}\n\n/**\n * Rows as CSV text: a header record of column names, then one record per\n * row, each value under its own column.\n *\n * RFC 4180 with every record ended by CRLF — byte for byte what\n * `POST /api/admin/sql/csv` streams for the same result, so \"Export CSV\" and\n * \"Download full result as CSV\" write one format. They used to differ in\n * the line break (LF here), which a diff, a checksum or a strict reader\n * notices.\n */\nexport function toCsv(columns: readonly string[], rows: readonly Record<string, unknown>[]): string {\n const records = [csvRecord(columns), ...rows.map(row => csvRecord(columns.map(column => row[column])))];\n return records.map(record => record + \"\\r\\n\").join(\"\");\n}\n\n/**\n * The rows on screen as CSV text — {@link toCsv} over the first row's\n * columns.\n *\n * A value was quoted only when it held a comma, and a quote in it was never\n * doubled nor a line break quoted, so `say \"hi\"` or a two-line note broke the\n * row it was in.\n */\nexport function resultsToCsv(rows: Record<string, unknown>[]): string {\n if (rows.length === 0) return \"\";\n return toCsv(Object.keys(rows[0]), rows);\n}\n\n/** Column widths per query, as the console keeps them: SQL text → column → width. */\nexport type ColumnWidthsByQuery = Record<string, Record<string, number>>;\n\n/** How many queries' column widths the console remembers. */\nexport const COLUMN_WIDTHS_REMEMBERED = 50;\n\n/**\n * The widths kept, at most `limit` queries' worth, the most recently resized\n * last.\n *\n * They are kept per query text, in `localStorage`, and used to be kept\n * forever: every query anybody ever resized a column of stayed, whole SQL\n * text and all, in an origin's storage — which the console's tabs, snippets\n * and history share, and which a full store refuses to write at all. The\n * oldest go first; resizing a query's column again makes it the newest.\n */\nexport function rememberColumnWidth(\n widths: ColumnWidthsByQuery,\n sql: string,\n column: string,\n width: number,\n limit: number = COLUMN_WIDTHS_REMEMBERED\n): ColumnWidthsByQuery {\n const { [sql]: current, ...others } = widths;\n return keepLatestColumnWidths({ ...others, [sql]: { ...current, [column]: width } }, limit);\n}\n\n/**\n * The `limit` most recent queries' widths — the last ones in insertion\n * order — and only entries that are a column → width record.\n */\nexport function keepLatestColumnWidths(widths: Record<string, unknown>, limit: number = COLUMN_WIDTHS_REMEMBERED): ColumnWidthsByQuery {\n const kept: ColumnWidthsByQuery = {};\n for (const [sql, value] of Object.entries(widths).slice(-limit)) {\n if (typeof value !== \"object\" || value === null || Array.isArray(value)) continue;\n const columns: Record<string, number> = {};\n for (const [column, width] of Object.entries(value)) {\n if (typeof width === \"number\" && Number.isFinite(width)) columns[column] = width;\n }\n kept[sql] = columns;\n }\n return kept;\n}\n\n/**\n * The schema browser's tables, by schema, from `information_schema.columns`\n * rows: each table in the order it first appears, its columns in row order.\n *\n * A table is found by its key, not looked up among the schema's tables: that\n * lookup ran once per row over every table so far — 142 ms at 2,000 tables,\n * 923 ms at 5,000, on every open of the console.\n */\nexport function groupSchemaRows(data: Record<string, unknown>[]): Record<string, TableInfo[]> {\n const grouped: Record<string, TableInfo[]> = {};\n const tables = new Map<string, TableInfo>();\n for (const curr of data) {\n const schema = (curr.schema || curr.SCHEMA || curr.table_schema || \"public\") as string;\n const table = (curr.table || curr.TABLE || curr.table_name) as string;\n const column = (curr.column || curr.COLUMN || curr.column_name) as string;\n const dataType = (curr.data_type || curr.DATA_TYPE || \"\") as string;\n const isPrimaryKey = curr.is_pk === true || curr.is_pk === \"true\";\n\n const key = `${schema}\\u0000${table}`;\n let tableInfo = tables.get(key);\n if (!tableInfo) {\n tableInfo = { schemaName: schema, tableName: table, columns: [] };\n tables.set(key, tableInfo);\n (grouped[schema] ??= []).push(tableInfo);\n }\n tableInfo.columns.push({ name: column, dataType, isPrimaryKey });\n }\n return grouped;\n}\n\nconst INTEGER_TYPES = new Set([\"smallint\", \"integer\", \"bigint\"]);\nconst FLOAT_TYPES = new Set([\"real\", \"double precision\"]);\n\n/** How many significant digits a plain decimal (`-0012.3400`) has. */\nfunction significantDigits(decimal: string): number {\n const digits = decimal.replace(/^-/, \"\").replace(\".\", \"\").replace(/^0+/, \"\");\n return digits.replace(/0+$/, \"\").length;\n}\n\n/**\n * One value of the console's result, read by its column's type: an integer a\n * double holds exactly, a float, a numeric of at most 15 significant digits,\n * a boolean (`t`/`f`), json. Anything else — and any value that would change\n * on the way (a bigint past 2^53, a 20-digit numeric, `NaN`) — stays the\n * database's text. A value that is not text (a backend that returned typed\n * rows) or a column with no type is left as it came.\n */\nfunction typedValue(value: unknown, type: string | undefined): unknown {\n if (typeof value !== \"string\" || !type) return value;\n const base = type.replace(/\\(.*\\)$/, \"\").trim();\n if (INTEGER_TYPES.has(base)) {\n const number = Number(value);\n return /^-?\\d+$/.test(value) && Number.isSafeInteger(number) ? number : value;\n }\n if (FLOAT_TYPES.has(base)) {\n const number = Number(value);\n return value.trim() !== \"\" && Number.isFinite(number) ? number : value;\n }\n if (base === \"numeric\") {\n return /^-?\\d+(\\.\\d+)?$/.test(value) && significantDigits(value) <= 15 ? Number(value) : value;\n }\n if (base === \"boolean\") {\n return value === \"t\" ? true : value === \"f\" ? false : value;\n }\n if (base === \"json\" || base === \"jsonb\") {\n try {\n return JSON.parse(value);\n } catch {\n return value;\n }\n }\n return value;\n}\n\n/**\n * The console's rows for a typed JSON export: each value read by the type of\n * the column it is in (see `typedValue`), where \"Export JSON\" writes the\n * database's text as the console holds it. A row holds the last of two\n * columns that share a name, so that column's type is the one read.\n */\nexport function typedJsonRows(\n rows: Record<string, unknown>[],\n columns: readonly { name: string; type?: string }[]\n): Record<string, unknown>[] {\n const typeByName = new Map<string, string | undefined>();\n for (const column of columns) typeByName.set(column.name, column.type);\n return rows.map(row => Object.fromEntries(\n Object.entries(row).map(([key, value]) => [key, typedValue(value, typeByName.get(key))])\n ));\n}\n"],"mappings":"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"}
@@ -1,5 +1,6 @@
1
1
  import type { AdminCollection } from "@rebasepro/cms-types";
2
2
  import type { SqlScriptResult } from "@rebasepro/types";
3
+ import type { TableInfo } from "../components/SQLEditor/sql_editor_types.js";
3
4
  /**
4
5
  * The `EXPLAIN` for one statement, or `null` when `sqlText` is not exactly one.
5
6
  *
@@ -140,3 +141,62 @@ export declare function formatSql(sqlText: string): string;
140
141
  * row, and a window `count(*) OVER ()` is no longer a way past the cap.
141
142
  */
142
143
  export declare function withAutoLimit(sqlText: string, limit: number): string | null;
144
+ /**
145
+ * Rows as CSV text: a header record of column names, then one record per
146
+ * row, each value under its own column.
147
+ *
148
+ * RFC 4180 with every record ended by CRLF — byte for byte what
149
+ * `POST /api/admin/sql/csv` streams for the same result, so "Export CSV" and
150
+ * "Download full result as CSV" write one format. They used to differ in
151
+ * the line break (LF here), which a diff, a checksum or a strict reader
152
+ * notices.
153
+ */
154
+ export declare function toCsv(columns: readonly string[], rows: readonly Record<string, unknown>[]): string;
155
+ /**
156
+ * The rows on screen as CSV text — {@link toCsv} over the first row's
157
+ * columns.
158
+ *
159
+ * A value was quoted only when it held a comma, and a quote in it was never
160
+ * doubled nor a line break quoted, so `say "hi"` or a two-line note broke the
161
+ * row it was in.
162
+ */
163
+ export declare function resultsToCsv(rows: Record<string, unknown>[]): string;
164
+ /** Column widths per query, as the console keeps them: SQL text → column → width. */
165
+ export type ColumnWidthsByQuery = Record<string, Record<string, number>>;
166
+ /** How many queries' column widths the console remembers. */
167
+ export declare const COLUMN_WIDTHS_REMEMBERED = 50;
168
+ /**
169
+ * The widths kept, at most `limit` queries' worth, the most recently resized
170
+ * last.
171
+ *
172
+ * They are kept per query text, in `localStorage`, and used to be kept
173
+ * forever: every query anybody ever resized a column of stayed, whole SQL
174
+ * text and all, in an origin's storage — which the console's tabs, snippets
175
+ * and history share, and which a full store refuses to write at all. The
176
+ * oldest go first; resizing a query's column again makes it the newest.
177
+ */
178
+ export declare function rememberColumnWidth(widths: ColumnWidthsByQuery, sql: string, column: string, width: number, limit?: number): ColumnWidthsByQuery;
179
+ /**
180
+ * The `limit` most recent queries' widths — the last ones in insertion
181
+ * order — and only entries that are a column → width record.
182
+ */
183
+ export declare function keepLatestColumnWidths(widths: Record<string, unknown>, limit?: number): ColumnWidthsByQuery;
184
+ /**
185
+ * The schema browser's tables, by schema, from `information_schema.columns`
186
+ * rows: each table in the order it first appears, its columns in row order.
187
+ *
188
+ * A table is found by its key, not looked up among the schema's tables: that
189
+ * lookup ran once per row over every table so far — 142 ms at 2,000 tables,
190
+ * 923 ms at 5,000, on every open of the console.
191
+ */
192
+ export declare function groupSchemaRows(data: Record<string, unknown>[]): Record<string, TableInfo[]>;
193
+ /**
194
+ * The console's rows for a typed JSON export: each value read by the type of
195
+ * the column it is in (see `typedValue`), where "Export JSON" writes the
196
+ * database's text as the console holds it. A row holds the last of two
197
+ * columns that share a name, so that column's type is the one read.
198
+ */
199
+ export declare function typedJsonRows(rows: Record<string, unknown>[], columns: readonly {
200
+ name: string;
201
+ type?: string;
202
+ }[]): Record<string, unknown>[];