Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
18 changes: 10 additions & 8 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 Expand Up @@ -673,8 +675,8 @@ fn val_to_primitive(val ?string, typ int) !orm.Primitive {
}
// u8
orm.type_idx['u8'] {
data := str.i8()
return orm.Primitive(*unsafe { &u8(&data) })
data := str.i16()
return orm.Primitive(u8(data))
}
// u16
orm.type_idx['u16'] {
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
14 changes: 8 additions & 6 deletions vlib/orm/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,9 +39,11 @@ struct Foo {
- `[sql: 'name']` sets a custom column name for the field
- `[sql_type: 'SQL TYPE']` explicitly sets the type in SQL
- `[sql_select: 'SQL expression']` uses a custom expression in `SELECT` for the field
- `[default: 'raw_sql']` inserts `raw_sql` verbatim in a "DEFAULT" clause when
creating a new table, allowing for SQL functions like `CURRENT_TIME`. For raw strings,
surround `raw_sql` with backticks (\`).
- `[default: 'raw_sql']` sets a "DEFAULT" clause when creating a new table.
String values are quoted, with single quotes escaped as `''`. SQL expressions
are emitted unquoted: function calls like `'gen_random_uuid()'` and paren-less
keywords like `CURRENT_TIMESTAMP` are recognized automatically; for any other
SQL expression use a bare identifier, e.g. `[default: CURRENT_TIMESTAMP]`.

- `[fkey: 'parent_id']` sets foreign key for an field which holds an array
- `[references]` or `[references: 'tablename']` or `[references: 'tablename(field_id)']`
Expand All @@ -61,7 +63,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 Expand Up @@ -243,7 +245,7 @@ If the `id` field is marked as `sql: serial` and `primary`, the insert expressio
returns the database ID of the newly added object. Getting an ID of a newly
added DB row is often useful.

When inserting, `[sql: serial]` fields, and fields with a `[default: 'raw_sql']`
When inserting, `[sql: serial]` fields, and fields with a `[default: ...]`
attribute, are not sent to the database when the value being sent is the default
for the V struct field (e.g., 0 int, or an empty string). This allows the
database to insert default values for auto-increment fields and where you have
Expand Down Expand Up @@ -479,7 +481,7 @@ import db.pg
struct Member {
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']
}

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

fn is_sql_expr(val string) bool {
// SQL expressions written as string defaults, e.g. @[default: 'gen_random_uuid()'].
// Two shapes are detected, so they are emitted unquoted in the DEFAULT clause:
// 1. Function calls: must start with a letter/underscore, have ( and end
// with ), and the part before ( must be a valid SQL identifier.
// 2. Paren-less SQL keywords like CURRENT_TIMESTAMP / CURRENT_DATE.
// Quoting them would silently turn the default into a string literal.
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
}
}
lower := val.trim_space().to_lower_ascii()
return match lower {
'current_date', 'current_time', 'current_timestamp', 'localtime', 'localtimestamp',
'current_user', 'session_user', 'system_user', 'current_role', 'current_catalog',
'current_schema' {
true
}
else {
false
}
}
}

fn tenant_filter_array_primitive_type[T](value []T) int {
if value.len > 0 {
first := value[0]
Expand Down Expand Up @@ -1417,6 +1448,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 +1501,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 +1558,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
34 changes: 29 additions & 5 deletions vlib/orm/orm_null_test.v
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,10 @@ mut:

struct MockDB {
use_num bool
st &MockDBState = unsafe { nil }
db sqlite.DB
mut:
capture_only bool // do not execute DDL against the real sqlite DB
st &MockDBState = unsafe { nil }
db sqlite.DB
}

fn MockDB.new() &MockDB {
Expand Down Expand Up @@ -84,7 +86,9 @@ fn mock_type_from_v(typ int) !string {
fn (db MockDB) create(table orm.Table, fields []orm.TableField) ! {
mut st := db.st
st.last = orm.orm_table_gen(.sqlite, table, '`', true, 0, fields, mock_type_from_v, false)!
return db.db.create(table, fields)
if !db.capture_only {
return db.db.create(table, fields)
}
}

fn (db MockDB) drop(table orm.Table) ! {
Expand All @@ -106,7 +110,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 +143,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 Expand Up @@ -369,3 +373,23 @@ fn test_distinct_select() {
}!
assert db.st.last == 'SELECT DISTINCT `id`, `a`, `b`, `c`, `d`, `e`, `f`, `g`, `h` FROM `foo` WHERE `e` > ?;'
}

@[table: 'default_exprs']
struct DefaultExprs {
mut:
id int @[primary; sql: serial]
uuid string @[default: 'gen_random_uuid()']
ts string @[default: 'CURRENT_TIMESTAMP'; sql_type: 'TIMESTAMP']
note string @[default: "Bob's account"]
state string @[default: 'pending (manual)']
}

fn test_default_sql_expr_and_string_quoting() {
mut db := MockDB.new()
db.capture_only = true

sql db {
create table DefaultExprs
}!
assert db.st.last == "CREATE TABLE IF NOT EXISTS `default_exprs` (`id` serial-type NOT NULL, `uuid` string-type DEFAULT gen_random_uuid() NOT NULL, `ts` TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL, `note` string-type DEFAULT 'Bob''s account' NOT NULL, `state` string-type DEFAULT 'pending (manual)' NOT NULL, PRIMARY KEY(`id`));"
}
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
Loading