fix(sqlserver): wrap identity inserts with SET IDENTITY_INSERT

This commit is contained in:
zipg
2026-09-20 23:49:28 +08:00
committed by GitHub
parent 42980e5163
commit d715b16ba5
20 changed files with 349 additions and 12 deletions
@@ -525,6 +525,7 @@ interface DataGridProps {
includeSqlSheet?: boolean;
exportTableName?: string;
exportColumnTypes?: Array<string | null | undefined>;
exportColumnExtras?: Array<string | null | undefined>;
insertMode?: SqlInsertMode;
}) => Promise<api.QueryResultExportRequest | undefined>;
allExportResults?: Array<{
@@ -2116,8 +2116,16 @@ defineExpose({
:on-execute-sql="async (sql: string) => emit('executeSql', activeTab.id, sql)"
:full-export-result="(onProgress?: (info: { rowsExported: number; totalRows: number | null }) => void) => queryStore.fetchTabResultForExport(activeTab.id, onProgress)"
:query-result-export-request="
(options: { exportId: string; filePath: string; format: 'csv' | 'xlsx' | 'json' | 'txt' | 'sql'; includeSqlSheet?: boolean; exportTableName?: string; exportColumnTypes?: Array<string | null | undefined>; insertMode?: SqlInsertMode }) =>
queryStore.buildQueryResultExportRequest(activeTab.id, options)
(options: {
exportId: string;
filePath: string;
format: 'csv' | 'xlsx' | 'json' | 'txt' | 'sql';
includeSqlSheet?: boolean;
exportTableName?: string;
exportColumnTypes?: Array<string | null | undefined>;
exportColumnExtras?: Array<string | null | undefined>;
insertMode?: SqlInsertMode;
}) => queryStore.buildQueryResultExportRequest(activeTab.id, options)
"
:all-export-results="allResultExportSheets"
:export-file-base-name="activeTab.title"
@@ -140,6 +140,25 @@ describe("query result SQL export progress", () => {
expect(api.startQueryResultExport).toHaveBeenCalledWith(expect.objectContaining({ format: "sql", exportTableName: "users", exportColumnTypes: ["int4", "text"] }), expect.any(Function));
});
it("forwards result column EXTRA metadata so identity INSERT exports can be replayed", async () => {
const state = useDataGridExport(
createOptions({
tableMeta: computed(() => ({
tableName: "users",
primaryKeys: ["id"],
columns: [
{ name: "id", data_type: "int", is_nullable: false, is_primary_key: true, extra: "identity(1,1)" },
{ name: "name", data_type: "text", is_nullable: true },
],
})),
}),
);
await state.exportSql();
expect(api.startQueryResultExport).toHaveBeenCalledWith(expect.objectContaining({ format: "sql", exportColumnTypes: ["int4", "text"], exportColumnExtras: ["identity(1,1)", null] }), expect.any(Function));
});
it("routes incomplete query-result JSON exports through the streaming backend", async () => {
const state = useDataGridExport(createOptions());
@@ -123,6 +123,7 @@ export interface UseDataGridExportOptions {
includeSqlSheet?: boolean;
exportTableName?: string;
exportColumnTypes?: Array<string | null | undefined>;
exportColumnExtras?: Array<string | null | undefined>;
insertMode?: SqlInsertMode;
}) => Promise<QueryResultExportRequest | undefined>;
/**
@@ -1277,6 +1278,18 @@ export function useDataGridExport(options: UseDataGridExportOptions) {
return autoGenerated.length > 0 ? autoGenerated : undefined;
}
/**
* 导出列对应的原表 EXTRA 元数据(SQL Server/Dameng 的 identity 等)。后端据此
* 给导出的 INSERT 包上 `SET IDENTITY_INSERT`,否则回放时报 SQL Server 544。
* 拿不到表元数据时返回 undefined,后端保持“未知”语义,不额外查一次元数据。
*/
function sqlExportColumnExtras(columnNames: string[]): Array<string | null> | undefined {
const metaColumns = tableMeta.value?.columns;
if (!metaColumns?.length) return undefined;
const extras = columnNames.map((column) => metaColumns.find((meta) => normalizeColumnName(meta.name) === normalizeColumnName(column))?.extra ?? null);
return extras.some((extra) => !!extra) ? extras : undefined;
}
/** 供导出请求使用:把“不含主键”设置转换成后端请求字段。 */
function sqlExportPrimaryKeyOptions(): { excludePrimaryKeys?: boolean; primaryKeys?: string[] } {
const excludeColumns = sqlExportExcludedColumns();
@@ -1347,6 +1360,7 @@ export function useDataGridExport(options: UseDataGridExportOptions) {
csvQuoteMode: editorSettings.csvQuoteMode,
columns: columns.value,
columnTypes: columnTypes.value,
...(format === "sql" ? { columnExtras: sqlExportColumnExtras(columns.value) } : {}),
columnComments: format === "xlsx" ? buildXlsxHeaderOverrides(columns.value, visibleXlsxColumnComments.value, headerMode) : undefined,
primaryKeys: meta.primaryKeys,
...sqlExportPrimaryKeyOptions(),
@@ -1416,6 +1430,7 @@ export function useDataGridExport(options: UseDataGridExportOptions) {
includeSqlSheet,
exportTableName: format === "sql" ? tableMeta.value?.tableName : undefined,
exportColumnTypes: format === "sql" ? allColumnTypes.value?.map((type) => type ?? null) : undefined,
exportColumnExtras: format === "sql" ? sqlExportColumnExtras(allColumns.value) : undefined,
...(format === "sql" && insertMode ? { insertMode } : {}),
});
const columnComments = format === "xlsx" ? buildXlsxHeaderOverrides(allColumns.value, allXlsxColumnComments.value, headerMode) : undefined;
@@ -1586,6 +1601,7 @@ export function useDataGridExport(options: UseDataGridExportOptions) {
tableName: tableMeta.value?.tableName || "table_name",
columns: exportData.columns,
columnTypes: exportData.columnTypes,
columnExtras: exportData.columnExtras,
spatialColumns: exportData.spatialColumns,
spatialValues: exportData.spatialValues,
rows: exportData.rows,
@@ -1628,6 +1644,7 @@ export function useDataGridExport(options: UseDataGridExportOptions) {
tableName: tableMeta.value?.tableName || "table_name",
columns: exportData.columns,
columnTypes: exportData.columnTypes,
columnExtras: exportData.columnExtras,
spatialColumns: exportData.spatialColumns,
spatialValues: exportData.spatialValues,
rows: exportData.rows,
@@ -1650,6 +1667,7 @@ export function useDataGridExport(options: UseDataGridExportOptions) {
function sqlInsertExportData(result: { columns: string[]; rows: CellValue[][]; spatialColumns?: QueryResult["spatial_columns"]; spatialValues?: QueryResult["spatial_values"] }): {
columns: string[];
columnTypes?: Array<string | undefined>;
columnExtras?: Array<string | null>;
spatialColumns?: QueryResult["spatial_columns"];
spatialValues?: QueryResult["spatial_values"];
rows: CellValue[][];
@@ -1657,6 +1675,8 @@ export function useDataGridExport(options: UseDataGridExportOptions) {
const exportColumns = context.value === "table-data" && tableMeta.value ? effectiveColumns(sourceColumns.value, result.columns) : result.columns;
const columnIndexes = exportColumns.map((column, index) => ({ column, index })).filter((item): item is { column: string; index: number } => !!item.column);
const exportColumnTypes = columnTypes.value?.length === result.columns.length ? columnTypes.value : undefined;
const metaColumns = tableMeta.value?.columns;
const exportColumnExtras = metaColumns?.length ? exportColumns.map((column) => (column ? (metaColumns.find((meta) => normalizeColumnName(meta.name) === normalizeColumnName(column))?.extra ?? null) : null)) : undefined;
const indexBySource = new Map(columnIndexes.map((item, index) => [item.index, index]));
const spatialColumns = result.spatialColumns?.flatMap((column) => {
const columnIndex = indexBySource.get(column.column_index);
@@ -1665,6 +1685,7 @@ export function useDataGridExport(options: UseDataGridExportOptions) {
return {
columns: columnIndexes.map((item) => item.column),
columnTypes: exportColumnTypes ? columnIndexes.map((item) => exportColumnTypes[item.index]) : undefined,
columnExtras: exportColumnExtras?.some((extra) => !!extra) ? columnIndexes.map((item) => exportColumnExtras[item.index] ?? null) : undefined,
...(spatialColumns?.length ? { spatialColumns } : {}),
...(result.spatialValues?.length ? { spatialValues: result.spatialValues.map((row) => columnIndexes.map((item) => row[item.index] ?? null)) } : {}),
rows: result.rows.map((row) => columnIndexes.map((item) => row[item.index] ?? null)),
+7
View File
@@ -5469,6 +5469,8 @@ export interface TableExportRequest {
csvQuoteMode?: CsvQuoteMode;
columns?: string[];
columnTypes?: Array<string | null | undefined>;
/** 与 `columns` 对齐的列 EXTRA 元数据(identity 等),用于 SQL INSERT 导出的 `SET IDENTITY_INSERT`。 */
columnExtras?: Array<string | null | undefined>;
columnComments?: Array<string | null> | null;
primaryKeys?: string[];
/** 导出 SQL 时是否排除主键列(对应数据提取设置里的“排除主键”)。 */
@@ -5531,6 +5533,11 @@ export interface QueryResultExportRequest {
dateTimeFormat?: string;
exportTableName?: string;
exportColumnTypes?: Array<string | null | undefined>;
/**
* 结果列对应的原表 EXTRA 元数据(identity 等)。后端据此为 SQL INSERT 导出
* 补上 `SET IDENTITY_INSERT` 包裹,缺省表示未知。
*/
exportColumnExtras?: Array<string | null | undefined>;
numericColumnRightAlign?: boolean;
columnComments?: Array<string | null> | null;
autoFilter?: boolean;
@@ -36,6 +36,8 @@ export interface FormatSqlInsertOptions {
qualifiedTableName?: string;
columns: string[];
columnTypes?: Array<string | null | undefined>;
/** 与 `columns` 对齐的列 EXTRA 元数据,用于 identity 列的 `SET IDENTITY_INSERT` 包裹。 */
columnExtras?: Array<string | null | undefined>;
spatialColumns?: QueryResult["spatial_columns"];
spatialValues?: QueryResult["spatial_values"];
rows: ExportCellValue[][];
+2
View File
@@ -168,6 +168,7 @@ interface BuildQueryResultExportRequestOptions {
includeSqlSheet?: boolean;
exportTableName?: string;
exportColumnTypes?: Array<string | null | undefined>;
exportColumnExtras?: Array<string | null | undefined>;
insertMode?: SqlInsertMode;
}
@@ -8768,6 +8769,7 @@ export const useQueryStore = defineStore("query", () => {
executionId: uuid(),
exportTableName: options.exportTableName,
exportColumnTypes: options.exportColumnTypes,
exportColumnExtras: options.exportColumnExtras,
numericColumnRightAlign: settings.numericColumnRightAlign,
identifierQuote: connStore.connectionIdentifierQuote(location.connectionId),
};
+100 -1
View File
@@ -112,6 +112,13 @@ pub struct QueryResultExportRequest {
/// Frontend sends these in original full-query column order.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub export_column_types: Option<Vec<Option<String>>>,
/// Column EXTRA metadata (for example SQL Server/Dameng `identity`) for SQL
/// INSERT export, sent by the frontend from the result's table metadata.
/// Without it the exported INSERT writes identity values without
/// `SET IDENTITY_INSERT` and fails with SQL Server error 544 on replay.
/// Entries align with `export_column_types` (original full-query order).
#[serde(default, skip_serializing_if = "Option::is_none")]
pub export_column_extras: Option<Vec<Option<String>>>,
#[serde(default)]
pub numeric_column_right_align: bool,
#[serde(default, skip_serializing_if = "Option::is_none")]
@@ -324,6 +331,7 @@ struct SqlInsertWriter {
insert_mode: SqlInsertMode,
columns: Vec<String>,
column_types: Vec<Option<String>>,
column_extras: Vec<Option<String>>,
spatial_columns: Vec<SpatialColumn>,
database_type: DatabaseType,
schema: Option<String>,
@@ -412,6 +420,7 @@ impl SqlInsertWriter {
insert_mode: request.insert_mode,
columns: Vec::new(),
column_types: Vec::new(),
column_extras: Vec::new(),
spatial_columns: Vec::new(),
database_type: request.database_type,
schema: request.schema.clone(),
@@ -432,6 +441,9 @@ impl SqlInsertWriter {
request: &QueryResultExportRequest,
) {
self.column_types = sql_insert_column_types(request, result_column_types);
// Column extras come from the request and align with the result columns the
// same way `export_column_types` does; missing entries simply mean "unknown".
self.column_extras = request.export_column_extras.clone().unwrap_or_default();
self.spatial_columns = spatial_columns.to_vec();
self.columns = columns;
}
@@ -458,7 +470,7 @@ impl SqlInsertWriter {
qualified_table_name: None,
columns: self.columns.clone(),
column_types: self.column_types.clone(),
column_extras: Vec::new(),
column_extras: self.column_extras.clone(),
spatial_columns: self.spatial_columns.clone(),
spatial_values: mem::take(&mut self.pending_spatial_values),
rows: mem::take(&mut self.pending_rows),
@@ -2091,6 +2103,91 @@ mod tests {
assert!(!stream_export_was_cancelled("network failure", false, false));
}
fn sqlserver_identity_export_request(
file_path: &std::path::Path,
export_column_extras: Option<Vec<Option<String>>>,
) -> QueryResultExportRequest {
QueryResultExportRequest {
export_id: "export-identity".to_string(),
connection_id: "conn-1".to_string(),
database: "dbx_test".to_string(),
schema: Some("dbo".to_string()),
catalog: None,
sql: "SELECT * FROM [dbo].[gen_table]".to_string(),
query_base_sql: "SELECT * FROM [dbo].[gen_table]".to_string(),
setup_sql: Vec::new(),
database_type: DatabaseType::SqlServer,
use_agent_cursor: false,
file_path: file_path.to_string_lossy().to_string(),
format: "sql".to_string(),
include_sql_sheet: false,
page_size: 1000,
row_limit: None,
total_rows: None,
timeout_secs: None,
keyset_optimization_enabled: false,
client_session_id: None,
execution_id: None,
date_time_format: None,
export_table_name: Some("gen_table".to_string()),
export_column_types: Some(vec![Some("int".to_string()), Some("nvarchar(200)".to_string())]),
export_column_extras,
numeric_column_right_align: false,
column_comments: None,
auto_filter: None,
identifier_quote: None,
insert_mode: SqlInsertMode::Batch,
csv_quote_mode: Default::default(),
exclude_primary_keys: false,
primary_keys: Vec::new(),
}
}
#[test]
fn sql_insert_writer_wraps_sqlserver_identity_columns_with_identity_insert() {
let dir = tempfile::tempdir().expect("temp dir");
let file_path = dir.path().join("gen_table.sql");
let request =
sqlserver_identity_export_request(&file_path, Some(vec![Some("identity(1,1)".to_string()), None]));
let mut writer = SqlInsertWriter::create(&request).expect("create sql insert writer");
writer.set_columns(
vec!["table_id".to_string(), "table_name".to_string()],
&["int".to_string(), "nvarchar(200)".to_string()],
&[],
&request,
);
writer.write_row(vec![json!(23), json!("t_destype")], None).expect("write export row");
writer.finish().expect("finish export");
assert_eq!(
std::fs::read_to_string(&file_path).expect("read sql export"),
"SET IDENTITY_INSERT [dbo].[gen_table] ON;\nINSERT INTO [dbo].[gen_table] ([table_id], [table_name]) VALUES (23, N't_destype');\nSET IDENTITY_INSERT [dbo].[gen_table] OFF;\n"
);
}
#[test]
fn sql_insert_writer_skips_identity_wrapper_without_column_extras() {
let dir = tempfile::tempdir().expect("temp dir");
let file_path = dir.path().join("gen_table.sql");
let request = sqlserver_identity_export_request(&file_path, None);
let mut writer = SqlInsertWriter::create(&request).expect("create sql insert writer");
writer.set_columns(
vec!["table_id".to_string(), "table_name".to_string()],
&["int".to_string(), "nvarchar(200)".to_string()],
&[],
&request,
);
writer.write_row(vec![json!(23), json!("t_destype")], None).expect("write export row");
writer.finish().expect("finish export");
assert_eq!(
std::fs::read_to_string(&file_path).expect("read sql export"),
"INSERT INTO [dbo].[gen_table] ([table_id], [table_name]) VALUES (23, N't_destype');\n"
);
}
#[test]
fn sql_insert_writer_omits_excluded_primary_key_columns() {
let dir = tempfile::tempdir().expect("temp dir");
@@ -2119,6 +2216,7 @@ mod tests {
date_time_format: None,
export_table_name: Some("users".to_string()),
export_column_types: None,
export_column_extras: None,
numeric_column_right_align: false,
column_comments: None,
auto_filter: None,
@@ -2201,6 +2299,7 @@ mod tests {
csv_quote_mode: CsvQuoteMode::All,
export_table_name: None,
export_column_types: None,
export_column_extras: None,
numeric_column_right_align: false,
column_comments: None,
auto_filter: None,
+64 -1
View File
@@ -89,6 +89,12 @@ pub struct TableExportRequest {
pub columns: Option<Vec<String>>,
#[serde(default)]
pub column_types: Option<Vec<Option<String>>>,
/// Column EXTRA metadata for `columns`, supplied by the data grid. SQL
/// INSERT exports need it for dialect rules such as SQL Server/Dameng
/// `SET IDENTITY_INSERT` (error 544) and MySQL generated-column skipping;
/// without it the export cannot know those rules without a metadata query.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub column_extras: Option<Vec<Option<String>>>,
#[serde(default)]
pub primary_keys: Option<Vec<String>>,
/// 导出 SQL 时是否排除主键列(对应前端数据提取设置里的“排除主键”)。
@@ -229,6 +235,24 @@ fn requested_mysql_sql_export_needs_column_metadata(database_type: DatabaseType,
database_type == DatabaseType::Mysql && format.eq_ignore_ascii_case("sql")
}
/// Column EXTRA values supplied by the data grid, filtered and aligned exactly
/// like [`resolve_requested_export_columns`] keeps name/type metadata aligned.
fn resolve_requested_column_extras_by_position(
database_type: DatabaseType,
columns: &[String],
column_extras: Option<&[Option<String>]>,
) -> Vec<Option<String>> {
let Some(column_extras) = column_extras else {
return Vec::new();
};
columns
.iter()
.enumerate()
.filter(|(_, column)| !is_internal_export_column(Some(database_type), column))
.map(|(index, _)| column_extras.get(index).cloned().flatten())
.collect()
}
fn resolve_requested_export_column_types(
requested_columns: &[String],
requested_column_types: &[Option<String>],
@@ -1540,7 +1564,14 @@ async fn export_table_data_core_inner(
resolve_requested_export_column_extras(&col_names, &table_columns),
)
} else {
(requested_column_types, Vec::new())
(
requested_column_types,
resolve_requested_column_extras_by_position(
db_type,
requested_columns,
request.column_extras.as_deref(),
),
)
};
(col_names, column_types, column_extras, primary_keys)
} else {
@@ -2386,6 +2417,7 @@ mod tests {
insert_mode: Default::default(),
columns: Some(vec!["id".to_string(), "name".to_string()]),
column_types: Some(vec![Some("INTEGER".to_string()), Some("VARCHAR".to_string())]),
column_extras: None,
primary_keys: Some(vec!["id".to_string()]),
exclude_primary_keys: false,
where_input: None,
@@ -2552,6 +2584,7 @@ mod tests {
exclude_primary_keys: false,
columns: None,
column_types: None,
column_extras: None,
primary_keys: None,
where_input: None,
order_by: None,
@@ -2717,6 +2750,7 @@ mod tests {
insert_mode: Default::default(),
columns: None,
column_types: None,
column_extras: None,
primary_keys: None,
exclude_primary_keys: false,
where_input: Some("WHERE temperature > 1".to_string()),
@@ -2777,6 +2811,7 @@ mod tests {
insert_mode: Default::default(),
columns: None,
column_types: None,
column_extras: None,
primary_keys: None,
exclude_primary_keys: false,
where_input: None,
@@ -2815,6 +2850,7 @@ mod tests {
insert_mode: Default::default(),
columns: None,
column_types: None,
column_extras: None,
primary_keys: None,
exclude_primary_keys: false,
where_input: None,
@@ -2848,6 +2884,7 @@ mod tests {
insert_mode: Default::default(),
columns: None,
column_types: None,
column_extras: None,
primary_keys: None,
exclude_primary_keys: false,
where_input: None,
@@ -2889,6 +2926,7 @@ mod tests {
insert_mode: Default::default(),
columns: None,
column_types: None,
column_extras: None,
primary_keys: None,
exclude_primary_keys: false,
where_input: None,
@@ -2942,6 +2980,7 @@ mod tests {
insert_mode: Default::default(),
columns: None,
column_types: None,
column_extras: None,
primary_keys: None,
exclude_primary_keys: false,
where_input: Some("WHERE status = 'active'".to_string()),
@@ -2989,6 +3028,7 @@ mod tests {
insert_mode: Default::default(),
columns: None,
column_types: None,
column_extras: None,
primary_keys: None,
exclude_primary_keys: false,
where_input: None,
@@ -3064,6 +3104,7 @@ mod tests {
insert_mode: Default::default(),
columns: None,
column_types: None,
column_extras: None,
primary_keys: None,
exclude_primary_keys: false,
where_input: None,
@@ -3121,6 +3162,7 @@ mod tests {
insert_mode: Default::default(),
columns: None,
column_types: None,
column_extras: None,
primary_keys: None,
exclude_primary_keys: false,
where_input: None,
@@ -3180,6 +3222,7 @@ mod tests {
insert_mode: Default::default(),
columns: None,
column_types: None,
column_extras: None,
primary_keys: None,
exclude_primary_keys: false,
where_input: None,
@@ -3265,6 +3308,26 @@ mod tests {
);
}
#[test]
fn requested_column_extras_stay_aligned_with_filtered_columns() {
let columns = vec!["table_id".to_string(), "table_name".to_string()];
let extras = vec![Some("identity(1,1)".to_string()), None];
assert_eq!(
resolve_requested_column_extras_by_position(DatabaseType::SqlServer, &columns, Some(&extras)),
vec![Some("identity(1,1)".to_string()), None]
);
assert!(resolve_requested_column_extras_by_position(DatabaseType::SqlServer, &columns, None).is_empty());
// The synthetic ROWID column never reaches the export, so its EXTRA entry
// must be dropped with it instead of shifting the remaining columns.
let oracle_columns = vec!["__DBX_ROWID".to_string(), "id".to_string()];
let oracle_extras = vec![Some("synthetic".to_string()), Some("identity".to_string())];
assert_eq!(
resolve_requested_column_extras_by_position(DatabaseType::Oracle, &oracle_columns, Some(&oracle_extras)),
vec![Some("identity".to_string())]
);
}
#[test]
fn agent_table_read_unsupported_detects_old_agent_errors() {
assert!(is_agent_table_read_unsupported("Agent RPC error (-1): unknown method: start_table_read"));
@@ -105,6 +105,7 @@ async fn live_clickhouse_query_result_export_xlsx_streams_random_order_query_onc
csv_quote_mode: Default::default(),
export_table_name: None,
export_column_types: None,
export_column_extras: None,
column_comments: None,
auto_filter: None,
identifier_quote: None,
+3
View File
@@ -480,6 +480,7 @@ async fn live_mysql_query_result_export_xlsx_streams_single_query_without_duplic
csv_quote_mode: Default::default(),
export_table_name: None,
export_column_types: None,
export_column_extras: None,
column_comments: None,
auto_filter: None,
identifier_quote: None,
@@ -573,6 +574,7 @@ async fn live_mysql_csv_temporal_export_round_trip_preserves_dbx_force_text_valu
csv_quote_mode: Default::default(),
export_table_name: None,
export_column_types: None,
export_column_extras: None,
column_comments: None,
auto_filter: None,
identifier_quote: None,
@@ -690,6 +692,7 @@ async fn live_mysql_xlsx_export_can_outlive_query_timeout_while_rows_keep_arrivi
csv_quote_mode: Default::default(),
export_table_name: None,
export_column_types: None,
export_column_extras: None,
column_comments: None,
auto_filter: None,
identifier_quote: None,
@@ -152,6 +152,7 @@ async fn live_postgres_query_result_export_uses_single_streamed_query() {
csv_quote_mode: Default::default(),
export_table_name: None,
export_column_types: None,
export_column_extras: None,
column_comments: None,
auto_filter: None,
identifier_quote: None,
@@ -238,6 +239,7 @@ async fn live_postgres_query_result_xlsx_preserves_temporal_cell_types() {
csv_quote_mode: Default::default(),
export_table_name: None,
export_column_types: None,
export_column_extras: None,
column_comments: None,
auto_filter: None,
identifier_quote: None,
@@ -313,6 +315,7 @@ async fn live_postgres_numeric_xlsx_ignores_fractional_trailing_zeros() {
csv_quote_mode: Default::default(),
export_table_name: None,
export_column_types: None,
export_column_extras: None,
column_comments: None,
auto_filter: None,
identifier_quote: None,
@@ -393,6 +396,7 @@ async fn live_postgres_truncated_batch_result_export_replays_safe_temp_setup() {
csv_quote_mode: Default::default(),
export_table_name: None,
export_column_types: None,
export_column_extras: None,
column_comments: None,
auto_filter: None,
identifier_quote: None,
@@ -475,6 +479,7 @@ async fn live_postgres_xlsx_export_can_outlive_query_timeout_while_rows_keep_arr
csv_quote_mode: Default::default(),
export_table_name: None,
export_column_types: None,
export_column_extras: None,
column_comments: None,
auto_filter: None,
identifier_quote: None,
@@ -548,6 +553,7 @@ async fn live_postgres_stream_still_times_out_without_progress_and_recovers() {
csv_quote_mode: Default::default(),
export_table_name: None,
export_column_types: None,
export_column_extras: None,
column_comments: None,
auto_filter: None,
identifier_quote: None,
@@ -1214,6 +1214,7 @@ async fn live_sqlserver_query_result_export_streams_cte_query_to_csv() {
csv_quote_mode: Default::default(),
export_table_name: None,
export_column_types: None,
export_column_extras: None,
column_comments: None,
auto_filter: None,
identifier_quote: None,
@@ -131,6 +131,7 @@ async fn live_sqlserver_xlsx_export_can_outlive_query_timeout_while_rows_keep_ar
csv_quote_mode: Default::default(),
export_table_name: None,
export_column_types: None,
export_column_extras: None,
column_comments: None,
auto_filter: None,
identifier_quote: None,
+111 -8
View File
@@ -606,22 +606,37 @@ pub fn build_data_grid_copy_insert_statement(options: DataGridCopyInsertStatemen
)
})
.collect::<Vec<_>>();
if options.insert_mode == DataGridCopyInsertMode::RowByRow
let statements = if options.insert_mode == DataGridCopyInsertMode::RowByRow
|| options.database_type.is_some_and(uses_single_row_insert_statements)
{
value_rows.iter().map(|values| format!("INSERT INTO {table} ({columns}) VALUES {values};")).collect::<Vec<_>>()
} else {
vec![format!(
"INSERT INTO {table} ({columns}) VALUES{}{};",
if value_rows.len() == 1 { " " } else { "\n" },
value_rows.join(",\n")
)]
};
// SQL Server and Dameng reject explicit values for identity columns unless the
// statement runs between `SET IDENTITY_INSERT <table> ON` and `OFF` (SQL Server
// error 544), so a copied INSERT that carries the identity column must ship the
// wrapper. Each statement is wrapped on its own so row-by-row copies stay
// individually executable, matching the SQL export path.
let needs_identity_insert_wrapper =
matches!(options.database_type, Some(DatabaseType::SqlServer | DatabaseType::Dameng))
&& insert_columns.iter().any(|(_, _, info)| info.as_ref().is_some_and(is_auto_generated_column));
if needs_identity_insert_wrapper {
return Some(
value_rows
statements
.iter()
.map(|values| format!("INSERT INTO {table} ({columns}) VALUES {values};"))
.map(|statement| {
format!("SET IDENTITY_INSERT {table} ON;\n{statement}\nSET IDENTITY_INSERT {table} OFF;")
})
.collect::<Vec<_>>()
.join("\n"),
);
}
Some(format!(
"INSERT INTO {table} ({columns}) VALUES{}{};",
if value_rows.len() == 1 { " " } else { "\n" },
value_rows.join(",\n")
))
Some(statements.join("\n"))
}
pub fn build_data_grid_context_filter_condition(options: DataGridContextFilterConditionOptions) -> Option<String> {
@@ -4629,6 +4644,94 @@ mod tests {
);
}
fn sqlserver_identity_copy_insert_options(
rows: Vec<Vec<Value>>,
insert_mode: DataGridCopyInsertMode,
) -> DataGridCopyInsertStatementOptions {
DataGridCopyInsertStatementOptions {
database_type: Some(DatabaseType::SqlServer),
identifier_quote: None,
table_meta: Some(DataGridTableMeta {
catalog: None,
database: Some("dbx_test".to_string()),
schema: Some("dbo".to_string()),
table_name: "gen_table".to_string(),
primary_keys: vec!["table_id".to_string()],
columns: Some(vec![
column("table_id", "int", false, Some("identity")),
column("table_name", "nvarchar(200)", false, None),
]),
}),
columns: vec!["table_id".to_string(), "table_name".to_string()],
column_types: None,
source_columns: None,
rows,
exclude_primary_keys: false,
include_computed_columns: false,
include_database_name: true,
insert_mode,
}
}
#[test]
fn sqlserver_copy_insert_wraps_identity_columns_with_identity_insert() {
let statement = build_data_grid_copy_insert_statement(sqlserver_identity_copy_insert_options(
vec![vec![json!(1), json!("t_destype")]],
DataGridCopyInsertMode::Merged,
));
assert_eq!(
statement.as_deref(),
Some(
"SET IDENTITY_INSERT [dbo].[gen_table] ON;\nINSERT INTO [dbo].[gen_table] ([table_id], [table_name]) VALUES (1, N't_destype');\nSET IDENTITY_INSERT [dbo].[gen_table] OFF;"
)
);
}
#[test]
fn sqlserver_row_by_row_copy_insert_wraps_every_statement() {
let statement = build_data_grid_copy_insert_statement(sqlserver_identity_copy_insert_options(
vec![vec![json!(1), json!("t_destype")], vec![json!(2), json!("t_user")]],
DataGridCopyInsertMode::RowByRow,
));
assert_eq!(
statement.as_deref(),
Some(
"SET IDENTITY_INSERT [dbo].[gen_table] ON;\nINSERT INTO [dbo].[gen_table] ([table_id], [table_name]) VALUES (1, N't_destype');\nSET IDENTITY_INSERT [dbo].[gen_table] OFF;\nSET IDENTITY_INSERT [dbo].[gen_table] ON;\nINSERT INTO [dbo].[gen_table] ([table_id], [table_name]) VALUES (2, N't_user');\nSET IDENTITY_INSERT [dbo].[gen_table] OFF;"
)
);
}
#[test]
fn sqlserver_copy_insert_omits_identity_wrapper_without_identity_columns() {
let mut options = sqlserver_identity_copy_insert_options(
vec![vec![json!(1), json!("t_destype")]],
DataGridCopyInsertMode::Merged,
);
options.table_meta.as_mut().expect("table meta").columns =
Some(vec![column("table_id", "int", false, None), column("table_name", "nvarchar(200)", false, None)]);
let statement = build_data_grid_copy_insert_statement(options);
assert_eq!(
statement.as_deref(),
Some("INSERT INTO [dbo].[gen_table] ([table_id], [table_name]) VALUES (1, N't_destype');")
);
}
#[test]
fn dameng_copy_insert_wraps_identity_columns_with_identity_insert() {
let mut options = sqlserver_identity_copy_insert_options(
vec![vec![json!(1), json!("t_destype")]],
DataGridCopyInsertMode::Merged,
);
options.database_type = Some(DatabaseType::Dameng);
let statement = build_data_grid_copy_insert_statement(options);
assert_eq!(
statement.as_deref(),
Some(
"SET IDENTITY_INSERT \"dbo\".\"gen_table\" ON;\nINSERT INTO \"dbo\".\"gen_table\" (\"table_id\", \"table_name\") VALUES (1, 't_destype');\nSET IDENTITY_INSERT \"dbo\".\"gen_table\" OFF;"
)
);
}
#[test]
fn mysql_copy_statements_preserve_blob_hex_literals() {
let table_meta = DataGridTableMeta {
Binary file not shown.

After

Width:  |  Height:  |  Size: 78 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 79 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 80 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 84 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 97 KiB