fix(query): 修复查询结果表头提示与全量排序 (#164)
* fix(grid): 修复查询结果表头提示与排序 修复 MySQL 查询结果列元数据取错库名的问题,并补齐查询结果全量排序链路,避免表头提示与排序行为失效。 * fix(query): 修复结果排序重复列名报错
This commit is contained in:
parent
c036dd19e4
commit
3b4b6c1ff7
|
|
@ -297,7 +297,7 @@ pub async fn get_columns_core(
|
|||
let pool = connections.get(&pool_key).ok_or("Pool not found")?;
|
||||
|
||||
match pool {
|
||||
PoolKind::Mysql(p, _) => db::mysql::get_columns(p, schema, table).await,
|
||||
PoolKind::Mysql(p, _) => db::mysql::get_columns(p, database, table).await,
|
||||
PoolKind::Postgres(p) => db::postgres::get_columns(p, schema, table).await,
|
||||
PoolKind::Sqlite(p) => db::sqlite::get_columns(p, schema, table).await,
|
||||
_ => Ok(vec![]),
|
||||
|
|
|
|||
|
|
@ -98,7 +98,7 @@ const props = defineProps<{
|
|||
const emit = defineEmits<{
|
||||
reload: [sql?: string, whereInput?: string];
|
||||
paginate: [offset: number, limit: number, whereInput?: string, orderBy?: string];
|
||||
sort: [column: string, direction: "asc" | "desc" | null, whereInput?: string];
|
||||
sort: [column: string, columnIndex: number, direction: "asc" | "desc" | null, whereInput?: string];
|
||||
}>();
|
||||
|
||||
const hasData = computed(() => props.result.columns.length > 0);
|
||||
|
|
@ -203,6 +203,7 @@ const showCellDetail = ref(false);
|
|||
const transposeRowIndex = ref<number | null>(null);
|
||||
const showTranspose = ref(false);
|
||||
const sortCol = ref<string | null>(null);
|
||||
const sortColIndex = ref<number | null>(null);
|
||||
const sortDir = ref<"asc" | "desc">("asc");
|
||||
const searchText = ref("");
|
||||
const searchSuggestions = ref<string[]>([]);
|
||||
|
|
@ -599,21 +600,23 @@ const activeCellDetail = computed(() => {
|
|||
};
|
||||
});
|
||||
|
||||
function toggleSort(colName: string) {
|
||||
function toggleSort(colName: string, colIdx: number) {
|
||||
if (isResizing) return;
|
||||
if (sortCol.value === colName) {
|
||||
if (sortCol.value === colName && sortColIndex.value === colIdx) {
|
||||
if (sortDir.value === "asc") {
|
||||
sortDir.value = "desc";
|
||||
emit("sort", colName, "desc", activeWhereInput.value);
|
||||
emit("sort", colName, colIdx, "desc", activeWhereInput.value);
|
||||
} else {
|
||||
sortCol.value = null;
|
||||
sortColIndex.value = null;
|
||||
sortDir.value = "asc";
|
||||
emit("sort", colName, null, activeWhereInput.value);
|
||||
emit("sort", colName, colIdx, null, activeWhereInput.value);
|
||||
}
|
||||
} else {
|
||||
sortCol.value = colName;
|
||||
sortColIndex.value = colIdx;
|
||||
sortDir.value = "asc";
|
||||
emit("sort", colName, "asc", activeWhereInput.value);
|
||||
emit("sort", colName, colIdx, "asc", activeWhereInput.value);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1415,17 +1418,23 @@ defineExpose({
|
|||
>
|
||||
#
|
||||
</div>
|
||||
<Tooltip v-for="(col, colIdx) in result.columns" :key="col">
|
||||
<Tooltip v-for="(col, colIdx) in result.columns" :key="`${col}-${colIdx}`">
|
||||
<TooltipTrigger as-child>
|
||||
<div
|
||||
class="shrink-0 px-3 py-1.5 border-r border-border whitespace-nowrap cursor-pointer hover:bg-accent/50 select-none relative overflow-hidden"
|
||||
:style="{ width: `var(--col-w-${colIdx})` }"
|
||||
@click="toggleSort(col)"
|
||||
@click="toggleSort(col, colIdx)"
|
||||
>
|
||||
<span class="flex min-w-0 items-center gap-1 overflow-hidden">
|
||||
<span class="min-w-0 truncate">{{ col }}</span>
|
||||
<ArrowUp v-if="sortCol === col && sortDir === 'asc'" class="h-3 w-3 shrink-0" />
|
||||
<ArrowDown v-else-if="sortCol === col && sortDir === 'desc'" class="h-3 w-3 shrink-0" />
|
||||
<ArrowUp
|
||||
v-if="sortCol === col && sortColIndex === colIdx && sortDir === 'asc'"
|
||||
class="h-3 w-3 shrink-0"
|
||||
/>
|
||||
<ArrowDown
|
||||
v-else-if="sortCol === col && sortColIndex === colIdx && sortDir === 'desc'"
|
||||
class="h-3 w-3 shrink-0"
|
||||
/>
|
||||
</span>
|
||||
<div
|
||||
class="absolute right-0 top-0 bottom-0 w-1.5 cursor-col-resize hover:bg-primary/30"
|
||||
|
|
@ -1434,12 +1443,14 @@ defineExpose({
|
|||
</div>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent
|
||||
v-if="columnTypeMap.get(col)"
|
||||
v-if="columnTypeMap.get(col) || columnCommentMap.get(col)"
|
||||
side="bottom"
|
||||
class="text-xs grid grid-cols-[auto_1fr] gap-x-2"
|
||||
>
|
||||
<span class="text-muted-foreground">{{ t("grid.columnType") }}</span>
|
||||
<span :class="typeColorClass(columnTypeMap.get(col)!)">{{ columnTypeMap.get(col) }}</span>
|
||||
<template v-if="columnTypeMap.get(col)">
|
||||
<span class="text-muted-foreground">{{ t("grid.columnType") }}</span>
|
||||
<span :class="typeColorClass(columnTypeMap.get(col)!)">{{ columnTypeMap.get(col) }}</span>
|
||||
</template>
|
||||
<template v-if="columnCommentMap.get(col)">
|
||||
<span class="text-muted-foreground">{{ t("grid.columnComment") }}</span>
|
||||
<span>{{ columnCommentMap.get(col) }}</span>
|
||||
|
|
|
|||
|
|
@ -41,7 +41,7 @@ const emit = defineEmits<{
|
|||
formatError: [];
|
||||
reload: [sql?: string, whereInput?: string];
|
||||
paginate: [offset: number, limit: number, whereInput?: string, orderBy?: string];
|
||||
sort: [column: string, direction: "asc" | "desc" | null, whereInput?: string];
|
||||
sort: [column: string, columnIndex: number, direction: "asc" | "desc" | null, whereInput?: string];
|
||||
executeSql: [sql: string];
|
||||
clickTable: [tableName: string];
|
||||
}>();
|
||||
|
|
@ -281,8 +281,8 @@ function onHandleCloseColumnPanel() {
|
|||
emit('paginate', offset, limit, whereInput, orderBy)
|
||||
"
|
||||
@sort="
|
||||
(column: string, direction: 'asc' | 'desc' | null, whereInput?: string) =>
|
||||
emit('sort', column, direction, whereInput)
|
||||
(column: string, columnIndex: number, direction: 'asc' | 'desc' | null, whereInput?: string) =>
|
||||
emit('sort', column, columnIndex, direction, whereInput)
|
||||
"
|
||||
/>
|
||||
<div
|
||||
|
|
@ -374,8 +374,8 @@ function onHandleCloseColumnPanel() {
|
|||
emit('paginate', offset, limit, whereInput, orderBy)
|
||||
"
|
||||
@sort="
|
||||
(column: string, direction: 'asc' | 'desc' | null, whereInput?: string) =>
|
||||
emit('sort', column, direction, whereInput)
|
||||
(column: string, columnIndex: number, direction: 'asc' | 'desc' | null, whereInput?: string) =>
|
||||
emit('sort', column, columnIndex, direction, whereInput)
|
||||
"
|
||||
/>
|
||||
<div
|
||||
|
|
|
|||
|
|
@ -1,10 +1,15 @@
|
|||
import { type ComputedRef } from "vue";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import { useConnectionStore } from "@/stores/connectionStore";
|
||||
import { useQueryStore } from "@/stores/queryStore";
|
||||
import { buildTableSelectSql, quoteTableIdentifier } from "@/lib/tableSelectSql";
|
||||
import { buildSortedQuerySql } from "@/lib/queryResultSort";
|
||||
import type { QueryTab } from "@/types/database";
|
||||
import { useToast } from "@/composables/useToast";
|
||||
|
||||
export function useDataGridActions(activeTab: ComputedRef<QueryTab | undefined>) {
|
||||
const { t } = useI18n();
|
||||
const { toast } = useToast();
|
||||
const connectionStore = useConnectionStore();
|
||||
const queryStore = useQueryStore();
|
||||
|
||||
|
|
@ -44,13 +49,24 @@ export function useDataGridActions(activeTab: ComputedRef<QueryTab | undefined>)
|
|||
if (!tab) return;
|
||||
if (tab.mode === "data" && tab.tableMeta) {
|
||||
queryStore.updateSql(tab.id, buildTableSql(tab, { whereInput }));
|
||||
return queryStore.executeCurrentTab();
|
||||
await queryStore.executeCurrentTab();
|
||||
return;
|
||||
}
|
||||
if (tab.resultSortedSql) {
|
||||
await queryStore.executeTabSql(tab.id, tab.resultSortedSql, {
|
||||
resultBaseSql: tab.resultBaseSql ?? tab.sql,
|
||||
resultSortedSql: tab.resultSortedSql,
|
||||
});
|
||||
return;
|
||||
}
|
||||
// Results mode: re-run only the SQL that produced the current result set
|
||||
if (sql?.trim()) {
|
||||
return queryStore.executeTabSql(tab.id, sql);
|
||||
await queryStore.executeTabSql(tab.id, sql, {
|
||||
resultBaseSql: sql,
|
||||
resultSortedSql: undefined,
|
||||
});
|
||||
return;
|
||||
}
|
||||
return queryStore.executeCurrentTab();
|
||||
await queryStore.executeCurrentTab();
|
||||
}
|
||||
|
||||
async function onPaginate(offset: number, limit: number, whereInput?: string, orderBy?: string) {
|
||||
|
|
@ -61,13 +77,48 @@ export function useDataGridActions(activeTab: ComputedRef<QueryTab | undefined>)
|
|||
await queryStore.executeCurrentTab();
|
||||
}
|
||||
|
||||
async function onSort(column: string, direction: "asc" | "desc" | null, whereInput?: string) {
|
||||
async function onSort(column: string, columnIndex: number, direction: "asc" | "desc" | null, whereInput?: string) {
|
||||
const tab = activeTab.value;
|
||||
if (!tab?.tableMeta) return;
|
||||
const orderBy = direction ? `${quoteIdent(tab, column)} ${direction.toUpperCase()}` : undefined;
|
||||
const sql = buildTableSql(tab, { orderBy, whereInput });
|
||||
queryStore.updateSql(tab.id, sql);
|
||||
await queryStore.executeCurrentTab();
|
||||
if (!tab) return;
|
||||
|
||||
if (tab.mode === "data") {
|
||||
if (!tab.tableMeta) return;
|
||||
const orderBy = direction ? `${quoteIdent(tab, column)} ${direction.toUpperCase()}` : undefined;
|
||||
const sql = buildTableSql(tab, { orderBy, whereInput });
|
||||
queryStore.updateSql(tab.id, sql);
|
||||
await queryStore.executeCurrentTab();
|
||||
return;
|
||||
}
|
||||
|
||||
const baseSql = tab.resultBaseSql ?? tab.sql;
|
||||
if (!baseSql.trim()) return;
|
||||
|
||||
if (!direction) {
|
||||
await queryStore.executeTabSql(tab.id, baseSql, {
|
||||
resultBaseSql: baseSql,
|
||||
resultSortedSql: undefined,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const config = connectionStore.getConfig(tab.connectionId);
|
||||
const built = buildSortedQuerySql(
|
||||
baseSql,
|
||||
config?.db_type,
|
||||
tab.result?.columns ?? [],
|
||||
columnIndex,
|
||||
column,
|
||||
direction,
|
||||
);
|
||||
if (!built.ok) {
|
||||
toast(t("grid.sortUnsupported"), 5000);
|
||||
return;
|
||||
}
|
||||
|
||||
await queryStore.executeTabSql(tab.id, built.sql, {
|
||||
resultBaseSql: baseSql,
|
||||
resultSortedSql: built.sql,
|
||||
});
|
||||
}
|
||||
|
||||
return { onExecuteSql, onReloadData, onPaginate, onSort };
|
||||
|
|
|
|||
|
|
@ -223,6 +223,7 @@ export default {
|
|||
commit: "Commit",
|
||||
rollback: "Rollback",
|
||||
transactionActive: "Editing",
|
||||
sortUnsupported: "This SQL does not support full-result sorting. Try again with a single SELECT query.",
|
||||
},
|
||||
welcome: {
|
||||
title: "Database Workspace",
|
||||
|
|
|
|||
|
|
@ -222,6 +222,7 @@ export default {
|
|||
commit: "提交",
|
||||
rollback: "回滚",
|
||||
transactionActive: "编辑中",
|
||||
sortUnsupported: "当前 SQL 不支持全量排序,请改为单条 SELECT 查询后再尝试。",
|
||||
},
|
||||
welcome: {
|
||||
title: "数据库工作台",
|
||||
|
|
|
|||
|
|
@ -0,0 +1,71 @@
|
|||
import type { DatabaseType } from "../types/database.ts";
|
||||
import { quoteTableIdentifier } from "./tableSelectSql.ts";
|
||||
import { findStatementAtCursor } from "./sqlStatementSplit.ts";
|
||||
|
||||
export type QuerySortDirection = "asc" | "desc";
|
||||
|
||||
export interface SortedQuerySqlResult {
|
||||
ok: true;
|
||||
sql: string;
|
||||
}
|
||||
|
||||
export interface SortedQuerySqlError {
|
||||
ok: false;
|
||||
reason: "empty" | "multi" | "not_select" | "with";
|
||||
}
|
||||
|
||||
export function buildSortedQuerySql(
|
||||
originalSql: string,
|
||||
databaseType: DatabaseType | undefined,
|
||||
resultColumns: string[],
|
||||
columnIndex: number,
|
||||
column: string,
|
||||
direction: QuerySortDirection,
|
||||
): SortedQuerySqlResult | SortedQuerySqlError {
|
||||
const baseSql = originalSql.trim();
|
||||
if (!baseSql) return { ok: false, reason: "empty" };
|
||||
|
||||
const statement = findStatementAtCursor(baseSql, 0)
|
||||
.trim()
|
||||
.replace(/;+\s*$/, "")
|
||||
.trim();
|
||||
if (!statement) return { ok: false, reason: "empty" };
|
||||
if (statement.length !== baseSql.replace(/;+\s*$/, "").trim().length) {
|
||||
return { ok: false, reason: "multi" };
|
||||
}
|
||||
if (/^\s*WITH\b/i.test(statement)) {
|
||||
return { ok: false, reason: "with" };
|
||||
}
|
||||
if (!/^\s*SELECT\b/i.test(statement)) {
|
||||
return { ok: false, reason: "not_select" };
|
||||
}
|
||||
|
||||
const aliases = buildDerivedColumnAliases(resultColumns);
|
||||
const sortAlias = aliases[columnIndex] ?? aliases[resultColumns.indexOf(column)] ?? fallbackAlias(columnIndex);
|
||||
const quotedColumn = quoteTableIdentifier(databaseType, sortAlias);
|
||||
const aliasList = aliases.map((alias) => quoteTableIdentifier(databaseType, alias)).join(", ");
|
||||
return {
|
||||
ok: true,
|
||||
sql: `SELECT * FROM (${statement}) t(${aliasList}) ORDER BY ${quotedColumn} ${direction.toUpperCase()};`,
|
||||
};
|
||||
}
|
||||
|
||||
function buildDerivedColumnAliases(resultColumns: string[]): string[] {
|
||||
const seen = new Map<string, number>();
|
||||
return resultColumns.map((column, index) => {
|
||||
const base = normalizeAliasBase(column, index);
|
||||
const count = (seen.get(base) ?? 0) + 1;
|
||||
seen.set(base, count);
|
||||
return count === 1 ? base : `${base}_${count}`;
|
||||
});
|
||||
}
|
||||
|
||||
function normalizeAliasBase(column: string, index: number): string {
|
||||
const compact = column.trim().replace(/\s+/g, "_");
|
||||
const safe = compact.replace(/[^\p{L}\p{N}_$]/gu, "_").replace(/^_+|_+$/g, "");
|
||||
return safe || fallbackAlias(index);
|
||||
}
|
||||
|
||||
function fallbackAlias(index: number): string {
|
||||
return `column_${index + 1}`;
|
||||
}
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
import type { DatabaseType } from "../types/database.ts";
|
||||
import { isSchemaAware, usesFetchFirst } from "@/lib/databaseCapabilities";
|
||||
import { isSchemaAware, usesFetchFirst } from "./databaseCapabilities.ts";
|
||||
|
||||
export interface BuildTableSelectSqlOptions {
|
||||
databaseType?: DatabaseType;
|
||||
|
|
|
|||
|
|
@ -136,7 +136,11 @@ export const useQueryStore = defineStore("query", () => {
|
|||
|
||||
function updateSql(id: string, sql: string) {
|
||||
const tab = tabs.value.find((t) => t.id === id);
|
||||
if (tab) tab.sql = sql;
|
||||
if (tab) {
|
||||
tab.sql = sql;
|
||||
tab.resultSortedSql = undefined;
|
||||
tab.resultBaseSql = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
function togglePinnedTab(id: string) {
|
||||
|
|
@ -153,6 +157,8 @@ export const useQueryStore = defineStore("query", () => {
|
|||
tab.schema = undefined;
|
||||
tab.result = undefined;
|
||||
tab.lastExecutedSql = undefined;
|
||||
tab.resultBaseSql = undefined;
|
||||
tab.resultSortedSql = undefined;
|
||||
clearExplain(tab);
|
||||
tab.tableMeta = undefined;
|
||||
}
|
||||
|
|
@ -171,6 +177,8 @@ export const useQueryStore = defineStore("query", () => {
|
|||
tab.schema = undefined;
|
||||
tab.result = undefined;
|
||||
tab.lastExecutedSql = undefined;
|
||||
tab.resultBaseSql = undefined;
|
||||
tab.resultSortedSql = undefined;
|
||||
clearExplain(tab);
|
||||
tab.tableMeta = undefined;
|
||||
}
|
||||
|
|
@ -226,14 +234,13 @@ export const useQueryStore = defineStore("query", () => {
|
|||
|
||||
async function executeCurrentSql(sql: string) {
|
||||
if (!activeTabId.value) return;
|
||||
await executeTabSql(activeTabId.value, sql);
|
||||
await executeTabSql(activeTabId.value, sql, { resultBaseSql: sql, resultSortedSql: undefined });
|
||||
}
|
||||
|
||||
/**
|
||||
* Analyze if the query result is editable (single-table SELECT with primary keys).
|
||||
* If editable, fetches table metadata and sets queryAnalysis + tableMeta on the tab.
|
||||
* Analyze query metadata for result tooltips and editability.
|
||||
*/
|
||||
async function analyzeQueryEditability(tab: QueryTab, sql: string) {
|
||||
async function analyzeQueryMetadata(tab: QueryTab, sql: string) {
|
||||
if (tab.mode !== "query") return;
|
||||
if (!tab.result || !tab.result.columns.length) {
|
||||
tab.queryAnalysis = undefined;
|
||||
|
|
@ -268,33 +275,31 @@ export const useQueryStore = defineStore("query", () => {
|
|||
const columns = await api.getColumns(tab.connectionId, tab.database, schema, analysis.tableName);
|
||||
const primaryKeys = columns.filter((c) => c.is_primary_key).map((c) => c.name);
|
||||
|
||||
if (primaryKeys.length === 0) {
|
||||
tab.queryAnalysis = undefined;
|
||||
tab.tableMeta = undefined;
|
||||
return;
|
||||
}
|
||||
|
||||
if (!allPrimaryKeysPresent(primaryKeys, tab.result.columns)) {
|
||||
tab.queryAnalysis = undefined;
|
||||
tab.tableMeta = undefined;
|
||||
return;
|
||||
}
|
||||
|
||||
tab.tableMeta = {
|
||||
schema: schema || undefined,
|
||||
tableName: analysis.tableName,
|
||||
columns,
|
||||
primaryKeys,
|
||||
};
|
||||
|
||||
if (primaryKeys.length === 0 || !allPrimaryKeysPresent(primaryKeys, tab.result.columns)) {
|
||||
tab.queryAnalysis = undefined;
|
||||
return;
|
||||
}
|
||||
|
||||
tab.queryAnalysis = analysis;
|
||||
} catch (err) {
|
||||
console.error("[DBX] ERROR fetching columns for editable query:", err);
|
||||
console.error("[DBX] ERROR fetching columns for query metadata:", err);
|
||||
tab.queryAnalysis = undefined;
|
||||
tab.tableMeta = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
async function executeTabSql(id: string, sql: string) {
|
||||
async function executeTabSql(
|
||||
id: string,
|
||||
sql: string,
|
||||
options?: { resultBaseSql?: string; resultSortedSql?: string | undefined },
|
||||
) {
|
||||
const tab = tabs.value.find((t) => t.id === id);
|
||||
if (!tab || !sql.trim()) return;
|
||||
|
||||
|
|
@ -316,8 +321,9 @@ export const useQueryStore = defineStore("query", () => {
|
|||
current.activeResultIndex = undefined;
|
||||
current.result = results[0];
|
||||
}
|
||||
// Analyze editability after successful execution
|
||||
await analyzeQueryEditability(current, sql);
|
||||
current.resultBaseSql = options?.resultBaseSql ?? sql;
|
||||
current.resultSortedSql = options?.resultSortedSql;
|
||||
await analyzeQueryMetadata(current, current.resultBaseSql);
|
||||
}
|
||||
} catch (e: any) {
|
||||
const current = tabs.value.find((t) => t.id === id);
|
||||
|
|
@ -326,6 +332,9 @@ export const useQueryStore = defineStore("query", () => {
|
|||
current.results = undefined;
|
||||
current.activeResultIndex = undefined;
|
||||
current.queryAnalysis = undefined;
|
||||
current.tableMeta = undefined;
|
||||
current.resultBaseSql = options?.resultBaseSql ?? sql;
|
||||
current.resultSortedSql = options?.resultSortedSql;
|
||||
}
|
||||
} finally {
|
||||
const current = tabs.value.find((t) => t.id === id);
|
||||
|
|
|
|||
|
|
@ -152,6 +152,8 @@ export interface QueryTab {
|
|||
schema?: string;
|
||||
sql: string;
|
||||
lastExecutedSql?: string;
|
||||
resultBaseSql?: string;
|
||||
resultSortedSql?: string;
|
||||
pinned?: boolean;
|
||||
result?: QueryResult;
|
||||
results?: QueryResult[];
|
||||
|
|
|
|||
|
|
@ -0,0 +1,49 @@
|
|||
import assert from "node:assert/strict";
|
||||
import test from "node:test";
|
||||
import { buildSortedQuerySql } from "../src/lib/queryResultSort.ts";
|
||||
|
||||
test("wraps a single select query with outer order by", () => {
|
||||
const result = buildSortedQuerySql("SELECT id, name FROM users;", "postgres", ["id", "name"], 1, "name", "asc");
|
||||
assert.deepEqual(result, {
|
||||
ok: true,
|
||||
sql: 'SELECT * FROM (SELECT id, name FROM users) t("id", "name") ORDER BY "name" ASC;',
|
||||
});
|
||||
});
|
||||
|
||||
test("preserves complex select body when wrapping sort sql", () => {
|
||||
const result = buildSortedQuerySql("SELECT id FROM users WHERE status = 'A'", "mysql", ["id"], 0, "id", "desc");
|
||||
assert.deepEqual(result, {
|
||||
ok: true,
|
||||
sql: "SELECT * FROM (SELECT id FROM users WHERE status = 'A') t(`id`) ORDER BY `id` DESC;",
|
||||
});
|
||||
});
|
||||
|
||||
test("assigns unique aliases for duplicate result column names", () => {
|
||||
const result = buildSortedQuerySql(
|
||||
"SELECT c.id, m.id FROM t_campaign c LEFT JOIN t_campaign_mdf m ON m.campaign_id = c.id",
|
||||
"mysql",
|
||||
["id", "id"],
|
||||
1,
|
||||
"id",
|
||||
"asc",
|
||||
);
|
||||
assert.deepEqual(result, {
|
||||
ok: true,
|
||||
sql: "SELECT * FROM (SELECT c.id, m.id FROM t_campaign c LEFT JOIN t_campaign_mdf m ON m.campaign_id = c.id) t(`id`, `id_2`) ORDER BY `id_2` ASC;",
|
||||
});
|
||||
});
|
||||
|
||||
test("rejects multiple statements for result sorting", () => {
|
||||
const result = buildSortedQuerySql("SELECT 1; SELECT 2;", "postgres", ["id"], 0, "id", "asc");
|
||||
assert.deepEqual(result, { ok: false, reason: "multi" });
|
||||
});
|
||||
|
||||
test("rejects cte queries for result sorting", () => {
|
||||
const result = buildSortedQuerySql("WITH cte AS (SELECT 1) SELECT * FROM cte", "postgres", ["id"], 0, "id", "asc");
|
||||
assert.deepEqual(result, { ok: false, reason: "with" });
|
||||
});
|
||||
|
||||
test("rejects non select statements for result sorting", () => {
|
||||
const result = buildSortedQuerySql("UPDATE users SET name = 'A'", "postgres", ["name"], 0, "name", "asc");
|
||||
assert.deepEqual(result, { ok: false, reason: "not_select" });
|
||||
});
|
||||
Loading…
Reference in New Issue