import type { DatabaseType } from "../types/database.ts"; import { isSchemaAware, usesFetchFirst } from "./databaseCapabilities.ts"; import { DBX_NEO4J_ELEMENT_ID_COLUMN, DBX_ROWID_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") 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"; return quoteTableIdentifier(databaseType, name); } export function qualifiedTableName( options: Pick, ): 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 defaultOrderBy = options.primaryKeys?.length ? options.primaryKeys.map((pk) => `${quoteOrderIdentifier(databaseType, pk, rowIdAlias)} 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` : 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") { const stableOrder = order || " ORDER BY (SELECT NULL)"; return `SELECT * FROM ${table}${where}${stableOrder} OFFSET ${options.offset ?? 0} ROWS FETCH NEXT ${limit} ROWS ONLY`; } const offset = options.offset ? ` OFFSET ${options.offset}` : ""; return `SELECT ${selectColumns} FROM ${table}${where}${order} LIMIT ${limit}${offset};`; } function buildSelectColumns(databaseType: DatabaseType | undefined, columns?: string[]): string { if (databaseType !== "hive" || !columns?.length) return "*"; return columns .map((column) => { const ident = quoteTableIdentifier(databaseType, column); return `${ident} AS ${ident}`; }) .join(", "); } 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};`; }