Initial project import
This commit is contained in:
commit
23f2d29e32
11957 files changed
+1754577
No files matched your search
@@ -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
@@ -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')
|
||||
}
|
||||
}
|
||||
@@ -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() }
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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']
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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'
|
||||
}
|
||||
@@ -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
@@ -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()!
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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;'
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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()!
|
||||
}
|
||||
@@ -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` > ?;'
|
||||
}
|
||||
@@ -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]
|
||||
}
|
||||
@@ -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');
|
||||
")!
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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'
|
||||
}
|
||||
@@ -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'
|
||||
}
|
||||
@@ -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
|
||||
}!
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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'
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
Reference in new issue
Block a user