Skip to content
Open
Show file tree
Hide file tree
Changes from 8 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion examples/veb_fullstack/product_entities.v
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,5 @@ struct Product {
id int @[primary; sql: serial]
user_id int
name string @[sql_type: 'TEXT']
created_at string @[default: 'CURRENT_TIMESTAMP']
created_at string @[default: CURRENT_TIMESTAMP]
}
6 changes: 3 additions & 3 deletions examples/veb_orm_jwt/user_entities.v
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@ mut:
id int @[primary; sql: serial]
username string @[required; sql_type: 'TEXT']
password string @[required; sql_type: 'TEXT']
created_at string @[default: 'CURRENT_TIMESTAMP']
updated_at string @[default: 'CURRENT_TIMESTAMP']
deleted_at string @[default: 'CURRENT_TIMESTAMP']
created_at string @[default: CURRENT_TIMESTAMP]
updated_at string @[default: CURRENT_TIMESTAMP]
deleted_at string @[default: CURRENT_TIMESTAMP]
active bool
}
4 changes: 2 additions & 2 deletions vlib/db/mysql/mysql_orm_test.v
Original file line number Diff line number Diff line change
Expand Up @@ -34,14 +34,14 @@ mut:
struct TestDefaultAttribute {
id string @[primary; sql: serial]
name string
created_at string @[default: 'CURRENT_TIMESTAMP'; sql_type: 'TIMESTAMP']
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']
created_at string @[default: CURRENT_TIMESTAMP; sql_type: 'TIMESTAMP']
}

fn test_mysql_orm() {
Expand Down
14 changes: 8 additions & 6 deletions vlib/db/pg/orm.v
Original file line number Diff line number Diff line change
Expand Up @@ -311,9 +311,10 @@ fn pg_stmt_match(mut types []u32, mut vals []&char, mut lens []int, mut formats
formats << 1
}
u8 {
types << u32(Oid.t_char)
vals << &char(&data)
lens << int(sizeof(u8))
types << u32(Oid.t_int2)
Comment thread
Jengro777 marked this conversation as resolved.
num := conv.hton16(u16(data))
vals << &char(&num)
lens << int(sizeof(u16))
formats << 1
}
u16 {
Expand All @@ -338,9 +339,10 @@ fn pg_stmt_match(mut types []u32, mut vals []&char, mut lens []int, mut formats
formats << 1
}
i8 {
types << u32(Oid.t_char)
vals << &char(&data)
lens << int(sizeof(i8))
types << u32(Oid.t_int2)
num := conv.hton16(u16(data))
vals << &char(&num)
lens << int(sizeof(i16))
formats << 1
}
i16 {
Expand Down
4 changes: 2 additions & 2 deletions vlib/db/pg/pg_orm_test.v
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ mut:
struct TestDefaultAttribute {
id string @[default: 'gen_random_uuid()'; primary; sql_type: 'uuid']
name string
created_at string @[default: 'CURRENT_TIMESTAMP'; sql_type: 'TIMESTAMP']
created_at string @[default: CURRENT_TIMESTAMP; sql_type: 'TIMESTAMP']
}

struct TestInsertDefaultValues {
Expand All @@ -45,7 +45,7 @@ struct TestInsertDefaultValues {
struct TestCommentAttribute {
id string @[primary; sql: serial]
name string @[comment: 'real user name']
created_at string @[default: 'CURRENT_TIMESTAMP'; sql_type: 'TIMESTAMP']
created_at string @[default: CURRENT_TIMESTAMP; sql_type: 'TIMESTAMP']
}

fn test_pg_orm() {
Expand Down
6 changes: 3 additions & 3 deletions vlib/db/sqlite/sqlite_orm_test.v
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,9 @@ struct TestCustomSqlType {
struct TestDefaultAttribute {
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']
created_at ?string @[default: CURRENT_TIME]
created_at1 ?string @[default: CURRENT_DATE]
created_at2 ?string @[default: CURRENT_TIMESTAMP]
}

struct TestDurationAlias {
Expand Down
28 changes: 27 additions & 1 deletion vlib/orm/orm.v
Original file line number Diff line number Diff line change
Expand Up @@ -499,6 +499,25 @@ fn trim_attr_arg(arg string) string {
return out
}

fn is_sql_expr(val string) bool {
// Function calls like gen_random_uuid(), NOW(), etc.
// Written as @[default: 'gen_random_uuid()'] — we detect the shape:
// must start with a letter/underscore, have ( and end with ),
// and the part before ( must be a valid SQL identifier.
if val.contains('(') && val.ends_with(')') {
before_paren := val.all_before('(')
if before_paren.len > 0 && (before_paren[0].is_letter() || before_paren[0] == `_`) {
for ch in before_paren {
if !ch.is_letter() && !ch.is_digit() && ch != `_` {
return false
Comment on lines +512 to +514

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Allow schema-qualified default functions

When a PostgreSQL default calls a schema-qualified function, such as @[default: 'extensions.uuid_generate_v4()'], the dot fails this identifier check and the generator emits DEFAULT 'extensions.uuid_generate_v4()'. PostgreSQL then treats the call as a literal—often rejecting it while creating a typed column—and the documented bare-identifier alternative cannot represent a dotted call, so qualified function defaults need to remain executable SQL.

Useful? React with 👍 / 👎.

}
}
return true
}
}
return false
}

fn tenant_filter_array_primitive_type[T](value []T) int {
if value.len > 0 {
first := value[0]
Expand Down Expand Up @@ -1417,6 +1436,7 @@ pub fn orm_table_gen(sql_dialect SQLDialect, table Table, q string, defaults boo
}
mut default_val := field.default_val
mut has_default := default_val != ''
mut is_str_default := false
mut nullable := field.nullable
mut is_unique := false
mut is_skip := false
Expand Down Expand Up @@ -1469,6 +1489,7 @@ pub fn orm_table_gen(sql_dialect SQLDialect, table Table, q string, defaults boo
}
'default' {
has_default = true
is_str_default = attr.kind == .string
if default_val == '' {
default_val = attr.arg.trim_space()
}
Expand Down Expand Up @@ -1525,7 +1546,12 @@ pub fn orm_table_gen(sql_dialect SQLDialect, table Table, q string, defaults boo
stmt = '${q}${field_name}${q} ${col_typ}'
if defaults && has_default {
if default_val != '' {
stmt += ' DEFAULT ${default_val}'
if is_str_default && !is_sql_expr(default_val) {
escaped := default_val.replace("'", "''")
stmt += " DEFAULT '${escaped}'"
Comment on lines +1561 to +1563

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Escape backslashes in MySQL string defaults

When this generator targets MySQL under its default SQL mode, escaping only apostrophes corrupts defaults containing backslash escape sequences. For example, a V default whose value is C:\temp becomes SQL DEFAULT 'C:\temp', where MySQL interprets \t as a tab; subsequent inserts receive a different value. Apply dialect-aware escaping so MySQL backslashes are preserved.

Useful? React with 👍 / 👎.

} else {
stmt += ' DEFAULT ${default_val}'
Comment thread
Jengro777 marked this conversation as resolved.
}
} else {
// Handle @[default: ''] - explicitly set DEFAULT '' for the column
stmt += " DEFAULT ''"
Expand Down
2 changes: 1 addition & 1 deletion vlib/orm/orm_func_test.v
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ struct UserPart {

struct UrlDefaultAttr {
id int @[primary; sql: serial]
url string @[default: '"https://example.test"']
url string @[default: 'https://example.test']
}

fn test_orm_func_field_attribute_argument_with_colon() {
Expand Down
4 changes: 2 additions & 2 deletions vlib/orm/orm_null_test.v
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@ struct Foo {
mut:
id u64 @[primary; sql: serial]
a string
b string @[default: '"yes"']
b string @[default: 'yes']
c ?string
d ?string = 'hi'
e int
Expand Down Expand Up @@ -139,7 +139,7 @@ fn test_option_struct_fields_and_none() {
sql db {
create table Foo
}!
assert db.st.last == 'CREATE TABLE IF NOT EXISTS `foo` (`id` serial-type NOT NULL, `a` string-type NOT NULL, `b` string-type DEFAULT "yes" NOT NULL, `c` string-type, `d` string-type, `e` int-type NOT NULL, `f` int-type DEFAULT 33 NOT NULL, `g` int-type, `h` int-type, PRIMARY KEY(`id`));'
assert db.st.last == "CREATE TABLE IF NOT EXISTS `foo` (`id` serial-type NOT NULL, `a` string-type NOT NULL, `b` string-type DEFAULT 'yes' NOT NULL, `c` string-type, `d` string-type, `e` int-type NOT NULL, `f` int-type DEFAULT 33 NOT NULL, `g` int-type, `h` int-type, PRIMARY KEY(`id`));"

_ := sql db {
select from Foo where e > 5 && c is none && c !is none && h == 2
Expand Down
2 changes: 1 addition & 1 deletion vlib/orm/orm_option_time_test.v
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import time
struct Foo {
id int @[primary; sql: serial]
name string
created_at time.Time @[default: 'CURRENT_TIME']
created_at time.Time @[default: CURRENT_TIME]
updated_at ?string @[sql_type: 'TIMESTAMP']
deleted_at ?time.Time
children []Child @[fkey: 'parent_id']
Expand Down
2 changes: 1 addition & 1 deletion vlib/orm/orm_upsert_test.v
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ mut:
struct DefaultUpsertUser {
mut:
id int @[primary]
status string @[default: '"active"']
status string @[default: 'active']
}

fn test_upsert_updates_existing_row_using_unique_field() {
Expand Down
4 changes: 2 additions & 2 deletions vlib/v/tests/orm_array_field_test.v
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@ struct TaskMetadata {
task_id string
key string
value string
created_at time.Time @[default: 'CURRENT_TIME']
updated_at time.Time @[default: 'CURRENT_TIME']
created_at time.Time @[default: CURRENT_TIME]
updated_at time.Time @[default: CURRENT_TIME]
}

@[table: 'tasks']
Expand Down
30 changes: 15 additions & 15 deletions vlib/v3/tests/orm_join_sql_attr_test.v
Original file line number Diff line number Diff line change
Expand Up @@ -149,16 +149,16 @@ fn main() {
")
assert selector_db_out == ''

mutated_insert_out := orm_join_sql_attr_run(v3_bin, 'orm_mutated_insert_fields', 'import db.sqlite
mutated_insert_out := orm_join_sql_attr_run(v3_bin, 'orm_mutated_insert_fields', "import db.sqlite

struct MutatedInsertUser {
id int @[primary; sql: serial]
mut:
name string @[default: \'"db_default"\']
mut:
name string @[default: 'db_default']
}

fn main() {
mut db := sqlite.connect(\':memory:\') or { panic(err) }
mut db := sqlite.connect(':memory:') or { panic(err) }
defer {
db.close() or {}
}
Expand All @@ -168,7 +168,7 @@ fn main() {
}!

mut user := MutatedInsertUser{}
user.name = \'Ada\'
user.name = 'Ada'
sql db {
insert user into MutatedInsertUser
}!
Expand All @@ -177,24 +177,24 @@ fn main() {
select from MutatedInsertUser
}!
assert rows.len == 1
assert rows[0].name == \'Ada\'
assert rows[0].name == 'Ada'
}
')
")
assert mutated_insert_out == ''

selector_insert_out := orm_join_sql_attr_run(v3_bin, 'orm_selector_insert_value', 'import db.sqlite
selector_insert_out := orm_join_sql_attr_run(v3_bin, 'orm_selector_insert_value', "import db.sqlite

struct SelectorInsertUser {
id int @[primary]
name string @[default: \'"db_default"\']
name string @[default: 'db_default']
}

struct SelectorInsertRequest {
user SelectorInsertUser
}

fn main() {
mut db := sqlite.connect(\':memory:\') or { panic(err) }
mut db := sqlite.connect(':memory:') or { panic(err) }
defer {
db.close() or {}
}
Expand All @@ -206,7 +206,7 @@ fn main() {
request := SelectorInsertRequest{
user: SelectorInsertUser{
id: 1
name: \'Ada\'
name: 'Ada'
}
}
sql db {
Expand All @@ -217,12 +217,12 @@ fn main() {
select from SelectorInsertUser where id == 1
}!
assert first.len == 1
assert first[0].name == \'Ada\'
assert first[0].name == 'Ada'

update_request := SelectorInsertRequest{
user: SelectorInsertUser{
id: 1
name: \'Grace\'
name: 'Grace'
}
}
sql db {
Expand All @@ -233,9 +233,9 @@ fn main() {
select from SelectorInsertUser where id == 1
}!
assert updated.len == 1
assert updated[0].name == \'Grace\'
assert updated[0].name == 'Grace'
}
')
")
assert selector_insert_out == ''

invalid_result := orm_join_sql_attr_compile(v3_bin, 'orm_invalid_static_where', "import db.sqlite
Expand Down