feat(sql): add table generate sql menu
This commit is contained in:
1 parent
535d247ae1
commit
59ea4eafdc
10 files changed
+354
-199
No files matched your search
@@ -0,0 +1,151 @@
|
||||
import type { ColumnInfo, DatabaseType } from "@/types/database";
|
||||
import { qualifiedTableName, quoteTableIdentifier } from "@/lib/tableSelectSql";
|
||||
|
||||
export interface TableSqlTemplateOptions {
|
||||
databaseType?: DatabaseType;
|
||||
schema?: string;
|
||||
tableName: string;
|
||||
columns?: ColumnInfo[];
|
||||
}
|
||||
|
||||
export function buildTableSelectTemplate(options: TableSqlTemplateOptions): string {
|
||||
const tableName = templateTableName(options);
|
||||
const columns = options.columns ?? [];
|
||||
if (!columns.length) {
|
||||
return `SELECT *\nFROM ${tableName};`;
|
||||
}
|
||||
const selectColumns = columns.map((column) => quoteTableIdentifier(options.databaseType, column.name));
|
||||
return `SELECT ${selectColumns.join(", ")}\nFROM ${tableName};`;
|
||||
}
|
||||
|
||||
export function buildTableInsertTemplate(options: TableSqlTemplateOptions): string {
|
||||
const tableName = templateTableName(options);
|
||||
const columns = (options.columns ?? []).filter((column) => !isAutoGeneratedColumn(column));
|
||||
|
||||
if (!columns.length) {
|
||||
return `INSERT INTO ${tableName}\n/* TODO: add column values */\nVALUES ();`;
|
||||
}
|
||||
|
||||
const columnNames = columns.map((column) => quoteTableIdentifier(options.databaseType, column.name));
|
||||
const values = columns.map((column) => columnPlaceholderValue(column, options.databaseType));
|
||||
return `INSERT INTO ${tableName} (${columnNames.join(", ")})\nVALUES (${values.join(", ")});`;
|
||||
}
|
||||
|
||||
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) => `${quoteTableIdentifier(options.databaseType, column.name)} = ${columnPlaceholderValue(column, options.databaseType)}`).join(",\n ") : "/* TODO: set column = value */";
|
||||
|
||||
return `UPDATE ${tableName}\nSET ${setClause}\n${buildWhereClause(options.databaseType, 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.databaseType, primaryKeys)};`;
|
||||
}
|
||||
|
||||
function templateTableName(options: TableSqlTemplateOptions): string {
|
||||
return qualifiedTableName({
|
||||
databaseType: options.databaseType,
|
||||
schema: options.schema,
|
||||
tableName: options.tableName,
|
||||
});
|
||||
}
|
||||
|
||||
function buildWhereClause(databaseType: DatabaseType | undefined, primaryKeys: ColumnInfo[]): string {
|
||||
if (!primaryKeys.length) return "WHERE /* TODO: add WHERE clause */";
|
||||
const conditions = primaryKeys.map((column) => `${quoteTableIdentifier(databaseType, column.name)} = ${columnPlaceholderValue(column, 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" ? "1" : "TRUE";
|
||||
if (typeLooksDateOnly(dataType)) return "'2024-01-01'";
|
||||
if (typeLooksTimeOnly(dataType)) return "'12:00:00'";
|
||||
if (typeLooksTimestamp(dataType)) return "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");
|
||||
}
|
||||
Reference in new issue
Block a user