breaking,orm: add table attrs; add table/field comment support for mysql and pg (#24744)

This commit is contained in:
kbkpbot 2025-06-18 15:20:09 +08:00 committed by GitHub
parent d52bac1301
commit d4097212b3
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
12 changed files with 376 additions and 116 deletions

View file

@ -91,6 +91,8 @@ pub enum OrderType {
pub enum SQLDialect {
default
mysql
pg
sqlite
}
@ -153,6 +155,12 @@ pub:
right Primitive
}
pub struct Table {
pub mut:
name string
attrs []VAttribute
}
pub struct TableField {
pub mut:
name string
@ -163,7 +171,7 @@ pub mut:
is_arr bool
}
// table - Table name
// table - Table struct
// is_count - Either the data will be returned or an integer with the count
// has_where - Select all or use a where expr
// has_order - Order the results
@ -176,7 +184,7 @@ pub mut:
// types - Types to select
pub struct SelectConfig {
pub mut:
table string
table Table
is_count bool
has_where bool
has_order bool
@ -200,11 +208,11 @@ pub mut:
pub interface Connection {
mut:
select(config SelectConfig, data QueryData, where QueryData) ![][]Primitive
insert(table string, data QueryData) !
update(table string, data QueryData, where QueryData) !
delete(table string, where QueryData) !
create(table string, fields []TableField) !
drop(table string) !
insert(table Table, data QueryData) !
update(table Table, data QueryData, where QueryData) !
delete(table Table, where QueryData) !
create(table Table, fields []TableField) !
drop(table Table) !
last_id() int
}
@ -213,7 +221,7 @@ mut:
// num - Stmt uses nums at prepared statements (? or ?1)
// qm - Character for prepared statement (qm for question mark, as in sqlite)
// start_pos - When num is true, it's the start position of the counter
pub fn orm_stmt_gen(sql_dialect SQLDialect, table string, q string, kind StmtKind, num bool, qm string,
pub fn orm_stmt_gen(sql_dialect SQLDialect, table Table, q string, kind StmtKind, num bool, qm string,
start_pos int, data QueryData, where QueryData) (string, QueryData) {
mut str := ''
mut c := start_pos
@ -257,7 +265,7 @@ pub fn orm_stmt_gen(sql_dialect SQLDialect, table string, q string, kind StmtKin
c++
}
str += 'INSERT INTO ${q}${table}${q} '
str += 'INSERT INTO ${q}${table.name}${q} '
are_values_empty := values.len == 0
@ -272,7 +280,7 @@ pub fn orm_stmt_gen(sql_dialect SQLDialect, table string, q string, kind StmtKin
}
}
.update {
str += 'UPDATE ${q}${table}${q} SET '
str += 'UPDATE ${q}${table.name}${q} SET '
for i, field in data.fields {
str += '${q}${field}${q} = '
if data.data.len > i {
@ -310,7 +318,7 @@ pub fn orm_stmt_gen(sql_dialect SQLDialect, table string, q string, kind StmtKin
str += ' WHERE '
}
.delete {
str += 'DELETE FROM ${q}${table}${q} WHERE '
str += 'DELETE FROM ${q}${table.name}${q} WHERE '
}
}
// where
@ -319,7 +327,7 @@ pub fn orm_stmt_gen(sql_dialect SQLDialect, table string, q string, kind StmtKin
}
str += ';'
$if trace_orm_stmt ? {
eprintln('> orm_stmt sql_dialect: ${sql_dialect} | table: ${table} | kind: ${kind} | query: ${str}')
eprintln('> orm_stmt sql_dialect: ${sql_dialect} | table: ${table.name} | kind: ${kind} | query: ${str}')
}
$if trace_orm ? {
eprintln('> orm: ${str}')
@ -352,7 +360,7 @@ pub fn orm_select_gen(cfg SelectConfig, q string, num bool, qm string, start_pos
}
}
str += ' FROM ${q}${cfg.table}${q}'
str += ' FROM ${q}${cfg.table.name}${q}'
mut c := start_pos
@ -441,19 +449,19 @@ fn gen_where_clause(where QueryData, q string, qm string, num bool, mut c &int)
}
// Generates an sql table stmt, from universal parameter
// table - Table name
// table - Table struct
// q - see orm_stmt_gen
// defaults - enables default values in stmt
// def_unique_len - sets default unique length for texts
// fields - See TableField
// sql_from_v - Function which maps type indices to sql type names
// alternative - Needed for msdb
pub fn orm_table_gen(table string, q string, defaults bool, def_unique_len int, fields []TableField, sql_from_v fn (int) !string,
pub fn orm_table_gen(sql_dialect SQLDialect, table Table, q string, defaults bool, def_unique_len int, fields []TableField, sql_from_v fn (int) !string,
alternative bool) !string {
mut str := 'CREATE TABLE IF NOT EXISTS ${q}${table}${q} ('
mut str := 'CREATE TABLE IF NOT EXISTS ${q}${table.name}${q} ('
if alternative {
str = 'IF NOT EXISTS (SELECT * FROM sysobjects WHERE name=${q}${table}${q} and xtype=${q}U${q}) CREATE TABLE ${q}${table}${q} ('
str = 'IF NOT EXISTS (SELECT * FROM sysobjects WHERE name=${q}${table.name}${q} and xtype=${q}U${q}) CREATE TABLE ${q}${table.name}${q} ('
}
mut fs := []string{}
@ -461,6 +469,19 @@ pub fn orm_table_gen(table string, q string, defaults bool, def_unique_len int,
mut unique := map[string][]string{}
mut primary := ''
mut primary_typ := 0
mut table_comment := ''
mut field_comments := map[string]string{}
for attr in table.attrs {
match attr.name {
'comment' {
if attr.arg != '' && attr.kind == .string {
table_comment = attr.arg.replace('"', '\\"')
}
}
else {}
}
}
for field in fields {
if field.is_arr {
@ -473,6 +494,7 @@ pub fn orm_table_gen(table string, q string, defaults bool, def_unique_len int,
mut unique_len := 0
mut references_table := ''
mut references_field := ''
mut field_comment := ''
mut field_name := sql_field_name(field)
mut col_typ := sql_from_v(sql_field_type(field)) or {
field_name = '${field_name}_id'
@ -540,6 +562,12 @@ pub fn orm_table_gen(table string, q string, defaults bool, def_unique_len int,
}
}
}
'comment' {
if attr.arg != '' && attr.kind == .string {
field_comment = attr.arg.replace("'", "\\'")
field_comments[field_name] = field_comment
}
}
else {}
}
}
@ -548,12 +576,15 @@ pub fn orm_table_gen(table string, q string, defaults bool, def_unique_len int,
}
mut stmt := ''
if col_typ == '' {
return error('Unknown type (${field.typ}) for field ${field.name} in struct ${table}')
return error('Unknown type (${field.typ}) for field ${field.name} in struct ${table.name}')
}
stmt = '${q}${field_name}${q} ${col_typ}'
if defaults && default_val != '' {
stmt += ' DEFAULT ${default_val}'
}
if sql_dialect == .mysql && field_comment != '' {
stmt += " COMMENT '${field_comment}'"
}
if !nullable {
stmt += ' NOT NULL'
}
@ -591,9 +622,22 @@ pub fn orm_table_gen(table string, q string, defaults bool, def_unique_len int,
fs << unique_fields
str += fs.join(', ')
str += ');'
str += ')'
if sql_dialect == .mysql && table_comment != '' {
str += " COMMENT = '${table_comment}'"
}
str += ';'
if sql_dialect == .pg {
if table_comment != '' {
str += "\nCOMMENT ON TABLE \"${table.name}\" IS '${table_comment}';"
}
for f, c in field_comments {
str += "\nCOMMENT ON COLUMN \"${table.name}\".\"${f}\" IS '${c}';"
}
}
$if trace_orm_create ? {
eprintln('> orm_create table: ${table} | query: ${str}')
eprintln('> orm_create table: ${table.name} | query: ${str}')
}
$if trace_orm ? {
eprintln('> orm: ${str}')

View file

@ -3,7 +3,10 @@
import orm
fn test_orm_stmt_gen_update() {
query_and, _ := orm.orm_stmt_gen(.default, 'Test', "'", .update, true, '?', 0, orm.QueryData{
table := orm.Table{
name: 'Test'
}
query_and, _ := orm.orm_stmt_gen(.default, table, "'", .update, true, '?', 0, orm.QueryData{
fields: ['test', 'a']
data: []
types: []
@ -17,7 +20,7 @@ fn test_orm_stmt_gen_update() {
})
assert query_and == "UPDATE 'Test' SET 'test' = ?0, 'a' = ?1 WHERE 'id' >= ?2 AND 'name' = ?3;"
query_or, _ := orm.orm_stmt_gen(.default, 'Test', "'", .update, true, '?', 0, orm.QueryData{
query_or, _ := orm.orm_stmt_gen(.default, table, "'", .update, true, '?', 0, orm.QueryData{
fields: ['test', 'a']
data: []
types: []
@ -33,7 +36,10 @@ fn test_orm_stmt_gen_update() {
}
fn test_orm_stmt_gen_insert() {
query, _ := orm.orm_stmt_gen(.default, 'Test', "'", .insert, true, '?', 0, orm.QueryData{
table := orm.Table{
name: 'Test'
}
query, _ := orm.orm_stmt_gen(.default, table, "'", .insert, true, '?', 0, orm.QueryData{
fields: ['test', 'a']
data: []
types: []
@ -43,7 +49,10 @@ fn test_orm_stmt_gen_insert() {
}
fn test_orm_stmt_gen_delete() {
query_and, _ := orm.orm_stmt_gen(.default, 'Test', "'", .delete, true, '?', 0, orm.QueryData{
table := orm.Table{
name: 'Test'
}
query_and, _ := orm.orm_stmt_gen(.default, table, "'", .delete, true, '?', 0, orm.QueryData{
fields: ['test', 'a']
data: []
types: []
@ -57,7 +66,7 @@ fn test_orm_stmt_gen_delete() {
})
assert query_and == "DELETE FROM 'Test' WHERE 'id' >= ?0 AND 'name' = ?1;"
query_or, _ := orm.orm_stmt_gen(.default, 'Test', "'", .delete, true, '?', 0, orm.QueryData{
query_or, _ := orm.orm_stmt_gen(.default, table, "'", .delete, true, '?', 0, orm.QueryData{
fields: ['test', 'a']
data: []
types: []
@ -78,7 +87,9 @@ fn get_select_fields() []string {
fn test_orm_select_gen() {
query := orm.orm_select_gen(orm.SelectConfig{
table: 'test_table'
table: orm.Table{
name: 'test_table'
}
fields: get_select_fields()
}, "'", true, '?', 0, orm.QueryData{})
@ -87,7 +98,9 @@ fn test_orm_select_gen() {
fn test_orm_select_gen_with_limit() {
query := orm.orm_select_gen(orm.SelectConfig{
table: 'test_table'
table: orm.Table{
name: 'test_table'
}
fields: get_select_fields()
has_limit: true
}, "'", true, '?', 0, orm.QueryData{})
@ -97,7 +110,9 @@ fn test_orm_select_gen_with_limit() {
fn test_orm_select_gen_with_where() {
query := orm.orm_select_gen(orm.SelectConfig{
table: 'test_table'
table: orm.Table{
name: 'test_table'
}
fields: get_select_fields()
has_where: true
}, "'", true, '?', 0, orm.QueryData{
@ -111,7 +126,9 @@ fn test_orm_select_gen_with_where() {
fn test_orm_select_gen_with_order() {
query := orm.orm_select_gen(orm.SelectConfig{
table: 'test_table'
table: orm.Table{
name: 'test_table'
}
fields: get_select_fields()
has_order: true
order_type: .desc
@ -122,7 +139,9 @@ fn test_orm_select_gen_with_order() {
fn test_orm_select_gen_with_offset() {
query := orm.orm_select_gen(orm.SelectConfig{
table: 'test_table'
table: orm.Table{
name: 'test_table'
}
fields: get_select_fields()
has_offset: true
}, "'", true, '?', 0, orm.QueryData{})
@ -132,7 +151,9 @@ fn test_orm_select_gen_with_offset() {
fn test_orm_select_gen_with_all() {
query := orm.orm_select_gen(orm.SelectConfig{
table: 'test_table'
table: orm.Table{
name: 'test_table'
}
fields: get_select_fields()
has_limit: true
has_order: true
@ -149,7 +170,10 @@ fn test_orm_select_gen_with_all() {
}
fn test_orm_table_gen() {
query := orm.orm_table_gen('test_table', "'", true, 0, [
table := orm.Table{
name: 'test_table'
}
query := orm.orm_table_gen(.default, table, "'", true, 0, [
orm.TableField{
name: 'id'
typ: typeof[int]().idx
@ -181,7 +205,7 @@ fn test_orm_table_gen() {
], sql_type_from_v, false) or { panic(err) }
assert query == "CREATE TABLE IF NOT EXISTS 'test_table' ('id' SERIAL DEFAULT 10, 'test' TEXT, 'abc' INT64 DEFAULT 6754, PRIMARY KEY('id'));"
alt_query := orm.orm_table_gen('test_table', "'", true, 0, [
alt_query := orm.orm_table_gen(.default, table, "'", true, 0, [
orm.TableField{
name: 'id'
typ: typeof[int]().idx
@ -213,7 +237,7 @@ fn test_orm_table_gen() {
], sql_type_from_v, true) or { panic(err) }
assert alt_query == "IF NOT EXISTS (SELECT * FROM sysobjects WHERE name='test_table' and xtype='U') CREATE TABLE 'test_table' ('id' SERIAL DEFAULT 10, 'test' TEXT, 'abc' INT64 DEFAULT 6754, PRIMARY KEY('id'));"
unique_query := orm.orm_table_gen('test_table', "'", true, 0, [
unique_query := orm.orm_table_gen(.default, table, "'", true, 0, [
orm.TableField{
name: 'id'
typ: typeof[int]().idx
@ -248,7 +272,7 @@ fn test_orm_table_gen() {
], sql_type_from_v, false) or { panic(err) }
assert unique_query == "CREATE TABLE IF NOT EXISTS 'test_table' ('id' SERIAL DEFAULT 10, 'test' TEXT NOT NULL, 'abc' INT64 DEFAULT 6754 NOT NULL, PRIMARY KEY('id'), UNIQUE('test'));"
mult_unique_query := orm.orm_table_gen('test_table', "'", true, 0, [
mult_unique_query := orm.orm_table_gen(.default, table, "'", true, 0, [
orm.TableField{
name: 'id'
typ: typeof[int]().idx

View file

@ -25,7 +25,7 @@ pub fn new_query[T](conn Connection) &QueryBuilder[T] {
valid_sql_field_names: meta.map(sql_field_name(it))
conn: conn
config: SelectConfig{
table: table_name_from_struct[T]()
table: table_from_struct[T]()
}
data: QueryData{}
where: QueryData{}
@ -35,9 +35,9 @@ pub fn new_query[T](conn Connection) &QueryBuilder[T] {
// reset reset a query object, but keep the connection and table name
pub fn (qb_ &QueryBuilder[T]) reset() &QueryBuilder[T] {
mut qb := unsafe { qb_ }
old_table_name := qb.config.table
old_table := qb.config.table
qb.config = SelectConfig{
table: old_table_name
table: old_table
}
qb.data = QueryData{}
qb.where = QueryData{}
@ -366,15 +366,20 @@ pub fn (qb_ &QueryBuilder[T]) set(assign string, values ...Primitive) !&QueryBui
return qb
}
// table_name_from_struct get table name from struct
fn table_name_from_struct[T]() string {
// table_from_struct get table from struct
fn table_from_struct[T]() Table {
mut table_name := T.name
mut attrs := []VAttribute{}
$for a in T.attributes {
$if a.name == 'table' && a.has_arg {
table_name = a.arg
}
attrs << a
}
return Table{
name: table_name
attrs: attrs
}
return table_name
}
// struct_meta return a struct's fields info

View file

@ -25,14 +25,14 @@ fn (mut db Database) select(config orm.SelectConfig, data orm.QueryData, where o
}
// insert is used internally by V's ORM for processing `INSERT` queries
fn (mut db Database) insert(table string, data orm.QueryData) ! {
fn (mut db Database) insert(table orm.Table, data orm.QueryData) ! {
query, _ := orm.orm_stmt_gen(.sqlite, table, '', .insert, false, '?', 1, data, orm.QueryData{})
db.query(query)!
}
// update is used internally by V's ORM for processing `UPDATE` queries
fn (mut db Database) update(table string, data orm.QueryData, where orm.QueryData) ! {
fn (mut db Database) update(table orm.Table, data orm.QueryData, where orm.QueryData) ! {
mut query, _ := orm.orm_stmt_gen(.sqlite, table, '', .update, true, ':', 1, data,
where)
@ -40,7 +40,7 @@ fn (mut db Database) update(table string, data orm.QueryData, where orm.QueryDat
}
// delete is used internally by V's ORM for processing `DELETE ` queries
fn (mut db Database) delete(table string, where orm.QueryData) ! {
fn (mut db Database) delete(table orm.Table, where orm.QueryData) ! {
query, converted := orm.orm_stmt_gen(.sqlite, table, '', .delete, true, ':', 1, orm.QueryData{},
where)
@ -66,16 +66,15 @@ fn sqlite_type_from_v(typ int) !string {
}
// create is used internally by V's ORM for processing table creation queries (DDL)
fn (mut db Database) create(table string, fields []orm.TableField) ! {
mut query := orm.orm_table_gen(table, '', true, 0, fields, sqlite_type_from_v, false) or {
return err
}
fn (mut db Database) create(table orm.Table, fields []orm.TableField) ! {
mut query := orm.orm_table_gen(.sqlite, table, '', true, 0, fields, sqlite_type_from_v,
false) or { return err }
db.query(query)!
}
// drop is used internally by V's ORM for processing table destroying queries (DDL)
fn (mut db Database) drop(table string) ! {
query := 'DROP TABLE ${table};'
fn (mut db Database) drop(table orm.Table) ! {
query := 'DROP TABLE ${table.name};'
$if trace_orm ? {
eprintln('> vsql drop: ${query}')
}

View file

@ -31,7 +31,7 @@ fn (db MockDB) select(config orm.SelectConfig, data orm.QueryData, where orm.Que
return db.db.select(config, data, where)
}
fn (db MockDB) insert(table string, data orm.QueryData) ! {
fn (db MockDB) insert(table orm.Table, data orm.QueryData) ! {
mut st := db.st
last, qdata := orm.orm_stmt_gen(.sqlite, table, '`', .insert, false, '?', 1, data,
orm.QueryData{})
@ -41,7 +41,7 @@ fn (db MockDB) insert(table string, data orm.QueryData) ! {
return db.db.insert(table, data)
}
fn (db MockDB) update(table string, data orm.QueryData, where orm.QueryData) ! {
fn (db MockDB) update(table orm.Table, data orm.QueryData, where orm.QueryData) ! {
mut st := db.st
st.last, _ = orm.orm_stmt_gen(.sqlite, table, '`', .update, false, '?', 1, data, where)
st.data = data.data
@ -49,7 +49,7 @@ fn (db MockDB) update(table string, data orm.QueryData, where orm.QueryData) ! {
return db.db.update(table, data, where)
}
fn (db MockDB) delete(table string, where orm.QueryData) ! {
fn (db MockDB) delete(table orm.Table, where orm.QueryData) ! {
mut st := db.st
st.last, _ = orm.orm_stmt_gen(.sqlite, table, '`', .delete, false, '?', 1, orm.QueryData{},
where)
@ -82,13 +82,14 @@ fn mock_type_from_v(typ int) !string {
}
}
fn (db MockDB) create(table string, fields []orm.TableField) ! {
fn (db MockDB) create(table orm.Table, fields []orm.TableField) ! {
mut st := db.st
st.last = orm.orm_table_gen(table, '`', true, 0, fields, mock_type_from_v, false)!
st.last = orm.orm_table_gen(.sqlite, table, '`', true, 0, fields, mock_type_from_v,
false)!
return db.db.create(table, fields)
}
fn (db MockDB) drop(table string) ! {
fn (db MockDB) drop(table orm.Table) ! {
return db.db.drop(table)
}