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
+13
View File
@@ -109,6 +109,19 @@ func (v *ColumnValidator) ValidateColumn(column string) error {
return nil
}
// JSON-traversing references (data->>'x', data#>>'{a,b}', or the dotted
// data.x shorthand): validate the base column, and for the ambiguous
// dotted form require that the base is actually a JSON column.
if ref, isJSON := ParseColumnRef(column); isJSON {
if ref.Ambiguous && !reflection.IsJSONColumn(v.model, ref.Base) {
return fmt.Errorf("invalid column '%s': '%s' is not a JSON column", column, ref.Base)
}
if _, exists := v.validColumns[strings.ToLower(ref.Base)]; !exists {
return fmt.Errorf("invalid column '%s': column does not exist in model", column)
}
return nil
}
// Extract source column name (remove JSON operators like ->> or ->)
sourceColumn := reflection.ExtractSourceColumn(column)