fix(kingbase): honor connection identifier quotes in exports

This commit is contained in:
dienaso
2026-08-18 12:43:31 +08:00
committed by GitHub
parent cb09a5c75b
commit 9fc7721590
18 changed files with 313 additions and 8 deletions
@@ -426,6 +426,7 @@ export function useDataGridExport(options: UseDataGridExportOptions) {
}
return buildDataGridCopyInsertStatement({
databaseType: databaseType.value,
identifierQuote: options.identifierQuote?.value,
tableMeta: tableMeta.value,
columns: data.columns,
columnTypes: data.columnTypes,
@@ -1250,6 +1251,7 @@ export function useDataGridExport(options: UseDataGridExportOptions) {
const exportData = sqlInsertExportData(result);
const content = await formatSqlInsert({
databaseType: databaseType.value,
identifierQuote: options.identifierQuote?.value,
schema: tableMeta.value?.schema,
tableName: tableMeta.value?.tableName || "table_name",
columns: exportData.columns,
@@ -1271,6 +1273,7 @@ export function useDataGridExport(options: UseDataGridExportOptions) {
const exportData = sqlInsertExportData(result);
const content = await formatSqlInsert({
databaseType: databaseType.value,
identifierQuote: options.identifierQuote?.value,
schema: tableMeta.value?.schema,
tableName: tableMeta.value?.tableName || "table_name",
columns: exportData.columns,
+1
View File
@@ -4310,6 +4310,7 @@ export interface QueryResultExportRequest {
exportColumnTypes?: Array<string | null | undefined>;
numericColumnRightAlign?: boolean;
columnComments?: Array<string | null> | null;
identifierQuote?: string;
}
export async function startTableExport(request: TableExportRequest, onProgress: (progress: TableExportProgress) => void): Promise<TableExportProgress> {
@@ -36,6 +36,7 @@ export interface DataGridSaveStatementOptions {
export interface DataGridCopyUpdateStatementOptions {
databaseType?: DatabaseType;
identifierQuote?: string;
tableMeta: DataGridTableMeta;
columns: string[];
sourceColumns?: Array<string | undefined>;
@@ -46,6 +47,7 @@ export type DataGridCopyInsertMode = "merged" | "row-by-row";
export interface DataGridCopyInsertStatementOptions {
databaseType?: DatabaseType;
identifierQuote?: string;
tableMeta?: DataGridTableMeta;
columns: string[];
columnTypes?: Array<string | null | undefined>;
@@ -29,6 +29,7 @@ export function formatTsv(columns: string[], rows: ExportCellValue[][]): string
export interface FormatSqlInsertOptions {
databaseType?: DatabaseType;
identifierQuote?: string;
schema?: string;
tableName?: string;
qualifiedTableName?: string;
@@ -0,0 +1,79 @@
import { createPinia, setActivePinia } from "pinia";
import { beforeEach, describe, expect, it, vi } from "vitest";
const mocks = vi.hoisted(() => ({
ensureConnected: vi.fn(),
getConfig: vi.fn(),
connectionIdentifierQuote: vi.fn(),
}));
vi.mock("@/lib/backend/api", () => ({
saveOpenTabsState: vi.fn().mockResolvedValue(undefined),
}));
vi.mock("@/stores/connectionStore", () => ({
useConnectionStore: () => ({
ensureConnected: mocks.ensureConnected,
getConfig: mocks.getConfig,
connectionIdentifierQuote: mocks.connectionIdentifierQuote,
}),
}));
vi.mock("@/stores/settingsStore", () => ({
useSettingsStore: () => ({
editorSettings: {
exportBatchSize: 1000,
exportRowLimitEnabled: false,
exportRowLimit: 100_000,
globalQueryTimeoutSecs: 30,
queryExportKeysetOptimizationEnabled: true,
numericColumnRightAlign: true,
},
}),
}));
function installLocalStorage() {
const data = new Map<string, string>();
vi.stubGlobal("localStorage", {
getItem: vi.fn((key: string) => data.get(key) ?? null),
setItem: vi.fn((key: string, value: string) => data.set(key, value)),
removeItem: vi.fn((key: string) => data.delete(key)),
});
}
describe("queryStore query result export", () => {
beforeEach(() => {
vi.clearAllMocks();
installLocalStorage();
setActivePinia(createPinia());
mocks.ensureConnected.mockResolvedValue(undefined);
mocks.getConfig.mockReturnValue({ id: "kingbase-1", name: "Kingbase", db_type: "kingbase", database: "app", query_timeout_secs: 30 });
mocks.connectionIdentifierQuote.mockReturnValue("[");
});
it("passes the live connection identifier quote to backend SQL export", async () => {
const { useQueryStore } = await import("@/stores/queryStore");
const store = useQueryStore();
const tabId = store.createTab("kingbase-1", "app", "Query");
const tab = store.tabs.find((item) => item.id === tabId)!;
tab.sql = "SELECT select FROM audit_log";
tab.lastExecutedSql = tab.sql;
tab.result = {
columns: ["select"],
rows: [["value"]],
affected_rows: 0,
execution_time_ms: 1,
};
const request = await store.buildQueryResultExportRequest(tabId, {
exportId: "export-1",
filePath: "audit.sql",
format: "sql",
exportTableName: "audit_log",
exportColumnTypes: ["text"],
});
expect(request?.identifierQuote).toBe("[");
expect(mocks.connectionIdentifierQuote).toHaveBeenCalledWith("kingbase-1");
});
});
+2
View File
@@ -6084,6 +6084,7 @@ export const useQueryStore = defineStore("query", () => {
exportTableName: options.exportTableName,
exportColumnTypes: options.exportColumnTypes,
numericColumnRightAlign: settings.numericColumnRightAlign,
identifierQuote: connStore.connectionIdentifierQuote(location.connectionId),
};
}
@@ -6119,6 +6120,7 @@ export const useQueryStore = defineStore("query", () => {
executionId: uuid(),
numericColumnRightAlign: settings.numericColumnRightAlign,
columnComments,
identifierQuote: connStore.connectionIdentifierQuote(tab.connectionId),
};
const tracker = useExportTracker();
@@ -48,6 +48,7 @@ pub(super) fn write_sql_inserts(
let data = sql_selected_data(context, false)?;
let statement = build_data_grid_copy_insert_statement(DataGridCopyInsertStatementOptions {
database_type: context.request.database_type,
identifier_quote: context.request.identifier_quote.clone(),
table_meta: context.request.table_meta.clone(),
columns: data.columns,
column_types: Some(data.column_types),
@@ -98,6 +99,7 @@ pub(super) fn write_sql_updates(
}
let statements = build_data_grid_copy_update_statements(DataGridCopyUpdateStatementOptions {
database_type: context.request.database_type,
identifier_quote: context.request.identifier_quote.clone(),
table_meta: table_meta.clone(),
columns: data.columns,
source_columns: Some(data.source_columns),
@@ -301,7 +303,7 @@ pub(super) fn write_where_clause(
value,
context.selected_column_info[column_index],
true,
None,
context.request.identifier_quote.as_deref(),
);
write_bytes(output, predicate.as_bytes())?;
}
@@ -246,6 +246,53 @@ fn builds_updates_with_hidden_primary_keys_and_selected_columns() {
);
}
#[test]
fn sql_copy_honors_kingbase_mysql_compat_connection_identifier_quote() {
let table_meta = DataGridTableMeta {
catalog: None,
database: None,
schema: Some("audit-schema".to_string()),
table_name: "events".to_string(),
primary_keys: vec!["id".to_string()],
columns: Some(vec![
DataGridColumnInfo {
name: "id".to_string(),
data_type: "int".to_string(),
is_nullable: false,
is_primary_key: true,
column_default: None,
extra: None,
},
DataGridColumnInfo {
name: "event_type".to_string(),
data_type: "varchar".to_string(),
is_nullable: false,
is_primary_key: false,
column_default: None,
extra: None,
},
]),
};
let mut insert = request(DataGridExtractorId::SqlInserts);
insert.database_type = Some(DatabaseType::Kingbase);
insert.identifier_quote = Some("`".to_string());
insert.table_meta = Some(table_meta.clone());
insert.columns = vec![column("id", 0), column("event_type", 1)];
insert.rows = vec![vec![json!(1), json!("login")]];
let insert_result = extract_data_grid_selection(insert).expect("Kingbase SQL INSERT extraction");
assert_eq!(insert_result.text, "INSERT INTO `audit-schema`.`events` (`id`, `event_type`) VALUES (1, 'login');");
let mut updates = request(DataGridExtractorId::SqlUpdates);
updates.database_type = Some(DatabaseType::Kingbase);
updates.identifier_quote = Some("`".to_string());
updates.table_meta = Some(table_meta);
updates.columns = vec![column("id", 0), column("event_type", 1)];
updates.rows = vec![vec![json!(1), json!("logout")]];
let updates_result = extract_data_grid_selection(updates).expect("Kingbase SQL UPDATE extraction");
assert_eq!(updates_result.text, "UPDATE `audit-schema`.`events` SET `event_type` = 'logout' WHERE `id` = 1;");
}
#[test]
fn sql_update_computed_column_option_matches_the_frontend_capability() {
let mut request = request(DataGridExtractorId::SqlUpdates);
@@ -595,6 +642,7 @@ fn build_data_grid_copy_update_statements_returns_empty_for_mongodb() {
use crate::data_grid_sql::{build_data_grid_copy_update_statements, DataGridCopyUpdateStatementOptions};
let options = DataGridCopyUpdateStatementOptions {
database_type: Some(DatabaseType::MongoDb),
identifier_quote: None,
table_meta: DataGridTableMeta {
catalog: None,
database: None,
+76 -5
View File
@@ -90,6 +90,8 @@ pub struct DataGridSaveStatementOptions {
pub struct DataGridCopyUpdateStatementOptions {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub database_type: Option<DatabaseType>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub identifier_quote: Option<String>,
pub table_meta: DataGridTableMeta,
pub columns: Vec<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
@@ -104,6 +106,8 @@ pub struct DataGridCopyInsertStatementOptions {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub database_type: Option<DatabaseType>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub identifier_quote: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub table_meta: Option<DataGridTableMeta>,
pub columns: Vec<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
@@ -362,7 +366,7 @@ pub fn build_data_grid_copy_update_statements(options: DataGridCopyUpdateStateme
options.table_meta.schema.as_deref(),
options.table_meta.database.as_deref(),
&options.table_meta.table_name,
None,
options.identifier_quote.as_deref(),
);
let mut statements = Vec::new();
for row in &options.rows {
@@ -374,7 +378,7 @@ pub fn build_data_grid_copy_update_statements(options: DataGridCopyUpdateStateme
.map(|(column, index, info)| {
format!(
"{} = {}",
data_grid_identifier(options.database_type, column, None),
data_grid_identifier(options.database_type, column, options.identifier_quote.as_deref()),
format_grid_sql_literal(row.get(*index).unwrap_or(&Value::Null), options.database_type, *info)
)
})
@@ -393,7 +397,7 @@ pub fn build_data_grid_copy_update_statements(options: DataGridCopyUpdateStateme
row.get(primary_key_indexes[index]).unwrap_or(&Value::Null),
primary_key_info[index],
false,
None,
options.identifier_quote.as_deref(),
)
})
.collect::<Vec<_>>()
@@ -447,18 +451,19 @@ 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| {
crate::sql_dialect::qualified_table_name_with_catalog(
data_grid_qualified_table_name(
options.database_type,
meta.catalog.as_deref(),
meta.schema.as_deref(),
meta.database.as_deref(),
&meta.table_name,
options.identifier_quote.as_deref(),
)
},
);
let columns = insert_columns
.iter()
.map(|(column, _, _)| quote_ident(options.database_type, column))
.map(|(column, _, _)| data_grid_identifier(options.database_type, column, options.identifier_quote.as_deref()))
.collect::<Vec<_>>()
.join(", ");
let value_rows = options
@@ -3233,6 +3238,7 @@ mod tests {
fn builds_copy_update_statements() {
let statements = build_data_grid_copy_update_statements(DataGridCopyUpdateStatementOptions {
database_type: Some(DatabaseType::Postgres),
identifier_quote: None,
table_meta: DataGridTableMeta {
catalog: None,
database: None,
@@ -3255,6 +3261,7 @@ mod tests {
fn builds_copy_insert_statement_without_primary_keys() {
let statement = build_data_grid_copy_insert_statement(DataGridCopyInsertStatementOptions {
database_type: Some(DatabaseType::Mysql),
identifier_quote: None,
table_meta: Some(DataGridTableMeta {
catalog: None,
database: None,
@@ -3281,6 +3288,7 @@ mod tests {
fn copy_insert_keeps_json_cells_as_single_json_literals() {
let statement = build_data_grid_copy_insert_statement(DataGridCopyInsertStatementOptions {
database_type: Some(DatabaseType::Elasticsearch),
identifier_quote: None,
table_meta: None,
columns: vec!["id".to_string(), "active".to_string(), "profile".to_string()],
column_types: Some(vec![Some("number".to_string()), Some("boolean".to_string()), Some("json".to_string())]),
@@ -3301,6 +3309,7 @@ mod tests {
fn copy_insert_keeps_mysql_json_array_as_single_json_literal() {
let statement = build_data_grid_copy_insert_statement(DataGridCopyInsertStatementOptions {
database_type: Some(DatabaseType::Mysql),
identifier_quote: None,
table_meta: None,
columns: vec!["data".to_string()],
column_types: Some(vec![Some("json".to_string())]),
@@ -3318,6 +3327,7 @@ mod tests {
fn copy_insert_keeps_mysql_json_empty_array_as_single_json_literal() {
let statement = build_data_grid_copy_insert_statement(DataGridCopyInsertStatementOptions {
database_type: Some(DatabaseType::Mysql),
identifier_quote: None,
table_meta: None,
columns: vec!["data".to_string()],
column_types: Some(vec![Some("json".to_string())]),
@@ -3335,6 +3345,7 @@ mod tests {
fn copy_insert_keeps_mysql_json_object_as_single_json_literal() {
let statement = build_data_grid_copy_insert_statement(DataGridCopyInsertStatementOptions {
database_type: Some(DatabaseType::Mysql),
identifier_quote: None,
table_meta: None,
columns: vec!["data".to_string()],
column_types: Some(vec![Some("json".to_string())]),
@@ -3352,6 +3363,7 @@ mod tests {
fn builds_copy_insert_without_primary_keys_when_primary_keys_are_hidden() {
let statement = build_data_grid_copy_insert_statement(DataGridCopyInsertStatementOptions {
database_type: Some(DatabaseType::Mysql),
identifier_quote: None,
table_meta: Some(DataGridTableMeta {
catalog: None,
database: None,
@@ -3379,6 +3391,7 @@ mod tests {
fn builds_copy_insert_statement_row_by_row() {
let statement = build_data_grid_copy_insert_statement(DataGridCopyInsertStatementOptions {
database_type: Some(DatabaseType::Mysql),
identifier_quote: None,
table_meta: Some(DataGridTableMeta {
catalog: None,
database: None,
@@ -3407,6 +3420,7 @@ mod tests {
fn oracle_copy_insert_statement_uses_one_statement_per_row() {
let statement = build_data_grid_copy_insert_statement(DataGridCopyInsertStatementOptions {
database_type: Some(DatabaseType::Oracle),
identifier_quote: None,
table_meta: Some(DataGridTableMeta {
catalog: None,
database: None,
@@ -3445,6 +3459,7 @@ mod tests {
let insert = build_data_grid_copy_insert_statement(DataGridCopyInsertStatementOptions {
database_type: Some(DatabaseType::Mysql),
identifier_quote: None,
table_meta: Some(table_meta.clone()),
columns: columns.clone(),
column_types: None,
@@ -3461,6 +3476,7 @@ mod tests {
let updates = build_data_grid_copy_update_statements(DataGridCopyUpdateStatementOptions {
database_type: Some(DatabaseType::Mysql),
identifier_quote: None,
table_meta,
columns,
source_columns: None,
@@ -3499,6 +3515,7 @@ mod tests {
fn builds_copy_insert_statement_omits_postgres_tsvector_columns() {
let statement = build_data_grid_copy_insert_statement(DataGridCopyInsertStatementOptions {
database_type: Some(DatabaseType::Postgres),
identifier_quote: None,
table_meta: Some(DataGridTableMeta {
catalog: None,
database: None,
@@ -3530,6 +3547,7 @@ mod tests {
fn oracle_copy_insert_uses_result_column_types_for_date_literals() {
let statement = build_data_grid_copy_insert_statement(DataGridCopyInsertStatementOptions {
database_type: Some(DatabaseType::Oracle),
identifier_quote: None,
table_meta: None,
columns: vec!["ID".to_string(), "CREATED_ON".to_string(), "RAW_TEXT".to_string()],
column_types: Some(vec![
@@ -5032,6 +5050,56 @@ mod tests {
.all(|statement| statement.contains("`gc`.`docfileinfo`") && !statement.contains('"')));
}
#[test]
fn kingbase_mysql_compat_copy_insert_uses_backtick_identifiers() {
let statement = build_data_grid_copy_insert_statement(DataGridCopyInsertStatementOptions {
database_type: Some(DatabaseType::Kingbase),
identifier_quote: Some("`".to_string()),
table_meta: Some(DataGridTableMeta {
catalog: None,
database: None,
schema: Some("audit-schema".to_string()),
table_name: "events".to_string(),
primary_keys: vec!["id".to_string()],
columns: None,
}),
columns: vec!["id".to_string(), "event_type".to_string()],
column_types: None,
source_columns: None,
rows: vec![vec![json!(1), json!("login")]],
exclude_primary_keys: false,
include_computed_columns: false,
insert_mode: DataGridCopyInsertMode::Merged,
});
assert_eq!(
statement.as_deref(),
Some("INSERT INTO `audit-schema`.`events` (`id`, `event_type`) VALUES (1, 'login');")
);
assert!(!statement.as_deref().unwrap_or_default().contains('"'));
}
#[test]
fn kingbase_mysql_compat_copy_update_uses_backtick_identifiers() {
let statements = build_data_grid_copy_update_statements(DataGridCopyUpdateStatementOptions {
database_type: Some(DatabaseType::Kingbase),
identifier_quote: Some("`".to_string()),
table_meta: DataGridTableMeta {
catalog: None,
database: None,
schema: Some("audit-schema".to_string()),
table_name: "events".to_string(),
primary_keys: vec!["id".to_string()],
columns: None,
},
columns: vec!["id".to_string(), "event_type".to_string()],
source_columns: None,
rows: vec![vec![json!(1), json!("logout")]],
});
assert_eq!(statements, vec!["UPDATE `audit-schema`.`events` SET `event_type` = 'logout' WHERE `id` = 1;"]);
}
#[test]
fn vastbase_query_result_writes_use_resolved_search_path_schema() {
let result = prepare_data_grid_save(DataGridSaveStatementOptions {
@@ -6419,6 +6487,7 @@ mod tests {
fn builds_clickhouse_copy_update_statements() {
let statements = build_data_grid_copy_update_statements(DataGridCopyUpdateStatementOptions {
database_type: Some(DatabaseType::ClickHouse),
identifier_quote: None,
table_meta: DataGridTableMeta {
catalog: None,
database: None,
@@ -6448,6 +6517,7 @@ mod tests {
let copy_updates = build_data_grid_copy_update_statements(DataGridCopyUpdateStatementOptions {
database_type: Some(DatabaseType::Doris),
identifier_quote: None,
table_meta: table_meta.clone(),
columns: vec!["id".to_string(), "status".to_string()],
source_columns: None,
@@ -6460,6 +6530,7 @@ mod tests {
let copy_insert = build_data_grid_copy_insert_statement(DataGridCopyInsertStatementOptions {
database_type: Some(DatabaseType::Doris),
identifier_quote: None,
table_meta: Some(table_meta.clone()),
columns: vec!["id".to_string(), "status".to_string()],
column_types: None,
+76 -2
View File
@@ -8,7 +8,7 @@ use crate::connection::task_client_session_id;
use crate::models::connection::DatabaseType;
use crate::mysql_ddl_normalize::DdlNormalizeOptions;
use crate::object_source_sql::build_export_object_source_sql;
use crate::sql_dialect::{qualified_table_name, quote_table_identifier, uses_single_row_insert_statements};
use crate::sql_dialect::{qualified_table_name, uses_single_row_insert_statements};
use crate::transfer::{
format_ch_array_sql_literal, format_pg_array_sql_literal, is_identity_column_extra,
is_mysql_generated_column_extra, keyset_pagination_sql_with_identifier_quote, quote_identifier,
@@ -266,6 +266,8 @@ pub struct ExportedTableSql {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub database_type: Option<DatabaseType>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub identifier_quote: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub schema: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub table_name: Option<String>,
@@ -291,6 +293,8 @@ pub struct BuildExportInsertStatementsOptions {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub database_type: Option<DatabaseType>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub identifier_quote: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub schema: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub table_name: Option<String>,
@@ -935,6 +939,7 @@ pub fn build_export_insert_statements(options: BuildExportInsertStatementsOption
options.schema.as_deref(),
options.table_name.as_deref(),
options.qualified_table_name.as_deref(),
options.identifier_quote.as_deref(),
)?;
let insert_columns = options
.columns
@@ -970,7 +975,13 @@ pub fn build_export_insert_statements(options: BuildExportInsertStatementsOption
};
let columns = insert_columns
.iter()
.map(|(_, column, _)| quote_table_identifier(options.database_type, column))
.map(|(_, column, _)| {
crate::sql_dialect::quote_table_data_identifier(
options.database_type,
column,
options.identifier_quote.as_deref(),
)
})
.collect::<Vec<_>>()
.join(", ");
let mut statements = Vec::new();
@@ -1131,6 +1142,7 @@ pub fn build_database_sql_export(options: BuildDatabaseSqlExportOptions) -> Resu
let inserts = build_export_insert_statements(BuildExportInsertStatementsOptions {
database_type: table.database_type,
identifier_quote: table.identifier_quote.clone(),
schema: table.schema,
table_name: table.table_name,
qualified_table_name: table.qualified_table_name,
@@ -1156,6 +1168,7 @@ fn export_qualified_table_name(
schema: Option<&str>,
table_name: Option<&str>,
qualified_name: Option<&str>,
identifier_quote: Option<&str>,
) -> Result<String, String> {
if let Some(name) = qualified_name.filter(|name| !name.trim().is_empty()) {
return Ok(name.to_string());
@@ -1163,6 +1176,14 @@ fn export_qualified_table_name(
let table_name = table_name
.filter(|name| !name.trim().is_empty())
.ok_or_else(|| "tableName is required when qualifiedTableName is not provided".to_string())?;
if crate::sql_dialect::uses_connection_identifier_quote(database_type, identifier_quote) {
return Ok(crate::sql_dialect::table_data_qualified_table_name(
database_type,
schema,
table_name,
identifier_quote,
));
}
Ok(qualified_table_name(database_type, schema, table_name))
}
@@ -1591,8 +1612,12 @@ fn write_database_export_rows<W: Write>(
// path and the legacy typed INSERT writer. In particular, MySQL uses the
// selected database rather than a schema-qualified table name.
let qualified_table_name = crate::transfer::qualified_table(table, schema, db_type, None);
// Batch database exports do not currently thread a per-connection identifier
// quote through BuildDatabaseSqlExportOptions; Kingbase MySQL-compat users
// should fall back to the single-table export path which carries the quote.
let statements = build_export_insert_statements(BuildExportInsertStatementsOptions {
database_type: Some(*db_type),
identifier_quote: None,
schema: (!schema.is_empty()).then(|| schema.to_string()),
table_name: Some(table.to_string()),
qualified_table_name: Some(qualified_table_name),
@@ -3052,6 +3077,7 @@ mod tests {
fn mysql_spatial_export_uses_wkb_constructor_and_preserves_srid() {
let statements = build_export_insert_statements(BuildExportInsertStatementsOptions {
database_type: Some(DatabaseType::Mysql),
identifier_quote: None,
schema: None,
table_name: Some("places".to_string()),
qualified_table_name: None,
@@ -3129,6 +3155,7 @@ mod tests {
fn database_specific_boolean_export_literals() {
let sqlserver_statements = build_export_insert_statements(BuildExportInsertStatementsOptions {
database_type: Some(DatabaseType::SqlServer),
identifier_quote: None,
schema: Some("dbo".to_string()),
table_name: Some("flags".to_string()),
qualified_table_name: None,
@@ -3141,6 +3168,7 @@ mod tests {
.unwrap();
let postgres_statements = build_export_insert_statements(BuildExportInsertStatementsOptions {
database_type: Some(DatabaseType::Postgres),
identifier_quote: None,
schema: Some("public".to_string()),
table_name: Some("flags".to_string()),
qualified_table_name: None,
@@ -3168,6 +3196,7 @@ mod tests {
fn sqlserver_export_prefixes_unicode_string_literals() {
let statements = build_export_insert_statements(BuildExportInsertStatementsOptions {
database_type: Some(DatabaseType::SqlServer),
identifier_quote: None,
schema: Some("dbo".to_string()),
table_name: Some("people".to_string()),
qualified_table_name: None,
@@ -3212,6 +3241,7 @@ mod tests {
fn mysql_export_inserts_escape_control_characters() {
let statements = build_export_insert_statements(BuildExportInsertStatementsOptions {
database_type: Some(DatabaseType::Mysql),
identifier_quote: None,
schema: None,
table_name: Some("notes".to_string()),
qualified_table_name: None,
@@ -3234,6 +3264,7 @@ mod tests {
let long_value = "x".repeat(300_000);
let statements = build_export_insert_statements(BuildExportInsertStatementsOptions {
database_type: Some(DatabaseType::Mysql),
identifier_quote: None,
schema: None,
table_name: Some("payloads".to_string()),
qualified_table_name: None,
@@ -3253,6 +3284,7 @@ mod tests {
fn sqlserver_export_caps_multi_row_insert_at_1000_rows() {
let statements = build_export_insert_statements(BuildExportInsertStatementsOptions {
database_type: Some(DatabaseType::SqlServer),
identifier_quote: None,
schema: Some("dbo".to_string()),
table_name: Some("items".to_string()),
qualified_table_name: None,
@@ -3273,6 +3305,7 @@ mod tests {
fn doris_export_inserts_escape_control_characters() {
let statements = build_export_insert_statements(BuildExportInsertStatementsOptions {
database_type: Some(DatabaseType::Doris),
identifier_quote: None,
schema: Some("warehouse".to_string()),
table_name: Some("events".to_string()),
qualified_table_name: None,
@@ -3291,6 +3324,7 @@ mod tests {
fn postgres_export_inserts_escape_control_characters() {
let statements = build_export_insert_statements(BuildExportInsertStatementsOptions {
database_type: Some(DatabaseType::Postgres),
identifier_quote: None,
schema: Some("public".to_string()),
table_name: Some("notes".to_string()),
qualified_table_name: None,
@@ -3309,6 +3343,7 @@ mod tests {
fn postgres_export_inserts_escape_quotes_and_backslashes_without_changing_plain_strings() {
let statements = build_export_insert_statements(BuildExportInsertStatementsOptions {
database_type: Some(DatabaseType::Postgres),
identifier_quote: None,
schema: Some("public".to_string()),
table_name: Some("notes".to_string()),
qualified_table_name: None,
@@ -3337,6 +3372,7 @@ mod tests {
fn postgres_jsonb_export_preserves_json_escape_sequences() {
let statements = build_export_insert_statements(BuildExportInsertStatementsOptions {
database_type: Some(DatabaseType::Postgres),
identifier_quote: None,
schema: Some("public".to_string()),
table_name: Some("events".to_string()),
qualified_table_name: None,
@@ -3360,6 +3396,7 @@ mod tests {
fn postgres_vector_export_preserves_pgvector_bracket_literals() {
let statements = build_export_insert_statements(BuildExportInsertStatementsOptions {
database_type: Some(DatabaseType::Postgres),
identifier_quote: None,
schema: Some("public".to_string()),
table_name: Some("items".to_string()),
qualified_table_name: None,
@@ -3393,6 +3430,7 @@ mod tests {
fn builds_batched_insert_statements_for_export() {
let statements = build_export_insert_statements(BuildExportInsertStatementsOptions {
database_type: Some(DatabaseType::Mysql),
identifier_quote: None,
schema: None,
table_name: Some("users".to_string()),
qualified_table_name: None,
@@ -3417,6 +3455,7 @@ mod tests {
fn oracle_export_inserts_use_one_statement_per_row() {
let statements = build_export_insert_statements(BuildExportInsertStatementsOptions {
database_type: Some(DatabaseType::Oracle),
identifier_quote: None,
schema: Some("APP".to_string()),
table_name: Some("USERS".to_string()),
qualified_table_name: None,
@@ -3441,6 +3480,7 @@ mod tests {
fn oracle_export_omits_synthetic_rowid_from_insert_columns() {
let statements = build_export_insert_statements(BuildExportInsertStatementsOptions {
database_type: Some(DatabaseType::Oracle),
identifier_quote: None,
schema: Some("APP".to_string()),
table_name: Some("USERS".to_string()),
qualified_table_name: None,
@@ -3459,6 +3499,7 @@ mod tests {
fn oceanbase_oracle_export_omits_synthetic_rowid_from_insert_columns() {
let statements = build_export_insert_statements(BuildExportInsertStatementsOptions {
database_type: Some(DatabaseType::OceanbaseOracle),
identifier_quote: None,
schema: Some("APP".to_string()),
table_name: Some("USERS".to_string()),
qualified_table_name: None,
@@ -3477,6 +3518,7 @@ mod tests {
fn non_oracle_export_preserves_dbx_rowid_named_column() {
let statements = build_export_insert_statements(BuildExportInsertStatementsOptions {
database_type: Some(DatabaseType::Mysql),
identifier_quote: None,
schema: None,
table_name: Some("users".to_string()),
qualified_table_name: None,
@@ -3495,6 +3537,7 @@ mod tests {
fn oracle_date_columns_export_as_date_literals() {
let statements = build_export_insert_statements(BuildExportInsertStatementsOptions {
database_type: Some(DatabaseType::Oracle),
identifier_quote: None,
schema: Some("APP".to_string()),
table_name: Some("EVENTS".to_string()),
qualified_table_name: None,
@@ -3523,6 +3566,7 @@ mod tests {
for database_type in [DatabaseType::Oracle, DatabaseType::OceanbaseOracle] {
let statements = build_export_insert_statements(BuildExportInsertStatementsOptions {
database_type: Some(database_type),
identifier_quote: None,
schema: Some("APP".to_string()),
table_name: Some("EVENTS".to_string()),
qualified_table_name: None,
@@ -3594,6 +3638,7 @@ mod tests {
fn non_oracle_timestamp_exports_keep_existing_literals() {
let statements = build_export_insert_statements(BuildExportInsertStatementsOptions {
database_type: Some(DatabaseType::Postgres),
identifier_quote: None,
schema: Some("public".to_string()),
table_name: Some("events".to_string()),
qualified_table_name: None,
@@ -3618,6 +3663,7 @@ mod tests {
fn mysql_bit_columns_export_without_quoted_string_values() {
let statements = build_export_insert_statements(BuildExportInsertStatementsOptions {
database_type: Some(DatabaseType::Mysql),
identifier_quote: None,
schema: None,
table_name: Some("flags".to_string()),
qualified_table_name: None,
@@ -3639,6 +3685,7 @@ mod tests {
fn dameng_bit_columns_export_as_numeric_literals() {
let statements = build_export_insert_statements(BuildExportInsertStatementsOptions {
database_type: Some(DatabaseType::Dameng),
identifier_quote: None,
schema: Some("DBX_TEST".to_string()),
table_name: Some("FLAGS".to_string()),
qualified_table_name: None,
@@ -3660,6 +3707,7 @@ mod tests {
fn dameng_strings_export_nul_as_chr_expression() {
let statements = build_export_insert_statements(BuildExportInsertStatementsOptions {
database_type: Some(DatabaseType::Dameng),
identifier_quote: None,
schema: Some("DBX_TEST".to_string()),
table_name: Some("NUL_VALUES".to_string()),
qualified_table_name: None,
@@ -3701,6 +3749,7 @@ mod tests {
fn mysql_export_uses_typed_literals_for_numeric_and_blob_columns() {
let statements = build_export_insert_statements(BuildExportInsertStatementsOptions {
database_type: Some(DatabaseType::Mysql),
identifier_quote: None,
schema: None,
table_name: Some("t_test_01".to_string()),
qualified_table_name: None,
@@ -3727,6 +3776,7 @@ mod tests {
fn temporal_columns_export_without_rfc3339_separator_or_utc_suffix() {
let statements = build_export_insert_statements(BuildExportInsertStatementsOptions {
database_type: Some(DatabaseType::Mysql),
identifier_quote: None,
schema: None,
table_name: Some("events".to_string()),
qualified_table_name: None,
@@ -3760,6 +3810,7 @@ mod tests {
fn postgres_timestamptz_export_keeps_timezone_without_rfc3339_t_separator() {
let statements = build_export_insert_statements(BuildExportInsertStatementsOptions {
database_type: Some(DatabaseType::Postgres),
identifier_quote: None,
schema: Some("public".to_string()),
table_name: Some("events".to_string()),
qualified_table_name: None,
@@ -3786,6 +3837,7 @@ mod tests {
fn sqlserver_rowversion_timestamp_type_is_not_treated_as_datetime() {
let statements = build_export_insert_statements(BuildExportInsertStatementsOptions {
database_type: Some(DatabaseType::SqlServer),
identifier_quote: None,
schema: Some("dbo".to_string()),
table_name: Some("events".to_string()),
qualified_table_name: None,
@@ -3809,6 +3861,7 @@ mod tests {
fn postgres_tsvector_columns_are_omitted_from_sql_insert_export() {
let statements = build_export_insert_statements(BuildExportInsertStatementsOptions {
database_type: Some(DatabaseType::Postgres),
identifier_quote: None,
schema: Some("public".to_string()),
table_name: Some("articles".to_string()),
qualified_table_name: None,
@@ -3832,6 +3885,7 @@ mod tests {
tables: vec![ExportedTableSql {
display_name: "orders".to_string(),
database_type: Some(DatabaseType::Mysql),
identifier_quote: None,
schema: None,
table_name: Some("orders".to_string()),
qualified_table_name: None,
@@ -3918,6 +3972,7 @@ mod tests {
fn dameng_identity_export_inserts_enable_identity_insert() {
let statements = build_export_insert_statements(BuildExportInsertStatementsOptions {
database_type: Some(DatabaseType::Dameng),
identifier_quote: None,
schema: Some("SYSDBA".to_string()),
table_name: Some("USERS".to_string()),
qualified_table_name: None,
@@ -3945,6 +4000,7 @@ mod tests {
tables: vec![ExportedTableSql {
display_name: "users".to_string(),
database_type: Some(DatabaseType::Mysql),
identifier_quote: None,
schema: None,
table_name: Some("users".to_string()),
qualified_table_name: None,
@@ -4296,4 +4352,22 @@ mod tests {
let _ = std::fs::remove_dir_all(&scratch);
}
#[test]
fn kingbase_mysql_compat_export_insert_uses_backtick_identifiers() {
let statements = build_export_insert_statements(BuildExportInsertStatementsOptions {
database_type: Some(DatabaseType::Kingbase),
identifier_quote: Some("`".to_string()),
schema: Some("audit-schema".to_string()),
table_name: Some("events".to_string()),
qualified_table_name: None,
columns: vec!["id".to_string(), "event_type".to_string()],
column_types: vec![None, None],
column_extras: vec![None, None],
rows: vec![vec![json!(1), json!("login")]],
batch_size: Some(100),
})
.unwrap();
assert_eq!(statements, vec!["INSERT INTO `audit-schema`.`events` (`id`, `event_type`) VALUES (1, 'login');"]);
}
}
@@ -104,6 +104,8 @@ pub struct QueryResultExportRequest {
pub numeric_column_right_align: bool,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub column_comments: Option<Vec<Option<String>>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub identifier_quote: Option<String>,
}
pub struct StagedExportTarget {
@@ -300,6 +302,7 @@ struct SqlInsertWriter {
database_type: DatabaseType,
schema: Option<String>,
table_name: String,
identifier_quote: Option<String>,
}
impl SqlInsertWriter {
@@ -325,6 +328,7 @@ impl SqlInsertWriter {
database_type: request.database_type,
schema: request.schema.clone(),
table_name,
identifier_quote: request.identifier_quote.clone(),
})
}
@@ -355,6 +359,7 @@ impl SqlInsertWriter {
}
let stmts = build_export_insert_statements(BuildExportInsertStatementsOptions {
database_type: Some(self.database_type),
identifier_quote: self.identifier_quote.clone(),
schema: self.schema.clone(),
table_name: Some(self.table_name.clone()),
qualified_table_name: None,
@@ -1869,6 +1874,7 @@ mod tests {
export_column_types: None,
numeric_column_right_align: false,
column_comments: None,
identifier_quote: None,
}
}
+3
View File
@@ -1179,6 +1179,7 @@ async fn try_export_native_table_stream(
}
let statements = build_export_insert_statements(BuildExportInsertStatementsOptions {
database_type: Some(*db_type),
identifier_quote: request.identifier_quote.clone(),
schema: request.schema.clone(),
table_name: Some(request.table_name.clone()),
qualified_table_name: None,
@@ -1993,6 +1994,7 @@ async fn export_table_data_core_inner(
let statements = build_export_insert_statements(BuildExportInsertStatementsOptions {
database_type: Some(db_type),
identifier_quote: request.identifier_quote.clone(),
schema: request.schema.clone(),
table_name: Some(request.table_name.clone()),
qualified_table_name: None,
@@ -2761,6 +2763,7 @@ mod tests {
let statements = build_export_insert_statements(BuildExportInsertStatementsOptions {
database_type: Some(DatabaseType::Oracle),
identifier_quote: request.identifier_quote,
schema: request.schema,
table_name: Some(request.table_name),
qualified_table_name: None,
@@ -104,6 +104,7 @@ async fn live_clickhouse_query_result_export_xlsx_streams_random_order_query_onc
export_table_name: None,
export_column_types: None,
column_comments: None,
identifier_quote: None,
numeric_column_right_align: false,
};
let done_seen = AtomicBool::new(false);
+2
View File
@@ -450,6 +450,7 @@ async fn live_mysql_query_result_export_xlsx_streams_single_query_without_duplic
export_table_name: None,
export_column_types: None,
column_comments: None,
identifier_quote: None,
numeric_column_right_align: false,
};
let done_seen = AtomicBool::new(false);
@@ -540,6 +541,7 @@ async fn live_mysql_xlsx_export_can_outlive_query_timeout_while_rows_keep_arrivi
export_table_name: None,
export_column_types: None,
column_comments: None,
identifier_quote: None,
numeric_column_right_align: false,
};
let rows_exported = AtomicU64::new(0);
@@ -144,6 +144,7 @@ async fn live_postgres_query_result_export_uses_single_streamed_query() {
export_table_name: None,
export_column_types: None,
column_comments: None,
identifier_quote: None,
numeric_column_right_align: false,
};
let done_seen = AtomicBool::new(false);
@@ -224,6 +225,7 @@ async fn live_postgres_query_result_xlsx_preserves_temporal_cell_types() {
export_table_name: None,
export_column_types: None,
column_comments: None,
identifier_quote: None,
numeric_column_right_align: false,
};
@@ -293,6 +295,7 @@ async fn live_postgres_numeric_xlsx_ignores_fractional_trailing_zeros() {
export_table_name: None,
export_column_types: None,
column_comments: None,
identifier_quote: None,
numeric_column_right_align: true,
};
@@ -367,6 +370,7 @@ async fn live_postgres_truncated_batch_result_export_replays_safe_temp_setup() {
export_table_name: None,
export_column_types: None,
column_comments: None,
identifier_quote: None,
numeric_column_right_align: false,
};
let csv_rows = AtomicU64::new(0);
@@ -443,6 +447,7 @@ async fn live_postgres_xlsx_export_can_outlive_query_timeout_while_rows_keep_arr
export_table_name: None,
export_column_types: None,
column_comments: None,
identifier_quote: None,
numeric_column_right_align: false,
};
let rows_exported = AtomicU64::new(0);
@@ -510,6 +515,7 @@ async fn live_postgres_stream_still_times_out_without_progress_and_recovers() {
export_table_name: None,
export_column_types: None,
column_comments: None,
identifier_quote: None,
numeric_column_right_align: false,
};
let started_at = Instant::now();
@@ -70,6 +70,7 @@ async fn postgres_tsvector_generated_columns_are_readable_and_omitted_from_inser
};
let copy_insert = build_data_grid_copy_insert_statement(DataGridCopyInsertStatementOptions {
database_type: Some(DatabaseType::Postgres),
identifier_quote: None,
table_meta: Some(table_meta),
columns: result.columns.clone(),
column_types: None,
@@ -85,6 +86,7 @@ async fn postgres_tsvector_generated_columns_are_readable_and_omitted_from_inser
let export_insert = build_export_insert_statements(BuildExportInsertStatementsOptions {
database_type: Some(DatabaseType::Postgres),
identifier_quote: None,
schema: Some(schema.clone()),
table_name: Some("articles".to_string()),
qualified_table_name: None,
@@ -1201,6 +1201,7 @@ async fn live_sqlserver_query_result_export_streams_cte_query_to_csv() {
export_table_name: None,
export_column_types: None,
column_comments: None,
identifier_quote: None,
numeric_column_right_align: false,
};
let done_seen = AtomicBool::new(false);
@@ -119,6 +119,7 @@ async fn live_sqlserver_xlsx_export_can_outlive_query_timeout_while_rows_keep_ar
export_table_name: None,
export_column_types: None,
column_comments: None,
identifier_quote: None,
numeric_column_right_align: false,
};
let rows_exported = AtomicU64::new(0);