Files
dbx/apps/desktop/src/lib/table/tableSqlTemplates.ts
T

215 lines
9.9 KiB
TypeScript

import type { ColumnInfo, DatabaseType } from "@/types/database";
import { metricRangeQuery, qualifiedTableName, quoteTableDataIdentifier, quoteTableIdentifierIfNeeded } from "@/lib/table/tableSelectSql";
export interface TableSqlTemplateOptions {
databaseType?: DatabaseType;
driverProfile?: string;
identifierQuote?: string;
schema?: string;
catalog?: string;
database?: string;
includeDatabaseName?: boolean;
/** Omit optional identifier quotes while retaining quotes required by the dialect. */
quoteIdentifiers?: boolean;
tableName: string;
columns?: ColumnInfo[];
tableType?: string;
}
export function buildTableSelectTemplate(options: TableSqlTemplateOptions): string {
if (options.databaseType === "victoriametrics") return metricRangeQuery(options.tableName);
const tableName = templateTableName(options);
const columns = options.columns ?? [];
if (!columns.length) {
return `SELECT *\nFROM ${tableName};`;
}
const selectColumns = columns.map((column) => templateIdentifier(options, column.name));
return `SELECT ${selectColumns.join(", ")}\nFROM ${tableName};`;
}
export function buildTableInsertTemplate(options: TableSqlTemplateOptions): string {
const strategy = options.databaseType ? insertTemplateStrategies[options.databaseType] : undefined;
return (strategy ?? buildDefaultInsertTemplate)(options);
}
type InsertTemplateStrategy = (options: TableSqlTemplateOptions) => string;
function buildDefaultInsertTemplate(options: TableSqlTemplateOptions): string {
const tableName = templateTableName(options);
const columns = (options.columns ?? []).filter((column) => !isAutoGeneratedColumn(column));
// TDengine does not support INSERT INTO t(col1, col2) VALUES(...) syntax
if (options.databaseType === "tdengine") {
const values = columns.map((column) => columnPlaceholderValue(column, options.databaseType));
if (!values.length) {
return `INSERT INTO ${tableName}\nVALUES (/* TODO: add values */);`;
}
return `INSERT INTO ${tableName}\nVALUES (${values.join(", ")});`;
}
if (!columns.length) {
return `INSERT INTO ${tableName}\n/* TODO: add column values */\nVALUES ();`;
}
const columnNames = columns.map((column) => templateIdentifier(options, column.name));
const values = columns.map((column) => columnPlaceholderValue(column, options.databaseType));
return `INSERT INTO ${tableName} (${columnNames.join(", ")})\nVALUES (${values.join(", ")});`;
}
function buildTdengineInsertTemplate(options: TableSqlTemplateOptions): string {
const columns = options.columns ?? [];
const isTagColumn = (column: ColumnInfo) => (column.comment ?? "").toUpperCase() === "TAG" || (column.extra ?? "").toUpperCase().includes("TAG");
const tagColumns = columns.filter((column) => isTagColumn(column));
const normalizedTableType = (options.tableType ?? "").toUpperCase();
const shouldUseStableTemplate = normalizedTableType === "STABLE" || (!normalizedTableType && tagColumns.length > 0);
if (!shouldUseStableTemplate) {
return buildDefaultInsertTemplate(options);
}
const valueColumns = columns.filter((column) => !isTagColumn(column)).filter((column) => !isAutoGeneratedColumn(column));
const tagPlaceholders = tagColumns.map((column) => columnPlaceholderValue(column, options.databaseType));
const tagsSql = tagPlaceholders.length > 0 ? `TAGS (${tagPlaceholders.join(", ")})` : "TAGS (/* TODO: add tag values */)";
const stableNameForUsing = templateTableName(options).replace(/^`|`$/g, "");
const valuePlaceholders = valueColumns.map((column) => columnPlaceholderValue(column, options.databaseType));
if (!valueColumns.length) {
return `INSERT INTO /* child_table_name */ USING ${stableNameForUsing}\n${tagsSql}\nVALUES (/* TODO: add values */);`;
}
return `INSERT INTO /* child_table_name */ USING ${stableNameForUsing}\n${tagsSql}\nVALUES (${valuePlaceholders.join(", ")});`;
}
const insertTemplateStrategies: Partial<Record<DatabaseType, InsertTemplateStrategy>> = {
tdengine: buildTdengineInsertTemplate,
};
export function buildTableUpdateTemplate(options: TableSqlTemplateOptions): string {
const tableName = templateTableName(options);
const columns = options.columns ?? [];
const primaryKeys = columns.filter((column) => column.is_primary_key);
const primaryKeyNames = new Set(primaryKeys.map((column) => column.name));
const updateColumns = columns.filter((column) => !primaryKeyNames.has(column.name));
const setClause = updateColumns.length ? updateColumns.map((column) => `${templateIdentifier(options, column.name)} = ${columnPlaceholderValue(column, options.databaseType)}`).join(",\n ") : "/* TODO: set column = value */";
return `UPDATE ${tableName}\nSET ${setClause}\n${buildWhereClause(options, primaryKeys)};`;
}
export function buildTableDeleteTemplate(options: TableSqlTemplateOptions): string {
const tableName = templateTableName(options);
const primaryKeys = (options.columns ?? []).filter((column) => column.is_primary_key);
return `DELETE FROM ${tableName}\n${buildWhereClause(options, primaryKeys)};`;
}
function templateTableName(options: TableSqlTemplateOptions): string {
return qualifiedTableName({
databaseType: options.databaseType,
driverProfile: options.driverProfile,
identifierQuote: options.identifierQuote,
catalog: options.catalog,
database: options.database,
includeDatabaseName: options.includeDatabaseName,
quoteIdentifiers: options.quoteIdentifiers,
schema: options.schema,
tableName: options.tableName,
});
}
function templateIdentifier(options: TableSqlTemplateOptions, name: string): string {
return options.quoteIdentifiers === false ? quoteTableIdentifierIfNeeded(options.databaseType, name, options.identifierQuote) : quoteTableDataIdentifier(options.databaseType, name, options.identifierQuote);
}
function buildWhereClause(options: TableSqlTemplateOptions, primaryKeys: ColumnInfo[]): string {
if (!primaryKeys.length) return "WHERE /* TODO: add WHERE clause */";
const conditions = primaryKeys.map((column) => `${templateIdentifier(options, column.name)} = ${columnPlaceholderValue(column, options.databaseType)}`);
return `WHERE ${conditions.join(" AND ")}`;
}
function isAutoGeneratedColumn(column: ColumnInfo): boolean {
const extra = (column.extra ?? "").toLowerCase().trim();
const colDefault = (column.column_default ?? "").toLowerCase().trim();
const dataType = (column.data_type ?? "").trim();
if (typeLooksPostgresTextSearchVector(dataType)) return true;
if (extra.includes("auto_increment")) return true;
if (extra.includes("autoincrement")) return true;
if (extra.includes("identity")) return true;
if (extra.includes("generated") && (extra.includes("as identity") || extra.includes("always as"))) return true;
if (colDefault.includes("nextval(")) return true;
if (colDefault.includes(".nextval")) return true;
if ((extra.includes("virtual") || extra.includes("computed")) && extra.includes("generated")) return true;
return false;
}
function columnPlaceholderValue(column: ColumnInfo, databaseType?: DatabaseType): string {
const dataType = (column.data_type ?? "").trim();
const colName = column.name;
if (typeLooksNumeric(dataType)) return "0";
if (typeLooksBoolean(dataType)) return databaseType === "mysql" || databaseType === "sqlite" || databaseType === "rqlite" || databaseType === "cloudflare-d1" ? "1" : "TRUE";
if (typeLooksDateOnly(dataType)) return "'2024-01-01'";
if (typeLooksTimeOnly(dataType)) return "'12:00:00'";
if (typeLooksTimestamp(dataType)) return databaseType === "tdengine" ? "NOW" : "CURRENT_TIMESTAMP";
if (typeLooksJson(dataType)) return databaseType === "postgres" || databaseType === "redshift" ? "'{}'::jsonb" : "'{}'";
if (typeLooksBinary(dataType)) return "''";
if (typeLooksUuid(dataType)) return databaseType === "postgres" || databaseType === "redshift" ? "'00000000-0000-0000-0000-000000000000'::uuid" : "'00000000-0000-0000-0000-000000000000'";
if (typeLooksXml(dataType)) return "'<root/>'";
if (typeLooksSpatial(dataType)) return "NULL";
if (typeLooksArray(dataType)) return "'{}'";
return `'${colName}_value'`;
}
function typeLooksNumeric(dataType: string): boolean {
return /^(tinyint|smallint|mediumint|int\d*|integer\d*|bigint|smallserial|serial\d*|bigserial|number|numeric|decimal|dec|float\d*|double|real|double\s+precision|money|smallmoney|fixed)/i.test(dataType.trim());
}
function typeLooksBoolean(dataType: string): boolean {
return /^(bool|boolean|bit\s*\(\s*1\s*\))$/i.test(dataType.trim());
}
function typeLooksDateOnly(dataType: string): boolean {
return /^date$/i.test(dataType.trim());
}
function typeLooksTimeOnly(dataType: string): boolean {
return /^time\b(?!stamp|stamptz|with|without)/i.test(dataType.trim());
}
function typeLooksTimestamp(dataType: string): boolean {
return /^(datetime|timestamp|timestamptz|timestamp\s+with\s+time\s+zone|timestamp\s+without\s+time\s+zone|smalldatetime|datetime2|datetimeoffset)/i.test(dataType.trim());
}
function typeLooksJson(dataType: string): boolean {
return /^(json|jsonb)$/i.test(dataType.trim());
}
function typeLooksBinary(dataType: string): boolean {
return /^(bytea|blob|binary|varbinary|image|raw|longblob|mediumblob|tinyblob|long\s+raw|bfile)$/i.test(dataType.trim());
}
function typeLooksUuid(dataType: string): boolean {
return /^uuid$/i.test(dataType.trim());
}
function typeLooksXml(dataType: string): boolean {
return /^xml$/i.test(dataType.trim());
}
function typeLooksSpatial(dataType: string): boolean {
return /^(geometry|geography|point|polygon|linestring|box|circle|path|line|lseg)/i.test(dataType.trim());
}
function typeLooksArray(dataType: string): boolean {
return /^(array|vector|_float\d*|_int\d*|_text|_varchar|_bool|_uuid)/i.test(dataType.trim());
}
function typeLooksPostgresTextSearchVector(dataType: string): boolean {
const normalized = dataType
.trim()
.replace(/^"+|"+$/g, "")
.toLowerCase();
return normalized === "tsvector" || normalized.endsWith(".tsvector");
}