@rebasepro/studio 0.23.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-pYMZflL3.js → ApiExplorer-CCtczj3F.js} +15 -28
- 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-7hjgSYId.js → CronJobsView-TDErHTuK.js} +126 -60
- 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/{RLSEditor-Djh43lBE.js → RLSEditor-BlLLV6xs.js} +237 -77
- package/dist/RLSEditor-BlLLV6xs.js.map +1 -0
- package/dist/{SQLEditor-TBw_K6CW.js → SQLEditor-1An7w0j6.js} +369 -294
- 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-CMG1DOVX.js → StorageView-CbsHuCBf.js} +355 -225
- 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/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 +90 -49
- package/package.json +9 -9
- package/dist/ApiExplorer-pYMZflL3.js.map +0 -1
- package/dist/ApiKeysView-qLSs47tw.js +0 -1239
- package/dist/ApiKeysView-qLSs47tw.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-7hjgSYId.js.map +0 -1
- package/dist/JSEditor-qiuwmcvk.js.map +0 -1
- package/dist/LogsExplorer-Xq8K4oo6.js.map +0 -1
- package/dist/RLSEditor-Djh43lBE.js.map +0 -1
- package/dist/SQLEditor-TBw_K6CW.js.map +0 -1
- package/dist/SchemaVisualizer-DtDzIUNy.js.map +0 -1
- package/dist/StorageView-CMG1DOVX.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
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 });
|
|
@@ -470,242 +470,255 @@ function needsDestructiveConfirmation(sqlText) {
|
|
|
470
470
|
return statements.some(isDestructiveStatement);
|
|
471
471
|
}
|
|
472
472
|
/**
|
|
473
|
-
* A table as SQL: `"schema"."table"` when
|
|
474
|
-
*
|
|
475
|
-
* the same way it resolved the query.
|
|
473
|
+
* A table as SQL: `"schema"."table"` when a schema is given, a bare `"table"`
|
|
474
|
+
* otherwise.
|
|
476
475
|
*/
|
|
477
476
|
function quoteTableName(tableName, schemaName) {
|
|
478
|
-
|
|
479
|
-
return schemaName ? `${quote(schemaName)}.${quote(tableName)}` : quote(tableName);
|
|
477
|
+
return schemaName ? `${quoteIdentifier(schemaName)}.${quoteIdentifier(tableName)}` : quoteIdentifier(tableName);
|
|
480
478
|
}
|
|
481
|
-
/**
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
* that name answers.
|
|
485
|
-
*/
|
|
486
|
-
function findTableInfo(schemas, table) {
|
|
487
|
-
if (table.schema) return schemas[table.schema]?.find((t) => t.tableName === table.name);
|
|
488
|
-
for (const schema of Object.values(schemas)) {
|
|
489
|
-
const tableInfo = schema.find((t) => t.tableName === table.name);
|
|
490
|
-
if (tableInfo) return tableInfo;
|
|
491
|
-
}
|
|
479
|
+
/** An identifier as SQL, whatever it holds: double-quoted, with `"` doubled. */
|
|
480
|
+
function quoteIdentifier(identifier) {
|
|
481
|
+
return `"${identifier.replace(/"/g, "\"\"")}"`;
|
|
492
482
|
}
|
|
483
|
+
var refuse = (key, params = {}) => ({ refusal: {
|
|
484
|
+
key,
|
|
485
|
+
params
|
|
486
|
+
} });
|
|
493
487
|
/**
|
|
494
|
-
*
|
|
495
|
-
*
|
|
496
|
-
* the name of what it casts, a function call the function's, and anything
|
|
497
|
-
* else a name no table column has.
|
|
498
|
-
*/
|
|
499
|
-
function expressionColumnName(expr) {
|
|
500
|
-
switch (expr.type) {
|
|
501
|
-
case "ref": return expr.name;
|
|
502
|
-
case "cast": {
|
|
503
|
-
const operandName = expressionColumnName(expr.operand);
|
|
504
|
-
if (operandName !== "?column?") return operandName;
|
|
505
|
-
return expr.to.kind === "array" ? operandName : expr.to.name;
|
|
506
|
-
}
|
|
507
|
-
case "call": return expr.function.name;
|
|
508
|
-
case "keyword": return expr.keyword;
|
|
509
|
-
case "case": return "case";
|
|
510
|
-
case "array select": return "array";
|
|
511
|
-
case "select": {
|
|
512
|
-
const [first] = expr.columns ?? [];
|
|
513
|
-
return first ? selectedColumnName(first) : "?column?";
|
|
514
|
-
}
|
|
515
|
-
default: return "?column?";
|
|
516
|
-
}
|
|
517
|
-
}
|
|
518
|
-
function selectedColumnName(column) {
|
|
519
|
-
return column.alias?.name ?? expressionColumnName(column.expr);
|
|
520
|
-
}
|
|
521
|
-
/**
|
|
522
|
-
* How many columns of the query's result may carry `name` — an upper bound:
|
|
523
|
-
* a `*` over a source whose columns are not known counts once for every name.
|
|
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.
|
|
524
490
|
*
|
|
525
|
-
*
|
|
526
|
-
*
|
|
527
|
-
* a
|
|
528
|
-
*
|
|
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.
|
|
529
508
|
*/
|
|
530
|
-
function
|
|
531
|
-
const
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
columns
|
|
541
|
-
};
|
|
542
|
-
}
|
|
543
|
-
if (item.type === "statement") {
|
|
544
|
-
const { statement } = item;
|
|
545
|
-
const named = statement.type === "select" && (statement.columns ?? []).every((c) => !(c.expr.type === "ref" && c.expr.name === "*"));
|
|
546
|
-
const renamed = item.columnNames ?? [];
|
|
547
|
-
const columns = named && statement.type === "select" ? (statement.columns ?? []).map((c, i) => renamed[i]?.name ?? selectedColumnName(c)) : void 0;
|
|
548
|
-
return {
|
|
549
|
-
refName: item.alias,
|
|
550
|
-
columns
|
|
551
|
-
};
|
|
552
|
-
}
|
|
553
|
-
return {
|
|
554
|
-
refName: item.alias?.name,
|
|
555
|
-
columns: void 0
|
|
556
|
-
};
|
|
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
|
|
557
519
|
});
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
}
|
|
568
|
-
|
|
569
|
-
|
|
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
|
+
} };
|
|
570
543
|
}
|
|
571
544
|
/**
|
|
572
|
-
*
|
|
573
|
-
*
|
|
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.
|
|
574
548
|
*/
|
|
575
|
-
function
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
}
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
} catch {
|
|
596
|
-
return [];
|
|
597
|
-
}
|
|
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);
|
|
566
|
+
}
|
|
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;
|
|
598
569
|
}
|
|
599
570
|
/**
|
|
600
|
-
*
|
|
601
|
-
*
|
|
602
|
-
*
|
|
603
|
-
* registered collections via `collection.table` (falling back to
|
|
604
|
-
* snake_case of `collection.slug`).
|
|
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.
|
|
605
574
|
*
|
|
606
|
-
*
|
|
607
|
-
*
|
|
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.
|
|
608
578
|
*/
|
|
609
|
-
function resolveQueryCollections(
|
|
610
|
-
const tables = extractTablesFromQuery(sqlString);
|
|
611
|
-
if (tables.length === 0) return [];
|
|
612
|
-
const selectColumns = [];
|
|
613
|
-
let countResultColumns;
|
|
614
|
-
try {
|
|
615
|
-
const ast = parseFirst(sqlString);
|
|
616
|
-
if (ast.type === "select") {
|
|
617
|
-
countResultColumns = resultColumnCounter(ast, schemas);
|
|
618
|
-
for (const col of ast.columns ?? []) if (col.expr?.type === "ref") selectColumns.push({
|
|
619
|
-
table: col.expr.table?.name,
|
|
620
|
-
column: col.expr.name,
|
|
621
|
-
alias: col.alias?.name
|
|
622
|
-
});
|
|
623
|
-
}
|
|
624
|
-
} catch {}
|
|
579
|
+
function resolveQueryCollections(provenance, collections) {
|
|
625
580
|
const results = [];
|
|
626
|
-
for (const table of tables) {
|
|
627
|
-
const
|
|
581
|
+
for (const table of provenance.tables) {
|
|
582
|
+
const collection = collections.find((c) => {
|
|
628
583
|
const tableName = ("table" in c ? c.table : void 0) || toSnakeCase(c.slug);
|
|
629
584
|
const schemaName = ("schema" in c ? c.schema : void 0) || "public";
|
|
630
|
-
return tableName === table.
|
|
585
|
+
return tableName === table.table && schemaName === table.schema;
|
|
631
586
|
});
|
|
632
|
-
if (!
|
|
633
|
-
const
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
const
|
|
637
|
-
if (
|
|
638
|
-
if (!pkColumn && resultColumns) {
|
|
639
|
-
if (resultColumns.includes("id")) pkColumn = "id";
|
|
640
|
-
}
|
|
641
|
-
if (!pkColumn && tableColumns.includes("id")) pkColumn = "id";
|
|
642
|
-
if (pkColumn && (!countResultColumns || countResultColumns(pkColumn) > 1)) pkColumn = void 0;
|
|
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;
|
|
643
593
|
results.push({
|
|
644
|
-
tableName: table.
|
|
645
|
-
|
|
646
|
-
collection
|
|
647
|
-
columns: tableColumns,
|
|
594
|
+
tableName: table.table,
|
|
595
|
+
schemaName: table.schema,
|
|
596
|
+
collection,
|
|
648
597
|
pkColumn
|
|
649
598
|
});
|
|
650
599
|
}
|
|
651
600
|
return results;
|
|
652
601
|
}
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
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;
|
|
666
634
|
}
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
if (
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
}
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
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;
|
|
648
|
+
}
|
|
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++;
|
|
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;
|
|
682
670
|
}
|
|
683
671
|
}
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
const pkDbColumns = (findTableInfo(schemas, resolvedTable)?.columns ?? []).filter((c) => c.isPrimaryKey).map((c) => c.name);
|
|
687
|
-
if (pkDbColumns.length === 0) return { error: `Table "${resolvedTableName}" has no primary key defined.` };
|
|
688
|
-
const tableAlias = resolvedTable.alias;
|
|
689
|
-
const primaryKeys = pkDbColumns.map((dbCol) => {
|
|
690
|
-
const selectCol = selectColumns.find((sc) => sc.column === dbCol && (!sc.table || sc.table === (tableAlias || resolvedTableName)));
|
|
691
|
-
return {
|
|
692
|
-
dbColumn: dbCol,
|
|
693
|
-
resultColumn: selectCol?.alias || selectCol?.column || dbCol
|
|
694
|
-
};
|
|
695
|
-
});
|
|
696
|
-
const countResultColumns = resultColumnCounter(ast, schemas);
|
|
697
|
-
if (countResultColumns(columnKey) > 1) return { error: `Ambiguous column "${columnKey}": the result has more than one column of that name. Give each one its own alias to edit it.` };
|
|
698
|
-
const sharedKey = primaryKeys.find((pk) => countResultColumns(pk.resultColumn) > 1);
|
|
699
|
-
if (sharedKey) return { error: `Cannot tell which "${sharedKey.resultColumn}" column is the key of "${resolvedTableName}": the result has more than one column of that name. Give it its own alias (${tableAlias || resolvedTableName}.${sharedKey.dbColumn} AS ${resolvedTableName}_${sharedKey.dbColumn}) to edit this table.` };
|
|
700
|
-
return {
|
|
701
|
-
tableName: resolvedTableName,
|
|
702
|
-
schemaName: resolvedTable.schema,
|
|
703
|
-
primaryKeys
|
|
704
|
-
};
|
|
705
|
-
} catch (e) {
|
|
706
|
-
console.warn("Failed to parse SQL AST:", e);
|
|
707
|
-
return { error: `Could not safely parse query for inline editing: ${e instanceof Error ? e.message : String(e)}` };
|
|
672
|
+
code += ch;
|
|
673
|
+
i++;
|
|
708
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()}`;
|
|
709
722
|
}
|
|
710
723
|
//#endregion
|
|
711
724
|
//#region src/components/SQLEditor/ExplainVisualizer.tsx
|
|
@@ -911,13 +924,13 @@ var FixedEditorOverlay = ({ displayValue, onSave, onCancel }) => {
|
|
|
911
924
|
e.target.value = val;
|
|
912
925
|
},
|
|
913
926
|
onBlur: (e) => {
|
|
914
|
-
onSave(e.target.value
|
|
927
|
+
onSave(e.target.value);
|
|
915
928
|
onCancel();
|
|
916
929
|
},
|
|
917
930
|
onKeyDown: (e) => {
|
|
918
931
|
if (e.key === "Enter" && !e.shiftKey) {
|
|
919
932
|
e.preventDefault();
|
|
920
|
-
onSave(e.currentTarget.value
|
|
933
|
+
onSave(e.currentTarget.value);
|
|
921
934
|
onCancel();
|
|
922
935
|
}
|
|
923
936
|
if (e.key === "Escape") onCancel();
|
|
@@ -958,7 +971,18 @@ var SQLEditor = () => {
|
|
|
958
971
|
return readStoredString(`rebase_sql_selected_role_${client?.baseUrl ? client.baseUrl.replace(/^https?:\/\//, "").replace(/[^a-zA-Z0-9]/g, "_") : "default"}`) || void 0;
|
|
959
972
|
});
|
|
960
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);
|
|
961
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);
|
|
962
986
|
const [isLoadingConfig, setIsLoadingConfig] = useState(true);
|
|
963
987
|
const [connectionConfigError, setConnectionConfigError] = useState(null);
|
|
964
988
|
const [tabs, setTabs] = useState(() => {
|
|
@@ -973,7 +997,10 @@ var SQLEditor = () => {
|
|
|
973
997
|
error: null,
|
|
974
998
|
execTime: null,
|
|
975
999
|
lastExecutedSql: null,
|
|
976
|
-
lastExecutedConnection: null
|
|
1000
|
+
lastExecutedConnection: null,
|
|
1001
|
+
ranAs: null,
|
|
1002
|
+
lastProvenance: null,
|
|
1003
|
+
notices: []
|
|
977
1004
|
}));
|
|
978
1005
|
if (restored.length > 0) return restored;
|
|
979
1006
|
return [{
|
|
@@ -987,7 +1014,10 @@ var SQLEditor = () => {
|
|
|
987
1014
|
error: null,
|
|
988
1015
|
execTime: null,
|
|
989
1016
|
lastExecutedSql: null,
|
|
990
|
-
lastExecutedConnection: null
|
|
1017
|
+
lastExecutedConnection: null,
|
|
1018
|
+
ranAs: null,
|
|
1019
|
+
lastProvenance: null,
|
|
1020
|
+
notices: []
|
|
991
1021
|
}];
|
|
992
1022
|
});
|
|
993
1023
|
const [activeTabId, setActiveTabId] = useState(() => {
|
|
@@ -1024,6 +1054,7 @@ var SQLEditor = () => {
|
|
|
1024
1054
|
if (mounted) {
|
|
1025
1055
|
setAvailableDatabases(dbs);
|
|
1026
1056
|
setAvailableRoles(roles);
|
|
1057
|
+
setAppDatabase(currentDbFromApi);
|
|
1027
1058
|
const loadedDb = readStoredString(`rebase_sql_selected_db_${projectPrefix}`) || void 0;
|
|
1028
1059
|
const loadedRole = readStoredString(`rebase_sql_selected_role_${projectPrefix}`) || void 0;
|
|
1029
1060
|
const initialActiveTabId = readStoredString(`rebase_sql_active_tab_${projectPrefix}`) || "1";
|
|
@@ -1200,8 +1231,23 @@ var SQLEditor = () => {
|
|
|
1200
1231
|
const [autoLimit, setAutoLimit] = useState(true);
|
|
1201
1232
|
const [isConfirmDialogOpen, setIsConfirmDialogOpen] = useState(false);
|
|
1202
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
|
+
*/
|
|
1203
1238
|
const [editingCell, setEditingCell] = useState(null);
|
|
1204
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]);
|
|
1205
1251
|
const handleDoubleClick = useCallback((rowIndex, columnKey, initialValue, rowData) => {
|
|
1206
1252
|
if (!activeTab.lastExecutedSql) {
|
|
1207
1253
|
snackbarController.open({
|
|
@@ -1217,15 +1263,15 @@ var SQLEditor = () => {
|
|
|
1217
1263
|
});
|
|
1218
1264
|
return;
|
|
1219
1265
|
}
|
|
1220
|
-
const resolution =
|
|
1221
|
-
if (
|
|
1266
|
+
const resolution = resolveEdit(columnKey);
|
|
1267
|
+
if (!resolution.target) {
|
|
1222
1268
|
snackbarController.open({
|
|
1223
1269
|
type: "error",
|
|
1224
|
-
message: resolution.
|
|
1270
|
+
message: t(resolution.refusal.key, resolution.refusal.params)
|
|
1225
1271
|
});
|
|
1226
1272
|
return;
|
|
1227
1273
|
}
|
|
1228
|
-
const missingPKs = resolution.
|
|
1274
|
+
const missingPKs = resolution.target.key.filter((pk) => rowData[pk.resultColumn] === void 0 || rowData[pk.resultColumn] === null);
|
|
1229
1275
|
if (missingPKs.length > 0) {
|
|
1230
1276
|
snackbarController.open({
|
|
1231
1277
|
type: "error",
|
|
@@ -1242,14 +1288,15 @@ var SQLEditor = () => {
|
|
|
1242
1288
|
activeTab.lastExecutedSql,
|
|
1243
1289
|
activeTab.lastExecutedConnection,
|
|
1244
1290
|
isCurrentConnection,
|
|
1245
|
-
|
|
1291
|
+
resolveEdit,
|
|
1246
1292
|
snackbarController,
|
|
1247
1293
|
t
|
|
1248
1294
|
]);
|
|
1249
|
-
const handleCellSave = useCallback(async (
|
|
1295
|
+
const handleCellSave = useCallback(async (text, rowData, columnKey, rowIndex) => {
|
|
1250
1296
|
if (!editingCell || !activeTab.lastExecutedSql) return;
|
|
1251
1297
|
setEditingCell(null);
|
|
1252
|
-
if (
|
|
1298
|
+
if (text === editingCell.initialValue) return;
|
|
1299
|
+
const newValue = text === "" ? null : text;
|
|
1253
1300
|
const connection = activeTab.lastExecutedConnection;
|
|
1254
1301
|
if (!isCurrentConnection(connection)) {
|
|
1255
1302
|
snackbarController.open({
|
|
@@ -1258,54 +1305,44 @@ var SQLEditor = () => {
|
|
|
1258
1305
|
});
|
|
1259
1306
|
return;
|
|
1260
1307
|
}
|
|
1261
|
-
const resolution =
|
|
1262
|
-
if (
|
|
1308
|
+
const resolution = resolveEdit(columnKey);
|
|
1309
|
+
if (!resolution.target) {
|
|
1263
1310
|
snackbarController.open({
|
|
1264
1311
|
type: "error",
|
|
1265
|
-
message: resolution.
|
|
1312
|
+
message: t(resolution.refusal.key, resolution.refusal.params)
|
|
1266
1313
|
});
|
|
1267
1314
|
return;
|
|
1268
1315
|
}
|
|
1269
|
-
const
|
|
1316
|
+
const target = resolution.target;
|
|
1270
1317
|
const formatValue = (val) => {
|
|
1271
1318
|
if (val === null || val === void 0) return "NULL";
|
|
1272
1319
|
if (typeof val === "number") return val;
|
|
1273
1320
|
if (typeof val === "boolean") return val ? "TRUE" : "FALSE";
|
|
1274
1321
|
return `'${String(val).replace(/'/g, "''")}'`;
|
|
1275
1322
|
};
|
|
1276
|
-
const
|
|
1277
|
-
|
|
1278
|
-
const ast = parseFirst(activeTab.lastExecutedSql);
|
|
1279
|
-
if (ast.type === "select" && ast.columns) {
|
|
1280
|
-
for (const col of ast.columns) if (col.expr?.type === "ref") {
|
|
1281
|
-
const alias = col.alias?.name;
|
|
1282
|
-
const colName = col.expr.name;
|
|
1283
|
-
if (alias === resultColKey || !alias && colName === resultColKey) return colName;
|
|
1284
|
-
}
|
|
1285
|
-
}
|
|
1286
|
-
} catch {}
|
|
1287
|
-
return resultColKey;
|
|
1288
|
-
};
|
|
1289
|
-
const dbColumnName = resolveDbColumnName(columnKey);
|
|
1290
|
-
const whereConditions = resolution.primaryKeys.map((pk) => `"${pk.dbColumn}" = ${formatValue(rowData[pk.resultColumn])}`).join(" AND ");
|
|
1291
|
-
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};`;
|
|
1292
1325
|
try {
|
|
1293
|
-
if (databaseAdmin?.
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
});
|
|
1298
|
-
const newResults = [...activeTab.results || []];
|
|
1299
|
-
if (newResults[rowIndex]) newResults[rowIndex] = {
|
|
1300
|
-
...newResults[rowIndex],
|
|
1301
|
-
[columnKey]: newValue
|
|
1302
|
-
};
|
|
1303
|
-
updateActiveTab({ results: newResults });
|
|
1326
|
+
if (!(await databaseAdmin?.runSqlScript?.(updateSql, {
|
|
1327
|
+
database: connection.database,
|
|
1328
|
+
role: connection.role
|
|
1329
|
+
}))?.rowCount) {
|
|
1304
1330
|
snackbarController.open({
|
|
1305
|
-
type: "
|
|
1306
|
-
message: t("
|
|
1331
|
+
type: "error",
|
|
1332
|
+
message: t("studio_sql_update_no_row")
|
|
1307
1333
|
});
|
|
1334
|
+
return;
|
|
1308
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
|
+
});
|
|
1309
1346
|
} catch (e) {
|
|
1310
1347
|
snackbarController.open({
|
|
1311
1348
|
type: "error",
|
|
@@ -1314,7 +1351,7 @@ var SQLEditor = () => {
|
|
|
1314
1351
|
}
|
|
1315
1352
|
}, [
|
|
1316
1353
|
editingCell,
|
|
1317
|
-
|
|
1354
|
+
resolveEdit,
|
|
1318
1355
|
activeTab.lastExecutedSql,
|
|
1319
1356
|
activeTab.lastExecutedConnection,
|
|
1320
1357
|
activeTab.results,
|
|
@@ -1390,7 +1427,10 @@ var SQLEditor = () => {
|
|
|
1390
1427
|
error: null,
|
|
1391
1428
|
execTime: null,
|
|
1392
1429
|
lastExecutedSql: null,
|
|
1393
|
-
lastExecutedConnection: null
|
|
1430
|
+
lastExecutedConnection: null,
|
|
1431
|
+
ranAs: null,
|
|
1432
|
+
lastProvenance: null,
|
|
1433
|
+
notices: []
|
|
1394
1434
|
}]);
|
|
1395
1435
|
setActiveTabId(newId);
|
|
1396
1436
|
};
|
|
@@ -1419,8 +1459,7 @@ var SQLEditor = () => {
|
|
|
1419
1459
|
});
|
|
1420
1460
|
}, [activeTab.sql, projectPrefix]);
|
|
1421
1461
|
const handlePrettify = () => {
|
|
1422
|
-
|
|
1423
|
-
setSql(formatted);
|
|
1462
|
+
setSql(formatSql(activeTab.sql));
|
|
1424
1463
|
};
|
|
1425
1464
|
/** The editor, for the selection the toolbar's buttons act on. */
|
|
1426
1465
|
const editorHandle = useRef(null);
|
|
@@ -1438,7 +1477,10 @@ var SQLEditor = () => {
|
|
|
1438
1477
|
error: null,
|
|
1439
1478
|
results: null,
|
|
1440
1479
|
lastExecutedSql: null,
|
|
1441
|
-
lastExecutedConnection: null
|
|
1480
|
+
lastExecutedConnection: null,
|
|
1481
|
+
ranAs: null,
|
|
1482
|
+
lastProvenance: null,
|
|
1483
|
+
notices: []
|
|
1442
1484
|
});
|
|
1443
1485
|
const start = performance.now();
|
|
1444
1486
|
try {
|
|
@@ -1460,33 +1502,39 @@ var SQLEditor = () => {
|
|
|
1460
1502
|
}
|
|
1461
1503
|
};
|
|
1462
1504
|
const executeRun = useCallback(async (sqlOverride) => {
|
|
1463
|
-
|
|
1464
|
-
const
|
|
1465
|
-
if (autoLimit && acceptsAutoLimit(sqlToRun) && !isAggregate) {
|
|
1466
|
-
sqlToRun = sqlToRun.trim().replace(/;$/, "");
|
|
1467
|
-
sqlToRun = `${sqlToRun} LIMIT 1000;`;
|
|
1468
|
-
}
|
|
1505
|
+
const written = sqlOverride || activeTab.sql;
|
|
1506
|
+
const sqlToRun = (autoLimit ? withAutoLimit(written, 1e3) : null) ?? written;
|
|
1469
1507
|
updateActiveTab({
|
|
1470
1508
|
loading: true,
|
|
1471
1509
|
error: null,
|
|
1472
|
-
results: null
|
|
1510
|
+
results: null,
|
|
1511
|
+
notices: []
|
|
1473
1512
|
});
|
|
1474
1513
|
const start = performance.now();
|
|
1475
1514
|
try {
|
|
1476
|
-
if (databaseAdmin?.executeSql) {
|
|
1477
|
-
const
|
|
1515
|
+
if (databaseAdmin?.runSqlScript || databaseAdmin?.executeSql) {
|
|
1516
|
+
const connection = {
|
|
1478
1517
|
database: selectedDatabase,
|
|
1479
1518
|
role: selectedRole
|
|
1480
|
-
}
|
|
1519
|
+
};
|
|
1520
|
+
const run = databaseAdmin.runSqlScript ? await databaseAdmin.runSqlScript(sqlToRun, connection) : void 0;
|
|
1521
|
+
const rows = run ? run.rows : await databaseAdmin.executeSql(sqlToRun, connection);
|
|
1481
1522
|
updateActiveTab({
|
|
1482
|
-
results:
|
|
1523
|
+
results: rows,
|
|
1483
1524
|
execTime: Math.round(performance.now() - start),
|
|
1484
1525
|
lastExecutedSql: sqlToRun,
|
|
1485
|
-
lastExecutedConnection:
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
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 ?? []
|
|
1489
1536
|
});
|
|
1537
|
+
if (run?.effectiveRole && connection.role) setConnectionOwner(run.effectiveRole !== connection.role ? run.effectiveRole : null);
|
|
1490
1538
|
if (history[history.length - 1] !== activeTab.sql) saveHistory([...history, activeTab.sql]);
|
|
1491
1539
|
} else updateActiveTab({ error: t("studio_sql_execution_not_supported") });
|
|
1492
1540
|
} catch (e) {
|
|
@@ -1516,6 +1564,7 @@ var SQLEditor = () => {
|
|
|
1516
1564
|
}, [activeTab.sql, executeRun]);
|
|
1517
1565
|
useEffect(() => {
|
|
1518
1566
|
const handleKeyDown = (e) => {
|
|
1567
|
+
if (isKeyHandled(e)) return;
|
|
1519
1568
|
if ((e.metaKey || e.ctrlKey) && e.key === "Enter") {
|
|
1520
1569
|
const activeElement = document.activeElement;
|
|
1521
1570
|
const isInput = activeElement?.tagName === "INPUT" || activeElement?.tagName === "TEXTAREA";
|
|
@@ -1664,14 +1713,8 @@ var SQLEditor = () => {
|
|
|
1664
1713
|
});
|
|
1665
1714
|
const savedWidths = columnWidths[activeTab.sql] || {};
|
|
1666
1715
|
const resultColumnKeys = Object.keys(results[0]);
|
|
1667
|
-
const
|
|
1668
|
-
|
|
1669
|
-
try {
|
|
1670
|
-
return resolveQueryCollections(activeTab.lastExecutedSql, schemas, collectionRegistry.collections, resultColumnKeys);
|
|
1671
|
-
} catch {
|
|
1672
|
-
return [];
|
|
1673
|
-
}
|
|
1674
|
-
})().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)) : [];
|
|
1675
1718
|
const getRowEntityActions = (rowData) => {
|
|
1676
1719
|
if (!rowData) return [];
|
|
1677
1720
|
return actionableCollections.filter((mc) => rowData[mc.pkColumn] != null).map((mc) => ({
|
|
@@ -1743,6 +1786,10 @@ var SQLEditor = () => {
|
|
|
1743
1786
|
}),
|
|
1744
1787
|
children: /* @__PURE__ */ jsx(IconButton, {
|
|
1745
1788
|
size: "small",
|
|
1789
|
+
"aria-label": t("studio_sql_edit_entity", {
|
|
1790
|
+
name: ra.collection.collection.name,
|
|
1791
|
+
id: String(ra.entityId)
|
|
1792
|
+
}),
|
|
1746
1793
|
className: "text-surface-400 dark:text-surface-500 hover:text-surface-600 dark:hover:text-surface-300 transition-colors",
|
|
1747
1794
|
onClick: (e) => {
|
|
1748
1795
|
e.stopPropagation();
|
|
@@ -1799,7 +1846,7 @@ var SQLEditor = () => {
|
|
|
1799
1846
|
children: /* @__PURE__ */ jsx("div", {
|
|
1800
1847
|
className: "truncate flex-grow",
|
|
1801
1848
|
title: displayValue,
|
|
1802
|
-
children:
|
|
1849
|
+
children: value === null || value === void 0 ? /* @__PURE__ */ jsx("span", {
|
|
1803
1850
|
className: "text-text-disabled dark:text-text-disabled-dark italic text-[11px]",
|
|
1804
1851
|
children: "NULL"
|
|
1805
1852
|
}) : displayValue
|
|
@@ -1949,6 +1996,7 @@ var SQLEditor = () => {
|
|
|
1949
1996
|
children: /* @__PURE__ */ jsx(IconButton, {
|
|
1950
1997
|
size: "small",
|
|
1951
1998
|
onClick: handlePrettify,
|
|
1999
|
+
"aria-label": t("studio_sql_format_sql"),
|
|
1952
2000
|
children: /* @__PURE__ */ jsx(MenuIcon, { size: iconSize.smallest })
|
|
1953
2001
|
})
|
|
1954
2002
|
}),
|
|
@@ -2029,7 +2077,7 @@ var SQLEditor = () => {
|
|
|
2029
2077
|
className: "mr-1.5 text-text-disabled dark:text-text-disabled-dark"
|
|
2030
2078
|
}), /* @__PURE__ */ jsx("span", {
|
|
2031
2079
|
className: "max-w-[160px] truncate",
|
|
2032
|
-
children: isLoadingConfig ? "..." : `${selectedDatabase || t("studio_sql_select_db")}${selectedRole ? ` (${selectedRole})` : ""}`
|
|
2080
|
+
children: isLoadingConfig ? "..." : `${selectedDatabase || t("studio_sql_select_db")}${connectionOwner ?? selectedRole ? ` (${connectionOwner ?? selectedRole})` : ""}`
|
|
2033
2081
|
})]
|
|
2034
2082
|
}),
|
|
2035
2083
|
children: /* @__PURE__ */ jsxs("div", {
|
|
@@ -2062,8 +2110,13 @@ var SQLEditor = () => {
|
|
|
2062
2110
|
children: t("studio_sql_role")
|
|
2063
2111
|
})
|
|
2064
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
|
+
}),
|
|
2065
2117
|
availableRoles.map((role) => /* @__PURE__ */ jsxs(MenuItem, {
|
|
2066
2118
|
dense: true,
|
|
2119
|
+
disabled: connectionOwner !== null,
|
|
2067
2120
|
onClick: () => handleRoleChange(role),
|
|
2068
2121
|
className: cls("text-xs", selectedRole === role && "text-primary dark:text-primary-dark"),
|
|
2069
2122
|
children: [role, role === "postgres" ? " " + t("studio_sql_collections_label") : ""]
|
|
@@ -2103,17 +2156,39 @@ var SQLEditor = () => {
|
|
|
2103
2156
|
}),
|
|
2104
2157
|
secondPanel: /* @__PURE__ */ jsxs("div", {
|
|
2105
2158
|
className: "h-full w-full flex flex-col bg-surface-well overflow-hidden min-h-0",
|
|
2106
|
-
children: [
|
|
2107
|
-
|
|
2108
|
-
|
|
2109
|
-
|
|
2110
|
-
|
|
2111
|
-
|
|
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()
|
|
2112
2190
|
})
|
|
2113
|
-
|
|
2114
|
-
className: "flex-grow flex flex-col min-h-0 overflow-hidden",
|
|
2115
|
-
children: renderResults()
|
|
2116
|
-
})]
|
|
2191
|
+
]
|
|
2117
2192
|
})
|
|
2118
2193
|
})]
|
|
2119
2194
|
})
|
|
@@ -2171,4 +2246,4 @@ var SQLEditor = () => {
|
|
|
2171
2246
|
//#endregion
|
|
2172
2247
|
export { SQLEditor };
|
|
2173
2248
|
|
|
2174
|
-
//# sourceMappingURL=SQLEditor-
|
|
2249
|
+
//# sourceMappingURL=SQLEditor-1An7w0j6.js.map
|