diff --git a/docs/v-client-library-docs.rst b/docs/v-client-library-docs.rst index d4265f4..2f9fe61 100644 --- a/docs/v-client-library-docs.rst +++ b/docs/v-client-library-docs.rst @@ -13,15 +13,29 @@ Constants -fn open -------- +fn open_database +---------------- .. code-block:: v - pub fn open(path string) !&Connection + pub fn open_database(path string, options ConnectionOptions) !&Connection -open is the convenience function for open_database() with default options. +open_database will open an existing database file or create a new file if the path does not exist. + +If the file does exist, open_database will assume that the file is a valid database file (not corrupt). Otherwise unexpected behavior or even a crash may occur. + +The special file name ":memory:" can be used to create an entirely in-memory database. This will be faster but all data will be lost when the connection is closed. + +open_database can be used concurrently for reading and writing to the same file and provides the following default protections: + +- Fine: Multiple processes open_database() the same file. + +- Fine: Multiple goroutines sharing an open_database() on the same file. + +- Bad: Multiple goroutines open_database() the same file. + +See ConnectionOptions and default_connection_options(). fn catalog_name_from_path ------------------------- @@ -85,29 +99,15 @@ fn default_connection_options default_connection_options returns the sensible defaults used by open() and the correct base to provide your own option overrides. See ConnectionOptions. -fn open_database ----------------- +fn open_orm +----------- .. code-block:: v - pub fn open_database(path string, options ConnectionOptions) !&Connection - -open_database will open an existing database file or create a new file if the path does not exist. - -If the file does exist, open_database will assume that the file is a valid database file (not corrupt). Otherwise unexpected behavior or even a crash may occur. + pub fn open_orm(path string) ORMConnection -The special file name ":memory:" can be used to create an entirely in-memory database. This will be faster but all data will be lost when the connection is closed. -open_database can be used concurrently for reading and writing to the same file and provides the following default protections: - -- Fine: Multiple processes open_database() the same file. - -- Fine: Multiple goroutines sharing an open_database() on the same file. - -- Bad: Multiple goroutines open_database() the same file. - -See ConnectionOptions and default_connection_options(). fn new_benchmark ---------------- @@ -217,6 +217,16 @@ new_numeric_value expects a value to be valid and the size and scale are determi +fn new_primitive_value +---------------------- + + +.. code-block:: v + + pub fn new_primitive_value(p orm.Primitive) !Value + +new_primitive_value returns the Value of a Primitive. Primitives are used by the ORM. + fn new_query_cache ------------------ @@ -287,25 +297,35 @@ fn new_timestamp_value new_timestamp_value creates a ``TIMESTAMP`` value. -fn new_varchar_value +fn new_unknown_value -------------------- .. code-block:: v - pub fn new_varchar_value(x string) Value + pub fn new_unknown_value() Value -new_varchar_value creates a ``CHARACTER VARYING`` value. +new_unknown_value returns an ``UNKNOWN`` value. This is the ``NULL`` representation of ``BOOLEAN``. -fn new_unknown_value +fn open +------- + + +.. code-block:: v + + pub fn open(path string) !&Connection + +open is the convenience function for open_database() with default options. + +fn new_varchar_value -------------------- .. code-block:: v - pub fn new_unknown_value() Value + pub fn new_varchar_value(x string) Value -new_unknown_value returns an ``UNKNOWN`` value. This is the ``NULL`` representation of ``BOOLEAN``. +new_varchar_value creates a ``CHARACTER VARYING`` value. type Row -------- @@ -341,6 +361,37 @@ enum Boolean Possible values for a BOOLEAN. +struct PageObject +----------------- + + +.. code-block:: v + + pub struct PageObject { + // The key is not required to be unique in the page. It becomes unique when + // combined with tid. However, no more than two version of the same key can + // exist in a page. See the caveats at the top of btree.v. + key []u8 + // The value contains the serialized data for the object. The first byte of + // key is used to both identify what type of object this is and also keep + // objects within the same collection also within the same range. + value []u8 + // When is_blob_ref is true, the value will be always be 5 bytes. See + // blob_info(). + is_blob_ref bool + mut: + // The tid is the transaction that created the object. + // + // TODO(elliotchance): It makes more sense to construct a new PageObject + // when changing the tid and xid. + tid int + // The xid is the transaciton that deleted the object, or zero if it has + // never been deleted. + xid int + } + +TODO(elliotchance): This does not need to be public. It was required for a bug at the time with V not being able to pass this to the shuffle function. At some point in the future remove the pub and see if it works. + struct ConnectionOptions ------------------------ @@ -684,36 +735,18 @@ struct Value A single value. It contains it's type information in ``typ``. -struct PageObject ------------------ +struct ORMConnection +-------------------- .. code-block:: v - pub struct PageObject { - // The key is not required to be unique in the page. It becomes unique when - // combined with tid. However, no more than two version of the same key can - // exist in a page. See the caveats at the top of btree.v. - key []u8 - // The value contains the serialized data for the object. The first byte of - // key is used to both identify what type of object this is and also keep - // objects within the same collection also within the same range. - value []u8 - // When is_blob_ref is true, the value will be always be 5 bytes. See - // blob_info(). - is_blob_ref bool + pub struct ORMConnection { mut: - // The tid is the transaction that created the object. - // - // TODO(elliotchance): It makes more sense to construct a new PageObject - // when changing the tid and xid. - tid int - // The xid is the transaciton that deleted the object, or zero if it has - // never been deleted. - xid int + c Connection } -TODO(elliotchance): This does not need to be public. It was required for a bug at the time with V not being able to pass this to the shuffle function. At some point in the future remove the pub and see if it works. + struct Identifier ----------------- diff --git a/examples/orm.v b/examples/orm.v new file mode 100644 index 0000000..42f4beb --- /dev/null +++ b/examples/orm.v @@ -0,0 +1,43 @@ +import os +import vsql + +fn main() { + os.rm('test.vsql') or {} + example() or { panic(err) } +} + +struct Product { + id int @[primary] + product_name string @[sql_type: 'varchar(100)'] + price f64 +} + +fn (p Product) str() string { + return '${p.product_name} ($${p.price})' +} + +fn example() ! { + mut db := vsql.open_orm('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 + }! + } + + println("Products over $2:") + for row in sql db { + select from Product where price > 2 + }! { + println(row) + } +} diff --git a/vsql/connection.v b/vsql/connection.v index 858e3aa..d2887c5 100644 --- a/vsql/connection.v +++ b/vsql/connection.v @@ -67,6 +67,10 @@ pub fn open(path string) !&Connection { return open_database(path, default_connection_options()) } +pub fn open_orm(path string) !ORMConnection { + return ORMConnection{open(path)!} +} + // open_database will open an existing database file or create a new file if the // path does not exist. // diff --git a/vsql/orm.v b/vsql/orm.v new file mode 100644 index 0000000..fe058dc --- /dev/null +++ b/vsql/orm.v @@ -0,0 +1,168 @@ +// 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() int +} +*/ + +pub struct ORMConnection { +mut: + c Connection +} + +pub fn (c ORMConnection) @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 := unsafe { c.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.str())! + } + + all_rows << primitive_row + } + + return all_rows +} + +pub fn (c ORMConnection) 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}' + } + orm.Null { + return 'NULL' + } + 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}' + } + } + }) + + unsafe { c.c.query('INSERT INTO ${table} (${data.fields.join(', ')}) VALUES (${values.join(', ')})')! } +} + +pub fn (c ORMConnection) update(table string, data orm.QueryData, where orm.QueryData) ! { + panic('update') +} + +pub fn (c ORMConnection) delete(table string, where orm.QueryData) ! { + panic('delete') +} + +pub fn (c ORMConnection) 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.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}' + } + + unsafe { c.c.query('CREATE TABLE ${table} (${sql_fields.join(', ')})')! } +} + +pub fn (c ORMConnection) drop(table string) ! { + panic('drop') +} + +pub fn (c ORMConnection) last_id() int { + panic('last_id') +} diff --git a/vsql/row.v b/vsql/row.v index db37c30..61ea8cd 100644 --- a/vsql/row.v +++ b/vsql/row.v @@ -4,6 +4,7 @@ module vsql import time +import orm // Represents a single row which may contain one or more columns. struct Row { @@ -105,6 +106,13 @@ fn (r Row) for_storage() Row { return Row{r.id, r.tid, new_data} } +// 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 0295d9f..ee6b9df 100644 --- a/vsql/value.v +++ b/vsql/value.v @@ -5,6 +5,7 @@ module vsql import regex +import orm // Possible values for a BOOLEAN. pub enum Boolean { @@ -186,6 +187,25 @@ pub fn new_numeric_value(x string) 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' } + 1 { 'TRUE' } + 2 { 'UNKNOWN' } + else { 'NULL' } + } +} + // new_decimal_value expects a value to be valid and the size and scale are // determined from the value as: // @@ -430,3 +450,14 @@ pub fn (v Value) numeric_value() Numeric { return v.v.numeric_value } } + +// primitives are used by the ORM. +pub fn (v Value) primitive() !orm.Primitive { + return match v.typ.typ { + // .is_boolean { orm.Primitive(v.f64_value() != 0) } + .is_integer { orm.Primitive(v.int_value()) } + .is_varchar { orm.Primitive(v.string_value()) } + .is_double_precision { orm.Primitive(v.f64_value()) } + else { error('${v.typ.typ}') } + } +}