From f249feb9da3120f7a39aaf2b6cc8a4fe5282aed3 Mon Sep 17 00:00:00 2001 From: Louis Schmieder Date: Fri, 9 Sep 2022 20:08:48 +0200 Subject: [PATCH] orm: support parenthesized expressions as in `select from User where (name == 'Sam' && is_customer == true) || id == 1` (#15693) --- vlib/orm/orm.v | 40 ++++++++++++++++++++++++---- vlib/orm/orm_sql_or_blocks_test.v | 15 ++++++++--- vlib/orm/orm_test.v | 6 +++++ vlib/sqlite/orm.v | 17 ++++++++---- vlib/v/gen/c/sql.v | 44 +++++++++++++++++++++++++++---- 5 files changed, 104 insertions(+), 18 deletions(-) diff --git a/vlib/orm/orm.v b/vlib/orm/orm.v index 097606c85b..810dd3657e 100644 --- a/vlib/orm/orm.v +++ b/vlib/orm/orm.v @@ -107,13 +107,15 @@ fn (kind OrderType) to_str() string { // => 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 +// parentheses defines which fields will be inside () pub struct QueryData { pub: - fields []string - data []Primitive - types []int - kinds []OperationKind - is_and []bool + fields []string + data []Primitive + types []int + parentheses [][]int + kinds []OperationKind + is_and []bool } pub struct InfixType { @@ -266,11 +268,25 @@ pub fn orm_stmt_gen(table string, q string, kind StmtKind, num bool, qm string, } if kind == .update || kind == .delete { for i, field in where.fields { + mut pre_par := false + mut post_par := false + for par in where.parentheses { + if i in par { + pre_par = par[0] == i + post_par = par[1] == i + } + } + if pre_par { + str += '(' + } str += '$q$field$q ${where.kinds[i].to_str()} $qm' if num { str += '$c' c++ } + if post_par { + str += ')' + } if i < where.fields.len - 1 { str += ' AND ' } @@ -311,11 +327,25 @@ pub fn orm_select_gen(orm SelectConfig, q string, num bool, qm string, start_pos if orm.has_where { str += ' WHERE ' for i, field in where.fields { + mut pre_par := false + mut post_par := false + for par in where.parentheses { + if i in par { + pre_par = par[0] == i + post_par = par[1] == i + } + } + if pre_par { + str += '(' + } str += '$q$field$q ${where.kinds[i].to_str()} $qm' if num { str += '$c' c++ } + if post_par { + str += ')' + } if i < where.fields.len - 1 { if where.is_and[i] { str += ' AND ' diff --git a/vlib/orm/orm_sql_or_blocks_test.v b/vlib/orm/orm_sql_or_blocks_test.v index bb62a70d66..6609840025 100644 --- a/vlib/orm/orm_sql_or_blocks_test.v +++ b/vlib/orm/orm_sql_or_blocks_test.v @@ -9,7 +9,7 @@ struct User { const db_path = os.join_path(os.temp_dir(), 'sql_statement_or_blocks.db') fn test_ensure_db_exists_and_user_table_is_ok() ? { - db := sqlite.connect(db_path)? + mut db := sqlite.connect(db_path)? assert true eprintln('> drop pre-existing User table...') @@ -20,10 +20,11 @@ fn test_ensure_db_exists_and_user_table_is_ok() ? { create table User } or { panic(err) } assert true + db.close()? } fn test_sql_or_block_for_insert() ? { - db := sqlite.connect(db_path)? + mut db := sqlite.connect(db_path)? user := User{1, 'bilbo'} eprintln('> inserting user 1 (first try)...') @@ -41,10 +42,12 @@ fn test_sql_or_block_for_insert() ? { assert true println('user could not be inserted, err: $err') } + eprintln('LINE: ${@LINE}') + db.close()? } fn test_sql_or_block_for_select() ? { - db := sqlite.connect(db_path)? + mut db := sqlite.connect(db_path)? eprintln('> selecting user with id 1...') single := sql db { @@ -53,6 +56,7 @@ fn test_sql_or_block_for_select() ? { eprintln('could not select user, err: $err') User{0, ''} } + eprintln('LINE: ${@LINE}') assert single.id == 1 @@ -62,9 +66,11 @@ fn test_sql_or_block_for_select() ? { eprintln('could not select user, err: $err') User{0, ''} } + eprintln('LINE: ${@LINE}') assert failed.id == 0 assert failed.name == '' + eprintln('LINE: ${@LINE}') eprintln('> selecting users...') multiple := sql db { @@ -73,8 +79,11 @@ fn test_sql_or_block_for_select() ? { eprintln('could not users, err: $err') []User{} } + eprintln('LINE: ${@LINE}') assert multiple.len == 1 + eprintln('LINE: ${@LINE}') + db.close()? } fn test_finish() ? { diff --git a/vlib/orm/orm_test.v b/vlib/orm/orm_test.v index 3602a0383c..50d4f4de97 100644 --- a/vlib/orm/orm_test.v +++ b/vlib/orm/orm_test.v @@ -360,6 +360,12 @@ fn test_orm() { // has them zeroed, because the db field resolution is seconds. assert updated_time_mod.created.format_ss() == t.format_ss() + para_select := sql db { + select from User where (name == 'Sam' && is_customer == true) || id == 1 + } + + assert para_select[0] == first + sql db { drop table Module drop table TestTime diff --git a/vlib/sqlite/orm.v b/vlib/sqlite/orm.v index 599783a2dc..950702d057 100644 --- a/vlib/sqlite/orm.v +++ b/vlib/sqlite/orm.v @@ -8,14 +8,16 @@ import time 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) + $if trace_sqlite ? { + eprintln('> @select query: "$query"') + } stmt := db.new_init_stmt(query)? - mut c := 1 - sqlite_stmt_binder(stmt, where, query, mut c)? - sqlite_stmt_binder(stmt, data, query, mut c)? - defer { stmt.finalize() } + mut c := 1 + sqlite_stmt_binder(stmt, where, query, mut c)? + sqlite_stmt_binder(stmt, data, query, mut c)? mut ret := [][]orm.Primitive{} @@ -89,12 +91,17 @@ pub fn (db DB) drop(table string) ? { // Executes query and bind prepared statement data directly fn sqlite_stmt_worker(db DB, query string, data orm.QueryData, where orm.QueryData) ? { + $if trace_sqlite ? { + eprintln('> sqlite_stmt_worker query: "$query"') + } stmt := db.new_init_stmt(query)? + defer { + stmt.finalize() + } mut c := 1 sqlite_stmt_binder(stmt, data, query, mut c)? sqlite_stmt_binder(stmt, where, query, mut c)? stmt.orm_step(query)? - stmt.finalize() } // Binds all values of d in the prepared statement diff --git a/vlib/v/gen/c/sql.v b/vlib/v/gen/c/sql.v index 440af1ae7f..e2f4e0d591 100644 --- a/vlib/v/gen/c/sql.v +++ b/vlib/v/gen/c/sql.v @@ -277,6 +277,7 @@ fn (mut g Gen) sql_update(node ast.SqlStmtLine, expr string, table_name string) g.write('.kinds = __new_array_with_default_noscan(0, 0, sizeof(orm__OperationKind), 0),') g.write('.is_and = __new_array_with_default_noscan(0, 0, sizeof(bool), 0),') g.write('.types = __new_array_with_default_noscan(0, 0, sizeof(int), 0),') + g.write('.parentheses = __new_array_with_default_noscan(0, 0, sizeof(Array_int), 0),') if node.updated_columns.len > 0 { g.write('.fields = new_array_from_c_array($node.updated_columns.len, $node.updated_columns.len, sizeof(string),') g.write(' _MOV((string[$node.updated_columns.len]){') @@ -380,11 +381,12 @@ fn (mut g Gen) sql_write_orm_primitive(t ast.Type, expr ast.Expr) { g.write('),') } -fn (mut g Gen) sql_where_data(expr ast.Expr, mut fields []string, mut kinds []string, mut data []ast.Expr, mut is_and []bool) { +fn (mut g Gen) sql_where_data(expr ast.Expr, mut fields []string, mut parentheses [][]int, mut kinds []string, mut data []ast.Expr, mut is_and []bool) { match expr { ast.InfixExpr { g.sql_side = .left - g.sql_where_data(expr.left, mut fields, mut kinds, mut data, mut is_and) + g.sql_where_data(expr.left, mut fields, mut parentheses, mut kinds, mut data, mut + is_and) mut kind := match expr.op { .ne { 'orm__OperationKind__neq' @@ -417,11 +419,19 @@ fn (mut g Gen) sql_where_data(expr ast.Expr, mut fields []string, mut kinds []st kind = 'orm__OperationKind__eq' } } - if expr.left !is ast.InfixExpr && expr.right !is ast.InfixExpr { + if expr.left !is ast.InfixExpr && expr.right !is ast.InfixExpr && kind != '' { kinds << kind } g.sql_side = .right - g.sql_where_data(expr.right, mut fields, mut kinds, mut data, mut is_and) + g.sql_where_data(expr.right, mut fields, mut parentheses, mut kinds, mut data, mut + is_and) + } + ast.ParExpr { + mut par := [fields.len] + g.sql_where_data(expr.expr, mut fields, mut parentheses, mut kinds, mut data, mut + is_and) + par << fields.len - 1 + parentheses << par } ast.Ident { if g.sql_side == .left { @@ -450,9 +460,11 @@ fn (mut g Gen) sql_gen_where_data(where_expr ast.Expr) { g.write('(orm__QueryData){') mut fields := []string{} mut kinds := []string{} + mut parentheses := [][]int{} mut data := []ast.Expr{} mut is_and := []bool{} - g.sql_where_data(where_expr, mut fields, mut kinds, mut data, mut is_and) + g.sql_where_data(where_expr, mut fields, mut parentheses, mut kinds, mut data, mut + is_and) g.write('.types = __new_array_with_default_noscan(0, 0, sizeof(int), 0),') if fields.len > 0 { g.write('.fields = new_array_from_c_array($fields.len, $fields.len, sizeof(string),') @@ -476,6 +488,26 @@ fn (mut g Gen) sql_gen_where_data(where_expr ast.Expr) { } g.write('),') + g.write('.parentheses = ') + if parentheses.len > 0 { + g.write('new_array_from_c_array($parentheses.len, $parentheses.len, sizeof(Array_int), _MOV((Array_int[$parentheses.len]){') + for par in parentheses { + if par.len > 0 { + g.write('new_array_from_c_array($par.len, $par.len, sizeof(int), _MOV((int[$par.len]){') + for val in par { + g.write('$val,') + } + g.write('})),') + } else { + g.write('__new_array_with_default_noscan(0, 0, sizeof(int), 0),') + } + } + g.write('}))') + } else { + g.write('__new_array_with_default_noscan(0, 0, sizeof(Array_int), 0)') + } + g.write(',') + if kinds.len > 0 { g.write('.kinds = new_array_from_c_array($kinds.len, $kinds.len, sizeof(orm__OperationKind),') g.write(' _MOV((orm__OperationKind[$kinds.len]){') @@ -618,6 +650,7 @@ fn (mut g Gen) sql_select(node ast.SqlExpr, expr string, left string, or_expr as g.write('.types = __new_array_with_default_noscan(0, 0, sizeof(int), 0),') g.write('.kinds = __new_array_with_default_noscan(0, 0, sizeof(orm__OperationKind), 0),') g.write('.is_and = __new_array_with_default_noscan(0, 0, sizeof(bool), 0),') + g.write('.parentheses = __new_array_with_default_noscan(0, 0, sizeof(Array_int), 0),') if exprs.len > 0 { g.write('.data = new_array_from_c_array($exprs.len, $exprs.len, sizeof(orm__Primitive),') g.write(' _MOV((orm__Primitive[$exprs.len]){') @@ -637,6 +670,7 @@ fn (mut g Gen) sql_select(node ast.SqlExpr, expr string, left string, or_expr as g.write('.types = __new_array_with_default_noscan(0, 0, sizeof(int), 0),') g.write('.kinds = __new_array_with_default_noscan(0, 0, sizeof(orm__OperationKind), 0),') g.write('.is_and = __new_array_with_default_noscan(0, 0, sizeof(bool), 0),') + g.write('.parentheses = __new_array_with_default_noscan(0, 0, sizeof(Array_int), 0),') g.write('.data = __new_array_with_default_noscan(0, 0, sizeof(orm__Primitive), 0)') g.write('}') }