feat: add JSON/JSONB sub-field select, filter and sort

Support column references that traverse into JSON/JSONB values —
data->>'x', data#>>'{a,b}', data->'a'->>'b', and the dotted data.a.b
shorthand — in SELECT column lists, WHERE filters and ORDER BY, across
the restheadspec, resolvespec, websocketspec and mqttspec handlers.

- pkg/common/json_column.go: canonical ParseColumnRef + ColumnRef.SQL()
  builder. JSON path segments are bound as a single ?::text[] parameter,
  never interpolated; cast targets are whitelisted via NormalizeCastTarget.
- pkg/common/json_condition.go: shared entry points mirroring
  BuildSpatialCondition - ResolveJSONColumnExpr (select/sort),
  BuildJSONFilterCondition (where, full operator set; infers ::numeric for
  ordered comparisons on numeric values when no explicit cast is given),
  and the ApplySelectColumns helper.
- pkg/reflection.IsJSONColumn / pkg/spectypes.IsJSONType: disambiguate the
  dotted shorthand (data.city is JSON only when the base is a JSON column).
- pkg/common/validation.go: ColumnValidator accepts JSON tokens.
- Handlers: thread model through the filter call chains and wire the
  select/sort paths.

funcspec (raw-SQL string builder, no param binding or model) and the
FetchRowNumber raw-SQL builders are left as follow-ups, as is OpenAPI
reporting of JSON sub-field columns.
This commit is contained in:
Hein
2026-09-07 17:05:52 +02:00
parent f841d58c59
commit 206edd4bfd
17 changed files with 1666 additions and 47 deletions
+6
View File
@@ -91,3 +91,9 @@ func IsVectorType(t reflect.Type) bool {
n, ok := SQLTypeName(t)
return ok && (n == "vector" || n == "halfvec" || n == "sparsevec")
}
// IsJSONType reports whether t is a spectypes JSON/JSONB wrapper.
func IsJSONType(t reflect.Type) bool {
n, ok := SQLTypeName(t)
return ok && (n == "jsonb" || n == "json")
}
+14
View File
@@ -51,6 +51,20 @@ func TestIsSpatialType(t *testing.T) {
}
}
func TestIsJSONType(t *testing.T) {
if !IsJSONType(reflect.TypeOf(SqlJSONB{})) {
t.Error("SqlJSONB should be a JSON type")
}
if !IsJSONType(reflect.TypeOf(&SqlJSONB{})) {
t.Error("*SqlJSONB should be a JSON type (pointer unwrapped)")
}
for _, v := range []any{SqlGeometry{}, SqlVector{}, SqlString{}, SqlStringArray{}, ""} {
if IsJSONType(reflect.TypeOf(v)) {
t.Errorf("%T should not be a JSON type", v)
}
}
}
func TestIsVectorType(t *testing.T) {
for _, v := range []any{SqlVector{}, SqlHalfVector{}, SqlSparseVector{}} {
if !IsVectorType(reflect.TypeOf(v)) {