fix(sqlserver): support legacy pagination syntax

This commit is contained in:
t8y2 2026-05-18 12:06:00 +08:00
parent 5549828e27
commit 82f78bc805
5 changed files with 81 additions and 20 deletions

View File

@ -87,9 +87,10 @@ export function buildPaginatedQuerySql(
const base = `SELECT * FROM (${statement.sql}) ${alias}`;
if (databaseType === "sqlserver") {
if (safeOffset > 0) return { ok: false, reason: "unsupported" };
return {
ok: true,
sql: `${base} ORDER BY (SELECT NULL) OFFSET ${safeOffset} ROWS FETCH NEXT ${safeLimit} ROWS ONLY`,
sql: `SELECT TOP (${safeLimit}) * FROM (${statement.sql}) ${alias}`,
};
}

View File

@ -90,8 +90,14 @@ export function buildTableSelectSql(options: BuildTableSelectSqlOptions): string
}
if (databaseType === "sqlserver") {
const stableOrder = order || " ORDER BY (SELECT NULL)";
return `SELECT * FROM ${table}${where}${stableOrder} OFFSET ${options.offset ?? 0} ROWS FETCH NEXT ${limit} ROWS ONLY`;
return buildSqlServerTableSelectSql({
table,
where,
orderBy: orderBy ?? "(SELECT NULL)",
columns: options.columns,
limit,
offset: options.offset ?? 0,
});
}
const offset = options.offset ? ` OFFSET ${options.offset}` : "";
@ -121,6 +127,34 @@ function buildSelectColumns(databaseType: DatabaseType | undefined, columns?: st
.join(", ");
}
function buildSqlServerTableSelectSql(options: {
table: string;
where: string;
orderBy: string;
columns?: string[];
limit: number;
offset: number;
}): string {
const columns = options.columns?.length
? options.columns.map((column) => quoteTableIdentifier("sqlserver", column)).join(", ")
: "*";
const order = options.orderBy === "(SELECT NULL)" ? "" : ` ORDER BY ${options.orderBy}`;
if (options.offset <= 0) {
return `SELECT TOP (${options.limit}) ${columns} FROM ${options.table}${options.where}${order}`;
}
const pageAlias = quoteTableIdentifier("sqlserver", "dbx_page");
const rowNumberAlias = quoteTableIdentifier("sqlserver", "__dbx_row_num");
const end = options.offset + options.limit;
const rowNumberOrder = `ORDER BY ${options.orderBy}`;
return (
`WITH ${pageAlias} AS (` +
`SELECT ${columns}, ROW_NUMBER() OVER (${rowNumberOrder}) AS ${rowNumberAlias} FROM ${options.table}${options.where}` +
`) SELECT ${columns} FROM ${pageAlias}` +
` WHERE ${rowNumberAlias} > ${options.offset} AND ${rowNumberAlias} <= ${end} ORDER BY ${rowNumberAlias}`
);
}
function isTdengineTbname(databaseType: DatabaseType | undefined, name: string): boolean {
return databaseType === "tdengine" && name.toLowerCase() === DBX_TDENGINE_TBNAME_COLUMN;
}

View File

@ -53,7 +53,7 @@ test("builds capped export page queries", () => {
tableName: "accounts",
limit: DATABASE_EXPORT_ROW_LIMIT,
}),
`SELECT * FROM [dbo].[accounts] ORDER BY (SELECT NULL) OFFSET 0 ROWS FETCH NEXT ${DATABASE_EXPORT_ROW_LIMIT} ROWS ONLY`,
`SELECT TOP (${DATABASE_EXPORT_ROW_LIMIT}) * FROM [dbo].[accounts]`,
);
});

View File

@ -24,12 +24,21 @@ test("uses MySQL style quoting for paginated query alias", () => {
});
});
test("uses SQL Server offset fetch pagination", () => {
const result = buildPaginatedQuerySql("SELECT id FROM users", "sqlserver", 100, 300);
test("uses SQL Server TOP pagination for the first page to support SQL Server 2008", () => {
const result = buildPaginatedQuerySql("SELECT id FROM users", "sqlserver", 100, 0);
assert.deepEqual(result, {
ok: true,
sql: "SELECT * FROM (SELECT id FROM users) [dbx_page] ORDER BY (SELECT NULL) OFFSET 300 ROWS FETCH NEXT 100 ROWS ONLY",
sql: "SELECT TOP (100) * FROM (SELECT id FROM users) [dbx_page]",
});
});
test("does not generate SQL Server OFFSET pagination for later query-result pages", () => {
const result = buildPaginatedQuerySql("SELECT id FROM users", "sqlserver", 100, 300);
assert.deepEqual(result, {
ok: false,
reason: "unsupported",
});
});
@ -133,7 +142,10 @@ test("uses SQL pagination instead of jdbc cursor for random agent page jumps", (
useAgentCursor: true,
});
assert.equal(plan.sqlToExecute, 'SELECT * FROM (SELECT * FROM events) "dbx_page" OFFSET 1500 ROWS FETCH FIRST 500 ROWS ONLY');
assert.equal(
plan.sqlToExecute,
'SELECT * FROM (SELECT * FROM events) "dbx_page" OFFSET 1500 ROWS FETCH FIRST 500 ROWS ONLY',
);
assert.equal(plan.pageSql, plan.sqlToExecute);
assert.equal(plan.pageLimit, 500);
assert.equal(plan.pageOffset, 1500);

View File

@ -99,10 +99,7 @@ test("qualifies JDBC table data default order columns with a table alias", () =>
limit: 100,
});
assert.equal(
sql,
'SELECT * FROM "SJT_THEME"."BATCH_QUERY" dbx_t ORDER BY dbx_t."TABLE_NAME" ASC LIMIT 100;',
);
assert.equal(sql, 'SELECT * FROM "SJT_THEME"."BATCH_QUERY" dbx_t ORDER BY dbx_t."TABLE_NAME" ASC LIMIT 100;');
});
test("expands Hive table data queries into aliased table columns", () => {
@ -116,7 +113,7 @@ test("expands Hive table data queries into aliased table columns", () => {
assert.equal(sql, "SELECT `id` AS `id`, `name` AS `name` FROM `departments` LIMIT 100;");
});
test("builds SQL Server first page query with schema-aware brackets", () => {
test("builds SQL Server first page query with TOP for SQL Server 2008 compatibility", () => {
const sql = buildTableSelectSql({
databaseType: "sqlserver",
schema: "dbo",
@ -126,23 +123,36 @@ test("builds SQL Server first page query with schema-aware brackets", () => {
primaryKeys: ["id"],
});
assert.equal(
sql,
"SELECT * FROM [dbo].[accounts] WHERE (id = 1) ORDER BY [id] ASC OFFSET 0 ROWS FETCH NEXT 25 ROWS ONLY",
);
assert.equal(sql, "SELECT TOP (25) * FROM [dbo].[accounts] WHERE (id = 1) ORDER BY [id] ASC");
});
test("builds SQL Server later pages with OFFSET and FETCH", () => {
test("builds SQL Server first page query without a synthetic ORDER BY when no order is available", () => {
const sql = buildTableSelectSql({
databaseType: "sqlserver",
schema: "dbo",
tableName: "logs",
columns: ["message"],
limit: 25,
});
assert.equal(sql, "SELECT TOP (25) [message] FROM [dbo].[logs]");
});
test("builds SQL Server later pages with ROW_NUMBER for SQL Server 2008 compatibility", () => {
const sql = buildTableSelectSql({
databaseType: "sqlserver",
schema: "sales",
tableName: "orders",
columns: ["order_id", "customer"],
primaryKeys: ["order_id"],
limit: 50,
offset: 100,
});
assert.equal(sql, "SELECT * FROM [sales].[orders] ORDER BY [order_id] ASC OFFSET 100 ROWS FETCH NEXT 50 ROWS ONLY");
assert.equal(
sql,
"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]",
);
});
test("builds SQL Server pages with fallback order columns when there is no primary key", () => {
@ -150,12 +160,16 @@ test("builds SQL Server pages with fallback order columns when there is no prima
databaseType: "sqlserver",
schema: "dbo",
tableName: "logs",
columns: ["created_at", "message"],
fallbackOrderColumns: ["created_at"],
limit: 50,
offset: 50,
});
assert.equal(sql, "SELECT * FROM [dbo].[logs] ORDER BY [created_at] ASC OFFSET 50 ROWS FETCH NEXT 50 ROWS ONLY");
assert.equal(
sql,
"WITH [dbx_page] AS (SELECT [created_at], [message], ROW_NUMBER() OVER (ORDER BY [created_at] ASC) AS [__dbx_row_num] FROM [dbo].[logs]) SELECT [created_at], [message] FROM [dbx_page] WHERE [__dbx_row_num] > 50 AND [__dbx_row_num] <= 100 ORDER BY [__dbx_row_num]",
);
});
test("builds Oracle table data queries with ROWID for keyless editing", () => {