1
0
mirror of https://github.com/vlang/v.git synced 2023-08-10 21:13:21 +03:00

orm: default attribute (#15221)

This commit is contained in:
Hitalo de Jesus do Rosário Souza 2022-07-26 18:59:32 -03:00 committed by GitHub
parent c976a691ad
commit e5e750d533
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 450 additions and 90 deletions

View File

@ -1,6 +1,7 @@
import orm
import mysql
import time
import v.ast
struct TestCustomSqlType {
id int [primary; sql: serial]
@ -29,20 +30,28 @@ mut:
deleted_at time.Time
}
struct TestDefaultAtribute {
id string [primary; sql: serial]
name string
created_at string [default: 'CURRENT_TIMESTAMP'; sql_type: 'TIMESTAMP']
}
fn test_mysql_orm() {
mut mdb := mysql.Connection{
mut db := mysql.Connection{
host: 'localhost'
port: 3306
username: 'root'
password: ''
dbname: 'mysql'
}
mdb.connect() or { panic(err) }
db := orm.Connection(mdb)
db.connect() or { panic(err) }
defer {
db.close()
}
db.create('Test', [
orm.TableField{
name: 'id'
typ: 7
typ: ast.int_type_idx
attrs: [
StructAttribute{
name: 'primary'
@ -57,12 +66,12 @@ fn test_mysql_orm() {
},
orm.TableField{
name: 'name'
typ: 20
typ: ast.string_type_idx
attrs: []
},
orm.TableField{
name: 'age'
typ: 7
typ: ast.int_type_idx
},
]) or { panic(err) }
@ -75,11 +84,11 @@ fn test_mysql_orm() {
table: 'Test'
has_where: true
fields: ['id', 'name', 'age']
types: [7, 20, 8]
types: [ast.int_type_idx, ast.string_type_idx, ast.i64_type_idx]
}, orm.QueryData{}, orm.QueryData{
fields: ['name', 'age']
data: [orm.Primitive('Louis'), i64(101)]
types: [20, 8]
types: [ast.string_type_idx, ast.i64_type_idx]
is_and: [true, true]
kinds: [.eq, .eq]
}) or { panic(err) }
@ -88,8 +97,6 @@ fn test_mysql_orm() {
name := res[0][1]
age := res[0][2]
mdb.close()
assert id is int
if id is int {
assert id == 1
@ -104,22 +111,10 @@ fn test_mysql_orm() {
if age is i64 {
assert age == 101
}
}
fn test_orm() {
mut db := mysql.Connection{
host: 'localhost'
port: 3306
username: 'root'
password: ''
dbname: 'mysql'
}
db.connect() or {
println(err)
panic(err)
}
/** test orm sql type
* - verify if all type create by attribute sql_type has created
*/
sql db {
create table TestCustomSqlType
}
@ -168,27 +163,19 @@ fn test_orm() {
sql db {
drop table TestCustomSqlType
}
db.close()
assert result_custom_sql.maps() == information_schema_custom_sql
}
fn test_orm_time_type() ? {
mut db := mysql.Connection{
host: 'localhost'
port: 3306
username: 'root'
password: ''
dbname: 'mysql'
}
db.connect() or {
/** test_orm_time_type
* - test time.Time v type with sql_type: 'TIMESTAMP'
* - test string v type with sql_type: 'TIMESTAMP'
* - test time.Time v type without
*/
today := time.parse('2022-07-16 15:13:27') or {
println(err)
panic(err)
}
today := time.parse('2022-07-16 15:13:27')?
model := TestTimeType{
username: 'hitalo'
created_at: today
@ -212,10 +199,38 @@ fn test_orm_time_type() ? {
drop table TestTimeType
}
db.close()
assert results[0].username == model.username
assert results[0].created_at == model.created_at
assert results[0].updated_at == model.updated_at
assert results[0].deleted_at == model.deleted_at
/** test default attribute
*/
sql db {
create table TestDefaultAtribute
}
mut result_defaults := db.query("
SELECT COLUMN_DEFAULT
FROM INFORMATION_SCHEMA.COLUMNS
WHERE TABLE_NAME = 'TestDefaultAtribute'
ORDER BY ORDINAL_POSITION
") or {
println(err)
panic(err)
}
mut information_schema_defaults_results := []string{}
sql db {
drop table TestDefaultAtribute
}
information_schema_column_default_sql := [{
'COLUMN_DEFAULT': ''
}, {
'COLUMN_DEFAULT': ''
}, {
'COLUMN_DEFAULT': 'CURRENT_TIMESTAMP'
}]
assert information_schema_column_default_sql == result_defaults.maps()
}

View File

@ -124,7 +124,7 @@ pub fn (db Connection) insert(table string, data orm.QueryData) ? {
mut converted_primitive_array := db.factory_orm_primitive_converted_from_sql(table,
data)?
converted_data := orm.QueryData{
converted_primitive_data := orm.QueryData{
fields: data.fields
data: converted_primitive_array
types: []
@ -132,17 +132,19 @@ pub fn (db Connection) insert(table string, data orm.QueryData) ? {
is_and: []
}
query := orm.orm_stmt_gen(table, '`', .insert, false, '?', 1, converted_data, orm.QueryData{})
query, converted_data := orm.orm_stmt_gen(table, '`', .insert, false, '?', 1, converted_primitive_data,
orm.QueryData{})
mysql_stmt_worker(db, query, converted_data, orm.QueryData{})?
}
pub fn (db Connection) update(table string, data orm.QueryData, where orm.QueryData) ? {
query := orm.orm_stmt_gen(table, '`', .update, false, '?', 1, data, where)
query, _ := orm.orm_stmt_gen(table, '`', .update, false, '?', 1, data, where)
mysql_stmt_worker(db, query, data, where)?
}
pub fn (db Connection) delete(table string, where orm.QueryData) ? {
query := orm.orm_stmt_gen(table, '`', .delete, false, '?', 1, orm.QueryData{}, where)
query, _ := orm.orm_stmt_gen(table, '`', .delete, false, '?', 1, orm.QueryData{},
where)
mysql_stmt_worker(db, query, orm.QueryData{}, where)?
}
@ -158,7 +160,7 @@ pub fn (db Connection) last_id() orm.Primitive {
// table
pub fn (db Connection) create(table string, fields []orm.TableField) ? {
query := orm.orm_table_gen(table, '`', false, 0, fields, mysql_type_from_v, false) or {
query := orm.orm_table_gen(table, '`', true, 0, fields, mysql_type_from_v, false) or {
return err
}
mysql_stmt_worker(db, query, orm.QueryData{}, orm.QueryData{})?

View File

@ -15,6 +15,7 @@
- `[sql: type]` where `type` is a V type such as `int` or `f64`, or special type `serial`
- `[sql: 'name']` sets a custom column name for the field
- `[sql_type: 'SQL TYPE']` sets the sql type which is used in sql
- `[default: 'sql defaults']` sets the default value or function when create a new table
## Usage
@ -90,3 +91,53 @@ result := sql db {
select from Foo where id > 1 order by id
}
```
### Example
```v ignore
import pg
struct Member {
id string [default: 'gen_random_uuid()'; primary; sql_type: 'uuid']
name string
created_at string [default: 'CURRENT_TIMESTAMP'; sql_type: 'TIMESTAMP']
}
fn main() {
db := pg.connect(pg.Config{
host: 'localhost'
port: 5432
user: 'user'
password: 'password'
dbname: 'dbname'
}) or {
println(err)
return
}
defer {
db.close()
}
sql db {
create table Member
}
new_member := Member{
name: 'John Doe'
}
sql db {
insert new_member into Member
}
selected_member := sql db {
select from Member where name == 'John Doe' limit 1
}
sql db {
update Member set name = 'Hitalo' where id == selected_member.id
}
}
```

View File

@ -182,27 +182,42 @@ pub interface Connection {
// num - Stmt uses nums at prepared statements (? or ?1)
// qm - Character for prepared statment, qm because of quotation mark like in sqlite
// start_pos - When num is true, it's the start position of the counter
pub fn orm_stmt_gen(table string, q string, kind StmtKind, num bool, qm string, start_pos int, data QueryData, where QueryData) string {
pub fn orm_stmt_gen(table string, q string, kind StmtKind, num bool, qm string, start_pos int, data QueryData, where QueryData) (string, QueryData) {
mut str := ''
mut c := start_pos
mut data_fields := []string{}
mut data_data := []Primitive{}
match kind {
.insert {
mut values := []string{}
mut select_fields := []string{}
for _ in 0 .. data.fields.len {
// loop over the length of data.field and generate ?0, ?1 or just ? based on the $num qmeter for value placeholders
if num {
values << '$qm$c'
c++
} else {
values << '$qm'
for i in 0 .. data.fields.len {
if data.data.len > 0 {
match data.data[i].type_name() {
'string' {
if (data.data[i] as string).len == 0 {
continue
}
}
'time.Time' {
if (data.data[i] as time.Time).unix == 0 {
continue
}
}
else {}
}
data_data << data.data[i]
}
select_fields << '$q${data.fields[i]}$q'
values << factory_insert_qm_value(num, qm, c)
data_fields << data.fields[i]
c++
}
str += 'INSERT INTO $q$table$q ('
str += data.fields.map('$q$it$q').join(', ')
str += select_fields.join(', ')
str += ') VALUES ('
str += values.join(', ')
str += ')'
@ -262,7 +277,13 @@ pub fn orm_stmt_gen(table string, q string, kind StmtKind, num bool, qm string,
}
}
str += ';'
return str
return str, QueryData{
fields: data_fields
data: data_data
types: data.types
kinds: data.kinds
is_and: data.is_and
}
}
// Generates an sql select stmt, from universal parameter
@ -357,6 +378,7 @@ pub fn orm_table_gen(table string, q string, defaults bool, def_unique_len int,
if field.is_arr {
continue
}
mut default_val := field.default_val
mut no_null := false
mut is_unique := false
mut is_skip := false
@ -397,6 +419,14 @@ pub fn orm_table_gen(table string, q string, defaults bool, def_unique_len int,
}
ctyp = attr.arg
}
'default' {
if attr.kind != .string {
return error("default attribute need be string. Try [default: '$attr.arg'] instead of [default: $attr.arg]")
}
if default_val == '' {
default_val = attr.arg
}
}
/*'fkey' {
if attr.arg != '' {
if attr.kind == .string {
@ -416,8 +446,8 @@ pub fn orm_table_gen(table string, q string, defaults bool, def_unique_len int,
return error('Unknown type ($field.typ) for field $field.name in struct $table')
}
stmt = '$q$field_name$q $ctyp'
if defaults && field.default_val != '' {
stmt += ' DEFAULT $field.default_val'
if defaults && default_val != '' {
stmt += ' DEFAULT $default_val'
}
if no_null {
stmt += ' NOT NULL'
@ -543,3 +573,11 @@ pub fn time_to_primitive(b time.Time) Primitive {
pub fn infix_to_primitive(b InfixType) Primitive {
return Primitive(b)
}
fn factory_insert_qm_value(num bool, qm string, c int) string {
if num {
return '$qm$c'
} else {
return '$qm'
}
}

View File

@ -2,7 +2,7 @@ import orm
import v.ast
fn test_orm_stmt_gen_update() {
query := orm.orm_stmt_gen('Test', "'", .update, true, '?', 0, orm.QueryData{
query, _ := orm.orm_stmt_gen('Test', "'", .update, true, '?', 0, orm.QueryData{
fields: ['test', 'a']
data: []
types: []
@ -17,7 +17,7 @@ fn test_orm_stmt_gen_update() {
}
fn test_orm_stmt_gen_insert() {
query := orm.orm_stmt_gen('Test', "'", .insert, true, '?', 0, orm.QueryData{
query, _ := orm.orm_stmt_gen('Test', "'", .insert, true, '?', 0, orm.QueryData{
fields: ['test', 'a']
data: []
types: []
@ -27,7 +27,7 @@ fn test_orm_stmt_gen_insert() {
}
fn test_orm_stmt_gen_delete() {
query := orm.orm_stmt_gen('Test', "'", .delete, true, '?', 0, orm.QueryData{
query, _ := orm.orm_stmt_gen('Test', "'", .delete, true, '?', 0, orm.QueryData{
fields: ['test', 'a']
data: []
types: []

View File

@ -31,17 +31,18 @@ pub fn (db DB) @select(config orm.SelectConfig, data orm.QueryData, where orm.Qu
// sql stmt
pub fn (db DB) insert(table string, data orm.QueryData) ? {
query := orm.orm_stmt_gen(table, '"', .insert, true, '$', 1, data, orm.QueryData{})
pg_stmt_worker(db, query, data, orm.QueryData{})?
query, converted_data := orm.orm_stmt_gen(table, '"', .insert, true, '$', 1, data,
orm.QueryData{})
pg_stmt_worker(db, query, converted_data, orm.QueryData{})?
}
pub fn (db DB) update(table string, data orm.QueryData, where orm.QueryData) ? {
query := orm.orm_stmt_gen(table, '"', .update, true, '$', 1, data, where)
query, _ := orm.orm_stmt_gen(table, '"', .update, true, '$', 1, data, where)
pg_stmt_worker(db, query, data, where)?
}
pub fn (db DB) delete(table string, where orm.QueryData) ? {
query := orm.orm_stmt_gen(table, '"', .delete, true, '$', 1, orm.QueryData{}, where)
query, _ := orm.orm_stmt_gen(table, '"', .delete, true, '$', 1, orm.QueryData{}, where)
pg_stmt_worker(db, query, orm.QueryData{}, where)?
}
@ -163,18 +164,20 @@ fn pg_stmt_match(mut types []u32, mut vals []&char, mut lens []int, mut formats
formats << 1
}
string {
types << u32(Oid.t_text)
// If paramTypes is NULL, or any particular element in the array is zero,
// the server infers a data type for the parameter symbol in the same way
// it would do for an untyped literal string.
types << &u8(0)
vals << data.str
lens << data.len
formats << 0
}
time.Time {
types << u32(Oid.t_int4)
unix := int(data.unix)
num := conv.htn32(unsafe { &u32(&unix) })
vals << &char(&num)
lens << int(sizeof(u32))
formats << 1
datetime := ((d as time.Time).format_ss() as string)
types << &u8(0)
vals << datetime.str
lens << datetime.len
formats << 0
}
orm.InfixType {
pg_stmt_match(mut types, mut vals, mut lens, mut formats, data.right)
@ -190,9 +193,12 @@ fn pg_type_from_v(typ int) ?string {
orm.type_idx['bool'] {
'BOOLEAN'
}
orm.type_idx['int'], orm.type_idx['u32'], orm.time {
orm.type_idx['int'], orm.type_idx['u32'] {
'INT'
}
orm.time {
'TIMESTAMP'
}
orm.type_idx['i64'], orm.type_idx['u64'] {
'BIGINT'
}

View File

@ -2,39 +2,89 @@ module main
import orm
import pg
import v.ast
import time
struct TestCustomSqlType {
id int [primary; sql: serial]
custom string [sql_type: 'TEXT']
custom1 string [sql_type: 'VARCHAR(191)']
custom2 string [sql_type: 'TIMESTAMP']
custom3 string [sql_type: 'uuid']
}
struct TestCustomWrongSqlType {
id int [primary; sql: serial]
custom string
custom1 string [sql_type: 'VARCHAR']
custom2 string [sql_type: 'money']
custom3 string [sql_type: 'xml']
}
struct TestTimeType {
mut:
id int [primary; sql: serial]
username string
created_at time.Time [sql_type: 'TIMESTAMP']
updated_at string [sql_type: 'TIMESTAMP']
deleted_at time.Time
}
struct TestDefaultAtribute {
id string [default: 'gen_random_uuid()'; primary; sql_type: 'uuid']
name string
created_at string [default: 'CURRENT_TIMESTAMP'; sql_type: 'TIMESTAMP']
}
fn test_pg_orm() {
mut db := pg.connect(
host: 'localhost'
user: 'postgres'
password: ''
password: 'password'
dbname: 'postgres'
) or { panic(err) }
defer {
db.close()
}
db.create('Test', [
orm.TableField{
name: 'id'
typ: 7
typ: ast.string_type_idx
is_time: false
default_val: ''
is_arr: false
attrs: [
StructAttribute{
name: 'primary'
has_arg: false
arg: ''
kind: .plain
},
StructAttribute{
name: 'sql'
has_arg: true
kind: .plain
arg: 'serial'
kind: .plain
},
]
},
orm.TableField{
name: 'name'
typ: 18
typ: ast.string_type_idx
is_time: false
default_val: ''
is_arr: false
attrs: []
},
orm.TableField{
name: 'age'
typ: 7
typ: ast.i64_type_idx
is_time: false
default_val: ''
is_arr: false
attrs: []
},
]) or { panic(err) }
@ -45,15 +95,22 @@ fn test_pg_orm() {
res := db.@select(orm.SelectConfig{
table: 'Test'
is_count: false
has_where: true
has_order: false
order: ''
order_type: .asc
has_limit: false
primary: 'id'
has_offset: false
fields: ['id', 'name', 'age']
types: [7, 18, 8]
types: [ast.int_type_idx, ast.string_type_idx, ast.i64_type_idx]
}, orm.QueryData{}, orm.QueryData{
fields: ['name']
data: [orm.Primitive('Louis'), i64(101)]
types: [18]
fields: ['name', 'age']
data: [orm.Primitive('Louis'), orm.Primitive(101)]
types: []
kinds: [.eq, .eq]
is_and: [true]
kinds: [.eq]
}) or { panic(err) }
id := res[0][0]
@ -74,4 +131,97 @@ fn test_pg_orm() {
if age is i64 {
assert age == 101
}
/** test orm sql type
* - verify if all type create by attribute sql_type has created
*/
sql db {
create table TestCustomSqlType
}
mut result_custom_sql := db.exec("
SELECT DATA_TYPE
FROM INFORMATION_SCHEMA.COLUMNS
WHERE TABLE_NAME = 'TestCustomSqlType'
ORDER BY ORDINAL_POSITION
") or {
println(err)
panic(err)
}
mut information_schema_data_types_results := []string{}
information_schema_custom_sql := ['integer', 'text', 'character varying',
'timestamp without time zone', 'uuid']
for data_type in result_custom_sql {
information_schema_data_types_results << data_type.vals[0]
}
sql db {
drop table TestCustomSqlType
}
assert information_schema_data_types_results == information_schema_custom_sql
/** test_orm_time_type
* - test time.Time v type with sql_type: 'TIMESTAMP'
* - test string v type with sql_type: 'TIMESTAMP'
* - test time.Time v type without
*/
today := time.parse('2022-07-16 15:13:27') or {
println(err)
panic(err)
}
model := TestTimeType{
username: 'hitalo'
created_at: today
updated_at: today.str()
deleted_at: today
}
sql db {
create table TestTimeType
}
sql db {
insert model into TestTimeType
}
results := sql db {
select from TestTimeType where username == 'hitalo'
}
sql db {
drop table TestTimeType
}
assert results[0].username == model.username
assert results[0].created_at == model.created_at
assert results[0].updated_at == model.updated_at
assert results[0].deleted_at == model.deleted_at
/** test default attribute
*/
sql db {
create table TestDefaultAtribute
}
mut result_defaults := db.exec("
SELECT column_default
FROM INFORMATION_SCHEMA.COLUMNS
WHERE TABLE_NAME = 'TestDefaultAtribute'
ORDER BY ORDINAL_POSITION
") or {
println(err)
panic(err)
}
mut information_schema_defaults_results := []string{}
for defaults in result_defaults {
information_schema_defaults_results << defaults.vals[0]
}
sql db {
drop table TestDefaultAtribute
}
assert ['gen_random_uuid()', '', 'CURRENT_TIMESTAMP'] == information_schema_defaults_results
}

View File

@ -51,17 +51,18 @@ pub fn (db DB) @select(config orm.SelectConfig, data orm.QueryData, where orm.Qu
// sql stmt
pub fn (db DB) insert(table string, data orm.QueryData) ? {
query := orm.orm_stmt_gen(table, '`', .insert, true, '?', 1, data, orm.QueryData{})
sqlite_stmt_worker(db, query, data, orm.QueryData{})?
query, converted_data := orm.orm_stmt_gen(table, '`', .insert, true, '?', 1, data,
orm.QueryData{})
sqlite_stmt_worker(db, query, converted_data, orm.QueryData{})?
}
pub fn (db DB) update(table string, data orm.QueryData, where orm.QueryData) ? {
query := orm.orm_stmt_gen(table, '`', .update, true, '?', 1, data, where)
query, _ := orm.orm_stmt_gen(table, '`', .update, true, '?', 1, data, where)
sqlite_stmt_worker(db, query, data, where)?
}
pub fn (db DB) delete(table string, where orm.QueryData) ? {
query := orm.orm_stmt_gen(table, '`', .delete, true, '?', 1, orm.QueryData{}, where)
query, _ := orm.orm_stmt_gen(table, '`', .delete, true, '?', 1, orm.QueryData{}, where)
sqlite_stmt_worker(db, query, orm.QueryData{}, where)?
}

View File

@ -1,10 +1,32 @@
import orm
import sqlite
import v.ast
import time
struct TestCustomSqlType {
id int [primary; sql: serial]
custom string [sql_type: 'INTEGER']
custom1 string [sql_type: 'TEXT']
custom2 string [sql_type: 'REAL']
custom3 string [sql_type: 'NUMERIC']
custom4 string
custom5 int
custom6 time.Time
}
struct TestDefaultAtribute {
id string [primary; sql: serial]
name string
created_at string [default: 'CURRENT_TIME']
created_at1 string [default: 'CURRENT_DATE']
created_at2 string [default: 'CURRENT_TIMESTAMP']
}
fn test_sqlite_orm() {
sdb := sqlite.connect(':memory:') or { panic(err) }
db := orm.Connection(sdb)
mut db := sqlite.connect(':memory:') or { panic(err) }
defer {
db.close() or { panic(err) }
}
db.create('Test', [
orm.TableField{
name: 'id'
@ -68,4 +90,74 @@ fn test_sqlite_orm() {
if age is i64 {
assert age == 100
}
/** test orm sql type
* - verify if all type create by attribute sql_type has created
*/
sql db {
create table TestCustomSqlType
}
mut result_custom_sql, mut exec_custom_code := db.exec('
pragma table_info(TestCustomSqlType);
')
assert exec_custom_code == 101
mut table_info_types_results := []string{}
information_schema_custom_sql := ['INTEGER', 'INTEGER', 'TEXT', 'REAL', 'NUMERIC', 'TEXT',
'INTEGER', 'INTEGER']
for data_type in result_custom_sql {
table_info_types_results << data_type.vals[2]
}
assert table_info_types_results == information_schema_custom_sql
sql db {
drop table TestCustomSqlType
}
/** test default attribute
*/
sql db {
create table TestDefaultAtribute
}
mut result_default_sql, mut code := db.exec('
pragma table_info(TestDefaultAtribute);
')
assert code == 101
mut information_schema_data_types_results := []string{}
information_schema_default_sql := ['', '', 'CURRENT_TIME', 'CURRENT_DATE', 'CURRENT_TIMESTAMP']
for data_type in result_default_sql {
information_schema_data_types_results << data_type.vals[4]
}
assert information_schema_data_types_results == information_schema_default_sql
test_default_atribute := TestDefaultAtribute{
name: 'Hitalo'
}
sql db {
insert test_default_atribute into TestDefaultAtribute
}
result_test_default_atribute := sql db {
select from TestDefaultAtribute limit 1
}
assert result_test_default_atribute.name == 'Hitalo'
assert test_default_atribute.created_at.len == 0
assert test_default_atribute.created_at1.len == 0
assert test_default_atribute.created_at2.len == 0
assert result_test_default_atribute.created_at.len == 8 // HH:MM:SS
assert result_test_default_atribute.created_at1.len == 10 // YYYY-MM-DD
assert result_test_default_atribute.created_at2.len == 19 // YYYY-MM-DD HH:MM:SS
sql db {
drop table TestDefaultAtribute
}
}

View File

@ -33,6 +33,7 @@ fn test_sqlite() {
fn test_can_access_sqlite_result_consts() {
assert sqlite.sqlite_ok == 0
assert sqlite.sqlite_error == 1
// assert sqlite.misuse == 21
assert sqlite.sqlite_row == 100
assert sqlite.sqlite_done == 101
}

View File

@ -51,6 +51,10 @@ fn (stmt Stmt) get_f64(idx int) f64 {
fn (stmt Stmt) get_text(idx int) string {
b := &char(C.sqlite3_column_text(stmt.stmt, idx))
if b == &char(0) {
return ''
}
return unsafe { b.vstring() }
}