From d4105bae43c452d201cedd599d0a8bc400a370f6 Mon Sep 17 00:00:00 2001 From: daniel-le97 <107774403+daniel-le97@users.noreply.github.com> Date: Thu, 7 Nov 2024 20:26:49 -0700 Subject: [PATCH 01/13] v orm implementation with standard query --- Makefile | 3 + examples/orm.v | 62 +++++++ vsql/orm.v | 448 ++++++++++++++++++++++++++++++++++++++++++++++++ vsql/orm_test.v | 319 ++++++++++++++++++++++++++++++++++ 4 files changed, 832 insertions(+) create mode 100644 examples/orm.v create mode 100644 vsql/orm.v create mode 100644 vsql/orm_test.v diff --git a/Makefile b/Makefile index a194e11..b499469 100644 --- a/Makefile +++ b/Makefile @@ -87,6 +87,9 @@ btree-test: oldv sql-test: oldv $(V) -stats $(BUILD_OPTIONS) vsql/sql_test.v +orm-test: oldv + $(V) -stats $(BUILD_OPTIONS) vsql/orm_test.v + # CLI Tests cli-test: bin/vsql diff --git a/examples/orm.v b/examples/orm.v new file mode 100644 index 0000000..55b90a9 --- /dev/null +++ b/examples/orm.v @@ -0,0 +1,62 @@ +import os +import vsql +import time + +fn main() { + os.rm('test.vsql') or {} + + example() or { panic(err) } +} + +// NOTE for some reason if we declare a @[primary] on a struct field, we can not do delete queries on the tables... +// so id is not a primary key in this example +struct Product { + id int //@[primary] + product_name string @[sql_type: 'varchar(100)'] + price f64 +} + +fn example() ! { + timer := time.new_stopwatch() + mut db := vsql.open('test.vsql')! + + sql db { + create table Product + }! + + products := [ + Product{1, 'Ice Cream', 5.99}, + Product{2, 'Ham Sandwhich', 3.47}, + Product{3, 'Bagel', 1.25}, + ] + + // product := Product{1, 'Ice Cream', 5.99} + for product in products { + sql db { + insert product into Product + } or { panic(err) } + } + sql db { + update Product set product_name = 'Cereal' where id == 1 + } or { panic(err) } + + prod_one := sql db { + select from Product where id == 1 + }! + + assert prod_one.len == 1 + + sql db { + delete from Product where product_name == 'Cereal' + } or { panic(err) } + + all := sql db { + select from Product + }! + + assert all.len == 2 + + println(timer.elapsed()) + // println(typeof[?int]().idx) + // println(typeof[int]().idx) +} diff --git a/vsql/orm.v b/vsql/orm.v new file mode 100644 index 0000000..77a8029 --- /dev/null +++ b/vsql/orm.v @@ -0,0 +1,448 @@ +module vsql + +import orm +import time + +pub const varchar_default_len = 255 + +// DDL (table creation/destroying etc) +fn vsql_type_from_v(typ int) !string { + return if typ == orm.type_idx['i8'] || typ == orm.type_idx['i16'] || typ == orm.type_idx['u8'] { + 'SMALLINT' + } else if typ == orm.type_idx['bool'] { + 'BOOLEAN' + } else if typ == orm.type_idx['int'] || typ == orm.type_idx['u16'] || typ == 8 { + 'INT' + } else if typ == orm.type_idx['i64'] || typ == orm.type_idx['u32'] { + 'BIGINT' + } else if typ == orm.type_idx['f32'] { + 'REAL' + } else if typ == orm.type_idx['u64'] { + // 'NUMERIC(20)' + 'BIGINT' + } else if typ == orm.type_idx['f64'] { + 'DOUBLE PRECISION' + } else if typ == orm.type_idx['string'] { + 'VARCHAR(${varchar_default_len})' + } else if typ == typeof[time.Time]().idx || typ == -2 { + // time.Time will be converted to a string + // will be parsed as a time.Time when read + 'TIMESTAMP(6) WITHOUT TIME ZONE' + } else if typ == -1 { + // SERIAL + 'INTEGER' + } else { + error('Unknown type ${typ}') + } +} + +// fn get_prepared_args(query_data []orm.QueryData) map[string]Value { +// mut mp := map[string]Value{} +// for i , q in query_data { +// // for f in q.fields { +// // type_idx := +// // } +// // for j, v in f.data { +// // mp[':${i}_${j}'] = v +// // } +// } +// return mp +// } + +// `query_converter_lite` converts a statement like `INSERT INTO Product (id, product_name, price) VALUES (:1, :2, :3)` to `INSERT INTO Product (id, product_name, price) VALUES (:id, :product_name, :price)` +fn query_converter_lite(query string, query_data []orm.QueryData) !string { + mut counter := 1 + mut new_query := query + for data in query_data { + for field in data.fields { + new_query = new_query.replace(':${counter}', ':${field}') + counter++ + } + } + return new_query +} + +fn query_converter(query string, query_data []orm.QueryData) !string { + mut counter := 1 + mut new_query := query + + for data in query_data { + vals := primitive_array_to_string_array(data.data) + for val in vals { + new_query = new_query.replace(':${counter}', val) + counter++ + } + } + + return new_query +} + +fn primitive_array_to_string_array(prims []orm.Primitive) []string { + mut values := prims.map(fn (p orm.Primitive) string { + match p { + orm.InfixType { + // TODO(elliotchance): Not sure what this is? + return '${p}' + } + time.Time { + // + return 'TIMESTAMP \'${p}\'' + // return '${p}' + } + orm.Null { + return 'NULL' + } + bool { + if p { + return 'TRUE' + } + + return 'FALSE' + } + string { + 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}' + } + } + }) + return values +} + +// select is used internally by V's ORM for processing `SELECT` queries +pub fn (mut db Connection) select(config orm.SelectConfig, data orm.QueryData, where orm.QueryData) ![][]orm.Primitive { + // 1. Create query and bind necessary data + // println(orm.type_idx) + mut query := orm.orm_select_gen(config, '', true, ':', 1, where) + query = query_converter(query, [data, where])! + rows := db.query(query)! + mut ret := [][]orm.Primitive{} + for row in rows { + mut row_primitives := []orm.Primitive{} + keys := row.data.keys() + for idx, key in keys { + prim := row.get(key)! + type_idx := config.types[idx] + + // check orm.type_idx for what these numbers are + if type_idx == 5 { + row_primitives << i8(prim.int_value()) + } else if type_idx == 2 { + row_primitives << time.parse(prim.string_value())! + } else if type_idx == 6 { + row_primitives << i16(prim.int_value()) + } else if type_idx == 8 { + row_primitives << int(prim.int_value()) + } else if type_idx == 9 { + row_primitives << prim.int_value() + } else if type_idx == 11 { + row_primitives << u8(prim.int_value()) + } else if type_idx == 12 { + row_primitives << u16(prim.int_value()) + } else if type_idx == 13 { + row_primitives << u32(prim.int_value()) + } else if type_idx == 14 { + row_primitives << u64(prim.int_value()) + } else if type_idx == 16 { + row_primitives << f32(prim.f64_value()) + } else if type_idx == 17 { + row_primitives << prim.f64_value() + } else if type_idx == 19 { + row_primitives << prim.bool_value() == .is_true + } else { + row_primitives << prim.string_value() + } + } + ret << row_primitives + } + // println(ret) + return ret +} + +fn serial_name(table string) string { + return '${table}_SERIAL' +} + +fn get_table_columns(mut db Connection, table string, data []orm.QueryData) !map[string]Value { + mut mp := map[string]Value{} + mut tbl := Table{} + mut cat := db.catalog() + tables := cat.schema_tables('PUBLIC') or { [] } + for t in tables { + if t.name.entity_name == table.to_upper() { + tbl = t + break + } + } + if tbl.name.entity_name != table.to_upper() { + return error('Table ${table} not found') + } + for d in data { + for i, f in d.fields { + for c in tbl.columns { + if c.name.sub_entity_name == f.to_upper() { + mp[f] = primitive_to_value(c.typ, d.data[i])! + } + } + } + } + + return mp +} + +// insert is used internally by V's ORM for processing `INSERT` queries +pub fn (mut db Connection) insert(table string, data orm.QueryData) ! { + // println(data) + // mut tbl := get_table_columns(mut db, table, [data]) or { return err } + // println(tbl) + + mut values := primitive_array_to_string_array(data.data) + if data.auto_fields.len > 1 { + return error('multiple AUTO fields are not supported') + } else if data.auto_fields.len == 1 { + // println(data) + values[data.auto_fields[0]] = 'NEXT VALUE FOR "${serial_name(table)}"' + } + mut nums := []string{} + for i, _ in data.fields { + nums << '${i}' + } + insert_sql := 'INSERT INTO ${table} (${data.fields.join(', ')}) VALUES (${values.join(', ')})' + println(insert_sql) + // println(tbl) + $if trace_vsql_orm ? { + eprintln('> vsql insert: ${query}') + } + db.query(insert_sql) or { return err } + // mut stmt := db.prepare(insert_sql) or { return err } + // stmt.query(tbl) or { return err } +} + +// update is used internally by V's ORM for processing `UPDATE` queries +pub fn (mut db Connection) update(table string, data orm.QueryData, where orm.QueryData) ! { + mut query, _ := orm.orm_stmt_gen(.sqlite, table, '', .update, true, ':', 1, data, + where) + + // values := get_table_columns(mut db, table, [data, where]) or { return err } + query = query_converter(query, [data, where])! + println(query) + + $if trace_vsql_orm ? { + eprintln('> vsql update: ${query}') + } + db.query(query) or { return err } + // mut stmt := db.prepare(query) or { return err } + // stmt.query(values) or { return err } +} + +// delete is used internally by V's ORM for processing `DELETE ` queries +pub fn (mut db Connection) delete(table string, where orm.QueryData) ! { + mut query, _ := orm.orm_stmt_gen(.sqlite, table, '', .delete, true, ':', 1, orm.QueryData{}, + where) + + query = query_converter(query, [where])! + // values := get_table_columns(mut db, table, [where]) or { return err } + + $if trace_vsql_orm ? { + eprintln('> vsql delete: ${query}') + } + db.query(query) or { return err } + // mut stmt := db.prepare(query) or { return err } + // stmt.query(values) or { return err } +} + +// `last_id` is used internally by V's ORM for post-processing `INSERT` queries +// TODO i dont think vsql supports this +pub fn (mut db Connection) last_id() int { + return 0 +} + +// create is used internally by V's ORM for processing table creation queries (DDL) +pub fn (mut db Connection) create(table string, fields []orm.TableField) ! { + check_for_not_supported(mut db, table, fields) or { return err } + mut new_table := table + if is_reserved_word(table) { + new_table = '"${table}"' + } + mut query := orm.orm_table_gen(new_table, '', true, 0, fields, vsql_type_from_v, false) or { + return err + } + // 'IF NOT EXISTS' is not supported in vsql, so we remove it + query = query.replace(' IF NOT EXISTS ', ' ') + // `TEXT` is not supported in vsql, so we replace it with `VARCHAR(255) if its somehow used` + query = query.replace('TEXT', 'VARCHAR(${varchar_default_len})') + + $if trace_vsql_orm ? { + eprintln('> vsql create: ${query}') + } + // println(query) + // println(table) + // println(fields) + db.query(query) or { return err } +} + +// drop is used internally by V's ORM for processing table destroying queries (DDL) +pub fn (mut db Connection) drop(table string) ! { + query := 'DROP TABLE ${table};' + $if trace_vsql_orm ? { + eprintln('> vsql drop: ${query}') + } + + db.query(query) or { return err } + + // // check to see if there is a SEQUENCE for the table (for the @[sql: 'serial'] attribute) + db.query('EXPLAIN DROP SEQUENCE "${serial_name(table)}"') or { return } + // if we have not returned then we can drop the sequence + db.query('DROP SEQUENCE "${serial_name(table)}"') or { return err } +} + +fn check_for_not_supported(mut db Connection, table string, fields []orm.TableField) ! { + for field in fields { + if field.typ == orm.enum_ { + return error('enum is not supported in vsql') + } + if is_reserved_word(field.name) { + return error('reserved word ${field.name} cannot be used as a field name at ${table}.${field.name}') + } + for attr in field.attrs { + if attr.name == 'sql' { + if attr.arg == 'serial' { + db.query('CREATE SEQUENCE "${serial_name(table)}"')! + } + if is_reserved_word(attr.arg) { + return error('${attr.arg} is a reserved word in vsql') + } + } + if attr.name == 'default' { + return error('default is not supported in vsql') + } + if attr.name == 'unique' { + return error('unique is not supported in vsql') + } + if attr.name == 'primary' { + eprintln('primary is supported, but currently will break delete queries') + // return error('primary is supported, but currently will break delete queries') + } + } + } +} + +// primitive_to_value returns the Value of a Primitive based on the intended +// destination type. Primitives are used by the ORM. +// +// It's important to note that while types may be compatible, they can still be +// out of range, such as assigning an overflowing integer value to SMALLINT. +fn primitive_to_value(typ Type, p orm.Primitive) !Value { + // The match should be exhaustive for typ and p so that we can make sure we + // cover all combinations now and in the future. + match p { + orm.Null { + // In standard SQL, NULL's must be typed. + return new_null_value(typ.typ) + } + bool { + match typ.typ { + .is_boolean { + return new_boolean_value(p) + } + else {} + } + } + f32, f64 { + match typ.typ { + .is_real { + return new_real_value(f32(p)) + } + .is_double_precision { + return new_double_precision_value(f64(p)) + } + else {} + } + } + i16, i8, u8 { + match typ.typ { + .is_smallint { + return new_smallint_value(i16(p)) + } + else {} + } + } + int, u16 { + match typ.typ { + .is_smallint { + return new_smallint_value(i16(p)) + } + .is_integer { + return new_integer_value(int(p)) + } + else {} + } + } + u32, i64 { + match typ.typ { + .is_bigint { + return new_bigint_value(i64(p)) + } + else {} + } + } + u64 { + match typ.typ { + .is_smallint { + return new_smallint_value(i16(p)) + } + else {} + } + } + string { + match typ.typ { + .is_varchar { + return new_varchar_value(p) + } + .is_numeric { + return new_numeric_value(p) + } + else {} + } + } + time.Time { + match typ.typ { + .is_timestamp_with_time_zone, .is_timestamp_without_time_zone { + return new_timestamp_value(p.str())! + } + else {} + } + } + orm.InfixType {} + } + + return error('cannot assign ${p} to ${typ}') +} diff --git a/vsql/orm_test.v b/vsql/orm_test.v new file mode 100644 index 0000000..03a19d1 --- /dev/null +++ b/vsql/orm_test.v @@ -0,0 +1,319 @@ +module vsql + +// Structs intentionally have less than 6 fields, any more then inserts queries get exponentially slower. +import time + +// struct TestDateTypes { +// id int @[primary; sql: serial] +// custom1 string @[sql_type: 'TIME WITH TIME ZONE'] +// custom2 string @[sql_type: 'TIMESTAMP(3) WITH TIME ZONE'] +// custom3 string @[sql_type: 'INT'] +// custom4 string @[sql_type: 'DATE'] +// custom5 string @[sql_type: 'TIMESTAMP(3) WITHOUT TIME ZONE'] +// custom6 string @[sql_type: 'TIME WITHOUT TIME ZONE'] +// } + +@[table: 'testcustomtable'] +struct TestCustomTableAndSerial { + id int @[sql: serial] + an_bool bool +} + +struct TestPrimaryBroken { + id int @[primary; sql: serial] +} + +struct TestDefaultAttribute { + id int @[sql: serial] + name string + created_at string @[default: 'CURRENT_TIMESTAMP(6)'; sql_type: 'TIMESTAMP(3) WITHOUT TIME ZONE'] +} + +struct TestUniqueAttribute { + attribute string @[unique] +} + +struct TestReservedWordField { + where string +} + +struct TestReservedWordSqlAttribute { + ok string @[sql: 'ORDER'] +} + +struct TestOrmValuesOne { + an_f32 f32 // REAL + an_f64 f64 // DOUBLE PRECISION + an_i16 i16 // SMALLINT + an_i64 i64 // BIGINT +} + +struct TestOrmValuesTwo { + an_i8 i8 // SMALLINT + an_int int // INTEGER + a_string string // CHARACTER VARYING(255) +} + +struct TestOrmValuesThree { + an_u16 u16 // INTEGER + an_u32 u32 // BIGINT + an_u64 u64 // BIGINT + an_u8 u8 // SMALLINT +} + +struct TestOrmValuesFour { + a_time time.Time // TIMESTAMP(6) WITH TIME ZONE + a_bool bool // BOOLEAN + int_or_null ?int // optional int +} + +// ORMTableEnum is not supported. +struct ORMTableEnum { + an_enum Colors +} + +enum Colors { + red + green + blue +} + +// @[table: 'GROUP'] +struct ORMTable2 { + dummy int +} + +fn test_orm_create_success2() { + mut db := open(':memory:')! + sql db { + create table ORMTable2 + }! + dumm := ORMTable2{dummy: 1} + sql db { + insert dumm into ORMTable2 + }! + + sql db { + drop table ORMTable2 + }! +} + +// We cannot test this because it will make all tests fail, because the database itself will run into an error +// so it is put behind a -d flag if you really want to try it +fn test_primary_key_broken() { + $if test_primary_key ? { + mut db := open(':memory:')! + mut error := '' + sql db { + create table TestPrimaryBroken + } or { error = err.str() } + assert error == '' + + test_primary_broken := TestPrimaryBroken{} + + sql db { + insert test_primary_broken into TestPrimaryBroken + } or { error = err.str() } + assert error == '' + + mut all := sql db { + select from TestPrimaryBroken + }! + + sql db { + delete from TestPrimaryBroken where id == all[0].id + } or { error = err.str() } + } +} + +fn test_custom_table_name_and_serial_crud() { + mut db := open(':memory:')! + mut error := '' + sql db { + create table TestCustomTableAndSerial + } or { error = err.str() } + assert error == '' + + test_custom_sql := TestCustomTableAndSerial{ + an_bool: true + } + test_custom_sql2 := TestCustomTableAndSerial{ + an_bool: false + } + + sql db { + insert test_custom_sql into TestCustomTableAndSerial + insert test_custom_sql2 into TestCustomTableAndSerial + } or { error = err.str() } + + assert error == '' + mut all := sql db { + select from TestCustomTableAndSerial + }! + assert all[0].id != 0 + + sql db { + update TestCustomTableAndSerial set an_bool = false where id == all[0].id + } or { error = err.str() } + assert error == '' + + sql db { + delete from TestCustomTableAndSerial where id == all[1].id + } or { error = err.str() } + assert error == '' + + all = sql db { + select from TestCustomTableAndSerial + }! + assert all.len == 1 + assert all[0].an_bool == false + + sql db { + drop table TestCustomTableAndSerial + } or { error = err.str() } + + assert error == '' +} + +fn test_reserved_words() { + mut db := open(':memory:')! + mut error := '' + sql db { + create table TestReservedWordField + } or { error = err.str() } + assert error == 'reserved word where cannot be used as a field name at TestReservedWordField.where' + sql db { + create table TestReservedWordSqlAttribute + } or { error = err.str() } + assert error == 'ORDER is a reserved word in vsql' +} + +fn test_unsupported_attributes() { + mut db := open(':memory:')! + mut error := '' + sql db { + create table TestDefaultAttribute + } or { error = err.str() } + assert error == 'default is not supported in vsql' + sql db { + create table TestUniqueAttribute + } or { error = err.str() } + assert error == 'unique is not supported in vsql' +} + +fn test_default_orm_values() { + mut db := open(':memory:')! + mut error := '' + sql db { + create table TestOrmValuesOne + create table TestOrmValuesTwo + create table TestOrmValuesThree + create table TestOrmValuesFour + } or { error = err.str() } + assert error == '' + + values_one := TestOrmValuesOne{ + an_f32: 3.14 + an_f64: 2.718281828459 + an_i16: 12345 + an_i64: 123456789012345 + } + values_two := TestOrmValuesTwo{ + an_i8: 12 + an_int: 123456 + a_string: 'Hello, World!' + } + values_three := TestOrmValuesThree{ + an_u16: 54321 + an_u32: 1234567890 + an_u64: 1234 + an_u8: 255 + } + + values_four := TestOrmValuesFour{ + a_time: time.now() + a_bool: true + // int_or_null: 123 + } + values_four_b := TestOrmValuesFour{ + a_time: time.now() + a_bool: false + int_or_null: 123 + } + + sql db { + insert values_one into TestOrmValuesOne + insert values_two into TestOrmValuesTwo + insert values_three into TestOrmValuesThree + insert values_four into TestOrmValuesFour + insert values_four_b into TestOrmValuesFour + } or { error = err.str() } + assert error == '' + + result_values_one := sql db { + select from TestOrmValuesOne + }! + one := result_values_one[0] + + assert typeof(one.an_f32).idx == typeof[f32]().idx + assert one.an_f32 == 3.14 + assert typeof(one.an_f64).idx == typeof[f64]().idx + assert one.an_f64 == 2.718281828459 + assert typeof(one.an_i16).idx == typeof[i16]().idx + assert one.an_i16 == 12345 + assert typeof(one.an_i64).idx == typeof[i64]().idx + assert one.an_i64 == 123456789012345 + + result_values_two := sql db { + select from TestOrmValuesTwo + }! + + two := result_values_two[0] + + assert typeof(two.an_i8).idx == typeof[i8]().idx + assert two.an_i8 == 12 + assert typeof(two.an_int).idx == typeof[int]().idx + assert two.an_int == 123456 + assert typeof(two.a_string).idx == typeof[string]().idx + assert two.a_string == 'Hello, World!' + + result_values_three := sql db { + select from TestOrmValuesThree + }! + + three := result_values_three[0] + + assert typeof(three.an_u16).idx == typeof[u16]().idx + assert three.an_u16 == 54321 + assert typeof(three.an_u32).idx == typeof[u32]().idx + assert three.an_u32 == 1234567890 + // println(three.an_u64) + assert typeof(three.an_u64).idx == typeof[u64]().idx + assert three.an_u64 == 1234 + assert typeof(three.an_u8).idx == typeof[u8]().idx + assert three.an_u8 == 255 + + result_values_four := sql db { + select from TestOrmValuesFour + }! + + four := result_values_four[0] + + assert typeof(four.a_time).idx == typeof[time.Time]().idx + assert typeof(four.a_bool).idx == typeof[bool]().idx + assert four.a_bool == true + assert typeof(four.int_or_null).idx == typeof[?int]().idx + unwrapped_option_one := four.int_or_null or { 0 } + assert unwrapped_option_one == 0 + unwrapped_option_two := result_values_four[1].int_or_null or { 0 } + assert unwrapped_option_two == 123 +} + +fn test_orm_create_enum_is_not_supported() { + mut db := open(':memory:')! + mut error := '' + sql db { + create table ORMTableEnum + } or { error = err.str() } + assert error == 'enum is not supported in vsql' +} From 5851f4592df3c8d78a8a49b3af7d3fd7438bfc5a Mon Sep 17 00:00:00 2001 From: daniel-le97 <107774403+daniel-le97@users.noreply.github.com> Date: Sat, 9 Nov 2024 12:38:03 -0700 Subject: [PATCH 02/13] orm: prefer db.prepare over db.query where possible, test now fully pass --- examples/orm.v | 8 +- vsql/orm.v | 533 ++++++++++++++++++++++-------------------------- vsql/orm_test.v | 191 +++++++++++++---- 3 files changed, 405 insertions(+), 327 deletions(-) diff --git a/examples/orm.v b/examples/orm.v index 55b90a9..8748210 100644 --- a/examples/orm.v +++ b/examples/orm.v @@ -2,14 +2,16 @@ import os import vsql import time +// CHECK ./vsql/orm_test.v for more advanced usage + fn main() { os.rm('test.vsql') or {} - + example() or { panic(err) } } // NOTE for some reason if we declare a @[primary] on a struct field, we can not do delete queries on the tables... -// so id is not a primary key in this example +// so for now we are not supporting primary keys struct Product { id int //@[primary] product_name string @[sql_type: 'varchar(100)'] @@ -57,6 +59,4 @@ fn example() ! { assert all.len == 2 println(timer.elapsed()) - // println(typeof[?int]().idx) - // println(typeof[int]().idx) } diff --git a/vsql/orm.v b/vsql/orm.v index 77a8029..afdb9c3 100644 --- a/vsql/orm.v +++ b/vsql/orm.v @@ -24,7 +24,7 @@ fn vsql_type_from_v(typ int) !string { 'DOUBLE PRECISION' } else if typ == orm.type_idx['string'] { 'VARCHAR(${varchar_default_len})' - } else if typ == typeof[time.Time]().idx || typ == -2 { + } else if typ == orm.time_ { // time.Time will be converted to a string // will be parsed as a time.Time when read 'TIMESTAMP(6) WITHOUT TIME ZONE' @@ -36,167 +36,113 @@ fn vsql_type_from_v(typ int) !string { } } -// fn get_prepared_args(query_data []orm.QueryData) map[string]Value { -// mut mp := map[string]Value{} -// for i , q in query_data { -// // for f in q.fields { -// // type_idx := -// // } -// // for j, v in f.data { -// // mp[':${i}_${j}'] = v -// // } -// } -// return mp -// } - -// `query_converter_lite` converts a statement like `INSERT INTO Product (id, product_name, price) VALUES (:1, :2, :3)` to `INSERT INTO Product (id, product_name, price) VALUES (:id, :product_name, :price)` -fn query_converter_lite(query string, query_data []orm.QueryData) !string { - mut counter := 1 - mut new_query := query - for data in query_data { - for field in data.fields { - new_query = new_query.replace(':${counter}', ':${field}') - counter++ - } - } - return new_query -} - -fn query_converter(query string, query_data []orm.QueryData) !string { - mut counter := 1 - mut new_query := query - - for data in query_data { - vals := primitive_array_to_string_array(data.data) - for val in vals { - new_query = new_query.replace(':${counter}', val) - counter++ +// primitive_to_value returns the Value of a Primitive based on the intended +// destination type. Primitives are used by the ORM. +// +// It's important to note that while types may be compatible, they can still be +// out of range, such as assigning an overflowing integer value to SMALLINT. +fn primitive_to_value(typ Type, p orm.Primitive) !Value { + // The match should be exhaustive for typ and p so that we can make sure we + // cover all combinations now and in the future. + match p { + orm.Null { + // In standard SQL, NULL's must be typed. + return new_null_value(typ.typ) } - } - - return new_query -} - -fn primitive_array_to_string_array(prims []orm.Primitive) []string { - mut values := prims.map(fn (p orm.Primitive) string { - match p { - orm.InfixType { - // TODO(elliotchance): Not sure what this is? - return '${p}' - } - time.Time { - // - return 'TIMESTAMP \'${p}\'' - // return '${p}' - } - orm.Null { - return 'NULL' - } - bool { - if p { - return 'TRUE' + bool { + match typ.typ { + .is_boolean { + return new_boolean_value(p) } - - return 'FALSE' - } - string { - return '\'${p}\'' - } - f32 { - return '${p}' - } - f64 { - return '${p}' - } - i16 { - return '${p}' - } - i64 { - return '${p}' + else {} } - i8 { - return '${p}' + } + f32, f64 { + match typ.typ { + .is_real { + return new_real_value(f32(p)) + } + .is_double_precision { + return new_double_precision_value(f64(p)) + } + else {} } - int { - return '${p}' + } + i16, i8, u8 { + match typ.typ { + .is_smallint { + return new_smallint_value(i16(p)) + } + else {} } - u16 { - return '${p}' + } + int, u16 { + match typ.typ { + .is_smallint { + return new_smallint_value(i16(p)) + } + .is_integer { + return new_integer_value(int(p)) + } + else {} } - u32 { - return '${p}' + } + u32, i64 { + match typ.typ { + .is_bigint { + return new_bigint_value(i64(p)) + } + else {} } - u64 { - return '${p}' + } + u64 { + match typ.typ { + .is_smallint { + return new_smallint_value(i16(p)) + } + .is_bigint { + return new_bigint_value(i64(p)) + } + else {} } - u8 { - return '${p}' + } + string { + match typ.typ { + .is_varchar { + return new_varchar_value(p) + } + .is_numeric { + return new_numeric_value(p) + } + else {} } } - }) - return values -} - -// select is used internally by V's ORM for processing `SELECT` queries -pub fn (mut db Connection) select(config orm.SelectConfig, data orm.QueryData, where orm.QueryData) ![][]orm.Primitive { - // 1. Create query and bind necessary data - // println(orm.type_idx) - mut query := orm.orm_select_gen(config, '', true, ':', 1, where) - query = query_converter(query, [data, where])! - rows := db.query(query)! - mut ret := [][]orm.Primitive{} - for row in rows { - mut row_primitives := []orm.Primitive{} - keys := row.data.keys() - for idx, key in keys { - prim := row.get(key)! - type_idx := config.types[idx] - - // check orm.type_idx for what these numbers are - if type_idx == 5 { - row_primitives << i8(prim.int_value()) - } else if type_idx == 2 { - row_primitives << time.parse(prim.string_value())! - } else if type_idx == 6 { - row_primitives << i16(prim.int_value()) - } else if type_idx == 8 { - row_primitives << int(prim.int_value()) - } else if type_idx == 9 { - row_primitives << prim.int_value() - } else if type_idx == 11 { - row_primitives << u8(prim.int_value()) - } else if type_idx == 12 { - row_primitives << u16(prim.int_value()) - } else if type_idx == 13 { - row_primitives << u32(prim.int_value()) - } else if type_idx == 14 { - row_primitives << u64(prim.int_value()) - } else if type_idx == 16 { - row_primitives << f32(prim.f64_value()) - } else if type_idx == 17 { - row_primitives << prim.f64_value() - } else if type_idx == 19 { - row_primitives << prim.bool_value() == .is_true - } else { - row_primitives << prim.string_value() + time.Time { + match typ.typ { + .is_timestamp_with_time_zone, .is_timestamp_without_time_zone { + return new_timestamp_value(p.str())! + } + else {} } } - ret << row_primitives + orm.InfixType {} } - // println(ret) - return ret + + return error('cannot assign ${p} to ${typ}') } fn serial_name(table string) string { return '${table}_SERIAL' } -fn get_table_columns(mut db Connection, table string, data []orm.QueryData) !map[string]Value { +fn get_table_values_map(mut db Connection, table string, data []orm.QueryData) !map[string]Value { mut mp := map[string]Value{} mut tbl := Table{} mut cat := db.catalog() tables := cat.schema_tables('PUBLIC') or { [] } + // println(tables) for t in tables { + // println(t.name.entity_name) if t.name.entity_name == table.to_upper() { tbl = t break @@ -205,11 +151,18 @@ fn get_table_columns(mut db Connection, table string, data []orm.QueryData) !map if tbl.name.entity_name != table.to_upper() { return error('Table ${table} not found') } + mut field_counter := 1 for d in data { for i, f in d.fields { for c in tbl.columns { if c.name.sub_entity_name == f.to_upper() { - mp[f] = primitive_to_value(c.typ, d.data[i])! + if mp.keys().contains(f) { + field_key := '${f}_${field_counter.str()}' + mp[field_key] = primitive_to_value(c.typ, d.data[i])! + field_counter++ + } else { + mp[f] = primitive_to_value(c.typ, d.data[i])! + } } } } @@ -218,80 +171,170 @@ fn get_table_columns(mut db Connection, table string, data []orm.QueryData) !map return mp } +// `query_reformatter` converts a statement like `INSERT INTO Product (id, product_name, price) VALUES (:1, :2, :3)` to `INSERT INTO Product (id, product_name, price) VALUES (:id, :product_name, :price)` +fn query_reformatter(query string, query_data []orm.QueryData) string { + mut counter := 1 + mut field_counter := 1 + mut new_query := query + for data in query_data { + for field in data.fields { + // this if check is for if there are multiple of the same field being checked for + // products = sql db { + // select from Product where price > '3' && price < '3.50' + // }! + if new_query.contains(':${field}') { + new_query = new_query.replace(':${counter}', ':${field}_${field_counter.str()}') + field_counter++ + counter++ + } else { + new_query = new_query.replace(':${counter}', ':${field}') + counter++ + } + } + } + return new_query +} + +fn check_for_not_supported(mut db Connection, table string, fields []orm.TableField) ! { + for field in fields { + if field.typ == orm.enum_ { + return error('enum is not supported in vsql') + } + if is_reserved_word(field.name) { + return error('reserved word ${field.name} cannot be used as a field name at ${table}.${field.name}') + } + for attr in field.attrs { + if attr.name == 'sql' { + if attr.arg == 'serial' { + db.query('CREATE SEQUENCE "${serial_name(table)}"')! + } + if is_reserved_word(attr.arg) { + return error('${attr.arg} is a reserved word in vsql') + } + } + if attr.name == 'default' { + return error('default is not supported in vsql') + } + if attr.name == 'unique' { + return error('unique is not supported in vsql') + } + if attr.name == 'primary' { + // TODO (daniel-le97) figure out how we should be handling primary keys as it will break delete queries + return error('primary key is supported, but currently will break delete queries') + } + } + } +} + +// select is used internally by V's ORM for processing `SELECT` queries +pub fn (mut db Connection) select(config orm.SelectConfig, data orm.QueryData, where orm.QueryData) ![][]orm.Primitive { + conf := orm.SelectConfig{ + ...config + table: reformat_table_name(config.table) + } + mut query := orm.orm_select_gen(conf, '', true, ':', 1, where) + query = query_reformatter(query, [data, where]) + // println('query: ${query}') + + mut rows := Result{} + if data.data.len == 0 && where.data.len == 0 { + rows = db.query(query) or { return err } + } else { + values := get_table_values_map(mut db, config.table, [data, where]) or { return err } + mut stmt := db.prepare(query) or { return err } + rows = stmt.query(values) or { return err } + } + + mut ret := [][]orm.Primitive{} + for row in rows { + mut row_primitives := []orm.Primitive{} + keys := row.data.keys() + for _, key in keys { + prim := row.get(key)! + row_primitives << prim.primitive() or { return err } + } + ret << row_primitives + } + + return ret +} + // insert is used internally by V's ORM for processing `INSERT` queries pub fn (mut db Connection) insert(table string, data orm.QueryData) ! { - // println(data) - // mut tbl := get_table_columns(mut db, table, [data]) or { return err } - // println(tbl) + new_table := reformat_table_name(table) + mut tbl := get_table_values_map(mut db, table, [data]) or { return err } + + mut query := 'INSERT INTO ${new_table} (${data.fields.join(', ')}) VALUES (:${data.fields.join(', :')})' - mut values := primitive_array_to_string_array(data.data) + // if a table has a serial field, we need to remove it from the insert statement + // only allows one serial field per table, as do most RDBMS if data.auto_fields.len > 1 { return error('multiple AUTO fields are not supported') } else if data.auto_fields.len == 1 { - // println(data) - values[data.auto_fields[0]] = 'NEXT VALUE FOR "${serial_name(table)}"' + autofield_idx := data.auto_fields[0] + autofield := data.fields[autofield_idx] + tbl.delete(autofield) + query = query.replace(':${autofield}', 'NEXT VALUE FOR "${serial_name(table)}"') } - mut nums := []string{} - for i, _ in data.fields { - nums << '${i}' - } - insert_sql := 'INSERT INTO ${table} (${data.fields.join(', ')}) VALUES (${values.join(', ')})' - println(insert_sql) - // println(tbl) $if trace_vsql_orm ? { eprintln('> vsql insert: ${query}') } - db.query(insert_sql) or { return err } - // mut stmt := db.prepare(insert_sql) or { return err } - // stmt.query(tbl) or { return err } + mut stmt := db.prepare(query) or { return err } + stmt.query(tbl) or { return err } } // update is used internally by V's ORM for processing `UPDATE` queries pub fn (mut db Connection) update(table string, data orm.QueryData, where orm.QueryData) ! { - mut query, _ := orm.orm_stmt_gen(.sqlite, table, '', .update, true, ':', 1, data, + new_table := reformat_table_name(table) + mut query, _ := orm.orm_stmt_gen(.sqlite, new_table, '', .update, true, ':', 1, data, where) - // values := get_table_columns(mut db, table, [data, where]) or { return err } - query = query_converter(query, [data, where])! - println(query) + values := get_table_values_map(mut db, table, [data, where]) or { return err } + query = query_reformatter(query, [data, where]) + // println(query) $if trace_vsql_orm ? { eprintln('> vsql update: ${query}') } - db.query(query) or { return err } - // mut stmt := db.prepare(query) or { return err } - // stmt.query(values) or { return err } + mut stmt := db.prepare(query) or { return err } + stmt.query(values) or { return err } } // delete is used internally by V's ORM for processing `DELETE ` queries pub fn (mut db Connection) delete(table string, where orm.QueryData) ! { - mut query, _ := orm.orm_stmt_gen(.sqlite, table, '', .delete, true, ':', 1, orm.QueryData{}, + new_table := reformat_table_name(table) + mut query, _ := orm.orm_stmt_gen(.sqlite, new_table, '', .delete, true, ':', 1, orm.QueryData{}, where) - query = query_converter(query, [where])! - // values := get_table_columns(mut db, table, [where]) or { return err } + query = query_reformatter(query, [where]) + values := get_table_values_map(mut db, table, [where]) or { return err } $if trace_vsql_orm ? { eprintln('> vsql delete: ${query}') } - db.query(query) or { return err } - // mut stmt := db.prepare(query) or { return err } - // stmt.query(values) or { return err } + // db.query(query) or { return err } + mut stmt := db.prepare(query) or { return err } + stmt.query(values) or { return err } } // `last_id` is used internally by V's ORM for post-processing `INSERT` queries -// TODO i dont think vsql supports this +//