@rebasepro/studio 0.22.0 → 0.24.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.
- package/dist/{ApiExplorer-D-jLWAaY.js → ApiExplorer-CCtczj3F.js} +16 -32
- package/dist/ApiExplorer-CCtczj3F.js.map +1 -0
- package/dist/ApiKeysView-B66TdzhH.js +1738 -0
- package/dist/ApiKeysView-B66TdzhH.js.map +1 -0
- package/dist/BackupsView-DmSMzAbT.js +368 -0
- package/dist/BackupsView-DmSMzAbT.js.map +1 -0
- package/dist/{BranchesView-D6Sz1a5f.js → BranchesView-DP1a87E-.js} +12 -8
- package/dist/BranchesView-DP1a87E-.js.map +1 -0
- package/dist/{CronJobsView-JwRjQCde.js → CronJobsView-TDErHTuK.js} +143 -68
- package/dist/CronJobsView-TDErHTuK.js.map +1 -0
- package/dist/{JSEditor-qiuwmcvk.js → JSEditor-B59pwg4c.js} +120 -74
- package/dist/JSEditor-B59pwg4c.js.map +1 -0
- package/dist/{LogsExplorer-Xq8K4oo6.js → LogsExplorer-BKM4yfP8.js} +48 -22
- package/dist/LogsExplorer-BKM4yfP8.js.map +1 -0
- package/dist/{MonacoEditor-D0_QljcX.js → MonacoEditor-CUxy5_k5.js} +11 -9
- package/dist/MonacoEditor-CUxy5_k5.js.map +1 -0
- package/dist/{RLSEditor-B-qrgdHy.js → RLSEditor-BlLLV6xs.js} +350 -98
- package/dist/RLSEditor-BlLLV6xs.js.map +1 -0
- package/dist/{SQLEditor-sdtoZv5F.js → SQLEditor-1An7w0j6.js} +496 -224
- package/dist/SQLEditor-1An7w0j6.js.map +1 -0
- package/dist/{SchemaVisualizer-DtDzIUNy.js → SchemaVisualizer-DCLVfA5t.js} +20 -24
- package/dist/SchemaVisualizer-DCLVfA5t.js.map +1 -0
- package/dist/{StorageView-C52dSZNf.js → StorageView-CbsHuCBf.js} +382 -228
- package/dist/StorageView-CbsHuCBf.js.map +1 -0
- package/dist/components/ApiExplorer/TryItPanel.d.ts +6 -1
- package/dist/components/ApiKeys/CreateApiKeyDialog.d.ts +21 -2
- package/dist/components/ApiKeys/scope-words.d.ts +37 -0
- package/dist/components/ApiKeys/scopes.d.ts +113 -0
- package/dist/components/AuthSimulationSelector.d.ts +5 -1
- package/dist/components/LogsExplorer/useLogTail.d.ts +9 -0
- package/dist/components/RLSEditor/policyListing.d.ts +44 -0
- package/dist/components/RLSEditor/policyRules.d.ts +49 -0
- package/dist/components/SQLEditor/MonacoEditor.d.ts +13 -1
- package/dist/components/SchemaVisualizer/schema-visualizer.utils.d.ts +0 -1
- package/dist/components/SchemaVisualizer/useSchemaGraph.d.ts +6 -0
- package/dist/components/load-failure.d.ts +1 -1
- package/dist/components/useRunAsUsers.d.ts +25 -0
- package/dist/index.es.js +11 -11
- package/dist/{load-failure-C6OMJSGC.js → load-failure-Bd8lFIO7.js} +2 -2
- package/dist/load-failure-Bd8lFIO7.js.map +1 -0
- package/dist/useRunAsUsers-CLaAtXt6.js +122 -0
- package/dist/useRunAsUsers-CLaAtXt6.js.map +1 -0
- package/dist/utils/sql_utils.d.ts +126 -36
- package/package.json +9 -9
- package/dist/ApiExplorer-D-jLWAaY.js.map +0 -1
- package/dist/ApiKeysView-OfJLH2x7.js +0 -1239
- package/dist/ApiKeysView-OfJLH2x7.js.map +0 -1
- package/dist/AuthSimulationSelector-BewmhMBw.js +0 -56
- package/dist/AuthSimulationSelector-BewmhMBw.js.map +0 -1
- package/dist/BackupsView-DYTAD6aW.js +0 -253
- package/dist/BackupsView-DYTAD6aW.js.map +0 -1
- package/dist/BranchesView-D6Sz1a5f.js.map +0 -1
- package/dist/CronJobsView-JwRjQCde.js.map +0 -1
- package/dist/JSEditor-qiuwmcvk.js.map +0 -1
- package/dist/LogsExplorer-Xq8K4oo6.js.map +0 -1
- package/dist/MonacoEditor-D0_QljcX.js.map +0 -1
- package/dist/RLSEditor-B-qrgdHy.js.map +0 -1
- package/dist/SQLEditor-sdtoZv5F.js.map +0 -1
- package/dist/SchemaVisualizer-DtDzIUNy.js.map +0 -1
- package/dist/StorageView-C52dSZNf.js.map +0 -1
- package/dist/components/ApiKeys/permissions.d.ts +0 -66
- package/dist/load-failure-C6OMJSGC.js.map +0 -1
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
import { t as MonacoEditor } from "./MonacoEditor-
|
|
1
|
+
import { t as MonacoEditor } from "./MonacoEditor-CUxy5_k5.js";
|
|
2
2
|
import { ConfirmationDialog, ErrorView, IconForView, useRebaseContext, useSnackbarController, useStudioCollectionRegistry, useStudioSidePanelController, useTranslation } from "@rebasepro/app";
|
|
3
3
|
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
4
|
-
import { Button, Checkbox, CircularProgress, Collapse, CopyIcon, DatabaseIcon, Dialog, DialogActions, DialogContent, DialogTitle, IconButton, Menu, MenuIcon, MenuItem, MoreVerticalIcon, PencilIcon, PlayIcon, PlusIcon, RefreshCwIcon, ResizablePanels, Tab, Tabs, TerminalIcon, TextField, TextareaAutosize, Tooltip, Trash2Icon, Typography, VirtualTable, XIcon, cls, defaultBorderMixin, iconSize } from "@rebasepro/ui";
|
|
4
|
+
import { Alert, Button, Checkbox, CircularProgress, Collapse, CopyIcon, DatabaseIcon, Dialog, DialogActions, DialogContent, DialogTitle, IconButton, Menu, MenuIcon, MenuItem, MoreVerticalIcon, PencilIcon, PlayIcon, PlusIcon, RefreshCwIcon, ResizablePanels, Tab, Tabs, TerminalIcon, TextField, TextareaAutosize, Tooltip, Trash2Icon, Typography, VirtualTable, XIcon, cls, defaultBorderMixin, iconSize, isKeyHandled } from "@rebasepro/ui";
|
|
5
5
|
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
6
6
|
import { isArrayValue, isRecordValue, readStoredJson, readStoredString, toSnakeCase, writeStoredJson, writeStoredString } from "@rebasepro/utils";
|
|
7
7
|
import { createPortal } from "react-dom";
|
|
8
|
-
import {
|
|
8
|
+
import { astVisitor, parse } from "pgsql-ast-parser";
|
|
9
9
|
//#region src/components/SQLEditor/SchemaBrowser.tsx
|
|
10
10
|
var SchemaBrowser = ({ onTableClick, schemas, isSchemaLoading, schemaError, onRetrySchema }) => {
|
|
11
11
|
const [expandedSchemas, setExpandedSchemas] = useState({ public: true });
|
|
@@ -393,152 +393,332 @@ var SQLEditorSidebar = ({ onSelectSnippet, onTableClick, snippets, history, onDe
|
|
|
393
393
|
};
|
|
394
394
|
//#endregion
|
|
395
395
|
//#region src/utils/sql_utils.ts
|
|
396
|
-
/**
|
|
397
|
-
|
|
398
|
-
* Returns an empty array for non-SELECT queries or parse failures.
|
|
399
|
-
*/
|
|
400
|
-
function extractTablesFromQuery(sqlString) {
|
|
396
|
+
/** The statements in `sqlText`, or `null` when the parser cannot read it. */
|
|
397
|
+
function parseStatements(sqlText) {
|
|
401
398
|
try {
|
|
402
|
-
|
|
403
|
-
if (ast.type !== "select") return [];
|
|
404
|
-
const tables = [];
|
|
405
|
-
const processFrom = (fromItems) => {
|
|
406
|
-
for (const item of fromItems) {
|
|
407
|
-
if (item.type === "table" && item.name) tables.push({
|
|
408
|
-
name: item.name.name,
|
|
409
|
-
alias: item.name.alias
|
|
410
|
-
});
|
|
411
|
-
if (item.type === "join") {
|
|
412
|
-
if (item.left) processFrom([item.left]);
|
|
413
|
-
if (item.right) processFrom([item.right]);
|
|
414
|
-
}
|
|
415
|
-
}
|
|
416
|
-
};
|
|
417
|
-
if (ast.from) processFrom(ast.from);
|
|
418
|
-
return tables;
|
|
399
|
+
return parse(sqlText);
|
|
419
400
|
} catch {
|
|
420
|
-
return
|
|
401
|
+
return null;
|
|
421
402
|
}
|
|
422
403
|
}
|
|
423
404
|
/**
|
|
424
|
-
*
|
|
405
|
+
* The `EXPLAIN` for one statement, or `null` when `sqlText` is not exactly one.
|
|
425
406
|
*
|
|
426
|
-
*
|
|
427
|
-
*
|
|
428
|
-
* snake_case of `collection.slug`).
|
|
407
|
+
* Never `ANALYZE`: that executes the statement to time it, so explaining a
|
|
408
|
+
* `DELETE` deleted the rows — without the confirmation "Run" asks for.
|
|
429
409
|
*
|
|
430
|
-
*
|
|
431
|
-
*
|
|
410
|
+
* And never more than one statement. Sent as one simple query, `EXPLAIN (…)
|
|
411
|
+
* SELECT 1; DELETE …` explains the SELECT and *runs* the DELETE. Text the
|
|
412
|
+
* parser cannot read is still explained, as long as no `;` could be hiding a
|
|
413
|
+
* second statement in it.
|
|
432
414
|
*/
|
|
433
|
-
function
|
|
434
|
-
const
|
|
435
|
-
if (
|
|
436
|
-
const
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
415
|
+
function buildExplainSql(sqlText) {
|
|
416
|
+
const statement = sqlText.trim().replace(/;+$/, "").trim();
|
|
417
|
+
if (!statement) return null;
|
|
418
|
+
const statements = parseStatements(statement);
|
|
419
|
+
return (statements ? statements.length === 1 : !statement.includes(";")) ? `EXPLAIN (FORMAT JSON) ${statement}` : null;
|
|
420
|
+
}
|
|
421
|
+
/**
|
|
422
|
+
* Whether the console's automatic `LIMIT` may be appended to `sqlText`: one
|
|
423
|
+
* top-level SELECT that has no limit of its own.
|
|
424
|
+
*
|
|
425
|
+
* Decided on the parsed statement, not the text. A search for the word SELECT
|
|
426
|
+
* also found it inside `INSERT INTO … SELECT` and `CREATE TABLE … AS SELECT`,
|
|
427
|
+
* which then copied a thousand rows and reported success, and at the start of
|
|
428
|
+
* a script whose last statement the appended `LIMIT` turned into a syntax
|
|
429
|
+
* error. Text the parser cannot read is left as written.
|
|
430
|
+
*/
|
|
431
|
+
function acceptsAutoLimit(sqlText) {
|
|
432
|
+
const statements = parseStatements(sqlText);
|
|
433
|
+
if (!statements || statements.length !== 1) return false;
|
|
434
|
+
const [statement] = statements;
|
|
435
|
+
return statement.type === "select" && !statement.limit;
|
|
436
|
+
}
|
|
437
|
+
/** A destructive command named anywhere in a text, comments included. */
|
|
438
|
+
var DESTRUCTIVE_COMMAND = /\b(DELETE|UPDATE|DROP|TRUNCATE)\b/i;
|
|
439
|
+
function isDestructiveStatement(statement) {
|
|
440
|
+
switch (statement.type) {
|
|
441
|
+
case "delete":
|
|
442
|
+
case "update": return !statement.where;
|
|
443
|
+
case "truncate table":
|
|
444
|
+
case "drop table":
|
|
445
|
+
case "drop sequence":
|
|
446
|
+
case "drop index":
|
|
447
|
+
case "drop type":
|
|
448
|
+
case "drop trigger":
|
|
449
|
+
case "drop function": return true;
|
|
450
|
+
case "alter table": return statement.changes.some((change) => change.type === "drop column" || change.type === "drop constraint");
|
|
451
|
+
case "with": return statement.bind.some((binding) => isDestructiveStatement(binding.statement)) || isDestructiveStatement(statement.in);
|
|
452
|
+
case "with recursive": return isDestructiveStatement(statement.in);
|
|
453
|
+
case "do": return DESTRUCTIVE_COMMAND.test(statement.code);
|
|
454
|
+
default: return false;
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
/**
|
|
458
|
+
* Whether "Run" asks before running `sqlText`: a statement in it deletes or
|
|
459
|
+
* updates without a WHERE of its own, drops or truncates.
|
|
460
|
+
*
|
|
461
|
+
* Decided per parsed statement, not on the text. A search of the text for the
|
|
462
|
+
* word WHERE found it in another statement or in a comment, so `DELETE FROM
|
|
463
|
+
* posts; SELECT * FROM posts WHERE id = 1` deleted every post without asking.
|
|
464
|
+
* Text the parser cannot read is confirmed whenever it names a destructive
|
|
465
|
+
* command.
|
|
466
|
+
*/
|
|
467
|
+
function needsDestructiveConfirmation(sqlText) {
|
|
468
|
+
const statements = parseStatements(sqlText);
|
|
469
|
+
if (!statements) return DESTRUCTIVE_COMMAND.test(sqlText);
|
|
470
|
+
return statements.some(isDestructiveStatement);
|
|
471
|
+
}
|
|
472
|
+
/**
|
|
473
|
+
* A table as SQL: `"schema"."table"` when a schema is given, a bare `"table"`
|
|
474
|
+
* otherwise.
|
|
475
|
+
*/
|
|
476
|
+
function quoteTableName(tableName, schemaName) {
|
|
477
|
+
return schemaName ? `${quoteIdentifier(schemaName)}.${quoteIdentifier(tableName)}` : quoteIdentifier(tableName);
|
|
478
|
+
}
|
|
479
|
+
/** An identifier as SQL, whatever it holds: double-quoted, with `"` doubled. */
|
|
480
|
+
function quoteIdentifier(identifier) {
|
|
481
|
+
return `"${identifier.replace(/"/g, "\"\"")}"`;
|
|
482
|
+
}
|
|
483
|
+
var refuse = (key, params = {}) => ({ refusal: {
|
|
484
|
+
key,
|
|
485
|
+
params
|
|
486
|
+
} });
|
|
487
|
+
/**
|
|
488
|
+
* Which stored cell an edit of the result column `resultColumn` writes, and
|
|
489
|
+
* how its row is found again — or why no edit is safe.
|
|
490
|
+
*
|
|
491
|
+
* Decided on what the database reported, not on the query's text. Each
|
|
492
|
+
* column of the result says which table column it was read from, or that it
|
|
493
|
+
* was computed; a cell is editable only when it was read from a column of a
|
|
494
|
+
* table, every column of that table's primary key was read too, and each of
|
|
495
|
+
* them is the only column of the result with its name — a row holds one value
|
|
496
|
+
* per name.
|
|
497
|
+
*
|
|
498
|
+
* The text was the only evidence before, and it was wrong twice over. For
|
|
499
|
+
* `SELECT p.id, a.name FROM posts p JOIN authors a …` it took the post's `id`
|
|
500
|
+
* for the author's key and updated whichever author shared the post's id.
|
|
501
|
+
* For `SELECT id, lower(email) AS name FROM users` it wrote the edited text
|
|
502
|
+
* to `users.name`.
|
|
503
|
+
*
|
|
504
|
+
* The database reports a table, not which mention of it: in a self-join both
|
|
505
|
+
* sides are `authors`, and a key from one with a value from the other finds
|
|
506
|
+
* the wrong row. So the query is still read, for one thing — the table must
|
|
507
|
+
* be read once, and nothing it is read through may be read twice.
|
|
508
|
+
*/
|
|
509
|
+
function resolveCellEdit(sqlText, provenance, resultColumn) {
|
|
510
|
+
const sameName = provenance.columns.filter((column) => column.name === resultColumn);
|
|
511
|
+
if (sameName.length !== 1) return refuse("studio_sql_edit_duplicate_column", { column: resultColumn });
|
|
512
|
+
const source = sameName[0].source;
|
|
513
|
+
if (!source) return refuse("studio_sql_edit_computed_column", { column: resultColumn });
|
|
514
|
+
const tableLabel = `${source.schema}.${source.table}`;
|
|
515
|
+
const table = provenance.tables.find((t) => t.schema === source.schema && t.table === source.table);
|
|
516
|
+
if (!table || table.kind !== "table" && table.kind !== "partitioned table") return refuse("studio_sql_edit_not_a_table", {
|
|
517
|
+
column: resultColumn,
|
|
518
|
+
table: tableLabel
|
|
519
|
+
});
|
|
520
|
+
if (table.hasInheritors) return refuse("studio_sql_edit_inherited_table", { table: tableLabel });
|
|
521
|
+
if (table.primaryKey.length === 0) return refuse("studio_sql_edit_no_primary_key", { table: tableLabel });
|
|
522
|
+
const key = [];
|
|
523
|
+
for (const keyColumn of table.primaryKey) {
|
|
524
|
+
const holders = provenance.columns.filter((column) => column.source?.schema === source.schema && column.source.table === source.table && column.source.column === keyColumn);
|
|
525
|
+
const holder = holders.length === 1 && provenance.columns.filter((column) => column.name === holders[0].name).length === 1 ? holders[0] : void 0;
|
|
526
|
+
if (!holder) return refuse("studio_sql_edit_key_missing", {
|
|
527
|
+
table: tableLabel,
|
|
528
|
+
columns: table.primaryKey.join(", ")
|
|
529
|
+
});
|
|
530
|
+
key.push({
|
|
531
|
+
column: keyColumn,
|
|
532
|
+
resultColumn: holder.name
|
|
533
|
+
});
|
|
534
|
+
}
|
|
535
|
+
const readOnce = tableReadOnce(sqlText, source.table);
|
|
536
|
+
if (readOnce !== true) return readOnce;
|
|
537
|
+
return { target: {
|
|
538
|
+
schema: source.schema,
|
|
539
|
+
table: source.table,
|
|
540
|
+
column: source.column,
|
|
541
|
+
key
|
|
542
|
+
} };
|
|
543
|
+
}
|
|
544
|
+
/**
|
|
545
|
+
* Whether `sqlText` is one SELECT that reads `table` once — counting every
|
|
546
|
+
* mention of a table of that name, in any schema and at any depth, and every
|
|
547
|
+
* mention of a `WITH` query, since one read twice reads its tables twice.
|
|
548
|
+
*/
|
|
549
|
+
function tableReadOnce(sqlText, table) {
|
|
550
|
+
const statements = parseStatements(sqlText);
|
|
551
|
+
if (!statements) return refuse("studio_sql_edit_unreadable_query");
|
|
552
|
+
if (statements.length !== 1) return refuse("studio_sql_edit_not_a_select");
|
|
553
|
+
const [statement] = statements;
|
|
554
|
+
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");
|
|
555
|
+
const mentions = /* @__PURE__ */ new Map();
|
|
556
|
+
const withNames = [];
|
|
557
|
+
astVisitor((visitor) => ({
|
|
558
|
+
fromTable: (from) => {
|
|
559
|
+
const name = from.name.name.toLowerCase();
|
|
560
|
+
mentions.set(name, (mentions.get(name) ?? 0) + 1);
|
|
561
|
+
visitor.super().fromTable(from);
|
|
562
|
+
},
|
|
563
|
+
with: (withStatement) => {
|
|
564
|
+
withNames.push(...withStatement.bind.map((binding) => binding.alias.name.toLowerCase()));
|
|
565
|
+
visitor.super().with(withStatement);
|
|
445
566
|
}
|
|
446
|
-
}
|
|
567
|
+
})).statement(statement);
|
|
568
|
+
return (mentions.get(table.toLowerCase()) ?? 0) !== 1 || withNames.some((name) => (mentions.get(name) ?? 0) > 1) ? refuse("studio_sql_edit_table_read_twice", { table }) : true;
|
|
569
|
+
}
|
|
570
|
+
/**
|
|
571
|
+
* The collections a result's rows can be opened as: those whose table one of
|
|
572
|
+
* the result's columns was read from, with the table's (single-column) primary
|
|
573
|
+
* key read too, under a name no other column of the result has.
|
|
574
|
+
*
|
|
575
|
+
* Matched on the provenance the database reported. A text match took the
|
|
576
|
+
* result's `id` for every table of a join: `SELECT p.id, a.name FROM posts p
|
|
577
|
+
* JOIN authors a …` offered to open the author whose id was the post's.
|
|
578
|
+
*/
|
|
579
|
+
function resolveQueryCollections(provenance, collections) {
|
|
447
580
|
const results = [];
|
|
448
|
-
for (const table of tables) {
|
|
449
|
-
const
|
|
450
|
-
|
|
581
|
+
for (const table of provenance.tables) {
|
|
582
|
+
const collection = collections.find((c) => {
|
|
583
|
+
const tableName = ("table" in c ? c.table : void 0) || toSnakeCase(c.slug);
|
|
584
|
+
const schemaName = ("schema" in c ? c.schema : void 0) || "public";
|
|
585
|
+
return tableName === table.table && schemaName === table.schema;
|
|
451
586
|
});
|
|
452
|
-
if (!
|
|
453
|
-
const
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
break;
|
|
459
|
-
}
|
|
460
|
-
}
|
|
461
|
-
let pkColumn;
|
|
462
|
-
const tableRef = table.alias || table.name;
|
|
463
|
-
const idSelectCol = selectColumns.find((sc) => sc.column === "id" && (!sc.table || sc.table === tableRef || sc.table === table.name));
|
|
464
|
-
if (idSelectCol) pkColumn = idSelectCol.alias || idSelectCol.column;
|
|
465
|
-
if (!pkColumn && resultColumns) {
|
|
466
|
-
if (resultColumns.includes("id")) pkColumn = "id";
|
|
467
|
-
}
|
|
468
|
-
if (!pkColumn && tableColumns.includes("id")) pkColumn = "id";
|
|
587
|
+
if (!collection || table.primaryKey.length !== 1) continue;
|
|
588
|
+
const [keyColumn] = table.primaryKey;
|
|
589
|
+
const holders = provenance.columns.filter((column) => column.source?.schema === table.schema && column.source.table === table.table && column.source.column === keyColumn);
|
|
590
|
+
if (holders.length !== 1) continue;
|
|
591
|
+
const pkColumn = holders[0].name;
|
|
592
|
+
if (provenance.columns.filter((column) => column.name === pkColumn).length !== 1) continue;
|
|
469
593
|
results.push({
|
|
470
|
-
tableName: table.
|
|
471
|
-
|
|
472
|
-
collection
|
|
473
|
-
columns: tableColumns,
|
|
594
|
+
tableName: table.table,
|
|
595
|
+
schemaName: table.schema,
|
|
596
|
+
collection,
|
|
474
597
|
pkColumn
|
|
475
598
|
});
|
|
476
599
|
}
|
|
477
600
|
return results;
|
|
478
601
|
}
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
602
|
+
/** A character an unquoted identifier or keyword can hold, `$` included. */
|
|
603
|
+
var IDENTIFIER_CHARACTER = /[A-Za-z0-9_$]/;
|
|
604
|
+
/**
|
|
605
|
+
* Cut `sqlText` into code and the things in it whose text is data.
|
|
606
|
+
*
|
|
607
|
+
* A lexer, not a parser: it reads text the parser refuses as well, which is
|
|
608
|
+
* most of what a person types while writing a query.
|
|
609
|
+
*/
|
|
610
|
+
function splitSql(sqlText) {
|
|
611
|
+
const segments = [];
|
|
612
|
+
let code = "";
|
|
613
|
+
const push = (text, kind) => {
|
|
614
|
+
if (code) segments.push({
|
|
615
|
+
text: code,
|
|
616
|
+
kind: "code"
|
|
617
|
+
});
|
|
618
|
+
code = "";
|
|
619
|
+
segments.push({
|
|
620
|
+
text,
|
|
621
|
+
kind
|
|
622
|
+
});
|
|
623
|
+
};
|
|
624
|
+
let i = 0;
|
|
625
|
+
while (i < sqlText.length) {
|
|
626
|
+
const ch = sqlText[i];
|
|
627
|
+
const next = sqlText[i + 1];
|
|
628
|
+
if (ch === "-" && next === "-") {
|
|
629
|
+
const newline = sqlText.indexOf("\n", i);
|
|
630
|
+
const end = newline === -1 ? sqlText.length : newline + 1;
|
|
631
|
+
push(sqlText.slice(i, end), "line comment");
|
|
632
|
+
i = end;
|
|
633
|
+
continue;
|
|
492
634
|
}
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
if (
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
}
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
const tableInfo = schema.find((ti) => ti.tableName === t.name);
|
|
507
|
-
if (tableInfo && tableInfo.columns.some((c) => c.name === actualDbColumnName)) return true;
|
|
508
|
-
}
|
|
509
|
-
return false;
|
|
510
|
-
});
|
|
511
|
-
if (matchedTables.length === 1) resolvedTableName = matchedTables[0].name;
|
|
512
|
-
else if (matchedTables.length > 1) return { error: `Ambiguous column "${columnKey}": Found in multiple queried tables.` };
|
|
513
|
-
else return { error: `Could not find column "${columnKey}" in the queried tables.` };
|
|
514
|
-
}
|
|
635
|
+
if (ch === "/" && next === "*") {
|
|
636
|
+
let depth = 1;
|
|
637
|
+
let j = i + 2;
|
|
638
|
+
while (j < sqlText.length && depth > 0) if (sqlText[j] === "/" && sqlText[j + 1] === "*") {
|
|
639
|
+
depth++;
|
|
640
|
+
j += 2;
|
|
641
|
+
} else if (sqlText[j] === "*" && sqlText[j + 1] === "/") {
|
|
642
|
+
depth--;
|
|
643
|
+
j += 2;
|
|
644
|
+
} else j++;
|
|
645
|
+
push(sqlText.slice(i, j), "block comment");
|
|
646
|
+
i = j;
|
|
647
|
+
continue;
|
|
515
648
|
}
|
|
516
|
-
if (
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
if (
|
|
521
|
-
|
|
649
|
+
if (ch === "'" || ch === "\"") {
|
|
650
|
+
const backslashEscapes = ch === "'" && /(^|[^A-Za-z0-9_$])[eE]$/.test(code);
|
|
651
|
+
let j = i + 1;
|
|
652
|
+
while (j < sqlText.length) if (backslashEscapes && sqlText[j] === "\\") j += 2;
|
|
653
|
+
else if (sqlText[j] === ch && sqlText[j + 1] === ch) j += 2;
|
|
654
|
+
else if (sqlText[j] === ch) {
|
|
655
|
+
j++;
|
|
522
656
|
break;
|
|
657
|
+
} else j++;
|
|
658
|
+
push(sqlText.slice(i, j), ch === "'" ? "literal" : "identifier");
|
|
659
|
+
i = j;
|
|
660
|
+
continue;
|
|
661
|
+
}
|
|
662
|
+
if (ch === "$" && !IDENTIFIER_CHARACTER.test(code.slice(-1))) {
|
|
663
|
+
const tag = /^\$(?:[A-Za-z_][A-Za-z0-9_]*)?\$/.exec(sqlText.slice(i));
|
|
664
|
+
if (tag) {
|
|
665
|
+
const close = sqlText.indexOf(tag[0], i + tag[0].length);
|
|
666
|
+
const end = close === -1 ? sqlText.length : close + tag[0].length;
|
|
667
|
+
push(sqlText.slice(i, end), "literal");
|
|
668
|
+
i = end;
|
|
669
|
+
continue;
|
|
523
670
|
}
|
|
524
671
|
}
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
const primaryKeys = pkDbColumns.map((dbCol) => {
|
|
528
|
-
const selectCol = selectColumns.find((sc) => sc.column === dbCol && (!sc.table || sc.table === (tableAlias || resolvedTableName)));
|
|
529
|
-
return {
|
|
530
|
-
dbColumn: dbCol,
|
|
531
|
-
resultColumn: selectCol?.alias || selectCol?.column || dbCol
|
|
532
|
-
};
|
|
533
|
-
});
|
|
534
|
-
return {
|
|
535
|
-
tableName: resolvedTableName,
|
|
536
|
-
primaryKeys
|
|
537
|
-
};
|
|
538
|
-
} catch (e) {
|
|
539
|
-
console.warn("Failed to parse SQL AST:", e);
|
|
540
|
-
return { error: `Could not safely parse query for inline editing: ${e instanceof Error ? e.message : String(e)}` };
|
|
672
|
+
code += ch;
|
|
673
|
+
i++;
|
|
541
674
|
}
|
|
675
|
+
if (code) segments.push({
|
|
676
|
+
text: code,
|
|
677
|
+
kind: "code"
|
|
678
|
+
});
|
|
679
|
+
return segments;
|
|
680
|
+
}
|
|
681
|
+
/**
|
|
682
|
+
* The console's "Format SQL": runs of whitespace become one space, a comma is
|
|
683
|
+
* followed by one, and an `=` standing alone gets one on each side — in the
|
|
684
|
+
* code only.
|
|
685
|
+
*
|
|
686
|
+
* A literal, a quoted identifier and a comment are copied as they are: their
|
|
687
|
+
* text is data, and rewriting `'a,b'` to `'a, b'` changes the value the next
|
|
688
|
+
* Run writes. A line comment keeps the line break that ends it, or it would
|
|
689
|
+
* swallow the code after it. `>=`, `<=`, `!=`, `:=` and `=>` are operators of
|
|
690
|
+
* their own and are left whole.
|
|
691
|
+
*/
|
|
692
|
+
function formatSql(sqlText) {
|
|
693
|
+
const segments = splitSql(sqlText);
|
|
694
|
+
return segments.map((segment, index) => {
|
|
695
|
+
if (segment.kind !== "code") return segment.text;
|
|
696
|
+
const tidied = segment.text.replace(/\s+/g, " ").replace(/\s*,\s*/g, ", ").replace(/(?<![<>!:=~*+\-/%^&|#@?])\s*=\s*(?![=>~*<])/g, " = ");
|
|
697
|
+
return segments[index - 1]?.kind === "line comment" ? tidied.trimStart() : tidied;
|
|
698
|
+
}).join("").trim();
|
|
699
|
+
}
|
|
700
|
+
/**
|
|
701
|
+
* `sqlText` with the console's automatic `LIMIT` on it, or `null` when it
|
|
702
|
+
* takes none (see {@link acceptsAutoLimit}).
|
|
703
|
+
*
|
|
704
|
+
* The limit goes straight after the statement's last token — before a
|
|
705
|
+
* trailing comment, which would otherwise swallow it, and in place of a
|
|
706
|
+
* trailing `;`. A query that computes an aggregate is limited like any other:
|
|
707
|
+
* the limit applies to the rows it returns, so a `count(*)` still counts every
|
|
708
|
+
* row, and a window `count(*) OVER ()` is no longer a way past the cap.
|
|
709
|
+
*/
|
|
710
|
+
function withAutoLimit(sqlText, limit) {
|
|
711
|
+
if (!acceptsAutoLimit(sqlText)) return null;
|
|
712
|
+
const segments = splitSql(sqlText);
|
|
713
|
+
const isComment = (segment) => segment.kind === "line comment" || segment.kind === "block comment";
|
|
714
|
+
const isFiller = (segment) => isComment(segment) || segment.kind === "code" && /^[\s;]*$/.test(segment.text);
|
|
715
|
+
let last = segments.length - 1;
|
|
716
|
+
while (last >= 0 && isFiller(segments[last])) last--;
|
|
717
|
+
if (last < 0) return null;
|
|
718
|
+
const lastSegment = segments[last];
|
|
719
|
+
const head = lastSegment.kind === "code" ? lastSegment.text.replace(/[\s;]+$/, "") : lastSegment.text;
|
|
720
|
+
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("");
|
|
721
|
+
return `${segments.slice(0, last).map((segment) => segment.text).join("")}${head} LIMIT ${limit};${tail.trimEnd()}`;
|
|
542
722
|
}
|
|
543
723
|
//#endregion
|
|
544
724
|
//#region src/components/SQLEditor/ExplainVisualizer.tsx
|
|
@@ -744,13 +924,13 @@ var FixedEditorOverlay = ({ displayValue, onSave, onCancel }) => {
|
|
|
744
924
|
e.target.value = val;
|
|
745
925
|
},
|
|
746
926
|
onBlur: (e) => {
|
|
747
|
-
onSave(e.target.value
|
|
927
|
+
onSave(e.target.value);
|
|
748
928
|
onCancel();
|
|
749
929
|
},
|
|
750
930
|
onKeyDown: (e) => {
|
|
751
931
|
if (e.key === "Enter" && !e.shiftKey) {
|
|
752
932
|
e.preventDefault();
|
|
753
|
-
onSave(e.currentTarget.value
|
|
933
|
+
onSave(e.currentTarget.value);
|
|
754
934
|
onCancel();
|
|
755
935
|
}
|
|
756
936
|
if (e.key === "Escape") onCancel();
|
|
@@ -791,7 +971,18 @@ var SQLEditor = () => {
|
|
|
791
971
|
return readStoredString(`rebase_sql_selected_role_${client?.baseUrl ? client.baseUrl.replace(/^https?:\/\//, "").replace(/[^a-zA-Z0-9]/g, "_") : "default"}`) || void 0;
|
|
792
972
|
});
|
|
793
973
|
const [availableDatabases, setAvailableDatabases] = useState([]);
|
|
974
|
+
/**
|
|
975
|
+
* The database the app reads and writes — the one the CMS opens a record
|
|
976
|
+
* on. Rows read anywhere else are not that record.
|
|
977
|
+
*/
|
|
978
|
+
const [appDatabase, setAppDatabase] = useState(void 0);
|
|
794
979
|
const [availableRoles, setAvailableRoles] = useState([]);
|
|
980
|
+
/**
|
|
981
|
+
* Who every statement runs as, once a run has shown that the server does
|
|
982
|
+
* not switch roles (`DISABLE_DB_ROLE_SWITCHING`): the role picker then
|
|
983
|
+
* changes nothing, and says so.
|
|
984
|
+
*/
|
|
985
|
+
const [connectionOwner, setConnectionOwner] = useState(null);
|
|
795
986
|
const [isLoadingConfig, setIsLoadingConfig] = useState(true);
|
|
796
987
|
const [connectionConfigError, setConnectionConfigError] = useState(null);
|
|
797
988
|
const [tabs, setTabs] = useState(() => {
|
|
@@ -805,7 +996,11 @@ var SQLEditor = () => {
|
|
|
805
996
|
loading: false,
|
|
806
997
|
error: null,
|
|
807
998
|
execTime: null,
|
|
808
|
-
lastExecutedSql: null
|
|
999
|
+
lastExecutedSql: null,
|
|
1000
|
+
lastExecutedConnection: null,
|
|
1001
|
+
ranAs: null,
|
|
1002
|
+
lastProvenance: null,
|
|
1003
|
+
notices: []
|
|
809
1004
|
}));
|
|
810
1005
|
if (restored.length > 0) return restored;
|
|
811
1006
|
return [{
|
|
@@ -818,7 +1013,11 @@ var SQLEditor = () => {
|
|
|
818
1013
|
loading: false,
|
|
819
1014
|
error: null,
|
|
820
1015
|
execTime: null,
|
|
821
|
-
lastExecutedSql: null
|
|
1016
|
+
lastExecutedSql: null,
|
|
1017
|
+
lastExecutedConnection: null,
|
|
1018
|
+
ranAs: null,
|
|
1019
|
+
lastProvenance: null,
|
|
1020
|
+
notices: []
|
|
822
1021
|
}];
|
|
823
1022
|
});
|
|
824
1023
|
const [activeTabId, setActiveTabId] = useState(() => {
|
|
@@ -855,6 +1054,7 @@ var SQLEditor = () => {
|
|
|
855
1054
|
if (mounted) {
|
|
856
1055
|
setAvailableDatabases(dbs);
|
|
857
1056
|
setAvailableRoles(roles);
|
|
1057
|
+
setAppDatabase(currentDbFromApi);
|
|
858
1058
|
const loadedDb = readStoredString(`rebase_sql_selected_db_${projectPrefix}`) || void 0;
|
|
859
1059
|
const loadedRole = readStoredString(`rebase_sql_selected_role_${projectPrefix}`) || void 0;
|
|
860
1060
|
const initialActiveTabId = readStoredString(`rebase_sql_active_tab_${projectPrefix}`) || "1";
|
|
@@ -1031,7 +1231,23 @@ var SQLEditor = () => {
|
|
|
1031
1231
|
const [autoLimit, setAutoLimit] = useState(true);
|
|
1032
1232
|
const [isConfirmDialogOpen, setIsConfirmDialogOpen] = useState(false);
|
|
1033
1233
|
const [pendingAction, setPendingAction] = useState(null);
|
|
1234
|
+
/**
|
|
1235
|
+
* The cell being edited, and the text its editor opened with — what
|
|
1236
|
+
* closing the editor unchanged is compared against.
|
|
1237
|
+
*/
|
|
1034
1238
|
const [editingCell, setEditingCell] = useState(null);
|
|
1239
|
+
const isCurrentConnection = useCallback((connection) => connection !== null && connection.database === selectedDatabase && connection.role === selectedRole, [selectedDatabase, selectedRole]);
|
|
1240
|
+
/**
|
|
1241
|
+
* Which stored cell a result cell is — by what the database said about the
|
|
1242
|
+
* last run's columns, and nothing else.
|
|
1243
|
+
*/
|
|
1244
|
+
const resolveEdit = useCallback((columnKey) => {
|
|
1245
|
+
if (!activeTab.lastExecutedSql || !activeTab.lastProvenance) return { refusal: {
|
|
1246
|
+
key: "studio_sql_edit_computed_column",
|
|
1247
|
+
params: { column: columnKey }
|
|
1248
|
+
} };
|
|
1249
|
+
return resolveCellEdit(activeTab.lastExecutedSql, activeTab.lastProvenance, columnKey);
|
|
1250
|
+
}, [activeTab.lastExecutedSql, activeTab.lastProvenance]);
|
|
1035
1251
|
const handleDoubleClick = useCallback((rowIndex, columnKey, initialValue, rowData) => {
|
|
1036
1252
|
if (!activeTab.lastExecutedSql) {
|
|
1037
1253
|
snackbarController.open({
|
|
@@ -1040,15 +1256,22 @@ var SQLEditor = () => {
|
|
|
1040
1256
|
});
|
|
1041
1257
|
return;
|
|
1042
1258
|
}
|
|
1043
|
-
|
|
1044
|
-
|
|
1259
|
+
if (!isCurrentConnection(activeTab.lastExecutedConnection)) {
|
|
1260
|
+
snackbarController.open({
|
|
1261
|
+
type: "error",
|
|
1262
|
+
message: t("studio_sql_cannot_edit_other_connection")
|
|
1263
|
+
});
|
|
1264
|
+
return;
|
|
1265
|
+
}
|
|
1266
|
+
const resolution = resolveEdit(columnKey);
|
|
1267
|
+
if (!resolution.target) {
|
|
1045
1268
|
snackbarController.open({
|
|
1046
1269
|
type: "error",
|
|
1047
|
-
message: resolution.
|
|
1270
|
+
message: t(resolution.refusal.key, resolution.refusal.params)
|
|
1048
1271
|
});
|
|
1049
1272
|
return;
|
|
1050
1273
|
}
|
|
1051
|
-
const missingPKs = resolution.
|
|
1274
|
+
const missingPKs = resolution.target.key.filter((pk) => rowData[pk.resultColumn] === void 0 || rowData[pk.resultColumn] === null);
|
|
1052
1275
|
if (missingPKs.length > 0) {
|
|
1053
1276
|
snackbarController.open({
|
|
1054
1277
|
type: "error",
|
|
@@ -1063,61 +1286,63 @@ var SQLEditor = () => {
|
|
|
1063
1286
|
});
|
|
1064
1287
|
}, [
|
|
1065
1288
|
activeTab.lastExecutedSql,
|
|
1066
|
-
|
|
1067
|
-
|
|
1289
|
+
activeTab.lastExecutedConnection,
|
|
1290
|
+
isCurrentConnection,
|
|
1291
|
+
resolveEdit,
|
|
1292
|
+
snackbarController,
|
|
1293
|
+
t
|
|
1068
1294
|
]);
|
|
1069
|
-
const handleCellSave = useCallback(async (
|
|
1295
|
+
const handleCellSave = useCallback(async (text, rowData, columnKey, rowIndex) => {
|
|
1070
1296
|
if (!editingCell || !activeTab.lastExecutedSql) return;
|
|
1071
1297
|
setEditingCell(null);
|
|
1072
|
-
if (
|
|
1073
|
-
const
|
|
1074
|
-
|
|
1298
|
+
if (text === editingCell.initialValue) return;
|
|
1299
|
+
const newValue = text === "" ? null : text;
|
|
1300
|
+
const connection = activeTab.lastExecutedConnection;
|
|
1301
|
+
if (!isCurrentConnection(connection)) {
|
|
1075
1302
|
snackbarController.open({
|
|
1076
1303
|
type: "error",
|
|
1077
|
-
message:
|
|
1304
|
+
message: t("studio_sql_cannot_edit_other_connection")
|
|
1078
1305
|
});
|
|
1079
1306
|
return;
|
|
1080
1307
|
}
|
|
1081
|
-
const
|
|
1308
|
+
const resolution = resolveEdit(columnKey);
|
|
1309
|
+
if (!resolution.target) {
|
|
1310
|
+
snackbarController.open({
|
|
1311
|
+
type: "error",
|
|
1312
|
+
message: t(resolution.refusal.key, resolution.refusal.params)
|
|
1313
|
+
});
|
|
1314
|
+
return;
|
|
1315
|
+
}
|
|
1316
|
+
const target = resolution.target;
|
|
1082
1317
|
const formatValue = (val) => {
|
|
1083
1318
|
if (val === null || val === void 0) return "NULL";
|
|
1084
1319
|
if (typeof val === "number") return val;
|
|
1085
1320
|
if (typeof val === "boolean") return val ? "TRUE" : "FALSE";
|
|
1086
1321
|
return `'${String(val).replace(/'/g, "''")}'`;
|
|
1087
1322
|
};
|
|
1088
|
-
const
|
|
1089
|
-
|
|
1090
|
-
const ast = parseFirst(activeTab.lastExecutedSql);
|
|
1091
|
-
if (ast.type === "select" && ast.columns) {
|
|
1092
|
-
for (const col of ast.columns) if (col.expr?.type === "ref") {
|
|
1093
|
-
const alias = col.alias?.name;
|
|
1094
|
-
const colName = col.expr.name;
|
|
1095
|
-
if (alias === resultColKey || !alias && colName === resultColKey) return colName;
|
|
1096
|
-
}
|
|
1097
|
-
}
|
|
1098
|
-
} catch {}
|
|
1099
|
-
return resultColKey;
|
|
1100
|
-
};
|
|
1101
|
-
const dbColumnName = resolveDbColumnName(columnKey);
|
|
1102
|
-
const whereConditions = resolution.primaryKeys.map((pk) => `"${pk.dbColumn}" = ${formatValue(rowData[pk.resultColumn])}`).join(" AND ");
|
|
1103
|
-
const updateSql = `UPDATE "${tableName}" SET "${dbColumnName}" = ${formatValue(newValue)} WHERE ${whereConditions};`;
|
|
1323
|
+
const whereConditions = target.key.map((pk) => `${quoteIdentifier(pk.column)} = ${formatValue(rowData[pk.resultColumn])}`).join(" AND ");
|
|
1324
|
+
const updateSql = `UPDATE ${quoteTableName(target.table, target.schema)} SET ${quoteIdentifier(target.column)} = ${formatValue(newValue)} WHERE ${whereConditions};`;
|
|
1104
1325
|
try {
|
|
1105
|
-
if (databaseAdmin?.
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
});
|
|
1110
|
-
const newResults = [...activeTab.results || []];
|
|
1111
|
-
if (newResults[rowIndex]) newResults[rowIndex] = {
|
|
1112
|
-
...newResults[rowIndex],
|
|
1113
|
-
[columnKey]: newValue
|
|
1114
|
-
};
|
|
1115
|
-
updateActiveTab({ results: newResults });
|
|
1326
|
+
if (!(await databaseAdmin?.runSqlScript?.(updateSql, {
|
|
1327
|
+
database: connection.database,
|
|
1328
|
+
role: connection.role
|
|
1329
|
+
}))?.rowCount) {
|
|
1116
1330
|
snackbarController.open({
|
|
1117
|
-
type: "
|
|
1118
|
-
message: t("
|
|
1331
|
+
type: "error",
|
|
1332
|
+
message: t("studio_sql_update_no_row")
|
|
1119
1333
|
});
|
|
1334
|
+
return;
|
|
1120
1335
|
}
|
|
1336
|
+
const newResults = [...activeTab.results || []];
|
|
1337
|
+
if (newResults[rowIndex]) newResults[rowIndex] = {
|
|
1338
|
+
...newResults[rowIndex],
|
|
1339
|
+
[columnKey]: newValue
|
|
1340
|
+
};
|
|
1341
|
+
updateActiveTab({ results: newResults });
|
|
1342
|
+
snackbarController.open({
|
|
1343
|
+
type: "success",
|
|
1344
|
+
message: t("studio_sql_row_updated")
|
|
1345
|
+
});
|
|
1121
1346
|
} catch (e) {
|
|
1122
1347
|
snackbarController.open({
|
|
1123
1348
|
type: "error",
|
|
@@ -1126,14 +1351,15 @@ var SQLEditor = () => {
|
|
|
1126
1351
|
}
|
|
1127
1352
|
}, [
|
|
1128
1353
|
editingCell,
|
|
1129
|
-
|
|
1354
|
+
resolveEdit,
|
|
1130
1355
|
activeTab.lastExecutedSql,
|
|
1356
|
+
activeTab.lastExecutedConnection,
|
|
1131
1357
|
activeTab.results,
|
|
1358
|
+
isCurrentConnection,
|
|
1132
1359
|
databaseAdmin,
|
|
1133
1360
|
updateActiveTab,
|
|
1134
1361
|
snackbarController,
|
|
1135
|
-
|
|
1136
|
-
selectedRole
|
|
1362
|
+
t
|
|
1137
1363
|
]);
|
|
1138
1364
|
const [columnWidths, setColumnWidths] = useState(() => {
|
|
1139
1365
|
return readStoredJson(`rebase_sql_column_widths_${client?.baseUrl ? client.baseUrl.replace(/^https?:\/\//, "").replace(/[^a-zA-Z0-9]/g, "_") : "default"}`, {
|
|
@@ -1200,7 +1426,11 @@ var SQLEditor = () => {
|
|
|
1200
1426
|
loading: false,
|
|
1201
1427
|
error: null,
|
|
1202
1428
|
execTime: null,
|
|
1203
|
-
lastExecutedSql: null
|
|
1429
|
+
lastExecutedSql: null,
|
|
1430
|
+
lastExecutedConnection: null,
|
|
1431
|
+
ranAs: null,
|
|
1432
|
+
lastProvenance: null,
|
|
1433
|
+
notices: []
|
|
1204
1434
|
}]);
|
|
1205
1435
|
setActiveTabId(newId);
|
|
1206
1436
|
};
|
|
@@ -1229,15 +1459,28 @@ var SQLEditor = () => {
|
|
|
1229
1459
|
});
|
|
1230
1460
|
}, [activeTab.sql, projectPrefix]);
|
|
1231
1461
|
const handlePrettify = () => {
|
|
1232
|
-
|
|
1233
|
-
setSql(formatted);
|
|
1462
|
+
setSql(formatSql(activeTab.sql));
|
|
1234
1463
|
};
|
|
1464
|
+
/** The editor, for the selection the toolbar's buttons act on. */
|
|
1465
|
+
const editorHandle = useRef(null);
|
|
1235
1466
|
const handleExplain = async () => {
|
|
1236
|
-
const explainSql =
|
|
1467
|
+
const explainSql = buildExplainSql(editorHandle.current?.getSelectedText() ?? activeTab.sql);
|
|
1468
|
+
if (!explainSql) {
|
|
1469
|
+
updateActiveTab({
|
|
1470
|
+
results: null,
|
|
1471
|
+
error: t("studio_sql_explain_single_statement")
|
|
1472
|
+
});
|
|
1473
|
+
return;
|
|
1474
|
+
}
|
|
1237
1475
|
updateActiveTab({
|
|
1238
1476
|
loading: true,
|
|
1239
1477
|
error: null,
|
|
1240
|
-
results: null
|
|
1478
|
+
results: null,
|
|
1479
|
+
lastExecutedSql: null,
|
|
1480
|
+
lastExecutedConnection: null,
|
|
1481
|
+
ranAs: null,
|
|
1482
|
+
lastProvenance: null,
|
|
1483
|
+
notices: []
|
|
1241
1484
|
});
|
|
1242
1485
|
const start = performance.now();
|
|
1243
1486
|
try {
|
|
@@ -1259,31 +1502,39 @@ var SQLEditor = () => {
|
|
|
1259
1502
|
}
|
|
1260
1503
|
};
|
|
1261
1504
|
const executeRun = useCallback(async (sqlOverride) => {
|
|
1262
|
-
|
|
1263
|
-
const
|
|
1264
|
-
const isAggregate = /\b(COUNT|SUM|AVG|MIN|MAX)\s*\(/i.test(sqlToRun);
|
|
1265
|
-
const isExplain = /\bEXPLAIN\b/i.test(sqlToRun);
|
|
1266
|
-
if (autoLimit && upperSql.includes("SELECT") && !upperSql.includes("LIMIT") && !isAggregate && !isExplain) {
|
|
1267
|
-
sqlToRun = sqlToRun.trim().replace(/;$/, "");
|
|
1268
|
-
sqlToRun = `${sqlToRun} LIMIT 1000;`;
|
|
1269
|
-
}
|
|
1505
|
+
const written = sqlOverride || activeTab.sql;
|
|
1506
|
+
const sqlToRun = (autoLimit ? withAutoLimit(written, 1e3) : null) ?? written;
|
|
1270
1507
|
updateActiveTab({
|
|
1271
1508
|
loading: true,
|
|
1272
1509
|
error: null,
|
|
1273
|
-
results: null
|
|
1510
|
+
results: null,
|
|
1511
|
+
notices: []
|
|
1274
1512
|
});
|
|
1275
1513
|
const start = performance.now();
|
|
1276
1514
|
try {
|
|
1277
|
-
if (databaseAdmin?.executeSql) {
|
|
1278
|
-
const
|
|
1515
|
+
if (databaseAdmin?.runSqlScript || databaseAdmin?.executeSql) {
|
|
1516
|
+
const connection = {
|
|
1279
1517
|
database: selectedDatabase,
|
|
1280
1518
|
role: selectedRole
|
|
1281
|
-
}
|
|
1519
|
+
};
|
|
1520
|
+
const run = databaseAdmin.runSqlScript ? await databaseAdmin.runSqlScript(sqlToRun, connection) : void 0;
|
|
1521
|
+
const rows = run ? run.rows : await databaseAdmin.executeSql(sqlToRun, connection);
|
|
1282
1522
|
updateActiveTab({
|
|
1283
|
-
results:
|
|
1523
|
+
results: rows,
|
|
1284
1524
|
execTime: Math.round(performance.now() - start),
|
|
1285
|
-
lastExecutedSql: sqlToRun
|
|
1525
|
+
lastExecutedSql: sqlToRun,
|
|
1526
|
+
lastExecutedConnection: connection,
|
|
1527
|
+
ranAs: run?.effectiveRole ?? null,
|
|
1528
|
+
lastProvenance: run ? {
|
|
1529
|
+
columns: run.columns,
|
|
1530
|
+
tables: run.tables
|
|
1531
|
+
} : {
|
|
1532
|
+
columns: Object.keys(rows[0] ?? {}).map((name) => ({ name })),
|
|
1533
|
+
tables: []
|
|
1534
|
+
},
|
|
1535
|
+
notices: run?.notices ?? []
|
|
1286
1536
|
});
|
|
1537
|
+
if (run?.effectiveRole && connection.role) setConnectionOwner(run.effectiveRole !== connection.role ? run.effectiveRole : null);
|
|
1287
1538
|
if (history[history.length - 1] !== activeTab.sql) saveHistory([...history, activeTab.sql]);
|
|
1288
1539
|
} else updateActiveTab({ error: t("studio_sql_execution_not_supported") });
|
|
1289
1540
|
} catch (e) {
|
|
@@ -1304,14 +1555,7 @@ var SQLEditor = () => {
|
|
|
1304
1555
|
const handleRun = useCallback(async (selectedText) => {
|
|
1305
1556
|
const sqlTarget = selectedText || activeTab.sql;
|
|
1306
1557
|
if (!sqlTarget.trim()) return;
|
|
1307
|
-
|
|
1308
|
-
"DELETE",
|
|
1309
|
-
"DROP",
|
|
1310
|
-
"TRUNCATE",
|
|
1311
|
-
"UPDATE"
|
|
1312
|
-
].some((kw) => sqlTarget.toUpperCase().includes(kw));
|
|
1313
|
-
const hasWhere = sqlTarget.toUpperCase().includes("WHERE");
|
|
1314
|
-
if (hasDestructive && (!hasWhere || sqlTarget.toUpperCase().includes("DROP") || sqlTarget.toUpperCase().includes("TRUNCATE"))) {
|
|
1558
|
+
if (needsDestructiveConfirmation(sqlTarget)) {
|
|
1315
1559
|
setPendingAction(() => () => executeRun(selectedText));
|
|
1316
1560
|
setIsConfirmDialogOpen(true);
|
|
1317
1561
|
return;
|
|
@@ -1320,12 +1564,13 @@ var SQLEditor = () => {
|
|
|
1320
1564
|
}, [activeTab.sql, executeRun]);
|
|
1321
1565
|
useEffect(() => {
|
|
1322
1566
|
const handleKeyDown = (e) => {
|
|
1567
|
+
if (isKeyHandled(e)) return;
|
|
1323
1568
|
if ((e.metaKey || e.ctrlKey) && e.key === "Enter") {
|
|
1324
1569
|
const activeElement = document.activeElement;
|
|
1325
1570
|
const isInput = activeElement?.tagName === "INPUT" || activeElement?.tagName === "TEXTAREA";
|
|
1326
1571
|
if (!activeElement?.className?.includes("monaco-mouse-cursor-text") && !isInput) {
|
|
1327
1572
|
e.preventDefault();
|
|
1328
|
-
handleRun();
|
|
1573
|
+
handleRun(editorHandle.current?.getSelectedText());
|
|
1329
1574
|
}
|
|
1330
1575
|
}
|
|
1331
1576
|
};
|
|
@@ -1468,14 +1713,8 @@ var SQLEditor = () => {
|
|
|
1468
1713
|
});
|
|
1469
1714
|
const savedWidths = columnWidths[activeTab.sql] || {};
|
|
1470
1715
|
const resultColumnKeys = Object.keys(results[0]);
|
|
1471
|
-
const
|
|
1472
|
-
|
|
1473
|
-
try {
|
|
1474
|
-
return resolveQueryCollections(activeTab.lastExecutedSql, schemas, collectionRegistry.collections, resultColumnKeys);
|
|
1475
|
-
} catch {
|
|
1476
|
-
return [];
|
|
1477
|
-
}
|
|
1478
|
-
})().filter((mc) => mc.pkColumn && resultColumnKeys.includes(mc.pkColumn));
|
|
1716
|
+
const readFromAppDatabase = activeTab.lastExecutedConnection !== null && (activeTab.lastExecutedConnection.database === void 0 || activeTab.lastExecutedConnection.database === appDatabase);
|
|
1717
|
+
const actionableCollections = activeTab.lastProvenance && collectionRegistry.collections && readFromAppDatabase ? resolveQueryCollections(activeTab.lastProvenance, collectionRegistry.collections).filter((mc) => resultColumnKeys.includes(mc.pkColumn)) : [];
|
|
1479
1718
|
const getRowEntityActions = (rowData) => {
|
|
1480
1719
|
if (!rowData) return [];
|
|
1481
1720
|
return actionableCollections.filter((mc) => rowData[mc.pkColumn] != null).map((mc) => ({
|
|
@@ -1547,6 +1786,10 @@ var SQLEditor = () => {
|
|
|
1547
1786
|
}),
|
|
1548
1787
|
children: /* @__PURE__ */ jsx(IconButton, {
|
|
1549
1788
|
size: "small",
|
|
1789
|
+
"aria-label": t("studio_sql_edit_entity", {
|
|
1790
|
+
name: ra.collection.collection.name,
|
|
1791
|
+
id: String(ra.entityId)
|
|
1792
|
+
}),
|
|
1550
1793
|
className: "text-surface-400 dark:text-surface-500 hover:text-surface-600 dark:hover:text-surface-300 transition-colors",
|
|
1551
1794
|
onClick: (e) => {
|
|
1552
1795
|
e.stopPropagation();
|
|
@@ -1603,7 +1846,7 @@ var SQLEditor = () => {
|
|
|
1603
1846
|
children: /* @__PURE__ */ jsx("div", {
|
|
1604
1847
|
className: "truncate flex-grow",
|
|
1605
1848
|
title: displayValue,
|
|
1606
|
-
children:
|
|
1849
|
+
children: value === null || value === void 0 ? /* @__PURE__ */ jsx("span", {
|
|
1607
1850
|
className: "text-text-disabled dark:text-text-disabled-dark italic text-[11px]",
|
|
1608
1851
|
children: "NULL"
|
|
1609
1852
|
}) : displayValue
|
|
@@ -1753,6 +1996,7 @@ var SQLEditor = () => {
|
|
|
1753
1996
|
children: /* @__PURE__ */ jsx(IconButton, {
|
|
1754
1997
|
size: "small",
|
|
1755
1998
|
onClick: handlePrettify,
|
|
1999
|
+
"aria-label": t("studio_sql_format_sql"),
|
|
1756
2000
|
children: /* @__PURE__ */ jsx(MenuIcon, { size: iconSize.smallest })
|
|
1757
2001
|
})
|
|
1758
2002
|
}),
|
|
@@ -1833,7 +2077,7 @@ var SQLEditor = () => {
|
|
|
1833
2077
|
className: "mr-1.5 text-text-disabled dark:text-text-disabled-dark"
|
|
1834
2078
|
}), /* @__PURE__ */ jsx("span", {
|
|
1835
2079
|
className: "max-w-[160px] truncate",
|
|
1836
|
-
children: isLoadingConfig ? "..." : `${selectedDatabase || t("studio_sql_select_db")}${selectedRole ? ` (${selectedRole})` : ""}`
|
|
2080
|
+
children: isLoadingConfig ? "..." : `${selectedDatabase || t("studio_sql_select_db")}${connectionOwner ?? selectedRole ? ` (${connectionOwner ?? selectedRole})` : ""}`
|
|
1837
2081
|
})]
|
|
1838
2082
|
}),
|
|
1839
2083
|
children: /* @__PURE__ */ jsxs("div", {
|
|
@@ -1866,8 +2110,13 @@ var SQLEditor = () => {
|
|
|
1866
2110
|
children: t("studio_sql_role")
|
|
1867
2111
|
})
|
|
1868
2112
|
}),
|
|
2113
|
+
connectionOwner && /* @__PURE__ */ jsx("div", {
|
|
2114
|
+
className: "px-3 py-1.5 text-xs text-text-secondary dark:text-text-secondary-dark max-w-[240px] break-words",
|
|
2115
|
+
children: t("studio_sql_role_switching_disabled", { role: connectionOwner })
|
|
2116
|
+
}),
|
|
1869
2117
|
availableRoles.map((role) => /* @__PURE__ */ jsxs(MenuItem, {
|
|
1870
2118
|
dense: true,
|
|
2119
|
+
disabled: connectionOwner !== null,
|
|
1871
2120
|
onClick: () => handleRoleChange(role),
|
|
1872
2121
|
className: cls("text-xs", selectedRole === role && "text-primary dark:text-primary-dark"),
|
|
1873
2122
|
children: [role, role === "postgres" ? " " + t("studio_sql_collections_label") : ""]
|
|
@@ -1876,7 +2125,7 @@ var SQLEditor = () => {
|
|
|
1876
2125
|
})
|
|
1877
2126
|
}),
|
|
1878
2127
|
/* @__PURE__ */ jsxs(Button, {
|
|
1879
|
-
onClick: () => handleRun(),
|
|
2128
|
+
onClick: () => handleRun(editorHandle.current?.getSelectedText()),
|
|
1880
2129
|
disabled: loading,
|
|
1881
2130
|
size: "small",
|
|
1882
2131
|
color: "primary",
|
|
@@ -1898,6 +2147,7 @@ var SQLEditor = () => {
|
|
|
1898
2147
|
firstPanel: /* @__PURE__ */ jsx("div", {
|
|
1899
2148
|
className: "h-full w-full relative flex flex-col min-h-0",
|
|
1900
2149
|
children: /* @__PURE__ */ jsx(MonacoEditor, {
|
|
2150
|
+
ref: editorHandle,
|
|
1901
2151
|
value: sql,
|
|
1902
2152
|
onChange: (v) => setSql(v || ""),
|
|
1903
2153
|
onRun: handleRun,
|
|
@@ -1906,17 +2156,39 @@ var SQLEditor = () => {
|
|
|
1906
2156
|
}),
|
|
1907
2157
|
secondPanel: /* @__PURE__ */ jsxs("div", {
|
|
1908
2158
|
className: "h-full w-full flex flex-col bg-surface-well overflow-hidden min-h-0",
|
|
1909
|
-
children: [
|
|
1910
|
-
|
|
1911
|
-
|
|
1912
|
-
|
|
1913
|
-
|
|
1914
|
-
|
|
2159
|
+
children: [
|
|
2160
|
+
/* @__PURE__ */ jsx("div", {
|
|
2161
|
+
className: cls("p-2 px-4 bg-surface-raised border-b shrink-0 flex items-center", defaultBorderMixin),
|
|
2162
|
+
children: /* @__PURE__ */ jsx(Typography, {
|
|
2163
|
+
variant: "caption",
|
|
2164
|
+
className: "font-semibold text-text-disabled dark:text-text-disabled-dark uppercase tracking-widest text-[10px]",
|
|
2165
|
+
children: t("studio_sql_query_results")
|
|
2166
|
+
})
|
|
2167
|
+
}),
|
|
2168
|
+
!loading && results && activeTab.ranAs && activeTab.lastExecutedConnection?.role && activeTab.ranAs !== activeTab.lastExecutedConnection.role && /* @__PURE__ */ jsx("div", {
|
|
2169
|
+
className: "px-4 pt-2 shrink-0",
|
|
2170
|
+
children: /* @__PURE__ */ jsx(Alert, {
|
|
2171
|
+
color: "warning",
|
|
2172
|
+
size: "small",
|
|
2173
|
+
children: t("studio_sql_ran_as_other_role", {
|
|
2174
|
+
ranAs: activeTab.ranAs,
|
|
2175
|
+
role: activeTab.lastExecutedConnection.role
|
|
2176
|
+
})
|
|
2177
|
+
})
|
|
2178
|
+
}),
|
|
2179
|
+
!loading && activeTab.notices.length > 0 && /* @__PURE__ */ jsx("div", {
|
|
2180
|
+
className: "px-4 pt-2 flex flex-col gap-1 shrink-0",
|
|
2181
|
+
children: activeTab.notices.map((notice, index) => /* @__PURE__ */ jsx(Alert, {
|
|
2182
|
+
color: "warning",
|
|
2183
|
+
size: "small",
|
|
2184
|
+
children: `${notice.severity}: ${notice.message}`
|
|
2185
|
+
}, index))
|
|
2186
|
+
}),
|
|
2187
|
+
/* @__PURE__ */ jsx("div", {
|
|
2188
|
+
className: "flex-grow flex flex-col min-h-0 overflow-hidden",
|
|
2189
|
+
children: renderResults()
|
|
1915
2190
|
})
|
|
1916
|
-
|
|
1917
|
-
className: "flex-grow flex flex-col min-h-0 overflow-hidden",
|
|
1918
|
-
children: renderResults()
|
|
1919
|
-
})]
|
|
2191
|
+
]
|
|
1920
2192
|
})
|
|
1921
2193
|
})]
|
|
1922
2194
|
})
|
|
@@ -1974,4 +2246,4 @@ var SQLEditor = () => {
|
|
|
1974
2246
|
//#endregion
|
|
1975
2247
|
export { SQLEditor };
|
|
1976
2248
|
|
|
1977
|
-
//# sourceMappingURL=SQLEditor-
|
|
2249
|
+
//# sourceMappingURL=SQLEditor-1An7w0j6.js.map
|