orm #
ORM
V has a powerful, concise ORM baked in! Create tables, insert records, manage relationships, all regardless of the DB driver you decide to use.
Nullable
For a nullable column, use an option field. If the field is non-option, the column will be defined with NOT NULL
at table creation.
struct Foo {
notnull string
nullable ?string
}
Attributes
Structs
[table: 'name']
explicitly sets the name of the table for the struct
Fields
[primary]
sets the field as the primary key[unique]
gives the field aUNIQUE
constraint[unique: 'foo']
adds the field to aUNIQUE
group[skip]
or[sql: '-']
field will be skipped[sql: type]
wheretype
is a V type such asint
orf64
[serial]
or[sql: serial]
lets the DB backend choose a column type for an auto-increment field[sql: 'name']
sets a custom column name for the field[sql_type: 'SQL TYPE']
explicitly sets the type in SQL[default: 'raw_sql']
insertsraw_sql
verbatim in a "DEFAULT" clause whencreating a new table, allowing for SQL functions likeCURRENT_TIME
. For raw strings, surroundraw_sql
with backticks (`).[fkey: 'parent_id']
sets foreign key for an field which holds an array
Usage
[!NOTE] > For using the Function Call API for
orm
, please checkFunction Call API
.
Here are a couple example structs showing most of the features outlined above.
import time
@[table: 'foos']
struct Foo {
id int @[primary; sql: serial]
name string
created_at time.Time @[default: 'CURRENT_TIME']
updated_at ?string @[sql_type: 'TIMESTAMP']
deleted_at ?time.Time
children []Child @[fkey: 'parent_id']
}
struct Child {
id int @[primary; sql: serial]
parent_id int
name string
}
To use the ORM, there is a special interface that lets you use the structs and V itself in queries. This interface takes the database instance as an argument.
import db.sqlite
db := sqlite.connect(':memory:')!
sql db {
// query; see below
}!
When you need to reference the table, simply pass the struct itself.
import models.Foo
struct Bar {
id int @[primary; sql: serial]
}
sql db {
create table models.Foo
create table Bar
}!
Create & Drop Tables
You can create and drop tables by passing the struct to create table
and drop table
.
import models.Foo
struct Bar {
id int @[primary; sql: serial]
}
sql db {
create table models.Foo
drop table Bar
}!
Insert Records
To insert a record, create a struct and pass the variable to the query. Again, reference the struct as the table.
foo := Foo{
name: 'abc'
created_at: time.now()
// updated_at defaults to none
// deleted_at defaults to none
children: [
Child{
name: 'abc'
},
Child{
name: 'def'
},
]
}
foo_id := sql db {
insert foo into Foo
}!
If the id
field is marked as sql: serial
and primary
, the insert expression 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']
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 specified a default.
Select
You can select rows from the database by passing the struct as the table, and use V syntax and functions for expressions. Selecting returns an array of the results.
result := sql db {
select from Foo where id == 1
}!
foo := result.first()
result := sql db {
select from Foo where id > 1 && name != 'lasanha' limit 5
}!
result := sql db {
select from Foo where id > 1 order by id
}!
Update
You can update fields in a row using V syntax and functions. Again, pass the struct as the table.
sql db {
update Foo set updated_at = time.now() where name == 'abc' && updated_at is none
}!
Note that is none
and !is none
can be used to select for NULL fields.
Delete
You can delete rows using V syntax and functions. Again, pass the struct as the table.
sql db {
delete from Foo where id > 10
}!
time.Time Fields
It's definitely useful to cast a field as time.Time
so you can use V's built-in time functions; however, this is handled a bit differently than expected in the ORM. time.Time
fields are created as integer columns in the database. Because of this, the usual time functions (current_timestamp
, NOW()
, etc) in SQL do not work as defaults.
Example
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']
}
fn main() {
db := pg.connect(pg.Config{
host: 'localhost'
port: 5432
user: 'user'
password: 'password'
dbname: 'dbname'
})!
defer {
db.close()
}
sql db {
create table Member
}!
new_member := Member{
name: 'John Doe'
}
sql db {
insert new_member into Member
}!
selected_members := sql db {
select from Member where name == 'John Doe' limit 1
}!
john_doe := selected_members.first()
sql db {
update Member set name = 'Hitalo' where id == john_doe.id
}!
}
Function Call API
You can utilize the Function Call API
to work with ORM
. It provides the capability to dynamically construct SQL statements. The Function Call API supports common operations such as Create Table
/Drop Table
/Insert
/Delete
/Update
/Select
, and offers convenient yet powerful features for constructing WHERE
clauses, SET
clauses, SELECT
clauses, and more.
A complete example is available here.
Below, we illustrate its usage through several examples.
1. Define your struct with the same method definitions as before:
@[table: 'sys_users']
struct User {
id int @[primary;serial]
name string
age int
role string
status int
salary int
title string
score int
created_at ?time.Time @[sql_type: 'TIMESTAMP']
}
2. Create a database connection:
mut db := sqlite.connect(':memory:')!
defer { db.close() or {} }
- Create a
QueryBuilder
(which also completes struct mapping):
mut qb := orm.new_query[User](db)
- Create a database table:
qb.create()!
- Insert multiple records into the table:
qb.insert_many(users)!
- Delete records (note:
delete()
must followwhere()
):
qb.where('name = ?','John')!.delete()!
- Query records (you can specify fields of interest via
select
):
// Returns []User with only 'name' populated; other fields are zero values.
only_names := qb.select('name')!.query()!
- Update records (note:
update()
must be placed last):
qb.set('age = ?, title = ?', 71, 'boss')!.where('name = ?','John')!.update()!
- Drop the table:
qb.drop()!
- Chainable method calls: Most Function Call API support chainable calls, allowing easy method chaining:
final_users :=
qb
.drop()!
.create()!
.insert_many(users)!
.set('name = ?', 'haha')!.where('name = ?', 'Tom')!.update()!
.where('age >= ?', 30)!.delete()!
.query()!
- Writing complex nested
WHERE
clauses: The API includes a built-in parser to handle intricateWHERE
clause conditions. For example:
where('created_at IS NULL && ((salary > ? && age < ?) || (role LIKE ?))', 2000, 30, '%employee%')!
Note the use of placeholders ?
. The conditional expressions support logical operators including AND
, OR
, ||
, and &&
.
Constants #
const num64 = [typeof[i64]().idx, typeof[u64]().idx]
const nums = [
typeof[i8]().idx,
typeof[i16]().idx,
typeof[int]().idx,
typeof[u8]().idx,
typeof[u16]().idx,
typeof[u32]().idx,
typeof[bool]().idx,
]
const float = [
typeof[f32]().idx,
typeof[f64]().idx,
]
const type_string = typeof[string]().idx
const serial = -1
const time_ = -2
const enum_ = -3
const type_idx = {
'i8': typeof[i8]().idx
'i16': typeof[i16]().idx
'int': typeof[int]().idx
'i64': typeof[i64]().idx
'u8': typeof[u8]().idx
'u16': typeof[u16]().idx
'u32': typeof[u32]().idx
'u64': typeof[u64]().idx
'f32': typeof[f32]().idx
'f64': typeof[f64]().idx
'bool': typeof[bool]().idx
'string': typeof[string]().idx
}
const string_max_len = 2048
const null_primitive = Primitive(Null{})
fn new_query #
fn new_query[T](conn Connection) &QueryBuilder[T]
new_query create a new query object for struct T
fn orm_select_gen #
fn orm_select_gen(cfg SelectConfig, q string, num bool, qm string, start_pos int, where QueryData) string
Generates an sql select stmt, from universal parameter orm - See SelectConfig q, num, qm, start_pos - see orm_stmt_gen where - See QueryData
fn orm_stmt_gen #
fn orm_stmt_gen(sql_dialect SQLDialect, table string, q string, kind StmtKind, num bool, qm string,
start_pos int, data QueryData, where QueryData) (string, QueryData)
Generates an sql stmt, from universal parameter q - The quotes character, which can be different in every type, so it's variable 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
fn orm_table_gen #
fn orm_table_gen(table string, q string, defaults bool, def_unique_len int, fields []TableField, sql_from_v fn (int) !string,
alternative bool) !string
Generates an sql table stmt, from universal parameter table - Table name 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
interface Connection #
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) !
last_id() int
}
Interfaces gets called from the backend and can be implemented Since the orm supports arrays aswell, they have to be returned too. A row is represented as []Primitive, where the data is connected to the fields of the struct by their index. The indices are mapped with the SelectConfig.field array. This is the mapping for a struct. To have an array, there has to be an array of structs, basically [][]Primitive
Every function without last_id() returns an optional, which returns an error if present last_id returns the last inserted id of the db
type Primitive #
type Primitive = InfixType
| Null
| bool
| f32
| f64
| i16
| i64
| i8
| int
| string
| time.Time
| u16
| u32
| u64
| u8
fn (QueryBuilder[T]) reset #
fn (qb_ &QueryBuilder[T]) reset() &QueryBuilder[T]
reset reset a query object, but keep the connection and table name
fn (QueryBuilder[T]) where #
fn (qb_ &QueryBuilder[T]) where(condition string, params ...Primitive) !&QueryBuilder[T]
where create a where
clause valid token in the condition
include: field's names
, operator
, (
, )
, ?
, AND
, OR
, ||
, &&
, valid operator
incldue: =
, !=
, <>
, >=
, <=
, >
, <
, LIKE
, ILIKE
, IS NULL
, IS NOT NULL
example: where('(a > ? AND b <= ?) OR (c <> ? AND (x = ? OR y = ?))', a, b, c, x, y)
fn (QueryBuilder[T]) order #
fn (qb_ &QueryBuilder[T]) order(order_type OrderType, field string) !&QueryBuilder[T]
order create a order
clause
fn (QueryBuilder[T]) limit #
fn (qb_ &QueryBuilder[T]) limit(limit int) !&QueryBuilder[T]
limit create a limit
clause
fn (QueryBuilder[T]) offset #
fn (qb_ &QueryBuilder[T]) offset(offset int) !&QueryBuilder[T]
offset create a offset
clause
fn (QueryBuilder[T]) select #
fn (qb_ &QueryBuilder[T]) select(fields ...string) !&QueryBuilder[T]
select create a select
clause
fn (QueryBuilder[T]) set #
fn (qb_ &QueryBuilder[T]) set(assign string, values ...Primitive) !&QueryBuilder[T]
set create a set
clause for update
fn (QueryBuilder[T]) query #
fn (qb_ &QueryBuilder[T]) query() ![]T
query start a query and return result in struct T
fn (QueryBuilder[T]) count #
fn (qb_ &QueryBuilder[T]) count() !int
count start a count query and return result
fn (QueryBuilder[T]) insert #
fn (qb_ &QueryBuilder[T]) insert[T](value T) !&QueryBuilder[T]
insert insert a record into the database
fn (QueryBuilder[T]) insert_many #
fn (qb_ &QueryBuilder[T]) insert_many[T](values []T) !&QueryBuilder[T]
insert_many insert records into the database
fn (QueryBuilder[T]) update #
fn (qb_ &QueryBuilder[T]) update() !&QueryBuilder[T]
update update record(s) in the database
fn (QueryBuilder[T]) delete #
fn (qb_ &QueryBuilder[T]) delete() !&QueryBuilder[T]
delete delete record(s) in the database
fn (QueryBuilder[T]) create #
fn (qb_ &QueryBuilder[T]) create() !&QueryBuilder[T]
create create a table
fn (QueryBuilder[T]) drop #
fn (qb_ &QueryBuilder[T]) drop() !&QueryBuilder[T]
drop drop a table
fn (QueryBuilder[T]) last_id #
fn (qb_ &QueryBuilder[T]) last_id() int
last_id returns the last inserted id of the db
enum MathOperationKind #
enum MathOperationKind {
add // +
sub // -
mul // *
div // /
}
enum OperationKind #
enum OperationKind {
neq // !=
eq // ==
gt // >
lt // <
ge // >=
le // <=
orm_like // LIKE
orm_ilike // ILIKE
is_null // IS NULL
is_not_null // IS NOT NULL
}
enum OrderType #
enum OrderType {
asc
desc
}
enum SQLDialect #
enum SQLDialect {
default
sqlite
}
enum StmtKind #
enum StmtKind {
insert
update
delete
}
struct InfixType #
struct InfixType {
pub:
name string
operator MathOperationKind
right Primitive
}
struct Null #
struct Null {}
struct QueryBuilder #
struct QueryBuilder[T] {
pub mut:
meta []TableField
valid_sql_field_names []string
conn Connection
config SelectConfig
data QueryData
where QueryData
}
struct QueryData #
struct QueryData {
pub mut:
fields []string
data []Primitive
types []int
parentheses [][]int
kinds []OperationKind
auto_fields []int
is_and []bool
}
Examples for QueryData in SQL: abc == 3 && b == 'test' => fields[abc, b]; data[3, 'test']; types[index of int, index of string]; kinds[.eq, .eq]; is_and[true]; Every field, data, type & kind of operation in the expr share the same index in the arrays is_and defines how they're addicted to each other either and or or parentheses defines which fields will be inside () auto_fields are indexes of fields where db should generate a value when absent in an insert
struct SelectConfig #
struct SelectConfig {
pub mut:
table string
is_count bool
has_where bool
has_order bool
order string
order_type OrderType
has_limit bool
primary string = 'id' // should be set if primary is different than 'id' and 'has_limit' is false
has_offset bool
fields []string
types []int
}
table - Table name 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 order - Name of the column which will be ordered order_type - Type of order (asc, desc) has_limit - Limits the output data primary - Name of the primary field has_offset - Add an offset to the result fields - Fields to select types - Types to select
struct TableField #
struct TableField {
pub mut:
name string
typ int
nullable bool
default_val string
attrs []VAttribute
is_arr bool
}
- README
- Constants
- fn new_query
- fn orm_select_gen
- fn orm_stmt_gen
- fn orm_table_gen
- interface Connection
- type Primitive
- type QueryBuilder[T]
- enum MathOperationKind
- enum OperationKind
- enum OrderType
- enum SQLDialect
- enum StmtKind
- struct InfixType
- struct Null
- struct QueryBuilder
- struct QueryData
- struct SelectConfig
- struct TableField