diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index f015726..49da74f 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -35,7 +35,7 @@ jobs: steps: - name: Check out code into the Go module directory - uses: actions/checkout@v4.1.1 + uses: actions/checkout@v7 with: repository: ${{ env.STACKQL_CORE_REPOSITORY }} ref: ${{ env.STACKQL_CORE_REF }} @@ -43,7 +43,7 @@ jobs: path: stackql-core-pkg - name: Setup Python - uses: actions/setup-python@v5.0.0 + uses: actions/setup-python@v6 with: cache: pip python-version: '3.12' @@ -64,7 +64,7 @@ jobs: cicd/util/01-build-robot-lib.sh - name: Upload python package artifact - uses: actions/upload-artifact@v4.3.1 + uses: actions/upload-artifact@v7 with: name: python-package-dist-folder path: stackql-core-pkg/test/dist @@ -74,9 +74,10 @@ jobs: runs-on: windows-latest steps: - name: Set up Go 1.x - uses: actions/setup-go@v2 + uses: actions/setup-go@v6 with: go-version: ^${{ env.GOLANG_VERSION }} + cache: false id: go - name: Get rid of dissruptive line endings before checkout @@ -84,7 +85,7 @@ jobs: git config --global core.autocrlf false - name: Check out code into the Go module directory - uses: actions/checkout@v2 + uses: actions/checkout@v7 - name: Get dependencies run: | @@ -133,13 +134,14 @@ jobs: runs-on: ubuntu-latest steps: - name: Set up Go 1.x - uses: actions/setup-go@v2 + uses: actions/setup-go@v6 with: go-version: ^${{ env.GOLANG_VERSION }} + cache: false id: go - name: Check out code into the Go module directory - uses: actions/checkout@v2 + uses: actions/checkout@v7 - name: API sprawl check run: | @@ -156,7 +158,7 @@ jobs: sudo apt-get install -y jq - name: Setup Python - uses: actions/setup-python@v5.0.0 + uses: actions/setup-python@v6 with: # cache: pip # this requires requirements in source control python-version: '3.12' @@ -167,7 +169,7 @@ jobs: pip3 install -r cicd/testing-requirements.txt - name: Download python package dist folder - uses: actions/download-artifact@v4.1.2 + uses: actions/download-artifact@v8 with: name: python-package-dist-folder path: test/dist @@ -222,7 +224,7 @@ jobs: fi - name: Download core - uses: actions/checkout@v2 + uses: actions/checkout@v7 with: repository: ${{ env.STACKQL_CORE_REPOSITORY }} ref: ${{ env.STACKQL_CORE_REF }} @@ -407,7 +409,7 @@ jobs: - name: upload mocked test results if: always() - uses: actions/upload-artifact@v4.3.1 + uses: actions/upload-artifact@v7 with: name: mocked-robot-test-results path: test/robot/reports/mocked @@ -438,13 +440,14 @@ jobs: runs-on: macos-latest steps: - name: Set up Go 1.x - uses: actions/setup-go@v2 + uses: actions/setup-go@v6 with: go-version: ^${{ env.GOLANG_VERSION }} + cache: false id: go - name: Check out code into the Go module directory - uses: actions/checkout@v2 + uses: actions/checkout@v7 - name: Get dependencies run: | @@ -484,13 +487,14 @@ jobs: runs-on: macos-latest steps: - name: Set up Go 1.x - uses: actions/setup-go@v2 + uses: actions/setup-go@v6 with: go-version: ^${{ env.GOLANG_VERSION }} + cache: false id: go - name: Check out code into the Go module directory - uses: actions/checkout@v2 + uses: actions/checkout@v7 - name: Get dependencies run: | diff --git a/.github/workflows/mock-experiment.yml b/.github/workflows/mock-experiment.yml index 26b5704..0677fe4 100644 --- a/.github/workflows/mock-experiment.yml +++ b/.github/workflows/mock-experiment.yml @@ -33,18 +33,19 @@ jobs: steps: - name: Set up golang - uses: actions/setup-go@v5 + uses: actions/setup-go@v6 with: go-version: ^${{ env.GOLANG_VERSION }} + cache: false id: go - name: Setup Python - uses: actions/setup-python@v5 + uses: actions/setup-python@v6 with: python-version: ${{ env.PYTHON_VERSION }} - name: Download core - uses: actions/checkout@v4 + uses: actions/checkout@v7 with: repository: ${{ env.STACKQL_CORE_REPOSITORY }} ref: ${{ env.STACKQL_CORE_REF }} @@ -58,7 +59,7 @@ jobs: python3 cicd/python/build.py --build - name: Upload stackql binary - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 with: name: stackql_binary path: stackql-core/build/stackql @@ -72,10 +73,10 @@ jobs: steps: - name: Check out code - uses: actions/checkout@v4 + uses: actions/checkout@v7 - name: Setup Python - uses: actions/setup-python@v5 + uses: actions/setup-python@v6 with: python-version: ${{ env.PYTHON_VERSION }} @@ -83,7 +84,7 @@ jobs: run: pip install flask - name: Download stackql binary - uses: actions/download-artifact@v4 + uses: actions/download-artifact@v8 with: name: stackql_binary path: build @@ -413,7 +414,7 @@ jobs: PYEOF - name: Upload Mock Test Results - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 if: always() with: name: mock-test-results diff --git a/.github/workflows/provider-analysis.yml b/.github/workflows/provider-analysis.yml index 77a0a2f..1cf592b 100644 --- a/.github/workflows/provider-analysis.yml +++ b/.github/workflows/provider-analysis.yml @@ -30,13 +30,14 @@ jobs: steps: - name: Set up golang - uses: actions/setup-go@v2 + uses: actions/setup-go@v6 with: go-version: ^${{ env.GOLANG_VERSION }} + cache: false id: go - name: Check out code into the Go module directory - uses: actions/checkout@v2 + uses: actions/checkout@v7 - name: Get dependencies run: | @@ -70,7 +71,7 @@ jobs: run: go test -timeout 240s -v ./... - name: Upload any-sdk binary - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 with: name: anysdk_binary path: build/anysdk @@ -83,13 +84,14 @@ jobs: steps: - name: Set up golang - uses: actions/setup-go@v2 + uses: actions/setup-go@v6 with: go-version: ^${{ env.GOLANG_VERSION }} + cache: false id: go - name: Download core - uses: actions/checkout@v4.1.1 + uses: actions/checkout@v7 with: repository: ${{ env.STACKQL_CORE_REPOSITORY }} ref: ${{ env.STACKQL_CORE_REF }} @@ -103,7 +105,7 @@ jobs: python3 cicd/python/build.py --build - name: Upload stackql binary - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 with: name: stackql_binary path: stackql-core/build/stackql @@ -117,16 +119,16 @@ jobs: steps: - name: Check out code into the Go module directory - uses: actions/checkout@v2 + uses: actions/checkout@v7 - name: Download stackql binary - uses: actions/download-artifact@v4 + uses: actions/download-artifact@v8 with: name: stackql_binary path: build - name: Download any-sdk binary - uses: actions/download-artifact@v4 + uses: actions/download-artifact@v8 with: name: anysdk_binary path: build @@ -138,7 +140,7 @@ jobs: chmod +x "${{ github.workspace }}/build/anysdk" - name: Download core - uses: actions/checkout@v2 + uses: actions/checkout@v7 with: repository: ${{ env.STACKQL_CORE_REPOSITORY }} ref: ${{ env.STACKQL_CORE_REF }} @@ -205,35 +207,35 @@ jobs: done - name: Upload Provider Analysis results - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 if: always() with: name: provider-analysis-results path: cicd/out/aot/ - name: Upload Auto Mocks - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 if: always() with: name: auto-mocks path: cicd/out/auto-mocks/ - name: Upload Mock Expectations - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 if: always() with: name: mock-expectations path: cicd/out/mock-expectations/ - name: Upload Mock Queries - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 if: always() with: name: mock-queries path: cicd/out/mock-queries/ - name: Upload Closures - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 if: always() with: name: closures diff --git a/internal/anysdk/pagination.go b/internal/anysdk/pagination.go index 0ce05ff..0e763b0 100644 --- a/internal/anysdk/pagination.go +++ b/internal/anysdk/pagination.go @@ -20,6 +20,13 @@ const ( // pagination strategy: termination is by comparing the current page number // in the response against a page-count field (`responseTerminator`). PaginationAlgorithmPageNumber = "page_number" + // PaginationAlgorithmODataNextLink identifies the OData v4 follow-the-link + // strategy: the `@odata.nextLink` value in the response body is used verbatim + // as the next request URL, and traversal terminates when it is absent/empty. + // Like page_number this is a registered identifier; the caller drives the loop + // using the public Pagination / TokenSemantic accessors (responseToken keyed at + // `@odata.nextLink`). + PaginationAlgorithmODataNextLink = "odata_next_link" ) type Pagination interface { diff --git a/internal/anysdk/pagination_test.go b/internal/anysdk/pagination_test.go index 632e2b6..b85c138 100644 --- a/internal/anysdk/pagination_test.go +++ b/internal/anysdk/pagination_test.go @@ -81,3 +81,9 @@ responseToken: func TestPaginationAlgorithmConstant(t *testing.T) { assert.Equal(t, PaginationAlgorithmPageNumber, "page_number") } + +// TestPaginationODataNextLinkConstant pins the odata_next_link identifier so +// provider YAML and the downstream invoker switch match on this exact string. +func TestPaginationODataNextLinkConstant(t *testing.T) { + assert.Equal(t, PaginationAlgorithmODataNextLink, "odata_next_link") +} diff --git a/internal/anysdk/query_param_pushdown.go b/internal/anysdk/query_param_pushdown.go index 1e5b494..e8f7fc3 100644 --- a/internal/anysdk/query_param_pushdown.go +++ b/internal/anysdk/query_param_pushdown.go @@ -17,6 +17,7 @@ const ( DefaultOrderByParamName = "$orderby" DefaultOrderBySyntax = "odata" DefaultTopParamName = "$top" + DefaultSkipParamName = "$skip" DefaultCountParamName = "$count" DefaultCountParamValue = "true" DefaultCountResponseKey = "@odata.count" @@ -29,6 +30,7 @@ var ( _ FilterPushdown = &standardFilterPushdown{} _ OrderByPushdown = &standardOrderByPushdown{} _ TopPushdown = &standardTopPushdown{} + _ SkipPushdown = &standardSkipPushdown{} _ CountPushdown = &standardCountPushdown{} ) @@ -39,6 +41,7 @@ type QueryParamPushdown interface { GetFilter() (FilterPushdown, bool) GetOrderBy() (OrderByPushdown, bool) GetTop() (TopPushdown, bool) + GetSkip() (SkipPushdown, bool) GetCount() (CountPushdown, bool) } @@ -78,6 +81,13 @@ type TopPushdown interface { GetMaxValue() int } +// SkipPushdown represents configuration for OFFSET clause pushdown +type SkipPushdown interface { + GetDialect() string + GetParamName() string + GetMaxValue() int +} + // CountPushdown represents configuration for SELECT COUNT(*) pushdown type CountPushdown interface { GetDialect() string @@ -92,6 +102,7 @@ type standardQueryParamPushdown struct { Filter *standardFilterPushdown `json:"filter,omitempty" yaml:"filter,omitempty"` OrderBy *standardOrderByPushdown `json:"orderBy,omitempty" yaml:"orderBy,omitempty"` Top *standardTopPushdown `json:"top,omitempty" yaml:"top,omitempty"` + Skip *standardSkipPushdown `json:"skip,omitempty" yaml:"skip,omitempty"` Count *standardCountPushdown `json:"count,omitempty" yaml:"count,omitempty"` } @@ -105,6 +116,8 @@ func (qpp standardQueryParamPushdown) JSONLookup(token string) (interface{}, err return qpp.OrderBy, nil case "top": return qpp.Top, nil + case "skip": + return qpp.Skip, nil case "count": return qpp.Count, nil default: @@ -140,6 +153,13 @@ func (qpp *standardQueryParamPushdown) GetTop() (TopPushdown, bool) { return qpp.Top, true } +func (qpp *standardQueryParamPushdown) GetSkip() (SkipPushdown, bool) { + if qpp.Skip == nil { + return nil, false + } + return qpp.Skip, true +} + func (qpp *standardQueryParamPushdown) GetCount() (CountPushdown, bool) { if qpp.Count == nil { return nil, false @@ -295,6 +315,31 @@ func (tp *standardTopPushdown) GetMaxValue() int { return tp.MaxValue } +// standardSkipPushdown implements SkipPushdown +type standardSkipPushdown struct { + Dialect string `json:"dialect,omitempty" yaml:"dialect,omitempty"` + ParamName string `json:"paramName,omitempty" yaml:"paramName,omitempty"` + MaxValue int `json:"maxValue,omitempty" yaml:"maxValue,omitempty"` +} + +func (sp *standardSkipPushdown) GetDialect() string { + if sp.Dialect == "" { + return CustomDialect + } + return sp.Dialect +} + +func (sp *standardSkipPushdown) GetParamName() string { + if sp.ParamName == "" && sp.GetDialect() == ODataDialect { + return DefaultSkipParamName + } + return sp.ParamName +} + +func (sp *standardSkipPushdown) GetMaxValue() int { + return sp.MaxValue +} + // standardCountPushdown implements CountPushdown type standardCountPushdown struct { Dialect string `json:"dialect,omitempty" yaml:"dialect,omitempty"` diff --git a/internal/anysdk/query_param_pushdown_test.go b/internal/anysdk/query_param_pushdown_test.go index 61ce57e..ed34051 100644 --- a/internal/anysdk/query_param_pushdown_test.go +++ b/internal/anysdk/query_param_pushdown_test.go @@ -293,3 +293,64 @@ filter: t.Logf("TestWildcardSupportedColumns passed") } + +func TestSkipPushdownODataDefaults(t *testing.T) { + qpp := GetTestingQueryParamPushdown() + yamlInput := ` +skip: + dialect: odata + maxValue: 5000 +` + if err := yaml.Unmarshal([]byte(yamlInput), &qpp); err != nil { + t.Fatalf("TestSkipPushdownODataDefaults failed at unmarshal step, err = '%s'", err.Error()) + } + + skipPD, ok := qpp.GetSkip() + if !ok { + t.Fatalf("TestSkipPushdownODataDefaults failed: expected skip pushdown to exist") + } + assert.Equal(t, skipPD.GetDialect(), "odata") + assert.Equal(t, skipPD.GetParamName(), "$skip") // OData default + assert.Equal(t, skipPD.GetMaxValue(), 5000) + + t.Logf("TestSkipPushdownODataDefaults passed") +} + +func TestSkipPushdownCustom(t *testing.T) { + qpp := GetTestingQueryParamPushdown() + yamlInput := ` +skip: + paramName: "offset" + maxValue: 100 +` + if err := yaml.Unmarshal([]byte(yamlInput), &qpp); err != nil { + t.Fatalf("TestSkipPushdownCustom failed at unmarshal step, err = '%s'", err.Error()) + } + + skipPD, ok := qpp.GetSkip() + if !ok { + t.Fatalf("TestSkipPushdownCustom failed: expected skip pushdown to exist") + } + assert.Equal(t, skipPD.GetDialect(), "custom") + assert.Equal(t, skipPD.GetParamName(), "offset") + assert.Equal(t, skipPD.GetMaxValue(), 100) + + t.Logf("TestSkipPushdownCustom passed") +} + +func TestSkipPushdownAbsent(t *testing.T) { + qpp := GetTestingQueryParamPushdown() + yamlInput := ` +top: + dialect: odata +` + if err := yaml.Unmarshal([]byte(yamlInput), &qpp); err != nil { + t.Fatalf("TestSkipPushdownAbsent failed at unmarshal step, err = '%s'", err.Error()) + } + + if _, ok := qpp.GetSkip(); ok { + t.Fatalf("TestSkipPushdownAbsent failed: expected skip pushdown to NOT exist") + } + + t.Logf("TestSkipPushdownAbsent passed") +} diff --git a/public/formulation/interfaces.go b/public/formulation/interfaces.go index 0a96398..81f73d4 100644 --- a/public/formulation/interfaces.go +++ b/public/formulation/interfaces.go @@ -203,6 +203,7 @@ type OperationStore interface { GetPaginationRequestTokenSemantic() (TokenSemantic, bool) GetPaginationResponseTokenSemantic() (TokenSemantic, bool) GetPaginationResponseTerminatorTokenSemantic() (TokenSemantic, bool) + GetQueryParamPushdown() (QueryParamPushdown, bool) GetParameter(paramKey string) (Addressable, bool) GetRequestBodySchema() (Schema, error) GetRequiredNonBodyParameters() map[string]Addressable @@ -227,6 +228,69 @@ type OperationStore interface { unwrap() anysdk.OperationStore } +// QueryParamPushdown mirrors anysdk.QueryParamPushdown: the top-level +// configuration for query parameter pushdown (the public JSONLookup is +// intentionally omitted). +type QueryParamPushdown interface { + GetSelect() (SelectPushdown, bool) + GetFilter() (FilterPushdown, bool) + GetOrderBy() (OrderByPushdown, bool) + GetTop() (TopPushdown, bool) + GetSkip() (SkipPushdown, bool) + GetCount() (CountPushdown, bool) +} + +// SelectPushdown mirrors anysdk.SelectPushdown (SELECT column projection pushdown). +type SelectPushdown interface { + GetDialect() string + GetParamName() string + GetDelimiter() string + GetSupportedColumns() []string + IsColumnSupported(column string) bool +} + +// FilterPushdown mirrors anysdk.FilterPushdown (WHERE clause filter pushdown). +type FilterPushdown interface { + GetDialect() string + GetParamName() string + GetSyntax() string + GetSupportedOperators() []string + GetSupportedColumns() []string + IsOperatorSupported(operator string) bool + IsColumnSupported(column string) bool +} + +// OrderByPushdown mirrors anysdk.OrderByPushdown (ORDER BY clause pushdown). +type OrderByPushdown interface { + GetDialect() string + GetParamName() string + GetSyntax() string + GetSupportedColumns() []string + IsColumnSupported(column string) bool +} + +// TopPushdown mirrors anysdk.TopPushdown (LIMIT clause pushdown). +type TopPushdown interface { + GetDialect() string + GetParamName() string + GetMaxValue() int +} + +// SkipPushdown mirrors anysdk.SkipPushdown (OFFSET clause pushdown). +type SkipPushdown interface { + GetDialect() string + GetParamName() string + GetMaxValue() int +} + +// CountPushdown mirrors anysdk.CountPushdown (SELECT COUNT(*) pushdown). +type CountPushdown interface { + GetDialect() string + GetParamName() string + GetParamValue() string + GetResponseKey() string +} + // ProcessedOperationResponse mirrors methods on ProcessedOperationResponse type ProcessedOperationResponse interface { GetResponse() (Response, bool) @@ -415,6 +479,12 @@ type TokenSemantic interface { GetKey() string GetLocation() string GetTransformer() (TokenTransformer, error) + // GetAlgorithm returns the per-token algorithm (e.g. the request-token + // increment style for offset/page_number pagination); empty when unset. + GetAlgorithm() string + // GetArgs returns the token's free-form args (e.g. an offset/page start value + // or a transform regex) as a plain map, decoupled from any internal type. + GetArgs() map[string]interface{} } // Transform mirrors methods on Transform diff --git a/public/formulation/pagination_drivability_test.go b/public/formulation/pagination_drivability_test.go new file mode 100644 index 0000000..4c68021 --- /dev/null +++ b/public/formulation/pagination_drivability_test.go @@ -0,0 +1,120 @@ +package formulation + +import ( + "fmt" + "testing" + + "github.com/stackql/any-sdk/internal/anysdk" + "gopkg.in/yaml.v3" +) + +// These tests confirm the public surface exposes everything stackql needs to +// drive the pagination loop itself: the page_number algorithm (request increment +// key + start value + total_pages terminator), the OData @odata.nextLink +// response token, and the $skip offset request token. + +func wrapToken(ts anysdk.TokenSemantic) TokenSemantic { + if ts == nil { + return nil + } + return &wrappedTokenSemantic{inner: ts} +} + +func TestPagination_PageNumberDrivable(t *testing.T) { + const pageNumberYaml = ` +algorithm: page_number +requestToken: + key: page + location: query + algorithm: page_number + args: + initialValue: 1 +responseToken: + key: result_info.page + location: body +responseTerminator: + key: result_info.total_pages + location: body +` + pg := anysdk.GetTestingPagination() + if err := yaml.Unmarshal([]byte(pageNumberYaml), &pg); err != nil { + t.Fatalf("failed to unmarshal pagination config: %v", err) + } + + if pg.GetAlgorithm() != "page_number" { + t.Fatalf("algorithm = %q, want page_number", pg.GetAlgorithm()) + } + + // Request increment: which query param holds the page number + start value. + req := wrapToken(pg.GetRequestToken()) + if req.GetKey() != "page" { + t.Fatalf("request key = %q, want page", req.GetKey()) + } + if req.GetLocation() != "query" { + t.Fatalf("request location = %q, want query", req.GetLocation()) + } + if req.GetAlgorithm() != "page_number" { + t.Fatalf("request algorithm = %q, want page_number", req.GetAlgorithm()) + } + if got := fmt.Sprintf("%v", req.GetArgs()["initialValue"]); got != "1" { + t.Fatalf("request args[initialValue] = %v, want 1", got) + } + + // Terminator: stop when result_info.page >= result_info.total_pages. + term := wrapToken(pg.GetResponseTerminator()) + if term.GetKey() != "result_info.total_pages" { + t.Fatalf("terminator key = %q, want result_info.total_pages", term.GetKey()) + } +} + +func TestPagination_ODataNextLinkDrivable(t *testing.T) { + const nextLinkYaml = ` +algorithm: odata_next_link +responseToken: + key: "@odata.nextLink" + location: body +` + pg := anysdk.GetTestingPagination() + if err := yaml.Unmarshal([]byte(nextLinkYaml), &pg); err != nil { + t.Fatalf("failed to unmarshal pagination config: %v", err) + } + + if pg.GetAlgorithm() != anysdk.PaginationAlgorithmODataNextLink { + t.Fatalf("algorithm = %q, want %q", pg.GetAlgorithm(), anysdk.PaginationAlgorithmODataNextLink) + } + + resp := wrapToken(pg.GetResponseToken()) + if resp.GetKey() != "@odata.nextLink" { + t.Fatalf("response token key = %q, want @odata.nextLink", resp.GetKey()) + } + if resp.GetLocation() != "body" { + t.Fatalf("response token location = %q, want body", resp.GetLocation()) + } +} + +func TestPagination_SkipOffsetDrivable(t *testing.T) { + const skipYaml = ` +algorithm: offset +requestToken: + key: $skip + location: query + algorithm: offset + args: + initialValue: 0 +` + pg := anysdk.GetTestingPagination() + if err := yaml.Unmarshal([]byte(skipYaml), &pg); err != nil { + t.Fatalf("failed to unmarshal pagination config: %v", err) + } + + req := wrapToken(pg.GetRequestToken()) + if req.GetKey() != "$skip" { + t.Fatalf("request key = %q, want $skip", req.GetKey()) + } + if req.GetAlgorithm() != "offset" { + t.Fatalf("request algorithm = %q, want offset", req.GetAlgorithm()) + } + if got := fmt.Sprintf("%v", req.GetArgs()["initialValue"]); got != "0" { + t.Fatalf("request args[initialValue] = %v, want 0", got) + } +} diff --git a/public/formulation/query_param_pushdown_apply.go b/public/formulation/query_param_pushdown_apply.go new file mode 100644 index 0000000..3846588 --- /dev/null +++ b/public/formulation/query_param_pushdown_apply.go @@ -0,0 +1,339 @@ +package formulation + +import ( + "fmt" + "strconv" + "strings" +) + +// dialectODataSyntax is the dialect/syntax token used by OData pushdown configs. +// It mirrors anysdk.ODataDialect / anysdk.DefaultFilterSyntax without re-exporting +// the internal constant. +const dialectODataSyntax = "odata" + +// PushdownPredicate is a single neutral, dialect-agnostic WHERE predicate. +// Operator accepts either SQL-style symbols ("=", "!=", ">", ">=", "<", "<=") +// or OData logical names ("eq", "ne", "gt", "ge", "lt", "le", "startswith", +// "endswith", "contains"). Value is the raw comparison value. +type PushdownPredicate struct { + Column string + Operator string + Value interface{} +} + +// PushdownOrder is a single neutral ORDER BY term. +type PushdownOrder struct { + Column string + Descending bool +} + +// PushdownIntent is a neutral, dialect-agnostic description of the query options +// stackql would like to push down to the upstream API. It carries no foreign +// (OData/custom) syntax; ApplyPushdown performs the dialect translation. +type PushdownIntent struct { + // Projection holds the SELECT columns. + Projection []string + // Predicates holds the WHERE predicates. + Predicates []PushdownPredicate + // OrderBy holds the ORDER BY terms. + OrderBy []PushdownOrder + // Limit is the LIMIT value, honoured only when LimitSet is true. + Limit int + // LimitSet reports whether Limit is meaningful. + LimitSet bool + // Offset is the OFFSET value, honoured only when OffsetSet is true. + Offset int + // OffsetSet reports whether Offset is meaningful. + OffsetSet bool + // Count requests a SELECT COUNT(*) pushdown. + Count bool +} + +// PushdownResult is the outcome of translating a PushdownIntent against an +// OperationStore's pushdown config. It is returned as an interface so no mutable +// data-carrier struct leaks across the public boundary. +type PushdownResult interface { + // QueryParams are the request query params to set, e.g. + // {"$filter": "startswith(displayName,'A')", "$top": "10"}. + QueryParams() map[string]string + // PushedPredicates were fully translated to QueryParams; the caller may skip + // client-side filtering for these. + PushedPredicates() []PushdownPredicate + // ResidualPredicates were NOT pushed; the caller must still filter these + // client-side (the partial-pushdown contract). + ResidualPredicates() []PushdownPredicate + // CountResponseKey is the response key carrying the count (e.g. "@odata.count") + // when a COUNT(*) was pushed; empty otherwise. + CountResponseKey() string +} + +// PushdownConfigSource is the minimal surface ApplyPushdown needs to resolve the +// pushdown config (with the Method -> Resource -> Service -> ProviderService -> +// Provider inheritance already implemented internally). OperationStore satisfies +// it, so callers normally pass an OperationStore directly. +type PushdownConfigSource interface { + GetQueryParamPushdown() (QueryParamPushdown, bool) +} + +type standardPushdownResult struct { + queryParams map[string]string + pushedPredicates []PushdownPredicate + residualPredicates []PushdownPredicate + countResponseKey string +} + +func (r *standardPushdownResult) QueryParams() map[string]string { return r.queryParams } + +func (r *standardPushdownResult) PushedPredicates() []PushdownPredicate { return r.pushedPredicates } + +func (r *standardPushdownResult) ResidualPredicates() []PushdownPredicate { + return r.residualPredicates +} + +func (r *standardPushdownResult) CountResponseKey() string { return r.countResponseKey } + +// ApplyPushdown translates a neutral PushdownIntent into the request query params +// supported by the supplied config source. Anything not reported supported by the +// config (unknown column, unsupported operator, missing sub-config, or a dialect +// this helper cannot render) is left for the caller: predicates land in +// ResidualPredicates, and projection/order-by/limit/count are simply not emitted. +// With no pushdown config at all the result is empty and every predicate is +// residual, preserving current behaviour. +func ApplyPushdown(src PushdownConfigSource, intent PushdownIntent) PushdownResult { + res := &standardPushdownResult{queryParams: map[string]string{}} + + var qpp QueryParamPushdown + if src != nil { + qpp, _ = src.GetQueryParamPushdown() + } + if qpp == nil { + // Absent config: no params, all predicates residual. + res.residualPredicates = append(res.residualPredicates, intent.Predicates...) + return res + } + + applySelect(qpp, intent, res) + applyFilter(qpp, intent, res) + applyOrderBy(qpp, intent, res) + applyTop(qpp, intent, res) + applySkip(qpp, intent, res) + applyCount(qpp, intent, res) + + return res +} + +func applySelect(qpp QueryParamPushdown, intent PushdownIntent, res *standardPushdownResult) { + if len(intent.Projection) == 0 { + return + } + sel, ok := qpp.GetSelect() + if !ok { + return + } + // All-or-nothing: pushing a partial projection would silently drop columns + // the caller still needs, so emit $select only when every column is supported. + for _, col := range intent.Projection { + if !sel.IsColumnSupported(col) { + return + } + } + paramName := sel.GetParamName() + if paramName == "" { + return + } + res.queryParams[paramName] = strings.Join(intent.Projection, sel.GetDelimiter()) +} + +func applyFilter(qpp QueryParamPushdown, intent PushdownIntent, res *standardPushdownResult) { + if len(intent.Predicates) == 0 { + return + } + fil, ok := qpp.GetFilter() + if !ok { + // No filter config: everything residual. + res.residualPredicates = append(res.residualPredicates, intent.Predicates...) + return + } + + odataSyntax := strings.EqualFold(fil.GetSyntax(), dialectODataSyntax) + paramName := fil.GetParamName() + + var pushable []PushdownPredicate + for _, p := range intent.Predicates { + odataOp := normalizeFilterOperator(p.Operator) + // Only OData-syntax filters can be rendered here; a column/operator must be + // supported, and the operator must be one we know how to emit. + if odataSyntax && paramName != "" && odataOp != "" && + fil.IsColumnSupported(p.Column) && fil.IsOperatorSupported(odataOp) { + pushable = append(pushable, p) + } else { + res.residualPredicates = append(res.residualPredicates, p) + } + } + + if len(pushable) == 0 { + return + } + // Combining predicates needs the OData "and" operator. If the config does not + // allow it, only the first predicate is pushed and the rest stay residual. + if len(pushable) > 1 && !fil.IsOperatorSupported("and") { + res.residualPredicates = append(res.residualPredicates, pushable[1:]...) + pushable = pushable[:1] + } + + parts := make([]string, 0, len(pushable)) + for _, p := range pushable { + parts = append(parts, buildODataFilterTerm(p)) + } + res.queryParams[paramName] = strings.Join(parts, " and ") + res.pushedPredicates = append(res.pushedPredicates, pushable...) +} + +func applyOrderBy(qpp QueryParamPushdown, intent PushdownIntent, res *standardPushdownResult) { + if len(intent.OrderBy) == 0 { + return + } + ob, ok := qpp.GetOrderBy() + if !ok { + return + } + // Only the OData syntax has a well-defined "col asc|desc" rendering here. + if !strings.EqualFold(ob.GetSyntax(), dialectODataSyntax) { + return + } + paramName := ob.GetParamName() + if paramName == "" { + return + } + // All-or-nothing: a partial ordering would mis-order results. + for _, o := range intent.OrderBy { + if !ob.IsColumnSupported(o.Column) { + return + } + } + parts := make([]string, 0, len(intent.OrderBy)) + for _, o := range intent.OrderBy { + dir := "asc" + if o.Descending { + dir = "desc" + } + parts = append(parts, o.Column+" "+dir) + } + res.queryParams[paramName] = strings.Join(parts, ",") +} + +func applyTop(qpp QueryParamPushdown, intent PushdownIntent, res *standardPushdownResult) { + if !intent.LimitSet { + return + } + tp, ok := qpp.GetTop() + if !ok { + return + } + paramName := tp.GetParamName() + if paramName == "" { + return + } + v := intent.Limit + if v < 0 { + return + } + if maxV := tp.GetMaxValue(); maxV > 0 && v > maxV { + v = maxV + } + res.queryParams[paramName] = strconv.Itoa(v) +} + +func applySkip(qpp QueryParamPushdown, intent PushdownIntent, res *standardPushdownResult) { + if !intent.OffsetSet { + return + } + sp, ok := qpp.GetSkip() + if !ok { + return + } + paramName := sp.GetParamName() + if paramName == "" { + return + } + v := intent.Offset + if v < 0 { + return + } + if maxV := sp.GetMaxValue(); maxV > 0 && v > maxV { + v = maxV + } + res.queryParams[paramName] = strconv.Itoa(v) +} + +func applyCount(qpp QueryParamPushdown, intent PushdownIntent, res *standardPushdownResult) { + if !intent.Count { + return + } + cp, ok := qpp.GetCount() + if !ok { + return + } + paramName := cp.GetParamName() + if paramName == "" { + return + } + res.queryParams[paramName] = cp.GetParamValue() + res.countResponseKey = cp.GetResponseKey() +} + +// buildODataFilterTerm renders one supported predicate as an OData $filter term. +func buildODataFilterTerm(p PushdownPredicate) string { + op := normalizeFilterOperator(p.Operator) + switch op { + case "startswith", "endswith", "contains": + return fmt.Sprintf("%s(%s,%s)", op, p.Column, formatODataValue(p.Value)) + default: // eq, ne, gt, ge, lt, le + return fmt.Sprintf("%s %s %s", p.Column, op, formatODataValue(p.Value)) + } +} + +// normalizeFilterOperator maps a neutral operator (SQL symbol or OData name) to +// its canonical OData logical name, or "" if it is not translatable. +func normalizeFilterOperator(op string) string { + switch strings.ToLower(strings.TrimSpace(op)) { + case "eq", "=", "==": + return "eq" + case "ne", "!=", "<>": + return "ne" + case "gt", ">": + return "gt" + case "ge", ">=": + return "ge" + case "lt", "<": + return "lt" + case "le", "<=": + return "le" + case "startswith": + return "startswith" + case "endswith": + return "endswith" + case "contains": + return "contains" + default: + return "" + } +} + +// formatODataValue renders a comparison value using OData literal conventions: +// strings are single-quoted (embedded quotes doubled), bools/numbers are bare. +func formatODataValue(v interface{}) string { + switch t := v.(type) { + case nil: + return "null" + case string: + return "'" + strings.ReplaceAll(t, "'", "''") + "'" + case bool: + if t { + return "true" + } + return "false" + default: + return fmt.Sprintf("%v", t) + } +} diff --git a/public/formulation/query_param_pushdown_apply_test.go b/public/formulation/query_param_pushdown_apply_test.go new file mode 100644 index 0000000..bbda259 --- /dev/null +++ b/public/formulation/query_param_pushdown_apply_test.go @@ -0,0 +1,260 @@ +package formulation + +import ( + "testing" + + "github.com/stackql/any-sdk/internal/anysdk" + "gopkg.in/yaml.v3" +) + +// fakeConfigSource is a minimal PushdownConfigSource for exercising ApplyPushdown +// without standing up a full OperationStore. +type fakeConfigSource struct { + qpp QueryParamPushdown +} + +func (f fakeConfigSource) GetQueryParamPushdown() (QueryParamPushdown, bool) { + if f.qpp == nil { + return nil, false + } + return f.qpp, true +} + +func buildPushdown(t *testing.T, yamlStr string) QueryParamPushdown { + t.Helper() + qpp := anysdk.GetTestingQueryParamPushdown() + if err := yaml.Unmarshal([]byte(yamlStr), &qpp); err != nil { + t.Fatalf("failed to unmarshal pushdown config: %v", err) + } + return &wrappedQueryParamPushdown{inner: &qpp} +} + +func assertParam(t *testing.T, params map[string]string, key, want string) { + t.Helper() + got, ok := params[key] + if !ok { + t.Fatalf("expected query param %q to be set, params=%v", key, params) + } + if got != want { + t.Fatalf("query param %q = %q, want %q", key, got, want) + } +} + +const odataFullPushdownYaml = ` +select: + dialect: odata + supportedColumns: + - "id" + - "displayName" +filter: + dialect: odata + supportedOperators: + - "eq" + - "ne" + - "gt" + - "startswith" + - "and" + supportedColumns: + - "displayName" + - "status" + - "createdYear" +orderBy: + dialect: odata + supportedColumns: + - "displayName" + - "createdYear" +top: + dialect: odata + maxValue: 1000 +skip: + dialect: odata + maxValue: 2000 +count: + dialect: odata +` + +func TestApplyPushdown_FullOData(t *testing.T) { + src := fakeConfigSource{qpp: buildPushdown(t, odataFullPushdownYaml)} + intent := PushdownIntent{ + Projection: []string{"id", "displayName"}, + Predicates: []PushdownPredicate{ + {Column: "displayName", Operator: "startswith", Value: "A"}, + {Column: "status", Operator: "=", Value: "active"}, + {Column: "createdYear", Operator: ">", Value: 2020}, + }, + OrderBy: []PushdownOrder{{Column: "displayName", Descending: true}}, + Limit: 10, + LimitSet: true, + Offset: 20, + OffsetSet: true, + Count: true, + } + + res := ApplyPushdown(src, intent) + + assertParam(t, res.QueryParams(), "$select", "id,displayName") + assertParam(t, res.QueryParams(), "$filter", + "startswith(displayName,'A') and status eq 'active' and createdYear gt 2020") + assertParam(t, res.QueryParams(), "$orderby", "displayName desc") + assertParam(t, res.QueryParams(), "$top", "10") + assertParam(t, res.QueryParams(), "$skip", "20") + assertParam(t, res.QueryParams(), "$count", "true") + + if res.CountResponseKey() != "@odata.count" { + t.Fatalf("CountResponseKey = %q, want %q", res.CountResponseKey(), "@odata.count") + } + if len(res.PushedPredicates()) != 3 { + t.Fatalf("expected 3 pushed predicates, got %d", len(res.PushedPredicates())) + } + if len(res.ResidualPredicates()) != 0 { + t.Fatalf("expected 0 residual predicates, got %d", len(res.ResidualPredicates())) + } +} + +func TestApplyPushdown_PartialResidual(t *testing.T) { + const partialYaml = ` +select: + dialect: odata + supportedColumns: + - "displayName" +filter: + dialect: odata + supportedOperators: + - "eq" + - "startswith" + supportedColumns: + - "displayName" +` + src := fakeConfigSource{qpp: buildPushdown(t, partialYaml)} + intent := PushdownIntent{ + // "secret" is not a supported select column -> whole $select suppressed. + Projection: []string{"displayName", "secret"}, + Predicates: []PushdownPredicate{ + {Column: "displayName", Operator: "eq", Value: "A"}, // pushable + {Column: "unknownCol", Operator: "eq", Value: "B"}, // unsupported column + {Column: "displayName", Operator: "gt", Value: 5}, // unsupported operator + }, + } + + res := ApplyPushdown(src, intent) + + if _, ok := res.QueryParams()["$select"]; ok { + t.Fatalf("expected $select to be suppressed when a column is unsupported") + } + assertParam(t, res.QueryParams(), "$filter", "displayName eq 'A'") + if len(res.PushedPredicates()) != 1 { + t.Fatalf("expected 1 pushed predicate, got %d", len(res.PushedPredicates())) + } + if len(res.ResidualPredicates()) != 2 { + t.Fatalf("expected 2 residual predicates, got %d", len(res.ResidualPredicates())) + } +} + +func TestApplyPushdown_CustomDialect(t *testing.T) { + const customYaml = ` +select: + paramName: "fields" + delimiter: "|" + supportedColumns: + - "*" +filter: + paramName: "filter" + syntax: "key_value" + supportedOperators: + - "eq" + supportedColumns: + - "status" +top: + paramName: "limit" + maxValue: 100 +count: + paramName: "include_count" + paramValue: "1" + responseKey: "meta.total" +` + src := fakeConfigSource{qpp: buildPushdown(t, customYaml)} + intent := PushdownIntent{ + Projection: []string{"a", "b"}, + Predicates: []PushdownPredicate{{Column: "status", Operator: "eq", Value: "x"}}, + Limit: 250, // above maxValue -> clamped + LimitSet: true, + Count: true, + } + + res := ApplyPushdown(src, intent) + + assertParam(t, res.QueryParams(), "fields", "a|b") + assertParam(t, res.QueryParams(), "limit", "100") + assertParam(t, res.QueryParams(), "include_count", "1") + + // Non-OData filter syntax is not renderable here -> residual, no filter param. + if _, ok := res.QueryParams()["filter"]; ok { + t.Fatalf("expected custom non-odata filter to be left residual") + } + if len(res.ResidualPredicates()) != 1 { + t.Fatalf("expected 1 residual predicate, got %d", len(res.ResidualPredicates())) + } + if res.CountResponseKey() != "meta.total" { + t.Fatalf("CountResponseKey = %q, want %q", res.CountResponseKey(), "meta.total") + } +} + +func TestApplyPushdown_TopClamp(t *testing.T) { + src := fakeConfigSource{qpp: buildPushdown(t, odataFullPushdownYaml)} + res := ApplyPushdown(src, PushdownIntent{Limit: 5000, LimitSet: true}) + assertParam(t, res.QueryParams(), "$top", "1000") +} + +func TestApplyPushdown_SkipClamp(t *testing.T) { + src := fakeConfigSource{qpp: buildPushdown(t, odataFullPushdownYaml)} + res := ApplyPushdown(src, PushdownIntent{Offset: 9000, OffsetSet: true}) + assertParam(t, res.QueryParams(), "$skip", "2000") +} + +func TestApplyPushdown_SkipCustomDialect(t *testing.T) { + const customSkipYaml = ` +skip: + paramName: "offset" + maxValue: 100 +` + src := fakeConfigSource{qpp: buildPushdown(t, customSkipYaml)} + res := ApplyPushdown(src, PushdownIntent{Offset: 30, OffsetSet: true}) + assertParam(t, res.QueryParams(), "offset", "30") +} + +func TestApplyPushdown_SkipAbsentConfig(t *testing.T) { + // Filter-only config: no skip sub-config -> $skip not emitted even when OffsetSet. + const filterOnlyYaml = ` +filter: + dialect: odata + supportedOperators: + - "eq" +` + src := fakeConfigSource{qpp: buildPushdown(t, filterOnlyYaml)} + res := ApplyPushdown(src, PushdownIntent{Offset: 10, OffsetSet: true}) + if _, ok := res.QueryParams()["$skip"]; ok { + t.Fatalf("expected no $skip when skip config is absent") + } +} + +func TestApplyPushdown_AbsentConfigNoOp(t *testing.T) { + intent := PushdownIntent{ + Projection: []string{"a"}, + Predicates: []PushdownPredicate{{Column: "a", Operator: "eq", Value: 1}}, + Limit: 10, + LimitSet: true, + Count: true, + } + + res := ApplyPushdown(fakeConfigSource{qpp: nil}, intent) + + if len(res.QueryParams()) != 0 { + t.Fatalf("expected zero query params with absent config, got %v", res.QueryParams()) + } + if len(res.PushedPredicates()) != 0 { + t.Fatalf("expected zero pushed predicates, got %d", len(res.PushedPredicates())) + } + if len(res.ResidualPredicates()) != 1 { + t.Fatalf("expected all predicates residual, got %d", len(res.ResidualPredicates())) + } +} diff --git a/public/formulation/wrappers.go b/public/formulation/wrappers.go index ff4e60b..1c6a579 100644 --- a/public/formulation/wrappers.go +++ b/public/formulation/wrappers.go @@ -894,6 +894,11 @@ func (w *wrappedOperationStore) GetPaginationResponseTerminatorTokenSemantic() ( return &wrappedTokenSemantic{inner: r0}, r1 } +func (w *wrappedOperationStore) GetQueryParamPushdown() (QueryParamPushdown, bool) { + r0, r1 := w.inner.GetQueryParamPushdown() + return wrapQueryParamPushdown(r0, r1) +} + func (w *wrappedOperationStore) GetParameter(paramKey string) (Addressable, bool) { r0, r1 := w.inner.GetParameter(paramKey) return &wrappedAddressable{inner: r0}, r1 @@ -1566,6 +1571,11 @@ func (w *wrappedStandardOperationStore) GetPaginationResponseTerminatorTokenSema return &wrappedTokenSemantic{inner: r0}, r1 } +func (w *wrappedStandardOperationStore) GetQueryParamPushdown() (QueryParamPushdown, bool) { + r0, r1 := w.inner.GetQueryParamPushdown() + return wrapQueryParamPushdown(r0, r1) +} + func (w *wrappedStandardOperationStore) GetParameter(paramKey string) (Addressable, bool) { r0, r1 := w.inner.GetParameter(paramKey) return &wrappedAddressable{inner: r0}, r1 @@ -1673,6 +1683,159 @@ func (w *wrappedTokenSemantic) GetTransformer() (TokenTransformer, error) { return TokenTransformer(r0), r1 } +func (w *wrappedTokenSemantic) GetAlgorithm() string { + return w.inner.GetAlgorithm() +} + +func (w *wrappedTokenSemantic) GetArgs() map[string]interface{} { + return map[string]interface{}(w.inner.GetArgs()) +} + +func wrapQueryParamPushdown(inner anysdk.QueryParamPushdown, ok bool) (QueryParamPushdown, bool) { + if !ok || inner == nil { + return nil, false + } + return &wrappedQueryParamPushdown{inner: inner}, true +} + +type wrappedQueryParamPushdown struct { + inner anysdk.QueryParamPushdown +} + +func (w *wrappedQueryParamPushdown) GetSelect() (SelectPushdown, bool) { + r0, r1 := w.inner.GetSelect() + if !r1 { + return nil, false + } + return &wrappedSelectPushdown{inner: r0}, true +} + +func (w *wrappedQueryParamPushdown) GetFilter() (FilterPushdown, bool) { + r0, r1 := w.inner.GetFilter() + if !r1 { + return nil, false + } + return &wrappedFilterPushdown{inner: r0}, true +} + +func (w *wrappedQueryParamPushdown) GetOrderBy() (OrderByPushdown, bool) { + r0, r1 := w.inner.GetOrderBy() + if !r1 { + return nil, false + } + return &wrappedOrderByPushdown{inner: r0}, true +} + +func (w *wrappedQueryParamPushdown) GetTop() (TopPushdown, bool) { + r0, r1 := w.inner.GetTop() + if !r1 { + return nil, false + } + return &wrappedTopPushdown{inner: r0}, true +} + +func (w *wrappedQueryParamPushdown) GetSkip() (SkipPushdown, bool) { + r0, r1 := w.inner.GetSkip() + if !r1 { + return nil, false + } + return &wrappedSkipPushdown{inner: r0}, true +} + +func (w *wrappedQueryParamPushdown) GetCount() (CountPushdown, bool) { + r0, r1 := w.inner.GetCount() + if !r1 { + return nil, false + } + return &wrappedCountPushdown{inner: r0}, true +} + +type wrappedSelectPushdown struct { + inner anysdk.SelectPushdown +} + +func (w *wrappedSelectPushdown) GetDialect() string { return w.inner.GetDialect() } + +func (w *wrappedSelectPushdown) GetParamName() string { return w.inner.GetParamName() } + +func (w *wrappedSelectPushdown) GetDelimiter() string { return w.inner.GetDelimiter() } + +func (w *wrappedSelectPushdown) GetSupportedColumns() []string { return w.inner.GetSupportedColumns() } + +func (w *wrappedSelectPushdown) IsColumnSupported(c string) bool { return w.inner.IsColumnSupported(c) } + +type wrappedFilterPushdown struct { + inner anysdk.FilterPushdown +} + +func (w *wrappedFilterPushdown) GetDialect() string { return w.inner.GetDialect() } + +func (w *wrappedFilterPushdown) GetParamName() string { return w.inner.GetParamName() } + +func (w *wrappedFilterPushdown) GetSyntax() string { return w.inner.GetSyntax() } + +func (w *wrappedFilterPushdown) GetSupportedOperators() []string { + return w.inner.GetSupportedOperators() +} + +func (w *wrappedFilterPushdown) GetSupportedColumns() []string { return w.inner.GetSupportedColumns() } + +func (w *wrappedFilterPushdown) IsOperatorSupported(o string) bool { + return w.inner.IsOperatorSupported(o) +} + +func (w *wrappedFilterPushdown) IsColumnSupported(c string) bool { return w.inner.IsColumnSupported(c) } + +type wrappedOrderByPushdown struct { + inner anysdk.OrderByPushdown +} + +func (w *wrappedOrderByPushdown) GetDialect() string { return w.inner.GetDialect() } + +func (w *wrappedOrderByPushdown) GetParamName() string { return w.inner.GetParamName() } + +func (w *wrappedOrderByPushdown) GetSyntax() string { return w.inner.GetSyntax() } + +func (w *wrappedOrderByPushdown) GetSupportedColumns() []string { + return w.inner.GetSupportedColumns() +} + +func (w *wrappedOrderByPushdown) IsColumnSupported(c string) bool { + return w.inner.IsColumnSupported(c) +} + +type wrappedTopPushdown struct { + inner anysdk.TopPushdown +} + +func (w *wrappedTopPushdown) GetDialect() string { return w.inner.GetDialect() } + +func (w *wrappedTopPushdown) GetParamName() string { return w.inner.GetParamName() } + +func (w *wrappedTopPushdown) GetMaxValue() int { return w.inner.GetMaxValue() } + +type wrappedSkipPushdown struct { + inner anysdk.SkipPushdown +} + +func (w *wrappedSkipPushdown) GetDialect() string { return w.inner.GetDialect() } + +func (w *wrappedSkipPushdown) GetParamName() string { return w.inner.GetParamName() } + +func (w *wrappedSkipPushdown) GetMaxValue() int { return w.inner.GetMaxValue() } + +type wrappedCountPushdown struct { + inner anysdk.CountPushdown +} + +func (w *wrappedCountPushdown) GetDialect() string { return w.inner.GetDialect() } + +func (w *wrappedCountPushdown) GetParamName() string { return w.inner.GetParamName() } + +func (w *wrappedCountPushdown) GetParamValue() string { return w.inner.GetParamValue() } + +func (w *wrappedCountPushdown) GetResponseKey() string { return w.inner.GetResponseKey() } + type wrappedTransform struct { inner anysdk.Transform }