diff --git a/Makefile b/Makefile
index 8266914..41cbd40 100644
--- a/Makefile
+++ b/Makefile
@@ -86,6 +86,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..91d4f66
--- /dev/null
+++ b/examples/orm.v
@@ -0,0 +1,61 @@
+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...
+// https://github.com/elliotchance/vsql/issues/200
+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},
+ ]
+
+ 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())
+}
diff --git a/vsql/orm.v b/vsql/orm.v
new file mode 100644
index 0000000..9a76e54
--- /dev/null
+++ b/vsql/orm.v
@@ -0,0 +1,371 @@
+module vsql
+
+import orm
+import time
+
+pub const varchar_default_len = 255
+
+// Returns a string of the vsql type based on the v type index
+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 {
+ 'INTEGER'
+ } 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'] {
+ 'BIGINT'
+ } else if typ == orm.type_idx['f64'] {
+ 'DOUBLE PRECISION'
+ } else if typ == orm.type_idx['string'] {
+ 'VARCHAR(${varchar_default_len})'
+ } else if typ == orm.time_ {
+ 'TIMESTAMP(6) WITHOUT TIME ZONE'
+ } else if typ == -1 {
+ // -1 is a field with @[sql: serial]
+ 'INTEGER'
+ } else {
+ error('Unknown type ${typ}')
+ }
+}
+
+// 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))
+ }
+ .is_bigint {
+ return new_bigint_value(i64(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}')
+}
+
+fn serial_name(table string) string {
+ return '${table}_SERIAL'
+}
+
+fn reformat_table_name(table string) string {
+ mut new_table := table
+ if is_reserved_word(table) {
+ new_table = '"${table}"'
+ }
+ return new_table
+}
+
+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 { [] }
+
+ 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')
+ }
+ 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() {
+ 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])!
+ }
+ }
+ }
+ }
+ }
+
+ 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
+ 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')
+ }
+ }
+ }
+}
+
+// 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])
+ 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) ! {
+ 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(', :')})'
+ // 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 {
+ 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 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) ! {
+ new_table := reformat_table_name(table)
+ mut query, _ := orm.orm_stmt_gen(.sqlite, new_table, '', .update, true, ':', 1, data,
+ where)
+ values := get_table_values_map(mut db, table, [data, where]) or { return err }
+ query = query_reformatter(query, [data, where])
+ 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) ! {
+ new_table := reformat_table_name(table)
+ mut query, _ := orm.orm_stmt_gen(.sqlite, new_table, '', .delete, true, ':', 1, orm.QueryData{},
+ where)
+ query = query_reformatter(query, [where])
+ values := get_table_values_map(mut db, table, [where]) 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 }
+ new_table := reformat_table_name(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})')
+ 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) ! {
+ new_table := reformat_table_name(table)
+ mut query := 'DROP TABLE ${new_table};'
+ db.query(query) or { return err }
+
+ // check to see if there is a SEQUENCE for the table (for the @[sql: 'serial'] attribute)
+ // if there is, drop it
+ mut cat := db.catalog()
+ seqs := cat.sequences('PUBLIC') or { [] }
+ for sequence in seqs {
+ if sequence.name.entity_name == serial_name(table).to_upper() {
+ query = 'DROP SEQUENCE "${serial_name(table)}"'
+ db.query(query) or { return err }
+ }
+ }
+}
+
+// convert a vsql row value to orm.Primitive
+fn (v Value) primitive() !orm.Primitive {
+ if v.is_null {
+ return orm.Null{}
+ }
+ return match v.typ.typ {
+ .is_boolean {
+ orm.Primitive(v.bool_value() == .is_true)
+ }
+ .is_smallint {
+ orm.Primitive(i16(v.int_value()))
+ }
+ .is_integer {
+ orm.Primitive(int(v.int_value()))
+ }
+ .is_bigint {
+ orm.Primitive(i64(v.int_value()))
+ }
+ .is_varchar, .is_character {
+ orm.Primitive(v.string_value())
+ }
+ .is_real {
+ orm.Primitive(f32(v.f64_value()))
+ }
+ .is_double_precision {
+ orm.Primitive(v.f64_value())
+ }
+ .is_decimal, .is_numeric {
+ orm.Primitive(v.str())
+ }
+ .is_date, .is_time_with_time_zone, .is_timestamp_without_time_zone,
+ .is_timestamp_with_time_zone, .is_time_without_time_zone {
+ orm.Primitive(v.str())
+ }
+ }
+}
diff --git a/vsql/orm_test.v b/vsql/orm_test.v
new file mode 100644
index 0000000..aa9e4c9
--- /dev/null
+++ b/vsql/orm_test.v
@@ -0,0 +1,404 @@
+module vsql
+
+// Structs intentionally have less than 6 fields, any more then inserts queries get exponentially slower.
+// https://github.com/elliotchance/vsql/issues/199
+import time
+
+@[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) / VARCHAR(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 INTEGER
+}
+
+// ORMTableEnum is not supported.
+struct ORMTableEnum {
+ an_enum Colors
+}
+
+enum Colors {
+ red
+ green
+ blue
+}
+
+@[table: 'GROUP']
+struct ORMTable2 {
+ dummy int
+}
+
+struct Product {
+ id int
+ product_name string
+ price string @[sql_type: 'NUMERIC(5,2)']
+ quantity ?i16
+}
+
+fn test_orm_create_table_with_reserved_word() {
+ mut db := open(':memory:')!
+ mut error := ''
+ sql db {
+ create table ORMTable2
+ } or { error = err.str() }
+ assert error == ''
+ dumm := ORMTable2{
+ dummy: 1
+ }
+ sql db {
+ insert dumm into ORMTable2
+ } or { error = err.str() }
+ assert error == ''
+
+ sql db {
+ update ORMTable2 set dummy = 2 where dummy == 1
+ }!
+
+ all := sql db {
+ select from ORMTable2
+ }!
+ assert all[0].dummy == 2
+
+ sql db {
+ delete from ORMTable2 where dummy == 2
+ } or { error = err.str() }
+ assert error == ''
+ sql db {
+ drop table ORMTable2
+ } or { error = err.str() }
+
+ assert error == ''
+}
+
+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
+ 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'
+}
+
+fn test_orm_select_where() {
+ mut db := open(':memory:')!
+ mut error := ''
+
+ sql db {
+ create table Product
+ } or { panic(err) }
+
+ prods := [
+ Product{1, 'Ice Cream', '5.99', 17},
+ Product{2, 'Ham Sandwhich', '3.47', none},
+ Product{3, 'Bagel', '1.25', 45},
+ ]
+ for product in prods {
+ sql db {
+ insert product into Product
+ } or { panic(err) }
+ }
+ mut products := sql db {
+ select from Product where id == 2
+ }!
+
+ assert products == [Product{2, 'Ham Sandwhich', '3.47', none}]
+
+ products = sql db {
+ select from Product where id == 5
+ }!
+
+ assert products == []
+
+ products = sql db {
+ select from Product where id != 3
+ }!
+
+ assert products == [Product{1, 'Ice Cream', '5.99', 17},
+ Product{2, 'Ham Sandwhich', '3.47', none}]
+
+ products = sql db {
+ select from Product where price > '3.47'
+ }!
+
+ assert products == [Product{1, 'Ice Cream', '5.99', 17}]
+
+ products = sql db {
+ select from Product where price >= '3'
+ }!
+
+ assert products == [Product{1, 'Ice Cream', '5.99', 17},
+ Product{2, 'Ham Sandwhich', '3.47', none}]
+
+ products = sql db {
+ select from Product where price < '3.47'
+ }!
+
+ assert products == [Product{3, 'Bagel', '1.25', 45}]
+
+ products = sql db {
+ select from Product where price <= '5'
+ }!
+
+ assert products == [Product{2, 'Ham Sandwhich', '3.47', none},
+ Product{3, 'Bagel', '1.25', 45}]
+
+ // TODO (daniel-le97): The ORM does not support a "not like" constraint right now.
+
+ products = sql db {
+ select from Product where product_name like 'Ham%'
+ }!
+
+ assert products == [Product{2, 'Ham Sandwhich', '3.47', none}]
+
+ products = sql db {
+ select from Product where quantity is none
+ }!
+
+ assert products == [Product{2, 'Ham Sandwhich', '3.47', none}]
+
+ products = sql db {
+ select from Product where quantity !is none
+ }!
+
+ assert products == [Product{1, 'Ice Cream', '5.99', 17}, Product{3, 'Bagel', '1.25', 45}]
+
+ products = sql db {
+ select from Product where price > '3' && price < '3.50'
+ }!
+
+ assert products == [Product{2, 'Ham Sandwhich', '3.47', none}]
+
+ products = sql db {
+ select from Product where price < '2.000' || price >= '5'
+ }!
+
+ assert products == [Product{1, 'Ice Cream', '5.99', 17}, Product{3, 'Bagel', '1.25', 45}]
+}