fix(postgres): handle PostgreSQL views

This commit is contained in:
ptma 2026-06-25 10:54:38 +08:00 committed by GitHub
parent 2999d9c56f
commit 484d20922a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 19 additions and 16 deletions

View File

@ -269,10 +269,10 @@ function handleOptionsUpdate(options: SchemaDiffCompareOptions) {
* Views and materialized views need the object_type parameter so the
* backend can call DBMS_METADATA.GET_DDL with the correct type. */
function isViewOrMaterializedView(tableType: string): ObjectSourceKind | undefined {
switch (tableType.toUpperCase()) {
switch (tableType.toUpperCase().replace(/\s+/g, "_")) {
case "VIEW":
return "VIEW";
case "MATERIALIZED VIEW":
case "MATERIALIZED_VIEW":
return "MATERIALIZED_VIEW";
default:
return undefined;

View File

@ -163,7 +163,7 @@ async function del<T>(url: string): Promise<T> {
return res.json();
}
function qs(params: Record<string, string | number | undefined>): string {
function qs(params: Record<string, string | number | boolean | undefined>): string {
const sp = new URLSearchParams();
for (const [k, v] of Object.entries(params)) {
if (v !== undefined && v !== null) sp.set(k, String(v));
@ -564,7 +564,7 @@ export async function listFunctions(connectionId: string, database: string, sche
}
export async function listSequences(connectionId: string, database: string, schema: string, withLastValues: boolean): Promise<SequenceInfo[]> {
return get(`/api/schema/sequences?${qs({ connection_id: connectionId, database, schema, with_last_values: withLastValues ? 1 : 0 })}`);
return get(`/api/schema/sequences?${qs({ connection_id: connectionId, database, schema, with_last_values: withLastValues })}`);
}
export async function listRules(connectionId: string, database: string, schema: string): Promise<RuleInfo[]> {

View File

@ -578,8 +578,8 @@ pub async fn export_database_sql_core(
}
// 7. Separate tables and views
let mut tables: Vec<_> = all_tables.iter().filter(|t| t.table_type != "VIEW").collect();
let views: Vec<_> = all_tables.iter().filter(|t| t.table_type == "VIEW").collect();
let mut tables: Vec<_> = all_tables.iter().filter(|t| !t.table_type.contains("VIEW")).collect();
let views: Vec<_> = all_tables.iter().filter(|t| t.table_type.contains("VIEW")).collect();
let postgres_sequences = if request.include_structure && matches!(db_type, DatabaseType::Postgres) {
match list_postgres_export_sequences(
state,

View File

@ -1153,7 +1153,7 @@ pub async fn completion_assistant_search(
let table_type: String = row.get(2);
candidates.push(CompletionAssistantCandidate {
name: row.get(0),
kind: if table_type == "VIEW" {
kind: if table_type.contains("VIEW") {
CompletionAssistantCandidateKind::View
} else {
CompletionAssistantCandidateKind::Table

View File

@ -432,6 +432,8 @@ fn build_sqlserver_alter_view_sql(schema: Option<&str>, name: &str, source: &str
fn parse_object_source_kind(value: &str) -> Option<ObjectSourceKind> {
if value.eq_ignore_ascii_case("VIEW") {
Some(ObjectSourceKind::View)
} else if value.eq_ignore_ascii_case("MATERIALIZED VIEW") || value.eq_ignore_ascii_case("MATERIALIZED_VIEW") {
Some(ObjectSourceKind::MaterializedView)
} else if value.eq_ignore_ascii_case("PROCEDURE") {
Some(ObjectSourceKind::Procedure)
} else if value.eq_ignore_ascii_case("FUNCTION") {

View File

@ -1779,9 +1779,7 @@ async fn completion_assistant_fallback_core(
)
.await?;
for table in tables {
let kind = if table.table_type.eq_ignore_ascii_case("VIEW")
|| table.table_type.eq_ignore_ascii_case("MATERIALIZED_VIEW")
{
let kind = if table.table_type.to_uppercase().contains("VIEW") {
db::CompletionAssistantCandidateKind::View
} else {
db::CompletionAssistantCandidateKind::Table
@ -2819,7 +2817,9 @@ pub fn postgres_object_source_sql(schema: &str, name: &str, kind: &db::ObjectSou
match kind {
db::ObjectSourceKind::View | db::ObjectSourceKind::MaterializedView => {
format!(
"SELECT pg_get_viewdef(c.oid, 0) \
"SELECT CASE WHEN c.relkind = 'm' THEN format('CREATE MATERIALIZED VIEW %I.%I AS ', n.nspname, c.relname) || regexp_replace(pg_get_viewdef(c.oid, 0), ';[[:space:]]*$', '') || CASE WHEN c.relispopulated THEN ' WITH DATA' ELSE ' WITH NO DATA' END \
ELSE format('CREATE OR REPLACE VIEW %I.%I AS ', n.nspname, c.relname) || pg_get_viewdef(c.oid, 0) \
END \
FROM pg_catalog.pg_class c \
JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace \
WHERE n.nspname = {} AND c.relname = {} AND c.relkind IN ('v','m') \
@ -3178,7 +3178,7 @@ mod object_source_tests {
fn builds_postgres_object_source_sql_for_views_and_functions() {
assert_eq!(
postgres_object_source_sql("public", "active_users", &ObjectSourceKind::View),
"SELECT pg_get_viewdef(c.oid, 0) FROM pg_catalog.pg_class c JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace WHERE n.nspname = 'public' AND c.relname = 'active_users' AND c.relkind IN ('v','m') ORDER BY c.oid LIMIT 1"
"SELECT CASE WHEN c.relkind = 'm' THEN format('CREATE MATERIALIZED VIEW %I.%I AS ', n.nspname, c.relname) || regexp_replace(pg_get_viewdef(c.oid, 0), ';[[:space:]]*$', '') || CASE WHEN c.relispopulated THEN ' WITH DATA' ELSE ' WITH NO DATA' END ELSE format('CREATE OR REPLACE VIEW %I.%I AS ', n.nspname, c.relname) || pg_get_viewdef(c.oid, 0) END FROM pg_catalog.pg_class c JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace WHERE n.nspname = 'public' AND c.relname = 'active_users' AND c.relkind IN ('v','m') ORDER BY c.oid LIMIT 1"
);
assert_eq!(
postgres_object_source_sql("public", "recalc_score", &ObjectSourceKind::Function),
@ -3192,6 +3192,7 @@ mod object_source_tests {
assert!(!sql.contains("::regclass"));
assert!(sql.contains("pg_get_viewdef(c.oid, 0)"));
assert!(sql.contains("format('CREATE OR REPLACE VIEW %I.%I AS ', n.nspname, c.relname)"));
assert!(sql.contains("n.nspname = 'tenant''s schema'"));
assert!(sql.contains("c.relname = 'active users'"));
assert!(sql.contains("c.relkind IN ('v','m')"));

View File

@ -263,25 +263,25 @@ fn diff_schema(options: &SchemaDiffPreparationOptions) -> Vec<TableDiff> {
let source_table_names: Vec<String> = options
.source_tables
.iter()
.filter(|table| table.table_type != "VIEW")
.filter(|table| !table.table_type.contains("VIEW"))
.map(|table| table.name.clone())
.collect();
let target_table_names: Vec<String> = options
.target_tables
.iter()
.filter(|table| table.table_type != "VIEW")
.filter(|table| !table.table_type.contains("VIEW"))
.map(|table| table.name.clone())
.collect();
let source_view_names: Vec<String> = options
.source_tables
.iter()
.filter(|table| table.table_type == "VIEW")
.filter(|table| table.table_type.contains("VIEW"))
.map(|table| table.name.clone())
.collect();
let target_view_names: Vec<String> = options
.target_tables
.iter()
.filter(|table| table.table_type == "VIEW")
.filter(|table| table.table_type.contains("VIEW"))
.map(|table| table.name.clone())
.collect();