212 lines
9.6 KiB
TypeScript
212 lines
9.6 KiB
TypeScript
import type { ColumnInfo, DatabaseType } from "@/types/database";
|
|
import { metricRangeQuery, qualifiedTableName, quoteTableDataIdentifier } from "@/lib/table/tableSelectSql";
|
|
|
|
export interface TableSqlTemplateOptions {
|
|
databaseType?: DatabaseType;
|
|
driverProfile?: string;
|
|
identifierQuote?: string;
|
|
schema?: string;
|
|
catalog?: string;
|
|
database?: string;
|
|
includeDatabaseName?: 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,
|
|
schema: options.schema,
|
|
tableName: options.tableName,
|
|
});
|
|
}
|
|
|
|
function templateIdentifier(options: TableSqlTemplateOptions, name: string): string {
|
|
return 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");
|
|
}
|