Skip to content

Commit 708e6f6

Browse files
fix: Fail on unknown PostgreSQL types instead of defaulting to string (fixes #30) (#72)
Changes: - Added explicit support for UUID, INET, CIDR, MACADDR, XML, MONEY, TSVECTOR, TSQUERY - Unknown PostgreSQL types now return found=false instead of silently becoming strings - Prevents silent type mismatches and incorrect SQL generation - Added comprehensive tests for all newly supported types - Updated CLAUDE.md with complete PostgreSQL type mapping documentation Breaking Change: Code using unsupported PostgreSQL types will now fail at type provider creation instead of silently converting to string. This provides better type safety and prevents hard-to-debug SQL generation issues. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude <noreply@anthropic.com>
1 parent 9ba03aa commit 708e6f6

File tree

3 files changed

+156
-8
lines changed

3 files changed

+156
-8
lines changed

CLAUDE.md

Lines changed: 27 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -77,15 +77,36 @@ go test -v -run TestFunctionName ./...
7777

7878
The library uses CEL's protobuf-based type system (`exprpb.Type`, `exprpb.Expr`). PostgreSQL types are mapped to CEL types through `pg.TypeProvider`:
7979

80-
- `text``decls.String`
81-
- `bigint` / `integer``decls.Int`
82-
- `boolean``decls.Bool`
83-
- `double precision``decls.Double`
84-
- `timestamp with time zone``decls.Timestamp`
85-
- `json` / `jsonb``decls.String` (with automatic JSON path support)
80+
**Text and String Types:**
81+
- `text`, `varchar`, `char`, `character varying`, `character``decls.String`
82+
- `xml``decls.String`
83+
- `inet`, `cidr``decls.String` (network addresses)
84+
- `macaddr`, `macaddr8``decls.String` (MAC addresses)
85+
- `tsvector`, `tsquery``decls.String` (full-text search)
86+
87+
**Numeric Types:**
88+
- `bigint`, `integer`, `int`, `int4`, `int8`, `smallint`, `int2``decls.Int`
89+
- `double precision`, `real`, `float4`, `float8`, `numeric`, `decimal``decls.Double`
90+
- `money``decls.Double`
91+
92+
**Boolean and Binary:**
93+
- `boolean`, `bool``decls.Bool`
94+
- `bytea``decls.Bytes`
95+
- `uuid``decls.Bytes`
96+
97+
**Temporal Types:**
98+
- `timestamp`, `timestamptz`, `timestamp with time zone`, `timestamp without time zone``decls.Timestamp`
99+
- `date``sqltypes.Date`
100+
- `time`, `timetz`, `time with time zone`, `time without time zone``sqltypes.Time`
101+
102+
**Structured Types:**
103+
- `json`, `jsonb``decls.Dyn` (with automatic JSON path support)
86104
- Arrays: Set `Repeated: true` in schema
87105
- Composite types: Use nested `Schema` fields
88106

107+
**Unsupported Types:**
108+
Unknown PostgreSQL types (e.g., `point`, `polygon`, `box`, custom enums) will cause `FindStructFieldType()` to return `found=false`. This prevents silent type mismatches. Add explicit support for custom types or use composite type definitions.
109+
89110
### JSON/JSONB Support
90111

91112
CEL field access on JSON/JSONB columns automatically converts to PostgreSQL JSON path operations:

pg/provider.go

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -265,13 +265,33 @@ func (p *typeProvider) FindStructFieldType(structType, fieldName string) (*types
265265
case "json", "jsonb":
266266
// JSON and JSONB types are treated as dynamic objects in CEL
267267
exprType = decls.Dyn
268+
case "uuid":
269+
// UUID is represented as bytes for proper type handling
270+
exprType = decls.Bytes
271+
case "inet", "cidr":
272+
// Network address types are represented as strings
273+
// Note: Limited CEL operations available (equality, comparison)
274+
exprType = decls.String
275+
case "macaddr", "macaddr8":
276+
// MAC address types are represented as strings
277+
exprType = decls.String
278+
case "xml":
279+
// XML data is represented as string
280+
exprType = decls.String
281+
case "money":
282+
// Money type is represented as double for numeric operations
283+
exprType = decls.Double
284+
case "tsvector", "tsquery":
285+
// Full-text search types are represented as strings
286+
exprType = decls.String
268287
default:
269288
// Handle composite types
270289
if strings.Contains(field.Type, "composite") || len(field.Schema) > 0 {
271290
exprType = decls.NewObjectType(strings.Join([]string{structType, fieldName}, "."))
272291
} else {
273-
// Default to string for unknown types
274-
exprType = decls.String
292+
// Unknown type - return not found instead of defaulting to string
293+
// This prevents silent type mismatches and incorrect SQL generation
294+
return nil, false
275295
}
276296
}
277297

pg/provider_test.go

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -236,3 +236,110 @@ func Test_typeProvider_FindStructFieldType(t *testing.T) {
236236
})
237237
}
238238
}
239+
240+
func Test_typeProvider_PostgreSQLTypes(t *testing.T) {
241+
tests := []struct {
242+
name string
243+
pgType string
244+
repeated bool
245+
wantType *types.Type
246+
wantFound bool
247+
}{
248+
{
249+
name: "uuid",
250+
pgType: "uuid",
251+
wantType: types.BytesType,
252+
wantFound: true,
253+
},
254+
{
255+
name: "uuid array",
256+
pgType: "uuid",
257+
repeated: true,
258+
wantType: types.NewListType(types.BytesType),
259+
wantFound: true,
260+
},
261+
{
262+
name: "inet",
263+
pgType: "inet",
264+
wantType: types.StringType,
265+
wantFound: true,
266+
},
267+
{
268+
name: "cidr",
269+
pgType: "cidr",
270+
wantType: types.StringType,
271+
wantFound: true,
272+
},
273+
{
274+
name: "macaddr",
275+
pgType: "macaddr",
276+
wantType: types.StringType,
277+
wantFound: true,
278+
},
279+
{
280+
name: "macaddr8",
281+
pgType: "macaddr8",
282+
wantType: types.StringType,
283+
wantFound: true,
284+
},
285+
{
286+
name: "xml",
287+
pgType: "xml",
288+
wantType: types.StringType,
289+
wantFound: true,
290+
},
291+
{
292+
name: "money",
293+
pgType: "money",
294+
wantType: types.DoubleType,
295+
wantFound: true,
296+
},
297+
{
298+
name: "tsvector",
299+
pgType: "tsvector",
300+
wantType: types.StringType,
301+
wantFound: true,
302+
},
303+
{
304+
name: "tsquery",
305+
pgType: "tsquery",
306+
wantType: types.StringType,
307+
wantFound: true,
308+
},
309+
{
310+
name: "unknown_type returns false",
311+
pgType: "unknown_custom_type",
312+
wantFound: false,
313+
},
314+
{
315+
name: "point returns false",
316+
pgType: "point",
317+
wantFound: false,
318+
},
319+
{
320+
name: "polygon returns false",
321+
pgType: "polygon",
322+
wantFound: false,
323+
},
324+
}
325+
326+
for _, tt := range tests {
327+
t.Run(tt.name, func(t *testing.T) {
328+
schema := pg.Schema{
329+
{Name: "test_field", Type: tt.pgType, Repeated: tt.repeated},
330+
}
331+
typeProvider := pg.NewTypeProvider(map[string]pg.Schema{
332+
"test_table": schema,
333+
})
334+
335+
got, gotFound := typeProvider.FindStructFieldType("test_table", "test_field")
336+
assert.Equal(t, tt.wantFound, gotFound)
337+
if tt.wantFound {
338+
assert.NotNil(t, got)
339+
assert.Equal(t, tt.wantType, got.Type)
340+
} else {
341+
assert.Nil(t, got)
342+
}
343+
})
344+
}
345+
}

0 commit comments

Comments
 (0)