mirror of
https://github.com/t8y2/dbx.git
synced 2026-10-02 02:34:42 +08:00
fix(jdbc): send rowOffset for generic JDBC table pagination
This commit is contained in:
@@ -20,6 +20,7 @@ import {
|
||||
gaussdbIdentifierQuoteStyle,
|
||||
gaussdbTargetServerType,
|
||||
inferJdbcDialect,
|
||||
jdbcConnectionUsesDriverRowOffset,
|
||||
metadataSchemaForConnection,
|
||||
objectListSchemaForConnection,
|
||||
setGaussdbConnectionMode,
|
||||
@@ -328,6 +329,29 @@ describe("jdbc dialect inference", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("JDBC driver row offset", () => {
|
||||
it("lets the driver skip rows for dialects without SQL offset pagination", () => {
|
||||
// Unknown vendor dialects stay on the generic JDBC dialect, which emits a
|
||||
// bare SELECT, so the page offset can only be applied by the driver (#9015).
|
||||
expect(jdbcConnectionUsesDriverRowOffset({ db_type: "jdbc", connection_string: "jdbc:sybase:Tds:db.example.com:5000/app" }, "jdbc")).toBe(true);
|
||||
expect(jdbcConnectionUsesDriverRowOffset({ db_type: "jdbc", connection_string: "jdbc:hsqldb:hsql://127.0.0.1:9001/probe" }, "jdbc")).toBe(true);
|
||||
});
|
||||
|
||||
it("keeps the Caché/IRIS ResultSet offset behavior", () => {
|
||||
expect(jdbcConnectionUsesDriverRowOffset({ db_type: "jdbc", connection_string: "jdbc:Cache://localhost:1972/USER" }, "iris")).toBe(true);
|
||||
});
|
||||
|
||||
it("leaves SQL-paginated dialects and driver-capped JDBC drivers alone", () => {
|
||||
expect(jdbcConnectionUsesDriverRowOffset({ db_type: "jdbc", connection_string: "jdbc:mysql://localhost:3306/app" }, "mysql")).toBe(false);
|
||||
expect(jdbcConnectionUsesDriverRowOffset({ db_type: "jdbc", connection_string: "jdbc:oracle:thin:@localhost:1521:XE" }, "oracle")).toBe(false);
|
||||
// YashanDB keeps the generic dialect, but its agent applies
|
||||
// Statement.setMaxRows, which would cap the result set before the skipped rows.
|
||||
expect(jdbcConnectionUsesDriverRowOffset({ db_type: "jdbc", connection_string: "jdbc:yasdb://localhost:1688/app" }, "jdbc")).toBe(false);
|
||||
expect(jdbcConnectionUsesDriverRowOffset({ db_type: "postgres" }, "postgres")).toBe(false);
|
||||
expect(jdbcConnectionUsesDriverRowOffset(undefined, "jdbc")).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("GaussDB connection mode", () => {
|
||||
it("keeps native connections compatible and configures M mode for the vendor JDBC driver", () => {
|
||||
const connection = { db_type: "gaussdb", driver_profile: "gaussdb", driver_label: "GaussDB" } as ConnectionConfig;
|
||||
|
||||
@@ -67,6 +67,29 @@ export function jdbcDriverProfileUsesSchemaQualification(driverProfile?: string)
|
||||
return inferJdbcDialect({ db_type: "jdbc", driver_profile: driverProfile }) === "jdbc";
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether a JDBC-backed table view must let the driver skip `rowOffset` rows
|
||||
* instead of expecting SQL pagination.
|
||||
*
|
||||
* Generic JDBC (`DatabaseType::Jdbc` → `AgentMaxRows`) and Iris (`IrisTop`) are
|
||||
* the two dialects that cannot express a page offset in the generated SELECT:
|
||||
* generic JDBC emits no LIMIT/OFFSET at all and Iris only has `TOP`. Without the
|
||||
* driver-side offset the agent re-runs the same unbounded statement for every
|
||||
* page, so the grid keeps rendering page one (#9015).
|
||||
*
|
||||
* The Oracle, Dameng and Yashan driver families are excluded because the JDBC
|
||||
* agent passes `maxRows + 1` to `Statement.setMaxRows` for them, which caps the
|
||||
* result set *before* the skipped rows — those drivers paginate in SQL instead.
|
||||
*/
|
||||
const JDBC_STATEMENT_MAX_ROWS_URL_PREFIXES = ["jdbc:oracle:", "jdbc:dm:", "jdbc:yasdb:"];
|
||||
|
||||
export function jdbcConnectionUsesDriverRowOffset(connection: JdbcDialectConnection | undefined, effectiveDatabaseType: DatabaseType | undefined): boolean {
|
||||
if (connection?.db_type !== "jdbc") return false;
|
||||
if (effectiveDatabaseType !== "iris" && effectiveDatabaseType !== "jdbc") return false;
|
||||
const url = connection.connection_string?.trim().toLowerCase() ?? "";
|
||||
return !JDBC_STATEMENT_MAX_ROWS_URL_PREFIXES.some((prefix) => url.startsWith(prefix));
|
||||
}
|
||||
|
||||
export function effectiveDatabaseTypeForConnection(connection?: JdbcDialectConnection): DatabaseType | undefined {
|
||||
if (!connection) return undefined;
|
||||
if (connection.db_type === "gbase" && isGbase8sProfile(connection.driver_profile)) return "informix";
|
||||
|
||||
@@ -179,6 +179,75 @@ describe("queryStore table data refresh", () => {
|
||||
expect(mocks.executeMulti).toHaveBeenCalledWith("cache-1", "USER", 'SELECT * FROM "SS"."SS_User" ORDER BY "ID" ASC', undefined, expect.any(String), expect.objectContaining({ maxRows: 100, fetchSize: 100, rowOffset: 100 }));
|
||||
});
|
||||
|
||||
it("uses JDBC ResultSet offset pagination for dialects that cannot paginate in SQL", async () => {
|
||||
mocks.getConnectionConfig.mockReturnValue({
|
||||
id: "sybase-1",
|
||||
name: "Sybase ASE 16",
|
||||
db_type: "jdbc",
|
||||
database: "app",
|
||||
connection_string: "jdbc:sybase:Tds:db.example.com:5000/app",
|
||||
query_timeout_secs: 30,
|
||||
});
|
||||
mocks.buildTableSelectSql.mockResolvedValue("SELECT * FROM dbo.users ORDER BY id ASC");
|
||||
const { useQueryStore } = await import("@/stores/queryStore");
|
||||
const store = useQueryStore();
|
||||
const tabId = store.createTab("sybase-1", "app", "users", "data", "dbo");
|
||||
store.setTableMeta(tabId, {
|
||||
schema: "dbo",
|
||||
tableName: "users",
|
||||
tableType: "TABLE",
|
||||
columns: [{ name: "id", data_type: "int", is_nullable: false, column_default: null, is_primary_key: true, extra: null }],
|
||||
primaryKeys: ["id"],
|
||||
});
|
||||
const tab = store.tabs.find((candidate) => candidate.id === tabId)!;
|
||||
tab.orderByInput = "id ASC";
|
||||
tab.resultPageLimit = 100;
|
||||
tab.resultPageOffset = 100;
|
||||
|
||||
await store.refreshDataTab(tabId);
|
||||
|
||||
// Generic JDBC emits a bare SELECT, so the agent has to skip the offset;
|
||||
// otherwise page 2 re-renders page 1 (#9015).
|
||||
expect(mocks.buildTableSelectSql).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
databaseType: "jdbc",
|
||||
limit: 100,
|
||||
offset: 100,
|
||||
useDriverRowOffset: true,
|
||||
}),
|
||||
);
|
||||
expect(mocks.executeMulti).toHaveBeenCalledWith("sybase-1", "app", "SELECT * FROM dbo.users ORDER BY id ASC", undefined, expect.any(String), expect.objectContaining({ maxRows: 100, fetchSize: 100, rowOffset: 100 }));
|
||||
});
|
||||
|
||||
it("keeps YashanDB JDBC tabs on server-side pagination", async () => {
|
||||
mocks.getConnectionConfig.mockReturnValue({
|
||||
id: "yasdb-1",
|
||||
name: "YashanDB",
|
||||
db_type: "jdbc",
|
||||
database: "app",
|
||||
connection_string: "jdbc:yasdb://localhost:1688/app",
|
||||
query_timeout_secs: 30,
|
||||
});
|
||||
mocks.buildTableSelectSql.mockResolvedValue("SELECT * FROM app.users");
|
||||
const { useQueryStore } = await import("@/stores/queryStore");
|
||||
const store = useQueryStore();
|
||||
const tabId = store.createTab("yasdb-1", "app", "users", "data");
|
||||
store.setTableMeta(tabId, {
|
||||
tableName: "users",
|
||||
tableType: "TABLE",
|
||||
columns: [{ name: "id", data_type: "int", is_nullable: false, column_default: null, is_primary_key: true, extra: null }],
|
||||
primaryKeys: ["id"],
|
||||
});
|
||||
const tab = store.tabs.find((candidate) => candidate.id === tabId)!;
|
||||
tab.resultPageLimit = 100;
|
||||
tab.resultPageOffset = 100;
|
||||
|
||||
await store.refreshDataTab(tabId);
|
||||
|
||||
expect(mocks.buildTableSelectSql).toHaveBeenCalledWith(expect.not.objectContaining({ useDriverRowOffset: true }));
|
||||
expect(mocks.executeMulti).toHaveBeenCalledWith("yasdb-1", "app", "SELECT * FROM app.users", undefined, expect.any(String), expect.not.objectContaining({ rowOffset: expect.anything() }));
|
||||
});
|
||||
|
||||
it("executes Doris external catalog data tabs against the catalog database, not the connection default database", async () => {
|
||||
mocks.getConnectionConfig.mockReturnValue({
|
||||
id: "doris-1",
|
||||
|
||||
@@ -55,7 +55,7 @@ import { tableOpenPageLimit } from "@/lib/table/tableOpenPageLimit";
|
||||
import { getCachedTableMetadata, loadTableColumns, loadTableIndexes, loadTableMetadata, tableMetadataToDataTabMeta, updateCachedTableMetadataType, type TableMetadataRequest } from "@/lib/metadata/tableMetadataCache";
|
||||
import { MetadataTaskLimiter } from "@/lib/metadata/metadataTaskLimiter";
|
||||
import { buildTableSelectSql, quoteTableDataIdentifier } from "@/lib/table/tableSelectSql";
|
||||
import { connectionObjectTreeNodeSchema, connectionQueryExecutionSchema, connectionUsesDatabaseObjectTreeMode, effectiveDatabaseTypeForConnection, gaussdbCountQueryDopHint, metadataSchemaForConnection } from "@/lib/database/jdbcDialect";
|
||||
import { connectionObjectTreeNodeSchema, connectionQueryExecutionSchema, connectionUsesDatabaseObjectTreeMode, effectiveDatabaseTypeForConnection, gaussdbCountQueryDopHint, jdbcConnectionUsesDriverRowOffset, metadataSchemaForConnection } from "@/lib/database/jdbcDialect";
|
||||
import { frontendQueryTimeoutDelayMs, frontendQueryTimeoutSecsForSql, queryTimeoutSecsForConnection } from "@/lib/sql/queryTimeout";
|
||||
import { queryResultNameFromPreamble, queryResultSourceLabel } from "@/lib/sql/queryResultSource";
|
||||
import { sqlServerCountUsesLocalTempTable } from "@/lib/query/queryResultCountSession";
|
||||
@@ -4546,7 +4546,7 @@ export const useQueryStore = defineStore("query", () => {
|
||||
const orderBy = tab.orderByInput?.trim() || sortOrder;
|
||||
const limit = tab.resultPageLimit ?? tableOpenPageLimit(settingsStore.editorSettings.tableOpenPageSize);
|
||||
const offset = tab.resultPageOffset ?? 0;
|
||||
const useDriverRowOffset = conn?.db_type === "jdbc" && effectiveDbType === "iris";
|
||||
const useDriverRowOffset = jdbcConnectionUsesDriverRowOffset(conn, effectiveDbType);
|
||||
|
||||
const sql = await buildTableSelectSql({
|
||||
databaseType: effectiveDbType,
|
||||
@@ -7263,7 +7263,7 @@ export const useQueryStore = defineStore("query", () => {
|
||||
// connection-local state and avoid MySQL pool resets on every refresh.
|
||||
const dataTabMeta = tab.mode === "data" ? tableMetaForDataTab(tab) : undefined;
|
||||
const useTableDataPreview = canUseTableDataLargeValuePreview(effectiveDbType, dataTabMeta?.columns ?? [], dataTabMeta?.primaryKeys ?? []);
|
||||
const useJdbcDriverRowOffset = tab.mode === "data" && conn?.db_type === "jdbc" && effectiveDbType === "iris";
|
||||
const useJdbcDriverRowOffset = tab.mode === "data" && jdbcConnectionUsesDriverRowOffset(conn, effectiveDbType);
|
||||
const executionOptions = {
|
||||
...(typeof pageLimit === "number"
|
||||
? useAgentResultSession
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 53 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 64 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 72 KiB |
Reference in New Issue
Block a user