From c621c5ea132eb3c07e84f8db49b715aed9db64e7 Mon Sep 17 00:00:00 2001 From: Elliot Chance Date: Mon, 20 Jun 2022 18:06:55 -0400 Subject: [PATCH] Support for the V ORM 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 --- examples/orm.v | 101 +++++++++++++++++++++++++++++++ vsql/orm.v | 160 +++++++++++++++++++++++++++++++++++++++++++++++++ vsql/row.v | 8 +++ vsql/value.v | 23 +++++++ 4 files changed, 292 insertions(+) create mode 100644 examples/orm.v create mode 100644 vsql/orm.v diff --git a/examples/orm.v b/examples/orm.v new file mode 100644 index 0000000..4822b53 --- /dev/null +++ b/examples/orm.v @@ -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) + } +} diff --git a/vsql/orm.v b/vsql/orm.v new file mode 100644 index 0000000..39ecc36 --- /dev/null +++ b/vsql/orm.v @@ -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') +} diff --git a/vsql/row.v b/vsql/row.v index 9e7366c..c069920 100644 --- a/vsql/row.v +++ b/vsql/row.v @@ -4,6 +4,7 @@ module vsql import time +import orm struct Row { mut: @@ -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. diff --git a/vsql/value.v b/vsql/value.v index f8835c2..f07335b 100644 --- a/vsql/value.v +++ b/vsql/value.v @@ -4,6 +4,8 @@ module vsql +import orm + pub struct Value { pub mut: // TODO(elliotchance): Make these non-mutable. @@ -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' } @@ -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') } + } +}