Repository: sqlc-dev/sqlc
Pull request #2606 (2023-08-11 21:21:02)

fix Go struct tag parsing
304ba5de1 didn't correctly handle struct tags whose
value contained a space. (My fault.)

Use a proper struct tag library with a dedicated parser
that handles all the edge cases. Less code, and better.


Patch:
diff --git a/.gitignore b/.gitignore
index 038cc5bde..39961ebb0 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,6 +1,7 @@
 /.idea/
 __pycache__
 .DS_Store
+.*.swp
 
 # Devenv
 .envrc
diff --git a/Dockerfile b/Dockerfile
index ad3b62088..e6c258e69 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -1,5 +1,5 @@
 # STEP 1: Build sqlc
-FROM golang:1.20.7 AS builder
+FROM golang:1.21.0 AS builder
 
 COPY . /workspace
 WORKDIR /workspace
diff --git a/docs/reference/datatypes.md b/docs/reference/datatypes.md
index fa0809ed7..b7cf5f6ef 100644
--- a/docs/reference/datatypes.md
+++ b/docs/reference/datatypes.md
@@ -9,7 +9,7 @@ If you're unsatisfied with the default, you can override any type using the
 ## Arrays
 
 PostgreSQL [arrays](https://www.postgresql.org/docs/current/arrays.html) are
-materialized as Go slices. Currently, the `pgx/v5` sql package only supports multidimensional arrays.
+materialized as Go slices.
 
 ```sql
 CREATE TABLE places (
@@ -92,7 +92,7 @@ type Store struct {
 ## Null
 
 For structs, null values are represented using the appropriate type from the
-`database/sql` or `pgx` package. 
+`database/sql` or `pgx` package.
 
 ```sql
 CREATE TABLE authors (
diff --git a/docs/requirements.txt b/docs/requirements.txt
index c4ce70119..25d6e174d 100644
--- a/docs/requirements.txt
+++ b/docs/requirements.txt
@@ -9,7 +9,7 @@ imagesize==1.4.1
 Jinja2==3.1.2
 MarkupSafe==2.1.3
 packaging==23.1
-Pygments==2.15.1
+Pygments==2.16.1
 pyparsing==3.1.1
 pytz==2023.3
 recommonmark==0.7.1
@@ -18,10 +18,10 @@ snowballstemmer==2.2.0
 Sphinx==6.2.1
 sphinx-favicon==1.0.1
 sphinx-rtd-theme==1.2.2
-sphinxcontrib-applehelp==1.0.4
+sphinxcontrib-applehelp==1.0.7
 sphinxcontrib-devhelp==1.0.2
 sphinxcontrib-serializinghtml==1.1.5
 sphinxcontrib-htmlhelp==2.0.1
 sphinxcontrib-jsmath==1.0.1
-sphinxcontrib-qthelp==1.0.3
+sphinxcontrib-qthelp==1.0.6
 urllib3==2.0.4
diff --git a/go.mod b/go.mod
index ba2ef1388..0c9f4e145 100644
--- a/go.mod
+++ b/go.mod
@@ -8,15 +8,15 @@ require (
 	github.com/cubicdaiya/gonp v1.0.4
 	github.com/davecgh/go-spew v1.1.1
 	github.com/go-sql-driver/mysql v1.7.1
-	github.com/google/cel-go v0.17.1
+	github.com/google/cel-go v0.17.6
 	github.com/google/go-cmp v0.5.9
 	github.com/jackc/pgconn v1.14.1
 	github.com/jackc/pgx/v4 v4.18.1
-	github.com/jackc/pgx/v5 v5.4.2
+	github.com/jackc/pgx/v5 v5.4.3
 	github.com/jinzhu/inflection v1.0.0
 	github.com/lib/pq v1.10.9
 	github.com/mattn/go-sqlite3 v1.14.17
-	github.com/pganalyze/pg_query_go/v4 v4.2.1
+	github.com/pganalyze/pg_query_go/v4 v4.2.3
 	github.com/riza-io/grpc-go v0.2.0
 	github.com/spf13/cobra v1.7.0
 	github.com/spf13/pflag v1.0.5
@@ -33,6 +33,7 @@ require (
 
 require (
 	github.com/cznic/mathutil v0.0.0-20181122101859-297441e03548 // indirect
+	github.com/fatih/structtag v1.2.0 // indirect
 	github.com/golang/protobuf v1.5.3 // indirect
 	github.com/inconshreveable/mousetrap v1.1.0 // indirect
 	github.com/jackc/chunkreader/v2 v2.0.1 // indirect
diff --git a/go.sum b/go.sum
index 47eef02a5..7ac2f0456 100644
--- a/go.sum
+++ b/go.sum
@@ -20,6 +20,8 @@ github.com/cznic/mathutil v0.0.0-20181122101859-297441e03548/go.mod h1:e6NPNENfs
 github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
 github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
 github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/fatih/structtag v1.2.0 h1:/OdNE99OxoI/PqaW/SuSK9uxxT3f/tcSZgon/ssNSx4=
+github.com/fatih/structtag v1.2.0/go.mod h1:mBJUNpUnHmRKrKlQQlmCrh5PuhftFbNv8Ys4/aAZl94=
 github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY=
 github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A=
 github.com/go-sql-driver/mysql v1.7.1 h1:lUIinVbN1DY0xBg0eMOzmmtGoHwWBbvnWubQUrtU8EI=
@@ -36,8 +38,8 @@ github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw
 github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
 github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
 github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
-github.com/google/cel-go v0.17.1 h1:s2151PDGy/eqpCI80/8dl4VL3xTkqI/YubXLXCFw0mw=
-github.com/google/cel-go v0.17.1/go.mod h1:HXZKzB0LXqer5lHHgfWAnlYwJaQBDKMjxjulNQzhwhY=
+github.com/google/cel-go v0.17.6 h1:QDvHTIJunIsbgN8yVukx0HGnsqVLSY6xGqo+17IjIyM=
+github.com/google/cel-go v0.17.6/go.mod h1:HXZKzB0LXqer5lHHgfWAnlYwJaQBDKMjxjulNQzhwhY=
 github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
 github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
 github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
@@ -93,8 +95,8 @@ github.com/jackc/pgx/v4 v4.0.0-pre1.0.20190824185557-6972a5742186/go.mod h1:X+GQ
 github.com/jackc/pgx/v4 v4.12.1-0.20210724153913-640aa07df17c/go.mod h1:1QD0+tgSXP7iUjYm9C1NxKhny7lq6ee99u/z+IHFcgs=
 github.com/jackc/pgx/v4 v4.18.1 h1:YP7G1KABtKpB5IHrO9vYwSrCOhs7p3uqhvhhQBptya0=
 github.com/jackc/pgx/v4 v4.18.1/go.mod h1:FydWkUyadDmdNH/mHnGob881GawxeEm7TcMCzkb+qQE=
-github.com/jackc/pgx/v5 v5.4.2 h1:u1gmGDwbdRUZiwisBm/Ky2M14uQyUP65bG8+20nnyrg=
-github.com/jackc/pgx/v5 v5.4.2/go.mod h1:q6iHT8uDNXWiFNOlRqJzBTaSH3+2xCXkokxHZC5qWFY=
+github.com/jackc/pgx/v5 v5.4.3 h1:cxFyXhxlvAifxnkKKdlxv8XqUf59tDlYjnV5YYfsJJY=
+github.com/jackc/pgx/v5 v5.4.3/go.mod h1:Ig06C2Vu0t5qXC60W8sqIthScaEnFvojjj9dSljmHRA=
 github.com/jackc/puddle v0.0.0-20190413234325-e4ced69a3a2b/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
 github.com/jackc/puddle v0.0.0-20190608224051-11cab39313c9/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
 github.com/jackc/puddle v1.1.3/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
@@ -123,8 +125,8 @@ github.com/mattn/go-isatty v0.0.7/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hd
 github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
 github.com/mattn/go-sqlite3 v1.14.17 h1:mCRHCLDUBXgpKAqIKsaAaAsrAlbkeomtRFKXh2L6YIM=
 github.com/mattn/go-sqlite3 v1.14.17/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
-github.com/pganalyze/pg_query_go/v4 v4.2.1 h1:id/vuyIQccb9f6Yx3pzH5l4QYrxE3v6/m8RPlgMrprc=
-github.com/pganalyze/pg_query_go/v4 v4.2.1/go.mod h1:aEkDNOXNM5j0YGzaAapwJ7LB3dLNj+bvbWcLv1hOVqA=
+github.com/pganalyze/pg_query_go/v4 v4.2.3 h1:cNLqyiVMasV7YGWyYV+fkXyHp32gDfXVNCqoHztEGNk=
+github.com/pganalyze/pg_query_go/v4 v4.2.3/go.mod h1:aEkDNOXNM5j0YGzaAapwJ7LB3dLNj+bvbWcLv1hOVqA=
 github.com/pingcap/errors v0.11.0/go.mod h1:Oi8TUi2kEtXXLMJk9l1cGmz20kV3TaQ0usTwv5KuLY8=
 github.com/pingcap/errors v0.11.5-0.20210425183316-da1aaba5fb63 h1:+FZIDR/D97YOPik4N4lPDaUcLDF/EQPogxtlHB2ZZRM=
 github.com/pingcap/errors v0.11.5-0.20210425183316-da1aaba5fb63/go.mod h1:X2r9ueLEUZgtx2cIogM0v4Zj5uvvzhuuiu7Pn8HzMPg=
diff --git a/internal/config/go_type.go b/internal/config/go_type.go
index 078360e57..14765e5f3 100644
--- a/internal/config/go_type.go
+++ b/internal/config/go_type.go
@@ -6,6 +6,8 @@ import (
 	"go/types"
 	"regexp"
 	"strings"
+
+	"github.com/fatih/structtag"
 )
 
 type GoType struct {
@@ -171,16 +173,12 @@ type GoStructTag string
 // `a:"b" x:"y,z"`   {"a": "b", "x": "y,z"}
 func (s GoStructTag) Parse() (map[string]string, error) {
 	m := make(map[string]string)
-	fields := strings.Fields(string(s))
-	for _, f := range fields {
-		k, v, ok := strings.Cut(f, ":")
-		if !ok {
-			return nil, fmt.Errorf("Failed to parse Go struct tag: no colon in field %q", f)
-		}
-		if len(v) < 2 || v[0] != '"' || v[len(v)-1] != '"' {
-			return nil, fmt.Errorf("Failed to parse Go struct tag: missing quotes around value in field %q", f)
-		}
-		m[k] = v[1 : len(v)-1] // trim quotes off of v
+	tags, err := structtag.Parse(string(s))
+	if err != nil {
+		return nil, err
+	}
+	for _, tag := range tags.Tags() {
+		m[tag.Key] = tag.Value()
 	}
 	return m, nil
 }


Test patch:
diff --git a/internal/endtoend/testdata/overrides_go_struct_tags/invalid_tags/stderr.txt b/internal/endtoend/testdata/overrides_go_struct_tags/invalid_tags/stderr.txt
index 4624d612a..cb6fb84ae 100644
--- a/internal/endtoend/testdata/overrides_go_struct_tags/invalid_tags/stderr.txt
+++ b/internal/endtoend/testdata/overrides_go_struct_tags/invalid_tags/stderr.txt
@@ -1,1 +1,1 @@
-error parsing sqlc.json: Failed to parse Go struct tag: no colon in field "abc"
+error parsing sqlc.json: bad syntax for struct tag pair
diff --git a/internal/endtoend/testdata/overrides_go_struct_tags/sqlite/go/models.go b/internal/endtoend/testdata/overrides_go_struct_tags/sqlite/go/models.go
index 5942323c9..bb63a04c3 100644
--- a/internal/endtoend/testdata/overrides_go_struct_tags/sqlite/go/models.go
+++ b/internal/endtoend/testdata/overrides_go_struct_tags/sqlite/go/models.go
@@ -9,14 +9,17 @@ import ()
 type Bar struct {
 	Other      string
 	AlsoTagged string `also:"tagged"`
+	Tag3       string `tag_with_space:" it's legal!"`
 }
 
 type Baz struct {
 	Other      string
 	AlsoTagged string `also:"tagged"`
+	Tag3       string `tag_with_space:" it's legal!"`
 }
 
 type Foo struct {
 	Other  string
 	Tagged string `a:"b" x:"y,z"`
+	Tag3   string `tag_with_space:" it's legal!"`
 }
diff --git a/internal/endtoend/testdata/overrides_go_struct_tags/sqlite/schema.sql b/internal/endtoend/testdata/overrides_go_struct_tags/sqlite/schema.sql
index 4d5233cc3..f908060db 100644
--- a/internal/endtoend/testdata/overrides_go_struct_tags/sqlite/schema.sql
+++ b/internal/endtoend/testdata/overrides_go_struct_tags/sqlite/schema.sql
@@ -1,14 +1,17 @@
 CREATE TABLE foo (
     other     text NOT NULL,
-    tagged    text NOT NULL
+    tagged    text NOT NULL,
+    tag3      text NOT NULL
 );
 
 CREATE TABLE bar (
     other          text NOT NULL,
-    also_tagged    text NOT NULL
+    also_tagged    text NOT NULL,
+    tag3           text NOT NULL
 );
 
 CREATE TABLE baz (
     other          text NOT NULL,
-    also_tagged    text NOT NULL
+    also_tagged    text NOT NULL,
+    tag3           text NOT NULL
 );
diff --git a/internal/endtoend/testdata/overrides_go_struct_tags/sqlite/sqlc.json b/internal/endtoend/testdata/overrides_go_struct_tags/sqlite/sqlc.json
index 0be626d44..a4d54cc3d 100644
--- a/internal/endtoend/testdata/overrides_go_struct_tags/sqlite/sqlc.json
+++ b/internal/endtoend/testdata/overrides_go_struct_tags/sqlite/sqlc.json
@@ -15,6 +15,10 @@
         {
           "go_struct_tag": "also:\"tagged\"",
           "column": "*.also_tagged"
+        },
+        {
+          "go_struct_tag": "tag_with_space:\" it's legal!\"",
+          "column": "*.tag3"
         }
       ]
     }