feat(data-grid): optional database/schema prefix in copied INSERT SQL

This commit is contained in:
二丫讲梵 authored and GitHub committed 2026-09-18 14:11:41 +08:00
1 parent a782039e1f
commit 3dea25eaa7
19 files changed
+220 -1

No files matched your search

@@ -78,6 +78,7 @@ async fn postgres_tsvector_generated_columns_are_readable_and_omitted_from_inser
rows: result.rows.clone(),
exclude_primary_keys: false,
include_computed_columns: false,
include_database_name: true,
insert_mode: DataGridCopyInsertMode::Merged,
})
.expect("copy insert");
@@ -94,6 +94,7 @@ pub struct DataGridSqlExtractorOptions {
pub skip_generated_columns: bool,
pub insert_mode: DataGridCopyInsertMode,
pub exclude_primary_keys_from_insert: bool,
pub include_database_name: bool,
}
impl Default for DataGridSqlExtractorOptions {
@@ -103,6 +104,7 @@ impl Default for DataGridSqlExtractorOptions {
skip_generated_columns: DEFAULT_SQL_SKIP_GENERATED_COLUMNS,
insert_mode: DataGridCopyInsertMode::Merged,
exclude_primary_keys_from_insert: false,
include_database_name: true,
}
}
}
@@ -72,6 +72,7 @@ pub(super) fn write_sql_inserts(
rows: data.rows,
exclude_primary_keys: context.request.options.sql.exclude_primary_keys_from_insert,
include_computed_columns: !context.request.options.sql.skip_computed_columns,
include_database_name: context.request.options.sql.include_database_name,
insert_mode: context.request.options.sql.insert_mode,
})
.ok_or_else(|| {
@@ -42,10 +42,133 @@ fn partially_deserialized_options_use_the_canonical_defaults() {
assert!(!options.sql.skip_generated_columns);
assert_eq!(options.sql.insert_mode, crate::data_grid_sql::DataGridCopyInsertMode::Merged);
assert!(!options.sql.exclude_primary_keys_from_insert);
assert!(options.sql.include_database_name);
assert!(options.json.pretty);
assert!(!options.json.camel_case_field_names);
}
#[test]
fn insert_database_name_option_handles_engine_namespaces() {
use crate::data_grid_sql::DataGridCopyInsertMode;
use crate::sql_dialect::uses_single_row_insert_statements;
for (database_type, catalog, schema, qualified, unqualified) in [
(DatabaseType::Mysql, None, "a`b", "`a``b`.`users`", "`users`"),
(DatabaseType::Goldendb, None, "app", "`app`.`users`", "`users`"),
(DatabaseType::Doris, Some("internal"), "app", "`app`.`users`", "`users`"),
(DatabaseType::StarRocks, None, "app", "`app`.`users`", "`users`"),
(DatabaseType::Sqlite, None, "main", "\"main\".\"users\"", "\"users\""),
(DatabaseType::Sqlite, None, "attached", "\"attached\".\"users\"", "\"users\""),
(DatabaseType::DuckDb, None, "main", "\"main\".\"users\"", "\"users\""),
(DatabaseType::Oracle, None, "APP", "\"APP\".\"users\"", "\"users\""),
(DatabaseType::Dameng, None, "APP", "\"APP\".\"users\"", "\"users\""),
(DatabaseType::Kingbase, None, "audit", "\"audit\".\"users\"", "\"users\""),
(DatabaseType::Gaussdb, None, "audit", "\"audit\".\"users\"", "\"users\""),
(DatabaseType::OpenGauss, None, "audit", "\"audit\".\"users\"", "\"users\""),
(DatabaseType::Db2, None, "APP", "\"APP\".\"users\"", "\"users\""),
(DatabaseType::StarRocks, Some("iceberg"), "app", "`iceberg`.`app`.`users`", "`users`"),
(DatabaseType::Postgres, None, "audit", "\"audit\".\"users\"", "\"users\""),
(DatabaseType::SqlServer, Some("app"), "audit", "[app].[audit].[users]", "[users]"),
(DatabaseType::Doris, Some("iceberg"), "app", "`iceberg`.`app`.`users`", "`users`"),
] {
for include_database_name in [true, false] {
for insert_mode in [DataGridCopyInsertMode::Merged, DataGridCopyInsertMode::RowByRow] {
let mut request = request(DataGridExtractorId::SqlInserts);
request.database_type = Some(database_type);
request.table_meta = Some(DataGridTableMeta {
catalog: catalog.map(str::to_owned),
database: Some("app".to_owned()),
schema: Some(schema.to_owned()),
table_name: "users".to_owned(),
primary_keys: vec![],
columns: None,
});
request.options.sql.include_database_name = include_database_name;
request.options.sql.insert_mode = insert_mode;
// Exercise the same camelCase contract sent by the frontend.
let request = serde_json::from_value(serde_json::to_value(request).unwrap()).unwrap();
let result = extract_data_grid_selection(request).unwrap();
let table = if include_database_name { qualified } else { unqualified };
let statements: Vec<_> = result.text.split(";\n").collect();
let expected_statement_count = if insert_mode == DataGridCopyInsertMode::RowByRow
|| uses_single_row_insert_statements(database_type)
{
2
} else {
1
};
assert_eq!(statements.len(), expected_statement_count);
for statement in statements {
assert!(statement.starts_with(&format!("INSERT INTO {table} (")), "{statement}");
}
}
}
}
}
#[test]
fn insert_database_name_option_uses_database_when_schema_is_absent() {
use crate::data_grid_sql::DataGridCopyInsertMode;
for database_type in [DatabaseType::Mysql, DatabaseType::Goldendb, DatabaseType::Doris, DatabaseType::StarRocks] {
for (database, schema, qualified) in [
(Some("a`b"), None, "`a``b`.`users`"),
(Some("app"), Some(""), "`app`.`users`"),
(Some("app"), Some(" "), "`app`.`users`"),
(Some("app"), Some("other"), "`other`.`users`"),
(None, Some("other"), "`other`.`users`"),
(None, None, "`users`"),
(Some(" "), None, "`users`"),
] {
for include_database_name in [true, false] {
for insert_mode in [DataGridCopyInsertMode::Merged, DataGridCopyInsertMode::RowByRow] {
let mut request = request(DataGridExtractorId::SqlInserts);
request.database_type = Some(database_type);
request.table_meta = Some(DataGridTableMeta {
catalog: None,
database: database.map(str::to_owned),
schema: schema.map(str::to_owned),
table_name: "users".to_owned(),
primary_keys: vec![],
columns: None,
});
request.options.sql.include_database_name = include_database_name;
request.options.sql.insert_mode = insert_mode;
let request = serde_json::from_value(serde_json::to_value(request).unwrap()).unwrap();
let result = extract_data_grid_selection(request).unwrap();
let table = if include_database_name { qualified } else { "`users`" };
for statement in result.text.split(";\n") {
assert!(statement.starts_with(&format!("INSERT INTO {table} (")), "{statement}");
}
}
}
}
}
}
#[test]
fn postgres_insert_can_omit_schema_namespace() {
for schema in [Some("dbx_dst_ccfebf6b"), None] {
for include_database_name in [true, false] {
let mut request = request(DataGridExtractorId::SqlInserts);
request.database_type = Some(DatabaseType::Postgres);
request.table_meta = Some(DataGridTableMeta {
catalog: None,
database: Some("app".to_owned()),
schema: schema.map(str::to_owned),
table_name: "users".to_owned(),
primary_keys: vec![],
columns: None,
});
request.options.sql.include_database_name = include_database_name;
let result = extract_data_grid_selection(request).unwrap();
let table =
if include_database_name && schema.is_some() { r#""dbx_dst_ccfebf6b"."users""# } else { r#""users""# };
assert!(result.text.starts_with(&format!("INSERT INTO {table} (")), "{}", result.text);
}
}
}
#[test]
fn extracts_csv_with_minimal_standard_quoting() {
let result = extract_data_grid_selection(request(DataGridExtractorId::Csv)).expect("CSV extraction");
+69 -1
View File
@@ -134,10 +134,16 @@ pub struct DataGridCopyInsertStatementOptions {
pub exclude_primary_keys: bool,
#[serde(default)]
pub include_computed_columns: bool,
#[serde(default = "default_include_database_name")]
pub include_database_name: bool,
#[serde(default)]
pub insert_mode: DataGridCopyInsertMode,
}
fn default_include_database_name() -> bool {
true
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
#[serde(rename_all = "kebab-case")]
@@ -519,10 +525,53 @@ pub fn build_data_grid_copy_insert_statement(options: DataGridCopyInsertStatemen
let table = options.table_meta.as_ref().map_or_else(
|| "table_name".to_string(),
|meta| {
// This copy option controls every namespace prefix, including
// SQLite's main schema and external catalogs.
if !options.include_database_name {
return data_grid_identifier(
options.database_type,
&meta.table_name,
options.identifier_quote.as_deref(),
);
}
// MySQL-compatible engines can store the database in either field.
let use_database_fallback = match options.database_type {
Some(DatabaseType::Mysql | DatabaseType::Goldendb | DatabaseType::ClickHouse) => true,
Some(DatabaseType::Doris | DatabaseType::StarRocks) => meta
.catalog
.as_deref()
.is_none_or(|catalog| catalog.trim().is_empty() || catalog.trim() == "internal"),
_ => false,
};
// MySQL table tabs store the namespace in `database` without a
// schema. Explicit schemas still take priority for cross-database
// sources; schema-based engines must not use this fallback.
let schema = if use_database_fallback {
meta.schema
.as_deref()
.filter(|schema| !schema.trim().is_empty())
.or_else(|| meta.database.as_deref().filter(|database| !database.trim().is_empty()))
} else {
meta.schema.as_deref()
};
// ClickHouse uses a database qualifier rather than SQL schemas.
if options.database_type == Some(DatabaseType::ClickHouse) {
if let Some(database) = schema {
return format!(
"{}.{}",
data_grid_identifier(options.database_type, database, options.identifier_quote.as_deref()),
data_grid_identifier(
options.database_type,
&meta.table_name,
options.identifier_quote.as_deref()
),
);
}
}
data_grid_qualified_table_name(
options.database_type,
meta.catalog.as_deref(),
meta.schema.as_deref(),
schema,
meta.database.as_deref(),
&meta.table_name,
options.identifier_quote.as_deref(),
@@ -4016,6 +4065,7 @@ mod tests {
rows: vec![vec![json!(1), json!("ada"), json!("Ada")], vec![json!(2), json!("linus"), json!("Linus")]],
exclude_primary_keys: true,
include_computed_columns: false,
include_database_name: true,
insert_mode: DataGridCopyInsertMode::Merged,
});
assert_eq!(
@@ -4067,6 +4117,7 @@ mod tests {
rows: vec![vec![json!(1), json!("Ada")]],
exclude_primary_keys: true,
include_computed_columns: false,
include_database_name: true,
insert_mode: DataGridCopyInsertMode::Merged,
});
@@ -4113,6 +4164,7 @@ mod tests {
rows: vec![vec![json!("A-1"), json!("A-1"), json!("Ada")]],
exclude_primary_keys: false,
include_computed_columns: false,
include_database_name: true,
insert_mode: DataGridCopyInsertMode::Merged,
});
assert_eq!(
@@ -4169,6 +4221,7 @@ mod tests {
rows: vec![vec![json!(1), json!("Ada"), json!("ADA")]],
exclude_primary_keys: true,
include_computed_columns: false,
include_database_name: true,
insert_mode: DataGridCopyInsertMode::Merged,
});
assert_eq!(statement.as_deref(), Some("INSERT INTO `users` (`label`) VALUES ('Ada');"));
@@ -4238,6 +4291,7 @@ mod tests {
rows: vec![vec![json!(1), json!("2026-08-18"), json!("sweater")]],
exclude_primary_keys: true,
include_computed_columns: false,
include_database_name: true,
insert_mode: DataGridCopyInsertMode::Merged,
});
assert_eq!(
@@ -4282,6 +4336,7 @@ mod tests {
rows: vec![vec![json!("AD"), json!("Andorra")]],
exclude_primary_keys: true,
include_computed_columns: false,
include_database_name: true,
insert_mode: DataGridCopyInsertMode::Merged,
});
assert_eq!(
@@ -4311,6 +4366,7 @@ mod tests {
rows: vec![vec![json!(1), json!("ada")]],
exclude_primary_keys: true,
include_computed_columns: false,
include_database_name: true,
insert_mode: DataGridCopyInsertMode::Merged,
});
assert_eq!(statement.as_deref(), Some("INSERT INTO `users` (`id`, `login_name`) VALUES (1, 'ada');"));
@@ -4328,6 +4384,7 @@ mod tests {
rows: vec![vec![json!(7), json!(true), json!(r#"{"name":"Ada","roles":["admin"]}"#)]],
exclude_primary_keys: false,
include_computed_columns: false,
include_database_name: true,
insert_mode: DataGridCopyInsertMode::Merged,
});
@@ -4349,6 +4406,7 @@ mod tests {
rows: vec![vec![json!([1, 2, 3])]],
exclude_primary_keys: false,
include_computed_columns: false,
include_database_name: true,
insert_mode: DataGridCopyInsertMode::Merged,
});
@@ -4367,6 +4425,7 @@ mod tests {
rows: vec![vec![json!([])]],
exclude_primary_keys: false,
include_computed_columns: false,
include_database_name: true,
insert_mode: DataGridCopyInsertMode::Merged,
});
@@ -4385,6 +4444,7 @@ mod tests {
rows: vec![vec![json!({"a": 1})]],
exclude_primary_keys: false,
include_computed_columns: false,
include_database_name: true,
insert_mode: DataGridCopyInsertMode::Merged,
});
@@ -4498,6 +4558,7 @@ mod tests {
rows: vec![vec![json!("ada"), json!("Ada")]],
exclude_primary_keys: true,
include_computed_columns: false,
include_database_name: true,
insert_mode: DataGridCopyInsertMode::Merged,
});
@@ -4526,6 +4587,7 @@ mod tests {
rows: vec![vec![json!(1), json!("ada"), json!("Ada")], vec![json!(2), json!("linus"), json!("Linus")]],
exclude_primary_keys: false,
include_computed_columns: false,
include_database_name: true,
insert_mode: DataGridCopyInsertMode::RowByRow,
});
assert_eq!(
@@ -4555,6 +4617,7 @@ mod tests {
rows: vec![vec![json!(1), json!("Ada")], vec![json!(2), json!("Linus")]],
exclude_primary_keys: false,
include_computed_columns: false,
include_database_name: true,
insert_mode: DataGridCopyInsertMode::Merged,
});
@@ -4587,6 +4650,7 @@ mod tests {
rows: rows.clone(),
exclude_primary_keys: false,
include_computed_columns: false,
include_database_name: true,
insert_mode: DataGridCopyInsertMode::Merged,
});
assert_eq!(
@@ -4654,6 +4718,7 @@ mod tests {
rows: vec![vec![json!(1), json!("Hello"), json!("'hello':1A")]],
exclude_primary_keys: false,
include_computed_columns: false,
include_database_name: true,
insert_mode: DataGridCopyInsertMode::Merged,
});
@@ -4679,6 +4744,7 @@ mod tests {
rows: vec![vec![json!(1), json!("2022-08-25T09:58:43Z"), json!("2022-08-25T09:58:43Z")]],
exclude_primary_keys: false,
include_computed_columns: false,
include_database_name: true,
insert_mode: DataGridCopyInsertMode::Merged,
});
@@ -6697,6 +6763,7 @@ mod tests {
rows: vec![vec![json!(1), json!("login")]],
exclude_primary_keys: false,
include_computed_columns: false,
include_database_name: true,
insert_mode: DataGridCopyInsertMode::Merged,
});
@@ -8571,6 +8638,7 @@ mod tests {
rows: vec![vec![json!(2), json!("new")]],
exclude_primary_keys: false,
include_computed_columns: false,
include_database_name: true,
insert_mode: DataGridCopyInsertMode::Merged,
});
assert_eq!(