///|
const PKG_NAME = "@morm"
///|
const ENGINE_PKG = "@morm/engine"
///|
/// 变换阶段:从 EntitySpec 构建 @morm.Table,不接触解析细节
fn transform_entity_to_table(
spec : EntitySpec,
source : String,
enum_specs : Array[EnumSpec],
) -> @morm.Table {
let columns : Array[@morm.Column] = []
let foreign_keys : Array[@morm.ForeignKey] = []
for f in spec.fields {
if field_is_one_to_many(f.attrs, source) ||
field_is_transient(f.attrs, source) {
continue
}
let fk_col_opt = field_many_to_one_fk_column(f.label, f.attrs, source)
let db_type = match fk_col_opt {
Some(_) => @morm.ColumnType::Int
None => map_type_to_db_by_name(f.field_type, f.attrs, enum_specs)
}
let (is_primary_key, is_auto_increment, _, primary_key_strategy) = collect_field_flags(
f.attrs,
source,
)
let is_nullable = f.nullable && !is_primary_key
let col_name = match fk_col_opt {
Some(n) => n
None => f.label
}
let mut col : @morm.Column = {
name: col_name,
column_type: db_type,
size: 0,
nullable: is_nullable,
default_value: None,
auto_increment: is_auto_increment,
primary_key: is_primary_key,
unique: false,
comment: "",
charset: None,
collation: None,
engine_options: Map([]),
}
let parsed_attrs = collect_orm_attributes_from_source(f.attrs, source)
for attr in parsed_attrs {
if attr.name is ("#morm.primary_key" | "#morm.id") &&
attr.value.get("strategy") is Some(strategy) {
col = @morm.add_column_engine_option(col, "pk.strategy", strategy)
}
if attr.name == "#morm.default" &&
attr.value.get("value") is Some(default_expr) {
if normalize_default_expr(default_expr) is Some(normalized) {
col = { ..col, default_value: Some(normalized), }
}
}
if attr.name is ("#morm.json" | "#morm.jsonb") &&
attr.value.get("storage") is Some(storage) {
col = @morm.add_column_engine_option(col, "json.storage", storage)
}
if attr.name
is ("#morm.date" | "#morm.time" | "#morm.datetime" | "#morm.timestamp") &&
attr.value.get("format") is Some(format) {
col = @morm.add_column_engine_option(col, "temporal.format", format)
}
}
if primary_key_strategy is Some(strategy) {
if !col.engine_options.contains("pk.strategy") {
col = @morm.add_column_engine_option(col, "pk.strategy", strategy)
}
}
columns.push(col)
if parse_foreign_key_for_field(
spec.name,
f.label,
f.field_type,
f.attrs,
source,
)
is Some(fk) {
foreign_keys.push(fk)
}
}
let mut table = @morm.new_table(
spec.table_name,
columns=FixedArray::from_array(columns),
)
for fk in foreign_keys {
table = @morm.add_foreign_key(table, fk)
}
apply_entity_table_attributes(spec, table)
}
///|
fn entity_table_attr_value(spec : EntitySpec, key : String) -> String? {
let mut result : String? = None
for attr in spec.attrs {
if attr.name is ("#morm.model" | "#morm.entity" | "#morm.table") &&
attr.value.get(key) is Some(value) {
result = Some(value)
}
}
result
}
///|
fn attr_bool(value : String) -> Bool {
let text = value.to_lower()
text is ("true" | "1" | "yes" | "on")
}
///|
fn attr_int(value : String) -> Int? {
Some(@string.parse_int(value)) catch {
_ => None
}
}
///|
fn entity_table_attr_int(spec : EntitySpec, key : String) -> Int? {
match entity_table_attr_value(spec, key) {
Some(raw) => attr_int(raw)
None => None
}
}
///|
fn entity_table_attr_enabled(spec : EntitySpec, key : String) -> Bool {
match entity_table_attr_value(spec, key) {
Some(raw) => attr_bool(raw)
None => false
}
}
///|
fn orm_attr_bool(attr : ORMAttribute, key : String) -> Bool {
match attr.value.get(key) {
Some(raw) => attr_bool(raw)
None => false
}
}
///|
fn orm_attr_string(attr : ORMAttribute, key : String) -> String? {
match attr.value.get(key) {
Some(value) => Some(value)
None => None
}
}
///|
fn orm_attr_name_or_value(attr : ORMAttribute) -> String? {
match orm_attr_string(attr, "name") {
Some(value) => Some(value)
None => orm_attr_string(attr, "value")
}
}
///|
fn unquote_string_literal(text : String) -> String {
if text.length() >= 2 && text.has_prefix("\"") && text.has_suffix("\"") {
text[1:text.length() - 1].to_owned()
} else {
text
}
}
///|
fn single_quote_sql_string(text : String) -> String {
"'" + text.replace(old="'", new="''") + "'"
}
///|
fn normalize_default_expr(raw_expr : String) -> String? {
let expr = raw_expr.trim().to_owned()
if expr == "" {
return None
}
// Simplified parsing without lexer dependency
let compact_lower = expr.replace_all(old=" ", new="").to_lower()
if compact_lower is ("autoincrement()" | "autoincrement") {
return None
}
if compact_lower == "true" {
return Some("true")
}
if compact_lower == "false" {
return Some("false")
}
if attr_int(expr) is Some(_) {
return Some(expr)
}
if expr.has_prefix("[") || expr.has_prefix("{") {
return Some(single_quote_sql_string(expr))
}
if expr.has_prefix("\"") && expr.has_suffix("\"") {
return Some(single_quote_sql_string(unquote_string_literal(expr)))
}
Some(expr)
}
///|
fn apply_entity_table_attributes(
spec : EntitySpec,
table : @morm.Table,
) -> @morm.Table {
let mut table = table
for attr in spec.attrs {
if attr.name == "#morm.postgres.schema" {
if orm_attr_name_or_value(attr) is Some(schema_name) {
table = @morm.table_schema(table, schema_name)
}
if orm_attr_string(attr, "authorization") is Some(role) {
table = @morm.pg_schema_authorization(table, role)
}
}
if attr.name == "#morm.postgres.table" {
if orm_attr_name_or_value(attr) is Some(table_name) {
table = { ..table, name: table_name, }
}
if orm_attr_string(attr, "owner") is Some(role) {
table = @morm.pg_table_owner(table, role)
}
if orm_attr_bool(attr, "unlogged") {
table = @morm.pg_table_unlogged(table)
}
if orm_attr_string(attr, "with") is Some(with_clause) {
table = @morm.pg_table_with(table, with_clause)
}
if orm_attr_string(attr, "on_commit") is Some(on_commit) {
table = @morm.pg_table_on_commit(table, on_commit)
}
if orm_attr_string(attr, "tablespace") is Some(tablespace) {
table = @morm.table_tablespace(table, tablespace)
}
}
if attr.name == "#morm.postgres.grant" {
let with_grant_option = orm_attr_bool(attr, "with_grant_option")
if orm_attr_string(attr, "role") is Some(role) {
if orm_attr_string(attr, "schema") is Some(privileges) {
table = @morm.pg_schema_grant(
table, role, privileges, with_grant_option,
)
}
if orm_attr_string(attr, "table") is Some(privileges) {
table = @morm.pg_table_grant(
table, role, privileges, with_grant_option,
)
}
if orm_attr_string(attr, "sequence") is Some(privileges) {
table = @morm.pg_sequence_grant(
table, role, privileges, with_grant_option,
)
}
if orm_attr_string(attr, "schema_sequence") is Some(privileges) {
table = @morm.pg_schema_sequence_grant(
table, role, privileges, with_grant_option,
)
}
if orm_attr_string(attr, "default_table") is Some(privileges) {
table = @morm.pg_default_table_grant(
table,
role,
privileges,
with_grant_option,
orm_attr_string(attr, "for_role"),
)
}
if orm_attr_string(attr, "default_sequence") is Some(privileges) {
table = @morm.pg_default_sequence_grant(
table,
role,
privileges,
with_grant_option,
orm_attr_string(attr, "for_role"),
)
}
}
}
}
if entity_table_attr_value(spec, "name") is Some(name) {
table = { ..table, name, }
}
if entity_table_attr_value(spec, "schema") is Some(schema) {
table = @morm.table_schema(table, schema)
}
if entity_table_attr_value(spec, "catalog") is Some(catalog) {
table = @morm.table_catalog(table, catalog)
}
if entity_table_attr_value(spec, "temporary") is Some(temporary) {
table = @morm.table_temporary(table, attr_bool(temporary))
}
if entity_table_attr_value(spec, "if_not_exists") is Some(if_not_exists) {
table = @morm.table_if_not_exists(table, attr_bool(if_not_exists))
}
if entity_table_attr_value(spec, "engine") is Some(engine) {
table = @morm.table_engine(table, engine)
}
if entity_table_attr_value(spec, "charset") is Some(charset) {
table = { ..table, charset: Some(charset), }
}
if entity_table_attr_value(spec, "collation") is Some(collation) {
table = { ..table, collation: Some(collation), }
}
if entity_table_attr_value(spec, "tablespace") is Some(tablespace) {
table = @morm.table_tablespace(table, tablespace)
}
if entity_table_attr_value(spec, "comment") is Some(comment) {
table = @morm.table_comment(table, comment)
}
if entity_table_attr_int(spec, "auto_increment") is Some(value) {
table = { ..table, auto_increment: Some(value), }
}
if entity_table_attr_value(spec, "create_suffix") is Some(create_suffix) {
table = @morm.table_create_suffix(table, create_suffix)
}
if entity_table_attr_value(spec, "mysql_row_format") is Some(row_format) {
table = @morm.mysql_table_row_format(table, row_format)
}
if entity_table_attr_int(spec, "mysql_key_block_size") is Some(value) {
table = @morm.mysql_table_key_block_size(table, value)
}
if entity_table_attr_value(spec, "mysql_compression") is Some(compression) {
table = @morm.mysql_table_compression(table, compression)
}
if entity_table_attr_enabled(spec, "pg_unlogged") {
table = @morm.pg_table_unlogged(table)
}
if entity_table_attr_value(spec, "pg_with") is Some(with_clause) {
table = @morm.pg_table_with(table, with_clause)
}
if entity_table_attr_value(spec, "pg_on_commit") is Some(on_commit) {
table = @morm.pg_table_on_commit(table, on_commit)
}
if entity_table_attr_value(spec, "oracle_organization") is Some(organization) {
table = @morm.oracle_table_organization(table, organization)
}
if entity_table_attr_value(spec, "oracle_on_commit") is Some(on_commit) {
table = @morm.oracle_table_on_commit(table, on_commit)
}
if entity_table_attr_enabled(spec, "sqlserver_memory_optimized") {
table = @morm.sqlserver_table_memory_optimized(table)
}
if entity_table_attr_value(spec, "sqlserver_durability") is Some(durability) {
table = @morm.sqlserver_table_durability(table, durability)
}
if entity_table_attr_value(spec, "sqlserver_filegroup") is Some(filegroup) {
table = @morm.sqlserver_table_filegroup(table, filegroup)
}
if entity_table_attr_value(spec, "sqlserver_textimage_on") is Some(filegroup) {
table = @morm.sqlserver_table_textimage_on(table, filegroup)
}
if entity_table_attr_enabled(spec, "sqlite_without_rowid") {
table = @morm.sqlite_table_without_rowid(table)
}
if entity_table_attr_enabled(spec, "sqlite_strict") {
table = @morm.sqlite_table_strict(table)
}
table
}
///|
fn escape_mbt_string(s : String) -> String {
s
.replace(old="\\", new="\\\\")
.replace(old="\"", new="\\\"")
.replace(old="\n", new="\\n")
.replace(old="\r", new="\\r")
.replace(old="\t", new="\\t")
}
///|
fn mbt_string(s : String) -> String {
"\"" + escape_mbt_string(s) + "\""
}
///|
fn mbt_opt_string(v : String?) -> String {
match v {
Some(text) => "Some(" + mbt_string(text) + ")"
None => "None"
}
}
///|
fn mbt_opt_int(v : Int?) -> String {
match v {
Some(value) => "Some(" + value.to_string() + ")"
None => "None"
}
}
///|
fn render_column_type(ty : @morm.ColumnType) -> String {
match ty {
TinyInt => "TinyInt"
SmallInt => "SmallInt"
MediumInt => "MediumInt"
Int => "Int"
BigInt => "BigInt"
Float => "Float"
Double => "Double"
Decimal(p, s) => "Decimal(\{p}, \{s})"
Char(len) => "Char(\{len})"
VarChar(len) => "VarChar(\{len})"
Text => "Text"
MediumText => "MediumText"
LongText => "LongText"
Binary(len) => "Binary(\{len})"
VarBinary(len) => "VarBinary(\{len})"
Blob => "Blob"
MediumBlob => "MediumBlob"
LongBlob => "LongBlob"
Date => "Date"
Time => "Time"
DateTime => "DateTime"
Timestamp => "Timestamp"
Year => "Year"
Json => "Json"
JsonB => "JsonB"
Enum(name, values) => {
let rendered_values = values.map(mbt_string).join(", ")
"Enum(\{mbt_string(name)}, [\{rendered_values}])"
}
Boolean => "Boolean"
}
}
///|
fn render_index_type(ty : @morm.IndexType) -> String {
match ty {
Primary => "\{PKG_NAME}.Primary"
Unique => "\{PKG_NAME}.Unique"
Index => "\{PKG_NAME}.Index"
FullText => "\{PKG_NAME}.FullText"
Spatial => "\{PKG_NAME}.Spatial"
}
}
///|
fn render_string_array(values : Array[String]) -> String {
values.map(mbt_string).join(", ")
}
///|
fn render_engine_options(options : Map[String, Array[String]]) -> String {
let entries = options.to_array()
entries.sort_by_key(entry => entry.0)
entries
.map(entry => {
let (key, values) = entry
" \{mbt_string(key)}: [\{render_string_array(values)}]"
})
.join(",\n")
}
///|
fn render_column(col : @morm.Column) -> String {
let engine_options = render_engine_options(col.engine_options)
(
$| {
$| name: \{mbt_string(col.name)},
$| column_type: \{render_column_type(col.column_type)},
$| size: \{col.size},
$| nullable: \{col.nullable},
$| auto_increment: \{col.auto_increment},
$| primary_key: \{col.primary_key},
$| unique: \{col.unique},
$| comment: \{mbt_string(col.comment)},
$| charset: \{mbt_opt_string(col.charset)},
$| collation: \{mbt_opt_string(col.collation)},
$| default_value: \{mbt_opt_string(col.default_value)},
$| engine_options: \{if engine_options.is_empty() { "Map([])" } else { "{\n" + engine_options + "\n }" }},
$| }
)
}
///|
fn render_index(idx : @morm.Index) -> String {
let cols = idx.columns.map(c => mbt_string(c)).join(", ")
(
$| {
$| name: \{mbt_string(idx.name)},
$| index_type: \{render_index_type(idx.index_type)},
$| columns: [\{cols}],
$| comment: \{mbt_string(idx.comment)},
$| }
)
}
///|
fn render_foreign_key(fk : @morm.ForeignKey) -> String {
(
$| {
$| name: \{mbt_string(fk.name)},
$| column: \{mbt_string(fk.column)},
$| referenced_table: \{mbt_string(fk.referenced_table)},
$| referenced_column: \{mbt_string(fk.referenced_column)},
$| on_delete: \{mbt_string(fk.on_delete)},
$| on_update: \{mbt_string(fk.on_update)},
$| }
)
}
///|
fn render_table_literal(table : @morm.Table) -> String {
let columns = table.columns.map(render_column).join(",\n")
let indexes = table.indexes.map(render_index).join(",\n")
let foreign_keys = table.foreign_keys.map(render_foreign_key).join(",\n")
let engine_options = render_engine_options(table.engine_options)
(
$| {
$| name: \{mbt_string(table.name)},
$| schema: \{mbt_opt_string(table.schema)},
$| catalog: \{mbt_opt_string(table.catalog)},
$| columns: [
$|\{columns}
$| ],
$| indexes: [
$|\{indexes}
$| ],
$| foreign_keys: [
$|\{foreign_keys}
$| ],
$| temporary: \{table.temporary},
$| if_not_exists: \{table.if_not_exists},
$| engine: \{mbt_opt_string(table.engine)},
$| charset: \{mbt_opt_string(table.charset)},
$| collation: \{mbt_opt_string(table.collation)},
$| tablespace: \{mbt_opt_string(table.tablespace)},
$| comment: \{mbt_string(table.comment)},
$| auto_increment: \{mbt_opt_int(table.auto_increment)},
$| engine_options: \{if engine_options.is_empty() { "Map([])" } else { "{\n" + engine_options + "\n }" }},
$| }
)
}
///|
/// 代码生成阶段:从表结构生成实现代码
fn is_param_decodable_type(
type_name : String,
enum_specs : Array[EnumSpec],
) -> Bool {
if find_enum_spec(enum_specs, type_name) is Some(_) {
return true
}
match type_name {
"Unit"
| "Bool"
| "Byte"
| "Int"
| "Int16"
| "UInt16"
| "UInt"
| "UInt64"
| "Int64"
| "BigInt"
| "Float"
| "Double"
| "String"
| "Bytes"
| "Json"
| "PlainDate"
| "PlainTime"
| "PlainDateTime"
| "ZonedDateTime" => true
_ => false
}
}
///|
fn entity_supports_row_decoder(
spec : EntitySpec,
enum_specs : Array[EnumSpec],
) -> Bool {
for field in spec.fields {
let attrs = collect_orm_attributes(field.attrs)
for attr in attrs {
if attr.name is ("#morm.one_to_many" | "#morm.many_to_one") {
return false
}
}
if !is_param_decodable_type(field.field_type, enum_specs) {
return false
}
}
true
}
///|
fn render_from_param_expr(
type_name : String,
value_expr : String,
nullable : Bool,
) -> String {
if nullable {
return "match " +
value_expr +
" { Null => None; _ => Some((try! \{ENGINE_PKG}.from_param(" +
value_expr +
") : " +
type_name +
")) }"
}
"(try! \{ENGINE_PKG}.from_param(\{value_expr}) : \{type_name})"
}
///|
fn field_json_storage(field : FieldSpec) -> String? {
for attr in collect_orm_attributes(field.attrs) {
if attr.name is ("#morm.json" | "#morm.jsonb") &&
attr.value.get("storage") is Some(storage) {
return Some(storage)
}
}
None
}
///|
fn render_field__from_expr(field : FieldSpec, value_expr : String) -> String {
if field.field_type == "Json" {
if field_json_storage(field) is Some(storage) {
if field.nullable {
return "match \{value_expr} { Null => None; _ => Some(try! \{ENGINE_PKG}.decode_json_column(\{value_expr}, \{mbt_string(storage)})) }"
}
return "(try! \{ENGINE_PKG}.decode_json_column(\{value_expr}, \{mbt_string(storage)}))"
}
}
render_from_param_expr(field.field_type, value_expr, field.nullable)
}
///|
fn render_entity__from(spec : EntitySpec) -> String {
let field_lines : Array[String] = []
let ctor_lines : Array[String] = []
for field in spec.fields {
let param_name = field.label + "_param"
field_lines.push(
" let " +
param_name +
" = match row.get(\"" +
field.label +
"\") { Some(value) => value; None => panic() }",
)
let field_ty = if field.nullable {
field.field_type + "?"
} else {
field.field_type
}
field_lines.push(
" let " +
field.label +
" : " +
field_ty +
" = " +
render_field__from_expr(field, param_name),
)
ctor_lines.push(" " + field.label + ",")
}
let fields_code = field_lines.join("\n")
let ctor_code = ctor_lines.join("\n")
(
$| pub fn \{spec.name}::_from(row : Map[String, \{ENGINE_PKG}.Param]) -> \{spec.name} {
$|\{fields_code}
$| return {
$|\{ctor_code}
$| }
$| }
)
}
///|
fn generate_entity_impl(
spec : EntitySpec,
table : @morm.Table,
enum_specs : Array[EnumSpec],
) -> String {
let name = spec.name
let table_literal = render_table_literal(table)
let row_decoder = if entity_supports_row_decoder(spec, enum_specs) {
"\n" +
render_entity__from(spec) +
"\n" +
(
$| pub impl \{ENGINE_PKG}.FromParam for \{name} with fn from_param(value) {
$| match value {
$| Object(obj) => {
$| let row : Map[String, \{ENGINE_PKG}.Param] = Map([])
$| for key in obj.keys() {
$| if obj.get(key) is Some(value) {
$| row.set(key, value)
$| }
$| }
$| \{name}::_from(row)
$| }
$| _ => raise ParamDecodeError("\{name}::from_param: expected Json object row")
$| }
$| }
$|
$| pub extend \{name} with \{ENGINE_PKG}.FromParam::{from_param}
)
} else {
""
}
(
$| pub impl \{PKG_NAME}.Entity for \{name} with fn table(_self) -> \{PKG_NAME}.Table { \{name}::table() }
$| pub fn \{name}::table() -> \{PKG_NAME}.Table {
$|\{table_literal}
$| }
$|\{row_decoder}
)
}
///|
fn find_entity_spec(specs : Array[EntitySpec], name : String) -> EntitySpec? {
for spec in specs {
if spec.name == name {
return Some(spec)
}
}
None
}
///|
fn find_entity_spec_by_table(
specs : Array[EntitySpec],
table_name : String,
) -> EntitySpec? {
for spec in specs {
if spec.table_name == table_name {
return Some(spec)
}
}
None
}
///|
fn field_has_orm_attr_name(field : FieldSpec, attr_name : String) -> Bool {
for attr in collect_orm_attributes(field.attrs) {
if attr.name == attr_name {
return true
}
}
false
}
///|
fn field_is_primary_key_for_codegen(field : FieldSpec) -> Bool {
field.label == "id" ||
field_has_orm_attr_name(field, "#morm.id") ||
field_has_orm_attr_name(field, "#morm.primary_key")
}
///|
fn field_is_relation_or_virtual_for_codegen(field : FieldSpec) -> Bool {
field_has_orm_attr_name(field, "#morm.one_to_many") ||
field_has_orm_attr_name(field, "#morm.many_to_one") ||
field_has_orm_attr_name(field, "#morm.transient")
}
///|
fn entity_primary_key_field_for_codegen(spec : EntitySpec) -> FieldSpec? {
for field in spec.fields {
if field_is_primary_key_for_codegen(field) {
return Some(field)
}
}
None
}
///|
fn render_partial_update_expr(
table_expr : String,
entity_spec : EntitySpec,
param_name : String,
) -> String? {
let set_fields : Array[String] = []
let where_fields : Array[String] = []
for field in entity_spec.fields {
if field_is_relation_or_virtual_for_codegen(field) {
continue
}
if field_is_primary_key_for_codegen(field) {
where_fields.push(field.label)
} else {
set_fields.push(field.label)
}
}
if set_fields.length() == 0 || where_fields.length() == 0 {
return None
}
let parts : Array[String] = []
parts.push("\{PKG_NAME}.update(\{table_expr})")
for label in set_fields {
parts.push(".set(\"\{label}\", \{param_name}.\{label})")
}
for label in where_fields {
parts.push(".where_eq(\"\{label}\", \{param_name}.\{label})")
}
Some(parts.join(""))
}
///|
fn option_inner_type(type_name : String) -> String? {
if type_name.has_suffix("?") {
let n = type_name.length()
return Some(type_name[0:n - 1].to_owned())
}
None
}
///|
fn is_pageable_type_name(type_name : String) -> Bool {
type_name is ("Pageable" | "@morm/engine.Pageable")
}
///|
fn find_pageable_param_name(params : Array[(String, String)]) -> String? {
for p in params {
let (param_name, param_type) = p
if is_pageable_type_name(param_type) || param_name == "pageable" {
return Some(param_name)
}
}
None
}
///|
fn render_collection_map_key_expr(
key_type : String,
value_type : String,
row_expr : String,
value_expr : String,
entity_specs : Array[EntitySpec],
) -> String {
match find_entity_spec(entity_specs, value_type) {
Some(spec) =>
match entity_primary_key_field_for_codegen(spec) {
Some(field) => "\{value_expr}.\{field.label}"
None =>
(
$| let key_param = match \{row_expr}.get("id") {
$| Some(value) => value
$| None => panic()
$| }
$| \{render_from_param_expr(key_type, "key_param", false)}
)
}
None =>
(
$| let key_param = match \{row_expr}.get("id") {
$| Some(value) => value
$| None => panic()
$| }
$| \{render_from_param_expr(key_type, "key_param", false)}
)
}
}
///|
fn render_collection_decode_code(
ret_type : String,
return_kind : String,
item_type : String?,
map_key_type : String?,
map_value_type : String?,
query_expr : String,
row_decode_expr : String,
entity_specs : Array[EntitySpec],
) -> String {
match return_kind {
"FixedArray" =>
(
$| \{query_expr}.rows.map(row => \{row_decode_expr})
$| }
$|
)
"Array" =>
(
$| let out : \{ret_type} = []
$| for row in \{query_expr}.rows {
$| out.push(\{row_decode_expr})
$| }
$| out
$| }
$|
)
"Set" =>
(
$| let out : \{ret_type} = @set.Set([])
$| for row in \{query_expr}.rows {
$| out.add(\{row_decode_expr})
$| }
$| out
$| }
$|
)
"List" =>
match item_type {
Some(_) =>
(
$| let out : Array[\{item_type.unwrap()}] = []
$| for row in \{query_expr}.rows {
$| out.push(\{row_decode_expr})
$| }
$| @list.List::from_array(out)
$| }
$|
)
None =>
(
$| panic()
$| }
$|
)
}
"Map" =>
match (map_key_type, map_value_type) {
(Some(key_type), Some(value_type)) => {
let key_expr = render_collection_map_key_expr(
key_type, value_type, "row", "value", entity_specs,
)
(
$| let out : \{ret_type} = Map([])
$| for row in \{query_expr}.rows {
$| let value : \{value_type} = \{row_decode_expr}
$| let key : \{key_type} = \{key_expr}
$| out.set(key, value)
$| }
$| out
$| }
$|
)
}
_ =>
(
$| panic()
$| }
$|
)
}
_ =>
(
$| panic()
$| }
$|
)
}
}
///|
fn render_query_result_decode_expr(
type_name : String,
value_expr : String,
entity_specs : Array[EntitySpec],
enum_specs : Array[EnumSpec],
) -> String {
match find_entity_spec(entity_specs, type_name) {
Some(spec) =>
if entity_supports_row_decoder(spec, enum_specs) {
"\{type_name}::_from(\{value_expr})"
} else {
"(try! \{ENGINE_PKG}.from_param(\{value_expr}) : \{type_name})"
}
None => "(try! \{ENGINE_PKG}.from_param(\{value_expr}) : \{type_name})"
}
}
///|
fn render_enum_codec_impl(enum_spec : EnumSpec) -> String {
(
$| pub impl \{ENGINE_PKG}.ToParam for \{enum_spec.name} with fn to_param(self) -> \{ENGINE_PKG}.Param {
$| try! \{ENGINE_PKG}.enum_to_param(self)
$| }
$|
$| pub impl \{ENGINE_PKG}.FromParam for \{enum_spec.name} with fn from_param(value) {
$| try! \{ENGINE_PKG}.enum_from_param(value)
$| }
$|
$| pub extend \{enum_spec.name} with \{ENGINE_PKG}.ToParam::{to_param}
$| pub extend \{enum_spec.name} with \{ENGINE_PKG}.FromParam::{from_param}
)
}
///|
fn field_has_attr(field : FieldSpec, attr_name : String) -> Bool {
let orm_attrs = collect_orm_attributes(field.attrs)
let mut hit = false
orm_attrs.each(a => if a.name == attr_name { hit = true })
hit
}
///|
fn field_auto_create_time(field : FieldSpec) -> Bool {
field_has_attr(field, "#morm.auto_create_time") || field.label == "created_at"
}
///|
fn field_auto_update_time(field : FieldSpec) -> Bool {
field_has_attr(field, "#morm.auto_update_time") || field.label == "updated_at"
}
///|
fn auto_time_value_expr(field : FieldSpec) -> String? {
let base = match field.field_type {
"PlainDateTime" | "DateTime" =>
Some("\{PKG_NAME}.current_plain_date_time_utc()")
"ZonedDateTime" | "Timestamp" => Some("\{PKG_NAME}.current_timestamp_utc()")
_ => None
}
match base {
Some(expr) =>
if field.nullable {
Some("Some(" + expr + ")")
} else {
Some(expr)
}
None => None
}
}
///|
fn path_dirname(path : String) -> String {
let slash = path.rev_find("/")
let backslash = path.rev_find("\\")
let cut = match (slash, backslash) {
(Some(a), Some(b)) => Some(if a > b { a } else { b })
(Some(a), None) => Some(a)
(None, Some(b)) => Some(b)
(None, None) => None
}
match cut {
Some(0) => "/"
Some(idx) => path[0:idx].to_owned()
None => "."
}
}
///|
fn path_join(dir : String, name : String) -> String {
if dir is ("." | "") {
name
} else if dir == "/" {
"/" + name
} else {
dir + "/" + name
}
}
///|
fn is_source_mbt_file(name : String) -> Bool {
name.has_suffix(".mbt") &&
!name.has_suffix(".g.mbt") &&
!name.has_suffix("_test.mbt") &&
!name.has_suffix("_wbtest.mbt")
}
///|
fn collect_package_entity_specs(
input_file : String,
) -> Array[EntitySpec] raise @fs.IOError {
let dir = path_dirname(input_file)
let merged : Array[EntitySpec] = []
let seen : Map[String, Bool] = Map([])
for entry in @fs.read_dir(dir) {
if !is_source_mbt_file(entry) {
continue
}
let path = path_join(dir, entry)
guard @fs.is_file(path) else { continue }
let (file_ast, _) = @parser.parse_file(path)
for spec in parse_entities_from_ast(file_ast) {
if seen.contains(spec.name) {
continue
}
seen.set(spec.name, true)
merged.push(spec)
}
}
merged
}
///|
fn collect_package_enum_specs(
input_file : String,
) -> Array[EnumSpec] raise @fs.IOError {
let dir = path_dirname(input_file)
let merged : Array[EnumSpec] = []
let seen : Map[String, Bool] = Map([])
for entry in @fs.read_dir(dir) {
if !is_source_mbt_file(entry) {
continue
}
let path = path_join(dir, entry)
guard @fs.is_file(path) else { continue }
let source = @fs.read_file_to_string(path)
for spec in parse_enum_specs_from_source(source) {
if seen.contains(spec.name) {
continue
}
seen.set(spec.name, true)
merged.push(spec)
}
}
merged
}
///|
/// 代码生成阶段:根据 MapperSpec 生成 struct、mapper 函数与方法实现
fn generate_mapper_code(
spec : MapperSpec,
entity_specs : Array[EntitySpec],
enum_specs : Array[EnumSpec],
) -> String {
let trait_name = spec.trait_name
let impl_name = trait_name + "Impl"
let entity_name = spec.entity_name
let entity_spec = match entity_name {
Some(name) => find_entity_spec(entity_specs, name)
None => None
}
let table_expr = match spec.table_name {
Some(t) => "\"\{t}\""
None =>
match entity_name {
Some(n) => "\{n}::table().name"
None => "\"\""
}
}
let struct_def =
$| pub struct \{impl_name} {
$| engine : &\{ENGINE_PKG}.Engine
$| }
$|
let mapper_fn =
$| pub fn \{impl_name}::\{impl_name}(engine : &\{ENGINE_PKG}.Engine) -> \{impl_name} {
$| { engine, }
$| }
$|
let mut has_sync_methods = false
for m in spec.methods {
if !m.is_async {
has_sync_methods = true
}
}
let mapper_helpers = if has_sync_methods {
(
$| pub fn[Q : \{ENGINE_PKG}.QueryBuilder] \{impl_name}::exec_query_sync(
$| self : \{impl_name},
$| q : Q,
$| ) -> \{ENGINE_PKG}.QueryResult {
$| let mut res : \{ENGINE_PKG}.QueryResult = {
$| ok: false,
$| rows_affected: 0,
$| rows: [],
$| error: Some("async execution did not return"),
$| }
$| @async.run_async_main(() => {
$| res = self.engine.exec(q)
$| })
$| res
$| }
$|
$| pub fn \{impl_name}::exec_raw_sync(
$| self : \{impl_name},
$| sql : String,
$| params : FixedArray[\{ENGINE_PKG}.Param],
$| ) -> \{ENGINE_PKG}.QueryResult {
$| let mut res : \{ENGINE_PKG}.QueryResult = {
$| ok: false,
$| rows_affected: 0,
$| rows: [],
$| error: Some("async execution did not return"),
$| }
$| @async.run_async_main(() => {
$| res = self.engine.exec_raw(sql, params)
$| })
$| res
$| }
$|
)
} else {
""
}
let mut impls = ""
for m in spec.methods {
let param_sig_parts : Array[String] = []
let sql_args_parts : Array[String] = []
let pageable_param_name = find_pageable_param_name(m.params)
for p in m.params {
let (pname, ptype) = p
param_sig_parts.push("\{pname} : \{ptype}")
if pageable_param_name != Some(pname) {
sql_args_parts.push("\{ENGINE_PKG}.to_param(\{pname})")
}
}
let param_sig = if param_sig_parts.length() == 0 {
"self"
} else {
"self, " + param_sig_parts.join(", ")
}
let query_args = "[" + sql_args_parts.join(", ") + "]"
// 返回类型字符串
let ret_type = m.return_type
let return_kind = m.return_kind
let array_item_type = match m.return_item_type {
Some(inner) => inner
None => "Param"
}
let map_key_type = m.return_map_key_type
let map_value_type = m.return_map_value_type
let option_value_type = match option_inner_type(ret_type) {
Some(inner) => inner
None => ret_type
}
let collection_item_type = match map_value_type {
Some(value_type) => value_type
None => array_item_type
}
let collection_decode_expr = render_query_result_decode_expr(
collection_item_type, "row", entity_specs, enum_specs,
)
let option_decode_expr = render_query_result_decode_expr(
option_value_type, "result", entity_specs, enum_specs,
)
let value_decode_expr = render_query_result_decode_expr(
ret_type, "result", entity_specs, enum_specs,
)
let method_header =
$| pub impl \{trait_name} for \{impl_name} with fn \{m.name}(
$| \{param_sig}
$| ) -> \{ret_type} {
let body = if m.name == "create" && m.params.length() == 1 {
let entity_param_name = m.params[0].0
let create_entity_spec = match entity_spec {
Some(spec) => Some(spec)
None =>
match find_entity_spec(entity_specs, m.params[0].1) {
Some(spec) => Some(spec)
None =>
match spec.table_name {
Some(table_name) =>
find_entity_spec_by_table(entity_specs, table_name)
None => None
}
}
}
let auto_assigns : Array[String] = []
match create_entity_spec {
Some(create_spec) =>
for field in create_spec.fields {
if field_auto_create_time(field) || field_auto_update_time(field) {
match auto_time_value_expr(field) {
Some(expr) => auto_assigns.push(field.label + ": " + expr)
None => ()
}
}
}
None => ()
}
let auto_stamp_code = if auto_assigns.length() > 0 {
let assigns = auto_assigns.join(", ")
(
$| let \{entity_param_name} = {
$| ..\{entity_param_name},
$| \{assigns},
$| }
)
} else {
""
}
if ret_type == "Bool" {
(
$|\{auto_stamp_code}
$| let q = \{PKG_NAME}.insert_into(\{table_expr}).from(\{entity_param_name})
$| self.engine.exec(q).ok
$| }
$|
)
} else if m.returns_option {
(
$|\{auto_stamp_code}
$| let q = \{PKG_NAME}.insert_into(\{table_expr}).from(\{entity_param_name})
$| let res = self.engine.exec(q)
$| if res.rows is [result, ..] {
$| return Some(\{option_decode_expr})
$| }
$| None
$| }
$|
)
} else {
let entity_ty = m.params[0].1
(
$|\{auto_stamp_code}
$| let q = \{PKG_NAME}.insert_into(\{table_expr}).from(\{entity_param_name})
$| let res = self.engine.exec(q)
$| if res.rows is [result, ..] {
$| return \{render_query_result_decode_expr(entity_ty, "result", entity_specs, enum_specs)}
$| }
$| panic()
$| }
$|
)
}
} else if m.name == "save" && m.params.length() == 1 {
let entity_param_name = m.params[0].0
let save_entity_spec = match entity_spec {
Some(spec) => Some(spec)
None =>
match find_entity_spec(entity_specs, m.params[0].1) {
Some(spec) => Some(spec)
None =>
match spec.table_name {
Some(table_name) =>
find_entity_spec_by_table(entity_specs, table_name)
None => None
}
}
}
let auto_assigns : Array[String] = []
match save_entity_spec {
Some(save_spec) =>
for field in save_spec.fields {
if field_auto_create_time(field) || field_auto_update_time(field) {
match auto_time_value_expr(field) {
Some(expr) => auto_assigns.push(field.label + ": " + expr)
None => ()
}
}
}
None => ()
}
let auto_stamp_code = if auto_assigns.length() > 0 {
let assigns = auto_assigns.join(", ")
(
$| let \{entity_param_name} = {
$| ..\{entity_param_name},
$| \{assigns},
$| }
)
} else {
""
}
if ret_type == "Bool" {
(
$|\{auto_stamp_code}
$| let q = \{PKG_NAME}.upsert_into(\{table_expr}).from(\{entity_param_name})
$| self.engine.exec(q).ok
$| }
$|
)
} else if m.returns_option {
(
$|\{auto_stamp_code}
$| let q = \{PKG_NAME}.upsert_into(\{table_expr}).from(\{entity_param_name})
$| let res = self.engine.exec(q)
$| if res.rows is [result, ..] {
$| return Some(\{option_decode_expr})
$| }
$| None
$| }
$|
)
} else {
let entity_ty = m.params[0].1
(
$|\{auto_stamp_code}
$| let q = \{PKG_NAME}.upsert_into(\{table_expr}).from(\{entity_param_name})
$| let res = self.engine.exec(q)
$| if res.rows is [result, ..] {
$| return \{render_query_result_decode_expr(entity_ty, "result", entity_specs, enum_specs)}
$| }
$| panic()
$| }
$|
)
}
} else if m.name == "update" && m.params.length() == 1 {
let entity_param_name = m.params[0].0
let update_entity_spec = match entity_spec {
Some(spec) => Some(spec)
None =>
match find_entity_spec(entity_specs, m.params[0].1) {
Some(spec) => Some(spec)
None =>
match spec.table_name {
Some(table_name) =>
find_entity_spec_by_table(entity_specs, table_name)
None => None
}
}
}
let auto_assigns : Array[String] = []
match update_entity_spec {
Some(update_spec) =>
for field in update_spec.fields {
if field_auto_update_time(field) {
match auto_time_value_expr(field) {
Some(expr) => auto_assigns.push(field.label + ": " + expr)
None => ()
}
}
}
None => ()
}
let auto_stamp_code = if auto_assigns.length() > 0 {
let assigns = auto_assigns.join(", ")
(
$| let \{entity_param_name} = {
$| ..\{entity_param_name},
$| \{assigns},
$| }
)
} else {
""
}
let partial_update_code = match update_entity_spec {
Some(update_spec) =>
render_partial_update_expr(table_expr, update_spec, entity_param_name)
None => None
}
let update_query_expr = match partial_update_code {
Some(code) => code
None => "\{PKG_NAME}.update(\{table_expr}).from(\{entity_param_name})"
}
if ret_type == "Bool" {
(
$|\{auto_stamp_code}
$| let q = \{update_query_expr}
$| self.engine.exec(q).ok
$| }
$|
)
} else if m.returns_option {
(
$|\{auto_stamp_code}
$| let q = \{update_query_expr}
$| let res = self.engine.exec(q)
$| if res.rows is [result, ..] {
$| return Some(\{option_decode_expr})
$| }
$| None
$| }
$|
)
} else {
(
$|\{auto_stamp_code}
$| let q = \{update_query_expr}
$| let res = self.engine.exec(q)
$| if res.rows is [result, ..] {
$| return \{value_decode_expr}
$| }
$| panic()
$| }
$|
)
}
} else if m.name == "delete" && m.params.length() == 1 {
let entity_param_name = m.params[0].0
(
$| let q = \{PKG_NAME}.delete_from(\{table_expr}).from(\{entity_param_name})
$| self.engine.exec(q).ok
$| }
$|
)
} else {
let builder_code_with_fetch = match m.fetch_join {
Some(join_sql) => {
let join_escaped = join_sql.replace(old="\"", new="\\\"")
match m.builder_code {
Some(code) => Some(code + ".join(\"" + join_escaped + "\")")
None => None
}
}
None => m.builder_code
}
match m.builder_code {
Some(_) =>
match builder_code_with_fetch {
Some(code) =>
if ret_type == "Bool" &&
(
code.has_prefix("\{PKG_NAME}.update(") ||
code.has_prefix("\{PKG_NAME}.delete_from(") ||
code.has_prefix("\{PKG_NAME}.insert_into(") ||
code.has_prefix("\{PKG_NAME}.upsert_into(")
) {
(
$| let q = \{code}
$| self.engine.exec(q).ok
$| }
$|
)
} else if return_kind == "Page" {
match pageable_param_name {
Some(pageable_name) =>
(
$| \{PKG_NAME}.paginate(self.engine, \{code}, \{pageable_name})
$| }
$|
)
None =>
(
$| panic()
$| }
$|
)
}
} else if return_kind
is ("FixedArray" | "Array" | "Set" | "List" | "Map") {
render_collection_decode_code(
ret_type,
return_kind,
m.return_item_type,
map_key_type,
map_value_type,
"self.engine.exec(\{code})",
collection_decode_expr,
entity_specs,
)
} else if m.returns_option {
(
$| let q = \{code}
$| if self.engine.exec(q).rows
$| is [result, ..] {
$| return Some(\{option_decode_expr})
$| }
$| None
$| }
$|
)
} else {
(
$| let q = \{code}
$| if self.engine.exec(q).rows
$| is [result, ..] {
$| return \{value_decode_expr}
$| }
$| panic()
$| }
$|
)
}
None => panic()
}
None => {
let sql = m.sql.unwrap()
if ret_type == "Bool" {
(
$| self.engine.exec_raw("\{sql}", \{query_args}).ok
$| }
$|
)
} else if return_kind == "Page" {
match pageable_param_name {
Some(pageable_name) =>
(
$| \{PKG_NAME}.paginate_raw(self.engine, "\{sql}", \{query_args}, \{pageable_name})
$| }
$|
)
None =>
(
$| panic()
$| }
$|
)
}
} else if return_kind
is ("FixedArray" | "Array" | "Set" | "List" | "Map") {
render_collection_decode_code(
ret_type,
return_kind,
m.return_item_type,
map_key_type,
map_value_type,
"self.engine.exec_raw(\"\{sql}\", \{query_args})",
collection_decode_expr,
entity_specs,
)
} else if m.returns_option {
(
$| if self.engine.exec_raw("\{sql}", \{query_args}).rows
$| is [result, ..] {
$| return Some(\{option_decode_expr})
$| }
$| None
$| }
$|
)
} else {
(
$| if self.engine.exec_raw("\{sql}", \{query_args}).rows
$| is [result, ..] {
$| return \{value_decode_expr}
$| }
$| panic()
$| }
$|
)
}
}
}
}
impls = impls + method_header + body
}
let method_names = spec.methods.map(mapper_method => mapper_method.name)
let method_extensions = if method_names.length() == 0 {
""
} else {
"\npub extend \{impl_name} with \{trait_name}::{\{method_names.join(", ")}}\n"
}
struct_def + mapper_fn + mapper_helpers + impls + method_extensions
}
///|
fn main {
let argv = @env.args()
let cli_args = if argv.length() > 0 { argv[1:] } else { [] }
let cmd = @argparse.Command(
"mormgen",
about="MoonBit ORM CLI tool for generating database schema from MoonBit files",
options=[
OptionArg("output-file", short='o', about="Output file", required=true),
],
positionals=[
PositionArg(
"input_file",
about="Input MoonBit source file",
num_args=@argparse.ValueRange::single(),
),
],
)
let matches = cmd.parse(argv=cli_args, env=Map([])) catch {
e => {
println("\{e}")
return
}
}
let input_file = match matches.values.get("input_file") {
Some([file, ..]) => file
_ => {
println(cmd.render_help())
return
}
}
let output_file = match matches.values.get("output-file") {
Some([file, ..]) => file
_ => {
println(cmd.render_help())
return
}
}
try {
let (ast, _) = @parser.parse_file(input_file)
let source_content = @fs.read_file_to_string(input_file)
let specs = parse_entities_from_ast(ast)
let all_entity_specs = collect_package_entity_specs(input_file)
let package_enum_specs = collect_package_enum_specs(input_file)
let source_enum_specs = parse_enum_specs_from_source(source_content)
let mapper_specs = parse_mappers_from_ast(
ast, source_content, all_entity_specs,
)
//
let codes : Array[String] = []
source_enum_specs.each(enum_spec => {
codes.push(render_enum_codec_impl(enum_spec))
})
specs.each(spec => {
let table = transform_entity_to_table(
spec, source_content, package_enum_specs,
)
let code = generate_entity_impl(spec, table, package_enum_specs)
codes.push(code)
})
mapper_specs.each(ms => {
let mcode = generate_mapper_code(ms, all_entity_specs, package_enum_specs)
codes.push(mcode)
})
let code = codes.join("\n\n")
@fs.write_string_to_file(output_file, code, encoding="utf8")
let entity_names = specs.map(spec => spec.name)
let mapper_names = mapper_specs.map(ms => ms.trait_name)
if entity_names.length() > 0 {
println("Generated \{output_file} for entities: \{Repr(entity_names)}")
}
if mapper_names.length() > 0 {
println("Generated \{output_file} for mappers: \{Repr(mapper_names)}")
}
} catch {
IOError(e) => println("IOError: \{e}")
}
}