дзеркало
https://github.com/l0ng-ai/openidea.git
синхронізовано 2026-11-09 11:15:40 +02:00
Initial open-source release
Claude-Session: https://claude.ai/code/session_01B65qLEKnkeYTGdbwhLpjCu
Цей коміт міститься в:
коміт
9a2025b76e
2034 files changed
+243381
No files matched your search
@@ -0,0 +1,225 @@
|
||||
/**
|
||||
* SQL that the Database tree's object actions run or paste (IDEA: Rename,
|
||||
* Drop, Truncate, SQL Scripts › …, Modify Table, Copy Reference), per dialect.
|
||||
* Pure functions.
|
||||
*/
|
||||
import type { DbColumnInfo, DbDriver } from "@/platform/ipc";
|
||||
|
||||
export function quoteIdent(driver: DbDriver, name: string): string {
|
||||
return driver === "mysql" ? `\`${name.replace(/`/g, "``")}\`` : `"${name.replace(/"/g, '""')}"`;
|
||||
}
|
||||
|
||||
/** Quotes only when needed: lower-case (PostgreSQL) / plain words stay bare, like IDEA's generated SQL. */
|
||||
export function ident(driver: DbDriver, name: string): string {
|
||||
const plain = driver === "postgres" ? /^[a-z_][a-z0-9_$]*$/ : /^[A-Za-z_][A-Za-z0-9_$]*$/;
|
||||
return plain.test(name) && !RESERVED.has(name.toUpperCase()) ? name : quoteIdent(driver, name);
|
||||
}
|
||||
|
||||
const RESERVED = new Set(
|
||||
(
|
||||
"ALL AND AS ASC BETWEEN BY CASE CHECK COLUMN CONSTRAINT CREATE CROSS DEFAULT DELETE DESC DISTINCT DROP " +
|
||||
"ELSE END EXISTS FOREIGN FROM FULL GROUP HAVING IN INDEX INNER INSERT INTO IS JOIN KEY LEFT LIKE LIMIT NOT " +
|
||||
"NULL OFFSET ON OR ORDER OUTER PRIMARY REFERENCES RIGHT SELECT SET TABLE THEN TO UNION UNIQUE UPDATE USER " +
|
||||
"USING VALUES VIEW WHEN WHERE WITH"
|
||||
).split(" "),
|
||||
);
|
||||
|
||||
/** `schema.name`; SQLite's `main` and an empty schema are left out. */
|
||||
export function qualifiedName(driver: DbDriver, schema: string, name: string): string {
|
||||
if (!schema || (driver === "sqlite" && schema === "main")) return ident(driver, name);
|
||||
return `${ident(driver, schema)}.${ident(driver, name)}`;
|
||||
}
|
||||
|
||||
export type DbObjectKind = "table" | "view" | "column" | "index" | "schema";
|
||||
|
||||
/** A schema object of a data source, as the tree actions see it. */
|
||||
export interface DbObjectRef {
|
||||
kind: DbObjectKind;
|
||||
schema: string;
|
||||
/** Table / view / index / schema name; the column's table for a column. */
|
||||
name: string;
|
||||
/** Column name (kind `column`). */
|
||||
column?: string;
|
||||
/** Table of an index (MySQL drops an index `ON` its table). */
|
||||
table?: string;
|
||||
}
|
||||
|
||||
/** IDEA's Copy Reference: the object's qualified name. */
|
||||
export function reference(driver: DbDriver, o: DbObjectRef): string {
|
||||
if (o.kind === "schema") return ident(driver, o.name);
|
||||
const q = qualifiedName(driver, o.schema, o.name);
|
||||
return o.kind === "column" && o.column ? `${q}.${ident(driver, o.column)}` : q;
|
||||
}
|
||||
|
||||
export const canRename = (driver: DbDriver, o: DbObjectRef) =>
|
||||
o.kind === "table" || o.kind === "column" || (o.kind === "view" && driver !== "sqlite") || (o.kind === "index" && driver === "postgres");
|
||||
|
||||
/** The statement renaming `o` to `to`. */
|
||||
export function renameSql(driver: DbDriver, o: DbObjectRef, to: string): string {
|
||||
const q = qualifiedName(driver, o.schema, o.name);
|
||||
switch (o.kind) {
|
||||
case "column":
|
||||
return `alter table ${q} rename column ${ident(driver, o.column ?? "")} to ${ident(driver, to)};`;
|
||||
case "index":
|
||||
return `alter index ${q} rename to ${ident(driver, to)};`;
|
||||
case "view":
|
||||
if (driver === "postgres") return `alter view ${q} rename to ${ident(driver, to)};`;
|
||||
return `rename table ${q} to ${qualifiedName(driver, o.schema, to)};`;
|
||||
default:
|
||||
if (driver === "mysql") return `rename table ${q} to ${qualifiedName(driver, o.schema, to)};`;
|
||||
return `alter table ${q} rename to ${ident(driver, to)};`;
|
||||
}
|
||||
}
|
||||
|
||||
export interface DropOptions {
|
||||
ifExists: boolean;
|
||||
/** PostgreSQL `CASCADE`. */
|
||||
cascade: boolean;
|
||||
}
|
||||
|
||||
export const canDrop = (driver: DbDriver, o: DbObjectRef) => !(o.kind === "schema" && driver === "sqlite");
|
||||
|
||||
/** IDEA's Confirm Drop preview. */
|
||||
export function dropSql(driver: DbDriver, o: DbObjectRef, opts: DropOptions): string {
|
||||
const ifExists = opts.ifExists ? "if exists " : "";
|
||||
const cascade = opts.cascade && driver === "postgres" ? " cascade" : "";
|
||||
switch (o.kind) {
|
||||
case "column":
|
||||
return `alter table ${qualifiedName(driver, o.schema, o.name)} drop column ${ifExists && driver === "postgres" ? ifExists : ""}${ident(driver, o.column ?? "")}${cascade};`;
|
||||
case "index":
|
||||
if (driver === "mysql") return `drop index ${ident(driver, o.name)} on ${qualifiedName(driver, o.schema, o.table ?? "")};`;
|
||||
return `drop index ${ifExists}${qualifiedName(driver, o.schema, o.name)}${cascade};`;
|
||||
case "schema":
|
||||
return `drop ${driver === "mysql" ? "database" : "schema"} ${ifExists}${ident(driver, o.name)}${cascade};`;
|
||||
default:
|
||||
return `drop ${o.kind} ${ifExists}${qualifiedName(driver, o.schema, o.name)}${cascade};`;
|
||||
}
|
||||
}
|
||||
|
||||
/** Object Actions › Truncate (SQLite has no TRUNCATE: `delete from`). */
|
||||
export function truncateSql(driver: DbDriver, schema: string, table: string, opts: { restartIdentity: boolean; cascade: boolean }): string {
|
||||
const q = qualifiedName(driver, schema, table);
|
||||
if (driver === "sqlite") return `delete from ${q};`;
|
||||
if (driver === "mysql") return `truncate table ${q};`;
|
||||
return `truncate table ${q}${opts.restartIdentity ? " restart identity" : ""}${opts.cascade ? " cascade" : ""};`;
|
||||
}
|
||||
|
||||
export type ScriptKind = "selectAll" | "selectFirst" | "insert" | "update" | "delete";
|
||||
|
||||
export const SCRIPT_LABELS: Record<ScriptKind, string> = {
|
||||
selectAll: "Select all rows from a table",
|
||||
selectFirst: "Select first N rows from a table",
|
||||
insert: "Insert rows into a table",
|
||||
update: "Update values in a table",
|
||||
delete: "Delete rows from a table",
|
||||
};
|
||||
|
||||
/** SQL Scripts › … templates (IDEA's live templates `sel`, `selc`, `ins`, `upd`, `del`). */
|
||||
export function scriptSql(driver: DbDriver, kind: ScriptKind, schema: string, table: string, columns: readonly DbColumnInfo[]): string {
|
||||
const q = qualifiedName(driver, schema, table);
|
||||
const names = columns.map((c) => ident(driver, c.name));
|
||||
const keys = columns.filter((c) => c.primaryKey).map((c) => ident(driver, c.name));
|
||||
const where = (keys.length ? keys : names.slice(0, 1)).map((k) => `${k} = ?`).join(" and ") || "1 = 0";
|
||||
switch (kind) {
|
||||
case "selectAll":
|
||||
return `select *\nfrom ${q};\n`;
|
||||
case "selectFirst":
|
||||
return `select *\nfrom ${q}\nlimit 10;\n`;
|
||||
case "insert":
|
||||
return `insert into ${q} (${names.join(", ")})\nvalues (${names.map(() => "?").join(", ")});\n`;
|
||||
case "update": {
|
||||
const set = columns.filter((c) => !c.primaryKey).map((c) => `${ident(driver, c.name)} = ?`);
|
||||
return `update ${q}\nset ${(set.length ? set : names.map((n) => `${n} = ?`)).join(",\n ")}\nwhere ${where};\n`;
|
||||
}
|
||||
case "delete":
|
||||
return `delete\nfrom ${q}\nwhere ${where};\n`;
|
||||
}
|
||||
}
|
||||
|
||||
// ---- Modify Table ------------------------------------------------------------
|
||||
|
||||
/** A column row of the Modify Table dialog; `original` is the column it started as (absent: new). */
|
||||
export interface ColumnDraft {
|
||||
key: number;
|
||||
original: DbColumnInfo | null;
|
||||
name: string;
|
||||
dataType: string;
|
||||
notNull: boolean;
|
||||
/** SQL expression; empty for none. */
|
||||
default: string;
|
||||
primaryKey: boolean;
|
||||
dropped: boolean;
|
||||
}
|
||||
|
||||
export function draftOf(c: DbColumnInfo, key: number): ColumnDraft {
|
||||
return {
|
||||
key,
|
||||
original: c,
|
||||
name: c.name,
|
||||
dataType: c.dataType,
|
||||
notNull: !c.nullable,
|
||||
default: c.default ?? "",
|
||||
primaryKey: c.primaryKey,
|
||||
dropped: false,
|
||||
};
|
||||
}
|
||||
|
||||
function columnDefinition(driver: DbDriver, c: ColumnDraft): string {
|
||||
let s = `${ident(driver, c.name)} ${c.dataType.trim() || (driver === "sqlite" ? "TEXT" : "text")}`;
|
||||
if (c.default.trim()) s += ` default ${c.default.trim()}`;
|
||||
if (c.notNull) s += " not null";
|
||||
return s;
|
||||
}
|
||||
|
||||
/** `create table` for New › Table. */
|
||||
export function createTableSql(driver: DbDriver, schema: string, name: string, columns: readonly ColumnDraft[]): string {
|
||||
const live = columns.filter((c) => !c.dropped && c.name.trim());
|
||||
const keys = live.filter((c) => c.primaryKey).map((c) => ident(driver, c.name));
|
||||
const items = live.map((c) => ` ${columnDefinition(driver, c)}`);
|
||||
if (keys.length) items.push(` primary key (${keys.join(", ")})`);
|
||||
return `create table ${qualifiedName(driver, schema, name)}\n(\n${items.join(",\n")}\n);\n`;
|
||||
}
|
||||
|
||||
/** Whether `alterTableSql` can express changing column `c` (SQLite only renames, adds and drops). */
|
||||
export function canAlterColumn(driver: DbDriver, c: ColumnDraft): boolean {
|
||||
return driver !== "sqlite" || !c.original;
|
||||
}
|
||||
|
||||
/**
|
||||
* The statements turning the table `table` into `name` with `columns`
|
||||
* (Modify Table): renames, added / dropped columns, and — PostgreSQL / MySQL —
|
||||
* type, nullability and default changes.
|
||||
*/
|
||||
export function alterTableSql(driver: DbDriver, schema: string, table: string, name: string, columns: readonly ColumnDraft[]): string[] {
|
||||
const out: string[] = [];
|
||||
const q = () => qualifiedName(driver, schema, table);
|
||||
for (const c of columns) {
|
||||
const o = c.original;
|
||||
if (!o) {
|
||||
if (!c.dropped && c.name.trim()) out.push(`alter table ${q()} add column ${columnDefinition(driver, c)};`);
|
||||
continue;
|
||||
}
|
||||
if (c.dropped) {
|
||||
out.push(`alter table ${q()} drop column ${ident(driver, o.name)};`);
|
||||
continue;
|
||||
}
|
||||
const renamed = c.name.trim() && c.name !== o.name;
|
||||
const changed =
|
||||
c.dataType.trim() !== o.dataType || c.notNull !== !o.nullable || c.default.trim() !== (o.default ?? "").trim();
|
||||
if (driver === "mysql" && (renamed || changed)) {
|
||||
out.push(`alter table ${q()} change column ${ident(driver, o.name)} ${columnDefinition(driver, c)};`);
|
||||
continue;
|
||||
}
|
||||
if (renamed) out.push(`alter table ${q()} rename column ${ident(driver, o.name)} to ${ident(driver, c.name)};`);
|
||||
if (driver !== "postgres") continue;
|
||||
const col = ident(driver, renamed ? c.name : o.name);
|
||||
if (c.dataType.trim() && c.dataType.trim() !== o.dataType)
|
||||
out.push(`alter table ${q()} alter column ${col} type ${c.dataType.trim()};`);
|
||||
if (c.notNull !== !o.nullable) out.push(`alter table ${q()} alter column ${col} ${c.notNull ? "set" : "drop"} not null;`);
|
||||
if (c.default.trim() !== (o.default ?? "").trim())
|
||||
out.push(`alter table ${q()} alter column ${col} ${c.default.trim() ? `set default ${c.default.trim()}` : "drop default"};`);
|
||||
}
|
||||
// Renamed last: the column statements above name the table as it was.
|
||||
if (name.trim() && name.trim() !== table) out.push(renameSql(driver, { kind: "table", schema, name: table }, name.trim()));
|
||||
return out;
|
||||
}
|
||||
Reference in new issue
Заблокувати користувача