feat: orm-cj 仓颉版 EF Core 风格 ORM 初始版本
- 基于数据模型(POCO + 注解)的映射与增删改查:DbContext/DbSet/ChangeTracker/QueryBuilder - ISqlDialect 方言接口 + PostgreSQL/openGauss 实现,零外部依赖 - @DbContext 宏:纯声明 DbSet 类自动展开为完整 DbContext 子类 - 迁移:Migration/Migrator/ModelSnapshot(快照 JSON)/MiniJson,从模型生成迁移 - 迁移 CLI:add/rm/update/downgrade/list/help,应用内嵌一行 db.cli(args) 接入 - DbContext 便捷方法:databaseExists/hasPendingMigrations/migrate - 57 个单元测试全部通过
This commit is contained in:
@@ -0,0 +1,402 @@
|
||||
/*
|
||||
* Copyright (c) 2025 SimcuTeam. All rights reserved.
|
||||
* 迁移基础设施:Migration 基类 + MigrationBuilder 声明式操作 + DDL SQL 生成。
|
||||
*
|
||||
* 用法(对齐 EF Core 迁移):
|
||||
* public class InitialCreate <: Migration {
|
||||
* public init() { super("20250701000000_InitialCreate", "创建用户表") }
|
||||
* public override func up(builder: MigrationBuilder): Unit {
|
||||
* builder.createTable("users") { tb =>
|
||||
* tb.column("id", ColumnTypes.BigIntCol).primary().autoInc().notNull()
|
||||
* tb.column("name", ColumnTypes.TextCol).withMaxLength(100).notNull()
|
||||
* }
|
||||
* builder.createIndex("ix_users_name", "users", ["name"])
|
||||
* }
|
||||
* public override func down(builder: MigrationBuilder): Unit {
|
||||
* builder.dropTable("users")
|
||||
* }
|
||||
* }
|
||||
*/
|
||||
|
||||
package simcu::orm.migrations
|
||||
|
||||
import std.collection.*
|
||||
import std.time.*
|
||||
import simcu::orm.sql.*
|
||||
|
||||
/**
|
||||
* 列定义(表结构/迁移用)。列类型 ColumnTypes 定义于 simcu::orm.sql,DDL 映射由方言决定。
|
||||
*/
|
||||
public class ColumnDefinition {
|
||||
public var name: String = ""
|
||||
public var columnType: ColumnTypes = ColumnTypes.TextCol
|
||||
public var primaryKey: Bool = false
|
||||
public var autoIncrement: Bool = false
|
||||
public var nullable: Bool = true
|
||||
public var unique: Bool = false
|
||||
public var maxLength: Int64 = 255
|
||||
public var defaultValue: ?Any = None
|
||||
|
||||
public init(name: String, columnType: ColumnTypes) {
|
||||
this.name = name
|
||||
this.columnType = columnType
|
||||
}
|
||||
|
||||
/// 标记主键
|
||||
public func primary(): ColumnDefinition {
|
||||
this.primaryKey = true
|
||||
this
|
||||
}
|
||||
|
||||
/// 标记自增(仅整型列有效)
|
||||
public func autoInc(): ColumnDefinition {
|
||||
this.autoIncrement = true
|
||||
this
|
||||
}
|
||||
|
||||
/// 标记非空
|
||||
public func notNull(): ColumnDefinition {
|
||||
this.nullable = false
|
||||
this
|
||||
}
|
||||
|
||||
/// 标记唯一
|
||||
public func withUnique(): ColumnDefinition {
|
||||
this.unique = true
|
||||
this
|
||||
}
|
||||
|
||||
/// 指定字符串最大长度
|
||||
public func withMaxLength(n: Int64): ColumnDefinition {
|
||||
this.maxLength = n
|
||||
this
|
||||
}
|
||||
|
||||
/// 指定默认值
|
||||
public func withDefault(value: Any): ColumnDefinition {
|
||||
this.defaultValue = Some(value)
|
||||
this
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 迁移操作类型。
|
||||
*/
|
||||
public enum MigrationOperationKind {
|
||||
| CreateTable
|
||||
| DropTable
|
||||
| AddColumn
|
||||
| DropColumn
|
||||
| AlterColumn
|
||||
| RenameColumn
|
||||
| CreateIndex
|
||||
| DropIndex
|
||||
| RawSql
|
||||
}
|
||||
|
||||
/**
|
||||
* 一条迁移操作(不同 kind 使用对应字段)。
|
||||
*/
|
||||
public class MigrationOperation {
|
||||
public let kind: MigrationOperationKind
|
||||
public var tableName: String = ""
|
||||
public var columnName: String = ""
|
||||
public var newColumnName: String = ""
|
||||
public var column: ?ColumnDefinition = None
|
||||
public var columnDefs: ArrayList<ColumnDefinition> = ArrayList()
|
||||
public var indexName: String = ""
|
||||
public var columnNames: ArrayList<String> = ArrayList()
|
||||
public var unique: Bool = false
|
||||
public var sql: String = ""
|
||||
|
||||
public init(kind: MigrationOperationKind) {
|
||||
this.kind = kind
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 建表辅助(createTable 的闭包参数)。
|
||||
*/
|
||||
public class TableBuilder {
|
||||
public let tableName: String
|
||||
internal let columns = ArrayList<ColumnDefinition>()
|
||||
|
||||
public init(tableName: String) {
|
||||
this.tableName = tableName
|
||||
}
|
||||
|
||||
/// 声明一列(返回列定义以支持链式标记)
|
||||
public func column(name: String, columnType: ColumnTypes): ColumnDefinition {
|
||||
let c = ColumnDefinition(name, columnType)
|
||||
columns.add(c)
|
||||
c
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 迁移构建器:声明式累积操作(对齐 EF Core MigrationBuilder)。
|
||||
*/
|
||||
public class MigrationBuilder {
|
||||
internal let operations = ArrayList<MigrationOperation>()
|
||||
|
||||
public init() {}
|
||||
|
||||
/// 当前累积的操作列表(供执行器/测试读取)
|
||||
public func getOperations(): ArrayList<MigrationOperation> {
|
||||
operations
|
||||
}
|
||||
|
||||
public func createTable(name: String, build: (TableBuilder) -> Unit): MigrationBuilder {
|
||||
let tb = TableBuilder(name)
|
||||
build(tb)
|
||||
let op = MigrationOperation(MigrationOperationKind.CreateTable)
|
||||
op.tableName = name
|
||||
op.columnDefs = tb.columns
|
||||
operations.add(op)
|
||||
this
|
||||
}
|
||||
|
||||
public func dropTable(name: String): MigrationBuilder {
|
||||
let op = MigrationOperation(MigrationOperationKind.DropTable)
|
||||
op.tableName = name
|
||||
operations.add(op)
|
||||
this
|
||||
}
|
||||
|
||||
public func addColumn(table: String, column: ColumnDefinition): MigrationBuilder {
|
||||
let op = MigrationOperation(MigrationOperationKind.AddColumn)
|
||||
op.tableName = table
|
||||
op.column = Some(column)
|
||||
operations.add(op)
|
||||
this
|
||||
}
|
||||
|
||||
public func dropColumn(table: String, columnName: String): MigrationBuilder {
|
||||
let op = MigrationOperation(MigrationOperationKind.DropColumn)
|
||||
op.tableName = table
|
||||
op.columnName = columnName
|
||||
operations.add(op)
|
||||
this
|
||||
}
|
||||
|
||||
public func alterColumn(table: String, column: ColumnDefinition): MigrationBuilder {
|
||||
let op = MigrationOperation(MigrationOperationKind.AlterColumn)
|
||||
op.tableName = table
|
||||
op.column = Some(column)
|
||||
operations.add(op)
|
||||
this
|
||||
}
|
||||
|
||||
public func renameColumn(table: String, oldName: String, newName: String): MigrationBuilder {
|
||||
let op = MigrationOperation(MigrationOperationKind.RenameColumn)
|
||||
op.tableName = table
|
||||
op.columnName = oldName
|
||||
op.newColumnName = newName
|
||||
operations.add(op)
|
||||
this
|
||||
}
|
||||
|
||||
public func createIndex(indexName: String, table: String, columns: ArrayList<String>): MigrationBuilder {
|
||||
createIndex(indexName, table, columns, false)
|
||||
}
|
||||
|
||||
public func createIndex(indexName: String, table: String, columns: ArrayList<String>,
|
||||
unique: Bool): MigrationBuilder {
|
||||
let op = MigrationOperation(MigrationOperationKind.CreateIndex)
|
||||
op.indexName = indexName
|
||||
op.tableName = table
|
||||
op.columnNames = columns
|
||||
op.unique = unique
|
||||
operations.add(op)
|
||||
this
|
||||
}
|
||||
|
||||
public func dropIndex(indexName: String, table: String): MigrationBuilder {
|
||||
let op = MigrationOperation(MigrationOperationKind.DropIndex)
|
||||
op.indexName = indexName
|
||||
op.tableName = table
|
||||
operations.add(op)
|
||||
this
|
||||
}
|
||||
|
||||
public func rawSql(sql: String): MigrationBuilder {
|
||||
let op = MigrationOperation(MigrationOperationKind.RawSql)
|
||||
op.sql = sql
|
||||
operations.add(op)
|
||||
this
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 迁移基类:migrationId 形如 "20250701000000_InitialCreate"(时间戳_名称),按字典序应用。
|
||||
* 两种使用方式:
|
||||
* 1. 子类 override up/down 手写声明式操作(MigrationBuilder);
|
||||
* 2. 由 MigrationGenerator 从模型生成时注入 preset 操作,基类 up/down 自动重放。
|
||||
*/
|
||||
public open class Migration {
|
||||
public let migrationId: String
|
||||
public let description: String
|
||||
|
||||
/// 由 MigrationGenerator 注入的 up 操作(按序执行)
|
||||
internal var presetOperations = ArrayList<MigrationOperation>()
|
||||
/// 由 MigrationGenerator 注入的 down 操作(按序执行,已按 up 逆序反转)
|
||||
internal var presetDownOperations = ArrayList<MigrationOperation>()
|
||||
|
||||
public init(migrationId: String, description: String) {
|
||||
this.migrationId = migrationId
|
||||
this.description = description
|
||||
}
|
||||
|
||||
public init(migrationId: String) {
|
||||
this.migrationId = migrationId
|
||||
this.description = ""
|
||||
}
|
||||
|
||||
public open func up(builder: MigrationBuilder): Unit {
|
||||
for (op in presetOperations) {
|
||||
builder.operations.add(op)
|
||||
}
|
||||
}
|
||||
|
||||
public open func down(builder: MigrationBuilder): Unit {
|
||||
for (op in presetDownOperations) {
|
||||
builder.operations.add(op)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* DDL SQL 工厂:MigrationOperation → SQL(类型映射与差异语法委托给 ISqlDialect)。
|
||||
*/
|
||||
public class DdlFactory {
|
||||
public init() {}
|
||||
|
||||
public func toSql(op: MigrationOperation, dialect: ISqlDialect): String {
|
||||
match (op.kind) {
|
||||
case MigrationOperationKind.CreateTable => createTableSql(op, dialect)
|
||||
case MigrationOperationKind.DropTable => "DROP TABLE IF EXISTS ${dialect.quoteName(op.tableName)}"
|
||||
case MigrationOperationKind.AddColumn =>
|
||||
"ALTER TABLE ${dialect.quoteName(op.tableName)} ADD COLUMN ${columnDefSql(op.column.getOrThrow(), dialect)}"
|
||||
case MigrationOperationKind.DropColumn =>
|
||||
"ALTER TABLE ${dialect.quoteName(op.tableName)} DROP COLUMN ${dialect.quoteName(op.columnName)}"
|
||||
case MigrationOperationKind.AlterColumn =>
|
||||
dialect.buildAlterColumn(op.tableName, columnDefSql(op.column.getOrThrow(), dialect))
|
||||
case MigrationOperationKind.RenameColumn =>
|
||||
"ALTER TABLE ${dialect.quoteName(op.tableName)} RENAME COLUMN ${dialect.quoteName(op.columnName)} TO ${dialect.quoteName(op.newColumnName)}"
|
||||
case MigrationOperationKind.CreateIndex => createIndexSql(op, dialect)
|
||||
case MigrationOperationKind.DropIndex => dialect.buildDropIndex(op.indexName, op.tableName)
|
||||
case MigrationOperationKind.RawSql => op.sql
|
||||
}
|
||||
}
|
||||
|
||||
private func createTableSql(op: MigrationOperation, dialect: ISqlDialect): String {
|
||||
let sb = StringBuilder()
|
||||
sb.append("CREATE TABLE IF NOT EXISTS ${dialect.quoteName(op.tableName)} (\n")
|
||||
var first = true
|
||||
for (c in op.columnDefs) {
|
||||
if (!first) {
|
||||
sb.append(",\n")
|
||||
}
|
||||
sb.append(" ${columnDefSql(c, dialect)}")
|
||||
first = false
|
||||
}
|
||||
sb.append("\n)")
|
||||
sb.toString()
|
||||
}
|
||||
|
||||
private func createIndexSql(op: MigrationOperation, dialect: ISqlDialect): String {
|
||||
let sb = StringBuilder()
|
||||
sb.append("CREATE ")
|
||||
if (op.unique) {
|
||||
sb.append("UNIQUE ")
|
||||
}
|
||||
sb.append("INDEX ${dialect.quoteName(op.indexName)} ON ${dialect.quoteName(op.tableName)} (")
|
||||
var first = true
|
||||
for (c in op.columnNames) {
|
||||
if (!first) {
|
||||
sb.append(", ")
|
||||
}
|
||||
sb.append(dialect.quoteName(c))
|
||||
first = false
|
||||
}
|
||||
sb.append(")")
|
||||
sb.toString()
|
||||
}
|
||||
|
||||
private func columnDefSql(col: ColumnDefinition, dialect: ISqlDialect): String {
|
||||
let sb = StringBuilder()
|
||||
sb.append(dialect.quoteName(col.name))
|
||||
sb.append(" ${dialect.columnTypeSql(col.columnType, col.autoIncrement, col.maxLength)}")
|
||||
if (!col.nullable || col.primaryKey) {
|
||||
sb.append(" NOT NULL")
|
||||
}
|
||||
if (col.primaryKey) {
|
||||
sb.append(" PRIMARY KEY")
|
||||
}
|
||||
if (col.unique) {
|
||||
sb.append(" UNIQUE")
|
||||
}
|
||||
if (let Some(dv) <- col.defaultValue) {
|
||||
sb.append(" DEFAULT ${defaultValueSql(dv)}")
|
||||
}
|
||||
sb.toString()
|
||||
}
|
||||
|
||||
private func defaultValueSql(v: Any): String {
|
||||
if (let s: String <- v) {
|
||||
return "'${escapeQuotes(s)}'"
|
||||
}
|
||||
if (let b: Bool <- v) {
|
||||
return if (b) { "TRUE" } else { "FALSE" }
|
||||
}
|
||||
if (let i64: Int64 <- v) {
|
||||
return "${i64}"
|
||||
}
|
||||
if (let i32: Int32 <- v) {
|
||||
return "${i32}"
|
||||
}
|
||||
if (let i16: Int16 <- v) {
|
||||
return "${i16}"
|
||||
}
|
||||
if (let i8: Int8 <- v) {
|
||||
return "${i8}"
|
||||
}
|
||||
if (let u64: UInt64 <- v) {
|
||||
return "${u64}"
|
||||
}
|
||||
if (let u32: UInt32 <- v) {
|
||||
return "${u32}"
|
||||
}
|
||||
if (let u16: UInt16 <- v) {
|
||||
return "${u16}"
|
||||
}
|
||||
if (let u8: UInt8 <- v) {
|
||||
return "${u8}"
|
||||
}
|
||||
if (let f64: Float64 <- v) {
|
||||
return "${f64}"
|
||||
}
|
||||
if (let f32: Float32 <- v) {
|
||||
return "${f32}"
|
||||
}
|
||||
if (let r: Rune <- v) {
|
||||
return "'${r}'"
|
||||
}
|
||||
if (let d: DateTime <- v) {
|
||||
return "'${d.toString()}'"
|
||||
}
|
||||
throw Exception("simorm: 不支持的默认值类型,请使用 String/Bool/数值/Rune/DateTime")
|
||||
}
|
||||
|
||||
private func escapeQuotes(s: String): String {
|
||||
var sb = StringBuilder()
|
||||
for (c in s.runes()) {
|
||||
if (c == Rune(0x27)) {
|
||||
sb.append("''")
|
||||
} else {
|
||||
sb.append(c)
|
||||
}
|
||||
}
|
||||
sb.toString()
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user