Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
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
232 changes: 232 additions & 0 deletions .claude/skills/stackql-provider-development.md
Original file line number Diff line number Diff line change
Expand Up @@ -521,6 +521,238 @@ sqlExternalTables:
precision: integer
```

### Query Parameter Pushdown (`queryParamPushdown`)

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

**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:
queryParamPushdown:
# Column projection (SELECT clause pushdown)
select:
dialect: odata | custom # "custom" is default; "odata" applies OData defaults
paramName: "$select" # Not required for OData (default: "$select")
delimiter: "," # Not required for OData (default: ",")
supportedColumns: # Optional, omit or ["*"] for all columns
- "id"
- "name"
- "status"

# Row filtering (WHERE clause pushdown)
filter:
dialect: odata | custom # "custom" is default; "odata" applies OData defaults
paramName: "$filter" # Not required for OData (default: "$filter")
syntax: odata # Not required for OData (default: "odata")
supportedOperators: # Required - which operators can be pushed down
- "eq"
- "ne"
- "gt"
- "lt"
- "ge"
- "le"
- "contains"
- "startswith"
supportedColumns: # Optional, omit or ["*"] for all columns
- "displayName"
- "status"
- "createdDate"

# Ordering (ORDER BY clause pushdown)
orderBy:
dialect: odata | custom # "custom" is default; "odata" applies OData defaults
paramName: "$orderby" # Not required for OData (default: "$orderby")
syntax: odata # Not required for OData (default: "odata")
supportedColumns: # Optional, omit or ["*"] for all columns
- "name"
- "createdDate"

# Row limit (LIMIT clause pushdown)
top:
dialect: odata | custom # "custom" is default; "odata" applies OData defaults
paramName: "$top" # Not required for OData (default: "$top")
maxValue: 1000 # Optional, cap on pushdown value

# Count (SELECT COUNT(*) pushdown)
count:
dialect: odata | custom # "custom" is default; "odata" applies OData defaults
paramName: "$count" # Not required for OData (default: "$count")
paramValue: "true" # Not required for OData (default: "true")
responseKey: "@odata.count"# Not required for OData (default: "@odata.count")
```

**Minimal OData Configuration:**

When using OData dialect, defaults are applied automatically:

```yaml
x-stackQL-config:
queryParamPushdown:
select: {}
filter:
dialect: odata
supportedOperators: ["eq", "ne", "gt", "lt", "contains"]
orderBy:
dialect: odata
top:
dialect: odata
count:
dialect: odata
```

**Custom API Configuration:**

For APIs with custom query parameter names:

```yaml
x-stackQL-config:
queryParamPushdown:
select:
paramName: "fields"
delimiter: ","
filter:
paramName: "filter"
syntax: "key_value" # filter[status]=active&filter[region]=us-east-1
supportedOperators:
- "eq"
supportedColumns:
- "status"
- "region"
orderBy:
paramName: "sort"
syntax: "prefix" # sort=-createdAt (prefix - for desc)
supportedColumns:
- "createdAt"
- "name"
top:
paramName: "limit"
maxValue: 100
count:
paramName: "include_count"
paramValue: "1"
responseKey: "meta.total"
```

**Supported Filter Syntaxes:**

| Syntax | Example Output | Use Case |
|--------|---------------|----------|
| `odata` | `$filter=status eq 'active' and region eq 'us-east-1'` | OData APIs |
| `key_value` | `filter[status]=active&filter[region]=us-east-1` | Rails-style APIs |
| `simple` | `status=active&region=us-east-1` | Basic query params |

**Supported OrderBy Syntaxes:**

| Syntax | Example | Notes |
|--------|---------|-------|
| `odata` | `$orderby=name desc,date asc` | Space-separated direction |
| `prefix` | `sort=-name,+date` | `-` for desc, `+` or none for asc |
| `suffix` | `sort=name:desc,date:asc` | Colon-separated direction |

**Column/Operator Support Logic:**

| Value | Behavior |
|-------|----------|
| omitted / `null` | All items allowed |
| `["*"]` | Explicit "all items" (same as omitted) |
| `["col1", "col2"]` | Only these items supported |
| `[]` | No items supported (effectively disabled) |

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

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

```yaml
# 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'
```

---

## Method Definition
Expand Down
11 changes: 11 additions & 0 deletions anysdk/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ type StackQLConfig interface {
GetVariations() (Variations, bool)
GetViews() map[string]View
GetExternalTables() map[string]SQLExternalTable
GetQueryParamPushdown() (QueryParamPushdown, bool)
//
isObjectSchemaImplicitlyUnioned() bool
setResource(rsc Resource)
Expand All @@ -36,6 +37,7 @@ type standardStackQLConfig struct {
Views map[string]*standardViewContainer `json:"views" yaml:"views"`
ExternalTables map[string]standardSQLExternalTable `json:"sqlExternalTables" yaml:"sqlExternalTables"`
Auth *standardAuthDTO `json:"auth,omitempty" yaml:"auth,omitempty"`
QueryParamPushdown *standardQueryParamPushdown `json:"queryParamPushdown,omitempty" yaml:"queryParamPushdown,omitempty"`
}

func (qt standardStackQLConfig) JSONLookup(token string) (interface{}, error) {
Expand All @@ -48,6 +50,8 @@ func (qt standardStackQLConfig) JSONLookup(token string) (interface{}, error) {
return qt.RequestTranslate, nil
case "views":
return qt.Views, nil
case "queryParamPushdown":
return qt.QueryParamPushdown, nil
default:
return nil, fmt.Errorf("could not resolve token '%s' from QueryTranspose doc object", token)
}
Expand Down Expand Up @@ -126,6 +130,13 @@ func (cfg *standardStackQLConfig) GetAuth() (AuthDTO, bool) {
return cfg.Auth, cfg.Auth != nil
}

func (cfg *standardStackQLConfig) GetQueryParamPushdown() (QueryParamPushdown, bool) {
if cfg.QueryParamPushdown == nil {
return nil, false
}
return cfg.QueryParamPushdown, true
}

func (cfg *standardStackQLConfig) GetExternalTables() map[string]SQLExternalTable {
rv := make(map[string]SQLExternalTable, len(cfg.ExternalTables))
if cfg.ExternalTables != nil {
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
Loading
Loading