├── examples └── swapi │ ├── Dockerfile │ ├── Makefile │ ├── gooq.yml │ ├── model │ ├── swapi_model.generated.go │ └── swapi_enum.generated.go │ ├── README.md │ ├── cmd │ └── main.go │ ├── migrations │ └── 001_init.up.sql │ └── table │ └── swapi_table.generated.go ├── .gitignore ├── Makefile ├── cmd └── gooq │ ├── main.go │ └── generator │ ├── root.go │ └── generator.go ├── pkg ├── generator │ ├── plugin │ │ ├── plugin.go │ │ ├── enumgen │ │ │ ├── types.go │ │ │ ├── enumgen.go │ │ │ └── template.go │ │ └── modelgen │ │ │ ├── types.go │ │ │ ├── template.go │ │ │ └── modelgen.go │ ├── generator.go │ ├── utils │ │ └── utils.go │ ├── metadata │ │ ├── loader.go │ │ └── types.go │ └── postgres │ │ └── postgres_loader.go ├── nullable │ ├── bigfloat_test.go │ ├── jsonb_test.go │ ├── stringarray_test.go │ ├── bigfloat.go │ ├── jsonb.go │ ├── stringarray.go │ ├── uuid_test.go │ └── uuid.go ├── gooq │ ├── LICENSE │ ├── types.go │ ├── delete_test.go │ ├── constant.go │ ├── table.go │ ├── literal.go │ ├── utils.go │ ├── update_test.go │ ├── operator.go │ ├── gooq_test.go │ ├── insert_test.go │ ├── builder.go │ ├── delete.go │ ├── field.go │ ├── update.go │ ├── function_test.go │ ├── expression_test.go │ ├── insert.go │ ├── select_test.go │ ├── select.go │ └── function.go └── database │ ├── dockerized_db.go │ └── migration.go ├── README.md ├── go.mod └── .circleci └── config.yml /examples/swapi/Dockerfile: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .idea/ 3 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | 2 | generate-fields: 3 | go run cmd/fields_generator/main.go 4 | -------------------------------------------------------------------------------- /examples/swapi/Makefile: -------------------------------------------------------------------------------- 1 | 2 | run-example: 3 | go run cmd/main.go 4 | 5 | generate-database-models: 6 | go run ../../cmd/gooq/main.go generate-database-model --docker 7 | -------------------------------------------------------------------------------- /cmd/gooq/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/lumina-tech/gooq/cmd/gooq/generator" 5 | ) 6 | 7 | func main() { 8 | generator.Execute() 9 | } 10 | -------------------------------------------------------------------------------- /pkg/generator/plugin/plugin.go: -------------------------------------------------------------------------------- 1 | package plugin 2 | 3 | import "github.com/lumina-tech/gooq/pkg/generator/metadata" 4 | 5 | type Plugin interface { 6 | GenerateCode(data *metadata.Data) error 7 | } 8 | -------------------------------------------------------------------------------- /pkg/generator/plugin/enumgen/types.go: -------------------------------------------------------------------------------- 1 | package enumgen 2 | 3 | import "github.com/lumina-tech/gooq/pkg/generator/metadata" 4 | 5 | type templateArgs struct { 6 | Timestamp string 7 | Package string 8 | Schema string 9 | Enums []metadata.Enum 10 | } 11 | -------------------------------------------------------------------------------- /examples/swapi/gooq.yml: -------------------------------------------------------------------------------- 1 | host: "localhost" 2 | port: 5432 3 | username: "postgres" 4 | password: "password" 5 | databaseName: "swapi" 6 | sslmode: "disable" 7 | migrationPath: "migrations" 8 | modelPath: "model" 9 | tablePath: "table" 10 | modelOverrides: 11 | models: 12 | species: 13 | fields: 14 | average_lifespan: 15 | overrideType: BigFloat 16 | -------------------------------------------------------------------------------- /pkg/nullable/bigfloat_test.go: -------------------------------------------------------------------------------- 1 | package nullable_test 2 | 3 | import ( 4 | "github.com/lumina-tech/gooq/pkg/nullable" 5 | "github.com/stretchr/testify/require" 6 | "math/big" 7 | "testing" 8 | ) 9 | 10 | func TestBigFloatScan(t *testing.T) { 11 | b := new(big.Float) 12 | b.SetInt64(0) 13 | bigFloat := nullable.BigFloatFrom(*b) 14 | require.True(t, bigFloat.Valid) 15 | } 16 | -------------------------------------------------------------------------------- /pkg/nullable/jsonb_test.go: -------------------------------------------------------------------------------- 1 | package nullable_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/lumina-tech/gooq/pkg/nullable" 7 | "github.com/stretchr/testify/require" 8 | ) 9 | 10 | func TestJsonbScan(t *testing.T) { 11 | bytes := []byte("hello world") 12 | jsonb := nullable.JsonbFrom(nil) 13 | require.False(t, jsonb.Valid) 14 | require.Nil(t, jsonb.Jsonb) 15 | 16 | err := jsonb.Scan(bytes) 17 | require.NoError(t, err) 18 | require.True(t, jsonb.Valid) 19 | require.Equal(t, bytes, jsonb.Jsonb) 20 | 21 | bytes[0] = byte('H') 22 | require.NotEqual(t, bytes, jsonb.Jsonb) 23 | } 24 | -------------------------------------------------------------------------------- /pkg/gooq/LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2019 Lumina Technologies 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![CircleCI](https://circleci.com/gh/lumina-tech/gOOQ.svg?style=svg)](https://circleci.com/gh/lumina-tech/gOOQ) 2 | 3 | Golang Object Oriented Querying 4 | 5 | - inspired by JOOQ - https://www.jooq.org/ 6 | - based on https://github.com/relops/sqlc 7 | - we built this because we couldn't find a library similar to JOOQ but in Golang in August 2018. 8 | 9 | Maintainers (in alphabetical order) 10 | - @ppong 11 | - @ScottyFillups 12 | 13 | Contributors (in alphabetical order) 14 | - @arnold-yip 15 | - @CatastrophiClam 16 | - @ctuong 17 | - @daanielww 18 | - @erikterwiel 19 | - @JerryXie98 20 | - @joeldomjjd 21 | - @marceloneil 22 | - @MingjiaNi 23 | - @tigsss 24 | - @zahin-mohammad 25 | -------------------------------------------------------------------------------- /cmd/gooq/generator/root.go: -------------------------------------------------------------------------------- 1 | package generator 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | 7 | "github.com/spf13/cobra" 8 | ) 9 | 10 | var ( 11 | rootCmd = &cobra.Command{} 12 | configFile string 13 | ) 14 | 15 | func init() { 16 | generateDatabaseModelCommand.PersistentFlags().BoolVarP( 17 | &generateDatabaseModelCommandUseDocker, "docker", "d", true, "whether to use dockerized db") 18 | generateDatabaseModelCommand.PersistentFlags().StringVarP( 19 | &generateDatabaseModelConfigFilePath, "config-file", "f", "", "path to configuration file") 20 | rootCmd.AddCommand(generateDatabaseModelCommand) 21 | } 22 | 23 | func Execute() { 24 | if err := rootCmd.Execute(); err != nil { 25 | fmt.Println(err) 26 | os.Exit(1) 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /pkg/generator/generator.go: -------------------------------------------------------------------------------- 1 | package generator 2 | 3 | import ( 4 | "github.com/jmoiron/sqlx" 5 | "github.com/lumina-tech/gooq/pkg/generator/metadata" 6 | "github.com/lumina-tech/gooq/pkg/generator/plugin" 7 | "github.com/lumina-tech/gooq/pkg/generator/postgres" 8 | ) 9 | 10 | type Generator struct { 11 | plugins []plugin.Plugin 12 | } 13 | 14 | func NewGenerator( 15 | plugins ...plugin.Plugin, 16 | ) *Generator { 17 | return &Generator{plugins: plugins} 18 | } 19 | 20 | func (gen *Generator) Run( 21 | db *sqlx.DB, 22 | ) error { 23 | loader := postgres.NewPostgresLoader() 24 | data, err := metadata.NewData(db, loader) 25 | if err != nil { 26 | return err 27 | } 28 | for _, plugin := range gen.plugins { 29 | if err := plugin.GenerateCode(data); err != nil { 30 | return err 31 | } 32 | } 33 | return nil 34 | } 35 | -------------------------------------------------------------------------------- /pkg/generator/plugin/enumgen/enumgen.go: -------------------------------------------------------------------------------- 1 | package enumgen 2 | 3 | import ( 4 | "sort" 5 | "strings" 6 | "time" 7 | 8 | "github.com/lumina-tech/gooq/pkg/generator/utils" 9 | 10 | "github.com/lumina-tech/gooq/pkg/generator/metadata" 11 | ) 12 | 13 | type EnumGenerator struct { 14 | outputFile string 15 | } 16 | 17 | func NewEnumGenerator( 18 | outputFile string, 19 | ) *EnumGenerator { 20 | return &EnumGenerator{ 21 | outputFile: outputFile, 22 | } 23 | } 24 | 25 | func (gen *EnumGenerator) GenerateCode( 26 | data *metadata.Data, 27 | ) error { 28 | enums := append(data.Enums, data.ReferenceTableEnums...) 29 | sort.SliceStable(enums, func(i, j int) bool { 30 | return strings.Compare(enums[i].Name, enums[j].Name) < 0 31 | }) 32 | args := templateArgs{ 33 | Package: "model", 34 | Timestamp: time.Now().Format(time.RFC3339), 35 | Enums: enums, 36 | } 37 | enumTemplate := utils.GetTemplate(enumTemplate) 38 | return utils.RenderToFile(enumTemplate, gen.outputFile, args) 39 | } 40 | -------------------------------------------------------------------------------- /pkg/gooq/types.go: -------------------------------------------------------------------------------- 1 | package gooq 2 | 3 | import ( 4 | "context" 5 | "database/sql" 6 | 7 | "github.com/jmoiron/sqlx" 8 | "gopkg.in/guregu/null.v3" 9 | ) 10 | 11 | type Named interface { 12 | GetName() string 13 | GetQualifiedName() string 14 | } 15 | 16 | type Renderable interface { 17 | Render(builder *Builder) 18 | } 19 | 20 | type Selectable interface { 21 | Renderable 22 | } 23 | 24 | type DatabaseConstraint struct { 25 | Name string 26 | Columns []Field 27 | Predicate null.String 28 | } 29 | 30 | type DBInterface interface { 31 | sqlx.Execer 32 | sqlx.ExecerContext 33 | sqlx.Preparer 34 | sqlx.PreparerContext 35 | sqlx.Queryer 36 | sqlx.QueryerContext 37 | } 38 | 39 | type TxInterface interface { 40 | DBInterface 41 | Commit() error 42 | Rollback() error 43 | } 44 | 45 | type Executable interface { 46 | Exec(Dialect, DBInterface) (sql.Result, error) 47 | ExecWithContext(context.Context, Dialect, DBInterface) (sql.Result, error) 48 | } 49 | 50 | type Fetchable interface { 51 | Fetch(Dialect, DBInterface) (*sqlx.Rows, error) 52 | FetchRow(Dialect, DBInterface) *sqlx.Row 53 | FetchWithContext(context.Context, Dialect, DBInterface) (*sqlx.Rows, error) 54 | FetchRowWithContext(context.Context, Dialect, DBInterface) *sqlx.Row 55 | } 56 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/lumina-tech/gooq 2 | 3 | go 1.13 4 | 5 | require ( 6 | github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5 // indirect 7 | github.com/cenkalti/backoff v2.2.1+incompatible // indirect 8 | github.com/containerd/containerd v1.5.6 // indirect 9 | github.com/docker/docker v20.10.8+incompatible // indirect 10 | github.com/docker/go-connections v0.4.0 // indirect 11 | github.com/golang-migrate/migrate v3.5.4+incompatible 12 | github.com/google/go-cmp v0.5.6 // indirect 13 | github.com/google/uuid v1.2.0 14 | github.com/gotestyourself/gotestyourself v2.2.0+incompatible // indirect 15 | github.com/jmoiron/sqlx v1.2.0 16 | github.com/knq/snaker v0.0.0-20181215144011-2bc8a4db4687 17 | github.com/lib/pq v1.2.0 18 | github.com/moby/term v0.0.0-20210619224110-3f7ff695adc6 // indirect 19 | github.com/morikuni/aec v1.0.0 // indirect 20 | github.com/ory/dockertest v3.3.5+incompatible 21 | github.com/spf13/cobra v1.0.0 22 | github.com/spf13/viper v1.4.0 23 | github.com/stretchr/testify v1.7.0 24 | golang.org/x/time v0.0.0-20210723032227-1f47c861a9ac // indirect 25 | golang.org/x/tools v0.0.0-20210106214847-113979e3529a 26 | google.golang.org/appengine v1.6.7 // indirect 27 | google.golang.org/grpc v1.41.0 // indirect 28 | gopkg.in/guregu/null.v3 v3.4.0 29 | ) 30 | -------------------------------------------------------------------------------- /pkg/gooq/delete_test.go: -------------------------------------------------------------------------------- 1 | package gooq 2 | 3 | import "testing" 4 | 5 | var deleteTestCases = []TestCase{ 6 | { 7 | Constructed: Delete(Table1).Where(Table1.Column1.Eq(String("foo"))), 8 | ExpectedStmt: `DELETE FROM public.table1 WHERE "table1".column1 = $1`, 9 | }, 10 | { 11 | Constructed: Delete(Table1).Using(Table2).On(Table1.Column1.Eq(Table2.Column2)), 12 | ExpectedStmt: `DELETE FROM public.table1 USING public.table2 WHERE "table1".column1 = "table2".column2`, 13 | }, 14 | { 15 | Constructed: Delete(Table1).Using(Table2).On(Table1.Column1.Eq(Table2.Column2)).Where(Table1.Column1.Eq(String("foo"))), 16 | ExpectedStmt: `DELETE FROM public.table1 USING public.table2 WHERE "table1".column1 = "table2".column2 AND "table1".column1 = $1`, 17 | }, 18 | { 19 | Constructed: Delete(Table1).Using(Select().From(Table2).As("foo")).On(Table1.Column1.Eq(Table2.Column2)), 20 | ExpectedStmt: `DELETE FROM public.table1 USING (SELECT * FROM public.table2) AS "foo" WHERE "table1".column1 = "table2".column2`, 21 | }, 22 | { 23 | Constructed: Delete(Table1).Where(Table1.Column1.Eq(String("foo"))).Returning(Table1.Column1), 24 | ExpectedStmt: `DELETE FROM public.table1 WHERE "table1".column1 = $1 RETURNING "table1".column1`, 25 | }, 26 | } 27 | 28 | func TestDelete(t *testing.T) { 29 | runTestCases(t, deleteTestCases) 30 | } 31 | -------------------------------------------------------------------------------- /pkg/nullable/stringarray_test.go: -------------------------------------------------------------------------------- 1 | package nullable_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/lib/pq" 7 | 8 | "github.com/stretchr/testify/require" 9 | 10 | "github.com/lumina-tech/gooq/pkg/nullable" 11 | ) 12 | 13 | func TestStringArrayScan(t *testing.T) { 14 | stringArray := nullable.StringArrayFrom(nil) 15 | require.False(t, stringArray.Valid) 16 | require.Nil(t, stringArray.StringArray) 17 | 18 | bytes := []byte("{\"hello\",\"world\"}") 19 | err := stringArray.Scan(bytes) 20 | require.NoError(t, err) 21 | require.True(t, stringArray.Valid) 22 | require.Equal(t, pq.StringArray{"hello", "world"}, stringArray.StringArray) 23 | 24 | value, err := stringArray.Value() 25 | require.NoError(t, err) 26 | require.Equal(t, string(bytes), value) 27 | } 28 | 29 | func TestStringArrayMarshalText(t *testing.T) { 30 | stringArray := nullable.StringArrayFrom([]string{"test"}) 31 | 32 | err := stringArray.UnmarshalText(nil) 33 | require.NoError(t, err) 34 | require.False(t, stringArray.Valid) 35 | require.Nil(t, stringArray.StringArray) 36 | 37 | text := []byte("[\"hello\",\"world\"]") 38 | err = stringArray.UnmarshalText(text) 39 | require.NoError(t, err) 40 | require.True(t, stringArray.Valid) 41 | require.Equal(t, pq.StringArray{"hello", "world"}, stringArray.StringArray) 42 | 43 | marshalled, err := stringArray.MarshalText() 44 | require.NoError(t, err) 45 | require.Equal(t, text, marshalled) 46 | } 47 | -------------------------------------------------------------------------------- /pkg/nullable/bigfloat.go: -------------------------------------------------------------------------------- 1 | package nullable 2 | 3 | import ( 4 | "database/sql" 5 | "database/sql/driver" 6 | "fmt" 7 | "math/big" 8 | ) 9 | 10 | type BigFloat struct { 11 | BigFloat big.Float 12 | Valid bool //Valid is true if BigFloat is not NULL 13 | } 14 | 15 | func BigFloatFrom(value big.Float) BigFloat { 16 | bigFloat := BigFloat{ 17 | Valid: true, 18 | } 19 | bigFloat.BigFloat = value 20 | 21 | return bigFloat 22 | } 23 | 24 | // Scan implements the Scanner interface. 25 | func (b *BigFloat) Scan(v interface{}) error { 26 | if v == nil { 27 | b.Valid = false 28 | return nil 29 | } 30 | var i sql.NullString 31 | if err := i.Scan(v); err != nil { 32 | return err 33 | } 34 | if _, ok := b.BigFloat.SetString(i.String); ok { 35 | return nil 36 | } 37 | return fmt.Errorf("Could not scan type %T into BigFloat", v) 38 | } 39 | 40 | // Value implements the driver Valuer interface. 41 | func (b BigFloat) Value() (driver.Value, error) { 42 | return b.Value() 43 | } 44 | 45 | // MarshalText implements encoding.TextMarshaler. 46 | // It will encode a blank BigFloat when this BigFloat is null. 47 | func (b BigFloat) MarshalText() ([]byte, error) { 48 | return b.BigFloat.MarshalText() 49 | } 50 | 51 | // UnmarshalText implements encoding.TextUnmarshaler. 52 | // It will unmarshal to a null BigFloat if the input is a null BigFloat. 53 | func (b *BigFloat) UnmarshalText(text []byte) error { 54 | return b.BigFloat.UnmarshalText(text) 55 | } 56 | -------------------------------------------------------------------------------- /pkg/gooq/constant.go: -------------------------------------------------------------------------------- 1 | package gooq 2 | 3 | // Base on https://www.jooq.org/javadoc/latest/ 4 | 5 | type ConflictAction string 6 | type Dialect int 7 | type JoinType int 8 | 9 | const ( 10 | Sqlite Dialect = iota 11 | MySQL 12 | Postgres 13 | ) 14 | 15 | const ( 16 | ConflictActionNil = ConflictAction("") 17 | ConflictActionDoNothing ConflictAction = "DO NOTHING" 18 | ConflictActionDoUpdate ConflictAction = "DO UPDATE" 19 | ) 20 | 21 | const ( 22 | Join JoinType = iota 23 | LeftOuterJoin 24 | NotJoined 25 | ) 26 | 27 | type LockingType int 28 | 29 | const ( 30 | LockingTypeNone LockingType = iota 31 | LockingTypeUpdate 32 | LockingTypeNoKeyUpdate 33 | LockingTypeShare 34 | LockingTypeKeyShare 35 | ) 36 | 37 | func (t LockingType) String() string { 38 | switch t { 39 | case LockingTypeUpdate: 40 | return "FOR UPDATE" 41 | case LockingTypeNoKeyUpdate: 42 | return "FOR NO KEY UPDATE" 43 | case LockingTypeShare: 44 | return "FOR SHARE" 45 | case LockingTypeKeyShare: 46 | return "FOR KEY SHARE" 47 | default: 48 | return "" 49 | } 50 | } 51 | 52 | type LockingOption int 53 | 54 | const ( 55 | LockingOptionNone LockingOption = iota 56 | LockingOptionNoWait 57 | LockingOptionSkipLocked 58 | ) 59 | 60 | func (t LockingOption) String() string { 61 | switch t { 62 | case LockingOptionNoWait: 63 | return "NOWAIT" 64 | case LockingOptionSkipLocked: 65 | return "SKIP LOCKED" 66 | default: 67 | return "" 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /pkg/gooq/table.go: -------------------------------------------------------------------------------- 1 | package gooq 2 | 3 | import ( 4 | "fmt" 5 | 6 | "gopkg.in/guregu/null.v3" 7 | ) 8 | 9 | type Table interface { 10 | Named 11 | Selectable 12 | GetSchema() string 13 | GetUnqualifiedName() string 14 | } 15 | 16 | type TableImpl struct { 17 | name string 18 | schema string 19 | alias null.String 20 | } 21 | 22 | func NewTable(schema, name string) *TableImpl { 23 | return &TableImpl{ 24 | name: name, 25 | schema: schema, 26 | } 27 | } 28 | 29 | func (t *TableImpl) Initialize(schema, name string) { 30 | t.schema = schema 31 | t.name = name 32 | } 33 | 34 | func (t *TableImpl) As(alias string) *TableImpl { 35 | return &TableImpl{ 36 | name: t.name, 37 | schema: t.schema, 38 | alias: null.StringFrom(alias), 39 | } 40 | } 41 | 42 | func (t TableImpl) GetAlias() null.String { 43 | return t.alias 44 | } 45 | 46 | func (t TableImpl) GetName() string { 47 | return t.name 48 | } 49 | 50 | func (t TableImpl) GetQualifiedName() string { 51 | if t.schema == "" { 52 | return t.name 53 | } 54 | return fmt.Sprintf("%s.%s", t.schema, t.name) 55 | } 56 | 57 | func (t TableImpl) GetUnqualifiedName() string { 58 | if t.alias.Valid { 59 | return t.alias.String 60 | } 61 | return t.name 62 | } 63 | 64 | func (t TableImpl) GetSchema() string { 65 | return t.schema 66 | } 67 | 68 | func (t *TableImpl) Render( 69 | builder *Builder, 70 | ) { 71 | builder.Print(t.GetQualifiedName()) 72 | if t.alias.Valid { 73 | builder.Printf(" AS \"%s\"", t.alias.String) 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /pkg/nullable/jsonb.go: -------------------------------------------------------------------------------- 1 | package nullable 2 | 3 | import ( 4 | "database/sql/driver" 5 | ) 6 | 7 | type Jsonb struct { 8 | Jsonb []byte 9 | Valid bool // Valid is true if Jsonb is not NULL 10 | } 11 | 12 | // JsonbFrom creates a new Jsonb that will never be blank. 13 | func JsonbFrom(value []byte) Jsonb { 14 | jsonb := Jsonb{ 15 | Valid: value != nil, 16 | } 17 | if value != nil { 18 | jsonb.Jsonb = make([]byte, len(value)) 19 | copy(jsonb.Jsonb, value) 20 | } 21 | return jsonb 22 | } 23 | 24 | func (self Jsonb) MarshalText() ([]byte, error) { 25 | if !self.Valid { 26 | return []byte{}, nil 27 | } 28 | return []byte(self.Jsonb), nil 29 | } 30 | 31 | func (self *Jsonb) UnmarshalText(data []byte) error { 32 | str := string(data) 33 | if str == "" { 34 | self.Jsonb = nil 35 | self.Valid = false 36 | return nil 37 | } 38 | self.Valid = data != nil 39 | self.Jsonb = data 40 | return nil 41 | } 42 | 43 | // Scan implements the Scanner interface. 44 | func (self *Jsonb) Scan(v interface{}) error { 45 | if v == nil { 46 | self.Valid = false 47 | return nil 48 | } 49 | switch x := v.(type) { 50 | case []byte: 51 | // must copy bytes to Jsonb. 52 | // cannot just assign jsonb to v because v might be reused 53 | self.Valid = true 54 | self.Jsonb = make([]byte, len(x)) 55 | copy(self.Jsonb, x) 56 | } 57 | return nil 58 | } 59 | 60 | // Value implements the driver Valuer interface. 61 | func (self Jsonb) Value() (driver.Value, error) { 62 | if !self.Valid { 63 | return nil, nil 64 | } 65 | return self.Jsonb, nil 66 | } 67 | -------------------------------------------------------------------------------- /pkg/gooq/literal.go: -------------------------------------------------------------------------------- 1 | package gooq 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/google/uuid" 7 | ) 8 | 9 | func keyword(value string) Expression { 10 | return newKeywordExpression(value) 11 | } 12 | 13 | func Literal(value interface{}) Expression { 14 | return newLiteralExpression(value) 15 | } 16 | 17 | func Bool(value bool) BoolExpression { 18 | expr := &boolExpressionImpl{} 19 | expr.expressionImpl.initLiteralExpression(expr, value) 20 | return expr 21 | } 22 | 23 | func DateTime(value time.Time) DateTimeExpression { 24 | expr := &dateTimeExpressionImpl{} 25 | expr.expressionImpl.initLiteralExpression(expr, value) 26 | return expr 27 | } 28 | 29 | func Int64(value int64) NumericExpression { 30 | expr := &numericExpressionImpl{} 31 | expr.expressionImpl.initLiteralExpression(expr, value) 32 | return expr 33 | } 34 | 35 | func Float64(value float64) NumericExpression { 36 | expr := &numericExpressionImpl{} 37 | expr.expressionImpl.initLiteralExpression(expr, value) 38 | return expr 39 | } 40 | 41 | func String(value string) StringExpression { 42 | expr := &stringExpressionImpl{} 43 | expr.expressionImpl.initLiteralExpression(expr, value) 44 | return expr 45 | } 46 | 47 | func UUID(value uuid.UUID) UUIDExpression { 48 | expr := &uuidExpressionImpl{} 49 | expr.expressionImpl.initLiteralExpression(expr, value) 50 | return expr 51 | } 52 | 53 | /////////////////////////////////////////////////////////////////////////////// 54 | // Other literals 55 | /////////////////////////////////////////////////////////////////////////////// 56 | 57 | var ( 58 | Asterisk = keyword("*") 59 | ) 60 | -------------------------------------------------------------------------------- /pkg/gooq/utils.go: -------------------------------------------------------------------------------- 1 | package gooq 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/jmoiron/sqlx" 7 | ) 8 | 9 | func ScanRow( 10 | db DBInterface, stmt Fetchable, results interface{}, 11 | ) error { 12 | row := stmt.FetchRow(Postgres, db) 13 | return row.StructScan(results) 14 | } 15 | 16 | func ScanRows( 17 | db DBInterface, stmt Fetchable, results interface{}, 18 | ) error { 19 | rows, err := stmt.Fetch(Postgres, db) 20 | if err != nil { 21 | return err 22 | } 23 | defer rows.Close() 24 | return sqlx.StructScan(rows, results) 25 | } 26 | 27 | func ScanCount( 28 | db DBInterface, stmt Fetchable, 29 | ) (int, error) { 30 | row := stmt.FetchRow(Postgres, db) 31 | count := 0 32 | if err := row.Scan(&count); err != nil { 33 | return 0, err 34 | } 35 | return count, nil 36 | } 37 | 38 | func ScanRowWithContext( 39 | ctx context.Context, db DBInterface, stmt Fetchable, results interface{}, 40 | ) error { 41 | row := stmt.FetchRowWithContext(ctx, Postgres, db) 42 | return row.StructScan(results) 43 | } 44 | 45 | func ScanRowsWithContext( 46 | ctx context.Context, db DBInterface, stmt Fetchable, results interface{}, 47 | ) error { 48 | rows, err := stmt.FetchWithContext(ctx, Postgres, db) 49 | if err != nil { 50 | return err 51 | } 52 | defer rows.Close() 53 | return sqlx.StructScan(rows, results) 54 | } 55 | 56 | func ScanCountWithContext( 57 | ctx context.Context, db DBInterface, stmt Fetchable, 58 | ) (int, error) { 59 | row := stmt.FetchRowWithContext(ctx, Postgres, db) 60 | count := 0 61 | if err := row.Scan(&count); err != nil { 62 | return 0, err 63 | } 64 | return count, nil 65 | } 66 | -------------------------------------------------------------------------------- /pkg/generator/plugin/modelgen/types.go: -------------------------------------------------------------------------------- 1 | package modelgen 2 | 3 | import ( 4 | "github.com/lumina-tech/gooq/pkg/generator/metadata" 5 | "gopkg.in/guregu/null.v3" 6 | ) 7 | 8 | type ModelOverride struct { 9 | Models map[string]ModelOverrideModel `yaml:"models"` 10 | } 11 | 12 | type ModelOverrideModel struct { 13 | Fields map[string]ModelOverrideModelField `yaml:"fields"` 14 | } 15 | 16 | type ModelOverrideModelField struct { 17 | OverrideType string `yaml:"overrideType"` 18 | } 19 | 20 | type TemplateArgs struct { 21 | Timestamp string 22 | Package string 23 | Schema string 24 | Tables []TableTemplateArgs 25 | } 26 | 27 | type TableTemplateArgs struct { 28 | TableName string 29 | TableType string 30 | TableSingletonName string 31 | ModelType string 32 | QualifiedModelType string 33 | ReferenceTableEnumType string 34 | IsReferenceTable bool 35 | Fields []FieldTemplateArgs 36 | Constraints []ConstraintTemplateArgs 37 | ForeignKeyConstraints []ForeignKeyConstraintTemplateArgs 38 | } 39 | 40 | type ConstraintTemplateArgs struct { 41 | Name string 42 | Columns []string 43 | Predicate null.String 44 | } 45 | 46 | type ForeignKeyConstraintTemplateArgs struct { 47 | Name string 48 | ColumnName string 49 | ForeignTableName string 50 | ForeignColumnName string 51 | } 52 | 53 | type FieldTemplateArgs struct { 54 | GooqType string 55 | Name string 56 | Type string 57 | } 58 | 59 | type EnumType struct { 60 | Name string 61 | Values []metadata.EnumValueMetadata 62 | IsReferenceTable bool 63 | ReferenceTableModelType string 64 | } 65 | -------------------------------------------------------------------------------- /pkg/nullable/stringarray.go: -------------------------------------------------------------------------------- 1 | package nullable 2 | 3 | import ( 4 | "database/sql/driver" 5 | "encoding/json" 6 | 7 | "github.com/lib/pq" 8 | ) 9 | 10 | type StringArray struct { 11 | StringArray pq.StringArray 12 | Valid bool // Valid is true if StringArray is not NULL 13 | } 14 | 15 | // StringArrayFrom creates a new StringArray that will never be blank. 16 | func StringArrayFrom(value []string) StringArray { 17 | stringArray := StringArray{ 18 | Valid: value != nil, 19 | } 20 | if value != nil { 21 | stringArray.StringArray = make([]string, len(value)) 22 | copy(stringArray.StringArray, value) 23 | } 24 | return stringArray 25 | } 26 | 27 | func (self StringArray) MarshalText() ([]byte, error) { 28 | if !self.Valid { 29 | return []byte{}, nil 30 | } 31 | bytes, err := json.Marshal(self.StringArray) 32 | if err != nil { 33 | return nil, err 34 | } 35 | return bytes, nil 36 | } 37 | 38 | func (self *StringArray) UnmarshalText(data []byte) error { 39 | if len(data) == 0 { 40 | self.StringArray = nil 41 | self.Valid = false 42 | return nil 43 | } 44 | arr := make([]string, 0) 45 | if err := json.Unmarshal(data, &arr); err != nil { 46 | return err 47 | } 48 | self.Valid = true 49 | self.StringArray = arr 50 | return nil 51 | } 52 | 53 | // Scan implements the Scanner interface. 54 | func (self *StringArray) Scan(v interface{}) error { 55 | if v == nil { 56 | self.Valid = false 57 | return nil 58 | } 59 | err := self.StringArray.Scan(v) 60 | if err == nil { 61 | self.Valid = true 62 | } 63 | return err 64 | } 65 | 66 | // Value implements the driver Valuer interface. 67 | func (self StringArray) Value() (driver.Value, error) { 68 | if !self.Valid { 69 | return nil, nil 70 | } 71 | return self.StringArray.Value() 72 | } 73 | -------------------------------------------------------------------------------- /pkg/gooq/update_test.go: -------------------------------------------------------------------------------- 1 | package gooq 2 | 3 | import "testing" 4 | 5 | var updateTestCases = []TestCase{ 6 | { 7 | Constructed: Update(Table1).Set(Table1.Column1, "10"), 8 | ExpectedStmt: `UPDATE public.table1 SET column1 = $1`, 9 | }, 10 | { 11 | Constructed: Update(Table1).Set(Table1.Column3, Table1.Column4.Add(Int64(10))), 12 | ExpectedStmt: `UPDATE public.table1 SET column3 = "table1".column4 + $1`, 13 | }, 14 | { 15 | Constructed: Update(Table1).Set(Table1.Column3, Select().From(Table2)), 16 | ExpectedStmt: `UPDATE public.table1 SET column3 = (SELECT * FROM public.table2)`, 17 | }, 18 | { 19 | Constructed: Update(Table1).Set(Table1.Column1, "10").Where(Table1.Column2.Eq(String("foo"))), 20 | ExpectedStmt: `UPDATE public.table1 SET column1 = $1 WHERE "table1".column2 = $2`, 21 | }, 22 | { 23 | Constructed: Update(Table1).Set(Table1.Column1, Table2.Column1). 24 | From(Table2).Where(Table1.Column2.Eq(Table2.Column2)), 25 | ExpectedStmt: `UPDATE public.table1 SET column1 = "table2".column1 FROM public.table2 WHERE "table1".column2 = "table2".column2`, 26 | }, 27 | { 28 | Constructed: Update(Table1).Set(Table1.Column1, Table2.Column1). 29 | From(Select().From(Table2).As("foo")).Where(Table1.Column2.Eq(Table2.Column2)), 30 | ExpectedStmt: `UPDATE public.table1 SET column1 = "table2".column1 FROM (SELECT * FROM public.table2) AS "foo" WHERE "table1".column2 = "table2".column2`, 31 | }, 32 | { 33 | Constructed: Update(Table1).Set(Table1.Column1, "10").OnConflictDoNothing(), 34 | ExpectedStmt: `UPDATE public.table1 SET column1 = $1 ON CONFLICT DO NOTHING`, 35 | }, 36 | { 37 | Constructed: Update(Table1).Set(Table1.Column1, "10").Returning(Table1.Column1), 38 | ExpectedStmt: `UPDATE public.table1 SET column1 = $1 RETURNING "table1".column1`, 39 | }, 40 | } 41 | 42 | func TestUpdate(t *testing.T) { 43 | runTestCases(t, updateTestCases) 44 | } 45 | -------------------------------------------------------------------------------- /pkg/gooq/operator.go: -------------------------------------------------------------------------------- 1 | package gooq 2 | 3 | type Operator string 4 | 5 | var ( 6 | OperatorNil = Operator("") 7 | 8 | OperatorIsNull = Operator("IS NULL") 9 | OperatorIsNotNull = Operator("IS NOT NULL") 10 | 11 | OperatorAsc = Operator("ASC") 12 | OperatorDesc = Operator("DESC") 13 | 14 | // logical operators 15 | // https://www.postgresql.org/docs/11/functions-logical.html 16 | OperatorAnd = Operator("AND") 17 | OperatorOr = Operator("OR") 18 | OperatorNot = Operator("NOT") 19 | 20 | // comparison functions and operators 21 | // https://www.postgresql.org/docs/11/functions-comparison.html 22 | OperatorLt = Operator("<") 23 | OperatorLte = Operator("<=") 24 | OperatorGt = Operator(">") 25 | OperatorGte = Operator(">=") 26 | OperatorEq = Operator("=") 27 | OperatorNotEq = Operator("!=") 28 | OperatorLike = Operator("LIKE") 29 | OperatorILike = Operator("ILIKE") 30 | OperatorIsDistinctFrom = Operator("IS DISTINCT FROM") 31 | 32 | // mathematical operators 33 | // https://www.postgresql.org/docs/11/functions-math.html 34 | // [Good First Issue][Help Wanted] TODO: implement remaining 35 | OperatorAdd = Operator("+") 36 | OperatorSub = Operator("-") 37 | OperatorMult = Operator("*") 38 | OperatorDiv = Operator("/") 39 | OperatorSqrt = Operator("|/") 40 | 41 | // Table 9.13. Bit String Operators 42 | // https://www.postgresql.org/docs/11/functions-bitstring.html 43 | // [Good First Issue][Help Wanted] TODO: implement remaining 44 | 45 | // 9.7.1. LIKE 46 | // 9.7.2. SIMILAR TO Regular Expressions 47 | // 9.7.3. POSIX Regular Expressions 48 | // https://www.postgresql.org/docs/11/functions-matching.html 49 | // [Good First Issue][Help Wanted] TODO: implement remaining 50 | 51 | // Array Comparisons 52 | // https://www.postgresql.org/docs/11/functions-comparisons.html 53 | OperatorIn = Operator("IN") 54 | OperatorNotIn = Operator("NOT IN") 55 | ) 56 | 57 | func (op Operator) String() string { 58 | return string(op) 59 | } 60 | -------------------------------------------------------------------------------- /examples/swapi/model/swapi_model.generated.go: -------------------------------------------------------------------------------- 1 | // THIS FILE WAS AUTOGENERATED - ANY EDITS TO THIS WILL BE LOST WHEN IT IS REGENERATED 2 | 3 | package model 4 | 5 | import ( 6 | "github.com/google/uuid" 7 | "github.com/lumina-tech/gooq/pkg/nullable" 8 | ) 9 | 10 | type ColorReferenceTable struct { 11 | Value string `db:"value" json:"value"` 12 | } 13 | 14 | type Person struct { 15 | ID uuid.UUID `db:"id" json:"id"` 16 | Name string `db:"name" json:"name"` 17 | Height float64 `db:"height" json:"height"` 18 | Mass float64 `db:"mass" json:"mass"` 19 | HairColor Color `db:"hair_color" json:"hair_color"` 20 | SkinColor Color `db:"skin_color" json:"skin_color"` 21 | EyeColor Color `db:"eye_color" json:"eye_color"` 22 | BirthYear int `db:"birth_year" json:"birth_year"` 23 | Gender Gender `db:"gender" json:"gender"` 24 | HomeWorld string `db:"home_world" json:"home_world"` 25 | SpeciesID uuid.UUID `db:"species_id" json:"species_id"` 26 | WeaponID nullable.UUID `db:"weapon_id" json:"weapon_id"` 27 | Status string `db:"status" json:"status"` 28 | } 29 | 30 | type Species struct { 31 | ID uuid.UUID `db:"id" json:"id"` 32 | Name string `db:"name" json:"name"` 33 | Classification string `db:"classification" json:"classification"` 34 | AverageHeight float64 `db:"average_height" json:"average_height"` 35 | AverageLifespan nullable.BigFloat `db:"average_lifespan" json:"average_lifespan"` 36 | HairColor Color `db:"hair_color" json:"hair_color"` 37 | SkinColor Color `db:"skin_color" json:"skin_color"` 38 | EyeColor Color `db:"eye_color" json:"eye_color"` 39 | HomeWorld string `db:"home_world" json:"home_world"` 40 | Language string `db:"language" json:"language"` 41 | } 42 | 43 | type Weapon struct { 44 | ID uuid.UUID `db:"id" json:"id"` 45 | Damage int `db:"damage" json:"damage"` 46 | Price int `db:"price" json:"price"` 47 | } 48 | -------------------------------------------------------------------------------- /pkg/database/dockerized_db.go: -------------------------------------------------------------------------------- 1 | package database 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | 7 | "github.com/lumina-tech/gooq/pkg/generator/plugin/modelgen" 8 | 9 | "github.com/jmoiron/sqlx" 10 | "github.com/ory/dockertest" 11 | ) 12 | 13 | type DockerizedDB struct { 14 | DB *sqlx.DB 15 | pool *dockertest.Pool 16 | resource *dockertest.Resource 17 | } 18 | 19 | type DatabaseConfig struct { 20 | Host string 21 | Port int64 22 | Username string 23 | Password string 24 | DatabaseName string 25 | SSLMode string 26 | MigrationPath string 27 | ModelPath string 28 | TablePath string 29 | ModelOverrides modelgen.ModelOverride 30 | } 31 | 32 | func NewDockerizedDB( 33 | config *DatabaseConfig, dockerTag string, 34 | ) *DockerizedDB { 35 | pool, err := dockertest.NewPool("") 36 | if err != nil { 37 | panic(fmt.Sprintf("Could not connect to docker: %s", err)) 38 | } 39 | resource, err := pool.Run("postgres", dockerTag, []string{ 40 | fmt.Sprintf("POSTGRES_USER=%s", config.Username), 41 | fmt.Sprintf("POSTGRES_PASSWORD=%s", config.Password), 42 | fmt.Sprintf("POSTGRES_DB=%s", config.DatabaseName), 43 | }) 44 | if err != nil { 45 | panic(fmt.Sprintf("could not start resource: %s", err)) 46 | } 47 | result := DockerizedDB{ 48 | pool: pool, 49 | resource: resource, 50 | } 51 | 52 | if err = pool.Retry(func() error { 53 | hostPort := strings.Split(resource.GetHostPort(fmt.Sprintf("%d/tcp", config.Port)), ":") 54 | host := hostPort[0] 55 | port := hostPort[1] 56 | connStr := fmt.Sprintf("user=%s password=%s host=%s port=%s dbname=%s sslmode=%s", 57 | config.Username, config.Password, host, port, config.DatabaseName, config.SSLMode) 58 | fmt.Println(connStr) 59 | db, err := sqlx.Open("postgres", connStr) 60 | if err != nil { 61 | return err 62 | } 63 | result.DB = db 64 | return db.Ping() 65 | }); err != nil { 66 | panic(fmt.Sprintf("could not connect to docker: %s", err)) 67 | } 68 | return &result 69 | } 70 | 71 | func (db *DockerizedDB) Close() error { 72 | return db.pool.Purge(db.resource) 73 | } 74 | -------------------------------------------------------------------------------- /pkg/database/migration.go: -------------------------------------------------------------------------------- 1 | package database 2 | 3 | import ( 4 | "database/sql" 5 | "fmt" 6 | "log" 7 | 8 | // import file base migration 9 | _ "github.com/golang-migrate/migrate/source/file" 10 | // import pq driver 11 | _ "github.com/lib/pq" 12 | 13 | "github.com/golang-migrate/migrate" 14 | "github.com/golang-migrate/migrate/database/postgres" 15 | "github.com/jmoiron/sqlx" 16 | ) 17 | 18 | func GetConnectionString( 19 | config *DatabaseConfig, 20 | ) string { 21 | var connStr = fmt.Sprintf("user=%s password=%s host=%s dbname=%s", 22 | config.Username, config.Password, config.Host, config.DatabaseName) 23 | if config.SSLMode != "" { 24 | connStr = fmt.Sprintf("%s sslmode=%s", connStr, config.SSLMode) 25 | } 26 | return connStr 27 | } 28 | 29 | func NewDatabase( 30 | config *DatabaseConfig, 31 | ) *sqlx.DB { 32 | var connStr = GetConnectionString(config) 33 | log.Printf("connecting to database %s@%s/%s", 34 | config.Username, config.Host, config.DatabaseName) 35 | db := sqlx.MustOpen("postgres", connStr) 36 | err := db.Ping() 37 | if err != nil { 38 | log.Fatal("fail to ping database", err) 39 | } 40 | log.Print("connected to database") 41 | return db 42 | } 43 | 44 | func MigrateDatabase( 45 | db *sql.DB, migrationPath string, 46 | ) { 47 | driver, err := postgres.WithInstance(db, &postgres.Config{}) 48 | if err != nil { 49 | log.Fatal(err) 50 | } 51 | migrationDir := fmt.Sprintf("file://%s", migrationPath) 52 | m, err := migrate.NewWithDatabaseInstance(migrationDir, "postgres", driver) 53 | if err != nil { 54 | log.Fatal(err) 55 | } 56 | log.Printf("running database migrations dir=%s\n", migrationPath) 57 | version, dirty, _ := m.Version() 58 | log.Printf("current database schema version=%d dirty=%v\n", version, dirty) 59 | 60 | err = m.Up() 61 | if err != nil { 62 | if err != migrate.ErrNoChange { 63 | // not strictly necessary, but close the migration client in order to clean up 64 | // potentially dangling resources like locks 65 | closeMigrate(m) 66 | log.Fatal(err) 67 | } 68 | log.Print("no database migrations ran") 69 | } else { 70 | log.Print("successfully ran database migrations") 71 | } 72 | } 73 | 74 | func closeMigrate(migrate *migrate.Migrate) { 75 | sourceErr, databaseErr := migrate.Close() 76 | if sourceErr != nil { 77 | log.Fatal("error closing migration source", sourceErr) 78 | } 79 | if databaseErr != nil { 80 | log.Fatal("error closing database source", databaseErr) 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /pkg/generator/plugin/enumgen/template.go: -------------------------------------------------------------------------------- 1 | package enumgen 2 | 3 | const enumTemplate = ` 4 | // THIS FILE WAS AUTOGENERATED - ANY EDITS TO THIS WILL BE LOST WHEN IT IS REGENERATED 5 | 6 | package {{ .Package }} 7 | 8 | import "github.com/lumina-tech/gooq/pkg/gooq" 9 | 10 | {{ range $_, $enum := .Enums -}} 11 | {{- $type := (snakeToCamelID $enum.Name) -}} 12 | type {{ $type }} string 13 | 14 | const ( 15 | {{- range $enum.Values }} 16 | {{ $type }}{{ snakeToCamelID .EnumValue }} = {{ $type }}("{{ .EnumValue }}") 17 | {{- end }} 18 | {{ $type }}Null = {{ $type }}("") 19 | ) 20 | 21 | // String returns the string value of the {{ $type }}. 22 | func (enumType {{ $type }}) String() string { 23 | return string(enumType) 24 | } 25 | 26 | // MarshalText marshals {{ $type }} into text. 27 | func (enumType {{ $type }}) MarshalText() ([]byte, error) { 28 | return []byte(enumType.String()), nil 29 | } 30 | 31 | // UnmarshalText unmarshals {{ $type }} from text. 32 | func (enumType *{{ $type }}) UnmarshalText(text []byte) error { 33 | switch string(text) { 34 | {{- range $enum.Values }} 35 | case "{{ .EnumValue }}": 36 | *enumType = {{ $type }}{{ snakeToCamelID .EnumValue }} 37 | {{- end }} 38 | default: 39 | *enumType = {{ $type }}Null 40 | } 41 | return nil 42 | } 43 | 44 | // MarshalGQL satisfies gqlgen interface for {{ $type }}. 45 | func (enumType {{ $type }}) MarshalGQL(w io.Writer) { 46 | if enumType == {{ $type }}Null { 47 | io.WriteString(w, "null") 48 | } else { 49 | io.WriteString(w, strconv.Quote(enumType.String())) 50 | } 51 | } 52 | 53 | // UnmarshalText satisfies gqlgen interface for {{ $type }}. 54 | func (enumType *{{ $type }}) UnmarshalGQL(v interface{}) error { 55 | switch str := v.(type) { 56 | case string: 57 | return enumType.UnmarshalText([]byte(str)) 58 | case nil: 59 | return nil 60 | default: 61 | return fmt.Errorf("invalid enum value %v", v) 62 | } 63 | } 64 | 65 | // Value satisfies the sql/driver.Valuer interface for {{ $type }}. 66 | func (enumType {{ $type }}) Value() (driver.Value, error) { 67 | if enumType == {{ $type }}Null { 68 | return nil, nil 69 | } 70 | return enumType.String(), nil 71 | } 72 | 73 | // Scan satisfies the database/sql.Scanner interface for {{ $type }}. 74 | func (enumType *{{ $type }}) Scan(src interface{}) error { 75 | switch buf := src.(type) { 76 | case []byte: 77 | return enumType.UnmarshalText(buf) 78 | case string: 79 | return enumType.UnmarshalText([]byte(buf)) 80 | case nil: 81 | return nil 82 | default: 83 | return errors.New("invalid {{ $type }}") 84 | } 85 | } 86 | 87 | {{ end }} 88 | ` 89 | -------------------------------------------------------------------------------- /examples/swapi/README.md: -------------------------------------------------------------------------------- 1 | Using sqlx to map nested query 2 | 3 | ``` 4 | type PersonWithSpecies struct { 5 | model.Person 6 | Species *model.Species `db:"species"` 7 | } 8 | 9 | stmt := gooq.Select( 10 | table.Person.Asterisk, 11 | table.Species.Name.As("species.name"), 12 | table.Species.Classification.As("species.name"), 13 | table.Species.AverageHeight.As("species.average_height"), 14 | table.Species.AverageLifespan.As("species.average_lifespan"), 15 | table.Species.HairColor.As("species.hair_color"), 16 | table.Species.SkinColor.As("species.skin_color"), 17 | table.Species.EyeColor.As("species.eye_color"), 18 | table.Species.HomeWorld.As("species.home_world"), 19 | table.Species.Language.As("species.language"), 20 | ).From(table.Person). 21 | Join(table.Species). 22 | On(table.Person.SpeciesID.Eq(table.Species.ID)) 23 | 24 | builder := &gooq.Builder{} 25 | stmt.Render(builder) 26 | fmt.Println(builder.String()) 27 | 28 | var results []PersonWithSpecies 29 | if err := gooq.ScanRows(dockerDB.DB, stmt, &results); err != nil { 30 | fmt.Fprint(os.Stderr, err.Error()) 31 | return 32 | } 33 | ``` 34 | The following sql statement is generated 35 | ``` 36 | SELECT 37 | person.*, 38 | species.name AS "species.name", 39 | species.classification AS "species.name", 40 | species.average_height AS "species.average_height", 41 | species.average_lifespan AS "species.average_lifespan", 42 | species.hair_color AS "species.hair_color", 43 | species.skin_color AS "species.skin_color", 44 | species.eye_color AS "species.eye_color", 45 | species.home_world AS "species.home_world", 46 | species.language AS "species.language" 47 | FROM public.person 48 | JOIN public.species ON person.species_id = species.id 49 | ``` 50 | When results is mashalled as a JSON 51 | ``` 52 | [ 53 | { 54 | "id": "534df3d9-8239-4cad-a6bf-7cd9fe3c82be", 55 | "name": "Frank", 56 | "height": 170.3, 57 | "mass": 150.5, 58 | "hair_color": "black", 59 | "skin_color": "orange", 60 | "eye_color": "brown", 61 | "birth_year": 1998, 62 | "gender": "male", 63 | "home_world": "Runescape", 64 | "species_id": "d0269559-7772-470b-a5df-c67ad59c68dc", 65 | "species": { 66 | "id": "00000000-0000-0000-0000-000000000000", 67 | "name": "Mammal", 68 | "classification": "", 69 | "average_height": 160.5, 70 | "average_lifespan": 70, 71 | "hair_color": "black", 72 | "skin_color": "orange", 73 | "eye_color": "brown", 74 | "home_world": "Earth", 75 | "language": "English" 76 | } 77 | } 78 | ] 79 | ``` -------------------------------------------------------------------------------- /cmd/gooq/generator/generator.go: -------------------------------------------------------------------------------- 1 | package generator 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | 7 | "github.com/jmoiron/sqlx" 8 | "github.com/lumina-tech/gooq/pkg/database" 9 | "github.com/lumina-tech/gooq/pkg/generator" 10 | "github.com/lumina-tech/gooq/pkg/generator/plugin/enumgen" 11 | "github.com/lumina-tech/gooq/pkg/generator/plugin/modelgen" 12 | "github.com/spf13/cobra" 13 | "github.com/spf13/viper" 14 | ) 15 | 16 | var ( 17 | generateDatabaseModelCommandUseDocker bool 18 | generateDatabaseModelConfigFilePath string 19 | ) 20 | 21 | var generateDatabaseModelCommand = &cobra.Command{ 22 | Use: "generate-database-model", 23 | Short: "generate Go models by introspecting the database", 24 | Run: func(cmd *cobra.Command, args []string) { 25 | if err := loadConfig(); err != nil { 26 | _, _ = fmt.Fprint(os.Stderr, "cannot read configuration file:", err) 27 | os.Exit(1) 28 | } 29 | 30 | var config database.DatabaseConfig 31 | if err := viper.Unmarshal(&config); err != nil { 32 | _, _ = fmt.Fprint(os.Stderr, "cannot decode configuration file:", err) 33 | os.Exit(1) 34 | } 35 | if generateDatabaseModelCommandUseDocker { 36 | db := database.NewDockerizedDB(&config, viper.GetString("dockerTag")) 37 | defer db.Close() 38 | database.MigrateDatabase(db.DB.DB, config.MigrationPath) 39 | generateModelsForDB(db.DB, &config) 40 | } else { 41 | db := database.NewDatabase(&config) 42 | generateModelsForDB(db, &config) 43 | } 44 | }, 45 | } 46 | 47 | func loadConfig() error { 48 | viper.SetDefault("dockerTag", "11.4-alpine") 49 | if len(generateDatabaseModelConfigFilePath) != 0 { 50 | viper.SetConfigFile(generateDatabaseModelConfigFilePath) 51 | return viper.ReadInConfig() 52 | } 53 | viper.SetConfigName("gooq") 54 | wd, err := os.Getwd() 55 | if err != nil { 56 | wd = "." 57 | } 58 | viper.AddConfigPath(wd) 59 | return viper.ReadInConfig() 60 | } 61 | 62 | func generateModelsForDB( 63 | db *sqlx.DB, config *database.DatabaseConfig, 64 | ) { 65 | enumOutputFile := fmt.Sprintf("%s/%s_enum.generated.go", config.ModelPath, config.DatabaseName) 66 | modelOutputFile := fmt.Sprintf("%s/%s_model.generated.go", config.ModelPath, config.DatabaseName) 67 | tableOutputFile := fmt.Sprintf("%s/%s_table.generated.go", config.TablePath, config.DatabaseName) 68 | err := generator.NewGenerator( 69 | enumgen.NewEnumGenerator(enumOutputFile), 70 | modelgen.NewModelGenerator(modelOutputFile, "table", "model", &config.ModelOverrides), 71 | modelgen.NewTableGenerator(tableOutputFile, "table", "model", nil), 72 | ).Run(db) 73 | if err != nil { 74 | _, _ = fmt.Fprint(os.Stderr, "cannot generate code:", err) 75 | os.Exit(1) 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /pkg/gooq/gooq_test.go: -------------------------------------------------------------------------------- 1 | package gooq 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/require" 7 | "gopkg.in/guregu/null.v3" 8 | ) 9 | 10 | type testTable struct { 11 | TableImpl 12 | ID UUIDField 13 | Column1 StringField 14 | Column2 StringField 15 | Column3 IntField 16 | Column4 DecimalField 17 | BoolColumn BoolField 18 | DecimalColumn DecimalField 19 | StringColumn StringField 20 | TimeColumn TimeField 21 | } 22 | 23 | func newTestTable(name string) *testTable { 24 | instance := &testTable{} 25 | instance.TableImpl.Initialize("public", name) 26 | instance.ID = NewUUIDField(instance, "id") 27 | instance.Column1 = NewStringField(instance, "column1") 28 | instance.Column2 = NewStringField(instance, "column2") 29 | instance.Column3 = NewIntField(instance, "column3") 30 | instance.Column4 = NewDecimalField(instance, "column4") 31 | instance.BoolColumn = NewBoolField(instance, "bool_column") 32 | instance.DecimalColumn = NewDecimalField(instance, "decimal_column") 33 | instance.StringColumn = NewStringField(instance, "string_column") 34 | instance.TimeColumn = NewTimeField(instance, "time_column") 35 | return instance 36 | } 37 | 38 | func (t *testTable) As(alias string) Table { 39 | instance := newTestTable(t.name) 40 | instance.alias = null.StringFrom(alias) 41 | return instance 42 | } 43 | 44 | var ( 45 | Table1 = newTestTable("table1") 46 | Table2 = newTestTable("table2") 47 | Table3 = newTestTable("table3") 48 | Table1Constraint = DatabaseConstraint{ 49 | Name: "table1_pkey", 50 | Columns: []Field{Table1.Column1}, 51 | } 52 | Table2Constraint = DatabaseConstraint{ 53 | Name: "table2_partial_key", 54 | Columns: []Field{Table2.Column1, Table2.Column2}, 55 | Predicate: null.NewString("((bool_column)::bool <> 'true'::bool)", true), 56 | } 57 | //TimeBucket5MinutesField = TimeBucket("5 minutes", Table1.CreationDate).As("five_min") 58 | ) 59 | 60 | type TestCase struct { 61 | Constructed Renderable 62 | ExpectedStmt string 63 | Arguments interface{} 64 | Errors []error 65 | //ExpectedPreparedStmt string 66 | } 67 | 68 | func runTestCases(t *testing.T, testCases []TestCase) { 69 | for _, rendered := range testCases { 70 | t.Run(rendered.ExpectedStmt, func(t *testing.T) { 71 | builder := Builder{} 72 | rendered.Constructed.Render(&builder) 73 | require.Equal(t, rendered.ExpectedStmt, builder.String()) 74 | if rendered.Errors != nil { 75 | require.Equal(t, rendered.Errors, builder.errors) 76 | } 77 | if rendered.Arguments != nil { 78 | require.Equal(t, rendered.Arguments, builder.arguments) 79 | } 80 | }) 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /pkg/gooq/insert_test.go: -------------------------------------------------------------------------------- 1 | package gooq 2 | 3 | import "testing" 4 | 5 | var insertTestCases = []TestCase{ 6 | { 7 | Constructed: InsertInto(Table1).Set(Table1.Column1, "foo"), 8 | ExpectedStmt: `INSERT INTO public.table1 (column1) VALUES ($1)`, 9 | }, 10 | { 11 | Constructed: InsertInto(Table1).Set(Table1.Column1, "foo").Set(Table1.Column2, "bar"), 12 | ExpectedStmt: `INSERT INTO public.table1 (column1, column2) VALUES ($1, $2)`, 13 | }, 14 | { 15 | Constructed: InsertInto(Table1). 16 | Values("1", "2", 3, 4). 17 | Values("2", "3", 4, 5), 18 | ExpectedStmt: `INSERT INTO public.table1 VALUES ($1, $2, $3, $4), ($5, $6, $7, $8)`, 19 | }, 20 | { 21 | Constructed: InsertInto(Table1). 22 | Columns(Table1.Column1, Table1.Column2, Table1.Column3, Table1.Column4). 23 | Values("1", "2", 3, 4). 24 | Values("2", "3", 4, 5), 25 | ExpectedStmt: `INSERT INTO public.table1 (column1, column2, column3, column4) VALUES ($1, $2, $3, $4), ($5, $6, $7, $8)`, 26 | }, 27 | { 28 | Constructed: InsertInto(Table1).Select(Select(Table1.Column1).From(Table1)), 29 | ExpectedStmt: `INSERT INTO public.table1 (SELECT "table1".column1 FROM public.table1)`, 30 | }, 31 | { 32 | Constructed: InsertInto(Table1).Set(Table1.Column1, "foo").Returning(Table1.Column1), 33 | ExpectedStmt: `INSERT INTO public.table1 (column1) VALUES ($1) RETURNING "table1".column1`, 34 | }, 35 | { 36 | Constructed: InsertInto(Table1). 37 | Set(Table1.Column1, "foo").Set(Table1.Column2, "bar"). 38 | OnConflictDoUpdate(&Table1Constraint). 39 | SetUpdates(Table1.Column2, String("bar")), 40 | ExpectedStmt: `INSERT INTO public.table1 (column1, column2) VALUES ($1, $2) ON CONFLICT ON CONSTRAINT table1_pkey DO UPDATE SET column2 = $3`, 41 | }, 42 | { 43 | Constructed: InsertInto(Table1). 44 | Set(Table1.Column1, "foo").Set(Table1.Column2, "bar"). 45 | OnConflictDoUpdate(&Table1Constraint). 46 | SetUpdateColumns(Table1.Column2), 47 | ExpectedStmt: `INSERT INTO public.table1 (column1, column2) VALUES ($1, $2) ON CONFLICT ON CONSTRAINT table1_pkey DO UPDATE SET column2 = "excluded".column2`, 48 | }, 49 | { 50 | Constructed: InsertInto(Table1).Set(Table1.Column1, "foo").OnConflictDoNothing(), 51 | ExpectedStmt: `INSERT INTO public.table1 (column1) VALUES ($1) ON CONFLICT DO NOTHING`, 52 | }, 53 | { 54 | Constructed: InsertInto(Table2). 55 | Set(Table2.Column1, "foo"). 56 | Set(Table2.Column2, "bar"). 57 | Set(Table2.Column3, 1). 58 | OnConflictDoUpdate(&Table2Constraint). 59 | SetUpdateColumns(Table2.Column3), 60 | ExpectedStmt: `INSERT INTO public.table2 (column1, column2, column3) VALUES ($1, $2, $3) ON CONFLICT (column1, column2) WHERE ((bool_column)::bool <> 'true'::bool) DO UPDATE SET column3 = "excluded".column3`, 61 | }, 62 | } 63 | 64 | func TestInsert(t *testing.T) { 65 | runTestCases(t, insertTestCases) 66 | } 67 | -------------------------------------------------------------------------------- /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | # https://circleci.com/docs/2.0/language-go/ 2 | version: 2 # use CircleCI 2.0 3 | jobs: # basic units of work in a run 4 | build: # runs not using Workflows must have a `build` job as entry point 5 | docker: # run the steps with Docker 6 | # CircleCI Go images available at: https://hub.docker.com/r/circleci/golang/ 7 | - image: circleci/golang:1.13 # 8 | # CircleCI PostgreSQL images available at: https://hub.docker.com/r/circleci/postgres/ 9 | - image: circleci/postgres:9.6-alpine 10 | environment: # environment variables for primary container 11 | POSTGRES_USER: gooq 12 | POSTGRES_DB: gooq 13 | # directory where steps are run. Path must conform to the Go Workspace requirements 14 | working_directory: /go/src/github.com/lumina-tech/gooq 15 | 16 | environment: # environment variables for the build itself 17 | TEST_RESULTS: /tmp/test-results # path to where test results will be saved 18 | 19 | steps: # steps that comprise the `build` job 20 | - checkout # check out source code to working directory 21 | - run: mkdir -p $TEST_RESULTS # create the test results directory 22 | 23 | - restore_cache: # restores saved cache if no changes are detected since last run 24 | # Read about caching dependencies: https://circleci.com/docs/2.0/caching/ 25 | keys: 26 | - v1-pkg-cache 27 | 28 | # CircleCi's Go Docker image includes netcat 29 | # This allows polling the DB port to confirm it is open before proceeding 30 | - run: 31 | name: Waiting for Postgres to be ready 32 | command: | 33 | for i in `seq 1 10`; 34 | do 35 | nc -z localhost 5432 && echo Success && exit 0 36 | echo -n . 37 | sleep 1 38 | done 39 | echo Failed waiting for Postgres && exit 1 40 | 41 | - run: 42 | name: Run unit tests 43 | environment: # environment variables for the database url and path to migration files 44 | CONTACTS_DB_URL: "postgres://gooq@localhost:5432/gooq?sslmode=disable" 45 | CONTACTS_DB_MIGRATIONS: /go/src/github.com/lumina-tech/gooq/examples/swapi/migrations 46 | # Store the results of our tests in the $TEST_RESULTS directory 47 | command: | 48 | go test -v ./... | tee ${TEST_RESULTS}/go-test.out 49 | 50 | - save_cache: # Store cache in the /go/pkg directory 51 | key: v1-pkg-cache 52 | paths: 53 | - "/go/pkg" 54 | 55 | - store_artifacts: # Upload test summary for display in Artifacts: https://circleci.com/docs/2.0/artifacts/ 56 | path: /tmp/test-results 57 | destination: raw-test-output 58 | 59 | - store_test_results: # Upload test results for display in Test Summary: https://circleci.com/docs/2.0/collect-test-data/ 60 | path: /tmp/test-results 61 | -------------------------------------------------------------------------------- /pkg/generator/utils/utils.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "bytes" 5 | "errors" 6 | "fmt" 7 | "io/ioutil" 8 | "os" 9 | "path/filepath" 10 | "strings" 11 | "text/template" 12 | 13 | "github.com/knq/snaker" 14 | "golang.org/x/tools/imports" 15 | ) 16 | 17 | var ( 18 | templateFunctions = map[string]interface{}{ 19 | "capitalize": capitalize, 20 | "dict": dictionary, 21 | "snakeToCamel": snaker.SnakeToCamel, 22 | "snakeToCamelID": snaker.SnakeToCamelIdentifier, 23 | "forceLowerCamelIdentifier": snaker.ForceLowerCamelIdentifier, 24 | "toLower": strings.ToLower, 25 | "toUpper": strings.ToUpper, 26 | } 27 | ) 28 | 29 | func init() { 30 | _ = snaker.AddInitialisms("OS") 31 | } 32 | 33 | func GetTemplate( 34 | templateString string, 35 | ) *template.Template { 36 | return template.Must(template.New("template"). 37 | Funcs(templateFunctions).Parse(string(templateString))) 38 | } 39 | 40 | func RenderToFile(tpl *template.Template, filename string, data interface{}) error { 41 | buf := &bytes.Buffer{} 42 | if err := tpl.Execute(buf, data); err != nil { 43 | return err 44 | } 45 | if err := write(filename, buf.Bytes()); err != nil { 46 | return err 47 | } 48 | return nil 49 | } 50 | 51 | /////////////////////////////////////////////////////////////////////////////// 52 | // helpers 53 | /////////////////////////////////////////////////////////////////////////////// 54 | 55 | func capitalize(value string) string { 56 | if len(value) == 0 { 57 | return value 58 | } 59 | return strings.ToUpper(value[:1]) + value[1:] 60 | } 61 | 62 | func dictionary(values ...interface{}) (map[string]interface{}, error) { 63 | if len(values)%2 != 0 { 64 | return nil, errors.New("invalid dictionary call") 65 | } 66 | dict := make(map[string]interface{}, len(values)/2) 67 | for i := 0; i < len(values); i += 2 { 68 | key, ok := values[i].(string) 69 | if !ok { 70 | return nil, errors.New("dictionary keys must be strings") 71 | } 72 | dict[key] = values[i+1] 73 | } 74 | return dict, nil 75 | } 76 | 77 | func gofmt(filename string, b []byte) ([]byte, error) { 78 | out, err := imports.Process(filename, b, nil) 79 | if err != nil { 80 | return b, fmt.Errorf("unable to gofmt: %s", err.Error()) 81 | } 82 | return out, nil 83 | } 84 | 85 | func write(filename string, b []byte) error { 86 | err := os.MkdirAll(filepath.Dir(filename), 0755) 87 | if err != nil { 88 | return errors.New("failed to create directory") 89 | } 90 | 91 | formatted := b 92 | if strings.HasSuffix(filename, ".go") { 93 | formatted, err = gofmt(filename, b) 94 | if err != nil { 95 | fmt.Fprintf(os.Stderr, "gofmt failed: %s\n", err.Error()) 96 | formatted = b 97 | } 98 | } 99 | 100 | err = ioutil.WriteFile(filename, formatted, 0644) 101 | if err != nil { 102 | return fmt.Errorf("failed to write %s", filename) 103 | } 104 | return nil 105 | } 106 | -------------------------------------------------------------------------------- /pkg/nullable/uuid_test.go: -------------------------------------------------------------------------------- 1 | package nullable_test 2 | 3 | import ( 4 | "encoding/json" 5 | "testing" 6 | 7 | "github.com/google/uuid" 8 | "github.com/lumina-tech/gooq/pkg/nullable" 9 | "github.com/stretchr/testify/require" 10 | ) 11 | 12 | var ( 13 | nilJSON = []byte(`"00000000-0000-0000-0000-000000000000"`) 14 | uuidJSON = []byte(`"4925be64-f5dc-4a49-a682-98134ca5286d"`) 15 | blankUUIDJSON = []byte(`""`) 16 | nullJSON = []byte(`null`) 17 | invalidJSON = []byte(`:)`) 18 | boolJSON = []byte(`true`) 19 | ) 20 | 21 | type stringInStruct struct { 22 | Test nullable.UUID `json:"test,omitempty"` 23 | } 24 | 25 | func TestUUIDFrom(t *testing.T) { 26 | uuid1, _ := uuid.Parse("4925be64-f5dc-4a49-a682-98134ca5286d") 27 | u := nullable.UUIDFrom(uuid1) 28 | require.True(t, u.Valid) 29 | require.Equal(t, uuid1, u.UUID) 30 | 31 | // test nil case 32 | u = nullable.UUIDFrom(uuid.Nil) 33 | require.False(t, u.Valid) 34 | require.Equal(t, uuid.Nil, u.UUID) 35 | } 36 | 37 | func TestUUIDFromPtr(t *testing.T) { 38 | uuid1, _ := uuid.Parse("4925be64-f5dc-4a49-a682-98134ca5286d") 39 | uptr := &uuid1 40 | u := nullable.UUIDFromPtr(uptr) 41 | require.True(t, u.Valid) 42 | require.Equal(t, uuid1, u.UUID) 43 | 44 | u = nullable.UUIDFromPtr(&uuid.Nil) 45 | require.False(t, u.Valid) 46 | 47 | u = nullable.UUIDFromPtr(nil) 48 | require.False(t, u.Valid) 49 | } 50 | 51 | func TestUnmarshalUUID(t *testing.T) { 52 | expected, _ := uuid.Parse("4925be64-f5dc-4a49-a682-98134ca5286d") 53 | 54 | var u nullable.UUID 55 | err := json.Unmarshal(uuidJSON, &u) 56 | require.NoError(t, err) 57 | require.True(t, u.Valid) 58 | require.Equal(t, expected, u.UUID) 59 | 60 | var blank nullable.UUID 61 | err = json.Unmarshal(blankUUIDJSON, &blank) 62 | require.Error(t, err) 63 | require.False(t, blank.Valid) 64 | require.Equal(t, uuid.Nil, blank.UUID) 65 | 66 | var nilUUID nullable.UUID 67 | err = json.Unmarshal(nilJSON, &nilUUID) 68 | require.NoError(t, err) 69 | require.False(t, nilUUID.Valid) 70 | require.Equal(t, uuid.Nil, nilUUID.UUID) 71 | 72 | var null nullable.UUID 73 | err = json.Unmarshal(nullJSON, &null) 74 | require.NoError(t, err) 75 | require.False(t, null.Valid) 76 | require.Equal(t, uuid.Nil, null.UUID) 77 | 78 | var badType nullable.UUID 79 | err = json.Unmarshal(boolJSON, &badType) 80 | require.Error(t, err) 81 | require.False(t, badType.Valid) 82 | require.Equal(t, uuid.Nil, badType.UUID) 83 | 84 | var invalid nullable.UUID 85 | err = invalid.UnmarshalJSON(invalidJSON) 86 | require.Error(t, err) 87 | require.False(t, invalid.Valid) 88 | require.Equal(t, uuid.Nil, invalid.UUID) 89 | } 90 | 91 | func TestTextUnmarshalUUID(t *testing.T) { 92 | uuid1, _ := uuid.Parse("4925be64-f5dc-4a49-a682-98134ca5286d") 93 | 94 | var u nullable.UUID 95 | err := u.UnmarshalText([]byte(uuid1.String())) 96 | require.NoError(t, err) 97 | require.True(t, u.Valid) 98 | require.Equal(t, uuid1, u.UUID) 99 | 100 | var nilUUID nullable.UUID 101 | err = nilUUID.UnmarshalText([]byte("00000000-0000-0000-0000-000000000000")) 102 | require.NoError(t, err) 103 | require.False(t, nilUUID.Valid) 104 | require.Equal(t, uuid.Nil, nilUUID.UUID) 105 | 106 | var null nullable.UUID 107 | err = null.UnmarshalText([]byte("")) 108 | require.NoError(t, err) 109 | require.False(t, null.Valid) 110 | require.Equal(t, uuid.Nil, null.UUID) 111 | } 112 | -------------------------------------------------------------------------------- /pkg/gooq/builder.go: -------------------------------------------------------------------------------- 1 | package gooq 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | ) 7 | 8 | var ( 9 | invalidLiteralValueError = fmt.Errorf("literal value cannot be of kind slice") 10 | ) 11 | 12 | type Builder struct { 13 | isDebug bool 14 | buffer bytes.Buffer 15 | arguments []interface{} 16 | errors []error 17 | } 18 | 19 | func (builder *Builder) Printf( 20 | str string, args ...interface{}, 21 | ) *Builder { 22 | return builder.Print(fmt.Sprintf(str, args...)) 23 | } 24 | 25 | func (builder *Builder) Print( 26 | str string, 27 | ) *Builder { 28 | builder.buffer.Write([]byte(str)) 29 | return builder 30 | } 31 | 32 | func (builder *Builder) RenderExpression( 33 | expression Expression, 34 | ) *Builder { 35 | expression.Render(builder) 36 | return builder 37 | } 38 | 39 | func (builder *Builder) RenderLiteral( 40 | value interface{}, 41 | ) { 42 | if builder.isDebug { 43 | builder.Print(fmt.Sprintf("%v", value)) 44 | } else { 45 | placeholder := fmt.Sprintf("$%d", len(builder.arguments)+1) 46 | builder.Print(placeholder) 47 | builder.arguments = append(builder.arguments, value) 48 | } 49 | } 50 | 51 | func (builder *Builder) RenderExpressionArray( 52 | array []Expression, 53 | ) { 54 | builder.Print("(") 55 | for index, expression := range array { 56 | builder.RenderExpression(expression) 57 | if index != len(array)-1 { 58 | builder.Print(", ") 59 | } 60 | } 61 | builder.Print(")") 62 | } 63 | 64 | func (builder *Builder) RenderFieldArray( 65 | fields []Field, 66 | ) { 67 | builder.Print("(") 68 | for index, field := range fields { 69 | builder.Print(field.GetName()) 70 | if index != len(fields)-1 { 71 | builder.Print(", ") 72 | } 73 | } 74 | builder.Print(")") 75 | } 76 | 77 | func (builder *Builder) RenderConditions( 78 | conditions []Expression, 79 | ) { 80 | for index, expression := range conditions { 81 | expression.Render(builder) 82 | if index != len(conditions)-1 { 83 | builder.Print(" AND ") 84 | } 85 | } 86 | } 87 | 88 | func (builder *Builder) RenderExpressions( 89 | expressions []Expression, 90 | ) { 91 | for index, expression := range expressions { 92 | expression.Render(builder) 93 | if index != len(expressions)-1 { 94 | builder.Print(", ") 95 | } 96 | } 97 | } 98 | 99 | func (builder *Builder) RenderProjections( 100 | projections []Selectable, 101 | ) { 102 | for index, expression := range projections { 103 | expression.Render(builder) 104 | if index != len(projections)-1 { 105 | builder.Print(", ") 106 | } 107 | } 108 | } 109 | 110 | func (builder *Builder) RenderSetPredicates( 111 | predicates []setPredicate, 112 | ) *Builder { 113 | for index := range predicates { 114 | item := &predicates[index] 115 | // https://www.postgresql.org/docs/12/sql-update.html 116 | // do not include the table's name in the specification of a target column — for example, UPDATE table_name SET table_name.col = 1 is invalid 117 | builder.Printf("%s = ", item.field.GetName()) 118 | switch predicate := item.value.(type) { 119 | case *selection: 120 | builder.Print("(") 121 | predicate.Render(builder) 122 | builder.Printf(")") 123 | case Expression: 124 | builder.RenderExpression(predicate) 125 | default: 126 | builder.RenderExpression(newLiteralExpression(item.value)) 127 | } 128 | if index != len(predicates)-1 { 129 | builder.Print(", ") 130 | } 131 | } 132 | return builder 133 | } 134 | 135 | func (builder *Builder) String() string { 136 | return builder.buffer.String() 137 | } 138 | -------------------------------------------------------------------------------- /pkg/nullable/uuid.go: -------------------------------------------------------------------------------- 1 | package nullable 2 | 3 | import ( 4 | "database/sql/driver" 5 | "encoding/json" 6 | fmt "fmt" 7 | "reflect" 8 | 9 | "github.com/google/uuid" 10 | ) 11 | 12 | // UUID is a nullable UUID. It supports SQL and JSON serialization. 13 | // It will marshal to null if null. Blank UUID input will be considered null. 14 | type UUID struct { 15 | uuid.UUID 16 | Valid bool // Valid is true if String is not NULL 17 | } 18 | 19 | // newUUID creates a new UUID 20 | func newUUID(value uuid.UUID) UUID { 21 | return UUID{ 22 | UUID: value, 23 | Valid: value != uuid.Nil, 24 | } 25 | } 26 | 27 | // UUIDFrom creates a new UUID that will never be blank. 28 | func UUIDFrom(v uuid.UUID) UUID { 29 | return newUUID(v) 30 | } 31 | 32 | // UUIDFromPtr creates a new UUID that be null if s is nil. 33 | func UUIDFromPtr(v *uuid.UUID) UUID { 34 | if v == nil { 35 | return newUUID(uuid.Nil) 36 | } 37 | return newUUID(*v) 38 | } 39 | 40 | // Scan implements the Scanner interface. 41 | func (u *UUID) Scan(v interface{}) error { 42 | if v == nil { 43 | u.Valid = false 44 | return nil 45 | } 46 | switch x := v.(type) { 47 | case []byte: 48 | value, err := uuid.Parse(string(x)) 49 | if err != nil { 50 | return err 51 | } 52 | u.Valid = value != uuid.Nil 53 | u.UUID = value 54 | } 55 | return nil 56 | } 57 | 58 | // Value implements the driver Valuer interface. 59 | func (u UUID) Value() (driver.Value, error) { 60 | if !u.Valid { 61 | return nil, nil 62 | } 63 | return u.UUID.String(), nil 64 | } 65 | 66 | // MarshalJSON implements json.Marshaler. 67 | // It will encode null if this UUID is null. 68 | func (u UUID) MarshalJSON() ([]byte, error) { 69 | if !u.Valid { 70 | return []byte("null"), nil 71 | } 72 | return json.Marshal(u.UUID) 73 | } 74 | 75 | // UnmarshalJSON implements json.Unmarshaler. 76 | // It supports UUID and null input. Blank UUID input does not produce a null UUID. 77 | // It also supports unmarshalling a sql.UUID. 78 | func (u *UUID) UnmarshalJSON(data []byte) error { 79 | var v interface{} 80 | if err := json.Unmarshal(data, &v); err != nil { 81 | return err 82 | } 83 | switch x := v.(type) { 84 | case string: 85 | value, err := uuid.Parse(x) 86 | if err != nil { 87 | return err 88 | } 89 | u.Valid = value != uuid.Nil 90 | u.UUID = value 91 | return nil 92 | case nil: 93 | u.Valid = false 94 | u.UUID = uuid.Nil 95 | return nil 96 | } 97 | return fmt.Errorf("json: cannot unmarshal %v into Go value of type null.UUID", reflect.TypeOf(v).Name()) 98 | } 99 | 100 | // MarshalText implements encoding.TextMarshaler. 101 | // It will encode a blank UUID when this UUID is null. 102 | func (u UUID) MarshalText() ([]byte, error) { 103 | if !u.Valid { 104 | return []byte{}, nil 105 | } 106 | return []byte(u.UUID.String()), nil 107 | } 108 | 109 | // UnmarshalText implements encoding.TextUnmarshaler. 110 | // It will unmarshal to a null UUID if the input is a blank UUID. 111 | func (u *UUID) UnmarshalText(text []byte) error { 112 | str := string(text) 113 | if str == "" { 114 | u.UUID = uuid.Nil 115 | u.Valid = false 116 | return nil 117 | } 118 | value, err := uuid.Parse(string(text)) 119 | if err != nil { 120 | return err 121 | } 122 | u.Valid = value != uuid.Nil 123 | u.UUID = value 124 | return nil 125 | } 126 | 127 | // SetValue changes this UUID's value and also sets it to be non-null. 128 | func (u *UUID) SetValue(value uuid.UUID) { 129 | u.UUID = value 130 | u.Valid = value != uuid.Nil 131 | } 132 | 133 | // Ptr returns a pointer to this UUID's value, or a nil pointer if this UUID is null. 134 | func (u UUID) Ptr() *uuid.UUID { 135 | if !u.Valid { 136 | return nil 137 | } 138 | return &u.UUID 139 | } 140 | -------------------------------------------------------------------------------- /pkg/generator/metadata/loader.go: -------------------------------------------------------------------------------- 1 | package metadata 2 | 3 | import ( 4 | "strings" 5 | 6 | "github.com/jmoiron/sqlx" 7 | ) 8 | 9 | const ( 10 | ReferenceTableSuffix = "_reference_table" 11 | ) 12 | 13 | type Data struct { 14 | Schema string 15 | Tables []Table 16 | Enums []Enum 17 | ReferenceTableEnums []Enum 18 | Loader *Loader 19 | } 20 | 21 | type Enum struct { 22 | Name string 23 | Values []EnumValueMetadata 24 | IsReferenceTable bool 25 | ReferenceTableName string 26 | } 27 | 28 | type Table struct { 29 | Table TableMetadata 30 | Columns []ColumnMetadata 31 | Constraints []ConstraintMetadata 32 | ForeignKeyConstraints []ForeignKeyConstraintMetadata 33 | } 34 | 35 | func NewData( 36 | db *sqlx.DB, loader *Loader, 37 | ) (*Data, error) { 38 | schema, err := loader.Schema() 39 | if err != nil { 40 | return nil, err 41 | } 42 | tables, err := getDatabaseTables(db, loader, schema) 43 | if err != nil { 44 | return nil, err 45 | } 46 | dbEnums, err := getDatabaseEnums(db, loader, schema) 47 | if err != nil { 48 | return nil, err 49 | } 50 | refTableEnums, err := getReferenceTableEnums(db, loader, schema) 51 | if err != nil { 52 | return nil, err 53 | } 54 | return &Data{ 55 | Schema: schema, 56 | Tables: tables, 57 | Enums: dbEnums, 58 | ReferenceTableEnums: refTableEnums, 59 | Loader: loader, 60 | }, nil 61 | } 62 | 63 | func getDatabaseEnums( 64 | db *sqlx.DB, 65 | loader *Loader, 66 | schema string, 67 | ) ([]Enum, error) { 68 | enums, err := loader.EnumList(db, schema) 69 | if err != nil { 70 | return nil, err 71 | } 72 | var result []Enum 73 | for _, enum := range enums { 74 | enumValues, err := loader.EnumValueList(db, schema, enum.EnumName) 75 | if err != nil { 76 | return nil, err 77 | } 78 | result = append(result, Enum{ 79 | Name: enum.EnumName, 80 | Values: enumValues, 81 | IsReferenceTable: false, 82 | }) 83 | } 84 | return result, nil 85 | } 86 | 87 | func getReferenceTableEnums( 88 | db *sqlx.DB, 89 | loader *Loader, 90 | schema string, 91 | ) ([]Enum, error) { 92 | tables, err := loader.TableList(db, schema) 93 | if err != nil { 94 | return nil, err 95 | } 96 | var result []Enum 97 | for _, table := range tables { 98 | if !strings.HasSuffix(table.TableName, ReferenceTableSuffix) { 99 | continue 100 | } 101 | name := strings.ReplaceAll(table.TableName, ReferenceTableSuffix, "") 102 | enumValues, err := loader.ReferenceTableValueList(db, schema, table.TableName) 103 | if err != nil { 104 | return nil, err 105 | } 106 | result = append(result, Enum{ 107 | Name: name, 108 | Values: enumValues, 109 | IsReferenceTable: true, 110 | ReferenceTableName: table.TableName, 111 | }) 112 | } 113 | return result, nil 114 | } 115 | 116 | func getDatabaseTables( 117 | db *sqlx.DB, 118 | loader *Loader, 119 | schema string, 120 | ) ([]Table, error) { 121 | tables, err := loader.TableList(db, schema) 122 | if err != nil { 123 | return nil, err 124 | } 125 | var result []Table 126 | for _, table := range tables { 127 | columns, err := loader.ColumnList(db, schema, table.TableName) 128 | if err != nil { 129 | return nil, err 130 | } 131 | constraints, err := loader.ConstraintList(db, schema, table.TableName) 132 | if err != nil { 133 | return nil, err 134 | } 135 | foreignConstraints, err := loader.ForeignKeyConstraintList(db, table.TableName) 136 | if err != nil { 137 | return nil, err 138 | } 139 | result = append(result, Table{ 140 | Table: table, 141 | Columns: columns, 142 | Constraints: constraints, 143 | ForeignKeyConstraints: foreignConstraints, 144 | }) 145 | } 146 | return result, nil 147 | } 148 | -------------------------------------------------------------------------------- /pkg/generator/metadata/types.go: -------------------------------------------------------------------------------- 1 | package metadata 2 | 3 | import ( 4 | "github.com/jmoiron/sqlx" 5 | "gopkg.in/guregu/null.v3" 6 | ) 7 | 8 | type DataType struct { 9 | Name string 10 | Literal string 11 | NullableLiteral string 12 | } 13 | 14 | var ( 15 | DataTypeBool = DataType{Name: "Bool", Literal: "bool", NullableLiteral: "null.Bool"} 16 | DataTypeFloat32 = DataType{Name: "Decimal", Literal: "float32", NullableLiteral: "null.Float"} 17 | DataTypeFloat64 = DataType{Name: "Decimal", Literal: "float64", NullableLiteral: "null.Float"} 18 | DataTypeInt = DataType{Name: "Int", Literal: "int", NullableLiteral: "null.Int"} 19 | DataTypeInt64 = DataType{Name: "Int", Literal: "int64", NullableLiteral: "null.Int"} 20 | DataTypeBigInt = DataType{Name: "BigInt", Literal: "big.Int", NullableLiteral: "big.Int"} 21 | DataTypeBigFloat = DataType{Name: "BigFloat", Literal: "big.Float", NullableLiteral: "nullable.BigFloat"} 22 | DataTypeJSONB = DataType{Name: "Jsonb", Literal: "[]byte", NullableLiteral: "nullable.Jsonb"} 23 | DataTypeString = DataType{Name: "String", Literal: "string", NullableLiteral: "null.String"} 24 | DataTypeStringArray = DataType{Name: "StringArray", Literal: "pq.StringArray", NullableLiteral: "pq.StringArray"} 25 | DataTypeTime = DataType{Name: "Time", Literal: "time.Time", NullableLiteral: "null.Time"} 26 | DataTypeUUID = DataType{Name: "UUID", Literal: "uuid.UUID", NullableLiteral: "nullable.UUID"} 27 | ) 28 | var NameToType = map[string]DataType{ 29 | DataTypeBool.Name: DataTypeBool, 30 | DataTypeFloat32.Name: DataTypeFloat32, 31 | DataTypeFloat64.Name: DataTypeFloat64, 32 | DataTypeInt.Name: DataTypeInt, 33 | DataTypeInt64.Name: DataTypeInt64, 34 | DataTypeBigInt.Name: DataTypeBigInt, 35 | DataTypeBigFloat.Name: DataTypeBigFloat, 36 | DataTypeJSONB.Name: DataTypeJSONB, 37 | DataTypeString.Name: DataTypeString, 38 | DataTypeStringArray.Name: DataTypeStringArray, 39 | DataTypeTime.Name: DataTypeTime, 40 | DataTypeUUID.Name: DataTypeUUID, 41 | } 42 | 43 | type EnumMetadata struct { 44 | EnumName string `db:"enum_name"` 45 | } 46 | 47 | type EnumValueMetadata struct { 48 | Description string 49 | EnumValue string `db:"enum_value"` 50 | ConstValue int `db:"const_value"` 51 | } 52 | 53 | type TableMetadata struct { 54 | Type string `db:"type"` 55 | TableName string `db:"table_name"` 56 | ManualPk bool `db:"manual_pk"` 57 | } 58 | 59 | type ColumnMetadata struct { 60 | ColumnName string `db:"column_name"` 61 | DataType string `db:"data_type"` 62 | IsNullable bool `db:"is_nullable"` 63 | UserDefinedTypeName string `db:"udt_name"` 64 | } 65 | 66 | type ConstraintMetadata struct { 67 | Schema string `db:"schema"` 68 | Table string `db:"table"` 69 | IndexName string `db:"index_name"` 70 | IndexPredicate null.String `db:"index_predicate"` 71 | IsUnique bool `db:"is_unique"` 72 | IsPrimary bool `db:"is_primary"` 73 | IndexKeys string `db:"index_keys"` 74 | } 75 | 76 | type ForeignKeyConstraintMetadata struct { 77 | TableSchema string `db:"table_schema"` 78 | ConstraintName string `db:"constraint_name"` 79 | TableName string `db:"table_name"` 80 | ColumnName string `db:"column_name"` 81 | ForeignTableSchema string `db:"foreign_table_schema"` 82 | ForeignTableName string `db:"foreign_table_name"` 83 | ForeignColumnName string `db:"foreign_column_name"` 84 | } 85 | 86 | type Loader struct { 87 | Schema func() (string, error) 88 | TableList func(*sqlx.DB, string) ([]TableMetadata, error) 89 | ColumnList func(*sqlx.DB, string, string) ([]ColumnMetadata, error) 90 | ConstraintList func(*sqlx.DB, string, string) ([]ConstraintMetadata, error) 91 | ForeignKeyConstraintList func(*sqlx.DB, string) ([]ForeignKeyConstraintMetadata, error) 92 | EnumList func(*sqlx.DB, string) ([]EnumMetadata, error) 93 | EnumValueList func(*sqlx.DB, string, string) ([]EnumValueMetadata, error) 94 | ReferenceTableValueList func(*sqlx.DB, string, string) ([]EnumValueMetadata, error) 95 | GetDataType func(string) (DataType, error) 96 | GetTypeByName func(string) (DataType, error) 97 | } 98 | -------------------------------------------------------------------------------- /pkg/generator/plugin/modelgen/template.go: -------------------------------------------------------------------------------- 1 | package modelgen 2 | 3 | const modelTemplate = ` 4 | // THIS FILE WAS AUTOGENERATED - ANY EDITS TO THIS WILL BE LOST WHEN IT IS REGENERATED 5 | 6 | {{ $schema := .Schema }} 7 | package {{ .Package }} 8 | 9 | import "github.com/lumina-tech/gooq/pkg/gooq" 10 | 11 | {{ range $_, $table := .Tables }} 12 | type {{ $table.ModelType }} struct { 13 | {{ range $_, $f := $table.Fields -}} 14 | {{ snakeToCamelID $f.Name }} {{ $f.Type }} ` + "`db:\"{{ $f.Name }}\" json:\"{{ $f.Name }}\"`" + ` 15 | {{ end }} 16 | } 17 | {{ end }} 18 | ` 19 | 20 | const tableTemplate = ` 21 | // THIS FILE WAS AUTOGENERATED - ANY EDITS TO THIS WILL BE LOST WHEN IT IS REGENERATED 22 | 23 | {{ $schema := .Schema }} 24 | package {{ .Package }} 25 | 26 | import "github.com/lumina-tech/gooq/pkg/gooq" 27 | 28 | {{ range $_, $table := .Tables -}} 29 | 30 | type {{ $table.TableType }}Constraints struct { 31 | {{ range $_, $f := $table.Constraints -}} 32 | {{ snakeToCamel $f.Name }} gooq.DatabaseConstraint 33 | {{ end }} 34 | } 35 | 36 | type {{ $table.TableType }} struct { 37 | gooq.TableImpl 38 | Asterisk gooq.StringField 39 | {{ range $_, $f := $table.Fields -}} 40 | {{ snakeToCamel $f.Name }} gooq.{{ $f.GooqType }}Field 41 | {{ end }} 42 | Constraints *{{ $table.TableType }}Constraints 43 | } 44 | 45 | func new{{ capitalize $table.TableType }}Constraints( 46 | instance *{{ $table.TableType }}, 47 | ) *{{ $table.TableType }}Constraints { 48 | constraints := &{{ $table.TableType }}Constraints{} 49 | {{ range $_, $f := $table.Constraints -}} 50 | constraints.{{ snakeToCamel $f.Name }} = gooq.DatabaseConstraint{ 51 | Name: "{{$f.Name}}", 52 | Columns: []gooq.Field{ 53 | {{ range $f.Columns -}}instance.{{ snakeToCamel . }},{{ end -}} 54 | }, 55 | Predicate: null.NewString("{{$f.Predicate.String}}", {{$f.Predicate.Valid}}), 56 | } 57 | {{ end -}} 58 | return constraints 59 | } 60 | 61 | func new{{ capitalize $table.TableType }}() *{{ $table.TableType }} { 62 | instance := &{{ $table.TableType }}{} 63 | instance.Initialize("{{ $schema }}", "{{ $table.TableName }}") 64 | instance.Asterisk = gooq.NewStringField(instance, "*") 65 | {{ range $_, $f := $table.Fields -}} 66 | instance.{{ snakeToCamelID $f.Name }} = gooq.New{{ $f.GooqType }}Field(instance, "{{ $f.Name }}") 67 | {{ end -}} 68 | instance.Constraints = new{{ $table.ModelType }}Constraints(instance) 69 | return instance 70 | } 71 | 72 | func (t *{{ $table.TableType }}) As(alias string) *{{ $table.TableType }} { 73 | instance := new{{ $table.ModelType }}() 74 | instance.TableImpl = *instance.TableImpl.As(alias) 75 | return instance 76 | } 77 | 78 | func (t *{{ $table.TableType }}) GetColumns() []gooq.Expression { 79 | return []gooq.Expression{ 80 | {{ range $_, $f := $table.Fields -}} 81 | t.{{ snakeToCamelID $f.Name }}, 82 | {{ end -}} 83 | } 84 | } 85 | 86 | func (t *{{ $table.TableType }}) ScanRow( 87 | db gooq.DBInterface, stmt gooq.Fetchable, 88 | ) (*{{ $table.QualifiedModelType }}, error) { 89 | result := {{ $table.QualifiedModelType }}{} 90 | if err := gooq.ScanRow(db, stmt, &result); err != nil { 91 | return nil, err 92 | } 93 | return &result, nil 94 | } 95 | 96 | func (t *{{ $table.TableType }}) ScanRows( 97 | db gooq.DBInterface, stmt gooq.Fetchable, 98 | ) ([]{{ $table.QualifiedModelType }}, error) { 99 | results := []{{ $table.QualifiedModelType }}{} 100 | if err := gooq.ScanRows(db, stmt, &results); err != nil { 101 | return nil, err 102 | } 103 | return results, nil 104 | } 105 | 106 | func (t *{{ $table.TableType }}) ScanRowWithContext( 107 | ctx context.Context, db gooq.DBInterface, stmt gooq.Fetchable, 108 | ) (*{{ $table.QualifiedModelType }}, error) { 109 | result := {{ $table.QualifiedModelType }}{} 110 | if err := gooq.ScanRowWithContext(ctx, db, stmt, &result); err != nil { 111 | return nil, err 112 | } 113 | return &result, nil 114 | } 115 | 116 | func (t *{{ $table.TableType }}) ScanRowsWithContext( 117 | ctx context.Context, db gooq.DBInterface, stmt gooq.Fetchable, 118 | ) ([]{{ $table.QualifiedModelType }}, error) { 119 | results := []{{ $table.QualifiedModelType }}{} 120 | if err := gooq.ScanRowsWithContext(ctx, db, stmt, &results); err != nil { 121 | return nil, err 122 | } 123 | return results, nil 124 | } 125 | 126 | var {{ $table.TableSingletonName }} = new{{ capitalize $table.TableType }}() 127 | {{ end }} 128 | ` 129 | -------------------------------------------------------------------------------- /pkg/gooq/delete.go: -------------------------------------------------------------------------------- 1 | package gooq 2 | 3 | import ( 4 | "context" 5 | "database/sql" 6 | 7 | "github.com/jmoiron/sqlx" 8 | ) 9 | 10 | type DeleteUsingStep interface { 11 | DeleteWhereStep 12 | Using(Selectable) DeleteOnStep 13 | } 14 | 15 | type DeleteOnStep interface { 16 | DeleteWhereStep 17 | On(...Expression) DeleteWhereStep 18 | } 19 | 20 | type DeleteWhereStep interface { 21 | DeleteResultStep 22 | Where(...Expression) DeleteReturningStep 23 | } 24 | 25 | type DeleteReturningStep interface { 26 | DeleteFinalStep 27 | Returning(...Expression) DeleteResultStep 28 | } 29 | 30 | type DeleteResultStep interface { 31 | Fetchable 32 | Renderable 33 | } 34 | 35 | type DeleteFinalStep interface { 36 | Executable 37 | Renderable 38 | } 39 | 40 | /////////////////////////////////////////////////////////////////////////////// 41 | // Implementation 42 | /////////////////////////////////////////////////////////////////////////////// 43 | 44 | // https://www.postgresql.org/docs/11/sql-delete.html 45 | 46 | type deletion struct { 47 | table Table 48 | using Selectable 49 | conditions []Expression 50 | usingPredicate []Expression // where clause for using 51 | returning []Expression 52 | } 53 | 54 | func Delete(t Table) DeleteUsingStep { 55 | return &deletion{table: t} 56 | } 57 | 58 | func (d *deletion) Using(s Selectable) DeleteOnStep { 59 | d.using = s 60 | return d 61 | } 62 | 63 | func (d *deletion) On(c ...Expression) DeleteWhereStep { 64 | d.usingPredicate = c 65 | return d 66 | } 67 | 68 | func (d *deletion) Where(c ...Expression) DeleteReturningStep { 69 | d.conditions = c 70 | return d 71 | } 72 | 73 | func (d *deletion) Returning(f ...Expression) DeleteResultStep { 74 | d.returning = f 75 | return d 76 | } 77 | 78 | /////////////////////////////////////////////////////////////////////////////// 79 | // Executable 80 | /////////////////////////////////////////////////////////////////////////////// 81 | 82 | func (d *deletion) Exec(dl Dialect, db DBInterface) (sql.Result, error) { 83 | builder := d.Build(dl) 84 | return db.Exec(builder.String(), builder.arguments...) 85 | } 86 | 87 | func (d *deletion) ExecWithContext( 88 | ctx context.Context, dl Dialect, db DBInterface) (sql.Result, error) { 89 | builder := d.Build(dl) 90 | return db.ExecContext(ctx, builder.String(), builder.arguments...) 91 | } 92 | 93 | /////////////////////////////////////////////////////////////////////////////// 94 | // Fetchable 95 | /////////////////////////////////////////////////////////////////////////////// 96 | 97 | func (d *deletion) Fetch(dl Dialect, db DBInterface) (*sqlx.Rows, error) { 98 | builder := d.Build(dl) 99 | return db.Queryx(builder.String(), builder.arguments...) 100 | } 101 | 102 | func (d *deletion) FetchRow(dl Dialect, db DBInterface) *sqlx.Row { 103 | builder := d.Build(dl) 104 | return db.QueryRowx(builder.String(), builder.arguments...) 105 | } 106 | 107 | func (d *deletion) FetchWithContext( 108 | ctx context.Context, dl Dialect, db DBInterface) (*sqlx.Rows, error) { 109 | builder := d.Build(dl) 110 | return db.QueryxContext(ctx, builder.String(), builder.arguments...) 111 | } 112 | 113 | func (d *deletion) FetchRowWithContext( 114 | ctx context.Context, dl Dialect, db DBInterface) *sqlx.Row { 115 | builder := d.Build(dl) 116 | return db.QueryRowxContext(ctx, builder.String(), builder.arguments...) 117 | } 118 | 119 | /////////////////////////////////////////////////////////////////////////////// 120 | // Renderable 121 | /////////////////////////////////////////////////////////////////////////////// 122 | 123 | func (d *deletion) Build(dl Dialect) *Builder { 124 | builder := Builder{} 125 | d.Render(&builder) 126 | return &builder 127 | } 128 | 129 | // https://www.postgresql.org/docs/10/sql-delete.html 130 | func (d *deletion) Render( 131 | builder *Builder, 132 | ) { 133 | 134 | // DELETE FROM table_name 135 | builder.Printf("DELETE FROM %s", d.table.GetQualifiedName()) 136 | 137 | conditions := d.conditions 138 | if d.using != nil { 139 | // render USING clause 140 | builder.Printf(" USING ") 141 | d.using.Render(builder) 142 | // there is no "ON" clause in postgres, this is pattern is from jOOQ. 143 | // https://www.jooq.org/doc/3.12/manual-single-page/#delete-statement 144 | conditions = append(d.usingPredicate, conditions...) 145 | } 146 | 147 | if len(conditions) > 0 { 148 | // [ WHERE condition ] 149 | builder.Print(" WHERE ") 150 | builder.RenderConditions(conditions) 151 | } 152 | 153 | // [ RETURNING output_expression ] 154 | if d.returning != nil { 155 | builder.Print(" RETURNING ") 156 | builder.RenderExpressions(d.returning) 157 | } 158 | } 159 | -------------------------------------------------------------------------------- /examples/swapi/model/swapi_enum.generated.go: -------------------------------------------------------------------------------- 1 | // THIS FILE WAS AUTOGENERATED - ANY EDITS TO THIS WILL BE LOST WHEN IT IS REGENERATED 2 | 3 | package model 4 | 5 | import ( 6 | "database/sql/driver" 7 | "errors" 8 | "fmt" 9 | "io" 10 | "strconv" 11 | ) 12 | 13 | type Color string 14 | 15 | const ( 16 | ColorBlack = Color("black") 17 | ColorBlue = Color("blue") 18 | ColorBrown = Color("brown") 19 | ColorGreen = Color("green") 20 | ColorOrange = Color("orange") 21 | ColorPurple = Color("purple") 22 | ColorRed = Color("red") 23 | ColorYellow = Color("yellow") 24 | ColorNull = Color("") 25 | ) 26 | 27 | // String returns the string value of the Color. 28 | func (enumType Color) String() string { 29 | return string(enumType) 30 | } 31 | 32 | // MarshalText marshals Color into text. 33 | func (enumType Color) MarshalText() ([]byte, error) { 34 | return []byte(enumType.String()), nil 35 | } 36 | 37 | // UnmarshalText unmarshals Color from text. 38 | func (enumType *Color) UnmarshalText(text []byte) error { 39 | switch string(text) { 40 | case "black": 41 | *enumType = ColorBlack 42 | case "blue": 43 | *enumType = ColorBlue 44 | case "brown": 45 | *enumType = ColorBrown 46 | case "green": 47 | *enumType = ColorGreen 48 | case "orange": 49 | *enumType = ColorOrange 50 | case "purple": 51 | *enumType = ColorPurple 52 | case "red": 53 | *enumType = ColorRed 54 | case "yellow": 55 | *enumType = ColorYellow 56 | default: 57 | *enumType = ColorNull 58 | } 59 | return nil 60 | } 61 | 62 | // MarshalGQL satisfies gqlgen interface for Color. 63 | func (enumType Color) MarshalGQL(w io.Writer) { 64 | if enumType == ColorNull { 65 | io.WriteString(w, "null") 66 | } else { 67 | io.WriteString(w, strconv.Quote(enumType.String())) 68 | } 69 | } 70 | 71 | // UnmarshalText satisfies gqlgen interface for Color. 72 | func (enumType *Color) UnmarshalGQL(v interface{}) error { 73 | switch str := v.(type) { 74 | case string: 75 | return enumType.UnmarshalText([]byte(str)) 76 | case nil: 77 | return nil 78 | default: 79 | return fmt.Errorf("invalid enum value %v", v) 80 | } 81 | } 82 | 83 | // Value satisfies the sql/driver.Valuer interface for Color. 84 | func (enumType Color) Value() (driver.Value, error) { 85 | if enumType == ColorNull { 86 | return nil, nil 87 | } 88 | return enumType.String(), nil 89 | } 90 | 91 | // Scan satisfies the database/sql.Scanner interface for Color. 92 | func (enumType *Color) Scan(src interface{}) error { 93 | switch buf := src.(type) { 94 | case []byte: 95 | return enumType.UnmarshalText(buf) 96 | case string: 97 | return enumType.UnmarshalText([]byte(buf)) 98 | case nil: 99 | return nil 100 | default: 101 | return errors.New("invalid Color") 102 | } 103 | } 104 | 105 | type Gender string 106 | 107 | const ( 108 | GenderMale = Gender("male") 109 | GenderFemale = Gender("female") 110 | GenderNull = Gender("") 111 | ) 112 | 113 | // String returns the string value of the Gender. 114 | func (enumType Gender) String() string { 115 | return string(enumType) 116 | } 117 | 118 | // MarshalText marshals Gender into text. 119 | func (enumType Gender) MarshalText() ([]byte, error) { 120 | return []byte(enumType.String()), nil 121 | } 122 | 123 | // UnmarshalText unmarshals Gender from text. 124 | func (enumType *Gender) UnmarshalText(text []byte) error { 125 | switch string(text) { 126 | case "male": 127 | *enumType = GenderMale 128 | case "female": 129 | *enumType = GenderFemale 130 | default: 131 | *enumType = GenderNull 132 | } 133 | return nil 134 | } 135 | 136 | // MarshalGQL satisfies gqlgen interface for Gender. 137 | func (enumType Gender) MarshalGQL(w io.Writer) { 138 | if enumType == GenderNull { 139 | io.WriteString(w, "null") 140 | } else { 141 | io.WriteString(w, strconv.Quote(enumType.String())) 142 | } 143 | } 144 | 145 | // UnmarshalText satisfies gqlgen interface for Gender. 146 | func (enumType *Gender) UnmarshalGQL(v interface{}) error { 147 | switch str := v.(type) { 148 | case string: 149 | return enumType.UnmarshalText([]byte(str)) 150 | case nil: 151 | return nil 152 | default: 153 | return fmt.Errorf("invalid enum value %v", v) 154 | } 155 | } 156 | 157 | // Value satisfies the sql/driver.Valuer interface for Gender. 158 | func (enumType Gender) Value() (driver.Value, error) { 159 | if enumType == GenderNull { 160 | return nil, nil 161 | } 162 | return enumType.String(), nil 163 | } 164 | 165 | // Scan satisfies the database/sql.Scanner interface for Gender. 166 | func (enumType *Gender) Scan(src interface{}) error { 167 | switch buf := src.(type) { 168 | case []byte: 169 | return enumType.UnmarshalText(buf) 170 | case string: 171 | return enumType.UnmarshalText([]byte(buf)) 172 | case nil: 173 | return nil 174 | default: 175 | return errors.New("invalid Gender") 176 | } 177 | } 178 | -------------------------------------------------------------------------------- /pkg/gooq/field.go: -------------------------------------------------------------------------------- 1 | package gooq 2 | 3 | import "fmt" 4 | 5 | type Field interface { 6 | Named 7 | } 8 | 9 | type fieldImpl struct { 10 | expression Expression 11 | selectable Selectable 12 | name string 13 | } 14 | 15 | func (field *fieldImpl) initFieldImpl( 16 | expression Expression, selectable Selectable, name string, 17 | ) { 18 | field.expression = expression 19 | field.selectable = selectable 20 | field.name = name 21 | } 22 | 23 | func (field *fieldImpl) GetName() string { 24 | return field.name 25 | } 26 | 27 | func (field *fieldImpl) GetQualifiedName() string { 28 | var selectableName string 29 | switch selectable := field.selectable.(type) { 30 | case Table: 31 | selectableName = selectable.GetUnqualifiedName() 32 | case *selection: 33 | if selectable.GetAlias().Valid { 34 | selectableName = selectable.GetAlias().String 35 | } 36 | // TODO(Peter): can selectable be a anonymous select statement? 37 | } 38 | if selectableName == "" { 39 | return field.name 40 | } else { 41 | return fmt.Sprintf("\"%s\".%s", selectableName, field.name) 42 | } 43 | } 44 | 45 | func (field *fieldImpl) Render( 46 | builder *Builder, 47 | ) { 48 | builder.Print(field.GetQualifiedName()) 49 | } 50 | 51 | // BoolField 52 | 53 | type BoolField interface { 54 | BoolExpression 55 | Field 56 | } 57 | 58 | type defaultBoolField struct { 59 | boolExpressionImpl 60 | fieldImpl 61 | } 62 | 63 | func NewBoolField( 64 | table Table, name string, 65 | ) BoolField { 66 | field := &defaultBoolField{} 67 | field.expressionImpl.initFieldExpressionImpl(field) 68 | field.fieldImpl.initFieldImpl(field, table, name) 69 | return field 70 | } 71 | 72 | // DecimalField 73 | 74 | type DecimalField interface { 75 | NumericExpression 76 | Field 77 | } 78 | 79 | type defaultDecimalField struct { 80 | numericExpressionImpl 81 | fieldImpl 82 | } 83 | 84 | func NewDecimalField( 85 | table Table, name string, 86 | ) DecimalField { 87 | field := &defaultDecimalField{} 88 | field.expressionImpl.initFieldExpressionImpl(field) 89 | field.fieldImpl.initFieldImpl(field, table, name) 90 | return field 91 | } 92 | 93 | // IntField 94 | 95 | type IntField interface { 96 | NumericExpression 97 | Field 98 | } 99 | 100 | type defaultIntField struct { 101 | numericExpressionImpl 102 | fieldImpl 103 | } 104 | 105 | func NewIntField( 106 | table Table, name string, 107 | ) IntField { 108 | field := &defaultIntField{} 109 | field.expressionImpl.initFieldExpressionImpl(field) 110 | field.fieldImpl.initFieldImpl(field, table, name) 111 | return field 112 | } 113 | 114 | // JsonbField 115 | 116 | type JsonbField interface { 117 | StringExpression 118 | Field 119 | } 120 | 121 | type defaultJsonbField struct { 122 | stringExpressionImpl 123 | fieldImpl 124 | } 125 | 126 | func NewJsonbField( 127 | table Table, name string, 128 | ) JsonbField { 129 | field := &defaultJsonbField{} 130 | field.expressionImpl.initFieldExpressionImpl(field) 131 | field.fieldImpl.initFieldImpl(field, table, name) 132 | return field 133 | } 134 | 135 | // StringField 136 | 137 | type StringField interface { 138 | StringExpression 139 | Field 140 | } 141 | 142 | type defaultStringField struct { 143 | stringExpressionImpl 144 | fieldImpl 145 | } 146 | 147 | func NewStringField( 148 | table Selectable, name string, 149 | ) StringField { 150 | field := &defaultStringField{} 151 | field.expressionImpl.initFieldExpressionImpl(field) 152 | field.fieldImpl.initFieldImpl(field, table, name) 153 | return field 154 | } 155 | 156 | // StringArrayField 157 | 158 | type StringArrayField interface { 159 | StringExpression 160 | Field 161 | } 162 | 163 | type defaultStringArrayField struct { 164 | stringExpressionImpl 165 | fieldImpl 166 | } 167 | 168 | func NewStringArrayField( 169 | table Table, name string, 170 | ) StringArrayField { 171 | field := &defaultStringField{} 172 | field.expressionImpl.initFieldExpressionImpl(field) 173 | field.fieldImpl.initFieldImpl(field, table, name) 174 | return field 175 | } 176 | 177 | // UUIDField 178 | 179 | type UUIDField interface { 180 | UUIDExpression 181 | Field 182 | } 183 | 184 | type defaultUUIDField struct { 185 | uuidExpressionImpl 186 | fieldImpl 187 | } 188 | 189 | func NewUUIDField( 190 | table Table, name string, 191 | ) UUIDField { 192 | field := &defaultUUIDField{} 193 | field.expressionImpl.initFieldExpressionImpl(field) 194 | field.fieldImpl.initFieldImpl(field, table, name) 195 | return field 196 | } 197 | 198 | // TimeField 199 | 200 | type TimeField interface { 201 | DateTimeExpression 202 | Field 203 | } 204 | 205 | type defaultTimeField struct { 206 | dateTimeExpressionImpl 207 | fieldImpl 208 | } 209 | 210 | func NewTimeField( 211 | table Table, name string, 212 | ) TimeField { 213 | field := &defaultTimeField{} 214 | field.expressionImpl.initFieldExpressionImpl(field) 215 | field.fieldImpl.initFieldImpl(field, table, name) 216 | return field 217 | } 218 | -------------------------------------------------------------------------------- /pkg/gooq/update.go: -------------------------------------------------------------------------------- 1 | package gooq 2 | 3 | import ( 4 | "context" 5 | "database/sql" 6 | 7 | "github.com/jmoiron/sqlx" 8 | ) 9 | 10 | type UpdateSetStep interface { 11 | UpdateFromStep 12 | Set(f Field, v interface{}) UpdateSetStep 13 | } 14 | 15 | type UpdateFromStep interface { 16 | UpdateWhereStep 17 | From(Selectable) UpdateWhereStep 18 | } 19 | 20 | type UpdateWhereStep interface { 21 | UpdateOnConflictStep 22 | Where(conditions ...Expression) UpdateOnConflictStep 23 | } 24 | 25 | type UpdateOnConflictStep interface { 26 | UpdateReturningStep 27 | OnConflictDoNothing() UpdateReturningStep 28 | OnConflictDoUpdate() UpdateReturningStep 29 | } 30 | 31 | type UpdateReturningStep interface { 32 | UpdateFinalStep 33 | Returning(...Expression) UpdateResultStep 34 | } 35 | 36 | type UpdateResultStep interface { 37 | Fetchable 38 | Renderable 39 | } 40 | 41 | type UpdateFinalStep interface { 42 | Executable 43 | Renderable 44 | } 45 | 46 | /////////////////////////////////////////////////////////////////////////////// 47 | // Implementation 48 | /////////////////////////////////////////////////////////////////////////////// 49 | 50 | type setPredicate struct { 51 | field Field 52 | value interface{} 53 | } 54 | 55 | type update struct { 56 | table Table 57 | setPredicates []setPredicate // set predicates 58 | conditions []Expression // where conditions 59 | fromSelection Selectable // selection for from clause 60 | conflictAction ConflictAction 61 | returning []Expression 62 | } 63 | 64 | func Update(t Table) UpdateSetStep { 65 | return &update{table: t} 66 | } 67 | 68 | func (u *update) Set(field Field, value interface{}) UpdateSetStep { 69 | u.setPredicates = append(u.setPredicates, setPredicate{field, value}) 70 | return u 71 | } 72 | 73 | func (u *update) From(s Selectable) UpdateWhereStep { 74 | u.fromSelection = s 75 | return u 76 | } 77 | 78 | func (u *update) Where(c ...Expression) UpdateOnConflictStep { 79 | u.conditions = c 80 | return u 81 | } 82 | 83 | func (u *update) OnConflictDoNothing() UpdateReturningStep { 84 | u.conflictAction = ConflictActionDoNothing 85 | return u 86 | } 87 | 88 | func (u *update) OnConflictDoUpdate() UpdateReturningStep { 89 | u.conflictAction = ConflictActionDoUpdate 90 | panic("not implemented") 91 | return u 92 | } 93 | 94 | func (u *update) Returning(f ...Expression) UpdateResultStep { 95 | u.returning = f 96 | return u 97 | } 98 | 99 | /////////////////////////////////////////////////////////////////////////////// 100 | // Executable 101 | /////////////////////////////////////////////////////////////////////////////// 102 | 103 | func (u *update) Exec(dl Dialect, db DBInterface) (sql.Result, error) { 104 | builder := u.Build(dl) 105 | return db.Exec(builder.String(), builder.arguments...) 106 | } 107 | 108 | func (u *update) ExecWithContext( 109 | ctx context.Context, dl Dialect, db DBInterface) (sql.Result, error) { 110 | builder := u.Build(dl) 111 | return db.ExecContext(ctx, builder.String(), builder.arguments...) 112 | } 113 | 114 | /////////////////////////////////////////////////////////////////////////////// 115 | // Fetchable 116 | /////////////////////////////////////////////////////////////////////////////// 117 | 118 | func (u *update) Fetch(dl Dialect, db DBInterface) (*sqlx.Rows, error) { 119 | builder := u.Build(dl) 120 | return db.Queryx(builder.String(), builder.arguments...) 121 | } 122 | 123 | func (u *update) FetchRow(dl Dialect, db DBInterface) *sqlx.Row { 124 | builder := u.Build(dl) 125 | return db.QueryRowx(builder.String(), builder.arguments...) 126 | } 127 | 128 | func (u *update) FetchWithContext( 129 | ctx context.Context, dl Dialect, db DBInterface) (*sqlx.Rows, error) { 130 | builder := u.Build(dl) 131 | return db.QueryxContext(ctx, builder.String(), builder.arguments...) 132 | } 133 | 134 | func (u *update) FetchRowWithContext( 135 | ctx context.Context, dl Dialect, db DBInterface) *sqlx.Row { 136 | builder := u.Build(dl) 137 | return db.QueryRowxContext(ctx, builder.String(), builder.arguments...) 138 | } 139 | 140 | /////////////////////////////////////////////////////////////////////////////// 141 | // Renderable 142 | /////////////////////////////////////////////////////////////////////////////// 143 | 144 | func (u *update) Build(d Dialect) *Builder { 145 | builder := Builder{} 146 | u.Render(&builder) 147 | return &builder 148 | } 149 | 150 | func (u *update) Render( 151 | builder *Builder, 152 | ) { 153 | // UPDATE table_name SET 154 | builder.Printf("UPDATE %s", u.table.GetQualifiedName()) 155 | 156 | if len(u.setPredicates) > 0 { 157 | // render SET clause 158 | builder.Print(" SET ") 159 | builder.RenderSetPredicates(u.setPredicates) 160 | } 161 | 162 | if u.fromSelection != nil { 163 | // render WHERE clause 164 | builder.Print(" FROM ") 165 | u.fromSelection.Render(builder) 166 | } 167 | 168 | if len(u.conditions) > 0 { 169 | // render WHERE clause 170 | builder.Print(" WHERE ") 171 | builder.RenderConditions(u.conditions) 172 | } 173 | 174 | // render on conflict 175 | if u.conflictAction != ConflictActionNil { 176 | builder.Printf(" ON CONFLICT %s", string(u.conflictAction)) 177 | } 178 | 179 | // render returning 180 | if u.returning != nil { 181 | builder.Print(" RETURNING ") 182 | builder.RenderExpressions(u.returning) 183 | } 184 | } 185 | -------------------------------------------------------------------------------- /examples/swapi/cmd/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "fmt" 7 | "os" 8 | 9 | "github.com/google/uuid" 10 | "github.com/lumina-tech/gooq/examples/swapi/model" 11 | "github.com/lumina-tech/gooq/examples/swapi/table" 12 | "github.com/lumina-tech/gooq/pkg/database" 13 | "github.com/lumina-tech/gooq/pkg/gooq" 14 | ) 15 | 16 | func main() { 17 | dockerDB := database.NewDockerizedDB(&database.DatabaseConfig{ 18 | Host: "localhost", 19 | Port: 5432, 20 | Username: "postgres", 21 | Password: "password", 22 | DatabaseName: "swapi", 23 | SSLMode: "disable", 24 | }, "11.4-alpine") 25 | defer dockerDB.Close() 26 | 27 | ctx := context.Background() 28 | database.MigrateDatabase(dockerDB.DB.DB, "migrations") 29 | 30 | speciesStmt := gooq.InsertInto(table.Species). 31 | Set(table.Species.ID, uuid.New()). 32 | Set(table.Species.Name, "Human"). 33 | Set(table.Species.Classification, "Mammal"). 34 | Set(table.Species.AverageHeight, 160.5). 35 | Set(table.Species.AverageLifespan, 1000000000). 36 | Set(table.Species.HairColor, model.ColorBlack). 37 | Set(table.Species.SkinColor, model.ColorOrange). 38 | Set(table.Species.EyeColor, model.ColorBrown). 39 | Set(table.Species.HomeWorld, "Earth"). 40 | Set(table.Species.Language, "English"). 41 | Returning(table.Species.Asterisk) 42 | species, err := table.Species.ScanRowWithContext(ctx, dockerDB.DB, speciesStmt) 43 | if err != nil { 44 | fmt.Fprint(os.Stderr, err.Error()) 45 | return 46 | } 47 | 48 | personStmt := gooq.InsertInto(table.Person). 49 | Set(table.Person.ID, uuid.New()). 50 | Set(table.Person.Name, "Frank"). 51 | Set(table.Person.Height, 170.3). 52 | Set(table.Person.Mass, 150.5). 53 | Set(table.Person.BirthYear, 1998). 54 | Set(table.Person.HomeWorld, "Runescape"). 55 | Set(table.Person.Gender, model.GenderMale). 56 | Set(table.Person.EyeColor, model.ColorBrown). 57 | Set(table.Person.HairColor, model.ColorBlack). 58 | Set(table.Person.SkinColor, model.ColorOrange). 59 | Set(table.Person.SpeciesID, species.ID). 60 | Returning(table.Person.Asterisk) 61 | frank, err := table.Person.ScanRowWithContext(ctx, dockerDB.DB, personStmt) 62 | if err != nil { 63 | fmt.Fprint(os.Stderr, err.Error()) 64 | return 65 | } 66 | 67 | personStmtUpdate := gooq.InsertInto(table.Person). 68 | Set(table.Person.ID, uuid.New()). 69 | Set(table.Person.Name, "Frank"). 70 | Set(table.Person.Height, 170.3). 71 | Set(table.Person.Mass, 150.5). 72 | Set(table.Person.BirthYear, 1998). 73 | Set(table.Person.HomeWorld, "Runescape"). 74 | Set(table.Person.Gender, model.GenderMale). 75 | Set(table.Person.EyeColor, model.ColorBrown). 76 | Set(table.Person.HairColor, model.ColorBlue). 77 | Set(table.Person.SkinColor, model.ColorOrange). 78 | Set(table.Person.SpeciesID, species.ID). 79 | OnConflictDoUpdate(&table.Person.Constraints.NameBirthyearConstraint). 80 | SetUpdateColumns(table.Person.HairColor). 81 | Returning(table.Person.Asterisk) 82 | frankUpdated, err := table.Person.ScanRowWithContext(ctx, dockerDB.DB, personStmtUpdate) 83 | if err != nil { 84 | fmt.Fprintln(os.Stderr, err.Error()) 85 | return 86 | } 87 | fmt.Fprintf(os.Stderr, "frank updated haircolor: %s to %s\n", frank.HairColor, frankUpdated.HairColor) 88 | fmt.Fprintf(os.Stderr, "frank did not update eyecolor: %s to %s\n", frank.EyeColor, frankUpdated.EyeColor) 89 | 90 | type PersonWithSpecies struct { 91 | model.Person 92 | Species *model.Species `db:"species"` 93 | } 94 | 95 | { 96 | speciesWithAlias := table.Species.As("species_alias") 97 | stmt := gooq.Select( 98 | table.Person.Asterisk, 99 | speciesWithAlias.Name.As("species.name"), 100 | speciesWithAlias.Classification.As("species.name"), 101 | speciesWithAlias.AverageHeight.As("species.average_height"), 102 | speciesWithAlias.AverageLifespan.As("species.average_lifespan"), 103 | speciesWithAlias.HairColor.As("species.hair_color"), 104 | speciesWithAlias.SkinColor.As("species.skin_color"), 105 | speciesWithAlias.EyeColor.As("species.eye_color"), 106 | speciesWithAlias.HomeWorld.As("species.home_world"), 107 | speciesWithAlias.Language.As("species.language"), 108 | ).From(table.Person). 109 | Join(speciesWithAlias). 110 | On(table.Person.SpeciesID.Eq(speciesWithAlias.ID)) 111 | 112 | builder := &gooq.Builder{} 113 | stmt.Render(builder) 114 | fmt.Println(builder.String()) 115 | 116 | var results []PersonWithSpecies 117 | if err := gooq.ScanRowsWithContext(ctx, dockerDB.DB, stmt, &results); err != nil { 118 | fmt.Fprint(os.Stderr, err.Error()) 119 | return 120 | } 121 | bytes, _ := json.Marshal(results) 122 | fmt.Println(string(bytes)) 123 | } 124 | 125 | // same as above but we don't have to manually enumerate all the column in species 126 | // inside the projection 127 | { 128 | selection := []gooq.Selectable{table.Person.Asterisk} 129 | selection = append(selection, 130 | getColumnsWithPrefix("species", table.Species.GetColumns())...) 131 | stmt := gooq.Select(selection...).From(table.Person). 132 | Join(table.Species). 133 | On(table.Person.SpeciesID.Eq(table.Species.ID)) 134 | 135 | builder := &gooq.Builder{} 136 | stmt.Render(builder) 137 | fmt.Println(builder.String()) 138 | 139 | var results []PersonWithSpecies 140 | if err := gooq.ScanRowsWithContext(ctx, dockerDB.DB, stmt, &results); err != nil { 141 | fmt.Fprint(os.Stderr, err.Error()) 142 | return 143 | } 144 | bytes, _ := json.Marshal(results) 145 | fmt.Println(string(bytes)) 146 | } 147 | } 148 | 149 | func getColumnsWithPrefix( 150 | prefix string, expressions []gooq.Expression, 151 | ) []gooq.Selectable { 152 | results := make([]gooq.Selectable, 0) 153 | for _, exp := range expressions { 154 | if field, ok := exp.(gooq.Field); ok { 155 | alias := fmt.Sprintf("%s.%s", prefix, field.GetName()) 156 | results = append(results, exp.As(alias)) 157 | } 158 | } 159 | return results 160 | } 161 | -------------------------------------------------------------------------------- /pkg/gooq/function_test.go: -------------------------------------------------------------------------------- 1 | package gooq 2 | 3 | import "testing" 4 | 5 | var functionTestCases = []TestCase{ 6 | { 7 | Constructed: And(Table1.Column1.Eq(Table2.Column1), Table1.Column2.Eq(Table2.Column2), Table1.Column2.Eq(Table2.Column2)), 8 | ExpectedStmt: `("table1".column1 = "table2".column1 AND "table1".column2 = "table2".column2 AND "table1".column2 = "table2".column2)`, 9 | }, 10 | { 11 | Constructed: Or(Table1.Column1.Eq(Table2.Column1), Table1.Column2.Eq(Table2.Column2), Table1.Column2.Eq(Table2.Column2)), 12 | ExpectedStmt: `("table1".column1 = "table2".column1 OR "table1".column2 = "table2".column2 OR "table1".column2 = "table2".column2)`, 13 | }, 14 | { 15 | Constructed: Select(Coalesce(Table1.Column1, Table1.Column2)).From(Table1), 16 | ExpectedStmt: `SELECT COALESCE("table1".column1, "table1".column2) FROM public.table1`, 17 | }, 18 | { 19 | Constructed: Select(Coalesce(Table1.Column1, Int64(0))).From(Table1), 20 | ExpectedStmt: `SELECT COALESCE("table1".column1, $1) FROM public.table1`, 21 | }, 22 | { 23 | Constructed: Select(Count()).From(Table1), 24 | ExpectedStmt: `SELECT COUNT(*) FROM public.table1`, 25 | }, 26 | { 27 | Constructed: Select(Count(Asterisk)).From(Table1), 28 | ExpectedStmt: `SELECT COUNT(*) FROM public.table1`, 29 | }, 30 | { 31 | Constructed: Select(Distinct(Table1.Column1)).From(Table1), 32 | ExpectedStmt: `SELECT DISTINCT("table1".column1) FROM public.table1`, 33 | }, 34 | { 35 | Constructed: Greatest(Int64(10), Int64(2), Int64(23)), 36 | ExpectedStmt: `GREATEST($1, $2, $3)`, 37 | }, 38 | { 39 | Constructed: Least(String("a"), String("b")), 40 | ExpectedStmt: `LEAST($1, $2)`, 41 | }, 42 | { 43 | Constructed: Ascii(String("abc")), 44 | ExpectedStmt: `ASCII($1)`, 45 | }, 46 | { 47 | Constructed: Ascii(Table1.Column1), 48 | ExpectedStmt: `ASCII("table1".column1)`, 49 | }, 50 | { 51 | Constructed: BTrim(String(" abc ")), 52 | ExpectedStmt: `BTRIM($1)`, 53 | }, 54 | { 55 | Constructed: LTrim(Table1.Column1, String("xyz")), 56 | ExpectedStmt: `LTRIM("table1".column1, $1)`, 57 | }, 58 | { 59 | Constructed: RTrim(String("xyzxyzabcxyz"), Table1.Column1), 60 | ExpectedStmt: `RTRIM($1, "table1".column1)`, 61 | }, 62 | { 63 | Constructed: Chr(Table1.Column3), 64 | ExpectedStmt: `CHR("table1".column3)`, 65 | }, 66 | { 67 | Constructed: Concat(String("xyzxyzabcxyz"), Table1.Column3, Int64(3)), 68 | ExpectedStmt: `CONCAT($1, "table1".column3, $2)`, 69 | }, 70 | { 71 | Constructed: ConcatWs(String("x"), Table1.Column3, Int64(3), String("four")), 72 | ExpectedStmt: `CONCAT_WS($1, "table1".column3, $2, $3)`, 73 | }, 74 | { 75 | Constructed: Format(String("Hello %s, %1$s"), Table1.Column3), 76 | ExpectedStmt: `FORMAT($1, "table1".column3)`, 77 | }, 78 | { 79 | Constructed: Format(String("no formatting to be done")), 80 | ExpectedStmt: `FORMAT($1)`, 81 | }, 82 | { 83 | Constructed: InitCap(String("initCap THIS SenTEnce")), 84 | ExpectedStmt: `INITCAP($1)`, 85 | }, 86 | { 87 | Constructed: Left(Table1.Column1, Int64(3)), 88 | ExpectedStmt: `LEFT("table1".column1, $1)`, 89 | }, 90 | { 91 | Constructed: Right(String("take my right chars"), Table1.Column3), 92 | ExpectedStmt: `RIGHT($1, "table1".column3)`, 93 | }, 94 | { 95 | Constructed: Length(Table1.Column1), 96 | ExpectedStmt: `LENGTH("table1".column1)`, 97 | }, 98 | { 99 | Constructed: Length(String("jose"), String("UTF8")), 100 | ExpectedStmt: `LENGTH($1, $2)`, 101 | }, 102 | { 103 | Constructed: LPad(String("hi"), Int64(7)), 104 | ExpectedStmt: `LPAD($1, $2)`, 105 | }, 106 | { 107 | Constructed: RPad(Table1.Column2, Table1.Column3, Table1.Column1), 108 | ExpectedStmt: `RPAD("table1".column2, "table1".column3, "table1".column1)`, 109 | }, 110 | { 111 | Constructed: Md5(Table1.Column2), 112 | ExpectedStmt: `MD5("table1".column2)`, 113 | }, 114 | { 115 | Constructed: PgClientEncoding(), 116 | ExpectedStmt: `PG_CLIENT_ENCODING()`, 117 | }, 118 | { 119 | Constructed: QuoteIdent(String("foo bar")), 120 | ExpectedStmt: `QUOTE_IDENT($1)`, 121 | }, 122 | { 123 | Constructed: QuoteLiteral(String("foo bar")), 124 | ExpectedStmt: `QUOTE_LITERAL($1)`, 125 | }, 126 | { 127 | Constructed: QuoteLiteral(Float64(42.5)), 128 | ExpectedStmt: `QUOTE_LITERAL($1)`, 129 | }, 130 | { 131 | Constructed: QuoteNullable(Table3.ID), 132 | ExpectedStmt: `QUOTE_NULLABLE("table3".id)`, 133 | }, 134 | { 135 | Constructed: Repeat(String("abc"), Table1.Column3), 136 | ExpectedStmt: `REPEAT($1, "table1".column3)`, 137 | }, 138 | { 139 | Constructed: Replace(Table1.Column1, String("ab"), String("CD")), 140 | ExpectedStmt: `REPLACE("table1".column1, $1, $2)`, 141 | }, 142 | { 143 | Constructed: Reverse(String("reversable")), 144 | ExpectedStmt: `REVERSE($1)`, 145 | }, 146 | { 147 | Constructed: SplitPart(String("abc~@~def~@~ghi"), String("~@~"), Int64(2)), 148 | ExpectedStmt: `SPLIT_PART($1, $2, $3)`, 149 | }, 150 | { 151 | Constructed: Strpos(Table1.Column1, String("ab")), 152 | ExpectedStmt: `STRPOS("table1".column1, $1)`, 153 | }, 154 | { 155 | Constructed: Substr(Table1.Column1, Int64(2), Int64(5)), 156 | ExpectedStmt: `SUBSTR("table1".column1, $1, $2)`, 157 | }, 158 | { 159 | Constructed: StartsWith(String("alphabet"), String("alph")), 160 | ExpectedStmt: `STARTS_WITH($1, $2)`, 161 | }, 162 | { 163 | Constructed: ToAscii(Table1.Column1), 164 | ExpectedStmt: `TO_ASCII("table1".column1)`, 165 | }, 166 | { 167 | Constructed: ToAscii(String("Karel"), String("WIN1250")), 168 | ExpectedStmt: `TO_ASCII($1, $2)`, 169 | }, 170 | { 171 | Constructed: ToHex(Table1.Column3), 172 | ExpectedStmt: `TO_HEX("table1".column3)`, 173 | }, 174 | { 175 | Constructed: Translate(String("12345"), String("143"), String("ax")), 176 | ExpectedStmt: `TRANSLATE($1, $2, $3)`, 177 | }, 178 | { 179 | Constructed: TryAdvisoryLock(Int64(43)), 180 | ExpectedStmt: `pg_try_advisory_lock($1)`, 181 | }, 182 | { 183 | Constructed: ReleaseAdvisoryLock(Int64(52)), 184 | ExpectedStmt: `pg_advisory_unlock($1)`, 185 | }, 186 | } 187 | 188 | func TestFunctions(t *testing.T) { 189 | runTestCases(t, functionTestCases) 190 | } 191 | -------------------------------------------------------------------------------- /pkg/generator/plugin/modelgen/modelgen.go: -------------------------------------------------------------------------------- 1 | package modelgen 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "strconv" 7 | "strings" 8 | "time" 9 | 10 | "github.com/knq/snaker" 11 | "github.com/lumina-tech/gooq/pkg/generator/metadata" 12 | "github.com/lumina-tech/gooq/pkg/generator/utils" 13 | ) 14 | 15 | type ModelGenerator struct { 16 | templateString string 17 | outputFile string 18 | packageName string 19 | modelPackage string 20 | overrides *ModelOverride 21 | } 22 | 23 | func NewGenerator( 24 | templateString, outputFile, packageName, modelPackage string, overrides *ModelOverride, 25 | ) *ModelGenerator { 26 | return &ModelGenerator{ 27 | templateString: templateString, 28 | outputFile: outputFile, 29 | packageName: packageName, 30 | modelPackage: modelPackage, 31 | overrides: overrides, 32 | } 33 | } 34 | 35 | func NewModelGenerator( 36 | outputFile, tablePackage, modelPackage string, overrides *ModelOverride, 37 | ) *ModelGenerator { 38 | return NewGenerator(modelTemplate, outputFile, modelPackage, modelPackage, overrides) 39 | } 40 | 41 | func NewTableGenerator( 42 | outputFile, tablePackage, modelPackage string, overrides *ModelOverride, 43 | ) *ModelGenerator { 44 | return NewGenerator(tableTemplate, outputFile, tablePackage, modelPackage, overrides) 45 | } 46 | 47 | func (gen *ModelGenerator) GenerateCode( 48 | data *metadata.Data, 49 | ) error { 50 | args := TemplateArgs{ 51 | Timestamp: time.Now().Format(time.RFC3339), 52 | Package: gen.packageName, 53 | Schema: data.Schema, 54 | Tables: make([]TableTemplateArgs, 0), 55 | } 56 | for _, table := range data.Tables { 57 | tableName := table.Table.TableName 58 | fields, err := getFieldArgs(data, table, gen.overrides) 59 | if err != nil { 60 | return err 61 | } 62 | constraints, err := getConstraintArgs(table) 63 | if err != nil { 64 | return err 65 | } 66 | foreignKeyConstraints, err := getForeignKeyConstraintArgs(table) 67 | if err != nil { 68 | return err 69 | } 70 | modelType := snaker.SnakeToCamelIdentifier(tableName) 71 | args.Tables = append(args.Tables, TableTemplateArgs{ 72 | TableName: table.Table.TableName, 73 | TableType: snaker.ForceLowerCamelIdentifier(tableName), 74 | TableSingletonName: snaker.SnakeToCamelIdentifier(tableName), 75 | ModelType: modelType, 76 | QualifiedModelType: fmt.Sprintf("%s.%s", gen.modelPackage, modelType), 77 | IsReferenceTable: isReferenceTable(tableName), 78 | ReferenceTableEnumType: getEnumTypeFromReferenceTableName(tableName), 79 | Fields: fields, 80 | Constraints: constraints, 81 | ForeignKeyConstraints: foreignKeyConstraints, 82 | }) 83 | } 84 | enumTemplate := utils.GetTemplate(gen.templateString) 85 | return utils.RenderToFile(enumTemplate, gen.outputFile, args) 86 | } 87 | 88 | func getColumnToTypeMapping( 89 | table metadata.Table, 90 | ) map[string]string { 91 | result := make(map[string]string) 92 | for _, fk := range table.ForeignKeyConstraints { 93 | if isReferenceTable(fk.ForeignTableName) { 94 | result[fk.ColumnName] = getEnumTypeFromReferenceTableName(fk.ForeignTableName) 95 | } 96 | } 97 | return result 98 | } 99 | 100 | func getFieldArgs( 101 | data *metadata.Data, table metadata.Table, overrides *ModelOverride, 102 | ) ([]FieldTemplateArgs, error) { 103 | columnToRefTableMapping := getColumnToTypeMapping(table) 104 | var results []FieldTemplateArgs 105 | for _, column := range table.Columns { 106 | var dataType metadata.DataType 107 | var err error 108 | 109 | if dataTypeKey, ok := getOverrideDataType(table.Table.TableName, column.ColumnName, overrides); ok { 110 | dataType, err = data.Loader.GetTypeByName(dataTypeKey) 111 | } else { 112 | dataType, err = data.Loader.GetDataType(column.DataType) 113 | } 114 | 115 | if err != nil { 116 | return nil, err 117 | } 118 | literal := dataType.Literal 119 | if column.IsNullable { 120 | literal = dataType.NullableLiteral 121 | } 122 | if enumName, ok := columnToRefTableMapping[column.ColumnName]; ok { 123 | literal = enumName 124 | } else if column.DataType == "USER-DEFINED" && column.UserDefinedTypeName != "citext" { 125 | // citext is the only user-defined type that is not an enum 126 | literal = snaker.SnakeToCamelIdentifier(column.UserDefinedTypeName) 127 | } 128 | results = append(results, FieldTemplateArgs{ 129 | Name: column.ColumnName, 130 | GooqType: dataType.Name, 131 | Type: literal, 132 | }) 133 | } 134 | return results, nil 135 | } 136 | 137 | func getConstraintArgs( 138 | table metadata.Table, 139 | ) ([]ConstraintTemplateArgs, error) { 140 | var results []ConstraintTemplateArgs 141 | for _, constraint := range table.Constraints { 142 | var columns []string 143 | err := json.Unmarshal([]byte(constraint.IndexKeys), &columns) 144 | if err != nil { 145 | return nil, err 146 | } 147 | for index := range columns { 148 | column := columns[index] 149 | // some of the constraint columns have quotes we should unquote them 150 | if strings.ContainsAny(column, "\"") { 151 | unquotedColumn, _ := strconv.Unquote(column) 152 | columns[index] = unquotedColumn 153 | } 154 | } 155 | results = append(results, ConstraintTemplateArgs{ 156 | Name: constraint.IndexName, 157 | Columns: columns, 158 | Predicate: constraint.IndexPredicate, 159 | }) 160 | } 161 | return results, nil 162 | } 163 | 164 | func getForeignKeyConstraintArgs( 165 | table metadata.Table, 166 | ) ([]ForeignKeyConstraintTemplateArgs, error) { 167 | var results []ForeignKeyConstraintTemplateArgs 168 | for _, constraint := range table.ForeignKeyConstraints { 169 | results = append(results, ForeignKeyConstraintTemplateArgs{ 170 | Name: constraint.ConstraintName, 171 | ColumnName: constraint.ColumnName, 172 | ForeignTableName: constraint.ForeignTableName, 173 | ForeignColumnName: constraint.ForeignColumnName, 174 | }) 175 | } 176 | return results, nil 177 | } 178 | 179 | func isReferenceTable( 180 | tableName string, 181 | ) bool { 182 | return strings.HasSuffix(tableName, metadata.ReferenceTableSuffix) 183 | } 184 | 185 | func getEnumTypeFromReferenceTableName( 186 | tableName string, 187 | ) string { 188 | enumNameSnakeCase := strings.ReplaceAll(tableName, metadata.ReferenceTableSuffix, "") 189 | return snaker.SnakeToCamelIdentifier(enumNameSnakeCase) 190 | } 191 | 192 | func getOverrideDataType( 193 | tableName string, 194 | columnName string, 195 | overrides *ModelOverride, 196 | ) (string, bool) { 197 | if overrides == nil { 198 | return "", false 199 | } 200 | if model, ok := overrides.Models[tableName]; ok { 201 | if field, ok := model.Fields[columnName]; ok { 202 | return field.OverrideType, true 203 | } 204 | } 205 | return "", false 206 | } 207 | -------------------------------------------------------------------------------- /pkg/generator/postgres/postgres_loader.go: -------------------------------------------------------------------------------- 1 | package postgres 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | 7 | "github.com/jmoiron/sqlx" 8 | "github.com/lumina-tech/gooq/pkg/generator/metadata" 9 | ) 10 | 11 | func NewPostgresLoader() *metadata.Loader { 12 | return &metadata.Loader{ 13 | ConstraintList: getConstraintList, 14 | ForeignKeyConstraintList: getForeignKeyConstraintList, 15 | Schema: getSchema, 16 | EnumList: getEnums, 17 | EnumValueList: getEnumValues, 18 | ReferenceTableValueList: getReferenceTableValues, 19 | TableList: getTable, 20 | ColumnList: getColumns, 21 | GetDataType: parseType, 22 | GetTypeByName: getTypeByName, 23 | } 24 | } 25 | 26 | func getSchema() (string, error) { 27 | return "public", nil 28 | } 29 | 30 | func getConstraintList( 31 | db *sqlx.DB, schema, tableName string, 32 | ) ([]metadata.ConstraintMetadata, error) { 33 | constraints := []metadata.ConstraintMetadata{} 34 | err := db.Select(&constraints, constraintValuesQuery, schema, tableName) 35 | if err != nil { 36 | return nil, err 37 | } 38 | return constraints, nil 39 | } 40 | 41 | func getForeignKeyConstraintList( 42 | db *sqlx.DB, tableName string, 43 | ) ([]metadata.ForeignKeyConstraintMetadata, error) { 44 | constraints := []metadata.ForeignKeyConstraintMetadata{} 45 | err := db.Select(&constraints, foreignKeyConstraintValuesQuery, tableName) 46 | if err != nil { 47 | return nil, err 48 | } 49 | return constraints, nil 50 | } 51 | 52 | func getEnums( 53 | db *sqlx.DB, schema string, 54 | ) ([]metadata.EnumMetadata, error) { 55 | enums := []metadata.EnumMetadata{} 56 | err := db.Select(&enums, enumsQuery, schema) 57 | if err != nil { 58 | return nil, err 59 | } 60 | return enums, nil 61 | } 62 | 63 | func getEnumValues( 64 | db *sqlx.DB, schema, enumName string, 65 | ) ([]metadata.EnumValueMetadata, error) { 66 | enumValues := []metadata.EnumValueMetadata{} 67 | err := db.Select(&enumValues, enumValuesQuery, schema, enumName) 68 | if err != nil { 69 | return nil, err 70 | } 71 | return enumValues, nil 72 | } 73 | 74 | func getTable( 75 | db *sqlx.DB, schema string, 76 | ) ([]metadata.TableMetadata, error) { 77 | tables := []metadata.TableMetadata{} 78 | err := db.Select(&tables, tablesQuery, schema) 79 | if err != nil { 80 | return nil, err 81 | } 82 | return tables, nil 83 | } 84 | 85 | func getReferenceTableValues( 86 | db *sqlx.DB, schema, referenceTableName string, 87 | ) ([]metadata.EnumValueMetadata, error) { 88 | enumValues := []metadata.EnumValueMetadata{} 89 | query := fmt.Sprintf(referenceTableValuesQuery, schema, referenceTableName) 90 | err := db.Select(&enumValues, query) 91 | if err != nil { 92 | return nil, err 93 | } 94 | return enumValues, nil 95 | } 96 | 97 | func getColumns( 98 | db *sqlx.DB, schema, tableName string, 99 | ) ([]metadata.ColumnMetadata, error) { 100 | columns := []metadata.ColumnMetadata{} 101 | err := db.Select(&columns, columnsQuery, schema, tableName) 102 | if err != nil { 103 | return nil, err 104 | } 105 | return columns, nil 106 | } 107 | 108 | func getTypeByName( 109 | typeName string, 110 | ) (metadata.DataType, error) { 111 | dataType, ok := metadata.NameToType[typeName] 112 | if !ok { 113 | return metadata.DataType{}, fmt.Errorf("type with name %s does not exist", typeName) 114 | } 115 | return dataType, nil 116 | } 117 | 118 | func parseType( 119 | dataType string, 120 | ) (metadata.DataType, error) { 121 | var typ metadata.DataType 122 | switch strings.ToLower(dataType) { 123 | case "array": 124 | typ = metadata.DataTypeStringArray 125 | case "boolean": 126 | typ = metadata.DataTypeBool 127 | case "character", "character varying", "text", "user-defined": 128 | typ = metadata.DataTypeString 129 | case "inet": 130 | typ = metadata.DataTypeString 131 | case "smallint", "integer": 132 | typ = metadata.DataTypeInt 133 | case "bigint": 134 | typ = metadata.DataTypeInt64 135 | case "jsonb": 136 | typ = metadata.DataTypeJSONB 137 | case "float": 138 | typ = metadata.DataTypeFloat32 139 | case "decimal", "double precision", "numeric": 140 | typ = metadata.DataTypeFloat64 141 | case "date", "timestamp with time zone", "time with time zone", "time without time zone", "timestamp without time zone": 142 | typ = metadata.DataTypeTime 143 | case "uuid": 144 | typ = metadata.DataTypeUUID 145 | default: 146 | return metadata.DataType{}, fmt.Errorf("invalid type=%s", dataType) 147 | } 148 | return typ, nil 149 | } 150 | 151 | const tablesQuery = ` 152 | select table_name 153 | from information_schema.tables 154 | where table_schema = $1 AND table_name != 'schema_migrations' 155 | order by table_name 156 | ` 157 | 158 | const columnsQuery = ` 159 | SELECT column_name, data_type, is_nullable::boolean, udt_name 160 | FROM information_schema.columns 161 | WHERE table_schema = $1 and table_name = $2 162 | ` 163 | 164 | const enumsQuery = ` 165 | SELECT DISTINCT t.typname as enum_name 166 | FROM pg_type t 167 | JOIN ONLY pg_namespace n ON n.oid = t.typnamespace 168 | JOIN ONLY pg_enum e ON t.oid = e.enumtypid 169 | WHERE n.nspname = $1 170 | ` 171 | 172 | const enumValuesQuery = ` 173 | SELECT e.enumlabel as enum_value, e.enumsortorder as const_value 174 | FROM pg_type t 175 | JOIN ONLY pg_namespace n ON n.oid = t.typnamespace 176 | LEFT JOIN pg_enum e ON t.oid = e.enumtypid 177 | WHERE n.nspname = $1 AND t.typname = $2 178 | ` 179 | 180 | const referenceTableValuesQuery = ` 181 | SELECT value as enum_value from %s.%s order by value 182 | ` 183 | 184 | const constraintValuesQuery = ` 185 | SELECT 186 | indexes.schemaname AS schema, 187 | indexes.tablename AS table, 188 | indexes.indexname AS index_name, 189 | pg_get_expr(idx.indpred, idx.indrelid) AS index_predicate, 190 | idx.indisunique AS is_unique, 191 | idx.indisprimary AS is_primary, 192 | array_to_json(ARRAY ( 193 | SELECT 194 | pg_get_indexdef(idx.indexrelid, k + 1, TRUE) 195 | FROM 196 | generate_subscripts(idx.indkey, 1) AS k 197 | ORDER BY 198 | k)) AS index_keys 199 | FROM 200 | pg_indexes AS indexes 201 | JOIN pg_class AS i ON i.relname = indexes.indexname 202 | JOIN pg_index AS idx ON idx.indexrelid = i.oid 203 | WHERE 204 | schemaname = $1 205 | AND tablename = $2 206 | ORDER BY 207 | indexes.indexname 208 | ` 209 | 210 | const foreignKeyConstraintValuesQuery = ` 211 | SELECT 212 | tc.table_schema, 213 | tc.constraint_name, 214 | tc.table_name, 215 | kcu.column_name, 216 | ccu.table_schema AS foreign_table_schema, 217 | ccu.table_name AS foreign_table_name, 218 | ccu.column_name AS foreign_column_name 219 | FROM 220 | information_schema.table_constraints AS tc 221 | JOIN information_schema.key_column_usage AS kcu 222 | ON tc.constraint_name = kcu.constraint_name 223 | AND tc.table_schema = kcu.table_schema 224 | JOIN information_schema.constraint_column_usage AS ccu 225 | ON ccu.constraint_name = tc.constraint_name 226 | AND ccu.table_schema = tc.table_schema 227 | WHERE tc.constraint_type = 'FOREIGN KEY' AND tc.table_name=$1 228 | ` 229 | -------------------------------------------------------------------------------- /examples/swapi/migrations/001_init.up.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE color_reference_table( 2 | value text primary key NOT NULL 3 | ); 4 | 5 | INSERT INTO color_reference_table (value) VALUES 6 | ('black'), 7 | ('brown'), 8 | ('red'), 9 | ('orange'), 10 | ('yellow'), 11 | ('green'), 12 | ('blue'), 13 | ('purple'); 14 | 15 | CREATE TYPE gender AS ENUM ('male', 'female'); 16 | 17 | CREATE TABLE species( 18 | id uuid primary key NOT NULL, 19 | name text NOT NULL, 20 | classification text NOT NULL, 21 | average_height decimal NOT NULL, 22 | average_lifespan numeric, 23 | hair_color text NOT NULL, 24 | skin_color text NOT NULL, 25 | eye_color text NOT NULL, 26 | home_world text NOT NULL, 27 | language text NOT NULL, 28 | FOREIGN KEY (hair_color) REFERENCES color_reference_table(value), 29 | FOREIGN KEY (skin_color) REFERENCES color_reference_table(value), 30 | FOREIGN KEY (eye_color) REFERENCES color_reference_table(value) 31 | ); 32 | 33 | CREATE UNIQUE INDEX species_uniqueness_constraint ON species (name, classification); 34 | 35 | CREATE TABLE weapon( 36 | id uuid primary key NOT NULL, 37 | damage int NOT NULL, 38 | price int NOT NULL 39 | ); 40 | 41 | CREATE TABLE person( 42 | id uuid primary key NOT NULL, 43 | name text NOT NULL, 44 | height decimal NOT NULL, 45 | mass decimal NOT NULL, 46 | hair_color text NOT NULL, 47 | skin_color text NOT NULL, 48 | eye_color text NOT NULL, 49 | birth_year int NOT NULL, 50 | gender gender NOT NULL, 51 | home_world text NOT NULL, 52 | species_id uuid NOT NULL, 53 | weapon_id uuid, 54 | status varchar not null default 'alive', 55 | FOREIGN KEY (hair_color) REFERENCES color_reference_table(value), 56 | FOREIGN KEY (skin_color) REFERENCES color_reference_table(value), 57 | FOREIGN KEY (eye_color) REFERENCES color_reference_table(value), 58 | FOREIGN KEY (species_id) REFERENCES species(id), 59 | FOREIGN KEY (weapon_id) REFERENCES weapon(id) 60 | ); 61 | 62 | -- example to show partial unique index 63 | CREATE UNIQUE INDEX name_birthyear_constraint ON person (name, birth_year) WHERE status != 'dead'; 64 | 65 | 66 | 67 | -- type Person struct { 68 | -- Name string `json:"name"` 69 | -- Height string `json:"height"` 70 | -- Mass string `json:"mass"` 71 | -- HairColor string `json:"hair_color"` 72 | -- SkinColor string `json:"skin_color"` 73 | -- EyeColor string `json:"eye_color"` 74 | -- BirthYear string `json:"birth_year"` 75 | -- Gender string `json:"gender"` 76 | -- Homeworld string `json:"homeworld"` 77 | -- Films []string `json:"films"` 78 | -- Species []string `json:"species"` 79 | -- Vehicles []string `json:"vehicles"` 80 | -- Starships []string `json:"starships"` 81 | -- Created string `json:"created"` 82 | -- Edited string `json:"edited"` 83 | -- URL string `json:"url"` 84 | -- } 85 | -- 86 | -- type Film struct { 87 | -- Title string `json:"title"` 88 | -- EpisodeID int64 `json:"episode_id"` 89 | -- OpeningCrawl string `json:"opening_crawl"` 90 | -- Director string `json:"director"` 91 | -- Producer string `json:"producer"` 92 | -- Characters []string `json:"characters"` 93 | -- Planets []string `json:"planets"` 94 | -- Starships []string `json:"starships"` 95 | -- Vehicles []string `json:"vehicles"` 96 | -- Species []string `json:"species"` 97 | -- Created string `json:"created"` 98 | -- Edited string `json:"edited"` 99 | -- URL string `json:"url"` 100 | -- } 101 | -- 102 | -- type Planet struct { 103 | -- Name string `json:"name"` 104 | -- RotationPeriod string `json:"rotation_period"` 105 | -- OrbitalPeriod string `json:"orbital_period"` 106 | -- Diameter string `json:"diameter"` 107 | -- Climate string `json:"climate"` 108 | -- Gravity string `json:"gravity"` 109 | -- Terrain string `json:"terrain"` 110 | -- SurfaceWater string `json:"surface_water"` 111 | -- Population string `json:"population"` 112 | -- Residents []string `json:"residents"` 113 | -- Films []string `json:"films"` 114 | -- Created string `json:"created"` 115 | -- Edited string `json:"edited"` 116 | -- URL string `json:"url"` 117 | -- } 118 | -- 119 | -- type Species struct { 120 | -- Name string `json:"name"` 121 | -- Classification string `json:"classification"` 122 | -- Designation string `json:"designation"` 123 | -- AverageHeight string `json:"average_height"` 124 | -- SkinColors string `json:"skin_colors"` 125 | -- HairColors string `json:"hair_colors"` 126 | -- EyeColors string `json:"eye_colors"` 127 | -- AverageLifespan string `json:"average_lifespan"` 128 | -- Homeworld string `json:"homeworld"` 129 | -- Language string `json:"language"` 130 | -- People []string `json:"people"` 131 | -- Films []string `json:"films"` 132 | -- Created string `json:"created"` 133 | -- Edited string `json:"edited"` 134 | -- URL string `json:"url"` 135 | -- } 136 | -- 137 | -- type Starship struct { 138 | -- Name string `json:"name"` 139 | -- Model string `json:"model"` 140 | -- Manufacturer string `json:"manufacturer"` 141 | -- CostInCredits string `json:"cost_in_credits"` 142 | -- Length string `json:"length"` 143 | -- MaxAtmospheringSpeed string `json:"max_atmosphering_speed"` 144 | -- Crew string `json:"crew"` 145 | -- Passengers string `json:"passengers"` 146 | -- CargoCapacity string `json:"cargo_capacity"` 147 | -- Consumables string `json:"consumables"` 148 | -- HyperdriveRating string `json:"hyperdrive_rating"` 149 | -- MGLT string `json:"MGLT"` 150 | -- StarshipClass string `json:"starship_class"` 151 | -- Pilots []string `json:"pilots"` 152 | -- Films []string `json:"films"` 153 | -- Created string `json:"created"` 154 | -- Edited string `json:"edited"` 155 | -- URL string `json:"url"` 156 | -- } 157 | -- 158 | -- type Vehicle struct { 159 | -- Name string `json:"name"` 160 | -- Model string `json:"model"` 161 | -- Manufacturer string `json:"manufacturer"` 162 | -- CostInCredits string `json:"cost_in_credits"` 163 | -- Length string `json:"length"` 164 | -- MaxAtmospheringSpeed string `json:"max_atmosphering_speed"` 165 | -- Crew string `json:"crew"` 166 | -- Passengers string `json:"passengers"` 167 | -- CargoCapacity string `json:"cargo_capacity"` 168 | -- Consumables string `json:"consumables"` 169 | -- VehicleClass string `json:"vechicle_class"` 170 | -- Pilots []string `json:"pilots"` 171 | -- Films []string `json:"films"` 172 | -- Created string `json:"created"` 173 | -- Edited string `json:"edited"` 174 | -- URL string `json:"url"` 175 | -- } 176 | 177 | -------------------------------------------------------------------------------- /pkg/gooq/expression_test.go: -------------------------------------------------------------------------------- 1 | package gooq 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | 7 | "github.com/google/uuid" 8 | ) 9 | 10 | var expressionTestCases = []TestCase{ 11 | { 12 | Constructed: Count(Table1.Column1), 13 | ExpectedStmt: `COUNT("table1".column1)`, 14 | }, 15 | { 16 | Constructed: Count(Table1.Column1).IsGt(5), 17 | ExpectedStmt: `COUNT("table1".column1) > $1`, 18 | Arguments: []interface{}{float64(5)}, 19 | }, 20 | { 21 | Constructed: Table1.Column1.Asc(), 22 | ExpectedStmt: `"table1".column1 ASC`, 23 | }, 24 | { 25 | Constructed: Table1.Column1.Desc(), 26 | ExpectedStmt: `"table1".column1 DESC`, 27 | }, 28 | { 29 | Constructed: Table1.Column1.IsNull(), 30 | ExpectedStmt: `"table1".column1 IS NULL`, 31 | }, 32 | { 33 | Constructed: Table1.Column1.IsNotNull(), 34 | ExpectedStmt: `"table1".column1 IS NOT NULL`, 35 | }, 36 | { 37 | Constructed: Table1.Column1.Eq(Table2.Column1).Or(Table1.Column2.Eq(Table2.Column2)), 38 | ExpectedStmt: `("table1".column1 = "table2".column1 OR "table1".column2 = "table2".column2)`, 39 | }, 40 | { 41 | Constructed: Table1.Column1.Eq(Table2.Column1).And(Table1.Column2.Eq(Table2.Column2)), 42 | ExpectedStmt: `("table1".column1 = "table2".column1 AND "table1".column2 = "table2".column2)`, 43 | }, 44 | { 45 | Constructed: Table1.Column1.Eq(Table2.Column1).And(Table1.Column2.Eq(Table2.Column2)).And(Table1.Column2.Eq(Table2.Column2)), 46 | ExpectedStmt: `(("table1".column1 = "table2".column1 AND "table1".column2 = "table2".column2) AND "table1".column2 = "table2".column2)`, 47 | }, 48 | { 49 | Constructed: Table1.Column1.Eq(Table2.Column1).And(Table1.Column2.Eq(Table2.Column2)).Or(Table1.Column2.Eq(Table2.Column2)), 50 | ExpectedStmt: `(("table1".column1 = "table2".column1 AND "table1".column2 = "table2".column2) OR "table1".column2 = "table2".column2)`, 51 | }, 52 | { 53 | Constructed: Table1.BoolColumn.IsEq(true), 54 | ExpectedStmt: `"table1".bool_column = $1`, 55 | }, 56 | { 57 | Constructed: Table1.BoolColumn.IsNotEq(true), 58 | ExpectedStmt: `"table1".bool_column != $1`, 59 | }, 60 | { 61 | Constructed: Table1.DecimalColumn.IsGt(1.0), 62 | ExpectedStmt: `"table1".decimal_column > $1`, 63 | }, 64 | { 65 | Constructed: Table1.DecimalColumn.IsGte(1.0), 66 | ExpectedStmt: `"table1".decimal_column >= $1`, 67 | }, 68 | { 69 | Constructed: Table1.DecimalColumn.IsLt(1.0), 70 | ExpectedStmt: `"table1".decimal_column < $1`, 71 | }, 72 | { 73 | Constructed: Table1.DecimalColumn.IsLte(1.0), 74 | ExpectedStmt: `"table1".decimal_column <= $1`, 75 | }, 76 | { 77 | Constructed: Table1.DecimalColumn.IsEq(1.0), 78 | ExpectedStmt: `"table1".decimal_column = $1`, 79 | }, 80 | { 81 | Constructed: Table1.DecimalColumn.IsNotEq(1.0), 82 | ExpectedStmt: `"table1".decimal_column != $1`, 83 | }, 84 | { 85 | Constructed: Table1.DecimalColumn.Add(Int64(42)), 86 | ExpectedStmt: `"table1".decimal_column + $1`, 87 | }, 88 | { 89 | Constructed: Table1.DecimalColumn.Sub(Int64(-42)), 90 | ExpectedStmt: `"table1".decimal_column - $1`, 91 | }, 92 | { 93 | Constructed: Table1.DecimalColumn.Mult(Int64(42)), 94 | ExpectedStmt: `"table1".decimal_column * $1`, 95 | }, 96 | { 97 | Constructed: Table1.DecimalColumn.Div(Int64(0)), 98 | ExpectedStmt: `"table1".decimal_column / $1`, 99 | }, 100 | { 101 | Constructed: Table1.DecimalColumn.Sqrt(), 102 | ExpectedStmt: `|/ "table1".decimal_column`, 103 | }, 104 | { 105 | Constructed: Table1.StringColumn.Lt(Table2.StringColumn), 106 | ExpectedStmt: `"table1".string_column < "table2".string_column`, 107 | }, 108 | { 109 | Constructed: Table1.StringColumn.Lte(Table2.StringColumn), 110 | ExpectedStmt: `"table1".string_column <= "table2".string_column`, 111 | }, 112 | { 113 | Constructed: Table1.StringColumn.Gt(Table2.StringColumn), 114 | ExpectedStmt: `"table1".string_column > "table2".string_column`, 115 | }, 116 | { 117 | Constructed: Table1.StringColumn.Gte(Table2.StringColumn), 118 | ExpectedStmt: `"table1".string_column >= "table2".string_column`, 119 | }, 120 | { 121 | Constructed: Table1.StringColumn.Eq(Table2.StringColumn), 122 | ExpectedStmt: `"table1".string_column = "table2".string_column`, 123 | }, 124 | { 125 | Constructed: Table1.StringColumn.NotEq(Table2.StringColumn), 126 | ExpectedStmt: `"table1".string_column != "table2".string_column`, 127 | }, 128 | { 129 | Constructed: Table1.StringColumn.IsLt("foo"), 130 | ExpectedStmt: `"table1".string_column < $1`, 131 | }, 132 | { 133 | Constructed: Table1.StringColumn.IsLte("foo"), 134 | ExpectedStmt: `"table1".string_column <= $1`, 135 | }, 136 | { 137 | Constructed: Table1.StringColumn.IsGt("foo"), 138 | ExpectedStmt: `"table1".string_column > $1`, 139 | }, 140 | { 141 | Constructed: Table1.StringColumn.IsGte("foo"), 142 | ExpectedStmt: `"table1".string_column >= $1`, 143 | }, 144 | { 145 | Constructed: Table1.StringColumn.IsEq("foo"), 146 | ExpectedStmt: `"table1".string_column = $1`, 147 | }, 148 | { 149 | Constructed: Table1.StringColumn.IsNotEq("foo"), 150 | ExpectedStmt: `"table1".string_column != $1`, 151 | }, 152 | { 153 | Constructed: Table1.StringColumn.Like("foo%"), 154 | ExpectedStmt: `"table1".string_column LIKE $1`, 155 | }, 156 | { 157 | Constructed: Table1.StringColumn.ILike("%foo%"), 158 | ExpectedStmt: `"table1".string_column ILIKE $1`, 159 | }, 160 | { 161 | Constructed: Table1.TimeColumn.IsGt(time.Now()), 162 | ExpectedStmt: `"table1".time_column > $1`, 163 | }, 164 | { 165 | Constructed: Table1.TimeColumn.IsGte(time.Now()), 166 | ExpectedStmt: `"table1".time_column >= $1`, 167 | }, 168 | { 169 | Constructed: Table1.TimeColumn.IsLt(time.Now()), 170 | ExpectedStmt: `"table1".time_column < $1`, 171 | }, 172 | { 173 | Constructed: Table1.TimeColumn.IsLte(time.Now()), 174 | ExpectedStmt: `"table1".time_column <= $1`, 175 | }, 176 | { 177 | Constructed: Table1.TimeColumn.IsEq(time.Now()), 178 | ExpectedStmt: `"table1".time_column = $1`, 179 | }, 180 | { 181 | Constructed: Table1.TimeColumn.IsNotEq(time.Now()), 182 | ExpectedStmt: `"table1".time_column != $1`, 183 | }, 184 | { 185 | Constructed: Table1.ID.IsEq(uuid.Nil), 186 | ExpectedStmt: `"table1".id = $1`, 187 | }, 188 | { 189 | Constructed: Table1.ID.IsNotEq(uuid.Nil), 190 | ExpectedStmt: `"table1".id != $1`, 191 | }, 192 | { 193 | Constructed: Table1.ID.In(Select().From(Table1)), 194 | ExpectedStmt: `"table1".id IN (SELECT * FROM public.table1)`, 195 | }, 196 | { 197 | Constructed: Table1.ID.NotIn(Select().From(Table1)), 198 | ExpectedStmt: `"table1".id NOT IN (SELECT * FROM public.table1)`, 199 | }, 200 | { 201 | Constructed: Table1.ID.IsIn(uuid.Nil, uuid.Nil), 202 | ExpectedStmt: `"table1".id IN ($1, $2)`, 203 | Arguments: []interface{}{uuid.Nil, uuid.Nil}, 204 | }, 205 | { 206 | Constructed: Table1.ID.IsNotIn(uuid.Nil, uuid.Nil), 207 | ExpectedStmt: `"table1".id NOT IN ($1, $2)`, 208 | Arguments: []interface{}{uuid.Nil, uuid.Nil}, 209 | }, 210 | { 211 | Constructed: Coalesce(Sum(Table1.Column1).Filter(Table1.Column2.IsDistinctFrom("str1")), Float64(0)).As("new_name"), 212 | ExpectedStmt: `COALESCE(SUM("table1".column1) FILTER (WHERE "table1".column2 IS DISTINCT FROM $1), $2) AS "new_name"`, 213 | }, 214 | } 215 | 216 | func TestExpressions(t *testing.T) { 217 | runTestCases(t, expressionTestCases) 218 | } 219 | -------------------------------------------------------------------------------- /pkg/gooq/insert.go: -------------------------------------------------------------------------------- 1 | package gooq 2 | 3 | import ( 4 | "context" 5 | "database/sql" 6 | 7 | "github.com/jmoiron/sqlx" 8 | ) 9 | 10 | type InsertSetStep interface { 11 | InsertSetMoreStep 12 | Select(s Selectable) InsertOnConflictStep 13 | } 14 | 15 | type InsertSetMoreStep interface { 16 | InsertValuesStep 17 | Set(f Field, v interface{}) InsertSetMoreStep 18 | Columns(fields ...Field) InsertValuesStep 19 | } 20 | 21 | type InsertValuesStep interface { 22 | InsertOnConflictStep 23 | Values(v ...interface{}) InsertValuesStep 24 | } 25 | 26 | type InsertOnConflictStep interface { 27 | InsertReturningStep 28 | OnConflictDoNothing() InsertReturningStep 29 | OnConflictDoUpdate(*DatabaseConstraint) InsertOnConflictSetStep 30 | } 31 | 32 | type InsertOnConflictSetStep interface { 33 | InsertReturningStep 34 | SetUpdates(f Field, v interface{}) InsertOnConflictSetStep 35 | SetUpdateColumns(f ...Field) InsertOnConflictSetStep 36 | } 37 | 38 | type InsertReturningStep interface { 39 | InsertFinalStep 40 | Returning(...Expression) InsertResultStep 41 | } 42 | 43 | type InsertResultStep interface { 44 | Fetchable 45 | Renderable 46 | } 47 | 48 | type InsertFinalStep interface { 49 | Executable 50 | Renderable 51 | } 52 | 53 | /////////////////////////////////////////////////////////////////////////////// 54 | // Implementation 55 | /////////////////////////////////////////////////////////////////////////////// 56 | 57 | // https://www.postgresql.org/docs/current/sql-insert.html 58 | 59 | type insert struct { 60 | table Table 61 | selection Selectable 62 | columns []Field 63 | values [][]interface{} 64 | conflictAction ConflictAction 65 | conflictConstraint *DatabaseConstraint 66 | conflictSetPredicates []setPredicate 67 | returning []Expression 68 | } 69 | 70 | func InsertInto(t Table) InsertSetStep { 71 | return &insert{table: t} 72 | } 73 | 74 | func (i *insert) Select(s Selectable) InsertOnConflictStep { 75 | i.selection = s 76 | return i 77 | } 78 | 79 | func (i *insert) Columns(fields ...Field) InsertValuesStep { 80 | i.columns = fields 81 | return i 82 | } 83 | 84 | func (i *insert) Values(values ...interface{}) InsertValuesStep { 85 | i.values = append(i.values, values) 86 | return i 87 | } 88 | 89 | func (i *insert) Set( 90 | field Field, value interface{}, 91 | ) InsertSetMoreStep { 92 | i.columns = append(i.columns, field) 93 | if len(i.values) == 0 { 94 | i.values = append(i.values, []interface{}{}) 95 | } 96 | i.values[0] = append(i.values[0], value) 97 | return i 98 | } 99 | 100 | func (i *insert) OnConflictDoNothing() InsertReturningStep { 101 | i.conflictAction = ConflictActionDoNothing 102 | return i 103 | } 104 | 105 | func (i *insert) OnConflictDoUpdate( 106 | constraint *DatabaseConstraint, 107 | ) InsertOnConflictSetStep { 108 | i.conflictAction = ConflictActionDoUpdate 109 | i.conflictConstraint = constraint 110 | return i 111 | } 112 | 113 | func (i *insert) SetUpdates( 114 | field Field, value interface{}, 115 | ) InsertOnConflictSetStep { 116 | i.conflictSetPredicates = append(i.conflictSetPredicates, setPredicate{field, value}) 117 | return i 118 | } 119 | 120 | func (i *insert) SetUpdateColumns( 121 | fields ...Field, 122 | ) InsertOnConflictSetStep { 123 | // NOTE: excluded has to be lowercase 124 | excludedTable := NewTable("", "excluded") 125 | for _, field := range fields { 126 | i.conflictSetPredicates = append(i.conflictSetPredicates, setPredicate{ 127 | field: field, 128 | value: NewStringField(excludedTable, field.GetName()), 129 | }) 130 | } 131 | return i 132 | } 133 | 134 | func (i *insert) Returning(f ...Expression) InsertResultStep { 135 | i.returning = f 136 | return i 137 | } 138 | 139 | /////////////////////////////////////////////////////////////////////////////// 140 | // Executable 141 | /////////////////////////////////////////////////////////////////////////////// 142 | 143 | func (i *insert) Exec(dl Dialect, db DBInterface) (sql.Result, error) { 144 | builder := i.Build(dl) 145 | return db.Exec(builder.String(), builder.arguments...) 146 | } 147 | 148 | func (i *insert) ExecWithContext( 149 | ctx context.Context, dl Dialect, db DBInterface) (sql.Result, error) { 150 | builder := i.Build(dl) 151 | return db.ExecContext(ctx, builder.String(), builder.arguments...) 152 | } 153 | 154 | /////////////////////////////////////////////////////////////////////////////// 155 | // Fetchable 156 | /////////////////////////////////////////////////////////////////////////////// 157 | 158 | func (i *insert) Fetch(dl Dialect, db DBInterface) (*sqlx.Rows, error) { 159 | builder := i.Build(dl) 160 | return db.Queryx(builder.String(), builder.arguments...) 161 | } 162 | 163 | func (i *insert) FetchRow(dl Dialect, db DBInterface) *sqlx.Row { 164 | builder := i.Build(dl) 165 | return db.QueryRowx(builder.String(), builder.arguments...) 166 | } 167 | 168 | func (i *insert) FetchWithContext( 169 | ctx context.Context, dl Dialect, db DBInterface) (*sqlx.Rows, error) { 170 | builder := i.Build(dl) 171 | return db.QueryxContext(ctx, builder.String(), builder.arguments...) 172 | } 173 | 174 | func (i *insert) FetchRowWithContext( 175 | ctx context.Context, dl Dialect, db DBInterface) *sqlx.Row { 176 | builder := i.Build(dl) 177 | return db.QueryRowxContext(ctx, builder.String(), builder.arguments...) 178 | } 179 | 180 | /////////////////////////////////////////////////////////////////////////////// 181 | // Renderable 182 | /////////////////////////////////////////////////////////////////////////////// 183 | 184 | func (i *insert) Build(d Dialect) *Builder { 185 | builder := Builder{} 186 | i.Render(&builder) 187 | return &builder 188 | } 189 | 190 | func (i *insert) Render( 191 | builder *Builder, 192 | ) { 193 | // INSERT INTO table_name 194 | builder.Printf("INSERT INTO %s ", i.table.GetQualifiedName()) 195 | 196 | if i.selection != nil { 197 | // handle INSERT ...SELECT 198 | builder.Print("(") 199 | i.selection.Render(builder) 200 | builder.Print(")") 201 | } else { 202 | // handle INSERT .. SET 203 | i.renderColumnsAndValues(builder, i.columns, i.values) 204 | } 205 | 206 | // [ ON CONFLICT conflict_action ] 207 | if i.conflictAction != ConflictActionNil { 208 | builder.Printf(" ON CONFLICT") 209 | if i.conflictConstraint != nil { 210 | if i.conflictConstraint.Predicate.Valid { 211 | builder.Print(" ") 212 | builder.RenderFieldArray(i.conflictConstraint.Columns) 213 | builder.Printf(" WHERE %s", i.conflictConstraint.Predicate.String) 214 | } else { 215 | builder.Printf(" ON CONSTRAINT %s", i.conflictConstraint.Name) 216 | } 217 | } 218 | if i.conflictAction == ConflictActionDoNothing { 219 | builder.Print(" DO NOTHING") 220 | } else if i.conflictAction == ConflictActionDoUpdate { 221 | builder.Printf(" %s SET ", i.conflictAction) 222 | builder.RenderSetPredicates(i.conflictSetPredicates) 223 | } 224 | } 225 | 226 | // [ RETURNING output_expression ] 227 | if i.returning != nil { 228 | builder.Print(" RETURNING ") 229 | builder.RenderExpressions(i.returning) 230 | } 231 | } 232 | 233 | // render set columns and values 234 | func (i *insert) renderColumnsAndValues( 235 | builder *Builder, columns []Field, values [][]interface{}, 236 | ) *Builder { 237 | if len(columns) > 0 { 238 | builder.Print("(") 239 | for index, column := range columns { 240 | builder.Printf(column.GetName()) 241 | if index != len(columns)-1 { 242 | builder.Print(", ") 243 | } 244 | } 245 | builder.Printf(") ") 246 | } 247 | builder.Printf("VALUES ") 248 | for arrayIndex, array := range values { 249 | builder.Print("(") 250 | for index, value := range array { 251 | builder.RenderExpression(newLiteralExpression(value)) 252 | if index != len(array)-1 { 253 | builder.Print(", ") 254 | } 255 | } 256 | builder.Print(")") 257 | if arrayIndex != len(values)-1 { 258 | builder.Print(", ") 259 | } 260 | } 261 | return builder 262 | } 263 | -------------------------------------------------------------------------------- /pkg/gooq/select_test.go: -------------------------------------------------------------------------------- 1 | package gooq 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | var selectTestCases = []TestCase{ 8 | { 9 | Constructed: Select(Greatest(Int64(1), Int64(2), Int64(3))), 10 | ExpectedStmt: `SELECT GREATEST($1, $2, $3)`, 11 | Arguments: []interface{}{int64(1), int64(2), int64(3)}, 12 | }, 13 | { 14 | Constructed: Select().From(Table1), 15 | ExpectedStmt: `SELECT * FROM public.table1`, 16 | }, 17 | { 18 | Constructed: SelectCount().From(Table1), 19 | ExpectedStmt: `SELECT COUNT(*) FROM public.table1`, 20 | }, 21 | { 22 | Constructed: Select().Distinct().From(Table1), 23 | ExpectedStmt: `SELECT DISTINCT * FROM public.table1`, 24 | }, 25 | { 26 | Constructed: Select(Table1.Column1).DistinctOn(Table1.Column2, Table1.Column3).From(Table1), 27 | ExpectedStmt: `SELECT DISTINCT ON ("table1".column2, "table1".column3) "table1".column1 FROM public.table1`, 28 | }, 29 | { 30 | Constructed: Select(Table1.Column1).From(Table1), 31 | ExpectedStmt: `SELECT "table1".column1 FROM public.table1`, 32 | }, 33 | { 34 | Constructed: Select(Table1.Column1, Table1.Column2).From(Table1), 35 | ExpectedStmt: `SELECT "table1".column1, "table1".column2 FROM public.table1`, 36 | }, 37 | { 38 | Constructed: Select(Table1.Column1.As("result")).From(Table1), 39 | ExpectedStmt: `SELECT "table1".column1 AS "result" FROM public.table1`, 40 | }, 41 | { 42 | Constructed: Select(Table1.Column1).From(Table1).Where(Table1.Column2.Eq(String("foo"))), 43 | ExpectedStmt: `SELECT "table1".column1 FROM public.table1 WHERE "table1".column2 = $1`, 44 | }, 45 | { 46 | Constructed: Select(Table1.Column1.Filter(Table1.Column2.Eq(String("foo")))).From(Table1), 47 | ExpectedStmt: `SELECT "table1".column1 FILTER (WHERE "table1".column2 = $1) FROM public.table1`, 48 | }, 49 | { 50 | Constructed: Select(Table1.Column1).From(Table1).Where( 51 | Table1.Column2.IsIn("quix", "foo"), 52 | Table1.Column2.Eq(String("quack"))), 53 | ExpectedStmt: `SELECT "table1".column1 FROM public.table1 WHERE "table1".column2 IN ($1, $2) AND "table1".column2 = $3`, 54 | }, 55 | { 56 | Constructed: Select(Table1.Column3.Add(Int64(5))).From(Table1), 57 | ExpectedStmt: `SELECT "table1".column3 + $1 FROM public.table1`, 58 | }, 59 | { 60 | Constructed: Select(Table1.Column3.Add(Float64(1.72))).From(Table1), 61 | ExpectedStmt: `SELECT "table1".column3 + $1 FROM public.table1`, 62 | }, 63 | { 64 | Constructed: Select(Table1.Column3.Add(Table1.Column4)).From(Table1), 65 | ExpectedStmt: `SELECT "table1".column3 + "table1".column4 FROM public.table1`, 66 | }, 67 | { 68 | Constructed: Select(Table1.Column3.Sub(Table1.Column4)).From(Table1), 69 | ExpectedStmt: `SELECT "table1".column3 - "table1".column4 FROM public.table1`, 70 | }, 71 | { 72 | Constructed: Select(Table1.Column3.Mult(Table1.Column4)).From(Table1), 73 | ExpectedStmt: `SELECT "table1".column3 * "table1".column4 FROM public.table1`, 74 | }, 75 | { 76 | Constructed: Select(Table1.Column3.Div(Table1.Column4)).From(Table1), 77 | ExpectedStmt: `SELECT "table1".column3 / "table1".column4 FROM public.table1`, 78 | }, 79 | { 80 | Constructed: Select(Table1.Column3.Div(Table1.Column4).As("result")).From(Table1), 81 | ExpectedStmt: `SELECT "table1".column3 / "table1".column4 AS "result" FROM public.table1`, 82 | }, 83 | { 84 | Constructed: Select(Table1.Column1).From(Table1).Where( 85 | Table1.Column2.Eq(String("quix")), 86 | Table1.Column2.Eq(String("quack"))). 87 | Union( 88 | Select(Table1.Column1).From(Table1).Where( 89 | Table1.Column2.Eq(String("foo")), 90 | Table1.Column2.Eq(String("quack")))). 91 | OrderBy(NewStringField(NewTable("", ""), "column2").Asc()), 92 | ExpectedStmt: `SELECT "table1".column1 FROM public.table1 WHERE "table1".column2 = $1 AND "table1".column2 = $2 UNION (SELECT "table1".column1 FROM public.table1 WHERE "table1".column2 = $3 AND "table1".column2 = $4) ORDER BY column2 ASC`, 93 | }, 94 | { 95 | Constructed: Select().From(Table1).OrderBy(Table1.Column1.Asc()), 96 | ExpectedStmt: `SELECT * FROM public.table1 ORDER BY "table1".column1 ASC`, 97 | }, 98 | { 99 | Constructed: Select().From(Table1).OrderBy(Table1.Column1.Desc()), 100 | ExpectedStmt: `SELECT * FROM public.table1 ORDER BY "table1".column1 DESC`, 101 | }, 102 | { 103 | Constructed: Select().From(Table1).OrderBy(Table1.Column1, Table1.ID).Seek("foo", "bar"), 104 | ExpectedStmt: `SELECT * FROM public.table1 WHERE (("table1".column1 > $1) OR ("table1".column1 = $2 AND "table1".id > $3)) ORDER BY "table1".column1, "table1".id`, 105 | Arguments: []interface{}{"foo", "foo", "bar"}, 106 | }, 107 | { 108 | Constructed: Select().From(Table1).OrderBy(Table1.Column1.Desc(), Table1.ID.Desc()).Seek("foo", "bar"), 109 | ExpectedStmt: `SELECT * FROM public.table1 WHERE (("table1".column1 < $1) OR ("table1".column1 = $2 AND "table1".id < $3)) ORDER BY "table1".column1 DESC, "table1".id DESC`, 110 | Arguments: []interface{}{"foo", "foo", "bar"}, 111 | }, 112 | { 113 | Constructed: Select().From(Table1).OrderBy(Table1.Column1.Asc(), Table1.ID.Desc()).Seek("foo", "bar"), 114 | ExpectedStmt: `SELECT * FROM public.table1 WHERE (("table1".column1 > $1) OR ("table1".column1 = $2 AND "table1".id < $3)) ORDER BY "table1".column1 ASC, "table1".id DESC`, 115 | Arguments: []interface{}{"foo", "foo", "bar"}, 116 | }, 117 | { 118 | Constructed: Select().From(Table1).OrderBy(Table1.Column1.Desc(), Table1.ID.Asc()).Seek("foo", "bar"), 119 | ExpectedStmt: `SELECT * FROM public.table1 WHERE (("table1".column1 < $1) OR ("table1".column1 = $2 AND "table1".id > $3)) ORDER BY "table1".column1 DESC, "table1".id ASC`, 120 | Arguments: []interface{}{"foo", "foo", "bar"}, 121 | }, 122 | { 123 | Constructed: Select().From(Table1).OrderBy(Table1.Column1.Desc(), Table1.Column2.Desc(), Table1.ID.Asc()).Seek("foo", "bar", "baz"), 124 | ExpectedStmt: `SELECT * FROM public.table1 WHERE (("table1".column1 < $1) OR ("table1".column1 = $2 AND "table1".column2 < $3) OR ("table1".column1 = $4 AND "table1".column2 = $5 AND "table1".id > $6)) ORDER BY "table1".column1 DESC, "table1".column2 DESC, "table1".id ASC`, 125 | Arguments: []interface{}{"foo", "foo", "bar", "foo", "bar", "baz"}, 126 | }, 127 | { 128 | Constructed: Select().From(Table1).GroupBy(Table1.Column1), 129 | ExpectedStmt: `SELECT * FROM public.table1 GROUP BY "table1".column1`, 130 | }, 131 | { 132 | Constructed: Select(Table1.Column1.Filter(Table1.Column2.Eq(String("foo")))).From(Table1), 133 | ExpectedStmt: `SELECT "table1".column1 FILTER (WHERE "table1".column2 = $1) FROM public.table1`, 134 | }, 135 | { 136 | Constructed: Select(Coalesce(Table1.Column1.Filter(Table1.Column2.Eq(String("foo"))), Int64(0)).As("total")).From(Table1), 137 | ExpectedStmt: `SELECT COALESCE("table1".column1 FILTER (WHERE "table1".column2 = $1), $2) AS "total" FROM public.table1`, 138 | }, 139 | { 140 | Constructed: Select().From(Table1).Limit(10), 141 | ExpectedStmt: `SELECT * FROM public.table1 LIMIT 10`, 142 | }, 143 | { 144 | Constructed: Select(Table1.Column1).From(Table1).Join(Table2).On(Table2.Column1.Eq(Table1.Column1)), 145 | ExpectedStmt: `SELECT "table1".column1 FROM public.table1 JOIN public.table2 ON "table2".column1 = "table1".column1`, 146 | }, 147 | { 148 | Constructed: Select(Table1.Column1, Table2.Column1).From(Table1). 149 | Join(Table2).On(Table2.Column1.Eq(Table1.Column1), Table2.Column2.Eq(Table1.Column2)), 150 | ExpectedStmt: `SELECT "table1".column1, "table2".column1 FROM public.table1 JOIN public.table2 ON "table2".column1 = "table1".column1 AND "table2".column2 = "table1".column2`, 151 | }, 152 | { 153 | Constructed: Select(Table1.Column1).From(Table1). 154 | LeftOuterJoin(Table2).On(Table2.Column1.Eq(Table1.Column1)). 155 | LeftOuterJoin(Table3).On(Table3.Column1.Eq(Table1.Column1)), 156 | ExpectedStmt: `SELECT "table1".column1 FROM public.table1 LEFT OUTER JOIN public.table2 ON "table2".column1 = "table1".column1 LEFT OUTER JOIN public.table3 ON "table3".column1 = "table1".column1`, 157 | }, 158 | { 159 | Constructed: Select().From(Table1). 160 | LeftOuterJoin(Select(Table1.Column1).From(Table1).As("boo")). 161 | On(NewStringField(NewTable("", "boo"), "column1").Eq(Table1.Column1)), 162 | ExpectedStmt: `SELECT * FROM public.table1 LEFT OUTER JOIN (SELECT "table1".column1 FROM public.table1) AS "boo" ON "boo".column1 = "table1".column1`, 163 | }, 164 | { 165 | Constructed: Select().From(Select(Table1.Column1).From(Table1).As("boo")), 166 | ExpectedStmt: `SELECT * FROM (SELECT "table1".column1 FROM public.table1) AS "boo"`, 167 | }, 168 | { 169 | Constructed: Select(Table1.Column1, Table2.Column1).From( 170 | Select(Table1.Column1).From(Table1).As("boo")). 171 | Join(Table2).On(Table2.Column1.Eq(Table1.Column1)), 172 | ExpectedStmt: `SELECT "table1".column1, "table2".column1 FROM (SELECT "table1".column1 FROM public.table1) AS "boo" JOIN public.table2 ON "table2".column1 = "table1".column1`, 173 | }, 174 | { 175 | Constructed: Select().From(Table1).GroupBy(Table1.Column1).Having(Count(Asterisk).IsGt(5)), 176 | ExpectedStmt: `SELECT * FROM public.table1 GROUP BY "table1".column1 HAVING COUNT(*) > $1`, 177 | Arguments: []interface{}{float64(5)}, 178 | }, 179 | { 180 | Constructed: Select().From(Table1). 181 | Where(Table1.Column1.IsEq("foo")). 182 | For(LockingTypeUpdate, LockingOptionNone), 183 | ExpectedStmt: `SELECT * FROM public.table1 WHERE "table1".column1 = $1 FOR UPDATE`, 184 | Arguments: []interface{}{"foo"}, 185 | }, 186 | { 187 | Constructed: Select().From(Table1). 188 | Where(Table1.Column1.IsEq("foo")). 189 | For(LockingTypeUpdate, LockingOptionSkipLocked), 190 | ExpectedStmt: `SELECT * FROM public.table1 WHERE "table1".column1 = $1 FOR UPDATE SKIP LOCKED`, 191 | Arguments: []interface{}{"foo"}, 192 | }, 193 | { 194 | Constructed: With("temp", 195 | Update(Table1).Set(Table1.Column1, "Foo").Returning(Asterisk)). 196 | Select().From(NewTable("", "temp")).OrderBy(NewStringField(nil, "column2")), 197 | ExpectedStmt: `WITH temp AS (UPDATE public.table1 SET column1 = $1 RETURNING *) SELECT * FROM temp ORDER BY column2`, 198 | }, 199 | //{ 200 | // Select(TimeBucket5MinutesField, Table1.Column2.Avg()).From(Table1), 201 | // "SELECT time_bucket('5 minutes', "table1".creation_date) AS five_min, AVG("table1".column2) FROM public.table1", 202 | //}, 203 | //{ 204 | // Select(TimeBucket("5 minutes", Table1.CreationDate), Table1.Column1.Last(Table1.CreationDate.GetName()).As("last"), Table1.Column1.First(Table1.CreationDate.GetName()).As("first")).From(Table1), 205 | // "SELECT time_bucket('5 minutes', "table1".creation_date), last("table1".column1, creation_date) AS last, first("table1".column1, creation_date) AS first FROM public.table1", 206 | //}, 207 | //{ 208 | // Select(TimeBucket5MinutesField, Table1.Column2.Avg()).From(Table1).GroupBy(TimeBucket5MinutesField), 209 | // "SELECT time_bucket('5 minutes', "table1".creation_date) AS five_min, AVG("table1".column2) FROM public.table1 GROUP BY five_min", 210 | //}, 211 | } 212 | 213 | func TestSelects(t *testing.T) { 214 | runTestCases(t, selectTestCases) 215 | } 216 | -------------------------------------------------------------------------------- /pkg/gooq/select.go: -------------------------------------------------------------------------------- 1 | package gooq 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | 7 | "gopkg.in/guregu/null.v3" 8 | 9 | "github.com/jmoiron/sqlx" 10 | ) 11 | 12 | type join struct { 13 | target Selectable 14 | joinType JoinType 15 | conditions []Expression 16 | } 17 | 18 | type SelectWithStep interface { 19 | SelectFromStep 20 | Select(projections ...Selectable) SelectFromStep 21 | } 22 | 23 | type SelectDistinctStep interface { 24 | SelectWhereStep 25 | Distinct() SelectFromStep 26 | DistinctOn(...Expression) SelectFromStep 27 | From(Selectable) SelectJoinStep 28 | } 29 | 30 | type SelectFromStep interface { 31 | SelectWhereStep 32 | From(Selectable) SelectJoinStep 33 | } 34 | 35 | type SelectJoinStep interface { 36 | SelectWhereStep 37 | Join(Selectable) SelectOnStep 38 | LeftOuterJoin(Selectable) SelectOnStep 39 | } 40 | 41 | type SelectOnStep interface { 42 | SelectWhereStep 43 | On(...Expression) SelectJoinStep 44 | } 45 | 46 | type SelectWhereStep interface { 47 | SelectGroupByStep 48 | Where(conditions ...Expression) SelectGroupByStep 49 | } 50 | 51 | type SelectGroupByStep interface { 52 | SelectHavingStep 53 | GroupBy(...Expression) SelectHavingStep 54 | } 55 | 56 | type SelectHavingStep interface { 57 | SelectOrderByStep 58 | Having(conditions ...Expression) SelectOrderByStep 59 | } 60 | 61 | type SelectOrderByStep interface { 62 | SelectOffsetStep 63 | OrderBy(...Expression) SelectOffsetStep 64 | } 65 | 66 | type SelectOffsetStep interface { 67 | SelectLimitStep 68 | Offset(offset int) SelectLimitStep 69 | Seek(v ...interface{}) SelectLimitStep 70 | } 71 | 72 | type SelectLimitStep interface { 73 | SelectFinalStep 74 | Limit(limit int) SelectFinalStep 75 | } 76 | 77 | type SelectFinalStep interface { 78 | Selectable 79 | Fetchable 80 | As(alias string) Selectable 81 | For(LockingType, LockingOption) SelectFinalStep 82 | Union(SelectFinalStep) SelectOrderByStep 83 | } 84 | 85 | /////////////////////////////////////////////////////////////////////////////// 86 | // Implementation 87 | /////////////////////////////////////////////////////////////////////////////// 88 | 89 | type selection struct { 90 | with Selectable 91 | withAlias null.String 92 | selection Selectable 93 | distinctOn []Expression 94 | projections []Selectable 95 | joins []join 96 | joinTarget Selectable 97 | joinType JoinType 98 | predicate []Expression 99 | groups []Expression 100 | havings []Expression 101 | ordering []Expression 102 | unions []SelectFinalStep 103 | alias null.String 104 | isDistinct bool 105 | limit int 106 | offset int 107 | seek []interface{} 108 | lockingType LockingType 109 | lockingOption LockingOption 110 | } 111 | 112 | func Select(projections ...Selectable) SelectDistinctStep { 113 | return &selection{projections: projections} 114 | } 115 | 116 | func SelectCount() SelectDistinctStep { 117 | return &selection{ 118 | projections: []Selectable{Count(Asterisk)}, 119 | } 120 | } 121 | 122 | func With(withAlias string, t Selectable) SelectWithStep { 123 | s := &selection{ 124 | withAlias: null.StringFrom(withAlias), 125 | with: t, 126 | } 127 | return s 128 | } 129 | 130 | func (s *selection) Select(projections ...Selectable) SelectFromStep { 131 | s.projections = projections 132 | return s 133 | } 134 | 135 | func (s *selection) Distinct() SelectFromStep { 136 | s.isDistinct = true 137 | return s 138 | } 139 | 140 | func (s *selection) DistinctOn(f ...Expression) SelectFromStep { 141 | s.distinctOn = f 142 | return s 143 | } 144 | 145 | func (s *selection) From(t Selectable) SelectJoinStep { 146 | s.selection = t 147 | return s 148 | } 149 | 150 | func (s *selection) Join(t Selectable) SelectOnStep { 151 | s.joinTarget = t 152 | s.joinType = Join 153 | return s 154 | } 155 | 156 | func (s *selection) LeftOuterJoin(t Selectable) SelectOnStep { 157 | // TODO copy and paste from From(.) 158 | s.joinTarget = t 159 | s.joinType = LeftOuterJoin 160 | return s 161 | } 162 | 163 | func (s *selection) Union(t SelectFinalStep) SelectOrderByStep { 164 | s.unions = append(s.unions, t) 165 | return s 166 | } 167 | 168 | func (s *selection) On(c ...Expression) SelectJoinStep { 169 | j := join{ 170 | target: s.joinTarget, 171 | joinType: s.joinType, 172 | conditions: c, 173 | } 174 | s.joinTarget = nil 175 | s.joinType = NotJoined 176 | s.joins = append(s.joins, j) 177 | return s 178 | } 179 | 180 | func (s *selection) As(alias string) Selectable { 181 | s.alias = null.StringFrom(alias) 182 | return s 183 | } 184 | 185 | func (s *selection) Where(c ...Expression) SelectGroupByStep { 186 | s.predicate = c 187 | return s 188 | } 189 | 190 | func (s *selection) GroupBy(f ...Expression) SelectHavingStep { 191 | s.groups = f 192 | return s 193 | } 194 | 195 | func (s *selection) Having(c ...Expression) SelectOrderByStep { 196 | s.havings = c 197 | return s 198 | } 199 | 200 | func (s *selection) OrderBy(f ...Expression) SelectOffsetStep { 201 | s.ordering = f 202 | return s 203 | } 204 | 205 | func (s *selection) Offset(offset int) SelectLimitStep { 206 | s.offset = offset 207 | return s 208 | } 209 | 210 | func (s *selection) Seek(v ...interface{}) SelectLimitStep { 211 | s.seek = v 212 | return s 213 | } 214 | 215 | func (s *selection) Limit(limit int) SelectFinalStep { 216 | s.limit = limit 217 | return s 218 | } 219 | 220 | func (s *selection) For( 221 | lockingType LockingType, lockingOption LockingOption, 222 | ) SelectFinalStep { 223 | s.lockingType = lockingType 224 | s.lockingOption = lockingOption 225 | return s 226 | } 227 | 228 | func (s *selection) GetAlias() null.String { 229 | return s.alias 230 | } 231 | 232 | /////////////////////////////////////////////////////////////////////////////// 233 | // Fetchable 234 | /////////////////////////////////////////////////////////////////////////////// 235 | 236 | func (s *selection) Fetch(dl Dialect, db DBInterface) (*sqlx.Rows, error) { 237 | builder := s.Build(dl) 238 | return db.Queryx(builder.String(), builder.arguments...) 239 | } 240 | 241 | func (s *selection) FetchRow(dl Dialect, db DBInterface) *sqlx.Row { 242 | builder := s.Build(dl) 243 | return db.QueryRowx(builder.String(), builder.arguments...) 244 | } 245 | 246 | func (s *selection) FetchWithContext( 247 | ctx context.Context, dl Dialect, db DBInterface) (*sqlx.Rows, error) { 248 | builder := s.Build(dl) 249 | return db.QueryxContext(ctx, builder.String(), builder.arguments...) 250 | } 251 | 252 | func (s *selection) FetchRowWithContext( 253 | ctx context.Context, dl Dialect, db DBInterface) *sqlx.Row { 254 | builder := s.Build(dl) 255 | return db.QueryRowxContext(ctx, builder.String(), builder.arguments...) 256 | } 257 | 258 | /////////////////////////////////////////////////////////////////////////////// 259 | // Renderable 260 | /////////////////////////////////////////////////////////////////////////////// 261 | 262 | func (s *selection) Build(d Dialect) *Builder { 263 | builder := Builder{} 264 | s.Render(&builder) 265 | return &builder 266 | } 267 | 268 | func (s *selection) Render( 269 | builder *Builder, 270 | ) { 271 | 272 | hasAlias := s.alias.Valid 273 | if hasAlias { 274 | builder.Print("(") 275 | } 276 | 277 | if s.withAlias.Valid { 278 | builder.Printf("WITH %s AS (", s.withAlias.String) 279 | s.with.Render(builder) 280 | builder.Print(") ") 281 | } 282 | 283 | builder.Print("SELECT ") 284 | 285 | if s.isDistinct { 286 | builder.Print("DISTINCT ") 287 | } else if len(s.distinctOn) > 0 { 288 | builder.Print("DISTINCT ON (") 289 | builder.RenderExpressions(s.distinctOn) 290 | builder.Print(") ") 291 | } 292 | 293 | projections := s.projections 294 | if len(projections) == 0 { 295 | projections = []Selectable{Asterisk} 296 | } 297 | // It is incorrect to always override projection namespace with selection alias. 298 | // The original this logic turns the following into 299 | // e.g. select item.*, foo.bar from (select * from boo) as item ... 300 | // e.g. select item.*, item.bar from (select * from boo) as item ... 301 | // colClause := renderProjections(alias, s.projection) 302 | builder.RenderProjections(projections) 303 | 304 | // render FROM clause 305 | if s.selection != nil { 306 | builder.Print(" FROM ") 307 | s.selection.Render(builder) 308 | } 309 | 310 | // render JOIN/ON clause 311 | for _, join := range s.joins { 312 | var joinString string 313 | switch join.joinType { 314 | case LeftOuterJoin: 315 | joinString = "LEFT OUTER JOIN" 316 | case Join: 317 | joinString = "JOIN" 318 | } 319 | 320 | builder.Printf(" %s ", joinString) 321 | join.target.Render(builder) 322 | builder.Print(" ON ") 323 | builder.RenderConditions(join.conditions) 324 | } 325 | 326 | predicate := s.predicate 327 | if len(s.seek) > 0 { 328 | predicate = append(predicate, s.getSeekCondition()) 329 | } 330 | 331 | // render WHERE clause 332 | if len(predicate) > 0 { 333 | builder.Print(" WHERE ") 334 | builder.RenderConditions(predicate) 335 | } 336 | 337 | // render GROUP BY clause 338 | if (len(s.groups)) > 0 { 339 | builder.Print(" GROUP BY ") 340 | builder.RenderExpressions(s.groups) 341 | } 342 | 343 | // render HAVING clause 344 | if len(s.havings) > 0 { 345 | builder.Print(" HAVING ") 346 | builder.RenderConditions(s.havings) 347 | } 348 | 349 | // render UNION clause 350 | for _, union := range s.unions { 351 | builder.Print(" UNION (") 352 | union.Render(builder) 353 | builder.Print(")") 354 | } 355 | 356 | // render ORDER BY clause 357 | if (len(s.ordering)) > 0 { 358 | builder.Print(" ORDER BY ") 359 | builder.RenderExpressions(s.ordering) 360 | } 361 | 362 | // render LIMIT clause 363 | if s.limit > 0 { 364 | builder.Printf(" LIMIT %d", s.limit) 365 | } 366 | 367 | // render OFFSET clause 368 | if s.offset > 0 { 369 | builder.Printf(" OFFSET %d", s.offset) 370 | } 371 | 372 | // render LOCKING clause 373 | if s.lockingType != LockingTypeNone { 374 | builder.Printf(" %s", s.lockingType.String()) 375 | if s.lockingOption != LockingOptionNone { 376 | builder.Printf(" %s", s.lockingOption.String()) 377 | } 378 | } 379 | 380 | if hasAlias { 381 | builder.Printf(") AS \"%s\"", s.alias.String) 382 | } 383 | 384 | } 385 | 386 | // faster and stable pagination based on these two articles 387 | // https://blog.jooq.org/2013/10/26/faster-sql-paging-with-jooq-using-the-seek-method/ 388 | // https://blog.jooq.org/2013/11/18/faster-sql-pagination-with-keysets-continued/ 389 | // WARNING: seekAfter does not support seeking NULL values or the NULLS FIRST and 390 | // NULL LAST clauses. 391 | // e.g. Given the following scenario 392 | // Select().From(Table1). 393 | // OrderBy(Table1.Column1.Desc(), Table1.Column2.Desc(), Table1.Column3.Desc()). 394 | // Seek("foo1", "foo2", "foo3"), 395 | // We should generate the following where clause 396 | // WHERE ((column1 < "foo1") 397 | // OR (value1 = "foo1" AND value2 < "foo2") 398 | // OR (value1 = "foo1" AND value2 = "foo2" AND value3 < "foo3")) 399 | func (s *selection) getSeekCondition() Expression { 400 | if len(s.seek) < len(s.ordering) { 401 | panic("number of arguments in seek(...) must be gte number of arguments in orderBy") 402 | } 403 | // we went with the following approach to deal with mixed ordering 404 | var orExpressions []BoolExpression 405 | for i, order := range s.ordering { 406 | var operator Operator 407 | switch order.getOperator() { 408 | case OperatorDesc: 409 | operator = OperatorLt 410 | case OperatorAsc: 411 | operator = OperatorGt 412 | case OperatorNil: 413 | operator = OperatorGt 414 | default: 415 | panic(fmt.Sprintf("seek does not support operator=%s", order.getOperator())) 416 | } 417 | 418 | var andExpressions []BoolExpression 419 | for j := 0; j < i; j++ { 420 | expr := newBinaryBooleanExpressionImpl( 421 | OperatorEq, s.getOrderByField(s.ordering[j]), newLiteralExpression(s.seek[j])) 422 | andExpressions = append(andExpressions, expr) 423 | } 424 | expr := newBinaryBooleanExpressionImpl( 425 | operator, s.getOrderByField(order), newLiteralExpression(s.seek[i])) 426 | andExpressions = append(andExpressions, expr) 427 | orExpressions = append(orExpressions, And(andExpressions...)) 428 | } 429 | return Or(orExpressions...) 430 | } 431 | 432 | func (s *selection) getOrderByField( 433 | order Expression, 434 | ) Expression { 435 | if order.getOperator() == OperatorNil { 436 | return order.getOriginal() 437 | } 438 | return order.getExpressions()[0].getOriginal() 439 | } 440 | -------------------------------------------------------------------------------- /examples/swapi/table/swapi_table.generated.go: -------------------------------------------------------------------------------- 1 | // THIS FILE WAS AUTOGENERATED - ANY EDITS TO THIS WILL BE LOST WHEN IT IS REGENERATED 2 | 3 | package table 4 | 5 | import ( 6 | "context" 7 | 8 | "github.com/lumina-tech/gooq/examples/swapi/model" 9 | "github.com/lumina-tech/gooq/pkg/gooq" 10 | "gopkg.in/guregu/null.v3" 11 | ) 12 | 13 | type colorReferenceTableConstraints struct { 14 | ColorReferenceTablePkey gooq.DatabaseConstraint 15 | } 16 | 17 | type colorReferenceTable struct { 18 | gooq.TableImpl 19 | Asterisk gooq.StringField 20 | Value gooq.StringField 21 | 22 | Constraints *colorReferenceTableConstraints 23 | } 24 | 25 | func newColorReferenceTableConstraints( 26 | instance *colorReferenceTable, 27 | ) *colorReferenceTableConstraints { 28 | constraints := &colorReferenceTableConstraints{} 29 | constraints.ColorReferenceTablePkey = gooq.DatabaseConstraint{ 30 | Name: "color_reference_table_pkey", 31 | Columns: []gooq.Field{ 32 | instance.Value}, 33 | Predicate: null.NewString("", false), 34 | } 35 | return constraints 36 | } 37 | 38 | func newColorReferenceTable() *colorReferenceTable { 39 | instance := &colorReferenceTable{} 40 | instance.Initialize("public", "color_reference_table") 41 | instance.Asterisk = gooq.NewStringField(instance, "*") 42 | instance.Value = gooq.NewStringField(instance, "value") 43 | instance.Constraints = newColorReferenceTableConstraints(instance) 44 | return instance 45 | } 46 | 47 | func (t *colorReferenceTable) As(alias string) *colorReferenceTable { 48 | instance := newColorReferenceTable() 49 | instance.TableImpl = *instance.TableImpl.As(alias) 50 | return instance 51 | } 52 | 53 | func (t *colorReferenceTable) GetColumns() []gooq.Expression { 54 | return []gooq.Expression{ 55 | t.Value, 56 | } 57 | } 58 | 59 | func (t *colorReferenceTable) ScanRow( 60 | db gooq.DBInterface, stmt gooq.Fetchable, 61 | ) (*model.ColorReferenceTable, error) { 62 | result := model.ColorReferenceTable{} 63 | if err := gooq.ScanRow(db, stmt, &result); err != nil { 64 | return nil, err 65 | } 66 | return &result, nil 67 | } 68 | 69 | func (t *colorReferenceTable) ScanRows( 70 | db gooq.DBInterface, stmt gooq.Fetchable, 71 | ) ([]model.ColorReferenceTable, error) { 72 | results := []model.ColorReferenceTable{} 73 | if err := gooq.ScanRows(db, stmt, &results); err != nil { 74 | return nil, err 75 | } 76 | return results, nil 77 | } 78 | 79 | func (t *colorReferenceTable) ScanRowWithContext( 80 | ctx context.Context, db gooq.DBInterface, stmt gooq.Fetchable, 81 | ) (*model.ColorReferenceTable, error) { 82 | result := model.ColorReferenceTable{} 83 | if err := gooq.ScanRowWithContext(ctx, db, stmt, &result); err != nil { 84 | return nil, err 85 | } 86 | return &result, nil 87 | } 88 | 89 | func (t *colorReferenceTable) ScanRowsWithContext( 90 | ctx context.Context, db gooq.DBInterface, stmt gooq.Fetchable, 91 | ) ([]model.ColorReferenceTable, error) { 92 | results := []model.ColorReferenceTable{} 93 | if err := gooq.ScanRowsWithContext(ctx, db, stmt, &results); err != nil { 94 | return nil, err 95 | } 96 | return results, nil 97 | } 98 | 99 | var ColorReferenceTable = newColorReferenceTable() 100 | 101 | type personConstraints struct { 102 | NameBirthyearConstraint gooq.DatabaseConstraint 103 | PersonPkey gooq.DatabaseConstraint 104 | } 105 | 106 | type person struct { 107 | gooq.TableImpl 108 | Asterisk gooq.StringField 109 | ID gooq.UUIDField 110 | Name gooq.StringField 111 | Height gooq.DecimalField 112 | Mass gooq.DecimalField 113 | HairColor gooq.StringField 114 | SkinColor gooq.StringField 115 | EyeColor gooq.StringField 116 | BirthYear gooq.IntField 117 | Gender gooq.StringField 118 | HomeWorld gooq.StringField 119 | SpeciesID gooq.UUIDField 120 | WeaponID gooq.UUIDField 121 | Status gooq.StringField 122 | 123 | Constraints *personConstraints 124 | } 125 | 126 | func newPersonConstraints( 127 | instance *person, 128 | ) *personConstraints { 129 | constraints := &personConstraints{} 130 | constraints.NameBirthyearConstraint = gooq.DatabaseConstraint{ 131 | Name: "name_birthyear_constraint", 132 | Columns: []gooq.Field{ 133 | instance.Name, instance.BirthYear}, 134 | Predicate: null.NewString("((status)::text <> 'dead'::text)", true), 135 | } 136 | constraints.PersonPkey = gooq.DatabaseConstraint{ 137 | Name: "person_pkey", 138 | Columns: []gooq.Field{ 139 | instance.ID}, 140 | Predicate: null.NewString("", false), 141 | } 142 | return constraints 143 | } 144 | 145 | func newPerson() *person { 146 | instance := &person{} 147 | instance.Initialize("public", "person") 148 | instance.Asterisk = gooq.NewStringField(instance, "*") 149 | instance.ID = gooq.NewUUIDField(instance, "id") 150 | instance.Name = gooq.NewStringField(instance, "name") 151 | instance.Height = gooq.NewDecimalField(instance, "height") 152 | instance.Mass = gooq.NewDecimalField(instance, "mass") 153 | instance.HairColor = gooq.NewStringField(instance, "hair_color") 154 | instance.SkinColor = gooq.NewStringField(instance, "skin_color") 155 | instance.EyeColor = gooq.NewStringField(instance, "eye_color") 156 | instance.BirthYear = gooq.NewIntField(instance, "birth_year") 157 | instance.Gender = gooq.NewStringField(instance, "gender") 158 | instance.HomeWorld = gooq.NewStringField(instance, "home_world") 159 | instance.SpeciesID = gooq.NewUUIDField(instance, "species_id") 160 | instance.WeaponID = gooq.NewUUIDField(instance, "weapon_id") 161 | instance.Status = gooq.NewStringField(instance, "status") 162 | instance.Constraints = newPersonConstraints(instance) 163 | return instance 164 | } 165 | 166 | func (t *person) As(alias string) *person { 167 | instance := newPerson() 168 | instance.TableImpl = *instance.TableImpl.As(alias) 169 | return instance 170 | } 171 | 172 | func (t *person) GetColumns() []gooq.Expression { 173 | return []gooq.Expression{ 174 | t.ID, 175 | t.Name, 176 | t.Height, 177 | t.Mass, 178 | t.HairColor, 179 | t.SkinColor, 180 | t.EyeColor, 181 | t.BirthYear, 182 | t.Gender, 183 | t.HomeWorld, 184 | t.SpeciesID, 185 | t.WeaponID, 186 | t.Status, 187 | } 188 | } 189 | 190 | func (t *person) ScanRow( 191 | db gooq.DBInterface, stmt gooq.Fetchable, 192 | ) (*model.Person, error) { 193 | result := model.Person{} 194 | if err := gooq.ScanRow(db, stmt, &result); err != nil { 195 | return nil, err 196 | } 197 | return &result, nil 198 | } 199 | 200 | func (t *person) ScanRows( 201 | db gooq.DBInterface, stmt gooq.Fetchable, 202 | ) ([]model.Person, error) { 203 | results := []model.Person{} 204 | if err := gooq.ScanRows(db, stmt, &results); err != nil { 205 | return nil, err 206 | } 207 | return results, nil 208 | } 209 | 210 | func (t *person) ScanRowWithContext( 211 | ctx context.Context, db gooq.DBInterface, stmt gooq.Fetchable, 212 | ) (*model.Person, error) { 213 | result := model.Person{} 214 | if err := gooq.ScanRowWithContext(ctx, db, stmt, &result); err != nil { 215 | return nil, err 216 | } 217 | return &result, nil 218 | } 219 | 220 | func (t *person) ScanRowsWithContext( 221 | ctx context.Context, db gooq.DBInterface, stmt gooq.Fetchable, 222 | ) ([]model.Person, error) { 223 | results := []model.Person{} 224 | if err := gooq.ScanRowsWithContext(ctx, db, stmt, &results); err != nil { 225 | return nil, err 226 | } 227 | return results, nil 228 | } 229 | 230 | var Person = newPerson() 231 | 232 | type speciesConstraints struct { 233 | SpeciesPkey gooq.DatabaseConstraint 234 | SpeciesUniquenessConstraint gooq.DatabaseConstraint 235 | } 236 | 237 | type species struct { 238 | gooq.TableImpl 239 | Asterisk gooq.StringField 240 | ID gooq.UUIDField 241 | Name gooq.StringField 242 | Classification gooq.StringField 243 | AverageHeight gooq.DecimalField 244 | AverageLifespan gooq.DecimalField 245 | HairColor gooq.StringField 246 | SkinColor gooq.StringField 247 | EyeColor gooq.StringField 248 | HomeWorld gooq.StringField 249 | Language gooq.StringField 250 | 251 | Constraints *speciesConstraints 252 | } 253 | 254 | func newSpeciesConstraints( 255 | instance *species, 256 | ) *speciesConstraints { 257 | constraints := &speciesConstraints{} 258 | constraints.SpeciesPkey = gooq.DatabaseConstraint{ 259 | Name: "species_pkey", 260 | Columns: []gooq.Field{ 261 | instance.ID}, 262 | Predicate: null.NewString("", false), 263 | } 264 | constraints.SpeciesUniquenessConstraint = gooq.DatabaseConstraint{ 265 | Name: "species_uniqueness_constraint", 266 | Columns: []gooq.Field{ 267 | instance.Name, instance.Classification}, 268 | Predicate: null.NewString("", false), 269 | } 270 | return constraints 271 | } 272 | 273 | func newSpecies() *species { 274 | instance := &species{} 275 | instance.Initialize("public", "species") 276 | instance.Asterisk = gooq.NewStringField(instance, "*") 277 | instance.ID = gooq.NewUUIDField(instance, "id") 278 | instance.Name = gooq.NewStringField(instance, "name") 279 | instance.Classification = gooq.NewStringField(instance, "classification") 280 | instance.AverageHeight = gooq.NewDecimalField(instance, "average_height") 281 | instance.AverageLifespan = gooq.NewDecimalField(instance, "average_lifespan") 282 | instance.HairColor = gooq.NewStringField(instance, "hair_color") 283 | instance.SkinColor = gooq.NewStringField(instance, "skin_color") 284 | instance.EyeColor = gooq.NewStringField(instance, "eye_color") 285 | instance.HomeWorld = gooq.NewStringField(instance, "home_world") 286 | instance.Language = gooq.NewStringField(instance, "language") 287 | instance.Constraints = newSpeciesConstraints(instance) 288 | return instance 289 | } 290 | 291 | func (t *species) As(alias string) *species { 292 | instance := newSpecies() 293 | instance.TableImpl = *instance.TableImpl.As(alias) 294 | return instance 295 | } 296 | 297 | func (t *species) GetColumns() []gooq.Expression { 298 | return []gooq.Expression{ 299 | t.ID, 300 | t.Name, 301 | t.Classification, 302 | t.AverageHeight, 303 | t.AverageLifespan, 304 | t.HairColor, 305 | t.SkinColor, 306 | t.EyeColor, 307 | t.HomeWorld, 308 | t.Language, 309 | } 310 | } 311 | 312 | func (t *species) ScanRow( 313 | db gooq.DBInterface, stmt gooq.Fetchable, 314 | ) (*model.Species, error) { 315 | result := model.Species{} 316 | if err := gooq.ScanRow(db, stmt, &result); err != nil { 317 | return nil, err 318 | } 319 | return &result, nil 320 | } 321 | 322 | func (t *species) ScanRows( 323 | db gooq.DBInterface, stmt gooq.Fetchable, 324 | ) ([]model.Species, error) { 325 | results := []model.Species{} 326 | if err := gooq.ScanRows(db, stmt, &results); err != nil { 327 | return nil, err 328 | } 329 | return results, nil 330 | } 331 | 332 | func (t *species) ScanRowWithContext( 333 | ctx context.Context, db gooq.DBInterface, stmt gooq.Fetchable, 334 | ) (*model.Species, error) { 335 | result := model.Species{} 336 | if err := gooq.ScanRowWithContext(ctx, db, stmt, &result); err != nil { 337 | return nil, err 338 | } 339 | return &result, nil 340 | } 341 | 342 | func (t *species) ScanRowsWithContext( 343 | ctx context.Context, db gooq.DBInterface, stmt gooq.Fetchable, 344 | ) ([]model.Species, error) { 345 | results := []model.Species{} 346 | if err := gooq.ScanRowsWithContext(ctx, db, stmt, &results); err != nil { 347 | return nil, err 348 | } 349 | return results, nil 350 | } 351 | 352 | var Species = newSpecies() 353 | 354 | type weaponConstraints struct { 355 | WeaponPkey gooq.DatabaseConstraint 356 | } 357 | 358 | type weapon struct { 359 | gooq.TableImpl 360 | Asterisk gooq.StringField 361 | ID gooq.UUIDField 362 | Damage gooq.IntField 363 | Price gooq.IntField 364 | 365 | Constraints *weaponConstraints 366 | } 367 | 368 | func newWeaponConstraints( 369 | instance *weapon, 370 | ) *weaponConstraints { 371 | constraints := &weaponConstraints{} 372 | constraints.WeaponPkey = gooq.DatabaseConstraint{ 373 | Name: "weapon_pkey", 374 | Columns: []gooq.Field{ 375 | instance.ID}, 376 | Predicate: null.NewString("", false), 377 | } 378 | return constraints 379 | } 380 | 381 | func newWeapon() *weapon { 382 | instance := &weapon{} 383 | instance.Initialize("public", "weapon") 384 | instance.Asterisk = gooq.NewStringField(instance, "*") 385 | instance.ID = gooq.NewUUIDField(instance, "id") 386 | instance.Damage = gooq.NewIntField(instance, "damage") 387 | instance.Price = gooq.NewIntField(instance, "price") 388 | instance.Constraints = newWeaponConstraints(instance) 389 | return instance 390 | } 391 | 392 | func (t *weapon) As(alias string) *weapon { 393 | instance := newWeapon() 394 | instance.TableImpl = *instance.TableImpl.As(alias) 395 | return instance 396 | } 397 | 398 | func (t *weapon) GetColumns() []gooq.Expression { 399 | return []gooq.Expression{ 400 | t.ID, 401 | t.Damage, 402 | t.Price, 403 | } 404 | } 405 | 406 | func (t *weapon) ScanRow( 407 | db gooq.DBInterface, stmt gooq.Fetchable, 408 | ) (*model.Weapon, error) { 409 | result := model.Weapon{} 410 | if err := gooq.ScanRow(db, stmt, &result); err != nil { 411 | return nil, err 412 | } 413 | return &result, nil 414 | } 415 | 416 | func (t *weapon) ScanRows( 417 | db gooq.DBInterface, stmt gooq.Fetchable, 418 | ) ([]model.Weapon, error) { 419 | results := []model.Weapon{} 420 | if err := gooq.ScanRows(db, stmt, &results); err != nil { 421 | return nil, err 422 | } 423 | return results, nil 424 | } 425 | 426 | func (t *weapon) ScanRowWithContext( 427 | ctx context.Context, db gooq.DBInterface, stmt gooq.Fetchable, 428 | ) (*model.Weapon, error) { 429 | result := model.Weapon{} 430 | if err := gooq.ScanRowWithContext(ctx, db, stmt, &result); err != nil { 431 | return nil, err 432 | } 433 | return &result, nil 434 | } 435 | 436 | func (t *weapon) ScanRowsWithContext( 437 | ctx context.Context, db gooq.DBInterface, stmt gooq.Fetchable, 438 | ) ([]model.Weapon, error) { 439 | results := []model.Weapon{} 440 | if err := gooq.ScanRowsWithContext(ctx, db, stmt, &results); err != nil { 441 | return nil, err 442 | } 443 | return results, nil 444 | } 445 | 446 | var Weapon = newWeapon() 447 | -------------------------------------------------------------------------------- /pkg/gooq/function.go: -------------------------------------------------------------------------------- 1 | package gooq 2 | 3 | func Count( 4 | expr ...Expression, 5 | ) NumericExpression { 6 | expression := Asterisk 7 | if len(expr) == 1 { 8 | expression = expr[0] 9 | } else if len(expr) > 1 { 10 | panic("only support 1 arg") 11 | } 12 | return NewNumericExpressionFunction("COUNT", expression) 13 | } 14 | 15 | func Distinct(expr Expression) Expression { 16 | return NewExpressionFunction("DISTINCT", expr) 17 | } 18 | 19 | func Sum( 20 | expr Expression, 21 | ) NumericExpression { 22 | return NewNumericExpressionFunction("SUM", expr) 23 | } 24 | 25 | /////////////////////////////////////////////////////////////////////////////// 26 | // functions for expression 27 | /////////////////////////////////////////////////////////////////////////////// 28 | 29 | type aliasFunction struct { 30 | expressionImpl 31 | expression Expression 32 | alias string 33 | } 34 | 35 | func newAliasFunction( 36 | expression Expression, alias string, 37 | ) Expression { 38 | function := &aliasFunction{expression: expression, alias: alias} 39 | function.expressionImpl.initFunctionExpression(function) 40 | return function 41 | } 42 | 43 | func (expr *aliasFunction) Render( 44 | builder *Builder, 45 | ) { 46 | builder.RenderExpression(expr.expression) 47 | builder.Printf(" AS \"%s\"", expr.alias) 48 | } 49 | 50 | type filterWhereFunction struct { 51 | expressionImpl 52 | expression Expression 53 | } 54 | 55 | func newFilterWhereFunction( 56 | expression Expression, arguments ...Expression, 57 | ) Expression { 58 | function := &filterWhereFunction{expression: expression} 59 | function.expressionImpl.initFunctionExpression(function, arguments...) 60 | return function 61 | } 62 | 63 | func (expr *filterWhereFunction) Render( 64 | builder *Builder, 65 | ) { 66 | builder.RenderExpression(expr.expression) 67 | builder.Print(" FILTER (WHERE ") 68 | builder.RenderConditions(expr.expressions) 69 | builder.Printf(")") 70 | } 71 | 72 | /////////////////////////////////////////////////////////////////////////////// 73 | // Function Expression 74 | /////////////////////////////////////////////////////////////////////////////// 75 | 76 | // TODO(Peter): we should refactor these expressionFunctions 77 | 78 | type expressionFunctionImpl struct { 79 | expressionImpl 80 | name string 81 | } 82 | 83 | func NewExpressionFunction( 84 | name string, arguments ...Expression, 85 | ) Expression { 86 | function := &expressionFunctionImpl{name: name} 87 | function.expressionImpl.initFunctionExpression(function, arguments...) 88 | return function 89 | } 90 | 91 | func (expr *expressionFunctionImpl) Render( 92 | builder *Builder, 93 | ) { 94 | builder.Printf("%s(", expr.name) 95 | for index, argument := range expr.expressions { 96 | argument.Render(builder) 97 | if index != len(expr.expressions)-1 { 98 | builder.Print(", ") 99 | } 100 | } 101 | builder.Printf(")") 102 | } 103 | 104 | type numericExpressionFunctionImpl struct { 105 | numericExpressionImpl 106 | name string 107 | } 108 | 109 | func NewNumericExpressionFunction( 110 | name string, arguments ...Expression, 111 | ) NumericExpression { 112 | function := &numericExpressionFunctionImpl{name: name} 113 | function.expressionImpl.initFunctionExpression(function, arguments...) 114 | return function 115 | } 116 | 117 | func (expr *numericExpressionFunctionImpl) Render( 118 | builder *Builder, 119 | ) { 120 | builder.Printf("%s(", expr.name) 121 | for index, argument := range expr.expressions { 122 | argument.Render(builder) 123 | if index != len(expr.expressions)-1 { 124 | builder.Print(", ") 125 | } 126 | } 127 | builder.Printf(")") 128 | } 129 | 130 | type stringExpressionFunctionImpl struct { 131 | stringExpressionImpl 132 | name string 133 | } 134 | 135 | func NewStringExpressionFunction( 136 | name string, arguments ...Expression, 137 | ) StringExpression { 138 | function := &stringExpressionFunctionImpl{name: name} 139 | function.expressionImpl.initFunctionExpression(function, arguments...) 140 | return function 141 | } 142 | 143 | func (expr *stringExpressionFunctionImpl) Render( 144 | builder *Builder, 145 | ) { 146 | builder.Printf("%s(", expr.name) 147 | for index, argument := range expr.expressions { 148 | argument.Render(builder) 149 | if index != len(expr.expressions)-1 { 150 | builder.Print(", ") 151 | } 152 | } 153 | builder.Printf(")") 154 | } 155 | 156 | type boolExpressionFunctionImpl struct { 157 | boolExpressionImpl 158 | name string 159 | } 160 | 161 | func NewBoolExpressionFunction( 162 | name string, arguments ...Expression, 163 | ) BoolExpression { 164 | function := &boolExpressionFunctionImpl{name: name} 165 | function.expressionImpl.initFunctionExpression(function, arguments...) 166 | return function 167 | } 168 | 169 | func (expr *boolExpressionFunctionImpl) Render( 170 | builder *Builder, 171 | ) { 172 | builder.Printf("%s(", expr.name) 173 | for index, argument := range expr.expressions { 174 | argument.Render(builder) 175 | if index != len(expr.expressions)-1 { 176 | builder.Print(", ") 177 | } 178 | } 179 | builder.Printf(")") 180 | } 181 | 182 | // Multigrade AND, OR expressions 183 | // And(expr1, expr2, expr3) produces (expr1 AND expr2 AND expr3) 184 | // where as expr1.And(expr2).And(expr3) produces ((expr1 AND expr2) AND expr3) 185 | // They are equivalent since AND and OR are associative but in some cases the 186 | // parentheses causes confusions 187 | func And( 188 | boolExpressions ...BoolExpression, 189 | ) BoolExpression { 190 | var expressions []Expression 191 | for _, expr := range boolExpressions { 192 | expressions = append(expressions, expr) 193 | } 194 | return newMultigradeBooleanExpressionImpl(OperatorAnd, expressions, 195 | HasParentheses(true)) 196 | } 197 | 198 | func Or( 199 | boolExpressions ...BoolExpression, 200 | ) BoolExpression { 201 | var expressions []Expression 202 | for _, expr := range boolExpressions { 203 | expressions = append(expressions, expr) 204 | } 205 | return newMultigradeBooleanExpressionImpl(OperatorOr, expressions, 206 | HasParentheses(true)) 207 | } 208 | 209 | /////////////////////////////////////////////////////////////////////////////// 210 | // Table 9.3. Comparison Functions 211 | // https://www.postgresql.org/docs/11/functions-comparison.html 212 | // [Good First Issue][Help Wanted] TODO: implement remaining 213 | /////////////////////////////////////////////////////////////////////////////// 214 | 215 | /////////////////////////////////////////////////////////////////////////////// 216 | // Table 9.5. Mathematical Functions 217 | // Table 9.6. Random Functions 218 | // Table 9.7. Trigonometric Functions 219 | // https://www.postgresql.org/docs/11/functions-math.html 220 | // [Good First Issue][Help Wanted] TODO: implement remaining 221 | /////////////////////////////////////////////////////////////////////////////// 222 | 223 | /////////////////////////////////////////////////////////////////////////////// 224 | // Table 9.8. SQL String Functions and Operators 225 | // Table 9.9. Other String Functions 226 | // https://www.postgresql.org/docs/11/functions-string.html 227 | // [Good First Issue][Help Wanted] TODO: implement remaining functions (not operators) 228 | /////////////////////////////////////////////////////////////////////////////// 229 | 230 | func Ascii( 231 | input StringExpression, 232 | ) NumericExpression { 233 | return NewNumericExpressionFunction("ASCII", input) 234 | } 235 | 236 | func BTrim( 237 | source StringExpression, characters ...StringExpression, 238 | ) StringExpression { 239 | expressions := []Expression{source} 240 | if characters != nil { 241 | expressions = append(expressions, characters[0]) 242 | } 243 | return NewStringExpressionFunction("BTRIM", expressions...) 244 | } 245 | 246 | func LTrim( 247 | source StringExpression, characters ...StringExpression, 248 | ) StringExpression { 249 | expressions := []Expression{source} 250 | if characters != nil { 251 | expressions = append(expressions, characters[0]) 252 | } 253 | return NewStringExpressionFunction("LTRIM", expressions...) 254 | } 255 | 256 | func RTrim( 257 | source StringExpression, characters ...StringExpression, 258 | ) StringExpression { 259 | expressions := []Expression{source} 260 | if characters != nil { 261 | expressions = append(expressions, characters[0]) 262 | } 263 | return NewStringExpressionFunction("RTRIM", expressions...) 264 | } 265 | 266 | func Chr( 267 | asciiCode NumericExpression, 268 | ) StringExpression { 269 | // TODO: add strict checking on asciiCode (i.e. make sure is not 0) 270 | return NewStringExpressionFunction("CHR", asciiCode) 271 | } 272 | 273 | func Concat( 274 | text Expression, moreText ...Expression, 275 | ) StringExpression { 276 | expressions := append([]Expression{text}, moreText...) 277 | return NewStringExpressionFunction("CONCAT", expressions...) 278 | } 279 | 280 | func ConcatWs( 281 | separator StringExpression, 282 | text Expression, moreText ...Expression, 283 | ) StringExpression { 284 | expressions := append([]Expression{separator, text}, moreText...) 285 | return NewStringExpressionFunction("CONCAT_WS", expressions...) 286 | } 287 | 288 | // TODO: Convert, ConvertFrom, ConvertTo, Decode, Encode 289 | 290 | func Format( 291 | formatStr StringExpression, formatArg ...Expression, 292 | ) StringExpression { 293 | // TODO: enforce checking on number of formatArgs 294 | // (i.e. make sure is the same as the number of elements to be replaced in formatStr) 295 | expressions := append([]Expression{formatStr}, formatArg...) 296 | return NewStringExpressionFunction("FORMAT", expressions...) 297 | } 298 | 299 | func InitCap( 300 | text StringExpression, 301 | ) StringExpression { 302 | return NewStringExpressionFunction("INITCAP", text) 303 | } 304 | 305 | func Left( 306 | text StringExpression, n NumericExpression, 307 | ) StringExpression { 308 | return NewStringExpressionFunction("LEFT", text, n) 309 | } 310 | 311 | func Right( 312 | text StringExpression, n NumericExpression, 313 | ) StringExpression { 314 | return NewStringExpressionFunction("RIGHT", text, n) 315 | } 316 | 317 | func Length( 318 | text StringExpression, encoding ...StringExpression, 319 | ) NumericExpression { 320 | arguments := []Expression{text} 321 | if encoding != nil { 322 | arguments = append(arguments, encoding[0]) 323 | } 324 | return NewNumericExpressionFunction("LENGTH", arguments...) 325 | } 326 | 327 | func LPad( 328 | text StringExpression, len NumericExpression, 329 | fill ...StringExpression, 330 | ) StringExpression { 331 | arguments := []Expression{text, len} 332 | if fill != nil { 333 | arguments = append(arguments, fill[0]) 334 | } 335 | return NewStringExpressionFunction("LPAD", arguments...) 336 | } 337 | 338 | func RPad( 339 | text StringExpression, len NumericExpression, 340 | fill ...StringExpression, 341 | ) StringExpression { 342 | arguments := []Expression{text, len} 343 | if fill != nil { 344 | arguments = append(arguments, fill[0]) 345 | } 346 | return NewStringExpressionFunction("RPAD", arguments...) 347 | } 348 | 349 | func Md5( 350 | text StringExpression, 351 | ) StringExpression { 352 | return NewStringExpressionFunction("MD5", text) 353 | } 354 | 355 | // TODO: ParseIdent 356 | 357 | func PgClientEncoding() StringExpression { 358 | return NewStringExpressionFunction("PG_CLIENT_ENCODING") 359 | } 360 | 361 | func QuoteIdent( 362 | text StringExpression, 363 | ) StringExpression { 364 | return NewStringExpressionFunction("QUOTE_IDENT", text) 365 | } 366 | 367 | func QuoteLiteral( 368 | value Expression, 369 | ) StringExpression { 370 | return NewStringExpressionFunction("QUOTE_LITERAL", value) 371 | } 372 | 373 | func QuoteNullable( 374 | value Expression, 375 | ) StringExpression { 376 | return NewStringExpressionFunction("QUOTE_NULLABLE", value) 377 | } 378 | 379 | // TODO: RegexpMatch, RegexpMatches, RegexpReplace, 380 | // RegexpSplitToArray, RegexpSplitToTable 381 | 382 | func Repeat( 383 | text StringExpression, n NumericExpression, 384 | ) StringExpression { 385 | return NewStringExpressionFunction("REPEAT", text, n) 386 | } 387 | 388 | func Replace( 389 | text StringExpression, from StringExpression, to StringExpression, 390 | ) StringExpression { 391 | return NewStringExpressionFunction("REPLACE", text, from, to) 392 | } 393 | 394 | func Reverse( 395 | text StringExpression, 396 | ) StringExpression { 397 | return NewStringExpressionFunction("REVERSE", text) 398 | } 399 | 400 | func SplitPart( 401 | text StringExpression, delimiter StringExpression, 402 | field NumericExpression, 403 | ) StringExpression { 404 | return NewStringExpressionFunction("SPLIT_PART", text, delimiter, field) 405 | } 406 | 407 | func Strpos( 408 | text StringExpression, substring StringExpression, 409 | ) NumericExpression { 410 | return NewNumericExpressionFunction("STRPOS", text, substring) 411 | } 412 | 413 | func Substr( 414 | text StringExpression, from NumericExpression, 415 | count ...NumericExpression, 416 | ) StringExpression { 417 | arguments := []Expression{text, from} 418 | if count != nil { 419 | arguments = append(arguments, count[0]) 420 | } 421 | return NewStringExpressionFunction("SUBSTR", arguments...) 422 | } 423 | 424 | func StartsWith( 425 | text StringExpression, prefix StringExpression, 426 | ) BoolExpression { 427 | return NewBoolExpressionFunction("STARTS_WITH", text, prefix) 428 | } 429 | 430 | func ToAscii( 431 | text StringExpression, encoding ...StringExpression, 432 | ) StringExpression { 433 | arguments := []Expression{text} 434 | if encoding != nil { 435 | arguments = append(arguments, encoding[0]) 436 | } 437 | // TODO: enforce encoding to be one of {LATIN1, LATIN2, LATIN9, WIN1250} 438 | return NewStringExpressionFunction("TO_ASCII", arguments...) 439 | } 440 | 441 | func ToHex( 442 | number NumericExpression, 443 | ) StringExpression { 444 | // TODO: enforce integer requirement on number 445 | // (either int or bigint, but not decimal) 446 | return NewStringExpressionFunction("TO_HEX", number) 447 | } 448 | 449 | func Translate( 450 | text StringExpression, from StringExpression, to StringExpression, 451 | ) StringExpression { 452 | return NewStringExpressionFunction("TRANSLATE", text, from, to) 453 | } 454 | 455 | /////////////////////////////////////////////////////////////////////////////// 456 | // Table 9.11. SQL Binary String Functions and Operators 457 | // Table 9.12. Other Binary String Functions 458 | // https://www.postgresql.org/docs/11/functions-binarystring.html 459 | // [Good First Issue][Help Wanted] TODO: implement remaining functions (not operators) 460 | /////////////////////////////////////////////////////////////////////////////// 461 | 462 | /////////////////////////////////////////////////////////////////////////////// 463 | // Table 9.23. Formatting Functions 464 | // https://www.postgresql.org/docs/11/functions-formatting.html 465 | // [Good First Issue][Help Wanted] TODO: implement remaining functions 466 | //////////////////////////////////////////////////////////////////////////////// 467 | 468 | /////////////////////////////////////////////////////////////////////////////// 469 | // Table 9.30. Date/Time Functions 470 | // https://www.postgresql.org/docs/11/functions-datetime.html 471 | // [Good First Issue][Help Wanted] TODO: implement remaining functions 472 | //////////////////////////////////////////////////////////////////////////////// 473 | 474 | func DateTrunc( 475 | text string, timestamp DateTimeExpression, 476 | ) Expression { 477 | expressions := []Expression{String(text), timestamp} 478 | return NewExpressionFunction("DATE_TRUNC", expressions...) 479 | } 480 | 481 | func Greatest( 482 | expr Expression, rests ...Expression, 483 | ) Expression { 484 | expressions := append([]Expression{expr}, rests...) 485 | return NewExpressionFunction("GREATEST", expressions...) 486 | } 487 | 488 | func Least( 489 | expr Expression, rests ...Expression, 490 | ) Expression { 491 | expressions := append([]Expression{expr}, rests...) 492 | return NewExpressionFunction("LEAST", expressions...) 493 | } 494 | 495 | // TODO(Peter): implement Case When 496 | 497 | func Coalesce( 498 | expr Expression, rests ...Expression, 499 | ) Expression { 500 | expressions := append([]Expression{expr}, rests...) 501 | return NewExpressionFunction("COALESCE", expressions...) 502 | } 503 | 504 | func NullIf( 505 | value1, value2 Expression, 506 | ) Expression { 507 | expressions := []Expression{value1, value2} 508 | return NewExpressionFunction("NULLIF", expressions...) 509 | } 510 | 511 | /////////////////////////////////////////////////////////////////////////////// 512 | // Array Functions and Operators 513 | // https://www.postgresql.org/docs/11/functions-array.html 514 | // [Help Wanted] TODO: implement remaining functions 515 | /////////////////////////////////////////////////////////////////////////////// 516 | 517 | /////////////////////////////////////////////////////////////////////////////// 518 | // Range Functions and Operators 519 | // https://www.postgresql.org/docs/11/functions-range.html 520 | // [Help Wanted] TODO: implement remaining functions 521 | /////////////////////////////////////////////////////////////////////////////// 522 | 523 | /////////////////////////////////////////////////////////////////////////////// 524 | // Aggregate Functions 525 | // https://www.postgresql.org/docs/11/functions-aggregate.html 526 | // [Help Wanted] TODO: implement remaining functions 527 | /////////////////////////////////////////////////////////////////////////////// 528 | 529 | /////////////////////////////////////////////////////////////////////////////// 530 | // Subquery Expressions 531 | // https://www.postgresql.org/docs/11/functions-subquery.html 532 | // [Help Wanted] TODO: implement remaining functions 533 | /////////////////////////////////////////////////////////////////////////////// 534 | 535 | /////////////////////////////////////////////////////////////////////////////// 536 | // Window Functions 537 | // https://www.postgresql.org/docs/11/functions-window.html 538 | // [Help Wanted] TODO: implement remaining functions 539 | /////////////////////////////////////////////////////////////////////////////// 540 | 541 | /////////////////////////////////////////////////////////////////////////////// 542 | // Set Returning Functions 543 | // https://www.postgresql.org/docs/11/functions-srf.html 544 | // [Help Wanted] TODO: implement remaining functions 545 | /////////////////////////////////////////////////////////////////////////////// 546 | 547 | /////////////////////////////////////////////////////////////////////////////// 548 | // Name Conversion - Functions 549 | // https://www.postgresql.org/docs/11/typeconv-func.html 550 | // [Help Wanted] TODO: implement remaining functions 551 | /////////////////////////////////////////////////////////////////////////////// 552 | 553 | /////////////////////////////////////////////////////////////////////////////// 554 | // Advisory Lock Functions 555 | // https://www.postgresql.org/docs/11/functions-admin.html 556 | // [Help Wanted] TODO: implement remaining functions 557 | /////////////////////////////////////////////////////////////////////////////// 558 | 559 | func TryAdvisoryLock( 560 | number NumericExpression, 561 | ) BoolExpression { 562 | return NewBoolExpressionFunction("pg_try_advisory_lock", number) 563 | } 564 | 565 | func ReleaseAdvisoryLock( 566 | number NumericExpression, 567 | ) BoolExpression { 568 | return NewBoolExpressionFunction("pg_advisory_unlock", number) 569 | } 570 | --------------------------------------------------------------------------------