fix(export): handle table export edge cases
This commit is contained in:
parent
0611ed8e52
commit
2183c370dc
|
|
@ -74,7 +74,7 @@ import { copyToClipboard } from "@/lib/clipboard";
|
|||
import { formatSqlInsert } from "@/lib/exportFormats";
|
||||
import { fetchTableDataForExport } from "@/lib/tableDataExport";
|
||||
import { useConnectionStore } from "@/stores/connectionStore";
|
||||
import { useExportTracker } from "@/composables/useExportTracker";
|
||||
import { useExportTracker, type ExportTask } from "@/composables/useExportTracker";
|
||||
import { useSettingsStore } from "@/stores/settingsStore";
|
||||
import { useQueryStore } from "@/stores/queryStore";
|
||||
import QueryEditor from "@/components/editor/QueryEditor.vue";
|
||||
|
|
@ -861,38 +861,43 @@ async function exportTableData(row: ObjectBrowserRow, format: "csv" | "xlsx") {
|
|||
filePath = `__web_export_${webExportId}.${format}`;
|
||||
}
|
||||
|
||||
// Register task in export tracker (background)
|
||||
const task = addExportTask(row.name, format, filePath);
|
||||
|
||||
// Get columns for neo4j only
|
||||
const queryColumns =
|
||||
props.connection.db_type === "neo4j"
|
||||
? (await api.getColumns(props.connection.id, props.database, schema || props.database, row.name)).map(
|
||||
(column) => column.name,
|
||||
)
|
||||
: undefined;
|
||||
|
||||
const request: api.TableExportRequest = {
|
||||
exportId: task.exportId,
|
||||
connectionId: props.connection.id,
|
||||
database: props.database,
|
||||
schema,
|
||||
tableName: row.name,
|
||||
filePath,
|
||||
format,
|
||||
columns: queryColumns,
|
||||
batchSize: settingsStore.editorSettings.exportBatchSize,
|
||||
};
|
||||
|
||||
let task: ExportTask | null = null;
|
||||
try {
|
||||
await api.startTableExport(request, (progress) => {
|
||||
task.rowsExported = progress.rowsExported;
|
||||
task.totalRows = progress.totalRows;
|
||||
task.status = progress.status;
|
||||
task.errorMessage = progress.errorMessage || null;
|
||||
const queryColumns =
|
||||
props.connection.db_type === "neo4j"
|
||||
? (await api.getColumns(props.connection.id, props.database, schema || props.database, row.name)).map(
|
||||
(column) => column.name,
|
||||
)
|
||||
: undefined;
|
||||
|
||||
task = addExportTask(row.name, format, filePath);
|
||||
const currentTask = task;
|
||||
const request: api.TableExportRequest = {
|
||||
exportId: currentTask.exportId,
|
||||
connectionId: props.connection.id,
|
||||
database: props.database,
|
||||
schema,
|
||||
tableName: row.name,
|
||||
filePath,
|
||||
format,
|
||||
columns: queryColumns,
|
||||
batchSize: settingsStore.editorSettings.exportBatchSize,
|
||||
};
|
||||
|
||||
const terminalProgress = await api.startTableExport(request, (progress) => {
|
||||
currentTask.rowsExported = progress.rowsExported;
|
||||
currentTask.totalRows = progress.totalRows;
|
||||
currentTask.status = progress.status;
|
||||
currentTask.errorMessage = progress.errorMessage || null;
|
||||
});
|
||||
toast(t("grid.exported"));
|
||||
if (terminalProgress.status === "Done") {
|
||||
toast(t("grid.exported"));
|
||||
}
|
||||
} catch (e: any) {
|
||||
if (task) {
|
||||
task.status = "Error";
|
||||
task.errorMessage = e?.message || String(e);
|
||||
}
|
||||
toast(t("grid.exportFailed", { message: e?.message || String(e) }), 5000);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -126,7 +126,7 @@ import {
|
|||
} from "@/lib/sidebarTreeSelection";
|
||||
import DangerConfirmDialog from "@/components/editor/DangerConfirmDialog.vue";
|
||||
import ProcedureExecutionDialog from "@/components/objects/ProcedureExecutionDialog.vue";
|
||||
import { useExportTracker } from "@/composables/useExportTracker";
|
||||
import { useExportTracker, type ExportTask } from "@/composables/useExportTracker";
|
||||
import { isTauriRuntime } from "@/lib/tauriRuntime";
|
||||
import { copyToClipboard } from "@/lib/clipboard";
|
||||
import { formatShortcut } from "@/lib/shortcutRegistry";
|
||||
|
|
@ -2122,6 +2122,7 @@ async function exportTableData(format: "csv" | "xlsx") {
|
|||
const config = connectionStore.getConfig(node.connectionId);
|
||||
if (!config) return;
|
||||
|
||||
let task: ExportTask | null = null;
|
||||
try {
|
||||
await connectionStore.ensureConnected(connectionId);
|
||||
|
||||
|
|
@ -2138,7 +2139,8 @@ async function exportTableData(format: "csv" | "xlsx") {
|
|||
}
|
||||
|
||||
// Step 2: Register task in export tracker (background)
|
||||
const task = addExportTask(node.label, format, outputPath);
|
||||
task = addExportTask(node.label, format, outputPath);
|
||||
const currentTask = task;
|
||||
|
||||
// Step 3: Get query columns for neo4j
|
||||
const queryColumns =
|
||||
|
|
@ -2148,7 +2150,7 @@ async function exportTableData(format: "csv" | "xlsx") {
|
|||
|
||||
// Step 4: Start streaming export (background, non-blocking)
|
||||
const request: api.TableExportRequest = {
|
||||
exportId: task.exportId,
|
||||
exportId: currentTask.exportId,
|
||||
connectionId,
|
||||
database,
|
||||
schema: node.schema || undefined,
|
||||
|
|
@ -2160,10 +2162,10 @@ async function exportTableData(format: "csv" | "xlsx") {
|
|||
};
|
||||
|
||||
await api.startTableExport(request, (progress) => {
|
||||
task.rowsExported = progress.rowsExported;
|
||||
task.totalRows = progress.totalRows;
|
||||
task.status = progress.status;
|
||||
task.errorMessage = progress.errorMessage || null;
|
||||
currentTask.rowsExported = progress.rowsExported;
|
||||
currentTask.totalRows = progress.totalRows;
|
||||
currentTask.status = progress.status;
|
||||
currentTask.errorMessage = progress.errorMessage || null;
|
||||
if (progress.status === "Done") {
|
||||
toast(t("grid.exported"));
|
||||
} else if (progress.status === "Error") {
|
||||
|
|
@ -2171,6 +2173,10 @@ async function exportTableData(format: "csv" | "xlsx") {
|
|||
}
|
||||
});
|
||||
} catch (e: any) {
|
||||
if (task) {
|
||||
task.status = "Error";
|
||||
task.errorMessage = e?.message || String(e);
|
||||
}
|
||||
toast(t("grid.exportFailed", { message: e?.message || String(e) }), 5000);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1267,21 +1267,13 @@ pub fn keyset_pagination_sql(
|
|||
let order =
|
||||
primary_keys.iter().map(|pk| format!("{} ASC", quote_identifier(pk, db_type))).collect::<Vec<_>>().join(", ");
|
||||
|
||||
let where_clause = if primary_keys.is_empty() || last_pk_values.is_empty() {
|
||||
String::new()
|
||||
} else {
|
||||
let pk_list = primary_keys.iter().map(|pk| quote_identifier(pk, db_type)).collect::<Vec<_>>().join(", ");
|
||||
let vals = last_pk_values.iter().map(|v| value_to_sql_literal(v, db_type)).collect::<Vec<_>>().join(", ");
|
||||
if primary_keys.len() == 1 {
|
||||
format!(" WHERE {} > {}", pk_list, vals)
|
||||
} else {
|
||||
format!(" WHERE ({}) > ({})", pk_list, vals)
|
||||
}
|
||||
};
|
||||
let where_clause = keyset_where_clause(primary_keys, last_pk_values, db_type);
|
||||
|
||||
match db_type {
|
||||
DatabaseType::SqlServer | DatabaseType::Oracle => {
|
||||
format!("SELECT {col_list} FROM {full_table}{where_clause} ORDER BY {order} FETCH NEXT {limit} ROWS ONLY")
|
||||
format!(
|
||||
"SELECT {col_list} FROM {full_table}{where_clause} ORDER BY {order} OFFSET 0 ROWS FETCH NEXT {limit} ROWS ONLY"
|
||||
)
|
||||
}
|
||||
_ => {
|
||||
format!("SELECT {col_list} FROM {full_table}{where_clause} ORDER BY {order} LIMIT {limit}")
|
||||
|
|
@ -1289,7 +1281,44 @@ pub fn keyset_pagination_sql(
|
|||
}
|
||||
}
|
||||
|
||||
fn value_to_sql_literal(value: &serde_json::Value, db_type: &DatabaseType) -> String {
|
||||
fn keyset_where_clause(
|
||||
primary_keys: &[String],
|
||||
last_pk_values: &[serde_json::Value],
|
||||
db_type: &DatabaseType,
|
||||
) -> String {
|
||||
if primary_keys.is_empty() || last_pk_values.is_empty() {
|
||||
return String::new();
|
||||
}
|
||||
|
||||
let quoted_keys = primary_keys.iter().map(|pk| quote_identifier(pk, db_type)).collect::<Vec<_>>();
|
||||
let literals = last_pk_values.iter().map(|v| value_to_sql_literal(v, db_type)).collect::<Vec<_>>();
|
||||
let comparison_count = quoted_keys.len().min(literals.len());
|
||||
if comparison_count == 0 {
|
||||
return String::new();
|
||||
}
|
||||
|
||||
let mut clauses = Vec::with_capacity(comparison_count);
|
||||
for index in 0..comparison_count {
|
||||
let mut parts = Vec::with_capacity(index + 1);
|
||||
for prefix_index in 0..index {
|
||||
parts.push(format!("{} = {}", quoted_keys[prefix_index], literals[prefix_index]));
|
||||
}
|
||||
parts.push(format!("{} > {}", quoted_keys[index], literals[index]));
|
||||
if parts.len() == 1 {
|
||||
clauses.push(parts.remove(0));
|
||||
} else {
|
||||
clauses.push(format!("({})", parts.join(" AND ")));
|
||||
}
|
||||
}
|
||||
|
||||
if clauses.len() == 1 {
|
||||
format!(" WHERE {}", clauses[0])
|
||||
} else {
|
||||
format!(" WHERE ({})", clauses.join(" OR "))
|
||||
}
|
||||
}
|
||||
|
||||
fn value_to_sql_literal(value: &serde_json::Value, _db_type: &DatabaseType) -> String {
|
||||
match value {
|
||||
serde_json::Value::Null => "NULL".to_string(),
|
||||
serde_json::Value::Bool(b) => {
|
||||
|
|
@ -2798,6 +2827,42 @@ mod tests {
|
|||
assert_eq!(sql, "SELECT \"id\", \"name\" FROM \"public\".\"users\" ORDER BY \"id\" LIMIT 100 OFFSET 200");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sqlserver_keyset_pagination_includes_offset_fetch() {
|
||||
let sql = keyset_pagination_sql(
|
||||
&[String::from("id"), String::from("name")],
|
||||
"users",
|
||||
"dbo",
|
||||
&DatabaseType::SqlServer,
|
||||
&[String::from("id")],
|
||||
&[],
|
||||
100,
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
sql,
|
||||
"SELECT [id], [name] FROM [dbo].[users] ORDER BY [id] ASC OFFSET 0 ROWS FETCH NEXT 100 ROWS ONLY"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn composite_keyset_pagination_uses_portable_lexicographic_predicate() {
|
||||
let sql = keyset_pagination_sql(
|
||||
&[String::from("tenant_id"), String::from("id"), String::from("name")],
|
||||
"users",
|
||||
"dbo",
|
||||
&DatabaseType::SqlServer,
|
||||
&[String::from("tenant_id"), String::from("id")],
|
||||
&[json!(10), json!(25)],
|
||||
100,
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
sql,
|
||||
"SELECT [tenant_id], [id], [name] FROM [dbo].[users] WHERE ([tenant_id] > 10 OR ([tenant_id] = 10 AND [id] > 25)) ORDER BY [tenant_id] ASC, [id] ASC OFFSET 0 ROWS FETCH NEXT 100 ROWS ONLY"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn postgres_generates_index_and_foreign_key_sql() {
|
||||
let indexes = vec![db::IndexInfo {
|
||||
|
|
|
|||
Loading…
Reference in New Issue