@rebasepro/studio 0.22.0 → 0.23.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{ApiExplorer-D-jLWAaY.js → ApiExplorer-pYMZflL3.js} +2 -5
- package/dist/ApiExplorer-pYMZflL3.js.map +1 -0
- package/dist/{ApiKeysView-OfJLH2x7.js → ApiKeysView-qLSs47tw.js} +3 -3
- package/dist/{ApiKeysView-OfJLH2x7.js.map → ApiKeysView-qLSs47tw.js.map} +1 -1
- package/dist/{CronJobsView-JwRjQCde.js → CronJobsView-7hjgSYId.js} +18 -9
- package/dist/CronJobsView-7hjgSYId.js.map +1 -0
- package/dist/{MonacoEditor-D0_QljcX.js → MonacoEditor-CUxy5_k5.js} +11 -9
- package/dist/MonacoEditor-CUxy5_k5.js.map +1 -0
- package/dist/{RLSEditor-B-qrgdHy.js → RLSEditor-Djh43lBE.js} +118 -26
- package/dist/RLSEditor-Djh43lBE.js.map +1 -0
- package/dist/{SQLEditor-sdtoZv5F.js → SQLEditor-TBw_K6CW.js} +259 -62
- package/dist/SQLEditor-TBw_K6CW.js.map +1 -0
- package/dist/{StorageView-C52dSZNf.js → StorageView-CMG1DOVX.js} +40 -16
- package/dist/StorageView-CMG1DOVX.js.map +1 -0
- package/dist/components/RLSEditor/policyRules.d.ts +49 -0
- package/dist/components/SQLEditor/MonacoEditor.d.ts +13 -1
- package/dist/index.es.js +6 -6
- package/dist/utils/sql_utils.d.ts +49 -0
- package/package.json +9 -9
- package/dist/ApiExplorer-D-jLWAaY.js.map +0 -1
- package/dist/CronJobsView-JwRjQCde.js.map +0 -1
- package/dist/MonacoEditor-D0_QljcX.js.map +0 -1
- package/dist/RLSEditor-B-qrgdHy.js.map +0 -1
- package/dist/SQLEditor-sdtoZv5F.js.map +0 -1
- package/dist/StorageView-C52dSZNf.js.map +0 -1
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
import { t as MonacoEditor } from "./MonacoEditor-
|
|
1
|
+
import { t as MonacoEditor } from "./MonacoEditor-CUxy5_k5.js";
|
|
2
2
|
import { ConfirmationDialog, ErrorView, IconForView, useRebaseContext, useSnackbarController, useStudioCollectionRegistry, useStudioSidePanelController, useTranslation } from "@rebasepro/app";
|
|
3
3
|
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
4
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";
|
|
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 { parseFirst } from "pgsql-ast-parser";
|
|
8
|
+
import { parse, parseFirst } from "pgsql-ast-parser";
|
|
9
9
|
//#region src/components/SQLEditor/SchemaBrowser.tsx
|
|
10
10
|
var SchemaBrowser = ({ onTableClick, schemas, isSchemaLoading, schemaError, onRetrySchema }) => {
|
|
11
11
|
const [expandedSchemas, setExpandedSchemas] = useState({ public: true });
|
|
@@ -393,6 +393,181 @@ var SQLEditorSidebar = ({ onSelectSnippet, onTableClick, snippets, history, onDe
|
|
|
393
393
|
};
|
|
394
394
|
//#endregion
|
|
395
395
|
//#region src/utils/sql_utils.ts
|
|
396
|
+
/** The statements in `sqlText`, or `null` when the parser cannot read it. */
|
|
397
|
+
function parseStatements(sqlText) {
|
|
398
|
+
try {
|
|
399
|
+
return parse(sqlText);
|
|
400
|
+
} catch {
|
|
401
|
+
return null;
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
/**
|
|
405
|
+
* The `EXPLAIN` for one statement, or `null` when `sqlText` is not exactly one.
|
|
406
|
+
*
|
|
407
|
+
* Never `ANALYZE`: that executes the statement to time it, so explaining a
|
|
408
|
+
* `DELETE` deleted the rows — without the confirmation "Run" asks for.
|
|
409
|
+
*
|
|
410
|
+
* And never more than one statement. Sent as one simple query, `EXPLAIN (…)
|
|
411
|
+
* SELECT 1; DELETE …` explains the SELECT and *runs* the DELETE. Text the
|
|
412
|
+
* parser cannot read is still explained, as long as no `;` could be hiding a
|
|
413
|
+
* second statement in it.
|
|
414
|
+
*/
|
|
415
|
+
function buildExplainSql(sqlText) {
|
|
416
|
+
const statement = sqlText.trim().replace(/;+$/, "").trim();
|
|
417
|
+
if (!statement) return null;
|
|
418
|
+
const statements = parseStatements(statement);
|
|
419
|
+
return (statements ? statements.length === 1 : !statement.includes(";")) ? `EXPLAIN (FORMAT JSON) ${statement}` : null;
|
|
420
|
+
}
|
|
421
|
+
/**
|
|
422
|
+
* Whether the console's automatic `LIMIT` may be appended to `sqlText`: one
|
|
423
|
+
* top-level SELECT that has no limit of its own.
|
|
424
|
+
*
|
|
425
|
+
* Decided on the parsed statement, not the text. A search for the word SELECT
|
|
426
|
+
* also found it inside `INSERT INTO … SELECT` and `CREATE TABLE … AS SELECT`,
|
|
427
|
+
* which then copied a thousand rows and reported success, and at the start of
|
|
428
|
+
* a script whose last statement the appended `LIMIT` turned into a syntax
|
|
429
|
+
* error. Text the parser cannot read is left as written.
|
|
430
|
+
*/
|
|
431
|
+
function acceptsAutoLimit(sqlText) {
|
|
432
|
+
const statements = parseStatements(sqlText);
|
|
433
|
+
if (!statements || statements.length !== 1) return false;
|
|
434
|
+
const [statement] = statements;
|
|
435
|
+
return statement.type === "select" && !statement.limit;
|
|
436
|
+
}
|
|
437
|
+
/** A destructive command named anywhere in a text, comments included. */
|
|
438
|
+
var DESTRUCTIVE_COMMAND = /\b(DELETE|UPDATE|DROP|TRUNCATE)\b/i;
|
|
439
|
+
function isDestructiveStatement(statement) {
|
|
440
|
+
switch (statement.type) {
|
|
441
|
+
case "delete":
|
|
442
|
+
case "update": return !statement.where;
|
|
443
|
+
case "truncate table":
|
|
444
|
+
case "drop table":
|
|
445
|
+
case "drop sequence":
|
|
446
|
+
case "drop index":
|
|
447
|
+
case "drop type":
|
|
448
|
+
case "drop trigger":
|
|
449
|
+
case "drop function": return true;
|
|
450
|
+
case "alter table": return statement.changes.some((change) => change.type === "drop column" || change.type === "drop constraint");
|
|
451
|
+
case "with": return statement.bind.some((binding) => isDestructiveStatement(binding.statement)) || isDestructiveStatement(statement.in);
|
|
452
|
+
case "with recursive": return isDestructiveStatement(statement.in);
|
|
453
|
+
case "do": return DESTRUCTIVE_COMMAND.test(statement.code);
|
|
454
|
+
default: return false;
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
/**
|
|
458
|
+
* Whether "Run" asks before running `sqlText`: a statement in it deletes or
|
|
459
|
+
* updates without a WHERE of its own, drops or truncates.
|
|
460
|
+
*
|
|
461
|
+
* Decided per parsed statement, not on the text. A search of the text for the
|
|
462
|
+
* word WHERE found it in another statement or in a comment, so `DELETE FROM
|
|
463
|
+
* posts; SELECT * FROM posts WHERE id = 1` deleted every post without asking.
|
|
464
|
+
* Text the parser cannot read is confirmed whenever it names a destructive
|
|
465
|
+
* command.
|
|
466
|
+
*/
|
|
467
|
+
function needsDestructiveConfirmation(sqlText) {
|
|
468
|
+
const statements = parseStatements(sqlText);
|
|
469
|
+
if (!statements) return DESTRUCTIVE_COMMAND.test(sqlText);
|
|
470
|
+
return statements.some(isDestructiveStatement);
|
|
471
|
+
}
|
|
472
|
+
/**
|
|
473
|
+
* A table as SQL: `"schema"."table"` when 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.
|
|
476
|
+
*/
|
|
477
|
+
function quoteTableName(tableName, schemaName) {
|
|
478
|
+
const quote = (identifier) => `"${identifier.replace(/"/g, "\"\"")}"`;
|
|
479
|
+
return schemaName ? `${quote(schemaName)}.${quote(tableName)}` : quote(tableName);
|
|
480
|
+
}
|
|
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
|
+
}
|
|
492
|
+
}
|
|
493
|
+
/**
|
|
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.
|
|
524
|
+
*
|
|
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.
|
|
529
|
+
*/
|
|
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
|
+
};
|
|
557
|
+
});
|
|
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
|
+
};
|
|
570
|
+
}
|
|
396
571
|
/**
|
|
397
572
|
* Extract all tables referenced in a SQL query's FROM and JOIN clauses.
|
|
398
573
|
* Returns an empty array for non-SELECT queries or parse failures.
|
|
@@ -406,6 +581,7 @@ function extractTablesFromQuery(sqlString) {
|
|
|
406
581
|
for (const item of fromItems) {
|
|
407
582
|
if (item.type === "table" && item.name) tables.push({
|
|
408
583
|
name: item.name.name,
|
|
584
|
+
schema: item.name.schema,
|
|
409
585
|
alias: item.name.alias
|
|
410
586
|
});
|
|
411
587
|
if (item.type === "join") {
|
|
@@ -434,10 +610,12 @@ function resolveQueryCollections(sqlString, schemas, collections, resultColumns)
|
|
|
434
610
|
const tables = extractTablesFromQuery(sqlString);
|
|
435
611
|
if (tables.length === 0) return [];
|
|
436
612
|
const selectColumns = [];
|
|
613
|
+
let countResultColumns;
|
|
437
614
|
try {
|
|
438
615
|
const ast = parseFirst(sqlString);
|
|
439
|
-
if (ast.type === "select"
|
|
440
|
-
|
|
616
|
+
if (ast.type === "select") {
|
|
617
|
+
countResultColumns = resultColumnCounter(ast, schemas);
|
|
618
|
+
for (const col of ast.columns ?? []) if (col.expr?.type === "ref") selectColumns.push({
|
|
441
619
|
table: col.expr.table?.name,
|
|
442
620
|
column: col.expr.name,
|
|
443
621
|
alias: col.alias?.name
|
|
@@ -447,17 +625,12 @@ function resolveQueryCollections(sqlString, schemas, collections, resultColumns)
|
|
|
447
625
|
const results = [];
|
|
448
626
|
for (const table of tables) {
|
|
449
627
|
const matched = collections.find((c) => {
|
|
450
|
-
|
|
628
|
+
const tableName = ("table" in c ? c.table : void 0) || toSnakeCase(c.slug);
|
|
629
|
+
const schemaName = ("schema" in c ? c.schema : void 0) || "public";
|
|
630
|
+
return tableName === table.name && (!table.schema || table.schema === schemaName);
|
|
451
631
|
});
|
|
452
632
|
if (!matched) continue;
|
|
453
|
-
const tableColumns = [];
|
|
454
|
-
for (const schemaEntries of Object.values(schemas)) {
|
|
455
|
-
const tableInfo = schemaEntries.find((t) => t.tableName === table.name);
|
|
456
|
-
if (tableInfo) {
|
|
457
|
-
tableColumns.push(...tableInfo.columns.map((c) => c.name));
|
|
458
|
-
break;
|
|
459
|
-
}
|
|
460
|
-
}
|
|
633
|
+
const tableColumns = findTableInfo(schemas, table)?.columns.map((c) => c.name) ?? [];
|
|
461
634
|
let pkColumn;
|
|
462
635
|
const tableRef = table.alias || table.name;
|
|
463
636
|
const idSelectCol = selectColumns.find((sc) => sc.column === "id" && (!sc.table || sc.table === tableRef || sc.table === table.name));
|
|
@@ -466,6 +639,7 @@ function resolveQueryCollections(sqlString, schemas, collections, resultColumns)
|
|
|
466
639
|
if (resultColumns.includes("id")) pkColumn = "id";
|
|
467
640
|
}
|
|
468
641
|
if (!pkColumn && tableColumns.includes("id")) pkColumn = "id";
|
|
642
|
+
if (pkColumn && (!countResultColumns || countResultColumns(pkColumn) > 1)) pkColumn = void 0;
|
|
469
643
|
results.push({
|
|
470
644
|
tableName: table.name,
|
|
471
645
|
tableAlias: table.alias,
|
|
@@ -493,37 +667,25 @@ function determineTableAndPK(sqlString, columnKey, schemas) {
|
|
|
493
667
|
const resolvedColumn = selectColumns.find((sc) => sc.alias === columnKey || !sc.alias && sc.column === columnKey);
|
|
494
668
|
const actualDbColumnName = resolvedColumn?.column ?? columnKey;
|
|
495
669
|
const columnTableRef = resolvedColumn?.table;
|
|
496
|
-
let
|
|
497
|
-
if (tables.length === 1)
|
|
670
|
+
let resolvedTable = null;
|
|
671
|
+
if (tables.length === 1) resolvedTable = tables[0];
|
|
498
672
|
else {
|
|
499
673
|
if (columnTableRef) {
|
|
500
674
|
const matchedTable = tables.find((t) => t.alias === columnTableRef || t.name === columnTableRef);
|
|
501
|
-
if (matchedTable)
|
|
675
|
+
if (matchedTable) resolvedTable = matchedTable;
|
|
502
676
|
}
|
|
503
|
-
if (!
|
|
504
|
-
const matchedTables = tables.filter((t) =>
|
|
505
|
-
|
|
506
|
-
const tableInfo = schema.find((ti) => ti.tableName === t.name);
|
|
507
|
-
if (tableInfo && tableInfo.columns.some((c) => c.name === actualDbColumnName)) return true;
|
|
508
|
-
}
|
|
509
|
-
return false;
|
|
510
|
-
});
|
|
511
|
-
if (matchedTables.length === 1) resolvedTableName = matchedTables[0].name;
|
|
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];
|
|
512
680
|
else if (matchedTables.length > 1) return { error: `Ambiguous column "${columnKey}": Found in multiple queried tables.` };
|
|
513
681
|
else return { error: `Could not find column "${columnKey}" in the queried tables.` };
|
|
514
682
|
}
|
|
515
683
|
}
|
|
516
|
-
if (!
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
const tableInfo = schema.find((t) => t.tableName === resolvedTableName);
|
|
520
|
-
if (tableInfo) {
|
|
521
|
-
pkDbColumns = tableInfo.columns.filter((c) => c.isPrimaryKey).map((c) => c.name);
|
|
522
|
-
break;
|
|
523
|
-
}
|
|
524
|
-
}
|
|
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);
|
|
525
687
|
if (pkDbColumns.length === 0) return { error: `Table "${resolvedTableName}" has no primary key defined.` };
|
|
526
|
-
const tableAlias =
|
|
688
|
+
const tableAlias = resolvedTable.alias;
|
|
527
689
|
const primaryKeys = pkDbColumns.map((dbCol) => {
|
|
528
690
|
const selectCol = selectColumns.find((sc) => sc.column === dbCol && (!sc.table || sc.table === (tableAlias || resolvedTableName)));
|
|
529
691
|
return {
|
|
@@ -531,8 +693,13 @@ function determineTableAndPK(sqlString, columnKey, schemas) {
|
|
|
531
693
|
resultColumn: selectCol?.alias || selectCol?.column || dbCol
|
|
532
694
|
};
|
|
533
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.` };
|
|
534
700
|
return {
|
|
535
701
|
tableName: resolvedTableName,
|
|
702
|
+
schemaName: resolvedTable.schema,
|
|
536
703
|
primaryKeys
|
|
537
704
|
};
|
|
538
705
|
} catch (e) {
|
|
@@ -805,7 +972,8 @@ var SQLEditor = () => {
|
|
|
805
972
|
loading: false,
|
|
806
973
|
error: null,
|
|
807
974
|
execTime: null,
|
|
808
|
-
lastExecutedSql: null
|
|
975
|
+
lastExecutedSql: null,
|
|
976
|
+
lastExecutedConnection: null
|
|
809
977
|
}));
|
|
810
978
|
if (restored.length > 0) return restored;
|
|
811
979
|
return [{
|
|
@@ -818,7 +986,8 @@ var SQLEditor = () => {
|
|
|
818
986
|
loading: false,
|
|
819
987
|
error: null,
|
|
820
988
|
execTime: null,
|
|
821
|
-
lastExecutedSql: null
|
|
989
|
+
lastExecutedSql: null,
|
|
990
|
+
lastExecutedConnection: null
|
|
822
991
|
}];
|
|
823
992
|
});
|
|
824
993
|
const [activeTabId, setActiveTabId] = useState(() => {
|
|
@@ -1032,6 +1201,7 @@ var SQLEditor = () => {
|
|
|
1032
1201
|
const [isConfirmDialogOpen, setIsConfirmDialogOpen] = useState(false);
|
|
1033
1202
|
const [pendingAction, setPendingAction] = useState(null);
|
|
1034
1203
|
const [editingCell, setEditingCell] = useState(null);
|
|
1204
|
+
const isCurrentConnection = useCallback((connection) => connection !== null && connection.database === selectedDatabase && connection.role === selectedRole, [selectedDatabase, selectedRole]);
|
|
1035
1205
|
const handleDoubleClick = useCallback((rowIndex, columnKey, initialValue, rowData) => {
|
|
1036
1206
|
if (!activeTab.lastExecutedSql) {
|
|
1037
1207
|
snackbarController.open({
|
|
@@ -1040,6 +1210,13 @@ var SQLEditor = () => {
|
|
|
1040
1210
|
});
|
|
1041
1211
|
return;
|
|
1042
1212
|
}
|
|
1213
|
+
if (!isCurrentConnection(activeTab.lastExecutedConnection)) {
|
|
1214
|
+
snackbarController.open({
|
|
1215
|
+
type: "error",
|
|
1216
|
+
message: t("studio_sql_cannot_edit_other_connection")
|
|
1217
|
+
});
|
|
1218
|
+
return;
|
|
1219
|
+
}
|
|
1043
1220
|
const resolution = determineTableAndPK(activeTab.lastExecutedSql, columnKey, schemas);
|
|
1044
1221
|
if (resolution.error || !resolution.primaryKeys || resolution.primaryKeys.length === 0) {
|
|
1045
1222
|
snackbarController.open({
|
|
@@ -1063,13 +1240,24 @@ var SQLEditor = () => {
|
|
|
1063
1240
|
});
|
|
1064
1241
|
}, [
|
|
1065
1242
|
activeTab.lastExecutedSql,
|
|
1243
|
+
activeTab.lastExecutedConnection,
|
|
1244
|
+
isCurrentConnection,
|
|
1066
1245
|
schemas,
|
|
1067
|
-
snackbarController
|
|
1246
|
+
snackbarController,
|
|
1247
|
+
t
|
|
1068
1248
|
]);
|
|
1069
1249
|
const handleCellSave = useCallback(async (newValue, rowData, columnKey, rowIndex) => {
|
|
1070
1250
|
if (!editingCell || !activeTab.lastExecutedSql) return;
|
|
1071
1251
|
setEditingCell(null);
|
|
1072
1252
|
if (newValue === editingCell.initialValue) return;
|
|
1253
|
+
const connection = activeTab.lastExecutedConnection;
|
|
1254
|
+
if (!isCurrentConnection(connection)) {
|
|
1255
|
+
snackbarController.open({
|
|
1256
|
+
type: "error",
|
|
1257
|
+
message: t("studio_sql_cannot_edit_other_connection")
|
|
1258
|
+
});
|
|
1259
|
+
return;
|
|
1260
|
+
}
|
|
1073
1261
|
const resolution = determineTableAndPK(activeTab.lastExecutedSql, columnKey, schemas);
|
|
1074
1262
|
if (resolution.error || !resolution.tableName || !resolution.primaryKeys || resolution.primaryKeys.length === 0) {
|
|
1075
1263
|
snackbarController.open({
|
|
@@ -1078,7 +1266,7 @@ var SQLEditor = () => {
|
|
|
1078
1266
|
});
|
|
1079
1267
|
return;
|
|
1080
1268
|
}
|
|
1081
|
-
const tableName = resolution.tableName;
|
|
1269
|
+
const tableName = quoteTableName(resolution.tableName, resolution.schemaName);
|
|
1082
1270
|
const formatValue = (val) => {
|
|
1083
1271
|
if (val === null || val === void 0) return "NULL";
|
|
1084
1272
|
if (typeof val === "number") return val;
|
|
@@ -1100,12 +1288,12 @@ var SQLEditor = () => {
|
|
|
1100
1288
|
};
|
|
1101
1289
|
const dbColumnName = resolveDbColumnName(columnKey);
|
|
1102
1290
|
const whereConditions = resolution.primaryKeys.map((pk) => `"${pk.dbColumn}" = ${formatValue(rowData[pk.resultColumn])}`).join(" AND ");
|
|
1103
|
-
const updateSql = `UPDATE
|
|
1291
|
+
const updateSql = `UPDATE ${tableName} SET "${dbColumnName}" = ${formatValue(newValue)} WHERE ${whereConditions};`;
|
|
1104
1292
|
try {
|
|
1105
1293
|
if (databaseAdmin?.executeSql) {
|
|
1106
1294
|
await databaseAdmin.executeSql(updateSql, {
|
|
1107
|
-
database:
|
|
1108
|
-
role:
|
|
1295
|
+
database: connection.database,
|
|
1296
|
+
role: connection.role
|
|
1109
1297
|
});
|
|
1110
1298
|
const newResults = [...activeTab.results || []];
|
|
1111
1299
|
if (newResults[rowIndex]) newResults[rowIndex] = {
|
|
@@ -1128,12 +1316,13 @@ var SQLEditor = () => {
|
|
|
1128
1316
|
editingCell,
|
|
1129
1317
|
schemas,
|
|
1130
1318
|
activeTab.lastExecutedSql,
|
|
1319
|
+
activeTab.lastExecutedConnection,
|
|
1131
1320
|
activeTab.results,
|
|
1321
|
+
isCurrentConnection,
|
|
1132
1322
|
databaseAdmin,
|
|
1133
1323
|
updateActiveTab,
|
|
1134
1324
|
snackbarController,
|
|
1135
|
-
|
|
1136
|
-
selectedRole
|
|
1325
|
+
t
|
|
1137
1326
|
]);
|
|
1138
1327
|
const [columnWidths, setColumnWidths] = useState(() => {
|
|
1139
1328
|
return readStoredJson(`rebase_sql_column_widths_${client?.baseUrl ? client.baseUrl.replace(/^https?:\/\//, "").replace(/[^a-zA-Z0-9]/g, "_") : "default"}`, {
|
|
@@ -1200,7 +1389,8 @@ var SQLEditor = () => {
|
|
|
1200
1389
|
loading: false,
|
|
1201
1390
|
error: null,
|
|
1202
1391
|
execTime: null,
|
|
1203
|
-
lastExecutedSql: null
|
|
1392
|
+
lastExecutedSql: null,
|
|
1393
|
+
lastExecutedConnection: null
|
|
1204
1394
|
}]);
|
|
1205
1395
|
setActiveTabId(newId);
|
|
1206
1396
|
};
|
|
@@ -1232,12 +1422,23 @@ var SQLEditor = () => {
|
|
|
1232
1422
|
const formatted = activeTab.sql.replace(/\s+/g, " ").replace(/\s?,\s?/g, ", ").replace(/\s?=\s?/g, " = ").trim();
|
|
1233
1423
|
setSql(formatted);
|
|
1234
1424
|
};
|
|
1425
|
+
/** The editor, for the selection the toolbar's buttons act on. */
|
|
1426
|
+
const editorHandle = useRef(null);
|
|
1235
1427
|
const handleExplain = async () => {
|
|
1236
|
-
const explainSql =
|
|
1428
|
+
const explainSql = buildExplainSql(editorHandle.current?.getSelectedText() ?? activeTab.sql);
|
|
1429
|
+
if (!explainSql) {
|
|
1430
|
+
updateActiveTab({
|
|
1431
|
+
results: null,
|
|
1432
|
+
error: t("studio_sql_explain_single_statement")
|
|
1433
|
+
});
|
|
1434
|
+
return;
|
|
1435
|
+
}
|
|
1237
1436
|
updateActiveTab({
|
|
1238
1437
|
loading: true,
|
|
1239
1438
|
error: null,
|
|
1240
|
-
results: null
|
|
1439
|
+
results: null,
|
|
1440
|
+
lastExecutedSql: null,
|
|
1441
|
+
lastExecutedConnection: null
|
|
1241
1442
|
});
|
|
1242
1443
|
const start = performance.now();
|
|
1243
1444
|
try {
|
|
@@ -1260,10 +1461,8 @@ var SQLEditor = () => {
|
|
|
1260
1461
|
};
|
|
1261
1462
|
const executeRun = useCallback(async (sqlOverride) => {
|
|
1262
1463
|
let sqlToRun = sqlOverride || activeTab.sql;
|
|
1263
|
-
const upperSql = sqlToRun.toUpperCase();
|
|
1264
1464
|
const isAggregate = /\b(COUNT|SUM|AVG|MIN|MAX)\s*\(/i.test(sqlToRun);
|
|
1265
|
-
|
|
1266
|
-
if (autoLimit && upperSql.includes("SELECT") && !upperSql.includes("LIMIT") && !isAggregate && !isExplain) {
|
|
1465
|
+
if (autoLimit && acceptsAutoLimit(sqlToRun) && !isAggregate) {
|
|
1267
1466
|
sqlToRun = sqlToRun.trim().replace(/;$/, "");
|
|
1268
1467
|
sqlToRun = `${sqlToRun} LIMIT 1000;`;
|
|
1269
1468
|
}
|
|
@@ -1282,7 +1481,11 @@ var SQLEditor = () => {
|
|
|
1282
1481
|
updateActiveTab({
|
|
1283
1482
|
results: result,
|
|
1284
1483
|
execTime: Math.round(performance.now() - start),
|
|
1285
|
-
lastExecutedSql: sqlToRun
|
|
1484
|
+
lastExecutedSql: sqlToRun,
|
|
1485
|
+
lastExecutedConnection: {
|
|
1486
|
+
database: selectedDatabase,
|
|
1487
|
+
role: selectedRole
|
|
1488
|
+
}
|
|
1286
1489
|
});
|
|
1287
1490
|
if (history[history.length - 1] !== activeTab.sql) saveHistory([...history, activeTab.sql]);
|
|
1288
1491
|
} else updateActiveTab({ error: t("studio_sql_execution_not_supported") });
|
|
@@ -1304,14 +1507,7 @@ var SQLEditor = () => {
|
|
|
1304
1507
|
const handleRun = useCallback(async (selectedText) => {
|
|
1305
1508
|
const sqlTarget = selectedText || activeTab.sql;
|
|
1306
1509
|
if (!sqlTarget.trim()) return;
|
|
1307
|
-
|
|
1308
|
-
"DELETE",
|
|
1309
|
-
"DROP",
|
|
1310
|
-
"TRUNCATE",
|
|
1311
|
-
"UPDATE"
|
|
1312
|
-
].some((kw) => sqlTarget.toUpperCase().includes(kw));
|
|
1313
|
-
const hasWhere = sqlTarget.toUpperCase().includes("WHERE");
|
|
1314
|
-
if (hasDestructive && (!hasWhere || sqlTarget.toUpperCase().includes("DROP") || sqlTarget.toUpperCase().includes("TRUNCATE"))) {
|
|
1510
|
+
if (needsDestructiveConfirmation(sqlTarget)) {
|
|
1315
1511
|
setPendingAction(() => () => executeRun(selectedText));
|
|
1316
1512
|
setIsConfirmDialogOpen(true);
|
|
1317
1513
|
return;
|
|
@@ -1325,7 +1521,7 @@ var SQLEditor = () => {
|
|
|
1325
1521
|
const isInput = activeElement?.tagName === "INPUT" || activeElement?.tagName === "TEXTAREA";
|
|
1326
1522
|
if (!activeElement?.className?.includes("monaco-mouse-cursor-text") && !isInput) {
|
|
1327
1523
|
e.preventDefault();
|
|
1328
|
-
handleRun();
|
|
1524
|
+
handleRun(editorHandle.current?.getSelectedText());
|
|
1329
1525
|
}
|
|
1330
1526
|
}
|
|
1331
1527
|
};
|
|
@@ -1876,7 +2072,7 @@ var SQLEditor = () => {
|
|
|
1876
2072
|
})
|
|
1877
2073
|
}),
|
|
1878
2074
|
/* @__PURE__ */ jsxs(Button, {
|
|
1879
|
-
onClick: () => handleRun(),
|
|
2075
|
+
onClick: () => handleRun(editorHandle.current?.getSelectedText()),
|
|
1880
2076
|
disabled: loading,
|
|
1881
2077
|
size: "small",
|
|
1882
2078
|
color: "primary",
|
|
@@ -1898,6 +2094,7 @@ var SQLEditor = () => {
|
|
|
1898
2094
|
firstPanel: /* @__PURE__ */ jsx("div", {
|
|
1899
2095
|
className: "h-full w-full relative flex flex-col min-h-0",
|
|
1900
2096
|
children: /* @__PURE__ */ jsx(MonacoEditor, {
|
|
2097
|
+
ref: editorHandle,
|
|
1901
2098
|
value: sql,
|
|
1902
2099
|
onChange: (v) => setSql(v || ""),
|
|
1903
2100
|
onRun: handleRun,
|
|
@@ -1974,4 +2171,4 @@ var SQLEditor = () => {
|
|
|
1974
2171
|
//#endregion
|
|
1975
2172
|
export { SQLEditor };
|
|
1976
2173
|
|
|
1977
|
-
//# sourceMappingURL=SQLEditor-
|
|
2174
|
+
//# sourceMappingURL=SQLEditor-TBw_K6CW.js.map
|