feat(data-grid): optional database/schema prefix in copied INSERT SQL
This commit is contained in:
19 files changed
+220
-1
No files matched your search
@@ -209,6 +209,9 @@ function save() {
|
|||||||
<label v-if="draftExtractor === 'sql-inserts'" class="flex items-center gap-2 text-sm"
|
<label v-if="draftExtractor === 'sql-inserts'" class="flex items-center gap-2 text-sm"
|
||||||
><input type="checkbox" :checked="draftOptions.sql.excludePrimaryKeysFromInsert" @change="updateSql('excludePrimaryKeysFromInsert', ($event.target as HTMLInputElement).checked)" />{{ t("grid.copyExtractorExcludePrimaryKeys") }}</label
|
><input type="checkbox" :checked="draftOptions.sql.excludePrimaryKeysFromInsert" @change="updateSql('excludePrimaryKeysFromInsert', ($event.target as HTMLInputElement).checked)" />{{ t("grid.copyExtractorExcludePrimaryKeys") }}</label
|
||||||
>
|
>
|
||||||
|
<label v-if="draftExtractor === 'sql-inserts'" class="flex items-center gap-2 text-sm"
|
||||||
|
><input type="checkbox" :checked="draftOptions.sql.includeDatabaseName" @change="updateSql('includeDatabaseName', ($event.target as HTMLInputElement).checked)" />{{ t("grid.copyExtractorIncludeDatabaseName") }}</label
|
||||||
|
>
|
||||||
<div v-if="draftExtractor === 'sql-inserts'" class="space-y-1.5">
|
<div v-if="draftExtractor === 'sql-inserts'" class="space-y-1.5">
|
||||||
<Label>{{ t("grid.copyExtractorInsertMode") }}</Label>
|
<Label>{{ t("grid.copyExtractorInsertMode") }}</Label>
|
||||||
<Select :model-value="draftOptions.sql.insertMode" @update:model-value="updateSql('insertMode', $event as DataGridExtractorOptions['sql']['insertMode'])">
|
<Select :model-value="draftOptions.sql.insertMode" @update:model-value="updateSql('insertMode', $event as DataGridExtractorOptions['sql']['insertMode'])">
|
||||||
|
|||||||
@@ -1754,6 +1754,7 @@ export default withEnglishFallback({
|
|||||||
copyExtractorSkipComputed: "Hesablanan sütunları ötür",
|
copyExtractorSkipComputed: "Hesablanan sütunları ötür",
|
||||||
copyExtractorSkipGenerated: "Avtomatik yaradılan sütunları ötür",
|
copyExtractorSkipGenerated: "Avtomatik yaradılan sütunları ötür",
|
||||||
copyExtractorExcludePrimaryKeys: "Əsas açarları istisna et",
|
copyExtractorExcludePrimaryKeys: "Əsas açarları istisna et",
|
||||||
|
copyExtractorIncludeDatabaseName: "Verilənlər bazasının adını daxil et",
|
||||||
copyExtractorInsertMode: "INSERT əmrləri",
|
copyExtractorInsertMode: "INSERT əmrləri",
|
||||||
copyExtractorInsertMerged: "Sətirləri birləşdir",
|
copyExtractorInsertMerged: "Sətirləri birləşdir",
|
||||||
copyExtractorInsertRowByRow: "Hər sətir üçün bir əmr",
|
copyExtractorInsertRowByRow: "Hər sətir üçün bir əmr",
|
||||||
|
|||||||
@@ -1903,6 +1903,7 @@ export default {
|
|||||||
copyExtractorSkipComputed: "Skip computed columns",
|
copyExtractorSkipComputed: "Skip computed columns",
|
||||||
copyExtractorSkipGenerated: "Skip generated columns",
|
copyExtractorSkipGenerated: "Skip generated columns",
|
||||||
copyExtractorExcludePrimaryKeys: "Exclude primary keys",
|
copyExtractorExcludePrimaryKeys: "Exclude primary keys",
|
||||||
|
copyExtractorIncludeDatabaseName: "Include database name",
|
||||||
copyExtractorInsertMode: "INSERT statements",
|
copyExtractorInsertMode: "INSERT statements",
|
||||||
copyExtractorInsertMerged: "Merge rows",
|
copyExtractorInsertMerged: "Merge rows",
|
||||||
copyExtractorInsertRowByRow: "One statement per row",
|
copyExtractorInsertRowByRow: "One statement per row",
|
||||||
|
|||||||
@@ -2348,6 +2348,7 @@ export default withEnglishFallback({
|
|||||||
copyExtractorSkipComputed: "Saltar columnas calculadas",
|
copyExtractorSkipComputed: "Saltar columnas calculadas",
|
||||||
copyExtractorSkipGenerated: "Saltar columnas generadas",
|
copyExtractorSkipGenerated: "Saltar columnas generadas",
|
||||||
copyExtractorExcludePrimaryKeys: "Excluir claves primarias",
|
copyExtractorExcludePrimaryKeys: "Excluir claves primarias",
|
||||||
|
copyExtractorIncludeDatabaseName: "Incluir el nombre de la base de datos",
|
||||||
copyExtractorInsertMode: "Modo de sentencia INSERT",
|
copyExtractorInsertMode: "Modo de sentencia INSERT",
|
||||||
copyExtractorInsertMerged: "Combinar varias filas",
|
copyExtractorInsertMerged: "Combinar varias filas",
|
||||||
copyExtractorInsertRowByRow: "Una sentencia por fila",
|
copyExtractorInsertRowByRow: "Una sentencia por fila",
|
||||||
|
|||||||
@@ -2346,6 +2346,7 @@ export default withEnglishFallback({
|
|||||||
copyExtractorSkipComputed: "Salta colonne calcolate",
|
copyExtractorSkipComputed: "Salta colonne calcolate",
|
||||||
copyExtractorSkipGenerated: "Salta colonne generate",
|
copyExtractorSkipGenerated: "Salta colonne generate",
|
||||||
copyExtractorExcludePrimaryKeys: "Escludi chiavi primarie",
|
copyExtractorExcludePrimaryKeys: "Escludi chiavi primarie",
|
||||||
|
copyExtractorIncludeDatabaseName: "Includi il nome del database",
|
||||||
copyExtractorInsertMode: "Modalità istruzione INSERT",
|
copyExtractorInsertMode: "Modalità istruzione INSERT",
|
||||||
copyExtractorInsertMerged: "Unisci più righe",
|
copyExtractorInsertMerged: "Unisci più righe",
|
||||||
copyExtractorInsertRowByRow: "Una istruzione per riga",
|
copyExtractorInsertRowByRow: "Una istruzione per riga",
|
||||||
|
|||||||
@@ -2368,6 +2368,7 @@ export default withEnglishFallback({
|
|||||||
copyExtractorSkipComputed: "計算列をスキップ",
|
copyExtractorSkipComputed: "計算列をスキップ",
|
||||||
copyExtractorSkipGenerated: "生成列をスキップ",
|
copyExtractorSkipGenerated: "生成列をスキップ",
|
||||||
copyExtractorExcludePrimaryKeys: "主キーを除外",
|
copyExtractorExcludePrimaryKeys: "主キーを除外",
|
||||||
|
copyExtractorIncludeDatabaseName: "データベース名を含める",
|
||||||
copyExtractorInsertMode: "INSERT文モード",
|
copyExtractorInsertMode: "INSERT文モード",
|
||||||
copyExtractorInsertMerged: "複数行をマージ",
|
copyExtractorInsertMerged: "複数行をマージ",
|
||||||
copyExtractorInsertRowByRow: "1行ごとに1文",
|
copyExtractorInsertRowByRow: "1行ごとに1文",
|
||||||
|
|||||||
@@ -1773,6 +1773,7 @@ export default withEnglishFallback({
|
|||||||
copyExtractorSkipComputed: "계산된 컬럼 건너뛰기",
|
copyExtractorSkipComputed: "계산된 컬럼 건너뛰기",
|
||||||
copyExtractorSkipGenerated: "생성된 컬럼 건너뛰기",
|
copyExtractorSkipGenerated: "생성된 컬럼 건너뛰기",
|
||||||
copyExtractorExcludePrimaryKeys: "기본 키 제외",
|
copyExtractorExcludePrimaryKeys: "기본 키 제외",
|
||||||
|
copyExtractorIncludeDatabaseName: "데이터베이스 이름 포함",
|
||||||
copyExtractorInsertMode: "INSERT 구문",
|
copyExtractorInsertMode: "INSERT 구문",
|
||||||
copyExtractorInsertMerged: "행 병합",
|
copyExtractorInsertMerged: "행 병합",
|
||||||
copyExtractorInsertRowByRow: "행별 한 구문",
|
copyExtractorInsertRowByRow: "행별 한 구문",
|
||||||
|
|||||||
@@ -2348,6 +2348,7 @@ export default withEnglishFallback({
|
|||||||
copyExtractorSkipComputed: "Pular colunas computadas",
|
copyExtractorSkipComputed: "Pular colunas computadas",
|
||||||
copyExtractorSkipGenerated: "Pular colunas geradas",
|
copyExtractorSkipGenerated: "Pular colunas geradas",
|
||||||
copyExtractorExcludePrimaryKeys: "Excluir chaves primárias",
|
copyExtractorExcludePrimaryKeys: "Excluir chaves primárias",
|
||||||
|
copyExtractorIncludeDatabaseName: "Incluir nome do banco de dados",
|
||||||
copyExtractorInsertMode: "Modo de instrução INSERT",
|
copyExtractorInsertMode: "Modo de instrução INSERT",
|
||||||
copyExtractorInsertMerged: "Mesclar várias linhas",
|
copyExtractorInsertMerged: "Mesclar várias linhas",
|
||||||
copyExtractorInsertRowByRow: "Uma instrução por linha",
|
copyExtractorInsertRowByRow: "Uma instrução por linha",
|
||||||
|
|||||||
@@ -1755,6 +1755,7 @@ export default withEnglishFallback({
|
|||||||
copyExtractorSkipComputed: "Hesaplanan sütunları atla",
|
copyExtractorSkipComputed: "Hesaplanan sütunları atla",
|
||||||
copyExtractorSkipGenerated: "Üretilen sütunları atla",
|
copyExtractorSkipGenerated: "Üretilen sütunları atla",
|
||||||
copyExtractorExcludePrimaryKeys: "Birincil anahtarları hariç tut",
|
copyExtractorExcludePrimaryKeys: "Birincil anahtarları hariç tut",
|
||||||
|
copyExtractorIncludeDatabaseName: "Veritabanı adını dahil et",
|
||||||
copyExtractorInsertMode: "INSERT ifadeleri",
|
copyExtractorInsertMode: "INSERT ifadeleri",
|
||||||
copyExtractorInsertMerged: "Satırları birleştir",
|
copyExtractorInsertMerged: "Satırları birleştir",
|
||||||
copyExtractorInsertRowByRow: "Her satır için bir ifade",
|
copyExtractorInsertRowByRow: "Her satır için bir ifade",
|
||||||
|
|||||||
@@ -1817,6 +1817,7 @@ export default withEnglishFallback({
|
|||||||
copyExtractorSkipComputed: "跳过计算列",
|
copyExtractorSkipComputed: "跳过计算列",
|
||||||
copyExtractorSkipGenerated: "跳过生成列",
|
copyExtractorSkipGenerated: "跳过生成列",
|
||||||
copyExtractorExcludePrimaryKeys: "排除主键",
|
copyExtractorExcludePrimaryKeys: "排除主键",
|
||||||
|
copyExtractorIncludeDatabaseName: "包含数据库名称",
|
||||||
copyExtractorInsertMode: "INSERT 语句模式",
|
copyExtractorInsertMode: "INSERT 语句模式",
|
||||||
copyExtractorInsertMerged: "合并多行",
|
copyExtractorInsertMerged: "合并多行",
|
||||||
copyExtractorInsertRowByRow: "每行一条语句",
|
copyExtractorInsertRowByRow: "每行一条语句",
|
||||||
|
|||||||
@@ -2354,6 +2354,7 @@ export default withEnglishFallback({
|
|||||||
copyExtractorSkipComputed: "跳過計算列",
|
copyExtractorSkipComputed: "跳過計算列",
|
||||||
copyExtractorSkipGenerated: "跳過生成列",
|
copyExtractorSkipGenerated: "跳過生成列",
|
||||||
copyExtractorExcludePrimaryKeys: "排除主鍵",
|
copyExtractorExcludePrimaryKeys: "排除主鍵",
|
||||||
|
copyExtractorIncludeDatabaseName: "包含資料庫名稱",
|
||||||
copyExtractorInsertMode: "INSERT 語句模式",
|
copyExtractorInsertMode: "INSERT 語句模式",
|
||||||
copyExtractorInsertMerged: "合併多行",
|
copyExtractorInsertMerged: "合併多行",
|
||||||
copyExtractorInsertRowByRow: "每行一條語句",
|
copyExtractorInsertRowByRow: "每行一條語句",
|
||||||
|
|||||||
@@ -3,6 +3,13 @@ import { describe, expect, it } from "vitest";
|
|||||||
import { DEFAULT_DATA_GRID_EXTRACTOR_OPTIONS, normalizeDataGridCopyPreference, normalizeDataGridExtractorOptions, resolveDataGridCopyPreference, validateDataGridExtractorOptions } from "@/lib/dataGrid/dataGridCopyExtractor";
|
import { DEFAULT_DATA_GRID_EXTRACTOR_OPTIONS, normalizeDataGridCopyPreference, normalizeDataGridExtractorOptions, resolveDataGridCopyPreference, validateDataGridExtractorOptions } from "@/lib/dataGrid/dataGridCopyExtractor";
|
||||||
|
|
||||||
describe("data-grid extractor options", () => {
|
describe("data-grid extractor options", () => {
|
||||||
|
it("keeps database qualification for legacy options and persists explicit opt-out", () => {
|
||||||
|
expect(normalizeDataGridExtractorOptions({ sql: {} }).sql.includeDatabaseName).toBe(true);
|
||||||
|
const configured = normalizeDataGridExtractorOptions({ sql: { includeDatabaseName: false } });
|
||||||
|
expect(normalizeDataGridExtractorOptions(JSON.parse(JSON.stringify(configured))).sql.includeDatabaseName).toBe(false);
|
||||||
|
expect(normalizeDataGridExtractorOptions({ sql: { includeDatabaseName: "false" } }).sql.includeDatabaseName).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
it("defaults DSV NULL output to an empty spreadsheet field", () => {
|
it("defaults DSV NULL output to an empty spreadsheet field", () => {
|
||||||
expect(DEFAULT_DATA_GRID_EXTRACTOR_OPTIONS.dsv.nullText).toBe("");
|
expect(DEFAULT_DATA_GRID_EXTRACTOR_OPTIONS.dsv.nullText).toBe("");
|
||||||
expect(normalizeDataGridExtractorOptions({}).dsv.nullText).toBe("");
|
expect(normalizeDataGridExtractorOptions({}).dsv.nullText).toBe("");
|
||||||
|
|||||||
@@ -88,6 +88,7 @@ export interface DataGridExtractorOptions {
|
|||||||
skipGeneratedColumns: boolean;
|
skipGeneratedColumns: boolean;
|
||||||
insertMode: DataGridCopyInsertMode;
|
insertMode: DataGridCopyInsertMode;
|
||||||
excludePrimaryKeysFromInsert: boolean;
|
excludePrimaryKeysFromInsert: boolean;
|
||||||
|
includeDatabaseName: boolean;
|
||||||
};
|
};
|
||||||
json: {
|
json: {
|
||||||
pretty: boolean;
|
pretty: boolean;
|
||||||
@@ -148,6 +149,7 @@ export const DEFAULT_DATA_GRID_EXTRACTOR_OPTIONS: DataGridExtractorOptions = {
|
|||||||
skipGeneratedColumns: true,
|
skipGeneratedColumns: true,
|
||||||
insertMode: "merged",
|
insertMode: "merged",
|
||||||
excludePrimaryKeysFromInsert: false,
|
excludePrimaryKeysFromInsert: false,
|
||||||
|
includeDatabaseName: true,
|
||||||
},
|
},
|
||||||
json: { pretty: true, camelCaseFieldNames: false },
|
json: { pretty: true, camelCaseFieldNames: false },
|
||||||
};
|
};
|
||||||
@@ -178,6 +180,7 @@ export function normalizeDataGridExtractorOptions(value: unknown): DataGridExtra
|
|||||||
skipGeneratedColumns: sql.skipGeneratedColumns !== false,
|
skipGeneratedColumns: sql.skipGeneratedColumns !== false,
|
||||||
insertMode: sql.insertMode === "row-by-row" ? "row-by-row" : "merged",
|
insertMode: sql.insertMode === "row-by-row" ? "row-by-row" : "merged",
|
||||||
excludePrimaryKeysFromInsert: sql.excludePrimaryKeysFromInsert === true,
|
excludePrimaryKeysFromInsert: sql.excludePrimaryKeysFromInsert === true,
|
||||||
|
includeDatabaseName: sql.includeDatabaseName !== false,
|
||||||
},
|
},
|
||||||
json: {
|
json: {
|
||||||
pretty: json.pretty !== false,
|
pretty: json.pretty !== false,
|
||||||
|
|||||||
@@ -56,6 +56,7 @@ export interface DataGridCopyInsertStatementOptions {
|
|||||||
rows: GridCellValue[][];
|
rows: GridCellValue[][];
|
||||||
excludePrimaryKeys?: boolean;
|
excludePrimaryKeys?: boolean;
|
||||||
includeComputedColumns?: boolean;
|
includeComputedColumns?: boolean;
|
||||||
|
includeDatabaseName?: boolean;
|
||||||
insertMode?: DataGridCopyInsertMode;
|
insertMode?: DataGridCopyInsertMode;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -78,6 +78,7 @@ async fn postgres_tsvector_generated_columns_are_readable_and_omitted_from_inser
|
|||||||
rows: result.rows.clone(),
|
rows: result.rows.clone(),
|
||||||
exclude_primary_keys: false,
|
exclude_primary_keys: false,
|
||||||
include_computed_columns: false,
|
include_computed_columns: false,
|
||||||
|
include_database_name: true,
|
||||||
insert_mode: DataGridCopyInsertMode::Merged,
|
insert_mode: DataGridCopyInsertMode::Merged,
|
||||||
})
|
})
|
||||||
.expect("copy insert");
|
.expect("copy insert");
|
||||||
|
|||||||
@@ -94,6 +94,7 @@ pub struct DataGridSqlExtractorOptions {
|
|||||||
pub skip_generated_columns: bool,
|
pub skip_generated_columns: bool,
|
||||||
pub insert_mode: DataGridCopyInsertMode,
|
pub insert_mode: DataGridCopyInsertMode,
|
||||||
pub exclude_primary_keys_from_insert: bool,
|
pub exclude_primary_keys_from_insert: bool,
|
||||||
|
pub include_database_name: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for DataGridSqlExtractorOptions {
|
impl Default for DataGridSqlExtractorOptions {
|
||||||
@@ -103,6 +104,7 @@ impl Default for DataGridSqlExtractorOptions {
|
|||||||
skip_generated_columns: DEFAULT_SQL_SKIP_GENERATED_COLUMNS,
|
skip_generated_columns: DEFAULT_SQL_SKIP_GENERATED_COLUMNS,
|
||||||
insert_mode: DataGridCopyInsertMode::Merged,
|
insert_mode: DataGridCopyInsertMode::Merged,
|
||||||
exclude_primary_keys_from_insert: false,
|
exclude_primary_keys_from_insert: false,
|
||||||
|
include_database_name: true,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -72,6 +72,7 @@ pub(super) fn write_sql_inserts(
|
|||||||
rows: data.rows,
|
rows: data.rows,
|
||||||
exclude_primary_keys: context.request.options.sql.exclude_primary_keys_from_insert,
|
exclude_primary_keys: context.request.options.sql.exclude_primary_keys_from_insert,
|
||||||
include_computed_columns: !context.request.options.sql.skip_computed_columns,
|
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,
|
insert_mode: context.request.options.sql.insert_mode,
|
||||||
})
|
})
|
||||||
.ok_or_else(|| {
|
.ok_or_else(|| {
|
||||||
|
|||||||
@@ -42,10 +42,133 @@ fn partially_deserialized_options_use_the_canonical_defaults() {
|
|||||||
assert!(!options.sql.skip_generated_columns);
|
assert!(!options.sql.skip_generated_columns);
|
||||||
assert_eq!(options.sql.insert_mode, crate::data_grid_sql::DataGridCopyInsertMode::Merged);
|
assert_eq!(options.sql.insert_mode, crate::data_grid_sql::DataGridCopyInsertMode::Merged);
|
||||||
assert!(!options.sql.exclude_primary_keys_from_insert);
|
assert!(!options.sql.exclude_primary_keys_from_insert);
|
||||||
|
assert!(options.sql.include_database_name);
|
||||||
assert!(options.json.pretty);
|
assert!(options.json.pretty);
|
||||||
assert!(!options.json.camel_case_field_names);
|
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]
|
#[test]
|
||||||
fn extracts_csv_with_minimal_standard_quoting() {
|
fn extracts_csv_with_minimal_standard_quoting() {
|
||||||
let result = extract_data_grid_selection(request(DataGridExtractorId::Csv)).expect("CSV extraction");
|
let result = extract_data_grid_selection(request(DataGridExtractorId::Csv)).expect("CSV extraction");
|
||||||
|
|||||||
@@ -134,10 +134,16 @@ pub struct DataGridCopyInsertStatementOptions {
|
|||||||
pub exclude_primary_keys: bool,
|
pub exclude_primary_keys: bool,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub include_computed_columns: bool,
|
pub include_computed_columns: bool,
|
||||||
|
#[serde(default = "default_include_database_name")]
|
||||||
|
pub include_database_name: bool,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub insert_mode: DataGridCopyInsertMode,
|
pub insert_mode: DataGridCopyInsertMode,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn default_include_database_name() -> bool {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
|
#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
|
||||||
#[serde(rename_all = "kebab-case")]
|
#[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(
|
let table = options.table_meta.as_ref().map_or_else(
|
||||||
|| "table_name".to_string(),
|
|| "table_name".to_string(),
|
||||||
|meta| {
|
|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(
|
data_grid_qualified_table_name(
|
||||||
options.database_type,
|
options.database_type,
|
||||||
meta.catalog.as_deref(),
|
meta.catalog.as_deref(),
|
||||||
meta.schema.as_deref(),
|
schema,
|
||||||
meta.database.as_deref(),
|
meta.database.as_deref(),
|
||||||
&meta.table_name,
|
&meta.table_name,
|
||||||
options.identifier_quote.as_deref(),
|
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")]],
|
rows: vec![vec![json!(1), json!("ada"), json!("Ada")], vec![json!(2), json!("linus"), json!("Linus")]],
|
||||||
exclude_primary_keys: true,
|
exclude_primary_keys: true,
|
||||||
include_computed_columns: false,
|
include_computed_columns: false,
|
||||||
|
include_database_name: true,
|
||||||
insert_mode: DataGridCopyInsertMode::Merged,
|
insert_mode: DataGridCopyInsertMode::Merged,
|
||||||
});
|
});
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
@@ -4067,6 +4117,7 @@ mod tests {
|
|||||||
rows: vec![vec![json!(1), json!("Ada")]],
|
rows: vec![vec![json!(1), json!("Ada")]],
|
||||||
exclude_primary_keys: true,
|
exclude_primary_keys: true,
|
||||||
include_computed_columns: false,
|
include_computed_columns: false,
|
||||||
|
include_database_name: true,
|
||||||
insert_mode: DataGridCopyInsertMode::Merged,
|
insert_mode: DataGridCopyInsertMode::Merged,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -4113,6 +4164,7 @@ mod tests {
|
|||||||
rows: vec![vec![json!("A-1"), json!("A-1"), json!("Ada")]],
|
rows: vec![vec![json!("A-1"), json!("A-1"), json!("Ada")]],
|
||||||
exclude_primary_keys: false,
|
exclude_primary_keys: false,
|
||||||
include_computed_columns: false,
|
include_computed_columns: false,
|
||||||
|
include_database_name: true,
|
||||||
insert_mode: DataGridCopyInsertMode::Merged,
|
insert_mode: DataGridCopyInsertMode::Merged,
|
||||||
});
|
});
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
@@ -4169,6 +4221,7 @@ mod tests {
|
|||||||
rows: vec![vec![json!(1), json!("Ada"), json!("ADA")]],
|
rows: vec![vec![json!(1), json!("Ada"), json!("ADA")]],
|
||||||
exclude_primary_keys: true,
|
exclude_primary_keys: true,
|
||||||
include_computed_columns: false,
|
include_computed_columns: false,
|
||||||
|
include_database_name: true,
|
||||||
insert_mode: DataGridCopyInsertMode::Merged,
|
insert_mode: DataGridCopyInsertMode::Merged,
|
||||||
});
|
});
|
||||||
assert_eq!(statement.as_deref(), Some("INSERT INTO `users` (`label`) VALUES ('Ada');"));
|
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")]],
|
rows: vec![vec![json!(1), json!("2026-08-18"), json!("sweater")]],
|
||||||
exclude_primary_keys: true,
|
exclude_primary_keys: true,
|
||||||
include_computed_columns: false,
|
include_computed_columns: false,
|
||||||
|
include_database_name: true,
|
||||||
insert_mode: DataGridCopyInsertMode::Merged,
|
insert_mode: DataGridCopyInsertMode::Merged,
|
||||||
});
|
});
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
@@ -4282,6 +4336,7 @@ mod tests {
|
|||||||
rows: vec![vec![json!("AD"), json!("Andorra")]],
|
rows: vec![vec![json!("AD"), json!("Andorra")]],
|
||||||
exclude_primary_keys: true,
|
exclude_primary_keys: true,
|
||||||
include_computed_columns: false,
|
include_computed_columns: false,
|
||||||
|
include_database_name: true,
|
||||||
insert_mode: DataGridCopyInsertMode::Merged,
|
insert_mode: DataGridCopyInsertMode::Merged,
|
||||||
});
|
});
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
@@ -4311,6 +4366,7 @@ mod tests {
|
|||||||
rows: vec![vec![json!(1), json!("ada")]],
|
rows: vec![vec![json!(1), json!("ada")]],
|
||||||
exclude_primary_keys: true,
|
exclude_primary_keys: true,
|
||||||
include_computed_columns: false,
|
include_computed_columns: false,
|
||||||
|
include_database_name: true,
|
||||||
insert_mode: DataGridCopyInsertMode::Merged,
|
insert_mode: DataGridCopyInsertMode::Merged,
|
||||||
});
|
});
|
||||||
assert_eq!(statement.as_deref(), Some("INSERT INTO `users` (`id`, `login_name`) VALUES (1, 'ada');"));
|
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"]}"#)]],
|
rows: vec![vec![json!(7), json!(true), json!(r#"{"name":"Ada","roles":["admin"]}"#)]],
|
||||||
exclude_primary_keys: false,
|
exclude_primary_keys: false,
|
||||||
include_computed_columns: false,
|
include_computed_columns: false,
|
||||||
|
include_database_name: true,
|
||||||
insert_mode: DataGridCopyInsertMode::Merged,
|
insert_mode: DataGridCopyInsertMode::Merged,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -4349,6 +4406,7 @@ mod tests {
|
|||||||
rows: vec![vec![json!([1, 2, 3])]],
|
rows: vec![vec![json!([1, 2, 3])]],
|
||||||
exclude_primary_keys: false,
|
exclude_primary_keys: false,
|
||||||
include_computed_columns: false,
|
include_computed_columns: false,
|
||||||
|
include_database_name: true,
|
||||||
insert_mode: DataGridCopyInsertMode::Merged,
|
insert_mode: DataGridCopyInsertMode::Merged,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -4367,6 +4425,7 @@ mod tests {
|
|||||||
rows: vec![vec![json!([])]],
|
rows: vec![vec![json!([])]],
|
||||||
exclude_primary_keys: false,
|
exclude_primary_keys: false,
|
||||||
include_computed_columns: false,
|
include_computed_columns: false,
|
||||||
|
include_database_name: true,
|
||||||
insert_mode: DataGridCopyInsertMode::Merged,
|
insert_mode: DataGridCopyInsertMode::Merged,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -4385,6 +4444,7 @@ mod tests {
|
|||||||
rows: vec![vec![json!({"a": 1})]],
|
rows: vec![vec![json!({"a": 1})]],
|
||||||
exclude_primary_keys: false,
|
exclude_primary_keys: false,
|
||||||
include_computed_columns: false,
|
include_computed_columns: false,
|
||||||
|
include_database_name: true,
|
||||||
insert_mode: DataGridCopyInsertMode::Merged,
|
insert_mode: DataGridCopyInsertMode::Merged,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -4498,6 +4558,7 @@ mod tests {
|
|||||||
rows: vec![vec![json!("ada"), json!("Ada")]],
|
rows: vec![vec![json!("ada"), json!("Ada")]],
|
||||||
exclude_primary_keys: true,
|
exclude_primary_keys: true,
|
||||||
include_computed_columns: false,
|
include_computed_columns: false,
|
||||||
|
include_database_name: true,
|
||||||
insert_mode: DataGridCopyInsertMode::Merged,
|
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")]],
|
rows: vec![vec![json!(1), json!("ada"), json!("Ada")], vec![json!(2), json!("linus"), json!("Linus")]],
|
||||||
exclude_primary_keys: false,
|
exclude_primary_keys: false,
|
||||||
include_computed_columns: false,
|
include_computed_columns: false,
|
||||||
|
include_database_name: true,
|
||||||
insert_mode: DataGridCopyInsertMode::RowByRow,
|
insert_mode: DataGridCopyInsertMode::RowByRow,
|
||||||
});
|
});
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
@@ -4555,6 +4617,7 @@ mod tests {
|
|||||||
rows: vec![vec![json!(1), json!("Ada")], vec![json!(2), json!("Linus")]],
|
rows: vec![vec![json!(1), json!("Ada")], vec![json!(2), json!("Linus")]],
|
||||||
exclude_primary_keys: false,
|
exclude_primary_keys: false,
|
||||||
include_computed_columns: false,
|
include_computed_columns: false,
|
||||||
|
include_database_name: true,
|
||||||
insert_mode: DataGridCopyInsertMode::Merged,
|
insert_mode: DataGridCopyInsertMode::Merged,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -4587,6 +4650,7 @@ mod tests {
|
|||||||
rows: rows.clone(),
|
rows: rows.clone(),
|
||||||
exclude_primary_keys: false,
|
exclude_primary_keys: false,
|
||||||
include_computed_columns: false,
|
include_computed_columns: false,
|
||||||
|
include_database_name: true,
|
||||||
insert_mode: DataGridCopyInsertMode::Merged,
|
insert_mode: DataGridCopyInsertMode::Merged,
|
||||||
});
|
});
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
@@ -4654,6 +4718,7 @@ mod tests {
|
|||||||
rows: vec![vec![json!(1), json!("Hello"), json!("'hello':1A")]],
|
rows: vec![vec![json!(1), json!("Hello"), json!("'hello':1A")]],
|
||||||
exclude_primary_keys: false,
|
exclude_primary_keys: false,
|
||||||
include_computed_columns: false,
|
include_computed_columns: false,
|
||||||
|
include_database_name: true,
|
||||||
insert_mode: DataGridCopyInsertMode::Merged,
|
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")]],
|
rows: vec![vec![json!(1), json!("2022-08-25T09:58:43Z"), json!("2022-08-25T09:58:43Z")]],
|
||||||
exclude_primary_keys: false,
|
exclude_primary_keys: false,
|
||||||
include_computed_columns: false,
|
include_computed_columns: false,
|
||||||
|
include_database_name: true,
|
||||||
insert_mode: DataGridCopyInsertMode::Merged,
|
insert_mode: DataGridCopyInsertMode::Merged,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -6697,6 +6763,7 @@ mod tests {
|
|||||||
rows: vec![vec![json!(1), json!("login")]],
|
rows: vec![vec![json!(1), json!("login")]],
|
||||||
exclude_primary_keys: false,
|
exclude_primary_keys: false,
|
||||||
include_computed_columns: false,
|
include_computed_columns: false,
|
||||||
|
include_database_name: true,
|
||||||
insert_mode: DataGridCopyInsertMode::Merged,
|
insert_mode: DataGridCopyInsertMode::Merged,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -8571,6 +8638,7 @@ mod tests {
|
|||||||
rows: vec![vec![json!(2), json!("new")]],
|
rows: vec![vec![json!(2), json!("new")]],
|
||||||
exclude_primary_keys: false,
|
exclude_primary_keys: false,
|
||||||
include_computed_columns: false,
|
include_computed_columns: false,
|
||||||
|
include_database_name: true,
|
||||||
insert_mode: DataGridCopyInsertMode::Merged,
|
insert_mode: DataGridCopyInsertMode::Merged,
|
||||||
});
|
});
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
|
|||||||
Reference in new issue
Block a user