Skip to content

Commit 489a007

Browse files
Andrew Hardingzeebo
Andrew Harding
authored andcommitted
richer support for where expressions
1 parent df39b74 commit 489a007

37 files changed

+819
-293
lines changed

README.md

+26-5
Original file line numberDiff line numberDiff line change
@@ -452,11 +452,32 @@ read <views> (
452452
// "project.id"
453453
select <field refs>
454454
455-
// a read can have any number of where clauses. the clause refers to a
456-
// field, an operation like "!=" or "<=", and another field. if the right
457-
// side field is a question mark, the read will fill it in with an argument
458-
// and be generated with a parameter for that argument.
459-
where <model.field> <op> <model.field or "?">
455+
// a read can have any number of where clauses. the clause refers to an
456+
// expression, an operation like "!=" or "<=", and another expression. if
457+
// the right side field has a placeholder (?), the read will fill it in
458+
// with an argument and be generated with a parameter for that argument.
459+
//
460+
// <expr> can be one of the following:
461+
// 1) placeholder
462+
// where animal.name = ?
463+
// 2) null
464+
// where animal.name = null
465+
// 3) string literal
466+
// where animal.name = "Tiger"
467+
// 4) number literal
468+
// where animal.age < 30
469+
// 5) model field reference: <model>.<field>
470+
// where animal.height = animal.width
471+
// 6) SQL function call: <name>(<expr>)
472+
// where lower(animal.name) = "tiger"
473+
//
474+
// SQL function calls take an expression for each argument. Currently only
475+
// "lower" is implemented.
476+
//
477+
// <limited-expr> is the same as <expr> except that it can only contain
478+
// a model field reference, optionally wrapped in one or more function
479+
// calls.
480+
where <limited-expr> <op> <expr>
460481
461482
// a join describes a join for the read. it brings the right hand side
462483
// model into scope for the selects, and the joins must be in a consistent

ast/types.go

+63-6
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ package ast
1616

1717
import (
1818
"fmt"
19+
"strings"
1920
"text/scanner"
2021

2122
"gopkg.in/spacemonkeygo/dbx.v1/consts"
@@ -241,17 +242,73 @@ func (j *JoinType) Get() consts.JoinType {
241242

242243
type Where struct {
243244
Pos scanner.Position
244-
Left *FieldRef
245+
Left *Expr
245246
Op *Operator
246-
Right *FieldRef
247+
Right *Expr
247248
}
248249

249250
func (w *Where) String() string {
250-
right := "?"
251-
if w.Right != nil {
252-
right = w.Right.String()
251+
return fmt.Sprintf("%s %s %s", w.Left, w.Op, w.Right)
252+
}
253+
254+
type Expr struct {
255+
Pos scanner.Position
256+
// The following fields are mutually exclusive
257+
Null *Null
258+
StringLit *String
259+
NumberLit *String
260+
Placeholder *Placeholder
261+
FieldRef *FieldRef
262+
FuncCall *FuncCall
263+
}
264+
265+
func (e *Expr) String() string {
266+
switch {
267+
case e.Null != nil:
268+
return e.Null.String()
269+
case e.StringLit != nil:
270+
return fmt.Sprintf("%q", e.StringLit.Value)
271+
case e.NumberLit != nil:
272+
return e.NumberLit.Value
273+
case e.Placeholder != nil:
274+
return e.Placeholder.String()
275+
case e.FieldRef != nil:
276+
return e.FieldRef.String()
277+
case e.FuncCall != nil:
278+
return e.FuncCall.String()
279+
default:
280+
return ""
281+
}
282+
}
283+
284+
type Null struct {
285+
Pos scanner.Position
286+
}
287+
288+
func (p *Null) String() string {
289+
return "null"
290+
}
291+
292+
type Placeholder struct {
293+
Pos scanner.Position
294+
}
295+
296+
func (p *Placeholder) String() string {
297+
return "?"
298+
}
299+
300+
type FuncCall struct {
301+
Pos scanner.Position
302+
Name *String
303+
Args []*Expr
304+
}
305+
306+
func (f *FuncCall) String() string {
307+
var args []string
308+
for _, arg := range f.Args {
309+
args = append(args, arg.String())
253310
}
254-
return fmt.Sprintf("%s %s %s", w.Left, w.Op, right)
311+
return fmt.Sprintf("%s(%s)", f.Name.Value, strings.Join(args, ", "))
255312
}
256313

257314
type Operator struct {

code/golang/delete.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ type Delete struct {
2828
}
2929

3030
func DeleteFromIR(ir_del *ir.Delete, dialect sql.Dialect) *Delete {
31-
delete_sql := sql.DeleteSQL(ir_del)
31+
delete_sql := sql.DeleteSQL(ir_del, dialect)
3232
del := &Delete{
3333
PartitionedArgs: PartitionedArgsFromWheres(ir_del.Where),
3434
Info: sqlembedgo.Embed("__", delete_sql),

code/golang/get.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ type Get struct {
3232
}
3333

3434
func GetFromIR(ir_read *ir.Read, dialect sql.Dialect) *Get {
35-
select_sql := sql.SelectSQL(ir_read)
35+
select_sql := sql.SelectSQL(ir_read, dialect)
3636
get := &Get{
3737
PartitionedArgs: PartitionedArgsFromWheres(ir_read.Where),
3838
Info: sqlembedgo.Embed("__", select_sql),

code/golang/renderer.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -440,7 +440,7 @@ func (r *Renderer) renderDeleteWorld(w io.Writer, ir_models []*ir.Model,
440440
for i := len(ir_models) - 1; i >= 0; i-- {
441441
sql := sqlgen.Render(dialect, sql.DeleteSQL(&ir.Delete{
442442
Model: ir_models[i],
443-
}))
443+
}, dialect))
444444
del.SQLs = append(del.SQLs, sql)
445445
}
446446

code/golang/update.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ func UpdateFromIR(ir_upd *ir.Update, dialect sql.Dialect) *Update {
5555
Joins: ir_upd.Joins,
5656
Where: ir_upd.Where,
5757
View: ir.All,
58-
})
58+
}, dialect)
5959
upd.InfoGet = sqlembedgo.Embed("__", select_sql)
6060
}
6161

code/golang/var.go

+14-4
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import (
1818
"fmt"
1919

2020
"bitbucket.org/pkg/inflect"
21+
"gopkg.in/spacemonkeygo/dbx.v1/consts"
2122
"gopkg.in/spacemonkeygo/dbx.v1/ir"
2223
)
2324

@@ -74,16 +75,25 @@ func ArgFromField(field *ir.Field) *Var {
7475
}
7576

7677
func ArgFromWhere(where *ir.Where) *Var {
77-
name := where.Left.UnderRef()
78-
if suffix := where.Op.Suffix(); suffix != "" {
79-
name += "_" + suffix
78+
// TODO: clean this up when we do full expression type evaluation.
79+
// assume for now that the left hand side evaluates eventually to a single
80+
// field wrapped in zero or more function calls since that is all that is
81+
// possible via the xform package.
82+
expr := where.Left
83+
for expr.Field == nil {
84+
expr = expr.FuncCall.Args[0]
85+
}
86+
87+
name := expr.Field.UnderRef()
88+
if where.Op != consts.EQ {
89+
name += "_" + where.Op.Suffix()
8090
}
8191

8292
// we don't set ZeroVal or InitVal because these args should only be used
8393
// as incoming arguments to function calls.
8494
return &Var{
8595
Name: name,
86-
Type: ModelFieldFromIR(where.Left).StructName(),
96+
Type: ModelFieldFromIR(expr.Field).StructName(),
8797
}
8898
}
8999

code/golang/where.go

+2-2
Original file line numberDiff line numberDiff line change
@@ -24,14 +24,14 @@ type PartitionedArgs struct {
2424

2525
func PartitionedArgsFromWheres(wheres []*ir.Where) (out PartitionedArgs) {
2626
for _, where := range wheres {
27-
if where.Right != nil {
27+
if !where.Right.HasPlaceholder() {
2828
continue
2929
}
3030

3131
arg := ArgFromWhere(where)
3232
out.AllArgs = append(out.AllArgs, arg)
3333

34-
if where.Nullable() {
34+
if where.NeedsCondition() {
3535
out.NullableArgs = append(out.NullableArgs, arg)
3636
} else {
3737
out.StaticArgs = append(out.StaticArgs, arg)

compile_test.go

+6-1
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import (
1111
"io/ioutil"
1212
"os"
1313
"path/filepath"
14+
"runtime/debug"
1415
"testing"
1516

1617
"gopkg.in/spacemonkeygo/dbx.v1/testutil"
@@ -37,7 +38,7 @@ func TestCompilation(t *testing.T) {
3738
func testFile(t *testutil.T, file string) {
3839
defer func() {
3940
if val := recover(); val != nil {
40-
t.Fatal(val)
41+
t.Fatalf("%s\n%s", val, string(debug.Stack()))
4142
}
4243
}()
4344

@@ -57,18 +58,22 @@ func testFile(t *testutil.T, file string) {
5758
}
5859

5960
runBuild := func(rx, userdata bool) {
61+
t.Logf("[%s] generating... (rx=%t, userdata=%t)", file, rx, userdata)
6062
err = golangCmd("", dialects, "", rx, userdata, file, dir)
6163
t.AssertNoError(err)
6264

65+
t.Logf("[%s] loading...", file)
6366
go_file := filepath.Join(dir, filepath.Base(file)+".go")
6467
go_source, err := ioutil.ReadFile(go_file)
6568
t.AssertNoError(err)
6669
t.Context("go", linedSource(go_source))
6770

71+
t.Logf("[%s] parsing...", file)
6872
fset := token.NewFileSet()
6973
f, err := parser.ParseFile(fset, go_file, go_source, parser.AllErrors)
7074
t.AssertNoError(err)
7175

76+
t.Logf("[%s] compiling...", file)
7277
config := types.Config{
7378
Importer: importer.Default(),
7479
}

consts/consts.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ func (o Operator) Suffix() string {
4545
case GE:
4646
return "greater_or_equal"
4747
case EQ:
48-
return ""
48+
return "equal"
4949
case NE:
5050
return "not"
5151
case Like:

ir/expr.go

+74
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
// Copyright (C) 2017 Space Monkey, Inc.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package ir
16+
17+
type Expr struct {
18+
Null bool
19+
Placeholder bool
20+
StringLit *string
21+
NumberLit *string
22+
Field *Field
23+
FuncCall *FuncCall
24+
}
25+
26+
func (e *Expr) Nullable() bool {
27+
switch {
28+
case e.Null, e.Placeholder:
29+
return true
30+
case e.Field != nil:
31+
return e.Field.Nullable
32+
case e.FuncCall != nil:
33+
return e.FuncCall.Nullable()
34+
default:
35+
return false
36+
}
37+
}
38+
39+
func (e *Expr) HasPlaceholder() bool {
40+
if e.Placeholder {
41+
return true
42+
}
43+
if e.FuncCall != nil {
44+
return e.FuncCall.HasPlaceholder()
45+
}
46+
return false
47+
}
48+
49+
func (e *Expr) Unique() bool {
50+
return e.Field != nil && e.Field.Unique()
51+
}
52+
53+
type FuncCall struct {
54+
Name string
55+
Args []*Expr
56+
}
57+
58+
func (fc *FuncCall) Nullable() bool {
59+
for _, arg := range fc.Args {
60+
if arg.Nullable() {
61+
return true
62+
}
63+
}
64+
return false
65+
}
66+
67+
func (fc *FuncCall) HasPlaceholder() bool {
68+
for _, arg := range fc.Args {
69+
if arg.HasPlaceholder() {
70+
return true
71+
}
72+
}
73+
return false
74+
}

ir/helpers.go

+13-4
Original file line numberDiff line numberDiff line change
@@ -59,10 +59,19 @@ func fieldSetPrune(all, bad []*Field) (out []*Field) {
5959

6060
func whereUnique(wheres []*Where) (unique map[string]bool) {
6161
fields := map[*Model][]*Field{}
62-
for _, eq := range FilterWhere(wheres, consts.EQ) {
63-
fields[eq.Left.Model] = append(fields[eq.Left.Model], eq.Left)
64-
if eq.Right != nil {
65-
fields[eq.Right.Model] = append(fields[eq.Right.Model], eq.Right)
62+
for _, where := range wheres {
63+
if where.Op != consts.EQ {
64+
continue
65+
}
66+
67+
left := where.Left.Field
68+
right := where.Right.Field
69+
70+
if left != nil {
71+
fields[left.Model] = append(fields[left.Model], left)
72+
}
73+
if right != nil {
74+
fields[right.Model] = append(fields[right.Model], left)
6675
}
6776
}
6877

0 commit comments

Comments
 (0)