1
0
mirror of https://github.com/vlang/v.git synced 2023-08-10 21:13:21 +03:00

checker: add type checking for ORM limit, offset, and order by. (#17095)

This commit is contained in:
walking devel 2023-01-31 08:22:02 +00:00 committed by GitHub
parent b487c9d38e
commit d563739264
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 176 additions and 32 deletions

View File

@ -3,6 +3,10 @@
import time
import db.sqlite
const (
offset_const = 2
)
struct Module {
id int [primary; sql: serial]
name string
@ -233,16 +237,17 @@ fn test_orm() {
assert y.len == 2
assert y[0].id == 2
offset_const := 2
z := sql db {
select from User order by id limit 2 offset offset_const
}
assert z.len == 2
assert z[0].id == 3
oldest := sql db {
select from User order by age desc limit 1
}
assert oldest.age == 34
offs := 1
second_oldest := sql db {
select from User order by age desc limit 1 offset offs

View File

@ -1,7 +1,6 @@
module checker
import v.ast
import v.pref
import v.util
import v.token
import strings
@ -147,7 +146,10 @@ fn (mut c Checker) get_comptime_number_value(mut expr ast.Expr) ?i64 {
return expr.val.i64()
}
if mut expr is ast.Ident {
if mut obj := c.table.global_scope.find_const(expr.name) {
has_expr_mod_in_name := expr.name.contains('.')
expr_name := if has_expr_mod_in_name { expr.name } else { '${expr.mod}.${expr.name}' }
if mut obj := c.table.global_scope.find_const(expr_name) {
if obj.typ == 0 {
obj.typ = c.expr(obj.expr)
}

View File

@ -4,9 +4,11 @@ module checker
import v.ast
import v.token
import v.util
const (
fkey_attr_name = 'fkey'
v_orm_prefix = 'V ORM'
)
fn (mut c Checker) sql_expr(mut node ast.SqlExpr) ast.Type {
@ -22,7 +24,7 @@ fn (mut c Checker) sql_expr(mut node ast.SqlExpr) ast.Type {
c.cur_orm_ts = old_ts
}
if sym.info !is ast.Struct {
c.error('the table symbol `${sym.name}` has to be a struct', node.table_expr.pos)
c.orm_error('the table symbol `${sym.name}` has to be a struct', node.table_expr.pos)
return ast.void_type
}
info := sym.info as ast.Struct
@ -103,20 +105,42 @@ fn (mut c Checker) sql_expr(mut node ast.SqlExpr) ast.Type {
c.expr(node.where_expr)
c.check_expr_has_no_fn_calls_with_non_orm_return_type(&node.where_expr)
}
if node.has_offset {
c.expr(node.offset_expr)
if node.has_order {
if mut node.order_expr is ast.Ident {
order_ident_name := node.order_expr.name
sym.find_field(order_ident_name) or {
field_names := fields.map(it.name)
c.orm_error(util.new_suggestion(order_ident_name, field_names).say('`${sym.name}` structure has no field with name `${order_ident_name}`'),
node.order_expr.pos)
return ast.void_type
}
} else {
c.orm_error("expected `${sym.name}` structure's field", node.order_expr.pos())
return ast.void_type
}
c.expr(node.order_expr)
}
if node.has_limit {
c.expr(node.limit_expr)
c.check_sql_value_expr_is_comptime_with_natural_number_or_expr_with_int_type(mut node.limit_expr,
'limit')
}
if node.has_order {
c.expr(node.order_expr)
if node.has_offset {
c.expr(node.offset_expr)
c.check_sql_value_expr_is_comptime_with_natural_number_or_expr_with_int_type(mut node.offset_expr,
'offset')
}
c.expr(node.db_expr)
if node.or_expr.kind == .block {
if node.or_expr.stmts.len == 0 {
c.error('or block needs to return a default value', node.or_expr.pos)
c.orm_error('or block needs to return a default value', node.or_expr.pos)
}
if node.or_expr.stmts.len > 0 && node.or_expr.stmts.last() is ast.ExprStmt {
c.expected_or_type = node.typ
@ -176,6 +200,7 @@ fn (mut c Checker) sql_stmt_line(mut node ast.SqlStmtLine) ast.Type {
c.error('unknown type `${table_sym.name}`', node.pos)
return ast.void_type
}
info := table_sym.info as ast.Struct
fields := c.fetch_and_verify_orm_fields(info, node.table_expr.pos, table_sym.name)
mut sub_structs := map[int]ast.SqlStmtLine{}
@ -216,7 +241,7 @@ fn (mut c Checker) sql_stmt_line(mut node ast.SqlStmtLine) ast.Type {
for i, column in node.updated_columns {
x := node.fields.filter(it.name == column)
if x.len == 0 {
c.error('type `${table_sym.name}` has no field named `${column}`', node.pos)
c.orm_error('type `${table_sym.name}` has no field named `${column}`', node.pos)
continue
}
field := x[0]
@ -241,19 +266,19 @@ fn (mut c Checker) check_orm_struct_field_attributes(field ast.StructField) {
for attr in field.attrs {
if attr.name == checker.fkey_attr_name {
if field_type.kind != .array && field_type.kind != .struct_ {
c.error('The `${checker.fkey_attr_name}` attribute must be used only with arrays and structures',
c.orm_error('the `${checker.fkey_attr_name}` attribute must be used only with arrays and structures',
attr.pos)
return
}
if !attr.has_arg {
c.error('The `${checker.fkey_attr_name}` attribute must have an argument',
c.orm_error('the `${checker.fkey_attr_name}` attribute must have an argument',
attr.pos)
return
}
if attr.kind != .string {
c.error('`${checker.fkey_attr_name}` attribute must be string. Try [${checker.fkey_attr_name}: \'${attr.arg}\'] instead of [${checker.fkey_attr_name}: ${attr.arg}]',
c.orm_error("`${checker.fkey_attr_name}` attribute must be string. Try [${checker.fkey_attr_name}: '${attr.arg}'] instead of [${checker.fkey_attr_name}: ${attr.arg}]",
attr.pos)
return
}
@ -265,7 +290,7 @@ fn (mut c Checker) check_orm_struct_field_attributes(field ast.StructField) {
}
field_struct_type.find_field(attr.arg) or {
c.error('`${field_struct_type.name}` struct has no field with name `${attr.arg}`',
c.orm_error('`${field_struct_type.name}` struct has no field with name `${attr.arg}`',
attr.pos)
return
}
@ -275,7 +300,7 @@ fn (mut c Checker) check_orm_struct_field_attributes(field ast.StructField) {
}
if field_type.kind == .array && !has_fkey_attr {
c.error('A field that holds an array must be defined with the `${checker.fkey_attr_name}` attribute',
c.orm_error('a field that holds an array must be defined with the `${checker.fkey_attr_name}` attribute',
field.pos)
}
}
@ -288,15 +313,61 @@ fn (mut c Checker) fetch_and_verify_orm_fields(info ast.Struct, pos token.Pos, t
&& c.table.sym(c.table.sym(it.typ).array_info().elem_type).kind == .struct_))
&& !it.attrs.contains('skip'))
if fields.len == 0 {
c.error('V orm: select: empty fields in `${table_name}`', pos)
c.orm_error('select: empty fields in `${table_name}`', pos)
return []ast.StructField{}
}
if fields[0].name != 'id' {
c.error('V orm: `id int` must be the first field in `${table_name}`', pos)
c.orm_error('`id int` must be the first field in `${table_name}`', pos)
}
return fields
}
// check_sql_value_expr_is_comptime_with_natural_number_or_expr_with_int_type checks that an expression is compile-time
// and contains an integer greater than or equal to zero or it is a runtime expression with an integer type.
fn (mut c Checker) check_sql_value_expr_is_comptime_with_natural_number_or_expr_with_int_type(mut expr ast.Expr, sql_keyword string) {
comptime_number := c.get_comptime_number_value(mut expr) or {
c.check_sql_expr_type_is_int(expr, sql_keyword)
return
}
if comptime_number < 0 {
c.orm_error('`${sql_keyword}` must be greater than or equal to zero', expr.pos())
}
}
fn (mut c Checker) check_sql_expr_type_is_int(expr &ast.Expr, sql_keyword string) {
if expr is ast.Ident {
if expr.obj.typ.is_int() {
return
}
} else if expr is ast.CallExpr {
if expr.return_type == 0 {
return
}
type_symbol := c.table.sym(expr.return_type)
is_error_type := expr.return_type.has_flag(.result) || expr.return_type.has_flag(.option)
is_acceptable_type := type_symbol.is_int() && !is_error_type
if !is_acceptable_type {
error_type_symbol := c.fn_return_type_flag_to_string(expr.return_type)
c.orm_error('function calls in `${sql_keyword}` must return only an integer type, but `${expr.name}` returns `${error_type_symbol}${type_symbol.name}`',
expr.pos)
}
return
} else if expr is ast.ParExpr {
c.check_sql_expr_type_is_int(&expr.expr, sql_keyword)
return
}
c.orm_error('the type of `${sql_keyword}` must be an integer type', expr.pos())
}
fn (mut c Checker) orm_error(message string, pos token.Pos) {
c.error('${checker.v_orm_prefix}: ${message}', pos)
}
// check_expr_has_no_fn_calls_with_non_orm_return_type checks that an expression has no function calls
// that return complex types which can't be transformed into SQL.
fn (mut c Checker) check_expr_has_no_fn_calls_with_non_orm_return_type(expr &ast.Expr) {
@ -312,23 +383,15 @@ fn (mut c Checker) check_expr_has_no_fn_calls_with_non_orm_return_type(expr &ast
}
type_symbol := c.table.sym(expr.return_type)
is_result_type := expr.return_type.has_flag(.result)
is_option_type := expr.return_type.has_flag(.option)
is_time := type_symbol.cname == 'time__Time'
is_not_pointer := !type_symbol.is_pointer()
is_error_type := is_result_type || is_option_type
is_error_type := expr.return_type.has_flag(.result) || expr.return_type.has_flag(.option)
is_acceptable_type := (type_symbol.is_primitive() || is_time) && is_not_pointer
&& !is_error_type
if !is_acceptable_type {
error_type_symbol := if is_result_type {
'!'
} else if is_option_type {
'?'
} else {
''
}
c.error('V ORM: function calls must return only primitive types and time.Time, but `${expr.name}` returns `${error_type_symbol}${type_symbol.name}`',
error_type_symbol := c.fn_return_type_flag_to_string(expr.return_type)
c.orm_error('function calls must return only primitive types and time.Time, but `${expr.name}` returns `${error_type_symbol}${type_symbol.name}`',
expr.pos)
}
} else if expr is ast.ParExpr {
@ -338,3 +401,16 @@ fn (mut c Checker) check_expr_has_no_fn_calls_with_non_orm_return_type(expr &ast
c.check_expr_has_no_fn_calls_with_non_orm_return_type(&expr.right)
}
}
fn (_ &Checker) fn_return_type_flag_to_string(typ ast.Type) string {
is_result_type := typ.has_flag(.result)
is_option_type := typ.has_flag(.option)
return if is_result_type {
'!'
} else if is_option_type {
'?'
} else {
''
}
}

View File

@ -1,4 +1,4 @@
vlib/v/checker/tests/orm_empty_struct.vv:9:15: error: V orm: select: empty fields in `Person`
vlib/v/checker/tests/orm_empty_struct.vv:9:15: error: V ORM: select: empty fields in `Person`
7 | db := sqlite.connect(':memory:')!
8 | _ := sql db {
9 | select from Person

View File

@ -0,0 +1,7 @@
vlib/v/checker/tests/orm_limit_less_than_zero_error.vv:18:26: error: V ORM: `limit` must be greater than or equal to zero
16 |
17 | users := sql db {
18 | select from User limit user_limit
| ~~~~~~~~~~
19 | }
20 |

View File

@ -0,0 +1,22 @@
module main
import db.sqlite
const (
user_limit = -10
)
struct User {
id int [primary; sql: serial]
username string
}
fn main() {
db := sqlite.connect(':memory:') or { panic(err) }
users := sql db {
select from User limit user_limit
}
println(users)
}

View File

@ -1,4 +1,4 @@
vlib/v/checker/tests/orm_no_default_value.vv:11:4: error: or block needs to return a default value
vlib/v/checker/tests/orm_no_default_value.vv:11:4: error: V ORM: or block needs to return a default value
9 | _ := sql db {
10 | select from Person
11 | } or {

View File

@ -1,4 +1,4 @@
vlib/v/checker/tests/orm_not_a_struct.vv:10:15: error: the table symbol `Person` has to be a struct
vlib/v/checker/tests/orm_not_a_struct.vv:10:15: error: V ORM: the table symbol `Person` has to be a struct
8 | db := sqlite.connect(':memory:')!
9 | _ := sql db {
10 | select from Person

View File

@ -0,0 +1,14 @@
vlib/v/checker/tests/orm_using_non_struct_field_in_order_by_error.vv:14:29: error: V ORM: `User` structure has no field with name `database`.
2 possibilities: `id`, `username`.
12 |
13 | users := sql db {
14 | select from User order by database
| ~~~~~~~~
15 | }
16 |
vlib/v/checker/tests/orm_using_non_struct_field_in_order_by_error.vv:17:2: error: `println` can not print void expressions
15 | }
16 |
17 | println(users)
| ~~~~~~~~~~~~~~
18 | }

View File

@ -0,0 +1,18 @@
module main
import db.sqlite
struct User {
id int [primary; sql: serial]
username string
}
fn main() {
db := sqlite.connect(':memory:') or { panic(err) }
users := sql db {
select from User order by database
}
println(users)
}

View File

@ -307,7 +307,7 @@ fn (mut g Gen) sql_expr_to_orm_primitive(expr ast.Expr) {
}
else {
eprintln(expr)
verror('Unknown expr')
verror('V ORM: ${expr.type_name()} is not supported')
}
}
}