Skip to content

Commit

Permalink
Support for the V ORM
Browse files Browse the repository at this point in the history
DO NOT MERGE.

This is partial support for the ORM. However, there are some challenges
that need to be addressed before this can be properly reviewed and
landed:

1. The ORM in V requires drivers to be hard-coded. See #90.

2. The Connection doesn't _really_ implement orm.Connection because the
vsql Connection is required to be mut and the current interface
definition does not allow this.

3. We need to create a new test suite for the ORM. `vsql/orm_test.v`
filled with combinations of statements "sql" commands will work just
fine. Specifically, we need to test different combinations of
expressions and types.

Fixes #90
  • Loading branch information
elliotchance committed Jun 20, 2022
1 parent 9f82626 commit c621c5e
Show file tree
Hide file tree
Showing 4 changed files with 292 additions and 0 deletions.
101 changes: 101 additions & 0 deletions examples/orm.v
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
import os
import vsql
import orm
import v.ast

fn main() {
os.rm('test.vsql') or {}
example() or { panic(err) }
}

struct Product {
id int [primary]
product_name string [sql: 'varchar(100)']
price f32
}

fn example() ? {
mut db := vsql.open('test.vsql') ?

// sql db {
// create table Product
// }
db.create("PRODUCT", [
orm.TableField{
name: "id"
typ: ast.int_type_idx
}
orm.TableField{
name: "product_name"
typ: ast.string_type_idx,
attrs: [StructAttribute{
name: "sql"
arg: "varchar(100)"
}]
}
orm.TableField{
name: "price"
typ: ast.f32_type_idx
}
]) ?

products := [
Product{
id: 1
product_name: 'Ice Cream'
price: 5.99
}
Product{
id: 2
product_name: 'Ham Sandwhich'
price: 3.47
}
Product{
id: 3
product_name: 'Bagel'
price: 1.25
}
]

for product in products {
// sql db {
// insert product into Product
// }
db.insert("PRODUCT", orm.QueryData{
fields: ["id", "product_name", "price"]
data: [
orm.int_to_primitive(product.id)
orm.string_to_primitive(product.product_name)
orm.f32_to_primitive(product.price)
]
types: [ast.int_type_idx, ast.string_type_idx, ast.f32_type_idx]
kinds: [.eq, .eq, .eq]
is_and: [false, false, false]
}) ?
}

// result := sql db {
// select from Product where price > 2
// }
result := db.@select(orm.SelectConfig{
table: 'PRODUCT'
has_where: true
// primary string = 'id' // should be set if primary is different than 'id' and 'has_limit' is false
fields: ["ID", "PRODUCT_NAME", "PRICE"]
types: [ast.int_type_idx, ast.string_type_idx, ast.f32_type_idx]
}, orm.QueryData{

}, orm.QueryData{
fields: ['PRICE']
data: [orm.int_to_primitive(2)]
types: [ast.int_type_idx]
kinds: [.gt]
is_and: [false]
}) ?

for row in result {
// I guess internally the []Primitive would be mapped back to a Product
// object? For now, we can show all the individual fields.
println(row)
}
}
160 changes: 160 additions & 0 deletions vsql/orm.v
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
// orm.v implements the V language ORM: https://modules.vlang.io/orm.html

module vsql

import orm
import v.ast
import time

/*
interface Connection {
@select(config SelectConfig, data QueryData, where QueryData) ?[][]Primitive
insert(table string, data QueryData) ?
update(table string, data QueryData, where QueryData) ?
delete(table string, where QueryData) ?
create(table string, fields []TableField) ?
drop(table string) ?
last_id() Primitive
}
*/

pub fn (mut c Connection) @select(config orm.SelectConfig, data orm.QueryData, where orm.QueryData) ?[][]orm.Primitive {
stmt := orm.orm_select_gen(config, '"', true, ':p', 0, where)

mut bound := map[string]Value{}
for i, _ in where.fields {
bound['p$i'] = new_primitive_value(where.data[i]) ?
}

mut prepared := c.prepare(stmt)?
result := prepared.query(bound)?
mut all_rows := [][]orm.Primitive{}

for row in result {
mut primitive_row := []orm.Primitive{}
for column in result.columns {
primitive_row << row.get_primitive(column.name) ?
}

all_rows << primitive_row
}

return all_rows
}

pub fn (mut c Connection) insert(table string, data orm.QueryData) ? {
values := data.data.map(fn (p orm.Primitive) string {
match p {
orm.InfixType {
// TODO(elliotchance): Not sure what this is?
return '$p'
}
time.Time {
// TODO(elliotchance): This doesn't work.
return '$p'
}
bool {
if p {
return 'TRUE'
}

return 'FALSE'
}
string {
// TODO(elliotchance): Does not escape correctly.
return '\'$p\''
}
f32 {
return '$p'
}
f64 {
return '$p'
}
i16 {
return '$p'
}
i64 {
return '$p'
}
i8 {
return '$p'
}
int {
return '$p'
}
u16 {
return '$p'
}
u32 {
return '$p'
}
u64 {
return '$p'
}
u8 {
return '$p'
}
}
})

c.query('INSERT INTO "$table" (${data.fields.join(', ')}) VALUES (${values.join(', ')})') ?
}

pub fn (mut c Connection) update(table string, data orm.QueryData, where orm.QueryData) ? {
panic('update')
}

pub fn (mut c Connection) delete(table string, where orm.QueryData) ? {
panic('delete')
}

pub fn (mut c Connection) create(table string, fields []orm.TableField) ? {
mut sql_fields := []string{}
for field in fields {
mut typ := ""
match field.typ {
ast.i8_type_idx, ast.i16_type_idx, ast.byte_type_idx, ast.u16_type_idx {
typ = 'SMALLINT'
}
ast.int_type_idx {
typ = 'INTEGER'
}
ast.i64_type_idx, ast.u32_type_idx, ast.u64_type_idx {
typ = 'BIGINT'
}
ast.f32_type_idx {
typ = 'FLOAT'
}
ast.f64_type_idx {
typ = 'DOUBLE PRECISION'
}
ast.bool_type_idx {
typ = 'BOOLEAN'
}
ast.string_type_idx {
typ = 'VARCHAR(255)'
}
else {
return error('unsupported type: $field.typ')
}
}

for attr in field.attrs {
if attr.name == 'sql' {
typ = attr.arg
}
}

sql_fields << '$field.name $typ'
}

c.query('CREATE TABLE "$table" (${sql_fields.join(', ')})') ?
}

pub fn (mut c Connection) drop(table string) ? {
panic('drop')
}

pub fn (mut c Connection) last_id() orm.Primitive {
panic('last_id')
}
8 changes: 8 additions & 0 deletions vsql/row.v
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
module vsql

import time
import orm

struct Row {
mut:
Expand Down Expand Up @@ -89,6 +90,13 @@ fn (r Row) get(name string) ?Value {
}
}

// primitives are used by the ORM.
pub fn (r Row) get_primitive(name string) ?orm.Primitive {
value := r.get(name)?

return value.primitive()
}

// new_empty_row is used internally to generate a row with zero values for all
// the types in a Row. This is used for testing expressions without needing the
// actual row.
Expand Down
23 changes: 23 additions & 0 deletions vsql/value.v
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@

module vsql

import orm

pub struct Value {
pub mut:
// TODO(elliotchance): Make these non-mutable.
Expand Down Expand Up @@ -81,6 +83,16 @@ pub fn new_character_value(x string, size int) Value {
}
}

// new_primitive_value returns the Value of a Primitive. Primitives are used by
// the ORM.
pub fn new_primitive_value(p orm.Primitive) ?Value {
return match p {
int { new_integer_value(p) }
f64 { new_double_precision_value(p) }
else { error('$p') }
}
}

fn bool_str(x f64) string {
return match x {
0 { 'FALSE' }
Expand Down Expand Up @@ -163,3 +175,14 @@ fn (v Value) cmp(v2 Value) ?(int, bool) {

return error('cannot compare $v.typ and $v2.typ')
}

// primitives are used by the ORM.
pub fn (v Value) primitive() ?orm.Primitive {
return match v.typ.typ {
.is_boolean { orm.bool_to_primitive(v.f64_value != 0) }
.is_integer { orm.int_to_primitive(int(v.f64_value)) }
.is_varchar { orm.string_to_primitive(v.string_value) }
.is_double_precision { orm.f64_to_primitive(v.f64_value) }
else { error('$v.typ.typ') }
}
}

0 comments on commit c621c5e

Please sign in to comment.