@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.
Files changed (57) hide show
  1. package/dist/{ApiExplorer-pYMZflL3.js → ApiExplorer-CCtczj3F.js} +15 -28
  2. package/dist/ApiExplorer-CCtczj3F.js.map +1 -0
  3. package/dist/ApiKeysView-B66TdzhH.js +1738 -0
  4. package/dist/ApiKeysView-B66TdzhH.js.map +1 -0
  5. package/dist/BackupsView-DmSMzAbT.js +368 -0
  6. package/dist/BackupsView-DmSMzAbT.js.map +1 -0
  7. package/dist/{BranchesView-D6Sz1a5f.js → BranchesView-DP1a87E-.js} +12 -8
  8. package/dist/BranchesView-DP1a87E-.js.map +1 -0
  9. package/dist/{CronJobsView-7hjgSYId.js → CronJobsView-TDErHTuK.js} +126 -60
  10. package/dist/CronJobsView-TDErHTuK.js.map +1 -0
  11. package/dist/{JSEditor-qiuwmcvk.js → JSEditor-B59pwg4c.js} +120 -74
  12. package/dist/JSEditor-B59pwg4c.js.map +1 -0
  13. package/dist/{LogsExplorer-Xq8K4oo6.js → LogsExplorer-BKM4yfP8.js} +48 -22
  14. package/dist/LogsExplorer-BKM4yfP8.js.map +1 -0
  15. package/dist/{RLSEditor-Djh43lBE.js → RLSEditor-BlLLV6xs.js} +237 -77
  16. package/dist/RLSEditor-BlLLV6xs.js.map +1 -0
  17. package/dist/{SQLEditor-TBw_K6CW.js → SQLEditor-1An7w0j6.js} +369 -294
  18. package/dist/SQLEditor-1An7w0j6.js.map +1 -0
  19. package/dist/{SchemaVisualizer-DtDzIUNy.js → SchemaVisualizer-DCLVfA5t.js} +20 -24
  20. package/dist/SchemaVisualizer-DCLVfA5t.js.map +1 -0
  21. package/dist/{StorageView-CMG1DOVX.js → StorageView-CbsHuCBf.js} +355 -225
  22. package/dist/StorageView-CbsHuCBf.js.map +1 -0
  23. package/dist/components/ApiExplorer/TryItPanel.d.ts +6 -1
  24. package/dist/components/ApiKeys/CreateApiKeyDialog.d.ts +21 -2
  25. package/dist/components/ApiKeys/scope-words.d.ts +37 -0
  26. package/dist/components/ApiKeys/scopes.d.ts +113 -0
  27. package/dist/components/AuthSimulationSelector.d.ts +5 -1
  28. package/dist/components/LogsExplorer/useLogTail.d.ts +9 -0
  29. package/dist/components/RLSEditor/policyListing.d.ts +44 -0
  30. package/dist/components/SchemaVisualizer/schema-visualizer.utils.d.ts +0 -1
  31. package/dist/components/SchemaVisualizer/useSchemaGraph.d.ts +6 -0
  32. package/dist/components/load-failure.d.ts +1 -1
  33. package/dist/components/useRunAsUsers.d.ts +25 -0
  34. package/dist/index.es.js +11 -11
  35. package/dist/{load-failure-C6OMJSGC.js → load-failure-Bd8lFIO7.js} +2 -2
  36. package/dist/load-failure-Bd8lFIO7.js.map +1 -0
  37. package/dist/useRunAsUsers-CLaAtXt6.js +122 -0
  38. package/dist/useRunAsUsers-CLaAtXt6.js.map +1 -0
  39. package/dist/utils/sql_utils.d.ts +90 -49
  40. package/package.json +9 -9
  41. package/dist/ApiExplorer-pYMZflL3.js.map +0 -1
  42. package/dist/ApiKeysView-qLSs47tw.js +0 -1239
  43. package/dist/ApiKeysView-qLSs47tw.js.map +0 -1
  44. package/dist/AuthSimulationSelector-BewmhMBw.js +0 -56
  45. package/dist/AuthSimulationSelector-BewmhMBw.js.map +0 -1
  46. package/dist/BackupsView-DYTAD6aW.js +0 -253
  47. package/dist/BackupsView-DYTAD6aW.js.map +0 -1
  48. package/dist/BranchesView-D6Sz1a5f.js.map +0 -1
  49. package/dist/CronJobsView-7hjgSYId.js.map +0 -1
  50. package/dist/JSEditor-qiuwmcvk.js.map +0 -1
  51. package/dist/LogsExplorer-Xq8K4oo6.js.map +0 -1
  52. package/dist/RLSEditor-Djh43lBE.js.map +0 -1
  53. package/dist/SQLEditor-TBw_K6CW.js.map +0 -1
  54. package/dist/SchemaVisualizer-DtDzIUNy.js.map +0 -1
  55. package/dist/StorageView-CMG1DOVX.js.map +0 -1
  56. package/dist/components/ApiKeys/permissions.d.ts +0 -66
  57. 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 { parse, parseFirst } from "pgsql-ast-parser";
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 the query named its schema, and a
474
- * bare `"table"` when it left it to the search path — which then resolves it
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
- const quote = (identifier) => `"${identifier.replace(/"/g, "\"\"")}"`;
479
- return schemaName ? `${quote(schemaName)}.${quote(tableName)}` : quote(tableName);
477
+ return schemaName ? `${quoteIdentifier(schemaName)}.${quoteIdentifier(tableName)}` : quoteIdentifier(tableName);
480
478
  }
481
- /**
482
- * The introspected columns of a table the query named — in the schema it
483
- * named, when it named one. Unqualified, the first schema that has a table of
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
- * The name Postgres gives the result column of an unaliased select item, as
495
- * far as it can be told from the expression: a column keeps its name, a cast
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
- * A result row is an object keyed by column name, so where two columns share
526
- * a name the row holds only the last of them. A key or a value read from such
527
- * a row cannot be told apart from its namesake: for `SELECT * FROM posts p
528
- * JOIN authors a …` the row's `id` is the author's.
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 resultColumnCounter(ast, schemas) {
531
- const sources = (ast.from ?? []).map((item) => {
532
- if (item.type === "table") {
533
- const renamed = item.name.columnNames ?? [];
534
- const columns = findTableInfo(schemas, {
535
- name: item.name.name,
536
- schema: item.name.schema
537
- })?.columns.map((c, i) => renamed[i]?.name ?? c.name);
538
- return {
539
- refName: item.name.alias ?? item.name.name,
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
- return (name) => {
559
- let count = 0;
560
- for (const column of ast.columns ?? []) {
561
- const expr = column.expr;
562
- if (expr.type === "ref" && expr.name === "*") {
563
- const covered = expr.table ? sources.filter((source) => source.refName === expr.table?.name) : sources;
564
- if (covered.length === 0) count += 1;
565
- for (const source of covered) count += source.columns ? source.columns.filter((c) => c === name).length : 1;
566
- } else if (selectedColumnName(column) === name) count += 1;
567
- }
568
- return count;
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
- * Extract all tables referenced in a SQL query's FROM and JOIN clauses.
573
- * Returns an empty array for non-SELECT queries or parse failures.
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 extractTablesFromQuery(sqlString) {
576
- try {
577
- const ast = parseFirst(sqlString);
578
- if (ast.type !== "select") return [];
579
- const tables = [];
580
- const processFrom = (fromItems) => {
581
- for (const item of fromItems) {
582
- if (item.type === "table" && item.name) tables.push({
583
- name: item.name.name,
584
- schema: item.name.schema,
585
- alias: item.name.alias
586
- });
587
- if (item.type === "join") {
588
- if (item.left) processFrom([item.left]);
589
- if (item.right) processFrom([item.right]);
590
- }
591
- }
592
- };
593
- if (ast.from) processFrom(ast.from);
594
- return tables;
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
- * Resolve which collections are referenced by a SQL query.
601
- *
602
- * Parses the SQL, extracts table names, and matches each against
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
- * For each matched collection, determines which result columns
607
- * belong to that table using the database schema information.
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(sqlString, schemas, collections, resultColumns) {
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 matched = collections.find((c) => {
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.name && (!table.schema || table.schema === schemaName);
585
+ return tableName === table.table && schemaName === table.schema;
631
586
  });
632
- if (!matched) continue;
633
- const tableColumns = findTableInfo(schemas, table)?.columns.map((c) => c.name) ?? [];
634
- let pkColumn;
635
- const tableRef = table.alias || table.name;
636
- const idSelectCol = selectColumns.find((sc) => sc.column === "id" && (!sc.table || sc.table === tableRef || sc.table === table.name));
637
- if (idSelectCol) pkColumn = idSelectCol.alias || idSelectCol.column;
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.name,
645
- tableAlias: table.alias,
646
- collection: matched,
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
- function determineTableAndPK(sqlString, columnKey, schemas) {
654
- try {
655
- const tables = extractTablesFromQuery(sqlString);
656
- const ast = parseFirst(sqlString);
657
- if (ast.type !== "select") return { error: "Inline editing is only supported for SELECT queries." };
658
- if (tables.length === 0) return { error: "Could not find any tables in the query." };
659
- const selectColumns = [];
660
- if (ast.columns) {
661
- for (const col of ast.columns) if (col.expr?.type === "ref") selectColumns.push({
662
- table: col.expr.table?.name,
663
- column: col.expr.name,
664
- alias: col.alias?.name
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
- const resolvedColumn = selectColumns.find((sc) => sc.alias === columnKey || !sc.alias && sc.column === columnKey);
668
- const actualDbColumnName = resolvedColumn?.column ?? columnKey;
669
- const columnTableRef = resolvedColumn?.table;
670
- let resolvedTable = null;
671
- if (tables.length === 1) resolvedTable = tables[0];
672
- else {
673
- if (columnTableRef) {
674
- const matchedTable = tables.find((t) => t.alias === columnTableRef || t.name === columnTableRef);
675
- if (matchedTable) resolvedTable = matchedTable;
676
- }
677
- if (!resolvedTable) {
678
- const matchedTables = tables.filter((t) => findTableInfo(schemas, t)?.columns.some((c) => c.name === actualDbColumnName) ?? false);
679
- if (matchedTables.length === 1) resolvedTable = matchedTables[0];
680
- else if (matchedTables.length > 1) return { error: `Ambiguous column "${columnKey}": Found in multiple queried tables.` };
681
- else return { error: `Could not find column "${columnKey}" in the queried tables.` };
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
- if (!resolvedTable) return { error: "Could not resolve the target table." };
685
- const resolvedTableName = resolvedTable.name;
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 || null);
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 || null);
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 = determineTableAndPK(activeTab.lastExecutedSql, columnKey, schemas);
1221
- if (resolution.error || !resolution.primaryKeys || resolution.primaryKeys.length === 0) {
1266
+ const resolution = resolveEdit(columnKey);
1267
+ if (!resolution.target) {
1222
1268
  snackbarController.open({
1223
1269
  type: "error",
1224
- message: resolution.error || t("studio_sql_cannot_resolve_table")
1270
+ message: t(resolution.refusal.key, resolution.refusal.params)
1225
1271
  });
1226
1272
  return;
1227
1273
  }
1228
- const missingPKs = resolution.primaryKeys.filter((pk) => rowData[pk.resultColumn] === void 0 || rowData[pk.resultColumn] === null);
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
- schemas,
1291
+ resolveEdit,
1246
1292
  snackbarController,
1247
1293
  t
1248
1294
  ]);
1249
- const handleCellSave = useCallback(async (newValue, rowData, columnKey, rowIndex) => {
1295
+ const handleCellSave = useCallback(async (text, rowData, columnKey, rowIndex) => {
1250
1296
  if (!editingCell || !activeTab.lastExecutedSql) return;
1251
1297
  setEditingCell(null);
1252
- if (newValue === editingCell.initialValue) return;
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 = determineTableAndPK(activeTab.lastExecutedSql, columnKey, schemas);
1262
- if (resolution.error || !resolution.tableName || !resolution.primaryKeys || resolution.primaryKeys.length === 0) {
1308
+ const resolution = resolveEdit(columnKey);
1309
+ if (!resolution.target) {
1263
1310
  snackbarController.open({
1264
1311
  type: "error",
1265
- message: resolution.error || "Resolution failed."
1312
+ message: t(resolution.refusal.key, resolution.refusal.params)
1266
1313
  });
1267
1314
  return;
1268
1315
  }
1269
- const tableName = quoteTableName(resolution.tableName, resolution.schemaName);
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 resolveDbColumnName = (resultColKey) => {
1277
- try {
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?.executeSql) {
1294
- await databaseAdmin.executeSql(updateSql, {
1295
- database: connection.database,
1296
- role: connection.role
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: "success",
1306
- message: t("studio_sql_row_updated")
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
- schemas,
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
- const formatted = activeTab.sql.replace(/\s+/g, " ").replace(/\s?,\s?/g, ", ").replace(/\s?=\s?/g, " = ").trim();
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
- let sqlToRun = sqlOverride || activeTab.sql;
1464
- const isAggregate = /\b(COUNT|SUM|AVG|MIN|MAX)\s*\(/i.test(sqlToRun);
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 result = await databaseAdmin.executeSql(sqlToRun, {
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: result,
1523
+ results: rows,
1483
1524
  execTime: Math.round(performance.now() - start),
1484
1525
  lastExecutedSql: sqlToRun,
1485
- lastExecutedConnection: {
1486
- database: selectedDatabase,
1487
- role: selectedRole
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 actionableCollections = (() => {
1668
- if (!activeTab.lastExecutedSql || !collectionRegistry.collections) return [];
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: displayValue === "" ? /* @__PURE__ */ jsx("span", {
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: [/* @__PURE__ */ jsx("div", {
2107
- className: cls("p-2 px-4 bg-surface-raised border-b shrink-0 flex items-center", defaultBorderMixin),
2108
- children: /* @__PURE__ */ jsx(Typography, {
2109
- variant: "caption",
2110
- className: "font-semibold text-text-disabled dark:text-text-disabled-dark uppercase tracking-widest text-[10px]",
2111
- children: t("studio_sql_query_results")
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
- }), /* @__PURE__ */ jsx("div", {
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-TBw_K6CW.js.map
2249
+ //# sourceMappingURL=SQLEditor-1An7w0j6.js.map