From 6eaa8d1b9698a66097db63c072e0bf099155b0b4 Mon Sep 17 00:00:00 2001 From: Yunchi Luo Date: Sat, 25 Jan 2020 22:04:50 -0500 Subject: [PATCH 01/20] bootstrapping kotlin --- internal/cmd/generate.go | 13 +- internal/dinosql/config.go | 11 + internal/dinosql/ktgen.go | 1250 ++++++++++++++++++++++++++++++++++++ 3 files changed, 1273 insertions(+), 1 deletion(-) create mode 100644 internal/dinosql/ktgen.go diff --git a/internal/cmd/generate.go b/internal/cmd/generate.go index da4cfa8c4b..2ff3101cf0 100644 --- a/internal/cmd/generate.go +++ b/internal/cmd/generate.go @@ -110,7 +110,18 @@ func Generate(dir string, stderr io.Writer) (map[string]string, error) { } - files, err := dinosql.Generate(result, combo) + var files map[string]string + switch pkg.Language { + case dinosql.LanguageGo: + files, err = dinosql.Generate(result, combo) + case dinosql.LanguageKotlin: + ktRes, ok := result.(dinosql.KtGenerateable) + if !ok { + err = fmt.Errorf("Kotlin not supported") + break + } + files, err = dinosql.KtGenerate(ktRes, combo) + } if err != nil { fmt.Fprintf(stderr, "# package %s\n", name) fmt.Fprintf(stderr, "error generating code: %s\n", err) diff --git a/internal/dinosql/config.go b/internal/dinosql/config.go index e14dc9a2f0..b51cd0a6b1 100644 --- a/internal/dinosql/config.go +++ b/internal/dinosql/config.go @@ -41,9 +41,17 @@ const ( EnginePostgreSQL Engine = "postgresql" ) +type Language string + +const ( + LanguageGo Language = "go" + LanguageKotlin Language = "kotlin" +) + type PackageSettings struct { Name string `json:"name"` Engine Engine `json:"engine,omitempty"` + Language Language `json:"language,omitempty"` Path string `json:"path"` Schema string `json:"schema"` Queries string `json:"queries"` @@ -196,6 +204,9 @@ func ParseConfig(rd io.Reader) (GenerateSettings, error) { if config.Packages[j].Engine == "" { config.Packages[j].Engine = EnginePostgreSQL } + if config.Packages[j].Language == "" { + config.Packages[j].Language = LanguageGo + } } return config, nil } diff --git a/internal/dinosql/ktgen.go b/internal/dinosql/ktgen.go new file mode 100644 index 0000000000..a821dbb5be --- /dev/null +++ b/internal/dinosql/ktgen.go @@ -0,0 +1,1250 @@ +package dinosql + +import ( + "bufio" + "bytes" + "fmt" + "go/format" + "log" + "regexp" + "sort" + "strings" + "text/template" + "unicode" + + core "github.com/kyleconroy/sqlc/internal/pg" + + "github.com/jinzhu/inflection" +) + +var ktIdentPattern = regexp.MustCompile("[^a-zA-Z0-9_]+") + +type KtConstant struct { + Name string + Type string + Value string +} + +type KtEnum struct { + Name string + Comment string + Constants []KtConstant +} + +type KtField struct { + Name string + Type string + Tags map[string]string + Comment string +} + +func (gf KtField) Tag() string { + tags := make([]string, 0, len(gf.Tags)) + for key, val := range gf.Tags { + tags = append(tags, fmt.Sprintf("%s\"%s\"", key, val)) + } + if len(tags) == 0 { + return "" + } + sort.Strings(tags) + return strings.Join(tags, ",") +} + +type KtStruct struct { + Table core.FQN + Name string + Fields []KtField + Comment string +} + +type KtQueryValue struct { + Emit bool + Name string + Struct *KtStruct + Typ string +} + +func (v KtQueryValue) EmitStruct() bool { + return v.Emit +} + +func (v KtQueryValue) IsStruct() bool { + return v.Struct != nil +} + +func (v KtQueryValue) isEmpty() bool { + return v.Typ == "" && v.Name == "" && v.Struct == nil +} + +func (v KtQueryValue) Pair() string { + if v.isEmpty() { + return "" + } + return v.Name + " " + v.Type() +} + +func (v KtQueryValue) Type() string { + if v.Typ != "" { + return v.Typ + } + if v.Struct != nil { + return v.Struct.Name + } + panic("no type for KtQueryValue: " + v.Name) +} + +func (v KtQueryValue) Params() string { + if v.isEmpty() { + return "" + } + var out []string + if v.Struct == nil { + if strings.HasPrefix(v.Typ, "[]") && v.Typ != "[]byte" { + out = append(out, "pq.Array("+v.Name+")") + } else { + out = append(out, v.Name) + } + } else { + for _, f := range v.Struct.Fields { + if strings.HasPrefix(f.Type, "[]") && f.Type != "[]byte" { + out = append(out, "pq.Array("+v.Name+"."+f.Name+")") + } else { + out = append(out, v.Name+"."+f.Name) + } + } + } + if len(out) <= 3 { + return strings.Join(out, ",") + } + out = append(out, "") + return "\n" + strings.Join(out, ",\n") +} + +func (v KtQueryValue) Scan() string { + var out []string + if v.Struct == nil { + if strings.HasPrefix(v.Typ, "[]") && v.Typ != "[]byte" { + out = append(out, "pq.Array(&"+v.Name+")") + } else { + out = append(out, "&"+v.Name) + } + } else { + for _, f := range v.Struct.Fields { + if strings.HasPrefix(f.Type, "[]") && f.Type != "[]byte" { + out = append(out, "pq.Array(&"+v.Name+"."+f.Name+")") + } else { + out = append(out, "&"+v.Name+"."+f.Name) + } + } + } + if len(out) <= 3 { + return strings.Join(out, ",") + } + out = append(out, "") + return "\n" + strings.Join(out, ",\n") +} + +// A struct used to generate methods and fields on the Queries struct +type KtQuery struct { + Cmd string + Comments []string + MethodName string + FieldName string + ConstantName string + SQL string + SourceName string + Ret KtQueryValue + Arg KtQueryValue +} + +type KtGenerateable interface { + Structs(settings CombinedSettings) []KtStruct + KtQueries(settings CombinedSettings) []KtQuery + Enums(settings CombinedSettings) []KtEnum +} + +func KtUsesType(r KtGenerateable, typ string, settings CombinedSettings) bool { + for _, strct := range r.Structs(settings) { + for _, f := range strct.Fields { + fType := strings.TrimPrefix(f.Type, "[]") + if strings.HasPrefix(fType, typ) { + return true + } + } + } + return false +} + +func KtUsesArrays(r KtGenerateable, settings CombinedSettings) bool { + for _, strct := range r.Structs(settings) { + for _, f := range strct.Fields { + if strings.HasPrefix(f.Type, "[]") { + return true + } + } + } + return false +} + +func KtImports(r KtGenerateable, settings CombinedSettings) func(string) [][]string { + return func(filename string) [][]string { + if filename == "db.go" { + imps := []string{"context", "database/sql"} + if settings.Package.EmitPreparedQueries { + imps = append(imps, "fmt") + } + return [][]string{imps} + } + + if filename == "models.go" { + return ModelKtImports(r, settings) + } + + if filename == "querier.go" { + return InterfaceKtImports(r, settings) + } + + return QueryKtImports(r, settings, filename) + } +} + +func InterfaceKtImports(r KtGenerateable, settings CombinedSettings) [][]string { + gq := r.KtQueries(settings) + uses := func(name string) bool { + for _, q := range gq { + if !q.Ret.isEmpty() { + if strings.HasPrefix(q.Ret.Type(), name) { + return true + } + } + if !q.Arg.isEmpty() { + if strings.HasPrefix(q.Arg.Type(), name) { + return true + } + } + } + return false + } + + std := map[string]struct{}{ + "context": struct{}{}, + } + if uses("sql.Null") { + std["database/sql"] = struct{}{} + } + if uses("json.RawMessage") { + std["encoding/json"] = struct{}{} + } + if uses("time.Time") { + std["time"] = struct{}{} + } + if uses("net.IP") { + std["net"] = struct{}{} + } + + pkg := make(map[string]struct{}) + overrideTypes := map[string]string{} + for _, o := range settings.Overrides { + if o.ktBasicType { + continue + } + overrideTypes[o.ktTypeName] = o.ktPackage + } + + _, overrideNullTime := overrideTypes["pq.NullTime"] + if uses("pq.NullTime") && !overrideNullTime { + pkg["github.com/lib/pq"] = struct{}{} + } + _, overrideUUID := overrideTypes["uuid.UUID"] + if uses("uuid.UUID") && !overrideUUID { + pkg["github.com/ktogle/uuid"] = struct{}{} + } + + // Custom imports + for ktType, importPath := range overrideTypes { + if _, ok := std[importPath]; !ok && uses(ktType) { + pkg[importPath] = struct{}{} + } + } + + pkgs := make([]string, 0, len(pkg)) + for p, _ := range pkg { + pkgs = append(pkgs, p) + } + + stds := make([]string, 0, len(std)) + for s, _ := range std { + stds = append(stds, s) + } + + sort.Strings(stds) + sort.Strings(pkgs) + return [][]string{stds, pkgs} +} + +func ModelKtImports(r KtGenerateable, settings CombinedSettings) [][]string { + std := make(map[string]struct{}) + if KtUsesType(r, "sql.Null", settings) { + std["database/sql"] = struct{}{} + } + if KtUsesType(r, "json.RawMessage", settings) { + std["encoding/json"] = struct{}{} + } + if KtUsesType(r, "time.Time", settings) { + std["time"] = struct{}{} + } + if KtUsesType(r, "net.IP", settings) { + std["net"] = struct{}{} + } + + // Custom imports + pkg := make(map[string]struct{}) + overrideTypes := map[string]string{} + for _, o := range settings.Overrides { + if o.ktBasicType { + continue + } + overrideTypes[o.ktTypeName] = o.ktPackage + } + + _, overrideNullTime := overrideTypes["pq.NullTime"] + if KtUsesType(r, "pq.NullTime", settings) && !overrideNullTime { + pkg["github.com/lib/pq"] = struct{}{} + } + + _, overrideUUID := overrideTypes["uuid.UUID"] + if KtUsesType(r, "uuid.UUID", settings) && !overrideUUID { + pkg["github.com/ktogle/uuid"] = struct{}{} + } + + for ktType, importPath := range overrideTypes { + if _, ok := std[importPath]; !ok && KtUsesType(r, ktType, settings) { + pkg[importPath] = struct{}{} + } + } + + pkgs := make([]string, 0, len(pkg)) + for p, _ := range pkg { + pkgs = append(pkgs, p) + } + + stds := make([]string, 0, len(std)) + for s, _ := range std { + stds = append(stds, s) + } + + sort.Strings(stds) + sort.Strings(pkgs) + return [][]string{stds, pkgs} +} + +func QueryKtImports(r KtGenerateable, settings CombinedSettings, filename string) [][]string { + // for _, strct := range r.Structs() { + // for _, f := range strct.Fields { + // if strings.HasPrefix(f.Type, "[]") { + // return true + // } + // } + // } + var gq []KtQuery + for _, query := range r.KtQueries(settings) { + if query.SourceName == filename { + gq = append(gq, query) + } + } + + uses := func(name string) bool { + for _, q := range gq { + if !q.Ret.isEmpty() { + if q.Ret.EmitStruct() { + for _, f := range q.Ret.Struct.Fields { + fType := strings.TrimPrefix(f.Type, "[]") + if strings.HasPrefix(fType, name) { + return true + } + } + } + if strings.HasPrefix(q.Ret.Type(), name) { + return true + } + } + if !q.Arg.isEmpty() { + if q.Arg.EmitStruct() { + for _, f := range q.Arg.Struct.Fields { + fType := strings.TrimPrefix(f.Type, "[]") + if strings.HasPrefix(fType, name) { + return true + } + } + } + if strings.HasPrefix(q.Arg.Type(), name) { + return true + } + } + } + return false + } + + sliceScan := func() bool { + for _, q := range gq { + if !q.Ret.isEmpty() { + if q.Ret.IsStruct() { + for _, f := range q.Ret.Struct.Fields { + if strings.HasPrefix(f.Type, "[]") && f.Type != "[]byte" { + return true + } + } + } else { + if strings.HasPrefix(q.Ret.Type(), "[]") && q.Ret.Type() != "[]byte" { + return true + } + } + } + if !q.Arg.isEmpty() { + if q.Arg.IsStruct() { + for _, f := range q.Arg.Struct.Fields { + if strings.HasPrefix(f.Type, "[]") && f.Type != "[]byte" { + return true + } + } + } else { + if strings.HasPrefix(q.Arg.Type(), "[]") && q.Arg.Type() != "[]byte" { + return true + } + } + } + } + return false + } + + std := map[string]struct{}{ + "context": struct{}{}, + } + if uses("sql.Null") { + std["database/sql"] = struct{}{} + } + if uses("json.RawMessage") { + std["encoding/json"] = struct{}{} + } + if uses("time.Time") { + std["time"] = struct{}{} + } + if uses("net.IP") { + std["net"] = struct{}{} + } + + pkg := make(map[string]struct{}) + overrideTypes := map[string]string{} + for _, o := range settings.Overrides { + if o.ktBasicType { + continue + } + overrideTypes[o.ktTypeName] = o.ktPackage + } + + if sliceScan() { + pkg["github.com/lib/pq"] = struct{}{} + } + _, overrideNullTime := overrideTypes["pq.NullTime"] + if uses("pq.NullTime") && !overrideNullTime { + pkg["github.com/lib/pq"] = struct{}{} + } + _, overrideUUID := overrideTypes["uuid.UUID"] + if uses("uuid.UUID") && !overrideUUID { + pkg["github.com/ktogle/uuid"] = struct{}{} + } + + // Custom imports + for ktType, importPath := range overrideTypes { + if _, ok := std[importPath]; !ok && uses(ktType) { + pkg[importPath] = struct{}{} + } + } + + pkgs := make([]string, 0, len(pkg)) + for p, _ := range pkg { + pkgs = append(pkgs, p) + } + + stds := make([]string, 0, len(std)) + for s, _ := range std { + stds = append(stds, s) + } + + sort.Strings(stds) + sort.Strings(pkgs) + return [][]string{stds, pkgs} +} + +func ktEnumValueName(value string) string { + name := "" + id := strings.Replace(value, "-", "_", -1) + id = strings.Replace(id, ":", "_", -1) + id = strings.Replace(id, "/", "_", -1) + id = ktIdentPattern.ReplaceAllString(id, "") + for _, part := range strings.Split(id, "_") { + name += strings.Title(part) + } + return name +} + +func (r Result) KtEnums(settings CombinedSettings) []KtEnum { + var enums []KtEnum + for name, schema := range r.Catalog.Schemas { + if name == "pg_catalog" { + continue + } + for _, enum := range schema.Enums { + var enumName string + if name == "public" { + enumName = enum.Name + } else { + enumName = name + "_" + enum.Name + } + e := KtEnum{ + Name: KtStructName(enumName, settings), + Comment: enum.Comment, + } + for _, v := range enum.Vals { + e.Constants = append(e.Constants, KtConstant{ + Name: e.Name + ktEnumValueName(v), + Value: v, + Type: e.Name, + }) + } + enums = append(enums, e) + } + } + if len(enums) > 0 { + sort.Slice(enums, func(i, j int) bool { return enums[i].Name < enums[j].Name }) + } + return enums +} + +func KtStructName(name string, settings CombinedSettings) string { + if rename := settings.Global.Rename[name]; rename != "" { + return rename + } + out := "" + for _, p := range strings.Split(name, "_") { + if p == "id" { + out += "ID" + } else { + out += strings.Title(p) + } + } + return out +} + +func (r Result) Structs(settings CombinedSettings) []KtStruct { + var structs []KtStruct + for name, schema := range r.Catalog.Schemas { + if name == "pg_catalog" { + continue + } + for _, table := range schema.Tables { + var tableName string + if name == "public" { + tableName = table.Name + } else { + tableName = name + "_" + table.Name + } + s := KtStruct{ + Table: core.FQN{Schema: name, Rel: table.Name}, + Name: inflection.Singular(KtStructName(tableName, settings)), + Comment: table.Comment, + } + for _, column := range table.Columns { + s.Fields = append(s.Fields, KtField{ + Name: KtStructName(column.Name, settings), + Type: r.ktType(column, settings), + Tags: map[string]string{"json:": column.Name}, + Comment: column.Comment, + }) + } + structs = append(structs, s) + } + } + if len(structs) > 0 { + sort.Slice(structs, func(i, j int) bool { return structs[i].Name < structs[j].Name }) + } + return structs +} + +func (r Result) ktType(col core.Column, settings CombinedSettings) string { + // package overrides have a higher precedence + for _, oride := range settings.Overrides { + if oride.Column != "" && oride.columnName == col.Name && oride.table == col.Table { + return oride.ktTypeName + } + } + typ := r.ktInnerType(col, settings) + if col.IsArray { + return "[]" + typ + } + return typ +} + +func (r Result) ktInnerType(col core.Column, settings CombinedSettings) string { + columnType := col.DataType + notNull := col.NotNull || col.IsArray + + // package overrides have a higher precedence + for _, oride := range settings.Overrides { + if oride.PostgresType != "" && oride.PostgresType == columnType && oride.Null != notNull { + return oride.ktTypeName + } + } + + switch columnType { + case "serial", "pg_catalog.serial4": + if notNull { + return "int32" + } + return "sql.NullInt32" + + case "bigserial", "pg_catalog.serial8": + if notNull { + return "int64" + } + return "sql.NullInt64" + + case "smallserial", "pg_catalog.serial2": + return "int16" + + case "integer", "int", "int4", "pg_catalog.int4": + if notNull { + return "int32" + } + return "sql.NullInt32" + + case "bigint", "pg_catalog.int8": + if notNull { + return "int64" + } + return "sql.NullInt64" + + case "smallint", "pg_catalog.int2": + return "int16" + + case "float", "double precision", "pg_catalog.float8": + if notNull { + return "float64" + } + return "sql.NullFloat64" + + case "real", "pg_catalog.float4": + if notNull { + return "float32" + } + return "sql.NullFloat64" // TODO: Change to sql.NullFloat32 after updating the go.mod file + + case "pg_catalog.numeric": + // Since the Go standard library does not have a decimal type, lib/pq + // returns numerics as strings. + // + // https://github.com/lib/pq/issues/648 + if notNull { + return "string" + } + return "sql.NullString" + + case "bool", "pg_catalog.bool": + if notNull { + return "bool" + } + return "sql.NullBool" + + case "jsonb": + return "json.RawMessage" + + case "bytea", "blob", "pg_catalog.bytea": + return "[]byte" + + case "date": + if notNull { + return "time.Time" + } + return "sql.NullTime" + + case "pg_catalog.time", "pg_catalog.timetz": + if notNull { + return "time.Time" + } + return "sql.NullTime" + + case "pg_catalog.timestamp", "pg_catalog.timestamptz", "timestamptz": + if notNull { + return "time.Time" + } + return "sql.NullTime" + + case "text", "pg_catalog.varchar", "pg_catalog.bpchar", "string": + if notNull { + return "string" + } + return "sql.NullString" + + case "uuid": + return "uuid.UUID" + + case "inet": + return "net.IP" + + case "void": + // A void value always returns NULL. Since there is no built-in NULL + // value into the SQL package, we'll use sql.NullBool + return "sql.NullBool" + + case "any": + return "interface{}" + + default: + for name, schema := range r.Catalog.Schemas { + if name == "pg_catalog" { + continue + } + for _, enum := range schema.Enums { + if columnType == enum.Name { + if name == "public" { + return KtStructName(enum.Name, settings) + } + + return KtStructName(name+"_"+enum.Name, settings) + } + } + } + log.Printf("unknown PostgreSQL type: %s\n", columnType) + return "interface{}" + } +} + +// It's possible that this method will generate duplicate JSON tag values +// +// Columns: count, count, count_2 +// Fields: Count, Count_2, Count2 +// JSON tags: count, count_2, count_2 +// +// This is unlikely to happen, so don't fix it yet +func (r Result) columnsToStruct(name string, columns []core.Column, settings CombinedSettings) *KtStruct { + gs := KtStruct{ + Name: name, + } + seen := map[string]int{} + for i, c := range columns { + tagName := c.Name + fieldName := KtStructName(columnName(c, i), settings) + if v := seen[c.Name]; v > 0 { + tagName = fmt.Sprintf("%s_%d", tagName, v+1) + fieldName = fmt.Sprintf("%s_%d", fieldName, v+1) + } + gs.Fields = append(gs.Fields, KtField{ + Name: fieldName, + Type: r.ktType(c, settings), + Tags: map[string]string{"json:": tagName}, + }) + seen[c.Name]++ + } + return &gs +} + +func argName(name string) string { + out := "" + for i, p := range strings.Split(name, "_") { + if i == 0 { + out += strings.ToLower(p) + } else if p == "id" { + out += "ID" + } else { + out += strings.Title(p) + } + } + return out +} + +func paramName(p Parameter) string { + if p.Column.Name != "" { + return argName(p.Column.Name) + } + return fmt.Sprintf("dollar_%d", p.Number) +} + +func columnName(c core.Column, pos int) string { + if c.Name != "" { + return c.Name + } + return fmt.Sprintf("column_%d", pos+1) +} + +func compareFQN(a *core.FQN, b *core.FQN) bool { + if a == nil && b == nil { + return true + } + if a == nil || b == nil { + return false + } + return a.Catalog == b.Catalog && a.Schema == b.Schema && a.Rel == b.Rel +} + +func (r Result) KtQueries(settings CombinedSettings) []KtQuery { + structs := r.Structs(settings) + + qs := make([]KtQuery, 0, len(r.Queries)) + for _, query := range r.Queries { + if query.Name == "" { + continue + } + if query.Cmd == "" { + continue + } + + gq := KtQuery{ + Cmd: query.Cmd, + ConstantName: LowerTitle(query.Name), + FieldName: LowerTitle(query.Name) + "Stmt", + MethodName: query.Name, + SourceName: query.Filename, + SQL: query.SQL, + Comments: query.Comments, + } + + if len(query.Params) == 1 { + p := query.Params[0] + gq.Arg = KtQueryValue{ + Name: paramName(p), + Typ: r.ktType(p.Column, settings), + } + } else if len(query.Params) > 1 { + var cols []core.Column + for _, p := range query.Params { + cols = append(cols, p.Column) + } + gq.Arg = KtQueryValue{ + Emit: true, + Name: "arg", + Struct: r.columnsToStruct(gq.MethodName+"Params", cols, settings), + } + } + + if len(query.Columns) == 1 { + c := query.Columns[0] + gq.Ret = KtQueryValue{ + Name: columnName(c, 0), + Typ: r.ktType(c, settings), + } + } else if len(query.Columns) > 1 { + var gs *KtStruct + var emit bool + + for _, s := range structs { + if len(s.Fields) != len(query.Columns) { + continue + } + same := true + for i, f := range s.Fields { + c := query.Columns[i] + sameName := f.Name == KtStructName(columnName(c, i), settings) + sameType := f.Type == r.ktType(c, settings) + sameTable := s.Table.Catalog == c.Table.Catalog && s.Table.Schema == c.Table.Schema && s.Table.Rel == c.Table.Rel + + if !sameName || !sameType || !sameTable { + same = false + } + } + if same { + gs = &s + break + } + } + + if gs == nil { + gs = r.columnsToStruct(gq.MethodName+"Row", query.Columns, settings) + emit = true + } + gq.Ret = KtQueryValue{ + Emit: emit, + Name: "i", + Struct: gs, + } + } + + qs = append(qs, gq) + } + sort.Slice(qs, func(i, j int) bool { return qs[i].MethodName < qs[j].MethodName }) + return qs +} + +var dbTmpl = `// Code generated by sqlc. DO NOT EDIT. + +package {{.Package}} + +import ( + {{range imports .SourceName}} + {{range .}}"{{.}}" + {{end}} + {{end}} +) + +type DBTX interface { + ExecContext(context.Context, string, ...interface{}) (sql.Result, error) + PrepareContext(context.Context, string) (*sql.Stmt, error) + QueryContext(context.Context, string, ...interface{}) (*sql.Rows, error) + QueryRowContext(context.Context, string, ...interface{}) *sql.Row +} + +func New(db DBTX) *Queries { + return &Queries{db: db} +} + +{{if .EmitPreparedQueries}} +func Prepare(ctx context.Context, db DBTX) (*Queries, error) { + q := Queries{db: db} + var err error + {{- if eq (len .KtQueries) 0 }} + _ = err + {{- end }} + {{- range .KtQueries }} + if q.{{.FieldName}}, err = db.PrepareContext(ctx, {{.ConstantName}}); err != nil { + return nil, fmt.Errorf("error preparing query {{.MethodName}}: %w", err) + } + {{- end}} + return &q, nil +} + +func (q *Queries) Close() error { + var err error + {{- range .KtQueries }} + if q.{{.FieldName}} != nil { + if cerr := q.{{.FieldName}}.Close(); cerr != nil { + err = fmt.Errorf("error closing {{.FieldName}}: %w", cerr) + } + } + {{- end}} + return err +} + +func (q *Queries) exec(ctx context.Context, stmt *sql.Stmt, query string, args ...interface{}) (sql.Result, error) { + switch { + case stmt != nil && q.tx != nil: + return q.tx.StmtContext(ctx, stmt).ExecContext(ctx, args...) + case stmt != nil: + return stmt.ExecContext(ctx, args...) + default: + return q.db.ExecContext(ctx, query, args...) + } +} + +func (q *Queries) query(ctx context.Context, stmt *sql.Stmt, query string, args ...interface{}) (*sql.Rows, error) { + switch { + case stmt != nil && q.tx != nil: + return q.tx.StmtContext(ctx, stmt).QueryContext(ctx, args...) + case stmt != nil: + return stmt.QueryContext(ctx, args...) + default: + return q.db.QueryContext(ctx, query, args...) + } +} + +func (q *Queries) queryRow(ctx context.Context, stmt *sql.Stmt, query string, args ...interface{}) (*sql.Row) { + switch { + case stmt != nil && q.tx != nil: + return q.tx.StmtContext(ctx, stmt).QueryRowContext(ctx, args...) + case stmt != nil: + return stmt.QueryRowContext(ctx, args...) + default: + return q.db.QueryRowContext(ctx, query, args...) + } +} +{{end}} + +type Queries struct { + db DBTX + + {{- if .EmitPreparedQueries}} + tx *sql.Tx + {{- range .KtQueries}} + {{.FieldName}} *sql.Stmt + {{- end}} + {{- end}} +} + +func (q *Queries) WithTx(tx *sql.Tx) *Queries { + return &Queries{ + db: tx, + {{- if .EmitPreparedQueries}} + tx: tx, + {{- range .KtQueries}} + {{.FieldName}}: q.{{.FieldName}}, + {{- end}} + {{- end}} + } +} +` + +var ifaceTmpl = `// Code generated by sqlc. DO NOT EDIT. + +package {{.Package}} + +import ( + {{range imports .SourceName}} + {{range .}}"{{.}}" + {{end}} + {{end}} +) + +type Querier interface { + {{- range .KtQueries}} + {{- if eq .Cmd ":one"}} + {{.MethodName}}(ctx context.Context, {{.Arg.Pair}}) ({{.Ret.Type}}, error) + {{- end}} + {{- if eq .Cmd ":many"}} + {{.MethodName}}(ctx context.Context, {{.Arg.Pair}}) ([]{{.Ret.Type}}, error) + {{- end}} + {{- if eq .Cmd ":exec"}} + {{.MethodName}}(ctx context.Context, {{.Arg.Pair}}) error + {{- end}} + {{- if eq .Cmd ":execrows"}} + {{.MethodName}}(ctx context.Context, {{.Arg.Pair}}) (int64, error) + {{- end}} + {{- end}} +} + +var _ Querier = (*Queries)(nil) +` + +var modelsTmpl = `// Code generated by sqlc. DO NOT EDIT. + +package {{.Package}} + +import ( + {{range imports .SourceName}} + {{range .}}"{{.}}" + {{end}} + {{end}} +) + +{{range .Enums}} +{{if .Comment}}// {{.Comment}}{{end}} +type {{.Name}} string + +const ( + {{- range .Constants}} + {{.Name}} {{.Type}} = "{{.Value}}" + {{- end}} +) + +func (e *{{.Name}}) Scan(src interface{}) error { + *e = {{.Name}}(src.([]byte)) + return nil +} +{{end}} + +{{range .Structs}} +{{if .Comment}}// {{.Comment}}{{end}} +type {{.Name}} struct { {{- range .Fields}} + {{- if .Comment}} + // {{.Comment}}{{else}} + {{- end}} + {{.Name}} {{.Type}} {{if $.EmitJSONTags}}{{$.Q}}{{.Tag}}{{$.Q}}{{end}} + {{- end}} +} +{{end}} +` + +var sqlTmpl = `// Code generated by sqlc. DO NOT EDIT. +// source: {{.SourceName}} + +package {{.Package}} + +import ( + {{range imports .SourceName}} + {{range .}}"{{.}}" + {{end}} + {{end}} +) + +{{range .KtQueries}} +{{if eq .SourceName $.SourceName}} +const {{.ConstantName}} = {{$.Q}}-- name: {{.MethodName}} {{.Cmd}} +{{.SQL}} +{{$.Q}} + +{{if .Arg.EmitStruct}} +type {{.Arg.Type}} struct { {{- range .Arg.Struct.Fields}} + {{.Name}} {{.Type}} {{if $.EmitJSONTags}}{{$.Q}}{{.Tag}}{{$.Q}}{{end}} + {{- end}} +} +{{end}} + +{{if .Ret.EmitStruct}} +type {{.Ret.Type}} struct { {{- range .Ret.Struct.Fields}} + {{.Name}} {{.Type}} {{if $.EmitJSONTags}}{{$.Q}}{{.Tag}}{{$.Q}}{{end}} + {{- end}} +} +{{end}} + +{{if eq .Cmd ":one"}} +{{range .Comments}}//{{.}} +{{end -}} +func (q *Queries) {{.MethodName}}(ctx context.Context, {{.Arg.Pair}}) ({{.Ret.Type}}, error) { + {{- if $.EmitPreparedQueries}} + row := q.queryRow(ctx, q.{{.FieldName}}, {{.ConstantName}}, {{.Arg.Params}}) + {{- else}} + row := q.db.QueryRowContext(ctx, {{.ConstantName}}, {{.Arg.Params}}) + {{- end}} + var {{.Ret.Name}} {{.Ret.Type}} + err := row.Scan({{.Ret.Scan}}) + return {{.Ret.Name}}, err +} +{{end}} + +{{if eq .Cmd ":many"}} +{{range .Comments}}//{{.}} +{{end -}} +func (q *Queries) {{.MethodName}}(ctx context.Context, {{.Arg.Pair}}) ([]{{.Ret.Type}}, error) { + {{- if $.EmitPreparedQueries}} + rows, err := q.query(ctx, q.{{.FieldName}}, {{.ConstantName}}, {{.Arg.Params}}) + {{- else}} + rows, err := q.db.QueryContext(ctx, {{.ConstantName}}, {{.Arg.Params}}) + {{- end}} + if err != nil { + return nil, err + } + defer rows.Close() + var items []{{.Ret.Type}} + for rows.Next() { + var {{.Ret.Name}} {{.Ret.Type}} + if err := rows.Scan({{.Ret.Scan}}); err != nil { + return nil, err + } + items = append(items, {{.Ret.Name}}) + } + if err := rows.Close(); err != nil { + return nil, err + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} +{{end}} + +{{if eq .Cmd ":exec"}} +{{range .Comments}}//{{.}} +{{end -}} +func (q *Queries) {{.MethodName}}(ctx context.Context, {{.Arg.Pair}}) error { + {{- if $.EmitPreparedQueries}} + _, err := q.exec(ctx, q.{{.FieldName}}, {{.ConstantName}}, {{.Arg.Params}}) + {{- else}} + _, err := q.db.ExecContext(ctx, {{.ConstantName}}, {{.Arg.Params}}) + {{- end}} + return err +} +{{end}} + +{{if eq .Cmd ":execrows"}} +{{range .Comments}}//{{.}} +{{end -}} +func (q *Queries) {{.MethodName}}(ctx context.Context, {{.Arg.Pair}}) (int64, error) { + {{- if $.EmitPreparedQueries}} + result, err := q.exec(ctx, q.{{.FieldName}}, {{.ConstantName}}, {{.Arg.Params}}) + {{- else}} + result, err := q.db.ExecContext(ctx, {{.ConstantName}}, {{.Arg.Params}}) + {{- end}} + if err != nil { + return 0, err + } + return result.RowsAffected() +} +{{end}} +{{end}} +{{end}} +` + +type ktTmplCtx struct { + Q string + Package string + Enums []KtEnum + Structs []KtStruct + KtQueries []KtQuery + Settings GenerateSettings + + // TODO: Race conditions + SourceName string + + EmitJSONTags bool + EmitPreparedQueries bool + EmitInterface bool +} + +func KtGenerate(r KtGenerateable, settings CombinedSettings) (map[string]string, error) { + funcMap := template.FuncMap{ + "lowerTitle": LowerTitle, + "imports": KtImports(r, settings), + } + + dbFile := template.Must(template.New("table").Funcs(funcMap).Parse(dbTmpl)) + modelsFile := template.Must(template.New("table").Funcs(funcMap).Parse(modelsTmpl)) + sqlFile := template.Must(template.New("table").Funcs(funcMap).Parse(sqlTmpl)) + ifaceFile := template.Must(template.New("table").Funcs(funcMap).Parse(ifaceTmpl)) + + pkg := settings.Package + tctx := ktTmplCtx{ + Settings: settings.Global, + EmitInterface: pkg.EmitInterface, + EmitJSONTags: pkg.EmitJSONTags, + EmitPreparedQueries: pkg.EmitPreparedQueries, + Q: "`", + Package: pkg.Name, + KtQueries: r.KtQueries(settings), + Enums: r.Enums(settings), + Structs: r.Structs(settings), + } + + output := map[string]string{} + + execute := func(name string, t *template.Template) error { + var b bytes.Buffer + w := bufio.NewWriter(&b) + tctx.SourceName = name + err := t.Execute(w, tctx) + w.Flush() + if err != nil { + return err + } + code, err := format.Source(b.Bytes()) + if err != nil { + fmt.Println(b.String()) + return fmt.Errorf("source error: %w", err) + } + if !strings.HasSuffix(name, ".go") { + name += ".go" + } + output[name] = string(code) + return nil + } + + if err := execute("db.go", dbFile); err != nil { + return nil, err + } + if err := execute("models.go", modelsFile); err != nil { + return nil, err + } + if pkg.EmitInterface { + if err := execute("querier.go", ifaceFile); err != nil { + return nil, err + } + } + + files := map[string]struct{}{} + for _, gq := range r.KtQueries(settings) { + files[gq.SourceName] = struct{}{} + } + + for source := range files { + if err := execute(source, sqlFile); err != nil { + return nil, err + } + } + return output, nil +} From 7bf5d1d8fb15c815da39988cba5aaf7604b8e000 Mon Sep 17 00:00:00 2001 From: Yunchi Luo Date: Sat, 25 Jan 2020 22:05:00 -0500 Subject: [PATCH 02/20] kotlin: support primitives --- internal/dinosql/ktgen.go | 716 +++++++++++++------------------------- 1 file changed, 251 insertions(+), 465 deletions(-) diff --git a/internal/dinosql/ktgen.go b/internal/dinosql/ktgen.go index a821dbb5be..467c57d191 100644 --- a/internal/dinosql/ktgen.go +++ b/internal/dinosql/ktgen.go @@ -4,13 +4,11 @@ import ( "bufio" "bytes" "fmt" - "go/format" "log" "regexp" "sort" "strings" "text/template" - "unicode" core "github.com/kyleconroy/sqlc/internal/pg" @@ -34,22 +32,9 @@ type KtEnum struct { type KtField struct { Name string Type string - Tags map[string]string Comment string } -func (gf KtField) Tag() string { - tags := make([]string, 0, len(gf.Tags)) - for key, val := range gf.Tags { - tags = append(tags, fmt.Sprintf("%s\"%s\"", key, val)) - } - if len(tags) == 0 { - return "" - } - sort.Strings(tags) - return strings.Join(tags, ",") -} - type KtStruct struct { Table core.FQN Name string @@ -80,7 +65,7 @@ func (v KtQueryValue) Pair() string { if v.isEmpty() { return "" } - return v.Name + " " + v.Type() + return v.Name + ": " + v.Type() } func (v KtQueryValue) Type() string { @@ -93,31 +78,40 @@ func (v KtQueryValue) Type() string { panic("no type for KtQueryValue: " + v.Name) } -func (v KtQueryValue) Params() string { +func (v KtQueryValue) Params() []KtQueryParam { if v.isEmpty() { - return "" + return nil } - var out []string + var out []KtQueryParam if v.Struct == nil { if strings.HasPrefix(v.Typ, "[]") && v.Typ != "[]byte" { - out = append(out, "pq.Array("+v.Name+")") + // TODO: this won't compile + out = append(out, KtQueryParam{ + Name: "pq.Array(" + v.Name + ")", + Typ: v.Typ, + }) } else { - out = append(out, v.Name) + out = append(out, KtQueryParam{ + Name: v.Name, + Typ: v.Typ, + }) } } else { for _, f := range v.Struct.Fields { if strings.HasPrefix(f.Type, "[]") && f.Type != "[]byte" { - out = append(out, "pq.Array("+v.Name+"."+f.Name+")") + out = append(out, KtQueryParam{ + Name: "pq.Array(" + v.Name + "." + f.Name + ")", + Typ: f.Type, + }) } else { - out = append(out, v.Name+"."+f.Name) + out = append(out, KtQueryParam{ + Name: v.Name + "." + f.Name, + Typ: f.Type, + }) } } } - if len(out) <= 3 { - return strings.Join(out, ",") - } - out = append(out, "") - return "\n" + strings.Join(out, ",\n") + return out } func (v KtQueryValue) Scan() string { @@ -144,8 +138,22 @@ func (v KtQueryValue) Scan() string { return "\n" + strings.Join(out, ",\n") } +type KtQueryParam struct { + Name string + Typ string +} + +func (p KtQueryParam) Getter() string { + return "get" + strings.TrimSuffix(p.Typ, "?") +} + +func (p KtQueryParam) Setter() string { + return "set" + strings.TrimSuffix(p.Typ, "?") +} + // A struct used to generate methods and fields on the Queries struct type KtQuery struct { + ClassName string Cmd string Comments []string MethodName string @@ -158,13 +166,13 @@ type KtQuery struct { } type KtGenerateable interface { - Structs(settings CombinedSettings) []KtStruct + KtDataClasses(settings CombinedSettings) []KtStruct KtQueries(settings CombinedSettings) []KtQuery - Enums(settings CombinedSettings) []KtEnum + KtEnums(settings CombinedSettings) []KtEnum } func KtUsesType(r KtGenerateable, typ string, settings CombinedSettings) bool { - for _, strct := range r.Structs(settings) { + for _, strct := range r.KtDataClasses(settings) { for _, f := range strct.Fields { fType := strings.TrimPrefix(f.Type, "[]") if strings.HasPrefix(fType, typ) { @@ -175,32 +183,13 @@ func KtUsesType(r KtGenerateable, typ string, settings CombinedSettings) bool { return false } -func KtUsesArrays(r KtGenerateable, settings CombinedSettings) bool { - for _, strct := range r.Structs(settings) { - for _, f := range strct.Fields { - if strings.HasPrefix(f.Type, "[]") { - return true - } - } - } - return false -} - func KtImports(r KtGenerateable, settings CombinedSettings) func(string) [][]string { return func(filename string) [][]string { - if filename == "db.go" { - imps := []string{"context", "database/sql"} - if settings.Package.EmitPreparedQueries { - imps = append(imps, "fmt") - } - return [][]string{imps} - } - - if filename == "models.go" { + if filename == "Models.kt" { return ModelKtImports(r, settings) } - if filename == "querier.go" { + if filename == "Querier.kt" { return InterfaceKtImports(r, settings) } @@ -227,7 +216,8 @@ func InterfaceKtImports(r KtGenerateable, settings CombinedSettings) [][]string } std := map[string]struct{}{ - "context": struct{}{}, + "java.sql.Connection": {}, + "java.sql.SQLException": {}, } if uses("sql.Null") { std["database/sql"] = struct{}{} @@ -242,44 +232,13 @@ func InterfaceKtImports(r KtGenerateable, settings CombinedSettings) [][]string std["net"] = struct{}{} } - pkg := make(map[string]struct{}) - overrideTypes := map[string]string{} - for _, o := range settings.Overrides { - if o.ktBasicType { - continue - } - overrideTypes[o.ktTypeName] = o.ktPackage - } - - _, overrideNullTime := overrideTypes["pq.NullTime"] - if uses("pq.NullTime") && !overrideNullTime { - pkg["github.com/lib/pq"] = struct{}{} - } - _, overrideUUID := overrideTypes["uuid.UUID"] - if uses("uuid.UUID") && !overrideUUID { - pkg["github.com/ktogle/uuid"] = struct{}{} - } - - // Custom imports - for ktType, importPath := range overrideTypes { - if _, ok := std[importPath]; !ok && uses(ktType) { - pkg[importPath] = struct{}{} - } - } - - pkgs := make([]string, 0, len(pkg)) - for p, _ := range pkg { - pkgs = append(pkgs, p) - } - stds := make([]string, 0, len(std)) for s, _ := range std { stds = append(stds, s) } sort.Strings(stds) - sort.Strings(pkgs) - return [][]string{stds, pkgs} + return [][]string{stds} } func ModelKtImports(r KtGenerateable, settings CombinedSettings) [][]string { @@ -297,49 +256,17 @@ func ModelKtImports(r KtGenerateable, settings CombinedSettings) [][]string { std["net"] = struct{}{} } - // Custom imports - pkg := make(map[string]struct{}) - overrideTypes := map[string]string{} - for _, o := range settings.Overrides { - if o.ktBasicType { - continue - } - overrideTypes[o.ktTypeName] = o.ktPackage - } - - _, overrideNullTime := overrideTypes["pq.NullTime"] - if KtUsesType(r, "pq.NullTime", settings) && !overrideNullTime { - pkg["github.com/lib/pq"] = struct{}{} - } - - _, overrideUUID := overrideTypes["uuid.UUID"] - if KtUsesType(r, "uuid.UUID", settings) && !overrideUUID { - pkg["github.com/ktogle/uuid"] = struct{}{} - } - - for ktType, importPath := range overrideTypes { - if _, ok := std[importPath]; !ok && KtUsesType(r, ktType, settings) { - pkg[importPath] = struct{}{} - } - } - - pkgs := make([]string, 0, len(pkg)) - for p, _ := range pkg { - pkgs = append(pkgs, p) - } - stds := make([]string, 0, len(std)) for s, _ := range std { stds = append(stds, s) } sort.Strings(stds) - sort.Strings(pkgs) - return [][]string{stds, pkgs} + return [][]string{stds} } func QueryKtImports(r KtGenerateable, settings CombinedSettings, filename string) [][]string { - // for _, strct := range r.Structs() { + // for _, strct := range r.KtDataClasses() { // for _, f := range strct.Fields { // if strings.HasPrefix(f.Type, "[]") { // return true @@ -418,7 +345,8 @@ func QueryKtImports(r KtGenerateable, settings CombinedSettings, filename string } std := map[string]struct{}{ - "context": struct{}{}, + "java.sql.Connection": {}, + "java.sql.SQLException": {}, } if uses("sql.Null") { std["database/sql"] = struct{}{} @@ -434,32 +362,10 @@ func QueryKtImports(r KtGenerateable, settings CombinedSettings, filename string } pkg := make(map[string]struct{}) - overrideTypes := map[string]string{} - for _, o := range settings.Overrides { - if o.ktBasicType { - continue - } - overrideTypes[o.ktTypeName] = o.ktPackage - } if sliceScan() { pkg["github.com/lib/pq"] = struct{}{} } - _, overrideNullTime := overrideTypes["pq.NullTime"] - if uses("pq.NullTime") && !overrideNullTime { - pkg["github.com/lib/pq"] = struct{}{} - } - _, overrideUUID := overrideTypes["uuid.UUID"] - if uses("uuid.UUID") && !overrideUUID { - pkg["github.com/ktogle/uuid"] = struct{}{} - } - - // Custom imports - for ktType, importPath := range overrideTypes { - if _, ok := std[importPath]; !ok && uses(ktType) { - pkg[importPath] = struct{}{} - } - } pkgs := make([]string, 0, len(pkg)) for p, _ := range pkg { @@ -477,15 +383,11 @@ func QueryKtImports(r KtGenerateable, settings CombinedSettings, filename string } func ktEnumValueName(value string) string { - name := "" id := strings.Replace(value, "-", "_", -1) id = strings.Replace(id, ":", "_", -1) id = strings.Replace(id, "/", "_", -1) id = ktIdentPattern.ReplaceAllString(id, "") - for _, part := range strings.Split(id, "_") { - name += strings.Title(part) - } - return name + return strings.ToUpper(id) } func (r Result) KtEnums(settings CombinedSettings) []KtEnum { @@ -502,12 +404,12 @@ func (r Result) KtEnums(settings CombinedSettings) []KtEnum { enumName = name + "_" + enum.Name } e := KtEnum{ - Name: KtStructName(enumName, settings), + Name: KtDataClassName(enumName, settings), Comment: enum.Comment, } for _, v := range enum.Vals { e.Constants = append(e.Constants, KtConstant{ - Name: e.Name + ktEnumValueName(v), + Name: ktEnumValueName(v), Value: v, Type: e.Name, }) @@ -521,22 +423,22 @@ func (r Result) KtEnums(settings CombinedSettings) []KtEnum { return enums } -func KtStructName(name string, settings CombinedSettings) string { +func KtDataClassName(name string, settings CombinedSettings) string { if rename := settings.Global.Rename[name]; rename != "" { return rename } out := "" for _, p := range strings.Split(name, "_") { - if p == "id" { - out += "ID" - } else { - out += strings.Title(p) - } + out += strings.Title(p) } return out } -func (r Result) Structs(settings CombinedSettings) []KtStruct { +func KtMemberName(name string, settings CombinedSettings) string { + return LowerTitle(KtDataClassName(name, settings)) +} + +func (r Result) KtDataClasses(settings CombinedSettings) []KtStruct { var structs []KtStruct for name, schema := range r.Catalog.Schemas { if name == "pg_catalog" { @@ -551,14 +453,13 @@ func (r Result) Structs(settings CombinedSettings) []KtStruct { } s := KtStruct{ Table: core.FQN{Schema: name, Rel: table.Name}, - Name: inflection.Singular(KtStructName(tableName, settings)), + Name: inflection.Singular(KtDataClassName(tableName, settings)), Comment: table.Comment, } for _, column := range table.Columns { s.Fields = append(s.Fields, KtField{ - Name: KtStructName(column.Name, settings), + Name: KtMemberName(column.Name, settings), Type: r.ktType(column, settings), - Tags: map[string]string{"json:": column.Name}, Comment: column.Comment, }) } @@ -572,15 +473,9 @@ func (r Result) Structs(settings CombinedSettings) []KtStruct { } func (r Result) ktType(col core.Column, settings CombinedSettings) string { - // package overrides have a higher precedence - for _, oride := range settings.Overrides { - if oride.Column != "" && oride.columnName == col.Name && oride.table == col.Table { - return oride.ktTypeName - } - } typ := r.ktInnerType(col, settings) if col.IsArray { - return "[]" + typ + return fmt.Sprintf("Array<%s>", typ) } return typ } @@ -589,91 +484,84 @@ func (r Result) ktInnerType(col core.Column, settings CombinedSettings) string { columnType := col.DataType notNull := col.NotNull || col.IsArray - // package overrides have a higher precedence - for _, oride := range settings.Overrides { - if oride.PostgresType != "" && oride.PostgresType == columnType && oride.Null != notNull { - return oride.ktTypeName - } - } - switch columnType { case "serial", "pg_catalog.serial4": if notNull { - return "int32" + return "Int" } - return "sql.NullInt32" + return "Int?" case "bigserial", "pg_catalog.serial8": if notNull { - return "int64" + return "Long" } - return "sql.NullInt64" + return "Long?" case "smallserial", "pg_catalog.serial2": - return "int16" + return "Short" case "integer", "int", "int4", "pg_catalog.int4": if notNull { - return "int32" + return "Int" } - return "sql.NullInt32" + return "Int?" case "bigint", "pg_catalog.int8": if notNull { - return "int64" + return "Long" } - return "sql.NullInt64" + return "Long?" case "smallint", "pg_catalog.int2": - return "int16" + return "Short" case "float", "double precision", "pg_catalog.float8": if notNull { - return "float64" + return "Double" } - return "sql.NullFloat64" + return "Double?" case "real", "pg_catalog.float4": if notNull { - return "float32" + return "Float" } - return "sql.NullFloat64" // TODO: Change to sql.NullFloat32 after updating the go.mod file + return "Float?" case "pg_catalog.numeric": - // Since the Go standard library does not have a decimal type, lib/pq - // returns numerics as strings. - // - // https://github.com/lib/pq/issues/648 if notNull { - return "string" + return "java.math.BigDecimal" } - return "sql.NullString" + return "java.math.BigDecimal?" case "bool", "pg_catalog.bool": if notNull { - return "bool" + return "Boolean" } - return "sql.NullBool" + return "Boolean?" case "jsonb": - return "json.RawMessage" + // TODO: support json and byte types + return "String" case "bytea", "blob", "pg_catalog.bytea": - return "[]byte" + return "String" case "date": + // TODO if notNull { return "time.Time" } return "sql.NullTime" case "pg_catalog.time", "pg_catalog.timetz": + // TODO if notNull { return "time.Time" } return "sql.NullTime" case "pg_catalog.timestamp", "pg_catalog.timestamptz", "timestamptz": + // TODO if notNull { return "time.Time" } @@ -681,23 +569,27 @@ func (r Result) ktInnerType(col core.Column, settings CombinedSettings) string { case "text", "pg_catalog.varchar", "pg_catalog.bpchar", "string": if notNull { - return "string" + return "String" } - return "sql.NullString" + return "String?" case "uuid": + // TODO return "uuid.UUID" case "inet": + // TODO return "net.IP" case "void": + // TODO // A void value always returns NULL. Since there is no built-in NULL // value into the SQL package, we'll use sql.NullBool return "sql.NullBool" case "any": - return "interface{}" + // TODO + return "Any" default: for name, schema := range r.Catalog.Schemas { @@ -707,10 +599,10 @@ func (r Result) ktInnerType(col core.Column, settings CombinedSettings) string { for _, enum := range schema.Enums { if columnType == enum.Name { if name == "public" { - return KtStructName(enum.Name, settings) + return KtDataClassName(enum.Name, settings) } - return KtStructName(name+"_"+enum.Name, settings) + return KtDataClassName(name+"_"+enum.Name, settings) } } } @@ -726,29 +618,26 @@ func (r Result) ktInnerType(col core.Column, settings CombinedSettings) string { // JSON tags: count, count_2, count_2 // // This is unlikely to happen, so don't fix it yet -func (r Result) columnsToStruct(name string, columns []core.Column, settings CombinedSettings) *KtStruct { +func (r Result) ktColumnsToStruct(name string, columns []core.Column, settings CombinedSettings) *KtStruct { gs := KtStruct{ Name: name, } seen := map[string]int{} for i, c := range columns { - tagName := c.Name - fieldName := KtStructName(columnName(c, i), settings) + fieldName := KtMemberName(ktColumnName(c, i), settings) if v := seen[c.Name]; v > 0 { - tagName = fmt.Sprintf("%s_%d", tagName, v+1) fieldName = fmt.Sprintf("%s_%d", fieldName, v+1) } gs.Fields = append(gs.Fields, KtField{ Name: fieldName, Type: r.ktType(c, settings), - Tags: map[string]string{"json:": tagName}, }) seen[c.Name]++ } return &gs } -func argName(name string) string { +func ktArgName(name string) string { out := "" for i, p := range strings.Split(name, "_") { if i == 0 { @@ -762,32 +651,31 @@ func argName(name string) string { return out } -func paramName(p Parameter) string { +func ktParamName(p Parameter) string { if p.Column.Name != "" { - return argName(p.Column.Name) + return ktArgName(p.Column.Name) } return fmt.Sprintf("dollar_%d", p.Number) } -func columnName(c core.Column, pos int) string { +func ktColumnName(c core.Column, pos int) string { if c.Name != "" { return c.Name } return fmt.Sprintf("column_%d", pos+1) } -func compareFQN(a *core.FQN, b *core.FQN) bool { - if a == nil && b == nil { - return true - } - if a == nil || b == nil { - return false - } - return a.Catalog == b.Catalog && a.Schema == b.Schema && a.Rel == b.Rel +var jdbcSQLRe = regexp.MustCompile(`\B\$\d+\b`) + +// HACK: jdbc doesn't support numbered parameters, so we need to transform them to question marks... +// But there's no access to the SQL parser here, so we just do a dumb regexp replace instead. This won't work if +// the literal strings contain matching values, but good enough for a prototype. +func jdbcSQL(s string) string { + return jdbcSQLRe.ReplaceAllString(s, "?") } func (r Result) KtQueries(settings CombinedSettings) []KtQuery { - structs := r.Structs(settings) + structs := r.KtDataClasses(settings) qs := make([]KtQuery, 0, len(r.Queries)) for _, query := range r.Queries { @@ -800,18 +688,19 @@ func (r Result) KtQueries(settings CombinedSettings) []KtQuery { gq := KtQuery{ Cmd: query.Cmd, + ClassName: strings.Title(query.Name), ConstantName: LowerTitle(query.Name), FieldName: LowerTitle(query.Name) + "Stmt", - MethodName: query.Name, + MethodName: LowerTitle(query.Name), SourceName: query.Filename, - SQL: query.SQL, + SQL: jdbcSQL(query.SQL), Comments: query.Comments, } if len(query.Params) == 1 { p := query.Params[0] gq.Arg = KtQueryValue{ - Name: paramName(p), + Name: ktParamName(p), Typ: r.ktType(p.Column, settings), } } else if len(query.Params) > 1 { @@ -822,14 +711,14 @@ func (r Result) KtQueries(settings CombinedSettings) []KtQuery { gq.Arg = KtQueryValue{ Emit: true, Name: "arg", - Struct: r.columnsToStruct(gq.MethodName+"Params", cols, settings), + Struct: r.ktColumnsToStruct(gq.ClassName+"Params", cols, settings), } } if len(query.Columns) == 1 { c := query.Columns[0] gq.Ret = KtQueryValue{ - Name: columnName(c, 0), + Name: ktColumnName(c, 0), Typ: r.ktType(c, settings), } } else if len(query.Columns) > 1 { @@ -843,7 +732,7 @@ func (r Result) KtQueries(settings CombinedSettings) []KtQuery { same := true for i, f := range s.Fields { c := query.Columns[i] - sameName := f.Name == KtStructName(columnName(c, i), settings) + sameName := f.Name == KtMemberName(ktColumnName(c, i), settings) sameType := f.Type == r.ktType(c, settings) sameTable := s.Table.Catalog == c.Table.Catalog && s.Table.Schema == c.Table.Schema && s.Table.Rel == c.Table.Rel @@ -858,7 +747,7 @@ func (r Result) KtQueries(settings CombinedSettings) []KtQuery { } if gs == nil { - gs = r.columnsToStruct(gq.MethodName+"Row", query.Columns, settings) + gs = r.ktColumnsToStruct(gq.ClassName+"Row", query.Columns, settings) emit = true } gq.Ret = KtQueryValue{ @@ -874,125 +763,16 @@ func (r Result) KtQueries(settings CombinedSettings) []KtQuery { return qs } -var dbTmpl = `// Code generated by sqlc. DO NOT EDIT. +var ktIfaceTmpl = `// Code generated by sqlc. DO NOT EDIT. package {{.Package}} -import ( - {{range imports .SourceName}} - {{range .}}"{{.}}" - {{end}} - {{end}} -) - -type DBTX interface { - ExecContext(context.Context, string, ...interface{}) (sql.Result, error) - PrepareContext(context.Context, string) (*sql.Stmt, error) - QueryContext(context.Context, string, ...interface{}) (*sql.Rows, error) - QueryRowContext(context.Context, string, ...interface{}) *sql.Row -} - -func New(db DBTX) *Queries { - return &Queries{db: db} -} - -{{if .EmitPreparedQueries}} -func Prepare(ctx context.Context, db DBTX) (*Queries, error) { - q := Queries{db: db} - var err error - {{- if eq (len .KtQueries) 0 }} - _ = err - {{- end }} - {{- range .KtQueries }} - if q.{{.FieldName}}, err = db.PrepareContext(ctx, {{.ConstantName}}); err != nil { - return nil, fmt.Errorf("error preparing query {{.MethodName}}: %w", err) - } - {{- end}} - return &q, nil -} - -func (q *Queries) Close() error { - var err error - {{- range .KtQueries }} - if q.{{.FieldName}} != nil { - if cerr := q.{{.FieldName}}.Close(); cerr != nil { - err = fmt.Errorf("error closing {{.FieldName}}: %w", cerr) - } - } - {{- end}} - return err -} - -func (q *Queries) exec(ctx context.Context, stmt *sql.Stmt, query string, args ...interface{}) (sql.Result, error) { - switch { - case stmt != nil && q.tx != nil: - return q.tx.StmtContext(ctx, stmt).ExecContext(ctx, args...) - case stmt != nil: - return stmt.ExecContext(ctx, args...) - default: - return q.db.ExecContext(ctx, query, args...) - } -} - -func (q *Queries) query(ctx context.Context, stmt *sql.Stmt, query string, args ...interface{}) (*sql.Rows, error) { - switch { - case stmt != nil && q.tx != nil: - return q.tx.StmtContext(ctx, stmt).QueryContext(ctx, args...) - case stmt != nil: - return stmt.QueryContext(ctx, args...) - default: - return q.db.QueryContext(ctx, query, args...) - } -} - -func (q *Queries) queryRow(ctx context.Context, stmt *sql.Stmt, query string, args ...interface{}) (*sql.Row) { - switch { - case stmt != nil && q.tx != nil: - return q.tx.StmtContext(ctx, stmt).QueryRowContext(ctx, args...) - case stmt != nil: - return stmt.QueryRowContext(ctx, args...) - default: - return q.db.QueryRowContext(ctx, query, args...) - } -} +{{range imports .SourceName}} +{{range .}}import {{.}} +{{end}} {{end}} -type Queries struct { - db DBTX - - {{- if .EmitPreparedQueries}} - tx *sql.Tx - {{- range .KtQueries}} - {{.FieldName}} *sql.Stmt - {{- end}} - {{- end}} -} - -func (q *Queries) WithTx(tx *sql.Tx) *Queries { - return &Queries{ - db: tx, - {{- if .EmitPreparedQueries}} - tx: tx, - {{- range .KtQueries}} - {{.FieldName}}: q.{{.FieldName}}, - {{- end}} - {{- end}} - } -} -` - -var ifaceTmpl = `// Code generated by sqlc. DO NOT EDIT. - -package {{.Package}} - -import ( - {{range imports .SourceName}} - {{range .}}"{{.}}" - {{end}} - {{end}} -) - -type Querier interface { +interface Querier { {{- range .KtQueries}} {{- if eq .Cmd ":one"}} {{.MethodName}}(ctx context.Context, {{.Arg.Pair}}) ({{.Ret.Type}}, error) @@ -1008,143 +788,144 @@ type Querier interface { {{- end}} {{- end}} } - -var _ Querier = (*Queries)(nil) ` -var modelsTmpl = `// Code generated by sqlc. DO NOT EDIT. +var ktModelsTmpl = `// Code generated by sqlc. DO NOT EDIT. package {{.Package}} -import ( - {{range imports .SourceName}} - {{range .}}"{{.}}" - {{end}} - {{end}} -) +{{range imports .SourceName}} +{{range .}}import {{.}} +{{end}} +{{end}} {{range .Enums}} {{if .Comment}}// {{.Comment}}{{end}} -type {{.Name}} string - -const ( - {{- range .Constants}} - {{.Name}} {{.Type}} = "{{.Value}}" - {{- end}} -) - -func (e *{{.Name}}) Scan(src interface{}) error { - *e = {{.Name}}(src.([]byte)) - return nil +enum class {{.Name}}(val value: String) { + {{- range $i, $e := .Constants}} + {{- if $i }},{{end}} + {{.Name}}("{{.Value}}") + {{- end}} } {{end}} -{{range .Structs}} +{{range .KtDataClasses}} {{if .Comment}}// {{.Comment}}{{end}} -type {{.Name}} struct { {{- range .Fields}} +data class {{.Name}} ( {{- range $i, $e := .Fields}} + {{- if $i }},{{end}} {{- if .Comment}} // {{.Comment}}{{else}} {{- end}} - {{.Name}} {{.Type}} {{if $.EmitJSONTags}}{{$.Q}}{{.Tag}}{{$.Q}}{{end}} + val {{.Name}}: {{.Type}} {{- end}} -} +) {{end}} ` -var sqlTmpl = `// Code generated by sqlc. DO NOT EDIT. -// source: {{.SourceName}} +var ktSqlTmpl = `// Code generated by sqlc. DO NOT EDIT. package {{.Package}} -import ( - {{range imports .SourceName}} - {{range .}}"{{.}}" - {{end}} - {{end}} -) +{{range imports .SourceName}} +{{range .}}import {{.}} +{{end}} +{{end}} {{range .KtQueries}} -{{if eq .SourceName $.SourceName}} -const {{.ConstantName}} = {{$.Q}}-- name: {{.MethodName}} {{.Cmd}} +const val {{.ConstantName}} = {{$.Q}}-- name: {{.MethodName}} {{.Cmd}} {{.SQL}} {{$.Q}} {{if .Arg.EmitStruct}} -type {{.Arg.Type}} struct { {{- range .Arg.Struct.Fields}} - {{.Name}} {{.Type}} {{if $.EmitJSONTags}}{{$.Q}}{{.Tag}}{{$.Q}}{{end}} +data class {{.Arg.Type}} ( {{- range $i, $e := .Arg.Struct.Fields}} + {{- if $i }},{{end}} + val {{.Name}}: {{.Type}} {{- end}} -} +) {{end}} {{if .Ret.EmitStruct}} -type {{.Ret.Type}} struct { {{- range .Ret.Struct.Fields}} - {{.Name}} {{.Type}} {{if $.EmitJSONTags}}{{$.Q}}{{.Tag}}{{$.Q}}{{end}} +data class {{.Ret.Type}} ( {{- range $i, $e := .Ret.Struct.Fields}} + {{- if $i }},{{end}} + val {{.Name}}: {{.Type}} {{- end}} -} +) +{{end}} {{end}} +class Queries(private val conn: Connection) { +{{range .KtQueries}} {{if eq .Cmd ":one"}} {{range .Comments}}//{{.}} -{{end -}} -func (q *Queries) {{.MethodName}}(ctx context.Context, {{.Arg.Pair}}) ({{.Ret.Type}}, error) { - {{- if $.EmitPreparedQueries}} - row := q.queryRow(ctx, q.{{.FieldName}}, {{.ConstantName}}, {{.Arg.Params}}) - {{- else}} - row := q.db.QueryRowContext(ctx, {{.ConstantName}}, {{.Arg.Params}}) - {{- end}} - var {{.Ret.Name}} {{.Ret.Type}} - err := row.Scan({{.Ret.Scan}}) - return {{.Ret.Name}}, err -} +{{end}} + @Throws(SQLException::class) + fun {{.MethodName}}({{.Arg.Pair}}): {{.Ret.Type}} { + val stmt = conn.prepareStatement({{.ConstantName}}) {{- range $i, $e := .Arg.Params }} + stmt.{{.Setter}}({{offset $i}}, {{.Name}}) + {{- end}} + + val results = stmt.executeQuery() + if (!results.next()) { + throw SQLException("no rows in result set") + } + {{ if .Ret.IsStruct }} + val ret = {{.Ret.Type}}( {{- range $i, $e := .Ret.Params }} + {{- if $i }},{{end}} + results.{{.Getter}}({{offset $i}}) + {{- end -}} + ) + {{ else }} + val ret = results.{{(index .Ret.Params 0).Getter}}(1) + {{ end }} + if (results.next()) { + throw SQLException("expected one row in result set, but got many") + } + return ret + } {{end}} {{if eq .Cmd ":many"}} {{range .Comments}}//{{.}} -{{end -}} -func (q *Queries) {{.MethodName}}(ctx context.Context, {{.Arg.Pair}}) ([]{{.Ret.Type}}, error) { - {{- if $.EmitPreparedQueries}} - rows, err := q.query(ctx, q.{{.FieldName}}, {{.ConstantName}}, {{.Arg.Params}}) - {{- else}} - rows, err := q.db.QueryContext(ctx, {{.ConstantName}}, {{.Arg.Params}}) - {{- end}} - if err != nil { - return nil, err - } - defer rows.Close() - var items []{{.Ret.Type}} - for rows.Next() { - var {{.Ret.Name}} {{.Ret.Type}} - if err := rows.Scan({{.Ret.Scan}}); err != nil { - return nil, err - } - items = append(items, {{.Ret.Name}}) - } - if err := rows.Close(); err != nil { - return nil, err - } - if err := rows.Err(); err != nil { - return nil, err - } - return items, nil -} +{{end}} + @Throws(SQLException::class) + fun {{.MethodName}}({{.Arg.Pair}}): List<{{.Ret.Type}}> { + val stmt = conn.prepareStatement({{.ConstantName}}) {{- range $i, $e := .Arg.Params }} + stmt.{{.Setter}}({{offset $i}}, {{.Name}}) + {{- end}} + + val results = stmt.executeQuery() + val ret = mutableListOf<{{.Ret.Type}}>() + while (results.next()) { + {{ if .Ret.IsStruct }} + ret.add({{.Ret.Type}}( {{- range $i, $e := .Ret.Params }} + {{- if $i }},{{end}} + results.{{.Getter}}({{offset $i}}) + {{- end -}} + )) + {{ else }} + ret.add(results.{{(index .Ret.Params 0).Getter}}(1) + {{ end }} + } + return ret + } {{end}} {{if eq .Cmd ":exec"}} {{range .Comments}}//{{.}} -{{end -}} -func (q *Queries) {{.MethodName}}(ctx context.Context, {{.Arg.Pair}}) error { - {{- if $.EmitPreparedQueries}} - _, err := q.exec(ctx, q.{{.FieldName}}, {{.ConstantName}}, {{.Arg.Params}}) - {{- else}} - _, err := q.db.ExecContext(ctx, {{.ConstantName}}, {{.Arg.Params}}) - {{- end}} - return err -} +{{end}} + @Throws(SQLException::class) + fun {{.MethodName}}({{.Arg.Pair}}) { + val stmt = conn.prepareStatement({{.ConstantName}}) {{- range $i, $e := .Arg.Params }} + stmt.{{.Setter}}({{offset $i}}, {{.Name}}) + {{- end}} + + stmt.execute() + } {{end}} {{if eq .Cmd ":execrows"}} {{range .Comments}}//{{.}} -{{end -}} +{{end}} func (q *Queries) {{.MethodName}}(ctx context.Context, {{.Arg.Pair}}) (int64, error) { {{- if $.EmitPreparedQueries}} result, err := q.exec(ctx, q.{{.FieldName}}, {{.ConstantName}}, {{.Arg.Params}}) @@ -1158,16 +939,16 @@ func (q *Queries) {{.MethodName}}(ctx context.Context, {{.Arg.Pair}}) (int64, er } {{end}} {{end}} -{{end}} +} ` type ktTmplCtx struct { - Q string - Package string - Enums []KtEnum - Structs []KtStruct - KtQueries []KtQuery - Settings GenerateSettings + Q string + Package string + Enums []KtEnum + KtDataClasses []KtStruct + KtQueries []KtQuery + Settings GenerateSettings // TODO: Race conditions SourceName string @@ -1177,16 +958,36 @@ type ktTmplCtx struct { EmitInterface bool } +func Offset(v int) int { + return v + 1 +} + +func ktFormat(s string) string { + // TODO: do more than just skip multiple blank lines, like maybe run ktlint to format + skipNextSpace := false + var lines []string + for _, l := range strings.Split(s, "\n") { + isSpace := len(strings.TrimSpace(l)) == 0 + if !isSpace || !skipNextSpace { + lines = append(lines, l) + } + skipNextSpace = isSpace + } + o := strings.Join(lines, "\n") + o += "\n" + return o +} + func KtGenerate(r KtGenerateable, settings CombinedSettings) (map[string]string, error) { funcMap := template.FuncMap{ "lowerTitle": LowerTitle, "imports": KtImports(r, settings), + "offset": Offset, } - dbFile := template.Must(template.New("table").Funcs(funcMap).Parse(dbTmpl)) - modelsFile := template.Must(template.New("table").Funcs(funcMap).Parse(modelsTmpl)) - sqlFile := template.Must(template.New("table").Funcs(funcMap).Parse(sqlTmpl)) - ifaceFile := template.Must(template.New("table").Funcs(funcMap).Parse(ifaceTmpl)) + modelsFile := template.Must(template.New("table").Funcs(funcMap).Parse(ktModelsTmpl)) + sqlFile := template.Must(template.New("table").Funcs(funcMap).Parse(ktSqlTmpl)) + ifaceFile := template.Must(template.New("table").Funcs(funcMap).Parse(ktIfaceTmpl)) pkg := settings.Package tctx := ktTmplCtx{ @@ -1194,11 +995,11 @@ func KtGenerate(r KtGenerateable, settings CombinedSettings) (map[string]string, EmitInterface: pkg.EmitInterface, EmitJSONTags: pkg.EmitJSONTags, EmitPreparedQueries: pkg.EmitPreparedQueries, - Q: "`", + Q: `"""`, Package: pkg.Name, KtQueries: r.KtQueries(settings), - Enums: r.Enums(settings), - Structs: r.Structs(settings), + Enums: r.KtEnums(settings), + KtDataClasses: r.KtDataClasses(settings), } output := map[string]string{} @@ -1212,39 +1013,24 @@ func KtGenerate(r KtGenerateable, settings CombinedSettings) (map[string]string, if err != nil { return err } - code, err := format.Source(b.Bytes()) - if err != nil { - fmt.Println(b.String()) - return fmt.Errorf("source error: %w", err) - } - if !strings.HasSuffix(name, ".go") { - name += ".go" + if !strings.HasSuffix(name, ".kt") { + name += ".kt" } - output[name] = string(code) + output[name] = ktFormat(b.String()) return nil } - if err := execute("db.go", dbFile); err != nil { - return nil, err - } - if err := execute("models.go", modelsFile); err != nil { + if err := execute("Models.kt", modelsFile); err != nil { return nil, err } if pkg.EmitInterface { - if err := execute("querier.go", ifaceFile); err != nil { + if err := execute("Querier.kt", ifaceFile); err != nil { return nil, err } } - - files := map[string]struct{}{} - for _, gq := range r.KtQueries(settings) { - files[gq.SourceName] = struct{}{} + if err := execute("Queries.kt", sqlFile); err != nil { + return nil, err } - for source := range files { - if err := execute(source, sqlFile); err != nil { - return nil, err - } - } return output, nil } From 93b190b3028f827366669557d58ef57071f3a008 Mon Sep 17 00:00:00 2001 From: Yunchi Luo Date: Sat, 25 Jan 2020 22:05:06 -0500 Subject: [PATCH 03/20] kotlin: arrays, enums, and dates --- internal/dinosql/ktgen.go | 490 +++++++++++++++++--------------------- 1 file changed, 220 insertions(+), 270 deletions(-) diff --git a/internal/dinosql/ktgen.go b/internal/dinosql/ktgen.go index 467c57d191..dd9126faac 100644 --- a/internal/dinosql/ktgen.go +++ b/internal/dinosql/ktgen.go @@ -31,7 +31,7 @@ type KtEnum struct { type KtField struct { Name string - Type string + Type ktType Comment string } @@ -46,7 +46,7 @@ type KtQueryValue struct { Emit bool Name string Struct *KtStruct - Typ string + Typ ktType } func (v KtQueryValue) EmitStruct() bool { @@ -58,7 +58,7 @@ func (v KtQueryValue) IsStruct() bool { } func (v KtQueryValue) isEmpty() bool { - return v.Typ == "" && v.Name == "" && v.Struct == nil + return v.Typ == (ktType{}) && v.Name == "" && v.Struct == nil } func (v KtQueryValue) Pair() string { @@ -69,8 +69,8 @@ func (v KtQueryValue) Pair() string { } func (v KtQueryValue) Type() string { - if v.Typ != "" { - return v.Typ + if v.Typ != (ktType{}) { + return v.Typ.String() } if v.Struct != nil { return v.Struct.Name @@ -78,64 +78,35 @@ func (v KtQueryValue) Type() string { panic("no type for KtQueryValue: " + v.Name) } -func (v KtQueryValue) Params() []KtQueryParam { +func (v KtQueryValue) Params() string { if v.isEmpty() { - return nil + return "" } - var out []KtQueryParam + var out []string if v.Struct == nil { - if strings.HasPrefix(v.Typ, "[]") && v.Typ != "[]byte" { - // TODO: this won't compile - out = append(out, KtQueryParam{ - Name: "pq.Array(" + v.Name + ")", - Typ: v.Typ, - }) - } else { - out = append(out, KtQueryParam{ - Name: v.Name, - Typ: v.Typ, - }) - } + out = append(out, fmt.Sprintf("stmt.%s(%d, %s)", v.Typ.jdbcSetter(), 1, v.Typ.jdbcValue(v.Name))) } else { - for _, f := range v.Struct.Fields { - if strings.HasPrefix(f.Type, "[]") && f.Type != "[]byte" { - out = append(out, KtQueryParam{ - Name: "pq.Array(" + v.Name + "." + f.Name + ")", - Typ: f.Type, - }) - } else { - out = append(out, KtQueryParam{ - Name: v.Name + "." + f.Name, - Typ: f.Type, - }) - } + for i, f := range v.Struct.Fields { + out = append(out, fmt.Sprintf("stmt.%s(%d, %s)", f.Type.jdbcSetter(), i+1, f.Type.jdbcValue(v.Name+"."+f.Name))) } } - return out + return strings.Join(out, "\n ") } -func (v KtQueryValue) Scan() string { +func (v KtQueryValue) ResultSet() string { var out []string if v.Struct == nil { - if strings.HasPrefix(v.Typ, "[]") && v.Typ != "[]byte" { - out = append(out, "pq.Array(&"+v.Name+")") - } else { - out = append(out, "&"+v.Name) - } + out = append(out, v.Typ.fromJDBCValue(fmt.Sprintf("%s.%s(%d)", v.Name, v.Typ.jdbcGetter(), 1))) } else { - for _, f := range v.Struct.Fields { - if strings.HasPrefix(f.Type, "[]") && f.Type != "[]byte" { - out = append(out, "pq.Array(&"+v.Name+"."+f.Name+")") - } else { - out = append(out, "&"+v.Name+"."+f.Name) - } + for i, f := range v.Struct.Fields { + out = append(out, f.Type.fromJDBCValue(fmt.Sprintf("%s.%s(%d)", v.Name, f.Type.jdbcGetter(), i+1))) } } - if len(out) <= 3 { - return strings.Join(out, ",") + ret := strings.Join(out, ",\n ") + if v.Struct != nil { + ret = v.Struct.Name + "(" + "\n " + ret + "\n )" } - out = append(out, "") - return "\n" + strings.Join(out, ",\n") + return ret } type KtQueryParam struct { @@ -174,8 +145,7 @@ type KtGenerateable interface { func KtUsesType(r KtGenerateable, typ string, settings CombinedSettings) bool { for _, strct := range r.KtDataClasses(settings) { for _, f := range strct.Fields { - fType := strings.TrimPrefix(f.Type, "[]") - if strings.HasPrefix(fType, typ) { + if f.Type.Name == typ { return true } } @@ -219,17 +189,17 @@ func InterfaceKtImports(r KtGenerateable, settings CombinedSettings) [][]string "java.sql.Connection": {}, "java.sql.SQLException": {}, } - if uses("sql.Null") { - std["database/sql"] = struct{}{} + if uses("LocalDate") { + std["java.time.LocalDate"] = struct{}{} } - if uses("json.RawMessage") { - std["encoding/json"] = struct{}{} + if uses("LocalTime") { + std["java.time.LocalTime"] = struct{}{} } - if uses("time.Time") { - std["time"] = struct{}{} + if uses("LocalDateTime") { + std["java.time.LocalDateTime"] = struct{}{} } - if uses("net.IP") { - std["net"] = struct{}{} + if uses("OffsetDateTime") { + std["java.time.OffsetDateTime"] = struct{}{} } stds := make([]string, 0, len(std)) @@ -243,17 +213,17 @@ func InterfaceKtImports(r KtGenerateable, settings CombinedSettings) [][]string func ModelKtImports(r KtGenerateable, settings CombinedSettings) [][]string { std := make(map[string]struct{}) - if KtUsesType(r, "sql.Null", settings) { - std["database/sql"] = struct{}{} + if KtUsesType(r, "LocalDate", settings) { + std["java.time.LocalDate"] = struct{}{} } - if KtUsesType(r, "json.RawMessage", settings) { - std["encoding/json"] = struct{}{} + if KtUsesType(r, "LocalTime", settings) { + std["java.time.LocalTime"] = struct{}{} } - if KtUsesType(r, "time.Time", settings) { - std["time"] = struct{}{} + if KtUsesType(r, "LocalDateTime", settings) { + std["java.time.LocalDateTime"] = struct{}{} } - if KtUsesType(r, "net.IP", settings) { - std["net"] = struct{}{} + if KtUsesType(r, "OffsetDateTime", settings) { + std["java.time.OffsetDateTime"] = struct{}{} } stds := make([]string, 0, len(std)) @@ -275,36 +245,32 @@ func QueryKtImports(r KtGenerateable, settings CombinedSettings, filename string // } var gq []KtQuery for _, query := range r.KtQueries(settings) { - if query.SourceName == filename { - gq = append(gq, query) - } + gq = append(gq, query) } uses := func(name string) bool { for _, q := range gq { if !q.Ret.isEmpty() { - if q.Ret.EmitStruct() { + if q.Ret.Struct != nil { for _, f := range q.Ret.Struct.Fields { - fType := strings.TrimPrefix(f.Type, "[]") - if strings.HasPrefix(fType, name) { + if f.Type.Name == name { return true } } } - if strings.HasPrefix(q.Ret.Type(), name) { + if q.Ret.Type() == name { return true } } if !q.Arg.isEmpty() { if q.Arg.EmitStruct() { for _, f := range q.Arg.Struct.Fields { - fType := strings.TrimPrefix(f.Type, "[]") - if strings.HasPrefix(fType, name) { + if f.Type.Name == name { return true } } } - if strings.HasPrefix(q.Arg.Type(), name) { + if q.Arg.Type() == name { return true } } @@ -312,61 +278,25 @@ func QueryKtImports(r KtGenerateable, settings CombinedSettings, filename string return false } - sliceScan := func() bool { - for _, q := range gq { - if !q.Ret.isEmpty() { - if q.Ret.IsStruct() { - for _, f := range q.Ret.Struct.Fields { - if strings.HasPrefix(f.Type, "[]") && f.Type != "[]byte" { - return true - } - } - } else { - if strings.HasPrefix(q.Ret.Type(), "[]") && q.Ret.Type() != "[]byte" { - return true - } - } - } - if !q.Arg.isEmpty() { - if q.Arg.IsStruct() { - for _, f := range q.Arg.Struct.Fields { - if strings.HasPrefix(f.Type, "[]") && f.Type != "[]byte" { - return true - } - } - } else { - if strings.HasPrefix(q.Arg.Type(), "[]") && q.Arg.Type() != "[]byte" { - return true - } - } - } - } - return false - } - std := map[string]struct{}{ "java.sql.Connection": {}, "java.sql.SQLException": {}, } - if uses("sql.Null") { - std["database/sql"] = struct{}{} + if uses("LocalDate") { + std["java.time.LocalDate"] = struct{}{} } - if uses("json.RawMessage") { - std["encoding/json"] = struct{}{} + if uses("LocalTime") { + std["java.time.LocalTime"] = struct{}{} } - if uses("time.Time") { - std["time"] = struct{}{} + if uses("LocalDateTime") { + std["java.time.LocalDateTime"] = struct{}{} } - if uses("net.IP") { - std["net"] = struct{}{} + if uses("OffsetDateTime") { + std["java.time.OffsetDateTime"] = struct{}{} } pkg := make(map[string]struct{}) - if sliceScan() { - pkg["github.com/lib/pq"] = struct{}{} - } - pkgs := make([]string, 0, len(pkg)) for p, _ := range pkg { pkgs = append(pkgs, p) @@ -472,124 +402,165 @@ func (r Result) KtDataClasses(settings CombinedSettings) []KtStruct { return structs } -func (r Result) ktType(col core.Column, settings CombinedSettings) string { - typ := r.ktInnerType(col, settings) - if col.IsArray { - return fmt.Sprintf("Array<%s>", typ) +type ktType struct { + Name string + IsEnum bool + IsArray bool + IsNull bool + DataType string +} + +func (t ktType) String() string { + v := t.Name + if t.IsArray { + v = fmt.Sprintf("Array<%s>", v) + } else if t.IsNull { + v += "?" } - return typ + return v +} + +func (t ktType) jdbcSetter() string { + return "set" + t.jdbcType() +} + +func (t ktType) jdbcGetter() string { + return "get" + t.jdbcType() } -func (r Result) ktInnerType(col core.Column, settings CombinedSettings) string { +func (t ktType) jdbcType() string { + if t.IsArray { + return "Array" + } + if t.IsEnum { + return "String" + } + if t.IsTime() { + return "Object" + } + return t.Name +} + +func (t ktType) IsTime() bool { + return t.Name == "LocalDate" || t.Name == "LocalDateTime" || t.Name == "LocalTime" || t.Name == "OffsetDateTime" +} + +func (t ktType) jdbcValue(name string) string { + if t.IsEnum && t.IsArray { + return fmt.Sprintf(`conn.createArrayOf("%s", %s.map { v -> v.value }.toTypedArray())`, t.DataType, name) + } + if t.IsEnum { + return name + ".value" + } + if t.IsArray { + return fmt.Sprintf(`conn.createArrayOf("%s", %s)`, t.DataType, name) + } + return name +} + +func (t ktType) fromJDBCValue(expr string) string { + if t.IsEnum && t.IsArray { + return fmt.Sprintf(`(%s.array as Array).map { v -> %s.valueOf(v) }.toTypedArray()`, expr, t.Name) + } + if t.IsEnum { + return fmt.Sprintf("%s.valueOf(%s)", t.Name, expr) + } + if t.IsArray { + return fmt.Sprintf(`%s.array as Array<%s>`, expr, t.Name) + } + if t.IsTime() { + expr = strings.TrimSuffix(expr, ")") + return fmt.Sprintf(`%s, %s::class.java)`, expr, t.Name) + } + return expr +} + +func (r Result) ktType(col core.Column, settings CombinedSettings) ktType { + typ, isEnum := r.ktInnerType(col, settings) + return ktType{ + Name: typ, + IsEnum: isEnum, + IsArray: col.IsArray, + IsNull: !col.NotNull, + DataType: col.DataType, + } +} + +func (r Result) ktInnerType(col core.Column, settings CombinedSettings) (string, bool) { columnType := col.DataType - notNull := col.NotNull || col.IsArray switch columnType { case "serial", "pg_catalog.serial4": - if notNull { - return "Int" - } - return "Int?" + return "Int", false case "bigserial", "pg_catalog.serial8": - if notNull { - return "Long" - } - return "Long?" + return "Long", false case "smallserial", "pg_catalog.serial2": - return "Short" + return "Short", false case "integer", "int", "int4", "pg_catalog.int4": - if notNull { - return "Int" - } - return "Int?" + return "Int", false case "bigint", "pg_catalog.int8": - if notNull { - return "Long" - } - return "Long?" + return "Long", false case "smallint", "pg_catalog.int2": - return "Short" + return "Short", false case "float", "double precision", "pg_catalog.float8": - if notNull { - return "Double" - } - return "Double?" + return "Double", false case "real", "pg_catalog.float4": - if notNull { - return "Float" - } - return "Float?" + return "Float", false case "pg_catalog.numeric": - if notNull { - return "java.math.BigDecimal" - } - return "java.math.BigDecimal?" + return "java.math.BigDecimal", false case "bool", "pg_catalog.bool": - if notNull { - return "Boolean" - } - return "Boolean?" + return "Boolean", false case "jsonb": // TODO: support json and byte types - return "String" + return "String", false case "bytea", "blob", "pg_catalog.bytea": - return "String" + return "String", false case "date": - // TODO - if notNull { - return "time.Time" - } - return "sql.NullTime" + // Date and time mappings from https://jdbc.postgresql.org/documentation/head/java8-date-time.html + return "LocalDate", false case "pg_catalog.time", "pg_catalog.timetz": - // TODO - if notNull { - return "time.Time" - } - return "sql.NullTime" + return "LocalTime", false + + case "pg_catalog.timestamp": + return "LocalDateTime", false - case "pg_catalog.timestamp", "pg_catalog.timestamptz", "timestamptz": + case "pg_catalog.timestamptz", "timestamptz": // TODO - if notNull { - return "time.Time" - } - return "sql.NullTime" + return "OffsetDateTime", false case "text", "pg_catalog.varchar", "pg_catalog.bpchar", "string": - if notNull { - return "String" - } - return "String?" + return "String", false case "uuid": // TODO - return "uuid.UUID" + return "uuid.UUID", false case "inet": // TODO - return "net.IP" + return "net.IP", false case "void": // TODO // A void value always returns NULL. Since there is no built-in NULL // value into the SQL package, we'll use sql.NullBool - return "sql.NullBool" + return "sql.NullBool", false case "any": // TODO - return "Any" + return "Any", false default: for name, schema := range r.Catalog.Schemas { @@ -599,25 +570,18 @@ func (r Result) ktInnerType(col core.Column, settings CombinedSettings) string { for _, enum := range schema.Enums { if columnType == enum.Name { if name == "public" { - return KtDataClassName(enum.Name, settings) + return KtDataClassName(enum.Name, settings), true } - return KtDataClassName(name+"_"+enum.Name, settings) + return KtDataClassName(name+"_"+enum.Name, settings), true } } } log.Printf("unknown PostgreSQL type: %s\n", columnType) - return "interface{}" + return "interface{}", false } } -// It's possible that this method will generate duplicate JSON tag values -// -// Columns: count, count, count_2 -// Fields: Count, Count_2, Count2 -// JSON tags: count, count_2, count_2 -// -// This is unlikely to happen, so don't fix it yet func (r Result) ktColumnsToStruct(name string, columns []core.Column, settings CombinedSettings) *KtStruct { gs := KtStruct{ Name: name, @@ -642,8 +606,6 @@ func ktArgName(name string) string { for i, p := range strings.Split(name, "_") { if i == 0 { out += strings.ToLower(p) - } else if p == "id" { - out += "ID" } else { out += strings.Title(p) } @@ -718,7 +680,7 @@ func (r Result) KtQueries(settings CombinedSettings) []KtQuery { if len(query.Columns) == 1 { c := query.Columns[0] gq.Ret = KtQueryValue{ - Name: ktColumnName(c, 0), + Name: "results", Typ: r.ktType(c, settings), } } else if len(query.Columns) > 1 { @@ -752,7 +714,7 @@ func (r Result) KtQueries(settings CombinedSettings) []KtQuery { } gq.Ret = KtQueryValue{ Emit: emit, - Name: "i", + Name: "results", Struct: gs, } } @@ -772,21 +734,22 @@ package {{.Package}} {{end}} {{end}} -interface Querier { - {{- range .KtQueries}} - {{- if eq .Cmd ":one"}} - {{.MethodName}}(ctx context.Context, {{.Arg.Pair}}) ({{.Ret.Type}}, error) - {{- end}} - {{- if eq .Cmd ":many"}} - {{.MethodName}}(ctx context.Context, {{.Arg.Pair}}) ([]{{.Ret.Type}}, error) - {{- end}} - {{- if eq .Cmd ":exec"}} - {{.MethodName}}(ctx context.Context, {{.Arg.Pair}}) error - {{- end}} - {{- if eq .Cmd ":execrows"}} - {{.MethodName}}(ctx context.Context, {{.Arg.Pair}}) (int64, error) - {{- end}} - {{- end}} +interface Queries { + {{- range .KtQueries}} + @Throws(SQLException::class) + {{- if eq .Cmd ":one"}} + fun {{.MethodName}}({{.Arg.Pair}}): {{.Ret.Type}} + {{- end}} + {{- if eq .Cmd ":many"}} + fun {{.MethodName}}({{.Arg.Pair}}): List<{{.Ret.Type}}> + {{- end}} + {{- if eq .Cmd ":exec"}} + fun {{.MethodName}}({{.Arg.Pair}}) + {{- end}} + {{- if eq .Cmd ":execrows"}} + fun {{.MethodName}}({{.Arg.Pair}}): Int + {{- end}} + {{end}} } ` @@ -853,34 +816,27 @@ data class {{.Ret.Type}} ( {{- range $i, $e := .Ret.Struct.Fields}} {{end}} {{end}} -class Queries(private val conn: Connection) { +class QueriesImpl(private val conn: Connection){{ if .EmitInterface }} : Queries{{end}} { {{range .KtQueries}} {{if eq .Cmd ":one"}} {{range .Comments}}//{{.}} {{end}} @Throws(SQLException::class) + {{ if $.EmitInterface }}override {{ end -}} fun {{.MethodName}}({{.Arg.Pair}}): {{.Ret.Type}} { - val stmt = conn.prepareStatement({{.ConstantName}}) {{- range $i, $e := .Arg.Params }} - stmt.{{.Setter}}({{offset $i}}, {{.Name}}) - {{- end}} - - val results = stmt.executeQuery() - if (!results.next()) { - throw SQLException("no rows in result set") - } - {{ if .Ret.IsStruct }} - val ret = {{.Ret.Type}}( {{- range $i, $e := .Ret.Params }} - {{- if $i }},{{end}} - results.{{.Getter}}({{offset $i}}) - {{- end -}} - ) - {{ else }} - val ret = results.{{(index .Ret.Params 0).Getter}}(1) - {{ end }} - if (results.next()) { - throw SQLException("expected one row in result set, but got many") + val stmt = conn.prepareStatement({{.ConstantName}}) + {{.Arg.Params}} + + return stmt.executeQuery().use { results -> + if (!results.next()) { + throw SQLException("no rows in result set") + } + val ret = {{.Ret.ResultSet}} + if (results.next()) { + throw SQLException("expected one row in result set, but got many") + } + ret } - return ret } {{end}} @@ -888,25 +844,18 @@ class Queries(private val conn: Connection) { {{range .Comments}}//{{.}} {{end}} @Throws(SQLException::class) + {{ if $.EmitInterface }}override {{ end -}} fun {{.MethodName}}({{.Arg.Pair}}): List<{{.Ret.Type}}> { - val stmt = conn.prepareStatement({{.ConstantName}}) {{- range $i, $e := .Arg.Params }} - stmt.{{.Setter}}({{offset $i}}, {{.Name}}) - {{- end}} - - val results = stmt.executeQuery() - val ret = mutableListOf<{{.Ret.Type}}>() - while (results.next()) { - {{ if .Ret.IsStruct }} - ret.add({{.Ret.Type}}( {{- range $i, $e := .Ret.Params }} - {{- if $i }},{{end}} - results.{{.Getter}}({{offset $i}}) - {{- end -}} - )) - {{ else }} - ret.add(results.{{(index .Ret.Params 0).Getter}}(1) - {{ end }} + val stmt = conn.prepareStatement({{.ConstantName}}) + {{.Arg.Params}} + + return stmt.executeQuery().use { results -> + val ret = mutableListOf<{{.Ret.Type}}>() + while (results.next()) { + ret.add({{.Ret.ResultSet}}) + } + ret } - return ret } {{end}} @@ -914,29 +863,30 @@ class Queries(private val conn: Connection) { {{range .Comments}}//{{.}} {{end}} @Throws(SQLException::class) + {{ if $.EmitInterface }}override {{ end -}} fun {{.MethodName}}({{.Arg.Pair}}) { - val stmt = conn.prepareStatement({{.ConstantName}}) {{- range $i, $e := .Arg.Params }} - stmt.{{.Setter}}({{offset $i}}, {{.Name}}) - {{- end}} + val stmt = conn.prepareStatement({{.ConstantName}}) + {{ .Arg.Params }} stmt.execute() + stmt.close() } {{end}} {{if eq .Cmd ":execrows"}} {{range .Comments}}//{{.}} {{end}} -func (q *Queries) {{.MethodName}}(ctx context.Context, {{.Arg.Pair}}) (int64, error) { - {{- if $.EmitPreparedQueries}} - result, err := q.exec(ctx, q.{{.FieldName}}, {{.ConstantName}}, {{.Arg.Params}}) - {{- else}} - result, err := q.db.ExecContext(ctx, {{.ConstantName}}, {{.Arg.Params}}) - {{- end}} - if err != nil { - return 0, err - } - return result.RowsAffected() -} + @Throws(SQLException::class) + {{ if $.EmitInterface }}override {{ end -}} + fun {{.MethodName}}({{.Arg.Pair}}): Int { + val stmt = conn.prepareStatement({{.ConstantName}}) + {{ .Arg.Params }} + + stmt.execute() + val count = stmt.updateCount + stmt.close() + return count + } {{end}} {{end}} } @@ -1024,11 +974,11 @@ func KtGenerate(r KtGenerateable, settings CombinedSettings) (map[string]string, return nil, err } if pkg.EmitInterface { - if err := execute("Querier.kt", ifaceFile); err != nil { + if err := execute("Queries.kt", ifaceFile); err != nil { return nil, err } } - if err := execute("Queries.kt", sqlFile); err != nil { + if err := execute("QueriesImpl.kt", sqlFile); err != nil { return nil, err } From 37b98c98244426bc7a0126d9c39c4b2fa8eb81b4 Mon Sep 17 00:00:00 2001 From: Yunchi Luo Date: Sat, 25 Jan 2020 22:07:07 -0500 Subject: [PATCH 04/20] kotlin: generate examples --- examples/kotlin/.gitignore | 4 + examples/kotlin/build.gradle | 28 ++ examples/kotlin/gradle.properties | 1 + .../kotlin/gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 55190 bytes .../gradle/wrapper/gradle-wrapper.properties | 6 + examples/kotlin/gradlew | 172 ++++++++++ examples/kotlin/gradlew.bat | 84 +++++ examples/kotlin/settings.gradle | 2 + .../main/kotlin/com/example/authors/Models.kt | 10 + .../kotlin/com/example/authors/QueriesImpl.kt | 109 ++++++ .../com/example/booktest/postgresql/Models.kt | 27 ++ .../booktest/postgresql/QueriesImpl.kt | 292 ++++++++++++++++ .../main/kotlin/com/example/jets/Models.kt | 27 ++ .../kotlin/com/example/jets/QueriesImpl.kt | 64 ++++ .../main/kotlin/com/example/ondeck/Models.kt | 32 ++ .../main/kotlin/com/example/ondeck/Queries.kt | 41 +++ .../kotlin/com/example/ondeck/QueriesImpl.kt | 322 ++++++++++++++++++ examples/kotlin/src/main/resources/query.sql | 19 ++ examples/kotlin/src/main/resources/schema.sql | 5 + .../com/example/authors/QueriesImplTest.kt | 86 +++++ examples/sqlc.json | 33 ++ internal/cmd/cmd.go | 4 +- internal/cmd/generate.go | 2 +- 23 files changed, 1367 insertions(+), 3 deletions(-) create mode 100644 examples/kotlin/.gitignore create mode 100644 examples/kotlin/build.gradle create mode 100644 examples/kotlin/gradle.properties create mode 100644 examples/kotlin/gradle/wrapper/gradle-wrapper.jar create mode 100644 examples/kotlin/gradle/wrapper/gradle-wrapper.properties create mode 100755 examples/kotlin/gradlew create mode 100644 examples/kotlin/gradlew.bat create mode 100644 examples/kotlin/settings.gradle create mode 100644 examples/kotlin/src/main/kotlin/com/example/authors/Models.kt create mode 100644 examples/kotlin/src/main/kotlin/com/example/authors/QueriesImpl.kt create mode 100644 examples/kotlin/src/main/kotlin/com/example/booktest/postgresql/Models.kt create mode 100644 examples/kotlin/src/main/kotlin/com/example/booktest/postgresql/QueriesImpl.kt create mode 100644 examples/kotlin/src/main/kotlin/com/example/jets/Models.kt create mode 100644 examples/kotlin/src/main/kotlin/com/example/jets/QueriesImpl.kt create mode 100644 examples/kotlin/src/main/kotlin/com/example/ondeck/Models.kt create mode 100644 examples/kotlin/src/main/kotlin/com/example/ondeck/Queries.kt create mode 100644 examples/kotlin/src/main/kotlin/com/example/ondeck/QueriesImpl.kt create mode 100644 examples/kotlin/src/main/resources/query.sql create mode 100644 examples/kotlin/src/main/resources/schema.sql create mode 100644 examples/kotlin/src/test/kotlin/com/example/authors/QueriesImplTest.kt diff --git a/examples/kotlin/.gitignore b/examples/kotlin/.gitignore new file mode 100644 index 0000000000..fbb16c8de7 --- /dev/null +++ b/examples/kotlin/.gitignore @@ -0,0 +1,4 @@ +/.gradle/ +/.idea/ +/build/ +/out/ diff --git a/examples/kotlin/build.gradle b/examples/kotlin/build.gradle new file mode 100644 index 0000000000..f20b3ae51a --- /dev/null +++ b/examples/kotlin/build.gradle @@ -0,0 +1,28 @@ +plugins { + id 'org.jetbrains.kotlin.jvm' version '1.3.60' +} + +group 'com.example' +version '1.0-SNAPSHOT' + +repositories { + mavenCentral() +} + +dependencies { + implementation 'org.postgresql:postgresql:42.2.9' + implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8" + testImplementation 'org.junit.jupiter:junit-jupiter-api:5.3.1' + testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.3.1' +} + +test { + useJUnitPlatform() +} + +compileKotlin { + kotlinOptions.jvmTarget = "1.8" +} +compileTestKotlin { + kotlinOptions.jvmTarget = "1.8" +} \ No newline at end of file diff --git a/examples/kotlin/gradle.properties b/examples/kotlin/gradle.properties new file mode 100644 index 0000000000..29e08e8ca8 --- /dev/null +++ b/examples/kotlin/gradle.properties @@ -0,0 +1 @@ +kotlin.code.style=official \ No newline at end of file diff --git a/examples/kotlin/gradle/wrapper/gradle-wrapper.jar b/examples/kotlin/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000000000000000000000000000000000000..87b738cbd051603d91cc39de6cb000dd98fe6b02 GIT binary patch literal 55190 zcmafaW0WS*vSoFbZQHhO+s0S6%`V%vZQJa!ZQHKus_B{g-pt%P_q|ywBQt-*Stldc z$+IJ3?^KWm27v+sf`9-50uuadKtMnL*BJ;1^6ynvR7H?hQcjE>7)art9Bu0Pcm@7C z@c%WG|JzYkP)<@zR9S^iR_sA`azaL$mTnGKnwDyMa;8yL_0^>Ba^)phg0L5rOPTbm7g*YIRLg-2^{qe^`rb!2KqS zk~5wEJtTdD?)3+}=eby3x6%i)sb+m??NHC^u=tcG8p$TzB<;FL(WrZGV&cDQb?O0GMe6PBV=V z?tTO*5_HTW$xea!nkc~Cnx#cL_rrUGWPRa6l+A{aiMY=<0@8y5OC#UcGeE#I>nWh}`#M#kIn-$A;q@u-p71b#hcSItS!IPw?>8 zvzb|?@Ahb22L(O4#2Sre&l9H(@TGT>#Py)D&eW-LNb!=S;I`ZQ{w;MaHW z#to!~TVLgho_Pm%zq@o{K3Xq?I|MVuVSl^QHnT~sHlrVxgsqD-+YD?Nz9@HA<;x2AQjxP)r6Femg+LJ-*)k%EZ}TTRw->5xOY z9#zKJqjZgC47@AFdk1$W+KhTQJKn7e>A&?@-YOy!v_(}GyV@9G#I?bsuto4JEp;5|N{orxi_?vTI4UF0HYcA( zKyGZ4<7Fk?&LZMQb6k10N%E*$gr#T&HsY4SPQ?yerqRz5c?5P$@6dlD6UQwZJ*Je9 z7n-@7!(OVdU-mg@5$D+R%gt82Lt%&n6Yr4=|q>XT%&^z_D*f*ug8N6w$`woqeS-+#RAOfSY&Rz z?1qYa5xi(7eTCrzCFJfCxc%j{J}6#)3^*VRKF;w+`|1n;Xaojr2DI{!<3CaP`#tXs z*`pBQ5k@JLKuCmovFDqh_`Q;+^@t_;SDm29 zCNSdWXbV?9;D4VcoV`FZ9Ggrr$i<&#Dx3W=8>bSQIU_%vf)#(M2Kd3=rN@^d=QAtC zI-iQ;;GMk|&A++W5#hK28W(YqN%?!yuW8(|Cf`@FOW5QbX|`97fxmV;uXvPCqxBD zJ9iI37iV)5TW1R+fV16y;6}2tt~|0J3U4E=wQh@sx{c_eu)t=4Yoz|%Vp<#)Qlh1V z0@C2ZtlT>5gdB6W)_bhXtcZS)`9A!uIOa`K04$5>3&8An+i9BD&GvZZ=7#^r=BN=k za+=Go;qr(M)B~KYAz|<^O3LJON}$Q6Yuqn8qu~+UkUKK~&iM%pB!BO49L+?AL7N7o z(OpM(C-EY753=G=WwJHE`h*lNLMNP^c^bBk@5MyP5{v7x>GNWH>QSgTe5 z!*GPkQ(lcbEs~)4ovCu!Zt&$${9$u(<4@9%@{U<-ksAqB?6F`bQ;o-mvjr)Jn7F&j$@`il1Mf+-HdBs<-`1FahTxmPMMI)@OtI&^mtijW6zGZ67O$UOv1Jj z;a3gmw~t|LjPkW3!EZ=)lLUhFzvO;Yvj9g`8hm%6u`;cuek_b-c$wS_0M4-N<@3l|88 z@V{Sd|M;4+H6guqMm4|v=C6B7mlpP(+It%0E;W`dxMOf9!jYwWj3*MRk`KpS_jx4c z=hrKBkFK;gq@;wUV2eqE3R$M+iUc+UD0iEl#-rECK+XmH9hLKrC={j@uF=f3UiceB zU5l$FF7#RKjx+6!JHMG5-!@zI-eG=a-!Bs^AFKqN_M26%cIIcSs61R$yuq@5a3c3& z4%zLs!g}+C5%`ja?F`?5-og0lv-;(^e<`r~p$x%&*89_Aye1N)9LNVk?9BwY$Y$$F^!JQAjBJvywXAesj7lTZ)rXuxv(FFNZVknJha99lN=^h`J2> zl5=~(tKwvHHvh|9-41@OV`c;Ws--PE%{7d2sLNbDp;A6_Ka6epzOSFdqb zBa0m3j~bT*q1lslHsHqaHIP%DF&-XMpCRL(v;MV#*>mB^&)a=HfLI7efblG z(@hzN`|n+oH9;qBklb=d^S0joHCsArnR1-h{*dIUThik>ot^!6YCNjg;J_i3h6Rl0ji)* zo(tQ~>xB!rUJ(nZjCA^%X;)H{@>uhR5|xBDA=d21p@iJ!cH?+%U|VSh2S4@gv`^)^ zNKD6YlVo$%b4W^}Rw>P1YJ|fTb$_(7C;hH+ z1XAMPb6*p^h8)e5nNPKfeAO}Ik+ZN_`NrADeeJOq4Ak;sD~ zTe77no{Ztdox56Xi4UE6S7wRVxJzWxKj;B%v7|FZ3cV9MdfFp7lWCi+W{}UqekdpH zdO#eoOuB3Fu!DU`ErfeoZWJbWtRXUeBzi zBTF-AI7yMC^ntG+8%mn(I6Dw}3xK8v#Ly{3w3_E?J4(Q5JBq~I>u3!CNp~Ekk&YH` z#383VO4O42NNtcGkr*K<+wYZ>@|sP?`AQcs5oqX@-EIqgK@Pmp5~p6O6qy4ml~N{D z{=jQ7k(9!CM3N3Vt|u@%ssTw~r~Z(}QvlROAkQQ?r8OQ3F0D$aGLh zny+uGnH5muJ<67Z=8uilKvGuANrg@s3Vu_lU2ajb?rIhuOd^E@l!Kl0hYIxOP1B~Q zggUmXbh$bKL~YQ#!4fos9UUVG#}HN$lIkM<1OkU@r>$7DYYe37cXYwfK@vrHwm;pg zbh(hEU|8{*d$q7LUm+x&`S@VbW*&p-sWrplWnRM|I{P;I;%U`WmYUCeJhYc|>5?&& zj}@n}w~Oo=l}iwvi7K6)osqa;M8>fRe}>^;bLBrgA;r^ZGgY@IC^ioRmnE&H4)UV5 zO{7egQ7sBAdoqGsso5q4R(4$4Tjm&&C|7Huz&5B0wXoJzZzNc5Bt)=SOI|H}+fbit z-PiF5(NHSy>4HPMrNc@SuEMDuKYMQ--G+qeUPqO_9mOsg%1EHpqoX^yNd~~kbo`cH zlV0iAkBFTn;rVb>EK^V6?T~t~3vm;csx+lUh_%ROFPy0(omy7+_wYjN!VRDtwDu^h4n|xpAMsLepm% zggvs;v8+isCW`>BckRz1MQ=l>K6k^DdT`~sDXTWQ<~+JtY;I~I>8XsAq3yXgxe>`O zZdF*{9@Z|YtS$QrVaB!8&`&^W->_O&-JXn1n&~}o3Z7FL1QE5R*W2W@=u|w~7%EeC1aRfGtJWxImfY-D3t!!nBkWM> zafu>^Lz-ONgT6ExjV4WhN!v~u{lt2-QBN&UxwnvdH|I%LS|J-D;o>@@sA62@&yew0 z)58~JSZP!(lX;da!3`d)D1+;K9!lyNlkF|n(UduR-%g>#{`pvrD^ClddhJyfL7C-(x+J+9&7EsC~^O`&}V%)Ut8^O_7YAXPDpzv8ir4 zl`d)(;imc6r16k_d^)PJZ+QPxxVJS5e^4wX9D=V2zH&wW0-p&OJe=}rX`*->XT=;_qI&)=WHkYnZx6bLoUh_)n-A}SF_ z9z7agNTM5W6}}ui=&Qs@pO5$zHsOWIbd_&%j^Ok5PJ3yUWQw*i4*iKO)_er2CDUME ztt+{Egod~W-fn^aLe)aBz)MOc_?i-stTj}~iFk7u^-gGSbU;Iem06SDP=AEw9SzuF zeZ|hKCG3MV(z_PJg0(JbqTRf4T{NUt%kz&}4S`)0I%}ZrG!jgW2GwP=WTtkWS?DOs znI9LY!dK+1_H0h+i-_~URb^M;4&AMrEO_UlDV8o?E>^3x%ZJyh$JuDMrtYL8|G3If zPf2_Qb_W+V?$#O; zydKFv*%O;Y@o_T_UAYuaqx1isMKZ^32JtgeceA$0Z@Ck0;lHbS%N5)zzAW9iz; z8tTKeK7&qw!8XVz-+pz>z-BeIzr*#r0nB^cntjQ9@Y-N0=e&ZK72vlzX>f3RT@i7@ z=z`m7jNk!9%^xD0ug%ptZnM>F;Qu$rlwo}vRGBIymPL)L|x}nan3uFUw(&N z24gdkcb7!Q56{0<+zu zEtc5WzG2xf%1<@vo$ZsuOK{v9gx^0`gw>@h>ZMLy*h+6ueoie{D#}}` zK2@6Xxq(uZaLFC%M!2}FX}ab%GQ8A0QJ?&!vaI8Gv=vMhd);6kGguDmtuOElru()) zuRk&Z{?Vp!G~F<1#s&6io1`poBqpRHyM^p;7!+L??_DzJ8s9mYFMQ0^%_3ft7g{PD zZd}8E4EV}D!>F?bzcX=2hHR_P`Xy6?FOK)mCj)Ym4s2hh z0OlOdQa@I;^-3bhB6mpw*X5=0kJv8?#XP~9){G-+0ST@1Roz1qi8PhIXp1D$XNqVG zMl>WxwT+K`SdO1RCt4FWTNy3!i?N>*-lbnn#OxFJrswgD7HjuKpWh*o@QvgF&j+CT z{55~ZsUeR1aB}lv#s_7~+9dCix!5(KR#c?K?e2B%P$fvrsZxy@GP#R#jwL{y#Ld$} z7sF>QT6m|}?V;msb?Nlohj7a5W_D$y+4O6eI;Zt$jVGymlzLKscqer9#+p2$0It&u zWY!dCeM6^B^Z;ddEmhi?8`scl=Lhi7W%2|pT6X6^%-=q90DS(hQ-%c+E*ywPvmoF(KqDoW4!*gmQIklm zk#!GLqv|cs(JRF3G?=AYY19{w@~`G3pa z@xR9S-Hquh*&5Yas*VI};(%9%PADn`kzm zeWMJVW=>>wap*9|R7n#!&&J>gq04>DTCMtj{P^d12|2wXTEKvSf?$AvnE!peqV7i4 zE>0G%CSn%WCW1yre?yi9*aFP{GvZ|R4JT}M%x_%Hztz2qw?&28l&qW<6?c6ym{f$d z5YCF+k#yEbjCN|AGi~-NcCG8MCF1!MXBFL{#7q z)HO+WW173?kuI}^Xat;Q^gb4Hi0RGyB}%|~j8>`6X4CPo+|okMbKy9PHkr58V4bX6<&ERU)QlF8%%huUz&f+dwTN|tk+C&&o@Q1RtG`}6&6;ncQuAcfHoxd5AgD7`s zXynq41Y`zRSiOY@*;&1%1z>oNcWTV|)sjLg1X8ijg1Y zbIGL0X*Sd}EXSQ2BXCKbJmlckY(@EWn~Ut2lYeuw1wg?hhj@K?XB@V_ZP`fyL~Yd3n3SyHU-RwMBr6t-QWE5TinN9VD4XVPU; zonIIR!&pGqrLQK)=#kj40Im%V@ij0&Dh0*s!lnTw+D`Dt-xmk-jmpJv$1-E-vfYL4 zqKr#}Gm}~GPE+&$PI@4ag@=M}NYi7Y&HW82Q`@Y=W&PE31D110@yy(1vddLt`P%N^ z>Yz195A%tnt~tvsSR2{m!~7HUc@x<&`lGX1nYeQUE(%sphTi>JsVqSw8xql*Ys@9B z>RIOH*rFi*C`ohwXjyeRBDt8p)-u{O+KWP;$4gg||%*u{$~yEj+Al zE(hAQRQ1k7MkCq9s4^N3ep*$h^L%2Vq?f?{+cicpS8lo)$Cb69b98au+m2J_e7nYwID0@`M9XIo1H~|eZFc8Hl!qly612ADCVpU zY8^*RTMX(CgehD{9v|^9vZ6Rab`VeZ2m*gOR)Mw~73QEBiktViBhR!_&3l$|be|d6 zupC`{g89Y|V3uxl2!6CM(RNpdtynaiJ~*DqSTq9Mh`ohZnb%^3G{k;6%n18$4nAqR zjPOrP#-^Y9;iw{J@XH9=g5J+yEVh|e=4UeY<^65`%gWtdQ=-aqSgtywM(1nKXh`R4 zzPP&7r)kv_uC7X9n=h=!Zrf<>X=B5f<9~Q>h#jYRD#CT7D~@6@RGNyO-#0iq0uHV1 zPJr2O4d_xLmg2^TmG7|dpfJ?GGa`0|YE+`2Rata9!?$j#e9KfGYuLL(*^z z!SxFA`$qm)q-YKh)WRJZ@S+-sD_1E$V?;(?^+F3tVcK6 z2fE=8hV*2mgiAbefU^uvcM?&+Y&E}vG=Iz!%jBF7iv){lyC`)*yyS~D8k+Mx|N3bm zI~L~Z$=W9&`x)JnO;8c>3LSDw!fzN#X3qi|0`sXY4?cz{*#xz!kvZ9bO=K3XbN z5KrgN=&(JbXH{Wsu9EdmQ-W`i!JWEmfI;yVTT^a-8Ch#D8xf2dtyi?7p z%#)W3n*a#ndFpd{qN|+9Jz++AJQO#-Y7Z6%*%oyEP5zs}d&kKIr`FVEY z;S}@d?UU=tCdw~EJ{b}=9x}S2iv!!8<$?d7VKDA8h{oeD#S-$DV)-vPdGY@x08n)@ zag?yLF_E#evvRTj4^CcrLvBL=fft&@HOhZ6Ng4`8ijt&h2y}fOTC~7GfJi4vpomA5 zOcOM)o_I9BKz}I`q)fu+Qnfy*W`|mY%LO>eF^a z;$)?T4F-(X#Q-m}!-k8L_rNPf`Mr<9IWu)f&dvt=EL+ESYmCvErd@8B9hd)afc(ZL94S z?rp#h&{7Ah5IJftK4VjATklo7@hm?8BX*~oBiz)jyc9FuRw!-V;Uo>p!CWpLaIQyt zAs5WN)1CCeux-qiGdmbIk8LR`gM+Qg=&Ve}w?zA6+sTL)abU=-cvU`3E?p5$Hpkxw znu0N659qR=IKnde*AEz_7z2pdi_Bh-sb3b=PdGO1Pdf_q2;+*Cx9YN7p_>rl``knY zRn%aVkcv1(W;`Mtp_DNOIECtgq%ufk-mu_<+Fu3Q17Tq4Rr(oeq)Yqk_CHA7LR@7@ zIZIDxxhS&=F2IQfusQ+Nsr%*zFK7S4g!U0y@3H^Yln|i;0a5+?RPG;ZSp6Tul>ezM z`40+516&719qT)mW|ArDSENle5hE2e8qY+zfeZoy12u&xoMgcP)4=&P-1Ib*-bAy` zlT?>w&B|ei-rCXO;sxo7*G;!)_p#%PAM-?m$JP(R%x1Hfas@KeaG%LO?R=lmkXc_MKZW}3f%KZ*rAN?HYvbu2L$ zRt_uv7~-IejlD1x;_AhwGXjB94Q=%+PbxuYzta*jw?S&%|qb=(JfJ?&6P=R7X zV%HP_!@-zO*zS}46g=J}#AMJ}rtWBr21e6hOn&tEmaM%hALH7nlm2@LP4rZ>2 zebe5aH@k!e?ij4Zwak#30|}>;`bquDQK*xmR=zc6vj0yuyC6+U=LusGnO3ZKFRpen z#pwzh!<+WBVp-!$MAc<0i~I%fW=8IO6K}bJ<-Scq>e+)951R~HKB?Mx2H}pxPHE@} zvqpq5j81_jtb_WneAvp<5kgdPKm|u2BdQx9%EzcCN&U{l+kbkhmV<1}yCTDv%&K^> zg;KCjwh*R1f_`6`si$h6`jyIKT7rTv5#k~x$mUyIw)_>Vr)D4fwIs@}{FSX|5GB1l z4vv;@oS@>Bu7~{KgUa_8eg#Lk6IDT2IY$41$*06{>>V;Bwa(-@N;ex4;D`(QK*b}{ z{#4$Hmt)FLqERgKz=3zXiV<{YX6V)lvYBr3V>N6ajeI~~hGR5Oe>W9r@sg)Na(a4- zxm%|1OKPN6^%JaD^^O~HbLSu=f`1px>RawOxLr+1b2^28U*2#h*W^=lSpSY4(@*^l z{!@9RSLG8Me&RJYLi|?$c!B0fP=4xAM4rerxX{xy{&i6=AqXueQAIBqO+pmuxy8Ib z4X^}r!NN3-upC6B#lt7&x0J;)nb9O~xjJMemm$_fHuP{DgtlU3xiW0UesTzS30L+U zQzDI3p&3dpONhd5I8-fGk^}@unluzu%nJ$9pzoO~Kk!>dLxw@M)M9?pNH1CQhvA`z zV;uacUtnBTdvT`M$1cm9`JrT3BMW!MNVBy%?@ZX%;(%(vqQAz<7I!hlDe|J3cn9=} zF7B;V4xE{Ss76s$W~%*$JviK?w8^vqCp#_G^jN0j>~Xq#Zru26e#l3H^{GCLEXI#n z?n~F-Lv#hU(bZS`EI9(xGV*jT=8R?CaK)t8oHc9XJ;UPY0Hz$XWt#QyLBaaz5+}xM zXk(!L_*PTt7gwWH*HLWC$h3Ho!SQ-(I||nn_iEC{WT3S{3V{8IN6tZ1C+DiFM{xlI zeMMk{o5;I6UvaC)@WKp9D+o?2Vd@4)Ue-nYci()hCCsKR`VD;hr9=vA!cgGL%3k^b(jADGyPi2TKr(JNh8mzlIR>n(F_hgiV(3@Ds(tjbNM7GoZ;T|3 zWzs8S`5PrA!9){jBJuX4y`f<4;>9*&NY=2Sq2Bp`M2(fox7ZhIDe!BaQUb@P(ub9D zlP8!p(AN&CwW!V&>H?yPFMJ)d5x#HKfwx;nS{Rr@oHqpktOg)%F+%1#tsPtq7zI$r zBo-Kflhq-=7_eW9B2OQv=@?|y0CKN77)N;z@tcg;heyW{wlpJ1t`Ap!O0`Xz{YHqO zI1${8Hag^r!kA<2_~bYtM=<1YzQ#GGP+q?3T7zYbIjN6Ee^V^b&9en$8FI*NIFg9G zPG$OXjT0Ku?%L7fat8Mqbl1`azf1ltmKTa(HH$Dqlav|rU{zP;Tbnk-XkGFQ6d+gi z-PXh?_kEJl+K98&OrmzgPIijB4!Pozbxd0H1;Usy!;V>Yn6&pu*zW8aYx`SC!$*ti zSn+G9p=~w6V(fZZHc>m|PPfjK6IN4(o=IFu?pC?+`UZAUTw!e`052{P=8vqT^(VeG z=psASIhCv28Y(;7;TuYAe>}BPk5Qg=8$?wZj9lj>h2kwEfF_CpK=+O6Rq9pLn4W)# zeXCKCpi~jsfqw7Taa0;!B5_C;B}e56W1s8@p*)SPzA;Fd$Slsn^=!_&!mRHV*Lmt| zBGIDPuR>CgS4%cQ4wKdEyO&Z>2aHmja;Pz+n|7(#l%^2ZLCix%>@_mbnyPEbyrHaz z>j^4SIv;ZXF-Ftzz>*t4wyq)ng8%0d;(Z_ExZ-cxwei=8{(br-`JYO(f23Wae_MqE z3@{Mlf^%M5G1SIN&en1*| zH~ANY1h3&WNsBy$G9{T=`kcxI#-X|>zLX2r*^-FUF+m0{k)n#GTG_mhG&fJfLj~K& zU~~6othMlvMm9<*SUD2?RD+R17|Z4mgR$L*R3;nBbo&Vm@39&3xIg;^aSxHS>}gwR zmzs?h8oPnNVgET&dx5^7APYx6Vv6eou07Zveyd+^V6_LzI$>ic+pxD_8s~ zC<}ucul>UH<@$KM zT4oI=62M%7qQO{}re-jTFqo9Z;rJKD5!X5$iwUsh*+kcHVhID08MB5cQD4TBWB(rI zuWc%CA}}v|iH=9gQ?D$1#Gu!y3o~p7416n54&Hif`U-cV?VrUMJyEqo_NC4#{puzU zzXEE@UppeeRlS9W*^N$zS`SBBi<@tT+<%3l@KhOy^%MWB9(A#*J~DQ;+MK*$rxo6f zcx3$3mcx{tly!q(p2DQrxcih|)0do_ZY77pyHGE#Q(0k*t!HUmmMcYFq%l$-o6%lS zDb49W-E?rQ#Hl``C3YTEdGZjFi3R<>t)+NAda(r~f1cT5jY}s7-2^&Kvo&2DLTPYP zhVVo-HLwo*vl83mtQ9)PR#VBg)FN}+*8c-p8j`LnNUU*Olm1O1Qqe62D#$CF#?HrM zy(zkX|1oF}Z=T#3XMLWDrm(|m+{1&BMxHY7X@hM_+cV$5-t!8HT(dJi6m9{ja53Yw z3f^`yb6Q;(e|#JQIz~B*=!-GbQ4nNL-NL z@^NWF_#w-Cox@h62;r^;Y`NX8cs?l^LU;5IWE~yvU8TqIHij!X8ydbLlT0gwmzS9} z@5BccG?vO;rvCs$mse1*ANi-cYE6Iauz$Fbn3#|ToAt5v7IlYnt6RMQEYLldva{~s zvr>1L##zmeoYgvIXJ#>bbuCVuEv2ZvZ8I~PQUN3wjP0UC)!U+wn|&`V*8?)` zMSCuvnuGec>QL+i1nCPGDAm@XSMIo?A9~C?g2&G8aNKjWd2pDX{qZ?04+2 zeyLw}iEd4vkCAWwa$ zbrHlEf3hfN7^1g~aW^XwldSmx1v~1z(s=1az4-wl} z`mM+G95*N*&1EP#u3}*KwNrPIgw8Kpp((rdEOO;bT1;6ea~>>sK+?!;{hpJ3rR<6UJb`O8P4@{XGgV%63_fs%cG8L zk9Fszbdo4tS$g0IWP1>t@0)E%-&9yj%Q!fiL2vcuL;90fPm}M==<>}Q)&sp@STFCY z^p!RzmN+uXGdtPJj1Y-khNyCb6Y$Vs>eZyW zPaOV=HY_T@FwAlleZCFYl@5X<<7%5DoO(7S%Lbl55?{2vIr_;SXBCbPZ(up;pC6Wx={AZL?shYOuFxLx1*>62;2rP}g`UT5+BHg(ju z&7n5QSvSyXbioB9CJTB#x;pexicV|9oaOpiJ9VK6EvKhl4^Vsa(p6cIi$*Zr0UxQ z;$MPOZnNae2Duuce~7|2MCfhNg*hZ9{+8H3?ts9C8#xGaM&sN;2lriYkn9W>&Gry! z3b(Xx1x*FhQkD-~V+s~KBfr4M_#0{`=Yrh90yj}Ph~)Nx;1Y^8<418tu!$1<3?T*~ z7Dl0P3Uok-7w0MPFQexNG1P5;y~E8zEvE49>$(f|XWtkW2Mj`udPn)pb%} zrA%wRFp*xvDgC767w!9`0vx1=q!)w!G+9(-w&p*a@WXg{?T&%;qaVcHo>7ca%KX$B z^7|KBPo<2;kM{2mRnF8vKm`9qGV%|I{y!pKm8B(q^2V;;x2r!1VJ^Zz8bWa)!-7a8 zSRf@dqEPlsj!7}oNvFFAA)75})vTJUwQ03hD$I*j6_5xbtd_JkE2`IJD_fQ;a$EkO z{fQ{~e%PKgPJsD&PyEvDmg+Qf&p*-qu!#;1k2r_(H72{^(Z)htgh@F?VIgK#_&eS- z$~(qInec>)XIkv@+{o6^DJLpAb>!d}l1DK^(l%#OdD9tKK6#|_R?-%0V!`<9Hj z3w3chDwG*SFte@>Iqwq`J4M&{aHXzyigT620+Vf$X?3RFfeTcvx_e+(&Q*z)t>c0e zpZH$1Z3X%{^_vylHVOWT6tno=l&$3 z9^eQ@TwU#%WMQaFvaYp_we%_2-9=o{+ck zF{cKJCOjpW&qKQquyp2BXCAP920dcrZ}T1@piukx_NY;%2W>@Wca%=Ch~x5Oj58Hv z;D-_ALOZBF(Mqbcqjd}P3iDbek#Dwzu`WRs`;hRIr*n0PV7vT+%Io(t}8KZ zpp?uc2eW!v28ipep0XNDPZt7H2HJ6oey|J3z!ng#1H~x_k%35P+Cp%mqXJ~cV0xdd z^4m5^K_dQ^Sg?$P`))ccV=O>C{Ds(C2WxX$LMC5vy=*44pP&)X5DOPYfqE${)hDg< z3hcG%U%HZ39=`#Ko4Uctg&@PQLf>?0^D|4J(_1*TFMOMB!Vv1_mnOq$BzXQdOGqgy zOp#LBZ!c>bPjY1NTXksZmbAl0A^Y&(%a3W-k>bE&>K?px5Cm%AT2E<&)Y?O*?d80d zgI5l~&Mve;iXm88Q+Fw7{+`PtN4G7~mJWR^z7XmYQ>uoiV!{tL)hp|= zS(M)813PM`d<501>{NqaPo6BZ^T{KBaqEVH(2^Vjeq zgeMeMpd*1tE@@);hGjuoVzF>Cj;5dNNwh40CnU+0DSKb~GEMb_# zT8Z&gz%SkHq6!;_6dQFYE`+b`v4NT7&@P>cA1Z1xmXy<2htaDhm@XXMp!g($ zw(7iFoH2}WR`UjqjaqOQ$ecNt@c|K1H1kyBArTTjLp%-M`4nzOhkfE#}dOpcd;b#suq8cPJ&bf5`6Tq>ND(l zib{VrPZ>{KuaIg}Y$W>A+nrvMg+l4)-@2jpAQ5h(Tii%Ni^-UPVg{<1KGU2EIUNGaXcEkOedJOusFT9X3%Pz$R+-+W+LlRaY-a$5r?4V zbPzgQl22IPG+N*iBRDH%l{Zh$fv9$RN1sU@Hp3m=M}{rX%y#;4(x1KR2yCO7Pzo>rw(67E{^{yUR`91nX^&MxY@FwmJJbyPAoWZ9Z zcBS$r)&ogYBn{DOtD~tIVJUiq|1foX^*F~O4hlLp-g;Y2wKLLM=?(r3GDqsPmUo*? zwKMEi*%f)C_@?(&&hk>;m07F$X7&i?DEK|jdRK=CaaNu-)pX>n3}@%byPKVkpLzBq z{+Py&!`MZ^4@-;iY`I4#6G@aWMv{^2VTH7|WF^u?3vsB|jU3LgdX$}=v7#EHRN(im zI(3q-eU$s~r=S#EWqa_2!G?b~ z<&brq1vvUTJH380=gcNntZw%7UT8tLAr-W49;9y^=>TDaTC|cKA<(gah#2M|l~j)w zY8goo28gj$n&zcNgqX1Qn6=<8?R0`FVO)g4&QtJAbW3G#D)uNeac-7cH5W#6i!%BH z=}9}-f+FrtEkkrQ?nkoMQ1o-9_b+&=&C2^h!&mWFga#MCrm85hW;)1pDt;-uvQG^D zntSB?XA*0%TIhtWDS!KcI}kp3LT>!(Nlc(lQN?k^bS8Q^GGMfo}^|%7s;#r+pybl@?KA++|FJ zr%se9(B|g*ERQU96az%@4gYrxRRxaM2*b}jNsG|0dQi;Rw{0WM0E>rko!{QYAJJKY z)|sX0N$!8d9E|kND~v|f>3YE|uiAnqbkMn)hu$if4kUkzKqoNoh8v|S>VY1EKmgO} zR$0UU2o)4i4yc1inx3}brso+sio{)gfbLaEgLahj8(_Z#4R-v) zglqwI%`dsY+589a8$Mu7#7_%kN*ekHupQ#48DIN^uhDxblDg3R1yXMr^NmkR z7J_NWCY~fhg}h!_aXJ#?wsZF$q`JH>JWQ9`jbZzOBpS`}-A$Vgkq7+|=lPx9H7QZG z8i8guMN+yc4*H*ANr$Q-3I{FQ-^;8ezWS2b8rERp9TMOLBxiG9J*g5=?h)mIm3#CGi4JSq1ohFrcrxx@`**K5%T}qbaCGldV!t zVeM)!U3vbf5FOy;(h08JnhSGxm)8Kqxr9PsMeWi=b8b|m_&^@#A3lL;bVKTBx+0v8 zLZeWAxJ~N27lsOT2b|qyp$(CqzqgW@tyy?CgwOe~^i;ZH zlL``i4r!>i#EGBNxV_P@KpYFQLz4Bdq{#zA&sc)*@7Mxsh9u%e6Ke`?5Yz1jkTdND zR8!u_yw_$weBOU}24(&^Bm|(dSJ(v(cBct}87a^X(v>nVLIr%%D8r|&)mi+iBc;B;x;rKq zd8*X`r?SZsTNCPQqoFOrUz8nZO?225Z#z(B!4mEp#ZJBzwd7jW1!`sg*?hPMJ$o`T zR?KrN6OZA1H{9pA;p0cSSu;@6->8aJm1rrO-yDJ7)lxuk#npUk7WNER1Wwnpy%u zF=t6iHzWU(L&=vVSSc^&D_eYP3TM?HN!Tgq$SYC;pSIPWW;zeNm7Pgub#yZ@7WPw#f#Kl)W4%B>)+8%gpfoH1qZ;kZ*RqfXYeGXJ_ zk>2otbp+1By`x^1V!>6k5v8NAK@T;89$`hE0{Pc@Q$KhG0jOoKk--Qx!vS~lAiypV zCIJ&6B@24`!TxhJ4_QS*S5;;Pk#!f(qIR7*(c3dN*POKtQe)QvR{O2@QsM%ujEAWEm) z+PM=G9hSR>gQ`Bv2(k}RAv2+$7qq(mU`fQ+&}*i%-RtSUAha>70?G!>?w%F(b4k!$ zvm;E!)2`I?etmSUFW7WflJ@8Nx`m_vE2HF#)_BiD#FaNT|IY@!uUbd4v$wTglIbIX zblRy5=wp)VQzsn0_;KdM%g<8@>#;E?vypTf=F?3f@SSdZ;XpX~J@l1;p#}_veWHp>@Iq_T z@^7|h;EivPYv1&u0~l9(a~>dV9Uw10QqB6Dzu1G~-l{*7IktljpK<_L8m0|7VV_!S zRiE{u97(%R-<8oYJ{molUd>vlGaE-C|^<`hppdDz<7OS13$#J zZ+)(*rZIDSt^Q$}CRk0?pqT5PN5TT`Ya{q(BUg#&nAsg6apPMhLTno!SRq1e60fl6GvpnwDD4N> z9B=RrufY8+g3_`@PRg+(+gs2(bd;5#{uTZk96CWz#{=&h9+!{_m60xJxC%r&gd_N! z>h5UzVX%_7@CUeAA1XFg_AF%(uS&^1WD*VPS^jcC!M2v@RHZML;e(H-=(4(3O&bX- zI6>usJOS+?W&^S&DL{l|>51ZvCXUKlH2XKJPXnHjs*oMkNM#ZDLx!oaM5(%^)5XaP zk6&+P16sA>vyFe9v`Cp5qnbE#r#ltR5E+O3!WnKn`56Grs2;sqr3r# zp@Zp<^q`5iq8OqOlJ`pIuyK@3zPz&iJ0Jcc`hDQ1bqos2;}O|$i#}e@ua*x5VCSx zJAp}+?Hz++tm9dh3Fvm_bO6mQo38al#>^O0g)Lh^&l82+&x)*<n7^Sw-AJo9tEzZDwyJ7L^i7|BGqHu+ea6(&7jKpBq>~V z8CJxurD)WZ{5D0?s|KMi=e7A^JVNM6sdwg@1Eg_+Bw=9j&=+KO1PG|y(mP1@5~x>d z=@c{EWU_jTSjiJl)d(>`qEJ;@iOBm}alq8;OK;p(1AdH$)I9qHNmxxUArdzBW0t+Qeyl)m3?D09770g z)hzXEOy>2_{?o%2B%k%z4d23!pZcoxyW1Ik{|m7Q1>fm4`wsRrl)~h z_=Z*zYL+EG@DV1{6@5@(Ndu!Q$l_6Qlfoz@79q)Kmsf~J7t1)tl#`MD<;1&CAA zH8;i+oBm89dTTDl{aH`cmTPTt@^K-%*sV+t4X9q0Z{A~vEEa!&rRRr=0Rbz4NFCJr zLg2u=0QK@w9XGE=6(-JgeP}G#WG|R&tfHRA3a9*zh5wNTBAD;@YYGx%#E4{C#Wlfo z%-JuW9=FA_T6mR2-Vugk1uGZvJbFvVVWT@QOWz$;?u6+CbyQsbK$>O1APk|xgnh_8 zc)s@Mw7#0^wP6qTtyNq2G#s?5j~REyoU6^lT7dpX{T-rhZWHD%dik*=EA7bIJgOVf_Ga!yC8V^tkTOEHe+JK@Fh|$kfNxO^= z#lpV^(ZQ-3!^_BhV>aXY~GC9{8%1lOJ}6vzXDvPhC>JrtXwFBC+!3a*Z-%#9}i z#<5&0LLIa{q!rEIFSFc9)>{-_2^qbOg5;_A9 ztQ))C6#hxSA{f9R3Eh^`_f${pBJNe~pIQ`tZVR^wyp}=gLK}e5_vG@w+-mp#Fu>e| z*?qBp5CQ5zu+Fi}xAs)YY1;bKG!htqR~)DB$ILN6GaChoiy%Bq@i+1ZnANC0U&D z_4k$=YP47ng+0NhuEt}6C;9-JDd8i5S>`Ml==9wHDQFOsAlmtrVwurYDw_)Ihfk35 zJDBbe!*LUpg%4n>BExWz>KIQ9vexUu^d!7rc_kg#Bf= z7TLz|l*y*3d2vi@c|pX*@ybf!+Xk|2*z$@F4K#MT8Dt4zM_EcFmNp31#7qT6(@GG? zdd;sSY9HHuDb=w&|K%sm`bYX#%UHKY%R`3aLMO?{T#EI@FNNFNO>p@?W*i0z(g2dt z{=9Ofh80Oxv&)i35AQN>TPMjR^UID-T7H5A?GI{MD_VeXZ%;uo41dVm=uT&ne2h0i zv*xI%9vPtdEK@~1&V%p1sFc2AA`9?H)gPnRdlO~URx!fiSV)j?Tf5=5F>hnO=$d$x zzaIfr*wiIc!U1K*$JO@)gP4%xp!<*DvJSv7p}(uTLUb=MSb@7_yO+IsCj^`PsxEl& zIxsi}s3L?t+p+3FXYqujGhGwTx^WXgJ1}a@Yq5mwP0PvGEr*qu7@R$9j>@-q1rz5T zriz;B^(ex?=3Th6h;7U`8u2sDlfS{0YyydK=*>-(NOm9>S_{U|eg(J~C7O zIe{|LK=Y`hXiF_%jOM8Haw3UtaE{hWdzo3BbD6ud7br4cODBtN(~Hl+odP0SSWPw;I&^m)yLw+nd#}3#z}?UIcX3=SssI}`QwY=% zAEXTODk|MqTx}2DVG<|~(CxgLyi*A{m>M@1h^wiC)4Hy>1K7@|Z&_VPJsaQoS8=ex zDL&+AZdQa>ylxhT_Q$q=60D5&%pi6+qlY3$3c(~rsITX?>b;({FhU!7HOOhSP7>bmTkC8KM%!LRGI^~y3Ug+gh!QM=+NZXznM)?L3G=4=IMvFgX3BAlyJ z`~jjA;2z+65D$j5xbv9=IWQ^&-K3Yh`vC(1Qz2h2`o$>Cej@XRGff!it$n{@WEJ^N z41qk%Wm=}mA*iwCqU_6}Id!SQd13aFER3unXaJJXIsSnxvG2(hSCP{i&QH$tL&TPx zDYJsuk+%laN&OvKb-FHK$R4dy%M7hSB*yj#-nJy?S9tVoxAuDei{s}@+pNT!vLOIC z8g`-QQW8FKp3cPsX%{)0B+x+OhZ1=L7F-jizt|{+f1Ga7%+!BXqjCjH&x|3%?UbN# zh?$I1^YokvG$qFz5ySK+Ja5=mkR&p{F}ev**rWdKMko+Gj^?Or=UH?SCg#0F(&a_y zXOh}dPv0D9l0RVedq1~jCNV=8?vZfU-Xi|nkeE->;ohG3U7z+^0+HV17~-_Mv#mV` zzvwUJJ15v5wwKPv-)i@dsEo@#WEO9zie7mdRAbgL2kjbW4&lk$vxkbq=w5mGKZK6@ zjXWctDkCRx58NJD_Q7e}HX`SiV)TZMJ}~zY6P1(LWo`;yDynY_5_L?N-P`>ALfmyl z8C$a~FDkcwtzK9m$tof>(`Vu3#6r#+v8RGy#1D2)F;vnsiL&P-c^PO)^B-4VeJteLlT@25sPa z%W~q5>YMjj!mhN})p$47VA^v$Jo6_s{!y?}`+h+VM_SN`!11`|;C;B};B&Z<@%FOG z_YQVN+zFF|q5zKab&e4GH|B;sBbKimHt;K@tCH+S{7Ry~88`si7}S)1E{21nldiu5 z_4>;XTJa~Yd$m4A9{Qbd)KUAm7XNbZ4xHbg3a8-+1uf*$1PegabbmCzgC~1WB2F(W zYj5XhVos!X!QHuZXCatkRsdEsSCc+D2?*S7a+(v%toqyxhjz|`zdrUvsxQS{J>?c& zvx*rHw^8b|v^7wq8KWVofj&VUitbm*a&RU_ln#ZFA^3AKEf<#T%8I!Lg3XEsdH(A5 zlgh&M_XEoal)i#0tcq8c%Gs6`xu;vvP2u)D9p!&XNt z!TdF_H~;`g@fNXkO-*t<9~;iEv?)Nee%hVe!aW`N%$cFJ(Dy9+Xk*odyFj72T!(b%Vo5zvCGZ%3tkt$@Wcx8BWEkefI1-~C_3y*LjlQ5%WEz9WD8i^ z2MV$BHD$gdPJV4IaV)G9CIFwiV=ca0cfXdTdK7oRf@lgyPx;_7*RRFk=?@EOb9Gcz zg~VZrzo*Snp&EE{$CWr)JZW)Gr;{B2ka6B!&?aknM-FENcl%45#y?oq9QY z3^1Y5yn&^D67Da4lI}ljDcphaEZw2;tlYuzq?uB4b9Mt6!KTW&ptxd^vF;NbX=00T z@nE1lIBGgjqs?ES#P{ZfRb6f!At51vk%<0X%d_~NL5b8UyfQMPDtfU@>ijA0NP3UU zh{lCf`Wu7cX!go`kUG`1K=7NN@SRGjUKuo<^;@GS!%iDXbJs`o6e`v3O8-+7vRkFm z)nEa$sD#-v)*Jb>&Me+YIW3PsR1)h=-Su)))>-`aRcFJG-8icomO4J@60 zw10l}BYxi{eL+Uu0xJYk-Vc~BcR49Qyyq!7)PR27D`cqGrik=?k1Of>gY7q@&d&Ds zt7&WixP`9~jjHO`Cog~RA4Q%uMg+$z^Gt&vn+d3&>Ux{_c zm|bc;k|GKbhZLr-%p_f%dq$eiZ;n^NxoS-Nu*^Nx5vm46)*)=-Bf<;X#?`YC4tLK; z?;u?shFbXeks+dJ?^o$l#tg*1NA?(1iFff@I&j^<74S!o;SWR^Xi);DM%8XiWpLi0 zQE2dL9^a36|L5qC5+&Pf0%>l&qQ&)OU4vjd)%I6{|H+pw<0(a``9w(gKD&+o$8hOC zNAiShtc}e~ob2`gyVZx59y<6Fpl*$J41VJ-H*e-yECWaDMmPQi-N8XI3 z%iI@ljc+d}_okL1CGWffeaejlxWFVDWu%e=>H)XeZ|4{HlbgC-Uvof4ISYQzZ0Um> z#Ov{k1c*VoN^f(gfiueuag)`TbjL$XVq$)aCUBL_M`5>0>6Ska^*Knk__pw{0I>jA zzh}Kzg{@PNi)fcAk7jMAdi-_RO%x#LQszDMS@_>iFoB+zJ0Q#CQJzFGa8;pHFdi`^ zxnTC`G$7Rctm3G8t8!SY`GwFi4gF|+dAk7rh^rA{NXzc%39+xSYM~($L(pJ(8Zjs* zYdN_R^%~LiGHm9|ElV4kVZGA*T$o@YY4qpJOxGHlUi*S*A(MrgQ{&xoZQo+#PuYRs zv3a$*qoe9gBqbN|y|eaH=w^LE{>kpL!;$wRahY(hhzRY;d33W)m*dfem@)>pR54Qy z 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 \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m"' + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn () { + echo "$*" +} + +die () { + echo + echo "$*" + echo + exit 1 +} + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MINGW* ) + msys=true + ;; + NONSTOP* ) + nonstop=true + ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin, switch paths to Windows format before running java +if $cygwin ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + JAVACMD=`cygpath --unix "$JAVACMD"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=$((i+1)) + done + case $i in + (0) set -- ;; + (1) set -- "$args0" ;; + (2) set -- "$args0" "$args1" ;; + (3) set -- "$args0" "$args1" "$args2" ;; + (4) set -- "$args0" "$args1" "$args2" "$args3" ;; + (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Escape application args +save () { + for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done + echo " " +} +APP_ARGS=$(save "$@") + +# Collect all arguments for the java command, following the shell quoting and substitution rules +eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" + +# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong +if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then + cd "$(dirname "$0")" +fi + +exec "$JAVACMD" "$@" diff --git a/examples/kotlin/gradlew.bat b/examples/kotlin/gradlew.bat new file mode 100644 index 0000000000..6d57edc706 --- /dev/null +++ b/examples/kotlin/gradlew.bat @@ -0,0 +1,84 @@ +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto init + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto init + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:init +@rem Get command-line arguments, handling Windows variants + +if not "%OS%" == "Windows_NT" goto win9xME_args + +:win9xME_args +@rem Slurp the command line arguments. +set CMD_LINE_ARGS= +set _SKIP=2 + +:win9xME_args_slurp +if "x%~1" == "x" goto execute + +set CMD_LINE_ARGS=%* + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/examples/kotlin/settings.gradle b/examples/kotlin/settings.gradle new file mode 100644 index 0000000000..d51bf78f7f --- /dev/null +++ b/examples/kotlin/settings.gradle @@ -0,0 +1,2 @@ +rootProject.name = 'dbtest' + diff --git a/examples/kotlin/src/main/kotlin/com/example/authors/Models.kt b/examples/kotlin/src/main/kotlin/com/example/authors/Models.kt new file mode 100644 index 0000000000..916a88d7c8 --- /dev/null +++ b/examples/kotlin/src/main/kotlin/com/example/authors/Models.kt @@ -0,0 +1,10 @@ +// Code generated by sqlc. DO NOT EDIT. + +package com.example.authors + +data class Author ( + val id: Long, + val name: String, + val bio: String? +) + diff --git a/examples/kotlin/src/main/kotlin/com/example/authors/QueriesImpl.kt b/examples/kotlin/src/main/kotlin/com/example/authors/QueriesImpl.kt new file mode 100644 index 0000000000..276de49970 --- /dev/null +++ b/examples/kotlin/src/main/kotlin/com/example/authors/QueriesImpl.kt @@ -0,0 +1,109 @@ +// Code generated by sqlc. DO NOT EDIT. + +package com.example.authors + +import java.sql.Connection +import java.sql.SQLException + +const val createAuthor = """-- name: createAuthor :one +INSERT INTO authors ( + name, bio +) VALUES ( + ?, ? +) +RETURNING id, name, bio +""" + +data class CreateAuthorParams ( + val name: String, + val bio: String? +) + +const val deleteAuthor = """-- name: deleteAuthor :exec +DELETE FROM authors +WHERE id = ? +""" + +const val getAuthor = """-- name: getAuthor :one +SELECT id, name, bio FROM authors +WHERE id = ? LIMIT 1 +""" + +const val listAuthors = """-- name: listAuthors :many +SELECT id, name, bio FROM authors +ORDER BY name +""" + +class QueriesImpl(private val conn: Connection) { + + @Throws(SQLException::class) + fun createAuthor(arg: CreateAuthorParams): Author { + val stmt = conn.prepareStatement(createAuthor) + stmt.setString(1, arg.name) + stmt.setString(2, arg.bio) + + return stmt.executeQuery().use { results -> + if (!results.next()) { + throw SQLException("no rows in result set") + } + val ret = Author( + results.getLong(1), + results.getString(2), + results.getString(3) + ) + if (results.next()) { + throw SQLException("expected one row in result set, but got many") + } + ret + } + } + + @Throws(SQLException::class) + fun deleteAuthor(id: Long) { + val stmt = conn.prepareStatement(deleteAuthor) + stmt.setLong(1, id) + + stmt.execute() + stmt.close() + } + + @Throws(SQLException::class) + fun getAuthor(id: Long): Author { + val stmt = conn.prepareStatement(getAuthor) + stmt.setLong(1, id) + + return stmt.executeQuery().use { results -> + if (!results.next()) { + throw SQLException("no rows in result set") + } + val ret = Author( + results.getLong(1), + results.getString(2), + results.getString(3) + ) + if (results.next()) { + throw SQLException("expected one row in result set, but got many") + } + ret + } + } + + @Throws(SQLException::class) + fun listAuthors(): List { + val stmt = conn.prepareStatement(listAuthors) + + return stmt.executeQuery().use { results -> + val ret = mutableListOf() + while (results.next()) { + ret.add(Author( + results.getLong(1), + results.getString(2), + results.getString(3) + )) + } + ret + } + } + +} + diff --git a/examples/kotlin/src/main/kotlin/com/example/booktest/postgresql/Models.kt b/examples/kotlin/src/main/kotlin/com/example/booktest/postgresql/Models.kt new file mode 100644 index 0000000000..46b71b248a --- /dev/null +++ b/examples/kotlin/src/main/kotlin/com/example/booktest/postgresql/Models.kt @@ -0,0 +1,27 @@ +// Code generated by sqlc. DO NOT EDIT. + +package com.example.booktest.postgresql + +import java.time.OffsetDateTime + +enum class BookType(val value: String) { + FICTION("FICTION"), + NONFICTION("NONFICTION") +} + +data class Author ( + val authorId: Int, + val name: String +) + +data class Book ( + val bookId: Int, + val authorId: Int, + val isbn: String, + val booktype: BookType, + val title: String, + val year: Int, + val available: OffsetDateTime, + val tags: Array +) + diff --git a/examples/kotlin/src/main/kotlin/com/example/booktest/postgresql/QueriesImpl.kt b/examples/kotlin/src/main/kotlin/com/example/booktest/postgresql/QueriesImpl.kt new file mode 100644 index 0000000000..e7d6d4764c --- /dev/null +++ b/examples/kotlin/src/main/kotlin/com/example/booktest/postgresql/QueriesImpl.kt @@ -0,0 +1,292 @@ +// Code generated by sqlc. DO NOT EDIT. + +package com.example.booktest.postgresql + +import java.sql.Connection +import java.sql.SQLException +import java.time.OffsetDateTime + +const val booksByTags = """-- name: booksByTags :many +SELECT + book_id, + title, + name, + isbn, + tags +FROM books +LEFT JOIN authors ON books.author_id = authors.author_id +WHERE tags && ?::varchar[] +""" + +data class BooksByTagsRow ( + val bookId: Int, + val title: String, + val name: String, + val isbn: String, + val tags: Array +) + +const val booksByTitleYear = """-- name: booksByTitleYear :many +SELECT book_id, author_id, isbn, booktype, title, year, available, tags FROM books +WHERE title = ? AND year = ? +""" + +data class BooksByTitleYearParams ( + val title: String, + val year: Int +) + +const val createAuthor = """-- name: createAuthor :one +INSERT INTO authors (name) VALUES (?) +RETURNING author_id, name +""" + +const val createBook = """-- name: createBook :one +INSERT INTO books ( + author_id, + isbn, + booktype, + title, + year, + available, + tags +) VALUES ( + ?, + ?, + ?, + ?, + ?, + ?, + ? +) +RETURNING book_id, author_id, isbn, booktype, title, year, available, tags +""" + +data class CreateBookParams ( + val authorId: Int, + val isbn: String, + val booktype: BookType, + val title: String, + val year: Int, + val available: OffsetDateTime, + val tags: Array +) + +const val deleteBook = """-- name: deleteBook :exec +DELETE FROM books +WHERE book_id = ? +""" + +const val getAuthor = """-- name: getAuthor :one +SELECT author_id, name FROM authors +WHERE author_id = ? +""" + +const val getBook = """-- name: getBook :one +SELECT book_id, author_id, isbn, booktype, title, year, available, tags FROM books +WHERE book_id = ? +""" + +const val updateBook = """-- name: updateBook :exec +UPDATE books +SET title = ?, tags = ? +WHERE book_id = ? +""" + +data class UpdateBookParams ( + val title: String, + val tags: Array, + val bookId: Int +) + +const val updateBookISBN = """-- name: updateBookISBN :exec +UPDATE books +SET title = ?, tags = ?, isbn = ? +WHERE book_id = ? +""" + +data class UpdateBookISBNParams ( + val title: String, + val tags: Array, + val bookId: Int, + val isbn: String +) + +class QueriesImpl(private val conn: Connection) { + + @Throws(SQLException::class) + fun booksByTags(dollar_1: Array): List { + val stmt = conn.prepareStatement(booksByTags) + stmt.setArray(1, conn.createArrayOf("pg_catalog.varchar", dollar_1)) + + return stmt.executeQuery().use { results -> + val ret = mutableListOf() + while (results.next()) { + ret.add(BooksByTagsRow( + results.getInt(1), + results.getString(2), + results.getString(3), + results.getString(4), + results.getArray(5).array as Array + )) + } + ret + } + } + + @Throws(SQLException::class) + fun booksByTitleYear(arg: BooksByTitleYearParams): List { + val stmt = conn.prepareStatement(booksByTitleYear) + stmt.setString(1, arg.title) + stmt.setInt(2, arg.year) + + return stmt.executeQuery().use { results -> + val ret = mutableListOf() + while (results.next()) { + ret.add(Book( + results.getInt(1), + results.getInt(2), + results.getString(3), + BookType.valueOf(results.getString(4)), + results.getString(5), + results.getInt(6), + results.getObject(7, OffsetDateTime::class.java), + results.getArray(8).array as Array + )) + } + ret + } + } + + @Throws(SQLException::class) + fun createAuthor(name: String): Author { + val stmt = conn.prepareStatement(createAuthor) + stmt.setString(1, name) + + return stmt.executeQuery().use { results -> + if (!results.next()) { + throw SQLException("no rows in result set") + } + val ret = Author( + results.getInt(1), + results.getString(2) + ) + if (results.next()) { + throw SQLException("expected one row in result set, but got many") + } + ret + } + } + + @Throws(SQLException::class) + fun createBook(arg: CreateBookParams): Book { + val stmt = conn.prepareStatement(createBook) + stmt.setInt(1, arg.authorId) + stmt.setString(2, arg.isbn) + stmt.setString(3, arg.booktype.value) + stmt.setString(4, arg.title) + stmt.setInt(5, arg.year) + stmt.setObject(6, arg.available) + stmt.setArray(7, conn.createArrayOf("pg_catalog.varchar", arg.tags)) + + return stmt.executeQuery().use { results -> + if (!results.next()) { + throw SQLException("no rows in result set") + } + val ret = Book( + results.getInt(1), + results.getInt(2), + results.getString(3), + BookType.valueOf(results.getString(4)), + results.getString(5), + results.getInt(6), + results.getObject(7, OffsetDateTime::class.java), + results.getArray(8).array as Array + ) + if (results.next()) { + throw SQLException("expected one row in result set, but got many") + } + ret + } + } + + @Throws(SQLException::class) + fun deleteBook(bookId: Int) { + val stmt = conn.prepareStatement(deleteBook) + stmt.setInt(1, bookId) + + stmt.execute() + stmt.close() + } + + @Throws(SQLException::class) + fun getAuthor(authorId: Int): Author { + val stmt = conn.prepareStatement(getAuthor) + stmt.setInt(1, authorId) + + return stmt.executeQuery().use { results -> + if (!results.next()) { + throw SQLException("no rows in result set") + } + val ret = Author( + results.getInt(1), + results.getString(2) + ) + if (results.next()) { + throw SQLException("expected one row in result set, but got many") + } + ret + } + } + + @Throws(SQLException::class) + fun getBook(bookId: Int): Book { + val stmt = conn.prepareStatement(getBook) + stmt.setInt(1, bookId) + + return stmt.executeQuery().use { results -> + if (!results.next()) { + throw SQLException("no rows in result set") + } + val ret = Book( + results.getInt(1), + results.getInt(2), + results.getString(3), + BookType.valueOf(results.getString(4)), + results.getString(5), + results.getInt(6), + results.getObject(7, OffsetDateTime::class.java), + results.getArray(8).array as Array + ) + if (results.next()) { + throw SQLException("expected one row in result set, but got many") + } + ret + } + } + + @Throws(SQLException::class) + fun updateBook(arg: UpdateBookParams) { + val stmt = conn.prepareStatement(updateBook) + stmt.setString(1, arg.title) + stmt.setArray(2, conn.createArrayOf("pg_catalog.varchar", arg.tags)) + stmt.setInt(3, arg.bookId) + + stmt.execute() + stmt.close() + } + + @Throws(SQLException::class) + fun updateBookISBN(arg: UpdateBookISBNParams) { + val stmt = conn.prepareStatement(updateBookISBN) + stmt.setString(1, arg.title) + stmt.setArray(2, conn.createArrayOf("pg_catalog.varchar", arg.tags)) + stmt.setInt(3, arg.bookId) + stmt.setString(4, arg.isbn) + + stmt.execute() + stmt.close() + } + +} + diff --git a/examples/kotlin/src/main/kotlin/com/example/jets/Models.kt b/examples/kotlin/src/main/kotlin/com/example/jets/Models.kt new file mode 100644 index 0000000000..d2bced3778 --- /dev/null +++ b/examples/kotlin/src/main/kotlin/com/example/jets/Models.kt @@ -0,0 +1,27 @@ +// Code generated by sqlc. DO NOT EDIT. + +package com.example.jets + +data class Jet ( + val id: Int, + val pilotId: Int, + val age: Int, + val name: String, + val color: String +) + +data class Language ( + val id: Int, + val language: String +) + +data class Pilot ( + val id: Int, + val name: String +) + +data class PilotLanguage ( + val pilotId: Int, + val languageId: Int +) + diff --git a/examples/kotlin/src/main/kotlin/com/example/jets/QueriesImpl.kt b/examples/kotlin/src/main/kotlin/com/example/jets/QueriesImpl.kt new file mode 100644 index 0000000000..35f60b681c --- /dev/null +++ b/examples/kotlin/src/main/kotlin/com/example/jets/QueriesImpl.kt @@ -0,0 +1,64 @@ +// Code generated by sqlc. DO NOT EDIT. + +package com.example.jets + +import java.sql.Connection +import java.sql.SQLException + +const val countPilots = """-- name: countPilots :one +SELECT COUNT(*) FROM pilots +""" + +const val deletePilot = """-- name: deletePilot :exec +DELETE FROM pilots WHERE id = ? +""" + +const val listPilots = """-- name: listPilots :many +SELECT id, name FROM pilots LIMIT 5 +""" + +class QueriesImpl(private val conn: Connection) { + + @Throws(SQLException::class) + fun countPilots(): Long { + val stmt = conn.prepareStatement(countPilots) + + return stmt.executeQuery().use { results -> + if (!results.next()) { + throw SQLException("no rows in result set") + } + val ret = results.getLong(1) + if (results.next()) { + throw SQLException("expected one row in result set, but got many") + } + ret + } + } + + @Throws(SQLException::class) + fun deletePilot(id: Int) { + val stmt = conn.prepareStatement(deletePilot) + stmt.setInt(1, id) + + stmt.execute() + stmt.close() + } + + @Throws(SQLException::class) + fun listPilots(): List { + val stmt = conn.prepareStatement(listPilots) + + return stmt.executeQuery().use { results -> + val ret = mutableListOf() + while (results.next()) { + ret.add(Pilot( + results.getInt(1), + results.getString(2) + )) + } + ret + } + } + +} + diff --git a/examples/kotlin/src/main/kotlin/com/example/ondeck/Models.kt b/examples/kotlin/src/main/kotlin/com/example/ondeck/Models.kt new file mode 100644 index 0000000000..6b7f38d395 --- /dev/null +++ b/examples/kotlin/src/main/kotlin/com/example/ondeck/Models.kt @@ -0,0 +1,32 @@ +// Code generated by sqlc. DO NOT EDIT. + +package com.example.ondeck + +import java.time.LocalDateTime + +// Venues can be either open or closed +enum class Status(val value: String) { + OPEN("op!en"), + CLOSED("clo@sed") +} + +data class City ( + val slug: String, + val name: String +) + +// Venues are places where muisc happens +data class Venue ( + val id: Int, + val status: Status, + val statuses: Array, + // This value appears in public URLs + val slug: String, + val name: String, + val city: String, + val spotifyPlaylist: String, + val songkickId: String?, + val tags: Array, + val createdAt: LocalDateTime +) + diff --git a/examples/kotlin/src/main/kotlin/com/example/ondeck/Queries.kt b/examples/kotlin/src/main/kotlin/com/example/ondeck/Queries.kt new file mode 100644 index 0000000000..fa540fc82e --- /dev/null +++ b/examples/kotlin/src/main/kotlin/com/example/ondeck/Queries.kt @@ -0,0 +1,41 @@ +// Code generated by sqlc. DO NOT EDIT. + +package com.example.ondeck + +import java.sql.Connection +import java.sql.SQLException +import java.time.LocalDateTime + +interface Queries { + @Throws(SQLException::class) + fun createCity(arg: CreateCityParams): City + + @Throws(SQLException::class) + fun createVenue(arg: CreateVenueParams): Int + + @Throws(SQLException::class) + fun deleteVenue(slug: String) + + @Throws(SQLException::class) + fun getCity(slug: String): City + + @Throws(SQLException::class) + fun getVenue(arg: GetVenueParams): Venue + + @Throws(SQLException::class) + fun listCities(): List + + @Throws(SQLException::class) + fun listVenues(city: String): List + + @Throws(SQLException::class) + fun updateCityName(arg: UpdateCityNameParams) + + @Throws(SQLException::class) + fun updateVenueName(arg: UpdateVenueNameParams): Int + + @Throws(SQLException::class) + fun venueCountByCity(): List + +} + diff --git a/examples/kotlin/src/main/kotlin/com/example/ondeck/QueriesImpl.kt b/examples/kotlin/src/main/kotlin/com/example/ondeck/QueriesImpl.kt new file mode 100644 index 0000000000..4da97fec32 --- /dev/null +++ b/examples/kotlin/src/main/kotlin/com/example/ondeck/QueriesImpl.kt @@ -0,0 +1,322 @@ +// Code generated by sqlc. DO NOT EDIT. + +package com.example.ondeck + +import java.sql.Connection +import java.sql.SQLException +import java.time.LocalDateTime + +const val createCity = """-- name: createCity :one +INSERT INTO city ( + name, + slug +) VALUES ( + ?, + ? +) RETURNING slug, name +""" + +data class CreateCityParams ( + val name: String, + val slug: String +) + +const val createVenue = """-- name: createVenue :one +INSERT INTO venue ( + slug, + name, + city, + created_at, + spotify_playlist, + status, + statuses, + tags +) VALUES ( + ?, + ?, + ?, + NOW(), + ?, + ?, + ?, + ? +) RETURNING id +""" + +data class CreateVenueParams ( + val slug: String, + val name: String, + val city: String, + val spotifyPlaylist: String, + val status: Status, + val statuses: Array, + val tags: Array +) + +const val deleteVenue = """-- name: deleteVenue :exec +DELETE FROM venue +WHERE slug = ? AND slug = ? +""" + +const val getCity = """-- name: getCity :one +SELECT slug, name +FROM city +WHERE slug = ? +""" + +const val getVenue = """-- name: getVenue :one +SELECT id, status, statuses, slug, name, city, spotify_playlist, songkick_id, tags, created_at +FROM venue +WHERE slug = ? AND city = ? +""" + +data class GetVenueParams ( + val slug: String, + val city: String +) + +const val listCities = """-- name: listCities :many +SELECT slug, name +FROM city +ORDER BY name +""" + +const val listVenues = """-- name: listVenues :many +SELECT id, status, statuses, slug, name, city, spotify_playlist, songkick_id, tags, created_at +FROM venue +WHERE city = ? +ORDER BY name +""" + +const val updateCityName = """-- name: updateCityName :exec +UPDATE city +SET name = ? +WHERE slug = ? +""" + +data class UpdateCityNameParams ( + val slug: String, + val name: String +) + +const val updateVenueName = """-- name: updateVenueName :one +UPDATE venue +SET name = ? +WHERE slug = ? +RETURNING id +""" + +data class UpdateVenueNameParams ( + val slug: String, + val name: String +) + +const val venueCountByCity = """-- name: venueCountByCity :many +SELECT + city, + count(*) +FROM venue +GROUP BY 1 +ORDER BY 1 +""" + +data class VenueCountByCityRow ( + val city: String, + val count: Long +) + +class QueriesImpl(private val conn: Connection) : Queries { + +// Create a new city. The slug must be unique. +// This is the second line of the comment +// This is the third line + + @Throws(SQLException::class) + override fun createCity(arg: CreateCityParams): City { + val stmt = conn.prepareStatement(createCity) + stmt.setString(1, arg.name) + stmt.setString(2, arg.slug) + + return stmt.executeQuery().use { results -> + if (!results.next()) { + throw SQLException("no rows in result set") + } + val ret = City( + results.getString(1), + results.getString(2) + ) + if (results.next()) { + throw SQLException("expected one row in result set, but got many") + } + ret + } + } + + @Throws(SQLException::class) + override fun createVenue(arg: CreateVenueParams): Int { + val stmt = conn.prepareStatement(createVenue) + stmt.setString(1, arg.slug) + stmt.setString(2, arg.name) + stmt.setString(3, arg.city) + stmt.setString(4, arg.spotifyPlaylist) + stmt.setString(5, arg.status.value) + stmt.setArray(6, conn.createArrayOf("status", arg.statuses.map { v -> v.value }.toTypedArray())) + stmt.setArray(7, conn.createArrayOf("text", arg.tags)) + + return stmt.executeQuery().use { results -> + if (!results.next()) { + throw SQLException("no rows in result set") + } + val ret = results.getInt(1) + if (results.next()) { + throw SQLException("expected one row in result set, but got many") + } + ret + } + } + + @Throws(SQLException::class) + override fun deleteVenue(slug: String) { + val stmt = conn.prepareStatement(deleteVenue) + stmt.setString(1, slug) + + stmt.execute() + stmt.close() + } + + @Throws(SQLException::class) + override fun getCity(slug: String): City { + val stmt = conn.prepareStatement(getCity) + stmt.setString(1, slug) + + return stmt.executeQuery().use { results -> + if (!results.next()) { + throw SQLException("no rows in result set") + } + val ret = City( + results.getString(1), + results.getString(2) + ) + if (results.next()) { + throw SQLException("expected one row in result set, but got many") + } + ret + } + } + + @Throws(SQLException::class) + override fun getVenue(arg: GetVenueParams): Venue { + val stmt = conn.prepareStatement(getVenue) + stmt.setString(1, arg.slug) + stmt.setString(2, arg.city) + + return stmt.executeQuery().use { results -> + if (!results.next()) { + throw SQLException("no rows in result set") + } + val ret = Venue( + results.getInt(1), + Status.valueOf(results.getString(2)), + (results.getArray(3).array as Array).map { v -> Status.valueOf(v) }.toTypedArray(), + results.getString(4), + results.getString(5), + results.getString(6), + results.getString(7), + results.getString(8), + results.getArray(9).array as Array, + results.getObject(10, LocalDateTime::class.java) + ) + if (results.next()) { + throw SQLException("expected one row in result set, but got many") + } + ret + } + } + + @Throws(SQLException::class) + override fun listCities(): List { + val stmt = conn.prepareStatement(listCities) + + return stmt.executeQuery().use { results -> + val ret = mutableListOf() + while (results.next()) { + ret.add(City( + results.getString(1), + results.getString(2) + )) + } + ret + } + } + + @Throws(SQLException::class) + override fun listVenues(city: String): List { + val stmt = conn.prepareStatement(listVenues) + stmt.setString(1, city) + + return stmt.executeQuery().use { results -> + val ret = mutableListOf() + while (results.next()) { + ret.add(Venue( + results.getInt(1), + Status.valueOf(results.getString(2)), + (results.getArray(3).array as Array).map { v -> Status.valueOf(v) }.toTypedArray(), + results.getString(4), + results.getString(5), + results.getString(6), + results.getString(7), + results.getString(8), + results.getArray(9).array as Array, + results.getObject(10, LocalDateTime::class.java) + )) + } + ret + } + } + + @Throws(SQLException::class) + override fun updateCityName(arg: UpdateCityNameParams) { + val stmt = conn.prepareStatement(updateCityName) + stmt.setString(1, arg.slug) + stmt.setString(2, arg.name) + + stmt.execute() + stmt.close() + } + + @Throws(SQLException::class) + override fun updateVenueName(arg: UpdateVenueNameParams): Int { + val stmt = conn.prepareStatement(updateVenueName) + stmt.setString(1, arg.slug) + stmt.setString(2, arg.name) + + return stmt.executeQuery().use { results -> + if (!results.next()) { + throw SQLException("no rows in result set") + } + val ret = results.getInt(1) + if (results.next()) { + throw SQLException("expected one row in result set, but got many") + } + ret + } + } + + @Throws(SQLException::class) + override fun venueCountByCity(): List { + val stmt = conn.prepareStatement(venueCountByCity) + + return stmt.executeQuery().use { results -> + val ret = mutableListOf() + while (results.next()) { + ret.add(VenueCountByCityRow( + results.getString(1), + results.getLong(2) + )) + } + ret + } + } + +} + diff --git a/examples/kotlin/src/main/resources/query.sql b/examples/kotlin/src/main/resources/query.sql new file mode 100644 index 0000000000..75e38b2caf --- /dev/null +++ b/examples/kotlin/src/main/resources/query.sql @@ -0,0 +1,19 @@ +-- name: GetAuthor :one +SELECT * FROM authors +WHERE id = $1 LIMIT 1; + +-- name: ListAuthors :many +SELECT * FROM authors +ORDER BY name; + +-- name: CreateAuthor :one +INSERT INTO authors ( + name, bio +) VALUES ( + $1, $2 +) +RETURNING *; + +-- name: DeleteAuthor :exec +DELETE FROM authors +WHERE id = $1; diff --git a/examples/kotlin/src/main/resources/schema.sql b/examples/kotlin/src/main/resources/schema.sql new file mode 100644 index 0000000000..b4fad78497 --- /dev/null +++ b/examples/kotlin/src/main/resources/schema.sql @@ -0,0 +1,5 @@ +CREATE TABLE authors ( + id BIGSERIAL PRIMARY KEY, + name text NOT NULL, + bio text +); diff --git a/examples/kotlin/src/test/kotlin/com/example/authors/QueriesImplTest.kt b/examples/kotlin/src/test/kotlin/com/example/authors/QueriesImplTest.kt new file mode 100644 index 0000000000..5bb4994848 --- /dev/null +++ b/examples/kotlin/src/test/kotlin/com/example/authors/QueriesImplTest.kt @@ -0,0 +1,86 @@ +package com.example.authors + +import org.junit.jupiter.api.AfterEach +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import java.nio.file.Files +import java.nio.file.Paths +import java.sql.Connection +import java.sql.DriverManager + +const val schema = "dinosql_test" + +class QueriesImplTest { + lateinit var schemaConn: Connection + lateinit var conn: Connection + + @BeforeEach + fun setup() { + val user = System.getenv("PG_USER") ?: "postgres" + val port = System.getenv("PG_PORT") ?: "5432" + val host = System.getenv("PG_HOST") ?: "127.0.0.1" + val db = System.getenv("PG_DB") ?: "postgres" + val pass = System.getenv("PG_PASS") ?: "postgres" + val url = "jdbc:postgresql://$host:$port/$db?user=$user&password=$pass&sslmode=disable" + println("db: $url") + + schemaConn = DriverManager.getConnection(url) + schemaConn.createStatement().execute("CREATE SCHEMA $schema") + + conn = DriverManager.getConnection("$url¤tSchema=$schema") + val stmt = Files.readString(Paths.get("src/main/resources/schema.sql")) + conn.createStatement().execute(stmt) + } + + @AfterEach + fun teardown() { + schemaConn.createStatement().execute("DROP SCHEMA $schema CASCADE") + } + + @Test + fun testCreateAuthor() { + val db = QueriesImpl(conn) + + val initialAuthors = db.listAuthors() + assert(initialAuthors.isEmpty()) + + val params = CreateAuthorParams( + name = "Brian Kernighan", + bio = "Co-author of The C Programming Language and The Go Programming Language" + ) + val insertedAuthor = db.createAuthor(params) + val expectedAuthor = Author(insertedAuthor.id, params.name, params.bio) + assertEquals(expectedAuthor, insertedAuthor) + + val fetchedAuthor = db.getAuthor(insertedAuthor.id) + assertEquals(expectedAuthor, fetchedAuthor) + + val listedAuthors = db.listAuthors() + assertEquals(1, listedAuthors.size) + assertEquals(expectedAuthor, listedAuthors[0]) + } + + @Test + fun testNull() { + val db = QueriesImpl(conn) + + val initialAuthors = db.listAuthors() + assert(initialAuthors.isEmpty()) + + val params = CreateAuthorParams( + name = "Brian Kernighan", + bio = null + ) + val insertedAuthor = db.createAuthor(params) + val expectedAuthor = Author(insertedAuthor.id, params.name, params.bio) + assertEquals(expectedAuthor, insertedAuthor) + + val fetchedAuthor = db.getAuthor(insertedAuthor.id) + assertEquals(expectedAuthor, fetchedAuthor) + + val listedAuthors = db.listAuthors() + assertEquals(1, listedAuthors.size) + assertEquals(expectedAuthor, listedAuthors[0]) + } +} diff --git a/examples/sqlc.json b/examples/sqlc.json index 9662784e93..98f3d5fdac 100644 --- a/examples/sqlc.json +++ b/examples/sqlc.json @@ -35,6 +35,39 @@ "schema": "booktest/mysql/schema.sql", "queries": "booktest/mysql/query.sql", "engine": "mysql" + }, + { + "name": "com.example.authors", + "path": "kotlin/src/main/kotlin/com/example/authors", + "schema": "kotlin/src/main/resources/schema.sql", + "queries": "kotlin/src/main/resources/query.sql", + "engine": "postgresql", + "language": "kotlin" + }, + { + "name": "com.example.ondeck", + "path": "kotlin/src/main/kotlin/com/example/ondeck", + "schema": "ondeck/schema", + "queries": "ondeck/query", + "engine": "postgresql", + "emit_interface": true, + "language": "kotlin" + }, + { + "name": "com.example.jets", + "path": "kotlin/src/main/kotlin/com/example/jets", + "schema": "jets/schema.sql", + "queries": "jets/query-building.sql", + "engine": "postgresql", + "language": "kotlin" + }, + { + "name": "com.example.booktest.postgresql", + "path": "kotlin/src/main/kotlin/com/example/booktest/postgresql", + "schema": "booktest/postgresql/schema.sql", + "queries": "booktest/postgresql/query.sql", + "engine": "postgresql", + "language": "kotlin" } ] } diff --git a/internal/cmd/cmd.go b/internal/cmd/cmd.go index 487deb07ef..3e4f7fa301 100644 --- a/internal/cmd/cmd.go +++ b/internal/cmd/cmd.go @@ -9,11 +9,11 @@ import ( "os/exec" "path/filepath" - "github.com/kyleconroy/sqlc/internal/dinosql" - "github.com/davecgh/go-spew/spew" pg "github.com/lfittl/pg_query_go" "github.com/spf13/cobra" + + "github.com/kyleconroy/sqlc/internal/dinosql" ) // Do runs the command logic. diff --git a/internal/cmd/generate.go b/internal/cmd/generate.go index 2ff3101cf0..cc3ad0769f 100644 --- a/internal/cmd/generate.go +++ b/internal/cmd/generate.go @@ -117,7 +117,7 @@ func Generate(dir string, stderr io.Writer) (map[string]string, error) { case dinosql.LanguageKotlin: ktRes, ok := result.(dinosql.KtGenerateable) if !ok { - err = fmt.Errorf("Kotlin not supported") + err = fmt.Errorf("kotlin not supported for engine %s", pkg.Engine) break } files, err = dinosql.KtGenerate(ktRes, combo) From 5a3c1549050d2e84b61d78e88231674395151981 Mon Sep 17 00:00:00 2001 From: Yunchi Luo Date: Sat, 25 Jan 2020 22:07:35 -0500 Subject: [PATCH 05/20] kotlin: README for examples --- examples/kotlin/README.md | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 examples/kotlin/README.md diff --git a/examples/kotlin/README.md b/examples/kotlin/README.md new file mode 100644 index 0000000000..ed910aa966 --- /dev/null +++ b/examples/kotlin/README.md @@ -0,0 +1,17 @@ +# Kotlin examples + +This is a Kotlin gradle project configured to compile and test all examples. Currently tests have only been written for the `authors` example. + +To run tests: + +```shell script +./gradlew clean test +``` + +The project can be easily imported into Intellij. + +1. Install Java if you don't already have it +1. Download Intellij IDEA Community Edition +1. In the "Welcome" modal, click "Import Project" +1. Open the `build.gradle` file adjacent to this README file +1. Wait for Intellij to sync the gradle modules and complete indexing From d636b0fb43d0ebea2759cf136c4148e3080d7909 Mon Sep 17 00:00:00 2001 From: Yunchi Luo Date: Sun, 26 Jan 2020 11:08:38 -0500 Subject: [PATCH 06/20] kotlin CI (#2) --- .github/workflows/ci-kotlin.yml | 36 +++++++++++++++++++ examples/kotlin/build.gradle | 4 ++- .../com/example/authors/QueriesImplTest.kt | 6 ++-- internal/endtoend/endtoend_test.go | 4 +-- internal/sqltest/postgres.go | 4 +++ 5 files changed, 48 insertions(+), 6 deletions(-) create mode 100644 .github/workflows/ci-kotlin.yml diff --git a/.github/workflows/ci-kotlin.yml b/.github/workflows/ci-kotlin.yml new file mode 100644 index 0000000000..f864c2fc7e --- /dev/null +++ b/.github/workflows/ci-kotlin.yml @@ -0,0 +1,36 @@ +name: sqlc kotlin test suite +on: [push, pull_request] +jobs: + + build: + name: Build And Test + runs-on: ubuntu-latest + + services: + postgres: + image: postgres:11 + env: + POSTGRES_USER: postgres + POSTGRES_PASSWORD: postgres + POSTGRES_DB: postgres + ports: + - 5432:5432 + # needed because the postgres container does not provide a healthcheck + options: --health-cmd pg_isready --health-interval 10s --health-timeout 5s --health-retries 5 + + steps: + - uses: actions/checkout@master + - uses: actions/setup-java@v1 + with: + java-version: 11 + - uses: eskatos/gradle-command-action@v1 + env: + PG_USER: postgres + PG_HOST: localhost + PG_DATABASE: postgres + PG_PASSWORD: postgres + PG_PORT: ${{ job.services.postgres.ports['5432'] }} + with: + build-root-directory: examples/kotlin + wrapper-directory: examples/kotlin + arguments: test --scan diff --git a/examples/kotlin/build.gradle b/examples/kotlin/build.gradle index f20b3ae51a..7f4477e225 100644 --- a/examples/kotlin/build.gradle +++ b/examples/kotlin/build.gradle @@ -25,4 +25,6 @@ compileKotlin { } compileTestKotlin { kotlinOptions.jvmTarget = "1.8" -} \ No newline at end of file +} + +buildScan { termsOfServiceUrl = "https://gradle.com/terms-of-service"; termsOfServiceAgree = "yes" } \ No newline at end of file diff --git a/examples/kotlin/src/test/kotlin/com/example/authors/QueriesImplTest.kt b/examples/kotlin/src/test/kotlin/com/example/authors/QueriesImplTest.kt index 5bb4994848..fb0ce85904 100644 --- a/examples/kotlin/src/test/kotlin/com/example/authors/QueriesImplTest.kt +++ b/examples/kotlin/src/test/kotlin/com/example/authors/QueriesImplTest.kt @@ -18,10 +18,10 @@ class QueriesImplTest { @BeforeEach fun setup() { val user = System.getenv("PG_USER") ?: "postgres" - val port = System.getenv("PG_PORT") ?: "5432" + val pass = System.getenv("PG_PASSWORD") ?: "mysecretpassword" val host = System.getenv("PG_HOST") ?: "127.0.0.1" - val db = System.getenv("PG_DB") ?: "postgres" - val pass = System.getenv("PG_PASS") ?: "postgres" + val port = System.getenv("PG_PORT") ?: "5432" + val db = System.getenv("PG_DATABASE") ?: "dinotest" val url = "jdbc:postgresql://$host:$port/$db?user=$user&password=$pass&sslmode=disable" println("db: $url") diff --git a/internal/endtoend/endtoend_test.go b/internal/endtoend/endtoend_test.go index ac69188b04..abccbc3cc1 100644 --- a/internal/endtoend/endtoend_test.go +++ b/internal/endtoend/endtoend_test.go @@ -58,10 +58,10 @@ func cmpDirectory(t *testing.T, dir string, actual map[string]string) { if file.IsDir() { return nil } - if !strings.HasSuffix(path, ".go") { + if !strings.HasSuffix(path, ".go") && !strings.HasSuffix(path, ".kt") { return nil } - if strings.HasSuffix(path, "_test.go") { + if strings.HasSuffix(path, "_test.go") || strings.Contains(path, "src/test/") { return nil } blob, err := ioutil.ReadFile(path) diff --git a/internal/sqltest/postgres.go b/internal/sqltest/postgres.go index be86df4e65..5a283ac33a 100644 --- a/internal/sqltest/postgres.go +++ b/internal/sqltest/postgres.go @@ -42,6 +42,10 @@ func PostgreSQL(t *testing.T, migrations string) (*sql.DB, func()) { pgUser = "postgres" } + if pgPass == "" { + pgPass = "mysecretpassword" + } + if pgPort == "" { pgPort = "5432" } From c7801bea5595df9daec7debce8c5dcb799c77f00 Mon Sep 17 00:00:00 2001 From: Yunchi Luo Date: Sun, 26 Jan 2020 12:43:27 -0500 Subject: [PATCH 07/20] move ktgen.go to kotlin/gen.go (#3) --- internal/cmd/generate.go | 7 ++- internal/dinosql/{ktgen.go => kotlin/gen.go} | 59 +++++++++++--------- 2 files changed, 38 insertions(+), 28 deletions(-) rename internal/dinosql/{ktgen.go => kotlin/gen.go} (90%) diff --git a/internal/cmd/generate.go b/internal/cmd/generate.go index cc3ad0769f..1b8e592c4e 100644 --- a/internal/cmd/generate.go +++ b/internal/cmd/generate.go @@ -8,6 +8,7 @@ import ( "path/filepath" "github.com/kyleconroy/sqlc/internal/dinosql" + "github.com/kyleconroy/sqlc/internal/dinosql/kotlin" "github.com/kyleconroy/sqlc/internal/mysql" ) @@ -106,7 +107,7 @@ func Generate(dir string, stderr io.Writer) (map[string]string, error) { errored = true continue } - result = q + result = &kotlin.Result{Result: q} } @@ -115,12 +116,12 @@ func Generate(dir string, stderr io.Writer) (map[string]string, error) { case dinosql.LanguageGo: files, err = dinosql.Generate(result, combo) case dinosql.LanguageKotlin: - ktRes, ok := result.(dinosql.KtGenerateable) + ktRes, ok := result.(kotlin.KtGenerateable) if !ok { err = fmt.Errorf("kotlin not supported for engine %s", pkg.Engine) break } - files, err = dinosql.KtGenerate(ktRes, combo) + files, err = kotlin.KtGenerate(ktRes, combo) } if err != nil { fmt.Fprintf(stderr, "# package %s\n", name) diff --git a/internal/dinosql/ktgen.go b/internal/dinosql/kotlin/gen.go similarity index 90% rename from internal/dinosql/ktgen.go rename to internal/dinosql/kotlin/gen.go index dd9126faac..c7947bf71d 100644 --- a/internal/dinosql/ktgen.go +++ b/internal/dinosql/kotlin/gen.go @@ -1,4 +1,4 @@ -package dinosql +package kotlin import ( "bufio" @@ -10,6 +10,7 @@ import ( "strings" "text/template" + "github.com/kyleconroy/sqlc/internal/dinosql" core "github.com/kyleconroy/sqlc/internal/pg" "github.com/jinzhu/inflection" @@ -137,12 +138,12 @@ type KtQuery struct { } type KtGenerateable interface { - KtDataClasses(settings CombinedSettings) []KtStruct - KtQueries(settings CombinedSettings) []KtQuery - KtEnums(settings CombinedSettings) []KtEnum + KtDataClasses(settings dinosql.CombinedSettings) []KtStruct + KtQueries(settings dinosql.CombinedSettings) []KtQuery + KtEnums(settings dinosql.CombinedSettings) []KtEnum } -func KtUsesType(r KtGenerateable, typ string, settings CombinedSettings) bool { +func KtUsesType(r KtGenerateable, typ string, settings dinosql.CombinedSettings) bool { for _, strct := range r.KtDataClasses(settings) { for _, f := range strct.Fields { if f.Type.Name == typ { @@ -153,7 +154,7 @@ func KtUsesType(r KtGenerateable, typ string, settings CombinedSettings) bool { return false } -func KtImports(r KtGenerateable, settings CombinedSettings) func(string) [][]string { +func KtImports(r KtGenerateable, settings dinosql.CombinedSettings) func(string) [][]string { return func(filename string) [][]string { if filename == "Models.kt" { return ModelKtImports(r, settings) @@ -167,7 +168,7 @@ func KtImports(r KtGenerateable, settings CombinedSettings) func(string) [][]str } } -func InterfaceKtImports(r KtGenerateable, settings CombinedSettings) [][]string { +func InterfaceKtImports(r KtGenerateable, settings dinosql.CombinedSettings) [][]string { gq := r.KtQueries(settings) uses := func(name string) bool { for _, q := range gq { @@ -211,7 +212,7 @@ func InterfaceKtImports(r KtGenerateable, settings CombinedSettings) [][]string return [][]string{stds} } -func ModelKtImports(r KtGenerateable, settings CombinedSettings) [][]string { +func ModelKtImports(r KtGenerateable, settings dinosql.CombinedSettings) [][]string { std := make(map[string]struct{}) if KtUsesType(r, "LocalDate", settings) { std["java.time.LocalDate"] = struct{}{} @@ -235,7 +236,7 @@ func ModelKtImports(r KtGenerateable, settings CombinedSettings) [][]string { return [][]string{stds} } -func QueryKtImports(r KtGenerateable, settings CombinedSettings, filename string) [][]string { +func QueryKtImports(r KtGenerateable, settings dinosql.CombinedSettings, filename string) [][]string { // for _, strct := range r.KtDataClasses() { // for _, f := range strct.Fields { // if strings.HasPrefix(f.Type, "[]") { @@ -320,7 +321,15 @@ func ktEnumValueName(value string) string { return strings.ToUpper(id) } -func (r Result) KtEnums(settings CombinedSettings) []KtEnum { +// Result is a wrapper around *dinosql.Result that extends it with Kotlin support. +// It can be used to generate both Go and Kotlin code. +// TODO: This is a temporary hack to ensure minimal chance of merge conflicts while Kotlin support is forked. +// Once it is merged upstream, we can factor split out Go support from the core dinosql.Result. +type Result struct { + *dinosql.Result +} + +func (r Result) KtEnums(settings dinosql.CombinedSettings) []KtEnum { var enums []KtEnum for name, schema := range r.Catalog.Schemas { if name == "pg_catalog" { @@ -353,7 +362,7 @@ func (r Result) KtEnums(settings CombinedSettings) []KtEnum { return enums } -func KtDataClassName(name string, settings CombinedSettings) string { +func KtDataClassName(name string, settings dinosql.CombinedSettings) string { if rename := settings.Global.Rename[name]; rename != "" { return rename } @@ -364,11 +373,11 @@ func KtDataClassName(name string, settings CombinedSettings) string { return out } -func KtMemberName(name string, settings CombinedSettings) string { - return LowerTitle(KtDataClassName(name, settings)) +func KtMemberName(name string, settings dinosql.CombinedSettings) string { + return dinosql.LowerTitle(KtDataClassName(name, settings)) } -func (r Result) KtDataClasses(settings CombinedSettings) []KtStruct { +func (r Result) KtDataClasses(settings dinosql.CombinedSettings) []KtStruct { var structs []KtStruct for name, schema := range r.Catalog.Schemas { if name == "pg_catalog" { @@ -475,7 +484,7 @@ func (t ktType) fromJDBCValue(expr string) string { return expr } -func (r Result) ktType(col core.Column, settings CombinedSettings) ktType { +func (r Result) ktType(col core.Column, settings dinosql.CombinedSettings) ktType { typ, isEnum := r.ktInnerType(col, settings) return ktType{ Name: typ, @@ -486,7 +495,7 @@ func (r Result) ktType(col core.Column, settings CombinedSettings) ktType { } } -func (r Result) ktInnerType(col core.Column, settings CombinedSettings) (string, bool) { +func (r Result) ktInnerType(col core.Column, settings dinosql.CombinedSettings) (string, bool) { columnType := col.DataType switch columnType { @@ -582,7 +591,7 @@ func (r Result) ktInnerType(col core.Column, settings CombinedSettings) (string, } } -func (r Result) ktColumnsToStruct(name string, columns []core.Column, settings CombinedSettings) *KtStruct { +func (r Result) ktColumnsToStruct(name string, columns []core.Column, settings dinosql.CombinedSettings) *KtStruct { gs := KtStruct{ Name: name, } @@ -613,7 +622,7 @@ func ktArgName(name string) string { return out } -func ktParamName(p Parameter) string { +func ktParamName(p dinosql.Parameter) string { if p.Column.Name != "" { return ktArgName(p.Column.Name) } @@ -636,7 +645,7 @@ func jdbcSQL(s string) string { return jdbcSQLRe.ReplaceAllString(s, "?") } -func (r Result) KtQueries(settings CombinedSettings) []KtQuery { +func (r Result) KtQueries(settings dinosql.CombinedSettings) []KtQuery { structs := r.KtDataClasses(settings) qs := make([]KtQuery, 0, len(r.Queries)) @@ -651,9 +660,9 @@ func (r Result) KtQueries(settings CombinedSettings) []KtQuery { gq := KtQuery{ Cmd: query.Cmd, ClassName: strings.Title(query.Name), - ConstantName: LowerTitle(query.Name), - FieldName: LowerTitle(query.Name) + "Stmt", - MethodName: LowerTitle(query.Name), + ConstantName: dinosql.LowerTitle(query.Name), + FieldName: dinosql.LowerTitle(query.Name) + "Stmt", + MethodName: dinosql.LowerTitle(query.Name), SourceName: query.Filename, SQL: jdbcSQL(query.SQL), Comments: query.Comments, @@ -898,7 +907,7 @@ type ktTmplCtx struct { Enums []KtEnum KtDataClasses []KtStruct KtQueries []KtQuery - Settings GenerateSettings + Settings dinosql.GenerateSettings // TODO: Race conditions SourceName string @@ -928,9 +937,9 @@ func ktFormat(s string) string { return o } -func KtGenerate(r KtGenerateable, settings CombinedSettings) (map[string]string, error) { +func KtGenerate(r KtGenerateable, settings dinosql.CombinedSettings) (map[string]string, error) { funcMap := template.FuncMap{ - "lowerTitle": LowerTitle, + "lowerTitle": dinosql.LowerTitle, "imports": KtImports(r, settings), "offset": Offset, } From f1f5a41ba54fc97ff15ea7cecbc49fc71a95c525 Mon Sep 17 00:00:00 2001 From: Yunchi Luo Date: Sun, 26 Jan 2020 17:50:26 -0500 Subject: [PATCH 08/20] upgrade to gradle 6.1.1 --- examples/kotlin/build.gradle | 5 ++++- examples/kotlin/gradle/wrapper/gradle-wrapper.properties | 2 +- examples/kotlin/settings.gradle | 5 ++++- 3 files changed, 9 insertions(+), 3 deletions(-) diff --git a/examples/kotlin/build.gradle b/examples/kotlin/build.gradle index 7f4477e225..fd331077ae 100644 --- a/examples/kotlin/build.gradle +++ b/examples/kotlin/build.gradle @@ -27,4 +27,7 @@ compileTestKotlin { kotlinOptions.jvmTarget = "1.8" } -buildScan { termsOfServiceUrl = "https://gradle.com/terms-of-service"; termsOfServiceAgree = "yes" } \ No newline at end of file +buildScan { + termsOfServiceUrl = "https://gradle.com/terms-of-service" + termsOfServiceAgree = "yes" +} diff --git a/examples/kotlin/gradle/wrapper/gradle-wrapper.properties b/examples/kotlin/gradle/wrapper/gradle-wrapper.properties index 3cdb37dd29..b5354905d6 100644 --- a/examples/kotlin/gradle/wrapper/gradle-wrapper.properties +++ b/examples/kotlin/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ #Sat Jan 25 10:45:34 EST 2020 -distributionUrl=https\://services.gradle.org/distributions/gradle-5.2.1-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-6.1.1-all.zip distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStorePath=wrapper/dists diff --git a/examples/kotlin/settings.gradle b/examples/kotlin/settings.gradle index d51bf78f7f..5a094655c6 100644 --- a/examples/kotlin/settings.gradle +++ b/examples/kotlin/settings.gradle @@ -1,2 +1,5 @@ -rootProject.name = 'dbtest' +plugins { + id("com.gradle.enterprise").version("3.1.1") +} +rootProject.name = 'dbtest' From d17db0e668bd7edad98c9ef6f1012feae519d330 Mon Sep 17 00:00:00 2001 From: Yunchi Luo Date: Sun, 26 Jan 2020 18:13:01 -0500 Subject: [PATCH 09/20] kotlin: factor out db setup extension --- .../com/example/authors/QueriesImplTest.kt | 36 ++----------- .../com/example/dbtest/DbTestExtension.kt | 51 +++++++++++++++++++ 2 files changed, 56 insertions(+), 31 deletions(-) create mode 100644 examples/kotlin/src/test/kotlin/com/example/dbtest/DbTestExtension.kt diff --git a/examples/kotlin/src/test/kotlin/com/example/authors/QueriesImplTest.kt b/examples/kotlin/src/test/kotlin/com/example/authors/QueriesImplTest.kt index fb0ce85904..fd52cc78e9 100644 --- a/examples/kotlin/src/test/kotlin/com/example/authors/QueriesImplTest.kt +++ b/examples/kotlin/src/test/kotlin/com/example/authors/QueriesImplTest.kt @@ -1,41 +1,15 @@ package com.example.authors -import org.junit.jupiter.api.AfterEach +import com.example.dbtest.DbTestExtension import org.junit.jupiter.api.Assertions.assertEquals -import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test -import java.nio.file.Files -import java.nio.file.Paths +import org.junit.jupiter.api.extension.RegisterExtension import java.sql.Connection -import java.sql.DriverManager -const val schema = "dinosql_test" +class QueriesImplTest(private val conn: Connection) { -class QueriesImplTest { - lateinit var schemaConn: Connection - lateinit var conn: Connection - - @BeforeEach - fun setup() { - val user = System.getenv("PG_USER") ?: "postgres" - val pass = System.getenv("PG_PASSWORD") ?: "mysecretpassword" - val host = System.getenv("PG_HOST") ?: "127.0.0.1" - val port = System.getenv("PG_PORT") ?: "5432" - val db = System.getenv("PG_DATABASE") ?: "dinotest" - val url = "jdbc:postgresql://$host:$port/$db?user=$user&password=$pass&sslmode=disable" - println("db: $url") - - schemaConn = DriverManager.getConnection(url) - schemaConn.createStatement().execute("CREATE SCHEMA $schema") - - conn = DriverManager.getConnection("$url¤tSchema=$schema") - val stmt = Files.readString(Paths.get("src/main/resources/schema.sql")) - conn.createStatement().execute(stmt) - } - - @AfterEach - fun teardown() { - schemaConn.createStatement().execute("DROP SCHEMA $schema CASCADE") + companion object { + @JvmField @RegisterExtension val db = DbTestExtension("src/main/resources/schema.sql") } @Test diff --git a/examples/kotlin/src/test/kotlin/com/example/dbtest/DbTestExtension.kt b/examples/kotlin/src/test/kotlin/com/example/dbtest/DbTestExtension.kt new file mode 100644 index 0000000000..4e5c01ffb9 --- /dev/null +++ b/examples/kotlin/src/test/kotlin/com/example/dbtest/DbTestExtension.kt @@ -0,0 +1,51 @@ +package com.example.dbtest + +import org.junit.jupiter.api.extension.AfterEachCallback +import org.junit.jupiter.api.extension.BeforeEachCallback +import org.junit.jupiter.api.extension.ExtensionContext +import org.junit.jupiter.api.extension.ParameterContext +import org.junit.jupiter.api.extension.ParameterResolver +import java.nio.file.Files +import java.nio.file.Paths +import java.sql.Connection +import java.sql.DriverManager + +const val schema = "dinosql_test" + +class DbTestExtension(private val migrationsPath: String) : BeforeEachCallback, AfterEachCallback, ParameterResolver { + private val schemaConn: Connection + private val url: String + + init { + val user = System.getenv("PG_USER") ?: "postgres" + val pass = System.getenv("PG_PASSWORD") ?: "mysecretpassword" + val host = System.getenv("PG_HOST") ?: "127.0.0.1" + val port = System.getenv("PG_PORT") ?: "5432" + val db = System.getenv("PG_DATABASE") ?: "dinotest" + url = "jdbc:postgresql://$host:$port/$db?user=$user&password=$pass&sslmode=disable" + + schemaConn = DriverManager.getConnection(url) + } + + override fun beforeEach(context: ExtensionContext) { + schemaConn.createStatement().execute("CREATE SCHEMA $schema") + val stmt = Files.readString(Paths.get(migrationsPath)) + getConnection().createStatement().execute(stmt) + } + + override fun afterEach(context: ExtensionContext) { + schemaConn.createStatement().execute("DROP SCHEMA $schema CASCADE") + } + + private fun getConnection(): Connection { + return DriverManager.getConnection("$url¤tSchema=$schema") + } + + override fun supportsParameter(parameterContext: ParameterContext, extensionContext: ExtensionContext): Boolean { + return parameterContext.parameter.type == Connection::class.java + } + + override fun resolveParameter(parameterContext: ParameterContext, extensionContext: ExtensionContext): Any { + return getConnection() + } +} \ No newline at end of file From de9ede20c5e33e75a21056fc7adaafb53ef31383 Mon Sep 17 00:00:00 2001 From: Yunchi Luo Date: Sun, 26 Jan 2020 21:49:22 -0500 Subject: [PATCH 10/20] kotlin: fix enums, use List instead of Array --- internal/dinosql/kotlin/gen.go | 123 ++++++++++++++++++--------------- 1 file changed, 66 insertions(+), 57 deletions(-) diff --git a/internal/dinosql/kotlin/gen.go b/internal/dinosql/kotlin/gen.go index c7947bf71d..a6029ecd44 100644 --- a/internal/dinosql/kotlin/gen.go +++ b/internal/dinosql/kotlin/gen.go @@ -79,28 +79,60 @@ func (v KtQueryValue) Type() string { panic("no type for KtQueryValue: " + v.Name) } +func jdbcSet(t ktType, idx int, name string) string { + if t.IsEnum && t.IsArray { + return fmt.Sprintf(`stmt.setArray(%d, conn.createArrayOf("%s", %s.map { v -> v.value }.toTypedArray()))`, idx, t.DataType, name) + } + if t.IsEnum { + return fmt.Sprintf("stmt.setObject(%d, %s.value, %s)", idx, name, "Types.OTHER") + } + if t.IsArray { + return fmt.Sprintf(`stmt.setArray(%d, conn.createArrayOf("%s", %s.toTypedArray()))`, idx, t.DataType, name) + } + if t.IsTime() { + return fmt.Sprintf("stmt.setObject(%d, %s)", idx, name) + } + return fmt.Sprintf("stmt.set%s(%d, %s)", t.Name, idx, name) +} + func (v KtQueryValue) Params() string { if v.isEmpty() { return "" } var out []string if v.Struct == nil { - out = append(out, fmt.Sprintf("stmt.%s(%d, %s)", v.Typ.jdbcSetter(), 1, v.Typ.jdbcValue(v.Name))) + out = append(out, jdbcSet(v.Typ, 1, v.Name)) } else { for i, f := range v.Struct.Fields { - out = append(out, fmt.Sprintf("stmt.%s(%d, %s)", f.Type.jdbcSetter(), i+1, f.Type.jdbcValue(v.Name+"."+f.Name))) + out = append(out, jdbcSet(f.Type, i+1, v.Name+"."+f.Name)) } } return strings.Join(out, "\n ") } +func jdbcGet(t ktType, idx int) string { + if t.IsEnum && t.IsArray { + return fmt.Sprintf(`(results.getArray(%d).array as Array).map { v -> %s.lookup(v)!! }.toList()`, idx, t.Name) + } + if t.IsEnum { + return fmt.Sprintf("%s.lookup(results.getString(%d))!!", t.Name, idx) + } + if t.IsArray { + return fmt.Sprintf(`(results.getArray(%d).array as Array<%s>).toList()`, idx, t.Name) + } + if t.IsTime() { + return fmt.Sprintf(`results.getObject(%d, %s::class.java)`, idx, t.Name) + } + return fmt.Sprintf(`results.get%s(%d)`, t.Name, idx) +} + func (v KtQueryValue) ResultSet() string { var out []string if v.Struct == nil { - out = append(out, v.Typ.fromJDBCValue(fmt.Sprintf("%s.%s(%d)", v.Name, v.Typ.jdbcGetter(), 1))) + out = append(out, jdbcGet(v.Typ, 1)) } else { for i, f := range v.Struct.Fields { - out = append(out, f.Type.fromJDBCValue(fmt.Sprintf("%s.%s(%d)", v.Name, f.Type.jdbcGetter(), i+1))) + out = append(out, jdbcGet(f.Type, i+1)) } } ret := strings.Join(out, ",\n ") @@ -110,19 +142,6 @@ func (v KtQueryValue) ResultSet() string { return ret } -type KtQueryParam struct { - Name string - Typ string -} - -func (p KtQueryParam) Getter() string { - return "get" + strings.TrimSuffix(p.Typ, "?") -} - -func (p KtQueryParam) Setter() string { - return "set" + strings.TrimSuffix(p.Typ, "?") -} - // A struct used to generate methods and fields on the Queries struct type KtQuery struct { ClassName string @@ -279,6 +298,25 @@ func QueryKtImports(r KtGenerateable, settings dinosql.CombinedSettings, filenam return false } + hasEnum := func() bool { + for _, q := range gq { + if !q.Arg.isEmpty() { + if q.Arg.IsStruct() { + for _, f := range q.Arg.Struct.Fields { + if f.Type.IsEnum { + return true + } + } + } else { + if q.Arg.Typ.IsEnum { + return true + } + } + } + } + return false + } + std := map[string]struct{}{ "java.sql.Connection": {}, "java.sql.SQLException": {}, @@ -295,6 +333,9 @@ func QueryKtImports(r KtGenerateable, settings dinosql.CombinedSettings, filenam if uses("OffsetDateTime") { std["java.time.OffsetDateTime"] = struct{}{} } + if hasEnum() { + std["java.sql.Types"] = struct{}{} + } pkg := make(map[string]struct{}) @@ -422,7 +463,7 @@ type ktType struct { func (t ktType) String() string { v := t.Name if t.IsArray { - v = fmt.Sprintf("Array<%s>", v) + v = fmt.Sprintf("List<%s>", v) } else if t.IsNull { v += "?" } @@ -433,18 +474,11 @@ func (t ktType) jdbcSetter() string { return "set" + t.jdbcType() } -func (t ktType) jdbcGetter() string { - return "get" + t.jdbcType() -} - func (t ktType) jdbcType() string { if t.IsArray { return "Array" } - if t.IsEnum { - return "String" - } - if t.IsTime() { + if t.IsEnum || t.IsTime() { return "Object" } return t.Name @@ -454,36 +488,6 @@ func (t ktType) IsTime() bool { return t.Name == "LocalDate" || t.Name == "LocalDateTime" || t.Name == "LocalTime" || t.Name == "OffsetDateTime" } -func (t ktType) jdbcValue(name string) string { - if t.IsEnum && t.IsArray { - return fmt.Sprintf(`conn.createArrayOf("%s", %s.map { v -> v.value }.toTypedArray())`, t.DataType, name) - } - if t.IsEnum { - return name + ".value" - } - if t.IsArray { - return fmt.Sprintf(`conn.createArrayOf("%s", %s)`, t.DataType, name) - } - return name -} - -func (t ktType) fromJDBCValue(expr string) string { - if t.IsEnum && t.IsArray { - return fmt.Sprintf(`(%s.array as Array).map { v -> %s.valueOf(v) }.toTypedArray()`, expr, t.Name) - } - if t.IsEnum { - return fmt.Sprintf("%s.valueOf(%s)", t.Name, expr) - } - if t.IsArray { - return fmt.Sprintf(`%s.array as Array<%s>`, expr, t.Name) - } - if t.IsTime() { - expr = strings.TrimSuffix(expr, ")") - return fmt.Sprintf(`%s, %s::class.java)`, expr, t.Name) - } - return expr -} - func (r Result) ktType(col core.Column, settings dinosql.CombinedSettings) ktType { typ, isEnum := r.ktInnerType(col, settings) return ktType{ @@ -777,7 +781,12 @@ enum class {{.Name}}(val value: String) { {{- range $i, $e := .Constants}} {{- if $i }},{{end}} {{.Name}}("{{.Value}}") - {{- end}} + {{- end}}; + + companion object { + private val map = {{.Name}}.values().associateBy({{.Name}}::value) + fun lookup(value: String) = map[value] + } } {{end}} From f44fe824e07b13b876aa866ef29f69be8c7b2660 Mon Sep 17 00:00:00 2001 From: Yunchi Luo Date: Sun, 26 Jan 2020 21:50:00 -0500 Subject: [PATCH 11/20] kotlin: port Go tests for examples --- .../com/example/booktest/postgresql/Models.kt | 9 +- .../booktest/postgresql/QueriesImpl.kt | 35 +++--- .../main/kotlin/com/example/ondeck/Models.kt | 11 +- .../main/kotlin/com/example/ondeck/Queries.kt | 1 + .../kotlin/com/example/ondeck/QueriesImpl.kt | 21 ++-- .../main/resources/{ => authors}/query.sql | 0 .../main/resources/{ => authors}/schema.sql | 0 .../resources/booktest/postgresql/query.sql | 60 ++++++++++ .../resources/booktest/postgresql/schema.sql | 37 ++++++ .../src/main/resources/ondeck/query/city.sql | 26 +++++ .../src/main/resources/ondeck/query/venue.sql | 49 ++++++++ .../resources/ondeck/schema/0001_city.sql | 4 + .../resources/ondeck/schema/0002_venue.sql | 18 +++ .../ondeck/schema/0003_add_column.sql | 3 + .../com/example/authors/QueriesImplTest.kt | 8 +- .../booktest/postgresql/QueriesImplTest.kt | 109 ++++++++++++++++++ .../com/example/dbtest/DbTestExtension.kt | 26 ++--- .../com/example/ondeck/QueriesImplTest.kt | 51 ++++++++ examples/sqlc.json | 8 +- 19 files changed, 422 insertions(+), 54 deletions(-) rename examples/kotlin/src/main/resources/{ => authors}/query.sql (100%) rename examples/kotlin/src/main/resources/{ => authors}/schema.sql (100%) create mode 100644 examples/kotlin/src/main/resources/booktest/postgresql/query.sql create mode 100644 examples/kotlin/src/main/resources/booktest/postgresql/schema.sql create mode 100644 examples/kotlin/src/main/resources/ondeck/query/city.sql create mode 100644 examples/kotlin/src/main/resources/ondeck/query/venue.sql create mode 100644 examples/kotlin/src/main/resources/ondeck/schema/0001_city.sql create mode 100644 examples/kotlin/src/main/resources/ondeck/schema/0002_venue.sql create mode 100644 examples/kotlin/src/main/resources/ondeck/schema/0003_add_column.sql create mode 100644 examples/kotlin/src/test/kotlin/com/example/booktest/postgresql/QueriesImplTest.kt create mode 100644 examples/kotlin/src/test/kotlin/com/example/ondeck/QueriesImplTest.kt diff --git a/examples/kotlin/src/main/kotlin/com/example/booktest/postgresql/Models.kt b/examples/kotlin/src/main/kotlin/com/example/booktest/postgresql/Models.kt index 46b71b248a..066918ee6b 100644 --- a/examples/kotlin/src/main/kotlin/com/example/booktest/postgresql/Models.kt +++ b/examples/kotlin/src/main/kotlin/com/example/booktest/postgresql/Models.kt @@ -6,7 +6,12 @@ import java.time.OffsetDateTime enum class BookType(val value: String) { FICTION("FICTION"), - NONFICTION("NONFICTION") + NONFICTION("NONFICTION"); + + companion object { + private val map = BookType.values().associateBy(BookType::value) + fun lookup(value: String) = map[value] + } } data class Author ( @@ -22,6 +27,6 @@ data class Book ( val title: String, val year: Int, val available: OffsetDateTime, - val tags: Array + val tags: List ) diff --git a/examples/kotlin/src/main/kotlin/com/example/booktest/postgresql/QueriesImpl.kt b/examples/kotlin/src/main/kotlin/com/example/booktest/postgresql/QueriesImpl.kt index e7d6d4764c..e81fc242e1 100644 --- a/examples/kotlin/src/main/kotlin/com/example/booktest/postgresql/QueriesImpl.kt +++ b/examples/kotlin/src/main/kotlin/com/example/booktest/postgresql/QueriesImpl.kt @@ -4,6 +4,7 @@ package com.example.booktest.postgresql import java.sql.Connection import java.sql.SQLException +import java.sql.Types import java.time.OffsetDateTime const val booksByTags = """-- name: booksByTags :many @@ -23,7 +24,7 @@ data class BooksByTagsRow ( val title: String, val name: String, val isbn: String, - val tags: Array + val tags: List ) const val booksByTitleYear = """-- name: booksByTitleYear :many @@ -69,7 +70,7 @@ data class CreateBookParams ( val title: String, val year: Int, val available: OffsetDateTime, - val tags: Array + val tags: List ) const val deleteBook = """-- name: deleteBook :exec @@ -95,7 +96,7 @@ WHERE book_id = ? data class UpdateBookParams ( val title: String, - val tags: Array, + val tags: List, val bookId: Int ) @@ -107,7 +108,7 @@ WHERE book_id = ? data class UpdateBookISBNParams ( val title: String, - val tags: Array, + val tags: List, val bookId: Int, val isbn: String ) @@ -115,9 +116,9 @@ data class UpdateBookISBNParams ( class QueriesImpl(private val conn: Connection) { @Throws(SQLException::class) - fun booksByTags(dollar_1: Array): List { + fun booksByTags(dollar_1: List): List { val stmt = conn.prepareStatement(booksByTags) - stmt.setArray(1, conn.createArrayOf("pg_catalog.varchar", dollar_1)) + stmt.setArray(1, conn.createArrayOf("pg_catalog.varchar", dollar_1.toTypedArray())) return stmt.executeQuery().use { results -> val ret = mutableListOf() @@ -127,7 +128,7 @@ class QueriesImpl(private val conn: Connection) { results.getString(2), results.getString(3), results.getString(4), - results.getArray(5).array as Array + (results.getArray(5).array as Array).toList() )) } ret @@ -147,11 +148,11 @@ class QueriesImpl(private val conn: Connection) { results.getInt(1), results.getInt(2), results.getString(3), - BookType.valueOf(results.getString(4)), + BookType.lookup(results.getString(4))!!, results.getString(5), results.getInt(6), results.getObject(7, OffsetDateTime::class.java), - results.getArray(8).array as Array + (results.getArray(8).array as Array).toList() )) } ret @@ -183,11 +184,11 @@ class QueriesImpl(private val conn: Connection) { val stmt = conn.prepareStatement(createBook) stmt.setInt(1, arg.authorId) stmt.setString(2, arg.isbn) - stmt.setString(3, arg.booktype.value) + stmt.setObject(3, arg.booktype.value, Types.OTHER) stmt.setString(4, arg.title) stmt.setInt(5, arg.year) stmt.setObject(6, arg.available) - stmt.setArray(7, conn.createArrayOf("pg_catalog.varchar", arg.tags)) + stmt.setArray(7, conn.createArrayOf("pg_catalog.varchar", arg.tags.toTypedArray())) return stmt.executeQuery().use { results -> if (!results.next()) { @@ -197,11 +198,11 @@ class QueriesImpl(private val conn: Connection) { results.getInt(1), results.getInt(2), results.getString(3), - BookType.valueOf(results.getString(4)), + BookType.lookup(results.getString(4))!!, results.getString(5), results.getInt(6), results.getObject(7, OffsetDateTime::class.java), - results.getArray(8).array as Array + (results.getArray(8).array as Array).toList() ) if (results.next()) { throw SQLException("expected one row in result set, but got many") @@ -252,11 +253,11 @@ class QueriesImpl(private val conn: Connection) { results.getInt(1), results.getInt(2), results.getString(3), - BookType.valueOf(results.getString(4)), + BookType.lookup(results.getString(4))!!, results.getString(5), results.getInt(6), results.getObject(7, OffsetDateTime::class.java), - results.getArray(8).array as Array + (results.getArray(8).array as Array).toList() ) if (results.next()) { throw SQLException("expected one row in result set, but got many") @@ -269,7 +270,7 @@ class QueriesImpl(private val conn: Connection) { fun updateBook(arg: UpdateBookParams) { val stmt = conn.prepareStatement(updateBook) stmt.setString(1, arg.title) - stmt.setArray(2, conn.createArrayOf("pg_catalog.varchar", arg.tags)) + stmt.setArray(2, conn.createArrayOf("pg_catalog.varchar", arg.tags.toTypedArray())) stmt.setInt(3, arg.bookId) stmt.execute() @@ -280,7 +281,7 @@ class QueriesImpl(private val conn: Connection) { fun updateBookISBN(arg: UpdateBookISBNParams) { val stmt = conn.prepareStatement(updateBookISBN) stmt.setString(1, arg.title) - stmt.setArray(2, conn.createArrayOf("pg_catalog.varchar", arg.tags)) + stmt.setArray(2, conn.createArrayOf("pg_catalog.varchar", arg.tags.toTypedArray())) stmt.setInt(3, arg.bookId) stmt.setString(4, arg.isbn) diff --git a/examples/kotlin/src/main/kotlin/com/example/ondeck/Models.kt b/examples/kotlin/src/main/kotlin/com/example/ondeck/Models.kt index 6b7f38d395..e4dd8e7db7 100644 --- a/examples/kotlin/src/main/kotlin/com/example/ondeck/Models.kt +++ b/examples/kotlin/src/main/kotlin/com/example/ondeck/Models.kt @@ -7,7 +7,12 @@ import java.time.LocalDateTime // Venues can be either open or closed enum class Status(val value: String) { OPEN("op!en"), - CLOSED("clo@sed") + CLOSED("clo@sed"); + + companion object { + private val map = Status.values().associateBy(Status::value) + fun lookup(value: String) = map[value] + } } data class City ( @@ -19,14 +24,14 @@ data class City ( data class Venue ( val id: Int, val status: Status, - val statuses: Array, + val statuses: List, // This value appears in public URLs val slug: String, val name: String, val city: String, val spotifyPlaylist: String, val songkickId: String?, - val tags: Array, + val tags: List, val createdAt: LocalDateTime ) diff --git a/examples/kotlin/src/main/kotlin/com/example/ondeck/Queries.kt b/examples/kotlin/src/main/kotlin/com/example/ondeck/Queries.kt index fa540fc82e..c67fd0b83f 100644 --- a/examples/kotlin/src/main/kotlin/com/example/ondeck/Queries.kt +++ b/examples/kotlin/src/main/kotlin/com/example/ondeck/Queries.kt @@ -4,6 +4,7 @@ package com.example.ondeck import java.sql.Connection import java.sql.SQLException +import java.sql.Types import java.time.LocalDateTime interface Queries { diff --git a/examples/kotlin/src/main/kotlin/com/example/ondeck/QueriesImpl.kt b/examples/kotlin/src/main/kotlin/com/example/ondeck/QueriesImpl.kt index 4da97fec32..60777d19c9 100644 --- a/examples/kotlin/src/main/kotlin/com/example/ondeck/QueriesImpl.kt +++ b/examples/kotlin/src/main/kotlin/com/example/ondeck/QueriesImpl.kt @@ -4,6 +4,7 @@ package com.example.ondeck import java.sql.Connection import java.sql.SQLException +import java.sql.Types import java.time.LocalDateTime const val createCity = """-- name: createCity :one @@ -49,8 +50,8 @@ data class CreateVenueParams ( val city: String, val spotifyPlaylist: String, val status: Status, - val statuses: Array, - val tags: Array + val statuses: List, + val tags: List ) const val deleteVenue = """-- name: deleteVenue :exec @@ -159,9 +160,9 @@ class QueriesImpl(private val conn: Connection) : Queries { stmt.setString(2, arg.name) stmt.setString(3, arg.city) stmt.setString(4, arg.spotifyPlaylist) - stmt.setString(5, arg.status.value) + stmt.setObject(5, arg.status.value, Types.OTHER) stmt.setArray(6, conn.createArrayOf("status", arg.statuses.map { v -> v.value }.toTypedArray())) - stmt.setArray(7, conn.createArrayOf("text", arg.tags)) + stmt.setArray(7, conn.createArrayOf("text", arg.tags.toTypedArray())) return stmt.executeQuery().use { results -> if (!results.next()) { @@ -216,14 +217,14 @@ class QueriesImpl(private val conn: Connection) : Queries { } val ret = Venue( results.getInt(1), - Status.valueOf(results.getString(2)), - (results.getArray(3).array as Array).map { v -> Status.valueOf(v) }.toTypedArray(), + Status.lookup(results.getString(2))!!, + (results.getArray(3).array as Array).map { v -> Status.lookup(v)!! }.toList(), results.getString(4), results.getString(5), results.getString(6), results.getString(7), results.getString(8), - results.getArray(9).array as Array, + (results.getArray(9).array as Array).toList(), results.getObject(10, LocalDateTime::class.java) ) if (results.next()) { @@ -259,14 +260,14 @@ class QueriesImpl(private val conn: Connection) : Queries { while (results.next()) { ret.add(Venue( results.getInt(1), - Status.valueOf(results.getString(2)), - (results.getArray(3).array as Array).map { v -> Status.valueOf(v) }.toTypedArray(), + Status.lookup(results.getString(2))!!, + (results.getArray(3).array as Array).map { v -> Status.lookup(v)!! }.toList(), results.getString(4), results.getString(5), results.getString(6), results.getString(7), results.getString(8), - results.getArray(9).array as Array, + (results.getArray(9).array as Array).toList(), results.getObject(10, LocalDateTime::class.java) )) } diff --git a/examples/kotlin/src/main/resources/query.sql b/examples/kotlin/src/main/resources/authors/query.sql similarity index 100% rename from examples/kotlin/src/main/resources/query.sql rename to examples/kotlin/src/main/resources/authors/query.sql diff --git a/examples/kotlin/src/main/resources/schema.sql b/examples/kotlin/src/main/resources/authors/schema.sql similarity index 100% rename from examples/kotlin/src/main/resources/schema.sql rename to examples/kotlin/src/main/resources/authors/schema.sql diff --git a/examples/kotlin/src/main/resources/booktest/postgresql/query.sql b/examples/kotlin/src/main/resources/booktest/postgresql/query.sql new file mode 100644 index 0000000000..f4537c603e --- /dev/null +++ b/examples/kotlin/src/main/resources/booktest/postgresql/query.sql @@ -0,0 +1,60 @@ +-- name: GetAuthor :one +SELECT * FROM authors +WHERE author_id = $1; + +-- name: GetBook :one +SELECT * FROM books +WHERE book_id = $1; + +-- name: DeleteBook :exec +DELETE FROM books +WHERE book_id = $1; + +-- name: BooksByTitleYear :many +SELECT * FROM books +WHERE title = $1 AND year = $2; + +-- name: BooksByTags :many +SELECT + book_id, + title, + name, + isbn, + tags +FROM books +LEFT JOIN authors ON books.author_id = authors.author_id +WHERE tags && $1::varchar[]; + +-- name: CreateAuthor :one +INSERT INTO authors (name) VALUES ($1) +RETURNING *; + +-- name: CreateBook :one +INSERT INTO books ( + author_id, + isbn, + booktype, + title, + year, + available, + tags +) VALUES ( + $1, + $2, + $3, + $4, + $5, + $6, + $7 +) +RETURNING *; + +-- name: UpdateBook :exec +UPDATE books +SET title = $1, tags = $2 +WHERE book_id = $3; + +-- name: UpdateBookISBN :exec +UPDATE books +SET title = $1, tags = $2, isbn = $4 +WHERE book_id = $3; diff --git a/examples/kotlin/src/main/resources/booktest/postgresql/schema.sql b/examples/kotlin/src/main/resources/booktest/postgresql/schema.sql new file mode 100644 index 0000000000..0816931a81 --- /dev/null +++ b/examples/kotlin/src/main/resources/booktest/postgresql/schema.sql @@ -0,0 +1,37 @@ +DROP TABLE IF EXISTS books CASCADE; +DROP TYPE IF EXISTS book_type CASCADE; +DROP TABLE IF EXISTS authors CASCADE; +DROP FUNCTION IF EXISTS say_hello(text) CASCADE; + +CREATE TABLE authors ( + author_id SERIAL PRIMARY KEY, + name text NOT NULL DEFAULT '' +); + +CREATE INDEX authors_name_idx ON authors(name); + +CREATE TYPE book_type AS ENUM ( + 'FICTION', + 'NONFICTION' +); + +CREATE TABLE books ( + book_id SERIAL PRIMARY KEY, + author_id integer NOT NULL REFERENCES authors(author_id), + isbn text NOT NULL DEFAULT '' UNIQUE, + booktype book_type NOT NULL DEFAULT 'FICTION', + title text NOT NULL DEFAULT '', + year integer NOT NULL DEFAULT 2000, + available timestamp with time zone NOT NULL DEFAULT 'NOW()', + tags varchar[] NOT NULL DEFAULT '{}' +); + +CREATE INDEX books_title_idx ON books(title, year); + +CREATE FUNCTION say_hello(text) RETURNS text AS $$ +BEGIN + RETURN CONCAT('hello ', $1); +END; +$$ LANGUAGE plpgsql; + +CREATE INDEX books_title_lower_idx ON books(title); diff --git a/examples/kotlin/src/main/resources/ondeck/query/city.sql b/examples/kotlin/src/main/resources/ondeck/query/city.sql new file mode 100644 index 0000000000..f34dc9961e --- /dev/null +++ b/examples/kotlin/src/main/resources/ondeck/query/city.sql @@ -0,0 +1,26 @@ +-- name: ListCities :many +SELECT * +FROM city +ORDER BY name; + +-- name: GetCity :one +SELECT * +FROM city +WHERE slug = $1; + +-- name: CreateCity :one +-- Create a new city. The slug must be unique. +-- This is the second line of the comment +-- This is the third line +INSERT INTO city ( + name, + slug +) VALUES ( + $1, + $2 +) RETURNING *; + +-- name: UpdateCityName :exec +UPDATE city +SET name = $2 +WHERE slug = $1; diff --git a/examples/kotlin/src/main/resources/ondeck/query/venue.sql b/examples/kotlin/src/main/resources/ondeck/query/venue.sql new file mode 100644 index 0000000000..8c6bd02664 --- /dev/null +++ b/examples/kotlin/src/main/resources/ondeck/query/venue.sql @@ -0,0 +1,49 @@ +-- name: ListVenues :many +SELECT * +FROM venue +WHERE city = $1 +ORDER BY name; + +-- name: DeleteVenue :exec +DELETE FROM venue +WHERE slug = $1 AND slug = $1; + +-- name: GetVenue :one +SELECT * +FROM venue +WHERE slug = $1 AND city = $2; + +-- name: CreateVenue :one +INSERT INTO venue ( + slug, + name, + city, + created_at, + spotify_playlist, + status, + statuses, + tags +) VALUES ( + $1, + $2, + $3, + NOW(), + $4, + $5, + $6, + $7 +) RETURNING id; + +-- name: UpdateVenueName :one +UPDATE venue +SET name = $2 +WHERE slug = $1 +RETURNING id; + +-- name: VenueCountByCity :many +SELECT + city, + count(*) +FROM venue +GROUP BY 1 +ORDER BY 1; diff --git a/examples/kotlin/src/main/resources/ondeck/schema/0001_city.sql b/examples/kotlin/src/main/resources/ondeck/schema/0001_city.sql new file mode 100644 index 0000000000..af38f16bb5 --- /dev/null +++ b/examples/kotlin/src/main/resources/ondeck/schema/0001_city.sql @@ -0,0 +1,4 @@ +CREATE TABLE city ( + slug text PRIMARY KEY, + name text NOT NULL +) diff --git a/examples/kotlin/src/main/resources/ondeck/schema/0002_venue.sql b/examples/kotlin/src/main/resources/ondeck/schema/0002_venue.sql new file mode 100644 index 0000000000..940de7a5a8 --- /dev/null +++ b/examples/kotlin/src/main/resources/ondeck/schema/0002_venue.sql @@ -0,0 +1,18 @@ +CREATE TYPE status AS ENUM ('op!en', 'clo@sed'); +COMMENT ON TYPE status IS 'Venues can be either open or closed'; + +CREATE TABLE venues ( + id SERIAL primary key, + dropped text, + status status not null, + statuses status[], + slug text not null, + name varchar(255) not null, + city text not null references city(slug), + spotify_playlist varchar not null, + songkick_id text, + tags text[] +); +COMMENT ON TABLE venues IS 'Venues are places where muisc happens'; +COMMENT ON COLUMN venues.slug IS 'This value appears in public URLs'; + diff --git a/examples/kotlin/src/main/resources/ondeck/schema/0003_add_column.sql b/examples/kotlin/src/main/resources/ondeck/schema/0003_add_column.sql new file mode 100644 index 0000000000..9b334bccce --- /dev/null +++ b/examples/kotlin/src/main/resources/ondeck/schema/0003_add_column.sql @@ -0,0 +1,3 @@ +ALTER TABLE venues RENAME TO venue; +ALTER TABLE venue ADD COLUMN created_at TIMESTAMP NOT NULL DEFAULT NOW(); +ALTER TABLE venue DROP COLUMN dropped; diff --git a/examples/kotlin/src/test/kotlin/com/example/authors/QueriesImplTest.kt b/examples/kotlin/src/test/kotlin/com/example/authors/QueriesImplTest.kt index fd52cc78e9..0a42a0dcc7 100644 --- a/examples/kotlin/src/test/kotlin/com/example/authors/QueriesImplTest.kt +++ b/examples/kotlin/src/test/kotlin/com/example/authors/QueriesImplTest.kt @@ -6,15 +6,15 @@ import org.junit.jupiter.api.Test import org.junit.jupiter.api.extension.RegisterExtension import java.sql.Connection -class QueriesImplTest(private val conn: Connection) { +class QueriesImplTest() { companion object { - @JvmField @RegisterExtension val db = DbTestExtension("src/main/resources/schema.sql") + @JvmField @RegisterExtension val dbtest = DbTestExtension("src/main/resources/authors/schema.sql") } @Test fun testCreateAuthor() { - val db = QueriesImpl(conn) + val db = QueriesImpl(dbtest.getConnection()) val initialAuthors = db.listAuthors() assert(initialAuthors.isEmpty()) @@ -37,7 +37,7 @@ class QueriesImplTest(private val conn: Connection) { @Test fun testNull() { - val db = QueriesImpl(conn) + val db = QueriesImpl(dbtest.getConnection()) val initialAuthors = db.listAuthors() assert(initialAuthors.isEmpty()) diff --git a/examples/kotlin/src/test/kotlin/com/example/booktest/postgresql/QueriesImplTest.kt b/examples/kotlin/src/test/kotlin/com/example/booktest/postgresql/QueriesImplTest.kt new file mode 100644 index 0000000000..fb0e48e3d4 --- /dev/null +++ b/examples/kotlin/src/test/kotlin/com/example/booktest/postgresql/QueriesImplTest.kt @@ -0,0 +1,109 @@ +package com.example.booktest.postgresql + +import com.example.dbtest.DbTestExtension +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.RegisterExtension +import java.time.OffsetDateTime +import java.time.format.DateTimeFormatter + +class QueriesImplTest { + companion object { + @JvmField @RegisterExtension val dbtest = DbTestExtension("src/main/resources/booktest/postgresql/schema.sql") + } + + @Test + fun testQueries() { + val conn = dbtest.getConnection() + val db = QueriesImpl(conn) + val author = db.createAuthor("Unknown Master") + + // Start a transaction + conn.autoCommit = false + db.createBook( + CreateBookParams( + authorId = author.authorId, + isbn = "1", + title = "my book title", + booktype = BookType.NONFICTION, + year = 2016, + available = OffsetDateTime.now(), + tags = listOf() + ) + ) + + val b1 = db.createBook( + CreateBookParams( + authorId = author.authorId, + isbn = "2", + title = "the second book", + booktype = BookType.NONFICTION, + year = 2016, + available = OffsetDateTime.now(), + tags = listOf("cool", "unique") + ) + ) + + db.updateBook( + UpdateBookParams( + bookId = b1.bookId, + title = "changed second title", + tags = listOf("cool", "disastor") + ) + ) + + val b3 = db.createBook( + CreateBookParams( + authorId = author.authorId, + isbn = "3", + title = "the third book", + booktype = BookType.NONFICTION, + year = 2001, + available = OffsetDateTime.now(), + tags = listOf("cool") + ) + ) + + db.createBook( + CreateBookParams( + authorId = author.authorId, + isbn = "4", + title = "4th place finisher", + booktype = BookType.NONFICTION, + year = 2011, + available = OffsetDateTime.now(), + tags = listOf("other") + ) + ) + + // Commit transaction + conn.commit() + conn.autoCommit = true + + // ISBN update fails because parameters are not in sequential order. After changing $N to ?, ordering is lost, + // and the parameters are filled into the wrong slots. +// db.updateBookISBN( +// UpdateBookISBNParams( +// bookId = b3.bookId, +// isbn = "NEW ISBN", +// title = "never ever gonna finish, a quatrain", +// tags = listOf("someother") +// ) +// ) + + val books0 = db.booksByTitleYear(BooksByTitleYearParams("my book title", 2016)) + + val formatter = DateTimeFormatter.ISO_DATE_TIME + for (book in books0) { + println("Book ${book.bookId} (${book.booktype}): ${book.title} available: ${book.available.format(formatter)}") + val author = db.getAuthor(book.authorId) + println("Book ${book.bookId} author: ${author.name}") + } + + // find a book with either "cool" or "other" tag + println("---------\\nTag search results:\\n") + val res = db.booksByTags(listOf("cool", "other", "someother")) + for (ab in res) { + println("Book ${ab.bookId}: '${ab.title}', Author: '${ab.name}', ISBN: '${ab.isbn}' Tags: '${ab.tags.toList()}'") + } + } +} \ No newline at end of file diff --git a/examples/kotlin/src/test/kotlin/com/example/dbtest/DbTestExtension.kt b/examples/kotlin/src/test/kotlin/com/example/dbtest/DbTestExtension.kt index 4e5c01ffb9..66f831477e 100644 --- a/examples/kotlin/src/test/kotlin/com/example/dbtest/DbTestExtension.kt +++ b/examples/kotlin/src/test/kotlin/com/example/dbtest/DbTestExtension.kt @@ -3,16 +3,15 @@ package com.example.dbtest import org.junit.jupiter.api.extension.AfterEachCallback import org.junit.jupiter.api.extension.BeforeEachCallback import org.junit.jupiter.api.extension.ExtensionContext -import org.junit.jupiter.api.extension.ParameterContext -import org.junit.jupiter.api.extension.ParameterResolver import java.nio.file.Files import java.nio.file.Paths import java.sql.Connection import java.sql.DriverManager +import kotlin.streams.toList const val schema = "dinosql_test" -class DbTestExtension(private val migrationsPath: String) : BeforeEachCallback, AfterEachCallback, ParameterResolver { +class DbTestExtension(private val migrationsPath: String) : BeforeEachCallback, AfterEachCallback { private val schemaConn: Connection private val url: String @@ -29,23 +28,22 @@ class DbTestExtension(private val migrationsPath: String) : BeforeEachCallback, override fun beforeEach(context: ExtensionContext) { schemaConn.createStatement().execute("CREATE SCHEMA $schema") - val stmt = Files.readString(Paths.get(migrationsPath)) - getConnection().createStatement().execute(stmt) + val path = Paths.get(migrationsPath) + val migrations = if (Files.isDirectory(path)) { + Files.list(path).filter{ it.toString().endsWith(".sql")}.sorted().map { Files.readString(it) }.toList() + } else { + listOf(Files.readString(path)) + } + migrations.forEach { + getConnection().createStatement().execute(it) + } } override fun afterEach(context: ExtensionContext) { schemaConn.createStatement().execute("DROP SCHEMA $schema CASCADE") } - private fun getConnection(): Connection { + fun getConnection(): Connection { return DriverManager.getConnection("$url¤tSchema=$schema") } - - override fun supportsParameter(parameterContext: ParameterContext, extensionContext: ExtensionContext): Boolean { - return parameterContext.parameter.type == Connection::class.java - } - - override fun resolveParameter(parameterContext: ParameterContext, extensionContext: ExtensionContext): Any { - return getConnection() - } } \ No newline at end of file diff --git a/examples/kotlin/src/test/kotlin/com/example/ondeck/QueriesImplTest.kt b/examples/kotlin/src/test/kotlin/com/example/ondeck/QueriesImplTest.kt new file mode 100644 index 0000000000..8a2ff68dce --- /dev/null +++ b/examples/kotlin/src/test/kotlin/com/example/ondeck/QueriesImplTest.kt @@ -0,0 +1,51 @@ +package com.example.ondeck + +import com.example.dbtest.DbTestExtension +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.RegisterExtension + +class QueriesImplTest { + companion object { + @JvmField @RegisterExtension val dbtest = DbTestExtension("src/main/resources/ondeck/schema") + } + + @Test + fun testQueries() { + val q = QueriesImpl(dbtest.getConnection()) + val city = q.createCity( + CreateCityParams( + slug = "san-francisco", + name = "San Francisco" + ) + ) + val venueId = q.createVenue( + CreateVenueParams( + slug = "the-fillmore", + name = "The Fillmore", + city = city.slug, + spotifyPlaylist = "spotify=uri", + status = Status.OPEN, + statuses = listOf(Status.OPEN, Status.CLOSED), + tags = listOf("rock", "punk") + ) + ) + val venue = q.getVenue( + GetVenueParams( + slug = "the-fillmore", + city = city.slug + ) + ) + assertEquals(venueId, venue.id) + + assertEquals(city, q.getCity(city.slug)) + assertEquals(listOf(VenueCountByCityRow(city.slug, 1)), q.venueCountByCity()) + assertEquals(listOf(city), q.listCities()) + assertEquals(listOf(venue), q.listVenues(city.slug)) + + // These updates fail because parameters are not in sequential order. After changing $N to ?, ordering is lost, + // and the parameters are filled into the wrong slots. +// q.updateCityName(UpdateCityNameParams(slug = city.slug, name = "SF")) +// q.updateVenueName(UpdateVenueNameParams(slug = venue.slug, name = "Fillmore")) + } +} \ No newline at end of file diff --git a/examples/sqlc.json b/examples/sqlc.json index 98f3d5fdac..f892a0bb11 100644 --- a/examples/sqlc.json +++ b/examples/sqlc.json @@ -39,8 +39,8 @@ { "name": "com.example.authors", "path": "kotlin/src/main/kotlin/com/example/authors", - "schema": "kotlin/src/main/resources/schema.sql", - "queries": "kotlin/src/main/resources/query.sql", + "schema": "kotlin/src/main/resources/authors/schema.sql", + "queries": "kotlin/src/main/resources/authors/query.sql", "engine": "postgresql", "language": "kotlin" }, @@ -64,8 +64,8 @@ { "name": "com.example.booktest.postgresql", "path": "kotlin/src/main/kotlin/com/example/booktest/postgresql", - "schema": "booktest/postgresql/schema.sql", - "queries": "booktest/postgresql/query.sql", + "schema": "kotlin/src/main/resources/booktest/postgresql/schema.sql", + "queries": "kotlin/src/main/resources/booktest/postgresql/query.sql", "engine": "postgresql", "language": "kotlin" } From 0f5afd35cd06c71efd3a8ed482016e55ddcda422 Mon Sep 17 00:00:00 2001 From: Yunchi Luo Date: Tue, 28 Jan 2020 15:01:28 -0500 Subject: [PATCH 12/20] support rewriting numbered params to positional params --- internal/dinosql/config.go | 2 + internal/dinosql/parser.go | 77 ++++++++++++++++++++++++++-------- internal/dinosql/query_test.go | 2 +- 3 files changed, 62 insertions(+), 19 deletions(-) diff --git a/internal/dinosql/config.go b/internal/dinosql/config.go index b51cd0a6b1..4f267f3946 100644 --- a/internal/dinosql/config.go +++ b/internal/dinosql/config.go @@ -59,6 +59,8 @@ type PackageSettings struct { EmitJSONTags bool `json:"emit_json_tags"` EmitPreparedQueries bool `json:"emit_prepared_queries"` Overrides []Override `json:"overrides"` + // HACK: this is only set in tests, only here till Kotlin support can be merged. + rewriteParams bool } type Override struct { diff --git a/internal/dinosql/parser.go b/internal/dinosql/parser.go index 8154f1a5c6..1106fb4e38 100644 --- a/internal/dinosql/parser.go +++ b/internal/dinosql/parser.go @@ -229,7 +229,8 @@ func ParseQueries(c core.Catalog, pkg PackageSettings) (*Result, error) { continue } for _, stmt := range tree.Statements { - query, err := parseQuery(c, stmt, source) + rewriteParameters := pkg.rewriteParams + query, err := parseQuery(c, stmt, source, rewriteParameters) if err == errUnsupportedStatementType { continue } @@ -407,7 +408,7 @@ func validateCmd(n nodes.Node, name, cmd string) error { var errUnsupportedStatementType = errors.New("parseQuery: unsupported statement type") -func parseQuery(c core.Catalog, stmt nodes.Node, source string) (*Query, error) { +func parseQuery(c core.Catalog, stmt nodes.Node, source string, rewriteParameters bool) (*Query, error) { if err := validateParamRef(stmt); err != nil { return nil, err } @@ -443,6 +444,16 @@ func parseQuery(c core.Catalog, stmt nodes.Node, source string) (*Query, error) } rvs := rangeVars(raw.Stmt) refs := findParameters(raw.Stmt) + var edits []edit + if rewriteParameters { + edits, err = rewriteNumberedParameters(refs, raw, rawSQL) + if err != nil { + return nil, err + } + } else { + refs = uniqueParamRefs(refs) + sort.Slice(refs, func(i, j int) bool { return refs[i].ref.Number < refs[j].ref.Number }) + } params, err := resolveCatalogRefs(c, rvs, refs) if err != nil { return nil, err @@ -452,7 +463,13 @@ func parseQuery(c core.Catalog, stmt nodes.Node, source string) (*Query, error) if err != nil { return nil, err } - expanded, err := expand(c, raw, rawSQL) + expandEdits, err := expand(c, raw, rawSQL) + if err != nil { + return nil, err + } + edits = append(edits, expandEdits...) + + expanded, err := editQuery(rawSQL, edits) if err != nil { return nil, err } @@ -472,6 +489,18 @@ func parseQuery(c core.Catalog, stmt nodes.Node, source string) (*Query, error) }, nil } +func rewriteNumberedParameters(refs []paramRef, raw nodes.RawStmt, sql string) ([]edit, error) { + edits := make([]edit, len(refs)) + for i, ref := range refs { + edits[i] = edit{ + Location: ref.ref.Location - raw.StmtLocation, + Old: fmt.Sprintf("$%d", ref.ref.Number), + New: "?", + } + } + return edits, nil +} + func stripComments(sql string) (string, []string, error) { s := bufio.NewScanner(strings.NewReader(sql)) var lines, comments []string @@ -494,7 +523,7 @@ type edit struct { New string } -func expand(c core.Catalog, raw nodes.RawStmt, sql string) (string, error) { +func expand(c core.Catalog, raw nodes.RawStmt, sql string) ([]edit, error) { list := search(raw, func(node nodes.Node) bool { switch node.(type) { case nodes.DeleteStmt: @@ -507,17 +536,17 @@ func expand(c core.Catalog, raw nodes.RawStmt, sql string) (string, error) { return true }) if len(list.Items) == 0 { - return sql, nil + return nil, nil } var edits []edit for _, item := range list.Items { edit, err := expandStmt(c, raw, item) if err != nil { - return "", err + return nil, err } edits = append(edits, edit...) } - return editQuery(sql, edits) + return edits, nil } func expandStmt(c core.Catalog, raw nodes.RawStmt, node nodes.Node) ([]edit, error) { @@ -958,7 +987,8 @@ type paramRef struct { type paramSearch struct { parent nodes.Node rangeVar *nodes.RangeVar - refs map[int]paramRef + refs *[]paramRef + seen map[int]struct{} // XXX: Gross state hack for limit limitCount nodes.Node @@ -1005,7 +1035,8 @@ func (p paramSearch) Visit(node nodes.Node) Visitor { continue } // TODO: Out-of-bounds panic - p.refs[ref.Number] = paramRef{parent: n.Cols.Items[i], ref: ref, rv: p.rangeVar} + *p.refs = append(*p.refs, paramRef{parent: n.Cols.Items[i], ref: ref, rv: p.rangeVar}) + p.seen[ref.Location] = struct{}{} } for _, vl := range s.ValuesLists { for i, v := range vl { @@ -1014,7 +1045,8 @@ func (p paramSearch) Visit(node nodes.Node) Visitor { continue } // TODO: Out-of-bounds panic - p.refs[ref.Number] = paramRef{parent: n.Cols.Items[i], ref: ref, rv: p.rangeVar} + *p.refs = append(*p.refs, paramRef{parent: n.Cols.Items[i], ref: ref, rv: p.rangeVar}) + p.seen[ref.Location] = struct{}{} } } } @@ -1050,7 +1082,7 @@ func (p paramSearch) Visit(node nodes.Node) Visitor { parent = limitOffset{} } } - if _, found := p.refs[n.Number]; found { + if _, found := p.seen[n.Location]; found { break } @@ -1072,7 +1104,8 @@ func (p paramSearch) Visit(node nodes.Node) Visitor { } if set { - p.refs[n.Number] = paramRef{parent: parent, ref: n, rv: p.rangeVar} + *p.refs = append(*p.refs, paramRef{parent: parent, ref: n, rv: p.rangeVar}) + p.seen[n.Location] = struct{}{} } return nil } @@ -1080,13 +1113,9 @@ func (p paramSearch) Visit(node nodes.Node) Visitor { } func findParameters(root nodes.Node) []paramRef { - v := paramSearch{refs: map[int]paramRef{}} - Walk(v, root) refs := make([]paramRef, 0) - for _, r := range v.refs { - refs = append(refs, r) - } - sort.Slice(refs, func(i, j int) bool { return refs[i].ref.Number < refs[j].ref.Number }) + v := paramSearch{seen: make(map[int]struct{}), refs: &refs} + Walk(v, root) return refs } @@ -1348,3 +1377,15 @@ func resolveCatalogRefs(c core.Catalog, rvs []nodes.RangeVar, args []paramRef) ( } return a, nil } + +func uniqueParamRefs(in []paramRef) []paramRef { + m := make(map[int]struct{}, len(in)) + o := make([]paramRef, 0, len(in)) + for _, v := range in { + if _, ok := m[v.ref.Number]; !ok { + m[v.ref.Number] = struct{}{} + o = append(o, v) + } + } + return o +} diff --git a/internal/dinosql/query_test.go b/internal/dinosql/query_test.go index c80b1d85e6..8ee13ac451 100644 --- a/internal/dinosql/query_test.go +++ b/internal/dinosql/query_test.go @@ -21,7 +21,7 @@ func parseSQL(in string) (Query, error) { return Query{}, err } - q, err := parseQuery(c, tree.Statements[len(tree.Statements)-1], in) + q, err := parseQuery(c, tree.Statements[len(tree.Statements)-1], in, false) if q == nil { return Query{}, err } From 2cd3bb87f65f0909bdcbf60e9f6f724ecf362d2a Mon Sep 17 00:00:00 2001 From: Yunchi Luo Date: Tue, 28 Jan 2020 15:01:55 -0500 Subject: [PATCH 13/20] kotlin: rewrite numbered params to positional params --- .../booktest/postgresql/QueriesImpl.kt | 8 +- .../kotlin/com/example/ondeck/QueriesImpl.kt | 17 ++-- .../booktest/postgresql/QueriesImplTest.kt | 16 ++-- .../com/example/ondeck/QueriesImplTest.kt | 9 +- internal/dinosql/config.go | 2 + internal/dinosql/gen.go | 43 +++++++-- internal/dinosql/kotlin/gen.go | 89 +++++++++++++------ internal/dinosql/parser_test.go | 56 ++++++++++-- 8 files changed, 171 insertions(+), 69 deletions(-) diff --git a/examples/kotlin/src/main/kotlin/com/example/booktest/postgresql/QueriesImpl.kt b/examples/kotlin/src/main/kotlin/com/example/booktest/postgresql/QueriesImpl.kt index e81fc242e1..281ba0dd66 100644 --- a/examples/kotlin/src/main/kotlin/com/example/booktest/postgresql/QueriesImpl.kt +++ b/examples/kotlin/src/main/kotlin/com/example/booktest/postgresql/QueriesImpl.kt @@ -109,8 +109,8 @@ WHERE book_id = ? data class UpdateBookISBNParams ( val title: String, val tags: List, - val bookId: Int, - val isbn: String + val isbn: String, + val bookId: Int ) class QueriesImpl(private val conn: Connection) { @@ -282,8 +282,8 @@ class QueriesImpl(private val conn: Connection) { val stmt = conn.prepareStatement(updateBookISBN) stmt.setString(1, arg.title) stmt.setArray(2, conn.createArrayOf("pg_catalog.varchar", arg.tags.toTypedArray())) - stmt.setInt(3, arg.bookId) - stmt.setString(4, arg.isbn) + stmt.setString(3, arg.isbn) + stmt.setInt(4, arg.bookId) stmt.execute() stmt.close() diff --git a/examples/kotlin/src/main/kotlin/com/example/ondeck/QueriesImpl.kt b/examples/kotlin/src/main/kotlin/com/example/ondeck/QueriesImpl.kt index 60777d19c9..7b7911af70 100644 --- a/examples/kotlin/src/main/kotlin/com/example/ondeck/QueriesImpl.kt +++ b/examples/kotlin/src/main/kotlin/com/example/ondeck/QueriesImpl.kt @@ -96,8 +96,8 @@ WHERE slug = ? """ data class UpdateCityNameParams ( - val slug: String, - val name: String + val name: String, + val slug: String ) const val updateVenueName = """-- name: updateVenueName :one @@ -108,8 +108,8 @@ RETURNING id """ data class UpdateVenueNameParams ( - val slug: String, - val name: String + val name: String, + val slug: String ) const val venueCountByCity = """-- name: venueCountByCity :many @@ -180,6 +180,7 @@ class QueriesImpl(private val conn: Connection) : Queries { override fun deleteVenue(slug: String) { val stmt = conn.prepareStatement(deleteVenue) stmt.setString(1, slug) + stmt.setString(2, slug) stmt.execute() stmt.close() @@ -278,8 +279,8 @@ class QueriesImpl(private val conn: Connection) : Queries { @Throws(SQLException::class) override fun updateCityName(arg: UpdateCityNameParams) { val stmt = conn.prepareStatement(updateCityName) - stmt.setString(1, arg.slug) - stmt.setString(2, arg.name) + stmt.setString(1, arg.name) + stmt.setString(2, arg.slug) stmt.execute() stmt.close() @@ -288,8 +289,8 @@ class QueriesImpl(private val conn: Connection) : Queries { @Throws(SQLException::class) override fun updateVenueName(arg: UpdateVenueNameParams): Int { val stmt = conn.prepareStatement(updateVenueName) - stmt.setString(1, arg.slug) - stmt.setString(2, arg.name) + stmt.setString(1, arg.name) + stmt.setString(2, arg.slug) return stmt.executeQuery().use { results -> if (!results.next()) { diff --git a/examples/kotlin/src/test/kotlin/com/example/booktest/postgresql/QueriesImplTest.kt b/examples/kotlin/src/test/kotlin/com/example/booktest/postgresql/QueriesImplTest.kt index fb0e48e3d4..f7c54daf7b 100644 --- a/examples/kotlin/src/test/kotlin/com/example/booktest/postgresql/QueriesImplTest.kt +++ b/examples/kotlin/src/test/kotlin/com/example/booktest/postgresql/QueriesImplTest.kt @@ -81,14 +81,14 @@ class QueriesImplTest { // ISBN update fails because parameters are not in sequential order. After changing $N to ?, ordering is lost, // and the parameters are filled into the wrong slots. -// db.updateBookISBN( -// UpdateBookISBNParams( -// bookId = b3.bookId, -// isbn = "NEW ISBN", -// title = "never ever gonna finish, a quatrain", -// tags = listOf("someother") -// ) -// ) + db.updateBookISBN( + UpdateBookISBNParams( + bookId = b3.bookId, + isbn = "NEW ISBN", + title = "never ever gonna finish, a quatrain", + tags = listOf("someother") + ) + ) val books0 = db.booksByTitleYear(BooksByTitleYearParams("my book title", 2016)) diff --git a/examples/kotlin/src/test/kotlin/com/example/ondeck/QueriesImplTest.kt b/examples/kotlin/src/test/kotlin/com/example/ondeck/QueriesImplTest.kt index 8a2ff68dce..fdd1933862 100644 --- a/examples/kotlin/src/test/kotlin/com/example/ondeck/QueriesImplTest.kt +++ b/examples/kotlin/src/test/kotlin/com/example/ondeck/QueriesImplTest.kt @@ -43,9 +43,10 @@ class QueriesImplTest { assertEquals(listOf(city), q.listCities()) assertEquals(listOf(venue), q.listVenues(city.slug)) - // These updates fail because parameters are not in sequential order. After changing $N to ?, ordering is lost, - // and the parameters are filled into the wrong slots. -// q.updateCityName(UpdateCityNameParams(slug = city.slug, name = "SF")) -// q.updateVenueName(UpdateVenueNameParams(slug = venue.slug, name = "Fillmore")) + q.updateCityName(UpdateCityNameParams(slug = city.slug, name = "SF")) + val id = q.updateVenueName(UpdateVenueNameParams(slug = venue.slug, name = "Fillmore")) + assertEquals(venue.id, id) + + q.deleteVenue(venue.slug) } } \ No newline at end of file diff --git a/internal/dinosql/config.go b/internal/dinosql/config.go index 4f267f3946..8ae8c6b76f 100644 --- a/internal/dinosql/config.go +++ b/internal/dinosql/config.go @@ -208,6 +208,8 @@ func ParseConfig(rd io.Reader) (GenerateSettings, error) { } if config.Packages[j].Language == "" { config.Packages[j].Language = LanguageGo + } else if config.Packages[j].Language == "kotlin" { + config.Packages[j].rewriteParams = true } } return config, nil diff --git a/internal/dinosql/gen.go b/internal/dinosql/gen.go index 325eed5dbe..4a20999022 100644 --- a/internal/dinosql/gen.go +++ b/internal/dinosql/gen.go @@ -719,6 +719,11 @@ func (r Result) goInnerType(col core.Column, settings CombinedSettings) string { } } +type goColumn struct { + id int + core.Column +} + // It's possible that this method will generate duplicate JSON tag values // // Columns: count, count, count_2 @@ -726,21 +731,31 @@ func (r Result) goInnerType(col core.Column, settings CombinedSettings) string { // JSON tags: count, count_2, count_2 // // This is unlikely to happen, so don't fix it yet -func (r Result) columnsToStruct(name string, columns []core.Column, settings CombinedSettings) *GoStruct { +func (r Result) columnsToStruct(name string, columns []goColumn, settings CombinedSettings) *GoStruct { gs := GoStruct{ Name: name, } seen := map[string]int{} + suffixes := map[int]int{} for i, c := range columns { tagName := c.Name - fieldName := StructName(columnName(c, i), settings) - if v := seen[c.Name]; v > 0 { - tagName = fmt.Sprintf("%s_%d", tagName, v+1) - fieldName = fmt.Sprintf("%s_%d", fieldName, v+1) + fieldName := StructName(columnName(c.Column, i), settings) + // Track suffixes by the ID of the column, so that columns referring to the same numbered parameter can be + // reused. + suffix := 0 + if o, ok := suffixes[c.id]; ok { + suffix = o + } else if v := seen[c.Name]; v > 0 { + suffix = v+1 + } + suffixes[c.id] = suffix + if suffix > 0 { + tagName = fmt.Sprintf("%s_%d", tagName, suffix) + fieldName = fmt.Sprintf("%s_%d", fieldName, suffix) } gs.Fields = append(gs.Fields, GoField{ Name: fieldName, - Type: r.goType(c, settings), + Type: r.goType(c.Column, settings), Tags: map[string]string{"json:": tagName}, }) seen[c.Name]++ @@ -815,9 +830,12 @@ func (r Result) GoQueries(settings CombinedSettings) []GoQuery { Typ: r.goType(p.Column, settings), } } else if len(query.Params) > 1 { - var cols []core.Column + var cols []goColumn for _, p := range query.Params { - cols = append(cols, p.Column) + cols = append(cols, goColumn{ + id: p.Number, + Column: p.Column, + }) } gq.Arg = GoQueryValue{ Emit: true, @@ -858,7 +876,14 @@ func (r Result) GoQueries(settings CombinedSettings) []GoQuery { } if gs == nil { - gs = r.columnsToStruct(gq.MethodName+"Row", query.Columns, settings) + var columns []goColumn + for i, c := range query.Columns { + columns = append(columns, goColumn{ + id: i, + Column: c, + }) + } + gs = r.columnsToStruct(gq.MethodName+"Row", columns, settings) emit = true } gq.Ret = GoQueryValue{ diff --git a/internal/dinosql/kotlin/gen.go b/internal/dinosql/kotlin/gen.go index a6029ecd44..6107622983 100644 --- a/internal/dinosql/kotlin/gen.go +++ b/internal/dinosql/kotlin/gen.go @@ -37,17 +37,19 @@ type KtField struct { } type KtStruct struct { - Table core.FQN - Name string - Fields []KtField - Comment string + Table core.FQN + Name string + Fields []KtField + JDBCParamBindings []KtField + Comment string } type KtQueryValue struct { - Emit bool - Name string - Struct *KtStruct - Typ ktType + Emit bool + Name string + Struct *KtStruct + Typ ktType + JDBCParamBindCount int } func (v KtQueryValue) EmitStruct() bool { @@ -101,9 +103,15 @@ func (v KtQueryValue) Params() string { } var out []string if v.Struct == nil { - out = append(out, jdbcSet(v.Typ, 1, v.Name)) + repeat := 1 + if v.JDBCParamBindCount > 0 { + repeat = v.JDBCParamBindCount + } + for i := 1; i <= repeat; i++ { + out = append(out, jdbcSet(v.Typ, i, v.Name)) + } } else { - for i, f := range v.Struct.Fields { + for i, f := range v.Struct.JDBCParamBindings { out = append(out, jdbcSet(f.Type, i+1, v.Name+"."+f.Name)) } } @@ -595,21 +603,34 @@ func (r Result) ktInnerType(col core.Column, settings dinosql.CombinedSettings) } } -func (r Result) ktColumnsToStruct(name string, columns []core.Column, settings dinosql.CombinedSettings) *KtStruct { +type goColumn struct { + id int + core.Column +} + +func (r Result) ktColumnsToStruct(name string, columns []goColumn, settings dinosql.CombinedSettings) *KtStruct { gs := KtStruct{ Name: name, } - seen := map[string]int{} + idSeen := map[int]KtField{} + nameSeen := map[string]int{} for i, c := range columns { - fieldName := KtMemberName(ktColumnName(c, i), settings) - if v := seen[c.Name]; v > 0 { + if binding, ok := idSeen[c.id]; ok { + gs.JDBCParamBindings = append(gs.JDBCParamBindings, binding) + continue + } + fieldName := KtMemberName(ktColumnName(c.Column, i), settings) + if v := nameSeen[c.Name]; v > 0 { fieldName = fmt.Sprintf("%s_%d", fieldName, v+1) } - gs.Fields = append(gs.Fields, KtField{ + field := KtField{ Name: fieldName, - Type: r.ktType(c, settings), - }) - seen[c.Name]++ + Type: r.ktType(c.Column, settings), + } + gs.Fields = append(gs.Fields, field) + gs.JDBCParamBindings = append(gs.JDBCParamBindings, field) + nameSeen[c.Name]++ + idSeen[c.id] = field } return &gs } @@ -672,21 +693,26 @@ func (r Result) KtQueries(settings dinosql.CombinedSettings) []KtQuery { Comments: query.Comments, } - if len(query.Params) == 1 { + var cols []goColumn + for _, p := range query.Params { + cols = append(cols, goColumn{ + id: p.Number, + Column: p.Column, + }) + } + params := r.ktColumnsToStruct(gq.ClassName+"Params", cols, settings) + if len(params.Fields) == 1 { p := query.Params[0] gq.Arg = KtQueryValue{ - Name: ktParamName(p), - Typ: r.ktType(p.Column, settings), - } - } else if len(query.Params) > 1 { - var cols []core.Column - for _, p := range query.Params { - cols = append(cols, p.Column) + Name: ktParamName(p), + Typ: r.ktType(p.Column, settings), + JDBCParamBindCount: len(params.JDBCParamBindings), } + } else if len(params.Fields) > 1 { gq.Arg = KtQueryValue{ Emit: true, Name: "arg", - Struct: r.ktColumnsToStruct(gq.ClassName+"Params", cols, settings), + Struct: params, } } @@ -722,7 +748,14 @@ func (r Result) KtQueries(settings dinosql.CombinedSettings) []KtQuery { } if gs == nil { - gs = r.ktColumnsToStruct(gq.ClassName+"Row", query.Columns, settings) + var columns []goColumn + for i, c := range query.Columns { + columns = append(columns, goColumn{ + id: i, + Column: c, + }) + } + gs = r.ktColumnsToStruct(gq.ClassName+"Row", columns, settings) emit = true } gq.Ret = KtQueryValue{ diff --git a/internal/dinosql/parser_test.go b/internal/dinosql/parser_test.go index e53c59955c..d1741cc6be 100644 --- a/internal/dinosql/parser_test.go +++ b/internal/dinosql/parser_test.go @@ -3,6 +3,7 @@ package dinosql import ( "testing" + "github.com/google/go-cmp/cmp" pg "github.com/lfittl/pg_query_go" nodes "github.com/lfittl/pg_query_go/nodes" ) @@ -87,13 +88,14 @@ func TestLineColumn(t *testing.T) { func TestExtractArgs(t *testing.T) { queries := []struct { - query string - count int + query string + bindNumbers []int }{ - {"SELECT * FROM venue WHERE slug = $1 AND city = $2", 2}, - {"SELECT * FROM venue WHERE slug = $1", 1}, - {"SELECT * FROM venue LIMIT $1", 1}, - {"SELECT * FROM venue OFFSET $1", 1}, + {"SELECT * FROM venue WHERE slug = $1 AND city = $2", []int{1, 2}}, + {"SELECT * FROM venue WHERE slug = $1 AND region = $2 AND city = $3 AND country = $2", []int{1, 2, 3, 2}}, + {"SELECT * FROM venue WHERE slug = $1", []int{1}}, + {"SELECT * FROM venue LIMIT $1", []int{1}}, + {"SELECT * FROM venue OFFSET $1", []int{1}}, } for _, q := range queries { tree, err := pg.Parse(q.query) @@ -105,8 +107,46 @@ func TestExtractArgs(t *testing.T) { if err != nil { t.Error(err) } - if len(refs) != q.count { - t.Errorf("expected %d refs, got %d", q.count, len(refs)) + nums := make([]int, len(refs)) + for i, n := range refs { + nums[i] = n.ref.Number + } + if diff := cmp.Diff(q.bindNumbers, nums); diff != "" { + t.Errorf("expected bindings %v, got %v", q.bindNumbers, nums) + } + } + } +} + +func TestRewriteParameters(t *testing.T) { + queries := []struct { + orig string + new string + }{ + {"SELECT * FROM venue WHERE slug = $1 AND city = $3 AND bar = $2", "SELECT * FROM venue WHERE slug = ? AND city = ? AND bar = ?"}, + {"DELETE FROM venue WHERE slug = $1 AND slug = $1", "DELETE FROM venue WHERE slug = ? AND slug = ?"}, + {"SELECT * FROM venue LIMIT $1", "SELECT * FROM venue LIMIT ?"}, + } + for _, q := range queries { + tree, err := pg.Parse(q.orig) + if err != nil { + t.Fatal(err) + } + for _, stmt := range tree.Statements { + refs := findParameters(stmt) + if err != nil { + t.Error(err) + } + edits, err := rewriteNumberedParameters(refs, stmt.(nodes.RawStmt), q.orig) + if err != nil { + t.Error(err) + } + rewritten, err := editQuery(q.orig, edits) + if err != nil { + t.Error(err) + } + if rewritten != q.new { + t.Errorf("expected %q, got %q", q.new, rewritten) } } } From bfa6704a8a364eb89d0fa46034da10be4701af1e Mon Sep 17 00:00:00 2001 From: Yunchi Luo Date: Wed, 29 Jan 2020 15:34:22 -0500 Subject: [PATCH 14/20] kotlin: always use use, fix indents --- .../kotlin/com/example/authors/QueriesImpl.kt | 52 +++--- .../booktest/postgresql/QueriesImpl.kt | 166 +++++++++--------- .../kotlin/com/example/jets/QueriesImpl.kt | 26 +-- .../kotlin/com/example/ondeck/QueriesImpl.kt | 156 ++++++++-------- internal/dinosql/kotlin/gen.go | 65 ++++--- 5 files changed, 240 insertions(+), 225 deletions(-) diff --git a/examples/kotlin/src/main/kotlin/com/example/authors/QueriesImpl.kt b/examples/kotlin/src/main/kotlin/com/example/authors/QueriesImpl.kt index 276de49970..80dd14284e 100644 --- a/examples/kotlin/src/main/kotlin/com/example/authors/QueriesImpl.kt +++ b/examples/kotlin/src/main/kotlin/com/example/authors/QueriesImpl.kt @@ -38,19 +38,19 @@ class QueriesImpl(private val conn: Connection) { @Throws(SQLException::class) fun createAuthor(arg: CreateAuthorParams): Author { - val stmt = conn.prepareStatement(createAuthor) - stmt.setString(1, arg.name) - stmt.setString(2, arg.bio) + return conn.prepareStatement(createAuthor).use { stmt -> + stmt.setString(1, arg.name) + stmt.setString(2, arg.bio) - return stmt.executeQuery().use { results -> + val results = stmt.executeQuery() if (!results.next()) { throw SQLException("no rows in result set") } val ret = Author( - results.getLong(1), - results.getString(2), - results.getString(3) - ) + results.getLong(1), + results.getString(2), + results.getString(3) + ) if (results.next()) { throw SQLException("expected one row in result set, but got many") } @@ -60,27 +60,27 @@ class QueriesImpl(private val conn: Connection) { @Throws(SQLException::class) fun deleteAuthor(id: Long) { - val stmt = conn.prepareStatement(deleteAuthor) - stmt.setLong(1, id) + conn.prepareStatement(deleteAuthor).use { stmt -> + stmt.setLong(1, id) - stmt.execute() - stmt.close() + stmt.execute() + } } @Throws(SQLException::class) fun getAuthor(id: Long): Author { - val stmt = conn.prepareStatement(getAuthor) - stmt.setLong(1, id) + return conn.prepareStatement(getAuthor).use { stmt -> + stmt.setLong(1, id) - return stmt.executeQuery().use { results -> + val results = stmt.executeQuery() if (!results.next()) { throw SQLException("no rows in result set") } val ret = Author( - results.getLong(1), - results.getString(2), - results.getString(3) - ) + results.getLong(1), + results.getString(2), + results.getString(3) + ) if (results.next()) { throw SQLException("expected one row in result set, but got many") } @@ -90,16 +90,16 @@ class QueriesImpl(private val conn: Connection) { @Throws(SQLException::class) fun listAuthors(): List { - val stmt = conn.prepareStatement(listAuthors) - - return stmt.executeQuery().use { results -> + return conn.prepareStatement(listAuthors).use { stmt -> + + val results = stmt.executeQuery() val ret = mutableListOf() while (results.next()) { ret.add(Author( - results.getLong(1), - results.getString(2), - results.getString(3) - )) + results.getLong(1), + results.getString(2), + results.getString(3) + )) } ret } diff --git a/examples/kotlin/src/main/kotlin/com/example/booktest/postgresql/QueriesImpl.kt b/examples/kotlin/src/main/kotlin/com/example/booktest/postgresql/QueriesImpl.kt index 281ba0dd66..27a7c28602 100644 --- a/examples/kotlin/src/main/kotlin/com/example/booktest/postgresql/QueriesImpl.kt +++ b/examples/kotlin/src/main/kotlin/com/example/booktest/postgresql/QueriesImpl.kt @@ -117,19 +117,19 @@ class QueriesImpl(private val conn: Connection) { @Throws(SQLException::class) fun booksByTags(dollar_1: List): List { - val stmt = conn.prepareStatement(booksByTags) - stmt.setArray(1, conn.createArrayOf("pg_catalog.varchar", dollar_1.toTypedArray())) + return conn.prepareStatement(booksByTags).use { stmt -> + stmt.setArray(1, conn.createArrayOf("pg_catalog.varchar", dollar_1.toTypedArray())) - return stmt.executeQuery().use { results -> + val results = stmt.executeQuery() val ret = mutableListOf() while (results.next()) { ret.add(BooksByTagsRow( - results.getInt(1), - results.getString(2), - results.getString(3), - results.getString(4), - (results.getArray(5).array as Array).toList() - )) + results.getInt(1), + results.getString(2), + results.getString(3), + results.getString(4), + (results.getArray(5).array as Array).toList() + )) } ret } @@ -137,23 +137,23 @@ class QueriesImpl(private val conn: Connection) { @Throws(SQLException::class) fun booksByTitleYear(arg: BooksByTitleYearParams): List { - val stmt = conn.prepareStatement(booksByTitleYear) - stmt.setString(1, arg.title) - stmt.setInt(2, arg.year) + return conn.prepareStatement(booksByTitleYear).use { stmt -> + stmt.setString(1, arg.title) + stmt.setInt(2, arg.year) - return stmt.executeQuery().use { results -> + val results = stmt.executeQuery() val ret = mutableListOf() while (results.next()) { ret.add(Book( - results.getInt(1), - results.getInt(2), - results.getString(3), - BookType.lookup(results.getString(4))!!, - results.getString(5), - results.getInt(6), - results.getObject(7, OffsetDateTime::class.java), - (results.getArray(8).array as Array).toList() - )) + results.getInt(1), + results.getInt(2), + results.getString(3), + BookType.lookup(results.getString(4))!!, + results.getString(5), + results.getInt(6), + results.getObject(7, OffsetDateTime::class.java), + (results.getArray(8).array as Array).toList() + )) } ret } @@ -161,17 +161,17 @@ class QueriesImpl(private val conn: Connection) { @Throws(SQLException::class) fun createAuthor(name: String): Author { - val stmt = conn.prepareStatement(createAuthor) - stmt.setString(1, name) + return conn.prepareStatement(createAuthor).use { stmt -> + stmt.setString(1, name) - return stmt.executeQuery().use { results -> + val results = stmt.executeQuery() if (!results.next()) { throw SQLException("no rows in result set") } val ret = Author( - results.getInt(1), - results.getString(2) - ) + results.getInt(1), + results.getString(2) + ) if (results.next()) { throw SQLException("expected one row in result set, but got many") } @@ -181,29 +181,29 @@ class QueriesImpl(private val conn: Connection) { @Throws(SQLException::class) fun createBook(arg: CreateBookParams): Book { - val stmt = conn.prepareStatement(createBook) - stmt.setInt(1, arg.authorId) - stmt.setString(2, arg.isbn) - stmt.setObject(3, arg.booktype.value, Types.OTHER) - stmt.setString(4, arg.title) - stmt.setInt(5, arg.year) - stmt.setObject(6, arg.available) - stmt.setArray(7, conn.createArrayOf("pg_catalog.varchar", arg.tags.toTypedArray())) - - return stmt.executeQuery().use { results -> + return conn.prepareStatement(createBook).use { stmt -> + stmt.setInt(1, arg.authorId) + stmt.setString(2, arg.isbn) + stmt.setObject(3, arg.booktype.value, Types.OTHER) + stmt.setString(4, arg.title) + stmt.setInt(5, arg.year) + stmt.setObject(6, arg.available) + stmt.setArray(7, conn.createArrayOf("pg_catalog.varchar", arg.tags.toTypedArray())) + + val results = stmt.executeQuery() if (!results.next()) { throw SQLException("no rows in result set") } val ret = Book( - results.getInt(1), - results.getInt(2), - results.getString(3), - BookType.lookup(results.getString(4))!!, - results.getString(5), - results.getInt(6), - results.getObject(7, OffsetDateTime::class.java), - (results.getArray(8).array as Array).toList() - ) + results.getInt(1), + results.getInt(2), + results.getString(3), + BookType.lookup(results.getString(4))!!, + results.getString(5), + results.getInt(6), + results.getObject(7, OffsetDateTime::class.java), + (results.getArray(8).array as Array).toList() + ) if (results.next()) { throw SQLException("expected one row in result set, but got many") } @@ -213,26 +213,26 @@ class QueriesImpl(private val conn: Connection) { @Throws(SQLException::class) fun deleteBook(bookId: Int) { - val stmt = conn.prepareStatement(deleteBook) - stmt.setInt(1, bookId) + conn.prepareStatement(deleteBook).use { stmt -> + stmt.setInt(1, bookId) - stmt.execute() - stmt.close() + stmt.execute() + } } @Throws(SQLException::class) fun getAuthor(authorId: Int): Author { - val stmt = conn.prepareStatement(getAuthor) - stmt.setInt(1, authorId) + return conn.prepareStatement(getAuthor).use { stmt -> + stmt.setInt(1, authorId) - return stmt.executeQuery().use { results -> + val results = stmt.executeQuery() if (!results.next()) { throw SQLException("no rows in result set") } val ret = Author( - results.getInt(1), - results.getString(2) - ) + results.getInt(1), + results.getString(2) + ) if (results.next()) { throw SQLException("expected one row in result set, but got many") } @@ -242,23 +242,23 @@ class QueriesImpl(private val conn: Connection) { @Throws(SQLException::class) fun getBook(bookId: Int): Book { - val stmt = conn.prepareStatement(getBook) - stmt.setInt(1, bookId) + return conn.prepareStatement(getBook).use { stmt -> + stmt.setInt(1, bookId) - return stmt.executeQuery().use { results -> + val results = stmt.executeQuery() if (!results.next()) { throw SQLException("no rows in result set") } val ret = Book( - results.getInt(1), - results.getInt(2), - results.getString(3), - BookType.lookup(results.getString(4))!!, - results.getString(5), - results.getInt(6), - results.getObject(7, OffsetDateTime::class.java), - (results.getArray(8).array as Array).toList() - ) + results.getInt(1), + results.getInt(2), + results.getString(3), + BookType.lookup(results.getString(4))!!, + results.getString(5), + results.getInt(6), + results.getObject(7, OffsetDateTime::class.java), + (results.getArray(8).array as Array).toList() + ) if (results.next()) { throw SQLException("expected one row in result set, but got many") } @@ -268,25 +268,25 @@ class QueriesImpl(private val conn: Connection) { @Throws(SQLException::class) fun updateBook(arg: UpdateBookParams) { - val stmt = conn.prepareStatement(updateBook) - stmt.setString(1, arg.title) - stmt.setArray(2, conn.createArrayOf("pg_catalog.varchar", arg.tags.toTypedArray())) - stmt.setInt(3, arg.bookId) + conn.prepareStatement(updateBook).use { stmt -> + stmt.setString(1, arg.title) + stmt.setArray(2, conn.createArrayOf("pg_catalog.varchar", arg.tags.toTypedArray())) + stmt.setInt(3, arg.bookId) - stmt.execute() - stmt.close() + stmt.execute() + } } @Throws(SQLException::class) fun updateBookISBN(arg: UpdateBookISBNParams) { - val stmt = conn.prepareStatement(updateBookISBN) - stmt.setString(1, arg.title) - stmt.setArray(2, conn.createArrayOf("pg_catalog.varchar", arg.tags.toTypedArray())) - stmt.setString(3, arg.isbn) - stmt.setInt(4, arg.bookId) - - stmt.execute() - stmt.close() + conn.prepareStatement(updateBookISBN).use { stmt -> + stmt.setString(1, arg.title) + stmt.setArray(2, conn.createArrayOf("pg_catalog.varchar", arg.tags.toTypedArray())) + stmt.setString(3, arg.isbn) + stmt.setInt(4, arg.bookId) + + stmt.execute() + } } } diff --git a/examples/kotlin/src/main/kotlin/com/example/jets/QueriesImpl.kt b/examples/kotlin/src/main/kotlin/com/example/jets/QueriesImpl.kt index 35f60b681c..04ab619fca 100644 --- a/examples/kotlin/src/main/kotlin/com/example/jets/QueriesImpl.kt +++ b/examples/kotlin/src/main/kotlin/com/example/jets/QueriesImpl.kt @@ -21,9 +21,9 @@ class QueriesImpl(private val conn: Connection) { @Throws(SQLException::class) fun countPilots(): Long { - val stmt = conn.prepareStatement(countPilots) - - return stmt.executeQuery().use { results -> + return conn.prepareStatement(countPilots).use { stmt -> + + val results = stmt.executeQuery() if (!results.next()) { throw SQLException("no rows in result set") } @@ -37,24 +37,24 @@ class QueriesImpl(private val conn: Connection) { @Throws(SQLException::class) fun deletePilot(id: Int) { - val stmt = conn.prepareStatement(deletePilot) - stmt.setInt(1, id) + conn.prepareStatement(deletePilot).use { stmt -> + stmt.setInt(1, id) - stmt.execute() - stmt.close() + stmt.execute() + } } @Throws(SQLException::class) fun listPilots(): List { - val stmt = conn.prepareStatement(listPilots) - - return stmt.executeQuery().use { results -> + return conn.prepareStatement(listPilots).use { stmt -> + + val results = stmt.executeQuery() val ret = mutableListOf() while (results.next()) { ret.add(Pilot( - results.getInt(1), - results.getString(2) - )) + results.getInt(1), + results.getString(2) + )) } ret } diff --git a/examples/kotlin/src/main/kotlin/com/example/ondeck/QueriesImpl.kt b/examples/kotlin/src/main/kotlin/com/example/ondeck/QueriesImpl.kt index 7b7911af70..029e7eae1c 100644 --- a/examples/kotlin/src/main/kotlin/com/example/ondeck/QueriesImpl.kt +++ b/examples/kotlin/src/main/kotlin/com/example/ondeck/QueriesImpl.kt @@ -134,18 +134,18 @@ class QueriesImpl(private val conn: Connection) : Queries { @Throws(SQLException::class) override fun createCity(arg: CreateCityParams): City { - val stmt = conn.prepareStatement(createCity) - stmt.setString(1, arg.name) - stmt.setString(2, arg.slug) + return conn.prepareStatement(createCity).use { stmt -> + stmt.setString(1, arg.name) + stmt.setString(2, arg.slug) - return stmt.executeQuery().use { results -> + val results = stmt.executeQuery() if (!results.next()) { throw SQLException("no rows in result set") } val ret = City( - results.getString(1), - results.getString(2) - ) + results.getString(1), + results.getString(2) + ) if (results.next()) { throw SQLException("expected one row in result set, but got many") } @@ -155,16 +155,16 @@ class QueriesImpl(private val conn: Connection) : Queries { @Throws(SQLException::class) override fun createVenue(arg: CreateVenueParams): Int { - val stmt = conn.prepareStatement(createVenue) - stmt.setString(1, arg.slug) - stmt.setString(2, arg.name) - stmt.setString(3, arg.city) - stmt.setString(4, arg.spotifyPlaylist) - stmt.setObject(5, arg.status.value, Types.OTHER) - stmt.setArray(6, conn.createArrayOf("status", arg.statuses.map { v -> v.value }.toTypedArray())) - stmt.setArray(7, conn.createArrayOf("text", arg.tags.toTypedArray())) - - return stmt.executeQuery().use { results -> + return conn.prepareStatement(createVenue).use { stmt -> + stmt.setString(1, arg.slug) + stmt.setString(2, arg.name) + stmt.setString(3, arg.city) + stmt.setString(4, arg.spotifyPlaylist) + stmt.setObject(5, arg.status.value, Types.OTHER) + stmt.setArray(6, conn.createArrayOf("status", arg.statuses.map { v -> v.value }.toTypedArray())) + stmt.setArray(7, conn.createArrayOf("text", arg.tags.toTypedArray())) + + val results = stmt.executeQuery() if (!results.next()) { throw SQLException("no rows in result set") } @@ -178,27 +178,27 @@ class QueriesImpl(private val conn: Connection) : Queries { @Throws(SQLException::class) override fun deleteVenue(slug: String) { - val stmt = conn.prepareStatement(deleteVenue) - stmt.setString(1, slug) - stmt.setString(2, slug) + conn.prepareStatement(deleteVenue).use { stmt -> + stmt.setString(1, slug) + stmt.setString(2, slug) - stmt.execute() - stmt.close() + stmt.execute() + } } @Throws(SQLException::class) override fun getCity(slug: String): City { - val stmt = conn.prepareStatement(getCity) - stmt.setString(1, slug) + return conn.prepareStatement(getCity).use { stmt -> + stmt.setString(1, slug) - return stmt.executeQuery().use { results -> + val results = stmt.executeQuery() if (!results.next()) { throw SQLException("no rows in result set") } val ret = City( - results.getString(1), - results.getString(2) - ) + results.getString(1), + results.getString(2) + ) if (results.next()) { throw SQLException("expected one row in result set, but got many") } @@ -208,26 +208,26 @@ class QueriesImpl(private val conn: Connection) : Queries { @Throws(SQLException::class) override fun getVenue(arg: GetVenueParams): Venue { - val stmt = conn.prepareStatement(getVenue) - stmt.setString(1, arg.slug) - stmt.setString(2, arg.city) + return conn.prepareStatement(getVenue).use { stmt -> + stmt.setString(1, arg.slug) + stmt.setString(2, arg.city) - return stmt.executeQuery().use { results -> + val results = stmt.executeQuery() if (!results.next()) { throw SQLException("no rows in result set") } val ret = Venue( - results.getInt(1), - Status.lookup(results.getString(2))!!, - (results.getArray(3).array as Array).map { v -> Status.lookup(v)!! }.toList(), - results.getString(4), - results.getString(5), - results.getString(6), - results.getString(7), - results.getString(8), - (results.getArray(9).array as Array).toList(), - results.getObject(10, LocalDateTime::class.java) - ) + results.getInt(1), + Status.lookup(results.getString(2))!!, + (results.getArray(3).array as Array).map { v -> Status.lookup(v)!! }.toList(), + results.getString(4), + results.getString(5), + results.getString(6), + results.getString(7), + results.getString(8), + (results.getArray(9).array as Array).toList(), + results.getObject(10, LocalDateTime::class.java) + ) if (results.next()) { throw SQLException("expected one row in result set, but got many") } @@ -237,15 +237,15 @@ class QueriesImpl(private val conn: Connection) : Queries { @Throws(SQLException::class) override fun listCities(): List { - val stmt = conn.prepareStatement(listCities) - - return stmt.executeQuery().use { results -> + return conn.prepareStatement(listCities).use { stmt -> + + val results = stmt.executeQuery() val ret = mutableListOf() while (results.next()) { ret.add(City( - results.getString(1), - results.getString(2) - )) + results.getString(1), + results.getString(2) + )) } ret } @@ -253,24 +253,24 @@ class QueriesImpl(private val conn: Connection) : Queries { @Throws(SQLException::class) override fun listVenues(city: String): List { - val stmt = conn.prepareStatement(listVenues) - stmt.setString(1, city) + return conn.prepareStatement(listVenues).use { stmt -> + stmt.setString(1, city) - return stmt.executeQuery().use { results -> + val results = stmt.executeQuery() val ret = mutableListOf() while (results.next()) { ret.add(Venue( - results.getInt(1), - Status.lookup(results.getString(2))!!, - (results.getArray(3).array as Array).map { v -> Status.lookup(v)!! }.toList(), - results.getString(4), - results.getString(5), - results.getString(6), - results.getString(7), - results.getString(8), - (results.getArray(9).array as Array).toList(), - results.getObject(10, LocalDateTime::class.java) - )) + results.getInt(1), + Status.lookup(results.getString(2))!!, + (results.getArray(3).array as Array).map { v -> Status.lookup(v)!! }.toList(), + results.getString(4), + results.getString(5), + results.getString(6), + results.getString(7), + results.getString(8), + (results.getArray(9).array as Array).toList(), + results.getObject(10, LocalDateTime::class.java) + )) } ret } @@ -278,21 +278,21 @@ class QueriesImpl(private val conn: Connection) : Queries { @Throws(SQLException::class) override fun updateCityName(arg: UpdateCityNameParams) { - val stmt = conn.prepareStatement(updateCityName) - stmt.setString(1, arg.name) - stmt.setString(2, arg.slug) + conn.prepareStatement(updateCityName).use { stmt -> + stmt.setString(1, arg.name) + stmt.setString(2, arg.slug) - stmt.execute() - stmt.close() + stmt.execute() + } } @Throws(SQLException::class) override fun updateVenueName(arg: UpdateVenueNameParams): Int { - val stmt = conn.prepareStatement(updateVenueName) - stmt.setString(1, arg.name) - stmt.setString(2, arg.slug) + return conn.prepareStatement(updateVenueName).use { stmt -> + stmt.setString(1, arg.name) + stmt.setString(2, arg.slug) - return stmt.executeQuery().use { results -> + val results = stmt.executeQuery() if (!results.next()) { throw SQLException("no rows in result set") } @@ -306,15 +306,15 @@ class QueriesImpl(private val conn: Connection) : Queries { @Throws(SQLException::class) override fun venueCountByCity(): List { - val stmt = conn.prepareStatement(venueCountByCity) - - return stmt.executeQuery().use { results -> + return conn.prepareStatement(venueCountByCity).use { stmt -> + + val results = stmt.executeQuery() val ret = mutableListOf() while (results.next()) { ret.add(VenueCountByCityRow( - results.getString(1), - results.getLong(2) - )) + results.getString(1), + results.getLong(2) + )) } ret } diff --git a/internal/dinosql/kotlin/gen.go b/internal/dinosql/kotlin/gen.go index 6107622983..6bae4fa240 100644 --- a/internal/dinosql/kotlin/gen.go +++ b/internal/dinosql/kotlin/gen.go @@ -115,7 +115,7 @@ func (v KtQueryValue) Params() string { out = append(out, jdbcSet(f.Type, i+1, v.Name+"."+f.Name)) } } - return strings.Join(out, "\n ") + return indent(strings.Join(out, "\n"), 6, 0) } func jdbcGet(t ktType, idx int) string { @@ -137,19 +137,35 @@ func jdbcGet(t ktType, idx int) string { func (v KtQueryValue) ResultSet() string { var out []string if v.Struct == nil { - out = append(out, jdbcGet(v.Typ, 1)) - } else { - for i, f := range v.Struct.Fields { - out = append(out, jdbcGet(f.Type, i+1)) - } + return jdbcGet(v.Typ, 1) } - ret := strings.Join(out, ",\n ") - if v.Struct != nil { - ret = v.Struct.Name + "(" + "\n " + ret + "\n )" + for i, f := range v.Struct.Fields { + out = append(out, jdbcGet(f.Type, i+1)) } + ret := indent(strings.Join(out, ",\n"), 4, -1) + ret = indent(v.Struct.Name + "(\n" + ret + "\n)", 8, 0) return ret } +func indent(s string, n int, firstIndent int) string { + lines := strings.Split(s, "\n") + buf := bytes.NewBuffer(nil) + for i, l := range lines { + indent := n + if i == 0 && firstIndent != -1 { + indent = firstIndent + } + if i != 0 { + buf.WriteRune('\n') + } + for i := 0; i < indent; i++ { + buf.WriteRune(' ') + } + buf.WriteString(l) + } + return buf.String() +} + // A struct used to generate methods and fields on the Queries struct type KtQuery struct { ClassName string @@ -875,10 +891,10 @@ class QueriesImpl(private val conn: Connection){{ if .EmitInterface }} : Queries @Throws(SQLException::class) {{ if $.EmitInterface }}override {{ end -}} fun {{.MethodName}}({{.Arg.Pair}}): {{.Ret.Type}} { - val stmt = conn.prepareStatement({{.ConstantName}}) - {{.Arg.Params}} + return conn.prepareStatement({{.ConstantName}}).use { stmt -> + {{.Arg.Params}} - return stmt.executeQuery().use { results -> + val results = stmt.executeQuery() if (!results.next()) { throw SQLException("no rows in result set") } @@ -897,10 +913,10 @@ class QueriesImpl(private val conn: Connection){{ if .EmitInterface }} : Queries @Throws(SQLException::class) {{ if $.EmitInterface }}override {{ end -}} fun {{.MethodName}}({{.Arg.Pair}}): List<{{.Ret.Type}}> { - val stmt = conn.prepareStatement({{.ConstantName}}) - {{.Arg.Params}} + return conn.prepareStatement({{.ConstantName}}).use { stmt -> + {{.Arg.Params}} - return stmt.executeQuery().use { results -> + val results = stmt.executeQuery() val ret = mutableListOf<{{.Ret.Type}}>() while (results.next()) { ret.add({{.Ret.ResultSet}}) @@ -916,11 +932,11 @@ class QueriesImpl(private val conn: Connection){{ if .EmitInterface }} : Queries @Throws(SQLException::class) {{ if $.EmitInterface }}override {{ end -}} fun {{.MethodName}}({{.Arg.Pair}}) { - val stmt = conn.prepareStatement({{.ConstantName}}) - {{ .Arg.Params }} + conn.prepareStatement({{.ConstantName}}).use { stmt -> + {{ .Arg.Params }} - stmt.execute() - stmt.close() + stmt.execute() + } } {{end}} @@ -930,13 +946,12 @@ class QueriesImpl(private val conn: Connection){{ if .EmitInterface }} : Queries @Throws(SQLException::class) {{ if $.EmitInterface }}override {{ end -}} fun {{.MethodName}}({{.Arg.Pair}}): Int { - val stmt = conn.prepareStatement({{.ConstantName}}) - {{ .Arg.Params }} + return conn.prepareStatement({{.ConstantName}}).use { stmt -> + {{ .Arg.Params }} - stmt.execute() - val count = stmt.updateCount - stmt.close() - return count + stmt.execute() + stmt.updateCount + } } {{end}} {{end}} From 0f04fd30dfaeada6b7fd10a6036db2b81082bc5e Mon Sep 17 00:00:00 2001 From: Yunchi Luo Date: Fri, 31 Jan 2020 08:58:48 -0500 Subject: [PATCH 15/20] kotlin: unbox query params --- .../kotlin/com/example/authors/QueriesImpl.kt | 11 +- .../booktest/postgresql/QueriesImpl.kt | 86 +++++------ .../main/kotlin/com/example/ondeck/Queries.kt | 17 ++- .../kotlin/com/example/ondeck/QueriesImpl.kt | 77 ++++------ .../com/example/authors/QueriesImplTest.kt | 26 ++-- .../booktest/postgresql/QueriesImplTest.kt | 14 +- .../com/example/ondeck/QueriesImplTest.kt | 10 +- internal/dinosql/kotlin/gen.go | 142 ++++++++---------- 8 files changed, 154 insertions(+), 229 deletions(-) diff --git a/examples/kotlin/src/main/kotlin/com/example/authors/QueriesImpl.kt b/examples/kotlin/src/main/kotlin/com/example/authors/QueriesImpl.kt index 80dd14284e..9ee42a6abf 100644 --- a/examples/kotlin/src/main/kotlin/com/example/authors/QueriesImpl.kt +++ b/examples/kotlin/src/main/kotlin/com/example/authors/QueriesImpl.kt @@ -14,11 +14,6 @@ INSERT INTO authors ( RETURNING id, name, bio """ -data class CreateAuthorParams ( - val name: String, - val bio: String? -) - const val deleteAuthor = """-- name: deleteAuthor :exec DELETE FROM authors WHERE id = ? @@ -37,10 +32,10 @@ ORDER BY name class QueriesImpl(private val conn: Connection) { @Throws(SQLException::class) - fun createAuthor(arg: CreateAuthorParams): Author { + fun createAuthor(name: String, bio: String?): Author { return conn.prepareStatement(createAuthor).use { stmt -> - stmt.setString(1, arg.name) - stmt.setString(2, arg.bio) + stmt.setString(1, name) + stmt.setString(2, bio) val results = stmt.executeQuery() if (!results.next()) { diff --git a/examples/kotlin/src/main/kotlin/com/example/booktest/postgresql/QueriesImpl.kt b/examples/kotlin/src/main/kotlin/com/example/booktest/postgresql/QueriesImpl.kt index 27a7c28602..f6e13ab8db 100644 --- a/examples/kotlin/src/main/kotlin/com/example/booktest/postgresql/QueriesImpl.kt +++ b/examples/kotlin/src/main/kotlin/com/example/booktest/postgresql/QueriesImpl.kt @@ -32,11 +32,6 @@ SELECT book_id, author_id, isbn, booktype, title, year, available, tags FROM boo WHERE title = ? AND year = ? """ -data class BooksByTitleYearParams ( - val title: String, - val year: Int -) - const val createAuthor = """-- name: createAuthor :one INSERT INTO authors (name) VALUES (?) RETURNING author_id, name @@ -63,16 +58,6 @@ INSERT INTO books ( RETURNING book_id, author_id, isbn, booktype, title, year, available, tags """ -data class CreateBookParams ( - val authorId: Int, - val isbn: String, - val booktype: BookType, - val title: String, - val year: Int, - val available: OffsetDateTime, - val tags: List -) - const val deleteBook = """-- name: deleteBook :exec DELETE FROM books WHERE book_id = ? @@ -94,31 +79,18 @@ SET title = ?, tags = ? WHERE book_id = ? """ -data class UpdateBookParams ( - val title: String, - val tags: List, - val bookId: Int -) - const val updateBookISBN = """-- name: updateBookISBN :exec UPDATE books SET title = ?, tags = ?, isbn = ? WHERE book_id = ? """ -data class UpdateBookISBNParams ( - val title: String, - val tags: List, - val isbn: String, - val bookId: Int -) - class QueriesImpl(private val conn: Connection) { @Throws(SQLException::class) - fun booksByTags(dollar_1: List): List { + fun booksByTags(dollar1: List): List { return conn.prepareStatement(booksByTags).use { stmt -> - stmt.setArray(1, conn.createArrayOf("pg_catalog.varchar", dollar_1.toTypedArray())) + stmt.setArray(1, conn.createArrayOf("pg_catalog.varchar", dollar1.toTypedArray())) val results = stmt.executeQuery() val ret = mutableListOf() @@ -136,10 +108,10 @@ class QueriesImpl(private val conn: Connection) { } @Throws(SQLException::class) - fun booksByTitleYear(arg: BooksByTitleYearParams): List { + fun booksByTitleYear(title: String, year: Int): List { return conn.prepareStatement(booksByTitleYear).use { stmt -> - stmt.setString(1, arg.title) - stmt.setInt(2, arg.year) + stmt.setString(1, title) + stmt.setInt(2, year) val results = stmt.executeQuery() val ret = mutableListOf() @@ -180,15 +152,22 @@ class QueriesImpl(private val conn: Connection) { } @Throws(SQLException::class) - fun createBook(arg: CreateBookParams): Book { + fun createBook( + authorId: Int, + isbn: String, + booktype: BookType, + title: String, + year: Int, + available: OffsetDateTime, + tags: List): Book { return conn.prepareStatement(createBook).use { stmt -> - stmt.setInt(1, arg.authorId) - stmt.setString(2, arg.isbn) - stmt.setObject(3, arg.booktype.value, Types.OTHER) - stmt.setString(4, arg.title) - stmt.setInt(5, arg.year) - stmt.setObject(6, arg.available) - stmt.setArray(7, conn.createArrayOf("pg_catalog.varchar", arg.tags.toTypedArray())) + stmt.setInt(1, authorId) + stmt.setString(2, isbn) + stmt.setObject(3, booktype.value, Types.OTHER) + stmt.setString(4, title) + stmt.setInt(5, year) + stmt.setObject(6, available) + stmt.setArray(7, conn.createArrayOf("pg_catalog.varchar", tags.toTypedArray())) val results = stmt.executeQuery() if (!results.next()) { @@ -267,23 +246,30 @@ class QueriesImpl(private val conn: Connection) { } @Throws(SQLException::class) - fun updateBook(arg: UpdateBookParams) { + fun updateBook( + title: String, + tags: List, + bookId: Int) { conn.prepareStatement(updateBook).use { stmt -> - stmt.setString(1, arg.title) - stmt.setArray(2, conn.createArrayOf("pg_catalog.varchar", arg.tags.toTypedArray())) - stmt.setInt(3, arg.bookId) + stmt.setString(1, title) + stmt.setArray(2, conn.createArrayOf("pg_catalog.varchar", tags.toTypedArray())) + stmt.setInt(3, bookId) stmt.execute() } } @Throws(SQLException::class) - fun updateBookISBN(arg: UpdateBookISBNParams) { + fun updateBookISBN( + title: String, + tags: List, + isbn: String, + bookId: Int) { conn.prepareStatement(updateBookISBN).use { stmt -> - stmt.setString(1, arg.title) - stmt.setArray(2, conn.createArrayOf("pg_catalog.varchar", arg.tags.toTypedArray())) - stmt.setString(3, arg.isbn) - stmt.setInt(4, arg.bookId) + stmt.setString(1, title) + stmt.setArray(2, conn.createArrayOf("pg_catalog.varchar", tags.toTypedArray())) + stmt.setString(3, isbn) + stmt.setInt(4, bookId) stmt.execute() } diff --git a/examples/kotlin/src/main/kotlin/com/example/ondeck/Queries.kt b/examples/kotlin/src/main/kotlin/com/example/ondeck/Queries.kt index c67fd0b83f..69debb00a7 100644 --- a/examples/kotlin/src/main/kotlin/com/example/ondeck/Queries.kt +++ b/examples/kotlin/src/main/kotlin/com/example/ondeck/Queries.kt @@ -9,10 +9,17 @@ import java.time.LocalDateTime interface Queries { @Throws(SQLException::class) - fun createCity(arg: CreateCityParams): City + fun createCity(name: String, slug: String): City @Throws(SQLException::class) - fun createVenue(arg: CreateVenueParams): Int + fun createVenue( + slug: String, + name: String, + city: String, + spotifyPlaylist: String, + status: Status, + statuses: List, + tags: List): Int @Throws(SQLException::class) fun deleteVenue(slug: String) @@ -21,7 +28,7 @@ interface Queries { fun getCity(slug: String): City @Throws(SQLException::class) - fun getVenue(arg: GetVenueParams): Venue + fun getVenue(slug: String, city: String): Venue @Throws(SQLException::class) fun listCities(): List @@ -30,10 +37,10 @@ interface Queries { fun listVenues(city: String): List @Throws(SQLException::class) - fun updateCityName(arg: UpdateCityNameParams) + fun updateCityName(name: String, slug: String) @Throws(SQLException::class) - fun updateVenueName(arg: UpdateVenueNameParams): Int + fun updateVenueName(name: String, slug: String): Int @Throws(SQLException::class) fun venueCountByCity(): List diff --git a/examples/kotlin/src/main/kotlin/com/example/ondeck/QueriesImpl.kt b/examples/kotlin/src/main/kotlin/com/example/ondeck/QueriesImpl.kt index 029e7eae1c..774b67cb4d 100644 --- a/examples/kotlin/src/main/kotlin/com/example/ondeck/QueriesImpl.kt +++ b/examples/kotlin/src/main/kotlin/com/example/ondeck/QueriesImpl.kt @@ -17,11 +17,6 @@ INSERT INTO city ( ) RETURNING slug, name """ -data class CreateCityParams ( - val name: String, - val slug: String -) - const val createVenue = """-- name: createVenue :one INSERT INTO venue ( slug, @@ -44,16 +39,6 @@ INSERT INTO venue ( ) RETURNING id """ -data class CreateVenueParams ( - val slug: String, - val name: String, - val city: String, - val spotifyPlaylist: String, - val status: Status, - val statuses: List, - val tags: List -) - const val deleteVenue = """-- name: deleteVenue :exec DELETE FROM venue WHERE slug = ? AND slug = ? @@ -71,11 +56,6 @@ FROM venue WHERE slug = ? AND city = ? """ -data class GetVenueParams ( - val slug: String, - val city: String -) - const val listCities = """-- name: listCities :many SELECT slug, name FROM city @@ -95,11 +75,6 @@ SET name = ? WHERE slug = ? """ -data class UpdateCityNameParams ( - val name: String, - val slug: String -) - const val updateVenueName = """-- name: updateVenueName :one UPDATE venue SET name = ? @@ -107,11 +82,6 @@ WHERE slug = ? RETURNING id """ -data class UpdateVenueNameParams ( - val name: String, - val slug: String -) - const val venueCountByCity = """-- name: venueCountByCity :many SELECT city, @@ -133,10 +103,10 @@ class QueriesImpl(private val conn: Connection) : Queries { // This is the third line @Throws(SQLException::class) - override fun createCity(arg: CreateCityParams): City { + override fun createCity(name: String, slug: String): City { return conn.prepareStatement(createCity).use { stmt -> - stmt.setString(1, arg.name) - stmt.setString(2, arg.slug) + stmt.setString(1, name) + stmt.setString(2, slug) val results = stmt.executeQuery() if (!results.next()) { @@ -154,15 +124,22 @@ class QueriesImpl(private val conn: Connection) : Queries { } @Throws(SQLException::class) - override fun createVenue(arg: CreateVenueParams): Int { + override fun createVenue( + slug: String, + name: String, + city: String, + spotifyPlaylist: String, + status: Status, + statuses: List, + tags: List): Int { return conn.prepareStatement(createVenue).use { stmt -> - stmt.setString(1, arg.slug) - stmt.setString(2, arg.name) - stmt.setString(3, arg.city) - stmt.setString(4, arg.spotifyPlaylist) - stmt.setObject(5, arg.status.value, Types.OTHER) - stmt.setArray(6, conn.createArrayOf("status", arg.statuses.map { v -> v.value }.toTypedArray())) - stmt.setArray(7, conn.createArrayOf("text", arg.tags.toTypedArray())) + stmt.setString(1, slug) + stmt.setString(2, name) + stmt.setString(3, city) + stmt.setString(4, spotifyPlaylist) + stmt.setObject(5, status.value, Types.OTHER) + stmt.setArray(6, conn.createArrayOf("status", statuses.map { v -> v.value }.toTypedArray())) + stmt.setArray(7, conn.createArrayOf("text", tags.toTypedArray())) val results = stmt.executeQuery() if (!results.next()) { @@ -207,10 +184,10 @@ class QueriesImpl(private val conn: Connection) : Queries { } @Throws(SQLException::class) - override fun getVenue(arg: GetVenueParams): Venue { + override fun getVenue(slug: String, city: String): Venue { return conn.prepareStatement(getVenue).use { stmt -> - stmt.setString(1, arg.slug) - stmt.setString(2, arg.city) + stmt.setString(1, slug) + stmt.setString(2, city) val results = stmt.executeQuery() if (!results.next()) { @@ -277,20 +254,20 @@ class QueriesImpl(private val conn: Connection) : Queries { } @Throws(SQLException::class) - override fun updateCityName(arg: UpdateCityNameParams) { + override fun updateCityName(name: String, slug: String) { conn.prepareStatement(updateCityName).use { stmt -> - stmt.setString(1, arg.name) - stmt.setString(2, arg.slug) + stmt.setString(1, name) + stmt.setString(2, slug) stmt.execute() } } @Throws(SQLException::class) - override fun updateVenueName(arg: UpdateVenueNameParams): Int { + override fun updateVenueName(name: String, slug: String): Int { return conn.prepareStatement(updateVenueName).use { stmt -> - stmt.setString(1, arg.name) - stmt.setString(2, arg.slug) + stmt.setString(1, name) + stmt.setString(2, slug) val results = stmt.executeQuery() if (!results.next()) { diff --git a/examples/kotlin/src/test/kotlin/com/example/authors/QueriesImplTest.kt b/examples/kotlin/src/test/kotlin/com/example/authors/QueriesImplTest.kt index 0a42a0dcc7..93608a1dfc 100644 --- a/examples/kotlin/src/test/kotlin/com/example/authors/QueriesImplTest.kt +++ b/examples/kotlin/src/test/kotlin/com/example/authors/QueriesImplTest.kt @@ -4,12 +4,13 @@ import com.example.dbtest.DbTestExtension import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Test import org.junit.jupiter.api.extension.RegisterExtension -import java.sql.Connection class QueriesImplTest() { companion object { - @JvmField @RegisterExtension val dbtest = DbTestExtension("src/main/resources/authors/schema.sql") + @JvmField + @RegisterExtension + val dbtest = DbTestExtension("src/main/resources/authors/schema.sql") } @Test @@ -19,12 +20,13 @@ class QueriesImplTest() { val initialAuthors = db.listAuthors() assert(initialAuthors.isEmpty()) - val params = CreateAuthorParams( - name = "Brian Kernighan", - bio = "Co-author of The C Programming Language and The Go Programming Language" + val name = "Brian Kernighan" + val bio = "Co-author of The C Programming Language and The Go Programming Language" + val insertedAuthor = db.createAuthor( + name = name, + bio = bio ) - val insertedAuthor = db.createAuthor(params) - val expectedAuthor = Author(insertedAuthor.id, params.name, params.bio) + val expectedAuthor = Author(insertedAuthor.id, name, bio) assertEquals(expectedAuthor, insertedAuthor) val fetchedAuthor = db.getAuthor(insertedAuthor.id) @@ -42,12 +44,10 @@ class QueriesImplTest() { val initialAuthors = db.listAuthors() assert(initialAuthors.isEmpty()) - val params = CreateAuthorParams( - name = "Brian Kernighan", - bio = null - ) - val insertedAuthor = db.createAuthor(params) - val expectedAuthor = Author(insertedAuthor.id, params.name, params.bio) + val name = "Brian Kernighan" + val bio = null + val insertedAuthor = db.createAuthor(name, bio) + val expectedAuthor = Author(insertedAuthor.id, name, bio) assertEquals(expectedAuthor, insertedAuthor) val fetchedAuthor = db.getAuthor(insertedAuthor.id) diff --git a/examples/kotlin/src/test/kotlin/com/example/booktest/postgresql/QueriesImplTest.kt b/examples/kotlin/src/test/kotlin/com/example/booktest/postgresql/QueriesImplTest.kt index f7c54daf7b..d85747431e 100644 --- a/examples/kotlin/src/test/kotlin/com/example/booktest/postgresql/QueriesImplTest.kt +++ b/examples/kotlin/src/test/kotlin/com/example/booktest/postgresql/QueriesImplTest.kt @@ -20,7 +20,6 @@ class QueriesImplTest { // Start a transaction conn.autoCommit = false db.createBook( - CreateBookParams( authorId = author.authorId, isbn = "1", title = "my book title", @@ -28,11 +27,9 @@ class QueriesImplTest { year = 2016, available = OffsetDateTime.now(), tags = listOf() - ) ) val b1 = db.createBook( - CreateBookParams( authorId = author.authorId, isbn = "2", title = "the second book", @@ -40,19 +37,15 @@ class QueriesImplTest { year = 2016, available = OffsetDateTime.now(), tags = listOf("cool", "unique") - ) ) db.updateBook( - UpdateBookParams( bookId = b1.bookId, title = "changed second title", tags = listOf("cool", "disastor") - ) ) val b3 = db.createBook( - CreateBookParams( authorId = author.authorId, isbn = "3", title = "the third book", @@ -60,11 +53,9 @@ class QueriesImplTest { year = 2001, available = OffsetDateTime.now(), tags = listOf("cool") - ) ) db.createBook( - CreateBookParams( authorId = author.authorId, isbn = "4", title = "4th place finisher", @@ -72,7 +63,6 @@ class QueriesImplTest { year = 2011, available = OffsetDateTime.now(), tags = listOf("other") - ) ) // Commit transaction @@ -82,15 +72,13 @@ class QueriesImplTest { // ISBN update fails because parameters are not in sequential order. After changing $N to ?, ordering is lost, // and the parameters are filled into the wrong slots. db.updateBookISBN( - UpdateBookISBNParams( bookId = b3.bookId, isbn = "NEW ISBN", title = "never ever gonna finish, a quatrain", tags = listOf("someother") - ) ) - val books0 = db.booksByTitleYear(BooksByTitleYearParams("my book title", 2016)) + val books0 = db.booksByTitleYear("my book title", 2016) val formatter = DateTimeFormatter.ISO_DATE_TIME for (book in books0) { diff --git a/examples/kotlin/src/test/kotlin/com/example/ondeck/QueriesImplTest.kt b/examples/kotlin/src/test/kotlin/com/example/ondeck/QueriesImplTest.kt index fdd1933862..2ab1ba3ac5 100644 --- a/examples/kotlin/src/test/kotlin/com/example/ondeck/QueriesImplTest.kt +++ b/examples/kotlin/src/test/kotlin/com/example/ondeck/QueriesImplTest.kt @@ -14,13 +14,10 @@ class QueriesImplTest { fun testQueries() { val q = QueriesImpl(dbtest.getConnection()) val city = q.createCity( - CreateCityParams( slug = "san-francisco", name = "San Francisco" - ) ) val venueId = q.createVenue( - CreateVenueParams( slug = "the-fillmore", name = "The Fillmore", city = city.slug, @@ -28,13 +25,10 @@ class QueriesImplTest { status = Status.OPEN, statuses = listOf(Status.OPEN, Status.CLOSED), tags = listOf("rock", "punk") - ) ) val venue = q.getVenue( - GetVenueParams( slug = "the-fillmore", city = city.slug - ) ) assertEquals(venueId, venue.id) @@ -43,8 +37,8 @@ class QueriesImplTest { assertEquals(listOf(city), q.listCities()) assertEquals(listOf(venue), q.listVenues(city.slug)) - q.updateCityName(UpdateCityNameParams(slug = city.slug, name = "SF")) - val id = q.updateVenueName(UpdateVenueNameParams(slug = venue.slug, name = "Fillmore")) + q.updateCityName(slug = city.slug, name = "SF") + val id = q.updateVenueName(slug = venue.slug, name = "Fillmore") assertEquals(venue.id, id) q.deleteVenue(venue.slug) diff --git a/internal/dinosql/kotlin/gen.go b/internal/dinosql/kotlin/gen.go index 6bae4fa240..f30b43ad5f 100644 --- a/internal/dinosql/kotlin/gen.go +++ b/internal/dinosql/kotlin/gen.go @@ -64,13 +64,6 @@ func (v KtQueryValue) isEmpty() bool { return v.Typ == (ktType{}) && v.Name == "" && v.Struct == nil } -func (v KtQueryValue) Pair() string { - if v.isEmpty() { - return "" - } - return v.Name + ": " + v.Type() -} - func (v KtQueryValue) Type() string { if v.Typ != (ktType{}) { return v.Typ.String() @@ -97,23 +90,35 @@ func jdbcSet(t ktType, idx int, name string) string { return fmt.Sprintf("stmt.set%s(%d, %s)", t.Name, idx, name) } -func (v KtQueryValue) Params() string { +type KtParams struct { + Struct *KtStruct +} + +func (v KtParams) isEmpty() bool { + return len(v.Struct.Fields) == 0 +} + +func (v KtParams) Args() string { if v.isEmpty() { return "" } var out []string - if v.Struct == nil { - repeat := 1 - if v.JDBCParamBindCount > 0 { - repeat = v.JDBCParamBindCount - } - for i := 1; i <= repeat; i++ { - out = append(out, jdbcSet(v.Typ, i, v.Name)) - } - } else { - for i, f := range v.Struct.JDBCParamBindings { - out = append(out, jdbcSet(f.Type, i+1, v.Name+"."+f.Name)) - } + for _, f := range v.Struct.Fields { + out = append(out, f.Name+": "+f.Type.String()) + } + if len(out) < 3 { + return strings.Join(out, ", ") + } + return "\n" + indent(strings.Join(out, ",\n"), 6, -1) +} + +func (v KtParams) Bindings() string { + if v.isEmpty() { + return "" + } + var out []string + for i, f := range v.Struct.JDBCParamBindings { + out = append(out, jdbcSet(f.Type, i+1, f.Name)) } return indent(strings.Join(out, "\n"), 6, 0) } @@ -143,7 +148,7 @@ func (v KtQueryValue) ResultSet() string { out = append(out, jdbcGet(f.Type, i+1)) } ret := indent(strings.Join(out, ",\n"), 4, -1) - ret = indent(v.Struct.Name + "(\n" + ret + "\n)", 8, 0) + ret = indent(v.Struct.Name+"(\n"+ret+"\n)", 8, 0) return ret } @@ -177,7 +182,7 @@ type KtQuery struct { SQL string SourceName string Ret KtQueryValue - Arg KtQueryValue + Arg KtParams } type KtGenerateable interface { @@ -221,8 +226,10 @@ func InterfaceKtImports(r KtGenerateable, settings dinosql.CombinedSettings) [][ } } if !q.Arg.isEmpty() { - if strings.HasPrefix(q.Arg.Type(), name) { - return true + for _, f := range q.Arg.Struct.Fields { + if strings.HasPrefix(f.Type.Name, name) { + return true + } } } } @@ -307,16 +314,11 @@ func QueryKtImports(r KtGenerateable, settings dinosql.CombinedSettings, filenam } } if !q.Arg.isEmpty() { - if q.Arg.EmitStruct() { - for _, f := range q.Arg.Struct.Fields { - if f.Type.Name == name { - return true - } + for _, f := range q.Arg.Struct.Fields { + if f.Type.Name == name { + return true } } - if q.Arg.Type() == name { - return true - } } } return false @@ -325,14 +327,8 @@ func QueryKtImports(r KtGenerateable, settings dinosql.CombinedSettings, filenam hasEnum := func() bool { for _, q := range gq { if !q.Arg.isEmpty() { - if q.Arg.IsStruct() { - for _, f := range q.Arg.Struct.Fields { - if f.Type.IsEnum { - return true - } - } - } else { - if q.Arg.Typ.IsEnum { + for _, f := range q.Arg.Struct.Fields { + if f.Type.IsEnum { return true } } @@ -624,18 +620,18 @@ type goColumn struct { core.Column } -func (r Result) ktColumnsToStruct(name string, columns []goColumn, settings dinosql.CombinedSettings) *KtStruct { +func (r Result) ktColumnsToStruct(name string, columns []goColumn, settings dinosql.CombinedSettings, namer func(core.Column, int) string) *KtStruct { gs := KtStruct{ Name: name, } idSeen := map[int]KtField{} nameSeen := map[string]int{} - for i, c := range columns { + for _, c := range columns { if binding, ok := idSeen[c.id]; ok { gs.JDBCParamBindings = append(gs.JDBCParamBindings, binding) continue } - fieldName := KtMemberName(ktColumnName(c.Column, i), settings) + fieldName := KtMemberName(namer(c.Column, c.id), settings) if v := nameSeen[c.Name]; v > 0 { fieldName = fmt.Sprintf("%s_%d", fieldName, v+1) } @@ -663,13 +659,14 @@ func ktArgName(name string) string { return out } -func ktParamName(p dinosql.Parameter) string { - if p.Column.Name != "" { - return ktArgName(p.Column.Name) +func ktParamName(c core.Column, number int) string { + if c.Name != "" { + return ktArgName(c.Name) } - return fmt.Sprintf("dollar_%d", p.Number) + return fmt.Sprintf("dollar_%d", number) } + func ktColumnName(c core.Column, pos int) string { if c.Name != "" { return c.Name @@ -716,20 +713,9 @@ func (r Result) KtQueries(settings dinosql.CombinedSettings) []KtQuery { Column: p.Column, }) } - params := r.ktColumnsToStruct(gq.ClassName+"Params", cols, settings) - if len(params.Fields) == 1 { - p := query.Params[0] - gq.Arg = KtQueryValue{ - Name: ktParamName(p), - Typ: r.ktType(p.Column, settings), - JDBCParamBindCount: len(params.JDBCParamBindings), - } - } else if len(params.Fields) > 1 { - gq.Arg = KtQueryValue{ - Emit: true, - Name: "arg", - Struct: params, - } + params := r.ktColumnsToStruct(gq.ClassName+"Bindings", cols, settings, ktParamName) + gq.Arg = KtParams{ + Struct: params, } if len(query.Columns) == 1 { @@ -771,7 +757,7 @@ func (r Result) KtQueries(settings dinosql.CombinedSettings) []KtQuery { Column: c, }) } - gs = r.ktColumnsToStruct(gq.ClassName+"Row", columns, settings) + gs = r.ktColumnsToStruct(gq.ClassName+"Row", columns, settings, ktColumnName) emit = true } gq.Ret = KtQueryValue{ @@ -800,16 +786,16 @@ interface Queries { {{- range .KtQueries}} @Throws(SQLException::class) {{- if eq .Cmd ":one"}} - fun {{.MethodName}}({{.Arg.Pair}}): {{.Ret.Type}} + fun {{.MethodName}}({{.Arg.Args}}): {{.Ret.Type}} {{- end}} {{- if eq .Cmd ":many"}} - fun {{.MethodName}}({{.Arg.Pair}}): List<{{.Ret.Type}}> + fun {{.MethodName}}({{.Arg.Args}}): List<{{.Ret.Type}}> {{- end}} {{- if eq .Cmd ":exec"}} - fun {{.MethodName}}({{.Arg.Pair}}) + fun {{.MethodName}}({{.Arg.Args}}) {{- end}} {{- if eq .Cmd ":execrows"}} - fun {{.MethodName}}({{.Arg.Pair}}): Int + fun {{.MethodName}}({{.Arg.Args}}): Int {{- end}} {{end}} } @@ -866,14 +852,6 @@ const val {{.ConstantName}} = {{$.Q}}-- name: {{.MethodName}} {{.Cmd}} {{.SQL}} {{$.Q}} -{{if .Arg.EmitStruct}} -data class {{.Arg.Type}} ( {{- range $i, $e := .Arg.Struct.Fields}} - {{- if $i }},{{end}} - val {{.Name}}: {{.Type}} - {{- end}} -) -{{end}} - {{if .Ret.EmitStruct}} data class {{.Ret.Type}} ( {{- range $i, $e := .Ret.Struct.Fields}} {{- if $i }},{{end}} @@ -890,9 +868,9 @@ class QueriesImpl(private val conn: Connection){{ if .EmitInterface }} : Queries {{end}} @Throws(SQLException::class) {{ if $.EmitInterface }}override {{ end -}} - fun {{.MethodName}}({{.Arg.Pair}}): {{.Ret.Type}} { + fun {{.MethodName}}({{.Arg.Args}}): {{.Ret.Type}} { return conn.prepareStatement({{.ConstantName}}).use { stmt -> - {{.Arg.Params}} + {{.Arg.Bindings}} val results = stmt.executeQuery() if (!results.next()) { @@ -912,9 +890,9 @@ class QueriesImpl(private val conn: Connection){{ if .EmitInterface }} : Queries {{end}} @Throws(SQLException::class) {{ if $.EmitInterface }}override {{ end -}} - fun {{.MethodName}}({{.Arg.Pair}}): List<{{.Ret.Type}}> { + fun {{.MethodName}}({{.Arg.Args}}): List<{{.Ret.Type}}> { return conn.prepareStatement({{.ConstantName}}).use { stmt -> - {{.Arg.Params}} + {{.Arg.Bindings}} val results = stmt.executeQuery() val ret = mutableListOf<{{.Ret.Type}}>() @@ -931,9 +909,9 @@ class QueriesImpl(private val conn: Connection){{ if .EmitInterface }} : Queries {{end}} @Throws(SQLException::class) {{ if $.EmitInterface }}override {{ end -}} - fun {{.MethodName}}({{.Arg.Pair}}) { + fun {{.MethodName}}({{.Arg.Args}}) { conn.prepareStatement({{.ConstantName}}).use { stmt -> - {{ .Arg.Params }} + {{ .Arg.Bindings }} stmt.execute() } @@ -945,9 +923,9 @@ class QueriesImpl(private val conn: Connection){{ if .EmitInterface }} : Queries {{end}} @Throws(SQLException::class) {{ if $.EmitInterface }}override {{ end -}} - fun {{.MethodName}}({{.Arg.Pair}}): Int { + fun {{.MethodName}}({{.Arg.Args}}): Int { return conn.prepareStatement({{.ConstantName}}).use { stmt -> - {{ .Arg.Params }} + {{ .Arg.Bindings }} stmt.execute() stmt.updateCount From 7969b5cc8cebaed17b25a000678cf24adad78c7d Mon Sep 17 00:00:00 2001 From: Kyle Conroy Date: Fri, 31 Jan 2020 09:54:01 -0800 Subject: [PATCH 16/20] Initial commit --- internal/cmd/cmd.go | 25 ++---- internal/cmd/generate.go | 23 +++--- internal/config/config.go | 141 +++++++++++++++------------------ internal/config/config_test.go | 1 + internal/config/v_one.go | 121 ++++++++++++++++++++++++++++ internal/dinosql/gen.go | 18 ++--- internal/dinosql/parser.go | 2 +- 7 files changed, 216 insertions(+), 115 deletions(-) create mode 100644 internal/config/v_one.go diff --git a/internal/cmd/cmd.go b/internal/cmd/cmd.go index ce064596be..62d3571a81 100644 --- a/internal/cmd/cmd.go +++ b/internal/cmd/cmd.go @@ -14,7 +14,6 @@ import ( "github.com/spf13/cobra" "github.com/kyleconroy/sqlc/internal/config" - "github.com/kyleconroy/sqlc/internal/dinosql" ) // Do runs the command logic. @@ -75,7 +74,7 @@ var initCmd = &cobra.Command{ if _, err := os.Stat("sqlc.json"); !os.IsNotExist(err) { return nil } - blob, err := json.MarshalIndent(config.GenerateSettings{Version: "1"}, "", " ") + blob, err := json.MarshalIndent(config.Config{Version: "2"}, "", " ") if err != nil { return err } @@ -113,24 +112,14 @@ var checkCmd = &cobra.Command{ Use: "compile", Short: "Statically check SQL for syntax and type errors", RunE: func(cmd *cobra.Command, args []string) error { - file, err := os.Open("sqlc.json") - if err != nil { - return err - } - - settings, err := config.ParseConfig(file) + stderr := cmd.ErrOrStderr() + dir, err := os.Getwd() if err != nil { - return err + fmt.Fprintln(stderr, "error parsing sqlc.json: file does not exist") + os.Exit(1) } - - for _, pkg := range settings.Packages { - c, err := dinosql.ParseCatalog(pkg.Schema) - if err != nil { - return err - } - if _, err := dinosql.ParseQueries(c, pkg); err != nil { - return err - } + if _, err := Generate(dir, stderr); err != nil { + os.Exit(1) } return nil }, diff --git a/internal/cmd/generate.go b/internal/cmd/generate.go index b30a7c06e7..03a81e3870 100644 --- a/internal/cmd/generate.go +++ b/internal/cmd/generate.go @@ -40,7 +40,7 @@ func Generate(dir string, stderr io.Writer) (map[string]string, error) { return nil, err } - settings, err := config.ParseConfig(bytes.NewReader(blob)) + conf, err := config.ParseConfig(bytes.NewReader(blob)) if err != nil { switch err { case config.ErrMissingVersion: @@ -57,20 +57,19 @@ func Generate(dir string, stderr io.Writer) (map[string]string, error) { output := map[string]string{} errored := false - for _, pkg := range settings.Packages { - name := pkg.Name - combo := config.Combine(settings, pkg) + for _, sql := range conf.SQL { + combo := config.Combine(conf, sql) + name := combo.Go.Package var result dinosql.Generateable // TODO: This feels like a hack that will bite us later - pkg.Schema = filepath.Join(dir, pkg.Schema) - pkg.Queries = filepath.Join(dir, pkg.Queries) - - switch pkg.Engine { + sql.Schema = filepath.Join(dir, sql.Schema) + sql.Queries = filepath.Join(dir, sql.Queries) + switch sql.Engine { case config.EngineMySQL: // Experimental MySQL support - q, err := mysql.GeneratePkg(name, pkg.Schema, pkg.Queries, combo) + q, err := mysql.GeneratePkg(name, sql.Schema, sql.Queries, combo) if err != nil { fmt.Fprintf(stderr, "# package %s\n", name) if parserErr, ok := err.(*dinosql.ParserErr); ok { @@ -86,7 +85,7 @@ func Generate(dir string, stderr io.Writer) (map[string]string, error) { result = q case config.EnginePostgreSQL: - c, err := dinosql.ParseCatalog(pkg.Schema) + c, err := dinosql.ParseCatalog(sql.Schema) if err != nil { fmt.Fprintf(stderr, "# package %s\n", name) if parserErr, ok := err.(*dinosql.ParserErr); ok { @@ -100,7 +99,7 @@ func Generate(dir string, stderr io.Writer) (map[string]string, error) { continue } - q, err := dinosql.ParseQueries(c, pkg) + q, err := dinosql.ParseQueries(c, sql) if err != nil { fmt.Fprintf(stderr, "# package %s\n", name) if parserErr, ok := err.(*dinosql.ParserErr); ok { @@ -126,7 +125,7 @@ func Generate(dir string, stderr io.Writer) (map[string]string, error) { } for n, source := range files { - filename := filepath.Join(dir, pkg.Path, n) + filename := filepath.Join(dir, combo.Go.Out, n) output[filename] = source } } diff --git a/internal/config/config.go b/internal/config/config.go index 16cbf1852a..155e39eff2 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -1,13 +1,13 @@ package config import ( + "bytes" "encoding/json" "errors" "fmt" "go/types" "io" "os" - "path/filepath" "strings" "github.com/kyleconroy/sqlc/internal/pg" @@ -28,11 +28,8 @@ The only supported version is "1". const errMessageNoPackages = `No packages are configured` -type GenerateSettings struct { - Version string `json:"version"` - Packages []PackageSettings `json:"packages"` - Overrides []Override `json:"overrides,omitempty"` - Rename map[string]string `json:"rename,omitempty"` +type versionSetting struct { + Number string `json:"version"` } type Engine string @@ -42,16 +39,40 @@ const ( EnginePostgreSQL Engine = "postgresql" ) -type PackageSettings struct { - Name string `json:"name"` - Engine Engine `json:"engine,omitempty"` - Path string `json:"path"` - Schema string `json:"schema"` - Queries string `json:"queries"` - EmitInterface bool `json:"emit_interface"` - EmitJSONTags bool `json:"emit_json_tags"` - EmitPreparedQueries bool `json:"emit_prepared_queries"` - Overrides []Override `json:"overrides"` +type Config struct { + Version string `json:"version"` + SQL []SQL `json:"sql"` + Gen Gen `json:"overrides,omitempty"` +} + +type Gen struct { + Go *GenGo `json:"go,omitempty"` +} + +type GenGo struct { + Overrides []Override `json:"overrides,omitempty"` + Rename map[string]string `json:"rename,omitempty"` +} + +type SQL struct { + Engine Engine `json:"engine,omitempty"` + Schema string `json:"schema"` + Queries string `json:"queries"` + Gen SQLGen `json:"gen"` +} + +type SQLGen struct { + Go *SQLGo `json:"go,omitempty"` +} + +type SQLGo struct { + EmitInterface bool `json:"emit_interface"` + EmitJSONTags bool `json:"emit_json_tags"` + EmitPreparedQueries bool `json:"emit_prepared_queries"` + Package string `json:"package"` + Out string `json:"out"` + Overrides []Override `json:"overrides,omitempty"` + Rename map[string]string `json:"rename,omitempty"` } type Override struct { @@ -78,23 +99,6 @@ type Override struct { GoBasicType bool } -func (c *GenerateSettings) ValidateGlobalOverrides() error { - engines := map[Engine]struct{}{} - for _, pkg := range c.Packages { - if _, ok := engines[pkg.Engine]; !ok { - engines[pkg.Engine] = struct{}{} - } - } - - usesMultipleEngines := len(engines) > 1 - for _, oride := range c.Overrides { - if usesMultipleEngines && oride.Engine == "" { - return fmt.Errorf(`the "engine" field is required for global type overrides because your configuration uses multiple database engines`) - } - } - return nil -} - func (o *Override) Parse() error { // validate deprecated postgres_type field @@ -192,59 +196,46 @@ var ErrNoPackages = errors.New("no packages") var ErrNoPackageName = errors.New("missing package name") var ErrNoPackagePath = errors.New("missing package path") -func ParseConfig(rd io.Reader) (GenerateSettings, error) { - dec := json.NewDecoder(rd) - dec.DisallowUnknownFields() - var config GenerateSettings - if err := dec.Decode(&config); err != nil { +func ParseConfig(rd io.Reader) (Config, error) { + var buf bytes.Buffer + var config Config + var version versionSetting + ver := io.TeeReader(rd, &buf) + dec := json.NewDecoder(ver) + if err := dec.Decode(&version); err != nil { return config, err } - if config.Version == "" { + if version.Number == "" { return config, ErrMissingVersion } - if config.Version != "1" { + switch version.Number { + case "1": + return v1ParseConfig(&buf) + // case "2": + default: return config, ErrUnknownVersion } - if len(config.Packages) == 0 { - return config, ErrNoPackages - } - if err := config.ValidateGlobalOverrides(); err != nil { - return config, err - } - for i := range config.Overrides { - if err := config.Overrides[i].Parse(); err != nil { - return config, err - } - } - for j := range config.Packages { - if config.Packages[j].Path == "" { - return config, ErrNoPackagePath - } - for i := range config.Packages[j].Overrides { - if err := config.Packages[j].Overrides[i].Parse(); err != nil { - return config, err - } - } - if config.Packages[j].Name == "" { - config.Packages[j].Name = filepath.Base(config.Packages[j].Path) - } - if config.Packages[j].Engine == "" { - config.Packages[j].Engine = EnginePostgreSQL - } - } - return config, nil } type CombinedSettings struct { - Global GenerateSettings - Package PackageSettings + Global Config + Package SQL + Go SQLGo + Rename map[string]string Overrides []Override } -func Combine(gen GenerateSettings, pkg PackageSettings) CombinedSettings { - return CombinedSettings{ - Global: gen, - Package: pkg, - Overrides: append(gen.Overrides, pkg.Overrides...), +func Combine(conf Config, pkg SQL) CombinedSettings { + cs := CombinedSettings{ + Global: conf, + Package: pkg, + } + if conf.Gen.Go != nil { + cs.Rename = conf.Gen.Go.Rename + cs.Overrides = append(cs.Overrides, conf.Gen.Go.Overrides...) + } + if pkg.Gen.Go != nil { + cs.Go = *pkg.Gen.Go } + return cs } diff --git a/internal/config/config_test.go b/internal/config/config_test.go index eeabb5d88e..da09091c35 100644 --- a/internal/config/config_test.go +++ b/internal/config/config_test.go @@ -19,6 +19,7 @@ const unknownVersion = `{ }` const unknownFields = `{ + "version": "1", "foo": "bar" }` diff --git a/internal/config/v_one.go b/internal/config/v_one.go new file mode 100644 index 0000000000..29d3deb5f1 --- /dev/null +++ b/internal/config/v_one.go @@ -0,0 +1,121 @@ +package config + +import ( + "encoding/json" + "fmt" + "io" + "path/filepath" +) + +type v1GenerateSettings struct { + Version string `json:"version"` + Packages []v1PackageSettings `json:"packages"` + Overrides []Override `json:"overrides,omitempty"` + Rename map[string]string `json:"rename,omitempty"` +} + +type v1PackageSettings struct { + Name string `json:"name"` + Engine Engine `json:"engine,omitempty"` + Path string `json:"path"` + Schema string `json:"schema"` + Queries string `json:"queries"` + EmitInterface bool `json:"emit_interface"` + EmitJSONTags bool `json:"emit_json_tags"` + EmitPreparedQueries bool `json:"emit_prepared_queries"` + Overrides []Override `json:"overrides"` +} + +func v1ParseConfig(rd io.Reader) (Config, error) { + dec := json.NewDecoder(rd) + dec.DisallowUnknownFields() + var settings v1GenerateSettings + var config Config + if err := dec.Decode(&settings); err != nil { + return config, err + } + if settings.Version == "" { + return config, ErrMissingVersion + } + if settings.Version != "1" { + return config, ErrUnknownVersion + } + if len(settings.Packages) == 0 { + return config, ErrNoPackages + } + if err := settings.ValidateGlobalOverrides(); err != nil { + return config, err + } + for i := range settings.Overrides { + if err := settings.Overrides[i].Parse(); err != nil { + return config, err + } + } + for j := range settings.Packages { + if settings.Packages[j].Path == "" { + return config, ErrNoPackagePath + } + for i := range settings.Packages[j].Overrides { + if err := settings.Packages[j].Overrides[i].Parse(); err != nil { + return config, err + } + } + if settings.Packages[j].Name == "" { + settings.Packages[j].Name = filepath.Base(settings.Packages[j].Path) + } + if settings.Packages[j].Engine == "" { + settings.Packages[j].Engine = EnginePostgreSQL + } + } + return settings.Translate(), nil +} + +func (c *v1GenerateSettings) ValidateGlobalOverrides() error { + engines := map[Engine]struct{}{} + for _, pkg := range c.Packages { + if _, ok := engines[pkg.Engine]; !ok { + engines[pkg.Engine] = struct{}{} + } + } + + usesMultipleEngines := len(engines) > 1 + for _, oride := range c.Overrides { + if usesMultipleEngines && oride.Engine == "" { + return fmt.Errorf(`the "engine" field is required for global type overrides because your configuration uses multiple database engines`) + } + } + return nil +} + +func (c *v1GenerateSettings) Translate() Config { + conf := Config{ + Version: c.Version, + } + + for _, pkg := range c.Packages { + conf.SQL = append(conf.SQL, SQL{ + Engine: pkg.Engine, + Schema: pkg.Schema, + Queries: pkg.Queries, + Gen: SQLGen{ + Go: &SQLGo{ + EmitInterface: pkg.EmitInterface, + EmitJSONTags: pkg.EmitJSONTags, + EmitPreparedQueries: pkg.EmitPreparedQueries, + Package: pkg.Name, + Out: pkg.Path, + Overrides: pkg.Overrides, + }, + }, + }) + } + + if len(c.Overrides) > 0 || len(c.Rename) > 0 { + conf.Gen.Go = &GenGo{ + Overrides: c.Overrides, + Rename: c.Rename, + } + } + + return conf +} diff --git a/internal/dinosql/gen.go b/internal/dinosql/gen.go index 8c9dca8a25..728374b85a 100644 --- a/internal/dinosql/gen.go +++ b/internal/dinosql/gen.go @@ -192,7 +192,7 @@ func Imports(r Generateable, settings config.CombinedSettings) func(string) [][] return func(filename string) [][]string { if filename == "db.go" { imps := []string{"context", "database/sql"} - if settings.Package.EmitPreparedQueries { + if settings.Go.EmitPreparedQueries { imps = append(imps, "fmt") } return [][]string{imps} @@ -524,7 +524,7 @@ func (r Result) Enums(settings config.CombinedSettings) []GoEnum { } func StructName(name string, settings config.CombinedSettings) string { - if rename := settings.Global.Rename[name]; rename != "" { + if rename := settings.Rename[name]; rename != "" { return rename } out := "" @@ -1183,7 +1183,7 @@ type tmplCtx struct { Enums []GoEnum Structs []GoStruct GoQueries []GoQuery - Settings config.GenerateSettings + Settings config.Config // TODO: Race conditions SourceName string @@ -1210,14 +1210,14 @@ func Generate(r Generateable, settings config.CombinedSettings) (map[string]stri sqlFile := template.Must(template.New("table").Funcs(funcMap).Parse(sqlTmpl)) ifaceFile := template.Must(template.New("table").Funcs(funcMap).Parse(ifaceTmpl)) - pkg := settings.Package + golang := settings.Go tctx := tmplCtx{ Settings: settings.Global, - EmitInterface: pkg.EmitInterface, - EmitJSONTags: pkg.EmitJSONTags, - EmitPreparedQueries: pkg.EmitPreparedQueries, + EmitInterface: golang.EmitInterface, + EmitJSONTags: golang.EmitJSONTags, + EmitPreparedQueries: golang.EmitPreparedQueries, Q: "`", - Package: pkg.Name, + Package: golang.Package, GoQueries: r.GoQueries(settings), Enums: r.Enums(settings), Structs: r.Structs(settings), @@ -1252,7 +1252,7 @@ func Generate(r Generateable, settings config.CombinedSettings) (map[string]stri if err := execute("models.go", modelsFile); err != nil { return nil, err } - if pkg.EmitInterface { + if golang.EmitInterface { if err := execute("querier.go", ifaceFile); err != nil { return nil, err } diff --git a/internal/dinosql/parser.go b/internal/dinosql/parser.go index 381f415007..3c992570d8 100644 --- a/internal/dinosql/parser.go +++ b/internal/dinosql/parser.go @@ -189,7 +189,7 @@ type Result struct { Catalog core.Catalog } -func ParseQueries(c core.Catalog, pkg config.PackageSettings) (*Result, error) { +func ParseQueries(c core.Catalog, pkg config.SQL) (*Result, error) { f, err := os.Stat(pkg.Queries) if err != nil { return nil, fmt.Errorf("path %s does not exist", pkg.Queries) From 70007dffe93f0dd1ed6173a43b0c8534f42fad5d Mon Sep 17 00:00:00 2001 From: Kyle Conroy Date: Wed, 5 Feb 2020 13:30:50 -0800 Subject: [PATCH 17/20] Fix per-package overrides --- internal/config/config.go | 1 + 1 file changed, 1 insertion(+) diff --git a/internal/config/config.go b/internal/config/config.go index 155e39eff2..3bbdbd319e 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -236,6 +236,7 @@ func Combine(conf Config, pkg SQL) CombinedSettings { } if pkg.Gen.Go != nil { cs.Go = *pkg.Gen.Go + cs.Overrides = append(cs.Overrides, pkg.Gen.Go.Overrides...) } return cs } From 0f827835081ad1dc1636e081712c251cf5c42a11 Mon Sep 17 00:00:00 2001 From: Kyle Conroy Date: Wed, 5 Feb 2020 13:52:26 -0800 Subject: [PATCH 18/20] Parse config-v2 --- internal/config/config.go | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/internal/config/config.go b/internal/config/config.go index 3bbdbd319e..d61c02b9b8 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -192,6 +192,8 @@ func (o *Override) Parse() error { var ErrMissingVersion = errors.New("no version number") var ErrUnknownVersion = errors.New("invalid version number") +var ErrMissingEngine = errors.New("unknown engine") +var ErrUnknownEngine = errors.New("invalid engine") var ErrNoPackages = errors.New("no packages") var ErrNoPackageName = errors.New("missing package name") var ErrNoPackagePath = errors.New("missing package path") @@ -211,7 +213,8 @@ func ParseConfig(rd io.Reader) (Config, error) { switch version.Number { case "1": return v1ParseConfig(&buf) - // case "2": + case "2": + return v2ParseConfig(&buf) default: return config, ErrUnknownVersion } From 5f60cd7203ce2cc81d8fa69105438f1861a2f2c2 Mon Sep 17 00:00:00 2001 From: Kyle Conroy Date: Wed, 5 Feb 2020 14:03:03 -0800 Subject: [PATCH 19/20] config: Parse V2 config format --- examples/authors/sqlc.json | 16 +++++++ examples/booktest/sqlc.json | 19 ++++++++ examples/jets/sqlc.json | 12 +++++ examples/ondeck/sqlc.json | 15 ++++++ examples/sqlc.json | 40 ---------------- internal/cmd/cmd.go | 2 +- internal/config/v_two.go | 74 ++++++++++++++++++++++++++++++ internal/endtoend/endtoend_test.go | 31 ++++++++++--- 8 files changed, 162 insertions(+), 47 deletions(-) create mode 100644 examples/authors/sqlc.json create mode 100644 examples/booktest/sqlc.json create mode 100644 examples/jets/sqlc.json create mode 100644 examples/ondeck/sqlc.json delete mode 100644 examples/sqlc.json create mode 100644 internal/config/v_two.go diff --git a/examples/authors/sqlc.json b/examples/authors/sqlc.json new file mode 100644 index 0000000000..3db9676f1f --- /dev/null +++ b/examples/authors/sqlc.json @@ -0,0 +1,16 @@ +{ + "version": "2", + "sql": [ + { + "schema": "schema.sql", + "queries": "query.sql", + "engine": "postgresql", + "gen": { + "go": { + "package": "authors", + "out": "." + } + } + } + ] +} diff --git a/examples/booktest/sqlc.json b/examples/booktest/sqlc.json new file mode 100644 index 0000000000..419a759f19 --- /dev/null +++ b/examples/booktest/sqlc.json @@ -0,0 +1,19 @@ +{ + "version": "1", + "packages": [ + { + "name": "booktest", + "path": "postgresql", + "schema": "postgresql/schema.sql", + "queries": "postgresql/query.sql", + "engine": "postgresql" + }, + { + "name": "booktest", + "path": "mysql", + "schema": "mysql/schema.sql", + "queries": "mysql/query.sql", + "engine": "mysql" + } + ] +} diff --git a/examples/jets/sqlc.json b/examples/jets/sqlc.json new file mode 100644 index 0000000000..0bca6f48df --- /dev/null +++ b/examples/jets/sqlc.json @@ -0,0 +1,12 @@ +{ + "version": "1", + "packages": [ + { + "path": ".", + "name": "jets", + "schema": "schema.sql", + "queries": "query-building.sql", + "engine": "postgresql" + } + ] +} diff --git a/examples/ondeck/sqlc.json b/examples/ondeck/sqlc.json new file mode 100644 index 0000000000..3b604f4c63 --- /dev/null +++ b/examples/ondeck/sqlc.json @@ -0,0 +1,15 @@ +{ + "version": "1", + "packages": [ + { + "path": ".", + "name": "ondeck", + "schema": "schema", + "queries": "query", + "engine": "postgresql", + "emit_json_tags": true, + "emit_prepared_queries": true, + "emit_interface": true + } + ] +} diff --git a/examples/sqlc.json b/examples/sqlc.json deleted file mode 100644 index 9662784e93..0000000000 --- a/examples/sqlc.json +++ /dev/null @@ -1,40 +0,0 @@ -{ - "version": "1", - "packages": [ - { - "path": "authors", - "schema": "authors/schema.sql", - "queries": "authors/query.sql", - "engine": "postgresql" - }, - { - "path": "ondeck", - "schema": "ondeck/schema", - "queries": "ondeck/query", - "engine": "postgresql", - "emit_json_tags": true, - "emit_prepared_queries": true, - "emit_interface": true - }, - { - "path": "jets", - "schema": "jets/schema.sql", - "queries": "jets/query-building.sql", - "engine": "postgresql" - }, - { - "name": "booktest", - "path": "booktest/postgresql", - "schema": "booktest/postgresql/schema.sql", - "queries": "booktest/postgresql/query.sql", - "engine": "postgresql" - }, - { - "name": "booktest", - "path": "booktest/mysql", - "schema": "booktest/mysql/schema.sql", - "queries": "booktest/mysql/query.sql", - "engine": "mysql" - } - ] -} diff --git a/internal/cmd/cmd.go b/internal/cmd/cmd.go index 62d3571a81..49fe541a32 100644 --- a/internal/cmd/cmd.go +++ b/internal/cmd/cmd.go @@ -74,7 +74,7 @@ var initCmd = &cobra.Command{ if _, err := os.Stat("sqlc.json"); !os.IsNotExist(err) { return nil } - blob, err := json.MarshalIndent(config.Config{Version: "2"}, "", " ") + blob, err := json.MarshalIndent(config.Config{Version: "1"}, "", " ") if err != nil { return err } diff --git a/internal/config/v_two.go b/internal/config/v_two.go new file mode 100644 index 0000000000..78ff059843 --- /dev/null +++ b/internal/config/v_two.go @@ -0,0 +1,74 @@ +package config + +import ( + "encoding/json" + "fmt" + "io" + "path/filepath" +) + +func v2ParseConfig(rd io.Reader) (Config, error) { + dec := json.NewDecoder(rd) + dec.DisallowUnknownFields() + var conf Config + if err := dec.Decode(&conf); err != nil { + return conf, err + } + if conf.Version == "" { + return conf, ErrMissingVersion + } + if conf.Version != "2" { + return conf, ErrUnknownVersion + } + if len(conf.SQL) == 0 { + return conf, ErrNoPackages + } + if err := conf.validateGlobalOverrides(); err != nil { + return conf, err + } + if conf.Gen.Go != nil { + for i := range conf.Gen.Go.Overrides { + if err := conf.Gen.Go.Overrides[i].Parse(); err != nil { + return conf, err + } + } + } + for j := range conf.SQL { + if conf.SQL[j].Engine == "" { + return conf, ErrMissingEngine + } + if conf.SQL[j].Gen.Go != nil { + if conf.SQL[j].Gen.Go.Out == "" { + return conf, ErrNoPackagePath + } + if conf.SQL[j].Gen.Go.Package == "" { + conf.SQL[j].Gen.Go.Package = filepath.Base(conf.SQL[j].Gen.Go.Out) + } + for i := range conf.SQL[j].Gen.Go.Overrides { + if err := conf.SQL[j].Gen.Go.Overrides[i].Parse(); err != nil { + return conf, err + } + } + } + } + return conf, nil +} + +func (c *Config) validateGlobalOverrides() error { + engines := map[Engine]struct{}{} + for _, pkg := range c.SQL { + if _, ok := engines[pkg.Engine]; !ok { + engines[pkg.Engine] = struct{}{} + } + } + if c.Gen.Go == nil { + return nil + } + usesMultipleEngines := len(engines) > 1 + for _, oride := range c.Gen.Go.Overrides { + if usesMultipleEngines && oride.Engine == "" { + return fmt.Errorf(`the "engine" field is required for global type overrides because your configuration uses multiple database engines`) + } + } + return nil +} diff --git a/internal/endtoend/endtoend_test.go b/internal/endtoend/endtoend_test.go index 9611d38f17..d028f3f1b0 100644 --- a/internal/endtoend/endtoend_test.go +++ b/internal/endtoend/endtoend_test.go @@ -16,16 +16,32 @@ import ( func TestExamples(t *testing.T) { t.Parallel() + examples, err := filepath.Abs(filepath.Join("..", "..", "examples")) + if err != nil { + t.Fatal(err) + } - examples, _ := filepath.Abs(filepath.Join("..", "..", "examples")) - var stderr bytes.Buffer - - output, err := cmd.Generate(examples, &stderr) + files, err := ioutil.ReadDir(examples) if err != nil { - t.Fatalf("%s", stderr.String()) + t.Fatal(err) } - cmpDirectory(t, examples, output) + for _, replay := range files { + if !replay.IsDir() { + continue + } + tc := replay.Name() + t.Run(tc, func(t *testing.T) { + t.Parallel() + path := filepath.Join(examples, tc) + var stderr bytes.Buffer + output, err := cmd.Generate(path, &stderr) + if err != nil { + t.Fatalf("sqlc generate failed: %s", stderr.String()) + } + cmpDirectory(t, path, output) + }) + } } func TestReplay(t *testing.T) { @@ -37,6 +53,9 @@ func TestReplay(t *testing.T) { } for _, replay := range files { + if !replay.IsDir() { + continue + } tc := replay.Name() t.Run(tc, func(t *testing.T) { t.Parallel() From ada4dfc68eb88b2fb5e9e8ac850299ed29bd054b Mon Sep 17 00:00:00 2001 From: Yunchi Luo Date: Fri, 7 Feb 2020 10:36:38 -0500 Subject: [PATCH 20/20] integrate kotlin with config v2 --- examples/kotlin/sqlc.json | 49 ++++++ .../kotlin/com/example/authors/Queries.kt | 22 +++ .../kotlin/com/example/authors/QueriesImpl.kt | 10 +- .../example/booktest/postgresql/Queries.kt | 53 +++++++ .../booktest/postgresql/QueriesImpl.kt | 20 +-- .../main/kotlin/com/example/jets/Queries.kt | 19 +++ .../kotlin/com/example/jets/QueriesImpl.kt | 8 +- .../main/resources/jets/query-building.sql | 8 + .../kotlin/src/main/resources/jets/schema.sql | 35 +++++ examples/sqlc.json | 0 internal/cmd/generate.go | 144 +++++++++++------- internal/config/config.go | 16 +- internal/dinosql/kotlin/gen.go | 64 ++++---- internal/dinosql/parser.go | 22 +-- 14 files changed, 350 insertions(+), 120 deletions(-) create mode 100644 examples/kotlin/sqlc.json create mode 100644 examples/kotlin/src/main/kotlin/com/example/authors/Queries.kt create mode 100644 examples/kotlin/src/main/kotlin/com/example/booktest/postgresql/Queries.kt create mode 100644 examples/kotlin/src/main/kotlin/com/example/jets/Queries.kt create mode 100644 examples/kotlin/src/main/resources/jets/query-building.sql create mode 100644 examples/kotlin/src/main/resources/jets/schema.sql delete mode 100644 examples/sqlc.json diff --git a/examples/kotlin/sqlc.json b/examples/kotlin/sqlc.json new file mode 100644 index 0000000000..00267b1b01 --- /dev/null +++ b/examples/kotlin/sqlc.json @@ -0,0 +1,49 @@ +{ + "version": "2", + "sql": [ + { + "schema": "src/main/resources/authors/schema.sql", + "queries": "src/main/resources/authors/query.sql", + "engine": "postgresql", + "gen": { + "kotlin": { + "out": "src/main/kotlin/com/example/authors", + "package": "com.example.authors" + } + } + }, + { + "schema": "src/main/resources/ondeck/schema", + "queries": "src/main/resources/ondeck/query", + "engine": "postgresql", + "gen": { + "kotlin": { + "out": "src/main/kotlin/com/example/ondeck", + "package": "com.example.ondeck" + } + } + }, + { + "schema": "src/main/resources/jets/schema.sql", + "queries": "src/main/resources/jets/query-building.sql", + "engine": "postgresql", + "gen": { + "kotlin": { + "out": "src/main/kotlin/com/example/jets", + "package": "com.example.jets" + } + } + }, + { + "schema": "src/main/resources/booktest/postgresql/schema.sql", + "queries": "src/main/resources/booktest/postgresql/query.sql", + "engine": "postgresql", + "gen": { + "kotlin": { + "out": "src/main/kotlin/com/example/booktest/postgresql", + "package": "com.example.booktest.postgresql" + } + } + } + ] +} diff --git a/examples/kotlin/src/main/kotlin/com/example/authors/Queries.kt b/examples/kotlin/src/main/kotlin/com/example/authors/Queries.kt new file mode 100644 index 0000000000..4dde3eec1f --- /dev/null +++ b/examples/kotlin/src/main/kotlin/com/example/authors/Queries.kt @@ -0,0 +1,22 @@ +// Code generated by sqlc. DO NOT EDIT. + +package com.example.authors + +import java.sql.Connection +import java.sql.SQLException + +interface Queries { + @Throws(SQLException::class) + fun createAuthor(name: String, bio: String?): Author + + @Throws(SQLException::class) + fun deleteAuthor(id: Long) + + @Throws(SQLException::class) + fun getAuthor(id: Long): Author + + @Throws(SQLException::class) + fun listAuthors(): List + +} + diff --git a/examples/kotlin/src/main/kotlin/com/example/authors/QueriesImpl.kt b/examples/kotlin/src/main/kotlin/com/example/authors/QueriesImpl.kt index 9ee42a6abf..1cf7434630 100644 --- a/examples/kotlin/src/main/kotlin/com/example/authors/QueriesImpl.kt +++ b/examples/kotlin/src/main/kotlin/com/example/authors/QueriesImpl.kt @@ -29,10 +29,10 @@ SELECT id, name, bio FROM authors ORDER BY name """ -class QueriesImpl(private val conn: Connection) { +class QueriesImpl(private val conn: Connection) : Queries { @Throws(SQLException::class) - fun createAuthor(name: String, bio: String?): Author { + override fun createAuthor(name: String, bio: String?): Author { return conn.prepareStatement(createAuthor).use { stmt -> stmt.setString(1, name) stmt.setString(2, bio) @@ -54,7 +54,7 @@ class QueriesImpl(private val conn: Connection) { } @Throws(SQLException::class) - fun deleteAuthor(id: Long) { + override fun deleteAuthor(id: Long) { conn.prepareStatement(deleteAuthor).use { stmt -> stmt.setLong(1, id) @@ -63,7 +63,7 @@ class QueriesImpl(private val conn: Connection) { } @Throws(SQLException::class) - fun getAuthor(id: Long): Author { + override fun getAuthor(id: Long): Author { return conn.prepareStatement(getAuthor).use { stmt -> stmt.setLong(1, id) @@ -84,7 +84,7 @@ class QueriesImpl(private val conn: Connection) { } @Throws(SQLException::class) - fun listAuthors(): List { + override fun listAuthors(): List { return conn.prepareStatement(listAuthors).use { stmt -> val results = stmt.executeQuery() diff --git a/examples/kotlin/src/main/kotlin/com/example/booktest/postgresql/Queries.kt b/examples/kotlin/src/main/kotlin/com/example/booktest/postgresql/Queries.kt new file mode 100644 index 0000000000..addf28a721 --- /dev/null +++ b/examples/kotlin/src/main/kotlin/com/example/booktest/postgresql/Queries.kt @@ -0,0 +1,53 @@ +// Code generated by sqlc. DO NOT EDIT. + +package com.example.booktest.postgresql + +import java.sql.Connection +import java.sql.SQLException +import java.sql.Types +import java.time.OffsetDateTime + +interface Queries { + @Throws(SQLException::class) + fun booksByTags(dollar1: List): List + + @Throws(SQLException::class) + fun booksByTitleYear(title: String, year: Int): List + + @Throws(SQLException::class) + fun createAuthor(name: String): Author + + @Throws(SQLException::class) + fun createBook( + authorId: Int, + isbn: String, + booktype: BookType, + title: String, + year: Int, + available: OffsetDateTime, + tags: List): Book + + @Throws(SQLException::class) + fun deleteBook(bookId: Int) + + @Throws(SQLException::class) + fun getAuthor(authorId: Int): Author + + @Throws(SQLException::class) + fun getBook(bookId: Int): Book + + @Throws(SQLException::class) + fun updateBook( + title: String, + tags: List, + bookId: Int) + + @Throws(SQLException::class) + fun updateBookISBN( + title: String, + tags: List, + isbn: String, + bookId: Int) + +} + diff --git a/examples/kotlin/src/main/kotlin/com/example/booktest/postgresql/QueriesImpl.kt b/examples/kotlin/src/main/kotlin/com/example/booktest/postgresql/QueriesImpl.kt index f6e13ab8db..e4696f2cbc 100644 --- a/examples/kotlin/src/main/kotlin/com/example/booktest/postgresql/QueriesImpl.kt +++ b/examples/kotlin/src/main/kotlin/com/example/booktest/postgresql/QueriesImpl.kt @@ -85,10 +85,10 @@ SET title = ?, tags = ?, isbn = ? WHERE book_id = ? """ -class QueriesImpl(private val conn: Connection) { +class QueriesImpl(private val conn: Connection) : Queries { @Throws(SQLException::class) - fun booksByTags(dollar1: List): List { + override fun booksByTags(dollar1: List): List { return conn.prepareStatement(booksByTags).use { stmt -> stmt.setArray(1, conn.createArrayOf("pg_catalog.varchar", dollar1.toTypedArray())) @@ -108,7 +108,7 @@ class QueriesImpl(private val conn: Connection) { } @Throws(SQLException::class) - fun booksByTitleYear(title: String, year: Int): List { + override fun booksByTitleYear(title: String, year: Int): List { return conn.prepareStatement(booksByTitleYear).use { stmt -> stmt.setString(1, title) stmt.setInt(2, year) @@ -132,7 +132,7 @@ class QueriesImpl(private val conn: Connection) { } @Throws(SQLException::class) - fun createAuthor(name: String): Author { + override fun createAuthor(name: String): Author { return conn.prepareStatement(createAuthor).use { stmt -> stmt.setString(1, name) @@ -152,7 +152,7 @@ class QueriesImpl(private val conn: Connection) { } @Throws(SQLException::class) - fun createBook( + override fun createBook( authorId: Int, isbn: String, booktype: BookType, @@ -191,7 +191,7 @@ class QueriesImpl(private val conn: Connection) { } @Throws(SQLException::class) - fun deleteBook(bookId: Int) { + override fun deleteBook(bookId: Int) { conn.prepareStatement(deleteBook).use { stmt -> stmt.setInt(1, bookId) @@ -200,7 +200,7 @@ class QueriesImpl(private val conn: Connection) { } @Throws(SQLException::class) - fun getAuthor(authorId: Int): Author { + override fun getAuthor(authorId: Int): Author { return conn.prepareStatement(getAuthor).use { stmt -> stmt.setInt(1, authorId) @@ -220,7 +220,7 @@ class QueriesImpl(private val conn: Connection) { } @Throws(SQLException::class) - fun getBook(bookId: Int): Book { + override fun getBook(bookId: Int): Book { return conn.prepareStatement(getBook).use { stmt -> stmt.setInt(1, bookId) @@ -246,7 +246,7 @@ class QueriesImpl(private val conn: Connection) { } @Throws(SQLException::class) - fun updateBook( + override fun updateBook( title: String, tags: List, bookId: Int) { @@ -260,7 +260,7 @@ class QueriesImpl(private val conn: Connection) { } @Throws(SQLException::class) - fun updateBookISBN( + override fun updateBookISBN( title: String, tags: List, isbn: String, diff --git a/examples/kotlin/src/main/kotlin/com/example/jets/Queries.kt b/examples/kotlin/src/main/kotlin/com/example/jets/Queries.kt new file mode 100644 index 0000000000..7437a6f2b0 --- /dev/null +++ b/examples/kotlin/src/main/kotlin/com/example/jets/Queries.kt @@ -0,0 +1,19 @@ +// Code generated by sqlc. DO NOT EDIT. + +package com.example.jets + +import java.sql.Connection +import java.sql.SQLException + +interface Queries { + @Throws(SQLException::class) + fun countPilots(): Long + + @Throws(SQLException::class) + fun deletePilot(id: Int) + + @Throws(SQLException::class) + fun listPilots(): List + +} + diff --git a/examples/kotlin/src/main/kotlin/com/example/jets/QueriesImpl.kt b/examples/kotlin/src/main/kotlin/com/example/jets/QueriesImpl.kt index 04ab619fca..3214a8acdd 100644 --- a/examples/kotlin/src/main/kotlin/com/example/jets/QueriesImpl.kt +++ b/examples/kotlin/src/main/kotlin/com/example/jets/QueriesImpl.kt @@ -17,10 +17,10 @@ const val listPilots = """-- name: listPilots :many SELECT id, name FROM pilots LIMIT 5 """ -class QueriesImpl(private val conn: Connection) { +class QueriesImpl(private val conn: Connection) : Queries { @Throws(SQLException::class) - fun countPilots(): Long { + override fun countPilots(): Long { return conn.prepareStatement(countPilots).use { stmt -> val results = stmt.executeQuery() @@ -36,7 +36,7 @@ class QueriesImpl(private val conn: Connection) { } @Throws(SQLException::class) - fun deletePilot(id: Int) { + override fun deletePilot(id: Int) { conn.prepareStatement(deletePilot).use { stmt -> stmt.setInt(1, id) @@ -45,7 +45,7 @@ class QueriesImpl(private val conn: Connection) { } @Throws(SQLException::class) - fun listPilots(): List { + override fun listPilots(): List { return conn.prepareStatement(listPilots).use { stmt -> val results = stmt.executeQuery() diff --git a/examples/kotlin/src/main/resources/jets/query-building.sql b/examples/kotlin/src/main/resources/jets/query-building.sql new file mode 100644 index 0000000000..ede8952367 --- /dev/null +++ b/examples/kotlin/src/main/resources/jets/query-building.sql @@ -0,0 +1,8 @@ +-- name: CountPilots :one +SELECT COUNT(*) FROM pilots; + +-- name: ListPilots :many +SELECT * FROM pilots LIMIT 5; + +-- name: DeletePilot :exec +DELETE FROM pilots WHERE id = $1; diff --git a/examples/kotlin/src/main/resources/jets/schema.sql b/examples/kotlin/src/main/resources/jets/schema.sql new file mode 100644 index 0000000000..2cc4aca574 --- /dev/null +++ b/examples/kotlin/src/main/resources/jets/schema.sql @@ -0,0 +1,35 @@ +CREATE TABLE pilots ( + id integer NOT NULL, + name text NOT NULL +); + +ALTER TABLE pilots ADD CONSTRAINT pilot_pkey PRIMARY KEY (id); + +CREATE TABLE jets ( + id integer NOT NULL, + pilot_id integer NOT NULL, + age integer NOT NULL, + name text NOT NULL, + color text NOT NULL +); + +ALTER TABLE jets ADD CONSTRAINT jet_pkey PRIMARY KEY (id); +ALTER TABLE jets ADD CONSTRAINT jet_pilots_fkey FOREIGN KEY (pilot_id) REFERENCES pilots(id); + +CREATE TABLE languages ( + id integer NOT NULL, + language text NOT NULL +); + +ALTER TABLE languages ADD CONSTRAINT language_pkey PRIMARY KEY (id); + +-- Join table +CREATE TABLE pilot_languages ( + pilot_id integer NOT NULL, + language_id integer NOT NULL +); + +-- Composite primary key +ALTER TABLE pilot_languages ADD CONSTRAINT pilot_language_pkey PRIMARY KEY (pilot_id, language_id); +ALTER TABLE pilot_languages ADD CONSTRAINT pilot_language_pilots_fkey FOREIGN KEY (pilot_id) REFERENCES pilots(id); +ALTER TABLE pilot_languages ADD CONSTRAINT pilot_language_languages_fkey FOREIGN KEY (language_id) REFERENCES languages(id); diff --git a/examples/sqlc.json b/examples/sqlc.json deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/internal/cmd/generate.go b/internal/cmd/generate.go index 91115870d6..1185cac547 100644 --- a/internal/cmd/generate.go +++ b/internal/cmd/generate.go @@ -34,6 +34,11 @@ func printFileErr(stderr io.Writer, dir string, fileErr dinosql.FileErr) { fmt.Fprintf(stderr, "%s:%d:%d: %s\n", filename, fileErr.Line, fileErr.Column, fileErr.Err) } +type outPair struct { + Gen config.SQLGen + config.SQL +} + func Generate(dir string, stderr io.Writer) (map[string]string, error) { blob, err := ioutil.ReadFile(filepath.Join(dir, "sqlc.json")) if err != nil { @@ -58,73 +63,54 @@ func Generate(dir string, stderr io.Writer) (map[string]string, error) { output := map[string]string{} errored := false + var pairs []outPair for _, sql := range conf.SQL { - combo := config.Combine(conf, sql) - name := combo.Go.Package + if sql.Gen.Go != nil { + pairs = append(pairs, outPair{ + SQL: sql, + Gen: config.SQLGen{Go: sql.Gen.Go}, + }) + } + if sql.Gen.Kotlin != nil { + pairs = append(pairs, outPair{ + SQL: sql, + Gen: config.SQLGen{Kotlin: sql.Gen.Kotlin}, + }) + } + } + + for _, sql := range pairs { + combo := config.Combine(conf, sql.SQL) var result dinosql.Generateable // TODO: This feels like a hack that will bite us later sql.Schema = filepath.Join(dir, sql.Schema) sql.Queries = filepath.Join(dir, sql.Queries) - switch sql.Engine { - case config.EngineMySQL: - // Experimental MySQL support - q, err := mysql.GeneratePkg(name, sql.Schema, sql.Queries, combo) - if err != nil { - fmt.Fprintf(stderr, "# package %s\n", name) - if parserErr, ok := err.(*dinosql.ParserErr); ok { - for _, fileErr := range parserErr.Errs { - printFileErr(stderr, dir, fileErr) - } - } else { - fmt.Fprintf(stderr, "error parsing schema: %s\n", err) - } - errored = true - continue - } - result = q - - case config.EnginePostgreSQL: - c, err := dinosql.ParseCatalog(sql.Schema) - if err != nil { - fmt.Fprintf(stderr, "# package %s\n", name) - if parserErr, ok := err.(*dinosql.ParserErr); ok { - for _, fileErr := range parserErr.Errs { - printFileErr(stderr, dir, fileErr) - } - } else { - fmt.Fprintf(stderr, "error parsing schema: %s\n", err) - } - errored = true - continue - } - - q, err := dinosql.ParseQueries(c, sql) - if err != nil { - fmt.Fprintf(stderr, "# package %s\n", name) - if parserErr, ok := err.(*dinosql.ParserErr); ok { - for _, fileErr := range parserErr.Errs { - printFileErr(stderr, dir, fileErr) - } - } else { - fmt.Fprintf(stderr, "error parsing queries: %s\n", err) - } - errored = true - continue - } - result = &kotlin.Result{Result: q} + var name string + parseOpts := dinosql.ParserOpts{} + if sql.Gen.Go != nil { + name = combo.Go.Package + } else if sql.Gen.Kotlin != nil { + parseOpts.UsePositionalParameters = true + name = combo.Kotlin.Package + } + result, errored = parse(name, dir, sql.SQL, combo, parseOpts, stderr) + if errored { + break } var files map[string]string - switch pkg.Language { - case dinosql.LanguageGo: + var out string + if sql.Gen.Go != nil { + out = combo.Go.Out files, err = dinosql.Generate(result, combo) - case dinosql.LanguageKotlin: + } else if sql.Gen.Kotlin != nil { + out = combo.Kotlin.Out ktRes, ok := result.(kotlin.KtGenerateable) if !ok { - err = fmt.Errorf("kotlin not supported for engine %s", pkg.Engine) + err = fmt.Errorf("kotlin not supported for engine %s", combo.Package.Engine) break } files, err = kotlin.KtGenerate(ktRes, combo) @@ -137,7 +123,7 @@ func Generate(dir string, stderr io.Writer) (map[string]string, error) { } for n, source := range files { - filename := filepath.Join(dir, combo.Go.Out, n) + filename := filepath.Join(dir, out, n) output[filename] = source } } @@ -147,3 +133,53 @@ func Generate(dir string, stderr io.Writer) (map[string]string, error) { } return output, nil } + +func parse(name, dir string, sql config.SQL, combo config.CombinedSettings, parserOpts dinosql.ParserOpts, stderr io.Writer) (dinosql.Generateable, bool) { + switch sql.Engine { + case config.EngineMySQL: + // Experimental MySQL support + q, err := mysql.GeneratePkg(name, sql.Schema, sql.Queries, combo) + if err != nil { + fmt.Fprintf(stderr, "# package %s\n", name) + if parserErr, ok := err.(*dinosql.ParserErr); ok { + for _, fileErr := range parserErr.Errs { + printFileErr(stderr, dir, fileErr) + } + } else { + fmt.Fprintf(stderr, "error parsing schema: %s\n", err) + } + return nil, true + } + return q, false + + case config.EnginePostgreSQL: + c, err := dinosql.ParseCatalog(sql.Schema) + if err != nil { + fmt.Fprintf(stderr, "# package %s\n", name) + if parserErr, ok := err.(*dinosql.ParserErr); ok { + for _, fileErr := range parserErr.Errs { + printFileErr(stderr, dir, fileErr) + } + } else { + fmt.Fprintf(stderr, "error parsing schema: %s\n", err) + } + return nil, true + } + + q, err := dinosql.ParseQueries(c, sql.Queries, parserOpts) + if err != nil { + fmt.Fprintf(stderr, "# package %s\n", name) + if parserErr, ok := err.(*dinosql.ParserErr); ok { + for _, fileErr := range parserErr.Errs { + printFileErr(stderr, dir, fileErr) + } + } else { + fmt.Fprintf(stderr, "error parsing queries: %s\n", err) + } + return nil, true + } + return &kotlin.Result{Result: q}, false + default: + panic("invalid engine") + } +} diff --git a/internal/config/config.go b/internal/config/config.go index 3ab75f9e29..3675a630cb 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -46,7 +46,8 @@ type Config struct { } type Gen struct { - Go *GenGo `json:"go,omitempty"` + Go *GenGo `json:"go,omitempty"` + Kotlin *GenKotlin `json:"kotlin,omitempty"` } type GenGo struct { @@ -54,6 +55,10 @@ type GenGo struct { Rename map[string]string `json:"rename,omitempty"` } +type GenKotlin struct { + Rename map[string]string `json:"rename,omitempty"` +} + type SQL struct { Engine Engine `json:"engine,omitempty"` Schema string `json:"schema"` @@ -203,7 +208,7 @@ var ErrUnknownEngine = errors.New("invalid engine") var ErrNoPackages = errors.New("no packages") var ErrNoPackageName = errors.New("missing package name") var ErrNoPackagePath = errors.New("missing package path") -var ErrKotlinNoOutPath = errors.New("no output path for Kotlin") +var ErrKotlinNoOutPath = errors.New("no output path") func ParseConfig(rd io.Reader) (Config, error) { var buf bytes.Buffer @@ -231,6 +236,7 @@ type CombinedSettings struct { Global Config Package SQL Go SQLGo + Kotlin SQLKotlin Rename map[string]string Overrides []Override } @@ -244,9 +250,15 @@ func Combine(conf Config, pkg SQL) CombinedSettings { cs.Rename = conf.Gen.Go.Rename cs.Overrides = append(cs.Overrides, conf.Gen.Go.Overrides...) } + if conf.Gen.Kotlin != nil { + cs.Rename = conf.Gen.Kotlin.Rename + } if pkg.Gen.Go != nil { cs.Go = *pkg.Gen.Go cs.Overrides = append(cs.Overrides, pkg.Gen.Go.Overrides...) } + if pkg.Gen.Kotlin != nil { + cs.Kotlin = *pkg.Gen.Kotlin + } return cs } diff --git a/internal/dinosql/kotlin/gen.go b/internal/dinosql/kotlin/gen.go index f30b43ad5f..61fefb6091 100644 --- a/internal/dinosql/kotlin/gen.go +++ b/internal/dinosql/kotlin/gen.go @@ -10,6 +10,7 @@ import ( "strings" "text/template" + "github.com/kyleconroy/sqlc/internal/config" "github.com/kyleconroy/sqlc/internal/dinosql" core "github.com/kyleconroy/sqlc/internal/pg" @@ -186,12 +187,12 @@ type KtQuery struct { } type KtGenerateable interface { - KtDataClasses(settings dinosql.CombinedSettings) []KtStruct - KtQueries(settings dinosql.CombinedSettings) []KtQuery - KtEnums(settings dinosql.CombinedSettings) []KtEnum + KtDataClasses(settings config.CombinedSettings) []KtStruct + KtQueries(settings config.CombinedSettings) []KtQuery + KtEnums(settings config.CombinedSettings) []KtEnum } -func KtUsesType(r KtGenerateable, typ string, settings dinosql.CombinedSettings) bool { +func KtUsesType(r KtGenerateable, typ string, settings config.CombinedSettings) bool { for _, strct := range r.KtDataClasses(settings) { for _, f := range strct.Fields { if f.Type.Name == typ { @@ -202,7 +203,7 @@ func KtUsesType(r KtGenerateable, typ string, settings dinosql.CombinedSettings) return false } -func KtImports(r KtGenerateable, settings dinosql.CombinedSettings) func(string) [][]string { +func KtImports(r KtGenerateable, settings config.CombinedSettings) func(string) [][]string { return func(filename string) [][]string { if filename == "Models.kt" { return ModelKtImports(r, settings) @@ -216,7 +217,7 @@ func KtImports(r KtGenerateable, settings dinosql.CombinedSettings) func(string) } } -func InterfaceKtImports(r KtGenerateable, settings dinosql.CombinedSettings) [][]string { +func InterfaceKtImports(r KtGenerateable, settings config.CombinedSettings) [][]string { gq := r.KtQueries(settings) uses := func(name string) bool { for _, q := range gq { @@ -262,7 +263,7 @@ func InterfaceKtImports(r KtGenerateable, settings dinosql.CombinedSettings) [][ return [][]string{stds} } -func ModelKtImports(r KtGenerateable, settings dinosql.CombinedSettings) [][]string { +func ModelKtImports(r KtGenerateable, settings config.CombinedSettings) [][]string { std := make(map[string]struct{}) if KtUsesType(r, "LocalDate", settings) { std["java.time.LocalDate"] = struct{}{} @@ -286,7 +287,7 @@ func ModelKtImports(r KtGenerateable, settings dinosql.CombinedSettings) [][]str return [][]string{stds} } -func QueryKtImports(r KtGenerateable, settings dinosql.CombinedSettings, filename string) [][]string { +func QueryKtImports(r KtGenerateable, settings config.CombinedSettings, filename string) [][]string { // for _, strct := range r.KtDataClasses() { // for _, f := range strct.Fields { // if strings.HasPrefix(f.Type, "[]") { @@ -390,13 +391,13 @@ type Result struct { *dinosql.Result } -func (r Result) KtEnums(settings dinosql.CombinedSettings) []KtEnum { +func (r Result) KtEnums(settings config.CombinedSettings) []KtEnum { var enums []KtEnum for name, schema := range r.Catalog.Schemas { if name == "pg_catalog" { continue } - for _, enum := range schema.Enums { + for _, enum := range schema.Enums() { var enumName string if name == "public" { enumName = enum.Name @@ -423,8 +424,8 @@ func (r Result) KtEnums(settings dinosql.CombinedSettings) []KtEnum { return enums } -func KtDataClassName(name string, settings dinosql.CombinedSettings) string { - if rename := settings.Global.Rename[name]; rename != "" { +func KtDataClassName(name string, settings config.CombinedSettings) string { + if rename := settings.Rename[name]; rename != "" { return rename } out := "" @@ -434,11 +435,11 @@ func KtDataClassName(name string, settings dinosql.CombinedSettings) string { return out } -func KtMemberName(name string, settings dinosql.CombinedSettings) string { +func KtMemberName(name string, settings config.CombinedSettings) string { return dinosql.LowerTitle(KtDataClassName(name, settings)) } -func (r Result) KtDataClasses(settings dinosql.CombinedSettings) []KtStruct { +func (r Result) KtDataClasses(settings config.CombinedSettings) []KtStruct { var structs []KtStruct for name, schema := range r.Catalog.Schemas { if name == "pg_catalog" { @@ -508,7 +509,7 @@ func (t ktType) IsTime() bool { return t.Name == "LocalDate" || t.Name == "LocalDateTime" || t.Name == "LocalTime" || t.Name == "OffsetDateTime" } -func (r Result) ktType(col core.Column, settings dinosql.CombinedSettings) ktType { +func (r Result) ktType(col core.Column, settings config.CombinedSettings) ktType { typ, isEnum := r.ktInnerType(col, settings) return ktType{ Name: typ, @@ -519,7 +520,7 @@ func (r Result) ktType(col core.Column, settings dinosql.CombinedSettings) ktTyp } } -func (r Result) ktInnerType(col core.Column, settings dinosql.CombinedSettings) (string, bool) { +func (r Result) ktInnerType(col core.Column, settings config.CombinedSettings) (string, bool) { columnType := col.DataType switch columnType { @@ -600,7 +601,7 @@ func (r Result) ktInnerType(col core.Column, settings dinosql.CombinedSettings) if name == "pg_catalog" { continue } - for _, enum := range schema.Enums { + for _, enum := range schema.Enums() { if columnType == enum.Name { if name == "public" { return KtDataClassName(enum.Name, settings), true @@ -620,7 +621,7 @@ type goColumn struct { core.Column } -func (r Result) ktColumnsToStruct(name string, columns []goColumn, settings dinosql.CombinedSettings, namer func(core.Column, int) string) *KtStruct { +func (r Result) ktColumnsToStruct(name string, columns []goColumn, settings config.CombinedSettings, namer func(core.Column, int) string) *KtStruct { gs := KtStruct{ Name: name, } @@ -683,7 +684,7 @@ func jdbcSQL(s string) string { return jdbcSQLRe.ReplaceAllString(s, "?") } -func (r Result) KtQueries(settings dinosql.CombinedSettings) []KtQuery { +func (r Result) KtQueries(settings config.CombinedSettings) []KtQuery { structs := r.KtDataClasses(settings) qs := make([]KtQuery, 0, len(r.Queries)) @@ -861,14 +862,13 @@ data class {{.Ret.Type}} ( {{- range $i, $e := .Ret.Struct.Fields}} {{end}} {{end}} -class QueriesImpl(private val conn: Connection){{ if .EmitInterface }} : Queries{{end}} { +class QueriesImpl(private val conn: Connection) : Queries { {{range .KtQueries}} {{if eq .Cmd ":one"}} {{range .Comments}}//{{.}} {{end}} @Throws(SQLException::class) - {{ if $.EmitInterface }}override {{ end -}} - fun {{.MethodName}}({{.Arg.Args}}): {{.Ret.Type}} { + override fun {{.MethodName}}({{.Arg.Args}}): {{.Ret.Type}} { return conn.prepareStatement({{.ConstantName}}).use { stmt -> {{.Arg.Bindings}} @@ -889,8 +889,7 @@ class QueriesImpl(private val conn: Connection){{ if .EmitInterface }} : Queries {{range .Comments}}//{{.}} {{end}} @Throws(SQLException::class) - {{ if $.EmitInterface }}override {{ end -}} - fun {{.MethodName}}({{.Arg.Args}}): List<{{.Ret.Type}}> { + override fun {{.MethodName}}({{.Arg.Args}}): List<{{.Ret.Type}}> { return conn.prepareStatement({{.ConstantName}}).use { stmt -> {{.Arg.Bindings}} @@ -909,7 +908,7 @@ class QueriesImpl(private val conn: Connection){{ if .EmitInterface }} : Queries {{end}} @Throws(SQLException::class) {{ if $.EmitInterface }}override {{ end -}} - fun {{.MethodName}}({{.Arg.Args}}) { + override fun {{.MethodName}}({{.Arg.Args}}) { conn.prepareStatement({{.ConstantName}}).use { stmt -> {{ .Arg.Bindings }} @@ -923,7 +922,7 @@ class QueriesImpl(private val conn: Connection){{ if .EmitInterface }} : Queries {{end}} @Throws(SQLException::class) {{ if $.EmitInterface }}override {{ end -}} - fun {{.MethodName}}({{.Arg.Args}}): Int { + override fun {{.MethodName}}({{.Arg.Args}}): Int { return conn.prepareStatement({{.ConstantName}}).use { stmt -> {{ .Arg.Bindings }} @@ -942,7 +941,7 @@ type ktTmplCtx struct { Enums []KtEnum KtDataClasses []KtStruct KtQueries []KtQuery - Settings dinosql.GenerateSettings + Settings config.Config // TODO: Race conditions SourceName string @@ -972,7 +971,7 @@ func ktFormat(s string) string { return o } -func KtGenerate(r KtGenerateable, settings dinosql.CombinedSettings) (map[string]string, error) { +func KtGenerate(r KtGenerateable, settings config.CombinedSettings) (map[string]string, error) { funcMap := template.FuncMap{ "lowerTitle": dinosql.LowerTitle, "imports": KtImports(r, settings), @@ -986,11 +985,8 @@ func KtGenerate(r KtGenerateable, settings dinosql.CombinedSettings) (map[string pkg := settings.Package tctx := ktTmplCtx{ Settings: settings.Global, - EmitInterface: pkg.EmitInterface, - EmitJSONTags: pkg.EmitJSONTags, - EmitPreparedQueries: pkg.EmitPreparedQueries, Q: `"""`, - Package: pkg.Name, + Package: pkg.Gen.Kotlin.Package, KtQueries: r.KtQueries(settings), Enums: r.KtEnums(settings), KtDataClasses: r.KtDataClasses(settings), @@ -1017,11 +1013,9 @@ func KtGenerate(r KtGenerateable, settings dinosql.CombinedSettings) (map[string if err := execute("Models.kt", modelsFile); err != nil { return nil, err } - if pkg.EmitInterface { - if err := execute("Queries.kt", ifaceFile); err != nil { + if err := execute("Queries.kt", ifaceFile); err != nil { return nil, err } - } if err := execute("QueriesImpl.kt", sqlFile); err != nil { return nil, err } diff --git a/internal/dinosql/parser.go b/internal/dinosql/parser.go index 5041c31ccd..26f7d5c610 100644 --- a/internal/dinosql/parser.go +++ b/internal/dinosql/parser.go @@ -13,7 +13,6 @@ import ( "unicode" "github.com/kyleconroy/sqlc/internal/catalog" - "github.com/kyleconroy/sqlc/internal/config" core "github.com/kyleconroy/sqlc/internal/pg" "github.com/kyleconroy/sqlc/internal/postgres" @@ -189,23 +188,27 @@ type Result struct { Catalog core.Catalog } -func ParseQueries(c core.Catalog, pkg config.SQL) (*Result, error) { - f, err := os.Stat(pkg.Queries) +type ParserOpts struct { + UsePositionalParameters bool +} + +func ParseQueries(c core.Catalog, queries string, opts ParserOpts) (*Result, error) { + f, err := os.Stat(queries) if err != nil { - return nil, fmt.Errorf("path %s does not exist", pkg.Queries) + return nil, fmt.Errorf("path %s does not exist", queries) } var files []string if f.IsDir() { - listing, err := ioutil.ReadDir(pkg.Queries) + listing, err := ioutil.ReadDir(queries) if err != nil { return nil, err } for _, f := range listing { - files = append(files, filepath.Join(pkg.Queries, f.Name())) + files = append(files, filepath.Join(queries, f.Name())) } } else { - files = append(files, pkg.Queries) + files = append(files, queries) } merr := NewParserErr() @@ -230,8 +233,7 @@ func ParseQueries(c core.Catalog, pkg config.SQL) (*Result, error) { continue } for _, stmt := range tree.Statements { - rewriteParameters := pkg.rewriteParams - query, err := parseQuery(c, stmt, source, rewriteParameters) + query, err := parseQuery(c, stmt, source, opts.UsePositionalParameters) if err == errUnsupportedStatementType { continue } @@ -256,7 +258,7 @@ func ParseQueries(c core.Catalog, pkg config.SQL) (*Result, error) { return nil, merr } if len(q) == 0 { - return nil, fmt.Errorf("path %s contains no queries", pkg.Queries) + return nil, fmt.Errorf("path %s contains no queries", queries) } return &Result{ Catalog: c,