feat(sql): add table generate sql menu

This commit is contained in:
t8y2 2026-06-19 10:53:26 +08:00
parent 535d247ae1
commit 59ea4eafdc
10 changed files with 354 additions and 199 deletions

View File

@ -52,6 +52,7 @@ import {
HardDriveDownload,
FilePlus,
SquarePen,
ListX,
} from "@lucide/vue";
import CustomContextMenu, { type ContextMenuItem } from "@/components/ui/CustomContextMenu.vue";
import { useConnectionStore } from "@/stores/connectionStore";
@ -65,7 +66,8 @@ import * as api from "@/lib/api";
import { uuid } from "@/lib/utils";
import { resolveDefaultDatabase } from "@/lib/defaultDatabase";
import { canTreeNodeShowExpander, treeItemPaddingLeft, usesFullWidthTreeLabel } from "@/lib/sidebarTreeItemLayout";
import { buildTableSelectSql, quoteTableIdentifier, qualifiedTableName } from "@/lib/tableSelectSql";
import { buildTableSelectSql } from "@/lib/tableSelectSql";
import { buildTableDeleteTemplate, buildTableInsertTemplate, buildTableSelectTemplate, buildTableUpdateTemplate } from "@/lib/tableSqlTemplates";
import { connectionFilePath, defaultSqliteBackupFileName, isMemorySqlitePath, sqliteBackupSourcePath } from "@/lib/connectionFile";
import { revealPathInFileManager } from "@/lib/tauri";
import { clearActiveTableReferencePayload, createTableReferencePayload, createTableReferenceDropEvent, setActiveTableReferencePayload, type QueryEditorTableReferencePayload } from "@/lib/queryEditorTableDrop";
@ -946,15 +948,11 @@ async function newQuery() {
await connectionStore.ensureConnected(node.connectionId);
connectionStore.activeConnectionId = node.connectionId;
if (hasTreeNodeDatabaseContext(node)) {
const tabId = queryStore.createTab(node.connectionId, node.database, undefined, "query", node.schema);
// For table/view nodes, generate SELECT * FROM table_name SQL
if (node.type === "table" || node.type === "view" || node.type === "materialized_view") {
const config = connectionStore.getConfig(node.connectionId);
const dbType = config ? effectiveDatabaseTypeForConnection(config) : undefined;
const tableSchema = node.schema || node.database;
const tableName = qualifiedTableName({ databaseType: dbType, schema: tableSchema, tableName: node.label });
queryStore.updateSql(tabId, `SELECT * FROM ${tableName};`);
await newSelectTemplate();
return;
}
queryStore.createTab(node.connectionId, node.database, undefined, "query", node.schema);
return;
}
const connection = connectionStore.getConfig(node.connectionId);
@ -969,206 +967,124 @@ async function newQuery() {
}
}
// ---- SQL template helpers ----
/**
* Detect whether a column value is auto-generated by the database
* (auto-increment, identity, serial, computed/virtual, etc.).
* Such columns should be omitted from INSERT templates.
*/
function isAutoGeneratedColumn(column: ColumnInfo, _dbType?: DatabaseType): 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;
// MySQL / MariaDB: extra contains "auto_increment"
if (extra.includes("auto_increment")) return true;
// SQLite: extra contains "autoincrement"
if (extra.includes("autoincrement")) return true;
// SQL Server: extra contains "identity"
if (extra.includes("identity")) return true;
// PostgreSQL 10+ / GaussDB / Kingbase / etc.: GENERATED AS IDENTITY
if (extra.includes("generated") && (extra.includes("as identity") || extra.includes("always as"))) return true;
// PostgreSQL legacy SERIAL / Oracle sequences: column_default contains nextval
if (colDefault.includes("nextval(")) return true;
if (colDefault.includes(".nextval")) return true;
// Virtual / computed columns (MySQL, MariaDB, etc.)
if ((extra.includes("virtual") || extra.includes("computed")) && extra.includes("generated")) return true;
return false;
}
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");
}
/**
* Return a type-appropriate placeholder value for a template column.
* Inspects column.data_type and database type to produce the right
* literal so the user can execute the template immediately after
* replacing placeholders.
*/
function columnPlaceholderValue(column: ColumnInfo, dbType?: DatabaseType): string {
const dataType = (column.data_type ?? "").trim();
const colName = column.name;
if (typeLooksNumeric(dataType)) return "0";
if (typeLooksBoolean(dataType)) return dbType === "mysql" || dbType === "sqlite" || dbType === "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 dbType === "postgres" || dbType === "redshift" ? "'{}'::jsonb" : "'{}'";
if (typeLooksBinary(dataType)) return "''";
if (typeLooksUuid(dataType)) return dbType === "postgres" || dbType === "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 "'{}'";
// Fallback: quoted column-name placeholder so user knows what to fill
return `'${colName}_value'`;
}
// ---- Template actions ----
async function newInsertTemplate() {
async function loadTemplateContext(allowView = false) {
const node = props.node;
if (!node.connectionId || !hasTreeNodeDatabaseContext(node)) return;
if (node.type !== "table" && node.type !== "view") return;
if (!node.connectionId || !hasTreeNodeDatabaseContext(node)) return null;
const isTableNode = node.type === "table";
const isReadableObject = isTableNode || (allowView && (node.type === "view" || node.type === "materialized_view"));
if (!isReadableObject) return null;
await connectionStore.ensureConnected(node.connectionId);
connectionStore.activeConnectionId = node.connectionId;
const config = connectionStore.getConfig(node.connectionId);
const dbType = config ? effectiveDatabaseTypeForConnection(config) : undefined;
const tableSchema = node.schema || node.database;
let columns: ColumnInfo[] = [];
try {
await connectionStore.ensureConnected(node.connectionId);
connectionStore.activeConnectionId = node.connectionId;
const config = connectionStore.getConfig(node.connectionId);
const dbType = config ? effectiveDatabaseTypeForConnection(config) : undefined;
const tableSchema = node.schema || node.database;
const querySchema = connectionObjectTreeQuerySchema(config, node.database, tableSchema);
columns = await api.getColumns(node.connectionId, node.database, querySchema, node.label);
} catch (e) {
console.warn("[DBX][tableSqlTemplate:getColumns:error]", e);
}
let columns: ColumnInfo[] = [];
try {
const querySchema = connectionObjectTreeQuerySchema(config, node.database, tableSchema);
columns = await api.getColumns(node.connectionId, node.database, querySchema, node.label);
} catch (e) {
console.warn("[DBX][newInsertTemplate:getColumns:error]", e);
}
return { node, dbType, tableSchema, columns };
}
const tableName = qualifiedTableName({ databaseType: dbType, schema: tableSchema, tableName: node.label });
function openSqlTemplateTab(connectionId: string, database: string, schema: string | undefined, sql: string, title?: string) {
const tabId = queryStore.createTab(connectionId, database, title, "query", schema);
queryStore.updateSql(tabId, sql);
}
// Omit auto-generated columns (auto_increment, identity, serial, computed, etc.)
const insertColumns = columns.filter((c) => !isAutoGeneratedColumn(c, dbType));
async function newSelectTemplate() {
try {
const context = await loadTemplateContext(true);
if (!context) return;
const sql = buildTableSelectTemplate({
databaseType: context.dbType,
schema: context.tableSchema,
tableName: context.node.label,
columns: context.columns,
});
openSqlTemplateTab(context.node.connectionId!, context.node.database!, context.node.schema, sql);
} catch (e: any) {
toast(t("connection.connectFailed", { message: translateBackendError(t, e?.message || String(e)) }), 5000);
}
}
let sql: string;
if (insertColumns.length === 0) {
sql = `INSERT INTO ${tableName}\n/* TODO: add column values */\nVALUES ();`;
} else {
const colNames = insertColumns.map((c) => quoteTableIdentifier(dbType, c.name));
const placeholders = insertColumns.map((c) => columnPlaceholderValue(c, dbType));
sql = `INSERT INTO ${tableName} (${colNames.join(", ")})\nVALUES (${placeholders.join(", ")});`;
}
const tabId = queryStore.createTab(node.connectionId, node.database, undefined, "query", node.schema);
queryStore.updateSql(tabId, sql);
async function newInsertTemplate() {
try {
const context = await loadTemplateContext(false);
if (!context) return;
const sql = buildTableInsertTemplate({
databaseType: context.dbType,
schema: context.tableSchema,
tableName: context.node.label,
columns: context.columns,
});
openSqlTemplateTab(context.node.connectionId!, context.node.database!, context.node.schema, sql);
} catch (e: any) {
toast(t("connection.connectFailed", { message: translateBackendError(t, e?.message || String(e)) }), 5000);
}
}
async function newUpdateTemplate() {
try {
const context = await loadTemplateContext(false);
if (!context) return;
const sql = buildTableUpdateTemplate({
databaseType: context.dbType,
schema: context.tableSchema,
tableName: context.node.label,
columns: context.columns,
});
openSqlTemplateTab(context.node.connectionId!, context.node.database!, context.node.schema, sql);
} catch (e: any) {
toast(t("connection.connectFailed", { message: translateBackendError(t, e?.message || String(e)) }), 5000);
}
}
async function newDeleteTemplate() {
try {
const context = await loadTemplateContext(false);
if (!context) return;
const sql = buildTableDeleteTemplate({
databaseType: context.dbType,
schema: context.tableSchema,
tableName: context.node.label,
columns: context.columns,
});
openSqlTemplateTab(context.node.connectionId!, context.node.database!, context.node.schema, sql);
} catch (e: any) {
toast(t("connection.connectFailed", { message: translateBackendError(t, e?.message || String(e)) }), 5000);
}
}
async function generateDdlTemplate() {
const node = props.node;
if (!node.connectionId || !hasTreeNodeDatabaseContext(node)) return;
if (node.type !== "table" && node.type !== "view") return;
if (node.type !== "table" && node.type !== "view" && node.type !== "materialized_view") return;
try {
await connectionStore.ensureConnected(node.connectionId);
connectionStore.activeConnectionId = node.connectionId;
const config = connectionStore.getConfig(node.connectionId);
const dbType = config ? effectiveDatabaseTypeForConnection(config) : undefined;
const tableSchema = node.schema || node.database;
let columns: ColumnInfo[] = [];
let primaryKeys: string[] = [];
try {
const querySchema = connectionObjectTreeQuerySchema(config, node.database, tableSchema);
columns = await api.getColumns(node.connectionId, node.database, querySchema, node.label);
primaryKeys = columns.filter((c) => c.is_primary_key).map((c) => c.name);
} catch (e) {
console.warn("[DBX][newUpdateTemplate:getColumns:error]", e);
}
const tableName = qualifiedTableName({ databaseType: dbType, schema: tableSchema, tableName: node.label });
// SET clause: non-PK columns only (updating PKs is unusual and dangerous)
const pkNameSet = new Set(primaryKeys);
const updateColumns = columns.filter((c) => !pkNameSet.has(c.name));
const setClauses = updateColumns.map((c) => `${quoteTableIdentifier(dbType, c.name)} = ${columnPlaceholderValue(c, dbType)}`);
// WHERE clause: use primary keys to target a single row
let whereClause: string;
if (primaryKeys.length > 0) {
const pkColumns = columns.filter((c) => pkNameSet.has(c.name));
const pkConditions = pkColumns.map((c) => `${quoteTableIdentifier(dbType, c.name)} = ${columnPlaceholderValue(c, dbType)}`);
whereClause = `WHERE ${pkConditions.join(" AND ")}`;
const schema = node.schema || node.database;
let ddl: string;
if (node.type === "table") {
ddl = await api.getTableDdl(node.connectionId, node.database, schema, node.label);
} else {
whereClause = `WHERE /* TODO: add WHERE clause */`;
const objectType = node.type === "materialized_view" ? "MATERIALIZED_VIEW" : "VIEW";
const result = await api.getObjectSource(node.connectionId, node.database, schema, node.label, objectType);
ddl = await buildViewDdl({
databaseType: currentDatabaseType(),
schema,
name: node.label,
source: result.source,
});
}
const sql = `UPDATE ${tableName}\nSET ${setClauses.join(",\n ")}\n${whereClause};`;
const tabId = queryStore.createTab(node.connectionId, node.database, undefined, "query", node.schema);
queryStore.updateSql(tabId, sql);
openSqlTemplateTab(node.connectionId, node.database, node.schema, ddl, `DDL - ${node.label}`);
} catch (e: any) {
toast(t("connection.connectFailed", { message: translateBackendError(t, e?.message || String(e)) }), 5000);
toast(e?.message || String(e), 5000);
}
}
@ -3384,19 +3300,22 @@ function treeItemMenuItems(): ContextMenuItem[] {
variant: "destructive" as const,
});
}
if (isTableNotView.value) {
items.push({
label: t("contextMenu.newSql"),
icon: FilePlus,
children: [
{ label: t("contextMenu.newQuery"), action: newQuery, icon: TerminalSquare },
{ label: t("contextMenu.newInsert"), action: newInsertTemplate, icon: FilePlus },
{ label: t("contextMenu.newUpdate"), action: newUpdateTemplate, icon: SquarePen },
],
});
} else {
items.push({ label: t("contextMenu.newQuery"), action: newQuery, icon: TerminalSquare });
}
items.push({
label: t("contextMenu.generateSql"),
icon: FilePlus,
children: isTableNotView.value
? [
{ label: "SELECT", action: newSelectTemplate, icon: TerminalSquare },
{ label: "INSERT", action: newInsertTemplate, icon: FilePlus },
{ label: "UPDATE", action: newUpdateTemplate, icon: SquarePen },
{ label: "DELETE", action: newDeleteTemplate, icon: ListX },
{ label: "DDL", action: generateDdlTemplate, icon: FileCode },
]
: [
{ label: "SELECT", action: newSelectTemplate, icon: TerminalSquare },
{ label: "DDL", action: generateDdlTemplate, icon: FileCode },
],
});
const sqlHistoryMenu = savedSqlHistorySubmenu();
if (sqlHistoryMenu) items.push(sqlHistoryMenu);
if (canOpenDiagram.value) {

View File

@ -1066,6 +1066,7 @@ export default {
duplicateConnection: "Duplicate Connection",
newQuery: "New Query",
newSql: "New SQL",
generateSql: "Generate SQL",
newInsert: "New Insert",
newUpdate: "New Update",
sqlHistory: "Query History",

View File

@ -894,6 +894,7 @@ export default {
duplicateConnection: "Duplicar conexión",
newQuery: "Nueva consulta",
newSql: "Nuevo SQL",
generateSql: "Generar SQL",
newInsert: "Nueva Inserción",
newUpdate: "Nueva Actualización",
sqlHistory: "Historial de consultas",

View File

@ -1001,6 +1001,7 @@ export default {
duplicateConnection: "Duplica Connessione",
newQuery: "Nuova Query",
newSql: "Nuovo SQL",
generateSql: "Genera SQL",
newInsert: "Nuovo Insert",
newUpdate: "Nuovo Update",
sqlHistory: "Cronologia Query",

View File

@ -1055,6 +1055,7 @@ export default {
duplicateConnection: "接続を複製",
newQuery: "新しいクエリ",
newSql: "新しいSQL",
generateSql: "SQLを生成",
newInsert: "新しいINSERT",
newUpdate: "新しいUPDATE",
sqlHistory: "クエリ履歴",

View File

@ -1001,6 +1001,7 @@ export default {
duplicateConnection: "Duplicar Conexão",
newQuery: "Nova Consulta",
newSql: "Novo SQL",
generateSql: "Gerar SQL",
newInsert: "Novo Insert",
newUpdate: "Novo Update",
sqlHistory: "Histórico de Consultas",

View File

@ -1066,6 +1066,7 @@ export default {
newQuery: "新建查询",
newInsert: "新建新增",
newSql: "新建 SQL",
generateSql: "生成 SQL",
newUpdate: "新建更新",
sqlHistory: "历史查询",
noSqlHistory: "暂无关联 SQL",

View File

@ -981,6 +981,7 @@ export default {
duplicateConnection: "複製連線",
newQuery: "建立查詢",
newSql: "新增 SQL",
generateSql: "產生 SQL",
newInsert: "建立新增",
newUpdate: "建立更新",
sqlHistory: "歷史查詢",

View File

@ -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");
}

View File

@ -0,0 +1,78 @@
import { strict as assert } from "node:assert";
import { test } from "vitest";
import type { ColumnInfo } from "../../apps/desktop/src/types/database.ts";
import { buildTableDeleteTemplate, buildTableInsertTemplate, buildTableSelectTemplate, buildTableUpdateTemplate } from "../../apps/desktop/src/lib/tableSqlTemplates.ts";
function col(overrides: Partial<ColumnInfo> & { name: string; data_type: string }): ColumnInfo {
return {
is_nullable: true,
column_default: null,
is_primary_key: false,
extra: null,
...overrides,
};
}
const columns: ColumnInfo[] = [
col({ name: "id", data_type: "integer", is_primary_key: true, extra: "auto_increment" }),
col({ name: "name", data_type: "varchar" }),
col({ name: "created_at", data_type: "timestamp" }),
];
test("builds SELECT template with explicit table columns", () => {
assert.equal(
buildTableSelectTemplate({
databaseType: "postgres",
schema: "public",
tableName: "users",
columns,
}),
'SELECT "id", "name", "created_at"\nFROM "public"."users";',
);
});
test("builds INSERT template without auto generated columns", () => {
assert.equal(
buildTableInsertTemplate({
databaseType: "mysql",
tableName: "users",
columns,
}),
"INSERT INTO `users` (`name`, `created_at`)\nVALUES ('name_value', CURRENT_TIMESTAMP);",
);
});
test("builds UPDATE template with primary key WHERE clause", () => {
assert.equal(
buildTableUpdateTemplate({
databaseType: "postgres",
schema: "public",
tableName: "users",
columns,
}),
'UPDATE "public"."users"\nSET "name" = \'name_value\',\n "created_at" = CURRENT_TIMESTAMP\nWHERE "id" = 0;',
);
});
test("builds DELETE template with primary key WHERE clause", () => {
assert.equal(
buildTableDeleteTemplate({
databaseType: "postgres",
schema: "public",
tableName: "users",
columns,
}),
'DELETE FROM "public"."users"\nWHERE "id" = 0;',
);
});
test("builds DELETE template with TODO WHERE clause when no primary key exists", () => {
assert.equal(
buildTableDeleteTemplate({
databaseType: "sqlite",
tableName: "audit",
columns: [col({ name: "message", data_type: "text" })],
}),
'DELETE FROM "audit"\nWHERE /* TODO: add WHERE clause */;',
);
});