mirror of
https://github.com/t8y2/dbx.git
synced 2026-10-29 16:18:55 +00:00
180 lines
7.4 KiB
TypeScript
180 lines
7.4 KiB
TypeScript
import type { DatabaseType } from "../types/database.ts";
|
|
import { isSchemaAware, usesFetchFirst } from "./databaseCapabilities.ts";
|
|
import { DBX_NEO4J_ELEMENT_ID_COLUMN, DBX_ROWID_COLUMN, DBX_TDENGINE_TBNAME_COLUMN } from "./tableEditing.ts";
|
|
|
|
export interface BuildTableSelectSqlOptions {
|
|
databaseType?: DatabaseType;
|
|
schema?: string;
|
|
tableName: string;
|
|
primaryKeys?: string[];
|
|
columns?: string[];
|
|
fallbackOrderColumns?: string[];
|
|
orderBy?: string;
|
|
limit?: number;
|
|
offset?: number;
|
|
whereInput?: string;
|
|
includeRowId?: boolean;
|
|
}
|
|
|
|
export function quoteTableIdentifier(databaseType: DatabaseType | undefined, name: string): string {
|
|
if (databaseType === "mysql" || databaseType === "hive" || databaseType === "tdengine" || databaseType === "access")
|
|
return `\`${name.replace(/`/g, "``")}\``;
|
|
if (databaseType === "informix" && /^[A-Za-z_][A-Za-z0-9_$]*$/.test(name)) return name;
|
|
if (databaseType === "neo4j") return quoteCypherIdentifier(name);
|
|
if (databaseType === "sqlserver") return `[${name.replace(/\]/g, "]]")}]`;
|
|
return `"${name.replace(/"/g, '""')}"`;
|
|
}
|
|
|
|
function quoteCypherIdentifier(name: string): string {
|
|
return `\`${name.replace(/`/g, "``")}\``;
|
|
}
|
|
|
|
function isOracleRowId(databaseType: DatabaseType | undefined, name: string): boolean {
|
|
return databaseType === "oracle" && name.toUpperCase() === DBX_ROWID_COLUMN;
|
|
}
|
|
|
|
function quoteOrderIdentifier(databaseType: DatabaseType | undefined, name: string, tableAlias?: string): string {
|
|
if (isOracleRowId(databaseType, name)) return tableAlias ? `${tableAlias}.ROWID` : "ROWID";
|
|
if (isTdengineTbname(databaseType, name)) return DBX_TDENGINE_TBNAME_COLUMN;
|
|
const quoted = quoteTableIdentifier(databaseType, name);
|
|
return tableAlias ? `${tableAlias}.${quoted}` : quoted;
|
|
}
|
|
|
|
export function qualifiedTableName(
|
|
options: Pick<BuildTableSelectSqlOptions, "databaseType" | "schema" | "tableName">,
|
|
): string {
|
|
const { databaseType, schema, tableName } = options;
|
|
if (isSchemaAware(databaseType) && schema) {
|
|
return `${quoteTableIdentifier(databaseType, schema)}.${quoteTableIdentifier(databaseType, tableName)}`;
|
|
}
|
|
return quoteTableIdentifier(databaseType, tableName);
|
|
}
|
|
|
|
export function normalizeWhereInput(whereInput?: string): string {
|
|
const withoutSemicolon = whereInput?.trim().replace(/;+$/, "").trim() ?? "";
|
|
return withoutSemicolon.replace(/^where\b/i, "").trim();
|
|
}
|
|
|
|
export function buildTableSelectSql(options: BuildTableSelectSqlOptions): string {
|
|
const databaseType = options.databaseType;
|
|
const limit = options.limit ?? 100;
|
|
if (databaseType === "neo4j") return buildNeo4jTableSelectSql(options, limit);
|
|
|
|
const table = qualifiedTableName(options);
|
|
const predicate = normalizeWhereInput(options.whereInput);
|
|
const where = predicate ? ` WHERE (${predicate})` : "";
|
|
const rowIdAlias = options.includeRowId && databaseType === "oracle" ? "t" : undefined;
|
|
const defaultOrderAlias = databaseType === "jdbc" ? "dbx_t" : rowIdAlias;
|
|
const defaultOrderBy = options.primaryKeys?.length
|
|
? options.primaryKeys.map((pk) => `${quoteOrderIdentifier(databaseType, pk, defaultOrderAlias)} ASC`).join(", ")
|
|
: options.fallbackOrderColumns?.length
|
|
? options.fallbackOrderColumns.map((column) => `${quoteTableIdentifier(databaseType, column)} ASC`).join(", ")
|
|
: undefined;
|
|
const orderBy = options.orderBy ?? defaultOrderBy;
|
|
const order = orderBy ? ` ORDER BY ${orderBy}` : "";
|
|
|
|
const selectColumns =
|
|
options.includeRowId && databaseType === "oracle"
|
|
? `ROWIDTOCHAR(t.ROWID) AS "${DBX_ROWID_COLUMN}", t.*`
|
|
: buildSelectColumns(databaseType, options.columns);
|
|
const tableAlias =
|
|
options.includeRowId && usesFetchFirst(databaseType)
|
|
? `${table} t`
|
|
: databaseType === "jdbc" && defaultOrderBy
|
|
? `${table} dbx_t`
|
|
: table;
|
|
|
|
if (usesFetchFirst(databaseType)) {
|
|
const offset = options.offset ? ` OFFSET ${options.offset} ROWS` : "";
|
|
return `SELECT ${selectColumns} FROM ${tableAlias}${where}${order}${offset} FETCH FIRST ${limit} ROWS ONLY`;
|
|
}
|
|
|
|
if (databaseType === "sqlserver") {
|
|
return buildSqlServerTableSelectSql({
|
|
table,
|
|
where,
|
|
orderBy: orderBy ?? "(SELECT NULL)",
|
|
columns: options.columns,
|
|
limit,
|
|
offset: options.offset ?? 0,
|
|
});
|
|
}
|
|
|
|
const offset = options.offset ? ` OFFSET ${options.offset}` : "";
|
|
return `SELECT ${selectColumns} FROM ${tableAlias}${where}${order} LIMIT ${limit}${offset};`;
|
|
}
|
|
|
|
function buildSelectColumns(databaseType: DatabaseType | undefined, columns?: string[]): string {
|
|
if (!columns?.length) return "*";
|
|
if (databaseType === "tdengine") {
|
|
const tdengineColumns = columns.some((column) => column.toLowerCase() === DBX_TDENGINE_TBNAME_COLUMN)
|
|
? columns
|
|
: [DBX_TDENGINE_TBNAME_COLUMN, ...columns];
|
|
return tdengineColumns
|
|
.map((column) => {
|
|
if (isTdengineTbname(databaseType, column)) return DBX_TDENGINE_TBNAME_COLUMN;
|
|
const ident = quoteTableIdentifier(databaseType, column);
|
|
return `${ident} AS ${ident}`;
|
|
})
|
|
.join(", ");
|
|
}
|
|
if (databaseType !== "hive") return "*";
|
|
return columns
|
|
.map((column) => {
|
|
const ident = quoteTableIdentifier(databaseType, column);
|
|
return `${ident} AS ${ident}`;
|
|
})
|
|
.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;
|
|
}
|
|
|
|
function buildNeo4jTableSelectSql(options: BuildTableSelectSqlOptions, limit: number): string {
|
|
const label = quoteCypherIdentifier(options.tableName);
|
|
const predicate = normalizeWhereInput(options.whereInput);
|
|
const where = predicate ? ` WHERE ${predicate}` : "";
|
|
const returnedColumns = options.columns?.length
|
|
? options.columns
|
|
.map((column) => `n.${quoteCypherIdentifier(column)} AS ${quoteCypherIdentifier(column)}`)
|
|
.join(", ")
|
|
: "n";
|
|
const returns = `elementId(n) AS ${quoteCypherIdentifier(DBX_NEO4J_ELEMENT_ID_COLUMN)}, ${returnedColumns}`;
|
|
const defaultOrderBy = options.primaryKeys?.length
|
|
? options.primaryKeys.map((pk) => `n.${quoteCypherIdentifier(pk)} ASC`).join(", ")
|
|
: undefined;
|
|
const orderBy = options.orderBy ?? defaultOrderBy;
|
|
const order = orderBy ? ` ORDER BY ${orderBy}` : "";
|
|
const skip = options.offset ? ` SKIP ${options.offset}` : "";
|
|
return `MATCH (n:${label})${where} RETURN ${returns}${order}${skip} LIMIT ${limit};`;
|
|
}
|