Skip to content
Closed
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Add queryParamPushdown config inheritance support
Implement inheritance for queryParamPushdown configuration, following the
same pattern as other config options (pagination, requestTranslate, etc.):

  Method → Resource → Service → ProviderService → Provider

Changes:
- Add GetQueryParamPushdown() to OperationStore interface with inheritance
- Add GetQueryParamPushdown() to Provider, ProviderService, Resource interfaces
- Add getQueryParamPushdown() to OpenAPIService interface
- Update test provider to demonstrate all three inheritance levels:
  - Service-level: airlines (inherits from x-stackQL-config)
  - Resource-level: people (overrides service-level)
  - Method-level: airports (overrides service-level)
- Update skill file and provider_spec.md documentation

This enables setting a default OData config at the service level for
providers like EntraID, avoiding repetitive config at every method level.
  • Loading branch information
claude committed Nov 29, 2025
commit 353381f51d9481baf035a5a19ecf2a82678a9683
130 changes: 91 additions & 39 deletions .claude/skills/stackql-provider-development.md
Original file line number Diff line number Diff line change
Expand Up @@ -525,7 +525,13 @@ sqlExternalTables:

Enables SQL clause pushdown to API query parameters. Supports OData and custom API dialects for filter, projection, ordering, and limit operations.

**Location:** Must be set at the **method level** within `methods.<methodName>.config.queryParamPushdown`. Unlike other config options (pagination, requestTranslate, etc.), queryParamPushdown does NOT inherit from resource, service, or provider levels.
**Location:** Can be set at **provider, providerService, service, resource, or method level**. Config inherits from higher levels, with lower levels overriding higher levels:

```
Method -> Resource -> Service -> ProviderService -> Provider
```

This allows you to set a default OData config at the service level and have all resources/methods inherit it, while still allowing specific methods to override with custom settings.

```yaml
x-stackQL-config:
Expand Down Expand Up @@ -659,46 +665,92 @@ x-stackQL-config:
| `["col1", "col2"]` | Only these items supported |
| `[]` | No items supported (effectively disabled) |

**OData Example (TripPin Reference Service):**
**OData Example with Inheritance (TripPin Reference Service):**

Set a default config at service level, then override at resource or method level as needed:

```yaml
x-stackQL-resources:
people:
id: odata.trippin.people
name: people
methods:
list:
operation:
$ref: '#/paths/~1People/get'
response:
mediaType: application/json
openAPIDocKey: '200'
objectKey: $.value[*]
config:
queryParamPushdown:
select:
dialect: odata
filter:
dialect: odata
supportedOperators:
- "eq"
- "ne"
- "gt"
- "lt"
- "ge"
- "le"
- "contains"
- "startswith"
- "endswith"
orderBy:
dialect: odata
top:
dialect: odata
count:
dialect: odata
sqlVerbs:
select:
- $ref: '#/components/x-stackQL-resources/people/methods/list'
# Service-level config - inherited by all resources/methods
x-stackQL-config:
queryParamPushdown:
select:
dialect: odata
filter:
dialect: odata
supportedOperators:
- "eq"
- "ne"
orderBy:
dialect: odata
top:
dialect: odata
count:
dialect: odata

components:
x-stackQL-resources:
# Inherits service-level config (no override needed)
airlines:
id: odata.trippin.airlines
name: airlines
methods:
list:
operation:
$ref: '#/paths/~1Airlines/get'
# No config - inherits from service level
sqlVerbs:
select:
- $ref: '#/components/x-stackQL-resources/airlines/methods/list'

# Resource-level override with full operator support
people:
id: odata.trippin.people
name: people
config:
queryParamPushdown:
filter:
dialect: odata
supportedOperators:
- "eq"
- "ne"
- "gt"
- "lt"
- "contains"
- "startswith"
methods:
list:
operation:
$ref: '#/paths/~1People/get'
sqlVerbs:
select:
- $ref: '#/components/x-stackQL-resources/people/methods/list'

# Method-level override with restricted columns
airports:
id: odata.trippin.airports
name: airports
methods:
list:
operation:
$ref: '#/paths/~1Airports/get'
config:
queryParamPushdown:
select:
dialect: odata
supportedColumns:
- "Name"
- "IcaoCode"
filter:
dialect: odata
supportedColumns:
- "Name"
- "IcaoCode"
top:
dialect: odata
maxValue: 100
sqlVerbs:
select:
- $ref: '#/components/x-stackQL-resources/airports/methods/list'
```

---
Expand Down
37 changes: 37 additions & 0 deletions anysdk/operation_store.go
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ type OperationStore interface {
GetGraphQL() GraphQL
GetInverse() (OperationInverse, bool)
GetStackQLConfig() StackQLConfig
GetQueryParamPushdown() (QueryParamPushdown, bool)
GetParameters() map[string]Addressable
GetPathItem() *openapi3.PathItem
GetAPIMethod() string
Expand Down Expand Up @@ -405,6 +406,42 @@ func (op *standardOpenAPIOperationStore) getStackQLConfig() (StackQLConfig, bool
return rv, rv != nil
}

// GetQueryParamPushdown returns the queryParamPushdown config with inheritance.
// It walks up the hierarchy: Method -> Resource -> Service -> ProviderService -> Provider
func (op *standardOpenAPIOperationStore) GetQueryParamPushdown() (QueryParamPushdown, bool) {
// Check method-level config first
if op.StackQLConfig != nil {
if qpp, ok := op.StackQLConfig.GetQueryParamPushdown(); ok {
return qpp, true
}
}
// Check resource-level config
if op.Resource != nil {
if qpp, ok := op.Resource.GetQueryParamPushdown(); ok {
return qpp, true
}
}
// Check service-level config
if op.OpenAPIService != nil {
if qpp, ok := op.OpenAPIService.getQueryParamPushdown(); ok {
return qpp, true
}
}
// Check providerService-level config
if op.ProviderService != nil {
if qpp, ok := op.ProviderService.GetQueryParamPushdown(); ok {
return qpp, true
}
}
// Check provider-level config
if op.Provider != nil {
if qpp, ok := op.Provider.GetQueryParamPushdown(); ok {
return qpp, true
}
}
return nil, false
}

func (op *standardOpenAPIOperationStore) GetAPIMethod() string {
return op.APIMethod
}
Expand Down
8 changes: 8 additions & 0 deletions anysdk/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ type Provider interface {
GetProviderServices() map[string]ProviderService
GetPaginationRequestTokenSemantic() (TokenSemantic, bool)
GetPaginationResponseTokenSemantic() (TokenSemantic, bool)
GetQueryParamPushdown() (QueryParamPushdown, bool)
GetProviderService(key string) (ProviderService, error)
getQueryTransposeAlgorithm() string
GetRequestTranslateAlgorithm() string
Expand Down Expand Up @@ -128,6 +129,13 @@ func (pr *standardProvider) GetPaginationResponseTokenSemantic() (TokenSemantic,
return pr.StackQLConfig.Pagination.ResponseToken, true
}

func (pr *standardProvider) GetQueryParamPushdown() (QueryParamPushdown, bool) {
if pr.StackQLConfig != nil {
return pr.StackQLConfig.GetQueryParamPushdown()
}
return nil, false
}

func (pr *standardProvider) MarshalJSON() ([]byte, error) {
return jsoninfo.MarshalStrictStruct(pr)
}
Expand Down
8 changes: 8 additions & 0 deletions anysdk/providerService.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ type ProviderService interface {
GetResourcesShallow() (ResourceRegister, error)
GetPaginationRequestTokenSemantic() (TokenSemantic, bool)
getPaginationResponseTokenSemantic() (TokenSemantic, bool)
GetQueryParamPushdown() (QueryParamPushdown, bool)
ConditionIsValid(lhs string, rhs interface{}) bool
GetID() string
GetServiceFragment(resourceKey string) (Service, error)
Expand Down Expand Up @@ -178,6 +179,13 @@ func (sv *standardProviderService) getPaginationResponseTokenSemantic() (TokenSe
return sv.StackQLConfig.Pagination.ResponseToken, true
}

func (sv *standardProviderService) GetQueryParamPushdown() (QueryParamPushdown, bool) {
if sv.StackQLConfig != nil {
return sv.StackQLConfig.GetQueryParamPushdown()
}
return nil, false
}

func (sv *standardProviderService) ConditionIsValid(lhs string, rhs interface{}) bool {
elem := sv.ToMap()[lhs]
return reflect.TypeOf(elem) == reflect.TypeOf(rhs)
Expand Down
75 changes: 57 additions & 18 deletions anysdk/registry_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -665,7 +665,11 @@ func execTestQueryParamPushdownConfig(t *testing.T, r RegistryAPI) {
}
assert.Assert(t, sh != nil)

// Test 'people' resource - full OData support with all columns
// =====================================================
// Test RESOURCE-LEVEL inheritance: 'people' resource
// Config is at resource level, overrides service-level
// =====================================================
t.Log("Testing RESOURCE-level config inheritance (people)")
sv, err := r.GetServiceFragment(sh, "people")
assert.NilError(t, err)
assert.Assert(t, sv != nil)
Expand All @@ -677,22 +681,21 @@ func execTestQueryParamPushdownConfig(t *testing.T, r RegistryAPI) {
assert.NilError(t, methodErr)
assert.Assert(t, method != nil)

cfg := method.GetStackQLConfig()
assert.Assert(t, cfg != nil)

// Get queryParamPushdown config from the method's StackQL config
qpp, qppExists := cfg.GetQueryParamPushdown()
// Get queryParamPushdown config using the inheritance-aware method
// This walks up: Method -> Resource -> Service -> ProviderService -> Provider
qpp, qppExists := method.GetQueryParamPushdown()
assert.Assert(t, qppExists, "expected queryParamPushdown config to exist")

// Test filter config - full OData operator support
// Test filter config - full OData operator support (resource-level config)
filterPD, filterExists := qpp.GetFilter()
assert.Assert(t, filterExists, "expected filter pushdown config to exist")
assert.Equal(t, filterPD.GetDialect(), "odata")
assert.Equal(t, filterPD.GetParamName(), "$filter") // OData default
// Resource-level config has full operator support
assert.Assert(t, filterPD.IsOperatorSupported("eq"))
assert.Assert(t, filterPD.IsOperatorSupported("ne"))
assert.Assert(t, filterPD.IsOperatorSupported("gt"))
assert.Assert(t, filterPD.IsOperatorSupported("lt"))
assert.Assert(t, filterPD.IsOperatorSupported("gt"), "resource-level should have gt operator")
assert.Assert(t, filterPD.IsOperatorSupported("lt"), "resource-level should have lt operator")
assert.Assert(t, filterPD.IsOperatorSupported("contains"))
assert.Assert(t, filterPD.IsOperatorSupported("startswith"))
assert.Assert(t, filterPD.IsOperatorSupported("endswith"))
Expand Down Expand Up @@ -735,7 +738,43 @@ func execTestQueryParamPushdownConfig(t *testing.T, r RegistryAPI) {
assert.Equal(t, countPD.GetParamValue(), "true") // OData default
assert.Equal(t, countPD.GetResponseKey(), "@odata.count") // OData default

// Test 'airports' resource - OData support with restricted columns and maxValue
// =====================================================
// Test SERVICE-LEVEL inheritance: 'airlines' resource
// NO config at resource or method level - inherits from service
// =====================================================
t.Log("Testing SERVICE-level config inheritance (airlines)")
svAirlines, err := r.GetServiceFragment(sh, "airlines")
assert.NilError(t, err)

rscAirlines, err := svAirlines.GetResource("airlines")
assert.NilError(t, err)

methodAirlines, methodAirlinesErr := rscAirlines.GetMethods().FindMethod("list")
assert.NilError(t, methodAirlinesErr)

// Use inheritance-aware method - should get service-level config
qppAirlines, qppAirlinesExists := methodAirlines.GetQueryParamPushdown()
assert.Assert(t, qppAirlinesExists, "expected service-level queryParamPushdown to be inherited")

// Service-level config only has eq and ne operators
filterAirlines, filterAirlinesExists := qppAirlines.GetFilter()
assert.Assert(t, filterAirlinesExists)
assert.Assert(t, filterAirlines.IsOperatorSupported("eq"), "service-level should have eq")
assert.Assert(t, filterAirlines.IsOperatorSupported("ne"), "service-level should have ne")
// Service-level does NOT have these operators (only resource-level people has them)
assert.Assert(t, !filterAirlines.IsOperatorSupported("gt"), "service-level should NOT have gt")
assert.Assert(t, !filterAirlines.IsOperatorSupported("contains"), "service-level should NOT have contains")

// Service-level has select, orderBy, top, count
selectAirlines, selectAirlinesExists := qppAirlines.GetSelect()
assert.Assert(t, selectAirlinesExists, "service-level should have select config")
assert.Equal(t, selectAirlines.GetDialect(), "odata")

// =====================================================
// Test METHOD-LEVEL inheritance: 'airports' resource
// Config at method level overrides service-level
// =====================================================
t.Log("Testing METHOD-level config inheritance (airports)")
svAirports, err := r.GetServiceFragment(sh, "airports")
assert.NilError(t, err)

Expand All @@ -745,18 +784,18 @@ func execTestQueryParamPushdownConfig(t *testing.T, r RegistryAPI) {
methodAirports, methodAirportsErr := rscAirports.GetMethods().FindMethod("list")
assert.NilError(t, methodAirportsErr)

cfgAirports := methodAirports.GetStackQLConfig()
assert.Assert(t, cfgAirports != nil)

qppAirports, qppAirportsExists := cfgAirports.GetQueryParamPushdown()
// Use inheritance-aware method
qppAirports, qppAirportsExists := methodAirports.GetQueryParamPushdown()
assert.Assert(t, qppAirportsExists)

// Test filter with restricted columns
// Method-level has restricted columns (not present in service-level)
filterAirports, filterAirportsExists := qppAirports.GetFilter()
assert.Assert(t, filterAirportsExists)
assert.Assert(t, filterAirports.IsColumnSupported("Name"))
assert.Assert(t, filterAirports.IsColumnSupported("IcaoCode"))
assert.Assert(t, !filterAirports.IsColumnSupported("Location"), "Location is not in supportedColumns")
// Method-level has contains operator (unlike service-level)
assert.Assert(t, filterAirports.IsOperatorSupported("contains"), "method-level should have contains")

// Test select with restricted columns
selectAirports, selectAirportsExists := qppAirports.GetSelect()
Expand All @@ -765,10 +804,10 @@ func execTestQueryParamPushdownConfig(t *testing.T, r RegistryAPI) {
assert.Assert(t, selectAirports.IsColumnSupported("IataCode"))
assert.Assert(t, !selectAirports.IsColumnSupported("Location"))

// Test top with maxValue
// Test top with maxValue (method-level specific)
topAirports, topAirportsExists := qppAirports.GetTop()
assert.Assert(t, topAirportsExists)
assert.Equal(t, topAirports.GetMaxValue(), 100)
assert.Equal(t, topAirports.GetMaxValue(), 100, "method-level should have maxValue=100")

t.Logf("TestQueryParamPushdownConfig passed")
t.Logf("TestQueryParamPushdownConfig passed - all inheritance levels tested")
}
8 changes: 8 additions & 0 deletions anysdk/resource.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ type Resource interface {
GetRequestTranslateAlgorithm() string
GetPaginationRequestTokenSemantic() (TokenSemantic, bool)
GetPaginationResponseTokenSemantic() (TokenSemantic, bool)
GetQueryParamPushdown() (QueryParamPushdown, bool)
FindMethod(key string) (StandardOperationStore, error)
GetFirstMethodFromSQLVerb(sqlVerb string) (StandardOperationStore, string, bool)
GetFirstNamespaceMethodMatchFromSQLVerb(sqlVerb string, parameters map[string]interface{}) (StandardOperationStore, map[string]interface{}, bool)
Expand Down Expand Up @@ -201,6 +202,13 @@ func (r *standardResource) GetPaginationResponseTokenSemantic() (TokenSemantic,
return nil, false
}

func (r *standardResource) GetQueryParamPushdown() (QueryParamPushdown, bool) {
if r.StackQLConfig != nil {
return r.StackQLConfig.GetQueryParamPushdown()
}
return nil, false
}

func (rsc standardResource) JSONLookup(token string) (interface{}, error) {
ss := strings.Split(token, "/")
tokenRoot := ""
Expand Down
Loading
Loading