feat(gaussdb): apply query dop hint to count and page sql

This commit is contained in:
Andrew
2026-10-15 23:00:55 +08:00
committed by GitHub
parent 8be7a5b131
commit aae04246c4
18 changed files with 241 additions and 29 deletions
+2 -2
View File
@@ -1132,8 +1132,8 @@ export default withEnglishFallback({
gaussdbTargetServerTypeSlave: "İkinci dərəcəli (ehtiyat)",
gaussdbTargetServerTypeAny: "İstənilən (üstünlük yoxdur)",
gaussdbTargetServerTypeHint: "JDBC drayverinin GaussDB klasterində hansı qovşağa qoşulacağını idarə edir. Əsas rejim ilkin qovşağı, ikinci dərəcəli rejim ehtiyat qovşağı sınayır, istənilən rejim isə əlçatan olan qovşağa qoşulur. Yalnız M (JDBC) rejimində tətbiq olunur.",
gaussdbCountQueryDop: "COUNT(*) paralelliyi",
gaussdbCountQueryDopHint: "Böyük cədvəllərdə COUNT(*) sorğuları üçün query_dop (paralellik dərəcəsi) təyin edir. Daha yüksək dəyərlər daha çox resurs istifadə edir. Optimallaşdırıcı göstərişini deaktiv etmək üçün 1 (standart) təyin edin.",
gaussdbCountQueryDop: "Sorğu paralelliyi (query_dop)",
gaussdbCountQueryDopHint: "Böyük cədvəllərdə sətir sayma və nəticə səhifələmə sorğuları üçün query_dop (paralellik dərəcəsi) təyin edir. Daha yüksək dəyərlər daha çox resurs işlədir. Optimallaşdırıcı göstərişini deaktiv etmək üçün 1 (standart) təyin edin.",
saveAndConnect: "Yadda saxla və əlaqə qur",
save: "Yadda saxla",
editTitle: "Əlaqəni redaktə et",
+2 -2
View File
@@ -1768,8 +1768,8 @@ export default {
gaussdbTargetServerTypeSlave: "Slave (standby)",
gaussdbTargetServerTypeAny: "Any (no preference)",
gaussdbTargetServerTypeHint: "Controls which node in a GaussDB cluster the JDBC driver connects to. Master tries the primary, slave tries a standby, any connects to whichever is available. Only applies in M (JDBC) mode.",
gaussdbCountQueryDop: "COUNT(*) parallelism",
gaussdbCountQueryDopHint: "Sets the query_dop (degree of parallelism) for COUNT(*) queries on large tables. Higher values use more resources. Set to 1 (default) to disable the optimizer hint.",
gaussdbCountQueryDop: "Query parallelism (query_dop)",
gaussdbCountQueryDopHint: "Sets the query_dop (degree of parallelism) for row counting and result pagination on large tables. Higher values use more resources. Set to 1 (default) to disable the optimizer hint.",
saveAndConnect: "Save & Connect",
save: "Save",
editTitle: "Edit Connection",
+2 -2
View File
@@ -1434,8 +1434,8 @@ export default withEnglishFallback({
gaussdbTargetServerTypeSlave: "Réplica (en espera)",
gaussdbTargetServerTypeAny: "Cualquiera (sin preferencia)",
gaussdbTargetServerTypeHint: "Controla a qué nodo del clúster GaussDB se conecta el controlador JDBC. Se aplica solo al modo M (JDBC).",
gaussdbCountQueryDop: "Paralelismo de COUNT(*)",
gaussdbCountQueryDopHint: "Establece query_dop (grado de paralelismo) para consultas COUNT(*) en tablas grandes. Los valores más altos usan más recursos. Establezca 1 (predeterminado) para desactivar la sugerencia del optimizador.",
gaussdbCountQueryDop: "Paralelismo de consultas (query_dop)",
gaussdbCountQueryDopHint: "Establece query_dop (grado de paralelismo) para el recuento de filas y la paginación de resultados en tablas grandes. Los valores más altos usan más recursos. Establezca 1 (predeterminado) para desactivar la sugerencia del optimizador.",
saveAndConnect: "Guardar y conectar",
save: "Guardar",
editTitle: "Editar conexión",
+2 -2
View File
@@ -1395,8 +1395,8 @@ export default withEnglishFallback({
gaussdbTargetServerTypeSlave: "Slave (standby)",
gaussdbTargetServerTypeAny: "Any (tanpa preferensi)",
gaussdbTargetServerTypeHint: "Mengontrol node mana di klaster GaussDB yang akan dihubungkan oleh driver JDBC. Master mencoba primary, slave mencoba standby, any menghubungkan ke mana pun yang tersedia. Hanya berlaku pada mode M (JDBC).",
gaussdbCountQueryDop: "Paralelisme COUNT(*)",
gaussdbCountQueryDopHint: "Menetapkan query_dop (derajat paralelisme) untuk kueri COUNT(*) pada tabel besar. Nilai lebih tinggi menggunakan lebih banyak sumber daya. Tetapkan ke 1 (default) untuk menonaktifkan hint optimizer.",
gaussdbCountQueryDop: "Paralelisme kueri (query_dop)",
gaussdbCountQueryDopHint: "Menetapkan query_dop (derajat paralelisme) untuk penghitungan baris dan penomoran halaman hasil pada tabel besar. Nilai lebih tinggi menggunakan lebih banyak sumber daya. Tetapkan ke 1 (default) untuk menonaktifkan hint optimizer.",
saveAndConnect: "Simpan & Hubungkan",
save: "Simpan",
editTitle: "Edit Koneksi",
+2 -2
View File
@@ -1357,8 +1357,8 @@ export default withEnglishFallback({
gaussdbTargetServerTypeSlave: "Slave (standby)",
gaussdbTargetServerTypeAny: "Qualsiasi (nessuna preferenza)",
gaussdbTargetServerTypeHint: "Controlla il nodo del cluster GaussDB a cui si connette il driver JDBC. Si applica solo alla modalità M (JDBC).",
gaussdbCountQueryDop: "Parallelismo COUNT(*)",
gaussdbCountQueryDopHint: "Imposta query_dop (grado di parallelismo) per le query COUNT(*) su tabelle di grandi dimensioni. Valori più elevati usano più risorse. Imposta 1 (predefinito) per disabilitare l'hint dell'ottimizzatore.",
gaussdbCountQueryDop: "Parallelismo delle query (query_dop)",
gaussdbCountQueryDopHint: "Imposta query_dop (grado di parallelismo) per il conteggio delle righe e la paginazione dei risultati su tabelle di grandi dimensioni. Valori più elevati usano più risorse. Imposta 1 (predefinito) per disabilitare l'hint dell'ottimizzatore.",
saveAndConnect: "Salva & Connetti",
save: "Salva",
editTitle: "Modifica Connessione",
+2 -2
View File
@@ -1327,8 +1327,8 @@ export default withEnglishFallback({
gaussdbTargetServerTypeSlave: "スレーブ(スタンバイ)",
gaussdbTargetServerTypeAny: "任意(優先なし)",
gaussdbTargetServerTypeHint: "JDBC ドライバーが接続する GaussDB クラスターのノードを制御します。M モード(JDBC)にのみ適用されます。",
gaussdbCountQueryDop: "COUNT(*) の並列度",
gaussdbCountQueryDopHint: "大規模テーブルの COUNT(*) クエリに query_dop(並列度)を設定します。値を大きくするとより多くのリソースを使用します。オプティマイザーヒントを無効にするには 1(既定値)を設定します。",
gaussdbCountQueryDop: "クエリの並列度 (query_dop)",
gaussdbCountQueryDopHint: "大規模テーブルの行数カウントと結果ページングのクエリに query_dop(並列度)を設定します。値を大きくするとより多くのリソースを使用します。オプティマイザーヒントを無効にするには 1(既定値)を設定します。",
saveAndConnect: "保存して接続",
save: "保存",
editTitle: "接続を編集",
+2 -2
View File
@@ -1436,8 +1436,8 @@ export default withEnglishFallback({
gaussdbTargetServerTypeSlave: "슬레이브(대기)",
gaussdbTargetServerTypeAny: "임의(선호 없음)",
gaussdbTargetServerTypeHint: "JDBC 드라이버가 연결할 GaussDB 클러스터 노드를 제어합니다. M 모드(JDBC)에만 적용됩니다.",
gaussdbCountQueryDop: "COUNT(*) 병렬 처리 수준",
gaussdbCountQueryDopHint: "대형 테이블의 COUNT(*) 쿼리에 query_dop(병렬 처리 수준)를 설정합니다. 값이 클수록 더 많은 리소스를 사용합니다. 옵티마이저 힌트를 비활성화하려면 1(기본값)로 설정하세요.",
gaussdbCountQueryDop: "쿼리 병렬 처리 수준 (query_dop)",
gaussdbCountQueryDopHint: "대형 테이블의 행 수 계산 및 결과 페이징 쿼리에 query_dop(병렬 처리 수준)를 설정합니다. 값이 클수록 더 많은 리소스를 사용합니다. 옵티마이저 힌트를 비활성화하려면 1(기본값)로 설정하세요.",
saveAndConnect: "저장 후 연결",
save: "저장",
editTitle: "연결 편집",
+2 -2
View File
@@ -1358,8 +1358,8 @@ export default withEnglishFallback({
gaussdbTargetServerTypeSlave: "Slave (standby)",
gaussdbTargetServerTypeAny: "Qualquer (sem preferência)",
gaussdbTargetServerTypeHint: "Controla a qual nó do cluster GaussDB o driver JDBC se conecta. Aplica-se apenas ao modo M (JDBC).",
gaussdbCountQueryDop: "Paralelismo de COUNT(*)",
gaussdbCountQueryDopHint: "Define query_dop (grau de paralelismo) para consultas COUNT(*) em tabelas grandes. Valores maiores usam mais recursos. Defina como 1 (padrão) para desativar a dica do otimizador.",
gaussdbCountQueryDop: "Paralelismo de consultas (query_dop)",
gaussdbCountQueryDopHint: "Define query_dop (grau de paralelismo) para contagem de linhas e paginação de resultados em tabelas grandes. Valores maiores usam mais recursos. Defina como 1 (padrão) para desativar a dica do otimizador.",
saveAndConnect: "Salvar e Conectar",
save: "Salvar",
editTitle: "Editar Conexão",
+2 -2
View File
@@ -1533,8 +1533,8 @@ export default withEnglishFallback({
gaussdbTargetServerTypeSlave: "Slave (резервный)",
gaussdbTargetServerTypeAny: "Любой (без предпочтений)",
gaussdbTargetServerTypeHint: "Управляет тем, к какому узлу кластера GaussDB подключается JDBC-драйвер. Master пробует основной, slave — резервный, any подключается к доступному. Применяется только в режиме M (JDBC).",
gaussdbCountQueryDop: "Параллелизм COUNT(*)",
gaussdbCountQueryDopHint: "Задаёт query_dop (степень параллелизма) для запросов COUNT(*) на больших таблицах. Большие значения используют больше ресурсов. Установите 1 (по умолчанию), чтобы отключить подсказку оптимизатору.",
gaussdbCountQueryDop: "Параллелизм запросов (query_dop)",
gaussdbCountQueryDopHint: "Задаёт query_dop (степень параллелизма) для подсчёта строк и разбиения результатов на страницы на больших таблицах. Большие значения используют больше ресурсов. Установите 1 (по умолчанию), чтобы отключить подсказку оптимизатору.",
saveAndConnect: "Сохранить и подключиться",
save: "Сохранить",
editTitle: "Изменить подключение",
+2 -2
View File
@@ -1730,8 +1730,8 @@ export default withEnglishFallback({
gaussdbTargetServerTypeSlave: "Slave (สำรอง)",
gaussdbTargetServerTypeAny: "ใดก็ได้ (ไม่ต้องการเป็นพิเศษ)",
gaussdbTargetServerTypeHint: "ควบคุมว่า Driver JDBC จะเชื่อมต่อกับ Node ใดใน Cluster GaussDB Master จะลอง Node หลัก Slave จะลอง Node สำรอง และ Any จะเชื่อมต่อ Node ที่พร้อมใช้งาน ใช้เฉพาะโหมด M (JDBC)",
gaussdbCountQueryDop: "การทำงานแบบขนานของ COUNT(*)",
gaussdbCountQueryDopHint: "ตั้งค่า query_dop (ระดับการทำงานแบบขนาน) สำหรับ Query COUNT(*) บนตารางขนาดใหญ่ ค่าสูงใช้ทรัพยากรมากขึ้น ตั้งเป็น 1 (ค่าเริ่มต้น) เพื่อปิดคำแนะนำ Optimizer",
gaussdbCountQueryDop: "ความขนานของคิวรี (query_dop)",
gaussdbCountQueryDopHint: "ตั้งค่า query_dop (ระดับการทำงานแบบขนาน) สำหรับการนับแถวและการแบ่งหน้าผลลัพธ์บนตารางขนาดใหญ่ ค่าสูงใช้ทรัพยากรมากขึ้น ตั้งเป็น 1 (ค่าเริ่มต้น) เพื่อปิดคำแนะนำ Optimizer",
saveAndConnect: "บันทึกและเชื่อมต่อ",
save: "บันทึก",
editTitle: "แก้ไขการเชื่อมต่อ",
+2 -2
View File
@@ -1132,8 +1132,8 @@ export default withEnglishFallback({
gaussdbTargetServerTypeSlave: "Slave (yedek)",
gaussdbTargetServerTypeAny: "Herhangi biri (tercih yok)",
gaussdbTargetServerTypeHint: "JDBC sürücüsünün bir GaussDB kümesinde hangi düğüme bağlanacağını belirler. Master birincil düğümü, slave bir yedeği dener, herhangi biri ise erişilebilir olana bağlanır. Yalnızca M (JDBC) modunda geçerlidir.",
gaussdbCountQueryDop: "COUNT(*) paralelliği",
gaussdbCountQueryDopHint: "Büyük tablolardaki COUNT(*) sorguları için query_dop (paralellik derecesi) değerini ayarlar. Yüksek değerler daha çok kaynak kullanır. İyileştirici ipucunu kapatmak için 1 (varsayılan) yapın.",
gaussdbCountQueryDop: "Sorgu paralelliği (query_dop)",
gaussdbCountQueryDopHint: "Büyük tablolarda satır sayma ve sonuç sayfalama sorguları için query_dop (paralellik derecesi) değerini ayarlar. Yüksek değerler daha çok kaynak kullanır. İyileştirici ipucunu kapatmak için 1 (varsayılan) yapın.",
saveAndConnect: "Kaydet ve Bağlan",
save: "Kaydet",
editTitle: "Bağlantıyı Düzenle",
+2 -2
View File
@@ -1703,8 +1703,8 @@ export default withEnglishFallback({
gaussdbTargetServerTypeSlave: "备节点(Standby)",
gaussdbTargetServerTypeAny: "任意(不指定)",
gaussdbTargetServerTypeHint: "控制 JDBC 驱动连接到 GaussDB 集群中的哪个节点。主节点优先连接主库,备节点连接备库,任意节点连接可用节点。仅适用于 M 模式(JDBC)。",
gaussdbCountQueryDop: "COUNT(*) 并行度",
gaussdbCountQueryDopHint: "设置 COUNT(*) 查询的 query_dop(并行度),用于大表行数统计。较高的值会消耗更多资源。设置为 1(默认)则不传入优化器提示。",
gaussdbCountQueryDop: "查询并行度 (query_dop)",
gaussdbCountQueryDopHint: "设置行数统计与结果分页查询的 query_dop(并行度),用于大表数据量较大的场景。较高的值会消耗更多资源。设置为 1(默认)则不传入优化器提示。",
saveAndConnect: "保存并连接",
save: "保存",
editTitle: "编辑连接",
+2 -2
View File
@@ -1367,8 +1367,8 @@ export default withEnglishFallback({
gaussdbTargetServerTypeSlave: "備節點(Standby)",
gaussdbTargetServerTypeAny: "任意(不指定)",
gaussdbTargetServerTypeHint: "控制 JDBC 驅動程式連線到 GaussDB 叢集中的哪個節點。僅適用於 M 模式(JDBC)。",
gaussdbCountQueryDop: "COUNT(*) 平行度",
gaussdbCountQueryDopHint: "設定大型資料表 COUNT(*) 查詢的 query_dop(平行度)。較高的值會使用更多資源。設定為 1(預設值)可停用最佳化器提示。",
gaussdbCountQueryDop: "查詢平行度 (query_dop)",
gaussdbCountQueryDopHint: "設定行數統計與結果分頁查詢的 query_dop(平行度),用於大型資料表。較高的值會使用更多資源。設定為 1(預設值)可停用最佳化器提示。",
saveAndConnect: "儲存並連線",
save: "儲存",
editTitle: "編輯連線",
+3
View File
@@ -536,6 +536,9 @@ export interface QueryPaginationExecutionPlanOptions {
pagination: QueryPagination;
useAgentCursor: boolean;
firstPageUsesActualSql?: boolean;
/** Optimizer hint injected into the generated count (dbx_count) and page (dbx_page) SQL.
* Example: GaussDB parallel execution hint: + set(query_dop 8). */
executionHint?: string;
}
export interface QueryPaginationExecutionPlan {
@@ -2679,6 +2679,35 @@ describe("queryStore hidden primary key editing", () => {
);
});
it.each([
["disabled by default", undefined, undefined],
["explicitly enabled", { gaussdbCountQueryDop: 8 }, "/*+ set(query_dop 8) */"],
])("passes the GaussDB query_dop hint to query-result count and page planning %s", async (_label, externalConfig, expectedHint) => {
getConnectionConfig.mockReturnValue({
id: "gaussdb-1",
name: "GaussDB",
db_type: "gaussdb",
database: "app",
query_timeout_secs: 30,
external_config: externalConfig,
});
const { useQueryStore } = await import("@/stores/queryStore");
const store = useQueryStore();
const tabId = store.createTab("gaussdb-1", "app", "Query");
await store.executeTabSql(tabId, "SELECT id, name FROM users", {
pagination: { limit: 100, offset: 0 },
});
expect(prepareQueryPaginationExecutionPlan).toHaveBeenCalledWith(
expect.objectContaining({
databaseType: "gaussdb",
queryBaseSql: "SELECT id, name FROM users",
executionHint: expectedHint,
}),
);
});
it("advances an OceanBase duplicate projection cursor through an offset jump and next page", async () => {
getConnectionConfig.mockReturnValue({ id: "ob-1", name: "OceanBase", db_type: "oceanbase-oracle", database: "app", query_timeout_secs: 30 });
analyzeEditableQueryEditability.mockResolvedValue({ editable: false, reason: "complex-query" });
+2
View File
@@ -8403,6 +8403,7 @@ export const useQueryStore = defineStore("query", () => {
pagination,
useAgentCursor,
firstPageUsesActualSql: hiddenPrimaryKeys.length > 0,
executionHint: effectiveDbType === "gaussdb" ? gaussdbCountQueryDopHint(conn) : undefined,
});
const canPaginateSqlServerUseScript = !!sqlServerUseScript && !!plan.pageSql && typeof plan.pageLimit === "number" && typeof plan.pageOffset === "number";
if (plan.paginationError) throw new Error(plan.paginationError);
@@ -10238,6 +10239,7 @@ export const useQueryStore = defineStore("query", () => {
pagination: { limit: effectivePageLimit, offset, sessionId },
useAgentCursor,
firstPageUsesActualSql: true,
executionHint: effectiveDbType === "gaussdb" ? gaussdbCountQueryDopHint(conn) : undefined,
});
if (probeRowLimit && plan.paginationError) throw new Error(plan.paginationError);
if (typeof plan.pageLimit !== "number" || typeof plan.pageOffset !== "number") return cachedResultForExport();
@@ -695,6 +695,7 @@ fn supports_streaming_offset_pagination(request: &QueryResultExportRequest, page
pagination: QueryPagination { limit: page_size, offset: 0, session_id: None },
use_agent_cursor: false,
first_page_uses_actual_sql: true,
execution_hint: None,
});
let second_page = build_query_pagination_execution_plan(QueryPaginationExecutionPlanOptions {
sql: request.sql.clone(),
@@ -703,6 +704,7 @@ fn supports_streaming_offset_pagination(request: &QueryResultExportRequest, page
pagination: QueryPagination { limit: page_size, offset: page_size, session_id: None },
use_agent_cursor: false,
first_page_uses_actual_sql: true,
execution_hint: None,
});
let (Some(first_sql), Some(second_sql)) = (first_page.page_sql.as_deref(), second_page.page_sql.as_deref()) else {
@@ -1064,6 +1066,7 @@ async fn export_query_result_core_inner(
pagination: QueryPagination { limit: this_page, offset, session_id: session_id.clone() },
use_agent_cursor,
first_page_uses_actual_sql: true,
execution_hint: None,
});
if let Some(error) = plan.pagination_error {
return Err(error);
+178 -3
View File
@@ -46,6 +46,13 @@ pub struct QueryPaginationExecutionPlanOptions {
pub use_agent_cursor: bool,
#[serde(default)]
pub first_page_uses_actual_sql: bool,
/// Optional optimizer hint injected into the generated count (`dbx_count`)
/// and page (`dbx_page`) statements — e.g. GaussDB `/*+ set(query_dop N) */`
/// for parallel COUNT(*) and pagination. Only set for databases that accept
/// hints after the SELECT keyword (GaussDB/OpenGauss); other dialects and
/// callers without the feature keep the SQL unchanged.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub execution_hint: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
@@ -164,6 +171,7 @@ pub fn build_query_pagination_execution_plan(
pagination_row_number_column: None,
single_execution: false,
};
let execution_hint = options.execution_hint.as_deref().filter(|hint| !hint.trim().is_empty());
let sql_server_cte =
options.database_type == Some(DatabaseType::SqlServer) && starts_with_cte(&options.query_base_sql);
@@ -173,7 +181,10 @@ pub fn build_query_pagination_execution_plan(
database_type: options.database_type,
});
if counted.ok {
plan.count_sql = counted.sql;
plan.count_sql = counted.sql.map(|count_sql| match execution_hint {
Some(hint) => inject_execution_hint(&count_sql, hint),
None => count_sql,
});
}
}
@@ -231,8 +242,14 @@ pub fn build_query_pagination_execution_plan(
offset: options.pagination.offset,
});
if paginated.ok {
plan.sql_to_execute = paginated.sql.clone().unwrap_or_default();
plan.page_sql = paginated.sql;
// The executed statement and the recorded page SQL are the same string;
// build it once so the optimizer hint is applied consistently to both.
let page_sql = paginated.sql.clone().map(|sql| match execution_hint {
Some(hint) => inject_execution_hint(&sql, hint),
None => sql,
});
plan.sql_to_execute = page_sql.clone().unwrap_or_default();
plan.page_sql = page_sql;
plan.page_limit = Some(options.pagination.limit);
plan.page_offset = Some(options.pagination.offset);
if options.pagination.offset > 0
@@ -2385,6 +2402,30 @@ fn derived_table_sql(prefix: &str, statement: &str, suffix: &str) -> String {
format!("{prefix} ({}) {suffix}", statement_for_sql_suffix(statement))
}
/// Insert an optimizer hint (e.g. GaussDB `/*+ set(query_dop N) */`) right after
/// the top-level SELECT keyword that opens the statement's outermost projection.
///
/// GaussDB only accepts hints between the SELECT keyword and the select list, and
/// the tokenizer skips comments and parenthesised subqueries, so the first
/// top-level SELECT token is the right anchor even for statements with CTEs or
/// set operators. Callers only pass hints for databases that accept them; without
/// a hint this is a no-op passthrough.
fn inject_execution_hint(statement: &str, hint: &str) -> String {
let Some(select_end) = top_level_sql_tokens(statement)
.into_iter()
.find(|token| token.text == "SELECT")
.map(|token| token.start + token.text.len())
else {
return statement.to_string();
};
let (head, tail) = statement.split_at(select_end);
if tail.starts_with(char::is_whitespace) {
format!("{head} {hint}{tail}")
} else {
format!("{head} {hint} {tail}")
}
}
fn append_sql_suffix(statement: &str, suffix: &str) -> String {
let separator = if sql_suffix_needs_newline(statement) { "\n" } else { " " };
format!("{}{separator}{}", statement.trim_end(), suffix.trim_start())
@@ -2815,6 +2856,7 @@ mod tests {
pagination: QueryPagination { limit, offset, session_id: None },
use_agent_cursor: cursor,
first_page_uses_actual_sql: true,
execution_hint: None,
})
}
@@ -2893,6 +2935,7 @@ mod tests {
},
use_agent_cursor: true,
first_page_uses_actual_sql: true,
execution_hint: None,
};
let plan = build_query_pagination_execution_plan(options.clone());
assert_eq!(plan.sql_to_execute, sql);
@@ -2924,6 +2967,7 @@ mod tests {
pagination: QueryPagination { limit: 10, offset: 50, session_id: Some("cursor".into()) },
use_agent_cursor: false,
first_page_uses_actual_sql: false,
execution_hint: None,
});
assert!(plan.use_agent_result_session);
assert_eq!(plan.sql_to_execute, sql);
@@ -3041,6 +3085,7 @@ mod tests {
pagination: super::QueryPagination { limit: 100, offset, session_id: None },
use_agent_cursor: true,
first_page_uses_actual_sql: true,
execution_hint: None,
});
assert!(plan.use_agent_result_session, "{sql} offset {offset}");
assert_eq!(plan.sql_to_execute, sql);
@@ -3081,6 +3126,7 @@ mod tests {
pagination: QueryPagination { limit: 500, offset: 0, session_id: None },
use_agent_cursor: false,
first_page_uses_actual_sql: false,
execution_hint: None,
});
let page_sql = plan.page_sql.expect("pagination plan provides page SQL");
@@ -3590,6 +3636,7 @@ mod tests {
pagination: QueryPagination { limit: 100, offset: 100, session_id: None },
use_agent_cursor: false,
first_page_uses_actual_sql: false,
execution_hint: None,
});
assert_eq!(
@@ -3620,6 +3667,7 @@ mod tests {
pagination: QueryPagination { limit: 100, offset: 0, session_id: None },
use_agent_cursor: false,
first_page_uses_actual_sql: false,
execution_hint: None,
});
assert_eq!(first_page.sql_to_execute, sql);
assert!(first_page.page_sql.is_none());
@@ -3632,6 +3680,7 @@ mod tests {
pagination: QueryPagination { limit: 100, offset: 100, session_id: None },
use_agent_cursor: false,
first_page_uses_actual_sql: false,
execution_hint: None,
});
assert!(later_page.sql_to_execute.starts_with("EXEC sys.sp_executesql N'SET ROWCOUNT 200; "));
assert_eq!(sqlserver_result_offset(&later_page.sql_to_execute), 100);
@@ -3681,6 +3730,7 @@ mod tests {
pagination: QueryPagination { limit: 1000, offset: 0, session_id: None },
use_agent_cursor: false,
first_page_uses_actual_sql: false,
execution_hint: None,
});
assert_eq!(plan.count_sql.as_deref(), Some("SELECT COUNT(*) AS dbx_total_rows FROM s_room sr WHERE sr.RoomInfo LIKE N'%合肥天珺一期%' AND sr.x_JtRoomCode IS NULL;"));
assert!(plan.sql_to_execute.starts_with("SELECT TOP (1000)"));
@@ -3730,6 +3780,7 @@ mod tests {
pagination: QueryPagination { limit: 500, offset: 0, session_id: None },
use_agent_cursor: false,
first_page_uses_actual_sql: false,
execution_hint: None,
});
assert_eq!(
@@ -3852,6 +3903,7 @@ mod tests {
pagination: QueryPagination { limit: 100, offset: 100, session_id: None },
use_agent_cursor: false,
first_page_uses_actual_sql: false,
execution_hint: None,
});
assert!(plan.sql_to_execute.contains("ROW_NUMBER()"));
@@ -3871,6 +3923,7 @@ mod tests {
pagination: QueryPagination { limit: 100, offset: 0, session_id: None },
use_agent_cursor: false,
first_page_uses_actual_sql: false,
execution_hint: None,
});
assert_eq!(plan.sql_to_execute, sql);
@@ -3908,6 +3961,7 @@ mod tests {
pagination: QueryPagination { limit: 100, offset: 0, session_id: None },
use_agent_cursor: false,
first_page_uses_actual_sql: false,
execution_hint: None,
});
assert_eq!(plan.sql_to_execute, sql);
@@ -3945,6 +3999,7 @@ mod tests {
pagination: QueryPagination { limit: 100, offset: 0, session_id: None },
use_agent_cursor: false,
first_page_uses_actual_sql: false,
execution_hint: None,
});
assert!(plan.page_sql.is_some());
@@ -4080,6 +4135,7 @@ mod tests {
pagination: QueryPagination { limit: 100, offset: 0, session_id: None },
use_agent_cursor: false,
first_page_uses_actual_sql: false,
execution_hint: None,
});
assert_eq!(plan.sql_to_execute, sql);
@@ -4104,6 +4160,7 @@ mod tests {
pagination: QueryPagination { limit: 100, offset: 0, session_id: None },
use_agent_cursor: false,
first_page_uses_actual_sql: false,
execution_hint: None,
});
assert_eq!(plan.sql_to_execute, sql);
@@ -4165,6 +4222,7 @@ WHERE u.id = picked.id;
pagination: QueryPagination { limit: 100, offset: 0, session_id: None },
use_agent_cursor: false,
first_page_uses_actual_sql: false,
execution_hint: None,
});
assert_eq!(plan.sql_to_execute, sql);
@@ -4275,6 +4333,7 @@ WHERE u.id = picked.id;
pagination: QueryPagination { limit: 100, offset: 0, session_id: None },
use_agent_cursor: false,
first_page_uses_actual_sql: false,
execution_hint: None,
});
assert_eq!(plan.sql_to_execute, "SELECT * FROM system.clusters LIMIT 100 SETTINGS max_execution_time = 0;");
@@ -4309,6 +4368,7 @@ WHERE u.id = picked.id;
pagination: QueryPagination { limit: 100, offset: 0, session_id: None },
use_agent_cursor: false,
first_page_uses_actual_sql: false,
execution_hint: None,
});
assert_eq!(plan.sql_to_execute, "SELECT * FROM `test`\nwhere id=1 LIMIT 100 for update;");
@@ -4332,6 +4392,7 @@ WHERE u.id = picked.id;
pagination: QueryPagination { limit: 100, offset: 100, session_id: None },
use_agent_cursor: false,
first_page_uses_actual_sql: false,
execution_hint: None,
});
assert_eq!(
@@ -4858,6 +4919,7 @@ WHERE u.id = picked.id;
pagination: QueryPagination { limit: 100, offset: 0, session_id: None },
use_agent_cursor: false,
first_page_uses_actual_sql: false,
execution_hint: None,
});
assert_eq!(plan.sql_to_execute, "/*sets:allsets*/select @@server_id LIMIT 100;");
@@ -5511,6 +5573,7 @@ WHERE u.id = picked.id;
pagination: QueryPagination { limit: 100, offset: 0, session_id: None },
use_agent_cursor: true,
first_page_uses_actual_sql: false,
execution_hint: None,
});
assert_eq!(
@@ -5630,6 +5693,7 @@ WHERE u.id = picked.id;
pagination: QueryPagination { limit: 500, offset: 0, session_id: None },
use_agent_cursor: true,
first_page_uses_actual_sql: false,
execution_hint: None,
});
assert_eq!(plan.sql_to_execute, "SELECT * FROM events");
@@ -5648,6 +5712,7 @@ WHERE u.id = picked.id;
pagination: QueryPagination { limit: 100, offset: 200, session_id: None },
use_agent_cursor: true,
first_page_uses_actual_sql: false,
execution_hint: None,
});
// Oracle user queries are Unbounded: the SQL stays unchanged, so the
@@ -5667,6 +5732,7 @@ WHERE u.id = picked.id;
pagination: QueryPagination { limit: 100, offset: 200, session_id: None },
use_agent_cursor: true,
first_page_uses_actual_sql: false,
execution_hint: None,
});
assert!(plan.use_agent_result_session);
@@ -5682,6 +5748,7 @@ WHERE u.id = picked.id;
pagination: QueryPagination { limit: 100, offset: 200, session_id: None },
use_agent_cursor: true,
first_page_uses_actual_sql: false,
execution_hint: None,
});
assert_eq!(plan.sql_to_execute, "SELECT * FROM events;");
@@ -5701,6 +5768,7 @@ WHERE u.id = picked.id;
pagination: QueryPagination { limit: 100, offset: 200, session_id: None },
use_agent_cursor: true,
first_page_uses_actual_sql: false,
execution_hint: None,
});
assert!(plan.sql_to_execute.contains("LIMIT"));
@@ -5716,6 +5784,7 @@ WHERE u.id = picked.id;
pagination: QueryPagination { limit: 500, offset: 0, session_id: None },
use_agent_cursor: true,
first_page_uses_actual_sql: false,
execution_hint: None,
});
assert_eq!(first_page.sql_to_execute, "SELECT * FROM events LIMIT 500;");
@@ -5731,6 +5800,7 @@ WHERE u.id = picked.id;
pagination: QueryPagination { limit: 500, offset: 500, session_id: None },
use_agent_cursor: true,
first_page_uses_actual_sql: false,
execution_hint: None,
});
assert_eq!(second_page.sql_to_execute, "SELECT * FROM events LIMIT 500 OFFSET 500;");
@@ -5749,6 +5819,7 @@ WHERE u.id = picked.id;
pagination: QueryPagination { limit: 500, offset: 0, session_id: None },
use_agent_cursor: true,
first_page_uses_actual_sql: false,
execution_hint: None,
});
assert_eq!(first_page.sql_to_execute, "SELECT * FROM events FETCH FIRST 500 ROWS ONLY;");
@@ -5764,6 +5835,7 @@ WHERE u.id = picked.id;
pagination: QueryPagination { limit: 500, offset: 500, session_id: None },
use_agent_cursor: true,
first_page_uses_actual_sql: false,
execution_hint: None,
});
assert_eq!(second_page.sql_to_execute, "SELECT * FROM events OFFSET 500 ROWS FETCH FIRST 500 ROWS ONLY;");
@@ -5780,6 +5852,7 @@ WHERE u.id = picked.id;
pagination: QueryPagination { limit: 500, offset: 0, session_id: None },
use_agent_cursor: true,
first_page_uses_actual_sql: false,
execution_hint: None,
});
assert_eq!(fallback.sql_to_execute, sql);
@@ -5796,6 +5869,7 @@ WHERE u.id = picked.id;
pagination: QueryPagination { limit: 100, offset: 0, session_id: None },
use_agent_cursor: true,
first_page_uses_actual_sql: false,
execution_hint: None,
});
assert_eq!(first_page.sql_to_execute, "SELECT * FROM events LIMIT 100;");
@@ -5811,6 +5885,7 @@ WHERE u.id = picked.id;
pagination: QueryPagination { limit: 100, offset: 100, session_id: None },
use_agent_cursor: true,
first_page_uses_actual_sql: false,
execution_hint: None,
});
assert_eq!(second_page.sql_to_execute, "SELECT * FROM events LIMIT 100 OFFSET 100;");
@@ -5830,6 +5905,7 @@ WHERE u.id = picked.id;
pagination: QueryPagination { limit: 100, offset: 0, session_id: None },
use_agent_cursor: true,
first_page_uses_actual_sql: false,
execution_hint: None,
});
assert_eq!(plan.sql_to_execute, sql);
@@ -5851,6 +5927,7 @@ WHERE u.id = picked.id;
pagination: QueryPagination { limit: 100, offset: 0, session_id: None },
use_agent_cursor: true,
first_page_uses_actual_sql: false,
execution_hint: None,
});
assert_eq!(plan.sql_to_execute, sql);
@@ -5872,6 +5949,7 @@ WHERE u.id = picked.id;
pagination: QueryPagination { limit: 100, offset: 0, session_id: None },
use_agent_cursor: false,
first_page_uses_actual_sql: true,
execution_hint: None,
});
assert_eq!(plan.sql_to_execute, sql);
@@ -5908,6 +5986,7 @@ WHERE u.id = picked.id;
pagination: QueryPagination { limit: 500, offset: 0, session_id: None },
use_agent_cursor: true,
first_page_uses_actual_sql: false,
execution_hint: None,
});
assert_eq!(first_page.sql_to_execute, "SELECT * FROM (SELECT id FROM events) WHERE ROWNUM <= 500;");
assert_eq!(first_page.page_sql.as_deref(), Some(first_page.sql_to_execute.as_str()));
@@ -5924,6 +6003,7 @@ WHERE u.id = picked.id;
pagination: QueryPagination { limit: 500, offset: 500, session_id: None },
use_agent_cursor: true,
first_page_uses_actual_sql: false,
execution_hint: None,
});
assert!(next_page.sql_to_execute.contains("ROWNUM <= 1000"));
assert!(next_page.sql_to_execute.contains("\"__dbx_row_num\" > 500"));
@@ -5939,6 +6019,7 @@ WHERE u.id = picked.id;
pagination: QueryPagination { limit: 500, offset: 0, session_id: None },
use_agent_cursor: true,
first_page_uses_actual_sql: false,
execution_hint: None,
});
assert_eq!(fallback.sql_to_execute, unrewritable);
assert!(fallback.page_sql.is_none());
@@ -5955,6 +6036,7 @@ WHERE u.id = picked.id;
pagination: QueryPagination { limit: 5, offset: 5, session_id: None },
use_agent_cursor: true,
first_page_uses_actual_sql: false,
execution_hint: None,
});
assert_eq!(plan.pagination_row_number_column.as_deref(), Some("__dbx_row_num_1"));
assert!(plan.sql_to_execute.contains("ROWNUM AS \"__dbx_row_num_1\""));
@@ -5972,6 +6054,7 @@ WHERE u.id = picked.id;
pagination: QueryPagination { limit: 500, offset: 500, session_id: None },
use_agent_cursor: true,
first_page_uses_actual_sql: false,
execution_hint: None,
});
assert!(plan.pagination_row_number_column.is_none());
assert!(plan.use_agent_result_session);
@@ -5989,6 +6072,7 @@ WHERE u.id = picked.id;
pagination: QueryPagination { limit: 500, offset: 0, session_id: None },
use_agent_cursor: true,
first_page_uses_actual_sql: false,
execution_hint: None,
});
assert_eq!(plan.sql_to_execute, sql);
@@ -6007,6 +6091,7 @@ WHERE u.id = picked.id;
pagination: QueryPagination { limit: 500, offset: 0, session_id: None },
use_agent_cursor: true,
first_page_uses_actual_sql: false,
execution_hint: None,
});
assert_eq!(plan.sql_to_execute, "SELECT * FROM events ORDER BY id LIMIT 500;");
@@ -6025,6 +6110,7 @@ WHERE u.id = picked.id;
pagination: QueryPagination { limit: 500, offset: 0, session_id: None },
use_agent_cursor: true,
first_page_uses_actual_sql: false,
execution_hint: None,
});
assert_eq!(plan.sql_to_execute, "SELECT * FROM events");
@@ -6044,6 +6130,7 @@ WHERE u.id = picked.id;
pagination: QueryPagination { limit: 500, offset: 0, session_id: None },
use_agent_cursor: true,
first_page_uses_actual_sql: false,
execution_hint: None,
});
assert_eq!(plan.sql_to_execute, sql);
@@ -6070,6 +6157,7 @@ WHERE u.id = picked.id;
pagination: QueryPagination { limit: 500, offset: 0, session_id: None },
use_agent_cursor: true,
first_page_uses_actual_sql: false,
execution_hint: None,
});
assert_eq!(plan.sql_to_execute, sql, "sql_to_execute should stay untouched for {sql}");
@@ -6089,6 +6177,7 @@ WHERE u.id = picked.id;
pagination: QueryPagination { limit: 500, offset: 0, session_id: None },
use_agent_cursor: true,
first_page_uses_actual_sql: false,
execution_hint: None,
});
assert_eq!(plan.sql_to_execute, "SELECT * FROM (SELECT TOP 100 * FROM events) t");
@@ -6118,6 +6207,7 @@ WHERE u.id = picked.id;
pagination: QueryPagination { limit: 500, offset: 0, session_id: None },
use_agent_cursor: false,
first_page_uses_actual_sql: true,
execution_hint: None,
});
assert_eq!(plan.sql_to_execute, sql, "sql_to_execute should stay untouched for {sql}");
@@ -6140,6 +6230,7 @@ WHERE u.id = picked.id;
pagination: QueryPagination { limit: 500, offset: 0, session_id: None },
use_agent_cursor: true,
first_page_uses_actual_sql: false,
execution_hint: None,
});
assert_eq!(plan.sql_to_execute, "SELECT TOP 100 * FROM events");
@@ -6216,6 +6307,7 @@ WHERE u.id = picked.id;
pagination: QueryPagination { limit: 100, offset: 0, session_id: None },
use_agent_cursor: false,
first_page_uses_actual_sql: false,
execution_hint: None,
});
assert_eq!(plan.exact_query_row_bound, expected, "unexpected bound for {database_type:?}: {sql}");
@@ -6239,6 +6331,7 @@ WHERE u.id = picked.id;
pagination: QueryPagination { limit: 500, offset: 0, session_id: None },
use_agent_cursor: false,
first_page_uses_actual_sql: false,
execution_hint: None,
});
assert_eq!(plan.sql_to_execute, "SELECT top, name FROM users LIMIT 500;");
@@ -6257,6 +6350,7 @@ WHERE u.id = picked.id;
pagination: QueryPagination { limit: 500, offset: 0, session_id: None },
use_agent_cursor: true,
first_page_uses_actual_sql: true,
execution_hint: None,
});
assert_eq!(plan.sql_to_execute, "SELECT * FROM (SELECT * FROM events) t ORDER BY created_at DESC");
@@ -7291,4 +7385,85 @@ WHERE u.id = picked.id;
fn union_query_is_not_treated_as_dedup() {
assert_eq!(dedup_projection_count_without_order_by("SELECT a FROM t1 UNION SELECT b FROM t2"), None);
}
#[test]
fn gaussdb_execution_hint_flows_into_count_and_page_sql() {
let gaussdb = |sql: &str, limit: usize, offset: usize| {
build_query_pagination_execution_plan(QueryPaginationExecutionPlanOptions {
sql: sql.into(),
query_base_sql: sql.into(),
database_type: Some(DatabaseType::Gaussdb),
pagination: QueryPagination { limit, offset, session_id: None },
use_agent_cursor: false,
first_page_uses_actual_sql: true,
execution_hint: Some("/*+ set(query_dop 8) */".to_string()),
})
};
// Plain statement: the hint lands after the outer SELECT of page and count SQL.
let plan = gaussdb("SELECT id FROM users WHERE active = 1", 20, 0);
assert_eq!(plan.sql_to_execute, "SELECT /*+ set(query_dop 8) */ id FROM users WHERE active = 1 LIMIT 20;");
assert_eq!(
plan.page_sql.as_deref(),
Some("SELECT /*+ set(query_dop 8) */ id FROM users WHERE active = 1 LIMIT 20;")
);
assert_eq!(
plan.count_sql.as_deref(),
Some(
"SELECT /*+ set(query_dop 8) */ COUNT(*) AS dbx_total_rows FROM (SELECT id FROM users WHERE active = 1) dbx_count;"
)
);
// A user-written LIMIT forces the `dbx_page` wrapper; the hint stays on the outer SELECT.
let plan = gaussdb("SELECT id FROM users WHERE active = 1 LIMIT 1000", 20, 20);
assert_eq!(
plan.sql_to_execute,
"SELECT /*+ set(query_dop 8) */ * FROM (SELECT id FROM users WHERE active = 1 LIMIT 1000) dbx_page LIMIT 20 OFFSET 20;"
);
// CTEs keep the hint on the main projection, not inside the CTE body.
let plan = gaussdb("WITH recent AS (SELECT id FROM users) SELECT * FROM recent", 10, 0);
assert_eq!(
plan.sql_to_execute,
"WITH recent AS (SELECT id FROM users) SELECT /*+ set(query_dop 8) */ * FROM recent LIMIT 10;"
);
assert_eq!(
plan.count_sql.as_deref(),
Some(
"SELECT /*+ set(query_dop 8) */ COUNT(*) AS dbx_total_rows FROM (WITH recent AS (SELECT id FROM users) SELECT * FROM recent) dbx_count;"
)
);
}
#[test]
fn gaussdb_execution_hint_is_skipped_without_setting_or_on_other_dialects() {
let plan = build_query_pagination_execution_plan(QueryPaginationExecutionPlanOptions {
sql: "SELECT id FROM users".to_string(),
query_base_sql: "SELECT id FROM users".to_string(),
database_type: Some(DatabaseType::Gaussdb),
pagination: QueryPagination { limit: 20, offset: 0, session_id: None },
use_agent_cursor: false,
first_page_uses_actual_sql: true,
execution_hint: None,
});
assert_eq!(plan.sql_to_execute, "SELECT id FROM users LIMIT 20;");
assert!(plan.page_sql.as_deref().is_some_and(|sql| !sql.contains("query_dop")));
assert!(plan.count_sql.as_deref().is_some_and(|sql| !sql.contains("query_dop")));
// MySQL keeps its own derived-table quoting; with no hint the SQL is unchanged.
let plan = build_query_pagination_execution_plan(QueryPaginationExecutionPlanOptions {
sql: "SELECT id FROM users".to_string(),
query_base_sql: "SELECT id FROM users".to_string(),
database_type: Some(DatabaseType::Mysql),
pagination: QueryPagination { limit: 20, offset: 0, session_id: None },
use_agent_cursor: false,
first_page_uses_actual_sql: true,
execution_hint: None,
});
assert_eq!(plan.sql_to_execute, "SELECT id FROM users LIMIT 20;");
assert_eq!(
plan.count_sql.as_deref(),
Some("SELECT COUNT(*) AS dbx_total_rows FROM (SELECT id FROM users) `dbx_count`;")
);
}
}