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

all: like operator/keyword for V ORM (#18020)

This commit is contained in:
Mark aka walkingdevel 2023-04-23 00:40:54 +00:00 committed by GitHub
parent 5f870f41b5
commit 3fb32a866c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 171 additions and 5 deletions

View File

@ -131,6 +131,7 @@ const (
'vlib/orm/orm_interface_test.v',
'vlib/orm/orm_mut_db_test.v',
'vlib/orm/orm_result_test.v',
'vlib/orm/orm_custom_operators_test.v',
'vlib/db/sqlite/sqlite_test.v',
'vlib/db/sqlite/sqlite_orm_test.v',
'vlib/db/sqlite/sqlite_vfs_lowlevel_test.v',
@ -208,6 +209,7 @@ const (
'vlib/orm/orm_interface_test.v',
'vlib/orm/orm_mut_db_test.v',
'vlib/orm/orm_result_test.v',
'vlib/orm/orm_custom_operators_test.v',
'vlib/v/tests/orm_sub_struct_test.v',
'vlib/v/tests/orm_sub_array_struct_test.v',
'vlib/v/tests/orm_joined_tables_select_test.v',

View File

@ -59,6 +59,7 @@ pub enum OperationKind {
lt // <
ge // >=
le // <=
orm_like // LIKE
}
pub enum MathOperationKind {
@ -92,6 +93,7 @@ fn (kind OperationKind) to_str() string {
.lt { '<' }
.ge { '>=' }
.le { '<=' }
.orm_like { 'LIKE' }
}
return str
}

View File

@ -0,0 +1,57 @@
import db.sqlite
struct User {
id int [primary; sql: serial]
name string
country string
}
// like is an example function for checking that V allows using the `like` keyword as an identifier.
fn like() {}
fn test_like_operator() {
like()
db := sqlite.connect(':memory:')!
sql db {
create table User
}!
luke := User{
name: 'Luke'
country: 'US'
}
sql db {
insert luke into User
}!
james := User{
name: 'James'
country: 'UK'
}
sql db {
insert james into User
}!
lukas := User{
name: 'Lucas'
country: 'DE'
}
sql db {
insert lukas into User
}!
users_with_name_starting_with_letter_l := sql db {
select from User where name like 'L%'
}!
assert users_with_name_starting_with_letter_l.len == 2
assert users_with_name_starting_with_letter_l.filter(it.name.starts_with('L')).len == 2
users_with_name_with_second_letter_a := sql db {
select from User where name like '_a%'
}!
assert users_with_name_with_second_letter_a.len == 1
assert users_with_name_with_second_letter_a.first().name == james.name
}

View File

@ -457,6 +457,11 @@ fn (mut c Checker) infix_expr(mut node ast.InfixExpr) ast.Type {
opt_comp_pos)
}
}
.key_like {
node.promoted_type = ast.bool_type
return c.check_like_operator(node)
}
.left_shift {
if left_final_sym.kind == .array {
if !node.is_stmt {
@ -757,6 +762,19 @@ fn (mut c Checker) check_div_mod_by_zero(expr ast.Expr, op_kind token.Kind) {
}
}
fn (mut c Checker) check_like_operator(node &ast.InfixExpr) ast.Type {
if node.left !is ast.Ident || !node.left_type.is_string() {
c.error('the left operand of the `like` operator must be an identifier with a string type',
node.left.pos())
}
if !node.right_type.is_string() {
c.error('the right operand of the `like` operator must be a string type', node.right.pos())
}
return node.promoted_type
}
fn (mut c Checker) invalid_operator_error(op token.Kind, left_type ast.Type, right_type ast.Type, pos token.Pos) {
left_name := c.table.type_to_str(left_type)
right_name := c.table.type_to_str(right_type)

View File

@ -0,0 +1,6 @@
vlib/v/checker/tests/like_operator_outside_orm_error.vv:4:15: error: unexpected name `like`, expecting `,`
2 | name := 'Luke'
3 |
4 | println(name like 'L%')
| ~~~~
5 | }

View File

@ -0,0 +1,5 @@
fn main() {
name := 'Luke'
println(name like 'L%')
}

View File

@ -0,0 +1,14 @@
vlib/v/checker/tests/like_operator_with_non_string_type_error.vv:10:36: error: the right operand of the `like` operator must be a string type
8 |
9 | println(sql db {
10 | select from User where name like 10
| ~~
11 | }!)
12 |
vlib/v/checker/tests/like_operator_with_non_string_type_error.vv:14:26: error: the left operand of the `like` operator must be an identifier with a string type
12 |
13 | println(sql db {
14 | select from User where 10 like true
| ~~
15 | }!)
16 | }

View File

@ -0,0 +1,16 @@
struct User {
id int [primary]
name string
}
fn main() {
db := ''
println(sql db {
select from User where name like 10
}!)
println(sql db {
select from User where 10 like true
}!)
}

View File

@ -388,6 +388,9 @@ fn (mut g Gen) sql_where_data(expr ast.Expr, mut fields []string, mut parenthese
.le {
'orm__OperationKind__le'
}
.key_like {
'orm__OperationKind__orm_like'
}
else {
''
}

View File

@ -466,6 +466,9 @@ pub fn (mut p Parser) expr_with_left(left ast.Expr, precedence int, is_stmt_iden
if p.inside_asm && p.prev_tok.pos().line_nr < p.tok.pos().line_nr {
return node
}
p.process_custom_orm_operators()
// Infix
for precedence < p.tok.kind.precedence() {
if p.tok.kind == .dot {
@ -783,3 +786,25 @@ fn (mut p Parser) prefix_inc_dec_error() {
p.error_with_pos('prefix `${op}${expr}` is unsupported, use suffix form `${expr}${op}`',
full_expr_pos)
}
// process_custom_orm_operators checks whether a word in infix expressions is an ORM operator.
// If it is, then a new kind is assigned to it, so that the parser will process it as a keyword.
// This is necessary to ensure that parts of the ORM expression do not function
// outside of the ORM and are not recognized as keywords in the language.
// For example, there is a `like` operator in ORM, which should be used
// in expressions like `name like 'M%'`, but it should not be used in V directly.
[inline]
fn (mut p Parser) process_custom_orm_operators() {
if !p.inside_orm {
return
}
is_like_operator := p.tok.kind == .name && p.tok.lit == 'like'
if is_like_operator {
p.tok = token.Token{
...p.tok
kind: .key_like
}
}
}

View File

@ -7,6 +7,7 @@ import v.ast
fn (mut p Parser) sql_expr() ast.Expr {
tmp_inside_match := p.inside_match
p.inside_orm = true
p.inside_match = true
// `sql db {`
pos := p.tok.pos()
@ -83,6 +84,7 @@ fn (mut p Parser) sql_expr() ast.Expr {
p.check(.rcbr)
p.inside_match = false
p.inside_orm = false
or_expr := p.parse_sql_or_block()
p.inside_match = tmp_inside_match
@ -114,8 +116,10 @@ fn (mut p Parser) sql_expr() ast.Expr {
// update User set nr_oders=nr_orders+1 where id == user_id
fn (mut p Parser) sql_stmt() ast.SqlStmt {
mut pos := p.tok.pos()
p.inside_orm = true
p.inside_match = true
defer {
p.inside_orm = false
p.inside_match = false
}
// `sql db {`

View File

@ -63,6 +63,7 @@ mut:
inside_struct_field_decl bool
inside_struct_attr_decl bool
inside_map_init bool
inside_orm bool
or_is_handled bool // ignore `or` in this expression
builtin_mod bool // are we in the `builtin` module?
mod string // current module name

View File

@ -3,13 +3,15 @@
// that can be found in the LICENSE file.
module token
const orm_custom_operators = ['like']
[minify]
pub struct Token {
pub:
kind Kind // the token number/enum; for quick comparisons
lit string // literal representation of the token
line_nr int // the line number in the source where the token occured
col int // the column in the source where the token occured
line_nr int // the line number in the source where the token occurred
col int // the column in the source where the token occurred
// name_idx int // name table index for O(1) lookup
pos int // the position of the token in scanner text
len int // length of the literal
@ -115,6 +117,7 @@ pub enum Kind {
key_none
key_return
key_select
key_like
key_sizeof
key_isreftype
key_likely
@ -192,12 +195,18 @@ pub const (
pub const scanner_matcher = new_keywords_matcher_trie[Kind](keywords)
// build_keys genereates a map with keywords' string values:
// build_keys generates a map with keywords' string values:
// Keywords['return'] == .key_return
fn build_keys() map[string]Kind {
mut res := map[string]Kind{}
for t in int(Kind.keyword_beg) + 1 .. int(Kind.keyword_end) {
key := token.token_str[t]
// Exclude custom ORM operators from V keyword list
if key in token.orm_custom_operators {
continue
}
res[key] = unsafe { Kind(t) }
}
return res
@ -315,6 +324,7 @@ fn build_token_str() []string {
s[Kind.key_defer] = 'defer'
s[Kind.key_match] = 'match'
s[Kind.key_select] = 'select'
s[Kind.key_like] = 'like'
s[Kind.key_none] = 'none'
s[Kind.key_nil] = 'nil'
s[Kind.key_offsetof] = '__offsetof'
@ -426,13 +436,14 @@ pub fn build_precedences() []Precedence {
p[Kind.minus] = .sum
p[Kind.pipe] = .sum
p[Kind.xor] = .sum
// `==` | `!=` | `<` | `<=` | `>` | `>=`
// `==` | `!=` | `<` | `<=` | `>` | `>=` | `like`
p[Kind.eq] = .eq
p[Kind.ne] = .eq
p[Kind.lt] = .eq
p[Kind.le] = .eq
p[Kind.gt] = .eq
p[Kind.ge] = .eq
p[Kind.key_like] = .eq
// `=` | `+=` | ...
p[Kind.assign] = .assign
p[Kind.plus_assign] = .assign
@ -504,7 +515,7 @@ pub fn (kind Kind) is_prefix() bool {
pub fn (kind Kind) is_infix() bool {
return kind in [.plus, .minus, .mod, .mul, .div, .eq, .ne, .gt, .lt, .key_in, .key_as, .ge,
.le, .logical_or, .xor, .not_in, .key_is, .not_is, .and, .dot, .pipe, .amp, .left_shift,
.right_shift, .unsigned_right_shift, .arrow]
.right_shift, .unsigned_right_shift, .arrow, .key_like]
}
[inline]
@ -611,6 +622,7 @@ pub fn kind_to_string(k Kind) string {
.key_none { 'key_none' }
.key_return { 'key_return' }
.key_select { 'key_select' }
.key_like { 'key_like' }
.key_sizeof { 'key_sizeof' }
.key_isreftype { 'key_isreftype' }
.key_likely { 'key_likely' }
@ -733,6 +745,7 @@ pub fn kind_from_string(s string) !Kind {
'key_none' { .key_none }
'key_return' { .key_return }
'key_select' { .key_select }
'key_like' { .key_like }
'key_sizeof' { .key_sizeof }
'key_isreftype' { .key_isreftype }
'key_likely' { .key_likely }