diff --git a/apps/desktop/src/components/sidebar/TreeItem.vue b/apps/desktop/src/components/sidebar/TreeItem.vue index 88af3dd8a2..ec1e363742 100644 --- a/apps/desktop/src/components/sidebar/TreeItem.vue +++ b/apps/desktop/src/components/sidebar/TreeItem.vue @@ -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 = {}) => { + 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); } } diff --git a/apps/desktop/src/composables/useDataGridActions.ts b/apps/desktop/src/composables/useDataGridActions.ts index fdfd93056e..bed0b20d7d 100644 --- a/apps/desktop/src/composables/useDataGridActions.ts +++ b/apps/desktop/src/composables/useDataGridActions.ts @@ -34,7 +34,6 @@ export function useDataGridActions(activeTab: ComputedRef) 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) tableName: tableMeta?.tableName ?? "", columns: tableMeta?.columns.map((column) => column.name), primaryKeys, - fallbackOrderColumns, includeRowId: useRowId, limit: options.limit ?? settingsStore.editorSettings.pageSize, ...options, diff --git a/apps/desktop/src/stores/connectionStore.ts b/apps/desktop/src/stores/connectionStore.ts index e97ccc9fc3..2dd1242a6f 100644 --- a/apps/desktop/src/stores/connectionStore.ts +++ b/apps/desktop/src/stores/connectionStore.ts @@ -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([]); const pinnedTreeNodeIds = ref>(new Set()); const connectedIds = ref>(new Set()); + const lastConnectionHealthCheckAt = ref>({}); const loadedTreeNodeChildrenIds = ref>(new Set()); const connectionErrors = ref>({}); const editingConnectionId = ref(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); } diff --git a/apps/desktop/src/stores/queryStore.ts b/apps/desktop/src/stores/queryStore.ts index 7ceba0d798..c1b82ba990 100644 --- a/apps/desktop/src/stores/queryStore.ts +++ b/apps/desktop/src/stores/queryStore.ts @@ -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, diff --git a/crates/dbx-core/src/sql_dialect/table_select.rs b/crates/dbx-core/src/sql_dialect/table_select.rs index 7643e4191d..a25bb730f7 100644 --- a/crates/dbx-core/src/sql_dialect/table_select.rs +++ b/crates/dbx-core/src/sql_dialect/table_select.rs @@ -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::>() - .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::>() - .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, 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, 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, - 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, 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::>() - .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};") diff --git a/crates/dbx-core/src/sql_dialect/tests.rs b/crates/dbx-core/src/sql_dialect/tests.rs index ac8770f1fe..0f2e689c0f 100644 --- a/crates/dbx-core/src/sql_dialect/tests.rs +++ b/crates/dbx-core/src/sql_dialect/tests.rs @@ -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;" ); }