package aceboot::orm.common
import std.collection.*
/**
* 中性列类型(由方言映射到各数据库的具体 SQL 类型)。
* - TInt/TText/TReal/TBool:基础标量。
* - TDateTime/TDate:日期时间,统一以毫秒整型存储(三库均映射为整型,跨库一致、便于运算)。
* - TJson:JSON 文档,文本存储(SQLite TEXT / PG JSONB / MySQL JSON)。
* - TBlob:二进制大对象(SQLite BLOB / PG BYTEA / MySQL BLOB),对应 DbValue.DbBytes。
* - TDecimal:高精度十进制。仓颉标准库无 Decimal 类型,故以文本存储(PG NUMERIC / MySQL DECIMAL)。
*/
public enum DbType {
| TInt
| TText
| TReal
| TBool
| TDateTime
| TDate
| TJson
| TBlob
| TDecimal
}
/**
* 一列的中性元数据(@Entity 宏生成;方言据此建表,与具体数据库无关)。
* 基础 4 字段(name/dbType/isPk/fkRef)保持位置参数;扩展约束走带默认值的命名参数,
* 故旧调用 `ColumnSpec(name, dbType, isPk, fkRef)` 与既有宏发射保持向后兼容。
* - nullable:是否允许 NULL(false 且非主键 → 生成 `NOT NULL`)。
* - unique:是否列级唯一(true → `UNIQUE`)。
* - defaultSql:默认值 SQL 片段("" 表示无;原样拼入 `DEFAULT <x>`)。
* - length:文本长度(>0 且文本列 → `VARCHAR(length)`;0 表示按 DbType 推断)。
* - explicitType:显式 SQL 类型("" 表示按 DbType 推断;非空则覆盖)。
* - autoIncrement:主键是否自增(true=自增主键;false=手动主键 @PrimaryColumn)。
* - fkRefIdColumn:外键所引用的目标表主键列名(默认 "id";目标实体自定义主键列时与 JOIN 目标对齐)。
*/
public class ColumnSpec {
public let name: String
public let dbType: DbType
public let isPk: Bool
public let fkRef: String // "" 或引用表名(外键 -> ref(id))
public let nullable: Bool
public let unique: Bool
public let defaultSql: String
public let length: Int64
public let explicitType: String
public let autoIncrement: Bool
public let fkRefIdColumn: String
// 默认 SELECT 是否包含该列(false=隐藏列,如密码;仅影响 findAll/findById/QB 默认投影,不影响 insert/update/建表)。
public let selectable: Bool
// 列注释("" 表示无)。DDL 发射按方言分歧:PG 独立 `COMMENT ON COLUMN`、MySQL 内联 `COMMENT '...'`、SQLite 跳过。
public let comment: String
// Decimal 精度 p(0=不指定,回退方言默认);拼 DECIMAL(p,s)/NUMERIC(p,s)。
public let precision: Int64
// Decimal 小数位 s(仅 precision>0 时生效)。
public let scale: Int64
// 枚举允许值(空=非枚举列;非空则 DDL 追加三库通用的 CHECK(col IN ('a','b',...)))。
public let enumValues: Array<String>
// 外键引用动作("" 表示不指定):非空则 FK DDL 追加 `ON DELETE <x>` / `ON UPDATE <x>`。
// 取值如 "CASCADE"/"SET NULL"/"RESTRICT"/"NO ACTION"(原样拼入,由方言语法负责)。
public let fkOnDelete: String
public let fkOnUpdate: String
public init(name: String, dbType: DbType, isPk: Bool, fkRef: String, nullable!: Bool = true,
unique!: Bool = false, defaultSql!: String = "", length!: Int64 = 0, explicitType!: String = "",
autoIncrement!: Bool = true, fkRefIdColumn!: String = "id",
selectable!: Bool = true, comment!: String = "", precision!: Int64 = 0, scale!: Int64 = 0,
enumValues!: Array<String> = Array<String>(), fkOnDelete!: String = "", fkOnUpdate!: String = "") {
this.name = name
this.dbType = dbType
this.isPk = isPk
this.fkRef = fkRef
this.nullable = nullable
this.unique = unique
this.defaultSql = defaultSql
this.length = length
this.explicitType = explicitType
this.autoIncrement = autoIncrement
this.fkRefIdColumn = fkRefIdColumn
this.selectable = selectable
this.comment = comment
this.precision = precision
this.scale = scale
this.enumValues = enumValues
this.fkOnDelete = fkOnDelete
this.fkOnUpdate = fkOnUpdate
}
}
/** 一个索引的中性元数据(@Entity 宏据 @Index 收集;方言据此生成 CREATE INDEX)。 */
public class IndexSpec {
public let name: String
public let columns: Array<String>
public let unique: Bool
public init(name: String, columns: Array<String>, unique: Bool) {
this.name = name
this.columns = columns
this.unique = unique
}
}
/**
* SQL 方言:把「中性列元数据 + 参数占位」翻译成各数据库的具体语法。
* 让同一套实体/仓储跑在 SQLite / PostgreSQL / MySQL 上(占位符、自增主键、类型映射各异)。
*/
public interface Dialect {
/** 第 index(0 基)个参数占位符:SQLite/MySQL 为 "?",PostgreSQL 为 "$1/$2..."。 */
func placeholder(index: Int64): String
/** 中性列类型映射为本方言的具体 SQL 类型(建表与 ALTER ... ADD COLUMN 共用)。 */
func sqlType(t: DbType): String
/** 由列元数据生成 CREATE TABLE 语句(无表级 CHECK;默认委托 3 参重载,向后兼容旧调用点)。 */
func createTable(table: String, cols: Array<ColumnSpec>): String {
createTable(table, cols, Array<CheckSpec>())
}
/** 由列元数据 + 表级 CHECK 约束生成 CREATE TABLE 语句(各方言实现)。 */
func createTable(table: String, cols: Array<ColumnSpec>, checks: Array<CheckSpec>): String
/** 由索引元数据生成 CREATE [UNIQUE] INDEX 语句。 */
func createIndex(table: String, idx: IndexSpec): String
/** 插入自增行后取 id 是否需 `RETURNING`(PostgreSQL 为 true;SQLite/MySQL 用驱动取 last-insert-id)。 */
func usesReturning(): Bool
/**
* 生成 upsert 冲突子句,追加到 `INSERT INTO ... VALUES(...)` 之后(对标 TypeORM Repository.upsert):
* - SQLite/PostgreSQL:`ON CONFLICT(conflictColumns) DO UPDATE SET c = excluded.c, ...`(无更新列则 DO NOTHING)。
* - MySQL:`ON DUPLICATE KEY UPDATE c = VALUES(c), ...`(由唯一键触发,conflictColumns 仅作回退占位)。
* conflictColumns 为冲突判定列;updateColumns 为冲突时要更新的列(已剔除冲突列与不应覆盖的列)。
*/
func upsertClause(conflictColumns: Array<String>, updateColumns: Array<String>): String
/**
* 悲观锁尾部子句(SELECT 末尾,LIMIT/OFFSET 之后追加):
* - "update" → " FOR UPDATE";"share" → " FOR SHARE";其它/"" → ""(不加锁,向后兼容)。
* 默认给出标准 SQL 行锁子句(PostgreSQL/MySQL 8.0+ 通用);SQLite 无行锁 → 覆盖返回 ""。
*/
func lockClause(mode: String): String {
if (mode == "update") {
return " FOR UPDATE"
}
if (mode == "share") {
return " FOR SHARE"
}
return ""
}
/**
* schema 内省:判定表是否存在的查询(返回 SQL + 参数;有任意行即视为存在)。
* 默认走 SQL 标准 information_schema(PostgreSQL/MySQL 适用);SQLite 覆盖为 sqlite_master。
*/
func introspectTableExists(table: String): (String, Array<DbValue>) {
("SELECT table_name FROM information_schema.tables WHERE table_name = " + placeholder(0), [DbText(table)])
}
/**
* schema 内省:列出某表所有列名的查询(每行含名为 "name" 的列)。
* 默认走 information_schema.columns;SQLite 覆盖为 PRAGMA table_info。
*/
func introspectColumns(table: String): (String, Array<DbValue>) {
("SELECT column_name AS name FROM information_schema.columns WHERE table_name = " + placeholder(0),
[DbText(table)])
}
/**
* schema 内省:列出某表所有列「名 + 库现状类型」的查询(每行含 "name" 与 "type" 两列),供改列类型 diff。
* 默认走 information_schema.columns(data_type);SQLite 覆盖为 PRAGMA table_info(其结果天然含 name/type 列)。
*/
func introspectColumnTypes(table: String): (String, Array<DbValue>) {
("SELECT column_name AS name, data_type AS type FROM information_schema.columns WHERE table_name = " +
placeholder(0), [DbText(table)])
}
/**
* 「改列类型」DDL:把 col 改成目标类型。返回 None 表示本方言不支持原地改列类型
* (SQLite 需「建新表+拷贝+换名」较重,本轮 best-effort 跳过);PG/MySQL 覆盖返回具体语句。
*/
func alterColumnType(_: String, _: ColumnSpec): ?String {
None
}
/** 「删列」DDL(ALTER TABLE ... DROP COLUMN)。三方言通用(SQLite 需 3.35+,本机满足)。 */
func dropColumn(table: String, col: String): String {
"ALTER TABLE " + table + " DROP COLUMN " + col
}
/**
* 库现状列类型(currentRaw,来自内省)是否与目标列(target)等价。
* 默认 true(视为一致 → 不触发改列,best-effort);PG/MySQL 覆盖做规范化比较以驱动 ALTER。
*/
func columnTypeMatches(_: String, _: ColumnSpec): Bool {
true
}
/**
* 建表后单独发射的「列注释」DDL(每列一条,建表 exec 之后逐条 exec)。
* 默认返回空 → SQLite/MySQL 天然继承(SQLite 无列注释;MySQL 走内联 COMMENT,不用独立语句)。
* 仅 PostgreSQL 覆盖:返回每个含 comment 列的 `COMMENT ON COLUMN <table>.<col> IS '...'`。
*/
func columnComments(_: String, _: Array<ColumnSpec>): Array<String> {
Array<String>()
}
}
/**
* 规范化 SQL 类型串以做跨方言「库现状 vs 目标」比较:小写、截断括号内长度/精度、归并常见同义词。
* 例:"VARCHAR(255)"→"varchar"、PG "character varying"→"varchar"、"INTEGER"/"int4"→"int"、"int8"→"bigint"。
*/
protected func normalizeSqlType(raw: String): String {
let out = ArrayList<UInt8>()
for (b in raw.toArray()) {
if (b == 40u8) {
// '(' 之后是长度/精度,忽略
break
}
var c = b
if (c >= 65u8 && c <= 90u8) {
// A-Z → 小写
c = c + 32u8
}
out.add(c)
}
let s = String.fromUtf8(out.toArray())
if (s == "character varying") {
return "varchar"
}
if (s == "integer" || s == "int4") {
return "int"
}
if (s == "int8") {
return "bigint"
}
return s
}
/**
* 由列元数据生成 `ALTER TABLE <table> ADD COLUMN <def>`(schema sync 补列用)。
* 列定义复用建表的类型映射与约束拼接,故与 CREATE TABLE 中的列定义一致。
* 注:SQLite 对已有数据的表加 NOT NULL 列须带 DEFAULT,调用方应在 ColumnSpec 上配 defaultSql 或保持 nullable。
*/
public func buildAddColumn(table: String, c: ColumnSpec, d: Dialect): String {
let typ = columnType(c, {t => d.sqlType(t)})
return "ALTER TABLE " + table + " ADD COLUMN " + c.name + " " + typ + columnConstraints(c)
}
// 逗号连接列名(upsert 子句构造用)。
func joinColsComma(cols: Array<String>): String {
var s = ""
var first = true
for (c in cols) {
if (!first) {
s = s + ", "
}
first = false
s = s + c
}
return s
}
// SQLite/PostgreSQL 的 ON CONFLICT 子句:有更新列则 DO UPDATE SET c = excluded.c,否则 DO NOTHING。
protected func onConflictClause(conflictColumns: Array<String>, updateColumns: Array<String>): String {
let conf = joinColsComma(conflictColumns)
if (updateColumns.size == 0) {
return " ON CONFLICT(" + conf + ") DO NOTHING"
}
var sets = ""
var first = true
for (c in updateColumns) {
if (!first) {
sets = sets + ", "
}
first = false
sets = sets + c + " = excluded." + c
}
return " ON CONFLICT(" + conf + ") DO UPDATE SET " + sets
}
// MySQL 的 ON DUPLICATE KEY UPDATE 子句:c = VALUES(c)(无更新列时回退用冲突列做空更新)。
protected func onDuplicateClause(updateColumns: Array<String>, conflictColumns: Array<String>): String {
var cols = updateColumns
if (cols.size == 0) {
cols = conflictColumns
}
var sets = ""
var first = true
for (c in cols) {
if (!first) {
sets = sets + ", "
}
first = false
sets = sets + c + " = VALUES(" + c + ")"
}
return " ON DUPLICATE KEY UPDATE " + sets
}
// 列的 SQL 类型:explicitType 优先;否则 Decimal 列带 precision 拼 NUMERIC/DECIMAL(p,s)(SQLite TEXT 忽略精度);
// 否则文本列带 length 用 VARCHAR(length);否则按 DbType 推断。
protected func columnType(c: ColumnSpec, typeOf: (DbType) -> String): String {
if (c.explicitType != "") {
return c.explicitType
}
if (isDecimalType(c.dbType) && c.precision > 0) {
// typeOf(TDecimal):MySQL 'DECIMAL(38, 10)' / PG 'NUMERIC' / SQLite 'TEXT'。
// 先截断 '(' 前的关键字(避免 MySQL 出现 DECIMAL(38, 10)(p,s) 双括号),再拼 (p,s)。
let base = decimalKeyword(typeOf(TDecimal))
if (base == "TEXT") {
// SQLite 无 Decimal 类型(TEXT 存储),精度无意义。
return "TEXT"
}
if (c.scale > 0) {
return base + "(" + c.precision.toString() + ", " + c.scale.toString() + ")"
}
return base + "(" + c.precision.toString() + ")"
}
if (c.length > 0 && isTextType(c.dbType)) {
return "VARCHAR(" + c.length.toString() + ")"
}
return typeOf(c.dbType)
}
func isTextType(t: DbType): Bool {
match (t) {
case TText => true
case _ => false
}
}
func isDecimalType(t: DbType): Bool {
match (t) {
case TDecimal => true
case _ => false
}
}
// 取原始类型串中 '(' 之前的关键字(保留大小写);无 '(' 则原样返回,顺带去尾部空格。
// 例:"DECIMAL(38, 10)"→"DECIMAL"、"NUMERIC"→"NUMERIC"、"TEXT"→"TEXT"。
func decimalKeyword(raw: String): String {
let out = ArrayList<UInt8>()
for (b in raw.toArray()) {
if (b == 40u8) {
break
}
out.add(b)
}
// 去尾部空格。
while (out.size > 0 && out[out.size - 1] == 32u8) {
out.remove(at: out.size - 1)
}
return String.fromUtf8(out.toArray())
}
// SQL 字符串字面量:单引号包裹,内部单引号加倍转义(enum CHECK 值来自编译期字面量,仍做转义防注入)。
protected func sqlStringLiteral(s: String): String {
let out = ArrayList<UInt8>()
out.add(39u8)
for (b in s.toArray()) {
if (b == 39u8) {
out.add(39u8)
}
out.add(b)
}
out.add(39u8)
return String.fromUtf8(out.toArray())
}
// 非主键列的内联约束:NOT NULL / UNIQUE / DEFAULT / CHECK(enum)(按需追加,顺序固定)。
// enum 用三库通用的 CHECK(col IN ('a','b',...))。
// commentInline:仅 MySQL 传 true → 列尾追加内联 COMMENT '...'(SQLite/PG 绝不内联,否则建表语法报错)。
func columnConstraints(c: ColumnSpec, commentInline!: Bool = false): String {
var s = ""
if (!c.nullable) {
s = s + " NOT NULL"
}
if (c.unique) {
s = s + " UNIQUE"
}
if (c.defaultSql != "") {
s = s + " DEFAULT " + c.defaultSql
}
if (c.enumValues.size > 0) {
var vals = ""
var first = true
for (v in c.enumValues) {
if (!first) {
vals = vals + ", "
}
first = false
vals = vals + sqlStringLiteral(v)
}
s = s + " CHECK(" + c.name + " IN (" + vals + "))"
}
if (commentInline && c.comment != "") {
s = s + " COMMENT " + sqlStringLiteral(c.comment)
}
return s
}
// 拼公共的列定义 + 外键约束 + 表级 CHECK(各方言传入自己的「自增主键定义」与「类型映射」函数)。
// 主键分两类:autoIncrement → 用 pkDef(自增);否则手动主键 `name type PRIMARY KEY`。
// checks:表级 CHECK 约束(默认空,向后兼容);具名 → `, CONSTRAINT n CHECK (expr)`,匿名 → `, CHECK (expr)`。
protected func buildCreateTable(table: String, cols: Array<ColumnSpec>, pkDef: (String) -> String,
typeOf: (DbType) -> String, checks!: Array<CheckSpec> = Array<CheckSpec>(),
commentInline!: Bool = false): String {
var defs = ""
var fks = ""
var first = true
// 主键列数:单主键(==1)走列级 PRIMARY KEY(输出与既有逐字节一致);复合主键(>1)走表级 PRIMARY KEY(a, b)。
var pkCount = 0
for (c in cols) {
if (c.isPk) {
pkCount += 1
}
}
let pkNames = ArrayList<String>()
for (c in cols) {
if (!first) {
defs = defs + ", "
}
first = false
if (c.isPk && pkCount == 1) {
if (c.autoIncrement) {
defs = defs + pkDef(c.name)
} else {
defs = defs + c.name + " " + columnType(c, typeOf) + " PRIMARY KEY"
}
} else if (c.isPk) {
// 复合主键列:作为普通列定义(复合主键恒非自增),主键约束在循环后以表级 PRIMARY KEY(...) 追加。
defs = defs + c.name + " " + columnType(c, typeOf) + columnConstraints(c, commentInline: commentInline)
pkNames.add(c.name)
} else {
defs = defs + c.name + " " + columnType(c, typeOf) + columnConstraints(c, commentInline: commentInline)
}
if (c.fkRef != "") {
// 外键引用目标表主键列(默认 "id";目标实体自定义主键列时与 JOIN 目标对齐)。
let refId = if (c.fkRefIdColumn == "") {
"id"
} else {
c.fkRefIdColumn
}
fks = fks + ", FOREIGN KEY(" + c.name + ") REFERENCES " + c.fkRef + "(" + refId + ")"
if (c.fkOnDelete != "") {
fks = fks + " ON DELETE " + c.fkOnDelete
}
if (c.fkOnUpdate != "") {
fks = fks + " ON UPDATE " + c.fkOnUpdate
}
}
}
if (pkCount > 1) {
defs = defs + ", PRIMARY KEY(" + joinColsComma(pkNames.toArray()) + ")"
}
var cks = ""
for (ck in checks) {
let named = if (ck.name != "") {
"CONSTRAINT " + ck.name + " "
} else {
""
}
cks = cks + ", " + named + "CHECK (" + ck.expr + ")"
}
return "CREATE TABLE IF NOT EXISTS " + table + "(" + defs + fks + cks + ")"
}
// 拼 CREATE INDEX(ifNotExists 控制是否带 IF NOT EXISTS:MySQL 不支持该子句)。
protected func buildCreateIndex(table: String, idx: IndexSpec, ifNotExists: Bool): String {
var cols = ""
var first = true
for (c in idx.columns) {
if (!first) {
cols = cols + ", "
}
first = false
cols = cols + c
}
let uniq = if (idx.unique) {
"UNIQUE "
} else {
""
}
let ine = if (ifNotExists) {
"IF NOT EXISTS "
} else {
""
}
return "CREATE " + uniq + "INDEX " + ine + idx.name + " ON " + table + "(" + cols + ")"
}