Initial project import

This commit is contained in:
Agila Forge Importer committed 2026-07-13 15:20:32 +00:00
commit 23f2d29e32
11957 files changed
+1754577

No files matched your search

+643
View File
@@ -0,0 +1,643 @@
# 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.
Driver authors using the shared SQL generators can target SQLite, PostgreSQL, MySQL, and
H2-backed connections with the built-in ORM dialect helpers.
## 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, except scalar foreign keys declared with `@[references]`, which
default to nullable so omitted relations can be stored as `NULL`.
```v ignore
struct Foo {
notnull string
nullable ?string
}
```
## Attributes
### Structs
- `[table: 'name']` explicitly sets the name of the table for the struct
- `[comment: 'table_comment']` explicitly sets the comment of the table for the struct
- `[index: 'f1, f2, f3']` explicitly sets fields of the table (`f1`, `f2`, `f3`) as indexed
- `[unique_key: 'f1, f2, f3']` adds a composite `UNIQUE` constraint for the listed fields
### Fields
- `[primary]` sets the field as the primary key
- `[unique]` gives the field a `UNIQUE` constraint
- `[unique: 'foo']` adds the field to a `UNIQUE` group
- `[skip]` or `[sql: '-']` field will be skipped
- `[sql: type]` where `type` is a V type such as `int` or `f64`
- `[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
- `[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 (\`).
- `[fkey: 'parent_id']` sets foreign key for an field which holds an array
- `[references]` or `[references: 'tablename']` or `[references: 'tablename(field_id)']`
- `[comment: 'field_comment']` set comment
- `[index]` creates index
## Usage
> [!NOTE]
> For using the Function Call API for `orm`, please check [`Function Call API`](#function-call-api).
Here are a couple example structs showing most of the features outlined above.
```v ignore
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
}
```
`sql_select` is useful for backend-specific read transformations such as:
```v ignore
geom string @[sql_type: 'geometry'; sql_select: 'ST_AsEWKT(geom)']
```
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.
```v ignore
import db.sqlite
db := sqlite.connect(':memory:')!
sql db {
// query; see below
}!
```
### Data Scope
`orm.DB` can wrap an `orm.Connection` with automatic scope filters. Scope filters
are useful for request-level tenancy, soft deletes, and row-level access checks.
```v ignore
mut db := orm.new_db(raw_db, orm.DataScope{
filters: [
orm.QueryFilter{
field: 'tenant_id'
value: orm.Primitive(tenant_id)
mode: .dynamic
},
orm.QueryFilter{
field: 'shop_id'
value: orm.Primitive(shop_id)
mode: .dynamic
},
orm.QueryFilter{
field: 'deleted_at'
operator: .is_null
mode: .dynamic
},
]
})
users := sql db {
select from User
}!
```
`QueryFilter.mode` must be explicitly set to `.static` or `.dynamic` (there is
no default — `.unset` causes a runtime error). Static filters are reserved for
future compiler-generated scope clauses. The runtime `orm.DB` wrapper applies
only filters explicitly marked with `mode: .dynamic`. Invalid dynamic filters
return an error instead of being silently skipped.
Call `db.unscoped()` to return a new `orm.DB` value that skips all scope filters.
Call `db.unscoped('tenant_id')` to skip only selected fields.
> [!TIP]
> This guide uses the built-in `db.sqlite` module. If you want SQLite without first installing
> system-level SQLite development files, the V team also maintains the
> [`sqlite`](https://vpm.vlang.io/packages/sqlite) VPM package.
>
> Install it with `v install sqlite` and change `import db.sqlite` to `import sqlite`.
> The package keeps the same API while bundling SQLite for you.
When you need to reference the table, simply pass the struct itself.
```v ignore
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`.
```v ignore
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.
```v ignore
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
}!
```
You can insert a flat array of records in one statement. Bulk inserts currently
support primitive, enum, and `time.Time` fields. Rows with `serial` or `default`
fields fall back to per-row inserts so each row keeps normal default handling.
```v ignore
users := [
User{
id: 1
name: 'Alice'
},
User{
id: 2
name: 'Bob'
},
]
sql db {
insert users into User
}!
```
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.
### Upsert
`upsert` inserts a row or updates the matching row when one of the table's
primary or unique keys already exists.
```v ignore
foo := Foo{
name: 'abc'
}
sql db {
upsert foo into Foo
}!
```
`upsert` currently supports flat ORM rows with primitive, enum, and `time.Time`
fields.
### 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.
```v ignore
result := sql db {
select from Foo where id == 1
}!
foo := result.first()
```
You can also select a subset of struct fields. The result type stays `[]Foo`;
the selected fields are populated and the rest keep their zero values. Use the
V struct field names here; `@[sql: 'column_name']` and `@[sql_select: 'expr']`
are still applied automatically.
```v ignore
partial := sql db {
select id, name from Foo where id > 1
}!
```
```v ignore
result := sql db {
select from Foo where id > 1 && name != 'lasanha' limit 5
}!
```
```v ignore
result := sql db {
select from Foo where id > 1 order by id desc
}!
```
Dynamic ORM blocks can build `WHERE` and `SET` data conditionally. Commas between
emitted dynamic `where` items are joined with `AND`; use `&&` and `||` inside an
item for explicit boolean conditions.
```v ignore
where_filter := {
if name := req.name {
name == name
},
id == user_id || tenant_id == tenant_id
}
rows := sql db {
dynamic select from Foo where where_filter
}!
update_data := {
name == new_name
}
sql db {
dynamic update Foo set update_data where {
id == user_id || tenant_id == tenant_id
}
}!
```
ORM select expressions also support built-in aggregate functions. `count` keeps
its legacy syntax, while the other aggregates use SQL-like function calls.
```v ignore
total_age := sql db {
select sum(age) from Foo
}!
average_age := sql db {
select avg(age) from Foo where id > 1
}!
lowest_name := sql db {
select min(name) from Foo
}!
highest_created_at := sql db {
select max(created_at) from Foo
}!
```
`sum`, `avg`, `min`, and `max` return options so empty result sets can surface
SQL `NULL` as `none`. `count` continues to return `int`.
### Transactions
ORM transactions work with both `sql tx {}` and the function-call API.
```v ignore
import orm
orm.transaction[int](mut db, fn (mut tx orm.Tx) !int {
user := User{
name: 'Alice'
}
sql tx {
insert user into User
}!
return tx.last_id()
})!
```
For manual control, start a transaction explicitly and commit or roll it back yourself.
```v ignore
import orm
mut tx := orm.begin(mut db)!
sql tx {
update User set name = 'Bob' where id == 1
}!
tx.commit()!
```
Nested transactions use savepoints instead of a second `BEGIN`.
```v ignore
import orm
orm.transaction[int](mut db, fn (mut tx orm.Tx) !int {
tx.transaction[int](fn (mut nested orm.Tx) !int {
sql nested {
delete from User where id == 2
}!
return 0
})!
return 0
})!
```
Transaction helpers use each driver's default transaction mode. v1 does not expose isolation
levels or SQLite begin-mode configuration yet.
### Update
You can update fields in a row using V syntax and functions. Again, pass the struct
as the table.
```v ignore
sql db {
update Foo set updated_at = time.now() where name == 'abc' && updated_at is none
}!
```
You can update multiple rows from an array in one statement by using the array
variable in each assigned value and in the key comparison.
```v ignore
updates := [
User{
id: 1
name: 'Alicia'
},
User{
id: 2
name: 'Robert'
},
]
sql db {
update User set name = updates.name where id == updates.id
}!
```
For a Rails-style full-record save, load a struct, mutate it, then call `orm.save`.
The helper uses the struct primary key, or an `id` field when present, for the
`WHERE` clause and updates the remaining mapped fields automatically.
```v ignore
import orm
mut foo := (sql db {
select from Foo where id == 1
}!).first()
foo.name = 'updated'
foo.updated_at = time.now()
orm.save(db, foo)!
```
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.
```v ignore
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
```v ignore
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](https://github.com/vlang/v/blob/master/vlib/orm/orm_func_test.v).
Below, we illustrate its usage through several examples.
​​1. Define your struct​​ with the same method definitions as before:
```v ignore
@[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​​:
```v ignore
mut db := sqlite.connect(':memory:')!
defer { db.close() or {} }
```
3. Create a `QueryBuilder`​​ (which also completes struct mapping):
```v ignore
mut qb := orm.new_query[User](db)
```
4. Create a database table​​:
```v ignore
qb.create()!
```
5. Insert multiple records​​ into the table:
```v ignore
qb.insert_many(users)!
```
6. Delete records​​ (note: `delete()` must follow `where()`):
```v ignore
qb.where('name = ?','John')!.delete()!
```
7. Query records​​ (you can specify fields of interest via `select`):
```v ignore
// Returns []User with only 'name' populated; other fields are zero values.
only_names := qb.select('name')!.query()!
```
8. Update records​​ (note: `update()` must be placed last):
```v ignore
qb.set('age = ?, title = ?', 71, 'boss')!.where('name = ?','John')!.update()!
```
For a full-record update without spelling out each `set(...)` clause, use `orm.save`:
```v ignore
selected := qb.where('name = ?', 'John')!.query()!
mut john := selected.first()
john.age = 72
john.title = 'lead'
orm.save(db, john)!
```
9. Query aggregate values​​:
```v ignore
total_age := qb.sum('age')!
average_score := qb.avg('score')!
first_name := qb.min('name')!
latest_created_at := qb.max('created_at')!
count := qb.count()!
assert total_age.as_int()? == 42
assert average_score.as_f64()? == 9.5
assert first_name.as_string()? == 'Alice'
assert latest_created_at.as_time()? == created_at
```
`sum`, `avg`, `min`, and `max` return an `AggregateValue`. Use
`as_int()`, `as_f64()`, `as_string()`, or `as_time()` to unwrap the typed
value, or check `has_value` for empty result sets. `count` returns `int`.
To remove duplicate rows from a query, mark it as `DISTINCT` before `query()`:
```v ignore
distinct_roles := qb.select('role')!.distinct()!.query()!
```
9. Drop the table​​:
```v ignore
qb.drop()!
```
10. Chainable method calls​​:
Most Function Call API support chainable calls, allowing easy method chaining:
```v ignore
final_users :=
qb
.drop()!
.create()!
.insert_many(users)!
.set('name = ?', 'haha')!.where('name = ?', 'Tom')!.update()!
.where('age >= ?', 30)!.delete()!
.query()!
```
11. Writing complex nested `WHERE` clauses​​:
The API includes a built-in parser to handle intricate `WHERE` clause conditions. For example:
```v ignore
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 `&&`.
Named arrays can also be passed directly for `IN` clauses:
```v ignore
user_ids := ['1', '2']
users := qb.where('id IN ?', user_ids)!.query()!
```
File diff suppressed because it is too large. Load diff
+156
View File
@@ -0,0 +1,156 @@
// vtest retry: 3
// vtest build: present_sqlite3? && !windows && !sanitize-memory-clang
import orm
import time
import db.sqlite
struct AggregateEntry {
id int @[primary; sql: serial]
age int
score f64
label string
created time.Time
}
fn test_sql_orm_aggregates() {
mut db := sqlite.connect(':memory:') or { panic(err) }
defer {
db.close() or {}
}
sql db {
create table AggregateEntry
}!
first_created := time.unix(1_700_000_000)
second_created := time.unix(1_700_000_100)
third_created := time.unix(1_700_000_200)
entries := [
AggregateEntry{
age: 20
score: 7.5
label: 'bravo'
created: second_created
},
AggregateEntry{
age: 30
score: 8.25
label: 'alpha'
created: first_created
},
AggregateEntry{
age: 40
score: 9.75
label: 'charlie'
created: third_created
},
]
for entry in entries {
sql db {
insert entry into AggregateEntry
}!
}
total_age := sql db {
select sum(age) from AggregateEntry
}!
if value := total_age {
assert value == 90
} else {
assert false
}
average_age := sql db {
select avg(age) from AggregateEntry where age >= 20
}!
if value := average_age {
assert value == 30.0
} else {
assert false
}
min_label := sql db {
select min(label) from AggregateEntry
}!
if value := min_label {
assert value == 'alpha'
} else {
assert false
}
max_created := sql db {
select max(created) from AggregateEntry
}!
if value := max_created {
assert value == third_created
} else {
assert false
}
empty_sum := sql db {
select sum(age) from AggregateEntry where age > 100
}!
assert empty_sum == none
empty_avg := sql db {
select avg(score) from AggregateEntry where age > 100
}!
assert empty_avg == none
}
fn test_query_builder_aggregates() {
mut db := sqlite.connect(':memory:') or { panic(err) }
defer {
db.close() or {}
}
mut qb := orm.new_query[AggregateEntry](db)
qb.create()!
first_created := time.unix(1_800_000_000)
second_created := time.unix(1_800_000_100)
entries := [
AggregateEntry{
age: 10
score: 1.5
label: 'delta'
created: second_created
},
AggregateEntry{
age: 25
score: 2.5
label: 'beta'
created: first_created
},
]
qb.insert_many(entries)!
assert qb.count()! == 2
sum_age := qb.sum('age')!
assert sum_age.has_value
assert sum_age.as_int()? == 35
avg_score := qb.avg('score')!
assert avg_score.has_value
assert avg_score.as_f64()? == 2.0
min_label := qb.min('label')!
assert min_label.has_value
assert min_label.as_string()? == 'beta'
max_created := qb.max('created')!
assert max_created.has_value
assert max_created.as_time()? == second_created
empty_max := qb.where('age > ?', 100)!.max('age')!
assert !empty_max.has_value
if _ := qb.sum('label') {
assert false
} else {
assert err.msg().contains('requires a numeric field')
}
}
+45
View File
@@ -0,0 +1,45 @@
// vtest build: present_sqlite3? && !sanitize-memory-clang
import db.sqlite
struct ComplexWhere {
pub mut:
id int
name string
rank f32
}
fn test_create_without_id_field() {
db := sqlite.connect(':memory:')!
sql db {
create table ComplexWhere
}!
datas := [
ComplexWhere{
id: 0
name: 'test1'
rank: 1.5
},
ComplexWhere{
id: 1
name: 'test2'
rank: 2.5
},
ComplexWhere{
id: 2
name: 'test3'
rank: 3.5
},
]
for data in datas {
sql db {
insert data into ComplexWhere
}!
}
res := sql db {
select from ComplexWhere where name == 'a' && (id > 1 || (rank > 2.5 && rank < 3.33))
} or { assert false, err.msg() }
}
+140
View File
@@ -0,0 +1,140 @@
// vtest retry: 3
import db.sqlite
struct Parent {
id int @[primary; sql: serial]
children []Child @[fkey: 'parent_id']
notes []Note @[fkey: 'owner_id']
}
struct Child {
mut:
id int @[primary; sql: serial]
parent_id int
}
struct Note {
mut:
id int @[primary; sql: serial]
owner_id int
}
struct Entity {
name string @[primary]
description string
}
fn test_create_without_id_field() {
db := sqlite.connect(':memory:')!
sql db {
create table Entity
}!
first := Entity{
name: 'First'
description: 'Such wow! No `id` field'
}
second := Entity{
name: 'Second'
description: 'Such wow! No `id` field again'
}
sql db {
insert first into Entity
insert second into Entity
}!
entities := sql db {
select from Entity
}!
assert entities.len == 2
first_entity := sql db {
select from Entity where name == 'First'
}!
assert first_entity.first().name == 'First'
second_entity := sql db {
select from Entity where name == 'Second'
}!
assert second_entity.first().name == 'Second'
}
fn test_create_only_one_table() {
mut db := sqlite.connect(':memory:')!
sql db {
create table Parent
}!
mut is_child_created := true
mut is_note_created := true
_ := sql db {
select count from Child
} or {
is_child_created = false
0
}
_ := sql db {
select count from Note
} or {
is_note_created = false
0
}
assert is_child_created == false
assert is_note_created == false
}
fn test_drop_only_one_table() {
mut db := sqlite.connect(':memory:')!
sql db {
create table Parent
}!
sql db {
create table Child
}!
sql db {
create table Note
}!
mut is_parent_dropped := false
mut is_child_dropped := false
mut is_note_dropped := false
sql db {
drop table Parent
}!
_ := sql db {
select count from Parent
} or {
is_parent_dropped = true
0
}
_ := sql db {
select count from Child
} or {
is_child_dropped = true
0
}
_ := sql db {
select count from Note
} or {
is_note_dropped = true
0
}
assert is_parent_dropped
assert is_child_dropped == false
assert is_note_dropped == false
}
@@ -0,0 +1,58 @@
// vtest retry: 3
import db.sqlite
struct User {
id int @[primary; sql: serial]
name string
country string
}
// like is an example function for checking that V allows using the `like` keyword as an identifier.
fn like() {}
fn test_like_operator() {
like()
db := sqlite.connect(':memory:')!
sql db {
create table User
}!
luke := User{
name: 'Luke'
country: 'US'
}
sql db {
insert luke into User
}!
james := User{
name: 'James'
country: 'UK'
}
sql db {
insert james into User
}!
lukas := User{
name: 'Lucas'
country: 'DE'
}
sql db {
insert lukas into User
}!
users_with_name_starting_with_letter_l := sql db {
select from User where name like 'L%'
}!
assert users_with_name_starting_with_letter_l.len == 2
assert users_with_name_starting_with_letter_l.filter(it.name.starts_with('L')).len == 2
users_with_name_with_second_letter_a := sql db {
select from User where name like '_a%'
}!
assert users_with_name_with_second_letter_a.len == 1
assert users_with_name_with_second_letter_a.first().name == james.name
}
+505
View File
@@ -0,0 +1,505 @@
// vtest retry: 3
import db.sqlite
@[table: 'dynamic_members']
struct DynamicMember {
mut:
id int @[primary; sql: serial]
name string
email string
age int
status string
}
struct DynamicMemberFilter {
name ?string
}
@[table: 'dynamic_cast_members']
struct DynamicCastMember {
mut:
id int @[primary; sql: serial]
name string
is_required u8
}
@[table: 'dynamic_or_members']
struct DynamicOrMember {
mut:
id int @[primary]
tenant_id int
name string
status string
}
fn test_dynamic_select_with_inline_where_block() {
mut db := sqlite.connect(':memory:')!
defer {
db.close() or { panic(err) }
}
sql db {
create table DynamicMember
}!
first := DynamicMember{
name: 'Alice'
email: 'alice@example.com'
age: 31
status: 'active'
}
second := DynamicMember{
name: 'Bob'
email: 'bob@example.com'
age: 24
status: 'pending'
}
third := DynamicMember{
name: 'Alice'
email: 'alice-two@example.com'
age: 19
status: 'inactive'
}
sql db {
insert first into DynamicMember
insert second into DynamicMember
insert third into DynamicMember
}!
name_filter := 'Alice'
min_age := 30
status_filter := ''
rows := sql db {
dynamic select from DynamicMember where {
if name_filter != '' {
name == name_filter
},
if min_age > 0 {
age >= min_age
},
if status_filter != '' {
status == status_filter
}
} order by id
}!
assert rows.len == 1
assert rows[0].name == 'Alice'
assert rows[0].email == 'alice@example.com'
assert rows[0].age == 31
filter := DynamicMemberFilter{
name: 'Alice'
}
where_expr := {
if name := filter.name {
name == name
}
}
guard_rows := sql db {
dynamic select from DynamicMember where where_expr
}!
assert guard_rows.len == 2
}
fn test_dynamic_update_with_alias_set_block() {
mut db := sqlite.connect(':memory:')!
defer {
db.close() or { panic(err) }
}
sql db {
create table DynamicMember
}!
member := DynamicMember{
name: 'Alice'
email: 'alice@example.com'
age: 31
status: 'active'
}
sql db {
insert member into DynamicMember
}!
id := db.last_id()
next_name := 'Alicia'
next_email := ''
next_status := 'inactive'
update_expr := {
if next_name != '' {
name == next_name
},
if next_email != '' {
email == next_email
},
status == next_status
}
sql db {
dynamic update DynamicMember set update_expr where id == id
}!
rows := sql db {
select from DynamicMember where id == id
}!
assert rows.len == 1
assert rows[0].name == next_name
assert rows[0].email == 'alice@example.com'
assert rows[0].status == next_status
}
fn test_dynamic_update_with_alias_set_block_cast_expr() {
mut db := sqlite.connect(':memory:')!
defer {
db.close() or { panic(err) }
}
sql db {
create table DynamicCastMember
}!
member := DynamicCastMember{
name: 'Alice'
is_required: 0
}
sql db {
insert member into DynamicCastMember
}!
id := db.last_id()
next_name := 'Alicia'
next_required := true
filter := DynamicMemberFilter{
name: next_name
}
update_expr := {
if name := filter.name {
name == name
},
is_required == u8(if next_required { 1 } else { 0 })
}
sql db {
dynamic update DynamicCastMember set update_expr where id == id
}!
rows := sql db {
select from DynamicCastMember where id == id
}!
assert rows.len == 1
assert rows[0].name == next_name
assert rows[0].is_required == 1
}
fn test_dynamic_select_with_in_operator_and_additional_condition() {
mut db := sqlite.connect(':memory:')!
defer {
db.close() or { panic(err) }
}
sql db {
create table DynamicMember
}!
members := [
DynamicMember{
name: 'Alice'
email: 'alice@example.com'
age: 31
status: 'active'
},
DynamicMember{
name: 'Bob'
email: 'bob@example.com'
age: 24
status: 'pending'
},
DynamicMember{
name: 'Charlie'
email: 'charlie@example.com'
age: 29
status: 'active'
},
DynamicMember{
name: 'Diana'
email: 'diana@example.com'
age: 35
status: 'inactive'
},
DynamicMember{
name: 'Eve'
email: 'eve@example.com'
age: 22
status: 'pending'
},
]
for member in members {
sql db {
insert member into DynamicMember
}!
}
valid_names := ['Alice', 'Charlie', 'Eve']
min_age := 25
rows := sql db {
dynamic select from DynamicMember where {
if valid_names.len > 0 {
name in valid_names
},
if min_age > 0 {
age >= min_age
}
} order by id
}!
assert rows.len == 2
assert rows[0].name == 'Alice'
assert rows[0].age == 31
assert rows[1].name == 'Charlie'
assert rows[1].age == 29
}
fn test_dynamic_select_with_explicit_order_by_asc() {
mut db := sqlite.connect(':memory:')!
defer {
db.close() or { panic(err) }
}
sql db {
create table DynamicMember
}!
members := [
DynamicMember{
name: 'Alice'
email: 'alice@example.com'
age: 31
status: 'active'
},
DynamicMember{
name: 'Bob'
email: 'bob@example.com'
age: 19
status: 'pending'
},
DynamicMember{
name: 'Charlie'
email: 'charlie@example.com'
age: 44
status: 'inactive'
},
]
for member in members {
sql db {
insert member into DynamicMember
}!
}
min_age := 19
// vfmt off
rows := sql db {
dynamic select from DynamicMember where {
if min_age > 0 {
age >= min_age
}
} order by age asc limit 2
}!
// vfmt on
assert rows.len == 2
assert rows[0].name == 'Bob'
assert rows[0].age == 19
assert rows[1].name == 'Alice'
assert rows[1].age == 31
}
fn test_dynamic_select_where_block_with_or_expression() {
mut db := sqlite.connect(':memory:')!
defer {
db.close() or { panic(err) }
}
sql db {
create table DynamicOrMember
}!
members := [
DynamicOrMember{
id: 1
tenant_id: 1
name: 'Alice'
status: 'active'
},
DynamicOrMember{
id: 2
tenant_id: 2
name: 'Bob'
status: 'active'
},
DynamicOrMember{
id: 3
tenant_id: 2
name: 'Charlie'
status: 'pending'
},
DynamicOrMember{
id: 4
tenant_id: 3
name: 'Diana'
status: 'active'
},
]
for member in members {
sql db {
insert member into DynamicOrMember
}!
}
active := 'active'
tenant_id := 2
rows := sql db {
dynamic select from DynamicOrMember where {
status == active && (name == 'Alice' || tenant_id == tenant_id)
} order by id
}!
assert rows.map(it.name) == ['Alice', 'Bob']
grouped_rows := sql db {
dynamic select from DynamicOrMember where {
(name == 'Alice' || status == active) && tenant_id == tenant_id
} order by id
}!
assert grouped_rows.map(it.name) == ['Bob']
}
fn test_dynamic_select_where_block_with_or_expression_and_comma_filter() {
mut db := sqlite.connect(':memory:')!
defer {
db.close() or { panic(err) }
}
sql db {
create table DynamicOrMember
}!
members := [
DynamicOrMember{
id: 1
tenant_id: 1
name: 'Alice'
status: 'active'
},
DynamicOrMember{
id: 2
tenant_id: 2
name: 'Bob'
status: 'active'
},
DynamicOrMember{
id: 3
tenant_id: 2
name: 'Charlie'
status: 'pending'
},
DynamicOrMember{
id: 4
tenant_id: 3
name: 'Diana'
status: 'active'
},
]
for member in members {
sql db {
insert member into DynamicOrMember
}!
}
tenant_id := 2
rows := sql db {
dynamic select from DynamicOrMember where {
tenant_id == tenant_id,
name == 'Alice' || status == 'active'
} order by id
}!
assert rows.map(it.name) == ['Bob']
}
fn test_dynamic_update_where_block_with_or_expression() {
mut db := sqlite.connect(':memory:')!
defer {
db.close() or { panic(err) }
}
sql db {
create table DynamicOrMember
}!
members := [
DynamicOrMember{
id: 1
tenant_id: 1
name: 'Alice'
status: 'active'
},
DynamicOrMember{
id: 2
tenant_id: 2
name: 'Bob'
status: 'active'
},
DynamicOrMember{
id: 3
tenant_id: 2
name: 'Charlie'
status: 'pending'
},
DynamicOrMember{
id: 4
tenant_id: 3
name: 'Diana'
status: 'active'
},
]
for member in members {
sql db {
insert member into DynamicOrMember
}!
}
next_status := 'archived'
update_expr := {
status == next_status
}
user_id := 1
tenant_id := 2
sql db {
dynamic update DynamicOrMember set update_expr where {
id == user_id || tenant_id == tenant_id
}
}!
rows := sql db {
select from DynamicOrMember order by id
}!
assert rows.map(it.status) == ['archived', 'archived', 'archived', 'active']
}
+442
View File
@@ -0,0 +1,442 @@
// vtest retry: 3
// vtest build: present_sqlite3? && !windows && !sanitize-memory-clang
import db.sqlite
// Basic embedded struct
struct Coordinates {
latitude f64
longitude f64
}
// Struct with embedded Coordinates
struct Location {
Coordinates
id int @[primary; sql: serial]
name string
}
// Another embedded struct with different types
struct Metadata {
created_by string
version int
is_active bool
}
// Struct with multiple fields and embedded struct
struct Document {
Metadata
id int @[primary; sql: serial]
title string
content string
}
// Struct embedding another struct that has various field types
struct PersonInfo {
first_name string
last_name string
age int
}
struct Employee {
PersonInfo
id int @[primary; sql: serial]
department string
salary f64
}
// Test basic embedded struct insert and select
fn test_embedded_struct_insert_and_select() {
db := sqlite.connect(':memory:')!
sql db {
create table Location
}!
loc := Location{
name: 'Paris'
latitude: 48.8566
longitude: 2.3522
}
sql db {
insert loc into Location
}!
locations := sql db {
select from Location
}!
assert locations.len == 1
fetched := locations.first()
assert fetched.name == 'Paris'
assert fetched.latitude == 48.8566
assert fetched.longitude == 2.3522
}
// Test multiple inserts with embedded struct
fn test_embedded_struct_multiple_inserts() {
db := sqlite.connect(':memory:')!
sql db {
create table Location
}!
locations_to_insert := [
Location{
name: 'New York'
latitude: 40.7128
longitude: -74.0060
},
Location{
name: 'Tokyo'
latitude: 35.6762
longitude: 139.6503
},
Location{
name: 'Sydney'
latitude: -33.8688
longitude: 151.2093
},
]
for loc in locations_to_insert {
sql db {
insert loc into Location
}!
}
locations := sql db {
select from Location
}!
assert locations.len == 3
// Verify each location
assert locations[0].name == 'New York'
assert locations[0].latitude == 40.7128
assert locations[0].longitude == -74.0060
assert locations[1].name == 'Tokyo'
assert locations[1].latitude == 35.6762
assert locations[1].longitude == 139.6503
assert locations[2].name == 'Sydney'
assert locations[2].latitude == -33.8688
assert locations[2].longitude == 151.2093
}
// Test embedded struct with different field types (string, int, bool)
fn test_embedded_struct_various_types() {
db := sqlite.connect(':memory:')!
sql db {
create table Document
}!
doc := Document{
title: 'Test Document'
content: 'This is the content'
created_by: 'admin'
version: 1
is_active: true
}
sql db {
insert doc into Document
}!
docs := sql db {
select from Document
}!
assert docs.len == 1
fetched := docs.first()
assert fetched.title == 'Test Document'
assert fetched.content == 'This is the content'
assert fetched.created_by == 'admin'
assert fetched.version == 1
assert fetched.is_active == true
}
// Test embedded struct with bool false value
fn test_embedded_struct_bool_false() {
db := sqlite.connect(':memory:')!
sql db {
create table Document
}!
doc := Document{
title: 'Inactive Doc'
content: 'Content here'
created_by: 'user'
version: 2
is_active: false
}
sql db {
insert doc into Document
}!
docs := sql db {
select from Document
}!
assert docs.len == 1
assert docs.first().is_active == false
}
// Test employee with person info embedded
fn test_employee_with_embedded_person_info() {
db := sqlite.connect(':memory:')!
sql db {
create table Employee
}!
emp := Employee{
first_name: 'John'
last_name: 'Doe'
age: 30
department: 'Engineering'
salary: 75000.50
}
sql db {
insert emp into Employee
}!
employees := sql db {
select from Employee
}!
assert employees.len == 1
fetched := employees.first()
assert fetched.first_name == 'John'
assert fetched.last_name == 'Doe'
assert fetched.age == 30
assert fetched.department == 'Engineering'
assert fetched.salary == 75000.50
}
// Test selecting specific records with where clause on non-embedded field
fn test_embedded_struct_select_with_where() {
db := sqlite.connect(':memory:')!
sql db {
create table Employee
}!
employees_to_insert := [
Employee{
first_name: 'Alice'
last_name: 'Smith'
age: 25
department: 'HR'
salary: 50000.0
},
Employee{
first_name: 'Bob'
last_name: 'Jones'
age: 35
department: 'Engineering'
salary: 80000.0
},
Employee{
first_name: 'Carol'
last_name: 'Williams'
age: 28
department: 'Engineering'
salary: 70000.0
},
]
for emp in employees_to_insert {
sql db {
insert emp into Employee
}!
}
// Select by department
engineers := sql db {
select from Employee where department == 'Engineering'
}!
assert engineers.len == 2
assert engineers[0].first_name == 'Bob'
assert engineers[1].first_name == 'Carol'
// Verify all embedded fields are correctly fetched
for eng in engineers {
assert eng.last_name.len > 0
assert eng.age > 0
assert eng.salary > 0
}
}
// Test that embedded struct fields are stored with correct column names
fn test_embedded_struct_field_values_integrity() {
db := sqlite.connect(':memory:')!
sql db {
create table Location
}!
// Insert with specific values that could be confused
loc := Location{
name: 'Test'
latitude: 12.345
longitude: 67.890
}
sql db {
insert loc into Location
}!
locations := sql db {
select from Location
}!
fetched := locations.first()
// Ensure latitude and longitude are not swapped
assert fetched.latitude == 12.345
assert fetched.longitude == 67.890
// Ensure they're not equal (which would indicate a bug)
assert fetched.latitude != fetched.longitude
}
// Test update on non-embedded fields preserves embedded field values
fn test_update_preserves_embedded_fields() {
db := sqlite.connect(':memory:')!
sql db {
create table Location
}!
loc := Location{
name: 'Original'
latitude: 10.0
longitude: 20.0
}
sql db {
insert loc into Location
}!
// Update only the name
sql db {
update Location set name = 'Updated' where id == 1
}!
locations := sql db {
select from Location where id == 1
}!
fetched := locations.first()
assert fetched.name == 'Updated'
// Embedded fields should be preserved
assert fetched.latitude == 10.0
assert fetched.longitude == 20.0
}
// Test delete with embedded struct
fn test_delete_with_embedded_struct() {
db := sqlite.connect(':memory:')!
sql db {
create table Location
}!
loc1 := Location{
name: 'ToDelete'
latitude: 1.0
longitude: 2.0
}
loc2 := Location{
name: 'ToKeep'
latitude: 3.0
longitude: 4.0
}
sql db {
insert loc1 into Location
insert loc2 into Location
}!
sql db {
delete from Location where name == 'ToDelete'
}!
locations := sql db {
select from Location
}!
assert locations.len == 1
assert locations.first().name == 'ToKeep'
assert locations.first().latitude == 3.0
assert locations.first().longitude == 4.0
}
// Test empty/zero values in embedded struct
fn test_embedded_struct_zero_values() {
db := sqlite.connect(':memory:')!
sql db {
create table Employee
}!
emp := Employee{
first_name: ''
last_name: ''
age: 0
department: 'Test'
salary: 0.0
}
sql db {
insert emp into Employee
}!
employees := sql db {
select from Employee
}!
assert employees.len == 1
fetched := employees.first()
assert fetched.first_name == ''
assert fetched.last_name == ''
assert fetched.age == 0
assert fetched.salary == 0.0
}
// Test that the struct equality works correctly with embedded fields
fn test_embedded_struct_equality() {
db := sqlite.connect(':memory:')!
sql db {
create table Location
}!
original := Location{
name: 'TestCity'
latitude: 45.0
longitude: 90.0
}
sql db {
insert original into Location
}!
locations := sql db {
select from Location
}!
fetched := locations.first()
// Compare all fields individually
assert fetched.name == original.name
assert fetched.latitude == original.latitude
assert fetched.longitude == original.longitude
}
+168
View File
@@ -0,0 +1,168 @@
// vtest retry: 3
import db.sqlite
struct Person {
id int @[primary; sql: serial]
age int
brothers []Brother @[fkey: 'person_id']
sisters []Sister @[fkey: 'person_id']
field_after_fkeys string
}
struct Brother {
id int @[primary; sql: serial]
person_id int
name string
}
struct Sister {
id int @[primary; sql: serial]
person_id int
name string
}
fn test_field_after_fkeys() {
db := sqlite.connect(':memory:') or { panic(err) }
sql db {
create table Brother
create table Sister
create table Person
}!
person := Person{
age: 21
brothers: [Brother{
name: 'aaa'
}, Brother{
name: 'bbb'
}]
sisters: [Sister{
name: 'ccc'
}, Sister{
name: 'ddd'
}]
field_after_fkeys: 'eee'
}
sql db {
insert person into Person
}!
persons := sql db {
select from Person
}!
assert persons[0].age == 21
assert persons[0].field_after_fkeys == 'eee'
}
@[table: 'foos']
struct Foo {
id int @[primary; sql: serial]
name string
children []Child @[fkey: 'parent_id']
}
struct Child {
id int @[primary; sql: serial]
parent_id int
name string
}
fn test_fkey_insert_as_assignment_expr() {
db := sqlite.connect(':memory:')!
sql db {
create table Foo
create table Child
}!
foo := Foo{
name: 'abc'
children: [
Child{
name: 'abc'
},
Child{
name: 'def'
},
]
}
// use insert as an assigment expr
_ := sql db {
insert foo into Foo
}!
result := sql db {
select from Foo where id == 1
}!
assert result[0].children.len == 2
res := sql db {
select from Child
}!
assert res.len == 2
}
struct Foo2 {
id int @[primary; sql: serial]
name string
children []Child2 @[fkey: 'parent_id']
}
struct Child2 {
id int @[primary; sql: serial]
parent_id int
name string
bar ?Bar2 @[fkey: 'child_id']
}
struct Bar2 {
id int @[primary; sql: serial]
child_id int
name string
}
fn test_double_fkey_insert() {
db := sqlite.connect(':memory:')!
sql db {
create table Foo2
create table Child2
create table Bar2
}!
child_one := Child2{
name: 'abc'
}
child_two := Child2{
name: 'def'
}
bar_one := Bar2{
id: 0
name: 'name'
}
foo := Foo2{
name: 'abc'
children: [
child_one,
child_two,
]
}
_ := sql db {
insert foo into Foo2
}!
result := sql db {
select from Foo2 where id == 1
}!
assert result[0].children.len == 2
res := sql db {
select from Child2
}!
assert res.len == 2
}
+49
View File
@@ -0,0 +1,49 @@
// vtest retry: 3
import db.sqlite
struct User {
id int @[primary; sql: serial]
name string
age int
}
fn get_acceptable_age() int {
return 21
}
fn test_fn_calls() {
mut db := sqlite.connect(':memory:') or { panic(err) }
sql db {
create table User
}!
first_user := User{
name: 'first'
age: 25
}
second_user := User{
name: 'second'
age: 14
}
sql db {
insert first_user into User
insert second_user into User
}!
users_with_acceptable_age := sql db {
select from User where age >= get_acceptable_age()
}!
assert users_with_acceptable_age.len == 1
assert users_with_acceptable_age.first().name == 'first'
users_with_non_acceptable_age := sql db {
select from User where age < get_acceptable_age()
}!
assert users_with_non_acceptable_age.len == 1
assert users_with_non_acceptable_age.first().name == 'second'
}
+789
View File
@@ -0,0 +1,789 @@
// vtest retry: 3
import orm
fn test_orm_stmt_gen_update() {
table := orm.Table{
name: 'Test'
}
query_and, _ := orm.orm_stmt_gen(.default, table, "'", .update, true, '?', 0, orm.QueryData{
fields: ['test', 'a']
data: []
types: []
kinds: []
}, orm.QueryData{
fields: ['id', 'name']
data: []
types: []
kinds: [.ge, .eq]
is_and: [true]
})
assert query_and == "UPDATE 'Test' SET 'test' = ?0, 'a' = ?1 WHERE 'id' >= ?2 AND 'name' = ?3;"
query_or, _ := orm.orm_stmt_gen(.default, table, "'", .update, true, '?', 0, orm.QueryData{
fields: ['test', 'a']
data: []
types: []
kinds: []
}, orm.QueryData{
fields: ['id', 'name']
data: []
types: []
kinds: [.ge, .eq]
is_and: [false]
})
assert query_or == "UPDATE 'Test' SET 'test' = ?0, 'a' = ?1 WHERE 'id' >= ?2 OR 'name' = ?3;"
}
fn test_orm_stmt_gen_insert() {
table := orm.Table{
name: 'Test'
}
query, _ := orm.orm_stmt_gen(.default, table, "'", .insert, true, '?', 0, orm.QueryData{
fields: ['test', 'a']
data: []
types: []
kinds: []
}, orm.QueryData{})
assert query == "INSERT INTO 'Test' ('test', 'a') VALUES (?0, ?1);"
}
fn test_orm_stmt_gen_bulk_insert() {
table := orm.Table{
name: 'Test'
}
query, converted := orm.orm_stmt_gen(.default, table, "'", .insert, true, '?', 0, orm.QueryData{
fields: ['name', 'age']
data: [orm.Primitive('Alice'), orm.Primitive(25), orm.Primitive('Bob'), orm.Primitive(30)]
batch_rows: 2
}, orm.QueryData{})
assert query == "INSERT INTO 'Test' ('name', 'age') VALUES (?0, ?1), (?2, ?3);"
assert converted.data.len == 4
pg_query, _ := orm.orm_stmt_gen(.pg, table, '"', .insert, true, '$', 1, orm.QueryData{
fields: ['name', 'age']
data: [orm.Primitive('Alice'), orm.Primitive(25), orm.Primitive('Bob'), orm.Primitive(30)]
batch_rows: 2
}, orm.QueryData{})
assert pg_query == 'INSERT INTO "Test" ("name", "age") VALUES ($1, $2), ($3, $4);'
mysql_query, _ := orm.orm_stmt_gen(.mysql, table, '`', .insert, false, '?', 1, orm.QueryData{
fields: ['name', 'age']
data: [orm.Primitive('Alice'), orm.Primitive(25), orm.Primitive('Bob'), orm.Primitive(30)]
batch_rows: 2
}, orm.QueryData{})
assert mysql_query == 'INSERT INTO `Test` (`name`, `age`) VALUES (?, ?), (?, ?);'
}
fn test_orm_stmt_gen_bulk_update() {
table := orm.Table{
name: 'Test'
}
query, _ := orm.orm_stmt_gen(.default, table, "'", .update, true, '?', 0, orm.QueryData{
fields: ['name', 'age']
data: [orm.Primitive(1), orm.Primitive('Alice'), orm.Primitive(2), orm.Primitive('Bob'),
orm.Primitive(1), orm.Primitive(25), orm.Primitive(2), orm.Primitive(30)]
batch_rows: 2
batch_key: 'id'
}, orm.QueryData{
fields: ['id', 'id']
data: [orm.Primitive(1), orm.Primitive(2)]
kinds: [.eq, .eq]
is_and: [false]
})
assert query == "UPDATE 'Test' SET 'name' = CASE 'id' WHEN ?0 THEN ?1 WHEN ?2 THEN ?3 ELSE 'name' END, 'age' = CASE 'id' WHEN ?4 THEN ?5 WHEN ?6 THEN ?7 ELSE 'age' END WHERE 'id' = ?8 OR 'id' = ?9;"
pg_query, _ := orm.orm_stmt_gen(.pg, table, '"', .update, true, '$', 1, orm.QueryData{
fields: ['name']
data: [orm.Primitive(1), orm.Primitive('Alice'), orm.Primitive(2), orm.Primitive('Bob')]
batch_rows: 2
batch_key: 'id'
}, orm.QueryData{
fields: ['id', 'id']
data: [orm.Primitive(1), orm.Primitive(2)]
kinds: [.eq, .eq]
is_and: [false]
})
assert pg_query == 'UPDATE "Test" SET "name" = CASE "id" WHEN $1 THEN $2 WHEN $3 THEN $4 ELSE "name" END WHERE "id" = $5 OR "id" = $6;'
mysql_query, _ := orm.orm_stmt_gen(.mysql, table, '`', .update, false, '?', 1, orm.QueryData{
fields: ['name']
data: [orm.Primitive(1), orm.Primitive('Alice'), orm.Primitive(2), orm.Primitive('Bob')]
batch_rows: 2
batch_key: 'id'
}, orm.QueryData{
fields: ['id', 'id']
data: [orm.Primitive(1), orm.Primitive(2)]
kinds: [.eq, .eq]
is_and: [false]
})
assert mysql_query == 'UPDATE `Test` SET `name` = CASE `id` WHEN ? THEN ? WHEN ? THEN ? ELSE `name` END WHERE `id` = ? OR `id` = ?;'
}
fn test_orm_stmt_gen_insert_default_values_pg() {
table := orm.Table{
name: 'Test'
}
query, converted := orm.orm_stmt_gen(.pg, table, "'", .insert, true, '$', 1, orm.QueryData{
fields: ['id', 'example']
data: [orm.Primitive(0), orm.Primitive('')]
types: []
kinds: []
auto_fields: [0, 1]
}, orm.QueryData{})
assert query == "INSERT INTO 'Test' DEFAULT VALUES;"
assert converted.fields.len == 0
assert converted.data.len == 0
}
fn test_orm_stmt_gen_insert_default_values_mysql() {
table := orm.Table{
name: 'Test'
}
query, converted := orm.orm_stmt_gen(.mysql, table, '`', .insert, false, '?', 1, orm.QueryData{
fields: ['id']
data: [orm.Primitive(0)]
auto_fields: [0]
}, orm.QueryData{})
assert query == 'INSERT INTO `Test` () VALUES ();'
assert converted.fields.len == 0
assert converted.data.len == 0
bulk_query, bulk_converted := orm.orm_stmt_gen(.mysql, table, '`', .insert, false, '?', 1, orm.QueryData{
fields: ['id']
data: [orm.Primitive(0), orm.Primitive(0), orm.Primitive(0)]
auto_fields: [0]
batch_rows: 3
}, orm.QueryData{})
assert bulk_query == 'INSERT INTO `Test` () VALUES (), (), ();'
assert bulk_converted.fields.len == 0
assert bulk_converted.data.len == 0
}
fn test_orm_stmt_gen_h2_insert_default_values() {
table := orm.Table{
name: 'Test'
}
query, _ :=
orm.orm_stmt_gen(.h2, table, '"', .insert, false, '?', 1, orm.QueryData{}, orm.QueryData{})
assert query == 'INSERT INTO "Test" DEFAULT VALUES;'
}
fn test_orm_stmt_gen_delete() {
table := orm.Table{
name: 'Test'
}
query_and, _ := orm.orm_stmt_gen(.default, table, "'", .delete, true, '?', 0, orm.QueryData{
fields: ['test', 'a']
data: []
types: []
kinds: []
}, orm.QueryData{
fields: ['id', 'name']
data: []
types: []
kinds: [.ge, .eq]
is_and: [true]
})
assert query_and == "DELETE FROM 'Test' WHERE 'id' >= ?0 AND 'name' = ?1;"
query_or, _ := orm.orm_stmt_gen(.default, table, "'", .delete, true, '?', 0, orm.QueryData{
fields: ['test', 'a']
data: []
types: []
kinds: []
}, orm.QueryData{
fields: ['id', 'name']
data: []
types: []
kinds: [.ge, .eq]
is_and: [false]
})
assert query_or == "DELETE FROM 'Test' WHERE 'id' >= ?0 OR 'name' = ?1;"
}
fn test_orm_stmt_gen_where_unary_before_array() {
table := orm.Table{
name: 'Test'
}
query, _ := orm.orm_stmt_gen(.default, table, "'", .delete, true, '?', 0, orm.QueryData{}, orm.QueryData{
fields: ['deleted_at', 'tenant_id']
data: [orm.Primitive([orm.Primitive(1), orm.Primitive(2)])]
kinds: [.is_null, .in]
is_and: [true]
})
assert query == "DELETE FROM 'Test' WHERE 'deleted_at' IS NULL AND 'tenant_id' IN (?0, ?1);"
}
fn test_orm_stmt_gen_where_typed_array() {
table := orm.Table{
name: 'Test'
}
query, _ := orm.orm_stmt_gen(.default, table, "'", .delete, true, '?', 0, orm.QueryData{}, orm.QueryData{
fields: ['tenant_id']
data: [orm.Primitive([1, 2])]
kinds: [.in]
})
assert query == "DELETE FROM 'Test' WHERE 'tenant_id' IN (?0, ?1);"
}
fn get_select_fields() []string {
return ['id', 'test', 'abc']
}
fn test_orm_select_gen() {
query := orm.orm_select_gen(orm.SelectConfig{
table: orm.Table{
name: 'test_table'
}
fields: get_select_fields()
}, "'", true, '?', 0, orm.QueryData{})
assert query == "SELECT 'id', 'test', 'abc' FROM 'test_table';"
}
fn test_orm_select_gen_with_limit() {
query := orm.orm_select_gen(orm.SelectConfig{
table: orm.Table{
name: 'test_table'
}
fields: get_select_fields()
has_limit: true
}, "'", true, '?', 0, orm.QueryData{})
assert query == "SELECT 'id', 'test', 'abc' FROM 'test_table' LIMIT ?0;"
}
fn test_orm_select_gen_with_where() {
query := orm.orm_select_gen(orm.SelectConfig{
table: orm.Table{
name: 'test_table'
}
fields: get_select_fields()
has_where: true
}, "'", true, '?', 0, orm.QueryData{
fields: ['abc', 'test']
kinds: [.eq, .gt]
is_and: [true]
})
assert query == "SELECT 'id', 'test', 'abc' FROM 'test_table' WHERE 'abc' = ?0 AND 'test' > ?1;"
}
fn test_orm_select_gen_preserves_embedded_column_dot() {
query := orm.orm_select_gen(orm.SelectConfig{
table: orm.Table{
name: 'test_table'
}
fields: get_select_fields()
has_where: true
}, "'", true, '?', 0, orm.QueryData{
fields: ['Coordinates.latitude']
kinds: [.eq]
})
assert query == "SELECT 'id', 'test', 'abc' FROM 'test_table' WHERE 'Coordinates.latitude' = ?0;"
}
fn test_orm_select_gen_with_order() {
query := orm.orm_select_gen(orm.SelectConfig{
table: orm.Table{
name: 'test_table'
}
fields: get_select_fields()
has_order: true
order_type: .desc
}, "'", true, '?', 0, orm.QueryData{})
assert query == "SELECT 'id', 'test', 'abc' FROM 'test_table' ORDER BY '' DESC;"
}
fn test_orm_select_gen_with_offset() {
query := orm.orm_select_gen(orm.SelectConfig{
table: orm.Table{
name: 'test_table'
}
fields: get_select_fields()
has_offset: true
}, "'", true, '?', 0, orm.QueryData{})
assert query == "SELECT 'id', 'test', 'abc' FROM 'test_table' OFFSET ?0;"
}
fn test_orm_select_gen_with_all() {
query := orm.orm_select_gen(orm.SelectConfig{
table: orm.Table{
name: 'test_table'
}
fields: get_select_fields()
has_limit: true
has_order: true
order_type: .desc
has_offset: true
has_where: true
}, "'", true, '?', 0, orm.QueryData{
fields: ['abc', 'test']
kinds: [.eq, .gt]
is_and: [true]
})
assert query == "SELECT 'id', 'test', 'abc' FROM 'test_table' WHERE 'abc' = ?0 AND 'test' > ?1 ORDER BY '' DESC LIMIT ?2 OFFSET ?3;"
}
fn test_orm_select_gen_with_distinct() {
query := orm.orm_select_gen(orm.SelectConfig{
table: orm.Table{
name: 'test_table'
}
fields: get_select_fields()
has_distinct: true
}, "'", true, '?', 0, orm.QueryData{})
assert query == "SELECT DISTINCT 'id', 'test', 'abc' FROM 'test_table';"
}
fn test_orm_select_gen_with_distinct_and_where() {
query := orm.orm_select_gen(orm.SelectConfig{
table: orm.Table{
name: 'test_table'
}
fields: get_select_fields()
has_distinct: true
has_where: true
}, "'", true, '?', 0, orm.QueryData{
fields: ['abc']
kinds: [.eq]
is_and: []
})
assert query == "SELECT DISTINCT 'id', 'test', 'abc' FROM 'test_table' WHERE 'abc' = ?0;"
}
fn test_orm_select_gen_with_sum() {
query := orm.orm_select_gen(orm.SelectConfig{
table: orm.Table{
name: 'test_table'
}
aggregate_kind: .sum
aggregate_field: 'age'
fields: ['age']
types: [orm.type_idx['int']]
}, "'", true, '?', 0, orm.QueryData{})
assert query == "SELECT SUM('age') FROM 'test_table';"
}
fn test_orm_select_gen_with_avg_and_where() {
query := orm.orm_select_gen(orm.SelectConfig{
table: orm.Table{
name: 'test_table'
}
aggregate_kind: .avg
aggregate_field: 'score'
fields: ['score']
types: [orm.type_idx['f64']]
has_where: true
}, "'", true, '?', 0, orm.QueryData{
fields: ['abc']
kinds: [.eq]
is_and: []
})
assert query == "SELECT AVG('score') FROM 'test_table' WHERE 'abc' = ?0;"
}
fn test_orm_select_gen_with_count() {
query := orm.orm_select_gen(orm.SelectConfig{
table: orm.Table{
name: 'test_table'
}
aggregate_kind: .count
types: [orm.type_idx['int']]
}, "'", true, '?', 0, orm.QueryData{})
assert query == "SELECT COUNT(*) FROM 'test_table';"
}
fn test_orm_table_gen() {
table := orm.Table{
name: 'test_table'
}
query := orm.orm_table_gen(.default, table, "'", true, 0, [
orm.TableField{
name: 'id'
typ: typeof[int]().idx
default_val: '10'
nullable: true
attrs: [
VAttribute{
name: 'primary'
},
VAttribute{
name: 'sql'
has_arg: true
arg: 'serial'
kind: .plain
},
]
},
orm.TableField{
name: 'test'
typ: typeof[string]().idx
nullable: true
},
orm.TableField{
name: 'abc'
typ: typeof[i64]().idx
nullable: true
default_val: '6754'
},
], 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(.default, table, "'", true, 0, [
orm.TableField{
name: 'id'
typ: typeof[int]().idx
nullable: true
default_val: '10'
attrs: [
VAttribute{
name: 'primary'
},
VAttribute{
name: 'sql'
has_arg: true
arg: 'serial'
kind: .plain
},
]
},
orm.TableField{
name: 'test'
typ: typeof[string]().idx
nullable: true
},
orm.TableField{
name: 'abc'
typ: typeof[i64]().idx
nullable: true
default_val: '6754'
},
], 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(.default, table, "'", true, 0, [
orm.TableField{
name: 'id'
typ: typeof[int]().idx
nullable: true
default_val: '10'
attrs: [
VAttribute{
name: 'primary'
},
VAttribute{
name: 'sql'
has_arg: true
arg: 'serial'
kind: .plain
},
]
},
orm.TableField{
name: 'test'
typ: typeof[string]().idx
attrs: [
VAttribute{
name: 'unique'
},
]
},
orm.TableField{
name: 'abc'
typ: typeof[i64]().idx
default_val: '6754'
},
], 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(.default, table, "'", true, 0, [
orm.TableField{
name: 'id'
typ: typeof[int]().idx
nullable: true
default_val: '10'
attrs: [
VAttribute{
name: 'primary'
},
VAttribute{
name: 'sql'
has_arg: true
arg: 'serial'
kind: .plain
},
]
},
orm.TableField{
name: 'test'
typ: typeof[string]().idx
nullable: true
attrs: [
VAttribute{
name: 'unique'
has_arg: true
arg: 'test'
kind: .string
},
]
},
orm.TableField{
name: 'abc'
typ: typeof[i64]().idx
nullable: true
default_val: '6754'
attrs: [
VAttribute{
name: 'unique'
has_arg: true
arg: 'test'
kind: .string
},
]
},
], sql_type_from_v, false) or { panic(err) }
assert mult_unique_query == "CREATE TABLE IF NOT EXISTS 'test_table' ('id' SERIAL DEFAULT 10, 'test' TEXT, 'abc' INT64 DEFAULT 6754, /* test */UNIQUE('test', 'abc'), PRIMARY KEY('id'));"
table_with_unique := orm.Table{
name: 'test_table'
attrs: [
VAttribute{
name: 'unique_key'
has_arg: true
arg: 'test, abc'
kind: .string
},
]
}
table_unique_query := orm.orm_table_gen(.default, table_with_unique, "'", true, 0, [
orm.TableField{
name: 'id'
typ: typeof[int]().idx
nullable: true
default_val: '10'
attrs: [
VAttribute{
name: 'primary'
},
VAttribute{
name: 'sql'
has_arg: true
arg: 'serial'
kind: .plain
},
]
},
orm.TableField{
name: 'test'
typ: typeof[string]().idx
nullable: true
},
orm.TableField{
name: 'abc'
typ: typeof[i64]().idx
nullable: true
default_val: '6754'
},
], sql_type_from_v, false) or { panic(err) }
assert table_unique_query == "CREATE TABLE IF NOT EXISTS 'test_table' ('id' SERIAL DEFAULT 10, 'test' TEXT, 'abc' INT64 DEFAULT 6754, UNIQUE('test', 'abc'), PRIMARY KEY('id'));"
}
fn test_orm_table_gen_h2() {
table := orm.Table{
name: 'test_table'
attrs: [
VAttribute{
name: 'comment'
has_arg: true
arg: 'test table'
kind: .string
},
]
}
query := orm.orm_table_gen(.h2, table, '"', true, 0, [
orm.TableField{
name: 'id'
typ: typeof[int]().idx
attrs: [
VAttribute{
name: 'primary'
},
VAttribute{
name: 'sql'
has_arg: true
arg: 'serial'
kind: .plain
},
]
},
orm.TableField{
name: 'name'
typ: typeof[string]().idx
attrs: [
VAttribute{
name: 'comment'
has_arg: true
arg: 'display name'
kind: .string
},
VAttribute{
name: 'index'
},
]
},
], sql_type_from_v, false) or { panic(err) }
assert query == 'CREATE TABLE IF NOT EXISTS "test_table" ("id" SERIAL NOT NULL, "name" TEXT NOT NULL, PRIMARY KEY("id"));\nCOMMENT ON TABLE "test_table" IS \'test table\';\nCOMMENT ON COLUMN "test_table"."name" IS \'display name\';\nCREATE INDEX "idx_test_table" ON "test_table" ("name");'
}
fn reset_tenant_filter() {
orm.configure_tenant_filter(enabled: false, field_name: 'tenant_id')
orm.clear_current_tenant_id()
}
fn test_apply_tenant_filter_appends_default_field() {
defer {
reset_tenant_filter()
}
reset_tenant_filter()
orm.configure_tenant_filter(field_name: 'tenant_id')
orm.set_current_tenant_id(77)
where := orm.QueryData{
fields: ['id']
data: [orm.Primitive(int(11))]
kinds: [.eq]
}
filtered := orm.apply_tenant_filter(orm.Table{
name: 'test_table'
}, where)
assert filtered.fields == ['id', 'tenant_id']
assert filtered.data == [orm.Primitive(int(11)), orm.Primitive(int(77))]
assert filtered.kinds == [.eq, .eq]
assert filtered.is_and == [true]
}
fn test_apply_tenant_filter_does_not_duplicate_existing_field() {
defer {
reset_tenant_filter()
}
reset_tenant_filter()
orm.configure_tenant_filter(field_name: 'tenant_id')
orm.set_current_tenant_id(77)
where := orm.QueryData{
fields: ['tenant_id']
data: [orm.Primitive(int(11))]
kinds: [.eq]
}
filtered := orm.apply_tenant_filter(orm.Table{
name: 'test_table'
}, where)
assert filtered.fields == ['tenant_id']
assert filtered.data == [orm.Primitive(int(11))]
assert filtered.kinds == [.eq]
assert filtered.is_and == []
}
fn test_apply_tenant_filter_with_table_override_and_ignore() {
defer {
reset_tenant_filter()
}
reset_tenant_filter()
orm.configure_tenant_filter(field_name: 'tenant_id')
orm.set_current_tenant_id(5)
with_override := orm.apply_tenant_filter(orm.Table{
name: 'test_table'
attrs: [
VAttribute{
name: 'tenant_field'
has_arg: true
arg: 'company_id'
kind: .string
},
]
}, orm.QueryData{})
assert with_override.fields == ['company_id']
assert with_override.data == [orm.Primitive(int(5))]
ignored := orm.apply_tenant_filter(orm.Table{
name: 'test_table'
attrs: [
VAttribute{
name: 'ignore_tenant_filter'
},
]
}, orm.QueryData{})
assert ignored.fields == []
assert ignored.data == []
}
fn test_apply_tenant_filter_wraps_existing_where_clause() {
defer {
reset_tenant_filter()
}
reset_tenant_filter()
orm.configure_tenant_filter(field_name: 'tenant_id')
orm.set_current_tenant_id(42)
filtered := orm.apply_tenant_filter(orm.Table{
name: 'test_table'
}, orm.QueryData{
fields: ['a', 'b']
kinds: [.eq, .eq]
is_and: [false]
})
assert filtered.fields == ['a', 'b', 'tenant_id']
assert filtered.is_and == [false, true]
assert filtered.parentheses.len == 1
assert filtered.parentheses[0] == [0, 1]
}
fn test_without_tenant_filter_and_with_tenant() {
defer {
reset_tenant_filter()
}
reset_tenant_filter()
orm.configure_tenant_filter(field_name: 'tenant_id')
orm.set_current_tenant_id(10)
without_filter := orm.without_tenant_filter[orm.QueryData](fn () !orm.QueryData {
return orm.apply_tenant_filter(orm.Table{
name: 'test_table'
}, orm.QueryData{})
}) or { panic(err) }
assert without_filter.fields == []
with_override := orm.with_tenant[orm.QueryData](22, fn () !orm.QueryData {
return orm.apply_tenant_filter(orm.Table{
name: 'test_table'
}, orm.QueryData{})
}) or { panic(err) }
assert with_override.fields == ['tenant_id']
assert with_override.data == [orm.Primitive(int(22))]
after_scope := orm.apply_tenant_filter(orm.Table{
name: 'test_table'
}, orm.QueryData{})
assert after_scope.data == [orm.Primitive(int(10))]
}
fn sql_type_from_v(typ int) !string {
return if typ in orm.nums {
'INT'
} else if typ in orm.num64 {
'INT64'
} else if typ in orm.float {
'DOUBLE'
} else if typ == orm.type_string {
'TEXT'
} else if typ == orm.serial {
'SERIAL'
} else {
error('Unknown type ${typ}')
}
}
File diff suppressed because it is too large. Load diff
+759
View File
@@ -0,0 +1,759 @@
// vtest retry: 2
import orm
import db.sqlite
import time
@[table: 'sys_users']
@[index: 'name, age']
struct User {
id int @[primary; serial]
name string @[index]
age int
role string @[index]
status int @[index]
level UserLevel
salary int
title string
score int
created_at ?time.Time @[sql_type: 'TIMESTAMP']
updated_at time.Time @[sql_type: 'TIMESTAMP']
type_i8 i8
type_i16 i16
type_int int
type_i64 i64
type_u8 u8
type_u16 u16
type_u32 u32
type_u64 u64
type_f32 f32
type_f64 f64
type_bool bool
type_string string
option_i8 ?i8
option_i16 ?i16
option_int ?int
option_i64 ?i64
option_u8 ?u8
option_u16 ?u16
option_u32 ?u32
option_u64 ?u64
option_f32 ?f32
option_f64 ?f64
option_bool ?bool
option_string ?string
}
enum UserLevel {
beginner
intermediate
advanced
}
// UserPart is part of User, so we can access only part of the `sys_users` table
// note: for test, we modify `created_at` field from option to require
// a `null` value in database, will map to default value of the require field in struct
@[table: 'sys_users']
struct UserPart {
id int @[primary; serial]
name string
created_at time.Time @[sql_type: 'TIMESTAMP']
updated_at time.Time @[sql_type: 'TIMESTAMP']
option_i8 ?i8 = 13 // option with default test
option_string ?string = 'this is not none'
}
fn test_orm_func_where() {
mut db := sqlite.connect(':memory:')!
defer { db.close() or {} }
mut qb := orm.new_query[User](db)
// single_condition
qb.reset()
qb.where('age > ?', 25)!
assert qb.where.fields == ['age']
assert qb.where.kinds == [.gt]
assert qb.where.data == [25]
// chain_condition
qb.reset()
qb.where('age > ?', 25)!.where('salary < ?', 1000)!
assert qb.where.fields == ['age', 'salary']
assert qb.where.kinds == [.gt, .lt]
assert qb.where.data == [25, 1000]
// and_or_combination
qb.reset()
qb.where('name = ? AND status = ? OR role = ? || id = ? && title = ?', 'Alice', 1, 'admin', 1,
'st')!
assert qb.where.fields == ['name', 'status', 'role', 'id', 'title']
assert qb.where.kinds == [.eq, .eq, .eq, .eq, .eq]
assert qb.where.is_and == [true, false, false, true]
// nested_parentheses
qb.reset()
qb.where('(salary >= ? AND (age <= ? OR title LIKE ?))', 50000, 35, '%Manager%')!
assert qb.where.parentheses == [[1, 2], [0, 2]]
// test lowercase `like`
qb.reset()
qb.where('(salary >= ? AND (age <= ? OR title like ?))', 50000, 35, '%Manager%')!
assert qb.where.parentheses == [[1, 2], [0, 2]]
assert qb.where.kinds == [.ge, .le, .orm_like]
// complex_nesting
qb.reset()
qb.where('((age = ? OR (salary > ? AND id < ?)) AND (name LIKE ?))', 1, 2, 3, '%test%')!
assert qb.where.parentheses == [[1, 2], [0, 2], [3, 3], [0, 3]]
// in and not in
qb.reset()
qb.where('name IN ? AND age NOT IN ?', ['Tom'], [2])!
assert qb.where.fields == ['name', 'age']
assert qb.where.kinds == [.in, .not_in]
// lowercase in and not in
qb.reset()
qb.where('name in ? AND age not in ?', ['Tom'], [2])!
assert qb.where.fields == ['name', 'age']
assert qb.where.kinds == [.in, .not_in]
// variable arrays for in and not in
names := ['Tom']
ages := [2]
qb.reset()
qb.where('name IN ? AND age NOT IN ?', names, ages)!
assert qb.where.fields == ['name', 'age']
assert qb.where.kinds == [.in, .not_in]
assert qb.where.data[0] is []orm.Primitive
assert qb.where.data[1] is []orm.Primitive
name_params := qb.where.data[0] as []orm.Primitive
age_params := qb.where.data[1] as []orm.Primitive
assert name_params == [orm.Primitive('Tom')]
assert age_params == [orm.Primitive(2)]
}
fn test_orm_func_stmts() {
users := [
User{
name: 'Tom'
age: 30
role: 'admin'
status: 1
salary: 5000
title: 'manager'
score: 90
created_at: time.now()
updated_at: time.now()
type_i8: 1
type_i16: 2
type_int: 3
type_i64: 4
type_u8: 5
type_u16: 6
type_u32: 7
type_u64: 8
type_f32: 1.1
type_f64: 2.2
type_bool: true
type_string: 'hello'
option_i8: 1
option_i16: 2
option_int: 3
option_i64: 4
option_u8: 5
option_u16: 6
option_u32: 7
option_u64: 8
option_f32: 1.1
option_f64: 2.2
option_bool: true
option_string: 'hello'
},
User{
name: 'Alice'
age: 20
role: 'employee'
status: 2
salary: 2000
title: 'doctor'
score: 95
created_at: time.now()
updated_at: time.now()
type_i8: 1
type_i16: 2
type_int: 3
type_i64: 4
type_u8: 5
type_u16: 6
type_u32: 7
type_u64: 8
type_f32: 1.1
type_f64: 2.2
type_bool: true
type_string: 'hello'
option_i8: 1
option_i16: 2
option_int: 3
option_i64: 4
option_u8: 5
option_u16: 6
option_u32: 7
option_u64: 8
option_f32: 1.1
option_f64: 2.2
option_bool: true
option_string: 'hello'
},
User{
name: 'Mars'
age: 40
role: 'employer'
status: 3
salary: 1000
title: 'doctor'
score: 85
created_at: time.now()
updated_at: time.now()
type_i8: 1
type_i16: 2
type_int: 3
type_i64: 4
type_u8: 5
type_u16: 6
type_u32: 7
type_u64: 8
type_f32: 1.1
type_f64: 2.2
type_bool: true
type_string: 'hello'
option_i8: 1
option_i16: 2
option_int: 3
option_i64: 4
option_u8: 5
option_u16: 6
option_u32: 7
option_u64: 8
option_f32: 1.1
option_f64: 2.2
option_bool: true
option_string: 'hello'
},
User{
name: 'Kitty'
age: 18
role: 'employer'
status: 1
salary: 1500
title: 'doctor'
score: 87
created_at: time.now()
updated_at: time.now()
type_i8: 1
type_i16: 2
type_int: 3
type_i64: 4
type_u8: 5
type_u16: 6
type_u32: 7
type_u64: 8
type_f32: 1.1
type_f64: 2.2
type_bool: true
type_string: 'hello'
option_i8: 1
option_i16: 2
option_int: 3
option_i64: 4
option_u8: 5
option_u16: 6
option_u32: 7
option_u64: 8
option_f32: 1.1
option_f64: 2.2
option_bool: true
option_string: 'hello'
},
User{
name: 'Silly'
age: 27
role: 'employer'
status: 5
level: .intermediate
salary: 2500
title: 'doctor'
score: 81
updated_at: time.now()
type_i8: 1
type_i16: 2
type_int: 3
type_i64: 4
type_u8: 5
type_u16: 6
type_u32: 7
type_u64: 8
type_f32: 1.1
type_f64: 2.2
type_bool: true
type_string: 'hello'
option_i8: 1
option_i16: 2
option_int: 3
option_i64: 4
option_u8: 5
option_u16: 6
option_u32: 7
option_u64: 8
option_f32: 1.1
option_f64: 2.2
option_bool: true
// option_string: 'hello' // option with default test
},
User{
name: 'Smith'
age: 37
role: 'employer'
status: 1
salary: 4500
title: 'doctor'
score: 89
created_at: time.now()
updated_at: time.now()
type_i8: 1
type_i16: 2
type_int: 3
type_i64: 4
type_u8: 5
type_u16: 6
type_u32: 7
type_u64: 8
type_f32: 1.1
type_f64: 2.2
type_bool: true
type_string: 'hello'
option_i8: 1
option_i16: 2
option_int: 3
option_i64: 4
option_u8: 5
option_u16: 6
option_u32: 7
option_u64: 8
option_f32: 1.1
option_f64: 2.2
option_bool: true
option_string: 'hello'
},
User{
name: 'Bob'
age: 26
role: 'employer'
status: 2
salary: 6500
title: 'doctor'
score: 81
created_at: time.now()
updated_at: time.now()
type_i8: 1
type_i16: 2
type_int: 3
type_i64: 4
type_u8: 5
type_u16: 6
type_u32: 7
type_u64: 8
type_f32: 1.1
type_f64: 2.2
type_bool: true
type_string: 'hello'
option_i8: 1
option_i16: 2
option_int: 3
option_i64: 4
option_u8: 5
option_u16: 6
option_u32: 7
option_u64: 8
option_f32: 1.1
option_f64: 2.2
option_bool: true
option_string: 'hello'
},
User{
name: 'Peter'
age: 29
role: 'employer'
status: 1
salary: 3500
title: 'doctor'
score: 80
created_at: time.now()
updated_at: time.now()
type_i8: 1
type_i16: 2
type_int: 3
type_i64: 4
type_u8: 5
type_u16: 6
type_u32: 7
type_u64: 8
type_f32: 1.1
type_f64: 2.2
type_bool: true
type_string: 'hello'
// option_i8: 1 // option with default test
option_i16: 2
option_int: 3
option_i64: 4
option_u8: 5
option_u16: 6
option_u32: 7
option_u64: 8
option_f32: 1.1
option_f64: 2.2
option_bool: true
option_string: 'hello'
},
User{
name: 'See'
age: 45
role: 'employer'
status: 2
salary: 8500
title: 'doctor'
score: 82
updated_at: time.now()
type_i8: 1
type_i16: 2
type_int: 3
type_i64: 4
type_u8: 5
type_u16: 6
type_u32: 7
type_u64: 8
type_f32: 1.1
type_f64: 2.2
type_bool: true
type_string: 'hello'
option_i8: 1
option_i16: 2
option_int: 3
option_i64: 4
option_u8: 5
option_u16: 6
option_u32: 7
option_u64: 8
option_f32: 1.1
option_f64: 2.2
option_bool: true
option_string: 'hello'
},
User{
name: 'John'
age: 42
role: 'employer'
status: 1
salary: 10000
title: 'doctor'
score: 88
updated_at: time.now()
type_i8: 1
type_i16: 2
type_int: 3
type_i64: 4
type_u8: 5
type_u16: 6
type_u32: 7
type_u64: 8
type_f32: 1.1
type_f64: 2.2
type_bool: true
type_string: 'hello'
option_i8: 1
option_i16: 2
option_int: 3
option_i64: 4
option_u8: 5
option_u16: 6
option_u32: 7
option_u64: 8
option_f32: 1.1
option_f64: 2.2
option_bool: true
option_string: 'hello'
},
]
mut db := sqlite.connect(':memory:')!
defer { db.close() or {} }
mut qb := orm.new_query[User](db)
// create table
qb.create()!
// insert many records
qb.insert_many(users)!
// select count(*)
mut count := qb.count()!
// last_id
mut last_id := qb.last_id()
assert count == last_id
assert count == users.len
// insert a single record
qb.insert(users[0])!
// select * from table
all_users := qb.query()!
assert all_users.len == users.len + 1
// select `name` from table
only_names := qb.select('name')!.query()!
assert only_names[0].name != ''
assert only_names[0].id == 0
assert only_names[0].age == 0
assert only_names[0].role == ''
assert only_names[0].status == 0
assert only_names[0].salary == 0
assert only_names[0].title == ''
assert only_names[0].score == 0
assert only_names[0].created_at == none
// select distinct `role` from table
distinct_roles := qb.select('role')!.distinct()!.order(.asc, 'role')!.query()!
assert distinct_roles.len == 3
assert distinct_roles.map(it.role) == ['admin', 'employee', 'employer']
assert distinct_roles[0].id == 0
assert distinct_roles[0].name == ''
// update with single `set()`
qb.set('age = ?, title = ?', 71, 'boss')!.where('name = ?', 'John')!.update()!
john := qb.where('name = ?', 'John')!.query()!
assert john[0].name == 'John'
assert john[0].age == 71
assert john[0].title == 'boss'
// update with multiple `set()`
qb.set('age = ?', 51)!
.set('title = ?', 'employee')!
.where('name = ?', 'John')!.update()!
john2 := qb.where('name = ?', 'John')!.query()!
assert john2[0].name == 'John'
assert john2[0].age == 51
assert john2[0].title == 'employee'
// delete
qb.where('name = ?', 'John')!.delete()!
no_john := qb.where('name = ?', 'John')!.query()!
assert no_john.len == 0
// complex select
selected_users := qb.where('created_at IS NULL && ((salary > ? && age < ?) || (role LIKE ?))',
2000, 30, '%employee%')!.query()!
assert selected_users[0].name == 'Silly'
// Check enum
assert selected_users[0].level == .intermediate
assert selected_users.len == 1
// complex select with lowercase `is null` and `like`
selected_users1 := qb.where('created_at is null && ((salary > ? && age < ?) || (role like ?))',
2000, 30, '%employee%')!.query()!
assert selected_users1[0].name == 'Silly'
assert selected_users1.len == 1
// chain where
and_where := qb.where('salary > ?', 2000)!.where('age > ?', 40)!.query()!
assert and_where.len == 1
or_where := qb.where('salary > ?', 2000)!.or_where('age > ? OR score > ?', 40, 85)!.query()!
assert or_where.len == 9
// chain calls
final_users := qb
.drop()!
.create()!
.insert_many(users)!
.set('name = ?', 'haha')!.where('name = ?', 'Tom')!.update()!
.where('age >= ?', 30)!.delete()!
.order(.asc, 'age')!
.limit(100)!
.query()!
assert final_users.len == 5
assert final_users[0].age == 18
// access only part of the table
mut part := orm.new_query[UserPart](db)
part_user := part.query()!
// a `null` value in database, will map to default value of the require field in struct
assert part_user.filter(it.name == 'Silly')[0].created_at == time.Time{}
assert part_user.len == 5
}
struct Person {
age_f32 f32
age_f64 f64
}
fn test_orm_func_f32_f64() {
p := Person{
age_f32: 10.33
age_f64: 10.343
}
db := sqlite.connect(':memory:')!
mut qb := orm.new_query[Person](db)
data := qb
.create()!
.insert(p)!
.query()!
.first()
assert data.age_f32 == p.age_f32
assert data.age_f64 == p.age_f64
}
@[index: 'age_f33, age_f64']
struct InvalidIndexFieldName1 {
age_f32 f32
age_f64 f64
}
fn test_orm_func_invalid_index_field_name1() {
p := InvalidIndexFieldName1{
age_f32: 10.33
age_f64: 10.343
}
db := sqlite.connect(':memory:')!
mut qb := orm.new_query[InvalidIndexFieldName1](db)
qb.create() or {
assert err.msg() == "table `invalidindexfieldname1` has no field's name: `age_f33`"
return
}
assert false, 'should not be here'
}
@[index: 'age_f32, age_f64']
struct InvalidIndexFieldName2 {
age_f32 f32 @[sql: abc]
age_f64 f64
}
fn test_orm_func_invalid_index_field_name2() {
p := InvalidIndexFieldName2{
age_f32: 10.33
age_f64: 10.343
}
db := sqlite.connect(':memory:')!
mut qb := orm.new_query[InvalidIndexFieldName2](db)
qb.create() or {
assert err.msg() == "table `invalidindexfieldname2` has no field's name: `age_f32`"
return
}
assert false, 'should not be here'
}
fn test_orm_func_update_many() {
mut db := sqlite.connect(':memory:')!
defer { db.close() or {} }
mut qb := orm.new_query[User](db)
qb.create()!
// Insert test records
users := [
User{
name: 'Alice'
age: 25
role: 'developer'
},
User{
name: 'Bob'
age: 30
role: 'manager'
},
User{
name: 'Carol'
age: 35
role: 'designer'
},
]
qb.insert_many(users)!
// Verify initial data
all_users := qb.query()!
assert all_users.len == 3
assert all_users[0].name == 'Alice'
assert all_users[1].name == 'Bob'
assert all_users[2].name == 'Carol'
// Batch update names by id
orm.update_many[User](mut db, [
User{
id: 1
name: 'Alice_updated'
age: 26
},
User{
id: 2
name: 'Bob_updated'
age: 31
},
], 'id', 'name', 'age')!
// Verify updated data
updated_users := qb.query()!
assert updated_users.len == 3
for u in updated_users {
match u.id {
1 {
assert u.name == 'Alice_updated'
assert u.age == 26
assert u.role == 'developer'
}
2 {
assert u.name == 'Bob_updated'
assert u.age == 31
assert u.role == 'manager'
}
3 {
assert u.name == 'Carol'
assert u.age == 35
assert u.role == 'designer'
}
else {
assert false
}
}
}
// Test update_many with single record
orm.update_many[User](mut db, [
User{
id: 3
name: 'Carol_updated'
age: 36
},
], 'id', 'name', 'age')!
single_result := qb.where('id = ?', 3)!.query()!
assert single_result.len == 1
assert single_result[0].name == 'Carol_updated'
assert single_result[0].age == 36
assert single_result[0].role == 'designer'
// Test update_many with empty values
orm.update_many[User](mut db, []User{}, 'id', 'name') or {
assert err.msg().contains('need at least one record')
return
}
assert false, 'should not be here'
}
@@ -0,0 +1,178 @@
// vtest build: present_sqlite3? && !windows && !sanitize-memory-clang
// vtest retry: 3
// Tests for generic structs where the type parameter is used as a field type
import db.sqlite
// Test struct that will be used as a generic type parameter
struct Payload {
some_field_1 string
some_field_2 int
}
// Generic struct with a field of type T - this requires proper ORM configuration
// The payload field will be treated as a foreign key reference
pub struct Message[T] {
id int @[primary; sql: serial]
data string
payload T @[fkey: 'id']
}
// Payload needs to be a proper ORM table with a primary key
struct PayloadTable {
id int @[primary; sql: serial]
some_field_1 string
some_field_2 int
}
// Generic struct with proper ORM configuration
pub struct MessageWithPayload[T] {
id int @[primary; sql: serial]
data string
payload T @[fkey: 'id']
}
fn test_generic_struct_with_struct_field_and_primary_key() {
mut db := sqlite.connect(':memory:')!
// Create tables - PayloadTable first since it's referenced
sql db {
create table PayloadTable
}!
// Insert a payload
payload := PayloadTable{
some_field_1: 'test'
some_field_2: 42
}
sql db {
insert payload into PayloadTable
}!
// Verify the payload was inserted
payloads := sql db {
select from PayloadTable
}!
assert payloads.len == 1
assert payloads[0].some_field_1 == 'test'
assert payloads[0].some_field_2 == 42
db.close()!
}
// Test that generic structs with simple fields work correctly
pub struct SimpleMessage[T] {
id int @[primary; sql: serial]
data string
status int
}
fn test_generic_struct_with_simple_fields() {
mut db := sqlite.connect(':memory:')!
sql db {
create table SimpleMessage[Payload]
}!
// The table should be created successfully
// Note: SimpleMessage[Payload] doesn't actually use Payload as a field type,
// so this works fine
db.close()!
}
// Test that skipping struct fields with @[sql: '-'] works
pub struct MessageWithSkippedField[T] {
id int @[primary; sql: serial]
data string
payload T @[sql: '-'] // Skip this field in ORM
}
fn test_generic_struct_with_skipped_field() {
mut db := sqlite.connect(':memory:')!
// This should work because the payload field is skipped
sql db {
create table MessageWithSkippedField[Payload]
}!
db.close()!
}
// Test generic struct creation inside a generic function
// This tests the fix for issue #26433
pub struct GenericMessage[T] {
id int @[primary; sql: serial]
data string
payload T @[sql: '-'] // Skipped to avoid needing fkey setup
}
pub struct Queue[T] {
pub mut:
conn &sqlite.DB
}
pub fn create_queue[T](path string) !Queue[T] {
mut conn := sqlite.connect(path)!
mut queue := Queue[T]{
conn: &conn
}
// This should work: creating a table with a generic struct inside a generic function
sql queue.conn {
create table GenericMessage[T]
}!
return queue
}
fn test_create_table_in_generic_function() {
mut queue := create_queue[Payload](':memory:')!
// Verify the table was created by inserting and selecting data
msg := GenericMessage[Payload]{
data: 'test message'
}
sql queue.conn {
insert msg into GenericMessage[Payload]
}!
messages := sql queue.conn {
select from GenericMessage[Payload]
}!
assert messages.len == 1
assert messages[0].data == 'test message'
queue.conn.close()!
}
// Test inserting from within a generic function
// This tests the fix for the "cannot use `Message` as `Message[Payload]`" error
pub fn (mut q Queue[T]) add_message(data string) !GenericMessage[T] {
msg := GenericMessage[T]{
data: data
}
sql q.conn {
insert msg into GenericMessage[T]
}!
return msg
}
fn test_insert_in_generic_function() {
mut queue := create_queue[Payload](':memory:')!
// Insert from within a generic function
msg := queue.add_message('inserted from generic fn')!
assert msg.data == 'inserted from generic fn'
// Verify it was inserted
messages := sql queue.conn {
select from GenericMessage[Payload]
}!
assert messages.len == 1
assert messages[0].data == 'inserted from generic fn'
queue.conn.close()!
}
+46
View File
@@ -0,0 +1,46 @@
// vtest build: present_sqlite3? && !sanitize-memory-clang
import db.sqlite
pub enum MessageStatus as u8 {
ready
}
pub struct Message[T] {
message_id int @[primary; sql: serial]
status MessageStatus
}
pub struct Queue[T] {
conn &sqlite.DB
}
pub struct Queue_config {
path string
db ?sqlite.DB @[omitempty]
}
pub fn new[T](config Queue_config) !Queue[T] {
mut conn := if db := config.db {
db
} else {
sqlite.connect(config.path) or { return error('Failed to connect to database: ${err}') }
}
mut queue := Queue[T]{
conn: &conn
}
return queue
}
pub fn (mut self Queue[T]) take() !Message[T] {
messages := sql self.conn {
select from Message[T] where status == MessageStatus.ready order by message_id limit 1
} or { return error('') }
message := messages.first()
return message
}
struct Payload {}
fn test_main() {
_ := new[Payload](path: ':memory:')!
}
@@ -0,0 +1,190 @@
// vtest build: present_sqlite3? && !sanitize-memory-clang
// vtest vflags: -prod
// Tests for generic ORM with WHERE clause variable shadowing field names.
// This tests the fix for the bug where `select from T where status == status`
// (where `status` is both a field name and a local variable) was incorrectly
// optimized to `true` by the transformer, resulting in empty WHERE clauses.
import db.sqlite
pub enum TestStatus as u8 {
pending
active
completed
}
pub struct GenericRecord[T] {
pub:
id int @[primary; sql: serial]
status TestStatus
payload T @[sql: '-']
}
struct TestPayload {
value int
}
// Test that WHERE clause works when variable name shadows field name
fn test_generic_where_variable_shadows_field() {
mut db := sqlite.connect(':memory:') or { panic(err) }
defer { db.close() or {} }
sql db {
create table GenericRecord[TestPayload]
} or { panic(err) }
// Insert records with different statuses
r1 := GenericRecord[TestPayload]{
status: .pending
payload: TestPayload{
value: 1
}
}
r2 := GenericRecord[TestPayload]{
status: .active
payload: TestPayload{
value: 2
}
}
r3 := GenericRecord[TestPayload]{
status: .active
payload: TestPayload{
value: 3
}
}
r4 := GenericRecord[TestPayload]{
status: .completed
payload: TestPayload{
value: 4
}
}
sql db {
insert r1 into GenericRecord[TestPayload]
insert r2 into GenericRecord[TestPayload]
insert r3 into GenericRecord[TestPayload]
insert r4 into GenericRecord[TestPayload]
} or { panic(err) }
// Test: variable `status` shadows field `status`
// Before the fix, this would return all rows or error because
// `status == status` was optimized to `true`
status := TestStatus.active
results := sql db {
select from GenericRecord[TestPayload] where status == status
} or { panic(err) }
assert results.len == 2, 'Expected 2 active records, got ${results.len}'
// Test with different status values
status2 := TestStatus.pending
pending_results := sql db {
select from GenericRecord[TestPayload] where status == status2
} or { panic(err) }
assert pending_results.len == 1, 'Expected 1 pending record, got ${pending_results.len}'
status3 := TestStatus.completed
completed_results := sql db {
select from GenericRecord[TestPayload] where status == status3
} or { panic(err) }
assert completed_results.len == 1, 'Expected 1 completed record, got ${completed_results.len}'
}
// Test generic select helper function with shadowed variable
fn generic_select_by_status[T](db &sqlite.DB, status TestStatus) ![]GenericRecord[T] {
return sql db {
select from GenericRecord[T] where status == status
}!
}
fn test_generic_function_where_shadow() {
mut db := sqlite.connect(':memory:') or { panic(err) }
defer { db.close() or {} }
sql db {
create table GenericRecord[TestPayload]
} or { panic(err) }
r1 := GenericRecord[TestPayload]{
status: .pending
payload: TestPayload{
value: 10
}
}
r2 := GenericRecord[TestPayload]{
status: .active
payload: TestPayload{
value: 20
}
}
sql db {
insert r1 into GenericRecord[TestPayload]
insert r2 into GenericRecord[TestPayload]
} or { panic(err) }
// Call generic function where parameter shadows field name
active_records := generic_select_by_status[TestPayload](&db, .active) or { panic(err) }
assert active_records.len == 1, 'Expected 1 active record from generic function'
assert active_records[0].status == .active
pending_records := generic_select_by_status[TestPayload](&db, .pending) or { panic(err) }
assert pending_records.len == 1, 'Expected 1 pending record from generic function'
assert pending_records[0].status == .pending
}
// Test count with shadowed variable
fn test_generic_count_where_shadow() {
mut db := sqlite.connect(':memory:') or { panic(err) }
defer { db.close() or {} }
sql db {
create table GenericRecord[TestPayload]
} or { panic(err) }
for i in 0 .. 5 {
r := GenericRecord[TestPayload]{
status: if i < 3 { .active } else { .completed }
payload: TestPayload{
value: i
}
}
sql db {
insert r into GenericRecord[TestPayload]
} or { panic(err) }
}
status := TestStatus.active
count := sql db {
select count from GenericRecord[TestPayload] where status == status
} or { panic(err) }
assert count == 3, 'Expected count 3, got ${count}'
}
// Test that table name doesn't include generic brackets
fn test_generic_table_name() {
mut db := sqlite.connect(':memory:') or { panic(err) }
defer { db.close() or {} }
// This should create table named "GenericRecord", not "GenericRecord[TestPayload]"
sql db {
create table GenericRecord[TestPayload]
} or { panic(err) }
// Insert and select should work without SQL syntax errors
r := GenericRecord[TestPayload]{
status: .pending
payload: TestPayload{
value: 42
}
}
sql db {
insert r into GenericRecord[TestPayload]
} or { panic(err) }
results := sql db {
select from GenericRecord[TestPayload]
} or { panic(err) }
assert results.len == 1
}
@@ -0,0 +1,34 @@
// vtest retry: 3
import db.sqlite
@[table: 'bad_table']
struct Bad {
id int @[primary; sql: serial]
link string
}
fn test_insert_with_reserved_name() {
db := sqlite.connect(':memory:') or { panic(err) }
bad := Bad{
link: 'test'
}
sql db {
create table Bad
}!
sql db {
insert bad into Bad
}!
sql db {
insert bad into Bad
insert bad into Bad
insert bad into Bad
}!
rows := sql db {
select from Bad
}!
assert rows.len == 4
}
+456
View File
@@ -0,0 +1,456 @@
// vtest retry: 3
import db.sqlite
import rand
struct Parent {
id int @[primary; sql: serial]
name string
children []Child @[fkey: 'parent_id']
notes []Note @[fkey: 'owner_id']
}
struct Child {
mut:
id int @[primary; sql: serial]
parent_id int
name string
}
struct Note {
mut:
id int @[primary; sql: serial]
owner_id int
text string
}
struct Account {
id int @[primary; sql: serial]
}
struct Package {
id int @[primary; sql: serial]
name string @[unique]
author User @[fkey: 'id'] // mandatory user
}
struct Delivery {
id int @[primary; sql: serial]
name string @[unique]
author ?User @[fkey: 'id'] // optional user
}
struct User {
pub mut:
id int @[primary; sql: serial]
username string @[unique]
}
struct Entity {
uuid string @[primary]
description string
}
struct EntityWithFloatPrimary {
id f64 @[primary]
name string
}
pub fn insert_parent(db sqlite.DB, mut parent Parent) ! {
sql db {
insert parent into Parent
}!
}
fn test_set_primary_value() {
// The primary key is an constraint that ensures each record in a table is unique.
// Primary keys must contain unique values and cannot contain `NULL` values.
// However, this statement does not imply that a value cannot be inserted by the user.
// Therefore, let's allow this.
db := sqlite.connect(':memory:')!
sql db {
create table Child
}!
child := Child{
id: 10
parent_id: 20
}
sql db {
insert child into Child
}!
children := sql db {
select from Child
}!
assert children.first() == child
}
fn test_uuid_primary_key() {
db := sqlite.connect(':memory:')!
uuid := rand.uuid_v4()
sql db {
create table Entity
}!
entity := Entity{
uuid: uuid
description: 'Test'
}
sql db {
insert entity into Entity
}!
entities := sql db {
select from Entity where uuid == uuid
}!
mut is_duplicate_inserted := true
sql db {
insert entity into Entity
} or { is_duplicate_inserted = false }
assert entities.len == 1
assert entities.first() == entity
assert is_duplicate_inserted == false
}
fn test_float_primary_key() {
db := sqlite.connect(':memory:')!
id := 3.14
sql db {
create table EntityWithFloatPrimary
}!
entity := EntityWithFloatPrimary{
id: id
name: 'Test'
}
sql db {
insert entity into EntityWithFloatPrimary
}!
entities := sql db {
select from EntityWithFloatPrimary where id == id
}!
assert entities.len == 1
assert entities.first() == entity
}
fn test_does_not_insert_uninitialized_mandatory_field() {
db := sqlite.connect(':memory:')!
sql db {
create table User
create table Package
}!
package := Package{
name: 'xml'
// author
}
mut query_successful := true
sql db {
insert package into Package
} or { query_successful = false }
assert !query_successful
users := sql db {
select from User
}!
// users must be empty because the package doesn't have an initialized `User` structure.
assert users.len == 0
}
fn test_insert_empty_mandatory_field() {
db := sqlite.connect(':memory:')!
sql db {
create table User
create table Package
}!
package := Package{
name: 'xml'
author: User{}
}
sql db {
insert package into Package
}!
users := sql db {
select from User
}!
assert users.len == 1
}
fn test_does_insert_uninitialized_optional_field() {
db := sqlite.connect(':memory:')!
sql db {
create table User
create table Delivery
}!
package := Delivery{
name: 'wow'
// author
}
sql db {
insert package into Delivery
}!
users := sql db {
select from User
}!
assert users.len == 0 // no user added
}
fn test_insert_empty_optional_field() {
db := sqlite.connect(':memory:')!
sql db {
create table User
create table Delivery
}!
package := Delivery{
name: 'bob'
author: User{}
}
sql db {
insert package into Delivery
}!
users := sql db {
select from User
}!
assert users.len == 1 // user was added
}
fn test_insert_empty_object() {
db := sqlite.connect(':memory:')!
account := Account{}
sql db {
create table Account
insert account into Account
}!
accounts := sql db {
select from Account
}!
assert accounts.len == 1
}
fn test_orm_insert_mut_object() {
db := sqlite.connect(':memory:')!
sql db {
create table Parent
create table Child
create table Note
}!
mut parent := Parent{
name: 'test'
}
insert_parent(db, mut parent)!
parents := sql db {
select from Parent
}!
assert parents.len == 1
}
fn test_orm_insert_with_multiple_child_elements() {
mut db := sqlite.connect(':memory:')!
sql db {
create table Parent
create table Child
create table Note
}!
new_parent := Parent{
name: 'test'
children: [
Child{
name: 'Lisa'
},
Child{
name: 'Steve'
},
]
notes: [
Note{
text: 'First note'
},
Note{
text: 'Second note'
},
Note{
text: 'Third note'
},
]
}
sql db {
insert new_parent into Parent
}!
parents := sql db {
select from Parent where id == 1
}!
parent := parents.first()
assert parent.children.len == new_parent.children.len
assert parent.notes.len == new_parent.notes.len
children_count := sql db {
select count from Child
}!
assert children_count == new_parent.children.len
note_count := sql db {
select count from Note
}!
assert note_count == new_parent.notes.len
assert parent.children[0].name == 'Lisa'
assert parent.children[1].name == 'Steve'
assert parent.notes[0].text == 'First note'
assert parent.notes[1].text == 'Second note'
assert parent.notes[2].text == 'Third note'
}
fn test_orm_insert_with_child_element_and_no_table() {
mut db := sqlite.connect(':memory:')!
sql db {
create table Parent
}!
new_parent := Parent{
name: 'test'
children: [
Child{
name: 'Lisa'
},
]
}
sql db {
insert new_parent into Parent
} or { assert true }
sql db {
create table Child
}!
sql db {
insert new_parent into Parent
} or { assert false }
new_parent_two := Parent{
name: 'retest'
children: [
Child{
name: 'Sophia'
},
]
}
sql db {
insert new_parent_two into Parent
} or { assert false }
p_table := sql db {
select from Parent
}!
assert p_table[2].children[0].name == 'Sophia'
}
@[table: 'customers']
struct Customer {
id i64 @[primary; sql: serial]
name string
}
fn test_i64_primary_field_works_with_insertions_of_id_0() {
db := sqlite.connect(':memory:')!
sql db {
create table Customer
}!
for i in ['Bob', 'Charlie'] {
new_customer := Customer{
name: i
}
sql db {
insert new_customer into Customer
}!
}
users := sql db {
select from Customer
}!
assert users.len == 2
// println("${users}")
}
struct Address {
id i64 @[primary; sql: serial]
street string
number int
}
fn test_the_result_of_insert_should_be_the_last_insert_id() {
db := sqlite.connect(':memory:')!
address := Address{
street: 'abc'
number: 123
}
dump(address)
sql db {
create table Address
} or {}
aid1 := sql db {
insert address into Address
} or { panic(err) }
dump(aid1)
assert aid1 == 1
aid2 := sql db {
insert address into Address
} or { panic(err) }
dump(aid2)
assert aid2 == 2
addresses := sql db {
select from Address
}!
dump(addresses)
assert addresses.len == 2
assert addresses.all(it.street == 'abc' && it.number == 123)
}
+32
View File
@@ -0,0 +1,32 @@
// vtest retry: 3
import db.sqlite
import orm
struct User {
id int @[primary; sql: serial]
name string
}
fn test_orm_interface() {
sqlite_db := sqlite.connect(':memory:') or { panic(err) }
db := orm.Connection(sqlite_db)
sql db {
create table User
}!
user := User{
name: 'test'
}
sql db {
insert user into User
}!
users := sql db {
select from User
}!
assert users.len == 1
assert users.first().name == user.name
}
+156
View File
@@ -0,0 +1,156 @@
// vtest retry: 3
// vtest build: present_sqlite3? && !windows && !sanitize-memory-clang
import db.sqlite
// Department table for testing JOINs - using dept_id to avoid column name conflicts
struct Department {
dept_id int @[primary; sql: serial]
dept_name string
}
// User table with a foreign key reference to Department
struct User {
user_id int @[primary; sql: serial]
user_name string
department_id int
}
fn test_inner_join() {
db := sqlite.connect(':memory:') or { panic(err) }
// Create tables
sql db {
create table Department
create table User
}!
// Insert departments
engineering := Department{
dept_name: 'Engineering'
}
sales := Department{
dept_name: 'Sales'
}
sql db {
insert engineering into Department
insert sales into Department
}!
// Insert users
alice := User{
user_name: 'Alice'
department_id: 1
}
bob := User{
user_name: 'Bob'
department_id: 2
}
charlie := User{
user_name: 'Charlie'
department_id: 1
}
sql db {
insert alice into User
insert bob into User
insert charlie into User
}!
// Test basic INNER JOIN
users := sql db {
select from User
join Department on User.department_id == Department.dept_id
}!
assert users.len == 3
}
fn test_inner_join_with_where() {
db := sqlite.connect(':memory:') or { panic(err) }
// Create tables
sql db {
create table Department
create table User
}!
// Insert departments
engineering := Department{
dept_name: 'Engineering'
}
sales := Department{
dept_name: 'Sales'
}
sql db {
insert engineering into Department
insert sales into Department
}!
// Insert users
alice := User{
user_name: 'Alice'
department_id: 1
}
bob := User{
user_name: 'Bob'
department_id: 2
}
charlie := User{
user_name: 'Charlie'
department_id: 1
}
sql db {
insert alice into User
insert bob into User
insert charlie into User
}!
// Test INNER JOIN with WHERE clause - use simple field name (not Table.field)
engineering_users := sql db {
select from User
join Department on User.department_id == Department.dept_id where department_id == 1
}!
assert engineering_users.len == 2
assert engineering_users[0].user_name == 'Alice' || engineering_users[0].user_name == 'Charlie'
}
fn test_left_join() {
db := sqlite.connect(':memory:') or { panic(err) }
// Create tables
sql db {
create table Department
create table User
}!
// Insert departments
engineering := Department{
dept_name: 'Engineering'
}
sql db {
insert engineering into Department
}!
// Insert users - one with a department, one without (orphan)
alice := User{
user_name: 'Alice'
department_id: 1
}
bob := User{
user_name: 'Bob'
department_id: 999 // No matching department
}
sql db {
insert alice into User
insert bob into User
}!
// Test LEFT JOIN - should return all users, even those without matching department
users := sql db {
select from User
left join Department on User.department_id == Department.dept_id
}!
// Both users should be returned since it's a LEFT JOIN
assert users.len == 2
}
+33
View File
@@ -0,0 +1,33 @@
// vtest retry: 3
import db.sqlite
struct User {
id int @[primary; sql: serial]
name string
}
fn test_last_id() {
mut db := sqlite.connect(':memory:') or { panic(err) }
sql db {
create table User
}!
first_user := User{
name: 'first'
}
second_user := User{
name: 'second'
}
sql db {
insert first_user into User
insert second_user into User
}!
last_id := db.last_id()
assert typeof(last_id).name == 'int'
assert last_id > 0
}
+107
View File
@@ -0,0 +1,107 @@
// vtest retry: 3
import orm
struct Database {
mut:
last_query string
}
struct User {
id int @[primary; sql: serial]
name string
}
fn (mut db Database) query(sql_stmt string) ! {
db.last_query = sql_stmt
}
// select is used internally by V's ORM for processing `SELECT` queries
fn (mut db Database) select(config orm.SelectConfig, data orm.QueryData, where orm.QueryData) ![][]orm.Primitive {
// 1. Create query and bind necessary data
query := orm.orm_select_gen(config, '', true, ':', 1, where)
mut ret := [][]orm.Primitive{}
return ret
}
// insert is used internally by V's ORM for processing `INSERT` queries
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 orm.Table, data orm.QueryData, where orm.QueryData) ! {
mut query, _ := orm.orm_stmt_gen(.sqlite, table, '', .update, true, ':', 1, data, where)
db.query(query)!
}
// delete is used internally by V's ORM for processing `DELETE ` queries
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)
db.query(query)!
}
// last_id is used internally by V's ORM for post-processing `INSERT` queries
fn (mut db Database) last_id() int {
return 0
}
// DDL (table creation/destroying etc)
fn sqlite_type_from_v(typ int) !string {
return if typ in orm.nums || typ in orm.num64 || typ in [orm.serial, orm.time_, orm.enum_] {
'INTEGER'
} else if typ in orm.float {
'REAL'
} else if typ == orm.type_string {
'TEXT'
} else {
error('Unknown type ${typ}')
}
}
// create is used internally by V's ORM for processing table creation queries (DDL)
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 orm.Table) ! {
query := 'DROP TABLE ${table.name};'
$if trace_orm ? {
eprintln('> vsql drop: ${query}')
}
db.query(query)!
}
fn test_orm_mut_connection() {
mut db := Database{}
sql db {
create table User
}!
first_user := User{
name: 'first'
}
second_user := User{
name: 'second'
}
sql db {
insert first_user into User
insert second_user into User
}!
sql db {
drop table User
}!
assert db.last_query == 'DROP TABLE user;'
}
+37
View File
@@ -0,0 +1,37 @@
// vtest retry: 3
import db.sqlite
struct User {
id int @[primary; sql: serial]
name string
}
fn get_users(mut db sqlite.DB) ![]User {
return sql db {
select from User
}!
}
fn test_orm_mut_db() {
mut db := sqlite.connect(':memory:') or { panic(err) }
sql db {
create table User
}!
first_user := User{
name: 'first'
}
second_user := User{
name: 'second'
}
sql db {
insert first_user into User
insert second_user into User
}!
users := get_users(mut db)!
assert users.len == 2
}
+132
View File
@@ -0,0 +1,132 @@
// vtest retry: 3
import db.sqlite
struct Address {
id int @[primary; sql: serial]
parent_id int
create_at string
update_at string
street string
city string
state string
zip_code string
proximity string
}
@[table: 'RealStates']
struct RealState {
id string @[primary; sql_type: 'uuid']
parent_id int
name string @[sql_type: 'varchar(80)']
cnpj string @[sql_type: 'varchar(14)']
address []Address @[fkey: 'parent_id']
}
@[table: 'Realtors']
struct Realtor {
id ?string @[primary; sql_type: 'uuid']
first_name string @[sql_type: 'VARCHAR(30)']
last_name string @[sql_type: 'VARCHAR(30)']
creci string @[sql_type: 'VARCHAR(8)']
cnpj ?string @[sql_type: 'VARCHAR(15)']
cpf ?string @[sql_type: 'VARCHAR(12)']
phone string @[sql_type: 'VARCHAR(15)']
real_state RealState @[fkey: 'id']
}
fn test_orm_nested_struct() {
mut db := sqlite.connect(':memory:')!
data := Realtor{
first_name: 'John'
last_name: 'Doe'
creci: '12345678'
cnpj: '12345678901234'
cpf: '12345678901'
phone: '1234567890'
real_state: RealState{
name: 'Teste'
cnpj: '12345678901234'
address: [
Address{
street: 'Teste'
city: 'Teste'
state: 'Teste'
zip_code: 'Teste'
proximity: 'Teste'
},
]
}
}
sql db {
create table Address
create table RealState
create table Realtor
}!
sql db {
insert data into Realtor
}!
x1 := sql db {
select from Address
}!
assert x1.str() == "[Address{
id: 1
parent_id: 0
create_at: ''
update_at: ''
street: 'Teste'
city: 'Teste'
state: 'Teste'
zip_code: 'Teste'
proximity: 'Teste'
}]"
x2 := sql db {
select from RealState
}!
assert x2.str() == "[RealState{
id: ''
parent_id: 0
name: 'Teste'
cnpj: '12345678901234'
address: [Address{
id: 1
parent_id: 0
create_at: ''
update_at: ''
street: 'Teste'
city: 'Teste'
state: 'Teste'
zip_code: 'Teste'
proximity: 'Teste'
}]
}]"
x3 := sql db {
select from Realtor
}!
// FIXME!
// I think this result is not correct.
assert x3.str() == "[Realtor{
id: Option(none)
first_name: 'John'
last_name: 'Doe'
creci: '12345678'
cnpj: Option('12345678901234')
cpf: Option('12345678901')
phone: '1234567890'
real_state: RealState{
id: ''
parent_id: 0
name: ''
cnpj: ''
address: []
}
}]"
db.close()!
}
+367
View File
@@ -0,0 +1,367 @@
// vtest retry: 3
import orm
import db.sqlite
struct MockDBState {
mut:
last string
data []orm.Primitive
where []orm.Primitive
}
struct MockDB {
use_num bool
st &MockDBState = unsafe { nil }
db sqlite.DB
}
fn MockDB.new() &MockDB {
return &MockDB{
st: &MockDBState{}
db: sqlite.connect(':memory:') or { panic(err) }
}
}
fn (db MockDB) select(config orm.SelectConfig, data orm.QueryData, where orm.QueryData) ![][]orm.Primitive {
mut st := db.st
st.last = orm.orm_select_gen(config, '`', false, '?', 5, where)
st.data = data.data
st.where = where.data
return db.db.select(config, data, where)
}
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{})
st.last = last
st.data = qdata.data
st.where = []orm.Primitive{}
return db.db.insert(table, data)
}
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
st.where = where.data
return db.db.update(table, data, where)
}
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)
return db.db.delete(table, where)
}
const typ_to_typename = {
typeof[i8]().idx: 'i8'
typeof[i16]().idx: 'i16'
typeof[int]().idx: 'int'
typeof[i64]().idx: 'i64'
typeof[u8]().idx: 'u8'
typeof[u16]().idx: 'u16'
typeof[u32]().idx: 'u32'
typeof[u64]().idx: 'u64'
typeof[f32]().idx: 'f32'
typeof[f64]().idx: 'f64'
typeof[string]().idx: 'string'
typeof[bool]().idx: 'bool'
orm.serial: 'serial'
orm.time_: 'time'
orm.enum_: 'enum'
}
fn mock_type_from_v(typ int) !string {
return if typ in typ_to_typename {
'${typ_to_typename[typ]}-type'
} else {
error('unknown type ${typ}')
}
}
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)
}
fn (db MockDB) drop(table orm.Table) ! {
return db.db.drop(table)
}
fn (db MockDB) last_id() int {
return db.db.last_id()
}
// --
@[table: 'foo']
struct Foo {
mut:
id u64 @[primary; sql: serial]
a string
b string @[default: '"yes"']
c ?string
d ?string = 'hi'
e int
f int @[default: 33]
g ?int
h ?int = 55
}
@[unique_key: 'role_id, api_id, source_type, source_id']
@[table: 'core_role_api']
struct CoreRoleApi {
role_id string
api_id string
source_type string
source_id string
}
fn test_struct_unique_key_attribute() {
db := MockDB.new()
sql db {
create table CoreRoleApi
}!
assert db.st.last == 'CREATE TABLE IF NOT EXISTS `core_role_api` (`role_id` string-type NOT NULL, `api_id` string-type NOT NULL, `source_type` string-type NOT NULL, `source_id` string-type NOT NULL, UNIQUE(`role_id`, `api_id`, `source_type`, `source_id`));'
}
fn test_option_struct_fields_and_none() {
db := MockDB.new()
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`));'
_ := sql db {
select from Foo where e > 5 && c is none && c !is none && h == 2
}!
assert db.st.last == 'SELECT `id`, `a`, `b`, `c`, `d`, `e`, `f`, `g`, `h` FROM `foo` WHERE `e` > ? AND `c` IS NULL AND `c` IS NOT NULL AND `h` = ?;'
assert db.st.data.len == 0
assert db.st.where.len == 2
assert db.st.where == [orm.Primitive(int(5)), orm.Primitive(int(2))]
foo := Foo{}
sql db {
insert foo into Foo
}!
assert db.st.last == 'INSERT INTO `foo` (`a`, `c`, `d`, `e`, `g`, `h`) VALUES (?, ?, ?, ?, ?, ?);'
assert db.st.data.len == 6
assert db.st.data == [orm.Primitive(string('')), orm.Null{}, orm.Primitive(string('hi')), int(0),
orm.Null{}, int(55)]
id := db.last_id()
res1 := sql db {
select from Foo where id == id
}!
assert db.st.last == 'SELECT `id`, `a`, `b`, `c`, `d`, `e`, `f`, `g`, `h` FROM `foo` WHERE `id` = ?;'
assert db.st.data.len == 0
assert db.st.where.len == 1
assert db.st.where == [orm.Primitive(int(id))]
assert res1.len == 1
assert res1[0] == Foo{
id: 1
a: ''
b: 'yes'
c: none
d: 'hi'
e: 0
f: 33
g: none
h: 55
}
sql db {
update Foo set c = 'yo', d = none, g = 44, h = none where id == id
}!
assert db.st.last == 'UPDATE `foo` SET `c` = ?, `d` = ?, `g` = ?, `h` = ? WHERE `id` = ?;'
assert db.st.data.len == 4
assert db.st.data == [orm.Primitive(string('yo')), orm.Null{}, int(44), orm.Null{}]
assert db.st.where.len == 1
assert db.st.where == [orm.Primitive(int(id))]
res2 := sql db {
select from Foo where id == id
}!
assert db.st.last == 'SELECT `id`, `a`, `b`, `c`, `d`, `e`, `f`, `g`, `h` FROM `foo` WHERE `id` = ?;'
assert db.st.data.len == 0
assert db.st.where.len == 1
assert db.st.where == [orm.Primitive(int(id))]
assert res2.len == 1
assert res2[0] == Foo{
id: 1
a: ''
b: 'yes'
c: 'yo'
d: none
e: 0
f: 33
g: 44
h: none
}
_ := sql db {
select from Foo where d == nil && c != nil && a == nil
}!
assert db.st.last == 'SELECT `id`, `a`, `b`, `c`, `d`, `e`, `f`, `g`, `h` FROM `foo` WHERE `d` IS NULL AND `c` IS NOT NULL AND `a` IS NULL;'
assert db.st.data.len == 0
assert db.st.where.len == 0
assert sql db {
select count from Foo where a == 'yo'
}! == 0
assert sql db {
select count from Foo where b == 'yes'
}! == 1
assert sql db {
select count from Foo where d == 'yo'
}! == 0
assert sql db {
select count from Foo where f == 33
}! == 1
assert sql db {
select count from Foo where c == 'yo'
}! == 1
assert sql db {
select count from Foo where a == ''
}! == 1
assert sql db {
select count from Foo where d == ''
}! == 0
assert sql db {
select count from Foo where c == ''
}! == 0
assert sql db {
select count from Foo where a == nil
}! == 0
assert sql db {
select count from Foo where d == nil
}! == 1
assert sql db {
select count from Foo where c != nil
}! == 1
assert sql db {
select count from Foo where a is none
}! == 0
assert sql db {
select count from Foo where d is none
}! == 1
assert sql db {
select count from Foo where c is none
}! == 0
assert sql db {
select count from Foo where a !is none
}! == 1
assert sql db {
select count from Foo where d !is none
}! == 0
assert sql db {
select count from Foo where c !is none
}! == 1
}
struct Bar {
id u64 @[primary; sql: serial]
name ?string
age int
}
fn update_bar1(db MockDB, id u64, name ?string) ! {
foo := 66
sql db {
update Bar set name = name, age = age + 3 + foo where id == id
}!
}
fn update_bar2(db MockDB, name ?string, new_name ?string) ! {
sql db {
update Bar set name = new_name where name == name
}!
}
type NameFn = fn () ?string
fn update_bar3(db MockDB, name_fn NameFn, new_name string) ! {
sql db {
update Bar set name = new_name where name == name_fn()
}!
}
fn test_inserting_passed_optionals() {
db := MockDB.new()
entry1 := Bar{}
entry2 := Bar{
name: 'Alice'
age: 55
}
entry3 := Bar{
name: 'Bob'
age: 66
}
sql db {
create table Bar
insert entry1 into Bar
insert entry2 into Bar
insert entry3 into Bar
}!
update_bar1(db, 2, none)!
update_bar1(db, 1, 'hi')!
res1 := sql db {
select from Bar
}!
assert res1.len == 3
assert res1[0].name or { '' } == 'hi'
assert res1[1].name == none
assert res1[2].name or { '' } == 'Bob'
update_bar2(db, none, 'xxx')! // no effect (select using "is none", not "== none")
update_bar2(db, 'hi', none)!
res2 := sql db {
select from Bar
}!
assert res2.len == 3
assert res2[0].name == none
assert res2[1].name == none
assert res2[2].name or { '' } == 'Bob'
update_bar3(db, fn () ?string {
return none // no effect (select using "is none", not "== none")
}, 'yyy')!
update_bar3(db, fn () ?string {
return 'Bob'
}, 'www')!
res3 := sql db {
select from Bar
}!
assert res3.len == 3
assert res3[0].name == none
assert res3[1].name == none
assert res3[2].name or { '' } == 'www'
}
fn test_distinct_select() {
db := MockDB.new()
sql db {
create table Foo
}!
_ := sql db {
select distinct from Foo
}!
assert db.st.last == 'SELECT DISTINCT `id`, `a`, `b`, `c`, `d`, `e`, `f`, `g`, `h` FROM `foo`;'
_ := sql db {
select distinct from Foo where e > 5
}!
assert db.st.last == 'SELECT DISTINCT `id`, `a`, `b`, `c`, `d`, `e`, `f`, `g`, `h` FROM `foo` WHERE `e` > ?;'
}
+46
View File
@@ -0,0 +1,46 @@
// vtest retry: 3
import db.sqlite
struct Member {
id int @[primary]
children ?[]Child @[fkey: parent_id]
}
struct Child {
id int @[primary]
parent_id int
}
fn test_main() {
mut db := sqlite.connect(':memory:') or { panic(err) }
defer {
db.close() or { panic(err) }
}
sql db {
create table Member
} or { println(err) }
sql db {
create table Child
} or { println(err) }
new_member := Member{
id: 1
children: [Child{
id: 1
parent_id: 1
}, Child{
id: 2
parent_id: 1
}]
}
sql db {
insert new_member into Member
}!
rows := sql db {
select from Member
}!
assert rows[0].children?.len == 2
assert rows[0].children?[0].id == 1
assert rows[0].children?[1].id == 2
}
@@ -0,0 +1,49 @@
import db.sqlite
fn test_main() {
db := sqlite.connect(':memory:')!
sql db {
create table Commit
create table Measurement
}!
c := Commit{
commit_hash: 'hash'
}
sql db {
insert c into Commit
}!
c2 := sql db {
select from Commit
}!
assert c2[0].v_self_default == none
c3 := Commit{
commit_hash: 'hash1'
v_self_default: Measurement{
id: 123
}
}
sql db {
insert c3 into Commit
}!
c4 := sql db {
select from Commit
}!
assert c4[0].v_self_default == none
assert c4[1].v_self_default != none
}
@[table: 'commits']
struct Commit {
commit_hash string @[primary]
v_self_default ?Measurement
}
@[table: 'measurements']
struct Measurement {
id int @[primary; serial]
}
+56
View File
@@ -0,0 +1,56 @@
// vtest retry: 3
import db.sqlite
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
}
fn test_main() {
mut db := sqlite.connect(':memory:') or { panic(err) }
defer {
db.close() or { panic(err) }
}
sql db {
create table Foo
create table Child
}!
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'
},
]
}
sql db {
insert foo into Foo
}!
data := sql db {
select from Foo
}![0]
assert data.id == 1
assert data.updated_at == none
assert data.deleted_at == none
}
@@ -0,0 +1,37 @@
import db.sqlite
@[table: 'prefixed_records']
struct Record {
id int @[primary; sql: 'CustomId']
name string @[sql: 'named_name']
}
fn test_main() {
mut db := sqlite.connect(':memory:')!
defer { db.close() or {} }
prepare(db)!
last := sql db {
select from Record where name == 'first' order by id limit 1
}!
assert last.len == 1
assert last[0].name == 'first'
}
fn prepare(db sqlite.DB) ! {
db.exec('
CREATE TABLE prefixed_records (
CustomId INTEGER PRIMARY KEY AUTOINCREMENT,
named_name TEXT
);
')!
db.exec("
INSERT INTO prefixed_records
(named_name)
VALUES
('first'),
('last');
")!
}
+81
View File
@@ -0,0 +1,81 @@
// vtest retry: 3
import db.sqlite
struct Boat {
id int @[primary; sql: serial]
color_id int @[references]
another1_id int @[references: 'size']
another2_id int @[references: 'size(secondary_id)']
}
struct Color {
id int @[primary; sql: serial]
hex string
}
struct Size {
id int @[primary; sql: serial]
secondary_id int
}
fn test_references_constraint() {
db := sqlite.connect(':memory:') or { panic(err) }
sql db {
create table Boat
create table Color
create table Size
} or { panic(err) }
// this pragma returns a row for each foreign key constraint on a table
pragma_result := db.exec('pragma foreign_key_list(boat)') or { panic('nope') }
assert pragma_result.len == 3
}
struct Member {
id int @[primary; sql: serial]
name string
}
struct Team {
id int @[primary; sql: serial]
name string
member_id int @[references: 'Member(id)']
}
fn test_omitted_references_field_inserts_null() {
mut db := sqlite.connect(':memory:')!
defer {
db.close() or {}
}
db.exec('PRAGMA foreign_keys=ON')!
sql db {
create table Member
create table Team
}!
team := Team{
name: 'unassigned'
}
sql db {
insert team into Team
}!
rows := db.exec('select member_id from Team where id = 1')!
assert rows.len == 1
assert rows[0].vals[0] == ''
invalid_team := Team{
name: 'invalid'
member_id: 0
}
mut failed := false
sql db {
insert invalid_team into Team
} or { failed = true }
assert failed
}
+89
View File
@@ -0,0 +1,89 @@
// vtest retry: 3
import db.sqlite
struct Account {
id int @[primary; sql: serial]
name string
}
struct Note {
id int @[primary; sql: serial]
content string
}
fn test_catch_table_is_not_created() {
mut db := sqlite.connect(':memory:')!
mut is_inserted := true
account := Account{}
sql db {
insert account into Account
} or { is_inserted = false }
assert !is_inserted
}
fn test_catch_one_of_queries() {
mut db := sqlite.connect(':memory:')!
sql db {
create table Account
}!
account := Account{}
sql db {
insert account into Account
}!
mut are_updated := true
sql db {
update Account set name = 'test' where id == 1
update Note set content = 'test' where id == 1
} or { are_updated = false }
assert !are_updated
}
fn test_print_results() {
mut db := sqlite.connect(':memory:')!
sql db {
create table Account
}!
account := Account{}
sql db {
insert account into Account
}!
i := sql db {
insert account into Account
}!
println(i)
count := sql db {
select count from Account
}!
println(count)
user := sql db {
select from Account
}!.first()
println(user)
users := sql db {
select from Account
}!
println(users)
assert true
}
+74
View File
@@ -0,0 +1,74 @@
// vtest retry: 3
// vtest build: present_sqlite3?
import db.sqlite
import orm
import time
@[table: 'save_users']
struct SaveUser {
mut:
id int @[primary; sql: serial]
name string
age int
nickname ?string
updated_at time.Time
}
fn test_save_updates_all_mapped_fields_using_primary_key() {
mut db := sqlite.connect(':memory:') or { panic(err) }
defer {
db.close() or {}
}
sql db {
create table SaveUser
}!
first := SaveUser{
name: 'Alice'
age: 30
nickname: 'ally'
updated_at: time.unix(100)
}
second := SaveUser{
name: 'Bob'
age: 28
nickname: 'b'
updated_at: time.unix(150)
}
sql db {
insert first into SaveUser
insert second into SaveUser
}!
mut selected := sql db {
select from SaveUser where name == 'Alice'
}!
assert selected.len == 1
mut updated := selected[0]
updated.name = 'Alice Updated'
updated.age = 31
updated.nickname = none
updated.updated_at = time.unix(200)
orm.save(db, updated)!
selected = sql db {
select from SaveUser where id == updated.id
}!
assert selected.len == 1
assert selected[0].id == updated.id
assert selected[0].name == 'Alice Updated'
assert selected[0].age == 31
assert selected[0].nickname == none
assert selected[0].updated_at == time.unix(200)
untouched := sql db {
select from SaveUser where name == 'Bob'
}!
assert untouched.len == 1
assert untouched[0].age == 28
assert untouched[0].nickname or { '' } == 'b'
}
+442
View File
@@ -0,0 +1,442 @@
module orm
import time
// DataScope provides per-instance request-level data filtering for ORM queries.
// It works with both `sql` block syntax and orm_func (QueryBuilder).
//
// Use `orm.new_db(raw_conn, scope)` to create a scoped connection, then pass it
// to `sql db { ... }` blocks or `orm.new_query[T](db)`. Call `db.unscoped()` or
// `db.unscoped('field')` to selectively skip scope filters.
// QueryFilterMode describes whether a DataScope filter has a stable SQL shape or
// needs runtime handling.
pub enum QueryFilterMode {
unset // .static is not yet implemented — use .dynamic explicitly
static
dynamic
}
fn table_ignores_data_scope(table Table) bool {
for attr in table.attrs {
if attr_name_matches(attr.name, 'unscoped') {
return true
}
}
return false
}
// DB implements orm.Connection with DataScope support.
// When the wrapped connection also implements TransactionalConnection,
// the DB will transparently proxy transaction methods (orm_begin, orm_commit, ...).
pub struct DB {
mut:
conn Connection
pub:
scope DataScope
skip_all_scopes bool
skip_fields []string // specific scope filter fields to skip, when skip_all_scopes is false
}
// DataScope holds the per-connection data scope configuration for automatic filtering.
pub struct DataScope {
pub:
enabled bool = true
filters []QueryFilter
}
// QueryFilter represents a single filter condition in a DataScope.
// `field` should normally be a struct field name rather than a SQL column name.
// When `Table.fields`/`Table.columns` metadata is available, it is resolved to the
// corresponding SQL column name at query time. If that metadata is unavailable,
// the ORM may fall back to using `field` directly as the SQL column name. In
// metadata-driven paths, unresolved fields are skipped for that table.
// `mode` must be explicitly set to .static or .dynamic. Static filters are
// reserved for future compiler-generated scope clauses. The runtime DB wrapper
// applies only filters explicitly marked with .dynamic.
pub struct QueryFilter {
pub:
field string
value Primitive
operator OperationKind = .eq
mode QueryFilterMode // must be explicitly set to .static or .dynamic
}
// new_db creates a new DB with DataScope applied.
pub fn new_db(conn Connection, scope DataScope) DB {
return DB{
conn: conn
scope: scope
skip_all_scopes: false
skip_fields: []
}
}
// unscoped returns a new DB with the specified fields excluded from DataScope filtering.
// Call without arguments to skip ALL scope filters.
pub fn (db DB) unscoped(unscoped_fields ...string) DB {
if unscoped_fields.len == 0 {
return DB{
conn: db.conn
scope: db.scope
skip_all_scopes: true
skip_fields: []
}
}
return DB{
conn: db.conn
scope: db.scope
skip_all_scopes: false
skip_fields: unscoped_fields.map(it)
}
}
// table_field_to_column_map builds an O(1) lookup from struct field names
// to SQL column names.
fn table_field_to_column_map(table Table) map[string]string {
mut m := map[string]string{}
if table.columns.len > 0 && table.columns.len == table.fields.len {
for j, field_name in table.fields {
m[field_name] = table.columns[j]
}
}
return m
}
// apply_data_scope applies DataScope filters to a WHERE QueryData and returns the scoped query data.
pub fn apply_data_scope(scope DataScope, table Table, where QueryData, scope_skip_fields []string, has_joins bool) !QueryData {
return apply_scope_filters(scope, table, where, scope_skip_fields, has_joins)
}
// apply_data_scope_insert applies DataScope filters to an INSERT QueryData and returns the scoped query data.
pub fn apply_data_scope_insert(scope DataScope, table Table, data QueryData, scope_skip_fields []string) !QueryData {
return apply_scope_insert_filters(scope, table, data, scope_skip_fields)
}
// apply_scope_filters applies DataScope filters to WHERE data. It wraps original
// conditions in parentheses and appends is_and / kinds markers.
// When has_joins is true, scope filter column names are qualified with table.name
// to avoid ambiguity in JOIN queries where joined tables share column names.
fn apply_scope_filters(scope DataScope, table Table, qd QueryData, scope_skip_fields []string, has_joins bool) !QueryData {
if !scope.enabled || scope.filters.len == 0 {
return qd
}
if table_ignores_data_scope(table) {
return qd
}
mut result := clone_query_data(qd)
field_to_column := table_field_to_column_map(table)
// Wrap original WHERE clause in parentheses once, before adding scope filters
if result.fields.len > 1 {
result.parentheses << [0, result.fields.len - 1]
}
for filter in scope.filters {
if filter.mode == .unset {
return error('orm.DataScope: QueryFilter.mode must be explicitly set. .static is not yet implemented — use .dynamic. Got .unset for field `${filter.field}`')
}
if filter.mode != .dynamic {
continue
}
if filter.field == '' {
return error('orm.DataScope: dynamic filter field must not be empty')
}
if filter.field in scope_skip_fields {
continue
}
// Note: we do NOT skip when filter.field is already in result.fields.
// The scope filter is always appended as an additional AND condition.
// This prevents a user from bypassing tenant isolation by including the
// scoped field in their own WHERE clause. The resolved SQL column name is
// also appended without deduplication for the same reason.
if table.fields.len > 0 && filter.field !in table.fields {
continue
}
if !filter_value_matches_operator(filter) {
return invalid_scope_filter_error(filter)
}
// Resolve SQL column name from struct field name (O(1) via lookup map)
mut column_name := filter.field
if resolved := field_to_column[filter.field] {
column_name = resolved
}
// Qualify with table name when joins are present to avoid ambiguity
if has_joins && table.name != '' {
column_name = table_qualified_field(table.name, column_name)
}
// Note: we do NOT skip when column_name is already in result.fields.
// The scope filter is always appended as an additional AND condition
// to prevent bypassing tenant isolation.
result.is_and << true
result.fields << column_name.clone()
if !filter.operator.is_unary() {
result.data << filter.value
result.types << primitive_type(filter.value)
}
result.kinds << filter.operator
}
return result
}
fn invalid_scope_filter_error(filter QueryFilter) IError {
if filter.operator in [.in, .not_in] {
return error('orm.DataScope: dynamic filter `${filter.field}` with `${filter.operator}` requires a non-empty array value')
}
return error('orm.DataScope: dynamic filter `${filter.field}` with `${filter.operator}` requires a scalar value')
}
fn filter_value_matches_operator(filter QueryFilter) bool {
array_len := primitive_array_len(filter.value)
if filter.operator in [.in, .not_in] {
return array_len > 0
}
return array_len < 0
}
fn apply_scope_insert_filters(scope DataScope, table Table, data QueryData, scope_skip_fields []string) !QueryData {
if !scope.enabled || scope.filters.len == 0 {
return data
}
if table_ignores_data_scope(table) {
return data
}
mut result := clone_query_data(data)
original_field_count := data.fields.len
field_to_column := table_field_to_column_map(table)
for filter in scope.filters {
if filter.mode == .unset {
return error('orm.DataScope: QueryFilter.mode must be explicitly set. .static is not yet implemented — use .dynamic. Got .unset for field `${filter.field}`')
}
if filter.mode != .dynamic {
continue
}
if filter.field == '' {
return error('orm.DataScope: dynamic filter field must not be empty')
}
if filter.field in scope_skip_fields {
continue
}
if table.fields.len > 0 && filter.field !in table.fields {
continue
}
if !filter_value_matches_operator(filter) {
return invalid_scope_filter_error(filter)
}
if filter.operator == .is_null {
continue
}
if filter.operator != .eq {
return error('orm.DataScope: dynamic filter `${filter.field}` with `${filter.operator}` cannot be applied to INSERT')
}
mut column_name := filter.field
if resolved := field_to_column[filter.field] {
column_name = resolved
}
field_index := result.fields.index(column_name)
if field_index >= 0 {
if result.batch_rows > 0 {
if field_index < original_field_count {
// Original field — data is per-row with original_field_count stride
for row in 0 .. result.batch_rows {
data_index := row * original_field_count + field_index
if data_index < result.data.len {
result.data[data_index] = filter.value
}
if data_index < result.types.len {
result.types[data_index] = primitive_type(filter.value)
}
}
} else {
// Scope field appended by a previous filter — single value at the end
data_index := original_field_count * result.batch_rows +
(field_index - original_field_count)
if data_index < result.data.len {
result.data[data_index] = filter.value
}
if data_index < result.types.len {
result.types[data_index] = primitive_type(filter.value)
}
}
} else {
// Single row — stride is irrelevant; directly index by field position
if field_index < result.data.len {
result.data[field_index] = filter.value
}
if field_index < result.types.len {
result.types[field_index] = primitive_type(filter.value)
}
}
continue
}
result.fields << column_name.clone()
result.data << filter.value
result.types << primitive_type(filter.value)
}
if result.batch_rows > 0 {
scope_field_count := result.fields.len - original_field_count
if scope_field_count > 0 {
mut new_data := []Primitive{cap: result.fields.len * result.batch_rows}
scope_data_start := original_field_count * result.batch_rows
for row in 0 .. result.batch_rows {
for col in 0 .. original_field_count {
new_data << result.data[row * original_field_count + col]
}
for s in 0 .. scope_field_count {
new_data << result.data[scope_data_start + s]
}
}
result.data = new_data
}
}
return result
}
fn primitive_is_array(value Primitive) bool {
return primitive_array_len(value) >= 0
}
fn primitive_array_len(value Primitive) int {
return match value {
[]Primitive, []bool, []f32, []f64, []i16, []i64, []i8, []int, []string, []time.Time, []u16,
[]u32, []u64, []u8, []InfixType {
value.len
}
else {
-1
}
}
}
// DB implements orm.Connection ------------------------------------------------
// select fetches rows through the wrapped connection, with DataScope applied.
pub fn (mut db DB) select(config SelectConfig, data QueryData, where QueryData) ![][]Primitive {
mut cfg := config
if db.scope.enabled && db.scope.filters.len > 0 && !db.skip_all_scopes
&& !table_ignores_data_scope(cfg.table) {
where_scoped := apply_data_scope(db.scope, cfg.table, where, db.skip_fields,
cfg.joins.len > 0)!
if where_scoped.fields.len > where.fields.len {
cfg.has_where = true
}
return db.conn.select(cfg, data, where_scoped)
}
return db.conn.select(cfg, data, where)
}
// insert inserts rows through the wrapped connection, with DataScope applied.
pub fn (mut db DB) insert(table Table, data QueryData) ! {
mut data_scoped := data
if db.scope.enabled && db.scope.filters.len > 0 && !db.skip_all_scopes
&& !table_ignores_data_scope(table) {
data_scoped = apply_data_scope_insert(db.scope, table, data, db.skip_fields)!
}
return db.conn.insert(table, data_scoped)
}
// update updates rows through the wrapped connection, with DataScope applied.
pub fn (mut db DB) update(table Table, data QueryData, where QueryData) ! {
mut where_scoped := where
if db.scope.enabled && db.scope.filters.len > 0 && !db.skip_all_scopes
&& !table_ignores_data_scope(table) {
where_scoped = apply_data_scope(db.scope, table, where, db.skip_fields, false)!
}
return db.conn.update(table, data, where_scoped)
}
// delete deletes rows through the wrapped connection, with DataScope applied.
pub fn (mut db DB) delete(table Table, where QueryData) ! {
mut where_scoped := where
if db.scope.enabled && db.scope.filters.len > 0 && !db.skip_all_scopes
&& !table_ignores_data_scope(table) {
where_scoped = apply_data_scope(db.scope, table, where, db.skip_fields, false)!
}
return db.conn.delete(table, where_scoped)
}
// create creates a table through the wrapped connection.
pub fn (mut db DB) create(table Table, fields []TableField) ! {
return db.conn.create(table, fields)
}
// drop drops a table through the wrapped connection.
pub fn (mut db DB) drop(table Table) ! {
return db.conn.drop(table)
}
// last_id returns the last inserted id from the wrapped connection.
pub fn (mut db DB) last_id() int {
return db.conn.last_id()
}
// DB implements orm.TransactionalConnection (decorator) -----------------------
// unwrap_to_tx extracts a TransactionalConnection from a Connection interface.
fn unwrap_to_tx(mut conn Connection) TransactionalConnection {
return conn as TransactionalConnection
}
// orm_begin begins a transaction on the underlying connection.
// Returns an error if the underlying connection does not support transactions.
pub fn (mut db DB) orm_begin() ! {
if db.conn is TransactionalConnection {
mut tc := unwrap_to_tx(mut db.conn)
tc.orm_begin()!
} else {
return error('orm.DB: underlying connection does not support transactions')
}
}
// orm_commit commits the current transaction on the underlying connection.
// Returns an error if the underlying connection does not support transactions.
pub fn (mut db DB) orm_commit() ! {
if db.conn is TransactionalConnection {
mut tc := unwrap_to_tx(mut db.conn)
tc.orm_commit()!
} else {
return error('orm.DB: underlying connection does not support transactions')
}
}
// orm_rollback rolls back the current transaction on the underlying connection.
// Returns an error if the underlying connection does not support transactions.
pub fn (mut db DB) orm_rollback() ! {
if db.conn is TransactionalConnection {
mut tc := unwrap_to_tx(mut db.conn)
tc.orm_rollback()!
} else {
return error('orm.DB: underlying connection does not support transactions')
}
}
// orm_savepoint creates a savepoint with the given name on the underlying connection.
// Returns an error if the underlying connection does not support transactions.
pub fn (mut db DB) orm_savepoint(name string) ! {
if db.conn is TransactionalConnection {
mut tc := unwrap_to_tx(mut db.conn)
tc.orm_savepoint(name)!
} else {
return error('orm.DB: underlying connection does not support transactions')
}
}
// orm_rollback_to rolls back to the named savepoint on the underlying connection.
// Returns an error if the underlying connection does not support transactions.
pub fn (mut db DB) orm_rollback_to(name string) ! {
if db.conn is TransactionalConnection {
mut tc := unwrap_to_tx(mut db.conn)
tc.orm_rollback_to(name)!
} else {
return error('orm.DB: underlying connection does not support transactions')
}
}
// orm_release_savepoint releases the named savepoint on the underlying connection.
// Returns an error if the underlying connection does not support transactions.
pub fn (mut db DB) orm_release_savepoint(name string) ! {
if db.conn is TransactionalConnection {
mut tc := unwrap_to_tx(mut db.conn)
tc.orm_release_savepoint(name)!
} else {
return error('orm.DB: underlying connection does not support transactions')
}
}
File diff suppressed because it is too large. Load diff
@@ -0,0 +1,93 @@
import db.sqlite
struct Child {
id int @[primary; serial]
parent_id int
}
struct PlainNoArg {
id int @[primary; serial; sql: 'custom_id']
name string
children []Child @[fkey: 'parent_id']
}
fn test_plain_no_arg() {
mut db := sqlite.connect(':memory:')!
defer { db.close() or {} }
sql db {
create table PlainNoArg
}!
first := PlainNoArg{
name: 'first'
}
second := PlainNoArg{
name: 'second'
}
sql db {
insert first into PlainNoArg
insert second into PlainNoArg
}!
rows := sql db {
select from PlainNoArg order by id desc
}!
assert rows[0].id == 2
assert rows[0].name == 'second'
}
fn test_plain_no_arg_with_children() {
mut db := sqlite.connect(':memory:')!
parent := PlainNoArg{
children: [Child{}, Child{}]
}
sql db {
create table Child
create table PlainNoArg
insert parent into PlainNoArg
}!
parents := sql db {
select from PlainNoArg limit 1
}!
assert parent.children.len == parents[0].children.len
}
struct SqlSerial {
id int @[primary; sql: serial]
name string
}
fn test_sql_serial() {
mut db := sqlite.connect(':memory:')!
defer { db.close() or {} }
sql db {
create table SqlSerial
}!
first := SqlSerial{
name: 'first'
}
second := SqlSerial{
name: 'second'
}
sql db {
insert first into SqlSerial
insert second into SqlSerial
}!
rows := sql db {
select from SqlSerial order by id desc
}!
assert rows[0].id == 2
assert rows[0].name == 'second'
}
+108
View File
@@ -0,0 +1,108 @@
// vtest retry: 3
import os
import db.sqlite
struct User {
id i64 @[primary; sql: serial]
name string @[unique]
}
const db_folder = os.join_path(os.vtmp_dir(), 'orm_sql')
const db_path = os.join_path(db_folder, 'sql_statement_or_blocks.db')
fn testsuite_begin() {
os.mkdir_all(db_folder) or {}
}
fn testsuite_end() {
os.rmdir_all(db_folder) or {}
}
fn test_ensure_db_exists_and_user_table_is_ok() {
mut db := sqlite.connect(db_path)!
assert true
eprintln('> drop pre-existing User table...')
db.exec('drop table if exists User')!
eprintln('> creating User table...')
sql db {
create table User
} or { panic(err) }
assert true
db.close()!
}
fn test_sql_or_block_for_insert() {
mut db := sqlite.connect(db_path)!
user := User{1, 'bilbo'}
eprintln('> inserting user 1 (first try)...')
mut is_user_inserted := true
sql db {
insert user into User
} or {
println('user should have been inserted, but could not, err: ${err}')
is_user_inserted = false
}
assert is_user_inserted
eprintln('> inserting user 1 (second try)...')
sql db {
insert user into User
} or {
println('user could not be inserted, err: ${err}')
is_user_inserted = false
}
assert !is_user_inserted
eprintln('LINE: ${@LINE}')
db.close()!
}
fn test_sql_or_block_for_select() {
mut db := sqlite.connect(db_path)!
eprintln('> selecting user with id 1...')
mut users := sql db {
select from User where id == 1
} or {
eprintln('could not select user, err: ${err}')
[]User{}
}
eprintln('LINE: ${@LINE}')
single := users.first()
assert single.id == 1
users = sql db {
select from User where id == 0
} or {
eprintln('could not select user, err: ${err}')
[]User{}
}
eprintln('LINE: ${@LINE}')
assert users.len == 0
eprintln('LINE: ${@LINE}')
eprintln('> selecting users...')
multiple := sql db {
select from User
} or {
eprintln('could not users, err: ${err}')
[]User{}
}
eprintln('LINE: ${@LINE}')
assert multiple.len == 1
eprintln('LINE: ${@LINE}')
db.close()!
}
fn test_finish() {
eprintln('done')
assert true
}
@@ -0,0 +1,37 @@
// vtest retry: 3
import db.sqlite
struct User {
id int @[primary; sql: serial]
name string
}
fn test_string_interpolation() {
mut db := sqlite.connect(':memory:') or { panic(err) }
sql db {
create table User
}!
user_suffix := '_user'
first_user := User{
name: 'first${user_suffix}'
}
second_user := User{
name: 'second${user_suffix}'
}
sql db {
insert first_user into User
insert second_user into User
}!
users := sql db {
select from User where name == 'first${user_suffix}'
}!
assert users.len == 1
assert users.first().name == 'first${user_suffix}'
}
@@ -0,0 +1,26 @@
import db.sqlite
struct SomeStruct {
foo int
bar string
}
struct OtherStruct {
baz f64
}
type SomeSum = SomeStruct | OtherStruct
fn test_sum_type_insert() {
db := sqlite.connect(':memory:')!
sql db {
create table SomeStruct
}!
some := SomeSum(SomeStruct{})
if some is SomeStruct {
sql db {
insert some into SomeStruct
}!
}
}
+550
View File
@@ -0,0 +1,550 @@
// vtest retry: 3
// vtest build: present_sqlite3? && !windows
// import db.mysql
// import db.pg
import orm
import time
import db.sqlite
const offset_const = 2
@[index: 'name, nr_downloads']
struct Module {
id int @[primary; sql: serial]
name string @[index]
nr_downloads int
test_id u64 @[index]
user ?User
created time.Time
}
@[table: 'userlist']
struct User {
id int @[primary; sql: serial]
age int
name string @[default: 'name'; sql: 'username']
is_customer bool
skipped_string string @[skip]
skipped_string2 string @[sql: '-']
skipped_array []string @[skip]
skipped_array2 []string @[sql: '-']
}
struct Foo {
age int
}
struct TestTime {
id int @[primary; sql: serial]
create time.Time
}
struct SelectTransformUser {
id int @[primary; sql: serial]
name string @[sql_select: 'upper(name)']
}
fn test_use_struct_field_as_limit() {
db := sqlite.connect(':memory:') or { panic(err) }
sql db {
create table User
}!
foo := Foo{
age: 10
}
sam := User{
age: 29
name: 'Sam'
skipped_string2: 'this should be ignored'
skipped_array: ['ignored', 'array']
skipped_array2: ['another', 'ignored', 'array']
}
sql db {
insert sam into User
}!
users := sql db {
select from User limit foo.age
}!
assert users.len == 1
assert users[0].name == 'Sam'
assert users[0].age == 29
assert users[0].skipped_string == ''
assert users[0].skipped_string2 == ''
assert users[0].skipped_array == [], 'skipped because of the @[skip] tag, used for both sql and json'
assert users[0].skipped_array2 == [], "should be skipped, because of the sql specific @[sql: '-'] tag"
}
fn test_orm_sql_select_attribute() {
mut db := sqlite.connect(':memory:') or { panic(err) }
defer {
db.close() or {}
}
sql db {
create table SelectTransformUser
}!
alice := SelectTransformUser{
name: 'Alice'
}
sql db {
insert alice into SelectTransformUser
}!
rows := sql db {
select from SelectTransformUser where id == 1
}!
assert rows.len == 1
assert rows[0].name == 'ALICE'
mut qb := orm.new_query[SelectTransformUser](db)
qb_rows := qb.query()!
assert qb_rows.len == 1
assert qb_rows[0].name == 'ALICE'
}
fn test_orm_select_specific_fields() {
mut db := sqlite.connect(':memory:') or { panic(err) }
defer {
db.close() or {}
}
sql db {
create table User
create table SelectTransformUser
}!
sam := User{
age: 29
name: 'Sam'
}
alice := SelectTransformUser{
name: 'Alice'
}
sql db {
insert sam into User
insert alice into SelectTransformUser
}!
users := sql db {
select id, name from User where name == 'Sam'
}!
assert users.len == 1
assert users[0].id == 1
assert users[0].name == 'Sam'
assert users[0].age == 0
assert users[0].is_customer == false
transformed := sql db {
select name from SelectTransformUser where id == 1
}!
assert transformed.len == 1
assert transformed[0].name == 'ALICE'
assert transformed[0].id == 0
}
fn test_orm_order_by_explicit_asc() {
mut db := sqlite.connect(':memory:')!
defer {
db.close() or { panic(err) }
}
sql db {
create table User
}!
users := [
User{
age: 31
name: 'Alice'
},
User{
age: 19
name: 'Bob'
},
User{
age: 44
name: 'Charlie'
},
]
for user in users {
sql db {
insert user into User
}!
}
// vfmt off
rows := sql db {
select from User order by age asc limit 2
}!
// vfmt on
assert rows.len == 2
assert rows[0].name == 'Bob'
assert rows[0].age == 19
assert rows[1].name == 'Alice'
assert rows[1].age == 31
}
fn test_orm() {
db := sqlite.connect(':memory:') or { panic(err) }
sql db {
create table Module
}!
sql db {
create table User
}!
name := 'Peter'
sam := User{
age: 29
name: 'Sam'
}
peter := User{
age: 31
name: 'Peter'
}
k := User{
age: 30
name: 'Kate'
is_customer: true
}
sql db {
insert sam into User
insert peter into User
insert k into User
}!
c := sql db {
select count from User where id != 1
}!
assert c == 2
nr_all_users := sql db {
select count from User
}!
assert nr_all_users == 3
nr_users1 := sql db {
select count from User where id == 1
}!
assert nr_users1 == 1
nr_peters := sql db {
select count from User where id == 2 && name == 'Peter'
}!
assert nr_peters == 1
nr_peters2 := sql db {
select count from User where id == 2 && name == name
}!
assert nr_peters2 == 1
nr_peters3 := sql db {
select count from User where name == name
}!
assert nr_peters3 == 1
peters := sql db {
select from User where name == name
}!
assert peters.len == 1
assert peters[0].name == 'Peter'
mut users := sql db {
select from User where name == name limit 1
}!
one_peter := users.first()
assert one_peter.name == 'Peter'
assert one_peter.id == 2
users = sql db {
select from User where id == 1
}!
user := users.first()
assert user.name == 'Sam'
assert user.id == 1
assert user.age == 29
users = sql db {
select from User where id > 0
}!
assert users.len == 3
assert users[0].name == 'Sam'
assert users[1].name == 'Peter'
assert users[1].age == 31
users2 := sql db {
select from User where id < 0
}!
assert users2.len == 0
users3 := sql db {
select from User where age == 29 || age == 31
}!
assert users3.len == 2
assert users3[0].age == 29
assert users3[1].age == 31
new_user := User{
name: 'New user'
age: 30
}
sql db {
insert new_user into User
}!
users = sql db {
select from User where id == 4
}!
x := users.first()
assert x.age == 30
assert x.id == 4
assert x.name == 'New user'
users = sql db {
select from User where id == 3
}!
kate := users.first()
assert kate.is_customer == true
users = sql db {
select from User where is_customer == true limit 1
}!
customer := users.first()
assert customer.is_customer == true
assert customer.name == 'Kate'
sql db {
update User set age = 31 where name == 'Kate'
}!
users = sql db {
select from User where id == 3
}!
kate2 := users.first()
assert kate2.age == 31
assert kate2.name == 'Kate'
sql db {
update User set age = 32, name = 'Kate N' where name == 'Kate'
}!
users = sql db {
select from User where id == 3
}!
mut kate3 := users.first()
assert kate3.age == 32
assert kate3.name == 'Kate N'
new_age := 33
sql db {
update User set age = new_age, name = 'Kate N' where id == 3
}!
users = sql db {
select from User where id == 3
}!
kate3 = users.first()
assert kate3.age == 33
assert kate3.name == 'Kate N'
foo := Foo{34}
sql db {
update User set age = foo.age, name = 'Kate N' where id == 3
}!
users = sql db {
select from User where id == 3
}!
kate3 = users.first()
assert kate3.age == 34
assert kate3.name == 'Kate N'
no_user := sql db {
select from User where id == 30
}!
assert no_user.len == 0
two_users := sql db {
select from User limit 2
}!
assert two_users.len == 2
assert two_users[0].id == 1
y := sql db {
select from User limit 2 offset 1
}!
assert y.len == 2
assert y[0].id == 2
z := sql db {
select from User order by id limit 2 offset offset_const
}!
assert z.len == 2
assert z[0].id == 3
users = sql db {
select from User order by age desc limit 1
}!
oldest := users.first()
assert oldest.age == 34
offs := 1
users = sql db {
select from User order by age desc limit 1 offset offs
}!
second_oldest := users.first()
assert second_oldest.age == 31
sql db {
delete from User where age == 34
}!
users = sql db {
select from User order by age desc limit 1
}!
updated_oldest := users.first()
assert updated_oldest.age == 31
// Remove this when pg is used
// db.exec('insert into User (name, age) values (NULL, 31)')
users = sql db {
select from User where id == 5
}!
assert users.len == 0
users = sql db {
select from User where id == 1
}!
age_test := users.first()
assert age_test.age == 29
sql db {
update User set age = age + 1 where id == 1
}!
users = sql db {
select from User where id == 1
}!
mut first := users.first()
assert first.age == 30
sql db {
update User set age = age * 2 where id == 1
}!
users = sql db {
select from User where id == 1
}!
first = users.first()
assert first.age == 60
sql db {
create table TestTime
}!
tnow := time.now()
time_test := TestTime{
create: tnow
}
sql db {
insert time_test into TestTime
}!
data := sql db {
select from TestTime where create == tnow
}!
assert data.len == 1
assert tnow.unix() == data[0].create.unix()
mod := Module{}
sql db {
insert mod into Module
}!
sql db {
update Module set test_id = 11 where id == 1
}!
mut modules := sql db {
select from Module where id == 1
}!
assert modules.first().test_id == 11
t := time.now()
sql db {
update Module set created = t where id == 1
}!
modules = sql db {
select from Module where id == 1
}!
// Note: usually updated_time_mod.created != t, because t has
// its microseconds set, while the value retrieved from the DB
// has them zeroed, because the db field resolution is seconds.
// Note: the database also stores the time in UTC, so the
// comparison must be done on the unix timestamp.
assert modules.first().created.unix() == t.unix()
users = sql db {
select from User where (name == 'Sam' && is_customer == true) || id == 1
}!
assert users.first() == first
sql db {
drop table Module
drop table TestTime
}!
}
fn test_distinct() {
db := sqlite.connect(':memory:') or { panic(err) }
// Create table without unique constraints to allow true duplicates
db.exec('CREATE TABLE items (name TEXT, category TEXT)')!
db.exec("INSERT INTO items VALUES ('Apple', 'Fruit'), ('Apple', 'Fruit'), ('Banana', 'Fruit')")!
without_distinct := db.exec('SELECT name, category FROM items')!
assert without_distinct.len == 3
with_distinct := db.exec('SELECT DISTINCT name, category FROM items')!
assert with_distinct.len == 2
}
+302
View File
@@ -0,0 +1,302 @@
// vtest build: present_sqlite3? && !sanitize-memory-clang
import db.sqlite
import orm
struct TxUser {
id int @[primary; sql: serial]
name string
}
fn setup_tx_db() !sqlite.DB {
mut db := sqlite.connect(':memory:')!
sql db {
create table TxUser
}!
return db
}
fn insert_callback_user(mut tx orm.Tx) !int {
user := TxUser{
name: 'callback_commit'
}
sql tx {
insert user into TxUser
}!
return tx.last_id()
}
fn insert_and_fail(mut tx orm.Tx) !int {
user := TxUser{
name: 'callback_rollback'
}
sql tx {
insert user into TxUser
}!
return error('force rollback')
}
fn nested_success_inner(mut tx orm.Tx) !int {
user := TxUser{
name: 'nested_success'
}
sql tx {
insert user into TxUser
}!
return tx.last_id()
}
fn nested_failure_inner(mut tx orm.Tx) !int {
user := TxUser{
name: 'nested_failure'
}
sql tx {
insert user into TxUser
}!
return error('nested failure')
}
fn count_tx_users(db sqlite.DB) !int {
return db.q_int('select count(*) from TxUser')!
}
fn tx_user_names(db sqlite.DB) ![]string {
rows := sql db {
select from TxUser order by id
}!
return rows.map(it.name)
}
fn test_transaction_callback_commits_on_success() {
mut db := setup_tx_db()!
defer {
db.close() or {}
}
id := orm.transaction[int](mut db, insert_callback_user)!
assert id == 1
assert count_tx_users(db)! == 1
assert tx_user_names(db)! == ['callback_commit']
}
fn test_transaction_callback_rolls_back_on_error() {
mut db := setup_tx_db()!
defer {
db.close() or {}
}
orm.transaction[int](mut db, insert_and_fail) or {
assert err.msg() == 'force rollback'
assert count_tx_users(db)! == 0
return
}
assert false
}
fn test_manual_begin_with_sql_tx_queries() {
mut db := setup_tx_db()!
defer {
db.close() or {}
}
mut tx := orm.begin(mut db)!
user := TxUser{
name: 'manual'
}
sql tx {
insert user into TxUser
}!
inserted := sql tx {
select from TxUser where name == 'manual'
}!
assert inserted.len == 1
assert inserted[0].name == 'manual'
sql tx {
update TxUser set name = 'manual_updated' where name == 'manual'
}!
updated := sql tx {
select from TxUser where name == 'manual_updated'
}!
assert updated.len == 1
sql tx {
delete from TxUser where name == 'manual_updated'
}!
assert count_tx_users(db)! == 0
tx.commit()!
}
fn test_query_builder_works_inside_transaction() {
mut db := setup_tx_db()!
defer {
db.close() or {}
}
mut tx := orm.begin(mut db)!
mut qb := orm.new_query[TxUser](tx)
qb.insert(TxUser{
name: 'qb_user'
})!
selected := qb.where('name = ?', 'qb_user')!.query()!
assert selected.len == 1
assert selected[0].name == 'qb_user'
tx.commit()!
assert tx_user_names(db)! == ['qb_user']
}
fn test_nested_transaction_success_releases_savepoint() {
mut db := setup_tx_db()!
defer {
db.close() or {}
}
orm.transaction[int](mut db, fn (mut tx orm.Tx) !int {
before := TxUser{
name: 'outer_before'
}
sql tx {
insert before into TxUser
}!
nested_id := tx.transaction[int](nested_success_inner)!
after := TxUser{
name: 'outer_after'
}
sql tx {
insert after into TxUser
}!
return nested_id
})!
assert tx_user_names(db)! == ['outer_before', 'nested_success', 'outer_after']
}
fn test_nested_transaction_failure_rolls_back_inner_work_only() {
mut db := setup_tx_db()!
defer {
db.close() or {}
}
orm.transaction[int](mut db, fn (mut tx orm.Tx) !int {
before := TxUser{
name: 'outer_before'
}
sql tx {
insert before into TxUser
}!
tx.transaction[int](nested_failure_inner) or {}
after := TxUser{
name: 'outer_after'
}
sql tx {
insert after into TxUser
}!
return 0
})!
assert tx_user_names(db)! == ['outer_before', 'outer_after']
}
fn test_manual_savepoint_rollback_and_release() {
mut db := setup_tx_db()!
defer {
db.close() or {}
}
mut tx := orm.begin(mut db)!
before := TxUser{
name: 'before_savepoint'
}
sql tx {
insert before into TxUser
}!
mut sp := tx.savepoint()!
rollback_user := TxUser{
name: 'rollback_me'
}
sql tx {
insert rollback_user into TxUser
}!
sp.rollback()!
after := TxUser{
name: 'after_rollback'
}
sql tx {
insert after into TxUser
}!
mut sp2 := tx.savepoint()!
released := TxUser{
name: 'keep_me'
}
sql tx {
insert released into TxUser
}!
sp2.release()!
tx.commit()!
assert tx_user_names(db)! == ['before_savepoint', 'after_rollback', 'keep_me']
}
fn test_inactive_transaction_errors() {
mut db := setup_tx_db()!
defer {
db.close() or {}
}
mut tx := orm.begin(mut db)!
tx.commit()!
tx.commit() or { assert err.msg().contains('inactive') }
user := TxUser{
name: 'after_close'
}
sql tx {
insert user into TxUser
} or {
assert err.msg().contains('inactive')
return
}
assert false
}
fn test_inactive_savepoint_errors() {
mut db := setup_tx_db()!
defer {
db.close() or {}
}
mut tx := orm.begin(mut db)!
mut sp := tx.savepoint()!
sp.release()!
sp.rollback() or {
assert err.msg().contains('inactive')
tx.rollback()!
return
}
assert false
}
fn test_savepoint_becomes_inactive_when_parent_transaction_closes() {
mut db := setup_tx_db()!
defer {
db.close() or {}
}
mut tx := orm.begin(mut db)!
mut sp := tx.savepoint()!
tx.commit()!
sp.release() or {
assert err.msg().contains('inactive')
return
}
assert false
}
+145
View File
@@ -0,0 +1,145 @@
// vtest retry: 3
// vtest build: present_sqlite3?
import db.sqlite
@[table: 'upsert_users']
struct UpsertUser {
mut:
id int @[primary; sql: serial]
username string @[unique]
age int
}
@[table: 'upsert_configs']
@[unique_key: 'scope, key']
struct UpsertConfig {
mut:
id int @[primary; sql: serial]
scope string
key string
value string
}
@[table: 'ambiguous_upsert_users']
struct AmbiguousUpsertUser {
mut:
id int @[primary]
username string @[unique]
note string
}
fn test_upsert_updates_existing_row_using_unique_field() {
mut db := sqlite.connect(':memory:') or { panic(err) }
defer {
db.close() or {}
}
sql db {
create table UpsertUser
}!
first := UpsertUser{
username: 'alice'
age: 30
}
second := UpsertUser{
username: 'alice'
age: 31
}
sql db {
upsert first into UpsertUser
upsert second into UpsertUser
}!
rows := sql db {
select from UpsertUser where username == 'alice'
}!
assert rows.len == 1
assert rows[0].username == 'alice'
assert rows[0].age == 31
assert rows[0].id == 1
}
fn test_upsert_updates_existing_row_using_composite_unique_key() {
mut db := sqlite.connect(':memory:') or { panic(err) }
defer {
db.close() or {}
}
sql db {
create table UpsertConfig
}!
first := UpsertConfig{
scope: 'web'
key: 'theme'
value: 'light'
}
second := UpsertConfig{
scope: 'web'
key: 'theme'
value: 'dark'
}
sql db {
upsert first into UpsertConfig
upsert second into UpsertConfig
}!
rows := sql db {
select from UpsertConfig where scope == 'web' && key == 'theme'
}!
assert rows.len == 1
assert rows[0].value == 'dark'
assert rows[0].id == 1
}
fn test_upsert_errors_when_conflict_groups_match_multiple_rows() {
mut db := sqlite.connect(':memory:') or { panic(err) }
defer {
db.close() or {}
}
sql db {
create table AmbiguousUpsertUser
}!
first := AmbiguousUpsertUser{
id: 1
username: 'alice'
note: 'first'
}
second := AmbiguousUpsertUser{
id: 2
username: 'bob'
note: 'second'
}
sql db {
insert first into AmbiguousUpsertUser
insert second into AmbiguousUpsertUser
}!
ambiguous := AmbiguousUpsertUser{
id: 1
username: 'bob'
note: 'updated'
}
mut got_error := false
sql db {
upsert ambiguous into AmbiguousUpsertUser
} or { got_error = true }
assert got_error
rows := sql db {
select from AmbiguousUpsertUser order by id
}!
assert rows.len == 2
assert rows[0].note == 'first'
assert rows[1].note == 'second'
}
+52
View File
@@ -0,0 +1,52 @@
// vtest build: present_sqlite3? && !sanitize-memory-clang
// vtest flaky: true
// vtest retry: 3
import db.sqlite
struct User {
id int @[primary; sql: serial]
name string
}
fn get_users_in(mut db sqlite.DB, names []string) ![]User {
return sql db {
select from User where name in names
}!
}
fn get_users_not_in(mut db sqlite.DB, names []string) ![]User {
return sql db {
select from User where name !in names
}!
}
fn test_orm_mut_db() {
mut db := sqlite.connect(':memory:') or { panic(err) }
sql db {
create table User
}!
first_user := User{
name: 'first'
}
second_user := User{
name: 'second'
}
sql db {
insert first_user into User
insert second_user into User
}!
in_users := get_users_in(mut db, ['first'])!
assert in_users.len == 1
not_in_users := get_users_not_in(mut db, ['second'])!
assert not_in_users.len == 1
all_users := get_users_in(mut db, ['first', 'second'])!
assert all_users.len == 2
}
+249
View File
@@ -0,0 +1,249 @@
module orm
const tx_callback_closed_err = 'transaction callback must not close the transaction explicitly'
enum TxKind {
root
savepoint
}
@[heap]
struct TxRootState {
mut:
next_savepoint_id int = 1
active bool = true
}
@[heap]
struct TxInner {
mut:
conn TransactionalConnection
kind TxKind
active bool = true
savepoint_name string
root_state &TxRootState = unsafe { nil }
}
// Tx is an ORM transaction handle that can be used anywhere an orm.Connection is accepted.
pub struct Tx {
mut:
inner &TxInner = unsafe { nil }
}
@[heap]
struct SavepointInner {
mut:
conn TransactionalConnection
owner &TxInner = unsafe { nil }
name string
active bool = true
}
// Savepoint is a manual savepoint handle created from an active transaction.
pub struct Savepoint {
mut:
inner &SavepointInner = unsafe { nil }
}
// begin starts a new ORM transaction on the provided connection.
pub fn begin(mut conn TransactionalConnection) !Tx {
conn.orm_begin()!
return Tx{
inner: &TxInner{
conn: conn
kind: .root
root_state: &TxRootState{}
}
}
}
// transaction runs a callback inside a transaction and commits or rolls back automatically.
pub fn transaction[T](mut conn TransactionalConnection, f fn (mut Tx) !T) !T {
mut tx := begin(mut conn)!
result := f(mut tx) or {
callback_err := err
if !tx.is_active() {
return error(tx_callback_closed_err)
}
tx.rollback() or {
return error('transaction callback failed: ${callback_err.msg()}; rollback failed: ${err.msg()}')
}
return callback_err
}
if !tx.is_active() {
return error(tx_callback_closed_err)
}
tx.commit()!
return result
}
// commit commits an active transaction.
pub fn (mut tx Tx) commit() ! {
tx.ensure_active('commit')!
match tx.inner.kind {
.root {
tx.inner.conn.orm_commit()!
tx.inner.root_state.active = false
}
.savepoint {
tx.inner.conn.orm_release_savepoint(tx.inner.savepoint_name)!
}
}
tx.inner.active = false
}
// rollback rolls back an active transaction.
pub fn (mut tx Tx) rollback() ! {
tx.ensure_active('rollback')!
match tx.inner.kind {
.root {
tx.inner.conn.orm_rollback()!
tx.inner.root_state.active = false
}
.savepoint {
tx.inner.conn.orm_rollback_to(tx.inner.savepoint_name)!
tx.inner.conn.orm_release_savepoint(tx.inner.savepoint_name)!
}
}
tx.inner.active = false
}
// savepoint creates a manual savepoint inside an active transaction.
pub fn (mut tx Tx) savepoint() !Savepoint {
tx.ensure_active('create a savepoint')!
name := tx.next_savepoint_name()!
tx.inner.conn.orm_savepoint(name)!
return Savepoint{
inner: &SavepointInner{
conn: tx.inner.conn
owner: tx.inner
name: name
}
}
}
// transaction runs a nested transaction backed by a savepoint.
pub fn (mut tx Tx) transaction[T](f fn (mut Tx) !T) !T {
tx.ensure_active('start a nested transaction')!
name := tx.next_savepoint_name()!
tx.inner.conn.orm_savepoint(name)!
mut nested := Tx{
inner: &TxInner{
conn: tx.inner.conn
kind: .savepoint
savepoint_name: name
root_state: tx.inner.root_state
}
}
result := f(mut nested) or {
callback_err := err
if !nested.is_active() {
return error(tx_callback_closed_err)
}
nested.rollback() or {
return error('transaction callback failed: ${callback_err.msg()}; rollback failed: ${err.msg()}')
}
return callback_err
}
if !nested.is_active() {
return error(tx_callback_closed_err)
}
nested.commit()!
return result
}
// rollback rolls back to the savepoint and releases it.
pub fn (mut sp Savepoint) rollback() ! {
sp.ensure_active('rollback')!
sp.inner.conn.orm_rollback_to(sp.inner.name)!
sp.inner.conn.orm_release_savepoint(sp.inner.name)!
sp.inner.active = false
}
// release releases the savepoint without rolling back.
pub fn (mut sp Savepoint) release() ! {
sp.ensure_active('release')!
sp.inner.conn.orm_release_savepoint(sp.inner.name)!
sp.inner.active = false
}
fn (tx Tx) is_active() bool {
return !isnil(tx.inner) && !isnil(tx.inner.root_state) && tx.inner.active
&& tx.inner.root_state.active
}
fn (tx Tx) ensure_active(action string) ! {
if isnil(tx.inner) {
return error('transaction is not initialized')
}
if !tx.inner.active {
return error('transaction is inactive; cannot ${action}')
}
}
fn (mut tx Tx) next_savepoint_name() !string {
if isnil(tx.inner) || isnil(tx.inner.root_state) {
return error('transaction is not initialized')
}
name := 'v_orm_sp_${tx.inner.root_state.next_savepoint_id}'
tx.inner.root_state.next_savepoint_id++
return name
}
fn (sp Savepoint) ensure_active(action string) ! {
if isnil(sp.inner) {
return error('savepoint is not initialized')
}
owner_tx := Tx{
inner: sp.inner.owner
}
if !sp.inner.active || isnil(sp.inner.owner) || !owner_tx.is_active() {
return error('savepoint is inactive; cannot ${action}')
}
}
// select forwards ORM select queries through the active transaction.
pub fn (mut tx Tx) select(config SelectConfig, data QueryData, where QueryData) ![][]Primitive {
tx.ensure_active('use the transaction')!
return tx.inner.conn.select(config, data, where)
}
// insert forwards ORM insert queries through the active transaction.
pub fn (mut tx Tx) insert(table Table, data QueryData) ! {
tx.ensure_active('use the transaction')!
tx.inner.conn.insert(table, data)!
}
// update forwards ORM update queries through the active transaction.
pub fn (mut tx Tx) update(table Table, data QueryData, where QueryData) ! {
tx.ensure_active('use the transaction')!
tx.inner.conn.update(table, data, where)!
}
// delete forwards ORM delete queries through the active transaction.
pub fn (mut tx Tx) delete(table Table, where QueryData) ! {
tx.ensure_active('use the transaction')!
tx.inner.conn.delete(table, where)!
}
// create forwards ORM create queries through the active transaction.
pub fn (mut tx Tx) create(table Table, fields []TableField) ! {
tx.ensure_active('use the transaction')!
tx.inner.conn.create(table, fields)!
}
// drop forwards ORM drop queries through the active transaction.
pub fn (mut tx Tx) drop(table Table) ! {
tx.ensure_active('use the transaction')!
tx.inner.conn.drop(table)!
}
// last_id forwards the last inserted id through the wrapped connection.
pub fn (mut tx Tx) last_id() int {
if isnil(tx.inner) {
return 0
}
return tx.inner.conn.last_id()
}