From d4097212b39c6341180d95443c2f3260a5d56b95 Mon Sep 17 00:00:00 2001 From: kbkpbot Date: Wed, 18 Jun 2025 15:20:09 +0800 Subject: [PATCH] breaking,orm: add table attrs; add table/field comment support for mysql and pg (#24744) --- vlib/db/mysql/mysql_orm_test.v | 64 ++++++++++++++++++-- vlib/db/mysql/orm.c.v | 19 +++--- vlib/db/pg/orm.v | 23 ++++--- vlib/db/pg/pg_orm_test.v | 78 ++++++++++++++++++++++-- vlib/db/sqlite/orm.v | 17 +++--- vlib/db/sqlite/sqlite_orm_test.v | 9 ++- vlib/orm/orm.v | 84 ++++++++++++++++++++------ vlib/orm/orm_fn_test.v | 54 ++++++++++++----- vlib/orm/orm_func.v | 17 ++++-- vlib/orm/orm_mut_connection_test.v | 17 +++--- vlib/orm/orm_null_test.v | 13 ++-- vlib/v/gen/c/orm.v | 97 ++++++++++++++++++++++++------ 12 files changed, 376 insertions(+), 116 deletions(-) diff --git a/vlib/db/mysql/mysql_orm_test.v b/vlib/db/mysql/mysql_orm_test.v index 39edafe825..cb999b4b02 100644 --- a/vlib/db/mysql/mysql_orm_test.v +++ b/vlib/db/mysql/mysql_orm_test.v @@ -37,6 +37,13 @@ struct TestDefaultAttribute { created_at string @[default: 'CURRENT_TIMESTAMP'; sql_type: 'TIMESTAMP'] } +@[comment: 'This is a table comment'] +struct TestCommentAttribute { + id string @[primary; sql: serial] + name string @[comment: 'real user name'] + created_at string @[default: 'CURRENT_TIMESTAMP'; sql_type: 'TIMESTAMP'] +} + fn test_mysql_orm() { $if !network ? { eprintln('> Skipping test ${@FN}, since `-d network` is not passed.') @@ -47,13 +54,17 @@ fn test_mysql_orm() { host: '127.0.0.1' port: 3306 username: 'root' - password: '' + password: '12345678' dbname: 'mysql' )! defer { db.close() } - db.create('Test', [ + table := orm.Table{ + name: 'Test' + } + db.drop(table) or {} + db.create(table, [ orm.TableField{ name: 'id' typ: typeof[int]().idx @@ -80,13 +91,13 @@ fn test_mysql_orm() { }, ]) or { panic(err) } - db.insert('Test', orm.QueryData{ + db.insert(table, orm.QueryData{ fields: ['name', 'age'] data: [orm.string_to_primitive('Louis'), orm.int_to_primitive(101)] }) or { panic(err) } res := db.select(orm.SelectConfig{ - table: 'Test' + table: table has_where: true fields: ['id', 'name', 'age'] types: [typeof[int]().idx, typeof[string]().idx, typeof[i64]().idx] @@ -272,4 +283,49 @@ fn test_mysql_orm() { 'COLUMN_DEFAULT': 'CURRENT_TIMESTAMP' }] assert information_schema_column_default_sql == result_defaults.maps() + + /** test comment attribute + */ + sql db { + create table TestCommentAttribute + }! + + mut column_comments := db.query(" + SELECT COLUMN_COMMENT + FROM INFORMATION_SCHEMA.COLUMNS + WHERE TABLE_NAME = 'TestCommentAttribute' + ORDER BY ORDINAL_POSITION + ") or { + println(err) + panic(err) + } + + mut table_comment := db.query(" + SELECT TABLE_COMMENT + FROM INFORMATION_SCHEMA.TABLES + WHERE TABLE_NAME = 'TestCommentAttribute' + ") or { + println(err) + panic(err) + } + + sql db { + drop table TestCommentAttribute + }! + + information_schema_column_comment_sql := [{ + 'COLUMN_COMMENT': '' + }, { + 'COLUMN_COMMENT': 'real user name' + }, { + 'COLUMN_COMMENT': '' + }] + assert information_schema_column_comment_sql == column_comments.maps() + + information_schema_table_comment_sql := [ + { + 'TABLE_COMMENT': 'This is a table comment' + }, + ] + assert information_schema_table_comment_sql == table_comment.maps() } diff --git a/vlib/db/mysql/orm.c.v b/vlib/db/mysql/orm.c.v index 113159c82b..3b779eb21b 100644 --- a/vlib/db/mysql/orm.c.v +++ b/vlib/db/mysql/orm.c.v @@ -122,8 +122,8 @@ pub fn (db DB) select(config orm.SelectConfig, data orm.QueryData, where orm.Que } // insert is used internally by V's ORM for processing `INSERT ` queries -pub fn (db DB) insert(table string, data orm.QueryData) ! { - mut converted_primitive_array := db.convert_query_data_to_primitives(table, data)! +pub fn (db DB) insert(table orm.Table, data orm.QueryData) ! { + mut converted_primitive_array := db.convert_query_data_to_primitives(table.name, data)! converted_primitive_data := orm.QueryData{ fields: data.fields @@ -139,13 +139,13 @@ pub fn (db DB) insert(table string, data orm.QueryData) ! { } // update is used internally by V's ORM for processing `UPDATE ` queries -pub fn (db DB) update(table string, data orm.QueryData, where orm.QueryData) ! { +pub fn (db DB) update(table orm.Table, data orm.QueryData, where orm.QueryData) ! { query, _ := orm.orm_stmt_gen(.default, table, '`', .update, false, '?', 1, data, where) mysql_stmt_worker(db, query, data, where)! } // delete is used internally by V's ORM for processing `DELETE ` queries -pub fn (db DB) delete(table string, where orm.QueryData) ! { +pub fn (db DB) delete(table orm.Table, where orm.QueryData) ! { query, _ := orm.orm_stmt_gen(.default, table, '`', .delete, false, '?', 1, orm.QueryData{}, where) mysql_stmt_worker(db, query, orm.QueryData{}, where)! @@ -160,16 +160,15 @@ pub fn (db DB) last_id() int { } // create is used internally by V's ORM for processing table creation queries (DDL) -pub fn (db DB) create(table string, fields []orm.TableField) ! { - query := orm.orm_table_gen(table, '`', true, 0, fields, mysql_type_from_v, false) or { - return err - } +pub fn (db DB) create(table orm.Table, fields []orm.TableField) ! { + query := orm.orm_table_gen(.mysql, table, '`', true, 0, fields, mysql_type_from_v, + false) or { return err } mysql_stmt_worker(db, query, orm.QueryData{}, orm.QueryData{})! } // drop is used internally by V's ORM for processing table destroying queries (DDL) -pub fn (db DB) drop(table string) ! { - query := 'DROP TABLE `${table}`;' +pub fn (db DB) drop(table orm.Table) ! { + query := 'DROP TABLE `${table.name}`;' mysql_stmt_worker(db, query, orm.QueryData{}, orm.QueryData{})! } diff --git a/vlib/db/pg/orm.v b/vlib/db/pg/orm.v index b80e9f82ff..4d6a29bbdd 100644 --- a/vlib/db/pg/orm.v +++ b/vlib/db/pg/orm.v @@ -31,20 +31,20 @@ pub fn (db DB) select(config orm.SelectConfig, data orm.QueryData, where orm.Que // sql stmt // insert is used internally by V's ORM for processing `INSERT ` queries -pub fn (db DB) insert(table string, data orm.QueryData) ! { +pub fn (db DB) insert(table orm.Table, data orm.QueryData) ! { query, converted_data := orm.orm_stmt_gen(.default, table, '"', .insert, true, '$', 1, data, orm.QueryData{}) pg_stmt_worker(db, query, converted_data, orm.QueryData{})! } // update is used internally by V's ORM for processing `UPDATE ` queries -pub fn (db DB) update(table string, data orm.QueryData, where orm.QueryData) ! { +pub fn (db DB) update(table orm.Table, data orm.QueryData, where orm.QueryData) ! { query, _ := orm.orm_stmt_gen(.default, table, '"', .update, true, '$', 1, data, where) pg_stmt_worker(db, query, data, where)! } // delete is used internally by V's ORM for processing `DELETE ` queries -pub fn (db DB) delete(table string, where orm.QueryData) ! { +pub fn (db DB) delete(table orm.Table, where orm.QueryData) ! { query, _ := orm.orm_stmt_gen(.default, table, '"', .delete, true, '$', 1, orm.QueryData{}, where) pg_stmt_worker(db, query, orm.QueryData{}, where)! @@ -60,14 +60,21 @@ pub fn (db DB) last_id() int { // DDL (table creation/destroying etc) // create is used internally by V's ORM for processing table creation queries (DDL) -pub fn (db DB) create(table string, fields []orm.TableField) ! { - query := orm.orm_table_gen(table, '"', true, 0, fields, pg_type_from_v, false) or { return err } - pg_stmt_worker(db, query, orm.QueryData{}, orm.QueryData{})! +pub fn (db DB) create(table orm.Table, fields []orm.TableField) ! { + query := orm.orm_table_gen(.pg, table, '"', true, 0, fields, pg_type_from_v, false) or { + return err + } + stmts := query.split(';') + for stmt in stmts { + if stmt != '' { + pg_stmt_worker(db, stmt + ';', orm.QueryData{}, orm.QueryData{})! + } + } } // drop is used internally by V's ORM for processing table destroying queries (DDL) -pub fn (db DB) drop(table string) ! { - query := 'DROP TABLE "${table}";' +pub fn (db DB) drop(table orm.Table) ! { + query := 'DROP TABLE "${table.name}";' pg_stmt_worker(db, query, orm.QueryData{}, orm.QueryData{})! } diff --git a/vlib/db/pg/pg_orm_test.v b/vlib/db/pg/pg_orm_test.v index 249183f646..391a89b548 100644 --- a/vlib/db/pg/pg_orm_test.v +++ b/vlib/db/pg/pg_orm_test.v @@ -36,6 +36,13 @@ struct TestDefaultAttribute { created_at string @[default: 'CURRENT_TIMESTAMP'; sql_type: 'TIMESTAMP'] } +@[comment: 'This is a table comment'] +struct TestCommentAttribute { + id string @[primary; sql: serial] + name string @[comment: 'real user name'] + created_at string @[default: 'CURRENT_TIMESTAMP'; sql_type: 'TIMESTAMP'] +} + fn test_pg_orm() { $if !network ? { eprintln('> Skipping test ${@FN}, since `-d network` is not passed.') @@ -46,15 +53,18 @@ fn test_pg_orm() { host: 'localhost' user: 'postgres' password: '12345678' - dbname: 'test' + dbname: 'postgres' ) or { panic(err) } defer { db.close() } - db.drop('Test')! + table := orm.Table{ + name: 'Test' + } + db.drop(table) or {} - db.create('Test', [ + db.create(table, [ orm.TableField{ name: 'id' typ: typeof[string]().idx @@ -94,13 +104,13 @@ fn test_pg_orm() { }, ]) or { panic(err) } - db.insert('Test', orm.QueryData{ + db.insert(table, orm.QueryData{ fields: ['name', 'age'] data: [orm.string_to_primitive('Louis'), orm.int_to_primitive(101)] }) or { panic(err) } res := db.select(orm.SelectConfig{ - table: 'Test' + table: table is_count: false has_where: true has_order: false @@ -143,7 +153,7 @@ fn test_pg_orm() { */ sql db { drop table TestCustomSqlType - }! + } or {} sql db { create table TestCustomSqlType @@ -232,4 +242,60 @@ fn test_pg_orm() { drop table TestDefaultAttribute }! assert ['gen_random_uuid()', '', 'CURRENT_TIMESTAMP'] == information_schema_defaults_results + + /** test comment attribute + */ + sql db { + create table TestCommentAttribute + }! + + mut column_comments := db.exec(" + SELECT + a.attname AS column_name, + col_description(a.attrelid, a.attnum) AS column_comment + FROM pg_attribute a + JOIN pg_class c ON c.oid = a.attrelid + JOIN pg_namespace n ON n.oid = c.relnamespace + WHERE c.relname = 'TestCommentAttribute' + AND n.nspname = 'public' + AND a.attnum > 0 + AND NOT a.attisdropped + ORDER BY a.attnum + ") or { + println(err) + panic(err) + } + + mut table_comment := db.exec(" + SELECT + nspname AS schema_name, + relname AS table_name, + obj_description(pc.oid) AS table_comment + FROM pg_class pc + JOIN pg_namespace pn ON pn.oid = pc.relnamespace + WHERE pc.relkind = 'r' AND pc.relname = 'TestCommentAttribute' + ORDER BY schema_name, table_name + ") or { + println(err) + panic(err) + } + + sql db { + drop table TestCommentAttribute + }! + + mut information_schema_column_comment_results := []string{} + + for comment in column_comments { + x := comment.vals[1] + information_schema_column_comment_results << x or { '' } + } + assert information_schema_column_comment_results == ['', 'real user name', ''] + + mut information_schema_table_comment_result := []string{} + for comment in table_comment { + x := comment.vals[2] + information_schema_table_comment_result << x or { '' } + } + assert information_schema_table_comment_result == ['This is a table comment'] } diff --git a/vlib/db/sqlite/orm.v b/vlib/db/sqlite/orm.v index f375893453..521b77f4d5 100644 --- a/vlib/db/sqlite/orm.v +++ b/vlib/db/sqlite/orm.v @@ -52,20 +52,20 @@ pub fn (db DB) select(config orm.SelectConfig, data orm.QueryData, where orm.Que // sql stmt // insert is used internally by V's ORM for processing `INSERT ` queries -pub fn (db DB) insert(table string, data orm.QueryData) ! { +pub fn (db DB) insert(table orm.Table, data orm.QueryData) ! { query, converted_data := orm.orm_stmt_gen(.sqlite, table, '`', .insert, true, '?', 1, data, orm.QueryData{}) sqlite_stmt_worker(db, query, converted_data, orm.QueryData{})! } // update is used internally by V's ORM for processing `UPDATE ` queries -pub fn (db DB) update(table string, data orm.QueryData, where orm.QueryData) ! { +pub fn (db DB) update(table orm.Table, data orm.QueryData, where orm.QueryData) ! { query, _ := orm.orm_stmt_gen(.sqlite, table, '`', .update, true, '?', 1, data, where) sqlite_stmt_worker(db, query, data, where)! } // delete is used internally by V's ORM for processing `DELETE ` queries -pub fn (db DB) delete(table string, where orm.QueryData) ! { +pub fn (db DB) delete(table orm.Table, where orm.QueryData) ! { query, _ := orm.orm_stmt_gen(.sqlite, table, '`', .delete, true, '?', 1, orm.QueryData{}, where) sqlite_stmt_worker(db, query, orm.QueryData{}, where)! @@ -81,16 +81,15 @@ pub fn (db DB) last_id() int { // DDL (table creation/destroying etc) // create is used internally by V's ORM for processing table creation queries (DDL) -pub fn (db DB) create(table string, fields []orm.TableField) ! { - query := orm.orm_table_gen(table, '`', true, 0, fields, sqlite_type_from_v, false) or { - return err - } +pub fn (db DB) create(table orm.Table, fields []orm.TableField) ! { + query := orm.orm_table_gen(.sqlite, table, '`', true, 0, fields, sqlite_type_from_v, + false) or { return err } sqlite_stmt_worker(db, query, orm.QueryData{}, orm.QueryData{})! } // drop is used internally by V's ORM for processing table destroying queries (DDL) -pub fn (db DB) drop(table string) ! { - query := 'DROP TABLE `${table}`;' +pub fn (db DB) drop(table orm.Table) ! { + query := 'DROP TABLE `${table.name}`;' sqlite_stmt_worker(db, query, orm.QueryData{}, orm.QueryData{})! } diff --git a/vlib/db/sqlite/sqlite_orm_test.v b/vlib/db/sqlite/sqlite_orm_test.v index 82e0ee0956..6cdff0c2cb 100644 --- a/vlib/db/sqlite/sqlite_orm_test.v +++ b/vlib/db/sqlite/sqlite_orm_test.v @@ -32,7 +32,10 @@ fn test_sqlite_orm() { defer { db.close() or { panic(err) } } - db.create('Test', [ + table := orm.Table{ + name: 'Test' + } + db.create(table, [ orm.TableField{ name: 'id' typ: typeof[int]().idx @@ -59,13 +62,13 @@ fn test_sqlite_orm() { }, ]) or { panic(err) } - db.insert('Test', orm.QueryData{ + db.insert(table, orm.QueryData{ fields: ['name', 'age'] data: [orm.string_to_primitive('Louis'), orm.i64_to_primitive(100)] }) or { panic(err) } res := db.select(orm.SelectConfig{ - table: 'Test' + table: table has_where: true fields: ['id', 'name', 'age'] types: [typeof[int]().idx, typeof[string]().idx, typeof[i64]().idx] diff --git a/vlib/orm/orm.v b/vlib/orm/orm.v index 14face0ddf..56ab71ff0e 100644 --- a/vlib/orm/orm.v +++ b/vlib/orm/orm.v @@ -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}') diff --git a/vlib/orm/orm_fn_test.v b/vlib/orm/orm_fn_test.v index 64fc37a471..ca2906fe16 100644 --- a/vlib/orm/orm_fn_test.v +++ b/vlib/orm/orm_fn_test.v @@ -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 diff --git a/vlib/orm/orm_func.v b/vlib/orm/orm_func.v index 7623e92f96..33fe870f0b 100644 --- a/vlib/orm/orm_func.v +++ b/vlib/orm/orm_func.v @@ -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 diff --git a/vlib/orm/orm_mut_connection_test.v b/vlib/orm/orm_mut_connection_test.v index c257bb2af9..99c92b1545 100644 --- a/vlib/orm/orm_mut_connection_test.v +++ b/vlib/orm/orm_mut_connection_test.v @@ -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}') } diff --git a/vlib/orm/orm_null_test.v b/vlib/orm/orm_null_test.v index bde01b3be4..a5705f6089 100644 --- a/vlib/orm/orm_null_test.v +++ b/vlib/orm/orm_null_test.v @@ -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) } diff --git a/vlib/v/gen/c/orm.v b/vlib/v/gen/c/orm.v index 9b41cc0e3b..a4b44c0141 100644 --- a/vlib/v/gen/c/orm.v +++ b/vlib/v/gen/c/orm.v @@ -48,6 +48,7 @@ fn (mut g Gen) sql_insert_expr(node ast.SqlExpr) { connection_var_name := g.new_tmp_var() g.write_orm_connection_init(connection_var_name, &node.db_expr) table_name := g.get_table_name_by_struct_type(node.table_expr.typ) + table_attrs := g.get_table_attrs_by_struct_type(node.table_expr.typ) result_var_name := g.new_tmp_var() g.sql_table_name = g.table.sym(node.table_expr.typ).name @@ -55,10 +56,11 @@ fn (mut g Gen) sql_insert_expr(node ast.SqlExpr) { hack_stmt_line := ast.SqlStmtLine{ object_var: node.inserted_var fields: node.fields + table_expr: node.table_expr // sub_structs: node.sub_structs } g.write_orm_insert(hack_stmt_line, table_name, connection_var_name, result_var_name, - node.or_expr) + node.or_expr, table_attrs) g.write2(left, 'orm__Connection_name_table[${connection_var_name}._typ]._method_last_id(${connection_var_name}._object)') } @@ -93,6 +95,7 @@ fn (mut g Gen) sql_stmt_line(stmt_line ast.SqlStmtLine, connection_var_name stri g.sql_last_stmt_out_len = g.out.len mut node := stmt_line table_name := g.get_table_name_by_struct_type(node.table_expr.typ) + table_attrs := g.get_table_attrs_by_struct_type(node.table_expr.typ) result_var_name := g.new_tmp_var() g.sql_table_name = g.table.sym(node.table_expr.typ).name @@ -101,15 +104,18 @@ fn (mut g Gen) sql_stmt_line(stmt_line ast.SqlStmtLine, connection_var_name stri } if node.kind == .create { - g.write_orm_create_table(node, table_name, connection_var_name, result_var_name) + g.write_orm_create_table(node, table_name, connection_var_name, result_var_name, + table_attrs) } else if node.kind == .drop { - g.write_orm_drop_table(table_name, connection_var_name, result_var_name) + g.write_orm_drop_table(node, table_name, connection_var_name, result_var_name, + table_attrs) } else if node.kind == .insert { - g.write_orm_insert(node, table_name, connection_var_name, result_var_name, or_expr) + g.write_orm_insert(node, table_name, connection_var_name, result_var_name, or_expr, + table_attrs) } else if node.kind == .update { - g.write_orm_update(node, table_name, connection_var_name, result_var_name) + g.write_orm_update(node, table_name, connection_var_name, result_var_name, table_attrs) } else if node.kind == .delete { - g.write_orm_delete(node, table_name, connection_var_name, result_var_name) + g.write_orm_delete(node, table_name, connection_var_name, result_var_name, table_attrs) } g.or_block(result_var_name, or_expr, ast.int_type.set_flag(.result)) @@ -138,14 +144,54 @@ fn (mut g Gen) write_orm_connection_init(connection_var_name string, db_expr &as } } +// write_orm_table_struct writes C code for the orm.Table struct +fn (mut g Gen) write_orm_table_struct(typ ast.Type) { + table_name := g.get_table_name_by_struct_type(typ) + table_attrs := g.get_table_attrs_by_struct_type(typ) + + g.writeln('((orm__Table){') + g.indent++ + g.writeln('.name = _S("${table_name}"),') + g.writeln('.attrs = new_array_from_c_array(${table_attrs.len}, ${table_attrs.len}, sizeof(VAttribute),') + g.indent++ + + if table_attrs.len > 0 { + g.write('_MOV((VAttribute[${table_attrs.len}]){') + g.indent++ + for attr in table_attrs { + g.write('(VAttribute){') + g.indent++ + name1 := util.smart_quote(attr.name, false) + name := cescape_nonascii(name1) + g.write(' .name = _S("${name}"),') + g.write(' .has_arg = ${attr.has_arg},') + arg1 := util.smart_quote(attr.arg, false) + arg := cescape_nonascii(arg1) + g.write(' .arg = _S("${arg}"),') + g.write(' .kind = ${int(attr.kind)},') + g.indent-- + g.write('},') + } + g.indent-- + g.writeln('})') + } else { + g.writeln('NULL // No attrs') + } + g.indent-- + g.writeln(')') + g.indent-- + g.write('})') +} + // write_orm_create_table writes C code that calls ORM functions for creating tables. fn (mut g Gen) write_orm_create_table(node ast.SqlStmtLine, table_name string, connection_var_name string, - result_var_name string) { + result_var_name string, table_attrs []ast.Attr) { g.writeln('// sql { create table `${table_name}` }') g.writeln('${result_name}_void ${result_var_name} = orm__Connection_name_table[${connection_var_name}._typ]._method_create(') g.indent++ g.writeln('${connection_var_name}._object, // Connection object') - g.writeln('_S("${table_name}"),') + g.write_orm_table_struct(node.table_expr.typ) + g.writeln(',') g.writeln('new_array_from_c_array(${node.fields.len}, ${node.fields.len}, sizeof(orm__TableField),') g.indent++ @@ -213,19 +259,19 @@ fn (mut g Gen) write_orm_create_table(node ast.SqlStmtLine, table_name string, c } // write_orm_drop_table writes C code that calls ORM functions for dropping tables. -fn (mut g Gen) write_orm_drop_table(table_name string, connection_var_name string, result_var_name string) { +fn (mut g Gen) write_orm_drop_table(node ast.SqlStmtLine, table_name string, connection_var_name string, result_var_name string, table_attrs []ast.Attr) { g.writeln('// sql { drop table `${table_name}` }') g.writeln('${result_name}_void ${result_var_name} = orm__Connection_name_table[${connection_var_name}._typ]._method_drop(') g.indent++ g.writeln('${connection_var_name}._object, // Connection object') - g.writeln('_S("${table_name}")') + g.write_orm_table_struct(node.table_expr.typ) g.indent-- g.writeln(');') } // write_orm_insert writes C code that calls ORM functions for inserting structs into a table. fn (mut g Gen) write_orm_insert(node &ast.SqlStmtLine, table_name string, connection_var_name string, result_var_name string, - or_expr &ast.OrExpr) { + or_expr &ast.OrExpr, table_attrs []ast.Attr) { last_ids_variable_name := g.new_tmp_var() g.writeln('Array_orm__Primitive ${last_ids_variable_name} = __new_array_with_default_noscan(0, 0, sizeof(orm__Primitive), 0);') @@ -234,12 +280,13 @@ fn (mut g Gen) write_orm_insert(node &ast.SqlStmtLine, table_name string, connec } // write_orm_update writes C code that calls ORM functions for updating rows. -fn (mut g Gen) write_orm_update(node &ast.SqlStmtLine, table_name string, connection_var_name string, result_var_name string) { +fn (mut g Gen) write_orm_update(node &ast.SqlStmtLine, table_name string, connection_var_name string, result_var_name string, table_attrs []ast.Attr) { g.writeln('// sql { update `${table_name}` }') g.writeln('${result_name}_void ${result_var_name} = orm__Connection_name_table[${connection_var_name}._typ]._method_update(') g.indent++ g.writeln('${connection_var_name}._object, // Connection object') - g.writeln('_S("${table_name}"),') + g.write_orm_table_struct(node.table_expr.typ) + g.writeln(',') g.writeln('(orm__QueryData){') g.indent++ g.writeln('.kinds = __new_array_with_default_noscan(0, 0, sizeof(orm__OperationKind), 0),') @@ -285,12 +332,13 @@ fn (mut g Gen) write_orm_update(node &ast.SqlStmtLine, table_name string, connec } // write_orm_delete writes C code that calls ORM functions for deleting rows. -fn (mut g Gen) write_orm_delete(node &ast.SqlStmtLine, table_name string, connection_var_name string, result_var_name string) { +fn (mut g Gen) write_orm_delete(node &ast.SqlStmtLine, table_name string, connection_var_name string, result_var_name string, table_attrs []ast.Attr) { g.writeln('// sql { delete from `${table_name}` }') g.writeln('${result_name}_void ${result_var_name} = orm__Connection_name_table[${connection_var_name}._typ]._method__v_delete(') g.indent++ g.writeln('${connection_var_name}._object, // Connection object') - g.writeln('_S("${table_name}"),') + g.write_orm_table_struct(node.table_expr.typ) + g.writeln(',') g.write_orm_where(node.where_expr) g.indent-- g.writeln(');') @@ -381,7 +429,8 @@ fn (mut g Gen) write_orm_insert_with_last_ids(node ast.SqlStmtLine, connection_v g.writeln('${result_name}_void ${res} = orm__Connection_name_table[${connection_var_name}._typ]._method_insert(') g.indent++ g.writeln('${connection_var_name}._object, // Connection object') - g.writeln('_S("${table_name}"),') + g.write_orm_table_struct(node.table_expr.typ) + g.writeln(',') g.writeln('(orm__QueryData){') g.indent++ g.writeln('.fields = new_array_from_c_array(${fields.len}, ${fields.len}, sizeof(string),') @@ -880,7 +929,6 @@ fn (mut g Gen) write_orm_select(node ast.SqlExpr, connection_var_name string, re select_result_var_name := g.new_tmp_var() table_name := g.get_table_name_by_struct_type(node.table_expr.typ) - escaped_table_name := cescape_nonascii(util.smart_quote(table_name, false)) g.sql_table_name = g.table.sym(node.table_expr.typ).name g.writeln('// sql { select from `${table_name}` }') @@ -889,7 +937,9 @@ fn (mut g Gen) write_orm_select(node ast.SqlExpr, connection_var_name string, re g.writeln('${connection_var_name}._object, // Connection object') g.writeln('(orm__SelectConfig){') g.indent++ - g.writeln('.table = _S("${escaped_table_name}"),') + g.writeln('.table = ') + g.write_orm_table_struct(node.table_expr.typ) + g.writeln(',') g.writeln('.is_count = ${node.is_count},') g.writeln('.has_where = ${node.has_where},') g.writeln('.has_order = ${node.has_order},') @@ -1246,6 +1296,13 @@ fn (g &Gen) get_db_expr_type(expr ast.Expr) ?ast.Type { return none } +// get_table_attrs_by_struct_type returns the struct attrs. +fn (g &Gen) get_table_attrs_by_struct_type(typ ast.Type) []ast.Attr { + sym := g.table.sym(typ) + info := sym.struct_info() + return info.attrs +} + // get_table_name_by_struct_type converts the struct type to a table name. fn (g &Gen) get_table_name_by_struct_type(typ ast.Type) string { sym := g.table.sym(typ) @@ -1255,8 +1312,8 @@ fn (g &Gen) get_table_name_by_struct_type(typ ast.Type) string { if attr := info.attrs.find_first('table') { table_name = attr.arg } - - return table_name + escaped_table_name := cescape_nonascii(util.smart_quote(table_name, false)) + return escaped_table_name } // get_orm_current_table_field returns the current processing table's struct field by name.