Skip to content

Commit 2566482

Browse files
Fix PostgreSQL timestamptz db_type override alias matching.
Canonicalize PostgreSQL datetime type names before comparing db_type overrides so timestamptz and timestamp with time zone are treated as equivalent. Fixes #2914. Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent ecec179 commit 2566482

9 files changed

Lines changed: 246 additions & 2 deletions

File tree

internal/codegen/golang/opts/override.go

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -78,9 +78,10 @@ func (o *Override) Matches(n *plugin.Identifier, defaultSchema string) bool {
7878
}
7979

8080
func (o *Override) MatchesColumn(col *plugin.Column) bool {
81-
columnType := sdk.DataType(col.Type)
81+
columnType := canonicalPostgreSQLType(sdk.DataType(col.Type))
82+
overrideType := canonicalPostgreSQLType(o.DBType)
8283
notNull := col.NotNull || col.IsArray
83-
return o.DBType != "" && o.DBType == columnType && o.Nullable != notNull && o.Unsigned == col.Unsigned
84+
return o.DBType != "" && overrideType == columnType && o.Nullable != notNull && o.Unsigned == col.Unsigned
8485
}
8586

8687
func (o *Override) parse(req *plugin.GenerateRequest) (err error) {

internal/codegen/golang/opts/override_test.go

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,12 @@
11
package opts
22

33
import (
4+
"strings"
45
"testing"
56

67
"github.com/google/go-cmp/cmp"
8+
9+
"github.com/sqlc-dev/sqlc/internal/plugin"
710
)
811

912
func TestTypeOverrides(t *testing.T) {
@@ -100,6 +103,104 @@ func TestTypeOverrides(t *testing.T) {
100103
}
101104
}
102105

106+
func TestMatchesColumnTimestamptzAliases(t *testing.T) {
107+
t.Parallel()
108+
109+
parseOverride := func(t *testing.T, dbType string, nullable bool) Override {
110+
t.Helper()
111+
o := Override{
112+
DBType: dbType,
113+
Nullable: nullable,
114+
GoType: GoType{Spec: "*time.Time"},
115+
}
116+
if err := o.parse(nil); err != nil {
117+
t.Fatalf("override parsing failed: %s", err)
118+
}
119+
return o
120+
}
121+
122+
column := func(typeName string, nullable bool) *plugin.Column {
123+
typ := &plugin.Identifier{Name: typeName}
124+
if schema, name, ok := strings.Cut(typeName, "."); ok && schema == "pg_catalog" {
125+
typ = &plugin.Identifier{Schema: schema, Name: name}
126+
}
127+
return &plugin.Column{
128+
Type: typ,
129+
NotNull: !nullable,
130+
}
131+
}
132+
133+
for _, test := range []struct {
134+
name string
135+
override Override
136+
column *plugin.Column
137+
wantMatch bool
138+
}{
139+
{
140+
name: "timestamptz override matches timestamptz column",
141+
override: parseOverride(t, "timestamptz", true),
142+
column: column("timestamptz", true),
143+
wantMatch: true,
144+
},
145+
{
146+
name: "timestamptz override matches timestamp with time zone column",
147+
override: parseOverride(t, "timestamptz", true),
148+
column: column("timestamp with time zone", true),
149+
wantMatch: true,
150+
},
151+
{
152+
name: "timestamptz override matches pg_catalog.timestamptz column",
153+
override: parseOverride(t, "timestamptz", true),
154+
column: column("pg_catalog.timestamptz", true),
155+
wantMatch: true,
156+
},
157+
{
158+
name: "pg_catalog.timestamptz override matches timestamptz column",
159+
override: parseOverride(t, "pg_catalog.timestamptz", true),
160+
column: column("timestamptz", true),
161+
wantMatch: true,
162+
},
163+
{
164+
name: "pg_catalog.timestamptz override matches timestamp with time zone column",
165+
override: parseOverride(t, "pg_catalog.timestamptz", true),
166+
column: column("timestamp with time zone", true),
167+
wantMatch: true,
168+
},
169+
{
170+
name: "timestamp with time zone override matches timestamptz column",
171+
override: parseOverride(t, "timestamp with time zone", true),
172+
column: column("timestamptz", true),
173+
wantMatch: true,
174+
},
175+
{
176+
name: "timestamptz override does not match not-null column",
177+
override: parseOverride(t, "timestamptz", true),
178+
column: column("timestamptz", false),
179+
wantMatch: false,
180+
},
181+
{
182+
name: "timestamptz override does not match timestamp without time zone",
183+
override: parseOverride(t, "timestamptz", true),
184+
column: column("timestamp", true),
185+
wantMatch: false,
186+
},
187+
{
188+
name: "timestamptz override does not match timestamp without time zone long form",
189+
override: parseOverride(t, "timestamptz", true),
190+
column: column("timestamp without time zone", true),
191+
wantMatch: false,
192+
},
193+
} {
194+
t.Run(test.name, func(t *testing.T) {
195+
t.Parallel()
196+
got := test.override.MatchesColumn(test.column)
197+
if got != test.wantMatch {
198+
t.Errorf("MatchesColumn() = %v, want %v", got, test.wantMatch)
199+
}
200+
})
201+
}
202+
}
203+
103204
func FuzzOverride(f *testing.F) {
104205
for _, spec := range []string{
105206
"string",
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
package opts
2+
3+
var pgTypeCanonicalNames map[string]string
4+
5+
func init() {
6+
groups := []struct {
7+
canonical string
8+
aliases []string
9+
}{
10+
{"pg_catalog.timestamptz", []string{"timestamptz", "timestamp with time zone"}},
11+
{"pg_catalog.timestamp", []string{"timestamp", "timestamp without time zone"}},
12+
{"pg_catalog.time", []string{"time", "time without time zone"}},
13+
{"pg_catalog.timetz", []string{"timetz", "time with time zone"}},
14+
}
15+
16+
pgTypeCanonicalNames = make(map[string]string, len(groups)*3)
17+
for _, g := range groups {
18+
pgTypeCanonicalNames[g.canonical] = g.canonical
19+
for _, alias := range g.aliases {
20+
pgTypeCanonicalNames[alias] = g.canonical
21+
}
22+
}
23+
}
24+
25+
// canonicalPostgreSQLType maps PostgreSQL type aliases to a single canonical name
26+
// so db_type overrides match regardless of spelling in schema SQL or config.
27+
func canonicalPostgreSQLType(t string) string {
28+
if canonical, ok := pgTypeCanonicalNames[t]; ok {
29+
return canonical
30+
}
31+
return t
32+
}

internal/endtoend/testdata/overrides_timestamptz_alias/postgresql/pgx/v5/go/db.go

Lines changed: 32 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

internal/endtoend/testdata/overrides_timestamptz_alias/postgresql/pgx/v5/go/models.go

Lines changed: 14 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

internal/endtoend/testdata/overrides_timestamptz_alias/postgresql/pgx/v5/go/query.sql.go

Lines changed: 34 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
-- name: ListUsers :many
2+
SELECT * FROM users;
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
CREATE TABLE users (
2+
short_form timestamptz,
3+
long_form timestamp with time zone
4+
);
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
{
2+
"version": "1",
3+
"packages": [
4+
{
5+
"path": "go",
6+
"engine": "postgresql",
7+
"sql_package": "pgx/v5",
8+
"name": "querytest",
9+
"schema": "schema.sql",
10+
"queries": "query.sql"
11+
}
12+
],
13+
"overrides": [
14+
{
15+
"db_type": "timestamptz",
16+
"nullable": true,
17+
"go_type": {
18+
"import": "time",
19+
"type": "Time",
20+
"pointer": true
21+
}
22+
}
23+
]
24+
}

0 commit comments

Comments
 (0)