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> = { 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 "''"; 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"); }