fix(postgresql): show trigger source details
This commit is contained in:
parent
6115771b34
commit
6b2f55e4de
|
|
@ -22,6 +22,7 @@ const props = withDefaults(
|
|||
database: string;
|
||||
schema?: string;
|
||||
name: string;
|
||||
relationName?: string;
|
||||
signature?: string;
|
||||
objectType: ObjectSourceKind;
|
||||
databaseType?: DatabaseType;
|
||||
|
|
@ -59,7 +60,7 @@ const canEdit = computed(() => sourceEditable.value && props.objectType !== "SEQ
|
|||
const title = computed(() => `${editing.value ? t("contextMenu.editView") : t("contextMenu.viewSource")} - ${props.name}`);
|
||||
|
||||
watch(
|
||||
() => [props.open, props.connectionId, props.database, props.schema, props.name, props.signature, props.objectType, props.initialEditing] as const,
|
||||
() => [props.open, props.connectionId, props.database, props.schema, props.name, props.relationName, props.signature, props.objectType, props.initialEditing] as const,
|
||||
() => {
|
||||
if (props.open) void loadSource();
|
||||
},
|
||||
|
|
@ -79,7 +80,7 @@ async function loadSource(nextEditing = props.initialEditing && canEdit.value) {
|
|||
try {
|
||||
if (!props.databaseType) throw new Error("Connection type is unavailable.");
|
||||
const schema = props.schema || props.database;
|
||||
const result = await api.getObjectSource(props.connectionId, props.database, schema, props.name, props.objectType, props.signature);
|
||||
const result = await api.getObjectSource(props.connectionId, props.database, schema, props.name, props.objectType, props.signature, props.relationName);
|
||||
const editableAllowed = result.editable !== false;
|
||||
const editable = await buildEditableObjectSource({
|
||||
databaseType: props.databaseType,
|
||||
|
|
|
|||
|
|
@ -1552,6 +1552,7 @@ defineExpose({ focusSearch, createNewGroup, collapseAllTreeNodes });
|
|||
:database="sidebarObjectSourceTarget.node.database!"
|
||||
:schema="sidebarObjectSourceTarget.node.schema"
|
||||
:name="sidebarObjectSourceTarget.node.objectName || sidebarObjectSourceTarget.node.label"
|
||||
:relation-name="sidebarObjectSourceTarget.node.tableName"
|
||||
:signature="sidebarObjectSourceTarget.node.signature"
|
||||
:object-type="sidebarObjectSourceType"
|
||||
:database-type="sidebarObjectSourceDatabaseType"
|
||||
|
|
|
|||
|
|
@ -681,10 +681,10 @@ function runRowClickAction(clickDetail: number) {
|
|||
if (!shouldRunTreeNodeRowAction(action, clickDetail)) return;
|
||||
if (action === "open-data") {
|
||||
scheduleOpenData(node);
|
||||
} else if (action === "open-source") {
|
||||
openObjectSourceDialog(false);
|
||||
} else if (isDocumentBrowserTreeNode(node.type)) {
|
||||
openMongoTreeData(node);
|
||||
} else if (node.type === "procedure" || node.type === "function" || node.type === "trigger" || node.type === "sequence" || node.type === "package" || node.type === "package-body" || node.type === "type" || node.type === "type-body") {
|
||||
openObjectSourceDialog(false);
|
||||
} else if (action === "toggle") {
|
||||
toggle();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import { createPinia, setActivePinia } from "pinia";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { loadSidebarObjectGroup } from "@/lib/sidebar/sidebarObjectGroupRouting";
|
||||
import type { ConnectionConfig, ObjectInfo, TreeNode } from "@/types/database";
|
||||
import type { ConnectionConfig, ObjectInfo, TreeNode, TriggerInfo } from "@/types/database";
|
||||
|
||||
function installLocalStorage() {
|
||||
const data = new Map<string, string>();
|
||||
|
|
@ -92,7 +92,13 @@ describe("sidebar object-group routing", () => {
|
|||
|
||||
it("keeps table-level trigger groups on listTriggers", async () => {
|
||||
const listObjects = vi.fn<() => Promise<ObjectInfo[]>>().mockResolvedValue([]);
|
||||
const listTriggers = vi.fn<() => Promise<never[]>>().mockResolvedValue([]);
|
||||
const listTriggers = vi.fn<() => Promise<TriggerInfo[]>>().mockResolvedValue([
|
||||
{
|
||||
name: "trg_orders_audit",
|
||||
timing: "AFTER",
|
||||
event: "UPDATE",
|
||||
},
|
||||
]);
|
||||
const { connection, store } = await createStore({ listObjects, listTriggers });
|
||||
const tableTriggerGroup: TreeNode = {
|
||||
...objectGroup("group-triggers", `${connection.id}:app:app:orders:__triggers`),
|
||||
|
|
@ -105,7 +111,18 @@ describe("sidebar object-group routing", () => {
|
|||
|
||||
expect(listTriggers).toHaveBeenCalledWith(connection.id, "app", "app", "orders", undefined);
|
||||
expect(listObjects).not.toHaveBeenCalled();
|
||||
expect(storedTriggerGroup).toMatchObject({ isExpanded: true, isLoading: false, children: [] });
|
||||
expect(storedTriggerGroup).toMatchObject({
|
||||
isExpanded: true,
|
||||
isLoading: false,
|
||||
children: [
|
||||
{
|
||||
label: "trg_orders_audit (AFTER UPDATE)",
|
||||
objectName: "trg_orders_audit",
|
||||
tableName: "orders",
|
||||
type: "trigger",
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
it("propagates rejected schema-level metadata while clearing the loading state", async () => {
|
||||
|
|
|
|||
|
|
@ -656,8 +656,8 @@ export async function completionAssistantSearch(request: CompletionAssistantRequ
|
|||
return post("/api/schema/completion-assistant", request);
|
||||
}
|
||||
|
||||
export async function getObjectSource(connectionId: string, database: string, schema: string, name: string, objectType: ObjectSourceKind, signature?: string): Promise<ObjectSource> {
|
||||
return get(`/api/schema/object-source?${qs({ connection_id: connectionId, database, schema, table: name, object_type: objectType, signature })}`);
|
||||
export async function getObjectSource(connectionId: string, database: string, schema: string, name: string, objectType: ObjectSourceKind, signature?: string, relationName?: string): Promise<ObjectSource> {
|
||||
return get(`/api/schema/object-source?${qs({ connection_id: connectionId, database, schema, table: name, object_type: objectType, signature, relation_name: relationName })}`);
|
||||
}
|
||||
|
||||
export async function getColumns(connectionId: string, database: string, schema: string, table: string, catalog?: string, clientSessionId?: string): Promise<ColumnInfo[]> {
|
||||
|
|
|
|||
|
|
@ -832,8 +832,8 @@ export async function completionAssistantSearch(request: CompletionAssistantRequ
|
|||
return invoke("completion_assistant_search", { request });
|
||||
}
|
||||
|
||||
export async function getObjectSource(connectionId: string, database: string, schema: string, name: string, objectType: ObjectSourceKind, signature?: string): Promise<ObjectSource> {
|
||||
return invoke("get_object_source", { connectionId, database, schema, name, objectType, signature });
|
||||
export async function getObjectSource(connectionId: string, database: string, schema: string, name: string, objectType: ObjectSourceKind, signature?: string, relationName?: string): Promise<ObjectSource> {
|
||||
return invoke("get_object_source", { connectionId, database, schema, name, objectType, signature, relationName });
|
||||
}
|
||||
|
||||
export async function listSchemas(connectionId: string, database: string, applyVisibleFilter = false): Promise<string[]> {
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import type { ObjectSourceKind, TreeNode, TreeNodeType } from "@/types/database";
|
||||
import { matchesShortcut, type ShortcutLikeEvent } from "@/lib/editor/keyboardShortcuts";
|
||||
|
||||
export type TreeNodeRowAction = "open-data" | "toggle" | "none";
|
||||
export type TreeNodeRowAction = "open-data" | "open-source" | "toggle" | "none";
|
||||
export type TreeNodeRowDoubleClickAction = "open-data" | "activate-data" | "open-object-browser" | "open-object-browser-and-expand" | "open-source" | "open-saved-sql" | "toggle" | "none";
|
||||
export type SidebarSelectionCopyAction = "copy-name" | "none";
|
||||
export type SidebarActivation = "single" | "double";
|
||||
|
|
@ -36,6 +36,7 @@ export function isDocumentBrowserTreeNode(type: TreeNodeType): boolean {
|
|||
export function treeNodeRowAction(type: TreeNodeType, canExpand: boolean, activation: SidebarActivation = "single"): TreeNodeRowAction {
|
||||
if (activation === "double") return "none";
|
||||
if (dataNodeTypes.has(type)) return "open-data";
|
||||
if (sourceNodeTypes.has(type)) return "open-source";
|
||||
if (toggleLeafNodeTypes.has(type)) return "toggle";
|
||||
if (canExpand) return "toggle";
|
||||
return "none";
|
||||
|
|
|
|||
|
|
@ -4013,6 +4013,7 @@ export const useConnectionStore = defineStore("connection", () => {
|
|||
triggers.map((tr) => ({
|
||||
id: `${parentId}:${tr.name}`,
|
||||
label: `${tr.name} (${tr.timing} ${tr.event})`,
|
||||
objectName: tr.name,
|
||||
type: "trigger" as const,
|
||||
connectionId,
|
||||
database,
|
||||
|
|
|
|||
|
|
@ -1851,6 +1851,7 @@ pub async fn export_database_sql_core(
|
|||
view_name,
|
||||
crate::db::ObjectSourceKind::View,
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.await
|
||||
{
|
||||
|
|
@ -1901,6 +1902,7 @@ pub async fn export_database_sql_core(
|
|||
proc_name,
|
||||
crate::db::ObjectSourceKind::Procedure,
|
||||
procedure.signature.as_deref(),
|
||||
None,
|
||||
)
|
||||
.await
|
||||
{
|
||||
|
|
@ -1955,6 +1957,7 @@ pub async fn export_database_sql_core(
|
|||
func_name,
|
||||
crate::db::ObjectSourceKind::Function,
|
||||
function.signature.as_deref(),
|
||||
None,
|
||||
)
|
||||
.await
|
||||
{
|
||||
|
|
|
|||
|
|
@ -5373,9 +5373,17 @@ pub async fn get_table_ddl_core(
|
|||
return Err("DDL is not supported for SQL Server linked server tables".to_string());
|
||||
}
|
||||
if matches!(object_type, Some(db::ObjectSourceKind::View)) {
|
||||
let source =
|
||||
get_object_source_core(state, connection_id, database, schema, table, db::ObjectSourceKind::View, None)
|
||||
.await?;
|
||||
let source = get_object_source_core(
|
||||
state,
|
||||
connection_id,
|
||||
database,
|
||||
schema,
|
||||
table,
|
||||
db::ObjectSourceKind::View,
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.await?;
|
||||
let database_type = connection_config(state, connection_id).await.map(|config| config.db_type);
|
||||
return Ok(crate::object_source_sql::build_view_ddl_sql(crate::object_source_sql::BuildViewDdlInput {
|
||||
database_type,
|
||||
|
|
@ -5393,6 +5401,7 @@ pub async fn get_table_ddl_core(
|
|||
table,
|
||||
db::ObjectSourceKind::MaterializedView,
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.await?;
|
||||
return Ok(source.source);
|
||||
|
|
@ -5675,6 +5684,24 @@ pub fn postgres_object_source_sql(
|
|||
postgres_object_source_sql_inner(schema, name, kind, signature, true, false)
|
||||
}
|
||||
|
||||
fn postgres_trigger_object_source_sql(schema: &str, name: &str, relation_name: Option<&str>) -> String {
|
||||
let relation_filter = relation_name
|
||||
.filter(|value| !value.trim().is_empty())
|
||||
.map(|value| format!(" AND c.relname = {}", sql_string(value)))
|
||||
.unwrap_or_default();
|
||||
format!(
|
||||
"SELECT pg_catalog.pg_get_triggerdef(t.oid, true) \
|
||||
FROM pg_catalog.pg_trigger t \
|
||||
JOIN pg_catalog.pg_class c ON c.oid = t.tgrelid \
|
||||
JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace \
|
||||
WHERE n.nspname = {} AND t.tgname = {} AND NOT t.tgisinternal{} \
|
||||
ORDER BY t.oid LIMIT 1",
|
||||
sql_string(schema),
|
||||
sql_string(name),
|
||||
relation_filter
|
||||
)
|
||||
}
|
||||
|
||||
fn opengauss_object_source_sql(
|
||||
schema: &str,
|
||||
name: &str,
|
||||
|
|
@ -5929,9 +5956,19 @@ pub async fn get_object_source_core(
|
|||
name: &str,
|
||||
object_type: db::ObjectSourceKind,
|
||||
signature: Option<&str>,
|
||||
relation_name: Option<&str>,
|
||||
) -> Result<db::ObjectSource, String> {
|
||||
retry_metadata_connection(state, connection_id, Some(database), || {
|
||||
get_object_source_once(state, connection_id, database, schema, name, object_type.clone(), signature)
|
||||
get_object_source_once(
|
||||
state,
|
||||
connection_id,
|
||||
database,
|
||||
schema,
|
||||
name,
|
||||
object_type.clone(),
|
||||
signature,
|
||||
relation_name,
|
||||
)
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
|
@ -5944,6 +5981,7 @@ async fn get_object_source_once(
|
|||
name: &str,
|
||||
object_type: db::ObjectSourceKind,
|
||||
signature: Option<&str>,
|
||||
relation_name: Option<&str>,
|
||||
) -> Result<db::ObjectSource, String> {
|
||||
let pool_key = state.get_or_create_pool(connection_id, Some(database)).await?;
|
||||
let db_config = connection_config(state, connection_id).await;
|
||||
|
|
@ -6015,7 +6053,16 @@ async fn get_object_source_once(
|
|||
}
|
||||
PoolKind::Postgres(pool) => {
|
||||
let unwrap_opengauss_record = db_config.as_ref().is_some_and(is_opengauss_family_config);
|
||||
postgres_object_source(pool, schema, name, &object_type, signature, unwrap_opengauss_record).await?
|
||||
postgres_object_source(
|
||||
pool,
|
||||
schema,
|
||||
name,
|
||||
&object_type,
|
||||
signature,
|
||||
relation_name,
|
||||
unwrap_opengauss_record,
|
||||
)
|
||||
.await?
|
||||
}
|
||||
PoolKind::Sqlite(pool) => first_string_cell(
|
||||
db::sqlite::execute_query(pool, &sqlite_object_source_sql(schema, name, &object_type)).await?,
|
||||
|
|
@ -6062,12 +6109,32 @@ async fn get_object_source_once(
|
|||
}
|
||||
};
|
||||
|
||||
let editable = if matches!(object_type, db::ObjectSourceKind::Trigger)
|
||||
&& db_config.as_ref().is_some_and(|config| {
|
||||
matches!(
|
||||
config.db_type,
|
||||
DatabaseType::Postgres
|
||||
| DatabaseType::Redshift
|
||||
| DatabaseType::Gaussdb
|
||||
| DatabaseType::Kwdb
|
||||
| DatabaseType::OpenGauss
|
||||
| DatabaseType::Questdb
|
||||
| DatabaseType::Kingbase
|
||||
| DatabaseType::Highgo
|
||||
| DatabaseType::Vastbase
|
||||
)
|
||||
}) {
|
||||
Some(false)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
Ok(db::ObjectSource {
|
||||
name: name.to_string(),
|
||||
object_type,
|
||||
schema: if schema.is_empty() { None } else { Some(schema.to_string()) },
|
||||
source,
|
||||
editable: None,
|
||||
editable,
|
||||
})
|
||||
}
|
||||
|
||||
|
|
@ -6370,9 +6437,12 @@ async fn postgres_object_source(
|
|||
name: &str,
|
||||
object_type: &db::ObjectSourceKind,
|
||||
signature: Option<&str>,
|
||||
relation_name: Option<&str>,
|
||||
unwrap_opengauss_record: bool,
|
||||
) -> Result<String, String> {
|
||||
let sql = if unwrap_opengauss_record {
|
||||
let sql = if matches!(object_type, db::ObjectSourceKind::Trigger) {
|
||||
postgres_trigger_object_source_sql(schema, name, relation_name)
|
||||
} else if unwrap_opengauss_record {
|
||||
opengauss_object_source_sql(schema, name, object_type, signature)
|
||||
} else {
|
||||
postgres_object_source_sql(schema, name, object_type, signature)
|
||||
|
|
@ -6475,6 +6545,17 @@ mod object_source_tests {
|
|||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn builds_postgres_object_source_sql_for_table_trigger() {
|
||||
let sql = postgres_trigger_object_source_sql("audit", "trg_orders_update", Some("orders"));
|
||||
|
||||
assert!(sql.contains("pg_get_triggerdef(t.oid, true)"));
|
||||
assert!(sql.contains("n.nspname = 'audit'"));
|
||||
assert!(sql.contains("c.relname = 'orders'"));
|
||||
assert!(sql.contains("t.tgname = 'trg_orders_update'"));
|
||||
assert!(sql.contains("NOT t.tgisinternal"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn builds_postgres_object_source_sql_without_relispopulated_for_legacy_catalogs() {
|
||||
let sql = postgres_object_source_sql_without_relispopulated(
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ pub struct SchemaQuery {
|
|||
pub offset: Option<usize>,
|
||||
pub object_type: Option<dbx_core::db::ObjectSourceKind>,
|
||||
pub signature: Option<String>,
|
||||
pub relation_name: Option<String>,
|
||||
pub object_types: Option<String>,
|
||||
pub apply_visible_filter: Option<bool>,
|
||||
pub client_session_id: Option<String>,
|
||||
|
|
@ -302,6 +303,7 @@ pub async fn get_object_source(
|
|||
name,
|
||||
object_type,
|
||||
q.signature.as_deref(),
|
||||
q.relation_name.as_deref(),
|
||||
)
|
||||
.await
|
||||
.map_err(AppError::from)?;
|
||||
|
|
|
|||
|
|
@ -7,6 +7,12 @@ test("table and view rows open data without toggling structure groups", () => {
|
|||
assert.equal(treeNodeRowAction("view", true), "open-data");
|
||||
});
|
||||
|
||||
test("single click navigation mode opens source-capable rows", () => {
|
||||
assert.equal(treeNodeRowAction("procedure", false), "open-source");
|
||||
assert.equal(treeNodeRowAction("trigger", false), "open-source");
|
||||
assert.equal(treeNodeRowAction("sequence", false), "open-source");
|
||||
});
|
||||
|
||||
test("double click navigation mode selects rows on single click", () => {
|
||||
assert.equal(treeNodeRowAction("table", true, "double"), "none");
|
||||
assert.equal(treeNodeRowAction("view", true, "double"), "none");
|
||||
|
|
@ -65,6 +71,8 @@ test("double-click follow-up clicks do not run row actions", () => {
|
|||
assert.equal(shouldRunTreeNodeRowAction("toggle", 3), false);
|
||||
assert.equal(shouldRunTreeNodeRowAction("open-data", 1), true);
|
||||
assert.equal(shouldRunTreeNodeRowAction("open-data", 2), false);
|
||||
assert.equal(shouldRunTreeNodeRowAction("open-source", 1), true);
|
||||
assert.equal(shouldRunTreeNodeRowAction("open-source", 2), false);
|
||||
assert.equal(shouldRunTreeNodeRowAction("none", 1), false);
|
||||
});
|
||||
|
||||
|
|
@ -77,6 +85,7 @@ test("maps source-capable sidebar nodes to object source kinds", () => {
|
|||
assert.equal(objectSourceKindForTreeNode("view"), "VIEW");
|
||||
assert.equal(objectSourceKindForTreeNode("procedure"), "PROCEDURE");
|
||||
assert.equal(objectSourceKindForTreeNode("function"), "FUNCTION");
|
||||
assert.equal(objectSourceKindForTreeNode("trigger"), "TRIGGER");
|
||||
assert.equal(objectSourceKindForTreeNode("sequence"), "SEQUENCE");
|
||||
assert.equal(objectSourceKindForTreeNode("package"), "PACKAGE");
|
||||
assert.equal(objectSourceKindForTreeNode("package-body"), "PACKAGE_BODY");
|
||||
|
|
|
|||
|
|
@ -280,6 +280,7 @@ pub async fn get_object_source(
|
|||
name: String,
|
||||
object_type: db::ObjectSourceKind,
|
||||
signature: Option<String>,
|
||||
relation_name: Option<String>,
|
||||
) -> Result<db::ObjectSource, String> {
|
||||
dbx_core::schema::get_object_source_core(
|
||||
&state,
|
||||
|
|
@ -289,6 +290,7 @@ pub async fn get_object_source(
|
|||
&name,
|
||||
object_type,
|
||||
signature.as_deref(),
|
||||
relation_name.as_deref(),
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue