orm: document & fix pg (#14533)

Louis Schmieder 2022-05-26 21:53:09 +02:00 committed by Chewing_Bever
parent dca8739eeb
commit 29fc96c040
Signed by: Jef Roosens
GPG Key ID: B75D4F293C7052DB
4 changed files with 112 additions and 45 deletions

View File

@ -102,6 +102,10 @@ fn (kind OrderType) to_str() string {
} }
} }
// Examples for QueryData in SQL: abc == 3 && b == 'test'
// => fields[abc, b]; data[3, 'test']; types[index of int, index of string]; kinds[.eq, .eq]; is_and[true];
// Every field, data, type & kind of operation in the expr share the same index in the arrays
// is_and defines how they're addicted to each other either and or or
pub struct QueryData { pub struct QueryData {
pub: pub:
fields []string fields []string
@ -128,6 +132,17 @@ pub:
attrs []StructAttribute attrs []StructAttribute
} }
// table - Table name
// is_count - Either the data will be returned or an integer with the count
// has_where - Select all or use a where expr
// has_order - Order the results
// order - Name of the column which will be ordered
// order_type - Type of order (asc, desc)
// has_limit - Limits the output data
// primary - Name of the primary field
// has_offset - Add an offset to the result
// fields - Fields to select
// types - Types to select
pub struct SelectConfig { pub struct SelectConfig {
pub: pub:
table string table string
@ -143,6 +158,14 @@ pub:
types []int types []int
} }
// Interfaces gets called from the backend and can be implemented
// Since the orm supports arrays aswell, they have to be returned too.
// A row is represented as []Primitive, where the data is connected to the fields of the struct by their
// index. The indices are mapped with the SelectConfig.field array. This is the mapping for a struct.
// To have an array, there has to be an array of structs, basically [][]Primitive
//
// Every function without last_id() returns an optional, which returns an error if present
// last_id returns the last inserted id of the db
pub interface Connection { pub interface Connection {
@select(config SelectConfig, data QueryData, where QueryData) ?[][]Primitive @select(config SelectConfig, data QueryData, where QueryData) ?[][]Primitive
insert(table string, data QueryData) ? insert(table string, data QueryData) ?
@ -153,7 +176,12 @@ pub interface Connection {
last_id() Primitive last_id() Primitive
} }
pub fn orm_stmt_gen(table string, para string, kind StmtKind, num bool, qm string, start_pos int, data QueryData, where QueryData) string { // Generates an sql stmt, from universal parameter
// q - The quotes character, which can be different in every type, so it's variable
// num - Stmt uses nums at prepared statements (? or ?1)
// qm - Character for prepared statment, qm because of quotation mark like in sqlite
// start_pos - When num is true, it's the start position of the counter
pub fn orm_stmt_gen(table string, q string, kind StmtKind, num bool, qm string, start_pos int, data QueryData, where QueryData) string {
mut str := '' mut str := ''
mut c := start_pos mut c := start_pos
@ -163,7 +191,7 @@ pub fn orm_stmt_gen(table string, para string, kind StmtKind, num bool, qm strin
mut values := []string{} mut values := []string{}
for _ in 0 .. data.fields.len { for _ in 0 .. data.fields.len {
// loop over the length of data.field and generate ?0, ?1 or just ? based on the $num parameter for value placeholders // loop over the length of data.field and generate ?0, ?1 or just ? based on the $num qmeter for value placeholders
if num { if num {
values << '$qm$c' values << '$qm$c'
c++ c++
@ -172,16 +200,16 @@ pub fn orm_stmt_gen(table string, para string, kind StmtKind, num bool, qm strin
} }
} }
str += 'INSERT INTO $para$table$para (' str += 'INSERT INTO $q$table$q ('
str += data.fields.map('$para$it$para').join(', ') str += data.fields.map('$q$it$q').join(', ')
str += ') VALUES (' str += ') VALUES ('
str += values.join(', ') str += values.join(', ')
str += ')' str += ')'
} }
.update { .update {
str += 'UPDATE $para$table$para SET ' str += 'UPDATE $q$table$q SET '
for i, field in data.fields { for i, field in data.fields {
str += '$para$field$para = ' str += '$q$field$q = '
if data.data.len > i { if data.data.len > i {
d := data.data[i] d := data.data[i]
if d is InfixType { if d is InfixType {
@ -217,12 +245,12 @@ pub fn orm_stmt_gen(table string, para string, kind StmtKind, num bool, qm strin
str += ' WHERE ' str += ' WHERE '
} }
.delete { .delete {
str += 'DELETE FROM $para$table$para WHERE ' str += 'DELETE FROM $q$table$q WHERE '
} }
} }
if kind == .update || kind == .delete { if kind == .update || kind == .delete {
for i, field in where.fields { for i, field in where.fields {
str += '$para$field$para ${where.kinds[i].to_str()} $qm' str += '$q$field$q ${where.kinds[i].to_str()} $qm'
if num { if num {
str += '$c' str += '$c'
c++ c++
@ -236,28 +264,32 @@ pub fn orm_stmt_gen(table string, para string, kind StmtKind, num bool, qm strin
return str return str
} }
pub fn orm_select_gen(orm SelectConfig, para string, num bool, qm string, start_pos int, where QueryData) string { // Generates an sql select stmt, from universal parameter
// orm - See SelectConfig
// q, num, qm, start_pos - see orm_stmt_gen
// where - See QueryData
pub fn orm_select_gen(orm SelectConfig, q string, num bool, qm string, start_pos int, where QueryData) string {
mut str := 'SELECT ' mut str := 'SELECT '
if orm.is_count { if orm.is_count {
str += 'COUNT(*)' str += 'COUNT(*)'
} else { } else {
for i, field in orm.fields { for i, field in orm.fields {
str += '$para$field$para' str += '$q$field$q'
if i < orm.fields.len - 1 { if i < orm.fields.len - 1 {
str += ', ' str += ', '
} }
} }
} }
str += ' FROM $para$orm.table$para' str += ' FROM $q$orm.table$q'
mut c := start_pos mut c := start_pos
if orm.has_where { if orm.has_where {
str += ' WHERE ' str += ' WHERE '
for i, field in where.fields { for i, field in where.fields {
str += '$para$field$para ${where.kinds[i].to_str()} $qm' str += '$q$field$q ${where.kinds[i].to_str()} $qm'
if num { if num {
str += '$c' str += '$c'
c++ c++
@ -276,7 +308,7 @@ pub fn orm_select_gen(orm SelectConfig, para string, num bool, qm string, start_
// ordering is *slow*, especially if there are no indexes! // ordering is *slow*, especially if there are no indexes!
if orm.has_order { if orm.has_order {
str += ' ORDER BY ' str += ' ORDER BY '
str += '$para$orm.order$para ' str += '$q$orm.order$q '
str += orm.order_type.to_str() str += orm.order_type.to_str()
} }
@ -300,11 +332,19 @@ pub fn orm_select_gen(orm SelectConfig, para string, num bool, qm string, start_
return str return str
} }
pub fn orm_table_gen(table string, para string, defaults bool, def_unique_len int, fields []TableField, sql_from_v fn (int) ?string, alternative bool) ?string { // Generates an sql table stmt, from universal parameter
mut str := 'CREATE TABLE IF NOT EXISTS $para$table$para (' // table - Table name
// q - see orm_stmt_gen
// defaults - enables default values in stmt
// def_unique_len - sets default unique length for texts
// fields - See TableField
// sql_from_v - Function which maps type indices to sql type names
// alternative - Needed for msdb
pub fn orm_table_gen(table string, q string, defaults bool, def_unique_len int, fields []TableField, sql_from_v fn (int) ?string, alternative bool) ?string {
mut str := 'CREATE TABLE IF NOT EXISTS $q$table$q ('
if alternative { if alternative {
str = 'IF NOT EXISTS (SELECT * FROM sysobjects WHERE name=$para$table$para and xtype=${para}U$para) CREATE TABLE $para$table$para (' str = 'IF NOT EXISTS (SELECT * FROM sysobjects WHERE name=$q$table$q and xtype=${q}U$q) CREATE TABLE $q$table$q ('
} }
mut fs := []string{} mut fs := []string{}
@ -368,7 +408,7 @@ pub fn orm_table_gen(table string, para string, defaults bool, def_unique_len in
if ctyp == '' { if ctyp == '' {
return error('Unknown type ($field.typ) for field $field.name in struct $table') return error('Unknown type ($field.typ) for field $field.name in struct $table')
} }
stmt = '$para$field_name$para $ctyp' stmt = '$q$field_name$q $ctyp'
if defaults && field.default_val != '' { if defaults && field.default_val != '' {
stmt += ' DEFAULT $field.default_val' stmt += ' DEFAULT $field.default_val'
} }
@ -376,7 +416,7 @@ pub fn orm_table_gen(table string, para string, defaults bool, def_unique_len in
stmt += ' NOT NULL' stmt += ' NOT NULL'
} }
if is_unique { if is_unique {
mut f := 'UNIQUE($para$field_name$para' mut f := 'UNIQUE($q$field_name$q'
if ctyp == 'TEXT' && def_unique_len > 0 { if ctyp == 'TEXT' && def_unique_len > 0 {
if unique_len > 0 { if unique_len > 0 {
f += '($unique_len)' f += '($unique_len)'
@ -396,18 +436,19 @@ pub fn orm_table_gen(table string, para string, defaults bool, def_unique_len in
for k, v in unique { for k, v in unique {
mut tmp := []string{} mut tmp := []string{}
for f in v { for f in v {
tmp << '$para$f$para' tmp << '$q$f$q'
} }
fs << '/* $k */UNIQUE(${tmp.join(', ')})' fs << '/* $k */UNIQUE(${tmp.join(', ')})'
} }
} }
fs << 'PRIMARY KEY($para$primary$para)' fs << 'PRIMARY KEY($q$primary$q)'
fs << unique_fields fs << unique_fields
str += fs.join(', ') str += fs.join(', ')
str += ');' str += ');'
return str return str
} }
// Get's the sql field type
fn sql_field_type(field TableField) int { fn sql_field_type(field TableField) int {
mut typ := field.typ mut typ := field.typ
if field.is_time { if field.is_time {
@ -426,6 +467,7 @@ fn sql_field_type(field TableField) int {
return typ return typ
} }
// Get's the sql field name
fn sql_field_name(field TableField) string { fn sql_field_name(field TableField) string {
mut name := field.name mut name := field.name
for attr in field.attrs { for attr in field.attrs {

View File

@ -1,7 +1,7 @@
// import os // import os
// import pg
// import term // import term
import time import time
// import pg
import sqlite import sqlite
struct Module { struct Module {
@ -31,9 +31,12 @@ struct TestTime {
create time.Time create time.Time
} }
fn test_orm_sqlite() { fn test_orm() {
db := sqlite.connect(':memory:') or { panic(err) } db := sqlite.connect(':memory:') or { panic(err) }
db.exec('drop table if exists User') // db.exec('drop table if exists User')
// db := pg.connect(host: 'localhost', port: 5432, user: 'louis', password: 'abc', dbname: 'orm') or { panic(err) }
sql db { sql db {
create table Module create table Module
} }
@ -242,7 +245,7 @@ fn test_orm_sqlite() {
// //
offset_const := 2 offset_const := 2
z := sql db { z := sql db {
select from User limit 2 offset offset_const select from User order by id limit 2 offset offset_const
} }
assert z.len == 2 assert z.len == 2
assert z[0].id == 3 assert z[0].id == 3
@ -264,6 +267,7 @@ fn test_orm_sqlite() {
} }
assert updated_oldest.age == 31 assert updated_oldest.age == 31
// Remove this when pg is used
db.exec('insert into User (name, age) values (NULL, 31)') db.exec('insert into User (name, age) values (NULL, 31)')
null_user := sql db { null_user := sql db {
select from User where id == 5 select from User where id == 5
@ -336,11 +340,18 @@ fn test_orm_sqlite() {
sql db { sql db {
update Module set created = t where id == 1 update Module set created = t where id == 1
} }
updated_time_mod := sql db { updated_time_mod := sql db {
select from Module where id == 1 select from Module where id == 1
} }
// Note: usually updated_time_mod.created != t, because t has // Note: usually updated_time_mod.created != t, because t has
// its microseconds set, while the value retrieved from the DB // its microseconds set, while the value retrieved from the DB
// has them zeroed, because the db field resolution is seconds. // has them zeroed, because the db field resolution is seconds.
assert updated_time_mod.created.format_ss() == t.format_ss() assert updated_time_mod.created.format_ss() == t.format_ss()
sql db {
drop table Module
drop table TestTime
}
} }

View File

@ -8,9 +8,13 @@ import net.conv
pub fn (db DB) @select(config orm.SelectConfig, data orm.QueryData, where orm.QueryData) ?[][]orm.Primitive { pub fn (db DB) @select(config orm.SelectConfig, data orm.QueryData, where orm.QueryData) ?[][]orm.Primitive {
query := orm.orm_select_gen(config, '"', true, '$', 1, where) query := orm.orm_select_gen(config, '"', true, '$', 1, where)
res := pg_stmt_worker(db, query, where, data)?
mut ret := [][]orm.Primitive{} mut ret := [][]orm.Primitive{}
res := pg_stmt_worker(db, query, orm.QueryData{}, where)? if config.is_count {
}
for row in res { for row in res {
mut row_data := []orm.Primitive{} mut row_data := []orm.Primitive{}
@ -166,7 +170,9 @@ fn pg_stmt_match(mut types []u32, mut vals []&char, mut lens []int, mut formats
} }
time.Time { time.Time {
types << u32(Oid.t_int4) types << u32(Oid.t_int4)
vals << &char(&int(data.unix)) unix := int(data.unix)
num := conv.htn32(unsafe { &u32(&unix) })
vals << &char(&num)
lens << int(sizeof(u32)) lens << int(sizeof(u32))
formats << 1 formats << 1
} }
@ -178,19 +184,22 @@ fn pg_stmt_match(mut types []u32, mut vals []&char, mut lens []int, mut formats
fn pg_type_from_v(typ int) ?string { fn pg_type_from_v(typ int) ?string {
str := match typ { str := match typ {
6, 10 { orm.type_idx['i8'], orm.type_idx['i16'], orm.type_idx['byte'], orm.type_idx['u16'] {
'SMALLINT' 'SMALLINT'
} }
7, 11, orm.time { orm.type_idx['bool'] {
'BOOLEAN'
}
orm.type_idx['int'], orm.type_idx['u32'], orm.time {
'INT' 'INT'
} }
8, 12 { orm.type_idx['i64'], orm.type_idx['u64'] {
'BIGINT' 'BIGINT'
} }
13 { orm.float[0] {
'REAL' 'REAL'
} }
14 { orm.float[1] {
'DOUBLE PRECISION' 'DOUBLE PRECISION'
} }
orm.string { orm.string {
@ -212,54 +221,51 @@ fn pg_type_from_v(typ int) ?string {
fn str_to_primitive(str string, typ int) ?orm.Primitive { fn str_to_primitive(str string, typ int) ?orm.Primitive {
match typ { match typ {
// bool // bool
16 { orm.type_idx['bool'] {
return orm.Primitive(str.i8() == 1)
}
18 {
return orm.Primitive(str == 't') return orm.Primitive(str == 't')
} }
// i8 // i8
5 { orm.type_idx['i8'] {
return orm.Primitive(str.i8()) return orm.Primitive(str.i8())
} }
// i16 // i16
6 { orm.type_idx['i16'] {
return orm.Primitive(str.i16()) return orm.Primitive(str.i16())
} }
// int // int
7 { orm.type_idx['int'] {
return orm.Primitive(str.int()) return orm.Primitive(str.int())
} }
// i64 // i64
8 { orm.type_idx['i64'] {
return orm.Primitive(str.i64()) return orm.Primitive(str.i64())
} }
// byte // byte
9 { orm.type_idx['byte'] {
data := str.i8() data := str.i8()
return orm.Primitive(*unsafe { &u8(&data) }) return orm.Primitive(*unsafe { &u8(&data) })
} }
// u16 // u16
10 { orm.type_idx['u16'] {
data := str.i16() data := str.i16()
return orm.Primitive(*unsafe { &u16(&data) }) return orm.Primitive(*unsafe { &u16(&data) })
} }
// u32 // u32
11 { orm.type_idx['u32'] {
data := str.int() data := str.int()
return orm.Primitive(*unsafe { &u32(&data) }) return orm.Primitive(*unsafe { &u32(&data) })
} }
// u64 // u64
12 { orm.type_idx['u64'] {
data := str.i64() data := str.i64()
return orm.Primitive(*unsafe { &u64(&data) }) return orm.Primitive(*unsafe { &u64(&data) })
} }
// f32 // f32
13 { orm.type_idx['f32'] {
return orm.Primitive(str.f32()) return orm.Primitive(str.f32())
} }
// f64 // f64
14 { orm.type_idx['f64'] {
return orm.Primitive(str.f64()) return orm.Primitive(str.f64())
} }
orm.string { orm.string {

View File

@ -6,6 +6,7 @@ import time
// sql expr // sql expr
pub fn (db DB) @select(config orm.SelectConfig, data orm.QueryData, where orm.QueryData) ?[][]orm.Primitive { pub fn (db DB) @select(config orm.SelectConfig, data orm.QueryData, where orm.QueryData) ?[][]orm.Primitive {
// 1. Create query and bind necessary data
query := orm.orm_select_gen(config, '`', true, '?', 1, where) query := orm.orm_select_gen(config, '`', true, '?', 1, where)
stmt := db.new_init_stmt(query)? stmt := db.new_init_stmt(query)?
mut c := 1 mut c := 1
@ -19,6 +20,7 @@ pub fn (db DB) @select(config orm.SelectConfig, data orm.QueryData, where orm.Qu
mut ret := [][]orm.Primitive{} mut ret := [][]orm.Primitive{}
if config.is_count { if config.is_count {
// 2. Get count of returned values & add it to ret array
step := stmt.step() step := stmt.step()
if step !in [sqlite_row, sqlite_ok, sqlite_done] { if step !in [sqlite_row, sqlite_ok, sqlite_done] {
return db.error_message(step, query) return db.error_message(step, query)
@ -28,6 +30,7 @@ pub fn (db DB) @select(config orm.SelectConfig, data orm.QueryData, where orm.Qu
return ret return ret
} }
for { for {
// 2. Parse returned values
step := stmt.step() step := stmt.step()
if step == sqlite_done { if step == sqlite_done {
break break
@ -83,6 +86,7 @@ pub fn (db DB) drop(table string) ? {
// helper // helper
// Executes query and bind prepared statement data directly
fn sqlite_stmt_worker(db DB, query string, data orm.QueryData, where orm.QueryData) ? { fn sqlite_stmt_worker(db DB, query string, data orm.QueryData, where orm.QueryData) ? {
stmt := db.new_init_stmt(query)? stmt := db.new_init_stmt(query)?
mut c := 1 mut c := 1
@ -92,6 +96,7 @@ fn sqlite_stmt_worker(db DB, query string, data orm.QueryData, where orm.QueryDa
stmt.finalize() stmt.finalize()
} }
// Binds all values of d in the prepared statement
fn sqlite_stmt_binder(stmt Stmt, d orm.QueryData, query string, mut c &int) ? { fn sqlite_stmt_binder(stmt Stmt, d orm.QueryData, query string, mut c &int) ? {
for data in d.data { for data in d.data {
err := bind(stmt, c, data) err := bind(stmt, c, data)
@ -103,6 +108,7 @@ fn sqlite_stmt_binder(stmt Stmt, d orm.QueryData, query string, mut c &int) ? {
} }
} }
// Universal bind function
fn bind(stmt Stmt, c &int, data orm.Primitive) int { fn bind(stmt Stmt, c &int, data orm.Primitive) int {
mut err := 0 mut err := 0
match data { match data {
@ -128,6 +134,7 @@ fn bind(stmt Stmt, c &int, data orm.Primitive) int {
return err return err
} }
// Selects column in result and converts it to an orm.Primitive
fn (stmt Stmt) sqlite_select_column(idx int, typ int) ?orm.Primitive { fn (stmt Stmt) sqlite_select_column(idx int, typ int) ?orm.Primitive {
mut primitive := orm.Primitive(0) mut primitive := orm.Primitive(0)
@ -149,6 +156,7 @@ fn (stmt Stmt) sqlite_select_column(idx int, typ int) ?orm.Primitive {
return primitive return primitive
} }
// Convert type int to sql type string
fn sqlite_type_from_v(typ int) ?string { fn sqlite_type_from_v(typ int) ?string {
return if typ in orm.nums || typ < 0 || typ in orm.num64 || typ == orm.time { return if typ in orm.nums || typ < 0 || typ in orm.num64 || typ == orm.time {
'INTEGER' 'INTEGER'