mirror of
https://github.com/bitechdev/ResolveSpec.git
synced 2026-09-10 10:22:35 +00:00
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.
160 lines
3.6 KiB
Go
160 lines
3.6 KiB
Go
package common
|
|
|
|
import (
|
|
"testing"
|
|
|
|
"github.com/bitechdev/ResolveSpec/pkg/reflection"
|
|
"github.com/bitechdev/ResolveSpec/pkg/spectypes"
|
|
)
|
|
|
|
func TestExtractSourceColumn(t *testing.T) {
|
|
testCases := []struct {
|
|
name string
|
|
input string
|
|
expected string
|
|
}{
|
|
{
|
|
name: "simple column name",
|
|
input: "columna",
|
|
expected: "columna",
|
|
},
|
|
{
|
|
name: "column with ->> operator",
|
|
input: "columna->>'val'",
|
|
expected: "columna",
|
|
},
|
|
{
|
|
name: "column with -> operator",
|
|
input: "columna->'key'",
|
|
expected: "columna",
|
|
},
|
|
{
|
|
name: "column with table prefix and ->> operator",
|
|
input: "table.columna->>'val'",
|
|
expected: "table.columna",
|
|
},
|
|
{
|
|
name: "column with table prefix and -> operator",
|
|
input: "table.columna->'key'",
|
|
expected: "table.columna",
|
|
},
|
|
{
|
|
name: "complex JSON path with ->>",
|
|
input: "data->>'nested'->>'value'",
|
|
expected: "data",
|
|
},
|
|
{
|
|
name: "column with spaces before operator",
|
|
input: "columna ->>'val'",
|
|
expected: "columna",
|
|
},
|
|
}
|
|
|
|
for _, tc := range testCases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
result := reflection.ExtractSourceColumn(tc.input)
|
|
if result != tc.expected {
|
|
t.Errorf("reflection.ExtractSourceColumn(%q) = %q; want %q", tc.input, result, tc.expected)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestValidateColumnWithJSONOperators(t *testing.T) {
|
|
// Create a test model
|
|
type TestModel struct {
|
|
ID int `json:"id"`
|
|
Name string `json:"name"`
|
|
Data string `json:"data"` // JSON column
|
|
Metadata string `json:"metadata"`
|
|
}
|
|
|
|
validator := NewColumnValidator(TestModel{})
|
|
|
|
testCases := []struct {
|
|
name string
|
|
column string
|
|
shouldErr bool
|
|
}{
|
|
{
|
|
name: "simple valid column",
|
|
column: "name",
|
|
shouldErr: false,
|
|
},
|
|
{
|
|
name: "valid column with ->> operator",
|
|
column: "data->>'field'",
|
|
shouldErr: false,
|
|
},
|
|
{
|
|
name: "valid column with -> operator",
|
|
column: "metadata->'key'",
|
|
shouldErr: false,
|
|
},
|
|
{
|
|
name: "invalid column",
|
|
column: "invalid_column",
|
|
shouldErr: true,
|
|
},
|
|
{
|
|
name: "invalid column with ->> operator",
|
|
column: "invalid_column->>'field'",
|
|
shouldErr: true,
|
|
},
|
|
{
|
|
name: "cql prefixed column (always valid)",
|
|
column: "cql_computed",
|
|
shouldErr: false,
|
|
},
|
|
{
|
|
name: "empty column",
|
|
column: "",
|
|
shouldErr: false,
|
|
},
|
|
}
|
|
|
|
for _, tc := range testCases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
err := validator.ValidateColumn(tc.column)
|
|
if tc.shouldErr && err == nil {
|
|
t.Errorf("ValidateColumn(%q) expected error, got nil", tc.column)
|
|
}
|
|
if !tc.shouldErr && err != nil {
|
|
t.Errorf("ValidateColumn(%q) expected no error, got %v", tc.column, err)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestValidateColumn_JSONPathsAndDottedShorthand(t *testing.T) {
|
|
type Model struct {
|
|
ID int64 `json:"id"`
|
|
Name string `json:"name"`
|
|
Data spectypes.SqlJSONB `json:"data"`
|
|
}
|
|
v := NewColumnValidator(Model{})
|
|
|
|
valid := []string{
|
|
"data->>'city'",
|
|
"data->'addr'->>'city'",
|
|
"data#>>'{addr,city}'",
|
|
"data.addr.city", // dotted shorthand, base is JSON -> allowed
|
|
"(data->>'age')::int", // cast + paren
|
|
}
|
|
for _, c := range valid {
|
|
if err := v.ValidateColumn(c); err != nil {
|
|
t.Errorf("ValidateColumn(%q) = %v, want nil", c, err)
|
|
}
|
|
}
|
|
|
|
invalid := []string{
|
|
"nope->>'city'", // base column does not exist
|
|
"name.first", // dotted shorthand but 'name' is not a JSON column
|
|
}
|
|
for _, c := range invalid {
|
|
if err := v.ValidateColumn(c); err == nil {
|
|
t.Errorf("ValidateColumn(%q) = nil, want error", c)
|
|
}
|
|
}
|
|
}
|