├── .gitignore ├── LICENSE ├── README.md ├── ast └── types.go ├── build_test.go ├── code ├── common.go └── golang │ ├── common.go │ ├── create.go │ ├── delete.go │ ├── get.go │ ├── helpers.go │ ├── model.go │ ├── renderer.go │ ├── struct.go │ ├── update.go │ ├── var.go │ └── where.go ├── consts └── consts.go ├── dialects.go ├── errutil └── common.go ├── go.mod ├── go.sum ├── helper_test.go ├── internal └── prettyprint │ └── pretty.go ├── ir ├── create.go ├── delete.go ├── expr.go ├── field.go ├── helpers.go ├── index.go ├── model.go ├── read.go ├── root.go ├── update.go ├── where.go └── xform │ ├── common.go │ ├── defaults.go │ ├── lookup.go │ ├── suffix.go │ ├── transform.go │ ├── transform_create.go │ ├── transform_delete.go │ ├── transform_expr.go │ ├── transform_field.go │ ├── transform_joins.go │ ├── transform_model.go │ ├── transform_models.go │ ├── transform_read.go │ ├── transform_update.go │ └── transform_where.go ├── main.go ├── run_test.go ├── sql ├── common.go ├── delete.go ├── expr.go ├── group_by.go ├── insert.go ├── join.go ├── order_by.go ├── postgres.go ├── schema.go ├── select.go ├── sqlite3.go ├── update.go └── where.go ├── sqlgen ├── common.go ├── common_test.go ├── dialects.go ├── dialects_test.go ├── sqlbundle │ ├── bundle.go │ └── gen_bundle.go ├── sqlcompile │ ├── compile.go │ ├── compile_test.go │ ├── equal.go │ └── equal_test.go ├── sqlembedgo │ ├── embed.go │ └── embed_test.go ├── sqlhelpers │ └── common.go ├── sqltest │ └── gen.go ├── types.go └── types_test.go ├── syntax ├── common.go ├── errors.go ├── format.go ├── fuzz.go ├── node.go ├── parse.go ├── parse_create.go ├── parse_delete.go ├── parse_expr.go ├── parse_field.go ├── parse_field_ref.go ├── parse_groupby.go ├── parse_index.go ├── parse_join.go ├── parse_model.go ├── parse_orderby.go ├── parse_read.go ├── parse_relation.go ├── parse_suffix.go ├── parse_test.go ├── parse_update.go ├── parse_view.go ├── parse_where.go └── scanner.go ├── templates ├── bindata.go ├── generate.go ├── golang.create-raw.tmpl ├── golang.create.tmpl ├── golang.decl.tmpl ├── golang.delete-all.tmpl ├── golang.delete-world.tmpl ├── golang.delete.tmpl ├── golang.dialect-postgres.tmpl ├── golang.dialect-sqlite3.tmpl ├── golang.footer.tmpl ├── golang.get-all.tmpl ├── golang.get-count.tmpl ├── golang.get-first.tmpl ├── golang.get-has.tmpl ├── golang.get-last.tmpl ├── golang.get-limitoffset.tmpl ├── golang.get-one-all.tmpl ├── golang.get-one.tmpl ├── golang.get-paged.tmpl ├── golang.get-scalar-all.tmpl ├── golang.get-scalar.tmpl ├── golang.header.tmpl ├── golang.misc.tmpl └── golang.update.tmpl ├── testdata ├── build │ ├── bad_key.dbx │ ├── basic.dbx │ ├── cyclic_foreign_key.dbx │ ├── exhaustive.dbx │ ├── fails_no_updatable.dbx │ ├── float64.dbx │ ├── joins.dbx │ ├── joins_bad.dbx │ ├── multicolumn_select.dbx │ ├── projection_name_clash.dbx │ ├── read_views.dbx │ ├── same_field_multiple_where_clause.dbx │ ├── single_column_select_paged.dbx │ ├── suffix.dbx │ └── unique_checking.dbx └── run │ ├── joins.dbx │ ├── joins.go │ ├── unique_checking.dbx │ └── unique_checking.go ├── testutil └── common.go └── tmplutil ├── common.go ├── loader.go └── utils.go /.gitignore: -------------------------------------------------------------------------------- 1 | ./bin/dbx 2 | .DS_Store 3 | vendor 4 | -------------------------------------------------------------------------------- /build_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2017 Space Monkey, Inc. 2 | 3 | package main 4 | 5 | import ( 6 | "go/parser" 7 | "go/token" 8 | "io/ioutil" 9 | "os" 10 | "path/filepath" 11 | "runtime/debug" 12 | "testing" 13 | 14 | "golang.org/x/tools/go/packages" 15 | "gopkg.in/spacemonkeygo/dbx.v1/testutil" 16 | ) 17 | 18 | func TestBuild(t *testing.T) { 19 | tw := testutil.Wrap(t) 20 | tw.Parallel() 21 | 22 | data_dir := filepath.Join("testdata", "build") 23 | 24 | names, err := filepath.Glob(filepath.Join(data_dir, "*.dbx")) 25 | tw.AssertNoError(err) 26 | 27 | for _, name := range names { 28 | name := name 29 | tw.Runp(filepath.Base(name), func(tw *testutil.T) { 30 | testBuildFile(tw, name) 31 | }) 32 | } 33 | } 34 | 35 | func testBuildFile(t *testutil.T, file string) { 36 | defer func() { 37 | if val := recover(); val != nil { 38 | t.Fatalf("%s\n%s", val, string(debug.Stack())) 39 | } 40 | }() 41 | 42 | dir, err := ioutil.TempDir("", "dbx") 43 | t.AssertNoError(err) 44 | defer os.RemoveAll(dir) 45 | 46 | dbx_source, err := ioutil.ReadFile(file) 47 | t.AssertNoError(err) 48 | t.Context("dbx", linedSource(dbx_source)) 49 | d := loadDirectives(t, dbx_source) 50 | 51 | dialects := []string{"postgres", "sqlite3"} 52 | if other := d.lookup("dialects"); other != nil { 53 | dialects = other 54 | t.Logf("using dialects: %q", dialects) 55 | } 56 | 57 | type options struct { 58 | rx bool 59 | userdata bool 60 | } 61 | 62 | runBuild := func(opts options) { 63 | t.Logf("[%s] generating... %+v", file, opts) 64 | err = golangCmd("", dialects, "", opts.rx, opts.userdata, file, dir) 65 | if d.has("fail_gen") { 66 | t.AssertError(err, d.get("fail_gen")) 67 | return 68 | } else { 69 | t.AssertNoError(err) 70 | } 71 | 72 | t.Logf("[%s] loading...", file) 73 | go_file := filepath.Join(dir, filepath.Base(file)+".go") 74 | go_source, err := ioutil.ReadFile(go_file) 75 | t.AssertNoError(err) 76 | t.Context("go", linedSource(go_source)) 77 | 78 | t.Logf("[%s] parsing...", file) 79 | fset := token.NewFileSet() 80 | _, err = parser.ParseFile(fset, go_file, go_source, parser.AllErrors) 81 | t.AssertNoError(err) 82 | 83 | t.Logf("[%s] compiling...", file) 84 | _, err = packages.Load(nil, go_file) 85 | 86 | if d.has("fail") { 87 | t.AssertError(err, d.get("fail")) 88 | } else { 89 | t.AssertNoError(err) 90 | } 91 | } 92 | 93 | runBuild(options{rx: false, userdata: false}) 94 | runBuild(options{rx: false, userdata: true}) 95 | runBuild(options{rx: true, userdata: false}) 96 | runBuild(options{rx: true, userdata: true}) 97 | } 98 | -------------------------------------------------------------------------------- /code/common.go: -------------------------------------------------------------------------------- 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 code 16 | 17 | import ( 18 | "gopkg.in/spacemonkeygo/dbx.v1/ir" 19 | "gopkg.in/spacemonkeygo/dbx.v1/sql" 20 | ) 21 | 22 | type Renderer interface { 23 | RenderCode(root *ir.Root, dialects []sql.Dialect) ([]byte, error) 24 | } 25 | -------------------------------------------------------------------------------- /code/golang/common.go: -------------------------------------------------------------------------------- 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 golang 16 | 17 | import "github.com/spacemonkeygo/errors" 18 | 19 | var ( 20 | Error = errors.NewClass("golang") 21 | ) 22 | -------------------------------------------------------------------------------- /code/golang/create.go: -------------------------------------------------------------------------------- 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 golang 16 | 17 | import ( 18 | "fmt" 19 | 20 | "gopkg.in/spacemonkeygo/dbx.v1/ir" 21 | "gopkg.in/spacemonkeygo/dbx.v1/sql" 22 | "gopkg.in/spacemonkeygo/dbx.v1/sqlgen/sqlembedgo" 23 | ) 24 | 25 | type RawCreate struct { 26 | Info sqlembedgo.Info 27 | Suffix string 28 | Return *Var 29 | Arg *Var 30 | Fields []*Var 31 | SupportsReturning bool 32 | } 33 | 34 | func RawCreateFromIR(ir_cre *ir.Create, dialect sql.Dialect) *RawCreate { 35 | insert_sql := sql.InsertSQL(ir_cre, dialect) 36 | ins := &RawCreate{ 37 | Info: sqlembedgo.Embed("__", insert_sql), 38 | Suffix: convertSuffix(ir_cre.Suffix), 39 | SupportsReturning: dialect.Features().Returning, 40 | } 41 | if !ir_cre.NoReturn { 42 | ins.Return = VarFromModel(ir_cre.Model) 43 | } 44 | 45 | // the model struct is the only arg. 46 | ins.Arg = VarFromModel(ir_cre.Model) 47 | ins.Arg.Name = "raw_" + ins.Arg.Name 48 | 49 | // each field in the model is initialized from the raw model struct. 50 | for _, field := range ir_cre.Fields() { 51 | f := ModelFieldFromIR(field) 52 | v := VarFromField(field) 53 | if field.Nullable { 54 | v.InitVal = fmt.Sprintf("%s_%s_Raw(%s.%s).value()", 55 | ins.Arg.Type, f.Name, ins.Arg.Name, f.Name) 56 | } else { 57 | v.InitVal = fmt.Sprintf("%s_%s(%s.%s).value()", 58 | ins.Arg.Type, f.Name, ins.Arg.Name, f.Name) 59 | } 60 | v.Name = fmt.Sprintf("__%s_val", v.Name) 61 | ins.Fields = append(ins.Fields, v) 62 | } 63 | 64 | return ins 65 | } 66 | 67 | type Create struct { 68 | Info sqlembedgo.Info 69 | Suffix string 70 | Return *Var 71 | Args []*Var 72 | Fields []*Var 73 | SupportsReturning bool 74 | NeedsNow bool 75 | } 76 | 77 | func CreateFromIR(ir_cre *ir.Create, dialect sql.Dialect) *Create { 78 | insert_sql := sql.InsertSQL(ir_cre, dialect) 79 | ins := &Create{ 80 | Info: sqlembedgo.Embed("__", insert_sql), 81 | Suffix: convertSuffix(ir_cre.Suffix), 82 | SupportsReturning: dialect.Features().Returning, 83 | } 84 | if !ir_cre.NoReturn { 85 | ins.Return = VarFromModel(ir_cre.Model) 86 | } 87 | 88 | args := map[string]*Var{} 89 | 90 | // All of the manual fields are arguments to the function. The Field struct 91 | // type is used (pointer if nullable). 92 | has_nullable := false 93 | for _, field := range ir_cre.InsertableFields() { 94 | arg := ArgFromField(field) 95 | args[field.Name] = arg 96 | if !field.Nullable { 97 | ins.Args = append(ins.Args, arg) 98 | } else { 99 | has_nullable = true 100 | } 101 | } 102 | 103 | if has_nullable { 104 | ins.Args = append(ins.Args, &Var{ 105 | Name: "optional", 106 | Type: ModelStructFromIR(ir_cre.Model).CreateStructName(), 107 | }) 108 | } 109 | 110 | // Now for each field 111 | for _, field := range ir_cre.Fields() { 112 | if field == ir_cre.Model.BasicPrimaryKey() { 113 | continue 114 | } 115 | v := VarFromField(field) 116 | v.Name = fmt.Sprintf("__%s_val", v.Name) 117 | if arg := args[field.Name]; arg != nil { 118 | if field.Nullable { 119 | f := ModelFieldFromIR(field) 120 | v.InitVal = fmt.Sprintf("optional.%s.value()", f.Name) 121 | } else { 122 | v.InitVal = fmt.Sprintf("%s.value()", arg.Name) 123 | } 124 | } else if field.IsTime() { 125 | ins.NeedsNow = true 126 | } 127 | ins.Fields = append(ins.Fields, v) 128 | } 129 | 130 | return ins 131 | } 132 | -------------------------------------------------------------------------------- /code/golang/delete.go: -------------------------------------------------------------------------------- 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 golang 16 | 17 | import ( 18 | "gopkg.in/spacemonkeygo/dbx.v1/ir" 19 | "gopkg.in/spacemonkeygo/dbx.v1/sql" 20 | "gopkg.in/spacemonkeygo/dbx.v1/sqlgen/sqlembedgo" 21 | ) 22 | 23 | type Delete struct { 24 | PartitionedArgs 25 | Info sqlembedgo.Info 26 | Suffix string 27 | Result *Var 28 | } 29 | 30 | func DeleteFromIR(ir_del *ir.Delete, dialect sql.Dialect) *Delete { 31 | delete_sql := sql.DeleteSQL(ir_del, dialect) 32 | del := &Delete{ 33 | PartitionedArgs: PartitionedArgsFromWheres(ir_del.Where), 34 | Info: sqlembedgo.Embed("__", delete_sql), 35 | Suffix: convertSuffix(ir_del.Suffix), 36 | } 37 | 38 | if ir_del.Distinct() { 39 | del.Result = &Var{ 40 | Name: "deleted", 41 | Type: "bool", 42 | } 43 | } else { 44 | del.Result = &Var{ 45 | Name: "count", 46 | Type: "int64", 47 | } 48 | } 49 | 50 | return del 51 | } 52 | -------------------------------------------------------------------------------- /code/golang/get.go: -------------------------------------------------------------------------------- 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 golang 16 | 17 | import ( 18 | "strings" 19 | 20 | "gopkg.in/spacemonkeygo/dbx.v1/ir" 21 | "gopkg.in/spacemonkeygo/dbx.v1/sql" 22 | "gopkg.in/spacemonkeygo/dbx.v1/sqlgen/sqlembedgo" 23 | ) 24 | 25 | type Get struct { 26 | PartitionedArgs 27 | Info sqlembedgo.Info 28 | Suffix string 29 | Row *Var 30 | LastPk *Var 31 | } 32 | 33 | func GetFromIR(ir_read *ir.Read, dialect sql.Dialect) *Get { 34 | select_sql := sql.SelectSQL(ir_read, dialect) 35 | get := &Get{ 36 | PartitionedArgs: PartitionedArgsFromWheres(ir_read.Where), 37 | Info: sqlembedgo.Embed("__", select_sql), 38 | Suffix: convertSuffix(ir_read.Suffix), 39 | } 40 | 41 | get.Row = GetRowFromIR(ir_read) 42 | 43 | if ir_read.View == ir.Paged { 44 | pk_var := VarFromField(ir_read.From.BasicPrimaryKey()) 45 | pk_var.Name = "__" + pk_var.Name 46 | get.LastPk = pk_var 47 | } 48 | 49 | return get 50 | } 51 | 52 | func GetRowFromIR(ir_read *ir.Read) *Var { 53 | if model := ir_read.SelectedModel(); model != nil { 54 | return VarFromModel(model) 55 | } 56 | 57 | return MakeResultVar(ir_read.Selectables) 58 | } 59 | 60 | func MakeResultVar(selectables []ir.Selectable) *Var { 61 | vars := VarsFromSelectables(selectables) 62 | 63 | // construct the aggregate struct name 64 | var parts []string 65 | for _, v := range vars { 66 | parts = append(parts, v.Name) 67 | } 68 | parts = append(parts, "Row") 69 | name := strings.Join(parts, "_") 70 | return StructVar("row", name, vars) 71 | } 72 | 73 | func ResultStructFromRead(ir_read *ir.Read) *Struct { 74 | // no result struct if there is just a single model selected 75 | if ir_read.SelectedModel() != nil { 76 | return nil 77 | } 78 | 79 | result := MakeResultVar(ir_read.Selectables) 80 | 81 | s := &Struct{ 82 | Name: result.Type, 83 | } 84 | 85 | for _, field := range result.Fields { 86 | s.Fields = append(s.Fields, Field{ 87 | Name: field.Name, 88 | Type: field.Type, 89 | }) 90 | } 91 | 92 | return s 93 | } 94 | -------------------------------------------------------------------------------- /code/golang/struct.go: -------------------------------------------------------------------------------- 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 golang 16 | 17 | type Struct struct { 18 | Name string 19 | Fields []Field 20 | } 21 | 22 | type Field struct { 23 | Name string 24 | Type string 25 | Tags []Tag 26 | } 27 | 28 | type Tag struct { 29 | Key string 30 | Value string 31 | } 32 | -------------------------------------------------------------------------------- /code/golang/update.go: -------------------------------------------------------------------------------- 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 golang 16 | 17 | import ( 18 | "gopkg.in/spacemonkeygo/dbx.v1/ir" 19 | "gopkg.in/spacemonkeygo/dbx.v1/sql" 20 | "gopkg.in/spacemonkeygo/dbx.v1/sqlgen/sqlembedgo" 21 | ) 22 | 23 | type Update struct { 24 | PartitionedArgs 25 | Info sqlembedgo.Info 26 | InfoGet sqlembedgo.Info 27 | Suffix string 28 | Struct *ModelStruct 29 | Return *Var 30 | AutoFields []*Var 31 | SupportsReturning bool 32 | NeedsNow bool 33 | } 34 | 35 | func UpdateFromIR(ir_upd *ir.Update, dialect sql.Dialect) *Update { 36 | update_sql := sql.UpdateSQL(ir_upd, dialect) 37 | upd := &Update{ 38 | PartitionedArgs: PartitionedArgsFromWheres(ir_upd.Where), 39 | Info: sqlembedgo.Embed("__", update_sql), 40 | Suffix: convertSuffix(ir_upd.Suffix), 41 | Struct: ModelStructFromIR(ir_upd.Model), 42 | SupportsReturning: dialect.Features().Returning, 43 | } 44 | if !ir_upd.NoReturn { 45 | upd.Return = VarFromModel(ir_upd.Model) 46 | } 47 | 48 | for _, field := range ir_upd.AutoUpdatableFields() { 49 | upd.NeedsNow = upd.NeedsNow || field.IsTime() 50 | upd.AutoFields = append(upd.AutoFields, VarFromField(field)) 51 | } 52 | 53 | if !upd.SupportsReturning { 54 | select_sql := sql.SelectSQL(&ir.Read{ 55 | From: ir_upd.Model, 56 | Selectables: []ir.Selectable{ir_upd.Model}, 57 | Joins: ir_upd.Joins, 58 | Where: ir_upd.Where, 59 | View: ir.All, 60 | }, dialect) 61 | upd.InfoGet = sqlembedgo.Embed("__", select_sql) 62 | } 63 | 64 | return upd 65 | } 66 | -------------------------------------------------------------------------------- /code/golang/where.go: -------------------------------------------------------------------------------- 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 golang 16 | 17 | import "gopkg.in/spacemonkeygo/dbx.v1/ir" 18 | 19 | type PartitionedArgs struct { 20 | AllArgs []*Var 21 | StaticArgs []*Var 22 | NullableArgs []*Var 23 | } 24 | 25 | func PartitionedArgsFromWheres(wheres []*ir.Where) (out PartitionedArgs) { 26 | for _, where := range wheres { 27 | if !where.Right.HasPlaceholder() { 28 | continue 29 | } 30 | 31 | arg := ArgFromWhere(where) 32 | out.AllArgs = append(out.AllArgs, arg) 33 | 34 | if where.NeedsCondition() { 35 | out.NullableArgs = append(out.NullableArgs, arg) 36 | } else { 37 | out.StaticArgs = append(out.StaticArgs, arg) 38 | } 39 | } 40 | return out 41 | } 42 | -------------------------------------------------------------------------------- /consts/consts.go: -------------------------------------------------------------------------------- 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 consts 16 | 17 | import "fmt" 18 | 19 | type JoinType int 20 | 21 | const ( 22 | InnerJoin JoinType = iota 23 | ) 24 | 25 | type Operator string 26 | 27 | const ( 28 | LT Operator = "<" 29 | LE Operator = "<=" 30 | GT Operator = ">" 31 | GE Operator = ">=" 32 | EQ Operator = "=" 33 | NE Operator = "!=" 34 | Like Operator = "like" 35 | ) 36 | 37 | func (o Operator) Suffix() string { 38 | switch o { 39 | case LT: 40 | return "less" 41 | case LE: 42 | return "less_or_equal" 43 | case GT: 44 | return "greater" 45 | case GE: 46 | return "greater_or_equal" 47 | case EQ: 48 | return "equal" 49 | case NE: 50 | return "not" 51 | case Like: 52 | return "like" 53 | default: 54 | panic(fmt.Sprintf("unhandled operation %q", o)) 55 | } 56 | } 57 | 58 | type FieldType int 59 | 60 | const ( 61 | SerialField FieldType = iota 62 | Serial64Field 63 | IntField 64 | Int64Field 65 | UintField 66 | Uint64Field 67 | FloatField 68 | Float64Field 69 | TextField 70 | BoolField 71 | TimestampField 72 | TimestampUTCField 73 | BlobField 74 | DateField 75 | ) 76 | 77 | func (f FieldType) String() string { 78 | switch f { 79 | case SerialField: 80 | return "serial" 81 | case Serial64Field: 82 | return "serial64" 83 | case IntField: 84 | return "int" 85 | case Int64Field: 86 | return "int64" 87 | case UintField: 88 | return "uint" 89 | case Uint64Field: 90 | return "uint64" 91 | case FloatField: 92 | return "float" 93 | case Float64Field: 94 | return "float64" 95 | case TextField: 96 | return "text" 97 | case BoolField: 98 | return "bool" 99 | case TimestampField: 100 | return "timestamp" 101 | case TimestampUTCField: 102 | return "utimestamp" 103 | case BlobField: 104 | return "blob" 105 | default: 106 | return "" 107 | } 108 | } 109 | 110 | func (f FieldType) AsLink() FieldType { 111 | switch f { 112 | case SerialField: 113 | return IntField 114 | case Serial64Field: 115 | return Int64Field 116 | default: 117 | return f 118 | } 119 | } 120 | 121 | type RelationKind int 122 | 123 | const ( 124 | SetNull RelationKind = iota 125 | Cascade 126 | Restrict 127 | ) 128 | -------------------------------------------------------------------------------- /dialects.go: -------------------------------------------------------------------------------- 1 | // +build dialects 2 | 3 | package main 4 | 5 | import ( 6 | _ "github.com/lib/pq" 7 | _ "github.com/mattn/go-sqlite3" 8 | ) 9 | -------------------------------------------------------------------------------- /errutil/common.go: -------------------------------------------------------------------------------- 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 errutil 16 | 17 | import ( 18 | "fmt" 19 | "strings" 20 | "text/scanner" 21 | 22 | "github.com/spacemonkeygo/errors" 23 | ) 24 | 25 | var ( 26 | Error = errors.NewClass("dbx") 27 | ) 28 | 29 | var errorPosition = errors.GenSym() 30 | 31 | func New(pos scanner.Position, format string, args ...interface{}) error { 32 | str := fmt.Sprintf("%s: %s", pos, fmt.Sprintf(format, args...)) 33 | return Error.NewWith(str, SetErrorPosition(pos)) 34 | } 35 | 36 | func SetErrorPosition(pos scanner.Position) errors.ErrorOption { 37 | return errors.SetData(errorPosition, pos) 38 | } 39 | 40 | func GetErrorPosition(err error) *scanner.Position { 41 | pos, ok := errors.GetData(err, errorPosition).(scanner.Position) 42 | if ok { 43 | return &pos 44 | } 45 | return nil 46 | } 47 | 48 | func GetContext(src []byte, err error) string { 49 | if src == nil { 50 | return "" 51 | } 52 | if pos := GetErrorPosition(err); pos != nil { 53 | return generateContext(src, *pos) 54 | } 55 | return "" 56 | } 57 | 58 | func lineAround(data []byte, offset int) (start, end int) { 59 | // find the index of the '\n' before data[offset] 60 | start = 0 61 | for i := offset - 1; i >= 0; i-- { 62 | if data[i] == '\n' { 63 | start = i + 1 64 | break 65 | } 66 | } 67 | 68 | // find the index of the '\n' after data[offset] 69 | end = len(data) 70 | for i := offset; i < len(data); i++ { 71 | if data[i] == '\n' { 72 | end = i 73 | break 74 | } 75 | } 76 | 77 | return start, end 78 | } 79 | 80 | func generateContext(source []byte, pos scanner.Position) (context string) { 81 | var context_bytes []byte 82 | 83 | if pos.Offset > len(source) { 84 | panic("internal error: underline on strange position") 85 | } 86 | 87 | line_start, line_end := lineAround(source, pos.Offset) 88 | line := string(source[line_start:line_end]) 89 | 90 | var before_line string 91 | if line_start > 0 { 92 | before_start, before_end := lineAround(source, line_start-1) 93 | before_line = string(source[before_start:before_end]) 94 | before_line = strings.Replace(before_line, "\t", " ", -1) 95 | context_bytes = append(context_bytes, 96 | fmt.Sprintf("% 4d: ", pos.Line-1)...) 97 | context_bytes = append(context_bytes, before_line...) 98 | context_bytes = append(context_bytes, '\n') 99 | } 100 | 101 | tabs := strings.Count(line, "\t") 102 | line = strings.Replace(line, "\t", " ", -1) 103 | context_bytes = append(context_bytes, fmt.Sprintf("% 4d: ", pos.Line)...) 104 | context_bytes = append(context_bytes, line...) 105 | context_bytes = append(context_bytes, '\n') 106 | 107 | offset := tabs*4 + pos.Column - 1 - tabs + 6 108 | underline := strings.Repeat(" ", offset) + "^" 109 | context_bytes = append(context_bytes, underline...) 110 | 111 | return string(context_bytes) 112 | } 113 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module gopkg.in/spacemonkeygo/dbx.v1 2 | 3 | require ( 4 | bitbucket.org/pkg/inflect v0.0.0-20130829110746-8961c3750a47 5 | github.com/jawher/mow.cli v1.0.4 6 | github.com/lib/pq v1.0.0 7 | github.com/mattn/go-sqlite3 v1.10.0 8 | github.com/spacemonkeygo/errors v0.0.0-20171212215202-9064522e9fd1 9 | github.com/stretchr/testify v1.3.0 // indirect 10 | golang.org/x/net v0.0.0-20190206173232-65e2d4e15006 // indirect 11 | golang.org/x/tools v0.0.0-20190208185513-a3f91d6be4f3 12 | ) 13 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | bitbucket.org/pkg/inflect v0.0.0-20130829110746-8961c3750a47 h1:XDrztcXGcx7jow3UUE2dpYtdSWVUigoyn8G+shykEtw= 2 | bitbucket.org/pkg/inflect v0.0.0-20130829110746-8961c3750a47/go.mod h1:8Rt8gHhG+tKz8P3SoEzL/ZNVl25fPhMFKItv5HLIdtY= 3 | github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= 4 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 5 | github.com/jawher/mow.cli v1.0.4 h1:hKjm95J7foZ2ngT8tGb15Aq9rj751R7IUDjG+5e3cGA= 6 | github.com/jawher/mow.cli v1.0.4/go.mod h1:5hQj2V8g+qYmLUVWqu4Wuja1pI57M83EChYLVZ0sMKk= 7 | github.com/lib/pq v1.0.0 h1:X5PMW56eZitiTeO7tKzZxFCSpbFZJtkMMooicw2us9A= 8 | github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= 9 | github.com/mattn/go-sqlite3 v1.10.0 h1:jbhqpg7tQe4SupckyijYiy0mJJ/pRyHvXf7JdWK860o= 10 | github.com/mattn/go-sqlite3 v1.10.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc= 11 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 12 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 13 | github.com/spacemonkeygo/errors v0.0.0-20171212215202-9064522e9fd1 h1:xHQewZjohU9/wUsyC99navCjQDNHtTgUOM/J1jAbzfw= 14 | github.com/spacemonkeygo/errors v0.0.0-20171212215202-9064522e9fd1/go.mod h1:7NL9UAYQnRM5iKHUCld3tf02fKb5Dft+41+VckASUy0= 15 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 16 | github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q= 17 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 18 | golang.org/x/net v0.0.0-20190206173232-65e2d4e15006 h1:bfLnR+k0tq5Lqt6dflRLcZiz6UaXCMt3vhYJ1l4FQ80= 19 | golang.org/x/net v0.0.0-20190206173232-65e2d4e15006/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 20 | golang.org/x/tools v0.0.0-20190208185513-a3f91d6be4f3 h1:9KLrAbyec1NfdzgY6JoTY4qqBmwBuXgIztYudsVeoQ8= 21 | golang.org/x/tools v0.0.0-20190208185513-a3f91d6be4f3/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 22 | -------------------------------------------------------------------------------- /helper_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2017 Space Monkey, Inc. 2 | 3 | package main 4 | 5 | import ( 6 | "bufio" 7 | "bytes" 8 | "fmt" 9 | "strings" 10 | 11 | "gopkg.in/spacemonkeygo/dbx.v1/testutil" 12 | ) 13 | 14 | func linedSource(source []byte) string { 15 | // scan once to find out how many lines 16 | scanner := bufio.NewScanner(bytes.NewReader(source)) 17 | var lines int 18 | for scanner.Scan() { 19 | lines++ 20 | } 21 | align := 1 22 | for ; lines > 0; lines = lines / 10 { 23 | align++ 24 | } 25 | 26 | // now dump with aligned line numbers 27 | buf := bytes.NewBuffer(make([]byte, 0, len(source)*2)) 28 | format := fmt.Sprintf("%%%dd: %%s\n", align) 29 | 30 | scanner = bufio.NewScanner(bytes.NewReader(source)) 31 | for i := 1; scanner.Scan(); i++ { 32 | line := scanner.Text() 33 | fmt.Fprintf(buf, format, i, line) 34 | } 35 | 36 | return buf.String() 37 | } 38 | 39 | type directives struct { 40 | ds map[string][]string 41 | } 42 | 43 | func (d *directives) add(name, value string) { 44 | if d.ds == nil { 45 | d.ds = make(map[string][]string) 46 | } 47 | d.ds[name] = append(d.ds[name], value) 48 | } 49 | 50 | func (d *directives) lookup(name string) (values []string) { 51 | if d.ds == nil { 52 | return nil 53 | } 54 | return d.ds[name] 55 | } 56 | 57 | func (d *directives) has(name string) bool { 58 | if d.ds == nil { 59 | return false 60 | } 61 | return d.ds[name] != nil 62 | } 63 | 64 | func (d *directives) get(name string) string { 65 | vals := d.lookup(name) 66 | if len(vals) == 0 { 67 | return "" 68 | } 69 | return vals[len(vals)-1] 70 | } 71 | 72 | func loadDirectives(t *testutil.T, source []byte) (d directives) { 73 | const prefix = "//test:" 74 | 75 | scanner := bufio.NewScanner(bytes.NewReader(source)) 76 | for scanner.Scan() { 77 | line := scanner.Text() 78 | if !strings.HasPrefix(line, prefix) { 79 | continue 80 | } 81 | parts := strings.SplitN(line, " ", 2) 82 | if len(parts) == 1 { 83 | parts = append(parts, "") 84 | } 85 | if len(parts) != 2 { 86 | t.Fatalf("weird directive parsing: %q", line) 87 | } 88 | d.add(parts[0][len(prefix):], parts[1]) 89 | } 90 | return d 91 | } 92 | -------------------------------------------------------------------------------- /ir/create.go: -------------------------------------------------------------------------------- 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 | import "fmt" 18 | 19 | type Create struct { 20 | Suffix []string 21 | Model *Model 22 | Raw bool 23 | NoReturn bool 24 | } 25 | 26 | func (cre *Create) Signature() string { 27 | prefix := "CREATE" 28 | if cre.Raw { 29 | prefix += "_RAW" 30 | } 31 | if cre.NoReturn { 32 | prefix += "_NORETURN" 33 | } 34 | return fmt.Sprintf("%s(%q)", prefix, cre.Suffix) 35 | } 36 | 37 | func (cre *Create) Fields() (fields []*Field) { 38 | return cre.Model.Fields 39 | } 40 | 41 | func (cre *Create) InsertableFields() (fields []*Field) { 42 | if cre.Raw { 43 | return cre.Model.Fields 44 | } 45 | return cre.Model.InsertableFields() 46 | } 47 | 48 | func (cre *Create) AutoInsertableFields() (fields []*Field) { 49 | if cre.Raw { 50 | return nil 51 | } 52 | return cre.Model.AutoInsertableFields() 53 | } 54 | -------------------------------------------------------------------------------- /ir/delete.go: -------------------------------------------------------------------------------- 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 | import "fmt" 18 | 19 | type Delete struct { 20 | Suffix []string 21 | Model *Model 22 | Joins []*Join 23 | Where []*Where 24 | } 25 | 26 | func (r *Delete) Signature() string { 27 | return fmt.Sprintf("DELETE(%q)", r.Suffix) 28 | } 29 | 30 | func (d *Delete) Distinct() bool { 31 | return queryUnique([]*Model{d.Model}, d.Joins, d.Where) 32 | } 33 | -------------------------------------------------------------------------------- /ir/expr.go: -------------------------------------------------------------------------------- 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 | BoolLit *bool 23 | Field *Field 24 | FuncCall *FuncCall 25 | } 26 | 27 | func (e *Expr) Nullable() bool { 28 | switch { 29 | case e.Null, e.Placeholder: 30 | return true 31 | case e.Field != nil: 32 | return e.Field.Nullable 33 | case e.FuncCall != nil: 34 | return e.FuncCall.Nullable() 35 | default: 36 | return false 37 | } 38 | } 39 | 40 | func (e *Expr) HasPlaceholder() bool { 41 | if e.Placeholder { 42 | return true 43 | } 44 | if e.FuncCall != nil { 45 | return e.FuncCall.HasPlaceholder() 46 | } 47 | return false 48 | } 49 | 50 | func (e *Expr) Unique() bool { 51 | return e.Field != nil && e.Field.Unique() 52 | } 53 | 54 | type FuncCall struct { 55 | Name string 56 | Args []*Expr 57 | } 58 | 59 | func (fc *FuncCall) Nullable() bool { 60 | for _, arg := range fc.Args { 61 | if arg.Nullable() { 62 | return true 63 | } 64 | } 65 | return false 66 | } 67 | 68 | func (fc *FuncCall) HasPlaceholder() bool { 69 | for _, arg := range fc.Args { 70 | if arg.HasPlaceholder() { 71 | return true 72 | } 73 | } 74 | return false 75 | } 76 | -------------------------------------------------------------------------------- /ir/field.go: -------------------------------------------------------------------------------- 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 | import ( 18 | "fmt" 19 | 20 | "gopkg.in/spacemonkeygo/dbx.v1/consts" 21 | ) 22 | 23 | type Relation struct { 24 | Field *Field 25 | Kind consts.RelationKind 26 | } 27 | 28 | type Field struct { 29 | Name string 30 | Column string 31 | Model *Model 32 | Type consts.FieldType 33 | Relation *Relation 34 | Nullable bool 35 | AutoInsert bool 36 | AutoUpdate bool 37 | Updatable bool 38 | Length int // Text only 39 | } 40 | 41 | func (f *Field) Insertable() bool { 42 | if f.Relation != nil { 43 | return true 44 | } 45 | return f.Type != consts.SerialField && f.Type != consts.Serial64Field 46 | } 47 | 48 | func (f *Field) Unique() bool { 49 | return f.Model.FieldUnique(f) 50 | } 51 | 52 | func (f *Field) IsInt() bool { 53 | switch f.Type { 54 | case consts.SerialField, consts.Serial64Field, consts.IntField, consts.Int64Field: 55 | return true 56 | default: 57 | return false 58 | } 59 | } 60 | 61 | func (f *Field) IsTime() bool { 62 | switch f.Type { 63 | case consts.TimestampField, consts.TimestampUTCField, consts.DateField: 64 | return true 65 | default: 66 | return false 67 | } 68 | } 69 | 70 | func (f *Field) ColumnRef() string { 71 | return fmt.Sprintf("%s.%s", f.Model.Table, f.Column) 72 | } 73 | 74 | func (f *Field) ModelOf() *Model { 75 | return f.Model 76 | } 77 | 78 | func (f *Field) UnderRef() string { 79 | return fmt.Sprintf("%s_%s", f.Model.Name, f.Name) 80 | } 81 | 82 | func (f *Field) SelectRefs() (refs []string) { 83 | return []string{f.ColumnRef()} 84 | } 85 | 86 | func (f *Field) selectable() {} 87 | -------------------------------------------------------------------------------- /ir/index.go: -------------------------------------------------------------------------------- 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 Index struct { 18 | Name string 19 | Model *Model 20 | Fields []*Field 21 | Unique bool 22 | } 23 | -------------------------------------------------------------------------------- /ir/model.go: -------------------------------------------------------------------------------- 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 Model struct { 18 | Name string 19 | Table string 20 | Fields []*Field 21 | PrimaryKey []*Field 22 | Unique [][]*Field 23 | Indexes []*Index 24 | } 25 | 26 | func (m *Model) BasicPrimaryKey() *Field { 27 | if len(m.PrimaryKey) == 1 && m.PrimaryKey[0].IsInt() { 28 | return m.PrimaryKey[0] 29 | } 30 | return nil 31 | } 32 | 33 | func (m *Model) InsertableFields() (fields []*Field) { 34 | for _, field := range m.Fields { 35 | if field.Insertable() && !field.AutoInsert { 36 | fields = append(fields, field) 37 | } 38 | } 39 | return fields 40 | } 41 | 42 | func (m *Model) AutoInsertableFields() (fields []*Field) { 43 | for _, field := range m.Fields { 44 | if field.Insertable() && field.AutoInsert { 45 | fields = append(fields, field) 46 | } 47 | } 48 | return fields 49 | } 50 | 51 | func (m *Model) UpdatableFields() (fields []*Field) { 52 | for _, field := range m.Fields { 53 | if field.Updatable && !field.AutoUpdate { 54 | fields = append(fields, field) 55 | } 56 | } 57 | return fields 58 | } 59 | 60 | func (m *Model) AutoUpdatableFields() (fields []*Field) { 61 | for _, field := range m.Fields { 62 | if field.Updatable && field.AutoUpdate { 63 | fields = append(fields, field) 64 | } 65 | } 66 | return fields 67 | } 68 | 69 | func (m *Model) HasUpdatableFields() bool { 70 | for _, field := range m.Fields { 71 | if field.Updatable { 72 | return true 73 | } 74 | } 75 | return false 76 | } 77 | 78 | func (m *Model) FieldUnique(field *Field) bool { 79 | return m.FieldSetUnique([]*Field{field}) 80 | } 81 | 82 | func (m *Model) FieldSetUnique(fields []*Field) bool { 83 | if fieldSetSubset(m.PrimaryKey, fields) { 84 | return true 85 | } 86 | for _, unique := range m.Unique { 87 | if fieldSetSubset(unique, fields) { 88 | return true 89 | } 90 | } 91 | return false 92 | } 93 | 94 | func (m *Model) ModelOf() *Model { 95 | return m 96 | } 97 | 98 | func (m *Model) UnderRef() string { 99 | return m.Name 100 | } 101 | 102 | func (m *Model) SelectRefs() (refs []string) { 103 | for _, field := range m.Fields { 104 | refs = append(refs, field.SelectRefs()...) 105 | } 106 | return refs 107 | } 108 | 109 | func (m *Model) selectable() {} 110 | -------------------------------------------------------------------------------- /ir/read.go: -------------------------------------------------------------------------------- 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 | import ( 18 | "fmt" 19 | 20 | "gopkg.in/spacemonkeygo/dbx.v1/consts" 21 | ) 22 | 23 | type Selectable interface { 24 | SelectRefs() []string 25 | ModelOf() *Model 26 | selectable() 27 | } 28 | 29 | type Read struct { 30 | Suffix []string 31 | Selectables []Selectable 32 | From *Model 33 | Joins []*Join 34 | Where []*Where 35 | OrderBy *OrderBy 36 | GroupBy *GroupBy 37 | View View 38 | } 39 | 40 | func (r *Read) Signature() string { 41 | return fmt.Sprintf("READ(%q,%q)", r.Suffix, r.View) 42 | } 43 | 44 | func (r *Read) Distinct() bool { 45 | var targets []*Model 46 | for _, selectable := range r.Selectables { 47 | targets = append(targets, selectable.ModelOf()) 48 | } 49 | return queryUnique(distinctModels(targets), r.Joins, r.Where) 50 | } 51 | 52 | // SelectedModel returns the single model being selected or nil if there are 53 | // more than one selectable or the selectable is a field. 54 | func (r *Read) SelectedModel() *Model { 55 | if len(r.Selectables) == 1 { 56 | if model, ok := r.Selectables[0].(*Model); ok { 57 | return model 58 | } 59 | } 60 | return nil 61 | } 62 | 63 | type View string 64 | 65 | const ( 66 | All View = "all" 67 | LimitOffset View = "limitoffset" 68 | Paged View = "paged" 69 | Count View = "count" 70 | Has View = "has" 71 | Scalar View = "scalar" 72 | One View = "one" 73 | First View = "first" 74 | ) 75 | 76 | type Join struct { 77 | Type consts.JoinType 78 | Left *Field 79 | Right *Field 80 | } 81 | 82 | type OrderBy struct { 83 | Fields []*Field 84 | Descending bool 85 | } 86 | 87 | type GroupBy struct { 88 | Fields []*Field 89 | } 90 | -------------------------------------------------------------------------------- /ir/root.go: -------------------------------------------------------------------------------- 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 Root struct { 18 | Models []*Model 19 | Creates []*Create 20 | Reads []*Read 21 | Updates []*Update 22 | Deletes []*Delete 23 | } 24 | -------------------------------------------------------------------------------- /ir/update.go: -------------------------------------------------------------------------------- 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 | import "fmt" 18 | 19 | type Update struct { 20 | Suffix []string 21 | Model *Model 22 | Joins []*Join 23 | Where []*Where 24 | NoReturn bool 25 | } 26 | 27 | func (r *Update) Signature() string { 28 | prefix := "UPDATE" 29 | if r.NoReturn { 30 | prefix += "_NORETURN" 31 | } 32 | return fmt.Sprintf("%s(%q)", prefix, r.Suffix) 33 | } 34 | 35 | func (upd *Update) AutoUpdatableFields() (fields []*Field) { 36 | return upd.Model.AutoUpdatableFields() 37 | } 38 | 39 | func (upd *Update) One() bool { 40 | return queryUnique([]*Model{upd.Model}, upd.Joins, upd.Where) 41 | } 42 | -------------------------------------------------------------------------------- /ir/where.go: -------------------------------------------------------------------------------- 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 | import "gopkg.in/spacemonkeygo/dbx.v1/consts" 18 | 19 | type Where struct { 20 | Left *Expr 21 | Op consts.Operator 22 | Right *Expr 23 | } 24 | 25 | func (w *Where) NeedsCondition() bool { 26 | // only EQ and NE need a condition to switch on "=" v.s. "is", etc. 27 | switch w.Op { 28 | case consts.EQ, consts.NE: 29 | default: 30 | return false 31 | } 32 | 33 | // null values are fixed and don't need a runtime condition to render 34 | // appropriately 35 | if w.Left.Null || w.Right.Null { 36 | return false 37 | } 38 | 39 | return w.Left.Nullable() && w.Right.Nullable() 40 | } 41 | -------------------------------------------------------------------------------- /ir/xform/common.go: -------------------------------------------------------------------------------- 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 xform 16 | 17 | import ( 18 | "text/scanner" 19 | 20 | "gopkg.in/spacemonkeygo/dbx.v1/ast" 21 | "gopkg.in/spacemonkeygo/dbx.v1/errutil" 22 | "gopkg.in/spacemonkeygo/dbx.v1/ir" 23 | ) 24 | 25 | func resolveFieldRefs(lookup *lookup, ast_refs []*ast.FieldRef) ( 26 | fields []*ir.Field, err error) { 27 | 28 | for _, ast_ref := range ast_refs { 29 | field, err := lookup.FindField(ast_ref) 30 | if err != nil { 31 | return nil, err 32 | } 33 | fields = append(fields, field) 34 | } 35 | return fields, nil 36 | } 37 | 38 | func resolveRelativeFieldRefs(model_entry *modelEntry, 39 | ast_refs []*ast.RelativeFieldRef) (fields []*ir.Field, err error) { 40 | 41 | for _, ast_ref := range ast_refs { 42 | field, err := model_entry.FindField(ast_ref) 43 | if err != nil { 44 | return nil, err 45 | } 46 | fields = append(fields, field) 47 | } 48 | return fields, nil 49 | } 50 | 51 | func previouslyDefined(pos scanner.Position, kind string, 52 | where scanner.Position) error { 53 | 54 | return errutil.New(pos, 55 | "%s already defined. previous definition at %s", 56 | kind, where) 57 | } 58 | 59 | func duplicateQuery(pos scanner.Position, kind string, 60 | where scanner.Position) error { 61 | return errutil.New(pos, 62 | "%s: duplicate %s (first defined at %s)", 63 | pos, kind, where) 64 | } 65 | -------------------------------------------------------------------------------- /ir/xform/lookup.go: -------------------------------------------------------------------------------- 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 xform 16 | 17 | import ( 18 | "gopkg.in/spacemonkeygo/dbx.v1/ast" 19 | "gopkg.in/spacemonkeygo/dbx.v1/errutil" 20 | "gopkg.in/spacemonkeygo/dbx.v1/ir" 21 | ) 22 | 23 | type lookup struct { 24 | models map[string]*modelEntry 25 | } 26 | 27 | type modelEntry struct { 28 | model *ir.Model 29 | ast *ast.Model 30 | fields map[string]*fieldEntry 31 | } 32 | 33 | type fieldEntry struct { 34 | field *ir.Field 35 | ast *ast.Field 36 | } 37 | 38 | func newLookup() *lookup { 39 | return &lookup{ 40 | models: make(map[string]*modelEntry), 41 | } 42 | } 43 | 44 | func (l *lookup) AddModel(ast_model *ast.Model) (link *modelEntry, err error) { 45 | if existing, ok := l.models[ast_model.Name.Value]; ok { 46 | return nil, previouslyDefined(ast_model.Pos, "model", existing.ast.Pos) 47 | } 48 | 49 | link = newModelEntry(ast_model) 50 | l.models[ast_model.Name.Value] = link 51 | return link, nil 52 | } 53 | 54 | func (l *lookup) GetModel(name string) *modelEntry { 55 | return l.models[name] 56 | } 57 | 58 | func (l *lookup) FindModel(ref *ast.ModelRef) (*ir.Model, error) { 59 | link := l.models[ref.Model.Value] 60 | if link != nil { 61 | return link.model, nil 62 | } 63 | return nil, errutil.New(ref.Pos, "no model %q defined", 64 | ref.Model.Value) 65 | } 66 | 67 | func (l *lookup) FindField(ref *ast.FieldRef) (*ir.Field, error) { 68 | model_link := l.models[ref.Model.Value] 69 | if model_link == nil { 70 | return nil, errutil.New(ref.Pos, "no model %q defined", 71 | ref.Model.Value) 72 | } 73 | return model_link.FindField(ref.Relative()) 74 | } 75 | 76 | func newModelEntry(ast_model *ast.Model) *modelEntry { 77 | return &modelEntry{ 78 | model: &ir.Model{ 79 | Name: ast_model.Name.Value, 80 | }, 81 | ast: ast_model, 82 | fields: make(map[string]*fieldEntry), 83 | } 84 | } 85 | 86 | func (m *modelEntry) newFieldEntry(ast_field *ast.Field) *fieldEntry { 87 | field := &ir.Field{ 88 | Name: ast_field.Name.Value, 89 | Model: m.model, 90 | } 91 | if ast_field.Type != nil { 92 | field.Type = ast_field.Type.Value 93 | } 94 | m.model.Fields = append(m.model.Fields, field) 95 | 96 | return &fieldEntry{ 97 | field: field, 98 | ast: ast_field, 99 | } 100 | } 101 | 102 | func (m *modelEntry) AddField(ast_field *ast.Field) (err error) { 103 | if existing, ok := m.fields[ast_field.Name.Value]; ok { 104 | return previouslyDefined(ast_field.Pos, "field", existing.ast.Pos) 105 | } 106 | m.fields[ast_field.Name.Value] = m.newFieldEntry(ast_field) 107 | return nil 108 | } 109 | 110 | func (m *modelEntry) GetField(name string) *fieldEntry { 111 | return m.fields[name] 112 | } 113 | 114 | func (m *modelEntry) FindField(ref *ast.RelativeFieldRef) (*ir.Field, error) { 115 | field_link := m.fields[ref.Field.Value] 116 | if field_link == nil { 117 | return nil, errutil.New(ref.Pos, "no field %q defined on model %q", 118 | ref.Field.Value, m.model.Name) 119 | } 120 | return field_link.field, nil 121 | } 122 | -------------------------------------------------------------------------------- /ir/xform/suffix.go: -------------------------------------------------------------------------------- 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 xform 16 | 17 | import "gopkg.in/spacemonkeygo/dbx.v1/ast" 18 | 19 | func transformSuffix(suffix *ast.Suffix) []string { 20 | var parts []string 21 | if suffix == nil { 22 | return parts 23 | } 24 | for _, part := range suffix.Parts { 25 | parts = append(parts, part.Value) 26 | } 27 | return parts 28 | } 29 | -------------------------------------------------------------------------------- /ir/xform/transform.go: -------------------------------------------------------------------------------- 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 xform 16 | 17 | import ( 18 | "gopkg.in/spacemonkeygo/dbx.v1/ast" 19 | "gopkg.in/spacemonkeygo/dbx.v1/ir" 20 | ) 21 | 22 | func Transform(ast_root *ast.Root) (root *ir.Root, err error) { 23 | lookup := newLookup() 24 | 25 | models, err := transformModels(lookup, ast_root.Models) 26 | if err != nil { 27 | return nil, err 28 | } 29 | models, err = ir.SortModels(models) 30 | if err != nil { 31 | return nil, err 32 | } 33 | 34 | root = &ir.Root{ 35 | Models: models, 36 | } 37 | 38 | create_signatures := map[string]*ast.Create{} 39 | for _, ast_cre := range ast_root.Creates { 40 | cre, err := transformCreate(lookup, ast_cre) 41 | if err != nil { 42 | return nil, err 43 | } 44 | 45 | if existing := create_signatures[cre.Signature()]; existing != nil { 46 | return nil, duplicateQuery(ast_cre.Pos, "create", existing.Pos) 47 | } 48 | create_signatures[cre.Signature()] = ast_cre 49 | 50 | root.Creates = append(root.Creates, cre) 51 | } 52 | 53 | read_signatures := map[string]*ast.Read{} 54 | for _, ast_read := range ast_root.Reads { 55 | reads, err := transformRead(lookup, ast_read) 56 | if err != nil { 57 | return nil, err 58 | } 59 | 60 | for _, read := range reads { 61 | if existing := read_signatures[read.Signature()]; existing != nil { 62 | return nil, duplicateQuery(ast_read.Pos, "read", existing.Pos) 63 | } 64 | read_signatures[read.Signature()] = ast_read 65 | } 66 | 67 | root.Reads = append(root.Reads, reads...) 68 | } 69 | 70 | update_signatures := map[string]*ast.Update{} 71 | for _, ast_upd := range ast_root.Updates { 72 | upd, err := transformUpdate(lookup, ast_upd) 73 | if err != nil { 74 | return nil, err 75 | } 76 | 77 | if existing := update_signatures[upd.Signature()]; existing != nil { 78 | return nil, duplicateQuery(ast_upd.Pos, "update", existing.Pos) 79 | } 80 | update_signatures[upd.Signature()] = ast_upd 81 | 82 | root.Updates = append(root.Updates, upd) 83 | } 84 | 85 | delete_signatures := map[string]*ast.Delete{} 86 | for _, ast_del := range ast_root.Deletes { 87 | del, err := transformDelete(lookup, ast_del) 88 | if err != nil { 89 | return nil, err 90 | } 91 | 92 | if existing := delete_signatures[del.Signature()]; existing != nil { 93 | return nil, duplicateQuery(ast_del.Pos, "delete", existing.Pos) 94 | } 95 | delete_signatures[del.Signature()] = ast_del 96 | 97 | root.Deletes = append(root.Deletes, del) 98 | } 99 | 100 | return root, nil 101 | } 102 | -------------------------------------------------------------------------------- /ir/xform/transform_create.go: -------------------------------------------------------------------------------- 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 xform 16 | 17 | import ( 18 | "gopkg.in/spacemonkeygo/dbx.v1/ast" 19 | "gopkg.in/spacemonkeygo/dbx.v1/ir" 20 | ) 21 | 22 | func transformCreate(lookup *lookup, ast_cre *ast.Create) ( 23 | cre *ir.Create, err error) { 24 | 25 | model, err := lookup.FindModel(ast_cre.Model) 26 | if err != nil { 27 | return nil, err 28 | } 29 | 30 | cre = &ir.Create{ 31 | Model: model, 32 | Raw: ast_cre.Raw.Get(), 33 | NoReturn: ast_cre.NoReturn.Get(), 34 | Suffix: transformSuffix(ast_cre.Suffix), 35 | } 36 | if cre.Suffix == nil { 37 | cre.Suffix = DefaultCreateSuffix(cre) 38 | } 39 | 40 | return cre, nil 41 | } 42 | -------------------------------------------------------------------------------- /ir/xform/transform_delete.go: -------------------------------------------------------------------------------- 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 xform 16 | 17 | import ( 18 | "gopkg.in/spacemonkeygo/dbx.v1/ast" 19 | "gopkg.in/spacemonkeygo/dbx.v1/errutil" 20 | "gopkg.in/spacemonkeygo/dbx.v1/ir" 21 | ) 22 | 23 | func transformDelete(lookup *lookup, ast_del *ast.Delete) ( 24 | del *ir.Delete, err error) { 25 | 26 | model, err := lookup.FindModel(ast_del.Model) 27 | if err != nil { 28 | return nil, err 29 | } 30 | 31 | if len(model.PrimaryKey) > 1 && len(ast_del.Joins) > 0 { 32 | return nil, errutil.New(ast_del.Joins[0].Pos, 33 | "delete with joins unsupported on multicolumn primary key") 34 | } 35 | 36 | del = &ir.Delete{ 37 | Model: model, 38 | Suffix: transformSuffix(ast_del.Suffix), 39 | } 40 | 41 | models, joins, err := transformJoins( 42 | lookup, []*ir.Model{model}, ast_del.Joins) 43 | if err != nil { 44 | return nil, err 45 | } 46 | models[model.Name] = ast_del.Model.Pos 47 | 48 | del.Joins = joins 49 | 50 | // Finalize the where conditions and make sure referenced models are part 51 | // of the select. 52 | del.Where, err = transformWheres(lookup, models, ast_del.Where) 53 | if err != nil { 54 | return nil, err 55 | } 56 | 57 | if del.Suffix == nil { 58 | del.Suffix = DefaultDeleteSuffix(del) 59 | } 60 | 61 | return del, nil 62 | } 63 | -------------------------------------------------------------------------------- /ir/xform/transform_field.go: -------------------------------------------------------------------------------- 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 xform 16 | 17 | import ( 18 | "gopkg.in/spacemonkeygo/dbx.v1/consts" 19 | "gopkg.in/spacemonkeygo/dbx.v1/errutil" 20 | "gopkg.in/spacemonkeygo/dbx.v1/ir" 21 | ) 22 | 23 | func transformField(lookup *lookup, field_entry *fieldEntry) (err error) { 24 | field := field_entry.field 25 | ast_field := field_entry.ast 26 | 27 | field.Name = ast_field.Name.Value 28 | field.Column = ast_field.Column.Get() 29 | field.Nullable = ast_field.Nullable.Get() 30 | field.Updatable = ast_field.Updatable.Get() 31 | field.AutoInsert = ast_field.AutoInsert.Get() 32 | field.AutoUpdate = ast_field.AutoUpdate.Get() 33 | field.Length = ast_field.Length.Get() 34 | 35 | if field.AutoUpdate { 36 | field.Updatable = true 37 | } 38 | 39 | if ast_field.Relation != nil { 40 | related, err := lookup.FindField(ast_field.Relation) 41 | if err != nil { 42 | return err 43 | } 44 | relation_kind := ast_field.RelationKind.Value 45 | 46 | if relation_kind == consts.SetNull && !field.Nullable { 47 | return errutil.New(ast_field.Pos, 48 | "setnull relationships must be nullable") 49 | } 50 | 51 | field.Relation = &ir.Relation{ 52 | Field: related, 53 | Kind: relation_kind, 54 | } 55 | field.Type = related.Type.AsLink() 56 | } else { 57 | field.Type = ast_field.Type.Value 58 | } 59 | 60 | if ast_field.AutoUpdate != nil && !podFields[field.Type] { 61 | return errutil.New(ast_field.AutoInsert.Pos, 62 | "autoinsert must be on plain data type") 63 | } 64 | if ast_field.AutoUpdate != nil && !podFields[field.Type] { 65 | return errutil.New(ast_field.AutoUpdate.Pos, 66 | "autoupdate must be on plain data type") 67 | } 68 | if ast_field.Length != nil && field.Type != consts.TextField { 69 | return errutil.New(ast_field.Length.Pos, 70 | "length must be on a text field") 71 | } 72 | 73 | if field.Column == "" { 74 | field.Column = field.Name 75 | } 76 | 77 | return nil 78 | } 79 | 80 | var podFields = map[consts.FieldType]bool{ 81 | consts.IntField: true, 82 | consts.Int64Field: true, 83 | consts.UintField: true, 84 | consts.Uint64Field: true, 85 | consts.BoolField: true, 86 | consts.TextField: true, 87 | consts.TimestampField: true, 88 | consts.TimestampUTCField: true, 89 | consts.FloatField: true, 90 | consts.Float64Field: true, 91 | consts.DateField: true, 92 | } 93 | -------------------------------------------------------------------------------- /ir/xform/transform_joins.go: -------------------------------------------------------------------------------- 1 | // Licensed under the Apache License, Version 2.0 (the "License"); 2 | // you may not use this file except in compliance with the License. 3 | // You may obtain a copy of the License at 4 | // 5 | // http://www.apache.org/licenses/LICENSE-2.0 6 | // 7 | // Unless required by applicable law or agreed to in writing, software 8 | // distributed under the License is distributed on an "AS IS" BASIS, 9 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | // See the License for the specific language governing permissions and 11 | // limitations under the License. 12 | 13 | package xform 14 | 15 | import ( 16 | "text/scanner" 17 | 18 | "gopkg.in/spacemonkeygo/dbx.v1/ast" 19 | "gopkg.in/spacemonkeygo/dbx.v1/errutil" 20 | "gopkg.in/spacemonkeygo/dbx.v1/ir" 21 | ) 22 | 23 | func transformJoins(lookup *lookup, in_scope []*ir.Model, 24 | ast_joins []*ast.Join) (models map[string]scanner.Position, 25 | joins []*ir.Join, err error) { 26 | 27 | models = make(map[string]scanner.Position) 28 | 29 | in_scope_set := make(map[*ir.Model]bool) 30 | for _, model := range in_scope { 31 | in_scope_set[model] = true 32 | } 33 | 34 | for _, ast_join := range ast_joins { 35 | left, err := lookup.FindField(ast_join.Left) 36 | if err != nil { 37 | return nil, nil, err 38 | } 39 | if !in_scope_set[left.Model] { 40 | return nil, nil, errutil.New(ast_join.Left.Pos, 41 | "model %q not in scope to join on", left.Model.Name) 42 | } 43 | 44 | right, err := lookup.FindField(ast_join.Right) 45 | if err != nil { 46 | return nil, nil, err 47 | } 48 | in_scope_set[right.Model] = true 49 | 50 | joins = append(joins, &ir.Join{ 51 | Type: ast_join.Type.Get(), 52 | Left: left, 53 | Right: right, 54 | }) 55 | 56 | models[ast_join.Left.Model.Value] = ast_join.Left.Pos 57 | models[ast_join.Right.Model.Value] = ast_join.Right.Pos 58 | } 59 | 60 | return models, joins, nil 61 | } 62 | -------------------------------------------------------------------------------- /ir/xform/transform_model.go: -------------------------------------------------------------------------------- 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 xform 16 | 17 | import ( 18 | "fmt" 19 | 20 | "bitbucket.org/pkg/inflect" 21 | "gopkg.in/spacemonkeygo/dbx.v1/ast" 22 | "gopkg.in/spacemonkeygo/dbx.v1/errutil" 23 | "gopkg.in/spacemonkeygo/dbx.v1/ir" 24 | ) 25 | 26 | func transformModel(lookup *lookup, model_entry *modelEntry) (err error) { 27 | model := model_entry.model 28 | ast_model := model_entry.ast 29 | 30 | model.Name = ast_model.Name.Value 31 | model.Table = ast_model.Table.Get() 32 | if model.Table == "" { 33 | model.Table = inflect.Pluralize(model.Name) 34 | } 35 | 36 | column_names := map[string]*ast.Field{} 37 | for _, ast_field := range ast_model.Fields { 38 | field_entry := model_entry.GetField(ast_field.Name.Value) 39 | if err := transformField(lookup, field_entry); err != nil { 40 | return err 41 | } 42 | 43 | field := field_entry.field 44 | 45 | if existing := column_names[field.Column]; existing != nil { 46 | return errutil.New(ast_field.Pos, 47 | "column %q already used by field %q at %s", 48 | field.Column, existing.Name.Get(), existing.Pos) 49 | } 50 | column_names[field.Column] = ast_field 51 | } 52 | 53 | if ast_model.PrimaryKey == nil || len(ast_model.PrimaryKey.Refs) == 0 { 54 | return errutil.New(ast_model.Pos, "no primary key defined") 55 | } 56 | 57 | for _, ast_fieldref := range ast_model.PrimaryKey.Refs { 58 | field, err := model_entry.FindField(ast_fieldref) 59 | if err != nil { 60 | return err 61 | } 62 | if field.Nullable { 63 | return errutil.New(ast_fieldref.Pos, 64 | "nullable field %q cannot be a primary key", 65 | ast_fieldref) 66 | } 67 | if field.Updatable { 68 | return errutil.New(ast_fieldref.Pos, 69 | "updatable field %q cannot be a primary key", 70 | ast_fieldref) 71 | } 72 | model.PrimaryKey = append(model.PrimaryKey, field) 73 | } 74 | 75 | for _, ast_unique := range ast_model.Unique { 76 | fields, err := resolveRelativeFieldRefs(model_entry, ast_unique.Refs) 77 | if err != nil { 78 | return err 79 | } 80 | model.Unique = append(model.Unique, fields) 81 | } 82 | 83 | index_names := map[string]*ast.Index{} 84 | for _, ast_index := range ast_model.Indexes { 85 | // BUG(jeff): we can only have one index without a name specified when 86 | // really we want to pick a name for them that won't collide. 87 | if ast_index.Fields == nil || len(ast_index.Fields.Refs) < 1 { 88 | return errutil.New(ast_index.Pos, 89 | "index %q has no fields defined", 90 | ast_index.Name.Get()) 91 | } 92 | 93 | fields, err := resolveRelativeFieldRefs( 94 | model_entry, ast_index.Fields.Refs) 95 | if err != nil { 96 | return err 97 | } 98 | 99 | index := &ir.Index{ 100 | Name: ast_index.Name.Get(), 101 | Model: fields[0].Model, 102 | Fields: fields, 103 | Unique: ast_index.Unique.Get(), 104 | } 105 | 106 | if index.Name == "" { 107 | index.Name = DefaultIndexName(index) 108 | } 109 | 110 | if existing, ok := index_names[index.Name]; ok { 111 | return previouslyDefined(ast_index.Pos, 112 | fmt.Sprintf("index (%s)", index.Name), 113 | existing.Pos) 114 | } 115 | index_names[index.Name] = ast_index 116 | 117 | model.Indexes = append(model.Indexes, index) 118 | } 119 | 120 | return nil 121 | } 122 | -------------------------------------------------------------------------------- /ir/xform/transform_models.go: -------------------------------------------------------------------------------- 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 xform 16 | 17 | import ( 18 | "gopkg.in/spacemonkeygo/dbx.v1/ast" 19 | "gopkg.in/spacemonkeygo/dbx.v1/errutil" 20 | "gopkg.in/spacemonkeygo/dbx.v1/ir" 21 | ) 22 | 23 | func transformModels(lookup *lookup, ast_models []*ast.Model) ( 24 | models []*ir.Model, err error) { 25 | 26 | // step 1. create all the Model and Field instances and set their pointers 27 | // to point at each other appropriately. 28 | for _, ast_model := range ast_models { 29 | link, err := lookup.AddModel(ast_model) 30 | if err != nil { 31 | return nil, err 32 | } 33 | for _, ast_field := range ast_model.Fields { 34 | if err := link.AddField(ast_field); err != nil { 35 | return nil, err 36 | } 37 | } 38 | } 39 | 40 | // step 2. resolve all of the other fields on the models and Fields 41 | // including references between them. also check for duplicate table names. 42 | table_names := map[string]*ast.Model{} 43 | for _, ast_model := range ast_models { 44 | model_entry := lookup.GetModel(ast_model.Name.Value) 45 | if err := transformModel(lookup, model_entry); err != nil { 46 | return nil, err 47 | } 48 | 49 | model := model_entry.model 50 | 51 | if existing := table_names[model.Table]; existing != nil { 52 | return nil, errutil.New(ast_model.Pos, 53 | "table %q already used by model %q (%s)", 54 | model.Table, existing.Name.Get(), existing.Pos) 55 | } 56 | table_names[model.Table] = ast_model 57 | 58 | models = append(models, model_entry.model) 59 | } 60 | 61 | return models, nil 62 | } 63 | -------------------------------------------------------------------------------- /ir/xform/transform_update.go: -------------------------------------------------------------------------------- 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 xform 16 | 17 | import ( 18 | "gopkg.in/spacemonkeygo/dbx.v1/ast" 19 | "gopkg.in/spacemonkeygo/dbx.v1/errutil" 20 | "gopkg.in/spacemonkeygo/dbx.v1/ir" 21 | ) 22 | 23 | func transformUpdate(lookup *lookup, ast_upd *ast.Update) ( 24 | upd *ir.Update, err error) { 25 | 26 | model, err := lookup.FindModel(ast_upd.Model) 27 | if err != nil { 28 | return nil, err 29 | } 30 | if !model.HasUpdatableFields() { 31 | return nil, errutil.New(ast_upd.Pos, 32 | "update on model with no updatable fields") 33 | } 34 | 35 | if len(model.PrimaryKey) > 1 && len(ast_upd.Joins) > 0 { 36 | return nil, errutil.New(ast_upd.Joins[0].Pos, 37 | "update with joins unsupported on multicolumn primary key:") 38 | } 39 | 40 | upd = &ir.Update{ 41 | Model: model, 42 | NoReturn: ast_upd.NoReturn.Get(), 43 | Suffix: transformSuffix(ast_upd.Suffix), 44 | } 45 | 46 | models, joins, err := transformJoins( 47 | lookup, []*ir.Model{model}, ast_upd.Joins) 48 | if err != nil { 49 | return nil, err 50 | } 51 | models[model.Name] = ast_upd.Model.Pos 52 | 53 | upd.Joins = joins 54 | 55 | // Finalize the where conditions and make sure referenced models are part 56 | // of the select. 57 | upd.Where, err = transformWheres(lookup, models, ast_upd.Where) 58 | if err != nil { 59 | return nil, err 60 | } 61 | 62 | if !upd.One() { 63 | return nil, errutil.New(ast_upd.Pos, 64 | "updates for more than one row are unsupported") 65 | } 66 | 67 | if upd.Suffix == nil { 68 | upd.Suffix = DefaultUpdateSuffix(upd) 69 | } 70 | 71 | return upd, nil 72 | } 73 | -------------------------------------------------------------------------------- /ir/xform/transform_where.go: -------------------------------------------------------------------------------- 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 xform 16 | 17 | import ( 18 | "text/scanner" 19 | 20 | "gopkg.in/spacemonkeygo/dbx.v1/ast" 21 | "gopkg.in/spacemonkeygo/dbx.v1/ir" 22 | ) 23 | 24 | func transformWheres(lookup *lookup, models map[string]scanner.Position, 25 | ast_wheres []*ast.Where) (wheres []*ir.Where, err error) { 26 | for _, ast_where := range ast_wheres { 27 | where, err := transformWhere(lookup, models, ast_where) 28 | if err != nil { 29 | return nil, err 30 | } 31 | 32 | wheres = append(wheres, where) 33 | } 34 | return wheres, nil 35 | } 36 | 37 | func transformWhere(lookup *lookup, models map[string]scanner.Position, 38 | ast_where *ast.Where) (where *ir.Where, err error) { 39 | 40 | lexpr, err := transformExpr(lookup, models, ast_where.Left, true) 41 | if err != nil { 42 | return nil, err 43 | } 44 | 45 | rexpr, err := transformExpr(lookup, models, ast_where.Right, false) 46 | if err != nil { 47 | return nil, err 48 | } 49 | 50 | return &ir.Where{ 51 | Left: lexpr, 52 | Op: ast_where.Op.Value, 53 | Right: rexpr, 54 | }, nil 55 | } 56 | -------------------------------------------------------------------------------- /run_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2017 Space Monkey, Inc. 2 | 3 | package main 4 | 5 | import ( 6 | "bytes" 7 | "io/ioutil" 8 | "os" 9 | "os/exec" 10 | "path/filepath" 11 | "runtime/debug" 12 | "testing" 13 | 14 | "gopkg.in/spacemonkeygo/dbx.v1/testutil" 15 | ) 16 | 17 | func TestRun(t *testing.T) { 18 | tw := testutil.Wrap(t) 19 | tw.Parallel() 20 | 21 | data_dir := filepath.Join("testdata", "run") 22 | 23 | names, err := filepath.Glob(filepath.Join(data_dir, "*.dbx")) 24 | tw.AssertNoError(err) 25 | 26 | for _, name := range names { 27 | name := name 28 | tw.Runp(filepath.Base(name), func(tw *testutil.T) { 29 | testRunFile(tw, name) 30 | }) 31 | } 32 | } 33 | 34 | func testRunFile(t *testutil.T, dbx_file string) { 35 | defer func() { 36 | if val := recover(); val != nil { 37 | t.Fatalf("%s\n%s", val, string(debug.Stack())) 38 | } 39 | }() 40 | 41 | dbx_source, err := ioutil.ReadFile(dbx_file) 42 | t.AssertNoError(err) 43 | t.Context("dbx", linedSource(dbx_source)) 44 | d := loadDirectives(t, dbx_source) 45 | 46 | dir, err := ioutil.TempDir("", "dbx") 47 | t.AssertNoError(err) 48 | defer os.RemoveAll(dir) 49 | 50 | t.Logf("[%s] generating... {rx:%t, userdata:%t}", dbx_file, 51 | d.has("rx"), d.has("userdata")) 52 | err = golangCmd("main", []string{"sqlite3"}, "", 53 | d.has("rx"), d.has("userdata"), dbx_file, dir) 54 | if d.has("fail_gen") { 55 | t.AssertError(err, d.get("fail_gen")) 56 | return 57 | } else { 58 | t.AssertNoError(err) 59 | } 60 | 61 | ext := filepath.Ext(dbx_file) 62 | go_file := dbx_file[:len(dbx_file)-len(ext)] + ".go" 63 | go_source, err := ioutil.ReadFile(go_file) 64 | t.AssertNoError(err) 65 | t.Context("go", linedSource(go_source)) 66 | 67 | t.Logf("[%s] copying go source...", dbx_file) 68 | t.AssertNoError(ioutil.WriteFile( 69 | filepath.Join(dir, filepath.Base(go_file)), go_source, 0644)) 70 | 71 | t.Logf("[%s] running output...", dbx_file) 72 | files, err := filepath.Glob(filepath.Join(dir, "*.go")) 73 | t.AssertNoError(err) 74 | 75 | var stdout, stderr bytes.Buffer 76 | 77 | cmd := exec.Command("go", append([]string{"run"}, files...)...) 78 | cmd.Stdout = &stdout 79 | cmd.Stderr = &stderr 80 | err = cmd.Run() 81 | 82 | t.Context("stdout", stdout.String()) 83 | t.Context("stderr", stderr.String()) 84 | 85 | if d.has("fail") { 86 | t.AssertError(err, "") 87 | t.AssertContains(stderr.String(), d.get("fail")) 88 | } else { 89 | t.AssertNoError(err) 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /sql/common.go: -------------------------------------------------------------------------------- 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 sql 16 | 17 | import "gopkg.in/spacemonkeygo/dbx.v1/ir" 18 | 19 | type Features struct { 20 | // Supports the RETURNING syntax on INSERT/UPDATE 21 | Returning bool 22 | 23 | // Supports positional argument placeholders 24 | PositionalArguments bool 25 | 26 | // Token used with LIMIT to mean "no limit" 27 | NoLimitToken string 28 | } 29 | 30 | type Dialect interface { 31 | Name() string 32 | Features() Features 33 | RowId() string 34 | ColumnType(field *ir.Field) string 35 | Rebind(sql string) string 36 | EscapeString(s string) string 37 | BoolLit(v bool) string 38 | } 39 | -------------------------------------------------------------------------------- /sql/delete.go: -------------------------------------------------------------------------------- 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 sql 16 | 17 | import ( 18 | "gopkg.in/spacemonkeygo/dbx.v1/ir" 19 | "gopkg.in/spacemonkeygo/dbx.v1/sqlgen" 20 | "gopkg.in/spacemonkeygo/dbx.v1/sqlgen/sqlcompile" 21 | . "gopkg.in/spacemonkeygo/dbx.v1/sqlgen/sqlhelpers" 22 | ) 23 | 24 | func DeleteSQL(ir_del *ir.Delete, dialect Dialect) sqlgen.SQL { 25 | stmt := Build(Lf("DELETE FROM %s", ir_del.Model.Table)) 26 | 27 | var wheres []sqlgen.SQL 28 | if len(ir_del.Joins) == 0 { 29 | wheres = WhereSQL(ir_del.Where, dialect) 30 | } else { 31 | pk_column := ir_del.Model.PrimaryKey[0].ColumnRef() 32 | sel := SelectSQL(&ir.Read{ 33 | View: ir.All, 34 | From: ir_del.Model, 35 | Selectables: []ir.Selectable{ir_del.Model.PrimaryKey[0]}, 36 | Joins: ir_del.Joins, 37 | Where: ir_del.Where, 38 | }, dialect) 39 | wheres = append(wheres, J("", L(pk_column), L(" IN ("), sel, L(")"))) 40 | } 41 | 42 | if len(wheres) > 0 { 43 | stmt.Add(L("WHERE"), J(" AND ", wheres...)) 44 | } 45 | 46 | return sqlcompile.Compile(stmt.SQL()) 47 | 48 | } 49 | -------------------------------------------------------------------------------- /sql/expr.go: -------------------------------------------------------------------------------- 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 sql 16 | 17 | import ( 18 | "fmt" 19 | 20 | "gopkg.in/spacemonkeygo/dbx.v1/ir" 21 | "gopkg.in/spacemonkeygo/dbx.v1/sqlgen" 22 | . "gopkg.in/spacemonkeygo/dbx.v1/sqlgen/sqlhelpers" 23 | ) 24 | 25 | func ExprSQL(expr *ir.Expr, dialect Dialect) sqlgen.SQL { 26 | switch { 27 | case expr.Null: 28 | return L("NULL") 29 | case expr.StringLit != nil: 30 | return J("", L("'"), L(dialect.EscapeString(*expr.StringLit)), L("'")) 31 | case expr.NumberLit != nil: 32 | return L(*expr.NumberLit) 33 | case expr.BoolLit != nil: 34 | return L(dialect.BoolLit(*expr.BoolLit)) 35 | case expr.Placeholder: 36 | return L("?") 37 | case expr.Field != nil: 38 | return L(expr.Field.ColumnRef()) 39 | case expr.FuncCall != nil: 40 | var args []sqlgen.SQL 41 | for _, arg := range expr.FuncCall.Args { 42 | args = append(args, ExprSQL(arg, dialect)) 43 | } 44 | return J("", L(expr.FuncCall.Name), L("("), J(", ", args...), L(")")) 45 | default: 46 | panic(fmt.Sprintf("unhandled expression variant: %+v", expr)) 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /sql/group_by.go: -------------------------------------------------------------------------------- 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 sql 16 | 17 | import ( 18 | "gopkg.in/spacemonkeygo/dbx.v1/ir" 19 | "gopkg.in/spacemonkeygo/dbx.v1/sqlgen" 20 | "gopkg.in/spacemonkeygo/dbx.v1/sqlgen/sqlcompile" 21 | . "gopkg.in/spacemonkeygo/dbx.v1/sqlgen/sqlhelpers" 22 | ) 23 | 24 | type GroupBy struct { 25 | Fields []string 26 | } 27 | 28 | func GroupByFromIRGroupBy(ir_group_by *ir.GroupBy) (group_by *GroupBy) { 29 | group_by = &GroupBy{} 30 | for _, ir_field := range ir_group_by.Fields { 31 | group_by.Fields = append(group_by.Fields, ir_field.ColumnRef()) 32 | } 33 | return group_by 34 | } 35 | 36 | func SQLFromGroupBy(group_by *GroupBy) sqlgen.SQL { 37 | stmt := Build(L("GROUP BY")) 38 | stmt.Add(J(", ", Strings(group_by.Fields)...)) 39 | return sqlcompile.Compile(stmt.SQL()) 40 | } 41 | -------------------------------------------------------------------------------- /sql/insert.go: -------------------------------------------------------------------------------- 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 sql 16 | 17 | import ( 18 | "gopkg.in/spacemonkeygo/dbx.v1/ir" 19 | "gopkg.in/spacemonkeygo/dbx.v1/sqlgen" 20 | "gopkg.in/spacemonkeygo/dbx.v1/sqlgen/sqlcompile" 21 | . "gopkg.in/spacemonkeygo/dbx.v1/sqlgen/sqlhelpers" 22 | ) 23 | 24 | func InsertSQL(ir_cre *ir.Create, dialect Dialect) sqlgen.SQL { 25 | return SQLFromInsert(InsertFromIRCreate(ir_cre, dialect)) 26 | } 27 | 28 | type Insert struct { 29 | Table string 30 | Columns []string 31 | Returning []string 32 | } 33 | 34 | func InsertFromIRCreate(ir_cre *ir.Create, dialect Dialect) *Insert { 35 | ins := &Insert{ 36 | Table: ir_cre.Model.Table, 37 | } 38 | if dialect.Features().Returning && !ir_cre.NoReturn { 39 | ins.Returning = ir_cre.Model.SelectRefs() 40 | } 41 | for _, field := range ir_cre.Fields() { 42 | if field == ir_cre.Model.BasicPrimaryKey() && !ir_cre.Raw { 43 | continue 44 | } 45 | ins.Columns = append(ins.Columns, field.Column) 46 | } 47 | return ins 48 | } 49 | 50 | func SQLFromInsert(insert *Insert) sqlgen.SQL { 51 | stmt := Build(Lf("INSERT INTO %s", insert.Table)) 52 | 53 | if cols := insert.Columns; len(cols) > 0 { 54 | stmt.Add(L("("), J(", ", Strings(cols)...), L(")")) 55 | stmt.Add(L("VALUES ("), J(", ", Placeholders(len(cols))...), L(")")) 56 | } else { 57 | stmt.Add(L("DEFAULT VALUES")) 58 | } 59 | 60 | if rets := insert.Returning; len(rets) > 0 { 61 | stmt.Add(L("RETURNING"), J(", ", Strings(rets)...)) 62 | } 63 | 64 | return sqlcompile.Compile(stmt.SQL()) 65 | } 66 | -------------------------------------------------------------------------------- /sql/join.go: -------------------------------------------------------------------------------- 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 sql 16 | 17 | import ( 18 | "fmt" 19 | 20 | "gopkg.in/spacemonkeygo/dbx.v1/consts" 21 | "gopkg.in/spacemonkeygo/dbx.v1/ir" 22 | "gopkg.in/spacemonkeygo/dbx.v1/sqlgen" 23 | "gopkg.in/spacemonkeygo/dbx.v1/sqlgen/sqlcompile" 24 | . "gopkg.in/spacemonkeygo/dbx.v1/sqlgen/sqlhelpers" 25 | ) 26 | 27 | type Join struct { 28 | Type string 29 | Table string 30 | Left string 31 | Right string 32 | } 33 | 34 | func JoinFromIRJoin(ir_join *ir.Join) Join { 35 | join := Join{ 36 | Table: ir_join.Right.Model.Table, 37 | Left: ir_join.Left.ColumnRef(), 38 | Right: ir_join.Right.ColumnRef(), 39 | } 40 | switch ir_join.Type { 41 | case consts.InnerJoin: 42 | default: 43 | panic(fmt.Sprintf("unhandled join type %q", join.Type)) 44 | } 45 | return join 46 | } 47 | 48 | func JoinsFromIRJoins(ir_joins []*ir.Join) (joins []Join) { 49 | for _, ir_join := range ir_joins { 50 | joins = append(joins, JoinFromIRJoin(ir_join)) 51 | } 52 | return joins 53 | } 54 | 55 | func SQLFromJoin(join Join) sqlgen.SQL { 56 | clause := Build(Lf("%s JOIN %s ON %s =", join.Type, join.Table, join.Left)) 57 | if join.Right != "" { 58 | clause.Add(L(join.Right)) 59 | } else { 60 | clause.Add(Placeholder) 61 | } 62 | return sqlcompile.Compile(clause.SQL()) 63 | } 64 | 65 | func SQLFromJoins(joins []Join) []sqlgen.SQL { 66 | var out []sqlgen.SQL 67 | for _, join := range joins { 68 | out = append(out, SQLFromJoin(join)) 69 | } 70 | return out 71 | } 72 | -------------------------------------------------------------------------------- /sql/order_by.go: -------------------------------------------------------------------------------- 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 sql 16 | 17 | import ( 18 | "gopkg.in/spacemonkeygo/dbx.v1/ir" 19 | "gopkg.in/spacemonkeygo/dbx.v1/sqlgen" 20 | "gopkg.in/spacemonkeygo/dbx.v1/sqlgen/sqlcompile" 21 | . "gopkg.in/spacemonkeygo/dbx.v1/sqlgen/sqlhelpers" 22 | ) 23 | 24 | type OrderBy struct { 25 | Fields []string 26 | Descending bool 27 | } 28 | 29 | func OrderByFromIROrderBy(ir_order_by *ir.OrderBy) (order_by *OrderBy) { 30 | order_by = &OrderBy{ 31 | Descending: ir_order_by.Descending, 32 | } 33 | for _, ir_field := range ir_order_by.Fields { 34 | order_by.Fields = append(order_by.Fields, ir_field.ColumnRef()) 35 | } 36 | return order_by 37 | } 38 | 39 | func SQLFromOrderBy(order_by *OrderBy) sqlgen.SQL { 40 | stmt := Build(L("ORDER BY")) 41 | stmt.Add(J(", ", Strings(order_by.Fields)...)) 42 | if order_by.Descending { 43 | stmt.Add(L("DESC")) 44 | } 45 | return sqlcompile.Compile(stmt.SQL()) 46 | } 47 | -------------------------------------------------------------------------------- /sql/postgres.go: -------------------------------------------------------------------------------- 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 sql 16 | 17 | import ( 18 | "fmt" 19 | "strconv" 20 | "strings" 21 | 22 | "gopkg.in/spacemonkeygo/dbx.v1/consts" 23 | "gopkg.in/spacemonkeygo/dbx.v1/ir" 24 | ) 25 | 26 | type postgres struct { 27 | } 28 | 29 | func Postgres() Dialect { 30 | return &postgres{} 31 | } 32 | 33 | func (p *postgres) Name() string { 34 | return "postgres" 35 | } 36 | 37 | func (p *postgres) Features() Features { 38 | return Features{ 39 | Returning: true, 40 | PositionalArguments: true, 41 | NoLimitToken: "ALL", 42 | } 43 | } 44 | 45 | func (p *postgres) RowId() string { 46 | return "" 47 | } 48 | 49 | func (p *postgres) ColumnType(field *ir.Field) string { 50 | switch field.Type { 51 | case consts.SerialField: 52 | return "serial" 53 | case consts.Serial64Field: 54 | return "bigserial" 55 | case consts.IntField: 56 | return "integer" 57 | case consts.Int64Field: 58 | return "bigint" 59 | case consts.UintField: 60 | return "integer" 61 | case consts.Uint64Field: 62 | return "bigint" 63 | case consts.FloatField: 64 | return "real" 65 | case consts.Float64Field: 66 | return "double precision" 67 | case consts.TextField: 68 | if field.Length > 0 { 69 | return fmt.Sprintf("varchar(%d)", field.Length) 70 | } 71 | return "text" 72 | case consts.BoolField: 73 | return "boolean" 74 | case consts.TimestampField: 75 | return "timestamp with time zone" 76 | case consts.TimestampUTCField: 77 | return "timestamp" 78 | case consts.BlobField: 79 | return "bytea" 80 | case consts.DateField: 81 | return "date" 82 | default: 83 | panic(fmt.Sprintf("unhandled field type %s", field.Type)) 84 | } 85 | } 86 | 87 | func (p *postgres) Rebind(sql string) string { 88 | out := make([]byte, 0, len(sql)+10) 89 | 90 | j := 1 91 | for i := 0; i < len(sql); i++ { 92 | ch := sql[i] 93 | if ch != '?' { 94 | out = append(out, ch) 95 | continue 96 | } 97 | 98 | out = append(out, '$') 99 | out = append(out, strconv.Itoa(j)...) 100 | j++ 101 | } 102 | 103 | return string(out) 104 | } 105 | 106 | func (p *postgres) ArgumentPrefix() string { return "$" } 107 | 108 | var postgresEscaper = strings.NewReplacer( 109 | `'`, `\'`, 110 | `\`, `\\`, 111 | ) 112 | 113 | func (p *postgres) EscapeString(s string) string { 114 | return postgresEscaper.Replace(s) 115 | } 116 | 117 | func (p *postgres) BoolLit(v bool) string { 118 | if v { 119 | return "true" 120 | } 121 | return "false" 122 | } 123 | -------------------------------------------------------------------------------- /sql/sqlite3.go: -------------------------------------------------------------------------------- 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 sql 16 | 17 | import ( 18 | "fmt" 19 | "strings" 20 | 21 | "gopkg.in/spacemonkeygo/dbx.v1/consts" 22 | "gopkg.in/spacemonkeygo/dbx.v1/ir" 23 | ) 24 | 25 | type sqlite3 struct { 26 | } 27 | 28 | func SQLite3() Dialect { 29 | return &sqlite3{} 30 | } 31 | 32 | func (s *sqlite3) Name() string { 33 | return "sqlite3" 34 | } 35 | 36 | func (s *sqlite3) Features() Features { 37 | return Features{ 38 | Returning: false, 39 | NoLimitToken: "-1", 40 | } 41 | } 42 | 43 | func (s *sqlite3) RowId() string { 44 | return "_rowid_" 45 | } 46 | 47 | func (s *sqlite3) ColumnType(field *ir.Field) string { 48 | switch field.Type { 49 | case consts.SerialField, consts.Serial64Field, 50 | consts.IntField, consts.Int64Field, 51 | consts.UintField, consts.Uint64Field: 52 | return "INTEGER" 53 | case consts.FloatField, consts.Float64Field: 54 | return "REAL" 55 | case consts.TextField: 56 | return "TEXT" 57 | case consts.BoolField: 58 | return "INTEGER" 59 | case consts.TimestampField, consts.TimestampUTCField: 60 | return "TIMESTAMP" 61 | case consts.BlobField: 62 | return "BLOB" 63 | case consts.DateField: 64 | return "DATE" 65 | default: 66 | panic(fmt.Sprintf("unhandled field type %s", field.Type)) 67 | } 68 | } 69 | 70 | func (s *sqlite3) Rebind(sql string) string { 71 | return sql 72 | } 73 | 74 | var sqlite3Replacer = strings.NewReplacer( 75 | `'`, `''`, 76 | ) 77 | 78 | func (p *sqlite3) EscapeString(s string) string { 79 | return sqlite3Replacer.Replace(s) 80 | } 81 | 82 | func (p *sqlite3) BoolLit(v bool) string { 83 | if v { 84 | return "1" 85 | } 86 | return "0" 87 | } 88 | -------------------------------------------------------------------------------- /sql/update.go: -------------------------------------------------------------------------------- 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 sql 16 | 17 | import ( 18 | "gopkg.in/spacemonkeygo/dbx.v1/ir" 19 | "gopkg.in/spacemonkeygo/dbx.v1/sqlgen" 20 | "gopkg.in/spacemonkeygo/dbx.v1/sqlgen/sqlcompile" 21 | . "gopkg.in/spacemonkeygo/dbx.v1/sqlgen/sqlhelpers" 22 | ) 23 | 24 | func UpdateSQL(ir_upd *ir.Update, dialect Dialect) sqlgen.SQL { 25 | return SQLFromUpdate(UpdateFromIRUpdate(ir_upd, dialect)) 26 | } 27 | 28 | type Update struct { 29 | Table string 30 | Where []sqlgen.SQL 31 | Returning []string 32 | In sqlgen.SQL 33 | } 34 | 35 | func UpdateFromIRUpdate(ir_upd *ir.Update, dialect Dialect) *Update { 36 | var returning []string 37 | if dialect.Features().Returning && !ir_upd.NoReturn { 38 | returning = ir_upd.Model.SelectRefs() 39 | } 40 | 41 | if len(ir_upd.Joins) == 0 { 42 | return &Update{ 43 | Table: ir_upd.Model.Table, 44 | Where: WhereSQL(ir_upd.Where, dialect), 45 | Returning: returning, 46 | } 47 | } 48 | 49 | pk_column := ir_upd.Model.PrimaryKey[0].Column 50 | sel := SQLFromSelect(&Select{ 51 | From: ir_upd.Model.Table, 52 | Fields: []string{pk_column}, 53 | Joins: JoinsFromIRJoins(ir_upd.Joins), 54 | Where: WhereSQL(ir_upd.Where, dialect), 55 | }) 56 | in := J("", L(pk_column), L(" IN ("), sel, L(")")) 57 | 58 | return &Update{ 59 | Table: ir_upd.Model.Table, 60 | Returning: returning, 61 | In: in, 62 | } 63 | } 64 | 65 | func SQLFromUpdate(upd *Update) sqlgen.SQL { 66 | stmt := Build(Lf("UPDATE %s SET", upd.Table)) 67 | 68 | stmt.Add(Hole("sets")) 69 | 70 | wheres := upd.Where 71 | if upd.In != nil { 72 | wheres = append(wheres, upd.In) 73 | } 74 | if len(wheres) > 0 { 75 | stmt.Add(L("WHERE"), J(" AND ", wheres...)) 76 | } 77 | 78 | if len(upd.Returning) > 0 { 79 | stmt.Add(L("RETURNING"), J(", ", Strings(upd.Returning)...)) 80 | } 81 | 82 | return sqlcompile.Compile(stmt.SQL()) 83 | } 84 | -------------------------------------------------------------------------------- /sql/where.go: -------------------------------------------------------------------------------- 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 sql 16 | 17 | import ( 18 | "fmt" 19 | "strings" 20 | 21 | "gopkg.in/spacemonkeygo/dbx.v1/consts" 22 | "gopkg.in/spacemonkeygo/dbx.v1/ir" 23 | "gopkg.in/spacemonkeygo/dbx.v1/sqlgen" 24 | . "gopkg.in/spacemonkeygo/dbx.v1/sqlgen/sqlhelpers" 25 | ) 26 | 27 | func WhereSQL(wheres []*ir.Where, dialect Dialect) (out []sqlgen.SQL) { 28 | // we put all the condition wheres at the end for ease of template 29 | // generation later. 30 | 31 | for _, where := range wheres { 32 | if where.NeedsCondition() { 33 | continue 34 | } 35 | out = append(out, 36 | J(" ", ExprSQL(where.Left, dialect), 37 | opSQL(where.Op, where.Left, where.Right), 38 | ExprSQL(where.Right, dialect))) 39 | } 40 | 41 | conditions := 0 42 | for _, where := range wheres { 43 | if !where.NeedsCondition() { 44 | continue 45 | } 46 | out = append(out, &sqlgen.Condition{ 47 | Name: fmt.Sprintf("cond_%d", conditions), 48 | Left: ExprSQL(where.Left, dialect).Render(), 49 | Equal: where.Op == "=", 50 | Right: ExprSQL(where.Right, dialect).Render(), 51 | }) 52 | conditions++ 53 | } 54 | 55 | return out 56 | } 57 | 58 | func opSQL(op consts.Operator, left, right *ir.Expr) sqlgen.SQL { 59 | switch op { 60 | case consts.EQ: 61 | if left.Null || right.Null { 62 | return L("is") 63 | } 64 | case consts.NE: 65 | if left.Null || right.Null { 66 | return L("is not") 67 | } 68 | } 69 | return L(strings.ToUpper(string(op))) 70 | } 71 | -------------------------------------------------------------------------------- /sqlgen/common.go: -------------------------------------------------------------------------------- 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 sqlgen 16 | 17 | import ( 18 | "strings" 19 | ) 20 | 21 | type SQL interface { 22 | Render() string 23 | 24 | private() 25 | } 26 | 27 | type Dialect interface { 28 | Rebind(sql string) string 29 | } 30 | 31 | type RenderOp int 32 | 33 | const ( 34 | NoFlatten RenderOp = iota 35 | NoTerminate 36 | ) 37 | 38 | func Render(dialect Dialect, sql SQL, ops ...RenderOp) string { 39 | out := sql.Render() 40 | 41 | flatten := true 42 | terminate := true 43 | for _, op := range ops { 44 | switch op { 45 | case NoFlatten: 46 | flatten = false 47 | case NoTerminate: 48 | terminate = false 49 | } 50 | } 51 | 52 | if flatten { 53 | out = flattenSQL(out) 54 | } 55 | if terminate { 56 | out += ";" 57 | } 58 | 59 | return dialect.Rebind(out) 60 | } 61 | 62 | func flattenSQL(x string) string { 63 | // trim whitespace from beginning and end 64 | s, e := 0, len(x)-1 65 | for s < len(x) && (x[s] == ' ' || x[s] == '\t' || x[s] == '\n') { 66 | s++ 67 | } 68 | for s <= e && (x[e] == ' ' || x[e] == '\t' || x[e] == '\n') { 69 | e-- 70 | } 71 | if s > e { 72 | return "" 73 | } 74 | x = x[s : e+1] 75 | 76 | // check for whitespace that needs fixing 77 | wasSpace := false 78 | for i := 0; i < len(x); i++ { 79 | r := x[i] 80 | justSpace := r == ' ' 81 | if (wasSpace && justSpace) || r == '\t' || r == '\n' { 82 | // whitespace detected, start writing a new string 83 | var result strings.Builder 84 | result.Grow(len(x)) 85 | if wasSpace { 86 | result.WriteString(x[:i-1]) 87 | } else { 88 | result.WriteString(x[:i]) 89 | } 90 | for p := i; p < len(x); p++ { 91 | for p < len(x) && (x[p] == ' ' || x[p] == '\t' || x[p] == '\n') { 92 | p++ 93 | } 94 | result.WriteByte(' ') 95 | 96 | start := p 97 | for p < len(x) && !(x[p] == ' ' || x[p] == '\t' || x[p] == '\n') { 98 | p++ 99 | } 100 | result.WriteString(x[start:p]) 101 | } 102 | 103 | return result.String() 104 | } 105 | wasSpace = justSpace 106 | } 107 | 108 | // no problematic whitespace found 109 | return x 110 | } 111 | -------------------------------------------------------------------------------- /sqlgen/common_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2019 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 sqlgen 16 | 17 | import ( 18 | "testing" 19 | ) 20 | 21 | func TestFlattenSQL(t *testing.T) { 22 | for _, test := range []struct { 23 | in string 24 | exp string 25 | }{ 26 | {"", ""}, 27 | {" ", ""}, 28 | {" x ", "x"}, 29 | {" x\t\t", "x"}, 30 | {" x ", "x"}, 31 | {"\t\tx\t\t", "x"}, 32 | {" \tx \t", "x"}, 33 | {"\t x\t ", "x"}, 34 | {"x\tx", "x x"}, 35 | {" x ", "x"}, 36 | {" \tx x\t ", "x x"}, 37 | {" x x x ", "x x x"}, 38 | {"\t\tx\t\tx\t\tx\t\t", "x x x"}, 39 | {" x \n\t x \n\t x ", "x x x"}, 40 | } { 41 | got := flattenSQL(test.in) 42 | if got != test.exp { 43 | t.Logf(" in: %q", test.in) 44 | t.Logf("got: %q", got) 45 | t.Logf("exp: %q", test.exp) 46 | t.Fail() 47 | } 48 | } 49 | } 50 | 51 | var benchStrings = []string{ 52 | `INSERT INTO example ( alpha, beta, gamma, delta, iota, kappa, lambda ) VALUES ( $1, $2, $3, $4, $5, $6, $7 ) RETURNING example.alpha, example.beta, example.gamma, example.delta, example.iota, example.kappa, example.lambda;`, 53 | `INSERT INTO example 54 | ( alpha, beta, 55 | gamma, delta, iota, 56 | 57 | kappa, lambda ) VALUES ( $1, $2, $3, $4, 58 | $5, $6, $7 ) RETURNING example.alpha, 59 | example.beta, example.gamma, 60 | example.delta, example.iota, example.kappa, 61 | 62 | example.lambda;`, 63 | } 64 | 65 | func BenchmarkFlattenSQL(b *testing.B) { 66 | for i := 0; i < b.N; i++ { 67 | for _, s := range benchStrings { 68 | _ = flattenSQL(s) 69 | } 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /sqlgen/dialects.go: -------------------------------------------------------------------------------- 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 sqlgen 16 | 17 | import "strconv" 18 | 19 | // this type is specially named to match up with the name returned by the 20 | // dialect impl in the sql package. 21 | type postgres struct{} 22 | 23 | func (p postgres) Rebind(sql string) string { 24 | out := make([]byte, 0, len(sql)+10) 25 | 26 | j := 1 27 | for i := 0; i < len(sql); i++ { 28 | ch := sql[i] 29 | if ch != '?' { 30 | out = append(out, ch) 31 | continue 32 | } 33 | 34 | out = append(out, '$') 35 | out = append(out, strconv.Itoa(j)...) 36 | j++ 37 | } 38 | 39 | return string(out) 40 | } 41 | 42 | // this type is specially named to match up with the name returned by the 43 | // dialect impl in the sql package. 44 | type sqlite3 struct{} 45 | 46 | func (s sqlite3) Rebind(sql string) string { 47 | return sql 48 | } 49 | -------------------------------------------------------------------------------- /sqlgen/dialects_test.go: -------------------------------------------------------------------------------- 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 sqlgen 16 | 17 | import ( 18 | "testing" 19 | 20 | "gopkg.in/spacemonkeygo/dbx.v1/testutil" 21 | ) 22 | 23 | type rebindTestCase struct { 24 | in string 25 | out string 26 | } 27 | 28 | func testDialectsRebind(tw *testutil.T, dialect Dialect, 29 | tests []rebindTestCase) { 30 | 31 | for i, test := range tests { 32 | if got := dialect.Rebind(test.in); got != test.out { 33 | tw.Errorf("%d: %q != %q", i, got, test.out) 34 | } 35 | } 36 | } 37 | 38 | func TestDialects(t *testing.T) { 39 | tw := testutil.Wrap(t) 40 | tw.Parallel() 41 | tw.Runp("postgres", testDialectsPostgres) 42 | tw.Runp("sqlite3", testDialectsSQLite3) 43 | } 44 | 45 | func testDialectsPostgres(tw *testutil.T) { 46 | tw.Runp("rebind", testDialectsPostgresRebind) 47 | } 48 | 49 | func testDialectsPostgresRebind(tw *testutil.T) { 50 | testDialectsRebind(tw, postgres{}, []rebindTestCase{ 51 | {in: "", out: ""}, 52 | {in: "? foo bar ? baz", out: "$1 foo bar $2 baz"}, 53 | {in: "? ? ?", out: "$1 $2 $3"}, 54 | }) 55 | } 56 | 57 | func testDialectsSQLite3(tw *testutil.T) { 58 | tw.Runp("rebind", testDialectsSQLite3Rebind) 59 | } 60 | 61 | func testDialectsSQLite3Rebind(tw *testutil.T) { 62 | testDialectsRebind(tw, sqlite3{}, []rebindTestCase{ 63 | {in: "", out: ""}, 64 | {in: "? foo bar ? baz", out: "? foo bar ? baz"}, 65 | {in: "? ? ?", out: "? ? ?"}, 66 | }) 67 | } 68 | -------------------------------------------------------------------------------- /sqlgen/sqlbundle/gen_bundle.go: -------------------------------------------------------------------------------- 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 | // +build ignore 16 | 17 | package main 18 | 19 | import ( 20 | "bufio" 21 | "bytes" 22 | "fmt" 23 | "io/ioutil" 24 | "os" 25 | "os/exec" 26 | "regexp" 27 | "strings" 28 | ) 29 | 30 | const prefix = "__sqlbundle_" 31 | 32 | const template = `%s 33 | // 34 | // DO NOT EDIT: automatically generated code. 35 | // 36 | 37 | //go:generate go run gen_bundle.go 38 | 39 | package sqlbundle 40 | 41 | const ( 42 | Source = %q 43 | Prefix = %q 44 | ) 45 | ` 46 | 47 | var afterImportRe = regexp.MustCompile(`(?m)^\)$`) 48 | 49 | func main() { 50 | copyright, bundle := loadCopyright(), loadBundle() 51 | output := []byte(fmt.Sprintf(template, copyright, bundle, prefix)) 52 | 53 | err := ioutil.WriteFile("bundle.go", output, 0644) 54 | if err != nil { 55 | panic(err) 56 | } 57 | } 58 | 59 | func loadCopyright() string { 60 | fh, err := os.Open("gen_bundle.go") 61 | if err != nil { 62 | panic(err) 63 | } 64 | defer fh.Close() 65 | 66 | var buf bytes.Buffer 67 | scanner := bufio.NewScanner(fh) 68 | 69 | for scanner.Scan() { 70 | text := scanner.Text() 71 | if !strings.HasPrefix(text, "//") { 72 | return buf.String() 73 | } 74 | buf.WriteString(text) 75 | buf.WriteString("\n") 76 | } 77 | 78 | if err := scanner.Err(); err != nil { 79 | panic(err) 80 | } 81 | panic("unreachable") 82 | } 83 | 84 | func loadBundle() string { 85 | source, err := exec.Command("bundle", 86 | "-dst", "gopkg.in/spacemonkeygo/dbx.v1/sqlgen/sqlbundle", 87 | "-prefix", prefix, 88 | "gopkg.in/spacemonkeygo/dbx.v1/sqlgen").Output() 89 | if err != nil { 90 | fmt.Fprintln(os.Stdout, `ensure "golang.org/x/tools/cmd/bundle" is installed`) 91 | panic(err) 92 | } 93 | 94 | index := afterImportRe.FindIndex(source) 95 | if index == nil { 96 | panic("unable to find package clause") 97 | } 98 | 99 | return string(bytes.TrimSpace(source[index[1]:])) 100 | } 101 | -------------------------------------------------------------------------------- /sqlgen/sqlcompile/compile_test.go: -------------------------------------------------------------------------------- 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 sqlcompile 16 | 17 | import ( 18 | "testing" 19 | 20 | "gopkg.in/spacemonkeygo/dbx.v1/sqlgen/sqltest" 21 | "gopkg.in/spacemonkeygo/dbx.v1/testutil" 22 | ) 23 | 24 | func TestCompile(t *testing.T) { 25 | tw := testutil.Wrap(t) 26 | tw.Parallel() 27 | tw.Runp("fuzz render", testCompileFuzzRender) 28 | tw.Runp("idempotent", testCompileIdempotent) 29 | tw.Runp("fuzz normal form", testCompileFuzzNormalForm) 30 | } 31 | 32 | func testCompileFuzzRender(tw *testutil.T) { 33 | g := sqltest.NewGenerator(tw) 34 | 35 | for i := 0; i < 1000; i++ { 36 | sql := g.Gen() 37 | compiled := Compile(sql) 38 | exp := sql.Render() 39 | got := compiled.Render() 40 | 41 | if exp != got { 42 | tw.Logf("sql: %#v", sql) 43 | tw.Logf("compiled: %#v", compiled) 44 | tw.Logf("exp: %q", exp) 45 | tw.Logf("got: %q", got) 46 | tw.Error() 47 | } 48 | } 49 | } 50 | 51 | func testCompileIdempotent(tw *testutil.T) { 52 | g := sqltest.NewGenerator(tw) 53 | 54 | for i := 0; i < 1000; i++ { 55 | sql := g.Gen() 56 | first := Compile(sql) 57 | second := Compile(first) 58 | 59 | if !sqlEqual(first, second) { 60 | tw.Logf("sql: %#v", sql) 61 | tw.Logf("first: %#v", first) 62 | tw.Logf("second: %#v", second) 63 | tw.Error() 64 | } 65 | } 66 | } 67 | 68 | func testCompileFuzzNormalForm(tw *testutil.T) { 69 | g := sqltest.NewGenerator(tw) 70 | 71 | for i := 0; i < 1000; i++ { 72 | sql := g.Gen() 73 | compiled := Compile(sql) 74 | 75 | if !sqlNormalForm(compiled) { 76 | tw.Logf("sql: %#v", sql) 77 | tw.Logf("compiled: %#v", compiled) 78 | tw.Error() 79 | } 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /sqlgen/sqlcompile/equal.go: -------------------------------------------------------------------------------- 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 sqlcompile 16 | 17 | import "gopkg.in/spacemonkeygo/dbx.v1/sqlgen" 18 | 19 | func sqlEqual(a, b sqlgen.SQL) bool { 20 | switch a := a.(type) { 21 | case sqlgen.Literal: 22 | if b, ok := b.(sqlgen.Literal); ok { 23 | return a == b 24 | } 25 | return false 26 | 27 | case sqlgen.Literals: 28 | if b, ok := b.(sqlgen.Literals); ok { 29 | return a.Join == b.Join && sqlsEqual(a.SQLs, b.SQLs) 30 | } 31 | return false 32 | 33 | case *sqlgen.Condition: 34 | if b, ok := b.(*sqlgen.Condition); ok { 35 | return a == b // pointer equality is correct 36 | } 37 | return false 38 | 39 | case *sqlgen.Hole: 40 | if b, ok := b.(*sqlgen.Hole); ok { 41 | return a == b // pointer equality is correct 42 | } 43 | return false 44 | 45 | default: 46 | panic("unhandled sql type") 47 | } 48 | } 49 | 50 | func sqlsEqual(as, bs []sqlgen.SQL) bool { 51 | if len(as) != len(bs) { 52 | return false 53 | } 54 | for i := range as { 55 | if !sqlEqual(as[i], bs[i]) { 56 | return false 57 | } 58 | } 59 | return true 60 | } 61 | 62 | func sqlNormalForm(sql sqlgen.SQL) bool { 63 | switch sql := sql.(type) { 64 | case sqlgen.Literal, *sqlgen.Condition, *sqlgen.Hole: 65 | return true 66 | 67 | case sqlgen.Literals: 68 | if sql.Join != "" { 69 | return false 70 | } 71 | 72 | // only allow Hole, Condition and Literal but disallow two Literal in 73 | // a row. 74 | 75 | last := "" 76 | 77 | for _, sql := range sql.SQLs { 78 | switch sql.(type) { 79 | case *sqlgen.Condition: 80 | last = "condition" 81 | 82 | case *sqlgen.Hole: 83 | last = "hole" 84 | 85 | case sqlgen.Literal: 86 | if last == "literal" { 87 | return false 88 | } 89 | last = "literal" 90 | 91 | case sqlgen.Literals: 92 | return false 93 | 94 | default: 95 | panic("unhandled sql type") 96 | } 97 | } 98 | 99 | return true 100 | 101 | default: 102 | panic("unhandled sql type") 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /sqlgen/sqlcompile/equal_test.go: -------------------------------------------------------------------------------- 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 sqlcompile 16 | 17 | import ( 18 | "testing" 19 | 20 | "gopkg.in/spacemonkeygo/dbx.v1/sqlgen" 21 | "gopkg.in/spacemonkeygo/dbx.v1/sqlgen/sqltest" 22 | "gopkg.in/spacemonkeygo/dbx.v1/testutil" 23 | ) 24 | 25 | func TestEqual(t *testing.T) { 26 | tw := testutil.Wrap(t) 27 | tw.Parallel() 28 | tw.Runp("fuzz identity", testEqualFuzzIdentity) 29 | tw.Runp("normal form", testEqualNormalForm) 30 | } 31 | 32 | func testEqualFuzzIdentity(tw *testutil.T) { 33 | g := sqltest.NewGenerator(tw) 34 | 35 | for i := 0; i < 1000; i++ { 36 | sql := g.Gen() 37 | 38 | if !sqlEqual(sql, sql) { 39 | tw.Logf("sql: %#v", sql) 40 | tw.Error() 41 | } 42 | } 43 | } 44 | 45 | func testEqualNormalForm(tw *testutil.T) { 46 | type normalFormTestCase struct { 47 | in sqlgen.SQL 48 | normal bool 49 | } 50 | 51 | tests := []normalFormTestCase{ 52 | {in: sqlgen.Literal(""), normal: true}, 53 | {in: new(sqlgen.Condition), normal: true}, 54 | {in: sqlgen.Literals{}, normal: true}, 55 | {in: sqlgen.Literals{Join: "foo"}, normal: false}, 56 | { 57 | in: sqlgen.Literals{Join: "", SQLs: []sqlgen.SQL{ 58 | sqlgen.Literal("foo baz"), 59 | }}, 60 | normal: true, 61 | }, 62 | { 63 | in: sqlgen.Literals{Join: "", SQLs: []sqlgen.SQL{ 64 | sqlgen.Literal("foo baz"), 65 | sqlgen.Literal("bif bar"), 66 | }}, 67 | normal: false, 68 | }, 69 | { 70 | in: sqlgen.Literals{Join: "", SQLs: []sqlgen.SQL{ 71 | new(sqlgen.Condition), 72 | sqlgen.Literal("foo baz"), 73 | }}, 74 | normal: true, 75 | }, 76 | { 77 | in: sqlgen.Literals{Join: "", SQLs: []sqlgen.SQL{ 78 | sqlgen.Literal("bif bar"), 79 | new(sqlgen.Condition), 80 | }}, 81 | normal: true, 82 | }, 83 | { 84 | in: sqlgen.Literals{Join: "", SQLs: []sqlgen.SQL{ 85 | sqlgen.Literal("foo baz"), 86 | new(sqlgen.Condition), 87 | sqlgen.Literal("bif bar"), 88 | }}, 89 | normal: true, 90 | }, 91 | { 92 | in: sqlgen.Literals{Join: "", SQLs: []sqlgen.SQL{ 93 | new(sqlgen.Condition), 94 | new(sqlgen.Condition), 95 | sqlgen.Literal("foo baz"), 96 | }}, 97 | normal: true, 98 | }, 99 | { 100 | in: sqlgen.Literals{Join: "", SQLs: []sqlgen.SQL{ 101 | sqlgen.Literal("bif bar"), 102 | new(sqlgen.Condition), 103 | new(sqlgen.Condition), 104 | }}, 105 | normal: true, 106 | }, 107 | { 108 | in: sqlgen.Literals{Join: "", SQLs: []sqlgen.SQL{ 109 | sqlgen.Literal("foo baz"), 110 | new(sqlgen.Condition), 111 | new(sqlgen.Condition), 112 | sqlgen.Literal("bif bar"), 113 | }}, 114 | normal: true, 115 | }, 116 | } 117 | for i, test := range tests { 118 | if got := sqlNormalForm(test.in); got != test.normal { 119 | tw.Errorf("%d: got:%v != exp:%v. sql:%#v", 120 | i, got, test.normal, test.in) 121 | } 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /sqlgen/sqlembedgo/embed.go: -------------------------------------------------------------------------------- 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 sqlembedgo 16 | 17 | import ( 18 | "bytes" 19 | "fmt" 20 | 21 | "gopkg.in/spacemonkeygo/dbx.v1/sqlgen" 22 | "gopkg.in/spacemonkeygo/dbx.v1/sqlgen/sqlbundle" 23 | ) 24 | 25 | type Condition struct { 26 | Name string 27 | Expression string 28 | } 29 | 30 | type Hole struct { 31 | Name string 32 | Expression string 33 | } 34 | 35 | type Info struct { 36 | Expression string 37 | Conditions []Condition 38 | Holes []Hole 39 | } 40 | 41 | func Embed(prefix string, sql sqlgen.SQL) Info { 42 | switch sql := sql.(type) { 43 | case sqlgen.Literal: 44 | return Info{ 45 | Expression: golangLiteral(sql), 46 | Conditions: nil, 47 | } 48 | 49 | case sqlgen.Literals: 50 | return golangLiterals(prefix, sql) 51 | 52 | case *sqlgen.Condition: 53 | cond := golangCondition(prefix, sql) 54 | return Info{ 55 | Expression: cond.Name, 56 | Conditions: []Condition{cond}, 57 | } 58 | 59 | case *sqlgen.Hole: 60 | hole := golangHole(prefix, sql) 61 | return Info{ 62 | Expression: hole.Name, 63 | Holes: []Hole{hole}, 64 | } 65 | 66 | default: 67 | panic("unhandled sql type") 68 | } 69 | } 70 | 71 | func golangLiteral(sql sqlgen.Literal) string { 72 | const format = "%[1]sLiteral(%[2]q)" 73 | 74 | return fmt.Sprintf(format, sqlbundle.Prefix, string(sql)) 75 | } 76 | 77 | func golangLiterals(prefix string, sql sqlgen.Literals) (info Info) { 78 | const format = "%[1]sLiterals{Join:%[2]q,SQLs:[]%[1]sSQL{" 79 | 80 | var conds []Condition 81 | var holes []Hole 82 | var expr bytes.Buffer 83 | fmt.Fprintf(&expr, format, sqlbundle.Prefix, sql.Join) 84 | 85 | first := true 86 | for _, sql := range sql.SQLs { 87 | if !first { 88 | expr.WriteString(",") 89 | } 90 | first = false 91 | 92 | switch sql := sql.(type) { 93 | case sqlgen.Literal: 94 | expr.WriteString(golangLiteral(sql)) 95 | 96 | case *sqlgen.Condition: 97 | cond := golangCondition(prefix, sql) 98 | expr.WriteString(cond.Name) 99 | 100 | // TODO(jeff): dedupe based on name? 101 | conds = append(conds, cond) 102 | 103 | case *sqlgen.Hole: 104 | hole := golangHole(prefix, sql) 105 | expr.WriteString(hole.Name) 106 | 107 | // TODO(jeff): dedupe based on name? 108 | holes = append(holes, hole) 109 | 110 | case sqlgen.Literals: 111 | panic("sql not in normal form") 112 | 113 | default: 114 | panic("unhandled sql type") 115 | } 116 | } 117 | expr.WriteString("}}") 118 | 119 | return Info{ 120 | Expression: expr.String(), 121 | Conditions: conds, 122 | Holes: holes, 123 | } 124 | } 125 | 126 | func golangCondition(prefix string, sql *sqlgen.Condition) Condition { 127 | // start off conditions as null to shrink generated code some. 128 | const format = "&%[1]sCondition{Left:%q, Equal:%t, Right: %q, Null:true}" 129 | 130 | return Condition{ 131 | Name: prefix + sql.Name, 132 | Expression: fmt.Sprintf( 133 | format, sqlbundle.Prefix, sql.Left, sql.Equal, sql.Right), 134 | } 135 | } 136 | 137 | func golangHole(prefix string, sql *sqlgen.Hole) Hole { 138 | const format = "&%[1]sHole{}" 139 | 140 | // TODO(jeff): embed what the hole is filled with? no use case yet. 141 | 142 | return Hole{ 143 | Name: prefix + sql.Name, 144 | Expression: fmt.Sprintf(format, sqlbundle.Prefix), 145 | } 146 | } 147 | -------------------------------------------------------------------------------- /sqlgen/sqlembedgo/embed_test.go: -------------------------------------------------------------------------------- 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 sqlembedgo 16 | 17 | import ( 18 | "go/parser" 19 | "testing" 20 | 21 | "gopkg.in/spacemonkeygo/dbx.v1/sqlgen" 22 | "gopkg.in/spacemonkeygo/dbx.v1/sqlgen/sqlcompile" 23 | "gopkg.in/spacemonkeygo/dbx.v1/sqlgen/sqltest" 24 | "gopkg.in/spacemonkeygo/dbx.v1/testutil" 25 | ) 26 | 27 | func TestGolang(t *testing.T) { 28 | tw := testutil.Wrap(t) 29 | tw.Parallel() 30 | tw.Runp("basic types", testGolangBasicTypes) 31 | tw.Runp("fuzz", testGolangFuzz) 32 | } 33 | 34 | func testGolangBasicTypes(tw *testutil.T) { 35 | tests := []sqlgen.SQL{ 36 | sqlgen.Literal(""), 37 | sqlgen.Literal("foo bar sql"), 38 | sqlgen.Literal("`"), 39 | sqlgen.Literal(`"`), 40 | 41 | &sqlgen.Condition{Name: "foo"}, 42 | 43 | sqlgen.Literals{}, 44 | sqlgen.Literals{Join: "foo"}, 45 | sqlgen.Literals{Join: "`"}, 46 | sqlgen.Literals{Join: `"`}, 47 | sqlgen.Literals{Join: "bar", SQLs: []sqlgen.SQL{ 48 | sqlgen.Literal("foo baz"), 49 | sqlgen.Literal("another"), 50 | &sqlgen.Condition{Name: "foo"}, 51 | }}, 52 | } 53 | for i, test := range tests { 54 | info := Embed("prefix_", test) 55 | if _, err := parser.ParseExpr(info.Expression); err != nil { 56 | tw.Errorf("%d: %+v but got error: %v", i, info, err) 57 | } 58 | } 59 | } 60 | 61 | func testGolangFuzz(tw *testutil.T) { 62 | g := sqltest.NewGenerator(tw) 63 | 64 | for i := 0; i < 1000; i++ { 65 | sql := g.Gen() 66 | compiled := sqlcompile.Compile(sql) 67 | info := Embed("prefix_", compiled) 68 | 69 | if _, err := parser.ParseExpr(info.Expression); err != nil { 70 | tw.Logf("sql: %#v", sql) 71 | tw.Logf("compiled: %#v", compiled) 72 | tw.Logf("info: %+v", info) 73 | tw.Logf("err: %v", err) 74 | tw.Error() 75 | } 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /sqlgen/sqlhelpers/common.go: -------------------------------------------------------------------------------- 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 sqlhelpers 16 | 17 | import ( 18 | "fmt" 19 | 20 | "gopkg.in/spacemonkeygo/dbx.v1/sqlgen" 21 | ) 22 | 23 | // ls is the basic primitive for constructing larger SQLs. The first argument 24 | // may be nil, and the result is a Literals. 25 | func ls(sql sqlgen.SQL, join string, sqls ...sqlgen.SQL) sqlgen.SQL { 26 | var joined []sqlgen.SQL 27 | if sql != nil { 28 | joined = append(joined, sql) 29 | } 30 | joined = append(joined, sqls...) 31 | 32 | return sqlgen.Literals{Join: join, SQLs: joined} 33 | } 34 | 35 | // Placeholder is a placeholder literal 36 | const Placeholder = sqlgen.Literal("?") 37 | 38 | // L constructs a Literal 39 | func L(sql string) sqlgen.SQL { 40 | return sqlgen.Literal(sql) 41 | } 42 | 43 | // Lf constructs a literal from a format string 44 | func Lf(sqlf string, args ...interface{}) sqlgen.SQL { 45 | return sqlgen.Literal(fmt.Sprintf(sqlf, args...)) 46 | } 47 | 48 | // J constructs a SQL by joining the given sqls with the string. 49 | func J(join string, sqls ...sqlgen.SQL) sqlgen.SQL { 50 | return ls(nil, join, sqls...) 51 | } 52 | 53 | // Strings turns a slice of strings into a slice of Literal. 54 | func Strings(parts []string) (out []sqlgen.SQL) { 55 | for _, part := range parts { 56 | out = append(out, sqlgen.Literal(part)) 57 | } 58 | return out 59 | } 60 | 61 | // Placeholders returns a slice of placeholder literals of the right size. 62 | func Placeholders(n int) (out []sqlgen.SQL) { 63 | for i := 0; i < n; i++ { 64 | out = append(out, Placeholder) 65 | } 66 | return out 67 | } 68 | 69 | func Hole(name string) *sqlgen.Hole { 70 | return &sqlgen.Hole{Name: name} 71 | } 72 | 73 | // 74 | // Builder constructs larger SQL statements by joining in pieces with spaces. 75 | // 76 | 77 | type Builder struct { 78 | sql sqlgen.SQL 79 | } 80 | 81 | func Build(sql sqlgen.SQL) *Builder { 82 | return &Builder{ 83 | sql: sql, 84 | } 85 | } 86 | 87 | func (b *Builder) Add(sqls ...sqlgen.SQL) { 88 | b.sql = ls(b.sql, " ", sqls...) 89 | } 90 | 91 | func (b *Builder) SQL() sqlgen.SQL { 92 | return b.sql 93 | } 94 | -------------------------------------------------------------------------------- /sqlgen/sqltest/gen.go: -------------------------------------------------------------------------------- 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 sqltest 16 | 17 | import ( 18 | "fmt" 19 | "math/rand" 20 | "time" 21 | 22 | "gopkg.in/spacemonkeygo/dbx.v1/sqlgen" 23 | "gopkg.in/spacemonkeygo/dbx.v1/testutil" 24 | ) 25 | 26 | type Generator struct { 27 | rng *rand.Rand 28 | conds []*sqlgen.Condition 29 | holes []*sqlgen.Hole 30 | } 31 | 32 | func NewGenerator(tw *testutil.T) *Generator { 33 | seed := time.Now().UnixNano() 34 | rng := rand.New(rand.NewSource(seed)) 35 | tw.Logf("seed: %d", seed) 36 | return &Generator{ 37 | rng: rng, 38 | } 39 | } 40 | 41 | func (g *Generator) Gen() (out sqlgen.SQL) { return g.genRecursive(3) } 42 | 43 | func (g *Generator) literal() sqlgen.Literal { 44 | return sqlgen.Literal(fmt.Sprintf("(literal %d)", g.rng.Intn(1000))) 45 | } 46 | 47 | func (g *Generator) condition() *sqlgen.Condition { 48 | if len(g.conds) == 0 || rand.Intn(2) == 0 { 49 | num := len(g.conds) 50 | condition := &sqlgen.Condition{ 51 | Name: fmt.Sprintf("cond%d", num), 52 | Left: fmt.Sprintf("left%d", num), 53 | Right: fmt.Sprintf("right%d", num), 54 | Null: rand.Intn(2) == 0, 55 | Equal: rand.Intn(2) == 0, 56 | } 57 | g.conds = append(g.conds, condition) 58 | return condition 59 | } 60 | return g.conds[rand.Intn(len(g.conds))] 61 | } 62 | 63 | func (g *Generator) hole() *sqlgen.Hole { 64 | if len(g.holes) == 0 || rand.Intn(2) == 0 { 65 | num := len(g.holes) 66 | hole := &sqlgen.Hole{ 67 | Name: fmt.Sprintf("hole%d", num), 68 | SQL: g.literal(), 69 | } 70 | g.holes = append(g.holes, hole) 71 | return hole 72 | } 73 | return g.holes[rand.Intn(len(g.holes))] 74 | } 75 | 76 | func (g *Generator) literals(depth int) sqlgen.Literals { 77 | amount := rand.Intn(30) 78 | 79 | sqls := make([]sqlgen.SQL, amount) 80 | for i := range sqls { 81 | sqls[i] = g.genRecursive(depth - 1) 82 | } 83 | 84 | join := fmt.Sprintf("|join %d|", g.rng.Intn(1000)) 85 | if rand.Intn(2) == 0 { 86 | join = "" 87 | } 88 | 89 | return sqlgen.Literals{ 90 | Join: join, 91 | SQLs: sqls, 92 | } 93 | } 94 | 95 | func (g *Generator) genRecursive(depth int) (out sqlgen.SQL) { 96 | if depth == 0 { 97 | return g.literal() 98 | } 99 | 100 | switch g.rng.Intn(10) { 101 | case 0, 1: 102 | return g.literal() 103 | case 2, 3: 104 | return g.condition() 105 | case 4, 5: 106 | return g.hole() 107 | default: 108 | return g.literals(depth) 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /sqlgen/types.go: -------------------------------------------------------------------------------- 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 sqlgen 16 | 17 | import "bytes" 18 | 19 | type Literal string 20 | 21 | func (Literal) private() {} 22 | 23 | func (l Literal) Render() string { return string(l) } 24 | 25 | type Literals struct { 26 | Join string 27 | SQLs []SQL 28 | } 29 | 30 | func (Literals) private() {} 31 | 32 | func (l Literals) Render() string { 33 | var out bytes.Buffer 34 | 35 | first := true 36 | for _, sql := range l.SQLs { 37 | if sql == nil { 38 | continue 39 | } 40 | if !first { 41 | out.WriteString(l.Join) 42 | } 43 | first = false 44 | out.WriteString(sql.Render()) 45 | } 46 | 47 | return out.String() 48 | } 49 | 50 | type Condition struct { 51 | // set at compile/embed time 52 | Name string 53 | Left string 54 | Equal bool 55 | Right string 56 | 57 | // set at runtime 58 | Null bool 59 | } 60 | 61 | func (*Condition) private() {} 62 | 63 | func (c *Condition) Render() string { 64 | // TODO(jeff): maybe check if we can use placeholders instead of the 65 | // literal null: this would make the templates easier. 66 | 67 | switch { 68 | case c.Equal && c.Null: 69 | return c.Left + " is null" 70 | case c.Equal && !c.Null: 71 | return c.Left + " = " + c.Right 72 | case !c.Equal && c.Null: 73 | return c.Left + " is not null" 74 | case !c.Equal && !c.Null: 75 | return c.Left + " != " + c.Right 76 | default: 77 | panic("unhandled case") 78 | } 79 | } 80 | 81 | type Hole struct { 82 | // set at compiile/embed time 83 | Name string 84 | 85 | // set at runtime 86 | SQL SQL 87 | } 88 | 89 | func (*Hole) private() {} 90 | 91 | func (h *Hole) Render() string { return h.SQL.Render() } 92 | -------------------------------------------------------------------------------- /sqlgen/types_test.go: -------------------------------------------------------------------------------- 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 sqlgen 16 | 17 | import ( 18 | "testing" 19 | 20 | "gopkg.in/spacemonkeygo/dbx.v1/testutil" 21 | ) 22 | 23 | func TestTypes(t *testing.T) { 24 | tw := testutil.Wrap(t) 25 | tw.Parallel() 26 | tw.Runp("render", testTypesRender) 27 | } 28 | 29 | func testTypesRender(tw *testutil.T) { 30 | type renderTestCase struct { 31 | in SQL 32 | out string 33 | } 34 | 35 | tests := []renderTestCase{ 36 | {in: Literal(""), out: ""}, 37 | {in: Literal("foo bar sql"), out: "foo bar sql"}, 38 | {in: Literal("`"), out: "`"}, 39 | {in: Literal(`"`), out: "\""}, 40 | 41 | {in: Literals{}, out: ""}, 42 | {in: Literals{Join: "foo"}, out: ""}, 43 | {in: Literals{Join: "`"}, out: ""}, 44 | {in: Literals{Join: `"`}, out: ""}, 45 | { 46 | in: Literals{Join: " bar ", SQLs: []SQL{ 47 | Literal("foo baz"), 48 | Literal("another"), 49 | }}, 50 | out: "foo baz bar another", 51 | }, 52 | { 53 | in: Literals{Join: " bar ", SQLs: []SQL{ 54 | Literal("inside first"), 55 | Literals{}, 56 | Literal("inside second"), 57 | }}, 58 | out: "inside first bar bar inside second", 59 | }, 60 | { 61 | in: Literals{Join: " recursive ", SQLs: []SQL{ 62 | Literals{Join: " bif ", SQLs: []SQL{ 63 | Literals{}, 64 | Literal("inside"), 65 | }}, 66 | Literal("outside"), 67 | }}, 68 | out: " bif inside recursive outside", 69 | }, 70 | 71 | {in: &Condition{Equal: false, Null: false}, out: " != "}, 72 | {in: &Condition{Equal: false, Null: true}, out: " is not null"}, 73 | {in: &Condition{Equal: true, Null: false}, out: " = "}, 74 | {in: &Condition{Equal: true, Null: true}, out: " is null"}, 75 | { 76 | in: &Condition{Left: "f", Right: "?", Equal: false, Null: false}, 77 | out: "f != ?", 78 | }, 79 | { 80 | in: &Condition{Left: "f", Right: "?", Equal: false, Null: true}, 81 | out: "f is not null", 82 | }, 83 | { 84 | in: &Condition{Left: "f", Right: "?", Equal: true, Null: false}, 85 | out: "f = ?", 86 | }, 87 | { 88 | in: &Condition{Left: "f", Right: "?", Equal: true, Null: true}, 89 | out: "f is null", 90 | }, 91 | 92 | {in: &Hole{SQL: Literal("hello")}, out: "hello"}, 93 | } 94 | for i, test := range tests { 95 | if got := test.in.Render(); got != test.out { 96 | tw.Errorf("%d: %q != %q", i, got, test.out) 97 | } 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /syntax/errors.go: -------------------------------------------------------------------------------- 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 syntax 16 | 17 | import ( 18 | "text/scanner" 19 | 20 | "gopkg.in/spacemonkeygo/dbx.v1/errutil" 21 | ) 22 | 23 | func expectedKeyword(pos scanner.Position, actual string, expected ...string) ( 24 | err error) { 25 | 26 | if len(expected) == 1 { 27 | return errutil.New(pos, "expected %q, got %q", expected[0], actual) 28 | } else { 29 | return errutil.New(pos, "expected one of %q, got %q", expected, actual) 30 | } 31 | } 32 | 33 | func expectedToken(pos scanner.Position, actual Token, expected ...Token) ( 34 | err error) { 35 | 36 | if len(expected) == 1 { 37 | return errutil.New(pos, "expected %q; got %q", expected[0], actual) 38 | } else { 39 | return errutil.New(pos, "expected one of %v; got %q", expected, actual) 40 | } 41 | } 42 | 43 | func previouslyDefined(pos scanner.Position, kind, field string, 44 | where scanner.Position) error { 45 | 46 | return errutil.New(pos, 47 | "%s already defined on %s. previous definition at %s", 48 | field, kind, where) 49 | } 50 | -------------------------------------------------------------------------------- /syntax/fuzz.go: -------------------------------------------------------------------------------- 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 | // +build gofuzz 16 | 17 | package syntax 18 | 19 | func Fuzz(data []byte) int { 20 | if _, err := Parse("", data); err != nil { 21 | return 0 22 | } 23 | return 1 24 | } 25 | -------------------------------------------------------------------------------- /syntax/parse.go: -------------------------------------------------------------------------------- 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 syntax 16 | 17 | import "gopkg.in/spacemonkeygo/dbx.v1/ast" 18 | 19 | func Parse(path string, data []byte) (root *ast.Root, err error) { 20 | scanner, err := NewScanner(path, data) 21 | if err != nil { 22 | return nil, err 23 | } 24 | 25 | return parseRoot(scanner) 26 | } 27 | 28 | func parseRoot(scanner *Scanner) (*ast.Root, error) { 29 | list, err := scanRoot(scanner) 30 | if err != nil { 31 | return nil, err 32 | } 33 | 34 | root := new(ast.Root) 35 | 36 | err = list.consumeAnyTuples(tupleCases{ 37 | "model": func(node *tupleNode) error { 38 | model, err := parseModel(node) 39 | if err != nil { 40 | return err 41 | } 42 | root.Models = append(root.Models, model) 43 | 44 | return nil 45 | }, 46 | "create": func(node *tupleNode) error { 47 | cre, err := parseCreate(node) 48 | if err != nil { 49 | return err 50 | } 51 | root.Creates = append(root.Creates, cre) 52 | 53 | return nil 54 | }, 55 | "read": func(node *tupleNode) error { 56 | read, err := parseRead(node) 57 | if err != nil { 58 | return err 59 | } 60 | root.Reads = append(root.Reads, read) 61 | 62 | return nil 63 | }, 64 | "update": func(node *tupleNode) error { 65 | upd, err := parseUpdate(node) 66 | if err != nil { 67 | return err 68 | } 69 | root.Updates = append(root.Updates, upd) 70 | 71 | return nil 72 | }, 73 | "delete": func(node *tupleNode) error { 74 | del, err := parseDelete(node) 75 | if err != nil { 76 | return err 77 | } 78 | root.Deletes = append(root.Deletes, del) 79 | 80 | return nil 81 | }, 82 | }) 83 | if err != nil { 84 | return nil, err 85 | } 86 | 87 | return root, nil 88 | } 89 | -------------------------------------------------------------------------------- /syntax/parse_create.go: -------------------------------------------------------------------------------- 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 syntax 16 | 17 | import "gopkg.in/spacemonkeygo/dbx.v1/ast" 18 | 19 | func parseCreate(node *tupleNode) (*ast.Create, error) { 20 | cre := new(ast.Create) 21 | cre.Pos = node.getPos() 22 | 23 | model_ref_token, err := node.consumeToken(Ident) 24 | if err != nil { 25 | return nil, err 26 | } 27 | cre.Model = modelRefFromToken(model_ref_token) 28 | 29 | list_token, err := node.consumeList() 30 | if err != nil { 31 | return nil, err 32 | } 33 | 34 | err = list_token.consumeAnyTuples(tupleCases{ 35 | "raw": tupleFlagField("create", "raw", &cre.Raw), 36 | "noreturn": tupleFlagField("create", "noreturn", &cre.NoReturn), 37 | "suffix": func(node *tupleNode) error { 38 | if cre.Suffix != nil { 39 | return previouslyDefined(node.getPos(), "create", "suffix", 40 | cre.Suffix.Pos) 41 | } 42 | 43 | suffix, err := parseSuffix(node) 44 | if err != nil { 45 | return err 46 | } 47 | cre.Suffix = suffix 48 | 49 | return nil 50 | }, 51 | }) 52 | if err != nil { 53 | return nil, err 54 | } 55 | 56 | return cre, nil 57 | } 58 | -------------------------------------------------------------------------------- /syntax/parse_delete.go: -------------------------------------------------------------------------------- 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 syntax 16 | 17 | import "gopkg.in/spacemonkeygo/dbx.v1/ast" 18 | 19 | func parseDelete(node *tupleNode) (*ast.Delete, error) { 20 | del := new(ast.Delete) 21 | del.Pos = node.getPos() 22 | 23 | model_ref_token, err := node.consumeToken(Ident) 24 | if err != nil { 25 | return nil, err 26 | } 27 | del.Model = modelRefFromToken(model_ref_token) 28 | 29 | list_token, err := node.consumeList() 30 | if err != nil { 31 | return nil, err 32 | } 33 | 34 | err = list_token.consumeAnyTuples(tupleCases{ 35 | "where": func(node *tupleNode) error { 36 | where, err := parseWhere(node) 37 | if err != nil { 38 | return err 39 | } 40 | del.Where = append(del.Where, where) 41 | 42 | return nil 43 | }, 44 | "join": func(node *tupleNode) error { 45 | join, err := parseJoin(node) 46 | if err != nil { 47 | return err 48 | } 49 | del.Joins = append(del.Joins, join) 50 | 51 | return nil 52 | }, 53 | "suffix": func(node *tupleNode) error { 54 | if del.Suffix != nil { 55 | return previouslyDefined(node.getPos(), "delete", "suffix", 56 | del.Suffix.Pos) 57 | } 58 | 59 | suffix, err := parseSuffix(node) 60 | if err != nil { 61 | return err 62 | } 63 | del.Suffix = suffix 64 | 65 | return nil 66 | }, 67 | }) 68 | if err != nil { 69 | return nil, err 70 | } 71 | 72 | return del, nil 73 | } 74 | -------------------------------------------------------------------------------- /syntax/parse_expr.go: -------------------------------------------------------------------------------- 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 syntax 16 | 17 | import ( 18 | "strconv" 19 | 20 | "gopkg.in/spacemonkeygo/dbx.v1/ast" 21 | "gopkg.in/spacemonkeygo/dbx.v1/errutil" 22 | ) 23 | 24 | func parseExpr(node *tupleNode) (*ast.Expr, error) { 25 | // expressions are one of the following: 26 | // placeholder : ? 27 | // string literal : "foo" 28 | // int literal : 9 29 | // float literal : 9.9 30 | // dotted field ref : model.field 31 | // function : foo() 32 | 33 | expr := &ast.Expr{ 34 | Pos: node.getPos(), 35 | } 36 | 37 | first, err := node.consumeToken(Question, String, Int, Float, Ident) 38 | if err != nil { 39 | return nil, err 40 | } 41 | 42 | switch first.tok { 43 | case Question: 44 | expr.Placeholder = placeholderFromToken(first) 45 | return expr, nil 46 | case String: 47 | unquoted, err := strconv.Unquote(first.text) 48 | if err != nil { 49 | return nil, errutil.New(first.getPos(), 50 | "(internal) unable to unquote string token text: %s", err) 51 | } 52 | expr.StringLit = stringFromValue(first, unquoted) 53 | return expr, nil 54 | case Int, Float: 55 | expr.NumberLit = stringFromValue(first, first.text) 56 | return expr, nil 57 | } 58 | 59 | first.debugAssertToken(Ident) 60 | 61 | if node.consumeIfToken(Dot) == nil { 62 | switch first.text { 63 | case "null": 64 | expr.Null = nullFromToken(first) 65 | return expr, nil 66 | case "true", "false": 67 | expr.BoolLit = boolFromToken(first) 68 | return expr, nil 69 | } 70 | 71 | list, err := node.consumeList() 72 | if err != nil { 73 | return nil, err 74 | } 75 | exprs, err := parseExprs(list) 76 | if err != nil { 77 | return nil, err 78 | } 79 | expr.FuncCall = funcCallFromTokenAndArgs(first, exprs) 80 | return expr, nil 81 | } 82 | 83 | second, err := node.consumeToken(Ident) 84 | if err != nil { 85 | return nil, err 86 | } 87 | 88 | expr.FieldRef = fieldRefFromTokens(first, second) 89 | return expr, nil 90 | } 91 | 92 | func parseExprs(list *listNode) (exprs []*ast.Expr, err error) { 93 | 94 | for { 95 | tuple, err := list.consumeTupleOrEmpty() 96 | if err != nil { 97 | return nil, err 98 | } 99 | if tuple == nil { 100 | return exprs, nil 101 | } 102 | expr, err := parseExpr(tuple) 103 | if err != nil { 104 | return nil, err 105 | } 106 | exprs = append(exprs, expr) 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /syntax/parse_field.go: -------------------------------------------------------------------------------- 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 syntax 16 | 17 | import ( 18 | "strconv" 19 | 20 | "gopkg.in/spacemonkeygo/dbx.v1/ast" 21 | "gopkg.in/spacemonkeygo/dbx.v1/errutil" 22 | ) 23 | 24 | func parseField(node *tupleNode) (*ast.Field, error) { 25 | field := new(ast.Field) 26 | field.Pos = node.getPos() 27 | 28 | field_name_token, err := node.consumeToken(Ident) 29 | if err != nil { 30 | return nil, err 31 | } 32 | field.Name = stringFromToken(field_name_token) 33 | 34 | field.Type, field.Relation, err = parseFieldType(node) 35 | if err != nil { 36 | return nil, err 37 | } 38 | 39 | if field.Relation != nil { 40 | err = parseRelation(node, field) 41 | if err != nil { 42 | return nil, err 43 | } 44 | return field, nil 45 | } 46 | 47 | attributes_list := node.consumeIfList() 48 | if attributes_list != nil { 49 | err := attributes_list.consumeAnyTuples(tupleCases{ 50 | "column": func(node *tupleNode) error { 51 | if field.Column != nil { 52 | return previouslyDefined(node.getPos(), "field", "column", 53 | field.Column.Pos) 54 | } 55 | 56 | name_token, err := node.consumeToken(Ident) 57 | if err != nil { 58 | return err 59 | } 60 | field.Column = stringFromToken(name_token) 61 | 62 | return nil 63 | }, 64 | "nullable": tupleFlagField("field", "nullable", 65 | &field.Nullable), 66 | "autoinsert": tupleFlagField("field", "autoinsert", 67 | &field.AutoInsert), 68 | "autoupdate": tupleFlagField("field", "autoupdate", 69 | &field.AutoUpdate), 70 | "updatable": tupleFlagField("field", "updatable", 71 | &field.Updatable), 72 | "length": func(node *tupleNode) error { 73 | if field.Length != nil { 74 | return previouslyDefined(node.getPos(), "field", "length", 75 | field.Length.Pos) 76 | } 77 | 78 | length_token, err := node.consumeToken(Int) 79 | if err != nil { 80 | return err 81 | } 82 | value, err := strconv.Atoi(length_token.text) 83 | if err != nil { 84 | return errutil.New(length_token.getPos(), 85 | "unable to parse integer %q: %v", 86 | length_token.text, err) 87 | } 88 | field.Length = intFromValue(length_token, value) 89 | 90 | return nil 91 | }, 92 | 93 | // TODO(jeff): do something with these values instead of allowing 94 | // anything. 95 | "default": debugConsume, 96 | "sqldefault": debugConsume, 97 | }) 98 | if err != nil { 99 | return nil, err 100 | } 101 | } 102 | 103 | if node.assertEmpty() != nil { 104 | invalid, _ := node.consume() 105 | return nil, errutil.New(invalid.getPos(), 106 | "expected list or end of tuple. got %s", invalid) 107 | } 108 | 109 | return field, nil 110 | } 111 | 112 | func parseFieldType(node *tupleNode) (*ast.FieldType, *ast.FieldRef, error) { 113 | first, second, err := node.consumeDottedIdents() 114 | if err != nil { 115 | return nil, nil, err 116 | } 117 | 118 | if second == nil { 119 | first.debugAssertToken(Ident) 120 | field_type, ok := fieldTypeMapping[first.text] 121 | if !ok { 122 | return nil, nil, expectedKeyword( 123 | first.getPos(), first.text, validFieldTypes()...) 124 | } 125 | return fieldTypeFromValue(first, field_type), nil, nil 126 | } 127 | 128 | return nil, &ast.FieldRef{ 129 | Pos: node.getPos(), 130 | Model: stringFromToken(first), 131 | Field: stringFromToken(second), 132 | }, nil 133 | } 134 | -------------------------------------------------------------------------------- /syntax/parse_field_ref.go: -------------------------------------------------------------------------------- 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 syntax 16 | 17 | import ( 18 | "gopkg.in/spacemonkeygo/dbx.v1/ast" 19 | "gopkg.in/spacemonkeygo/dbx.v1/errutil" 20 | ) 21 | 22 | func parseFieldRefs(node *tupleNode, needs_dot bool) (*ast.FieldRefs, error) { 23 | refs := new(ast.FieldRefs) 24 | refs.Pos = node.getPos() 25 | 26 | for { 27 | ref, err := parseFieldRefOrEmpty(node, needs_dot) 28 | if err != nil { 29 | return nil, err 30 | } 31 | if ref == nil { 32 | return refs, nil 33 | } 34 | refs.Refs = append(refs.Refs, ref) 35 | } 36 | } 37 | 38 | func parseFieldRefOrEmpty(node *tupleNode, needs_dot bool) ( 39 | *ast.FieldRef, error) { 40 | 41 | first, second, err := node.consumeDottedIdentsOrEmpty() 42 | if err != nil { 43 | return nil, err 44 | } 45 | if first == nil { 46 | return nil, nil 47 | } 48 | if second == nil && needs_dot { 49 | return nil, errutil.New(first.getPos(), 50 | "field ref must specify a model") 51 | } 52 | return fieldRefFromTokens(first, second), nil 53 | } 54 | 55 | func parseFieldRef(node *tupleNode, needs_dot bool) (*ast.FieldRef, error) { 56 | first, second, err := node.consumeDottedIdents() 57 | if err != nil { 58 | return nil, err 59 | } 60 | if second == nil && needs_dot { 61 | return nil, errutil.New(first.getPos(), 62 | "field ref must specify a model") 63 | } 64 | return fieldRefFromTokens(first, second), nil 65 | } 66 | 67 | func parseRelativeFieldRefs(node *tupleNode) (*ast.RelativeFieldRefs, error) { 68 | refs := new(ast.RelativeFieldRefs) 69 | refs.Pos = node.getPos() 70 | 71 | for { 72 | ref_token, err := node.consumeTokenOrEmpty(Ident) 73 | if err != nil { 74 | return nil, err 75 | } 76 | if ref_token == nil { 77 | if len(refs.Refs) == 0 { 78 | return nil, errutil.New(node.getPos(), 79 | "must specify some field references") 80 | } 81 | return refs, nil 82 | } 83 | refs.Refs = append(refs.Refs, relativeFieldRefFromToken(ref_token)) 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /syntax/parse_groupby.go: -------------------------------------------------------------------------------- 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 syntax 16 | 17 | import "gopkg.in/spacemonkeygo/dbx.v1/ast" 18 | 19 | func parseGroupBy(node *tupleNode) (*ast.GroupBy, error) { 20 | group_by := new(ast.GroupBy) 21 | group_by.Pos = node.getPos() 22 | 23 | field_refs, err := parseFieldRefs(node, true) 24 | if err != nil { 25 | return nil, err 26 | } 27 | group_by.Fields = field_refs 28 | 29 | return group_by, nil 30 | } 31 | -------------------------------------------------------------------------------- /syntax/parse_index.go: -------------------------------------------------------------------------------- 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 syntax 16 | 17 | import "gopkg.in/spacemonkeygo/dbx.v1/ast" 18 | 19 | func parseIndex(node *tupleNode) (*ast.Index, error) { 20 | index := new(ast.Index) 21 | index.Pos = node.getPos() 22 | 23 | list_token, err := node.consumeList() 24 | if err != nil { 25 | return nil, err 26 | } 27 | 28 | err = list_token.consumeAnyTuples(tupleCases{ 29 | "name": func(node *tupleNode) error { 30 | if index.Name != nil { 31 | return previouslyDefined(node.getPos(), "index", "name", 32 | index.Name.Pos) 33 | } 34 | 35 | name_token, err := node.consumeToken(Ident) 36 | if err != nil { 37 | return err 38 | } 39 | index.Name = stringFromToken(name_token) 40 | 41 | return nil 42 | }, 43 | "fields": func(node *tupleNode) error { 44 | if index.Fields != nil { 45 | return previouslyDefined(node.getPos(), "index", "fields", 46 | index.Fields.Pos) 47 | } 48 | 49 | fields, err := parseRelativeFieldRefs(node) 50 | if err != nil { 51 | return err 52 | } 53 | index.Fields = fields 54 | 55 | return nil 56 | }, 57 | "unique": tupleFlagField("index", "unique", &index.Unique), 58 | }) 59 | if err != nil { 60 | return nil, err 61 | } 62 | 63 | return index, nil 64 | } 65 | -------------------------------------------------------------------------------- /syntax/parse_join.go: -------------------------------------------------------------------------------- 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 syntax 16 | 17 | import "gopkg.in/spacemonkeygo/dbx.v1/ast" 18 | 19 | func parseJoin(node *tupleNode) (*ast.Join, error) { 20 | join := new(ast.Join) 21 | join.Pos = node.getPos() 22 | 23 | left_field_ref, err := parseFieldRef(node, true) 24 | if err != nil { 25 | return nil, err 26 | } 27 | join.Left = left_field_ref 28 | 29 | _, err = node.consumeToken(Equal) 30 | if err != nil { 31 | return nil, err 32 | } 33 | 34 | right_field_ref, err := parseFieldRef(node, true) 35 | if err != nil { 36 | return nil, err 37 | } 38 | join.Right = right_field_ref 39 | 40 | return join, nil 41 | } 42 | -------------------------------------------------------------------------------- /syntax/parse_model.go: -------------------------------------------------------------------------------- 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 syntax 16 | 17 | import ( 18 | "gopkg.in/spacemonkeygo/dbx.v1/ast" 19 | ) 20 | 21 | func parseModel(node *tupleNode) (*ast.Model, error) { 22 | model := new(ast.Model) 23 | model.Pos = node.getPos() 24 | 25 | name_token, err := node.consumeToken(Ident) 26 | if err != nil { 27 | return nil, err 28 | } 29 | model.Name = stringFromToken(name_token) 30 | 31 | list_token, err := node.consumeList() 32 | if err != nil { 33 | return nil, err 34 | } 35 | 36 | err = list_token.consumeAnyTuples(tupleCases{ 37 | "table": func(node *tupleNode) error { 38 | if model.Table != nil { 39 | return previouslyDefined(node.getPos(), "model", "table", 40 | model.Table.Pos) 41 | } 42 | 43 | name_token, err := node.consumeToken(Ident) 44 | if err != nil { 45 | return err 46 | } 47 | model.Table = stringFromToken(name_token) 48 | 49 | return nil 50 | }, 51 | "field": func(node *tupleNode) error { 52 | field, err := parseField(node) 53 | if err != nil { 54 | return err 55 | } 56 | model.Fields = append(model.Fields, field) 57 | 58 | return nil 59 | }, 60 | "key": func(node *tupleNode) error { 61 | if model.PrimaryKey != nil { 62 | return previouslyDefined(node.getPos(), "model", "key", 63 | model.PrimaryKey.Pos) 64 | } 65 | primary_key, err := parseRelativeFieldRefs(node) 66 | if err != nil { 67 | return err 68 | } 69 | model.PrimaryKey = primary_key 70 | return nil 71 | }, 72 | "unique": func(node *tupleNode) error { 73 | unique, err := parseRelativeFieldRefs(node) 74 | if err != nil { 75 | return err 76 | } 77 | model.Unique = append(model.Unique, unique) 78 | return nil 79 | }, 80 | "index": func(node *tupleNode) error { 81 | index, err := parseIndex(node) 82 | if err != nil { 83 | return err 84 | } 85 | model.Indexes = append(model.Indexes, index) 86 | return nil 87 | }, 88 | }) 89 | if err != nil { 90 | return nil, err 91 | } 92 | 93 | return model, nil 94 | } 95 | -------------------------------------------------------------------------------- /syntax/parse_orderby.go: -------------------------------------------------------------------------------- 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 syntax 16 | 17 | import "gopkg.in/spacemonkeygo/dbx.v1/ast" 18 | 19 | func parseOrderBy(node *tupleNode) (*ast.OrderBy, error) { 20 | order_by := new(ast.OrderBy) 21 | order_by.Pos = node.getPos() 22 | 23 | err := node.consumeTokenNamed(tokenCases{ 24 | {Ident, "asc"}: func(token *tokenNode) error { return nil }, 25 | {Ident, "desc"}: func(token *tokenNode) error { 26 | order_by.Descending = boolFromValue(token, true) 27 | return nil 28 | }, 29 | }) 30 | if err != nil { 31 | return nil, err 32 | } 33 | 34 | field_refs, err := parseFieldRefs(node, true) 35 | if err != nil { 36 | return nil, err 37 | } 38 | order_by.Fields = field_refs 39 | 40 | return order_by, nil 41 | } 42 | -------------------------------------------------------------------------------- /syntax/parse_read.go: -------------------------------------------------------------------------------- 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 syntax 16 | 17 | import "gopkg.in/spacemonkeygo/dbx.v1/ast" 18 | 19 | func parseRead(node *tupleNode) (*ast.Read, error) { 20 | read := new(ast.Read) 21 | read.Pos = node.getPos() 22 | 23 | view, err := parseView(node) 24 | if err != nil { 25 | return nil, err 26 | } 27 | read.View = view 28 | 29 | list_token, err := node.consumeList() 30 | if err != nil { 31 | return nil, err 32 | } 33 | 34 | err = list_token.consumeAnyTuples(tupleCases{ 35 | "select": func(node *tupleNode) error { 36 | if read.Select != nil { 37 | return previouslyDefined(node.getPos(), "read", "select", 38 | read.Select.Pos) 39 | } 40 | 41 | refs, err := parseFieldRefs(node, false) 42 | if err != nil { 43 | return err 44 | } 45 | read.Select = refs 46 | 47 | return nil 48 | }, 49 | "where": func(node *tupleNode) error { 50 | where, err := parseWhere(node) 51 | if err != nil { 52 | return err 53 | } 54 | read.Where = append(read.Where, where) 55 | 56 | return nil 57 | }, 58 | "join": func(node *tupleNode) error { 59 | join, err := parseJoin(node) 60 | if err != nil { 61 | return err 62 | } 63 | read.Joins = append(read.Joins, join) 64 | 65 | return nil 66 | }, 67 | "orderby": func(node *tupleNode) error { 68 | if read.OrderBy != nil { 69 | return previouslyDefined(node.getPos(), "read", "orderby", 70 | read.OrderBy.Pos) 71 | } 72 | 73 | order_by, err := parseOrderBy(node) 74 | if err != nil { 75 | return err 76 | } 77 | read.OrderBy = order_by 78 | 79 | return nil 80 | }, 81 | "groupby": func(node *tupleNode) error { 82 | if read.GroupBy != nil { 83 | return previouslyDefined(node.getPos(), "read", "groupby", 84 | read.GroupBy.Pos) 85 | } 86 | 87 | group_by, err := parseGroupBy(node) 88 | if err != nil { 89 | return err 90 | } 91 | read.GroupBy = group_by 92 | 93 | return nil 94 | }, 95 | "suffix": func(node *tupleNode) error { 96 | if read.Suffix != nil { 97 | return previouslyDefined(node.getPos(), "read", "suffix", 98 | read.Suffix.Pos) 99 | } 100 | 101 | suffix, err := parseSuffix(node) 102 | if err != nil { 103 | return err 104 | } 105 | read.Suffix = suffix 106 | 107 | return nil 108 | }, 109 | }) 110 | if err != nil { 111 | return nil, err 112 | } 113 | 114 | return read, nil 115 | } 116 | -------------------------------------------------------------------------------- /syntax/parse_relation.go: -------------------------------------------------------------------------------- 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 syntax 16 | 17 | import ( 18 | "gopkg.in/spacemonkeygo/dbx.v1/ast" 19 | "gopkg.in/spacemonkeygo/dbx.v1/consts" 20 | ) 21 | 22 | func parseRelation(node *tupleNode, field *ast.Field) error { 23 | err := node.consumeTokenNamed(tokenCases{ 24 | {Ident, "setnull"}: func(token *tokenNode) error { 25 | field.RelationKind = relationKindFromValue(token, consts.SetNull) 26 | return nil 27 | }, 28 | {Ident, "cascade"}: func(token *tokenNode) error { 29 | field.RelationKind = relationKindFromValue(token, consts.Cascade) 30 | return nil 31 | }, 32 | {Ident, "restrict"}: func(token *tokenNode) error { 33 | field.RelationKind = relationKindFromValue(token, consts.Restrict) 34 | return nil 35 | }, 36 | }) 37 | if err != nil { 38 | return err 39 | } 40 | 41 | attributes_list := node.consumeIfList() 42 | if attributes_list != nil { 43 | err := attributes_list.consumeAnyTuples(tupleCases{ 44 | "column": func(node *tupleNode) error { 45 | if field.Column != nil { 46 | return previouslyDefined(node.getPos(), "relation", "column", 47 | field.Column.Pos) 48 | } 49 | 50 | name_token, err := node.consumeToken(Ident) 51 | if err != nil { 52 | return err 53 | } 54 | field.Column = stringFromToken(name_token) 55 | 56 | return nil 57 | }, 58 | "nullable": tupleFlagField("relation", "nullable", 59 | &field.Nullable), 60 | "updatable": tupleFlagField("relation", "updatable", 61 | &field.Updatable), 62 | }) 63 | if err != nil { 64 | return err 65 | } 66 | } 67 | 68 | return nil 69 | } 70 | -------------------------------------------------------------------------------- /syntax/parse_suffix.go: -------------------------------------------------------------------------------- 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 syntax 16 | 17 | import "gopkg.in/spacemonkeygo/dbx.v1/ast" 18 | 19 | func parseSuffix(node *tupleNode) (*ast.Suffix, error) { 20 | suf := new(ast.Suffix) 21 | suf.Pos = node.getPos() 22 | 23 | tokens, err := node.consumeTokens(Ident) 24 | if err != nil { 25 | return nil, err 26 | } 27 | for _, token := range tokens { 28 | suf.Parts = append(suf.Parts, stringFromToken(token)) 29 | } 30 | 31 | return suf, nil 32 | } 33 | -------------------------------------------------------------------------------- /syntax/parse_test.go: -------------------------------------------------------------------------------- 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 syntax 16 | 17 | import ( 18 | "testing" 19 | 20 | "gopkg.in/spacemonkeygo/dbx.v1/testutil" 21 | ) 22 | 23 | func TestEmptyParse(t *testing.T) { 24 | tw := testutil.Wrap(t) 25 | tw.Parallel() 26 | 27 | _, err := Parse("", nil) 28 | tw.AssertNoError(err) 29 | } 30 | -------------------------------------------------------------------------------- /syntax/parse_update.go: -------------------------------------------------------------------------------- 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 syntax 16 | 17 | import "gopkg.in/spacemonkeygo/dbx.v1/ast" 18 | 19 | func parseUpdate(node *tupleNode) (*ast.Update, error) { 20 | upd := new(ast.Update) 21 | upd.Pos = node.getPos() 22 | 23 | model_ref_token, err := node.consumeToken(Ident) 24 | if err != nil { 25 | return nil, err 26 | } 27 | upd.Model = modelRefFromToken(model_ref_token) 28 | 29 | list_token, err := node.consumeList() 30 | if err != nil { 31 | return nil, err 32 | } 33 | 34 | err = list_token.consumeAnyTuples(tupleCases{ 35 | "where": func(node *tupleNode) error { 36 | where, err := parseWhere(node) 37 | if err != nil { 38 | return err 39 | } 40 | upd.Where = append(upd.Where, where) 41 | 42 | return nil 43 | }, 44 | "join": func(node *tupleNode) error { 45 | join, err := parseJoin(node) 46 | if err != nil { 47 | return err 48 | } 49 | upd.Joins = append(upd.Joins, join) 50 | 51 | return nil 52 | }, 53 | "noreturn": tupleFlagField("update", "noreturn", &upd.NoReturn), 54 | "suffix": func(node *tupleNode) error { 55 | if upd.Suffix != nil { 56 | return previouslyDefined(node.getPos(), "update", "suffix", 57 | upd.Suffix.Pos) 58 | } 59 | 60 | suffix, err := parseSuffix(node) 61 | if err != nil { 62 | return err 63 | } 64 | upd.Suffix = suffix 65 | 66 | return nil 67 | }, 68 | }) 69 | if err != nil { 70 | return nil, err 71 | } 72 | 73 | return upd, nil 74 | } 75 | -------------------------------------------------------------------------------- /syntax/parse_view.go: -------------------------------------------------------------------------------- 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 syntax 16 | 17 | import "gopkg.in/spacemonkeygo/dbx.v1/ast" 18 | 19 | func parseView(node *tupleNode) (*ast.View, error) { 20 | view := new(ast.View) 21 | view.Pos = node.getPos() 22 | 23 | err := node.consumeTokensNamedUntilList(tokenCases{ 24 | {Ident, "all"}: tokenFlagField("view", "all", &view.All), 25 | {Ident, "paged"}: tokenFlagField("view", "paged", &view.Paged), 26 | {Ident, "count"}: tokenFlagField("view", "count", &view.Count), 27 | {Ident, "has"}: tokenFlagField("view", "has", &view.Has), 28 | {Ident, "limitoffset"}: tokenFlagField("view", "limitoffset", 29 | &view.LimitOffset), 30 | {Ident, "scalar"}: tokenFlagField("view", "scalar", &view.Scalar), 31 | {Ident, "one"}: tokenFlagField("view", "one", &view.One), 32 | {Ident, "first"}: tokenFlagField("view", "first", &view.First), 33 | }) 34 | if err != nil { 35 | return nil, err 36 | } 37 | 38 | return view, nil 39 | } 40 | -------------------------------------------------------------------------------- /syntax/parse_where.go: -------------------------------------------------------------------------------- 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 syntax 16 | 17 | import ( 18 | "gopkg.in/spacemonkeygo/dbx.v1/ast" 19 | "gopkg.in/spacemonkeygo/dbx.v1/consts" 20 | ) 21 | 22 | func parseWhere(node *tupleNode) (where *ast.Where, err error) { 23 | where = new(ast.Where) 24 | where.Pos = node.getPos() 25 | 26 | where.Left, err = parseExpr(node) 27 | if err != nil { 28 | return nil, err 29 | } 30 | 31 | err = node.consumeTokenNamed(tokenCases{ 32 | Exclamation.tokenCase(): func(token *tokenNode) error { 33 | _, err := node.consumeToken(Equal) 34 | if err != nil { 35 | return err 36 | } 37 | where.Op = operatorFromValue(token, consts.NE) 38 | return nil 39 | }, 40 | {Ident, "like"}: func(token *tokenNode) error { 41 | where.Op = operatorFromValue(token, consts.Like) 42 | return nil 43 | }, 44 | Equal.tokenCase(): func(token *tokenNode) error { 45 | where.Op = operatorFromValue(token, consts.EQ) 46 | return nil 47 | }, 48 | LeftAngle.tokenCase(): func(token *tokenNode) error { 49 | if node.consumeIfToken(Equal) != nil { 50 | where.Op = operatorFromValue(token, consts.LE) 51 | } else { 52 | where.Op = operatorFromValue(token, consts.LT) 53 | } 54 | return nil 55 | }, 56 | RightAngle.tokenCase(): func(token *tokenNode) error { 57 | if node.consumeIfToken(Equal) != nil { 58 | where.Op = operatorFromValue(token, consts.GE) 59 | } else { 60 | where.Op = operatorFromValue(token, consts.GT) 61 | } 62 | return nil 63 | }, 64 | }) 65 | if err != nil { 66 | return nil, err 67 | } 68 | 69 | where.Right, err = parseExpr(node) 70 | if err != nil { 71 | return nil, err 72 | } 73 | 74 | return where, nil 75 | } 76 | -------------------------------------------------------------------------------- /templates/generate.go: -------------------------------------------------------------------------------- 1 | //go:generate go-bindata -pkg templates -ignore ".+\\.go" -modtime 946710000 . 2 | 3 | package templates 4 | -------------------------------------------------------------------------------- /templates/golang.create-raw.tmpl: -------------------------------------------------------------------------------- 1 | {{- define "name" -}} 2 | RawCreate{{ if not .Return }}NoReturn{{ end }}_{{ .Suffix }} 3 | {{- end -}} 4 | 5 | {{- define "signature" -}} 6 | {{- template "name" . }}({{ ctxparam .Arg }}) ( 7 | {{ if .Return }}{{ param .Return }}, {{ end }}err error) 8 | {{- end -}} 9 | 10 | {{- define "invoke" -}} 11 | {{- template "name" . }}({{ ctxarg .Arg }}) 12 | {{ end -}} 13 | 14 | {{- define "body" -}} 15 | {{ initnew .Fields }} 16 | 17 | {{ embedplaceholders .Info }} 18 | {{ embedsql .Info "__embed_stmt" }} 19 | 20 | var __stmt = __sqlbundle_Render(obj.dialect, __embed_stmt) 21 | obj.logStmt(__stmt, {{ arg .Fields }}) 22 | 23 | {{ if not .Return }} 24 | _, err = obj.driver.Exec(__stmt, {{ arg .Fields}}) 25 | if err != nil { 26 | return obj.makeErr(err) 27 | } 28 | return nil 29 | {{ else -}} 30 | {{ if .SupportsReturning }} 31 | {{ init .Return }} 32 | err = obj.driver.QueryRow(__stmt, {{ arg .Fields }}).Scan({{ addrof (flatten .Return) }}) 33 | if err != nil { 34 | return nil, obj.makeErr(err) 35 | } 36 | return {{ arg .Return }}, nil 37 | {{ else -}} 38 | __res, err := obj.driver.Exec(__stmt, {{ arg .Fields}}) 39 | if err != nil { 40 | return nil, obj.makeErr(err) 41 | } 42 | __pk, err := __res.LastInsertId() 43 | if err != nil { 44 | return nil, obj.makeErr(err) 45 | } 46 | return obj.getLast{{ .Return.Type }}(ctx, __pk) 47 | {{ end -}} 48 | {{ end -}} 49 | {{ end -}} 50 | -------------------------------------------------------------------------------- /templates/golang.create.tmpl: -------------------------------------------------------------------------------- 1 | {{- define "name" -}} 2 | Create{{ if not .Return }}NoReturn{{ end }}_{{ .Suffix }} 3 | {{- end -}} 4 | 5 | {{- define "signature" -}} 6 | {{- template "name" . }}({{ ctxparam .Args }}) ( 7 | {{ if .Return }}{{ param .Return }}, {{ end }}err error) 8 | {{- end -}} 9 | 10 | {{- define "invoke" -}} 11 | {{- template "name" . }}({{ ctxarg .Args }}) 12 | {{ end -}} 13 | 14 | {{- define "body" -}} 15 | {{- if .NeedsNow }} 16 | __now := obj.db.Hooks.Now().UTC() 17 | {{ end -}} 18 | {{ initnew .Fields }} 19 | 20 | {{ embedplaceholders .Info }} 21 | {{ embedsql .Info "__embed_stmt" }} 22 | 23 | var __stmt = __sqlbundle_Render(obj.dialect, __embed_stmt) 24 | obj.logStmt(__stmt, {{ arg .Fields }}) 25 | 26 | {{ if not .Return }} 27 | _, err = obj.driver.Exec(__stmt, {{ arg .Fields}}) 28 | if err != nil { 29 | return obj.makeErr(err) 30 | } 31 | return nil 32 | {{ else -}} 33 | {{ if .SupportsReturning }} 34 | {{ init .Return }} 35 | err = obj.driver.QueryRow(__stmt, {{ arg .Fields }}).Scan({{ addrof (flatten .Return) }}) 36 | if err != nil { 37 | return nil, obj.makeErr(err) 38 | } 39 | return {{ arg .Return }}, nil 40 | {{ else -}} 41 | __res, err := obj.driver.Exec(__stmt, {{ arg .Fields}}) 42 | if err != nil { 43 | return nil, obj.makeErr(err) 44 | } 45 | __pk, err := __res.LastInsertId() 46 | if err != nil { 47 | return nil, obj.makeErr(err) 48 | } 49 | return obj.getLast{{ .Return.Type }}(ctx, __pk) 50 | {{ end -}} 51 | {{ end -}} 52 | {{ end -}} 53 | -------------------------------------------------------------------------------- /templates/golang.decl.tmpl: -------------------------------------------------------------------------------- 1 | {{- define "decl" }} 2 | func (obj *{{ .ReceiverBase }}Impl) {{ .Signature }} { 3 | {{ .Body }} 4 | } 5 | {{ end -}} 6 | -------------------------------------------------------------------------------- /templates/golang.delete-all.tmpl: -------------------------------------------------------------------------------- 1 | {{- define "signature" -}} 2 | Delete_{{ .Suffix }}({{ ctxparam .AllArgs }}) ( 3 | count int64, err error) 4 | {{- end -}} 5 | 6 | {{- define "invoke" -}} 7 | Delete_{{ .Suffix }}({{ ctxarg .AllArgs }}) 8 | {{ end -}} 9 | 10 | {{- define "body" -}} 11 | {{ embedplaceholders .Info }} 12 | {{ embedsql .Info "__embed_stmt" }} 13 | 14 | var __values []interface{} 15 | __values = append(__values, {{ fieldvalue .StaticArgs }}) 16 | 17 | {{ range $i, $arg := .NullableArgs }} 18 | if !{{ $arg.Name }}.isnull() { 19 | __cond_{{ $i }}.Null = false 20 | __values = append(__values, {{ $arg.Name }}.value()) 21 | } 22 | {{ end }} 23 | 24 | var __stmt = __sqlbundle_Render(obj.dialect, __embed_stmt) 25 | obj.logStmt(__stmt, __values...) 26 | 27 | __res, err := obj.driver.Exec(__stmt, __values...) 28 | if err != nil { 29 | return 0, obj.makeErr(err) 30 | } 31 | 32 | count, err = __res.RowsAffected() 33 | if err != nil { 34 | return 0, obj.makeErr(err) 35 | } 36 | 37 | return count, nil 38 | {{ end -}} 39 | -------------------------------------------------------------------------------- /templates/golang.delete-world.tmpl: -------------------------------------------------------------------------------- 1 | {{- define "signature" -}} 2 | deleteAll(ctx context.Context) (count int64, err error) 3 | {{- end -}} 4 | 5 | {{- define "body" -}} 6 | var __res sql.Result 7 | var __count int64 8 | 9 | {{- range .SQLs }} 10 | __res, err = obj.driver.Exec({{ printf "%q" . }}) 11 | if err != nil { 12 | return 0, obj.makeErr(err) 13 | } 14 | 15 | __count, err = __res.RowsAffected() 16 | if err != nil { 17 | return 0, obj.makeErr(err) 18 | } 19 | count += __count 20 | {{- end }} 21 | 22 | return count, nil 23 | {{ end -}} 24 | -------------------------------------------------------------------------------- /templates/golang.delete.tmpl: -------------------------------------------------------------------------------- 1 | {{- define "signature" -}} 2 | Delete_{{ .Suffix }}({{ ctxparam .AllArgs }}) ( 3 | deleted bool, err error) 4 | {{- end -}} 5 | 6 | {{- define "invoke" -}} 7 | Delete_{{ .Suffix }}({{ ctxarg .AllArgs }}) 8 | {{- end -}} 9 | 10 | {{- define "body" -}} 11 | {{ embedplaceholders .Info }} 12 | {{ embedsql .Info "__embed_stmt" }} 13 | 14 | var __values []interface{} 15 | __values = append(__values, {{ fieldvalue .StaticArgs }}) 16 | 17 | {{ range $i, $arg := .NullableArgs }} 18 | if !{{ $arg.Name }}.isnull() { 19 | __cond_{{ $i }}.Null = false 20 | __values = append(__values, {{ $arg.Name }}.value()) 21 | } 22 | {{ end }} 23 | 24 | var __stmt = __sqlbundle_Render(obj.dialect, __embed_stmt) 25 | obj.logStmt(__stmt, __values...) 26 | 27 | __res, err := obj.driver.Exec(__stmt, __values...) 28 | if err != nil { 29 | return false, obj.makeErr(err) 30 | } 31 | 32 | __count, err := __res.RowsAffected() 33 | if err != nil { 34 | return false, obj.makeErr(err) 35 | } 36 | 37 | return __count > 0, nil 38 | {{ end -}} 39 | -------------------------------------------------------------------------------- /templates/golang.dialect-postgres.tmpl: -------------------------------------------------------------------------------- 1 | {{- define "import" }} 2 | "github.com/lib/pq" 3 | {{ end -}} 4 | 5 | {{- define "is-constraint-error" }} 6 | func (impl {{ .Receiver }}) isConstraintError(err error) ( 7 | constraint string, ok bool) { 8 | if e, ok := err.(*pq.Error); ok { 9 | if e.Code.Class() == "23" { 10 | return e.Constraint, true 11 | } 12 | } 13 | return "", false 14 | } 15 | {{ end -}} 16 | 17 | {{- define "open" }} 18 | func openpostgres(source string) (*sql.DB, error) { 19 | return sql.Open("postgres", source) 20 | } 21 | {{ end -}} -------------------------------------------------------------------------------- /templates/golang.dialect-sqlite3.tmpl: -------------------------------------------------------------------------------- 1 | {{- define "import" }} 2 | "math/rand" 3 | "github.com/mattn/go-sqlite3" 4 | {{ end -}} 5 | 6 | {{- define "is-constraint-error" }} 7 | func (impl {{ .Receiver }}) isConstraintError(err error) ( 8 | constraint string, ok bool) { 9 | if e, ok := err.(sqlite3.Error); ok { 10 | if e.Code == sqlite3.ErrConstraint { 11 | msg := err.Error() 12 | colon := strings.LastIndex(msg, ":") 13 | if colon != -1 { 14 | return strings.TrimSpace(msg[colon:]), true 15 | } 16 | return "", true 17 | } 18 | } 19 | return "", false 20 | } 21 | {{ end -}} 22 | 23 | {{- define "open" }} 24 | var sqlite3DriverName = func() string { 25 | var id [16]byte 26 | rand.Read(id[:]) 27 | return fmt.Sprintf("sqlite3_%x", string(id[:])) 28 | }() 29 | 30 | func init() { 31 | sql.Register(sqlite3DriverName, &sqlite3.SQLiteDriver{ 32 | ConnectHook: sqlite3SetupConn, 33 | }) 34 | } 35 | 36 | // SQLite3JournalMode controls the journal_mode pragma for all new connections. 37 | // Since it is read without a mutex, it must be changed to the value you want 38 | // before any Open calls. 39 | var SQLite3JournalMode = "WAL" 40 | 41 | func sqlite3SetupConn(conn *sqlite3.SQLiteConn) (err error) { 42 | _, err = conn.Exec("PRAGMA foreign_keys = ON", nil) 43 | if err != nil { 44 | return makeErr(err) 45 | } 46 | _, err = conn.Exec("PRAGMA journal_mode = " + SQLite3JournalMode, nil) 47 | if err != nil { 48 | return makeErr(err) 49 | } 50 | return nil 51 | } 52 | 53 | func opensqlite3(source string) (*sql.DB, error) { 54 | return sql.Open(sqlite3DriverName, source) 55 | } 56 | {{ end -}} -------------------------------------------------------------------------------- /templates/golang.footer.tmpl: -------------------------------------------------------------------------------- 1 | {{ if .SupportRx }} 2 | type Rx struct { 3 | db *DB 4 | tx *Tx 5 | } 6 | 7 | func (rx *Rx) UnsafeTx(ctx context.Context) (unsafe_tx *sql.Tx, err error) { 8 | tx, err := rx.getTx(ctx) 9 | if err != nil { 10 | return nil, err 11 | } 12 | return tx.Tx, nil 13 | } 14 | 15 | func (rx *Rx) getTx(ctx context.Context) (tx *Tx, err error) { 16 | if rx.tx == nil { 17 | if rx.tx, err = rx.db.Open(ctx); err != nil { 18 | return nil, err 19 | } 20 | } 21 | return rx.tx, nil 22 | } 23 | 24 | func (rx *Rx) Rebind(s string) string { 25 | return rx.db.Rebind(s) 26 | } 27 | 28 | func (rx *Rx) Commit() (err error) { 29 | if rx.tx != nil { 30 | err = rx.tx.Commit() 31 | rx.tx = nil 32 | } 33 | return err 34 | } 35 | 36 | func (rx *Rx) Rollback() (err error) { 37 | if rx.tx != nil { 38 | err = rx.tx.Rollback() 39 | rx.tx = nil 40 | } 41 | return err 42 | } 43 | 44 | {{ range .Methods }} 45 | func (rx *Rx) {{ .Signature }} { 46 | var tx *Tx 47 | if tx, err = rx.getTx(ctx); err != nil { 48 | return 49 | } 50 | return tx.{{ .Invoke }} 51 | } 52 | {{ end }} 53 | {{ end }} 54 | 55 | type Methods interface { 56 | {{- range .Methods }} 57 | {{ .Signature }} 58 | {{ end }} 59 | } 60 | 61 | type TxMethods interface { 62 | Methods 63 | 64 | Rebind(s string) string 65 | Commit() error 66 | Rollback() error 67 | } 68 | 69 | type txMethods interface { 70 | TxMethods 71 | 72 | deleteAll(ctx context.Context) (int64, error) 73 | makeErr(err error) error 74 | } 75 | 76 | type DBMethods interface { 77 | Methods 78 | 79 | Schema() string 80 | Rebind(sql string) string 81 | } 82 | 83 | type dbMethods interface { 84 | DBMethods 85 | 86 | wrapTx(tx *sql.Tx) txMethods 87 | makeErr(err error) error 88 | } 89 | -------------------------------------------------------------------------------- /templates/golang.get-all.tmpl: -------------------------------------------------------------------------------- 1 | {{- define "signature" -}} 2 | All_{{ .Suffix }}({{ ctxparam .AllArgs }}) ( 3 | rows {{ sliceof .Row }}, err error) 4 | {{- end -}} 5 | 6 | {{- define "invoke" -}} 7 | All_{{ .Suffix }}({{ ctxarg .AllArgs }}) 8 | {{- end -}} 9 | 10 | {{- define "body" }} 11 | {{ embedplaceholders .Info }} 12 | {{ embedsql .Info "__embed_stmt" }} 13 | 14 | var __values []interface{} 15 | __values = append(__values, {{ fieldvalue .StaticArgs }}) 16 | 17 | {{ range $i, $arg := .NullableArgs }} 18 | if !{{ $arg.Name }}.isnull() { 19 | __cond_{{ $i }}.Null = false 20 | __values = append(__values, {{ $arg.Name }}.value()) 21 | } 22 | {{ end }} 23 | 24 | var __stmt = __sqlbundle_Render(obj.dialect, __embed_stmt) 25 | obj.logStmt(__stmt, __values...) 26 | 27 | __rows, err := obj.driver.Query(__stmt, __values...) 28 | if err != nil { 29 | return nil, obj.makeErr(err) 30 | } 31 | defer __rows.Close() 32 | 33 | for __rows.Next() { 34 | {{ initnew .Row }} 35 | err = __rows.Scan({{ addrof (flatten .Row) }}) 36 | if err != nil { 37 | return nil, obj.makeErr(err) 38 | } 39 | rows = append(rows, {{ arg .Row }}) 40 | } 41 | if err := __rows.Err(); err != nil { 42 | return nil, obj.makeErr(err) 43 | } 44 | return rows, nil 45 | {{ end -}} 46 | -------------------------------------------------------------------------------- /templates/golang.get-count.tmpl: -------------------------------------------------------------------------------- 1 | {{- define "signature" -}} 2 | Count_{{ .Suffix }}({{ ctxparam .AllArgs }}) ( 3 | count int64, err error) 4 | {{- end -}} 5 | 6 | {{- define "invoke" -}} 7 | Count_{{ .Suffix }}({{ ctxarg .AllArgs }}) 8 | {{- end -}} 9 | 10 | {{- define "body" }} 11 | {{ embedplaceholders .Info }} 12 | {{ embedsql .Info "__embed_stmt" }} 13 | 14 | var __values []interface{} 15 | __values = append(__values, {{ fieldvalue .StaticArgs }}) 16 | 17 | {{ range $i, $arg := .NullableArgs }} 18 | if !{{ $arg.Name }}.isnull() { 19 | __cond_{{ $i }}.Null = false 20 | __values = append(__values, {{ $arg.Name }}.value()) 21 | } 22 | {{ end }} 23 | 24 | var __stmt = __sqlbundle_Render(obj.dialect, __embed_stmt) 25 | obj.logStmt(__stmt, __values...) 26 | 27 | err = obj.driver.QueryRow(__stmt, __values...).Scan(&count) 28 | if err != nil { 29 | return 0, obj.makeErr(err) 30 | } 31 | 32 | return count, nil 33 | {{ end -}} 34 | -------------------------------------------------------------------------------- /templates/golang.get-first.tmpl: -------------------------------------------------------------------------------- 1 | {{- define "signature" -}} 2 | First_{{ .Suffix }}({{ ctxparam .AllArgs }}) ( 3 | {{ param .Row }}, err error) 4 | {{- end -}} 5 | 6 | {{- define "invoke" -}} 7 | First_{{ .Suffix }}({{ ctxarg .AllArgs }}) 8 | {{- end -}} 9 | 10 | {{- define "body" }} 11 | {{ embedplaceholders .Info }} 12 | {{ embedsql .Info "__embed_stmt" }} 13 | 14 | var __values []interface{} 15 | __values = append(__values, {{ fieldvalue .StaticArgs }}) 16 | 17 | {{ range $i, $arg := .NullableArgs }} 18 | if !{{ $arg.Name }}.isnull() { 19 | __cond_{{ $i }}.Null = false 20 | __values = append(__values, {{ $arg.Name }}.value()) 21 | } 22 | {{ end }} 23 | 24 | var __stmt = __sqlbundle_Render(obj.dialect, __embed_stmt) 25 | obj.logStmt(__stmt, __values...) 26 | 27 | __rows, err := obj.driver.Query(__stmt, __values...) 28 | if err != nil { 29 | return nil, obj.makeErr(err) 30 | } 31 | defer __rows.Close() 32 | 33 | if !__rows.Next() { 34 | if err := __rows.Err(); err != nil { 35 | return nil, obj.makeErr(err) 36 | } 37 | return nil, nil 38 | } 39 | 40 | {{ init .Row }} 41 | err = __rows.Scan({{ addrof (flatten .Row) }}) 42 | if err != nil { 43 | return nil, obj.makeErr(err) 44 | } 45 | 46 | return {{ arg .Row }}, nil 47 | {{ end -}} 48 | -------------------------------------------------------------------------------- /templates/golang.get-has.tmpl: -------------------------------------------------------------------------------- 1 | {{- define "signature" -}} 2 | Has_{{ .Suffix }}({{ ctxparam .AllArgs }}) ( 3 | has bool, err error) 4 | {{- end -}} 5 | 6 | {{- define "invoke" -}} 7 | Has_{{ .Suffix }}({{ ctxarg .AllArgs }}) 8 | {{- end -}} 9 | 10 | {{- define "body" }} 11 | {{ embedplaceholders .Info }} 12 | {{ embedsql .Info "__embed_stmt" }} 13 | 14 | var __values []interface{} 15 | __values = append(__values, {{ fieldvalue .StaticArgs }}) 16 | 17 | {{ range $i, $arg := .NullableArgs }} 18 | if !{{ $arg.Name }}.isnull() { 19 | __cond_{{ $i }}.Null = false 20 | __values = append(__values, {{ $arg.Name }}.value()) 21 | } 22 | {{ end }} 23 | 24 | var __stmt = __sqlbundle_Render(obj.dialect, __embed_stmt) 25 | obj.logStmt(__stmt, __values...) 26 | 27 | err = obj.driver.QueryRow(__stmt, __values...).Scan(&has) 28 | if err != nil { 29 | return false, obj.makeErr(err) 30 | } 31 | return has, nil 32 | {{ end -}} 33 | -------------------------------------------------------------------------------- /templates/golang.get-last.tmpl: -------------------------------------------------------------------------------- 1 | {{- define "signature" -}} 2 | getLast{{ .Return.Type }}(ctx context.Context, 3 | pk int64) ( 4 | {{ param .Return }}, err error) 5 | {{- end -}} 6 | 7 | {{- define "body" -}} 8 | {{ embedplaceholders .Info }} 9 | {{ embedsql .Info "__embed_stmt" }} 10 | 11 | var __stmt = __sqlbundle_Render(obj.dialect, __embed_stmt) 12 | obj.logStmt(__stmt, pk) 13 | 14 | {{ init .Return }} 15 | err = obj.driver.QueryRow(__stmt, pk).Scan({{ addrof (flatten .Return) }}) 16 | if err != nil { 17 | return {{ zero .Return }}, obj.makeErr(err) 18 | } 19 | return {{ arg .Return }}, nil 20 | {{ end -}} 21 | -------------------------------------------------------------------------------- /templates/golang.get-limitoffset.tmpl: -------------------------------------------------------------------------------- 1 | {{- define "signature" -}} 2 | Limited_{{ .Suffix }}({{ ctxparam .AllArgs }}, 3 | limit int, offset int64) ( 4 | rows {{ sliceof .Row }}, err error) 5 | {{- end -}} 6 | 7 | 8 | {{- define "invoke" -}} 9 | Limited_{{ .Suffix }}({{ ctxarg .AllArgs }}, limit, offset) 10 | {{- end -}} 11 | 12 | {{- define "body" }} 13 | {{ embedplaceholders .Info }} 14 | {{ embedsql .Info "__embed_stmt" }} 15 | 16 | var __values []interface{} 17 | __values = append(__values, {{ fieldvalue .StaticArgs }}) 18 | 19 | {{ range $i, $arg := .NullableArgs }} 20 | if !{{ $arg.Name }}.isnull() { 21 | __cond_{{ $i }}.Null = false 22 | __values = append(__values, {{ $arg.Name }}.value()) 23 | } 24 | {{ end }} 25 | 26 | __values = append(__values, limit, offset) 27 | 28 | var __stmt = __sqlbundle_Render(obj.dialect, __embed_stmt) 29 | obj.logStmt(__stmt, __values...) 30 | 31 | __rows, err := obj.driver.Query(__stmt, __values...) 32 | if err != nil { 33 | return nil, obj.makeErr(err) 34 | } 35 | defer __rows.Close() 36 | 37 | for __rows.Next() { 38 | {{ initnew .Row }} 39 | err = __rows.Scan({{ addrof (flatten .Row) }}) 40 | if err != nil { 41 | return nil, obj.makeErr(err) 42 | } 43 | rows = append(rows, {{ arg .Row }}) 44 | } 45 | if err := __rows.Err(); err != nil { 46 | return nil, obj.makeErr(err) 47 | } 48 | return rows, nil 49 | {{ end -}} 50 | -------------------------------------------------------------------------------- /templates/golang.get-one-all.tmpl: -------------------------------------------------------------------------------- 1 | {{- define "signature" -}} 2 | Get_{{ .Suffix }}({{ ctxparam .AllArgs }}) ( 3 | {{ param .Row }}, err error) 4 | {{- end -}} 5 | 6 | {{- define "invoke" -}} 7 | Get_{{ .Suffix }}({{ ctxarg .AllArgs }}) 8 | {{- end -}} 9 | 10 | {{- define "body" }} 11 | {{ embedplaceholders .Info }} 12 | {{ embedsql .Info "__embed_stmt" }} 13 | 14 | var __values []interface{} 15 | __values = append(__values, {{ fieldvalue .StaticArgs }}) 16 | 17 | {{ range $i, $arg := .NullableArgs }} 18 | if !{{ $arg.Name }}.isnull() { 19 | __cond_{{ $i }}.Null = false 20 | __values = append(__values, {{ $arg.Name }}.value()) 21 | } 22 | {{ end }} 23 | 24 | var __stmt = __sqlbundle_Render(obj.dialect, __embed_stmt) 25 | obj.logStmt(__stmt, __values...) 26 | 27 | __rows, err := obj.driver.Query(__stmt, __values...) 28 | if err != nil { 29 | return nil, obj.makeErr(err) 30 | } 31 | defer __rows.Close() 32 | 33 | if !__rows.Next() { 34 | if err := __rows.Err(); err != nil { 35 | return nil, obj.makeErr(err) 36 | } 37 | return nil, makeErr(sql.ErrNoRows) 38 | } 39 | 40 | {{ init .Row }} 41 | err = __rows.Scan({{ addrof (flatten .Row) }}) 42 | if err != nil { 43 | return nil, obj.makeErr(err) 44 | } 45 | 46 | if __rows.Next() { 47 | return nil, tooManyRows({{ printf "%q" .Suffix }}) 48 | } 49 | 50 | if err := __rows.Err(); err != nil { 51 | return nil, obj.makeErr(err) 52 | } 53 | 54 | return {{ arg .Row }}, nil 55 | {{ end -}} 56 | -------------------------------------------------------------------------------- /templates/golang.get-one.tmpl: -------------------------------------------------------------------------------- 1 | {{- define "signature" -}} 2 | Get_{{ .Suffix }}({{ ctxparam .AllArgs }}) ( 3 | {{ param .Row }}, err error) 4 | {{- end -}} 5 | 6 | {{- define "invoke" -}} 7 | Get_{{ .Suffix }}({{ ctxarg .AllArgs }}) 8 | {{- end -}} 9 | 10 | {{- define "body" }} 11 | {{ embedplaceholders .Info }} 12 | {{ embedsql .Info "__embed_stmt" }} 13 | 14 | var __values []interface{} 15 | __values = append(__values, {{ fieldvalue .StaticArgs }}) 16 | 17 | {{ range $i, $arg := .NullableArgs }} 18 | if !{{ $arg.Name }}.isnull() { 19 | __cond_{{ $i }}.Null = false 20 | __values = append(__values, {{ $arg.Name }}.value()) 21 | } 22 | {{ end }} 23 | 24 | var __stmt = __sqlbundle_Render(obj.dialect, __embed_stmt) 25 | obj.logStmt(__stmt, __values...) 26 | 27 | {{ init .Row }} 28 | err = obj.driver.QueryRow(__stmt, __values...).Scan({{ addrof (flatten .Row) }}) 29 | if err != nil { 30 | return {{ zero .Row }}, obj.makeErr(err) 31 | } 32 | return {{ arg .Row }}, nil 33 | {{ end -}} 34 | -------------------------------------------------------------------------------- /templates/golang.get-paged.tmpl: -------------------------------------------------------------------------------- 1 | {{- define "signature" -}} 2 | Paged_{{ .Suffix }}({{ ctxparam .AllArgs }}, 3 | limit int, ctoken string) ( 4 | rows {{ sliceof .Row }}, ctokenout string, err error) 5 | {{- end -}} 6 | 7 | {{- define "invoke" -}} 8 | Paged_{{ .Suffix }}({{ ctxarg .AllArgs }}, limit, ctoken) 9 | {{- end -}} 10 | 11 | {{- define "body" }} 12 | if ctoken == "" { 13 | ctoken = "0" 14 | } 15 | 16 | {{ embedplaceholders .Info }} 17 | {{ embedsql .Info "__embed_stmt" }} 18 | 19 | var __values []interface{} 20 | __values = append(__values, {{ fieldvalue .StaticArgs }}) 21 | 22 | {{ range $i, $arg := .NullableArgs }} 23 | if !{{ $arg.Name }}.isnull() { 24 | __cond_{{ $i }}.Null = false 25 | __values = append(__values, {{ $arg.Name }}.value()) 26 | } 27 | {{ end }} 28 | 29 | __values = append(__values, ctoken, limit) 30 | 31 | var __stmt = __sqlbundle_Render(obj.dialect, __embed_stmt) 32 | obj.logStmt(__stmt, __values...) 33 | 34 | __rows, err := obj.driver.Query(__stmt, __values...) 35 | if err != nil { 36 | return nil, "", obj.makeErr(err) 37 | } 38 | defer __rows.Close() 39 | 40 | {{ initnew .LastPk }} 41 | for __rows.Next() { 42 | {{ initnew .Row }} 43 | err = __rows.Scan({{ addrof (flatten .Row) | comma }}{{ addrof .LastPk }}) 44 | if err != nil { 45 | return nil, "", obj.makeErr(err) 46 | } 47 | rows = append(rows, {{ arg .Row }}) 48 | } 49 | if err := __rows.Err(); err != nil { 50 | return nil, "", obj.makeErr(err) 51 | } 52 | 53 | if limit > 0 { 54 | if len(rows) == limit { 55 | ctokenout = fmt.Sprint({{ arg .LastPk }}) 56 | } 57 | } else { 58 | ctokenout = ctoken 59 | } 60 | 61 | return rows, ctokenout, nil 62 | {{ end -}} 63 | -------------------------------------------------------------------------------- /templates/golang.get-scalar-all.tmpl: -------------------------------------------------------------------------------- 1 | {{- define "signature" -}} 2 | Find_{{ .Suffix }}({{ ctxparam .AllArgs }}) ( 3 | {{ param .Row }}, err error) 4 | {{- end -}} 5 | 6 | {{- define "invoke" -}} 7 | Find_{{ .Suffix }}({{ ctxarg .AllArgs }}) 8 | {{- end -}} 9 | 10 | {{- define "body" }} 11 | {{ embedplaceholders .Info }} 12 | {{ embedsql .Info "__embed_stmt" }} 13 | 14 | var __values []interface{} 15 | __values = append(__values, {{ fieldvalue .StaticArgs }}) 16 | 17 | {{ range $i, $arg := .NullableArgs }} 18 | if !{{ $arg.Name }}.isnull() { 19 | __cond_{{ $i }}.Null = false 20 | __values = append(__values, {{ $arg.Name }}.value()) 21 | } 22 | {{ end }} 23 | 24 | var __stmt = __sqlbundle_Render(obj.dialect, __embed_stmt) 25 | obj.logStmt(__stmt, __values...) 26 | 27 | __rows, err := obj.driver.Query(__stmt, __values...) 28 | if err != nil { 29 | return nil, obj.makeErr(err) 30 | } 31 | defer __rows.Close() 32 | 33 | if !__rows.Next() { 34 | if err := __rows.Err(); err != nil { 35 | return nil, obj.makeErr(err) 36 | } 37 | return nil, nil 38 | } 39 | 40 | {{ init .Row }} 41 | err = __rows.Scan({{ addrof (flatten .Row) }}) 42 | if err != nil { 43 | return nil, obj.makeErr(err) 44 | } 45 | 46 | if __rows.Next() { 47 | return nil, tooManyRows({{ printf "%q" .Suffix }}) 48 | } 49 | 50 | if err := __rows.Err(); err != nil { 51 | return nil, obj.makeErr(err) 52 | } 53 | 54 | return {{ arg .Row }}, nil 55 | {{ end -}} 56 | -------------------------------------------------------------------------------- /templates/golang.get-scalar.tmpl: -------------------------------------------------------------------------------- 1 | {{- define "signature" -}} 2 | Find_{{ .Suffix }}({{ ctxparam .AllArgs }}) ( 3 | {{ param .Row }}, err error) 4 | {{- end -}} 5 | 6 | {{- define "invoke" -}} 7 | Find_{{ .Suffix }}({{ ctxarg .AllArgs }}) 8 | {{- end -}} 9 | 10 | {{- define "body" }} 11 | {{ embedplaceholders .Info }} 12 | {{ embedsql .Info "__embed_stmt" }} 13 | 14 | var __values []interface{} 15 | __values = append(__values, {{ fieldvalue .StaticArgs }}) 16 | 17 | {{ range $i, $arg := .NullableArgs }} 18 | if !{{ $arg.Name }}.isnull() { 19 | __cond_{{ $i }}.Null = false 20 | __values = append(__values, {{ $arg.Name }}.value()) 21 | } 22 | {{ end }} 23 | 24 | var __stmt = __sqlbundle_Render(obj.dialect, __embed_stmt) 25 | obj.logStmt(__stmt, __values...) 26 | 27 | {{ init .Row }} 28 | err = obj.driver.QueryRow(__stmt, __values...).Scan({{ addrof (flatten .Row) }}) 29 | if err == sql.ErrNoRows { 30 | return {{ zero .Row }}, nil 31 | } 32 | if err != nil { 33 | return {{ zero .Row }}, obj.makeErr(err) 34 | } 35 | return {{ arg .Row }}, nil 36 | {{ end -}} 37 | -------------------------------------------------------------------------------- /templates/golang.misc.tmpl: -------------------------------------------------------------------------------- 1 | {{- define "struct" }} 2 | type {{ .Name }} struct { 3 | {{ range .Fields }} 4 | {{ if .Name }}{{ .Name }} {{ end }}{{ .Type }}{{ if .Tags -}} 5 | `{{ range $i, $t := .Tags -}}{{ if $i }} {{ end }}{{ .Key }}:{{ .Value | printf "%q" }}{{ end }}` 6 | {{- end -}} 7 | {{ end }} 8 | } 9 | {{ end -}} 10 | -------------------------------------------------------------------------------- /templates/golang.update.tmpl: -------------------------------------------------------------------------------- 1 | {{- define "name" -}} 2 | Update{{ if not .Return }}NoReturn{{ end }}_{{ .Suffix }} 3 | {{- end -}} 4 | 5 | {{- define "signature" -}} 6 | {{- template "name" . }}({{ ctxparam .AllArgs }}, 7 | update {{ .Struct.UpdateStructName }}) ( 8 | {{ if .Return }}{{ param .Return }}, {{ end }}err error) 9 | {{- end -}} 10 | 11 | {{- define "invoke" -}} 12 | {{- template "name" . }}({{ ctxarg .AllArgs }}, update) 13 | {{- end -}} 14 | 15 | {{- define "body" -}} 16 | {{ embedplaceholders .Info }} 17 | {{ embedsql .Info "__embed_stmt" }} 18 | 19 | __sets_sql := __sqlbundle_Literals{Join: ", "} 20 | var __values []interface{} 21 | var __args []interface{} 22 | 23 | {{ range .Struct.UpdatableFields }} 24 | if update.{{ .Name }}._set { 25 | __values = append(__values, update.{{ .Name }}.value()) 26 | __sets_sql.SQLs = append(__sets_sql.SQLs, __sqlbundle_Literal("{{ .Column }} = ?")) 27 | } 28 | {{ end }} 29 | 30 | {{- if .NeedsNow }} 31 | __now := obj.db.Hooks.Now().UTC() 32 | {{ end -}} 33 | {{ range .AutoFields }} 34 | __values = append(__values, {{ .InitVal }}) 35 | __sets_sql.SQLs = append(__sets_sql.SQLs, __sqlbundle_Literal("{{ .Name }} = ?")) 36 | {{ end }} 37 | 38 | {{ if not .AutoFields }} 39 | if len(__sets_sql.SQLs) == 0 { 40 | {{- if .Return }} 41 | return nil, emptyUpdate() 42 | {{- else }} 43 | return emptyUpdate() 44 | {{- end }} 45 | } 46 | {{ end }} 47 | 48 | __args = append(__args, {{ fieldvalue .StaticArgs }}) 49 | 50 | {{ range $i, $arg := .NullableArgs }} 51 | if !{{ $arg.Name }}.isnull() { 52 | __cond_{{ $i }}.Null = false 53 | __args = append(__args, {{ $arg.Name }}.value()) 54 | } 55 | {{ end }} 56 | 57 | __values = append(__values, __args...) 58 | __sets.SQL = __sets_sql 59 | 60 | var __stmt = __sqlbundle_Render(obj.dialect, __embed_stmt) 61 | obj.logStmt(__stmt, __values...) 62 | {{- if not .Return }} 63 | 64 | _, err = obj.driver.Exec(__stmt, __values...) 65 | if err != nil { 66 | return obj.makeErr(err) 67 | } 68 | return nil 69 | {{- else }} 70 | 71 | {{ init .Return }} 72 | {{- if .SupportsReturning }} 73 | err = obj.driver.QueryRow(__stmt, __values...).Scan({{ addrof (flatten .Return) }}) 74 | if err == sql.ErrNoRows { 75 | return nil, nil 76 | } 77 | if err != nil { 78 | return nil, obj.makeErr(err) 79 | } 80 | {{- else }} 81 | _, err = obj.driver.Exec(__stmt, __values...) 82 | if err != nil { 83 | return nil, obj.makeErr(err) 84 | } 85 | 86 | {{ embedsql .InfoGet "__embed_stmt_get" }} 87 | var __stmt_get = __sqlbundle_Render(obj.dialect, __embed_stmt_get) 88 | obj.logStmt("(IMPLIED) " + __stmt_get, __args...) 89 | 90 | err = obj.driver.QueryRow(__stmt_get, __args...).Scan({{ addrof (flatten .Return) }}) 91 | if err == sql.ErrNoRows { 92 | return nil, nil 93 | } 94 | if err != nil { 95 | return nil, obj.makeErr(err) 96 | } 97 | {{- end }} 98 | return {{ arg .Return }}, nil 99 | {{- end }} 100 | {{- end -}} 101 | -------------------------------------------------------------------------------- /testdata/build/bad_key.dbx: -------------------------------------------------------------------------------- 1 | //test:fail_gen must specify some field references 2 | 3 | model foo ( 4 | key 5 | field pk serial64 6 | ) 7 | -------------------------------------------------------------------------------- /testdata/build/basic.dbx: -------------------------------------------------------------------------------- 1 | model foo ( 2 | key pk 3 | 4 | field pk serial64 5 | field data text (updatable) 6 | ) 7 | 8 | create foo () 9 | read all ( select foo ) 10 | update foo ( where foo.pk = ? ) 11 | delete foo ( where foo.pk = ? ) 12 | -------------------------------------------------------------------------------- /testdata/build/cyclic_foreign_key.dbx: -------------------------------------------------------------------------------- 1 | //test:fail_gen part of a cycle 2 | 3 | model a ( 4 | key a 5 | field a serial64 6 | field b b.b restrict 7 | ) 8 | 9 | model b ( 10 | key b 11 | field b serial64 12 | field a a.a restrict 13 | ) 14 | -------------------------------------------------------------------------------- /testdata/build/fails_no_updatable.dbx: -------------------------------------------------------------------------------- 1 | //test:fail_gen no updatable fields 2 | 3 | model foo ( 4 | key pk 5 | field pk serial64 6 | field foo int 7 | ) 8 | 9 | update foo ( where foo.pk = ? ) 10 | -------------------------------------------------------------------------------- /testdata/build/float64.dbx: -------------------------------------------------------------------------------- 1 | model foo ( 2 | key pk 3 | 4 | field pk serial64 5 | field data float 6 | field data64 float64 7 | field nullable_data float (nullable) 8 | field nullable_data64 float64 (nullable) 9 | ) 10 | -------------------------------------------------------------------------------- /testdata/build/joins.dbx: -------------------------------------------------------------------------------- 1 | model foo ( 2 | key id 3 | unique name 4 | unique bar_id 5 | unique baz_id 6 | 7 | field id serial64 8 | field bar_id bar.id restrict 9 | field baz_id baz.id restrict 10 | field name text (updatable) 11 | ) 12 | 13 | model bar ( 14 | key id 15 | unique name 16 | 17 | field id serial64 18 | field name text 19 | ) 20 | 21 | model baz ( 22 | key id 23 | unique name 24 | 25 | field id serial64 26 | field name text 27 | ) 28 | 29 | // Join to two different tables from the same table 30 | read all ( 31 | select foo bar baz 32 | join foo.bar_id = bar.id 33 | join foo.baz_id = baz.id 34 | ) 35 | 36 | update foo ( 37 | join foo.bar_id = bar.id 38 | where bar.name = ? 39 | ) 40 | 41 | delete foo ( 42 | join foo.bar_id = bar.id 43 | where bar.name = ? 44 | ) 45 | -------------------------------------------------------------------------------- /testdata/build/joins_bad.dbx: -------------------------------------------------------------------------------- 1 | //test:fail_gen not in scope to join on 2 | 3 | model session ( 4 | key id 5 | 6 | field id serial64 7 | field user_pk user.pk restrict 8 | ) 9 | 10 | model user ( 11 | key pk 12 | field pk serial64 13 | ) 14 | 15 | model associated_account ( 16 | key pk 17 | 18 | field pk serial64 19 | field user_pk user.pk restrict 20 | ) 21 | 22 | create session ( ) 23 | create user ( ) 24 | create associated_account ( ) 25 | 26 | read all ( 27 | select session.id 28 | join user.pk = session.user_pk 29 | join associated_account.user_pk = user.pk 30 | where associated_account.pk = ? 31 | ) 32 | -------------------------------------------------------------------------------- /testdata/build/multicolumn_select.dbx: -------------------------------------------------------------------------------- 1 | model foo ( 2 | key pk 3 | 4 | field pk serial64 5 | field one int 6 | field two int 7 | ) 8 | 9 | read all ( 10 | select foo.one foo.two 11 | ) 12 | -------------------------------------------------------------------------------- /testdata/build/projection_name_clash.dbx: -------------------------------------------------------------------------------- 1 | model t1 ( key id, field id serial64 ) 2 | model t2 ( key id, field id serial64 ) 3 | model t3 ( key id, field id serial64 ) 4 | 5 | read all ( 6 | select t1 t2.id t3.id 7 | join t1.id = t2.id 8 | join t2.id = t3.id 9 | ) 10 | -------------------------------------------------------------------------------- /testdata/build/read_views.dbx: -------------------------------------------------------------------------------- 1 | model foo ( 2 | key id 3 | 4 | field id serial64 5 | ) 6 | 7 | read all limitoffset paged has count scalar one ( 8 | select foo 9 | ) 10 | 11 | read scalar one first has count( 12 | select foo 13 | where foo.id = ? 14 | ) 15 | -------------------------------------------------------------------------------- /testdata/build/same_field_multiple_where_clause.dbx: -------------------------------------------------------------------------------- 1 | model foo ( 2 | key pk 3 | field pk serial64 4 | field u int (updatable) 5 | ) 6 | 7 | read all ( 8 | select foo 9 | where foo.pk > ? 10 | where foo.pk < ? 11 | ) 12 | 13 | update foo ( 14 | where foo.pk < ? 15 | where foo.pk = ? 16 | ) 17 | 18 | delete foo ( 19 | where foo.pk < ? 20 | where foo.pk = ? 21 | ) 22 | -------------------------------------------------------------------------------- /testdata/build/single_column_select_paged.dbx: -------------------------------------------------------------------------------- 1 | model foo ( 2 | key pk 3 | 4 | field pk serial64 5 | ) 6 | 7 | read paged ( 8 | select foo.pk 9 | ) 10 | -------------------------------------------------------------------------------- /testdata/build/suffix.dbx: -------------------------------------------------------------------------------- 1 | model foo ( 2 | key id 3 | 4 | field id serial64 5 | field u int (autoupdate) 6 | ) 7 | 8 | create foo ( 9 | suffix create suffix 10 | ) 11 | 12 | read all limitoffset paged has count scalar one ( 13 | suffix read suffix 14 | select foo 15 | ) 16 | 17 | read scalar one first has count ( 18 | suffix other_read suffix 19 | select foo 20 | 21 | where foo.id = ? 22 | ) 23 | 24 | update foo ( 25 | suffix update suffix 26 | 27 | where foo.id = ? 28 | ) 29 | 30 | delete foo ( 31 | suffix delete suffix 32 | 33 | where foo.id = ? 34 | ) 35 | 36 | -------------------------------------------------------------------------------- /testdata/build/unique_checking.dbx: -------------------------------------------------------------------------------- 1 | model a ( 2 | key id 3 | field id serial64 4 | ) 5 | 6 | model b ( 7 | key id 8 | field id serial64 9 | field a_id a.id restrict 10 | ) 11 | 12 | model c ( 13 | key id 14 | unique b_id 15 | field id serial64 16 | field lat float 17 | field lon float 18 | field b_id b.id restrict 19 | ) 20 | 21 | read all ( 22 | select a b c 23 | 24 | join a.id = b.a_id 25 | join b.id = c.b_id 26 | 27 | where a.id = ? 28 | where c.lat < ? 29 | where c.lat > ? 30 | where c.lon < ? 31 | where c.lon > ? 32 | ) 33 | 34 | -------------------------------------------------------------------------------- /testdata/run/joins.dbx: -------------------------------------------------------------------------------- 1 | model session ( 2 | key id 3 | 4 | field id serial64 5 | field user_pk user.pk restrict 6 | ) 7 | 8 | model user ( 9 | key pk 10 | field pk serial64 11 | ) 12 | 13 | model associated_account ( 14 | key pk 15 | 16 | field pk serial64 17 | field user_pk user.pk restrict 18 | ) 19 | 20 | create session ( ) 21 | create user ( ) 22 | create associated_account ( ) 23 | 24 | read all ( 25 | select session.id 26 | join session.user_pk = user.pk 27 | join user.pk = associated_account.user_pk 28 | where associated_account.pk = ? 29 | ) 30 | -------------------------------------------------------------------------------- /testdata/run/joins.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "context" 4 | 5 | func erre(err error) { 6 | if err != nil { 7 | panic(err) 8 | } 9 | } 10 | 11 | var ctx = context.Background() 12 | 13 | func main() { 14 | db, err := Open("sqlite3", ":memory:") 15 | erre(err) 16 | defer db.Close() 17 | 18 | _, err = db.Exec(db.Schema()) 19 | erre(err) 20 | 21 | user, err := db.Create_User(ctx) 22 | erre(err) 23 | 24 | aa, err := db.Create_AssociatedAccount(ctx, 25 | AssociatedAccount_UserPk(user.Pk)) 26 | erre(err) 27 | 28 | sess, err := db.Create_Session(ctx, 29 | Session_UserPk(user.Pk)) 30 | erre(err) 31 | 32 | rows, err := db.All_Session_Id_By_AssociatedAccount_Pk(ctx, 33 | AssociatedAccount_Pk(aa.Pk)) 34 | erre(err) 35 | 36 | if len(rows) != 1 || rows[0].Id != sess.Id { 37 | panic("invalid") 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /testdata/run/unique_checking.dbx: -------------------------------------------------------------------------------- 1 | model a ( 2 | table a 3 | key id 4 | field id serial64 5 | ) 6 | 7 | model b ( 8 | table b 9 | key id 10 | field id serial64 11 | field a_id a.id restrict 12 | ) 13 | 14 | model c ( 15 | table c 16 | key id 17 | unique b_id 18 | field id serial64 19 | field lat float 20 | field lon float 21 | field b_id b.id restrict 22 | ) 23 | 24 | create a () 25 | create b () 26 | create c () 27 | 28 | read all ( 29 | select a b c 30 | 31 | join a.id = b.a_id 32 | join b.id = c.b_id 33 | 34 | where a.id = ? 35 | where c.lat < ? 36 | where c.lat > ? 37 | where c.lon < ? 38 | where c.lon > ? 39 | ) 40 | -------------------------------------------------------------------------------- /testdata/run/unique_checking.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | ) 6 | 7 | func erre(err error) { 8 | if err != nil { 9 | panic(err) 10 | } 11 | } 12 | 13 | func assert(x bool) { 14 | if !x { 15 | panic("assertion failed") 16 | } 17 | } 18 | 19 | var ctx = context.Background() 20 | 21 | func main() { 22 | db, err := Open("sqlite3", ":memory:") 23 | erre(err) 24 | defer db.Close() 25 | 26 | _, err = db.Exec(db.Schema()) 27 | erre(err) 28 | 29 | a, err := db.Create_A(ctx) 30 | erre(err) 31 | 32 | b1, err := db.Create_B(ctx, B_AId(a.Id)) 33 | erre(err) 34 | c1, err := db.Create_C(ctx, C_Lat(0.0), C_Lon(0.0), C_BId(b1.Id)) 35 | erre(err) 36 | 37 | b2, err := db.Create_B(ctx, B_AId(a.Id)) 38 | erre(err) 39 | c2, err := db.Create_C(ctx, C_Lat(1.0), C_Lon(1.0), C_BId(b2.Id)) 40 | erre(err) 41 | 42 | rows, err := db.All_A_B_C_By_A_Id_And_C_Lat_Less_And_C_Lat_Greater_And_C_Lon_Less_And_C_Lon_Greater(ctx, 43 | A_Id(a.Id), 44 | C_Lat(10.0), C_Lat(-10.0), 45 | C_Lon(10.0), C_Lon(-10.0)) 46 | erre(err) 47 | 48 | assert(len(rows) == 2) 49 | 50 | assert(rows[0].A.Id == a.Id) 51 | assert(rows[0].B.Id == b1.Id) 52 | assert(rows[0].C.Id == c1.Id) 53 | assert(rows[0].C.Lat == 0) 54 | assert(rows[0].C.Lon == 0) 55 | 56 | assert(rows[1].A.Id == a.Id) 57 | assert(rows[1].B.Id == b2.Id) 58 | assert(rows[1].C.Id == c2.Id) 59 | assert(rows[1].C.Lat == 1) 60 | assert(rows[1].C.Lon == 1) 61 | } 62 | -------------------------------------------------------------------------------- /testutil/common.go: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2017 Space Monkey, Inc. 2 | 3 | package testutil 4 | 5 | import ( 6 | "sort" 7 | "strings" 8 | "testing" 9 | ) 10 | 11 | func Wrap(t *testing.T) *T { 12 | return &T{ 13 | T: t, 14 | 15 | context: make(map[string]string), 16 | } 17 | } 18 | 19 | type T struct { 20 | *testing.T 21 | 22 | context map[string]string 23 | } 24 | 25 | func (t *T) Context(name string, val string) { 26 | t.context[name] = val 27 | } 28 | 29 | func (t *T) dumpContext() { 30 | var keys []string 31 | for key := range t.context { 32 | keys = append(keys, key) 33 | } 34 | sort.Strings(keys) 35 | 36 | for _, key := range keys { 37 | t.Logf("%s:\n%s", key, t.context[key]) 38 | } 39 | } 40 | 41 | func (t *T) AssertNoError(err error) { 42 | t.Helper() 43 | if err != nil { 44 | t.dumpContext() 45 | t.Fatalf("expected no error. got %v", err) 46 | } 47 | } 48 | 49 | func (t *T) AssertError(err error, msg string) { 50 | t.Helper() 51 | if err == nil { 52 | t.dumpContext() 53 | t.Fatalf("expected an error containing %q. got nil", msg) 54 | } 55 | if !strings.Contains(err.Error(), msg) { 56 | t.dumpContext() 57 | t.Fatalf("expected an error containing %q. got %v", msg, err) 58 | } 59 | } 60 | 61 | func (t *T) AssertContains(msg, needle string) { 62 | t.Helper() 63 | if !strings.Contains(msg, needle) { 64 | t.dumpContext() 65 | t.Fatalf("expected the message to contain %q. got %s", needle, msg) 66 | } 67 | } 68 | 69 | func (t *T) Run(name string, f func(*T)) bool { 70 | return t.T.Run(name, func(t *testing.T) { f(Wrap(t)) }) 71 | } 72 | 73 | func (t *T) Runp(name string, f func(*T)) bool { 74 | return t.T.Run(name, func(t *testing.T) { t.Parallel(); f(Wrap(t)) }) 75 | } 76 | -------------------------------------------------------------------------------- /tmplutil/common.go: -------------------------------------------------------------------------------- 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 tmplutil 16 | 17 | import "github.com/spacemonkeygo/errors" 18 | 19 | var ( 20 | Error = errors.NewClass("tmplutil") 21 | ) 22 | -------------------------------------------------------------------------------- /tmplutil/loader.go: -------------------------------------------------------------------------------- 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 tmplutil 16 | 17 | import ( 18 | "io/ioutil" 19 | "os" 20 | "path/filepath" 21 | "text/template" 22 | 23 | "bitbucket.org/pkg/inflect" 24 | ) 25 | 26 | type Loader interface { 27 | Load(name string, funcs template.FuncMap) (*template.Template, error) 28 | } 29 | 30 | type LoaderFunc func(name string, funcs template.FuncMap) ( 31 | *template.Template, error) 32 | 33 | func (fn LoaderFunc) Load(name string, funcs template.FuncMap) ( 34 | *template.Template, error) { 35 | 36 | return fn(name, funcs) 37 | } 38 | 39 | type dirLoader struct { 40 | dir string 41 | fallback Loader 42 | } 43 | 44 | func DirLoader(dir string, fallback Loader) Loader { 45 | return dirLoader{ 46 | dir: dir, 47 | fallback: fallback, 48 | } 49 | } 50 | 51 | func (d dirLoader) Load(name string, funcs template.FuncMap) ( 52 | *template.Template, error) { 53 | 54 | data, err := ioutil.ReadFile(filepath.Join(d.dir, name)) 55 | if err != nil { 56 | if os.IsNotExist(err) { 57 | return d.fallback.Load(name, funcs) 58 | } 59 | return nil, Error.Wrap(err) 60 | } 61 | return loadTemplate(name, data, funcs) 62 | } 63 | 64 | type BinLoader func(name string) ([]byte, error) 65 | 66 | func (b BinLoader) Load(name string, funcs template.FuncMap) ( 67 | *template.Template, error) { 68 | 69 | data, err := b(name) 70 | if err != nil { 71 | return nil, Error.Wrap(err) 72 | } 73 | return loadTemplate(name, data, funcs) 74 | } 75 | 76 | func loadTemplate(name string, data []byte, funcs template.FuncMap) ( 77 | *template.Template, error) { 78 | 79 | if funcs == nil { 80 | funcs = make(template.FuncMap) 81 | } 82 | 83 | safeset := func(name string, fn interface{}) { 84 | if funcs[name] == nil { 85 | funcs[name] = fn 86 | } 87 | } 88 | 89 | safeset("pluralize", inflect.Pluralize) 90 | safeset("singularize", inflect.Singularize) 91 | safeset("camelize", inflect.Camelize) 92 | safeset("cameldown", inflect.CamelizeDownFirst) 93 | safeset("underscore", inflect.Underscore) 94 | 95 | tmpl, err := template.New(name).Funcs(funcs).Parse(string(data)) 96 | return tmpl, Error.Wrap(err) 97 | } 98 | -------------------------------------------------------------------------------- /tmplutil/utils.go: -------------------------------------------------------------------------------- 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 tmplutil 16 | 17 | import ( 18 | "bytes" 19 | "io" 20 | "text/template" 21 | ) 22 | 23 | func RenderString(tmpl *template.Template, name string, 24 | data interface{}) (string, error) { 25 | 26 | var buf bytes.Buffer 27 | if err := Render(tmpl, &buf, name, data); err != nil { 28 | return "", err 29 | } 30 | return buf.String(), nil 31 | } 32 | 33 | func Render(tmpl *template.Template, w io.Writer, name string, 34 | data interface{}) error { 35 | 36 | if name == "" { 37 | return Error.Wrap(tmpl.Execute(w, data)) 38 | } 39 | return Error.Wrap(tmpl.ExecuteTemplate(w, name, data)) 40 | } 41 | --------------------------------------------------------------------------------