mirror of
https://github.com/t8y2/dbx.git
synced 2026-10-22 19:58:51 +00:00
fix(data): align table preview ordering
This commit is contained in:
@@ -144,6 +144,7 @@ const { highlight } = useSqlHighlighter();
|
||||
|
||||
type StructureCopyFormat = "tsv" | "markdown";
|
||||
type DuplicateStructureSource = TreeNode & { connectionId: string; database: string };
|
||||
const DATA_TAB_METADATA_TTL_MS = 30_000;
|
||||
const { getDatabaseOptions } = useDatabaseOptions();
|
||||
const showVisibleDatabasesDialog = ref(false);
|
||||
const { addTask: addExportTask } = useExportTracker();
|
||||
@@ -834,7 +835,19 @@ async function openData() {
|
||||
const config = connectionStore.getConfig(node.connectionId);
|
||||
const traceId = uuid().slice(0, 8);
|
||||
const startedAt = performance.now();
|
||||
let lastPhaseAt = startedAt;
|
||||
const elapsed = () => `${Math.round(performance.now() - startedAt)}ms`;
|
||||
const logPhase = (phase: string, extra: Record<string, unknown> = {}) => {
|
||||
const now = performance.now();
|
||||
console.info("[DBX][openData:phase]", {
|
||||
traceId,
|
||||
phase,
|
||||
deltaMs: Math.round(now - lastPhaseAt),
|
||||
totalMs: Math.round(now - startedAt),
|
||||
...extra,
|
||||
});
|
||||
lastPhaseAt = now;
|
||||
};
|
||||
console.info("[DBX][openData:start]", {
|
||||
traceId,
|
||||
type: node.type,
|
||||
@@ -845,6 +858,7 @@ async function openData() {
|
||||
dbType: config?.db_type,
|
||||
});
|
||||
const tableSchema = connectionObjectTreeNodeSchema(config, node.database, node.schema);
|
||||
const tableType = node.type === "view" ? "VIEW" : node.type === "materialized_view" ? "MATERIALIZED_VIEW" : "TABLE";
|
||||
const tabId = (() => {
|
||||
if (settingsStore.editorSettings.reuseDataTab) {
|
||||
const existing = queryStore.tabs.find((tab) => tab.mode === "data" && tab.connectionId === node.connectionId && tab.database === node.database);
|
||||
@@ -858,11 +872,13 @@ async function openData() {
|
||||
return queryStore.createTab(node.connectionId, node.database, node.label, "data", tableSchema);
|
||||
})();
|
||||
console.info("[DBX][openData:tab-created]", { traceId, tabId, elapsed: elapsed() });
|
||||
logPhase("tab-created", { tabId });
|
||||
|
||||
// Cancel any previous execution on this tab before starting a new one
|
||||
const existingTab = queryStore.tabs.find((t) => t.id === tabId);
|
||||
if (existingTab?.isExecuting && existingTab.executionId) {
|
||||
await queryStore.cancelTabExecution(tabId);
|
||||
logPhase("previous-execution-cancelled", { tabId });
|
||||
}
|
||||
|
||||
const openDataId = uuid();
|
||||
@@ -872,70 +888,112 @@ async function openData() {
|
||||
tab.result = undefined;
|
||||
tab.results = undefined;
|
||||
}
|
||||
queryStore.setTableMeta(tabId, {
|
||||
schema: tableSchema,
|
||||
tableName: node.label,
|
||||
tableType: node.type === "view" ? "VIEW" : node.type === "materialized_view" ? "MATERIALIZED_VIEW" : "TABLE",
|
||||
columns: [],
|
||||
primaryKeys: [],
|
||||
});
|
||||
const existingTableMeta = tab?.tableMeta;
|
||||
const existingTableMetaAgeMs = tab?.tableMetaUpdatedAt ? Date.now() - tab.tableMetaUpdatedAt : Number.POSITIVE_INFINITY;
|
||||
const cachedTableMeta = existingTableMeta?.tableName === node.label && existingTableMeta.schema === tableSchema && existingTableMeta.tableType === tableType && existingTableMeta.columns.length > 0 && existingTableMetaAgeMs < DATA_TAB_METADATA_TTL_MS ? existingTableMeta : undefined;
|
||||
queryStore.setTableMeta(
|
||||
tabId,
|
||||
cachedTableMeta ?? {
|
||||
schema: tableSchema,
|
||||
tableName: node.label,
|
||||
tableType,
|
||||
columns: [],
|
||||
primaryKeys: [],
|
||||
},
|
||||
);
|
||||
queryStore.setExecutingWithId(tabId, openDataId);
|
||||
logPhase("state-prepared", { tabId });
|
||||
|
||||
// Helper to check if this openData call is still active (not superseded by a newer click)
|
||||
const isActive = () => queryStore.tabs.find((t) => t.id === tabId)?.executionId === openDataId;
|
||||
const isCurrentDataTab = () => {
|
||||
const current = queryStore.tabs.find((t) => t.id === tabId);
|
||||
return current?.mode === "data" && current.connectionId === node.connectionId && current.database === node.database && current.schema === tableSchema && current.title === node.label;
|
||||
};
|
||||
|
||||
try {
|
||||
console.info("[DBX][openData:ensure-connected:start]", { traceId, elapsed: elapsed() });
|
||||
await connectionStore.ensureConnected(node.connectionId);
|
||||
if (!isActive()) return;
|
||||
if (!isActive()) {
|
||||
logPhase("superseded-after-ensure-connected", { tabId });
|
||||
return;
|
||||
}
|
||||
console.info("[DBX][openData:ensure-connected:done]", { traceId, elapsed: elapsed() });
|
||||
logPhase("ensure-connected", { tabId });
|
||||
if (!config) throw new Error("Connection config not found");
|
||||
|
||||
const querySchema = connectionObjectTreeQuerySchema(config, node.database, tableSchema);
|
||||
const effectiveDbType = effectiveDatabaseTypeForConnection(config);
|
||||
const limit = settingsStore.editorSettings.pageSize;
|
||||
let columns: ColumnInfo[] = [];
|
||||
let primaryKeys: string[] = [];
|
||||
try {
|
||||
console.info("[DBX][openData:get-columns:start]", {
|
||||
const refreshTableMetaInBackground = async () => {
|
||||
const metadataStartedAt = performance.now();
|
||||
console.info("[DBX][openData:metadata:start]", {
|
||||
traceId,
|
||||
database: node.database,
|
||||
schema: querySchema,
|
||||
table: node.label,
|
||||
elapsed: elapsed(),
|
||||
});
|
||||
columns = await api.getColumns(node.connectionId, node.database, querySchema, node.label);
|
||||
if (!isActive()) return;
|
||||
primaryKeys = editablePrimaryKeys(effectiveDbType, columns, node.type === "view" ? "VIEW" : node.type === "materialized_view" ? "MATERIALIZED_VIEW" : "TABLE");
|
||||
console.info("[DBX][openData:get-columns:done]", {
|
||||
try {
|
||||
const nextColumns = await api.getColumns(node.connectionId, node.database, querySchema, node.label);
|
||||
if (!isCurrentDataTab()) {
|
||||
console.info("[DBX][openData:metadata:stale]", {
|
||||
traceId,
|
||||
tabId,
|
||||
columnCount: nextColumns.length,
|
||||
elapsed: elapsed(),
|
||||
});
|
||||
return;
|
||||
}
|
||||
const nextPrimaryKeys = editablePrimaryKeys(effectiveDbType, nextColumns, tableType);
|
||||
queryStore.setTableMeta(tabId, {
|
||||
schema: tableSchema,
|
||||
tableName: node.label,
|
||||
tableType,
|
||||
columns: nextColumns,
|
||||
primaryKeys: nextPrimaryKeys,
|
||||
});
|
||||
console.info("[DBX][openData:metadata:done]", {
|
||||
traceId,
|
||||
tabId,
|
||||
columnCount: nextColumns.length,
|
||||
primaryKeyCount: nextPrimaryKeys.length,
|
||||
elapsed: elapsed(),
|
||||
metadataMs: Math.round(performance.now() - metadataStartedAt),
|
||||
});
|
||||
} catch (error) {
|
||||
console.warn("[DBX][openData:metadata:error]", { traceId, tabId, elapsed: elapsed(), error });
|
||||
}
|
||||
};
|
||||
if (cachedTableMeta) {
|
||||
console.info("[DBX][openData:metadata:cache-hit]", {
|
||||
traceId,
|
||||
columnCount: columns.length,
|
||||
primaryKeys,
|
||||
tabId,
|
||||
columnCount: cachedTableMeta.columns.length,
|
||||
primaryKeyCount: cachedTableMeta.primaryKeys.length,
|
||||
ageMs: Math.round(existingTableMetaAgeMs),
|
||||
elapsed: elapsed(),
|
||||
});
|
||||
queryStore.setTableMeta(tabId, {
|
||||
schema: tableSchema,
|
||||
tableName: node.label,
|
||||
tableType: node.type === "view" ? "VIEW" : node.type === "materialized_view" ? "MATERIALIZED_VIEW" : "TABLE",
|
||||
columns,
|
||||
primaryKeys,
|
||||
});
|
||||
} catch (error) {
|
||||
console.warn("[DBX][openData:get-columns:error]", { traceId, elapsed: elapsed(), error });
|
||||
} else {
|
||||
void refreshTableMetaInBackground();
|
||||
logPhase("metadata-started", { tabId });
|
||||
}
|
||||
|
||||
// Check if superseded by a newer openData call
|
||||
if (!isActive()) return;
|
||||
if (!isActive()) {
|
||||
logPhase("superseded-before-build-sql", { tabId });
|
||||
return;
|
||||
}
|
||||
|
||||
const columns = cachedTableMeta?.columns ?? [];
|
||||
const primaryKeys = cachedTableMeta?.primaryKeys ?? [];
|
||||
const includeRowId = usesSyntheticRowIdKey(effectiveDbType, primaryKeys);
|
||||
const fallbackOrderColumns = effectiveDbType === "sqlserver" && !primaryKeys.length ? columns.slice(0, 1).map((column) => column.name) : undefined;
|
||||
const sql = await buildTableSelectSql({
|
||||
databaseType: effectiveDbType,
|
||||
schema: tableSchema,
|
||||
tableName: node.label,
|
||||
columns: columns.map((column) => column.name),
|
||||
primaryKeys,
|
||||
fallbackOrderColumns,
|
||||
limit,
|
||||
includeRowId,
|
||||
});
|
||||
@@ -946,14 +1004,21 @@ async function openData() {
|
||||
sql,
|
||||
elapsed: elapsed(),
|
||||
});
|
||||
logPhase("sql-built", { tabId, columnCount: columns.length, primaryKeyCount: primaryKeys.length });
|
||||
queryStore.updateSql(tabId, sql);
|
||||
logPhase("sql-updated", { tabId });
|
||||
|
||||
console.info("[DBX][openData:execute:start]", { traceId, tabId, elapsed: elapsed() });
|
||||
await queryStore.executeTabSql(tabId, sql);
|
||||
await queryStore.executeTabSql(tabId, sql, { sourceTraceId: traceId, skipEnsureConnected: true });
|
||||
console.info("[DBX][openData:execute:done]", { traceId, tabId, elapsed: elapsed() });
|
||||
logPhase("execute-tab-sql", { tabId });
|
||||
} catch (e: any) {
|
||||
if (!isActive()) return;
|
||||
if (!isActive()) {
|
||||
logPhase("superseded-after-error", { tabId });
|
||||
return;
|
||||
}
|
||||
console.error("[DBX][openData:error]", { traceId, elapsed: elapsed(), error: e });
|
||||
logPhase("error", { tabId });
|
||||
queryStore.setErrorResult(tabId, e);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,7 +34,6 @@ export function useDataGridActions(activeTab: ComputedRef<QueryTab | undefined>)
|
||||
if (tab.tableMeta && primaryKeys.join("\0") !== tab.tableMeta.primaryKeys.join("\0")) {
|
||||
tab.tableMeta.primaryKeys = primaryKeys;
|
||||
}
|
||||
const fallbackOrderColumns = effectiveDbType === "sqlserver" && !primaryKeys.length ? tableMeta?.columns.slice(0, 1).map((column) => column.name) : undefined;
|
||||
const useRowId = usesSyntheticRowIdKey(effectiveDbType, primaryKeys);
|
||||
return buildTableSelectSql({
|
||||
databaseType: effectiveDbType,
|
||||
@@ -42,7 +41,6 @@ export function useDataGridActions(activeTab: ComputedRef<QueryTab | undefined>)
|
||||
tableName: tableMeta?.tableName ?? "",
|
||||
columns: tableMeta?.columns.map((column) => column.name),
|
||||
primaryKeys,
|
||||
fallbackOrderColumns,
|
||||
includeRowId: useRowId,
|
||||
limit: options.limit ?? settingsStore.editorSettings.pageSize,
|
||||
...options,
|
||||
|
||||
@@ -55,6 +55,7 @@ import { completionSchemasFromTree, completionTablesFromTree } from "@/lib/compl
|
||||
|
||||
const PINNED_TREE_NODES_STORAGE_KEY = "dbx-pinned-tree-nodes";
|
||||
const ACTIVE_CONNECTION_STORAGE_KEY = "dbx-active-connection";
|
||||
const CONNECTION_HEALTH_CHECK_TTL_MS = 2000;
|
||||
function sidebarObjectGroupPageSize(): number {
|
||||
const settingsStore = useSettingsStore();
|
||||
const size = settingsStore.desktopSettings.sidebar_table_page_size;
|
||||
@@ -146,6 +147,7 @@ export const useConnectionStore = defineStore("connection", () => {
|
||||
const treeNodes = ref<TreeNode[]>([]);
|
||||
const pinnedTreeNodeIds = ref<Set<string>>(new Set());
|
||||
const connectedIds = ref<Set<string>>(new Set());
|
||||
const lastConnectionHealthCheckAt = ref<Record<string, number>>({});
|
||||
const loadedTreeNodeChildrenIds = ref<Set<string>>(new Set());
|
||||
const connectionErrors = ref<Record<string, string>>({});
|
||||
const editingConnectionId = ref<string | null>(null);
|
||||
@@ -255,6 +257,20 @@ export const useConnectionStore = defineStore("connection", () => {
|
||||
delete connectionErrors.value[connectionId];
|
||||
}
|
||||
|
||||
function markConnectionHealthChecked(connectionId: string) {
|
||||
lastConnectionHealthCheckAt.value[connectionId] = Date.now();
|
||||
}
|
||||
|
||||
function clearConnectionHealthCheck(connectionId: string) {
|
||||
if (!lastConnectionHealthCheckAt.value[connectionId]) return;
|
||||
delete lastConnectionHealthCheckAt.value[connectionId];
|
||||
}
|
||||
|
||||
function hasRecentConnectionHealthCheck(connectionId: string) {
|
||||
const checkedAt = lastConnectionHealthCheckAt.value[connectionId];
|
||||
return typeof checkedAt === "number" && Date.now() - checkedAt < CONNECTION_HEALTH_CHECK_TTL_MS;
|
||||
}
|
||||
|
||||
function recordConnectionError(connectionId: string, error: unknown): string {
|
||||
const message = connectionErrorMessage(error);
|
||||
setConnectionError(connectionId, message);
|
||||
@@ -263,6 +279,7 @@ export const useConnectionStore = defineStore("connection", () => {
|
||||
|
||||
function markConnectionLost(connectionId: string, error: unknown) {
|
||||
connectedIds.value.delete(connectionId);
|
||||
clearConnectionHealthCheck(connectionId);
|
||||
if (activeConnectionId.value === connectionId) activeConnectionId.value = null;
|
||||
recordConnectionError(connectionId, error);
|
||||
}
|
||||
@@ -733,6 +750,7 @@ export const useConnectionStore = defineStore("connection", () => {
|
||||
for (const id of removedIds) {
|
||||
clearConnectionError(id);
|
||||
connectedIds.value.delete(id);
|
||||
clearConnectionHealthCheck(id);
|
||||
sidebarLayout.value = removeConnectionFromSidebarLayout(sidebarLayout.value, id);
|
||||
}
|
||||
rebuildTreeNodes();
|
||||
@@ -763,6 +781,7 @@ export const useConnectionStore = defineStore("connection", () => {
|
||||
connections.value = nextConnections;
|
||||
rebuildTreeNodes();
|
||||
connectedIds.value.delete(config.id);
|
||||
clearConnectionHealthCheck(config.id);
|
||||
invalidateCompletionCache(config.id);
|
||||
clearLoadedChildrenCache(config.id);
|
||||
const node = findNode(treeNodes.value, config.id);
|
||||
@@ -851,6 +870,7 @@ export const useConnectionStore = defineStore("connection", () => {
|
||||
const id = await withConnectionAttemptTimeout(api.connectDb(config), config);
|
||||
activeConnectionId.value = id;
|
||||
connectedIds.value.add(id);
|
||||
markConnectionHealthChecked(id);
|
||||
clearConnectionError(config.id);
|
||||
if (id !== config.id) clearConnectionError(id);
|
||||
|
||||
@@ -897,6 +917,7 @@ export const useConnectionStore = defineStore("connection", () => {
|
||||
break;
|
||||
}
|
||||
connectedIds.value.delete(connectionId);
|
||||
clearConnectionHealthCheck(connectionId);
|
||||
const node = findNode(treeNodes.value, connectionId);
|
||||
if (node) {
|
||||
node.isExpanded = false;
|
||||
@@ -937,13 +958,16 @@ export const useConnectionStore = defineStore("connection", () => {
|
||||
|
||||
async function ensureConnected(connectionId: string) {
|
||||
if (connectedIds.value.has(connectionId)) {
|
||||
if (hasRecentConnectionHealthCheck(connectionId)) return;
|
||||
// Optimistic: verify backend pool is actually healthy
|
||||
try {
|
||||
await api.checkConnectionHealth(connectionId);
|
||||
markConnectionHealthChecked(connectionId);
|
||||
return;
|
||||
} catch {
|
||||
// Backend pool is dead — remove from connectedIds and reconnect
|
||||
connectedIds.value.delete(connectionId);
|
||||
clearConnectionHealthCheck(connectionId);
|
||||
if (activeConnectionId.value === connectionId) activeConnectionId.value = null;
|
||||
}
|
||||
}
|
||||
@@ -961,6 +985,7 @@ export const useConnectionStore = defineStore("connection", () => {
|
||||
await beforeConnectHandler?.(config);
|
||||
await withConnectionAttemptTimeout(api.connectDb(config), config);
|
||||
connectedIds.value.add(connectionId);
|
||||
markConnectionHealthChecked(connectionId);
|
||||
activeConnectionId.value = connectionId;
|
||||
clearConnectionError(connectionId);
|
||||
} catch (e) {
|
||||
@@ -3080,6 +3105,7 @@ export const useConnectionStore = defineStore("connection", () => {
|
||||
connections.value.push(normalized);
|
||||
}
|
||||
connectedIds.value.add(normalized.id);
|
||||
markConnectionHealthChecked(normalized.id);
|
||||
clearConnectionError(normalized.id);
|
||||
}
|
||||
|
||||
|
||||
@@ -1260,6 +1260,8 @@ export const useQueryStore = defineStore("query", () => {
|
||||
preserveResultDuringExecution?: boolean;
|
||||
preserveTotalRowCountDuringExecution?: boolean;
|
||||
skipRedisSafetyCheck?: boolean;
|
||||
sourceTraceId?: string;
|
||||
skipEnsureConnected?: boolean;
|
||||
},
|
||||
) {
|
||||
const tab = tabs.value.find((t) => t.id === id);
|
||||
@@ -1295,6 +1297,7 @@ export const useQueryStore = defineStore("query", () => {
|
||||
connectionId: tab.connectionId,
|
||||
database: tab.database,
|
||||
schema: tab.schema,
|
||||
sourceTraceId: options?.sourceTraceId,
|
||||
sql,
|
||||
});
|
||||
const queryBaseSql = options?.resultBaseSql ?? sql;
|
||||
@@ -1306,9 +1309,13 @@ export const useQueryStore = defineStore("query", () => {
|
||||
let useAgentResultSession = false;
|
||||
try {
|
||||
const connStore = useConnectionStore();
|
||||
console.info("[DBX][executeTabSql:ensure-connected:start]", { traceId, elapsed: elapsed() });
|
||||
await connStore.ensureConnected(tab.connectionId);
|
||||
console.info("[DBX][executeTabSql:ensure-connected:done]", { traceId, elapsed: elapsed() });
|
||||
if (options?.skipEnsureConnected) {
|
||||
console.info("[DBX][executeTabSql:ensure-connected:skip]", { traceId, elapsed: elapsed(), reason: "caller" });
|
||||
} else {
|
||||
console.info("[DBX][executeTabSql:ensure-connected:start]", { traceId, elapsed: elapsed() });
|
||||
await connStore.ensureConnected(tab.connectionId);
|
||||
console.info("[DBX][executeTabSql:ensure-connected:done]", { traceId, elapsed: elapsed() });
|
||||
}
|
||||
const conn = connStore.getConfig(tab.connectionId);
|
||||
const effectiveDbType = effectiveDatabaseTypeForConnection(conn);
|
||||
const useAgentCursor = conn?.db_type === "jdbc" || supportsDatabaseFeature(conn?.db_type, "driverManagement");
|
||||
@@ -1586,7 +1593,7 @@ export const useQueryStore = defineStore("query", () => {
|
||||
}
|
||||
|
||||
console.info("[DBX][executeTabSql:execute-multi:start]", { traceId, elapsed: elapsed() });
|
||||
const clientSessionId = tab.mode === "query" || tab.mode === "data" ? tabClientSessionId(tab) : undefined;
|
||||
const clientSessionId = tab.mode === "query" ? tabClientSessionId(tab) : undefined;
|
||||
const executionOptions = {
|
||||
...(typeof pageLimit === "number"
|
||||
? useAgentResultSession
|
||||
@@ -1606,6 +1613,7 @@ export const useQueryStore = defineStore("query", () => {
|
||||
elapsed: elapsed(),
|
||||
executionSchema,
|
||||
optionKeys: Object.keys(executionOptions),
|
||||
clientSession: Boolean(clientSessionId),
|
||||
});
|
||||
const executionPromise = api.executeMulti(tab.connectionId, tab.database, sqlToExecute, executionSchema, executionId, executionOptions);
|
||||
const frontendTimeoutSecs = Math.max(queryTimeoutSecs * 2, 60);
|
||||
@@ -2032,7 +2040,6 @@ export const useQueryStore = defineStore("query", () => {
|
||||
const pageLimit = TABLE_DATA_EXPORT_PAGE_SIZE;
|
||||
const effectiveDbType = effectiveDatabaseTypeForConnection(conn);
|
||||
const primaryKeys = tab.tableMeta ? editablePrimaryKeys(effectiveDbType, tab.tableMeta.columns, tab.tableMeta.tableType) : tableMeta.primaryKeys;
|
||||
const fallbackOrderColumns = effectiveDbType === "sqlserver" && !primaryKeys.length ? tableMeta.columns.slice(0, 1).map((column) => column.name) : undefined;
|
||||
const sortOrder = tab.resultSortColumn && tab.resultSortDirection ? `${quoteTableIdentifier(effectiveDbType, tab.resultSortColumn)} ${tab.resultSortDirection.toUpperCase()}` : undefined;
|
||||
const orderBy = tab.orderByInput?.trim() || sortOrder;
|
||||
const queryTimeoutSecs = queryTimeoutSecsForConnection(conn);
|
||||
@@ -2051,7 +2058,6 @@ export const useQueryStore = defineStore("query", () => {
|
||||
tableName: tableMeta.tableName,
|
||||
columns: tableMeta.columns.map((column) => column.name),
|
||||
primaryKeys,
|
||||
fallbackOrderColumns,
|
||||
whereInput: tab.whereInput,
|
||||
orderBy,
|
||||
limit: pageLimit,
|
||||
|
||||
@@ -21,30 +21,9 @@ pub fn build_table_data_select_sql(options: TableDataSelectSqlOptions) -> String
|
||||
let table = qualified_table_name(database_type, options.schema.as_deref(), &options.table_name);
|
||||
let predicate = normalize_where_input(options.where_input.as_deref());
|
||||
let where_clause = if predicate.is_empty() { String::new() } else { format!(" WHERE ({predicate})") };
|
||||
let row_id_alias =
|
||||
if options.include_row_id && database_type == Some(DatabaseType::Oracle) { Some("t") } else { None };
|
||||
let default_order_alias = if database_type == Some(DatabaseType::Jdbc) { None } else { row_id_alias };
|
||||
let default_order_by = if database_type == Some(DatabaseType::InfluxDb) {
|
||||
// InfluxQL only allows sorting of timestamp column
|
||||
Some("time DESC".to_string())
|
||||
} else if !options.primary_keys.is_empty() {
|
||||
Some(
|
||||
options
|
||||
.primary_keys
|
||||
.iter()
|
||||
.map(|pk| format!("{} ASC", quote_order_identifier(database_type, pk, default_order_alias)))
|
||||
.collect::<Vec<_>>()
|
||||
.join(", "),
|
||||
)
|
||||
} else if !options.fallback_order_columns.is_empty() {
|
||||
Some(
|
||||
options
|
||||
.fallback_order_columns
|
||||
.iter()
|
||||
.map(|column| format!("{} ASC", quote_table_identifier(database_type, column)))
|
||||
.collect::<Vec<_>>()
|
||||
.join(", "),
|
||||
)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
@@ -193,29 +172,10 @@ fn build_rownum_table_select_sql(
|
||||
format!("SELECT {select_columns} FROM ({inner_select}) WHERE ROWNUM <= {limit}")
|
||||
}
|
||||
|
||||
pub(super) fn is_oracle_row_id(database_type: Option<DatabaseType>, name: &str) -> bool {
|
||||
database_type == Some(DatabaseType::Oracle) && name.eq_ignore_ascii_case(DBX_ROWID_COLUMN)
|
||||
}
|
||||
|
||||
pub(super) fn is_tdengine_tbname(database_type: Option<DatabaseType>, name: &str) -> bool {
|
||||
database_type == Some(DatabaseType::Tdengine) && name.eq_ignore_ascii_case(DBX_TDENGINE_TBNAME_COLUMN)
|
||||
}
|
||||
|
||||
pub(super) fn quote_order_identifier(
|
||||
database_type: Option<DatabaseType>,
|
||||
name: &str,
|
||||
table_alias: Option<&str>,
|
||||
) -> String {
|
||||
if is_oracle_row_id(database_type, name) {
|
||||
return table_alias.map(|alias| format!("{alias}.ROWID")).unwrap_or_else(|| "ROWID".to_string());
|
||||
}
|
||||
if is_tdengine_tbname(database_type, name) {
|
||||
return DBX_TDENGINE_TBNAME_COLUMN.to_string();
|
||||
}
|
||||
let quoted = quote_table_identifier(database_type, name);
|
||||
table_alias.map(|alias| format!("{alias}.{quoted}")).unwrap_or(quoted)
|
||||
}
|
||||
|
||||
pub(super) fn build_select_columns(database_type: Option<DatabaseType>, columns: &[String]) -> String {
|
||||
if columns.is_empty() {
|
||||
return "*".to_string();
|
||||
@@ -338,19 +298,7 @@ pub(super) fn build_neo4j_table_select_sql(options: &TableDataSelectSqlOptions,
|
||||
"elementId(n) AS {}, {returned_columns}",
|
||||
quote_table_identifier(Some(DatabaseType::Neo4j), DBX_NEO4J_ELEMENT_ID_COLUMN)
|
||||
);
|
||||
let default_order_by = if options.primary_keys.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(
|
||||
options
|
||||
.primary_keys
|
||||
.iter()
|
||||
.map(|pk| format!("n.{} ASC", quote_table_identifier(Some(DatabaseType::Neo4j), pk)))
|
||||
.collect::<Vec<_>>()
|
||||
.join(", "),
|
||||
)
|
||||
};
|
||||
let order_by = options.order_by.as_deref().filter(|order| !order.trim().is_empty()).or(default_order_by.as_deref());
|
||||
let order_by = options.order_by.as_deref().filter(|order| !order.trim().is_empty());
|
||||
let order = order_by.map(|order_by| format!(" ORDER BY {order_by}")).unwrap_or_default();
|
||||
let skip = options.offset.filter(|offset| *offset > 0).map(|offset| format!(" SKIP {offset}")).unwrap_or_default();
|
||||
format!("MATCH (n:{label}){where_clause} RETURN {returns}{order}{skip} LIMIT {limit};")
|
||||
|
||||
@@ -221,7 +221,7 @@ fn builds_table_data_where_and_schema_queries() {
|
||||
where_input: Some("where status = 'active'".to_string()),
|
||||
include_row_id: false,
|
||||
}),
|
||||
"SELECT * FROM `users` WHERE (status = 'active') ORDER BY `id` ASC LIMIT 100;"
|
||||
"SELECT * FROM `users` WHERE (status = 'active') LIMIT 100;"
|
||||
);
|
||||
assert_eq!(
|
||||
build_table_data_select_sql(TableDataSelectSqlOptions {
|
||||
@@ -333,7 +333,7 @@ fn builds_table_data_where_and_schema_queries() {
|
||||
where_input: Some("WHERE amount > 10".to_string()),
|
||||
include_row_id: false,
|
||||
}),
|
||||
"SELECT * FROM (SELECT * FROM \"DBXTEST\".\"ORDERS\" WHERE (amount > 10) ORDER BY \"ID\" ASC) WHERE ROWNUM <= 50"
|
||||
"SELECT * FROM (SELECT * FROM \"DBXTEST\".\"ORDERS\" WHERE (amount > 10)) WHERE ROWNUM <= 50"
|
||||
);
|
||||
assert_eq!(
|
||||
build_table_data_select_sql(TableDataSelectSqlOptions {
|
||||
@@ -349,7 +349,7 @@ fn builds_table_data_where_and_schema_queries() {
|
||||
where_input: Some("WHERE amount > 10".to_string()),
|
||||
include_row_id: false,
|
||||
}),
|
||||
"SELECT \"ID\", \"AMOUNT\" FROM (SELECT dbx_t.\"ID\", dbx_t.\"AMOUNT\", ROW_NUMBER() OVER (ORDER BY \"ID\" ASC) AS \"__dbx_row_num\" FROM \"DB2INST1\".\"ORDERS\" dbx_t WHERE (amount > 10)) dbx_page WHERE \"__dbx_row_num\" > 100 AND \"__dbx_row_num\" <= 150 ORDER BY \"__dbx_row_num\""
|
||||
"SELECT \"ID\", \"AMOUNT\" FROM (SELECT dbx_t.\"ID\", dbx_t.\"AMOUNT\", ROW_NUMBER() OVER () AS \"__dbx_row_num\" FROM \"DB2INST1\".\"ORDERS\" dbx_t WHERE (amount > 10)) dbx_page WHERE \"__dbx_row_num\" > 100 AND \"__dbx_row_num\" <= 150 ORDER BY \"__dbx_row_num\""
|
||||
);
|
||||
assert_eq!(
|
||||
build_table_data_select_sql(TableDataSelectSqlOptions {
|
||||
@@ -401,7 +401,7 @@ fn builds_informix_table_data_with_skip_first_pagination() {
|
||||
where_input: Some("WHERE active = 1".to_string()),
|
||||
include_row_id: false,
|
||||
}),
|
||||
"SELECT SKIP 100 FIRST 50 * FROM users WHERE (active = 1) ORDER BY id ASC"
|
||||
"SELECT SKIP 100 FIRST 50 * FROM users WHERE (active = 1)"
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
@@ -418,7 +418,7 @@ fn builds_informix_table_data_with_skip_first_pagination() {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn explicit_table_data_order_overrides_default_key_order() {
|
||||
fn explicit_table_data_order_is_preserved() {
|
||||
assert_eq!(
|
||||
build_table_data_select_sql(TableDataSelectSqlOptions {
|
||||
database_type: Some(DatabaseType::Postgres),
|
||||
@@ -484,7 +484,7 @@ fn builds_table_data_special_column_queries() {
|
||||
where_input: None,
|
||||
include_row_id: false,
|
||||
}),
|
||||
"SELECT tbname, `ts` AS `ts`, `current` AS `current`, `voltage` AS `voltage`, `location` AS `location`, `groupid` AS `groupid` FROM `test_db`.`meters` ORDER BY `ts` ASC LIMIT 100;"
|
||||
"SELECT tbname, `ts` AS `ts`, `current` AS `current`, `voltage` AS `voltage`, `location` AS `location`, `groupid` AS `groupid` FROM `test_db`.`meters` LIMIT 100;"
|
||||
);
|
||||
assert_eq!(
|
||||
build_table_data_select_sql(TableDataSelectSqlOptions {
|
||||
@@ -520,7 +520,7 @@ fn builds_sqlserver_table_data_pages() {
|
||||
where_input: Some("where id = 1".to_string()),
|
||||
include_row_id: false,
|
||||
}),
|
||||
"SELECT TOP (25) * FROM [dbo].[accounts] WHERE (id = 1) ORDER BY [id] ASC"
|
||||
"SELECT TOP (25) * FROM [dbo].[accounts] WHERE (id = 1)"
|
||||
);
|
||||
assert_eq!(
|
||||
build_table_data_select_sql(TableDataSelectSqlOptions {
|
||||
@@ -536,28 +536,28 @@ fn builds_sqlserver_table_data_pages() {
|
||||
where_input: None,
|
||||
include_row_id: false,
|
||||
}),
|
||||
"WITH [dbx_page] AS (SELECT [order_id], [customer], ROW_NUMBER() OVER (ORDER BY [order_id] ASC) AS [__dbx_row_num] FROM [sales].[orders]) SELECT [order_id], [customer] FROM [dbx_page] WHERE [__dbx_row_num] > 100 AND [__dbx_row_num] <= 150 ORDER BY [__dbx_row_num]"
|
||||
"WITH [dbx_page] AS (SELECT [order_id], [customer], ROW_NUMBER() OVER (ORDER BY (SELECT NULL)) AS [__dbx_row_num] FROM [sales].[orders]) SELECT [order_id], [customer] FROM [dbx_page] WHERE [__dbx_row_num] > 100 AND [__dbx_row_num] <= 150 ORDER BY [__dbx_row_num]"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn builds_oracle_and_neo4j_table_data_queries() {
|
||||
assert_eq!(
|
||||
build_table_data_select_sql(TableDataSelectSqlOptions {
|
||||
database_type: Some(DatabaseType::Oracle),
|
||||
schema: Some("DBXTEST".to_string()),
|
||||
table_name: "DBX_LOAD_TABLE_006".to_string(),
|
||||
primary_keys: vec![DBX_ROWID_COLUMN.to_string()],
|
||||
columns: Vec::new(),
|
||||
fallback_order_columns: Vec::new(),
|
||||
order_by: None,
|
||||
limit: Some(100),
|
||||
offset: None,
|
||||
where_input: None,
|
||||
include_row_id: true,
|
||||
}),
|
||||
"SELECT ROWIDTOCHAR(t.ROWID) AS \"__DBX_ROWID\", t.* FROM \"DBXTEST\".\"DBX_LOAD_TABLE_006\" t ORDER BY t.ROWID ASC"
|
||||
);
|
||||
build_table_data_select_sql(TableDataSelectSqlOptions {
|
||||
database_type: Some(DatabaseType::Oracle),
|
||||
schema: Some("DBXTEST".to_string()),
|
||||
table_name: "DBX_LOAD_TABLE_006".to_string(),
|
||||
primary_keys: vec![DBX_ROWID_COLUMN.to_string()],
|
||||
columns: Vec::new(),
|
||||
fallback_order_columns: Vec::new(),
|
||||
order_by: None,
|
||||
limit: Some(100),
|
||||
offset: None,
|
||||
where_input: None,
|
||||
include_row_id: true,
|
||||
}),
|
||||
"SELECT ROWIDTOCHAR(t.ROWID) AS \"__DBX_ROWID\", t.* FROM \"DBXTEST\".\"DBX_LOAD_TABLE_006\" t"
|
||||
);
|
||||
assert_eq!(
|
||||
build_table_data_select_sql(TableDataSelectSqlOptions {
|
||||
database_type: Some(DatabaseType::Neo4j),
|
||||
@@ -572,7 +572,7 @@ fn builds_oracle_and_neo4j_table_data_queries() {
|
||||
where_input: None,
|
||||
include_row_id: false,
|
||||
}),
|
||||
"MATCH (n:`Employee`) RETURN elementId(n) AS `__DBX_ELEMENT_ID`, n.`id` AS `id`, n.`first name` AS `first name`, n.`role` AS `role` ORDER BY n.`id` ASC LIMIT 100;"
|
||||
"MATCH (n:`Employee`) RETURN elementId(n) AS `__DBX_ELEMENT_ID`, n.`id` AS `id`, n.`first name` AS `first name`, n.`role` AS `role` LIMIT 100;"
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user