diff --git a/docs/reference/config.md b/docs/reference/config.md index cbd6ab907b..32690477ce 100644 --- a/docs/reference/config.md +++ b/docs/reference/config.md @@ -11,11 +11,12 @@ packages: queries: "./sql/query/" schema: "./sql/schema/" engine: "postgresql" - emit_json_tags: true emit_prepared_queries: true emit_interface: false emit_exact_table_names: false emit_empty_slices: false + emit_json_tags: true + json_tags_case_style: "camel" ``` Each package document has the following keys: @@ -29,8 +30,6 @@ Each package document has the following keys: - Directory of SQL migrations or path to single SQL file; or a list of paths - `engine`: - Either `postgresql` or `mysql`. Defaults to `postgresql`. MySQL support is experimental -- `emit_json_tags`: - - If true, add JSON tags to generated structs. Defaults to `false`. - `emit_db_tags`: - If true, add DB tags to generated structs. Defaults to `false`. - `emit_prepared_queries`: @@ -41,6 +40,10 @@ Each package document has the following keys: - If true, struct names will mirror table names. Otherwise, sqlc attempts to singularize plural table names. Defaults to `false`. - `emit_empty_slices`: - If true, slices returned by `:many` queries will be empty instead of `nil`. Defaults to `false`. +- `emit_json_tags`: + - If true, add JSON tags to generated structs. Defaults to `false`. +- `json_tags_case_style`: + - `camel` for camelCase, `pascal` for PascalCase, `snake` for snake_case or `none` to use the column name in the DB. Defaults to `none`. ## Type Overrides @@ -118,5 +121,3 @@ packages: [...] rename: spotify_url: "SpotifyURL" ``` - - diff --git a/internal/codegen/golang/field.go b/internal/codegen/golang/field.go index 617ab42f76..e036b0e041 100644 --- a/internal/codegen/golang/field.go +++ b/internal/codegen/golang/field.go @@ -4,6 +4,8 @@ import ( "fmt" "sort" "strings" + + "github.com/kyleconroy/sqlc/internal/config" ) type Field struct { @@ -24,3 +26,53 @@ func (gf Field) Tag() string { sort.Strings(tags) return strings.Join(tags, " ") } + +func JSONTagName(name string, settings config.CombinedSettings) string { + style := settings.Go.JSONTagsCaseStyle + if style == "" || style == "none" { + return name + } else { + return SetCaseStyle(name, style) + } +} + +func SetCaseStyle(name string, style string) string { + switch style { + case "camel": + return toCamelCase(name) + case "pascal": + return toPascalCase(name) + case "snake": + return toSnakeCase(name) + default: + panic(fmt.Sprintf("unsupported JSON tags case style: '%s'", style)) + } +} + +func toSnakeCase(s string) string { + return strings.ToLower(s) +} + +func toCamelCase(s string) string { + return toCamelInitCase(s, false) +} + +func toPascalCase(s string) string { + return toCamelInitCase(s, true) +} + +func toCamelInitCase(name string, initUpper bool) string { + out := "" + for i, p := range strings.Split(name, "_") { + if !initUpper && i == 0 { + out += p + continue + } + if p == "id" { + out += "ID" + } else { + out += strings.Title(p) + } + } + return out +} diff --git a/internal/codegen/golang/result.go b/internal/codegen/golang/result.go index 3c534b19f7..38f30a521d 100644 --- a/internal/codegen/golang/result.go +++ b/internal/codegen/golang/result.go @@ -78,7 +78,7 @@ func buildStructs(r *compiler.Result, settings config.CombinedSettings) []Struct tags["db:"] = column.Name } if settings.Go.EmitJSONTags { - tags["json:"] = column.Name + tags["json:"] = JSONTagName(column.Name, settings) } s.Fields = append(s.Fields, Field{ Name: StructName(column.Name, settings), @@ -259,7 +259,7 @@ func columnsToStruct(r *compiler.Result, name string, columns []goColumn, settin tags["db:"] = tagName } if settings.Go.EmitJSONTags { - tags["json:"] = tagName + tags["json:"] = JSONTagName(tagName, settings) } gs.Fields = append(gs.Fields, Field{ Name: fieldName, diff --git a/internal/config/config.go b/internal/config/config.go index 3092965545..54001049e8 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -116,6 +116,7 @@ type SQLGo struct { EmitPreparedQueries bool `json:"emit_prepared_queries" yaml:"emit_prepared_queries"` EmitExactTableNames bool `json:"emit_exact_table_names,omitempty" yaml:"emit_exact_table_names"` EmitEmptySlices bool `json:"emit_empty_slices,omitempty" yaml:"emit_empty_slices"` + JSONTagsCaseStyle string `json:"json_tags_case_style,omitempty" yaml:"json_tags_case_style"` Package string `json:"package" yaml:"package"` Out string `json:"out" yaml:"out"` Overrides []Override `json:"overrides,omitempty" yaml:"overrides"` diff --git a/internal/config/v_one.go b/internal/config/v_one.go index 55d6b3ae32..19210df3aa 100644 --- a/internal/config/v_one.go +++ b/internal/config/v_one.go @@ -27,6 +27,7 @@ type v1PackageSettings struct { EmitPreparedQueries bool `json:"emit_prepared_queries" yaml:"emit_prepared_queries"` EmitExactTableNames bool `json:"emit_exact_table_names,omitempty" yaml:"emit_exact_table_names"` EmitEmptySlices bool `json:"emit_empty_slices,omitempty" yaml:"emit_empty_slices"` + JSONTagsCaseStyle string `json:"json_tags_case_style,omitempty" yaml:"json_tags_case_style"` Overrides []Override `json:"overrides" yaml:"overrides"` } @@ -112,6 +113,7 @@ func (c *V1GenerateSettings) Translate() Config { Package: pkg.Name, Out: pkg.Path, Overrides: pkg.Overrides, + JSONTagsCaseStyle: pkg.JSONTagsCaseStyle, }, }, }) diff --git a/internal/endtoend/testdata/json_tags/camel_case/go/db.go b/internal/endtoend/testdata/json_tags/camel_case/go/db.go new file mode 100644 index 0000000000..6a99519302 --- /dev/null +++ b/internal/endtoend/testdata/json_tags/camel_case/go/db.go @@ -0,0 +1,29 @@ +// Code generated by sqlc. DO NOT EDIT. + +package querytest + +import ( + "context" + "database/sql" +) + +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} +} + +type Queries struct { + db DBTX +} + +func (q *Queries) WithTx(tx *sql.Tx) *Queries { + return &Queries{ + db: tx, + } +} diff --git a/internal/endtoend/testdata/json_tags/camel_case/go/models.go b/internal/endtoend/testdata/json_tags/camel_case/go/models.go new file mode 100644 index 0000000000..01263341ea --- /dev/null +++ b/internal/endtoend/testdata/json_tags/camel_case/go/models.go @@ -0,0 +1,13 @@ +// Code generated by sqlc. DO NOT EDIT. + +package querytest + +import ( + "database/sql" +) + +type User struct { + FirstName sql.NullString `json:"firstName"` + LastName sql.NullString `json:"lastName"` + Age int16 `json:"age"` +} diff --git a/internal/endtoend/testdata/json_tags/camel_case/go/query.sql.go b/internal/endtoend/testdata/json_tags/camel_case/go/query.sql.go new file mode 100644 index 0000000000..e65d68c909 --- /dev/null +++ b/internal/endtoend/testdata/json_tags/camel_case/go/query.sql.go @@ -0,0 +1,35 @@ +// Code generated by sqlc. DO NOT EDIT. +// source: query.sql + +package querytest + +import ( + "context" +) + +const getAll = `-- name: GetAll :many +SELECT first_name, last_name, age FROM users +` + +func (q *Queries) GetAll(ctx context.Context) ([]User, error) { + rows, err := q.db.QueryContext(ctx, getAll) + if err != nil { + return nil, err + } + defer rows.Close() + var items []User + for rows.Next() { + var i User + if err := rows.Scan(&i.FirstName, &i.LastName, &i.Age); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Close(); err != nil { + return nil, err + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} diff --git a/internal/endtoend/testdata/json_tags/camel_case/query.sql b/internal/endtoend/testdata/json_tags/camel_case/query.sql new file mode 100644 index 0000000000..ab97e20a4b --- /dev/null +++ b/internal/endtoend/testdata/json_tags/camel_case/query.sql @@ -0,0 +1,8 @@ +CREATE TABLE users ( + first_name varchar(255), + last_name varchar(255), + age smallint +); + +-- name: GetAll :many +SELECT * FROM users; diff --git a/internal/endtoend/testdata/json_tags/camel_case/sqlc.json b/internal/endtoend/testdata/json_tags/camel_case/sqlc.json new file mode 100644 index 0000000000..048a1f76e1 --- /dev/null +++ b/internal/endtoend/testdata/json_tags/camel_case/sqlc.json @@ -0,0 +1,14 @@ +{ + "version": "1", + "packages": [ + { + "path": "go", + "engine": "postgresql", + "name": "querytest", + "schema": "query.sql", + "queries": "query.sql", + "emit_json_tags": true, + "json_tags_case_style": "camel" + } + ] +} diff --git a/internal/endtoend/testdata/json_tags/pascal_case/go/db.go b/internal/endtoend/testdata/json_tags/pascal_case/go/db.go new file mode 100644 index 0000000000..6a99519302 --- /dev/null +++ b/internal/endtoend/testdata/json_tags/pascal_case/go/db.go @@ -0,0 +1,29 @@ +// Code generated by sqlc. DO NOT EDIT. + +package querytest + +import ( + "context" + "database/sql" +) + +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} +} + +type Queries struct { + db DBTX +} + +func (q *Queries) WithTx(tx *sql.Tx) *Queries { + return &Queries{ + db: tx, + } +} diff --git a/internal/endtoend/testdata/json_tags/pascal_case/go/models.go b/internal/endtoend/testdata/json_tags/pascal_case/go/models.go new file mode 100644 index 0000000000..bbdb469c3e --- /dev/null +++ b/internal/endtoend/testdata/json_tags/pascal_case/go/models.go @@ -0,0 +1,13 @@ +// Code generated by sqlc. DO NOT EDIT. + +package querytest + +import ( + "database/sql" +) + +type User struct { + FirstName sql.NullString `json:"FirstName"` + LastName sql.NullString `json:"LastName"` + Age int16 `json:"Age"` +} diff --git a/internal/endtoend/testdata/json_tags/pascal_case/go/query.sql.go b/internal/endtoend/testdata/json_tags/pascal_case/go/query.sql.go new file mode 100644 index 0000000000..e65d68c909 --- /dev/null +++ b/internal/endtoend/testdata/json_tags/pascal_case/go/query.sql.go @@ -0,0 +1,35 @@ +// Code generated by sqlc. DO NOT EDIT. +// source: query.sql + +package querytest + +import ( + "context" +) + +const getAll = `-- name: GetAll :many +SELECT first_name, last_name, age FROM users +` + +func (q *Queries) GetAll(ctx context.Context) ([]User, error) { + rows, err := q.db.QueryContext(ctx, getAll) + if err != nil { + return nil, err + } + defer rows.Close() + var items []User + for rows.Next() { + var i User + if err := rows.Scan(&i.FirstName, &i.LastName, &i.Age); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Close(); err != nil { + return nil, err + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} diff --git a/internal/endtoend/testdata/json_tags/pascal_case/query.sql b/internal/endtoend/testdata/json_tags/pascal_case/query.sql new file mode 100644 index 0000000000..ab97e20a4b --- /dev/null +++ b/internal/endtoend/testdata/json_tags/pascal_case/query.sql @@ -0,0 +1,8 @@ +CREATE TABLE users ( + first_name varchar(255), + last_name varchar(255), + age smallint +); + +-- name: GetAll :many +SELECT * FROM users; diff --git a/internal/endtoend/testdata/json_tags/pascal_case/sqlc.json b/internal/endtoend/testdata/json_tags/pascal_case/sqlc.json new file mode 100644 index 0000000000..632a1e02a8 --- /dev/null +++ b/internal/endtoend/testdata/json_tags/pascal_case/sqlc.json @@ -0,0 +1,14 @@ +{ + "version": "1", + "packages": [ + { + "path": "go", + "engine": "postgresql", + "name": "querytest", + "schema": "query.sql", + "queries": "query.sql", + "emit_json_tags": true, + "json_tags_case_style": "pascal" + } + ] +} diff --git a/internal/endtoend/testdata/json_tags/snake_case/go/db.go b/internal/endtoend/testdata/json_tags/snake_case/go/db.go new file mode 100644 index 0000000000..6a99519302 --- /dev/null +++ b/internal/endtoend/testdata/json_tags/snake_case/go/db.go @@ -0,0 +1,29 @@ +// Code generated by sqlc. DO NOT EDIT. + +package querytest + +import ( + "context" + "database/sql" +) + +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} +} + +type Queries struct { + db DBTX +} + +func (q *Queries) WithTx(tx *sql.Tx) *Queries { + return &Queries{ + db: tx, + } +} diff --git a/internal/endtoend/testdata/json_tags/snake_case/go/models.go b/internal/endtoend/testdata/json_tags/snake_case/go/models.go new file mode 100644 index 0000000000..fdb14e1df5 --- /dev/null +++ b/internal/endtoend/testdata/json_tags/snake_case/go/models.go @@ -0,0 +1,13 @@ +// Code generated by sqlc. DO NOT EDIT. + +package querytest + +import ( + "database/sql" +) + +type User struct { + FirstName sql.NullString `json:"first_name"` + LastName sql.NullString `json:"last_name"` + Age int16 `json:"age"` +} diff --git a/internal/endtoend/testdata/json_tags/snake_case/go/query.sql.go b/internal/endtoend/testdata/json_tags/snake_case/go/query.sql.go new file mode 100644 index 0000000000..e65d68c909 --- /dev/null +++ b/internal/endtoend/testdata/json_tags/snake_case/go/query.sql.go @@ -0,0 +1,35 @@ +// Code generated by sqlc. DO NOT EDIT. +// source: query.sql + +package querytest + +import ( + "context" +) + +const getAll = `-- name: GetAll :many +SELECT first_name, last_name, age FROM users +` + +func (q *Queries) GetAll(ctx context.Context) ([]User, error) { + rows, err := q.db.QueryContext(ctx, getAll) + if err != nil { + return nil, err + } + defer rows.Close() + var items []User + for rows.Next() { + var i User + if err := rows.Scan(&i.FirstName, &i.LastName, &i.Age); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Close(); err != nil { + return nil, err + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} diff --git a/internal/endtoend/testdata/json_tags/snake_case/query.sql b/internal/endtoend/testdata/json_tags/snake_case/query.sql new file mode 100644 index 0000000000..ab97e20a4b --- /dev/null +++ b/internal/endtoend/testdata/json_tags/snake_case/query.sql @@ -0,0 +1,8 @@ +CREATE TABLE users ( + first_name varchar(255), + last_name varchar(255), + age smallint +); + +-- name: GetAll :many +SELECT * FROM users; diff --git a/internal/endtoend/testdata/json_tags/snake_case/sqlc.json b/internal/endtoend/testdata/json_tags/snake_case/sqlc.json new file mode 100644 index 0000000000..674a0a2919 --- /dev/null +++ b/internal/endtoend/testdata/json_tags/snake_case/sqlc.json @@ -0,0 +1,14 @@ +{ + "version": "1", + "packages": [ + { + "path": "go", + "engine": "postgresql", + "name": "querytest", + "schema": "query.sql", + "queries": "query.sql", + "emit_json_tags": true, + "json_tags_case_style": "snake" + } + ] +}