diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index 3b8067d..c00cf72 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -51,7 +51,7 @@ jobs: type=sha,format=short type=semver,pattern={{version}} type=semver,pattern={{major}}.{{minor}} - type=raw,value=latest,enable=${{ github.ref_type == 'tag' && !contains(github.ref_name, '-rc.') }} + type=raw,value=latest,enable=${{ github.ref == 'refs/heads/main' || (github.ref_type == 'tag' && !contains(github.ref_name, '-rc.')) }} - name: Log in to Quay.io uses: docker/login-action@v3 @@ -112,7 +112,7 @@ jobs: type=sha,format=short type=semver,pattern={{version}} type=semver,pattern={{major}}.{{minor}} - type=raw,value=latest,enable=${{ github.ref_type == 'tag' && !contains(github.ref_name, '-rc.') }} + type=raw,value=latest,enable=${{ github.ref == 'refs/heads/main' || (github.ref_type == 'tag' && !contains(github.ref_name, '-rc.')) }} - name: Log in to Quay.io uses: docker/login-action@v3 diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml index f2cd9e6..f679cf0 100644 --- a/.github/workflows/tests.yaml +++ b/.github/workflows/tests.yaml @@ -15,7 +15,7 @@ jobs: - name: Set up Go uses: actions/setup-go@v5 with: - go-version-file: go.mod + go-version: 1.26.5 cache: true - name: Generate FHIR code diff --git a/Dockerfile b/Dockerfile index c0c0242..e53fb89 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,6 +1,8 @@ # syntax=docker/dockerfile:1.7 -FROM golang:1.26.3-alpine3.22 AS builder -RUN apk add --no-cache git ca-certificates tzdata +FROM golang:1.26.5-bookworm AS builder +RUN apt-get update \ + && apt-get install -y --no-install-recommends git ca-certificates tzdata \ + && rm -rf /var/lib/apt/lists/* ENV CGO_ENABLED=0 @@ -41,6 +43,4 @@ COPY --from=builder /src/schemas /app/schemas USER arango-fhir EXPOSE 8080 STOPSIGNAL SIGTERM -HEALTHCHECK --interval=10s --timeout=3s --start-period=10s --retries=6 \ - CMD wget -q -O - http://127.0.0.1:8080/healthz >/dev/null || exit 1 ENTRYPOINT ["/app/arango-fhir-server"] diff --git a/Makefile b/Makefile index 4fad3d8..f11bb24 100644 --- a/Makefile +++ b/Makefile @@ -1,13 +1,15 @@ .PHONY: build build-cli build-server clean compiler-bench compiler-arango-bench dataframe-demo dataframe-profile dataframe-boundaries dataframe-test conformance generate-fhir generate-graphql graphql-check gqlgen-check test docker-build docker-run GO ?= go +GO_VERSION ?= 1.26.5 +GO_TOOLCHAIN ?= go$(GO_VERSION) GOCACHE_DIR ?= $(CURDIR)/.gocache GOFLAGS ?= SCHEMA_PATH ?= schemas/graph-fhir.json IMAGE ?= arango-fhir-proto:local BENCH_TIME ?= 10x BENCH_COUNT ?= 5 -GRAPHQL_URL ?= http://127.0.0.1:8080/graphql +GRAPHQL_URL ?= http://127.0.0.1:8080/graphql/graph DATAFRAME_REPEAT ?= 1 DATAFRAME_LIMIT ?= 0 DATAFRAME_TIMEOUT ?= 5m @@ -21,69 +23,71 @@ build: build-cli build-server build-cli: mkdir -p bin $(GOCACHE_DIR) - GOCACHE=$(GOCACHE_DIR) GOTOOLCHAIN=auto $(GO) build $(GOFLAGS) -o bin/arango-fhir-proto ./cmd/arango-fhir-proto + GOCACHE=$(GOCACHE_DIR) GOTOOLCHAIN=$(GO_TOOLCHAIN) $(GO) build $(GOFLAGS) -o bin/arango-fhir-proto ./cmd/arango-fhir-proto build-server: mkdir -p bin $(GOCACHE_DIR) - GOCACHE=$(GOCACHE_DIR) GOTOOLCHAIN=auto $(GO) build $(GOFLAGS) -o bin/arango-fhir-server ./cmd/arango-fhir-server + GOCACHE=$(GOCACHE_DIR) GOTOOLCHAIN=$(GO_TOOLCHAIN) $(GO) build $(GOFLAGS) -o bin/arango-fhir-server ./cmd/arango-fhir-server generate-graphql: mkdir -p $(GOCACHE_DIR) @status=0; fix_status=0; \ - GOFLAGS="$(GOFLAGS) -mod=mod" GOCACHE=$(GOCACHE_DIR) GOTOOLCHAIN=auto $(GO) run -mod=mod github.com/99designs/gqlgen generate || status=$$?; \ - if [ -f graphqlapi/generated.go ]; then \ - GOCACHE=$(GOCACHE_DIR) GOTOOLCHAIN=auto $(GO) run ./cmd/gqlgenfix graphqlapi/generated.go || fix_status=$$?; \ - fi; \ + for config in gqlgen.yml graphqlapi/clickhouse/gqlgen.yml; do \ + GOFLAGS="$(GOFLAGS) -mod=mod" GOCACHE=$(GOCACHE_DIR) GOTOOLCHAIN=$(GO_TOOLCHAIN) $(GO) run -mod=mod github.com/99designs/gqlgen generate --config $$config || status=$$?; \ + done; \ + for generated in graphqlapi/generated.go graphqlapi/clickhouse/generated.go; do \ + GOCACHE=$(GOCACHE_DIR) GOTOOLCHAIN=$(GO_TOOLCHAIN) $(GO) run ./cmd/gqlgenfix $$generated || fix_status=$$?; \ + done; \ test $$fix_status -eq 0; \ - test $$status -eq 0 -o -f graphqlapi/generated.go + test $$status -eq 0 generate-fhir: mkdir -p $(GOCACHE_DIR) - GOCACHE=$(GOCACHE_DIR) GOTOOLCHAIN=auto $(GO) run ./cmd/generate -schema $(SCHEMA_PATH) -structs-out fhirstructs -metadata-out fhirschema/generated.go + GOCACHE=$(GOCACHE_DIR) GOTOOLCHAIN=$(GO_TOOLCHAIN) $(GO) run ./cmd/generate -schema $(SCHEMA_PATH) -structs-out fhirstructs -metadata-out fhirschema/generated.go gofmt -w fhirstructs/model.go fhirstructs/validate.go fhirstructs/extract.go fhirstructs/helpers.go fhirschema/generated.go graphql-check: mkdir -p $(GOCACHE_DIR) - GOCACHE=$(GOCACHE_DIR) GOTOOLCHAIN=auto $(GO) test $(GOFLAGS) ./graphqlapi ./internal/dataframe -count=1 + GOCACHE=$(GOCACHE_DIR) GOTOOLCHAIN=$(GO_TOOLCHAIN) $(GO) test $(GOFLAGS) ./graphqlapi ./internal/dataframe -count=1 gqlgen-check: graphql-check test: mkdir -p $(GOCACHE_DIR) - GOCACHE=$(GOCACHE_DIR) GOTOOLCHAIN=auto $(GO) test $(GOFLAGS) ./... -count=1 + GOCACHE=$(GOCACHE_DIR) GOTOOLCHAIN=$(GO_TOOLCHAIN) $(GO) test $(GOFLAGS) ./... -count=1 compiler-bench: mkdir -p $(GOCACHE_DIR) - GOCACHE=$(GOCACHE_DIR) GOTOOLCHAIN=auto $(GO) test $(GOFLAGS) ./conformance/compiler -run '^$$' -bench '^BenchmarkCompilerOracle$$' -benchmem + GOCACHE=$(GOCACHE_DIR) GOTOOLCHAIN=$(GO_TOOLCHAIN) $(GO) test $(GOFLAGS) ./conformance/compiler -run '^$$' -bench '^BenchmarkCompilerOracle$$' -benchmem # Requires a locally loaded META fixture. Override BENCH_TIME/BENCH_COUNT for # a longer run, for example: make compiler-arango-bench BENCH_TIME=3s BENCH_COUNT=10. compiler-arango-bench: mkdir -p $(GOCACHE_DIR) - LOOM_COMPILER_ARANGO_INTEGRATION=1 GOCACHE=$(GOCACHE_DIR) GOTOOLCHAIN=auto $(GO) test $(GOFLAGS) ./internal/dataframe -run '^$$' -bench '^BenchmarkGenericCompilerAgainstArango$$' -benchmem -benchtime=$(BENCH_TIME) -count=$(BENCH_COUNT) + LOOM_COMPILER_ARANGO_INTEGRATION=1 GOCACHE=$(GOCACHE_DIR) GOTOOLCHAIN=$(GO_TOOLCHAIN) $(GO) test $(GOFLAGS) ./internal/dataframe -run '^$$' -bench '^BenchmarkGenericCompilerAgainstArango$$' -benchmem -benchtime=$(BENCH_TIME) -count=$(BENCH_COUNT) # Requires `arango-fhir-server --no-auth` and a loaded META project. Prints the # actual GraphQL dataframe response and per-request wall-clock timings. dataframe-demo: mkdir -p $(GOCACHE_DIR) - GOCACHE=$(GOCACHE_DIR) GOTOOLCHAIN=auto $(GO) run ./cmd/dataframe-query -url $(GRAPHQL_URL) -query $(DATAFRAME_QUERY) -variables $(DATAFRAME_VARIABLES) -repeat $(DATAFRAME_REPEAT) -limit $(DATAFRAME_LIMIT) -timeout $(DATAFRAME_TIMEOUT) -print-response=$(DATAFRAME_PRINT_RESPONSE) + GOCACHE=$(GOCACHE_DIR) GOTOOLCHAIN=$(GO_TOOLCHAIN) $(GO) run ./cmd/dataframe-query -url $(GRAPHQL_URL) -query $(DATAFRAME_QUERY) -variables $(DATAFRAME_VARIABLES) -repeat $(DATAFRAME_REPEAT) -limit $(DATAFRAME_LIMIT) -timeout $(DATAFRAME_TIMEOUT) -print-response=$(DATAFRAME_PRINT_RESPONSE) # Requires a loaded META fixture database. Compiles the checked-in GDC fixture, # writes exact rendered AQL, then runs Arango EXPLAIN and PROFILE 2. dataframe-profile: mkdir -p $(GOCACHE_DIR) - GOCACHE=$(GOCACHE_DIR) GOTOOLCHAIN=auto $(GO) run ./cmd/dataframe-profile -variables $(DATAFRAME_PROFILE_VARIABLES) -limit $(DATAFRAME_PROFILE_LIMIT) + GOCACHE=$(GOCACHE_DIR) GOTOOLCHAIN=$(GO_TOOLCHAIN) $(GO) run ./cmd/dataframe-profile -variables $(DATAFRAME_PROFILE_VARIABLES) -limit $(DATAFRAME_PROFILE_LIMIT) dataframe-boundaries: ./scripts/check_dataframe_package_boundaries.sh dataframe-test: dataframe-boundaries mkdir -p $(GOCACHE_DIR) - GOCACHE=$(GOCACHE_DIR) GOTOOLCHAIN=auto $(GO) test $(GOFLAGS) ./internal/dataframe/spec ./internal/dataframe/semantic ./internal/dataframe/compiler/ir ./internal/dataframe/compiler/lower ./internal/dataframe/compiler/optimize ./internal/dataframe/compiler/render/aql ./internal/dataframe/compiler ./internal/dataframe/runtime ./internal/dataframe -count=1 + GOCACHE=$(GOCACHE_DIR) GOTOOLCHAIN=$(GO_TOOLCHAIN) $(GO) test $(GOFLAGS) ./internal/dataframe/spec ./internal/dataframe/semantic ./internal/dataframe/compiler/ir ./internal/dataframe/compiler/lower ./internal/dataframe/compiler/optimize ./internal/dataframe/compiler/render/aql ./internal/dataframe/compiler ./internal/dataframe/runtime ./internal/dataframe -count=1 conformance: mkdir -p $(GOCACHE_DIR) - GOCACHE=$(GOCACHE_DIR) GOTOOLCHAIN=auto $(GO) test $(GOFLAGS) ./conformance/... -count=1 + GOCACHE=$(GOCACHE_DIR) GOTOOLCHAIN=$(GO_TOOLCHAIN) $(GO) test $(GOFLAGS) ./conformance/... -count=1 docker-build: docker build -t $(IMAGE) . diff --git a/README.md b/README.md index 65f904f..55fac6e 100644 --- a/README.md +++ b/README.md @@ -1,88 +1,101 @@ -# ARANGODB_PROTO - -FHIR graph loader and dataframe server, with ArangoDB as the primary execution -backend. - -This repo has two main runtime surfaces: - -- `arango-fhir-proto`: CLI for FHIR loading (including immutable complete - generations) and catalog diagnostics -- `arango-fhir-server`: Fiber server for compiler-backed GraphQL reads and a - temporary one-file import compatibility endpoint. It also exposes published - dataframe reads backed by ClickHouse when configured. - -The current product direction is: - -- load raw FHIR NDJSON into one collection per resource type -- store graph edges in `fhir_edge` -- profile populated fields into `fhir_field_catalog` -- lower typed dataframe requests through the FHIR-aware compiler into scoped AQL -- expose the current compiler-backed GraphQL transport - -ArangoDB is the only runtime backend. The tracked [`experimental/`](experimental/) -directory contains the local Arango compose setup. - -## Docs - -- [Quickstart](docs/QUICKSTART.md) -- Helm local-cluster deployment: `gen3-helm/helm/loom` -- [Developer Architecture](docs/DEVELOPER_ARCHITECTURE.md) -- [Compiler Performance](docs/COMPILER_PERFORMANCE.md) -- [Formal Product Gap Analysis](docs/FORMAL_GAP_ANALYSIS.md) -- [Compiler-First FHIR/AQL Plan](docs/COMPILER_FIRST_PLAN.md) -- [Physical Renderer Replacement Plan](docs/PHYSICAL_RENDERER_REPLACEMENT_PLAN.md) -- [Rich Physical Renderer Plan](docs/RICH_PHYSICAL_RENDERER_PLAN.md) -- [Luna Rich Physical Renderer Execution Plan](docs/LUNA_RICH_PHYSICAL_RENDERER_EXECUTION.md) -- [Luna Compiler Finalization Plan](docs/LUNA_COMPILER_FINALIZATION_PLAN.md) -- [Terra Ultra Parallel Execution Plan](docs/TERRA_ULTRA_EXECUTION_PLAN.md) -- [Part 5 Luna Frontend Enablement Plan](docs/LUNA_FRONTEND_ENABLEMENT_PART_5.md) - -## Current Layout - -- [`cmd/arango-fhir-proto/main.go`](cmd/arango-fhir-proto/main.go): CLI entrypoint -- [`cmd/arango-fhir-server/main.go`](cmd/arango-fhir-server/main.go): HTTP server entrypoint -- [`internal/ingest`](internal/ingest): load pipeline and Arango ingest bootstrap/runtime -- [`internal/catalog`](internal/catalog): populated-field and populated-reference discovery -- [`internal/dataset`](internal/dataset): dataset generation, schema, and scope lifecycle contract -- [`internal/dataset/arango`](internal/dataset/arango): Arango-backed immutable manifest and active-generation pointer store -- [`graphqlapi`](graphqlapi): GraphQL schema, request mapping, introspection service, and gqlgen output -- [`graphqlapi/query`](graphqlapi/query): GraphQL dataframe input translation, discovery, and builder introspection -- [`graphqlapi/materialization`](graphqlapi/materialization): GraphQL authorization and reads for published ClickHouse dataframes -- [`internal/dataframe`](internal/dataframe): dataframe validation, lowering, and AQL compilation -- [`fhirstructs`](fhirstructs): generated FHIR structs, validators, and graph-edge extraction -- [`fhirschema`](fhirschema): generated compiler schema metadata and selector/traversal resolution -- [`internal/graphschema`](internal/graphschema): exact graph-schema identity captured by dataset generations -- [`internal/httpapi`](internal/httpapi): HTTP host, authenticated GraphQL mounting, and legacy import compatibility wiring -- [`internal/authscope`](internal/authscope): shared request principal context and auth-resource-path scope resolution -- [`experimental/`](experimental/): local Arango development compose setup - -## Local Dev - -Start local Arango: - -```bash -rtk docker compose -f experimental/docker-compose.yml up -d +# Loom + +Loom turns FHIR-shaped data in ArangoDB into authorized, queryable flat +dataframes in ClickHouse. + +ArangoDB is the canonical graph store: Loom loads NDJSON, extracts FHIR +resources and relationships, profiles the populated graph, and compiles typed +dataframe recipes into scoped AQL. ClickHouse is the publication store: Loom +streams resolved recipe outputs into versioned physical tables, records their +schema and visibility in an Arango-backed catalog, and exposes them through a +stable reader API. + +Loom is deliberately not an arbitrary AQL gateway, ClickHouse SQL proxy, or a +replacement FHIR model. It is a recipe-driven graph-to-flat data service with +explicit authorization and publication boundaries. + +```mermaid +flowchart LR + NDJSON["FHIR NDJSON"] --> Ingest["Ingest and catalog"] + Ingest --> Arango["ArangoDB\nresources, fhir_edge, field catalog"] + Recipe["Versioned dataframe recipe"] --> Compile["Resolve, validate, compile"] + Arango --> Compile + Compile --> AQL["Scoped AQL stream"] + AQL --> Publish["Atomic publication catalog commit"] + Publish --> ClickHouse["Versioned flat tables"] + Publish --> Catalog["Arango publication catalog"] + Catalog["Arango publication catalog"] --> Flat["POST /graphql/flat"] + ClickHouse --> Flat + Arango --> Graph["POST /graphql/graph"] + Compile --> Graph ``` -Generate/build: +## What Loom owns + +- FHIR NDJSON ingestion into ArangoDB resource collections and `fhir_edge`. +- Immutable dataset generations and active-generation selection. +- Populated-field, reference, traversal, and pivot discovery. +- Strict, versioned dataframe recipes and compiler-backed AQL execution. +- Scoped publication of recipe outputs into ClickHouse. +- A durable Arango catalog that maps logical dataframe names to current READY + ClickHouse outputs. +- Multi-project, `auth_resource_path`-scoped flat-data reads. + +Loom does **not** expose arbitrary ClickHouse tables, arbitrary SQL, or an +Elasticsearch/Guppy fallback. A logical `dataType` is a catalog alias, never a +browser-supplied physical table name. + +## Runtime surfaces + +| Surface | Purpose | +| --- | --- | +| `arango-fhir-proto` | Operator CLI for loading data, loading immutable generations, catalog discovery, and local dataframe materialization. | +| `arango-fhir-server` | HTTP server for graph compilation/control and flat dataframe reads. | +| `POST /graphql/graph` | Arango graph and recipe control plane: builder introspection, dataframe/recipe validation, preview, execution, and publication. | +| `POST /graphql/flat` | ClickHouse reader: discover published datasets, fetch rows with filters/keyset cursors, and aggregate registered outputs. | +| `POST /api/v1/imports` | Legacy one-resource import compatibility path; disabled in dataset-generation mode. | + +`GET /graphql/graph` serves GraphQL Playground for the graph API. `GET /apollo` +opens Apollo Sandbox pointed at `/graphql/graph`. There is intentionally no +`/graphql` compatibility route. + +## Data lifecycle + +1. Load FHIR NDJSON into ArangoDB. Loom creates resource collections, + `fhir_edge`, and `fhir_field_catalog`. +2. For production snapshots, load an immutable dataset generation and let the + active manifest select it for a project. +3. Resolve a registered recipe against the populated graph and caller scope. +4. Compile the resolved recipe to parameterized AQL and stream rows from + ArangoDB. +5. Publish output streams to new ClickHouse physical tables. Loom injects the + reserved `auth_resource_path` column, records schema/provenance in Arango, + and atomically advances the logical publication pointer only once every + output is READY. +6. Query the logical dataframe through `/graphql/flat`. The reader resolves + authorized current outputs from the catalog; it never accepts a physical + ClickHouse table name. + +The configured ClickHouse database is created by the server during startup if +necessary. Publication creates its tables itself. No DDL or alias-management +HTTP endpoint is required. The ClickHouse account needs database creation plus +`CREATE`, `INSERT`, `SELECT`, and `DROP` privileges. + +## Local development + +The local Compose stack starts both ArangoDB and ClickHouse: ```bash +rtk docker compose -f experimental/docker-compose.yml up -d make generate-fhir make generate-graphql make build ``` -The repository root is also the server command, so a plain build works: - -```bash -go build . -``` - -Load the bundled sample dataset: +Load the bundled example data into a mutable local namespace: ```bash ./bin/arango-fhir-proto load \ - --backend arango \ --url http://127.0.0.1:8529 \ --database fhir_proto \ --meta-dir META \ @@ -90,148 +103,173 @@ Load the bundled sample dataset: --auth-resource-path EllrottLab-GDC_Data ``` -For an immutable, generation-qualified load instead of the mutable prototype -load above, use a complete `META` directory and an operator-supplied opaque -generation ID. This command deliberately has no `--truncate` flag: +For a production-shaped immutable snapshot, use `load-generation` instead. It +requires an opaque generation identifier and deliberately forbids truncation: ```bash ./bin/arango-fhir-proto load-generation \ - --generation local-meta-2026-07-11 \ - --backend arango \ --url http://127.0.0.1:8529 \ --database fhir_proto \ --meta-dir META \ --project ARANGODB_PROTO \ + --generation local-meta-1 \ --auth-resource-path EllrottLab-GDC_Data ``` -Start the server in local demo mode: +Start Loom locally with an unrestricted development principal: ```bash ./bin/arango-fhir-server \ --listen :8080 \ --no-auth \ - --backend arango \ --url http://127.0.0.1:8529 \ - --database fhir_proto + --database fhir_proto \ + --clickhouse-url clickhouse://127.0.0.1:9000 \ + --clickhouse-database loom ``` -To read the active immutable generation instead, add `--dataset-generations`. -That mode disables `POST /api/v1/imports`; use `load-generation` (or the future -bundle/job API) to create a complete snapshot. +Add `--dataset-generations` when reading the active immutable generation. That +mode disables the legacy one-file import endpoint. -Then open: +Useful local URLs: -- Apollo Sandbox: [http://127.0.0.1:8080/apollo](http://127.0.0.1:8080/apollo) -- GraphQL endpoint: [http://127.0.0.1:8080/graphql](http://127.0.0.1:8080/graphql) -- Health check: [http://127.0.0.1:8080/healthz](http://127.0.0.1:8080/healthz) +- [Graph Playground](http://127.0.0.1:8080/graphql/graph) +- [Apollo Sandbox](http://127.0.0.1:8080/apollo) +- Flat reader: `http://127.0.0.1:8080/graphql/flat` +- [Health check](http://127.0.0.1:8080/healthz) -The full step-by-step flow, including a sample GraphQL dataframe mutation, lives -in [docs/QUICKSTART.md](docs/QUICKSTART.md). +Run the checked-in graph dataframe example after the server is up: -## Local cluster deployment +```bash +make dataframe-demo +``` -The Helm deployment now lives in the `gen3-helm` repository at -`helm/loom`. It owns the Loom chart and its official ClickStack dependencies; -see that repository's README for kind/minikube installation and port -forwarding instructions. +See [the Quickstart](docs/QUICKSTART.md) for the complete example request and +response flow. -## Published dataframe materializations +## Graph and flat GraphQL contracts -Loom can stream a validated dataframe recipe into a versioned ClickHouse table. -The operator command accepts a compiler-shaped JSON recipe: +### Graph: compile, inspect, and publish -```json -{ - "project": "ARANGODB_PROTO", - "rootResourceType": "Patient", - "fields": [ - {"name": "patient_id", "select": "id", "valueMode": "FIRST"} - ], - "schema": [ - {"name": "patient_id", "clickhouseType": "Nullable(String)"} - ] -} -``` +`/graphql/graph` is where a caller asks Loom about the Arango graph or drives +recipe work. Its schema includes builder introspection, direct dataframe +execution, and recipe operations such as validation, explanation, preview, +execution, and `materializeDataframeRecipeBundle`. -```bash -./bin/arango-fhir-proto materialize-dataframe \ - --request dataframe.json \ - --name case-explorer \ - --clickhouse-url clickhouse://127.0.0.1:9000 \ - --clickhouse-database loom -``` +Publication is intentionally a recipe-level operation: Loom must read the +authorized Arango graph, resolve the recipe against the selected generation, +and then write its output to ClickHouse. It is not a raw “insert rows” API. -The server exposes READY materializations through the existing GraphQL endpoint: +### Flat: discover and read published dataframes + +`/graphql/flat` is the stable ClickHouse reader. The schema is intentionally +small and data-driven: ```graphql -query Rows($input: DataframeRowsInput!) { +query ExplorerRows($input: DataframeRowsInput!) { dataframeRows(input: $input) { + materialization { name revision rowCount columns { name logicalType } } columns rows + totalCount pageInfo { hasNextPage endCursor } - materialization { id name rowCount columns { name clickhouseType } } } } ``` -The browser never receives a ClickHouse table name or SQL capability. Loom -validates the requested columns, filters, sort, page size, project, generation, -and materialization state before querying ClickHouse. An explicit `schema` -preflight validates column names/types before the table is created and rejects -rows that emit undeclared or incompatible values. Aggregates accept the same -`EQ` and `CONTAINS` filters as row reads. - -If you are starting from a fresh checkout, go there next: - -- [Continue with the Quickstart](docs/QUICKSTART.md) - -## Build Targets - -Important make targets: - -- `make generate-fhir` -- `make generate-graphql` -- `make build` -- `make build-server` -- `make build-cli` -- `make graphql-check` -- `make test` -- `make conformance` -- `make compiler-bench` +```json +{ + "input": { + "dataType": "cases", + "first": 100, + "sort": { "column": "case_id" } + } +} +``` -`make generate-graphql` is important now. The GraphQL schema and generated -artifacts are not purely static, and the repo includes a small reproducible -workaround in the generation target for a gqlgen scalar/codegen edge case. +The public reader input contains a logical `dataType`, not a project, +generation, materialization ID, or physical table. Loom derives the authorized +project set from the principal, finds each project’s current READY publication, +and federates compatible outputs. Rows remain permissive JSON; columns and +capabilities are discovered at runtime so a new publication does not require a +GraphQL regeneration or server restart. + +## Authorization and tenancy + +In production, Loom uses either basic authentication or Calypr/Fence-backed +authorization. Calypr mode resolves the caller’s allowed +`auth_resource_path` values and applies them to graph reads and published flat +reads. Publication injects `auth_resource_path` into every ClickHouse output; +the flat reader treats it as an authorization predicate, never as a +client-controlled filter. + +Set `--no-auth` only for local development. It creates an explicit unrestricted +principal and must not be used in a deployment. + +The server can be configured with YAML: + +```yaml +server: + listen: ":8080" + backend: arango + url: http://arangodb:8529 + database: fhir_proto + dataset_generations: true + clickhouse: + url: clickhouse://clickhouse:9000 + database: loom + +auth: + mode: calypr +``` -## Current HTTP Surfaces +Basic mode reads `LOOM_AUTH_BASIC_USERNAME` and `LOOM_AUTH_BASIC_PASSWORD` if +they are not supplied in the config file. -The server mounts: +## Repository guide -- `GET /healthz` -- `GET /apollo` -- `GET /graphql` -- `POST /graphql` -- `POST /api/v1/imports` (legacy one-file import; disabled with `--dataset-generations`) +| Path | Responsibility | +| --- | --- | +| [`cmd/`](cmd) | Operator CLI, server executable, and developer tools. | +| [`internal/ingest`](internal/ingest) | NDJSON loading, validation, graph extraction, and ingest lifecycle. | +| [`internal/dataset`](internal/dataset) | Immutable generation and active-manifest contracts. | +| [`internal/catalog`](internal/catalog) | Evidence of populated fields, references, and authorization paths. | +| [`internal/dataframe/compiler`](internal/dataframe/compiler) | Typed plan IR, lowering, optimization, and AQL rendering. | +| [`internal/dataframe/recipe`](internal/dataframe/recipe) | Recipe contract, validation, schema resolution, execution, and control services. | +| [`internal/dataframe/publication`](internal/dataframe/publication) | Backend-neutral bounded streaming publication contract. | +| [`internal/dataframe/materialization`](internal/dataframe/materialization) | ClickHouse table lifecycle, durable publication pointers, and federated reads. | +| [`internal/store/arango`](internal/store/arango) | ArangoDB boundary. | +| [`internal/store/clickhouse`](internal/store/clickhouse) | Typed ClickHouse driver boundary and DDL/DML. | +| [`graphqlapi`](graphqlapi) | Graph control-plane schema and resolvers. | +| [`graphqlapi/clickhouse`](graphqlapi/clickhouse) | Dedicated flat-reader GraphQL schema and resolvers. | -HTTP wiring lives in [`internal/httpapi/routes.go`](internal/httpapi/routes.go) and [`internal/httpapi/server.go`](internal/httpapi/server.go). +## Build, generation, and tests -## Primary Collections +```bash +make build # server and CLI binaries +make generate-fhir # generated FHIR structs and schema metadata +make generate-graphql # gqlgen bindings +make graphql-check # GraphQL and dataframe checks +make test # full Go test suite +make conformance # compiler conformance corpus +``` -The loader bootstraps: +The generated FHIR metadata and GraphQL bindings are checked in. Regenerate +them when their inputs change; do not hand-edit generated files. -- one collection per FHIR resource type discovered in the NDJSON input -- `fhir_edge` -- `fhir_field_catalog` -- `loom_dataset_lifecycle` for generation-aware loads (never truncated) +For a direct build and full verification: -See [`internal/ingest/backend.go`](internal/ingest/backend.go). +```bash +go build . +go test ./... +``` -## Status +## Further reading -What is current and real: +- [Quickstart](docs/QUICKSTART.md) +- [Developer architecture](docs/DEVELOPER_ARCHITECTURE.md) +- [ClickHouse reader contract and execution plan](docs/CLICKHOUSE_GRAPHQL_READER_EXECUTION_PLAN.md) +- [Explorer/Loom parity plan](docs/EXPLORER_LOOM_SLICE_PARITY_PLAN.md) +- [Experimental local stack](experimental/README.md) -- GraphQL introspection for populated traversals/fields/pivots -- GraphQL dataframe execution on Arango -- generated schema metadata in `fhirschema` -- derived field aliases in `graphqlapi/query` and explicit lowering rules in `internal/dataframe` +The Helm chart lives in the separate `gen3-helm` repository under `helm/loom`. diff --git a/cmd/arango-fhir-proto/main.go b/cmd/arango-fhir-proto/main.go index dbefda1..35ca76d 100644 --- a/cmd/arango-fhir-proto/main.go +++ b/cmd/arango-fhir-proto/main.go @@ -13,6 +13,7 @@ import ( "github.com/calypr/loom/internal/catalog" "github.com/calypr/loom/internal/dataframe/materialization" materializationarango "github.com/calypr/loom/internal/dataframe/materialization/arango" + "github.com/calypr/loom/internal/dataframe/recipe" dataframeruntime "github.com/calypr/loom/internal/dataframe/runtime" "github.com/calypr/loom/internal/dataset" "github.com/calypr/loom/internal/ingest" @@ -257,40 +258,25 @@ func runMaterializeDataframe(ctx context.Context, args []string) error { } dataframes := dataframeruntime.NewService(dataframeruntime.ServiceConfig{ConnectionOptions: opts}) service := &materialization.Service{Dataframes: dataframes, ClickHouse: ch, Registry: registry} - builder := input.Builder() - result, err := service.Materialize(ctx, materialization.Request{Name: name, Run: dataframeruntime.RunRequest{Builder: builder}, Schema: input.Schema}) + result, err := service.Materialize(ctx, materialization.Request{Name: name, Run: dataframeruntime.RunRequest{ + Recipe: input.Recipe, + Bindings: recipe.RuntimeBindings{Project: input.Project, DatasetGeneration: input.DatasetGeneration, AuthResourcePaths: input.AuthResourcePaths}, + }, Schema: input.Schema}) if err != nil { return err } return printJSON(result) } -// materializationInput is intentionally the operator-facing, compiler-shaped -// recipe. The browser-facing GraphQL input can be mapped to the same builder -// by the GraphQL adapter without making this command depend on gqlgen output. +// materializationInput is the operator-facing recipe document. GraphQL and +// CLI callers now share the same recipe wire format; no request translation is +// performed at this boundary. type materializationInput struct { - Project string `json:"project"` - DatasetGeneration string `json:"datasetGeneration"` - AuthResourcePaths []string `json:"authResourcePaths"` - RootResourceType string `json:"rootResourceType"` - RowGrain dataframeruntime.RowGrain `json:"rowGrain"` - Fields []dataframeruntime.FieldSelect `json:"fields"` - Filters []dataframeruntime.TypedFilter `json:"filters"` - Pivots []dataframeruntime.PivotSelect `json:"pivots"` - Aggregates []dataframeruntime.AggregateSelect `json:"aggregates"` - Slices []dataframeruntime.RepresentativeSlice `json:"slices"` - Traversals []dataframeruntime.TraversalStep `json:"traversals"` - Schema []materialization.SchemaColumn `json:"schema"` -} - -func (in materializationInput) Builder() dataframeruntime.Builder { - return dataframeruntime.Builder{ - Project: in.Project, DatasetGeneration: in.DatasetGeneration, - AuthResourcePaths: in.AuthResourcePaths, RootResourceType: in.RootResourceType, - RowGrain: in.RowGrain, Fields: in.Fields, Filters: in.Filters, - Pivots: in.Pivots, Aggregates: in.Aggregates, Slices: in.Slices, - Traversals: in.Traversals, - } + Recipe recipe.Bundle `json:"recipe"` + Project string `json:"project"` + DatasetGeneration string `json:"datasetGeneration"` + AuthResourcePaths []string `json:"authResourcePaths"` + Schema []materialization.SchemaColumn `json:"schema"` } func parseDiscoverPopulatedReferenceOptions(args []string, errorHandling flag.ErrorHandling) (catalog.PopulatedReferenceOptions, error) { diff --git a/cmd/dataframe-profile/main.go b/cmd/dataframe-profile/main.go index c7bd6c6..f13bce5 100644 --- a/cmd/dataframe-profile/main.go +++ b/cmd/dataframe-profile/main.go @@ -6,33 +6,50 @@ package main import ( "context" "crypto/sha256" + "encoding/csv" "encoding/hex" "encoding/json" "flag" "fmt" "os" "path/filepath" + "strings" "time" compilerfixture "github.com/calypr/loom/conformance/compiler" "github.com/calypr/loom/graphqlapi/model" queryapi "github.com/calypr/loom/graphqlapi/query" "github.com/calypr/loom/internal/dataframe" + "github.com/calypr/loom/internal/dataframe/recipe" + "github.com/calypr/loom/internal/dataframe/semantic" arangostore "github.com/calypr/loom/internal/store/arango" ) type profileReport struct { - Fixture string `json:"fixture"` - AQLSHA256 string `json:"aql_sha256"` - ResultSHA256 string `json:"result_sha256,omitempty"` - BindVars map[string]any `json:"bind_vars"` - Rows int `json:"rows"` - Explain explainReport `json:"explain"` - Profile profileReportDetails `json:"profile"` - PlanDiagnostics dataframe.CompilerPlanDiagnostics `json:"plan_diagnostics"` - AQLPath string `json:"aql_path"` - ProfilePath string `json:"profile_path,omitempty"` - ProfileWallSeconds float64 `json:"profile_wall_seconds"` + ArtifactVersion string `json:"artifact_version"` + Status string `json:"status"` + Fixture string `json:"fixture"` + AQLSHA256 string `json:"aql_sha256"` + ResultSHA256 string `json:"result_sha256,omitempty"` + BindVars map[string]any `json:"bind_vars"` + Rows int `json:"rows"` + Explain explainReport `json:"explain"` + Profile profileReportDetails `json:"profile"` + PlanDiagnostics dataframe.CompilerPlanDiagnostics `json:"plan_diagnostics"` + AQLPath string `json:"aql_path"` + ProfilePath string `json:"profile_path,omitempty"` + ProfileWallSeconds float64 `json:"profile_wall_seconds"` + ProfileColdWallSeconds float64 `json:"profile_cold_wall_seconds"` + Expected expectedReport `json:"expected,omitempty"` +} + +type expectedReport struct { + Path string `json:"path,omitempty"` + Rows int `json:"rows,omitempty"` + Columns []string `json:"columns,omitempty"` + RowsMatch bool `json:"rows_match"` + ColumnsMatch bool `json:"columns_match"` + Compared bool `json:"compared"` } type explainReport struct { @@ -57,16 +74,17 @@ type profileReportDetails struct { func main() { var ( - fixtureDir = flag.String("fixtures", "conformance/compiler/fixtures", "compiler fixture directory") - fixtureID = flag.String("fixture", "gdc-case-matrix", "fixture ID to compile") - variables = flag.String("variables", "", "GraphQL variables JSON; when set, compile its input instead of the fixture builder") - limit = flag.Int("limit", 1000, "root row limit") - url = flag.String("url", "http://127.0.0.1:8529", "ArangoDB URL") - database = flag.String("database", "fhir_proto", "ArangoDB database") - aqlPath = flag.String("aql-out", "", "write exact rendered AQL to this path (default: -.aql)") - reportPath = flag.String("report-out", "", "write JSON profile report to this path") - profile = flag.Int("profile", 2, "Arango profile level") - batchSize = flag.Int("batch-size", 10000, "Arango profile cursor batch size") + fixtureDir = flag.String("fixtures", "conformance/compiler/fixtures", "compiler fixture directory") + fixtureID = flag.String("fixture", "gdc-case-matrix", "fixture ID to compile") + variables = flag.String("variables", "", "GraphQL variables JSON; when set, compile its input instead of the fixture builder") + limit = flag.Int("limit", 1000, "root row limit") + url = flag.String("url", "http://127.0.0.1:8529", "ArangoDB URL") + database = flag.String("database", "fhir_proto", "ArangoDB database") + aqlPath = flag.String("aql-out", "", "write exact rendered AQL to this path (default: -.aql)") + reportPath = flag.String("report-out", "", "write JSON profile report to this path") + profile = flag.Int("profile", 2, "Arango profile level") + batchSize = flag.Int("batch-size", 10000, "Arango profile cursor batch size") + expectedCSV = flag.String("expected-csv", "", "optional legacy CSV used for row/column parity") ) flag.Parse() if *limit <= 0 { @@ -77,9 +95,10 @@ func main() { } var ( - fixture compilerfixture.Fixture - builder dataframe.Builder - label = *fixtureID + fixture compilerfixture.Fixture + bundle recipe.Bundle + bindings recipe.RuntimeBindings + label = *fixtureID ) if *variables != "" { data, err := os.ReadFile(*variables) @@ -95,7 +114,11 @@ func main() { if payload.Input.Project == "" || payload.Input.RootResourceType == "" { fatalf("GraphQL variables %q do not contain a complete input", *variables) } - builder = queryapi.BuilderFromInput(payload.Input) + bundle, err = queryapi.RecipeBundleFromInput(payload.Input) + if err != nil { + fatalf("convert GraphQL variables to recipe: %v", err) + } + bindings = recipe.RuntimeBindings{Project: payload.Input.Project, AuthResourcePaths: payload.Input.AuthResourcePaths} label = filepath.Base(*variables) } else { fixtures, err := compilerfixture.LoadDir(*fixtureDir) @@ -111,12 +134,25 @@ func main() { if fixture.ID == "" { fatalf("fixture %q not found in %s", *fixtureID, *fixtureDir) } - builder = fixture.Builder + bundle = fixture.Recipe + bindings = recipe.RuntimeBindings{Project: fixture.Project} } - compiled, err := dataframe.CompileRequest(builder, *limit) + plan, err := semantic.BuildRecipePlan(bundle, bindings) + if err != nil { + fatalf("build recipe plan %q: %v", label, err) + } + resolved, err := semantic.ResolveRecipePlan(plan, "profile", bindings.DatasetGeneration) + if err != nil { + fatalf("resolve recipe plan %q: %v", label, err) + } + compiledQueries, err := dataframe.CompileResolvedRecipePlanWithPolicy(resolved, *limit, dataframe.DefaultPhysicalOptimizationPolicy()) if err != nil { fatalf("compile GDC request %q: %v", label, err) } + if len(compiledQueries) != 1 { + fatalf("compile GDC request %q produced %d outputs; profiling requires exactly one", label, len(compiledQueries)) + } + compiled := compiledQueries[0] hash := sha256.Sum256([]byte(compiled.Query)) aqlHash := hex.EncodeToString(hash[:]) if *aqlPath == "" { @@ -138,16 +174,24 @@ func main() { fatalf("EXPLAIN: %v", err) } started := time.Now() - profileResult, err := client.Profile(ctx, arangostore.ProfileRequest{ + profileRequest := arangostore.ProfileRequest{ Query: compiled.Query, BindVars: compiled.BindVars, BatchSize: *batchSize, Count: true, Options: arangostore.ProfileOptions{Profile: *profile}, - }) + } + _, err = client.Profile(ctx, profileRequest) if err != nil { fatalf("PROFILE: %v", err) } + coldSeconds := time.Since(started).Seconds() + warmStarted := time.Now() + profileResult, err := client.Profile(ctx, profileRequest) + if err != nil { + fatalf("warm PROFILE: %v", err) + } + warmSeconds := time.Since(warmStarted).Seconds() assessment := arangostore.AssessExplainResult(explain) summary := arangostore.SummarizeProfile(profileResult) @@ -158,11 +202,13 @@ func main() { traversalNodes := filterProfileNodes(summary.Nodes, "TraversalNode") enumerateListNodes := filterProfileNodes(summary.Nodes, "EnumerateListNode") report := profileReport{ - Fixture: label, - AQLSHA256: aqlHash, - ResultSHA256: canonicalResultHash(profileResult.Result), - BindVars: compiled.BindVars, - Rows: profileResult.Count, + ArtifactVersion: "loom-default-dataframer-parity/v1", + Status: "live-arango", + Fixture: label, + AQLSHA256: aqlHash, + ResultSHA256: canonicalResultHash(profileResult.Result), + BindVars: compiled.BindVars, + Rows: profileResult.Count, Explain: explainReport{ Plans: assessment.Plans, FullCollectionScans: assessment.FullCollectionScans, @@ -181,9 +227,17 @@ func main() { EnumerateListNodes: enumerateListNodes, TopNodes: topNodes, }, - PlanDiagnostics: compiled.PlanDiagnostics, - AQLPath: *aqlPath, - ProfileWallSeconds: time.Since(started).Seconds(), + PlanDiagnostics: compiled.PlanDiagnostics, + AQLPath: *aqlPath, + ProfileWallSeconds: warmSeconds, + ProfileColdWallSeconds: coldSeconds, + } + report.Expected = compareExpectedCSV(*expectedCSV, compiled.Columns, profileResult.Count) + if report.Expected.Compared && (!report.Expected.RowsMatch || !report.Expected.ColumnsMatch) { + report.Status = "live-arango-parity-mismatch" + } + if report.Expected.Compared && (!report.Expected.RowsMatch || !report.Expected.ColumnsMatch) { + report.Status = "live-arango-parity-mismatch" } if *reportPath != "" { report.ProfilePath = *reportPath @@ -227,6 +281,40 @@ func canonicalResultHash(rows []json.RawMessage) string { return hex.EncodeToString(hash.Sum(nil)) } +func compareExpectedCSV(path string, compiledColumns []string, rows int) expectedReport { + result := expectedReport{Path: path} + if strings.TrimSpace(path) == "" { + return result + } + result.Compared = true + file, err := os.Open(path) + if err != nil { + return result + } + defer file.Close() + records, err := csv.NewReader(file).ReadAll() + if err != nil || len(records) == 0 { + return result + } + result.Rows = len(records) - 1 + result.Columns = records[0] + result.RowsMatch = result.Rows == rows + result.ColumnsMatch = sameStrings(result.Columns, compiledColumns) + return result +} + +func sameStrings(left, right []string) bool { + if len(left) != len(right) { + return false + } + for i := range left { + if left[i] != right[i] { + return false + } + } + return true +} + func fatalf(format string, args ...any) { fmt.Fprintf(os.Stderr, "dataframe-profile: "+format+"\n", args...) os.Exit(1) diff --git a/cmd/dataframe-query/main.go b/cmd/dataframe-query/main.go index 8348db8..4f4c80d 100644 --- a/cmd/dataframe-query/main.go +++ b/cmd/dataframe-query/main.go @@ -32,7 +32,7 @@ func main() { timeout time.Duration printResponse bool ) - flag.StringVar(&url, "url", "http://127.0.0.1:8080/graphql", "GraphQL endpoint") + flag.StringVar(&url, "url", "http://127.0.0.1:8080/graphql/graph", "GraphQL endpoint") flag.StringVar(&queryPath, "query", "examples/meta_patient_dataframe.graphql", "GraphQL operation file") flag.StringVar(&variablesPath, "variables", "examples/meta_patient_dataframe.variables.json", "GraphQL variables JSON file") flag.IntVar(&repeat, "repeat", 1, "number of sequential requests; only the final response is printed") diff --git a/cmd/gqlgenfix/main.go b/cmd/gqlgenfix/main.go index f54be25..918ac41 100644 --- a/cmd/gqlgenfix/main.go +++ b/cmd/gqlgenfix/main.go @@ -27,7 +27,7 @@ func main() { if err != nil { fail("fix JSON unmarshal return: %v", err) } - source, err = replaceInFunction(source, + source, err = replaceInFunctionIfPresent(source, "func (ec *executionContext) unmarshalNFhirAggregateInput", "return res, graphql.ErrorOnPath(ctx, err)", "return &res, graphql.ErrorOnPath(ctx, err)", @@ -40,6 +40,13 @@ func main() { } } +func replaceInFunctionIfPresent(contents, signature, old, replacement string) (string, error) { + if !strings.Contains(contents, signature) { + return contents, nil + } + return replaceInFunction(contents, signature, old, replacement) +} + func replaceInFunction(contents, signature, old, replacement string) (string, error) { start := strings.Index(contents, signature) if start < 0 { diff --git a/conformance/compiler/benchmark_test.go b/conformance/compiler/benchmark_test.go index 590d8d0..197e4e8 100644 --- a/conformance/compiler/benchmark_test.go +++ b/conformance/compiler/benchmark_test.go @@ -6,13 +6,15 @@ import ( "slices" "testing" - "github.com/calypr/loom/internal/dataframe" "github.com/calypr/loom/fhirschema" + "github.com/calypr/loom/internal/dataframe" + "github.com/calypr/loom/internal/dataframe/recipe" ) type compilerBenchmarkCase struct { name string - builder dataframe.Builder + recipe recipe.Bundle + project string limit int } @@ -33,7 +35,7 @@ func BenchmarkCompilerOracle(b *testing.B) { b.ReportAllocs() b.ResetTimer() for i := 0; i < b.N; i++ { - benchmarkCompiled, benchmarkErr = dataframe.CompileRequest(testCase.builder, testCase.limit) + benchmarkCompiled, benchmarkErr = compileRecipe(testCase.recipe, testCase.project, testCase.limit, dataframe.DefaultPhysicalOptimizationPolicy()) } }) } @@ -83,18 +85,17 @@ func loadCompilerBenchmarkCases(t benchmarkTestingT) []compilerBenchmarkCase { for _, fixture := range fixtures { cases = append(cases, compilerBenchmarkCase{ name: "fixture/" + fixture.ID, - builder: fixture.Builder, + recipe: fixture.Recipe, + project: fixture.Project, limit: fixture.Limit, }) } for _, resourceType := range fhirschema.ResourceTypes() { cases = append(cases, compilerBenchmarkCase{ - name: "generated-root/" + resourceType, - builder: dataframe.Builder{ - Project: "compiler-benchmark", - RootResourceType: resourceType, - }, - limit: 1, + name: "generated-root/" + resourceType, + recipe: rootRecipe(resourceType), + project: "compiler-benchmark", + limit: 1, }) } return cases @@ -110,3 +111,13 @@ func benchmarkCaseNames(cases []compilerBenchmarkCase) []string { } return names } + +func rootRecipe(resourceType string) recipe.Bundle { + grain, ok := dataframe.InferRowGrain(resourceType) + if !ok { + // Keep malformed/abstract roots structurally valid long enough for the + // schema-backed compiler to emit the intended root rejection. + grain = dataframe.RowGrain("resource") + } + return recipe.Bundle{RecipeSchemaVersion: recipe.CurrentSchemaVersion, Name: "generated_" + resourceType, TranslationVersion: "benchmark", Outputs: []recipe.Output{{Name: "root", RootResourceType: resourceType, RowGrain: string(grain)}}} +} diff --git a/conformance/compiler/corpus_coverage_test.go b/conformance/compiler/corpus_coverage_test.go index 12e7c9f..e970182 100644 --- a/conformance/compiler/corpus_coverage_test.go +++ b/conformance/compiler/corpus_coverage_test.go @@ -15,7 +15,7 @@ func TestGenericFHIRCorpusCoverage(t *testing.T) { t.Fatal(err) } want := map[string]string{ - "patient-sibling-targets": "sibling", + "patient-sibling-targets": "sibling", "research-subject-study-required": "outbound", "patient-deep-filter": "deep", "specimen-aggregate-slice": "reuse", diff --git a/conformance/compiler/fixtures/gdc-case-matrix.json b/conformance/compiler/fixtures/gdc-case-matrix.json index d1edec3..f07e87a 100644 --- a/conformance/compiler/fixtures/gdc-case-matrix.json +++ b/conformance/compiler/fixtures/gdc-case-matrix.json @@ -2,101 +2,281 @@ "schema": "loom.compiler-oracle/v1", "id": "gdc-case-matrix", "description": "GDC-style patient dataframe with diagnoses, samples, files, groups, observations, pivots, slices, and nested traversal sets", - "tags": ["supported", "gdc", "patient", "nested", "aggregate", "pivot", "slice"], + "tags": [ + "supported", + "gdc", + "patient", + "nested", + "aggregate", + "pivot", + "slice" + ], "limit": 25, - "builder": { - "Project": "ARANGODB_PROTO", - "RootResourceType": "Patient", - "Fields": [ - {"Name": "patient_id", "Select": "id", "ValueMode": "AUTO"}, - {"Name": "case_identifier", "Select": "identifier[].value", "ValueMode": "AUTO"}, - {"Name": "gender", "Select": "gender", "ValueMode": "AUTO"} - ], - "Traversals": [ + "project": "ARANGODB_PROTO", + "recipe": { + "recipeSchemaVersion": 1, + "name": "fixture_Patient", + "translationVersion": "fixture", + "outputs": [ { - "Label": "subject_Patient", - "ToResourceType": "Condition", - "Alias": "diagnosis", - "Aggregates": [ - {"Name": "diagnosis_count", "Operation": "COUNT"}, - {"Name": "diagnosis_values", "Operation": "DISTINCT_VALUES", "Select": "code.coding[].display", "ValueMode": "AUTO"} - ], - "Slices": [ - {"Name": "representative_diagnoses", "Limit": 3, "Fields": [ - {"Name": "diagnosis_id", "Select": "id", "ValueMode": "AUTO"}, - {"Name": "diagnosis", "Select": "code.coding[].display", "ValueMode": "AUTO"} - ]} - ] - }, - { - "Label": "subject_Patient", - "ToResourceType": "Specimen", - "Alias": "sample", - "Aggregates": [ - {"Name": "sample_count", "Operation": "COUNT"}, - {"Name": "sample_types", "Operation": "DISTINCT_VALUES", "Select": "type.coding[].display", "ValueMode": "AUTO"} - ], - "Slices": [ - {"Name": "representative_samples", "Limit": 5, "Fields": [ - {"Name": "sample_id", "Select": "id", "ValueMode": "AUTO"}, - {"Name": "sample_type", "Select": "type.coding[].display", "ValueMode": "AUTO"} - ]} + "name": "Patient", + "rootResourceType": "Patient", + "rowGrain": "patient", + "fields": [ + { + "name": "patient_id", + "expr": { + "select": "id" + }, + "valueMode": "AUTO" + }, + { + "name": "case_identifier", + "expr": { + "select": "identifier[].value" + }, + "valueMode": "AUTO" + }, + { + "name": "gender", + "expr": { + "select": "gender" + }, + "valueMode": "AUTO" + } ], - "Traversals": [ + "traversals": [ { - "Label": "subject_Specimen", - "ToResourceType": "DocumentReference", - "Alias": "file", - "Aggregates": [ - {"Name": "file_count", "Operation": "COUNT"}, - {"Name": "file_names", "Operation": "DISTINCT_VALUES", "Select": "content[].attachment.title", "ValueMode": "AUTO"}, - {"Name": "file_formats", "Operation": "DISTINCT_VALUES", "Select": "type.coding[].display", "ValueMode": "AUTO"} + "name": "subject_Patient", + "toResourceType": "Condition", + "alias": "diagnosis", + "aggregates": [ + { + "name": "diagnosis_count", + "operation": "COUNT" + }, + { + "name": "diagnosis_values", + "operation": "DISTINCT_VALUES", + "expr": { + "select": "code.coding[].display" + }, + "valueMode": "AUTO" + } ], - "Slices": [ - {"Name": "representative_files", "Limit": 10, "Fields": [ - {"Name": "file_id", "Select": "id", "ValueMode": "AUTO"}, - {"Name": "file_name", "Select": "content[].attachment.title", "ValueMode": "AUTO"}, - {"Name": "file_url", "Select": "content[].attachment.url", "ValueMode": "AUTO"}, - {"Name": "file_size", "Select": "content[].attachment.size", "ValueMode": "AUTO"}, - {"Name": "file_format", "Select": "type.coding[].display", "ValueMode": "AUTO"} - ]} + "slices": [ + { + "name": "representative_diagnoses", + "limit": 3, + "fields": [ + { + "name": "diagnosis_id", + "expr": { + "select": "id" + }, + "valueMode": "AUTO" + }, + { + "name": "diagnosis", + "expr": { + "select": "code.coding[].display" + }, + "valueMode": "AUTO" + } + ] + } ] }, { - "Label": "member_entity_Specimen", - "ToResourceType": "Group", - "Alias": "sample_group", - "Aggregates": [{"Name": "sample_group_count", "Operation": "COUNT"}], - "Traversals": [{ - "Label": "subject_Group", - "ToResourceType": "DocumentReference", - "Alias": "group_file", - "Aggregates": [{"Name": "group_file_count", "Operation": "COUNT"}] - }] + "name": "subject_Patient", + "toResourceType": "Specimen", + "alias": "sample", + "aggregates": [ + { + "name": "sample_count", + "operation": "COUNT" + }, + { + "name": "sample_types", + "operation": "DISTINCT_VALUES", + "expr": { + "select": "type.coding[].display" + }, + "valueMode": "AUTO" + } + ], + "slices": [ + { + "name": "representative_samples", + "limit": 5, + "fields": [ + { + "name": "sample_id", + "expr": { + "select": "id" + }, + "valueMode": "AUTO" + }, + { + "name": "sample_type", + "expr": { + "select": "type.coding[].display" + }, + "valueMode": "AUTO" + } + ] + } + ], + "traversals": [ + { + "name": "subject_Specimen", + "toResourceType": "DocumentReference", + "alias": "file", + "aggregates": [ + { + "name": "file_count", + "operation": "COUNT" + }, + { + "name": "file_names", + "operation": "DISTINCT_VALUES", + "expr": { + "select": "content[].attachment.title" + }, + "valueMode": "AUTO" + }, + { + "name": "file_formats", + "operation": "DISTINCT_VALUES", + "expr": { + "select": "type.coding[].display" + }, + "valueMode": "AUTO" + } + ], + "slices": [ + { + "name": "representative_files", + "limit": 10, + "fields": [ + { + "name": "file_id", + "expr": { + "select": "id" + }, + "valueMode": "AUTO" + }, + { + "name": "file_name", + "expr": { + "select": "content[].attachment.title" + }, + "valueMode": "AUTO" + }, + { + "name": "file_url", + "expr": { + "select": "content[].attachment.url" + }, + "valueMode": "AUTO" + }, + { + "name": "file_size", + "expr": { + "select": "content[].attachment.size" + }, + "valueMode": "AUTO" + }, + { + "name": "file_format", + "expr": { + "select": "type.coding[].display" + }, + "valueMode": "AUTO" + } + ] + } + ] + }, + { + "name": "member_entity_Specimen", + "toResourceType": "Group", + "alias": "sample_group", + "aggregates": [ + { + "name": "sample_group_count", + "operation": "COUNT" + } + ], + "traversals": [ + { + "name": "subject_Group", + "toResourceType": "DocumentReference", + "alias": "group_file", + "aggregates": [ + { + "name": "group_file_count", + "operation": "COUNT" + } + ] + } + ] + } + ] + }, + { + "name": "subject_Patient", + "toResourceType": "Observation", + "alias": "observation", + "pivots": [ + { + "name": "observation_values", + "columnExpr": { + "select": "code.coding[].display" + }, + "valueExpr": { + "select": "valueQuantity.value" + }, + "columns": [ + "Tumor Purity", + "Hemoglobin", + "Platelet Count", + "White Blood Cell Count" + ] + } + ], + "aggregates": [ + { + "name": "observation_count", + "operation": "COUNT" + }, + { + "name": "observation_codes", + "operation": "DISTINCT_VALUES", + "expr": { + "select": "code.coding[].display" + }, + "valueMode": "AUTO" + } + ] } ] - }, - { - "Label": "subject_Patient", - "ToResourceType": "Observation", - "Alias": "observation", - "Aggregates": [ - {"Name": "observation_count", "Operation": "COUNT"}, - {"Name": "observation_codes", "Operation": "DISTINCT_VALUES", "Select": "code.coding[].display", "ValueMode": "AUTO"} - ], - "Pivots": [{ - "Name": "observation_values", - "ColumnSelect": "code.coding[].display", - "ValueSelect": "valueQuantity.value", - "Columns": ["Tumor Purity", "Hemoglobin", "Platelet Count", "White Blood Cell Count"] - }] } ] }, "expected": { "supported": true, - "planProfile": "generic_fhir_graph", - "bindVars": {"project": "ARANGODB_PROTO", "limit": 25}, - "outputColumns": ["patient_id", "case_identifier", "gender", "sample__sample_count", "observation__observation_count"] + "planProfile": "generic_fhir_graph_recipe", + "bindVars": { + "limit": 25, + "project": "ARANGODB_PROTO" + }, + "outputColumns": [ + "patient_id", + "case_identifier", + "gender", + "sample__sample_count", + "observation__observation_count" + ] } } diff --git a/conformance/compiler/fixtures/observation-pivot-filter.json b/conformance/compiler/fixtures/observation-pivot-filter.json index 826b242..b10f308 100644 --- a/conformance/compiler/fixtures/observation-pivot-filter.json +++ b/conformance/compiler/fixtures/observation-pivot-filter.json @@ -2,30 +2,72 @@ "schema": "loom.compiler-oracle/v1", "id": "observation-pivot-filter", "description": "Observation-root pivot with a typed status filter and sparse value columns", - "tags": ["supported", "observation", "pivot", "filter"], + "tags": [ + "supported", + "observation", + "pivot", + "filter" + ], "limit": 100, - "builder": { - "Project": "oracle-project", - "RootResourceType": "Observation", - "Fields": [{"Name": "observation_id", "Select": "id"}], - "Filters": [{ - "fieldRef": "Observation.status", - "selector": "status", - "fieldKind": "STRING", - "operator": "EQUALS", - "values": [{"kind": "STRING", "string": "final"}] - }], - "Pivots": [{ - "Name": "lab_values", - "ColumnSelect": "code.coding[].display", - "ValueSelect": "valueQuantity.value", - "Columns": ["Hemoglobin", "Platelet Count", "White Blood Cell Count"] - }] + "project": "oracle-project", + "recipe": { + "recipeSchemaVersion": 1, + "name": "fixture_Observation", + "translationVersion": "fixture", + "outputs": [ + { + "name": "Observation", + "rootResourceType": "Observation", + "rowGrain": "observation", + "fields": [ + { + "name": "observation_id", + "expr": { + "select": "id" + } + } + ], + "filters": [ + { + "select": "status", + "fieldRef": "Observation.status", + "operator": "EQUALS", + "values": [ + { + "kind": "STRING", + "string": "final" + } + ] + } + ], + "pivots": [ + { + "name": "lab_values", + "columnExpr": { + "select": "code.coding[].display" + }, + "valueExpr": { + "select": "valueQuantity.value" + }, + "columns": [ + "Hemoglobin", + "Platelet Count", + "White Blood Cell Count" + ] + } + ] + } + ] }, "expected": { "supported": true, - "planProfile": "generic_fhir_graph", - "bindVars": {"project": "oracle-project", "limit": 100}, - "outputColumns": ["observation_id"] + "planProfile": "generic_fhir_graph_recipe", + "bindVars": { + "limit": 100, + "project": "oracle-project" + }, + "outputColumns": [ + "observation_id" + ] } } diff --git a/conformance/compiler/fixtures/patient-deep-filter.json b/conformance/compiler/fixtures/patient-deep-filter.json index 395d706..fff9fe1 100644 --- a/conformance/compiler/fixtures/patient-deep-filter.json +++ b/conformance/compiler/fixtures/patient-deep-filter.json @@ -2,36 +2,74 @@ "schema": "loom.compiler-oracle/v1", "id": "patient-deep-filter", "description": "Three-hop Patient to Specimen to DocumentReference traversal with a typed child filter", - "tags": ["supported", "patient", "deep", "filter", "nested"], + "tags": [ + "supported", + "patient", + "deep", + "filter", + "nested" + ], "limit": 50, - "builder": { - "Project": "oracle-project", - "RootResourceType": "Patient", - "Fields": [{"Name": "patient_id", "Select": "id"}], - "Traversals": [{ - "Label": "subject_Patient", - "ToResourceType": "Specimen", - "Alias": "specimen", - "Traversals": [{ - "Label": "subject_Specimen", - "ToResourceType": "DocumentReference", - "Alias": "file", - "Filters": [{ - "fieldRef": "DocumentReference.file_name", - "selector": "content[].attachment.title", - "fieldKind": "STRING", - "repeated": true, - "quantifier": "ANY", - "operator": "EXISTS" - }], - "Fields": [{"Name": "file_name", "Select": "content[].attachment.title"}] - }] - }] + "project": "oracle-project", + "recipe": { + "recipeSchemaVersion": 1, + "name": "fixture_Patient", + "translationVersion": "fixture", + "outputs": [ + { + "name": "Patient", + "rootResourceType": "Patient", + "rowGrain": "patient", + "fields": [ + { + "name": "patient_id", + "expr": { + "select": "id" + } + } + ], + "traversals": [ + { + "name": "subject_Patient", + "toResourceType": "Specimen", + "alias": "specimen", + "traversals": [ + { + "name": "subject_Specimen", + "toResourceType": "DocumentReference", + "alias": "file", + "fields": [ + { + "name": "file_name", + "expr": { + "select": "content[].attachment.title" + } + } + ], + "filters": [ + { + "select": "content[].attachment.title", + "fieldRef": "DocumentReference.file_name", + "operator": "EXISTS", + "quantifier": "ANY" + } + ] + } + ] + } + ] + } + ] }, "expected": { "supported": true, - "planProfile": "generic_fhir_graph", - "bindVars": {"project": "oracle-project", "limit": 50}, - "outputColumns": ["patient_id"] + "planProfile": "generic_fhir_graph_recipe", + "bindVars": { + "limit": 50, + "project": "oracle-project" + }, + "outputColumns": [ + "patient_id" + ] } } diff --git a/conformance/compiler/fixtures/patient-sibling-targets.json b/conformance/compiler/fixtures/patient-sibling-targets.json index 6812077..750e6d3 100644 --- a/conformance/compiler/fixtures/patient-sibling-targets.json +++ b/conformance/compiler/fixtures/patient-sibling-targets.json @@ -2,22 +2,82 @@ "schema": "loom.compiler-oracle/v1", "id": "patient-sibling-targets", "description": "Patient dataframe with sibling Condition, Specimen, and Observation routes sharing one edge label", - "tags": ["supported", "patient", "sibling", "sharing", "aggregate"], + "tags": [ + "supported", + "patient", + "sibling", + "sharing", + "aggregate" + ], "limit": 100, - "builder": { - "Project": "oracle-project", - "RootResourceType": "Patient", - "Fields": [{"Name": "patient_id", "Select": "id"}], - "Traversals": [ - {"Label": "subject_Patient", "ToResourceType": "Condition", "Alias": "condition", "Aggregates": [{"Name": "condition_count", "Operation": "COUNT"}]}, - {"Label": "subject_Patient", "ToResourceType": "Specimen", "Alias": "specimen", "Aggregates": [{"Name": "specimen_count", "Operation": "COUNT"}]}, - {"Label": "subject_Patient", "ToResourceType": "Observation", "Alias": "observation", "Aggregates": [{"Name": "observation_count", "Operation": "COUNT"}]} + "project": "oracle-project", + "recipe": { + "recipeSchemaVersion": 1, + "name": "fixture_Patient", + "translationVersion": "fixture", + "outputs": [ + { + "name": "Patient", + "rootResourceType": "Patient", + "rowGrain": "patient", + "fields": [ + { + "name": "patient_id", + "expr": { + "select": "id" + } + } + ], + "traversals": [ + { + "name": "subject_Patient", + "toResourceType": "Condition", + "alias": "condition", + "aggregates": [ + { + "name": "condition_count", + "operation": "COUNT" + } + ] + }, + { + "name": "subject_Patient", + "toResourceType": "Specimen", + "alias": "specimen", + "aggregates": [ + { + "name": "specimen_count", + "operation": "COUNT" + } + ] + }, + { + "name": "subject_Patient", + "toResourceType": "Observation", + "alias": "observation", + "aggregates": [ + { + "name": "observation_count", + "operation": "COUNT" + } + ] + } + ] + } ] }, "expected": { "supported": true, - "planProfile": "generic_fhir_graph", - "bindVars": {"project": "oracle-project", "limit": 100}, - "outputColumns": ["patient_id", "condition__condition_count", "specimen__specimen_count", "observation__observation_count"] + "planProfile": "generic_fhir_graph_recipe", + "bindVars": { + "limit": 100, + "project": "oracle-project" + }, + "outputColumns": [ + "patient_id", + "condition__condition_count", + "specimen__specimen_count", + "observation__observation_count" + ] } } diff --git a/conformance/compiler/fixtures/patient-temporal-filter.json b/conformance/compiler/fixtures/patient-temporal-filter.json index 6130381..67626ef 100644 --- a/conformance/compiler/fixtures/patient-temporal-filter.json +++ b/conformance/compiler/fixtures/patient-temporal-filter.json @@ -2,26 +2,63 @@ "schema": "loom.compiler-oracle/v1", "id": "patient-temporal-filter", "description": "Patient date filtering is type checked from generated FHIR metadata and normalized for ordered comparison", - "tags": ["supported", "patient", "filter", "date"], + "tags": [ + "supported", + "patient", + "filter", + "date" + ], "limit": 25, - "builder": { - "Project": "oracle-project", - "RootResourceType": "Patient", - "Fields": [{"Name": "birth_date", "Select": "birthDate"}], - "Filters": [{ - "fieldRef": "Patient.birth_date", - "selector": "birthDate", - "fieldKind": "DATE", - "operator": "GTE", - "values": [{"kind": "DATE", "date": "1980-01-01"}] - }] + "project": "oracle-project", + "recipe": { + "recipeSchemaVersion": 1, + "name": "fixture_Patient", + "translationVersion": "fixture", + "outputs": [ + { + "name": "Patient", + "rootResourceType": "Patient", + "rowGrain": "patient", + "fields": [ + { + "name": "birth_date", + "expr": { + "select": "birthDate" + } + } + ], + "filters": [ + { + "select": "birthDate", + "fieldRef": "Patient.birth_date", + "operator": "GTE", + "values": [ + { + "kind": "DATE", + "date": "1980-01-01" + } + ] + } + ] + } + ] }, "expected": { "supported": true, - "planProfile": "generic_fhir_graph", - "optimizerRules": ["push_down_typed_filters"], - "queryContains": ["DATE_TIMESTAMP(", "DATE_TIMESTAMP(@"], - "bindVars": {"project": "oracle-project", "limit": 25}, - "outputColumns": ["birth_date"] + "planProfile": "generic_fhir_graph_recipe", + "optimizerRules": [ + "push_down_typed_filters" + ], + "queryContains": [ + "DATE_TIMESTAMP(", + "DATE_TIMESTAMP(@" + ], + "bindVars": { + "limit": 25, + "project": "oracle-project" + }, + "outputColumns": [ + "birth_date" + ] } } diff --git a/conformance/compiler/fixtures/patient_observation_pivot.json b/conformance/compiler/fixtures/patient_observation_pivot.json index deafc1a..b3b37d1 100644 --- a/conformance/compiler/fixtures/patient_observation_pivot.json +++ b/conformance/compiler/fixtures/patient_observation_pivot.json @@ -2,28 +2,59 @@ "schema": "loom.compiler-oracle/v1", "id": "patient-observation-pivot", "description": "Patient-grain sparse Observation pivot", - "tags": ["supported", "patient", "observation", "pivot"], + "tags": [ + "supported", + "patient", + "observation", + "pivot" + ], "limit": 10, - "builder": { - "Project": "oracle-project", - "RootResourceType": "Patient", - "Traversals": [{ - "Label": "subject_Patient", - "ToResourceType": "Observation", - "Alias": "observation", - "Pivots": [{ - "Name": "lab_values", - "ColumnSelect": "code.coding[].display", - "ValueSelect": "valueQuantity.value", - "Columns": ["Tumor Purity", "Hemoglobin"] - }] - }] + "project": "oracle-project", + "recipe": { + "recipeSchemaVersion": 1, + "name": "fixture_Patient", + "translationVersion": "fixture", + "outputs": [ + { + "name": "Patient", + "rootResourceType": "Patient", + "rowGrain": "patient", + "traversals": [ + { + "name": "subject_Patient", + "toResourceType": "Observation", + "alias": "observation", + "pivots": [ + { + "name": "lab_values", + "columnExpr": { + "select": "code.coding[].display" + }, + "valueExpr": { + "select": "valueQuantity.value" + }, + "columns": [ + "Tumor Purity", + "Hemoglobin" + ] + } + ] + } + ] + } + ] }, "expected": { "supported": true, - "planProfile": "generic_fhir_graph", - "queryContains": ["MERGE(", "FILTER LENGTH(__pivot_values) > 0"], - "bindVars": {"project": "oracle-project", "limit": 10}, + "planProfile": "generic_fhir_graph_recipe", + "queryContains": [ + "FIRST(", + "FILTER LENGTH(__pivot_values) \u003e 0" + ], + "bindVars": { + "limit": 10, + "project": "oracle-project" + }, "expectedTraversalSets": 1 } } diff --git a/conformance/compiler/fixtures/patient_specimen_file.json b/conformance/compiler/fixtures/patient_specimen_file.json index 4ca181a..d2219eb 100644 --- a/conformance/compiler/fixtures/patient_specimen_file.json +++ b/conformance/compiler/fixtures/patient_specimen_file.json @@ -2,30 +2,75 @@ "schema": "loom.compiler-oracle/v1", "id": "patient-specimen-file", "description": "Patient-grain projection through specimen to DocumentReference", - "tags": ["supported", "patient", "specimen", "document-reference"], + "tags": [ + "supported", + "patient", + "specimen", + "document-reference" + ], "limit": 25, - "builder": { - "Project": "oracle-project", - "RootResourceType": "Patient", - "Fields": [{"Name": "gender", "Select": "gender"}], - "Traversals": [{ - "Label": "subject_Patient", - "ToResourceType": "Specimen", - "Alias": "specimen", - "Fields": [{"Name": "specimen_type", "Select": "type.coding[].display"}], - "Traversals": [{ - "Label": "subject_Specimen", - "ToResourceType": "DocumentReference", - "Alias": "file", - "Fields": [{"Name": "file_name", "Select": "content[].attachment.title"}] - }] - }] + "project": "oracle-project", + "recipe": { + "recipeSchemaVersion": 1, + "name": "fixture_Patient", + "translationVersion": "fixture", + "outputs": [ + { + "name": "Patient", + "rootResourceType": "Patient", + "rowGrain": "patient", + "fields": [ + { + "name": "gender", + "expr": { + "select": "gender" + } + } + ], + "traversals": [ + { + "name": "subject_Patient", + "toResourceType": "Specimen", + "alias": "specimen", + "fields": [ + { + "name": "specimen_type", + "expr": { + "select": "type.coding[].display" + } + } + ], + "traversals": [ + { + "name": "subject_Specimen", + "toResourceType": "DocumentReference", + "alias": "file", + "fields": [ + { + "name": "file_name", + "expr": { + "select": "content[].attachment.title" + } + } + ] + } + ] + } + ] + } + ] }, "expected": { "supported": true, - "planProfile": "generic_fhir_graph", - "queryContains": ["FOR root IN @@root_collection", "LIMIT @limit"], - "bindVars": {"project": "oracle-project", "limit": 25}, + "planProfile": "generic_fhir_graph_recipe", + "queryContains": [ + "FOR root IN @@root_collection", + "LIMIT @limit" + ], + "bindVars": { + "limit": 25, + "project": "oracle-project" + }, "expectedTraversalSets": 2 } } diff --git a/conformance/compiler/fixtures/research-subject-study-required.json b/conformance/compiler/fixtures/research-subject-study-required.json index b6f49c0..a769c8f 100644 --- a/conformance/compiler/fixtures/research-subject-study-required.json +++ b/conformance/compiler/fixtures/research-subject-study-required.json @@ -2,24 +2,60 @@ "schema": "loom.compiler-oracle/v1", "id": "research-subject-study-required", "description": "Required outbound ResearchSubject to ResearchStudy relationship", - "tags": ["supported", "research-subject", "outbound", "required"], + "tags": [ + "supported", + "research-subject", + "outbound", + "required" + ], "limit": 25, - "builder": { - "Project": "oracle-project", - "RootResourceType": "ResearchSubject", - "Fields": [{"Name": "status", "Select": "status"}], - "Traversals": [{ - "Label": "study", - "ToResourceType": "ResearchStudy", - "Alias": "study", - "MatchMode": "REQUIRED", - "Fields": [{"Name": "study_title", "Select": "title"}] - }] + "project": "oracle-project", + "recipe": { + "recipeSchemaVersion": 1, + "name": "fixture_ResearchSubject", + "translationVersion": "fixture", + "outputs": [ + { + "name": "ResearchSubject", + "rootResourceType": "ResearchSubject", + "rowGrain": "study_enrollment", + "fields": [ + { + "name": "status", + "expr": { + "select": "status" + } + } + ], + "traversals": [ + { + "name": "study", + "toResourceType": "ResearchStudy", + "alias": "study", + "matchMode": "REQUIRED", + "fields": [ + { + "name": "study_title", + "expr": { + "select": "title" + } + } + ] + } + ] + } + ] }, "expected": { "supported": true, - "planProfile": "generic_fhir_graph", - "bindVars": {"project": "oracle-project", "limit": 25}, - "outputColumns": ["status", "study__study_title"] + "planProfile": "generic_fhir_graph_recipe", + "bindVars": { + "limit": 25, + "project": "oracle-project" + }, + "outputColumns": [ + "status", + "study__study_title" + ] } } diff --git a/conformance/compiler/fixtures/specimen-aggregate-slice.json b/conformance/compiler/fixtures/specimen-aggregate-slice.json index 622d588..f6e3c32 100644 --- a/conformance/compiler/fixtures/specimen-aggregate-slice.json +++ b/conformance/compiler/fixtures/specimen-aggregate-slice.json @@ -2,30 +2,87 @@ "schema": "loom.compiler-oracle/v1", "id": "specimen-aggregate-slice", "description": "Specimen dataframe combining child count, distinct values, and a bounded representative file slice", - "tags": ["supported", "specimen", "aggregate", "slice", "reuse"], + "tags": [ + "supported", + "specimen", + "aggregate", + "slice", + "reuse" + ], "limit": 100, - "builder": { - "Project": "oracle-project", - "RootResourceType": "Specimen", - "Fields": [{"Name": "specimen_id", "Select": "id"}], - "Traversals": [{ - "Label": "subject_Specimen", - "ToResourceType": "DocumentReference", - "Alias": "file", - "Aggregates": [ - {"Name": "file_count", "Operation": "COUNT"}, - {"Name": "file_formats", "Operation": "DISTINCT_VALUES", "Select": "type.coding[].display", "ValueMode": "AUTO"} - ], - "Slices": [{"Name": "representative_files", "Limit": 3, "Fields": [ - {"Name": "file_id", "Select": "id"}, - {"Name": "file_name", "Select": "content[].attachment.title"} - ]}] - }] + "project": "oracle-project", + "recipe": { + "recipeSchemaVersion": 1, + "name": "fixture_Specimen", + "translationVersion": "fixture", + "outputs": [ + { + "name": "Specimen", + "rootResourceType": "Specimen", + "rowGrain": "specimen", + "fields": [ + { + "name": "specimen_id", + "expr": { + "select": "id" + } + } + ], + "traversals": [ + { + "name": "subject_Specimen", + "toResourceType": "DocumentReference", + "alias": "file", + "aggregates": [ + { + "name": "file_count", + "operation": "COUNT" + }, + { + "name": "file_formats", + "operation": "DISTINCT_VALUES", + "expr": { + "select": "type.coding[].display" + }, + "valueMode": "AUTO" + } + ], + "slices": [ + { + "name": "representative_files", + "limit": 3, + "fields": [ + { + "name": "file_id", + "expr": { + "select": "id" + } + }, + { + "name": "file_name", + "expr": { + "select": "content[].attachment.title" + } + } + ] + } + ] + } + ] + } + ] }, "expected": { "supported": true, - "planProfile": "generic_fhir_graph", - "bindVars": {"project": "oracle-project", "limit": 100}, - "outputColumns": ["specimen_id", "file__file_count", "file__file_formats"] + "planProfile": "generic_fhir_graph_recipe", + "bindVars": { + "limit": 100, + "project": "oracle-project" + }, + "outputColumns": [ + "specimen_id", + "file__file_count", + "file__file_formats" + ] } } diff --git a/conformance/compiler/fixtures/specimen-file-filter-aggregate.json b/conformance/compiler/fixtures/specimen-file-filter-aggregate.json index 0b52ae4..fbcdb7f 100644 --- a/conformance/compiler/fixtures/specimen-file-filter-aggregate.json +++ b/conformance/compiler/fixtures/specimen-file-filter-aggregate.json @@ -2,33 +2,65 @@ "schema": "loom.compiler-oracle/v1", "id": "specimen-file-filter-aggregate", "description": "Generic Specimen traversal pushes a child existence filter into the DocumentReference set before counting it", - "tags": ["supported", "specimen", "document-reference", "filter", "aggregate"], + "tags": [ + "supported", + "specimen", + "document-reference", + "filter", + "aggregate" + ], "limit": 25, - "builder": { - "Project": "oracle-project", - "RootResourceType": "Specimen", - "Traversals": [{ - "Label": "subject_Specimen", - "ToResourceType": "DocumentReference", - "Alias": "file", - "Filters": [{ - "fieldRef": "DocumentReference.file_name", - "selector": "content[].attachment.title", - "fieldKind": "STRING", - "repeated": true, - "quantifier": "ANY", - "operator": "EXISTS" - }], - "Aggregates": [{"Name": "file_count", "Operation": "COUNT"}] - }] + "project": "oracle-project", + "recipe": { + "recipeSchemaVersion": 1, + "name": "fixture_Specimen", + "translationVersion": "fixture", + "outputs": [ + { + "name": "Specimen", + "rootResourceType": "Specimen", + "rowGrain": "specimen", + "traversals": [ + { + "name": "subject_Specimen", + "toResourceType": "DocumentReference", + "alias": "file", + "filters": [ + { + "select": "content[].attachment.title", + "fieldRef": "DocumentReference.file_name", + "operator": "EXISTS", + "quantifier": "ANY" + } + ], + "aggregates": [ + { + "name": "file_count", + "operation": "COUNT" + } + ] + } + ] + } + ] }, "expected": { "supported": true, - "planProfile": "generic_fhir_graph", - "optimizerRules": ["push_down_typed_filters"], - "queryContains": ["LET child_set_1 = UNIQUE(", "FILTER LENGTH(("], - "bindVars": {"project": "oracle-project", "limit": 25}, - "outputColumns": ["file__file_count"], + "planProfile": "generic_fhir_graph_recipe", + "optimizerRules": [ + "push_down_typed_filters" + ], + "queryContains": [ + "LET child_set_1 = UNIQUE(", + "FILTER LENGTH((" + ], + "bindVars": { + "limit": 25, + "project": "oracle-project" + }, + "outputColumns": [ + "file__file_count" + ], "expectedTraversalSets": 1 } } diff --git a/conformance/compiler/fixtures/unsupported_simple_projection.json b/conformance/compiler/fixtures/unsupported_simple_projection.json index 80952ca..57bd582 100644 --- a/conformance/compiler/fixtures/unsupported_simple_projection.json +++ b/conformance/compiler/fixtures/unsupported_simple_projection.json @@ -2,18 +2,46 @@ "schema": "loom.compiler-oracle/v1", "id": "patient-simple-projection", "description": "Patient root-only projection uses the generic FHIR graph plan", - "tags": ["supported", "patient", "root-only"], + "tags": [ + "supported", + "patient", + "root-only" + ], "limit": 25, - "builder": { - "Project": "oracle-project", - "RootResourceType": "Patient", - "Fields": [{"Name": "gender", "Select": "gender"}] + "project": "oracle-project", + "recipe": { + "recipeSchemaVersion": 1, + "name": "fixture_Patient", + "translationVersion": "fixture", + "outputs": [ + { + "name": "Patient", + "rootResourceType": "Patient", + "rowGrain": "patient", + "fields": [ + { + "name": "gender", + "expr": { + "select": "gender" + } + } + ] + } + ] }, "expected": { "supported": true, - "planProfile": "generic_fhir_graph", - "queryContains": ["FOR root IN @@root_collection", "root.payload.gender"], - "bindVars": {"project": "oracle-project", "limit": 25}, - "outputColumns": ["gender"] + "planProfile": "generic_fhir_graph_recipe", + "queryContains": [ + "FOR root IN @@root_collection", + "root.payload.gender" + ], + "bindVars": { + "limit": 25, + "project": "oracle-project" + }, + "outputColumns": [ + "gender" + ] } } diff --git a/conformance/compiler/fixtures/unsupported_specimen_root.json b/conformance/compiler/fixtures/unsupported_specimen_root.json index 934a675..695d0b7 100644 --- a/conformance/compiler/fixtures/unsupported_specimen_root.json +++ b/conformance/compiler/fixtures/unsupported_specimen_root.json @@ -2,18 +2,45 @@ "schema": "loom.compiler-oracle/v1", "id": "specimen-root-projection", "description": "Specimen root-only projection uses the generic FHIR graph plan", - "tags": ["supported", "specimen", "root-only"], + "tags": [ + "supported", + "specimen", + "root-only" + ], "limit": 25, - "builder": { - "Project": "oracle-project", - "RootResourceType": "Specimen", - "Fields": [{"Name": "specimen_type", "Select": "type.coding[].display"}] + "project": "oracle-project", + "recipe": { + "recipeSchemaVersion": 1, + "name": "fixture_Specimen", + "translationVersion": "fixture", + "outputs": [ + { + "name": "Specimen", + "rootResourceType": "Specimen", + "rowGrain": "specimen", + "fields": [ + { + "name": "specimen_type", + "expr": { + "select": "type.coding[].display" + } + } + ] + } + ] }, "expected": { "supported": true, - "planProfile": "generic_fhir_graph", - "queryContains": ["FOR root IN @@root_collection"], - "bindVars": {"project": "oracle-project", "limit": 25}, - "outputColumns": ["specimen_type"] + "planProfile": "generic_fhir_graph_recipe", + "queryContains": [ + "FOR root IN @@root_collection" + ], + "bindVars": { + "limit": 25, + "project": "oracle-project" + }, + "outputColumns": [ + "specimen_type" + ] } } diff --git a/conformance/compiler/fixtures/unsupported_unknown_root.json b/conformance/compiler/fixtures/unsupported_unknown_root.json index 556da6d..50e16e6 100644 --- a/conformance/compiler/fixtures/unsupported_unknown_root.json +++ b/conformance/compiler/fixtures/unsupported_unknown_root.json @@ -2,12 +2,31 @@ "schema": "loom.compiler-oracle/v1", "id": "unsupported-unknown-root", "description": "Unknown roots fail before AQL collection interpolation", - "tags": ["unsupported", "safety"], + "tags": [ + "unsupported", + "safety" + ], "limit": 25, - "builder": { - "Project": "oracle-project", - "RootResourceType": "NotAResource", - "Fields": [{"Name": "anything", "Select": "anything"}] + "project": "oracle-project", + "recipe": { + "recipeSchemaVersion": 1, + "name": "fixture_NotAResource", + "translationVersion": "fixture", + "outputs": [ + { + "name": "NotAResource", + "rootResourceType": "NotAResource", + "rowGrain": "resource", + "fields": [ + { + "name": "anything", + "expr": { + "select": "anything" + } + } + ] + } + ] }, "expected": { "supported": false, diff --git a/conformance/compiler/gdc_fixture_test.go b/conformance/compiler/gdc_fixture_test.go index 9a7cadc..c740888 100644 --- a/conformance/compiler/gdc_fixture_test.go +++ b/conformance/compiler/gdc_fixture_test.go @@ -6,6 +6,7 @@ import ( "testing" "github.com/calypr/loom/internal/dataframe" + "github.com/calypr/loom/internal/dataframe/recipe" ) // TestGDCFixtureCoversRichShape keeps the checked-in benchmark representative @@ -27,13 +28,13 @@ func TestGDCFixtureCoversRichShape(t *testing.T) { if fixture.ID == "" { t.Fatal("gdc-case-matrix fixture is missing") } - if !hasField(fixture.Builder.Fields, "patient_id") || !hasField(fixture.Builder.Fields, "case_identifier") { + if !hasField(fixture.Recipe.Outputs[0].Fields, "patient_id") || !hasField(fixture.Recipe.Outputs[0].Fields, "case_identifier") { t.Fatal("GDC fixture must retain patient identity and case identifier fields") } var hasPivot, hasSlice, hasAggregate bool maxDepth := 0 - var walk func([]dataframe.TraversalStep, int) - walk = func(steps []dataframe.TraversalStep, depth int) { + var walk func([]recipe.Traversal, int) + walk = func(steps []recipe.Traversal, depth int) { if depth > maxDepth { maxDepth = depth } @@ -50,7 +51,7 @@ func TestGDCFixtureCoversRichShape(t *testing.T) { walk(step.Traversals, depth+1) } } - walk(fixture.Builder.Traversals, 1) + walk(fixture.Recipe.Outputs[0].Traversals, 1) if !hasPivot || !hasSlice || !hasAggregate { t.Fatalf("GDC fixture coverage pivot=%t slice=%t aggregate=%t", hasPivot, hasSlice, hasAggregate) } @@ -60,7 +61,7 @@ func TestGDCFixtureCoversRichShape(t *testing.T) { if !strings.Contains(fixture.Description, "nested") { t.Fatal("GDC fixture description should identify nested shaping") } - compiled, err := dataframe.CompileRequest(fixture.Builder, 1000) + compiled, err := compileRecipe(fixture.Recipe, fixture.Project, 1000, dataframe.DefaultPhysicalOptimizationPolicy()) if err != nil { t.Fatalf("compile rich GDC fixture: %v", err) } @@ -77,7 +78,7 @@ func TestGDCFixtureCoversRichShape(t *testing.T) { } } -func hasField(fields []dataframe.FieldSelect, name string) bool { +func hasField(fields []recipe.Field, name string) bool { for _, field := range fields { if field.Name == name { return true diff --git a/conformance/compiler/generated_semantics_coverage_test.go b/conformance/compiler/generated_semantics_coverage_test.go index c6bc32e..bec32a0 100644 --- a/conformance/compiler/generated_semantics_coverage_test.go +++ b/conformance/compiler/generated_semantics_coverage_test.go @@ -10,8 +10,8 @@ import ( "strings" "testing" - "github.com/calypr/loom/internal/dataframe" "github.com/calypr/loom/fhirschema" + "github.com/calypr/loom/internal/dataframe" ) func TestPublicCompilerAcceptsEveryGeneratedFHIRRoot(t *testing.T) { @@ -21,16 +21,13 @@ func TestPublicCompilerAcceptsEveryGeneratedFHIRRoot(t *testing.T) { } for _, resourceType := range resourceTypes { t.Run(resourceType, func(t *testing.T) { - compiled, err := dataframe.CompileRequest(dataframe.Builder{ - Project: "compiler-oracle", - RootResourceType: resourceType, - }, 1) + compiled, err := compileRecipe(rootRecipe(resourceType), "compiler-oracle", 1, dataframe.DefaultPhysicalOptimizationPolicy()) if err != nil { - t.Fatalf("CompileRequest(%s): %v", resourceType, err) + t.Fatalf("compile recipe root %s: %v", resourceType, err) } assertOnlyValidatedRootCollection(t, compiled, resourceType) - if compiled.PlanProfile != "generic_fhir_graph" { - t.Fatalf("plan profile = %q, want generic_fhir_graph", compiled.PlanProfile) + if compiled.PlanProfile != "generic_fhir_graph_recipe" { + t.Fatalf("plan profile = %q, want generic_fhir_graph_recipe", compiled.PlanProfile) } }) } @@ -44,21 +41,13 @@ func TestPublicCompilerAcceptsEveryGeneratedBuilderTraversal(t *testing.T) { for _, traversal := range traversals { name := traversal.FromType + "__" + traversal.EdgeLabel + "__" + traversal.ToType t.Run(name, func(t *testing.T) { - compiled, err := dataframe.CompileRequest(dataframe.Builder{ - Project: "compiler-oracle", - RootResourceType: traversal.FromType, - Traversals: []dataframe.TraversalStep{{ - Label: traversal.EdgeLabel, - ToResourceType: traversal.ToType, - Alias: "related", - }}, - }, 1) + compiled, err := compileRecipe(recipeWithTraversal(traversal.FromType, traversal.EdgeLabel, traversal.ToType), "compiler-oracle", 1, dataframe.DefaultPhysicalOptimizationPolicy()) if err != nil { - t.Fatalf("CompileRequest(%s -> %s via %s): %v", traversal.FromType, traversal.ToType, traversal.EdgeLabel, err) + t.Fatalf("compile recipe %s -> %s via %s: %v", traversal.FromType, traversal.ToType, traversal.EdgeLabel, err) } assertOnlyValidatedRootCollection(t, compiled, traversal.FromType) - if compiled.PlanProfile != "generic_fhir_graph" { - t.Fatalf("plan profile = %q, want generic_fhir_graph", compiled.PlanProfile) + if compiled.PlanProfile != "generic_fhir_graph_recipe" { + t.Fatalf("plan profile = %q, want generic_fhir_graph_recipe", compiled.PlanProfile) } if !bindVarsContain(compiled.BindVars, traversal.ToType) { t.Fatalf("target resource type %q is not represented as a bind value: %#v", traversal.ToType, compiled.BindVars) @@ -72,10 +61,7 @@ func TestPublicCompilerRejectsUnadvertisedRootBeforeRenderingAQL(t *testing.T) { if fhirschema.HasResource(malicious) { t.Fatal("test root unexpectedly advertised by schema") } - compiled, err := dataframe.CompileRequest(dataframe.Builder{ - Project: "compiler-oracle", - RootResourceType: malicious, - }, 1) + compiled, err := compileRecipe(rootRecipe(malicious), "compiler-oracle", 1, dataframe.DefaultPhysicalOptimizationPolicy()) if err == nil || !strings.Contains(err.Error(), "not represented by the active generated FHIR schema") { t.Fatalf("error = %v, want generated-schema rejection", err) } diff --git a/conformance/compiler/physical_plan_cost_test.go b/conformance/compiler/physical_plan_cost_test.go index 909466a..e73c878 100644 --- a/conformance/compiler/physical_plan_cost_test.go +++ b/conformance/compiler/physical_plan_cost_test.go @@ -22,9 +22,9 @@ func TestPhysicalPlanOptimizationCorpus(t *testing.T) { } fixture := fixture t.Run(fixture.ID, func(t *testing.T) { - compiled, err := dataframe.CompileRequest(fixture.Builder, fixture.Limit) + compiled, err := compileRecipe(fixture.Recipe, fixture.Project, fixture.Limit, dataframe.DefaultPhysicalOptimizationPolicy()) if err != nil { - t.Fatalf("CompileRequest() error = %v", err) + t.Fatalf("compile recipe error = %v", err) } diagnostics := compiled.PlanDiagnostics t.Logf("plan traversal_sets=%d shared=%d required_reuse=%d rich_sources=%d", diagnostics.TraversalSets, diagnostics.SharedTraversalCount, diagnostics.RequiredMatchReuseCount, len(diagnostics.RichSourceReuse)) diff --git a/conformance/compiler/policy_ablation_test.go b/conformance/compiler/policy_ablation_test.go index 70f97f6..02d4351 100644 --- a/conformance/compiler/policy_ablation_test.go +++ b/conformance/compiler/policy_ablation_test.go @@ -63,7 +63,6 @@ func TestGDCOptimizationPolicyAblationAgainstArango(t *testing.T) { if project == "" { project = "ARANGODB_PROTO" } - fixture.Builder.Project = project ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute) defer cancel() opts := arango.ConnectionOptions{URL: url, Database: database} @@ -72,7 +71,7 @@ func TestGDCOptimizationPolicyAblationAgainstArango(t *testing.T) { for _, candidate := range policies { candidate := candidate t.Run(candidate.name, func(t *testing.T) { - compiled, err := dataframe.CompileRequestWithPolicy(fixture.Builder, 1000, candidate.policy) + compiled, err := compileRecipe(fixture.Recipe, project, 1000, candidate.policy) if err != nil { t.Fatal(err) } @@ -159,7 +158,6 @@ func TestTraversalSharingAblationAgainstArango(t *testing.T) { if !ok { t.Fatalf("fixture %q is missing", fixtureID) } - fixture.Builder.Project = project t.Run(fixtureID, func(t *testing.T) { policies := []struct { name string @@ -170,7 +168,7 @@ func TestTraversalSharingAblationAgainstArango(t *testing.T) { } var expectedHash string for _, candidate := range policies { - compiled, err := dataframe.CompileRequestWithPolicy(fixture.Builder, fixture.Limit, candidate.policy) + compiled, err := compileRecipe(fixture.Recipe, project, fixture.Limit, candidate.policy) if err != nil { t.Fatal(err) } @@ -270,7 +268,6 @@ func TestPreparedSelectorAblationAgainstArango(t *testing.T) { if !ok { t.Fatalf("fixture %q is missing", fixtureID) } - fixture.Builder.Project = project t.Run(fixtureID, func(t *testing.T) { policies := []struct { name string @@ -281,7 +278,7 @@ func TestPreparedSelectorAblationAgainstArango(t *testing.T) { } var expectedHash string for _, candidate := range policies { - compiled, err := dataframe.CompileRequestWithPolicy(fixture.Builder, fixture.Limit, candidate.policy) + compiled, err := compileRecipe(fixture.Recipe, project, fixture.Limit, candidate.policy) if err != nil { t.Fatal(err) } @@ -380,10 +377,9 @@ func TestRichConsumerReuseProfileAgainstArango(t *testing.T) { if !ok { t.Fatalf("fixture %q is missing", fixtureID) } - fixture.Builder.Project = project t.Run(fixtureID, func(t *testing.T) { policy := dataframe.DefaultPhysicalOptimizationPolicy().WithRule(dataframe.PhysicalOptimizationRulePreparedSelectors, false) - compiled, err := dataframe.CompileRequestWithPolicy(fixture.Builder, fixture.Limit, policy) + compiled, err := compileRecipe(fixture.Recipe, project, fixture.Limit, policy) if err != nil { t.Fatal(err) } @@ -472,7 +468,6 @@ func TestCompactSetProjectionAblationAgainstArango(t *testing.T) { if !ok { t.Fatalf("fixture %q is missing", fixtureID) } - fixture.Builder.Project = project t.Run(fixtureID, func(t *testing.T) { limit := fixture.Limit if fixtureID == "gdc-case-matrix" { @@ -488,7 +483,7 @@ func TestCompactSetProjectionAblationAgainstArango(t *testing.T) { } var expectedHash string for _, candidate := range policies { - compiled, err := dataframe.CompileRequestWithPolicy(fixture.Builder, limit, candidate.policy) + compiled, err := compileRecipe(fixture.Recipe, project, limit, candidate.policy) if err != nil { t.Fatal(err) } diff --git a/conformance/compiler/run.go b/conformance/compiler/run.go deleted file mode 100644 index 82e42dd..0000000 --- a/conformance/compiler/run.go +++ /dev/null @@ -1,72 +0,0 @@ -package compilerfixture - -import ( - "fmt" - "slices" - "strings" - - "github.com/calypr/loom/internal/dataframe" -) - -// Result is the pure-compiler outcome for one oracle fixture. It intentionally -// stops before execution; result-row parity against META belongs to the Arango -// integration corpus. -type Result struct { - FixtureID string - Compiled dataframe.CompiledQuery - Err error -} - -func Run(fixture Fixture) Result { - // Conformance exercises the production compiler entrypoint directly. - compiled, err := dataframe.CompileRequest(fixture.Builder, fixture.Limit) - return Result{FixtureID: fixture.ID, Compiled: compiled, Err: err} -} - -func Verify(fixture Fixture) error { - result := Run(fixture) - if !fixture.Expected.Supported { - if result.Err == nil { - return fmt.Errorf("expected compiler failure containing %q, got success", fixture.Expected.ErrorContains) - } - if !strings.Contains(result.Err.Error(), fixture.Expected.ErrorContains) { - return fmt.Errorf("compiler error %q does not contain %q", result.Err, fixture.Expected.ErrorContains) - } - return nil - } - if result.Err != nil { - return result.Err - } - if profile := fixture.Expected.PlanProfile; profile != "" && result.Compiled.PlanProfile != profile { - return fmt.Errorf("plan profile = %q, want %q", result.Compiled.PlanProfile, profile) - } - for _, want := range fixture.Expected.OptimizerRules { - if !slices.Contains(result.Compiled.OptimizationRules, want) { - return fmt.Errorf("compiled optimizer rules %v do not contain %q", result.Compiled.OptimizationRules, want) - } - } - for _, fragment := range fixture.Expected.QueryContains { - if !strings.Contains(result.Compiled.Query, fragment) { - return fmt.Errorf("compiled query does not contain %q", fragment) - } - } - for _, fragment := range fixture.Expected.QueryNotContains { - if strings.Contains(result.Compiled.Query, fragment) { - return fmt.Errorf("compiled query unexpectedly contains %q", fragment) - } - } - for key, want := range fixture.Expected.BindVars { - if got, ok := result.Compiled.BindVars[key]; !ok || fmt.Sprint(got) != fmt.Sprint(want) { - return fmt.Errorf("bind variable %q = %#v, want %#v", key, got, want) - } - } - for _, want := range fixture.Expected.OutputColumns { - if !slices.Contains(result.Compiled.Columns, want) { - return fmt.Errorf("compiled output columns %v do not contain %q", result.Compiled.Columns, want) - } - } - if fixture.Expected.ExpectedTraversalSets != nil && result.Compiled.PlanDiagnostics.TraversalSets != *fixture.Expected.ExpectedTraversalSets { - return fmt.Errorf("physical traversal sets = %d, want %d", result.Compiled.PlanDiagnostics.TraversalSets, *fixture.Expected.ExpectedTraversalSets) - } - return nil -} diff --git a/conformance/compiler/run_test.go b/conformance/compiler/run_test.go index e8c2a79..86dc055 100644 --- a/conformance/compiler/run_test.go +++ b/conformance/compiler/run_test.go @@ -1,10 +1,104 @@ package compilerfixture import ( + "fmt" "path/filepath" + "slices" + "strings" "testing" + + "github.com/calypr/loom/internal/dataframe" + "github.com/calypr/loom/internal/dataframe/recipe" + "github.com/calypr/loom/internal/dataframe/semantic" ) +// Result is the pure-compiler outcome for one oracle fixture. It intentionally +// stops before execution; result-row parity against META belongs to the Arango +// integration corpus. +type Result struct { + FixtureID string + Compiled dataframe.CompiledQuery + Err error +} + +func Run(fixture Fixture) Result { + compiled, err := compileRecipe(fixture.Recipe, fixture.Project, fixture.Limit, dataframe.DefaultPhysicalOptimizationPolicy()) + return Result{FixtureID: fixture.ID, Compiled: compiled, Err: err} +} + +func compileRecipe(bundle recipe.Bundle, project string, limit int, policy dataframe.PhysicalOptimizationPolicy) (dataframe.CompiledQuery, error) { + bindings := recipe.RuntimeBindings{Project: project} + plan, err := semantic.BuildRecipePlan(bundle, bindings) + if err != nil { + return dataframe.CompiledQuery{}, err + } + resolved, err := semantic.ResolveRecipePlan(plan, "conformance", "") + if err != nil { + return dataframe.CompiledQuery{}, err + } + queries, err := dataframe.CompileResolvedRecipePlanWithPolicy(resolved, limit, policy) + if err != nil { + return dataframe.CompiledQuery{}, err + } + if len(queries) != 1 { + return dataframe.CompiledQuery{}, fmt.Errorf("recipe outputs = %d, want one", len(queries)) + } + return queries[0], nil +} + +func recipeWithTraversal(from, label, to string) recipe.Bundle { + grain, _ := dataframe.InferRowGrain(from) + return recipe.Bundle{RecipeSchemaVersion: recipe.CurrentSchemaVersion, Name: "traversal", TranslationVersion: "conformance", Outputs: []recipe.Output{{Name: from, RootResourceType: from, RowGrain: string(grain), Traversals: []recipe.Traversal{{Name: label, ToResourceType: to, Alias: "related"}}}}} +} + +func Verify(fixture Fixture) error { + result := Run(fixture) + if !fixture.Expected.Supported { + if result.Err == nil { + return fmt.Errorf("expected compiler failure containing %q, got success", fixture.Expected.ErrorContains) + } + if !strings.Contains(result.Err.Error(), fixture.Expected.ErrorContains) { + return fmt.Errorf("compiler error %q does not contain %q", result.Err, fixture.Expected.ErrorContains) + } + return nil + } + if result.Err != nil { + return result.Err + } + if profile := fixture.Expected.PlanProfile; profile != "" && result.Compiled.PlanProfile != profile { + return fmt.Errorf("plan profile = %q, want %q", result.Compiled.PlanProfile, profile) + } + for _, want := range fixture.Expected.OptimizerRules { + if !slices.Contains(result.Compiled.OptimizationRules, want) { + return fmt.Errorf("compiled optimizer rules %v do not contain %q", result.Compiled.OptimizationRules, want) + } + } + for _, fragment := range fixture.Expected.QueryContains { + if !strings.Contains(result.Compiled.Query, fragment) { + return fmt.Errorf("compiled query does not contain %q", fragment) + } + } + for _, fragment := range fixture.Expected.QueryNotContains { + if strings.Contains(result.Compiled.Query, fragment) { + return fmt.Errorf("compiled query unexpectedly contains %q", fragment) + } + } + for key, want := range fixture.Expected.BindVars { + if got, ok := result.Compiled.BindVars[key]; !ok || fmt.Sprint(got) != fmt.Sprint(want) { + return fmt.Errorf("bind variable %q = %#v, want %#v", key, got, want) + } + } + for _, want := range fixture.Expected.OutputColumns { + if !slices.Contains(result.Compiled.Columns, want) { + return fmt.Errorf("compiled output columns %v do not contain %q", result.Compiled.Columns, want) + } + } + if fixture.Expected.ExpectedTraversalSets != nil && result.Compiled.PlanDiagnostics.TraversalSets != *fixture.Expected.ExpectedTraversalSets { + return fmt.Errorf("physical traversal sets = %d, want %d", result.Compiled.PlanDiagnostics.TraversalSets, *fixture.Expected.ExpectedTraversalSets) + } + return nil +} + func TestCompilerOracleFixtures(t *testing.T) { fixtures, err := LoadDir(filepath.Join("fixtures")) if err != nil { diff --git a/conformance/compiler/schema_roots_test.go b/conformance/compiler/schema_roots_test.go index 773fd1e..6ae96e7 100644 --- a/conformance/compiler/schema_roots_test.go +++ b/conformance/compiler/schema_roots_test.go @@ -16,12 +16,9 @@ func TestPublicCompilerAcceptsExpandedSchemaRootExamples(t *testing.T) { "Task", } { t.Run(resourceType, func(t *testing.T) { - compiled, err := dataframe.CompileRequest(dataframe.Builder{ - Project: "compiler-oracle", - RootResourceType: resourceType, - }, 1) + compiled, err := compileRecipe(rootRecipe(resourceType), "compiler-oracle", 1, dataframe.DefaultPhysicalOptimizationPolicy()) if err != nil { - t.Fatalf("CompileRequest(%q): %v", resourceType, err) + t.Fatalf("compile recipe root %q: %v", resourceType, err) } assertOnlyValidatedRootCollection(t, compiled, resourceType) }) @@ -31,15 +28,12 @@ func TestPublicCompilerAcceptsExpandedSchemaRootExamples(t *testing.T) { func TestPublicCompilerRejectsSchemaBackboneAndAbstractRoots(t *testing.T) { for _, resourceType := range []string{"Address", "PatientContact", "Resource"} { t.Run(resourceType, func(t *testing.T) { - compiled, err := dataframe.CompileRequest(dataframe.Builder{ - Project: "compiler-oracle", - RootResourceType: resourceType, - }, 1) + compiled, err := compileRecipe(rootRecipe(resourceType), "compiler-oracle", 1, dataframe.DefaultPhysicalOptimizationPolicy()) if err == nil || !strings.Contains(err.Error(), "not represented by the active generated FHIR schema") { - t.Fatalf("CompileRequest(%q) error = %v, want generated-schema rejection", resourceType, err) + t.Fatalf("compile recipe root %q error = %v, want generated-schema rejection", resourceType, err) } if compiled.Query != "" { - t.Fatalf("CompileRequest(%q) rendered AQL for a rejected root:\n%s", resourceType, compiled.Query) + t.Fatalf("compile recipe root %q rendered AQL for a rejected root:\n%s", resourceType, compiled.Query) } }) } diff --git a/conformance/compiler/test_helpers_test.go b/conformance/compiler/test_helpers_test.go deleted file mode 100644 index 7a83ba3..0000000 --- a/conformance/compiler/test_helpers_test.go +++ /dev/null @@ -1,7 +0,0 @@ -package compilerfixture - -import "github.com/calypr/loom/internal/dataframe" - -func validBuilder() dataframe.Builder { - return dataframe.Builder{Project: "p", RootResourceType: "Patient"} -} diff --git a/conformance/compiler/types.go b/conformance/compiler/types.go index 3897468..e2139a7 100644 --- a/conformance/compiler/types.go +++ b/conformance/compiler/types.go @@ -1,4 +1,4 @@ -// Package compilerfixture loads the machine-readable compiler oracle corpus. +// Package compilerfixture loads the canonical recipe compiler oracle corpus. package compilerfixture import ( @@ -10,33 +10,33 @@ import ( "sort" "strings" - "github.com/calypr/loom/internal/dataframe" + "github.com/calypr/loom/internal/dataframe/recipe" ) const SchemaVersion = "loom.compiler-oracle/v1" type Expected struct { - Supported bool `json:"supported"` - PlanProfile string `json:"planProfile,omitempty"` - OptimizerRules []string `json:"optimizerRules,omitempty"` - ErrorContains string `json:"errorContains,omitempty"` - QueryContains []string `json:"queryContains,omitempty"` - QueryNotContains []string `json:"queryNotContains,omitempty"` - BindVars map[string]any `json:"bindVars,omitempty"` - OutputColumns []string `json:"outputColumns,omitempty"` - // ExpectedTraversalSets describes the physical traversal count. - ExpectedTraversalSets *int `json:"expectedTraversalSets,omitempty"` + Supported bool `json:"supported"` + PlanProfile string `json:"planProfile,omitempty"` + OptimizerRules []string `json:"optimizerRules,omitempty"` + ErrorContains string `json:"errorContains,omitempty"` + QueryContains []string `json:"queryContains,omitempty"` + QueryNotContains []string `json:"queryNotContains,omitempty"` + BindVars map[string]any `json:"bindVars,omitempty"` + OutputColumns []string `json:"outputColumns,omitempty"` + ExpectedTraversalSets *int `json:"expectedTraversalSets,omitempty"` } type Fixture struct { - Schema string `json:"schema"` - ID string `json:"id"` - Description string `json:"description"` - Tags []string `json:"tags,omitempty"` - Limit int `json:"limit"` - Builder dataframe.Builder `json:"builder"` - Expected Expected `json:"expected"` - SourceFile string `json:"-"` + Schema string `json:"schema"` + ID string `json:"id"` + Description string `json:"description"` + Tags []string `json:"tags,omitempty"` + Limit int `json:"limit"` + Project string `json:"project"` + Recipe recipe.Bundle `json:"recipe"` + Expected Expected `json:"expected"` + SourceFile string `json:"-"` } func LoadDir(dir string) ([]Fixture, error) { @@ -44,7 +44,7 @@ func LoadDir(dir string) ([]Fixture, error) { if err != nil { return nil, err } - var fixtures []Fixture + fixtures := make([]Fixture, 0) seen := map[string]string{} for _, entry := range entries { if entry.IsDir() || filepath.Ext(entry.Name()) != ".json" { @@ -91,8 +91,14 @@ func (f Fixture) Validate() error { if f.Limit <= 0 { return errors.New("limit must be positive") } - if strings.TrimSpace(f.Builder.Project) == "" || strings.TrimSpace(f.Builder.RootResourceType) == "" { - return errors.New("builder project and rootResourceType are required") + if strings.TrimSpace(f.Project) == "" { + return errors.New("project is required") + } + if err := f.Recipe.Validate(); err != nil { + return fmt.Errorf("recipe: %w", err) + } + if len(f.Recipe.Outputs) != 1 { + return errors.New("recipe must contain exactly one output") } if f.Expected.Supported { if f.Expected.ErrorContains != "" { diff --git a/conformance/compiler/types_test.go b/conformance/compiler/types_test.go index b624941..6b4d2a5 100644 --- a/conformance/compiler/types_test.go +++ b/conformance/compiler/types_test.go @@ -45,7 +45,7 @@ func TestFixtureValidationRejectsContradictoryExpectation(t *testing.T) { ID: "contradictory", Description: "invalid fixture", Limit: 1, - Builder: validBuilder(), + Recipe: rootRecipe("Patient"), Expected: Expected{ Supported: true, ErrorContains: "failure", diff --git a/docs/CLICKHOUSE_GRAPHQL_READER_EXECUTION_PLAN.md b/docs/CLICKHOUSE_GRAPHQL_READER_EXECUTION_PLAN.md new file mode 100644 index 0000000..c837534 --- /dev/null +++ b/docs/CLICKHOUSE_GRAPHQL_READER_EXECUTION_PLAN.md @@ -0,0 +1,540 @@ +# ClickHouse GraphQL Reader Execution Plan + +## Status and decision + +This document defines the implementation plan for Loom's published-data read +surface. The selected architecture is a stable GraphQL control plane over a +dynamic ClickHouse data plane. + +GraphQL will not generate one object type or query field per dataframe, +ClickHouse table, or column. Dataset and column metadata will instead be +discovered at runtime, while row and aggregate values will be returned through +the existing `JSON` scalar. Publishing a new dataframe output or adding a +column must not require regenerating GraphQL code or restarting Loom. + +Every registered, READY Loom publication output is eligible for discovery and +query. Arbitrary ClickHouse tables that were not published and registered by +Loom are outside this API. + +This is a ClickHouse-only publication and reader API. Loom does not expose an +Elasticsearch/OpenSearch publication target, reader implementation, fallback, +or backend selector. Guppy and Elasticsearch are legacy systems outside the +Loom runtime boundary. Migration is a hard deployment cutover after offline +parity validation, not a permanent dual-read or runtime-switch architecture. + +### Current implementation status + +The first Loom backend vertical slice implemented project/generation-scoped +outputs and logical aliases. The current reader revision exposes an +authorized multi-project federation for each `dataType`; public reader inputs +do not contain project or generation selectors. Columns and a federation +revision are discovered from publication metadata, and GraphQL exposes +dataset, row, count, filter, sort, cursor, and aggregate reads. ClickHouse +filter and cursor values are driver-bound; query-controlled identifiers are +validated against the published schema. Elasticsearch publication and reader +code has been removed. + +The frontend compatibility adapter, full facet/histogram parity, streaming +exports, real-ClickHouse integration fixtures, and deployment cutover evidence +remain Phase 6-8 work. A materialization-ID query remains only for internal +compatibility; the Explorer reader path is principal-scoped and `dataType` +based. + +The detailed cross-repository execution plan for those remaining reader and +Explorer items is [EXPLORER_LOOM_SLICE_PARITY_PLAN.md](EXPLORER_LOOM_SLICE_PARITY_PLAN.md). + +## Goals + +- Read every registered Loom publication output through one stable GraphQL API. +- Discover new datasets and columns without a server restart or schema rebuild. +- Federate all current project outputs visible to the authenticated principal. +- Preserve project, per-project dataset-generation, and row authorization + boundaries inside that federation. +- Support the Explorer capabilities currently supplied by Guppy: + - schema and field discovery; + - filtered and sorted rows; + - total counts; + - facet buckets and statistics; + - stable pagination; + - download-oriented streaming in a later transport-specific endpoint. +- Keep Gecko's current Explorer configuration shape during migration. +- Treat `guppyConfig.dataType` as a logical dataset alias rather than a physical + legacy index or ClickHouse table name. +- Include canonical `auth_resource_path` in every published row. +- Return explicit column metadata alongside permissive JSON row values. +- Make publication commit the visibility boundary for readers. + +## Non-goals + +- Exposing arbitrary SQL or arbitrary ClickHouse tables through GraphQL. +- Recreating Guppy's dynamically generated GraphQL object types. +- Making a caller-supplied row filter an authorization boundary. +- Preserving Guppy's offset-pagination limit or legacy backend-specific query + representation. +- Supporting a pluggable publication/read backend or Elasticsearch fallback. +- Requiring recipes or Gecko config to declare the reserved + `auth_resource_path` system column manually. +- Moving Explorer configuration ownership out of Gecko in this work package. + +## Architecture + +```mermaid +flowchart LR + Gecko["Gecko Explorer config"] --> Frontend["Explorer data adapter"] + Frontend --> GraphQL["Stable Loom GraphQL API"] + GraphQL --> Resolver["Principal project and path resolver"] + Resolver --> Catalog["Authorized READY project outputs"] + Catalog --> Pointer["Per-project generation pointers"] + Pointer --> Output["Federated output metadata"] + GraphQL --> Reader["Federated typed reader"] + Reader --> Output + Reader --> ClickHouse["UNION ALL registered tables"] +``` + +The GraphQL schema describes operations and metadata envelopes. It does not +describe the columns of an individual dataframe. The dataset catalog resolves +one logical `dataType` to every authorized current READY project output, and +the reader only builds SQL from catalog-validated identifiers and supported +operators. + +## Dataset identity and visibility + +The physical publication identity is: + +```text +project + dataset generation + dataset alias +``` + +The public logical identity is: + +```text +authenticated principal + dataset alias +``` + +The catalog must retain the full publication identity: + +```text +project +dataset generation +recipe name and digest +resolved schema digest +authorization scope mode and paths +reserved auth_resource_path column +output name +logical dataset alias +physical ClickHouse table +columns and ClickHouse types +row count +publication state and timestamps +``` + +`dataset alias` is the frontend-facing name, initially matching the existing +Explorer `guppyConfig.dataType` values such as `observation` or `file`. The alias +must be explicit publication metadata. The reader must not infer it by parsing +physical table names or applying ad hoc naming conventions. + +The current bundle pointer is keyed only by bundle name. Replace this with a +canonical, collision-safe pointer key containing project, generation, and +dataset identity. Two projects publishing the same recipe or output name must +never advance each other's visible pointer. + +Publication becomes visible only after all outputs are READY and the catalog +pointer advances atomically. Readers resolve the pointer once per operation and +never scan for the newest table by timestamp. + +For public reads, Loom enumerates the principal's authorized projects, resolves +each project's active READY generation and alias pointer, reconciles their +schemas, and queries the resulting source set as one federation. The browser +does not provide a project boundary or project filter. + +## GraphQL contract + +The final names may be adjusted during schema review, but the operations and +separation of concerns are required. + +```graphql +extend type Query { + dataframeDatasets: [DataframeDataset!]! + dataframeDataset(dataType: String!): DataframeDataset + dataframeRows(input: DataframeRowsInput!): DataframeRowConnection! + dataframeAggregate(input: DataframeAggregateInput!): DataframeAggregateResult! +} + +type DataframeDataset { + dataType: String! + revision: String! + columns: [DataframeColumn!]! + rowCount: Int +} + +type DataframeColumn { + name: String! + clickhouseType: String! + logicalType: String! + nullable: Boolean! + repeated: Boolean! + filterable: Boolean! + sortable: Boolean! + aggregatable: Boolean! +} + +input DataframeRowsInput { + dataType: String! + columns: [String!] + filters: DataframeFilterExpression + sort: [DataframeSortInput!] + first: Int = 100 + after: String +} + +type DataframeRowConnection { + dataset: DataframeDataset! + columns: [DataframeColumn!]! + rows: JSON! + totalCount: Int + pageInfo: DataframePageInfo! +} +``` + +The current materialization-ID and project-required operations may remain +temporarily for internal compatibility, but Explorer uses only `dataType`. +Physical IDs, project selectors, and table names must not appear in Gecko +configuration. + +### Filter contract + +Replace the flat `EQ`/`CONTAINS` list with a recursive expression tree: + +```graphql +input DataframeFilterExpression { + and: [DataframeFilterExpression!] + or: [DataframeFilterExpression!] + not: DataframeFilterExpression + predicate: DataframeFilterPredicate +} + +input DataframeFilterPredicate { + column: String! + op: DataframeFilterOperator! + value: JSON +} + +enum DataframeFilterOperator { + EQ + NEQ + IN + NOT_IN + LT + LTE + GT + GTE + CONTAINS + STARTS_WITH + EXISTS + IS_NULL + ARRAY_CONTAINS + ARRAY_OVERLAPS +} +``` + +Each operator must declare its valid logical and ClickHouse types. Unsupported +operator/type combinations fail validation before SQL execution. All values +must be passed as ClickHouse parameters; values may never be interpolated into +SQL text. + +### Aggregation contract + +The aggregate operation must cover the Explorer use cases without copying +Guppy's nested GraphQL response shape into the canonical reader: + +- exact total count; +- value count and distinct count; +- terms/facet buckets with missing count and configurable limit; +- numeric/date histogram; +- minimum, maximum, sum, average, and basic statistics; +- optional group-by columns; +- the same filter-expression contract used by row reads; +- `filterSelf` behavior implemented by the frontend compatibility adapter. + +Canonical aggregate results should be normalized rows plus typed column +metadata. The frontend adapter may reshape them into the current Guppy bucket +objects during migration. + +## Runtime schema discovery + +The publication catalog is the primary schema authority. A successful +publication already knows its output names and declared ClickHouse column +types, so reads should not require a `system.columns` lookup on every request. + +ClickHouse introspection is a verification and recovery mechanism: + +- verify a registered table and its declared columns during startup recovery; +- diagnose catalog/table drift; +- rebuild missing derived column capabilities when explicitly requested; +- never discover and expose an unregistered physical table automatically. + +Dataset metadata may be cached by logical identity. Cache entries must be +invalidated when the corresponding publication pointer advances. A short TTL +is a fallback, not the primary freshness mechanism. New publications must be +readable immediately without restart. + +## ClickHouse type policy + +The first implementation must define and test a capability matrix rather than +silently coercing every value to a string. + +Initial required families: + +- signed and unsigned integers; +- floating-point values; +- boolean values; +- strings; +- `Date`, `Date32`, `DateTime`, and `DateTime64`; +- `Nullable(T)` for every supported scalar; +- `Array(T)` for supported scalar elements. + +Follow-up families may include Decimal, UUID, Enum, LowCardinality, Map, +Tuple, and ClickHouse JSON/Object types. Adding one of these types changes the +runtime codec and capability matrix, not the GraphQL schema. + +Unknown registered types must remain visible in dataset metadata. Row reads +may return them when the ClickHouse JSON projection can encode them safely; +otherwise the API returns an explicit unsupported-type error naming the column +and type. A new type must never require a Loom restart after its codec is +already present in the running version. + +## Authorization and tenant boundaries + +Authorization has two distinct layers: + +1. Source selection: derive the authorized project set from the principal, + resolve each project's active generation, and include only current READY + outputs for the requested `dataType`. +2. Row scope: apply a server-derived `auth_resource_path` predicate inside + every project union branch for restricted principals. + +A caller-supplied `auth_resource_path` filter may narrow a query, but is not an +authorization check. Physical table names, catalog lookups, dataset listings, +and error messages must not reveal unauthorized projects or outputs. + +The alias resolver performs an authorized `dataType -> project outputs` lookup. +There is no unauthenticated global alias-to-table lookup, and browser filters +cannot add a project or auth path to the source set. + +## Package ownership + +The implementation should preserve the current dataframe package boundaries: + +| Responsibility | Owner | +| --- | --- | +| Publication identity, output records, and pointer semantics | `internal/dataframe/materialization` | +| Arango-backed publication catalog | `internal/dataframe/materialization/arango` | +| Logical dataset alias resolution | new cohesive package under `internal/dataframe/materialization` | +| Safe ClickHouse row and aggregate query construction | `internal/dataframe/materialization` | +| ClickHouse execution and JSON decoding | `internal/store/clickhouse` | +| GraphQL authorization and transport mapping | `graphqlapi/materialization` | +| GraphQL schema and resolvers | `graphqlapi` | +| Guppy response compatibility | frontend data adapter, not canonical Loom reader | + +Do not add Explorer configuration parsing to the compiler. The compiler owns +dataframe meaning and publication; the reader resolves already-published +outputs. Gecko remains the configuration owner. + +## Execution phases + +### Phase 1: Contract and parity fixtures + +1. Inventory the legacy Guppy operations used by Explorer and reports. +2. Capture representative requests and normalized expected results for rows, + filters, sorting, counts, terms facets, numeric facets, details panels, and + downloads. +3. Define the canonical Loom filter, sort, aggregate, error, and pagination + contracts. +4. Decide the initial ClickHouse type capability matrix. + +Exit criteria: + +- Every required frontend behavior maps to a canonical Loom operation or an + explicitly deferred compatibility item. +- Contract tests exist independently of a running ClickHouse instance. + +### Phase 2: Unify publication and read metadata + +1. Remove the split between legacy `Materialization` records used by the reader + and `BundleExecution`/`BundleOutputRecord` records used by publication. +2. Make each READY bundle output directly resolvable as a published dataset. +3. Add explicit dataset aliases to publication metadata. +4. Namespace pointers by project, generation, and dataset identity. +5. Inject the reserved `auth_resource_path` column into every published + output. Keep project identity in publication/catalog metadata, not rows. +6. Add catalog operations to resolve every authorized current READY project + output for one alias. +7. Add schema reconciliation and a federation revision over the source set. +8. Add migration/recovery handling for existing records where practical. + +Exit criteria: + +- Publishing a bundle makes every output readable through the same catalog + transaction without writing a parallel registry record. +- Identical recipe/output names in different projects cannot collide. +- Failed or superseded outputs are not reader-visible. +- One public alias resolves to all and only principal-authorized project + outputs. + +### Phase 3: Canonical typed reader + +1. Replace materialization-ID and project-required lookup with + principal-scoped `dataType` federation at the public service boundary. +2. Validate selected columns, filters, sort columns, and aggregate columns + against registered metadata. +3. Introduce parameterized ClickHouse predicates for the filter expression. +4. Support deterministic multi-column keyset pagination with an internal row + identity and project/source tie-breaker. +5. Return runtime column metadata and JSON rows. +6. Add exact count support without coupling it to every row request when the + caller does not need it. +7. Add configurable page, query-complexity, bucket, and execution-time limits. + +Exit criteria: + +- No query-controlled identifier or value reaches SQL without catalog + validation or parameter binding. +- Pagination neither duplicates nor skips rows across equal sort values. +- Reads work for every type in the initial capability matrix. + +### Phase 4: Discovery and aggregations + +1. Add dataset listing and single-dataset schema discovery. +2. Derive filterable, sortable, and aggregatable capabilities per column. +3. Implement total count, terms facets, missing values, statistics, and + numeric/date histograms. +4. Add publication-driven cache invalidation. +5. Add catalog/table drift diagnostics and startup recovery checks. + +Exit criteria: + +- A newly published dataset and its columns are discoverable immediately. +- Explorer's mapping and facet requirements no longer depend on Guppy. +- Aggregate limits prevent unbounded cardinality responses. + +### Phase 5: GraphQL transport + +1. Add the stable dataset, rows, and aggregate inputs and result envelopes to + `graphqlapi/schema.graphqls`. +2. Regenerate gqlgen output; do not edit generated files manually. +3. Add transport adapters in `graphqlapi/materialization`. +4. Normalize authorization failures and validation errors into stable GraphQL + error codes. +5. Keep legacy materialization-ID operations only while an internal caller + still needs them, then remove them. + +Exit criteria: + +- Publishing a new dataset or column requires no GraphQL regeneration or Loom + restart. +- GraphQL introspection remains stable while runtime dataset introspection + reflects current publications. + +### Phase 6: Explorer compatibility adapter + +1. Add the Loom reader API client that replaces the current Guppy client in the + frontend. +2. Translate existing `guppyConfig.dataType`, table fields, filters, charts, + field mappings, and shared-filter configuration into canonical Loom calls. +3. Preserve Gecko's current Explorer configuration JSON. +4. Maintain cursor state per table page while preserving the current UI. +5. Reshape canonical aggregate results into the structures expected by current + facets and charts. +6. Leave manifest/download behavior on its existing path until the streaming + export endpoint is implemented and verified. + +Exit criteria: + +- One Explorer tab reads from Loom without changing its Gecko config document. +- Rows, counts, filters, sorting, facets, and details-panel lookup are equivalent + for the selected dataset. + +### Phase 7: Streaming export + +1. Add an authorized HTTP streaming endpoint for CSV, TSV, JSON Lines, and + manifest-oriented exports. +2. Reuse the canonical dataset resolver and filter contract. +3. Stream ClickHouse results without loading the complete export into memory. +4. Apply export-specific row, byte, concurrency, timeout, and audit controls. + +GraphQL initiates or describes exports but does not carry large files. + +Exit criteria: + +- Large downloads use Loom's ClickHouse streaming path. +- Export authorization and row selection match interactive reads. + +### Phase 8: Parity handoff for user-managed cutover + +1. Before deployment, run offline parity fixtures comparing captured legacy + results with Loom/ClickHouse results for every configured Explorer dataset. +2. Compare schemas, row identities, counts, sorted pages, filter results, + facets, details reads, and downloads. +3. Require every configured alias to resolve at least one authorized READY + project output and verify multi-project union behavior. +4. Produce latency, ClickHouse query-cost, error-rate, and authorization + evidence for the user-managed deployment cutover. + +Deployment timing and rollback are outside this implementation plan. + +## Test matrix + +Required automated coverage includes: + +- catalog resolution across projects, generations, aliases, and pointer + advancement; +- unauthorized dataset non-disclosure; +- restricted and unrestricted authorization scopes; +- every supported scalar, nullable, and array type; +- null, empty-array, unicode, date/time, and numeric-boundary values; +- every filter operator with valid and invalid type combinations; +- compound `AND`, `OR`, and `NOT` expressions; +- stable ascending and descending cursor pagination with duplicate and null sort + values; +- aggregate correctness, missing buckets, high-cardinality limits, and + `filterSelf` adapter behavior; +- publication/read concurrency and pointer swaps; +- stale catalog records and missing ClickHouse tables; +- SQL-injection attempts through identifiers, operators, cursors, and values; +- GraphQL complexity/page/bucket limits; +- captured legacy-result-versus-Loom parity fixtures for the initial Explorer + datasets; these fixtures must not require a live legacy backend. + +Integration coverage must run against a real ClickHouse instance. Unit tests +that only assert generated SQL are necessary but insufficient. + +## Operational requirements + +- Structured logs include project, generation, dataset alias, execution/output + ID, selected column count, filter complexity, rows returned, duration, and a + query digest. Logs must not include unrestricted row values. +- Metrics cover resolution failures, authorization denials, query latency, + scanned/read rows when available, result rows, aggregate bucket counts, + cache hits, ClickHouse errors, and publication-to-discovery delay. +- Reader limits are configurable and have conservative defaults. +- Health checks distinguish GraphQL availability, catalog availability, and + ClickHouse read readiness. +- Pointer advancement and rollback are auditable. + +## Definition of done + +The work is complete when: + +- every authorized READY registered Loom output participates in its logical + `dataType` federation without a browser-supplied project; +- every published row carries canonical `auth_resource_path`; +- arbitrary unregistered ClickHouse tables are not exposed; +- new datasets and columns require neither GraphQL regeneration nor restart; +- Explorer runs its table, filters, counts, facets, charts, and details reads + against Loom while retaining its Gecko configuration shape; +- authorization is enforced before physical dataset resolution and remains + generation-aligned; +- the initial ClickHouse type matrix passes unit and real integration tests; +- large exports have a bounded streaming path; +- offline parity evidence is ready for the user-managed migration from the + legacy read stack to Loom/ClickHouse. diff --git a/docs/COMPILER_FIRST_PLAN.md b/docs/COMPILER_FIRST_PLAN.md deleted file mode 100644 index bad1e84..0000000 --- a/docs/COMPILER_FIRST_PLAN.md +++ /dev/null @@ -1,900 +0,0 @@ -# Compiler-First FHIR Graph Lowering and AQL Optimization Plan - -## 1. Purpose - -This is the primary implementation program for Loom. - -Loom's durable value is not its current browser page, job API, or export -wrapper. Its durable value is the compiler that turns a user-oriented FHIR -dataframe request into correct, authorization-safe, efficient AQL over an -observed FHIR graph. - -The product should be built outward from this boundary: - -```text -dataframe intent - -> typed semantic request - -> FHIR graph logical plan - -> cardinality-aware relational/dataframe plan - -> optimized physical AQL plan - -> generated AQL + bind variables - -> Arango EXPLAIN validation - -> execution / preview / export -``` - -The compiler must be useful without a frontend. A frontend or future language -assistant should only have to produce the typed request and display validation, -plan explanation, and results. - -This plan refactors the earlier 20-gap roadmap. Compiler work happens first. -Jobs, recipe persistence, Elasticsearch, deployment polish, and a larger -frontend are downstream consumers and must not dictate the compiler design. - -## 2. Existing Compiler Backbone - -This is not a greenfield compiler. - -### Existing request and validation path - -- `graphqlapi/schema.graphqls` defines fields, traversals, pivots, - aggregates, slices, and value modes. -- `graphqlapi/dataframe` maps and resolves GraphQL input. -- `internal/dataframe/validation.go` validates fields and traversals against - populated catalog records and generated schema metadata. -- `fhirschema/generated.go` contains generated FHIR definitions and - traversals. -- `fhirschema/schema.go` resolves fields, selectors, traversals, and - pivot families. - -### Existing lowering path - -- `internal/dataframe/planner.go` converts the public traversal tree to named - sets and derived fields. -- `internal/dataframe/traversal_rules.go` classifies the currently optimized - traversal tuples. -- `internal/dataframe/lowered_types.go` defines named sets, derived fields, and - representative slices. -- `internal/dataframe/document_reference_semantics.go` defines specialized file - summary behavior. - -### Existing AQL compilation path - -- `internal/dataframe/lowered_compile.go` emits the optimized named-set AQL. -- `internal/dataframe/compile_fields.go` emits traversal, selection, pivot, and - aggregate expressions. -- `internal/dataframe/selectors.go` parses the current selector subset. -- `internal/dataframe/query_runtime.go` executes AQL through a callback cursor. - -### Existing optimizations worth preserving - -The current implementation already performs meaningful work: - -- shares one root-neighbor traversal when several Patient child resource types - use the same edge label -- filters the shared neighbor set into resource-specific named sets -- reuses named sets for multiple fields and aggregates -- unions DocumentReference routes before file-summary classification -- classifies DocumentReference payloads once for compatible selectors -- hydrates ResearchStudy through direct `DOCUMENT` lookup when required -- groups compatible pivots so one pivot map can serve multiple projections -- filters requested pivot columns through bind variables -- uses `LIMIT 1` for existence checks -- pushes project and auth-resource-path predicates into root and traversal AQL -- uses bind variables for user values and traversal labels/types -- validates against populated fields and references before compilation - -Those behaviors need parity tests before the compiler is reorganized. - -## 3. Current Compiler Gaps - -### Semantic limitations - -- Root lowering is limited to `Patient`. -- Traversal roles are a hardcoded subset of generated traversal metadata. -- A structurally valid root-only or simple one-hop request can be rejected - because it does not trigger the specialized optimized profile. -- Row grain is implicit. The planner does not formally distinguish selecting - arrays from exploding rows, aggregating related resources, or changing the - output entity. -- FHIR reference direction, multiplicity, choice types, extensions, - terminology, and repeated values do not have one complete semantic model. -- DocumentReference behavior is embedded as a special family rather than an - optimizer rule over typed semantics. - -### Intermediate representation limitations - -- `logicalNode` is still close to GraphQL input and does not express joins, - filters, grouping, row identity, or cardinality explicitly. -- `NamedSet` and `DerivedField` combine physical execution decisions with - semantic operations. -- Set modes are tracked through string-keyed maps. -- Important behavior depends on string constants and sanitized variable names. -- There is no structured physical AQL AST or operation graph. -- There is no stable compiler explain artifact between the request and AQL. - -### Filter and expression limitations - -- The selector grammar is intentionally narrow. -- Predicate support is primarily existence and equality/contains behavior. -- Typed comparisons, code/system matching, date ranges, null semantics, and - array quantifiers are incomplete. -- Filter pushdown is not represented as an optimizer decision. -- Some compiler paths treat invalid selector parsing as impossible after - validation rather than carrying typed validated expressions. -- Internal raw-expression support is difficult to reason about and should not - be reachable from untrusted product input. - -### Physical AQL limitations - -- AQL is assembled directly as formatted strings. -- `UNIQUE`, `FLATTEN`, and `MERGE` are used broadly and can eagerly materialize - large arrays per root row. -- Traversal deduplication policy is not cost-based and is sometimes unconditional. -- Repeated selections can rescan the same set or payload path. -- Filters cannot consistently move before set materialization. -- Projection pruning does not remove unused data from specialized summaries. -- There is no choice between traversal, indexed lookup, precomputed rollup, or - alternative join shapes based on observed statistics. -- Root `SORT`/`LIMIT` supports preview but is not a complete stable cursor or - filtered-query strategy. - -### Optimization evidence limitations - -- Tests mostly validate generated query fragments and selected result paths. -- There is no systematic corpus of logical-plan and physical-plan goldens. -- There is no automated Arango `EXPLAIN` inspection. -- There are no plan budgets for scanned rows, peak memory, intermediate arrays, - or execution time. -- There is no optimizer comparison against an unoptimized correct reference - plan. -- `META/` is not yet used as a formal compiler benchmark corpus. - -## 4. Compiler Definition of Done - -The compiler is ready to support the product when it can: - -1. accept any supported root/grain represented by generated FHIR metadata -2. validate every field, traversal, filter, aggregate, and pivot before AQL -3. model row identity and cardinality explicitly -4. produce a correct generic plan for every advertised supported request -5. apply optional optimizations without changing results -6. generate AQL only through typed validated physical operations -7. keep project, dataset generation, and authorization predicates on every - applicable read/traversal -8. explain why a traversal, aggregate, or pivot was chosen -9. report unsupported semantics through stable reason codes -10. show benchmark and `EXPLAIN` evidence for important plan families -11. preserve result parity between generic and optimized plans -12. stream rows without materializing the complete dataframe in Loom - -## 5. Compiler Work Packages - -The compiler program is divided into ten work packages. These replace the old -single large planner gap. - ---- - -# CP0: Compiler Corpus, Reference Semantics, and Baselines - -## Objective - -Create the executable oracle against which every compiler refactor and -optimization is judged. - -## Implementation - -1. Inventory the resource types, populated fields, pivot candidates, and - relationship tuples in `META/`. -2. Record the current GraphQL input, normalized builder, lowered sets, AQL, - bind variables, output columns, and result rows for representative queries. -3. Cover these current plan behaviors: - - root fields - - shared Patient neighbor set - - Patient to Condition - - Patient to Specimen - - Specimen/Group to DocumentReference - - DocumentReference summary classification - - ResearchSubject to ResearchStudy hydration - - Observation pivot - - count, count-distinct, exists, and distinct-values - - representative slices - - multi-auth-path and unrestricted scope -4. Add minimal synthetic fixtures only for semantics missing from `META/`. -5. Define output comparison rules: - - stable row key - - scalar equality - - array order or order-insensitive semantics - - sparse pivot columns - - null versus absent -6. Add a generic/reference execution mode or test evaluator. It may be slower, - but it must favor obvious correctness over optimization. -7. Add compiler benchmark commands using `META/` and configurable larger data. -8. Capture baseline Arango `EXPLAIN` plans and execution statistics. - -## Deliverables - -- `conformance/compiler/` -- compiler fixture schema -- logical/result goldens -- benchmark harness -- baseline report - -## Tests and gates - -- current compiler outputs are reproducible -- generated and generic ingestion produce equivalent graph inputs -- every later optimization runs result parity against the reference mode - -## Parallelism - -May run in parallel with read-only CP1/CP2 design, but its comparison contract -must freeze before those packages implement new IR. - ---- - -# CP1: Typed Dataframe and FHIR Semantic IR - -## Objective - -Replace compiler behavior inferred from nested GraphQL structs and strings with -a typed, backend-independent semantic request. - -## Implementation - -1. Define a `SemanticPlan` or equivalent in a new planner package. -2. Represent: - - project/generation/auth scope - - requested row grain - - root resource type - - semantic nodes and relationships - - fields/projections - - typed filters - - aggregates - - pivots - - representative slices - - output names and types -3. Introduce typed IDs for nodes, fields, relationships, and output columns. -4. Resolve raw GraphQL/recipe input into this IR once. -5. Store validated selectors as typed paths, not reparsed strings. -6. Keep source locations so errors point to the user's field/filter. -7. Define stable unsupported-reason and validation codes. -8. Make the IR serializable for golden tests and developer explain output. -9. Do not include AQL variable names, named-set choices, or optimizer hints. -10. Adapt the existing public builder into the semantic IR without breaking the - current API during migration. - -## Ownership - -- new `internal/dataframe/planner/semantic/` or equivalent -- adapters from `graphqlapi/dataframe` - -## Tests and gates - -- deterministic semantic-plan goldens -- all current supported builders resolve without semantic loss -- malformed/unsupported input fails before physical lowering -- no backend/AQL types leak into the semantic IR - -## Parallelism - -One contract owner. CP2 and CP3 may contribute requirements but cannot define -parallel competing semantic types. - ---- - -# CP2: Generated FHIR Graph Semantics - -## Objective - -Turn existing generated schema and traversal metadata into the authoritative -compiler knowledge base instead of maintaining a hardcoded Patient tuple list. - -## Implementation - -1. Inventory what `fhirschema/generated.go` already records: - - definitions - - properties/kinds - - traversals - - direction - - multiplicity - - backreferences -2. Define compiler-facing semantic APIs over `fhirschema`: - - resolve field and result kind - - resolve relationship/direction - - determine source/target multiplicity - - identify reference/choice/extension paths - - identify pivot-compatible code/value families -3. Replace tuple recognition as the support source with generated traversal - lookup plus explicit optimizer capabilities. -4. Retain domain-specific aliases or preferred paths as optional semantic rules, - not proof that a relationship exists. -5. Define how extensions are addressed: - - generic URL/value selectors - - friendly aliases layered above them -6. Define terminology-aware field semantics using system/code/display. -7. Add FHIR date, dateTime, instant, quantity, Coding, CodeableConcept, and - Reference type descriptors needed by filters and output typing. -8. Add generator changes only when the active graph schema contains information - that current generated metadata omits. -9. Regenerate through `make generate-fhir` and preserve deterministic output. -10. Test every traversal and generated field represented by `META/`. - -## Ownership - -- `fhirschema/` -- `cmd/generate/` only when necessary -- no copied FHIR structs or handwritten replacement schema - -## Tests and gates - -- generated metadata reproducibility -- every observed `META/` relationship resolves through the semantic API -- optimizer support is distinct from schema existence -- generated/generic ingestion parity remains intact - -## Parallelism - -Can proceed beside CP3 after CP1's node/field/relationship identifiers freeze. - ---- - -# CP3: Row Grain and Cardinality Algebra - -## Objective - -Make "one row per" a formal compiler property rather than a frontend hint. - -## Implementation - -1. Define grain identity for: - - Patient - - Specimen - - DocumentReference/File - - Condition/Diagnosis - - Observation - - ResearchSubject/Study enrollment -2. Define relationship cardinalities: - - required one - - optional one - - repeated/many - - unknown/observed-many -3. Define projection modes: - - scalar - - first/representative - - repeated array - - distinct array - - aggregate - - pivot - - explode to rows -4. Require every semantic plan to have one stable row identity. -5. Define duplicate semantics when multiple graph paths reach the same resource. -6. Define how joins affect row multiplicity. -7. Reject accidental Cartesian products. -8. Use generated multiplicity plus observed fanout statistics; never replace - formal semantics with observed data alone. -9. Emit row-expansion and ambiguous-grain warnings. -10. Add algebraic tests for nested one-to-many paths. - -## Tests and gates - -- exact row counts for compiler fixtures -- stable row identity across generic/optimized plans -- no implicit row explosion -- duplicate path convergence has defined behavior - -## Parallelism - -May proceed with CP2 after CP1's core semantic IR freezes. Must finish before -generic lowering and cost optimization are considered stable. - ---- - -# CP4: Typed Expression and Filter Compiler - -## Objective - -Implement safe, FHIR-aware fields and filtering independently of frontend -wording and raw AQL. - -## Implementation - -1. Define a typed expression tree: - - path access - - literal/bind value - - boolean composition - - comparison - - existence/missing - - array quantifiers - - terminology match - - date/range comparison -2. Support initial operators: - - equals/not-equals - - in/not-in - - exists/missing - - contains text - - greater/less comparisons - - between/date range -3. Define `ANY`, `NONE`, and only later `ALL` repeated-value semantics. -4. Define missing versus null versus empty array. -5. Match Coding/CodeableConcept by system and code when available; display is - presentation, not the canonical identity. -6. Normalize FHIR date/dateTime/instant comparison and timezone policy. -7. Type-check every operator against CP2 descriptors. -8. Lower all user values to bind variables. -9. Make raw AQL expressions inaccessible from public input. -10. Compile filters into logical predicates before physical AQL. -11. Support filter pushdown metadata: which node/path owns each predicate. -12. Add fuzz tests for selector and expression parsing. - -## Tests and gates - -- result-based operator tests -- repeated-value quantifier tests -- code/system tests -- null/missing tests -- injection tests -- auth predicates cannot be removed by filter rewrites - -## Parallelism - -Expression types need one owner. Operator implementations can be split after -the AST and type rules freeze. - ---- - -# CP5: Correct Generic FHIR Graph Lowering - -## Objective - -Guarantee a correct physical plan for every advertised semantic request before -applying special optimizations. - -## Implementation - -1. Define a generic logical-to-physical lowering using CP2 graph metadata. -2. Support arbitrary validated root resource types. -3. Represent physical operations: - - root scan - - indexed root lookup - - graph traversal - - filter - - project - - distinct/deduplicate - - aggregate/group - - pivot - - explode - - slice - - union - - direct document lookup -4. Propagate project, generation, and auth scope through every operation. -5. Choose traversal direction from generated semantics plus observed - `fhir_edge` layout. The current catalog's parent-to-child contract is - inbound; do not mistake FHIR reference direction for physical AQL direction. -6. Preserve row identity and grain from CP3. -7. Implement root-only and simple one-hop requests; they must not require a - specialized optimization profile. -8. Detect cycles and cap graph depth. -9. Produce a typed `PhysicalPlan`, not AQL strings. -10. Preserve the current Patient-case-assay planner as an optimization path for - parity comparison, not the only working path. - -## Tests and gates - -- all six initial grains lower generically -- simple/root-only requests work -- every traversal carries scope predicates -- generic plan results match reference semantics -- cycles and unsupported directions fail clearly - -## Parallelism - -One core lowerer owner. After physical operation interfaces freeze, workers may -add root/grain adapters in separate files. - ---- - -# CP6: Aggregation, Pivot, and Representative-Selection Engine - -## Objective - -Make dataframe shaping a first-class, correct subsystem rather than scattered -string templates. - -## Implementation - -1. Define aggregate semantics and output types for: - - count - - count distinct - - exists/any - - distinct values - - min/max where required -2. Define whether nulls participate in every aggregate. -3. Define pivot inputs: - - key expression - - value expression - - duplicate key/value policy - - requested versus discovered columns - - sparse output policy -4. Support CodeableConcept and Observation code/value pivot families through - CP2 metadata. -5. Preserve the existing compatible-pivot grouping optimization. -6. Prevent high-cardinality unbounded pivot materialization. -7. Define representative selection with explicit ordering and stable tie-break. -8. Distinguish representative selection from arbitrary `FIRST`. -9. Define array output encoding independently of CSV/Elasticsearch. -10. Add generic and optimized implementations for parity testing. - -## Tests and gates - -- duplicate pivot-key cases -- multiple values per key -- sparse/missing pivot keys -- high-cardinality rejection/warning -- aggregate null behavior -- deterministic representative rows -- current `META/` pivot parity - -## Parallelism - -Aggregate, pivot, and representative-selection implementations can run in -parallel after their shared semantic contracts freeze. - ---- - -# CP7: Typed AQL Physical IR and Code Generation - -## Objective - -Separate plan construction from AQL rendering so optimization operates on typed -nodes instead of formatted strings. - -## Implementation - -1. Define an AQL-oriented physical AST or strongly typed operation graph. -2. Represent: - - collection scans - - traversals - - LET bindings - - loops - - filters - - sorts/limits - - collect/group - - projections - - subqueries - - bind variables -3. Generate collision-free internal variables without user-controlled names. -4. Make collection/resource identifiers come only from validated schema - metadata. -5. Make every user value a bind variable. -6. Add a scope verifier that walks the physical plan and proves required - project/generation/auth predicates exist. -7. Render deterministic AQL for snapshot and cache use. -8. Attach source semantic node and optimizer-rule provenance to physical nodes. -9. Keep an escape hatch for audited internal expressions only; mark them - explicitly and prohibit product input from creating them. -10. Port existing named-set code generation incrementally behind this boundary. - -## Tests and gates - -- deterministic render tests -- bind-variable completeness -- identifier injection tests -- scope-verifier negative tests -- current AQL result parity during incremental port - -## Parallelism - -One AST/renderer contract owner. Individual operation renderers can be split -after the node interfaces freeze. - ---- - -# CP8: AQL Optimization Passes - -## Objective - -Build explicit, independently testable optimizer passes that reduce graph work, -intermediate materialization, and repeated payload extraction. - -## Required passes - -### Projection pruning - -- remove unused fields from summaries and slice projections -- avoid building payload-derived values that no output/filter consumes - -### Predicate pushdown - -- move root filters before traversal -- move child predicates into traversal subqueries -- apply resource type, label, project, generation, and auth filters at the - earliest valid point - -### Traversal sharing - -- generalize the existing shared Patient-neighbor optimization -- share identical traversal prefixes across selected fields/aggregates -- split only when predicates or cardinality semantics differ - -### Common-subexpression elimination - -- reuse identical selector extraction -- reuse identical distinct sets -- reuse compatible pivot maps -- reuse DocumentReference classification and study lookup - -### Set fusion and materialization control - -- fuse filter/project operations where doing so preserves reuse -- avoid `FLATTEN` when direct iteration suffices -- avoid `UNIQUE` when schema/cardinality proves uniqueness -- choose `COLLECT`/distinct strategy deliberately -- avoid creating a full union when downstream consumers can stream branches - -### Lookup selection - -- choose graph traversal, indexed collection lookup, direct `DOCUMENT`, or - precomputed rollup using semantics and evidence -- keep specialized paths optional and result-equivalent - -### Aggregate/pivot pushdown - -- count or test existence without materializing complete node arrays -- filter pivot keys before value aggregation -- share grouped pivot computation - -### Limit and cursor pushdown - -- apply preview/export boundaries only where they preserve requested semantics -- use stable grain identity for keyset cursors -- never limit a child set before an aggregate unless semantics request it - -## Optimizer framework - -1. Each rule has: - - stable name - - preconditions - - transformation - - correctness tests - - estimated effect -2. Apply rules to a fixed point or controlled sequence. -3. Record applied/skipped rules in plan explain. -4. Allow rules to be disabled for parity/debugging. -5. Compare optimized results against CP5 generic plans. -6. Add plan-size and intermediate-cardinality estimates. - -## Tests and gates - -- one test family per rule -- multi-rule interaction tests -- generic/optimized result parity -- scope verifier runs after all rewrites -- no rule is accepted based only on shorter AQL text - -## Parallelism - -After the optimizer framework and physical IR freeze, individual passes are -highly parallelizable. Each worker owns additive rule files and tests; one -optimizer lead owns pass ordering and central registration. - ---- - -# CP9: Arango-Aware Costing, Indexing, EXPLAIN, and Performance Gates - -## Objective - -Make optimization evidence-driven against real Arango behavior and the actual -FHIR data distribution. - -## Implementation - -1. Add an Arango `EXPLAIN` adapter that captures: - - plan nodes - - indexes used - - estimated cost/items - - optimizer rules - - warnings -2. Add execution profiling for benchmark mode: - - runtime - - scanned/full-scan counts - - peak/estimated memory where available - - result rows - - intermediate cardinalities when observable -3. Define required index families for: - - root project/generation/auth/key scans - - edge traversal and label/type/scope constraints - - direct resource ID/reference lookup - - any scalar index used for pushdown -4. Review existing edge indexes against emitted traversal direction and filter - order. Do not add indexes speculatively. -5. Feed resource counts, field coverage, distinct counts, and relationship - fanout into a small cost model. -6. Start with rule-based thresholds. Do not build an elaborate cost-based - optimizer until measurements require it. -7. Define performance budgets for plan families: - - root-only filtered table - - one-hop projection - - multi-hop file manifest - - high-volume Observation filter - - pivot - - aggregate-only query -8. Benchmark generic and optimized plans on `META/` and a scaled dataset. -9. Reject optimizer changes that regress important plans without an explicit - documented tradeoff. -10. Publish a compiler performance report from CI/scheduled runs. - -## Tests and gates - -- expected critical index used -- no unexpected full collection scan for indexed benchmark cases -- output parity -- p50/p95 timing and scanned-item budgets -- memory/intermediate-size budgets -- repeatable benchmark commands - -## Parallelism - -Index analysis, EXPLAIN tooling, cost inputs, and benchmark execution can be -parallelized after CP7 produces stable physical plans. - ---- - -# 6. Compiler Work-Package Dependency Graph - -```text -CP0 Corpus/baseline - ├──────────────┐ - v v -CP1 Semantic IR CP2 FHIR semantics - │ │ - ├──────┬───────┘ - v v -CP3 Grain/cardinality CP4 Expressions/filters - └──────────┬───────────┘ - v - CP5 Generic lowering - │ - ├────────> CP6 Aggregate/pivot engine - │ - v - CP7 Physical AQL IR - │ - v - CP8 Optimizer passes - │ - v - CP9 EXPLAIN/cost/index/performance -``` - -CP6 can begin its semantic contracts after CP1-CP4, but optimized code generation -must integrate through CP7/CP8. - -## 7. Compiler-First Parallel Waves - -### Compiler Wave A: Oracle and contracts - -- Worker A: CP0 corpus and result comparison -- Worker B: CP1 semantic IR contract -- Worker C: CP2 generated FHIR semantic APIs -- Worker D: current AQL/EXPLAIN baseline tooling - -Gate: - -- semantic IDs and result comparison freeze -- current supported results reproducible -- no replacement FHIR model introduced - -### Compiler Wave B: Semantics - -- Worker A: CP3 grain/cardinality -- Worker B: CP4 expression/filter AST and typing -- Worker C: CP6 aggregate semantic contract -- Worker D: CP6 pivot semantic contract -- Worker E: conformance fixtures for all above - -Gate: - -- typed semantic plan can express the six product families -- every operation has defined null/cardinality/output semantics - -### Compiler Wave C: Correct lowering - -- Worker A: CP5 generic lowering core -- Worker B: additive root/grain adapters after core freeze -- Worker C: CP7 physical AQL IR/renderer core -- Worker D: scope-verifier and bind/identifier safety -- Worker E: generic/reference parity suite - -Gate: - -- every advertised query has a correct generic plan -- root-only/simple queries work -- current Patient results remain correct - -### Compiler Wave D: Optimizer - -After CP7 freezes, parallel workers implement: - -- projection pruning and predicate pushdown -- traversal sharing and prefix reuse -- common-subexpression elimination -- set fusion/materialization control -- aggregate/existence pushdown -- grouped pivot optimization -- lookup/rollup selection, including a proven inbound/outbound direction choice -- cursor/limit pushdown - -One optimizer lead owns rule registration, ordering, and integration. - -Gate: - -- every rule has generic/optimized parity -- scope verifier passes after rewrites -- plan explain lists applied rules - -### Compiler Wave E: Arango performance - -- EXPLAIN/index worker -- cost/statistics worker -- benchmark execution worker -- large Observation/pivot stress worker -- regression and performance-report worker - -Gate: - -- important plan families meet written budgets -- optimizer decisions have Arango evidence -- slow paths are visible in explain/metrics - -## 8. What Moves Later - -The following former gaps become smaller downstream work because the compiler -owns the difficult semantics: - -### Reduced product facade - -- recipe templates become presets that construct semantic requests -- capability API delegates to compiler validation/capabilities -- frontend asks grain, columns, and filters, then displays compiler explain -- recipe persistence stores normalized compiler input - -### Reduced preview/export work - -- preview adds limits/cursor around compiled physical plans -- export consumes the compiler row stream -- NDJSON, CSV, and Elasticsearch are row sinks, not separate dataframe engines - -### Deferred infrastructure - -These remain necessary for a development service but should not block compiler -quality: - -- durable load/export jobs -- immutable dataset generations -- saved recipe CRUD -- artifact storage -- Elasticsearch retries -- readiness, metrics, and runbooks - -Implement only the minimum dataset identity, authorization context, catalog -statistics, and execution harness required to build and measure the compiler. - -## 9. Compiler Release Gate Before Product Expansion - -Do not invest in a larger frontend or broad service machinery until: - -- CP0-CP7 are complete -- at least the first CP8 optimizer passes are complete -- Patient, Specimen, File, Diagnosis, Observation, and Study Enrollment grains - have correct generic plans -- typed filters cover the initial product conversations -- pivots and aggregates have explicit tested semantics -- generic and optimized results match -- project/auth predicates are mechanically verified -- `META/` compiler benchmarks are repeatable -- Arango `EXPLAIN` output is available to developers -- current specialized Patient behavior is either preserved as a rule or beaten - by the new generic/optimized plan - -At that point the product layer becomes relatively thin and can be built with -far less risk. diff --git a/docs/DATAFRAME_PACKAGE_REORGANIZATION_PLAN.md b/docs/DATAFRAME_PACKAGE_REORGANIZATION_PLAN.md deleted file mode 100644 index d28f1e8..0000000 --- a/docs/DATAFRAME_PACKAGE_REORGANIZATION_PLAN.md +++ /dev/null @@ -1,469 +0,0 @@ -# Dataframe package reorganization plan - -## Objective - -Turn `internal/dataframe` from the repository's catch-all package into a thin, -backwards-compatible façade over small packages with one-way dependencies. The -move is organizational: rendered AQL, request semantics, authorization, -generation isolation, result shape, and public callers must retain their -current behavior. - -The goal is not to make every package tiny. The physical compiler is a real, -cohesive subsystem. The goal is that a new developer can find request types, -semantic meaning, physical AQL lowering, runtime execution, templates, and -errors without reading one 17.5k-line package. - -## Current evidence - -`internal/dataframe` contains 89 Go files and roughly 17.5k code lines. The -largest production responsibilities are currently interleaved: - -| Current files | Lines | Actual responsibility | -| --- | ---: | --- | -| `physical_render.go` | 1,539 | AQL serialization and renderer safety checks | -| `physical_plan.go` | 1,137 | typed physical IR and IR validation | -| `generic_physical_plan.go` | 790 | semantic-to-physical lowering | -| `semantic_plan.go` | 397 | request-to-semantic-plan construction | -| `physical_optimize.go`, `physical_cost.go`, `physical_prefix.go`, `physical_diagnostics.go` | 963 | physical optimization policy, decisions, and diagnostics | -| `service.go`, `execution.go`, `validation.go`, `pivots.go`, auth/generation files | 1,100+ | catalog-aware request preparation and execution | -| `errors.go` | 320 | transport-neutral product error contract | -| experiment/tournament tests | 4,000+ | AQL research evidence mixed into the core package | - -The root package is imported by the GraphQL service, HTTP error mapper, -server command, profile CLI, and compiler conformance suite. Those callers -need stable names such as `Builder`, `CompileRequest`, `Service`, -`CompiledQuery`, and `ErrorCode`; they do **not** need access to physical IR -implementation files. - -## Target layout - -```text -internal/dataframe/ - doc.go # package contract and architecture map - api.go # compatibility aliases/wrappers only - template/ # already separate; retain - spec/ # request, selection, filter, grain contracts - semantic/ # backend-independent FHIR dataframe meaning - physical/ - plan/ # physical IR, validation, shared diagnostics - lower/ # semantic -> physical FHIR graph lowering - optimize/ # semantics-preserving physical rewrites - render/ # physical IR -> parameterized AQL - compiler/ # orchestration and CompiledQuery output - runtime/ # catalog/scope/generation preparation, Run/Stream/Validate - errors/ # stable user-facing error taxonomy - -conformance/compiler/experiments/ # tournament and ablation tests that use public API only -``` - -The root `dataframe` package remains intentionally small: documented stable -entrypoints, type aliases, constant aliases, and thin wrappers. It must not -contain a child package's implementation. Existing imports therefore continue -to work while callers are migrated only when doing so clarifies ownership. - -## Dependency rule - -Child packages must never import `internal/dataframe` (their parent). The -allowed direction is: - -```text -fhirschema, fhirstructs, authscope, catalog, dataset, store/arango - │ - ▼ - spec - │ - ▼ - semantic - │ - ▼ - physical/plan - ▲ ▲ ▲ - │ │ │ - lower optimize render - \ | / - \ ▼ / - └ compiler ┘ - │ - ▼ - runtime - │ - ▼ - dataframe façade -``` - -`errors` is independent and may be imported by `runtime` and transport -adapters. `template` remains independent of compiler/runtime and may import -only generated schema metadata plus its own types. - -This direction prevents the two most likely Go-cycle failures: - -1. runtime/catalog code importing compiler code which imports the root - `dataframe` service; and -2. physical lowering importing a semantic package that imports physical types - merely to express a storage direction. - -Storage routes belong in `physical/lower`, because physical direction is a -stored-edge decision rather than semantic FHIR meaning. - -## Public API compatibility contract - -Before moving production files, freeze the root surface with an API inventory. -At minimum preserve: - -- `Builder`, request selection/filter/traversal types, selector helpers, row - grain/identity types, and typed filter validation; -- `CompileRequest`, `CompileRequestWithPolicy`, `CompiledQuery`, optimization - policy/rule/diagnostic types; -- `Service`, `ServiceConfig`, `RunRequest`, `Result`, `StreamResult`, - `ValidateRequest`, `ValidationResult`, and `QueryDiagnostics`; -- `ExplainCompiledQuery`, `ProfileCompiledQuery`, and `ExecuteQueryRows`; -- the existing `ErrorCode`, `UserError`, `Error`, and constructors. - -Use Go type aliases in `api.go` for types whose methods must remain visible: - -```go -type Builder = spec.Builder -type Service = runtime.Service -type CompiledQuery = compiler.CompiledQuery -type ErrorCode = dataframeerrors.Code -``` - -Use forwarding functions for constructors and functions. Preserve constant -names through constant aliases. Do not use wrapper structs for `Builder`, -`Service`, or `CompiledQuery`: wrappers would silently break assignment, -methods, JSON fixtures, and conformance callers. - -## Work packages - -### WP0 — Baseline, ownership, and failure gate - -**Purpose:** make the reorganization measurable before moving source. - -1. Record `go list` importers of `internal/dataframe`, exported API via - `go doc`, file LOC, and the existing compiler conformance result hash. -2. Add this plan to the developer architecture index and create a short - package-owner table in `internal/dataframe/doc.go`. -3. Capture the current rendered GDC AQL hash and result-parity fixtures. -4. Resolve or explicitly quarantine the current four failing root-package - assertions before extracting physical code: - - `TestCompileRequestUsesPhysicalExecutionForNavigationOnlyGenericPlan`; - - `TestRenderPhysicalPlanGenericNavigation`; - - `TestRenderPhysicalPlanTraversalSetsPreserveRootRowGrain`; - - `TestIdentityDedupCandidateBuildsActualGDC`. - - They currently assert old native-traversal/projection text while the active - renderer emits endpoint lowering. Reconcile them as a separate behavior - verification change, not as a side effect of the package move. -5. Establish a gate after every package move: - -```bash -GOCACHE="$(pwd)/.gocache" GOTOOLCHAIN=auto go test -short ./internal/dataframe/... ./conformance/compiler -GOCACHE="$(pwd)/.gocache" GOTOOLCHAIN=auto go test ./graphqlapi/dataframe ./graphqlapi ./internal/httpapi -git diff --check -``` - -**Acceptance:** a green baseline or a documented, isolated baseline exception -exists before any mechanical move. No behavior test is weakened to make a -move appear safe. - -### WP1 — Extract the request/specification package - -**New package:** `internal/dataframe/spec`. - -**Move as one cohesive unit:** - -- `builder_types.go`; -- `grain.go`; -- `relationship_match.go`; -- `selectors.go`; -- `filter.go` and `filter_semantics.go`. - -**Keep out of this package:** `filter_literal.go` and `selector_render.go`. -They turn validated values/selectors into AQL expressions and therefore belong -with the physical renderer. - -**Implementation steps:** - -1. Move all request AST types, row-grain identity, selector aliases/parsing, - traversal match mode, filter values, and schema-only filter validation. -2. Replace direct root-package references with `spec.*` in semantic and - physical code. -3. Add aliases/constants/functions in root `api.go` before updating external - callers. Keep existing JSON fixture shape unchanged. -4. Move the corresponding unit tests to `spec` and retain external conformance - tests through the root façade. -5. Add a compile-only test that proves a package importing only root - `dataframe` can still construct every public request shape. - -**Acceptance:** `spec` has no catalog, dataset, Arango, AQL-rendering, or -runtime-service import. The root package has no duplicated request type. - -### WP2 — Extract semantic planning - -**New package:** `internal/dataframe/semantic`. - -**Move:** - -- `semantic_plan.go`; -- `semantic_validation.go`; -- `selection_semantics.go`; -- semantic portions of `optimizer.go` if they only describe logical rules. - -**Implementation steps:** - -1. Make `semantic.Plan`, node/field/pivot/aggregate/slice types, plan - construction, graph validation, and selection normalization use `spec`. -2. Keep semantic plans free of AQL variable names, collection bind keys, - physical directions, catalog clients, and Arango explain data. -3. Export only the semantic API needed by compiler/physical lowering. Keep - internal traversal walk helpers private. -4. Move semantic-plan, selection-semantics, and semantic-validation tests with - the package. -5. Retain root aliases for `SemanticPlan` and public diagnostic explanation - types until all callers are deliberately migrated. - -**Acceptance:** semantic planning can compile and test with only `spec` and -generated FHIR schema dependencies. No semantic package import reaches -`physical`, `runtime`, catalog, or Arango. - -### WP3 — Split the physical compiler by responsibility - -This is the largest move and must be sequential; do not parallel-edit the -physical type graph. - -#### WP3a — Physical plan model - -**New package:** `internal/dataframe/physical/plan`. - -**Move:** - -- `physical_plan.go`; -- `physical_scope.go` only where it validates generic physical operations; -- `physical_diagnostics.go`; -- `physical_cost.go`. - -`plan` owns typed physical operations, their validation, policy/decision -records, and compiler diagnostics. It imports `spec`, but not lowering, -optimization, renderer, service, catalog, or Arango. - -#### WP3b — Lowering - -**New package:** `internal/dataframe/physical/lower`. - -**Move:** - -- `generic_physical_plan.go`; -- `physical_lowering.go`; -- `physical_helpers.go`; -- `physical_required_match.go`; -- `storage_route.go`; -- lowering-specific portions of `physical_scope.go`. - -It imports `semantic`, `physical/plan`, `spec`, and `fhirschema`. It is the -only package allowed to select a proven storage route and endpoint-versus-native -traversal strategy. - -#### WP3c — Optimization - -**New package:** `internal/dataframe/physical/optimize`. - -**Move:** - -- `physical_optimize.go`; -- `physical_prefix.go`; -- physical portions of `optimizer.go`. - -It imports `physical/plan` only. It must not render strings or call Arango. -Keep optimization policy defaults/decision reports in `plan` so they remain -available to the compiler façade and diagnostics without a cycle. - -#### WP3d — Rendering - -**New package:** `internal/dataframe/physical/render`. - -**Move:** - -- `physical_render.go`; -- `selector_render.go`; -- `filter_literal.go`. - -Split the current 1,662-line renderer into files in the same `render` package: - -| New file | Content | -| --- | --- | -| `render.go` | public `Render`, bind state, top-level orchestration | -| `render_root.go` | root scan/window/sort/limit operations | -| `render_traversal.go` | native and endpoint traversal sets, required matches | -| `render_expression.go` | extraction, predicates, aggregates, pivots, slices, projections | -| `render_selector.go` | selector and typed-filter expression emission | -| `validate.go` | renderer-only safety validation | - -The renderer imports `physical/plan` and `spec`, returns an immutable rendered -query value, and has no catalog/runtime dependency. - -**Acceptance for WP3:** the actual AQL generated for all conformance fixtures -is byte-for-byte identical unless a separately approved behavior change is in -flight; bind variables and result hashes remain equal; physical package tests -are co-located under the owning subpackage. - -### WP4 — Create a compiler façade - -**New package:** `internal/dataframe/compiler`. - -**Move/replace:** `compile.go` and `physical_execution.go`. - -1. Define `compiler.CompiledQuery` as the stable compiler output. -2. Orchestrate `semantic.Build`, `lower.Build`, `optimize.Apply`, and - `render.Render` in exactly the current order. -3. Preserve `CompileRequest` and `CompileRequestWithPolicy` through root - forwarding functions; retain direct root imports for conformance and CLIs - during the migration. -4. Move compiler integration tests and generic physical execution tests here. -5. Add result-parity tests that compile through both root façade and compiler - package during the migration, then remove the duplicate route after the - façade is proven. - -**Acceptance:** no runtime/catalog/Arango dependencies in compiler. Compiler -can be invoked by the profile CLI and conformance fixtures without constructing -a service. - -### WP5 — Extract runtime preparation and execution - -**New package:** `internal/dataframe/runtime`. - -**Move:** - -- `service.go`; -- `execution.go`; -- `validation.go` and `validation_service.go`; -- `pivots.go`; -- `active_generation.go`, `auth.go`, `auth_scope.go`, and - `dataset_generation.go`; -- `query_runtime.go`, `explain.go`, and `profile.go`. - -**Implementation steps:** - -1. Replace direct compiler calls with `compiler.CompileRequest`. -2. Retain dependency injection for catalog discovery and row execution so - runtime tests remain database-free. -3. Keep Arango access isolated to `ExecuteQueryRows`, Explain, and Profile; - the rest of runtime must remain testable from injected functions. -4. Move catalog-aware validation and pivot expansion here, because they depend - on observed populated fields rather than only FHIR schema semantics. -5. Move service, execution, validation, active-generation, auth-scope, pivot, - and Explain/Profile tests with this package. -6. Re-export `Service`, `ServiceConfig`, execution options, and validation - result types from root aliases/wrappers. - -**Acceptance:** runtime depends downward on compiler/spec/errors and external -infrastructure packages, but compiler/semantic/physical never import runtime. - -### WP6 — Extract error taxonomy and trim the root façade - -**New package:** `internal/dataframe/errors`. - -**Move:** `errors.go` and its tests. - -1. Preserve root aliases and constructors so `graphqlapi/errors.go` and - `internal/httpapi/errors.go` continue compiling unchanged. -2. Update transport adapters to import `dataframe/errors` only after the root - compatibility suite has passed; this is optional, not required for the - initial move. -3. Leave `internal/dataframe/template` intact; it is already a meaningful - standalone package and must not import runtime/compiler. -4. Reduce root production source to `doc.go` and `api.go` (target: under 400 - lines total, excluding compatibility comments). - -**Acceptance:** `internal/dataframe` contains no physical rendering, storage, -catalog, auth, generation, or error implementation. - -### WP7 — Retire research tests and benchmark artifacts - -The package was inflated by more than 4k lines of ablation/tournament tests. -Those harnesses were useful during compiler development but are not durable -correctness tests. - -1. Delete historical endpoint, selector, compact-projection, identity-order, - pivot, and materialization tournament/experiment harnesses. -2. Delete generated candidate AQL, profile JSON, and decision artifacts with - the harnesses. -3. Preserve stable compiler unit tests and generic opt-in Arango - Explain/result-parity tests beside the owning package. -4. Capture reusable conclusions in `docs/COMPILER_PERFORMANCE.md` rather than - preserving candidate rewrites as executable tests. - -**Acceptance:** production package directories contain ownership tests only, -and performance work is represented by production code, focused profiling, -and the concise performance note. - -### WP8 — Enforce the new architecture - -1. Update `docs/DEVELOPER_ARCHITECTURE.md` with the target dependency graph, - public façade policy, and “where does this change belong?” table. -2. Add a lightweight architecture test/script that rejects child packages - importing their parent `internal/dataframe` package. -3. Add package-scoped CI commands for `spec`, `semantic`, physical packages, - compiler, runtime, template, and root compatibility façade. -4. Add a review checklist: new AQL text goes to `physical/render`; FHIR graph - route decisions go to `physical/lower`; request/canonical semantics go to - `spec` or `semantic`; catalog/scope/streaming work goes to `runtime`. -5. Remove stale compatibility aliases only in a separately approved major - internal API cleanup after every direct importer has migrated. - -## Parallelism plan - -| Lane | May run in parallel with | Must wait for | -| --- | --- | --- | -| WP0 baseline and importer inventory | documentation only | nothing | -| WP1 spec | error extraction design | WP0 API inventory | -| WP2 semantic | root façade scaffolding | WP1 | -| WP3 physical | none within physical | WP2 | -| WP4 compiler façade | renderer test organization | WP3 | -| WP5 runtime | WP7 research-test inventory | WP4 | -| WP6 errors | WP7 inventory | root façade scaffolding | -| WP7 test retirement | WP5 only for runtime tests | each owning package move | -| WP8 enforcement/docs | final verification | WP1-WP7 | - -Do not assign separate workers to `physical/plan`, lowering, optimizer, and -renderer simultaneously. Their shared IR is the compiler's hottest and most -volatile boundary; serial ownership avoids accidental import cycles and -semantic drift. - -## Completion criteria - -The reorganization is complete only when: - -- root `internal/dataframe` is a documented façade under 400 production LOC; -- all production implementation belongs to named child packages; -- no child imports its parent package; -- existing GraphQL, HTTP, CLI, and conformance imports compile through the - root compatibility surface; -- compiler fixture AQL/bind/result parity and live Explain checks are preserved; -- runtime scope/generation/authorization tests remain green; -- historical tournament harnesses are retired; durable performance findings - live in [`COMPILER_PERFORMANCE.md`](COMPILER_PERFORMANCE.md); -- the developer architecture document explains where the next feature belongs. - -## One-shot implementation status - -The initial reorganization pass is complete with the following concrete -boundaries: - -- `internal/dataframe/compiler` now owns the pure request/semantic/physical - compiler and its compiler tests; -- `internal/dataframe/runtime` owns catalog-aware preparation, authorization, - generation pinning, validation, execution, streaming, Explain, and Profile; -- `internal/dataframe/errors` owns the structured error contract; -- `internal/dataframe/template` remains the independent guided-template - package; -- `internal/dataframe/api.go` is the compatibility façade used by existing - GraphQL, CLI, HTTP, and conformance imports. - -The follow-up split in -[`DATAFRAME_PACKAGE_REORGANIZATION_ROUND_2.md`](DATAFRAME_PACKAGE_REORGANIZATION_ROUND_2.md) -has now established the IR visibility boundary and moved production code into -`spec`, `semantic`, `compiler/ir`, `compiler/lower`, -`compiler/optimize`, and `compiler/render/aql`. The compiler facade remains the -stable orchestration/API package. Research tests are intentionally still in -their historical locations until the separate conformance-test relocation is -approved and its environment gates are preserved. diff --git a/docs/DATAFRAME_PACKAGE_REORGANIZATION_ROUND_2.md b/docs/DATAFRAME_PACKAGE_REORGANIZATION_ROUND_2.md deleted file mode 100644 index 755fac0..0000000 --- a/docs/DATAFRAME_PACKAGE_REORGANIZATION_ROUND_2.md +++ /dev/null @@ -1,414 +0,0 @@ -# Dataframe package reorganization — round 2 - -## Decision - -The first pass solved the root-level monolith: `internal/dataframe` is now a -small compatibility facade, and runtime, templates, and errors have real -homes. The remaining structural problem is that `internal/dataframe/compiler` -is a 7.8k-line package containing five separate layers: - -1. dataframe request language and FHIR selection rules; -2. logical/semantic planning; -3. physical IR and its safety invariants; -4. FHIR graph lowering and physical optimization; and -5. AQL rendering. - -Those layers should become packages. The target is not many tiny packages: it -is a compiler whose directory layout mirrors its pipeline and makes dependency -direction enforceable. - -This is a behavior-preserving reorganization. It must use the existing -`fhirschema` metadata, generated `fhirstructs`, graph storage contracts, and -current AQL parity fixtures. No work package invents a FHIR relationship, -storage direction, or selector rule. - -## Survey of the current code - -The current `internal/dataframe` subtree has about 10.5k production lines and -9.8k test lines. `compiler` owns 7.8k production lines and 3.35k unit-test -lines. Its largest files demonstrate real, distinct responsibilities: - -| Current file(s) | Approx. LOC | Actual responsibility | Target owner | -| --- | ---: | --- | --- | -| `builder_types.go`, `grain.go`, `filter*.go`, `selectors.go`, `relationship_match.go` | 0.8k | public request AST, selection/filter/grain contracts | `spec` | -| `semantic_plan.go`, `semantic_validation.go`, `selection_semantics.go` | 0.75k | schema-backed logical dataframe meaning | `semantic` | -| `physical_plan.go`, `physical_helpers.go`, `physical_scope.go` | 1.8k | typed physical IR, clone and scope validation | `compiler/ir` | -| `physical_cost.go`, `physical_diagnostics.go` | 0.55k | optimizer policy/report and explainable physical facts | `compiler/ir` | -| `generic_physical_plan.go`, `physical_lowering.go`, `physical_required_match.go`, `storage_route.go` | 1.25k | FHIR graph route selection and semantic-to-IR lowering | `compiler/lower` | -| `physical_optimize.go`, `physical_prefix.go`, `optimizer.go` | 0.62k | semantics-preserving IR rewrites | `compiler/optimize` | -| `physical_render.go`, `selector_render.go`, `filter_literal.go` | 1.74k | typed IR to parameterized AQL | `compiler/render/aql` | -| `compile.go`, `physical_execution.go` | 0.19k | pipeline orchestration and `CompiledQuery` | `compiler` facade | - -There is also an avoidable API-barrel problem. Root `dataframe/api.go` imports -only `runtime`; `runtime/api.go` then re-exports nearly all compiler and error -types/functions. Runtime is not the owner of the request language or compiler -IR, so this makes navigation misleading and risks runtime becoming another -mega-package. - -The former experiment/tournament/live-gate tests were research harnesses, not -normal package unit tests. They obscured production ownership and have now -been retired. Durable conclusions are summarized in -[`COMPILER_PERFORMANCE.md`](COMPILER_PERFORMANCE.md). - -## Target layout - -```text -internal/dataframe/ - doc.go # architecture map and facade contract - api.go # root compatibility aliases only - errors/ # existing structured user-error taxonomy - template/ # existing guided dataframe templates - spec/ # request AST, selector/filter/grain contracts - semantic/ # request -> backend-independent FHIR plan - compiler/ - api.go # compiler compatibility surface + orchestration - compile.go # semantic -> lower -> optimize -> render - ir/ # typed physical plan, validation, diagnostics - lower/ # FHIR graph/storage-route lowering - optimize/ # IR-only rewrites and policy decisions - render/aql/ # IR -> parameterized AQL - runtime/ # catalog preparation, scope, execution, profile - -conformance/compiler/ - experiments/ # ablations/tournaments; explicit environment gates - integration/ # live Arango parity/Explain coverage -``` - -`compiler` stays a package because it is the useful public programming model -for the profile CLI, conformance suite, and root facade. Its children are -implementation layers. All physical types live in `compiler/ir`, not in -`lower`, `optimize`, or `render`, so no implementation layer owns another -layer's data model. - -## Dependency rules - -```text -fhirschema, fhirstructs, authscope - | - v - spec - | - v - semantic - | - v - compiler/ir <---- compiler/optimize - ^ | - | v - compiler/lower ----> compiler/render/aql - \ / - \ / - v v - compiler - | - v - runtime - | - v - dataframe compatibility facade -``` - -Rules that must be mechanically enforced: - -- `spec` imports schema metadata and `authscope` only; it imports neither - runtime, catalog, Arango, semantic, nor compiler implementation packages. -- `semantic` imports `spec` and `fhirschema`; it does not know collection - names, bind variables, AQL, or `Physical*` types. -- `compiler/ir` imports `spec` only where the typed IR embeds a selector. It - must not import `lower`, `optimize`, `render`, runtime, catalog, or Arango. -- `compiler/lower` imports `semantic`, `spec`, `compiler/ir`, and generated - schema metadata. It is the only layer that may call `ResolveStorageRoute` or - select endpoint versus native graph traversal. -- `compiler/optimize` imports only `compiler/ir`; it never uses rendered AQL, - FHIR resource-name special cases, catalog data, or Arango clients. -- `compiler/render/aql` imports `compiler/ir` and `spec`; it serializes an - already-valid plan and never chooses a FHIR route or optimizer rule. -- `runtime` imports compiler, errors, catalog/dataset/authscope/Arango. No - compiler child may import runtime. -- The root `dataframe` package imports canonical owners directly; it must not - use `runtime` as a transitive alias barrel. - -## Work packages - -### P0 — Freeze the behavior and public surface - -**Owner:** one coordinator. **No source moves.** - -1. Record current public root symbols used by `graphqlapi`, `internal/httpapi`, - commands, and `conformance/compiler`. -2. Capture hashes for the canonical GDC fixture's rendered AQL, bind variables, - columns, result rows, and optimization diagnostics. -3. Run and retain baseline outputs: - - ```bash - GOCACHE="$(pwd)/.gocache" GOTOOLCHAIN=auto go test -short ./internal/dataframe/... ./conformance/compiler - GOCACHE="$(pwd)/.gocache" GOTOOLCHAIN=auto go test ./graphqlapi/dataframe ./graphqlapi ./internal/httpapi - ``` - -4. Add a temporary package-boundary test script that fails if a child of - `internal/dataframe` imports its parent. -5. Treat existing untracked benchmark artifacts and the previous reorg plan as - user work: do not rename, delete, or overwrite them. - -**Acceptance:** a move can be shown to preserve a known rendered query and -public API; no package split is allowed to mask a parity failure. - -### P1 — Remove the transitive runtime API barrel - -**Owner:** one worker. **Must complete before parallel package extraction.** - -1. Split `compiler/builder_types.go` conceptually before moving code: - `Builder`, traversals, selection types, filters, and grain are compiler - inputs; `RunRequest`, `Result`, `QueryDiagnostics`, and `StreamResult` are - runtime results. -2. Make `runtime/api.go` private to the runtime package's own types, then - delete it once all runtime files use explicit `compiler.*` and - `dataframeerrors.*` references. -3. Update root `dataframe/api.go` to alias compiler, runtime, and errors from - their canonical packages directly. Preserve every current root symbol. -4. Keep aliases—not wrapper structs—for `Builder`, `Service`, `CompiledQuery`, - and all physical types. Methods and JSON fixtures must remain unchanged. - -**Acceptance:** direct compiler imports remain limited to runtime and root -facade; `runtime` no longer exports the compiler's entire surface. - -### P2 — Extract `spec` - -**Owner:** one worker after P1. May run in parallel with P6 inventory only. - -Create `internal/dataframe/spec` and move: - -- request AST from `builder_types.go` (`Builder`, `TraversalStep`, - `FieldSelect`, `PivotSelect`, `AggregateSelect`, `RepresentativeSlice`); -- `grain.go`, `relationship_match.go`, `filter.go`, `filter_semantics.go`, and - `selectors.go`; -- the schema-only selector/filter unit tests. - -Do not move `filter_literal.go` or `selector_render.go`: their job is AQL -emission and they belong in `compiler/render/aql`. - -Expose constructor-free value types and validation helpers only. `spec` must -not absorb service result types, catalog-populated-field checks, or AQL text. -Add compiler aliases in `compiler/api.go`, then preserve root aliases in -`dataframe/api.go`. - -**Acceptance:** `go list -deps ./internal/dataframe/spec` contains no runtime, -catalog, Arango, semantic, lower, optimize, or render package. - -### P3 — Extract `semantic` - -**Owner:** one worker after P2. May not overlap P4-P5. - -Create `internal/dataframe/semantic` and move: - -- `semantic_plan.go`; -- `semantic_validation.go`; -- `selection_semantics.go`; -- the semantic plan/validation/selection tests. - -`semantic.Plan` owns `SemanticNode`, fields, pivots, aggregates, slices, row -identity, aliases, match modes, and schema validation. It must contain no AQL -variable names, collection binds, endpoint fields, or Arango explain data. -Move `selectorExecutionMode` out of `selection_semantics.go`: selector mode is -a physical rendering/lowering decision, not a semantic one. - -**Acceptance:** semantic package tests run against `fhirschema` and `spec` -without importing any `compiler/*` implementation package. - -### P4 — Establish `compiler/ir` and the physical proof boundary - -**Owner:** one coordinator. **Serial; it changes shared type ownership.** - -Create `internal/dataframe/compiler/ir`. Move and split the physical model: - -- `physical_plan.go` -> `plan.go`, `expression.go`, `predicate.go`, - `validate.go`; -- clone functions from `physical_helpers.go` -> `clone.go`; -- `physical_cost.go` -> `policy.go` and `policy_report.go`; -- `physical_diagnostics.go` -> `diagnostics.go`; -- IR-only portions of `physical_scope.go` -> `validate_scope.go`. - -Before moving `physical_prefix.go`, extract the generic navigation and exact -scope proofs currently shared with renderer (`validateGenericNavigationTraversal`, -`validateGenericNavigationScopeBlock`, and their dependencies) into -`ir/validate_navigation.go`. This deliberately removes the current wrong-way -dependency where optimizer analysis depends on helpers defined in renderer. - -Keep these invariants in IR: - -- all `Physical*` model types and `Plan.Validate`; -- bind/collection validation, variable definition/use rules, cloning, and - generic project/generation/auth scope proof; -- optimization policy/report data, but not environment-variable policy - loading; and -- renderer-independent diagnostics and traversal-prefix decomposition input. - -**Acceptance:** `compiler/ir` has no FHIR route resolution and no AQL string -construction. Both lowering and renderer can consume the same validated plan. - -### P5 — Extract `lower`, `optimize`, and `render/aql` - -**Owner:** serial coordinator for all three. Do not parallelize these moves. - -#### P5a — `compiler/lower` - -Move `generic_physical_plan.go`, `physical_lowering.go`, -`physical_required_match.go`, `storage_route.go`, compiler-specific -`auth_scope.go`, `dataset_generation.go`, and lowering helpers from -`physical_helpers.go`. - -Split into `lower.go`, `route.go`, `scope.go`, `required_match.go`, -`children.go`, and `rich_shapes.go`. The package accepts `semantic.Plan` and -returns `ir.Plan`. It remains the sole owner of FHIR storage direction, edge -endpoint contracts, project/generation/auth scope insertion, required -semi-joins, and compact/prepared child-set construction. - -#### P5b — `compiler/optimize` - -Move `physical_optimize.go`, `physical_prefix.go`, and `optimizer.go`. -Expose `Apply(plan ir.Plan, policy ir.OptimizationPolicy)`. Environment-based -default-policy construction belongs here or in compiler facade, but its result -must be an `ir.OptimizationPolicy`. All alpha-renaming, traversal-prefix -sharing, and plan rewrites stay here. - -#### P5c — `compiler/render/aql` - -Move `physical_render.go`, `selector_render.go`, and `filter_literal.go`. -Split the 1.6k-line renderer by behavior: `render.go`, `root.go`, -`traversal.go`, `set.go`, `expression.go`, `aggregate.go`, `pivot.go`, -`slice.go`, `selector.go`, and `validate.go`. - -`Render(ir.Plan)` returns an immutable rendered-query value. It may allocate -internal bind names and prune runtime binds, but it cannot alter the IR or -decide routes/optimization. - -**Acceptance:** lower, optimize, and render have only one-way dependencies: -they all consume `ir`; none imports another implementation sibling. - -### P6 — Rebuild the small compiler facade - -**Owner:** coordinator after P5. - -Keep `internal/dataframe/compiler` as the stable entry point, but reduce its -production implementation to: - -- `CompiledQuery` and its output metadata; -- `CompileRequest` / `CompileRequestWithPolicy` orchestration; -- `DefaultPhysicalOptimizationPolicy` forwarding; -- type/function aliases intentionally preserved for root callers and - conformance; and -- integration tests that assert the complete semantic -> lower -> optimize -> - render pipeline. - -`physical_execution.go` should become `compiler/output.go`: it adds the -root-window operation, invokes renderer, and derives user-visible columns and -diagnostics. It must not become a second renderer. - -**Acceptance:** `compiler` imports `spec`, `semantic`, `ir`, `lower`, -`optimize`, and `render/aql`; its own non-alias production code should be -under roughly 500 lines. - -### P7 — Finish runtime organization without over-splitting it - -**Owner:** may run alongside P6 only after compiler aliases are stable. - -Runtime is only ~1.4k production lines, so retain one `runtime` package but -make its file names reflect lifecycle: - -- `types.go`: `RunRequest`, `Result`, stream/diagnostic output; -- `service.go`: dependency injection and public Run/Stream; -- `prepare.go`: active generation and authorization resolution; -- `catalog_validation.go`: discovered field/reference checks; -- `pivot_materialization.go`: flattening result pivots; -- `cursor.go`: query execution; -- `observability.go`: Explain, Profile, and timing helpers. - -Move no compiler semantics into runtime. Runtime can validate what is -populated in a loaded dataset; it cannot reimplement schema/semantic or AQL -rules. - -**Acceptance:** a developer can locate a service request's preparation, -catalog validation, cursor execution, and profiling without opening an API -barrel. - -### P8 — Retire research and live tournament tests - -**Owner:** coordinator after P6/P7. No source move is required. - -Delete the historical `*_tournament_test.go`, `*_experiment_test.go`, and -strategy/live-gate harnesses once their durable conclusions have been captured -in [`COMPILER_PERFORMANCE.md`](COMPILER_PERFORMANCE.md). Keep stable compiler -unit tests and generic opt-in Explain/result-parity tests that exercise the -production path. Delete generated AQL/profile artifacts with the harnesses; -they are not fixtures and cannot act as regression tests. - -**Acceptance:** normal `go test ./internal/dataframe/...` is an ownership -suite, and performance work is represented by production code plus the concise -performance note rather than stale candidate rewrites. - -### P9 — Enforce and document the architecture - -1. Update `docs/DEVELOPER_ARCHITECTURE.md` with the target graph and a - “where does this change go?” table. -2. Add a dependency-check test/script for the rules above. -3. Add focused CI commands for spec, semantic, IR, lower, optimize, renderer, - compiler facade, runtime, and root compatibility. -4. Search for stale imports and require all public callers to compile through - the root facade before removing compatibility aliases in a separate change. - -## Execution order and parallelism - -| Phase | Work | Parallelism | -| --- | --- | --- | -| 0 | P0 baseline | coordinator only | -| 1 | P1 API-barrel removal | coordinator only | -| 2 | P2 spec | P8 test inventory may run in parallel | -| 3 | P3 semantic | no physical move yet | -| 4 | P4 IR | coordinator only | -| 5 | P5 lower -> optimize -> render | strictly serial | -| 6 | P6 compiler facade and P7 runtime | may run in parallel after stable aliases | -| 7 | P8 test retirement, P9 docs/enforcement | may run in parallel after P6/P7 | - -The physical packages must not be delegated to separate workers at the same -time: they share the IR, scope proof, and public aliases. Parallel work there -will create import cycles or silently weaken the authorization/generation -contract. - -## Global acceptance gates - -After every package move: - -```bash -GOCACHE="$(pwd)/.gocache" GOTOOLCHAIN=auto go test -short ./internal/dataframe/... ./conformance/compiler -GOCACHE="$(pwd)/.gocache" GOTOOLCHAIN=auto go test ./graphqlapi/dataframe ./graphqlapi ./internal/httpapi -GOCACHE="$(pwd)/.gocache" GOTOOLCHAIN=auto go test ./... -git diff --check -``` - -For any physical move, additionally prove the canonical GDC dataframe keeps -the same rendered AQL/bind/result hashes and run the existing live Arango -Explain/result-parity gate when its container is available. A package move may -change imports and file paths; it may not change FHIR schema behavior, graph -scope, authorization, dataset-generation isolation, or query results. - -## Implementation status - -The production extraction is now in place. `spec`, `semantic`, -`compiler/ir`, `compiler/lower`, `compiler/optimize`, and -`compiler/render/aql` are separate packages with one-way source dependencies. -The root `dataframe` package is a compatibility facade, and compiler -orchestration remains in the small `compiler` package. Runtime result types -are separate from request/spec types. `runtime/api.go` remains as a source- -compatibility barrel for direct runtime importers; new code should import the -canonical owner. - -The package-boundary checks are executable with: - -```bash -make dataframe-boundaries -``` - -The research/tournament tests and generated benchmark corpus have been -retired. Their durable strategy findings are preserved in -[`COMPILER_PERFORMANCE.md`](COMPILER_PERFORMANCE.md), while production package -tests and the conformance compiler suite remain the correctness suite. diff --git a/docs/DEVELOPER_ARCHITECTURE.md b/docs/DEVELOPER_ARCHITECTURE.md index d645d21..ee2d861 100644 --- a/docs/DEVELOPER_ARCHITECTURE.md +++ b/docs/DEVELOPER_ARCHITECTURE.md @@ -22,6 +22,16 @@ READY generation and rejects the legacy one-file HTTP import endpoint. The GraphQL dataframe mutation is the live compiler transport. Do not add a second query compiler or hand-maintained AQL path behind another endpoint. +The HTTP API names its backend boundaries explicitly. `/graphql/graph` is the +Arango control-plane GraphQL endpoint, while `/graphql/flat` is the dedicated +published ClickHouse dataframe reader. Published ClickHouse dataframe discovery and reads follow the stable-GraphQL, +dynamic-data contract defined in +[`CLICKHOUSE_GRAPHQL_READER_EXECUTION_PLAN.md`](CLICKHOUSE_GRAPHQL_READER_EXECUTION_PLAN.md). +Only registered READY publication outputs are exposed; adding a dataset or +column must not require GraphQL regeneration or a Loom restart. Publication +and published-data reads are ClickHouse-only; Loom has no Elasticsearch +publication or reader fallback. + ## Load and storage model `internal/ingest` reads an NDJSON directory, preflights it against the local diff --git a/docs/EXPLORER_LOOM_SLICE_PARITY_PLAN.md b/docs/EXPLORER_LOOM_SLICE_PARITY_PLAN.md new file mode 100644 index 0000000..4333554 --- /dev/null +++ b/docs/EXPLORER_LOOM_SLICE_PARITY_PLAN.md @@ -0,0 +1,1029 @@ +# Explorer Loom Slice and Read API Parity Plan + +## Status + +This is the cross-repository execution plan for replacing the Explorer's Guppy +read path with Loom and ClickHouse. It covers the previously identified parity +items 1, 2, 3, 4, 5, 7, 8, 9, 10, and 11. + +Item 6, Guppy's caller-selected `accessibility` modes, is intentionally not +recreated. Loom authorizes each request from the authenticated principal, +project, generation, and publication scope. The browser must not choose an +authorization tier. + +The implementation spans: + +- Loom: `/Users/peterkor/Desktop/BMEG/loom` +- Frontend: `/Users/peterkor/Desktop/FFNEW/IDP-Frontend` +- Gecko config ownership: `/Users/peterkor/Desktop/BMEG/gecko` + +The current frontend work branch is `feature/clickhouse-explorer`. + +## Outcome + +Explorer must retain its current Gecko configuration model and visible +behavior while all table, mapping, filter, sort, facet, details, and bulk +metadata reads move to Loom. Loom reads only registered READY ClickHouse +publication outputs. A read for one `dataType` federates every current project +output visible to the authenticated principal. Neither Loom nor the Explorer +adapter may fall back to Guppy or Elasticsearch at runtime. + +`guppyConfig.dataType` remains the configured logical dataset alias during the +migration. The unfortunate property name can be renamed in a later config +migration; it must not force a simultaneous Gecko configuration rewrite now. + +## Current contract gaps + +The first Loom reader slice already provides dataset discovery, runtime column +metadata, flat filters, one-column keyset sorting, total counts, JSON rows, and +basic aggregate functions. The current Explorer depends on additional Guppy +behavior: + +- `_mapping` for per-index and shared-field discovery; +- authorized federation of the same `dataType` across all visible projects; +- automatic `project` and `auth_resource_path` propagation into every + published row; +- recursive filter trees; +- multiple sort columns and offset-oriented page controls; +- batched terms facets, missing counts, statistics, and nested aggregations; +- nested response shaping for dotted field paths; +- identifier-based details reads; +- bulk JSON, CSV, and TSV downloads; +- stable limits and error contracts; +- authorization-safe discovery that does not reveal inaccessible datasets. + +## Assumptions and decisions + +### A1. Explorer reads are authorized multi-project federations + +Loom's server configuration remains projectless. The current YAML server +configuration already has no project setting; the project requirement to +remove is in the reader GraphQL inputs and project-scoped publication lookup. + +Each panel supplies only `panel.guppyConfig.dataType`. Loom obtains the +authenticated principal's authorized projects, resolves the current READY +publication for that alias independently in each project, and queries their +ClickHouse outputs as one logical dataset. + +`project` remains part of each physical publication identity and pointer. It is +not a browser input, filter, or returned dataframe field. + +When projects have different active generation identifiers, each source uses +its own active READY generation. The federation response carries an opaque +revision derived from all selected project publication pointers so cursors and +caches cannot survive a partial source update incorrectly. + +The boundary is deliberately asymmetric: + +- server startup configuration has no project; +- every dataset-generation load requires its owning project; +- generated graph documents and edges carry `project` and + `auth_resource_path`; +- every published dataframe row preserves `auth_resource_path`, while project + remains publication/catalog metadata; +- public Explorer reads require `dataType`, not project. + +Loom's ingest row builders already attach project and auth metadata to graph +records. The re-engineering work is to preserve `auth_resource_path` through +dataframe compilation/publication and use catalog project metadata to federate +sources safely at read time. + +### A2. Scope is Explorer and CohortBuilder first + +`loomSlice` initially replaces the Guppy operations used by Explorer and +CohortBuilder. The repository has other direct Guppy consumers, including the +GraphQL query editor and CohortDiscovery. Their deployment cutover is outside +this plan and will be handled separately. There will be no request-by-request +fallback inside `loomSlice`. + +### A3. Gecko JSON remains recognizable + +Existing panels, filters, tables, charts, `preFilters`, details configuration, +manifest mapping, and `guppyConfig.dataType` remain valid. No project or Loom +server context is added to Explorer configuration. Existing `fieldMapping` +remains a display-label mapping; it is not silently reinterpreted as a +physical ClickHouse column mapping. + +### A4. Logical field paths and physical columns are separate + +ClickHouse physical identifiers cannot safely reproduce every dotted Guppy +field path. Publication metadata must therefore distinguish a stable public +field path from the physical ClickHouse column name. Browsers send public paths +only. Loom resolves and validates the corresponding physical identifier. + +Every published row contains reserved `auth_resource_path`. It is injected by +the dataframe publication pipeline from Loom's loaded graph metadata rather +than being repeated in every recipe. Recipes cannot override or remove it. +Project remains internal load, publication, and catalog source metadata; it is +not duplicated into dataframe rows or exposed as a public row field. + +The frontend adapter reconstructs nested JSON objects from public dotted paths +for components that currently use JSONPath. Physical table and column names +never appear in Gecko config or frontend state. + +### A5. Cursor pagination remains canonical + +Loom keeps ClickHouse-natural keyset pagination. The frontend uses +previous/next navigation and maintains a cursor ledger keyed by dataset, +federation revision, filters, sort, and page size. Previously visited pages may +remain navigable from the ledger. Arbitrary jumps to an unvisited page and +offset compatibility are intentionally not implemented. + +### A6. Authorization replaces `accessibility` + +Frontend request types stop sending Guppy's `Accessibility` value to Loom. +Existing controls may remain visually hidden during the first cutover, then be +removed. Loom derives visibility exclusively from the authenticated principal +and registered publication scope. + +### A7. Downloads are HTTP streams + +GraphQL describes and queries interactive data. Large JSON, CSV, and TSV +downloads use an authenticated Loom HTTP endpoint so results stream from +ClickHouse without accumulating in GraphQL or browser memory. + +### A8. Routing + +The expected public service prefix is `${GEN3_API}/loom`, producing: + +- GraphQL: `${GEN3_LOOM_API}/graphql/flat` +- health: `${GEN3_LOOM_API}/healthz` +- export: `${GEN3_LOOM_API}/api/v1/dataframe/export` + +Recent routing work is assumed to provide this prefix. If the deployed +reverse-proxy prefix differs, only `GEN3_LOOM_API` and chart wiring change; +slice request shapes do not. + +## Target request flow + +```mermaid +flowchart LR + Gecko["Gecko Explorer config"] --> Page["Explorer page props"] + Page --> Ref["dataType"] + Ref --> Slice["Frontend loomSlice"] + Slice --> GQL["Loom GraphQL"] + Slice --> Export["Loom streaming export"] + GQL --> Auth["Principal project and path resolver"] + Export --> Auth + Auth --> Catalog["Authorized READY project outputs"] + Catalog --> Reader["Federated typed ClickHouse reader"] + Reader --> CH["UNION ALL registered project outputs"] +``` + +## Federated read model + +The catalog continues to store independent physical sources using: + +```text +project + project-active-generation + dataType -> current READY output +``` + +The public reader resolves a logical dataset using: + +```text +authenticated principal + dataType -> authorized current project outputs +``` + +Resolution follows this order: + +1. Require an authenticated principal. +2. Resolve the principal's authorized project set and auth resource paths. +3. For each authorized project, resolve that project's active READY generation. +4. Resolve the current READY output for `dataType` within that project and + generation. +5. Drop missing outputs without revealing them; fail only when no authorized + source remains. +6. Build a federation revision from the sorted project, generation, and output + pointer identities. +7. Reconcile the public schemas and compile one federated ClickHouse query. + +The ClickHouse query uses `UNION ALL` over catalog-approved physical tables. +Every branch selects the same public column order and includes the reserved +`auth_resource_path` and internal global-row-key columns. Project identity may +contribute to an internal source key but is not projected as a public column. +Missing optional columns are emitted as typed NULL values. Incompatible types +produce an explicit schema-conflict error before query execution. + +Authorization is applied twice by design: + +- source authorization restricts the projects and outputs entering the union; +- row authorization adds a bound `auth_resource_path` predicate to every + branch for restricted principals. + +A browser-supplied `auth_resource_path` filter is applied only after these +server-derived restrictions. It can reduce results but cannot add paths. + +Rows use a global tie-breaker derived from source project, publication output, +and `__loom_row_id`. Counts, facets, statistics, details, and exports operate +over the same authorized union rather than merging independently queried +results in the frontend. + +### Publication scope policy + +The current publication path can bind an output to the publishing caller's +`AuthResourcePaths`. That is unsuitable for a Guppy-like federated reader: it +would create partial, caller-shaped tables and make later readers depend on +which principal performed publication. + +The reader target therefore requires one complete current output per: + +```text +project + active generation + dataType +``` + +Publication runs under a trusted project-generation context, preserves each +row's `auth_resource_path`, and does not trim rows to an interactive caller's +read scope. `AuthResourcePaths` may remain as audit provenance during migration +but is not part of the public alias pointer or reader authorization decision. +Existing scope-trimmed outputs must be republished before they join a +federation. + +If complete project publication cannot be authorized operationally, the +alternative is explicit non-overlapping scope shards registered under one +project output. That is more complex because Loom must prove shard completeness +and prevent duplicate rows; it is not the initial design. + +### Federated schema policy + +Dataset discovery returns the union of public fields across authorized +sources. Schema reconciliation is deterministic: + +- identical types remain unchanged; +- integer families may promote to a common integer type; +- integer and floating-point mixtures promote to number; +- missing columns become nullable; +- scalar-versus-array and otherwise incompatible families are schema + conflicts unless publication metadata declares a supported coercion; +- capabilities are the safe intersection across every source containing the + field. + +Publication recipes should normally keep a `dataType` schema stable across +projects. The reconciliation policy handles optional dynamic columns without +silently stringifying incompatible data. + +## Canonical Loom contract changes + +### Public column metadata + +Extend published column metadata so the API can preserve Explorer field paths +without binding the config to ClickHouse identifiers: + +```graphql +type DataframeColumn { + name: String! # stable public field path used by clients + logicalType: String! + nullable: Boolean! + repeated: Boolean! + filterable: Boolean! + sortable: Boolean! + aggregatable: Boolean! + description: String +} +``` + +The physical ClickHouse name remains in internal publication metadata. If the +existing `name` must stay physical for migration, add `path` and make all new +inputs accept `path`; do not overload one value with both meanings. + +Publication validation must reject duplicate public paths and missing physical +targets. Dataset aliases must explicitly match configured +`guppyConfig.dataType` values. + +Dataset discovery becomes principal-scoped and federated: + +```graphql +type DataframeDataset { + dataType: String! + revision: String! + columns: [DataframeColumn!]! + rowCount: Int +} + +extend type Query { + dataframeDatasets: [DataframeDataset!]! + dataframeDataset(dataType: String!): DataframeDataset +} +``` + +`rowCount` is the authorized federated count when cheaply available; callers +use the row count operation otherwise. Project membership remains internal to +authorized federation resolution and is not part of this public contract. + +### Recursive filter contract + +Replace the public flat filter list with a recursive expression. Keep the old +input only until all internal callers migrate. + +```graphql +enum DataframeFilterOperator { + EQ + NEQ + IN + NOT_IN + LT + LTE + GT + GTE + CONTAINS + STARTS_WITH + EXISTS + IS_NULL + ARRAY_CONTAINS + ARRAY_OVERLAPS +} + +input DataframeFilterPredicateInput { + column: String! + op: DataframeFilterOperator! + value: JSON +} + +input DataframeFilterExpressionInput { + and: [DataframeFilterExpressionInput!] + or: [DataframeFilterExpressionInput!] + not: DataframeFilterExpressionInput + predicate: DataframeFilterPredicateInput +} +``` + +Exactly one of `and`, `or`, `not`, or `predicate` must be populated. Empty +groups are rejected except for an explicitly documented match-all root. +Operator and value types are validated against column capabilities before SQL +construction. Every value is driver-bound. + +Frontend translation covers the existing `FilterSet` operations: + +| Frontend operation | Loom operation | +| --- | --- | +| `and` | `and` | +| `or` | `or` | +| `=` | `EQ` | +| `!=` | `NEQ` | +| `in`, `includes` | `IN` or `ARRAY_OVERLAPS` from column metadata | +| `exclude` | `NOT_IN` | +| `excludeifany` | negated `ARRAY_OVERLAPS` | +| `<`, `<=`, `>`, `>=` | `LT`, `LTE`, `GT`, `GTE` | +| `exists` | `EXISTS` | +| `missing` | `IS_NULL` | +| `nested` | resolved public path plus nested child expression | + +### Multi-column sort and cursor contract + +Change row sort from one optional value to an ordered list: + +```graphql +enum DataframeSortDirection { ASC DESC } +enum DataframeNullsOrder { FIRST LAST } + +input DataframeSortInput { + column: String! + direction: DataframeSortDirection! = ASC + nulls: DataframeNullsOrder! = LAST +} + +input DataframeRowsInput { + dataType: String! + columns: [String!] + filter: DataframeFilterExpressionInput + sort: [DataframeSortInput!]! = [] + first: Int! = 20 + after: String + includeTotalCount: Boolean! = true +} +``` + +The cursor contains every normalized sort value, null marker, and the internal +row identity. SQL uses lexicographic keyset predicates and always appends the +internal row identity as the final tie-breaker. Cursor inputs are versioned and +signed or integrity-checked so malformed client values fail deterministically. + +### Batched facet and statistics contract + +Add a batched operation rather than issuing one GraphQL request per field: + +```graphql +enum DataframeAggregationKind { + TERMS + STATS + HISTOGRAM +} + +input DataframeAggregationSpecInput { + name: String! + kind: DataframeAggregationKind! + column: String! + limit: Int + interval: JSON + includeMissing: Boolean! = true + excludeSelfFilter: Boolean! = false + termsFields: [String!]! = [] + missingFields: [String!]! = [] +} + +input DataframeAggregationsInput { + dataType: String! + filter: DataframeFilterExpressionInput + aggregations: [DataframeAggregationSpecInput!]! +} + +type DataframeAggregationResult { + name: String! + kind: DataframeAggregationKind! + buckets: JSON + stats: JSON + missingCount: Int +} + +extend type Query { + dataframeAggregations( + input: DataframeAggregationsInput! + ): [DataframeAggregationResult!]! +} +``` + +`TERMS` returns normalized `{key, count}` buckets with a deterministic order +and optional missing bucket. `STATS` returns count, min, max, sum, and average. +`HISTOGRAM` supports validated numeric or date intervals. `termsFields` and +`missingFields` cover the existing nested aggregation display contract against +the denormalized published row. + +For `excludeSelfFilter`, Loom removes only predicates targeting that +aggregation's public column while preserving the remainder of the recursive +filter expression. This reproduces Guppy's `filterSelf` semantics without +making the frontend issue and merge a separate query for every facet. + +### Stable GraphQL errors + +All reader operations return GraphQL errors with an extension code from a +small stable set: + +- `DATASET_NOT_FOUND` +- `COLUMN_NOT_FOUND` +- `INVALID_FILTER` +- `INVALID_SORT` +- `INVALID_CURSOR` +- `QUERY_LIMIT_EXCEEDED` +- `QUERY_TIMEOUT` +- `UNAUTHORIZED` +- `CLICKHOUSE_UNAVAILABLE` + +Messages must use public project/dataset/field names only. Physical table +names, publication execution IDs, and authorization paths must not be exposed. + +## Frontend package design + +Create a new cohesive package under `packages/core/src/features/loom`: + +```text +loom/ + index.ts + loomApi.ts + loomSlice.ts + loomDownloadSlice.ts + types.ts + filters.ts + mapping.ts + pagination.ts + processing.ts + tests/ +``` + +## Loom package changes + +Keep physical publication ownership in `internal/dataframe/materialization`, +but split the federated reader by responsibility: + +```text +internal/dataframe/materialization/ + bundle.go # physical project publication and pointer records + federation.go # authorized dataType -> project source resolution + federation_schema.go # public schema union and type reconciliation + filter.go # recursive filter AST validation and SQL compiler + sort.go # multi-sort keyset predicates and cursor codec + read.go # federated rows and exact counts + aggregate.go # terms, stats, histograms, nested aggregations + export.go # reusable streaming query plan + +graphqlapi/materialization/ + service.go # principal-scoped transport authorization + input.go # GraphQL input to canonical reader models + output.go # public metadata and normalized result mapping + +internal/httpapi/ + dataframe_export.go # authenticated streaming export route +``` + +Publication code must inject `auth_resource_path` before ClickHouse table +creation. Ingest already records it on graph documents; the missing step is +preserving it automatically through compiled dataframe outputs. Project stays +in load and catalog metadata. The federation layer must not call +jsonschemagraph at read time or infer tenant identity from table names. + +### `loomApi.ts` + +- Create one RTK Query API with reducer path `loom`. +- POST GraphQL documents to `${GEN3_LOOM_API}/graphql/flat`. +- Forward CSRF and development bearer credentials consistently with the + existing authenticated APIs. +- Treat HTTP errors and GraphQL `errors` as failures. +- Normalize Loom extension codes into a typed `LoomApiError`. +- Define cache tags for dataset metadata, rows, aggregates, and counts. + +Register the reducer and middleware in: + +- `packages/core/src/reducers.ts` +- `packages/core/src/store.ts` +- `packages/core/src/index.ts` +- `packages/core/src/constants.ts` + +Add `GEN3_LOOM_API`, defaulting to `${GEN3_API}/loom`. + +### `types.ts` + +Define canonical frontend request types. Every operation uses the same dataset +reference: + +```ts +interface LoomDatasetRef { + dataType: string; +} +``` + +Do not put physical materialization IDs or ClickHouse names in component props. +Keep compatibility result types at the adapter boundary rather than polluting +the canonical Loom response types. + +### `filters.ts` + +- Convert `FilterSet` into `DataframeFilterExpressionInput`. +- Resolve `includes` differently for scalar and repeated columns using cached + dataset metadata. +- Preserve nested `and`, `or`, and `nested` structure. +- Remove a facet's own predicates only when `excludeSelfFilter` is requested. +- Return a typed client validation error when a configured field is absent or + the operation is incompatible with its logical type. +- Unit-test every existing `Operation` variant. + +### `mapping.ts` + +- Convert `dataframeDataset.columns` into the mapping consumed by facets and + table configuration. +- Group shared fields across configured `dataType` aliases by public path and + compatible logical type. +- Apply existing Gecko labels and `fieldsConfig` overrides after runtime + metadata, so configuration remains authoritative for display. +- Detect configured fields missing from the publication and report one clear + configuration error per panel. +- Reconstruct nested row objects from dotted public paths without changing + scalar and array values. + +### `pagination.ts` + +- Compute a stable request signature from dataType, federation revision, + filters, sorts, columns, and page size. +- Store `page -> after cursor` only within that signature. +- Invalidate the ledger whenever any signature input changes. +- Record the next page cursor only after a successful response. +- Preserve cursors for previously visited pages so back navigation does not + walk from page zero. +- Never reuse a cursor across federation revision changes. + +### `processing.ts` + +- Adapt normalized Loom rows into `{data: {[dataType]: rows}}` only at the + component boundary if temporarily needed. +- Adapt terms buckets and statistics into the existing `AggregationsData` and + `StatsData` shapes. +- Emit `_missing` only where current chart/facet components expect it. +- Keep canonical Loom result processing separate from Guppy-specific JSONPath + traversal helpers. + +## Detailed workstreams + +### 1. Frontend adapter and `loomSlice` + +Loom work: + +- Freeze `dataType` as the only browser-supplied dataset identity. +- Implement one shared federated-source resolver for dataset, row, + aggregation, detail, and export operations. +- Add stable error extensions and request IDs to GraphQL responses. + +Frontend work: + +- Create the `loom` feature package described above. +- Add `GEN3_LOOM_API`, reducer, middleware, and barrel exports. +- Implement endpoints: + - `getLoomDataset`; + - `getLoomDatasets`; + - `getLoomRows`; + - `getLoomCount`; + - `getLoomAggregations`; + - `getLoomRowDetails`; + - `downloadFromLoom`. +- Pass each panel's existing `guppyConfig.dataType` into Loom hooks without a + new project config. +- Replace Explorer/CohortBuilder Guppy imports with Loom hooks in one hard + switch after parity tests pass. +- Do not implement a `useGuppyOrLoom` runtime selector. + +Exit criteria: + +- Core compiles with the Loom reducer and middleware registered. +- One panel can discover its dataset and load an unfiltered first page. +- Network inspection shows no Explorer request to `/guppy`. + +### 2. Mapping and shared fields + +Loom work: + +- Persist public field path, physical column, logical type, description, and + capabilities in publication metadata. +- Reserve and inject canonical `auth_resource_path` into every output from the + loaded graph row metadata. +- Reject recipes that attempt to define the reserved column. +- Validate uniqueness and physical-target existence at publication time. +- Reconcile authorized project schemas and return only public metadata through + GraphQL. +- Make a new pointer immediately visible to dataset discovery without restart. + +Frontend work: + +- Replace `_mapping` calls in + `packages/frontend/src/pages/Explorer/data.ts` with Loom dataset discovery. +- Replace `useGetFieldsForIndexQuery` and + `useGetSharedFieldsForIndexQuery` usage for Explorer. +- Build shared filters by intersecting public paths and compatible types across + configured datasets. +- Surface a panel-level error when configured fields are unpublished. + +Exit criteria: + +- Adding a published column requires no GraphQL regeneration or frontend + release. +- Dataset metadata represents the authorized union across all READY project + sources. +- Existing filter labels and groups render from the same Gecko config. +- Shared filters select the same configured datasets as before. + +### 3. Recursive filters + +Loom work: + +- Add recursive filter models and schema inputs. +- Implement a validated AST compiler that produces SQL fragments plus bound + arguments. +- Enforce node depth, node count, values-per-IN, and array-size limits. +- Implement scalar, nullable, array, date, and numeric operator matrices. +- Make row, count, aggregate, details, and export reuse the same compiler. + +Frontend work: + +- Translate every `FilterSet` operation to the canonical AST. +- Merge panel `preFilters` into the same AST. +- Preserve shared-filter propagation between datasets. +- Remove Guppy JSON filter construction from Loom call sites. + +Exit criteria: + +- All existing filter unit fixtures have Loom translation fixtures. +- Compound `AND`/`OR`, exclusion, missing, array, range, and nested filters + produce matching captured results. +- No query-controlled value is interpolated into ClickHouse SQL. + +### 4. Multi-column sorting and pagination + +Loom work: + +- Accept ordered sort lists with explicit direction and null ordering. +- Validate every sort column as sortable. +- Generate lexicographic keyset predicates and versioned cursors. +- Include the federation revision and global source/row tie-breaker in cursor + validation so any project pointer swap invalidates stale cursors cleanly. +- Add tests for duplicates, nulls, mixed directions, and pointer changes. + +Frontend work: + +- Translate Mantine sorting state into ordered Loom sorts. +- Replace offset calculation with the cursor ledger. +- Reset cursors on filters, sorts, page size, dataType, or federation revision + change. +- Replace direct arbitrary page jumps with previous/next cursor navigation. + +Exit criteria: + +- Repeated sort values never duplicate or skip rows. +- Back navigation uses cached cursors. +- Sort changes return to the first page. + +### 5. Facets, histograms, statistics, and nested aggregations + +Loom work: + +- Add the batched `dataframeAggregations` operation. +- Implement terms, missing count, numeric/date histogram, and statistics. +- Implement `excludeSelfFilter` against the recursive filter AST. +- Implement terms and missing sub-aggregations used by `getSubAggs`. +- Enforce deterministic bucket ordering and per-request limits. +- Keep aggregation values and interval inputs driver-bound. + +Frontend work: + +- Replace `useGetAggsQuery`, `useGetStatsAggregationsQuery`, and + `useGetSubAggsQuery` in Explorer with Loom operations. +- Adapt normalized results to `AggregationsData` and `StatsData`. +- Preserve chart cleanup, excluded values, `_missing`, and range processing. +- Coalesce all visible facet requests into the smallest practical number of + batched calls. + +Exit criteria: + +- Enum, range, numeric/date chart, statistics, missing, nested terms, and + `filterSelf` fixtures match the captured Guppy results. +- High-cardinality fields cannot return unbounded buckets. + +### 7. Nested row shaping + +Loom work: + +- Publish an explicit public field path for every exposed physical column. +- Resolve selected public paths to validated physical columns. +- Return rows keyed by public path or return a parallel ordered field list that + permits lossless frontend reconstruction. +- Define collision rules for `a` and `a.b`; reject ambiguous publications. + +Frontend work: + +- Rebuild nested JSON objects from dotted paths before data reaches tables, + JSONPath accessors, details panels, or download post-processing. +- Preserve arrays as arrays rather than treating numeric path segments as + object keys. +- Apply table accessors and cell renderers after shaping. + +Exit criteria: + +- Existing dotted table fields and JSONPath-based cells render unchanged. +- Null, missing, array, and scalar values survive round-trip shaping. + +### 8. Details-panel lookup + +Loom work: + +- Support exact identifier predicates through the canonical row operation. +- Guarantee deterministic `first: 1` behavior by adding the internal row + identity tie-breaker. +- Return a stable validation error when the configured ID field is absent. + +Frontend work: + +- Implement `useGetLoomRowDetailsQuery` as a thin row-query specialization. +- Replace the Guppy hook in `QueryRowDetailsPanel.tsx`. +- Request only configured detail fields plus the ID field. +- Run nested row shaping before `dataPath` extraction and Study details state. +- Preserve loading, missing-ID, empty-result, and error views. + +Exit criteria: + +- Expanding a row loads the same logical record and details fields. +- Detail reads use the same authorized federation and filter semantics as + table reads. + +### 9. Streaming download + +Loom work: + +- Add `POST /api/v1/dataframe/export` with a JSON request body containing + `dataType`, public columns, recursive filter, sort list, and format. +- Support streaming JSON arrays, CSV, and TSV. JSON Lines may be added as a + separate explicit format. +- Reuse dataset resolution, authorization, column validation, filter + compilation, and sorting from interactive reads. +- Stream ClickHouse rows directly to the response writer. +- Set `Content-Type`, `Content-Disposition`, request ID, and optional download + progress headers. +- Cancel ClickHouse work when the client disconnects. + +Frontend work: + +- Implement `loomDownloadSlice.ts` and a replacement for + `downloadFromGuppyToBlob`. +- Send public fields and canonical filters to Loom. +- Prefer a streamed browser download for large results instead of building a + complete Blob in memory. +- Preserve abort, start, done, error, filename, and current format behavior. +- Keep `rootPath` handling only for explicitly small JSON consumers; it cannot + require buffering every production export. + +Exit criteria: + +- CSV, TSV, and JSON contents match the corresponding filtered table query. +- Large exports remain bounded in Loom and do not require full browser-memory + materialization. + +### 10. Limits, errors, and operations + +Loom work: + +- Add configurable limits for page size, selected columns, filter depth and + nodes, IN values, sorts, aggregation specs, buckets, export rows, export + bytes, concurrent exports, and execution time. +- Apply context deadlines and ClickHouse execution settings. +- Add GraphQL complexity costs for rows and aggregation fan-out. +- Emit structured logs with public dataset identity, query digest, selected + column count, filter complexity, duration, and returned rows. +- Add metrics for discovery, authorization denials, query latency, bucket + counts, timeouts, ClickHouse errors, and export volume. +- Expand health/readiness to distinguish HTTP availability from catalog and + ClickHouse read readiness. + +Frontend work: + +- Map stable Loom errors to actionable panel, table, and download messages. +- Do not automatically retry validation, authorization, or limit failures. +- Retry transient availability failures only under the existing RTK policy. +- Include request IDs in diagnostic UI/logging where available. + +Exit criteria: + +- Deliberately oversized requests fail before expensive ClickHouse execution. +- Timeouts and cancellations terminate server work. +- Operators can distinguish configuration errors from backend availability. + +### 11. Authorization-safe discovery and non-disclosure + +Loom work: + +- Replace project-required reader inputs with principal-scoped `dataType` + resolution. +- Enumerate only principal-authorized projects, resolve each active READY + output, and build one catalog-approved source set. +- Apply bound row predicates for the principal's allowed + `auth_resource_path` values inside every union branch. +- Filter dataset lists to aliases having at least one authorized READY source. +- Return the same public `DATASET_NOT_FOUND` behavior for missing and + inaccessible datasets where disclosure would be unsafe. +- Require an authenticated principal for all reader and export paths. +- Resolve project membership, each project's active generation, and row-level + auth paths before resolving physical tables. +- Never return publication execution IDs, physical tables, or raw auth paths in + browser-facing errors. +- Add cross-project, restricted-scope, unrestricted-scope, stale-pointer, and + unauthorized-listing tests. + +Frontend work: + +- Never derive authorization from `auth_resource_path`, `preFilters`, or the + old accessibility selector. +- Treat an absent federated dataset as unavailable without alternate backend + fallbacks. +- Clear dataset-specific RTK cache and cursors on logout or principal change. + +Exit criteria: + +- An unauthorized caller cannot distinguish an absent dataset from a hidden + one through data, error messages, timing fixtures, or IDs. +- A restricted caller sees the union of all and only rows permitted across + every authorized project. +- Row, facet, details, and export operations enforce the same scope. + +## Execution order + +### Phase 0: Freeze fixtures and resolve assumptions + +1. Record the actual Gecko Explorer config used for the first target + `dataType`. +2. Identify at least two authorized projects publishing that alias so + federation is exercised from the first integration fixture. +3. Capture Guppy results for mapping, rows, counts, filters, sorts, facets, + details, and downloads. + +### Phase 1: Harden the Loom contract + +1. Make `auth_resource_path` the reserved publication column; retain project + only in load and catalog metadata. +2. Implement authorized multi-project source resolution and schema + reconciliation. +3. Implement authorization-safe resolution and stable errors. +4. Add public field paths to publication metadata. +5. Add recursive filters and multi-sort keyset cursors. +6. Regenerate gqlgen output. +7. Add unit and real ClickHouse integration coverage. + +### Phase 2: Build the frontend foundation + +1. Add `GEN3_LOOM_API`, `loomApi`, reducer, middleware, and types. +2. Pass existing panel `dataType` values through Explorer and CohortBuilder. +3. Implement metadata, filter, row shaping, and cursor adapters. +4. Load one table and one details panel against Loom. + +### Phase 3: Aggregation parity + +1. Implement batched Loom aggregations. +2. Add frontend facet/statistics result adapters. +3. Switch one panel's facets and charts. +4. Run captured result parity tests. + +### Phase 4: Streaming export and operational controls + +1. Implement the Loom export route and frontend download client. +2. Add all query and export limits. +3. Add logs, metrics, cancellation, and readiness. + +### Phase 5: Complete the code handoff + +1. Switch all Explorer/CohortBuilder call sites to Loom hooks together. +2. Remove Explorer's SSR `_mapping` call to Guppy. +3. Remove Explorer Guppy imports, status checks, and download usage. +4. Verify no Explorer network request targets `/guppy`. +5. Produce parity results and deployment notes for the user-managed cutover. + +Deployment timing, rollout, and rollback are intentionally outside this plan. + +## Test plan + +### Loom + +- Unit tests for filter AST validation and SQL/argument output. +- Cursor tests for multiple sorts, nulls, duplicate values, and stale + publication identities. +- Catalog authorization and non-disclosure tests. +- Aggregation tests for terms, missing, stats, histograms, self-filter removal, + and nested terms. +- Export encoding, cancellation, and limit tests. +- Real ClickHouse tests for every supported type and operator. +- HTTP GraphQL and export authorization tests. + +Required validation: + +```bash +make generate-graphql +go test ./... -count=1 +go build ./... +``` + +### Frontend core + +- `FilterSet` to Loom AST table tests. +- Dataset mapping and shared-field tests. +- Nested row shaping tests. +- Cursor-ledger invalidation and navigation tests. +- Loom GraphQL success, HTTP failure, GraphQL error, auth, and cancellation + tests using mocked requests. +- Aggregation compatibility adapter tests using captured Guppy and Loom + fixtures. +- Download request and abort tests. + +Required validation: + +```bash +npm run test --workspace=@gen3/core -- loom +npm run compile --workspace=@gen3/core +npm run build --workspace=@gen3/core +``` + +### Frontend Explorer + +- Table loading, count, sorting, page navigation, filters, and reset behavior. +- Shared filters across tabs. +- Enum/range facets and charts. +- Details-panel expansion. +- Download start, completion, failure, and abort. +- Missing dataset and invalid config states. +- Browser-network assertion that Explorer does not call `/guppy`. + +Required validation: + +```bash +npm run test --workspace=@gen3/frontend -- Explorer CohortBuilder +npm run compile --workspace=@gen3/frontend +npm run build --workspace=@gen3/frontend +``` + +The package builds are mandatory because a focused TypeScript check can miss +barrel-export and workspace package-surface failures. + +## Definition of done + +- Every configured Explorer panel resolves one authorized multi-project Loom + dataset using only `guppyConfig.dataType`. +- Every returned row includes canonical `auth_resource_path`. +- Runtime metadata replaces Explorer `_mapping` requests. +- Rows, total counts, recursive filters, multi-sort pagination, facets, + statistics, nested shaping, and details reads match captured fixtures. +- CSV, TSV, and JSON downloads stream from Loom with identical row selection. +- Browser requests use no Guppy or Elasticsearch endpoint for Explorer. +- Authorization is enforced before physical resolution and inaccessible + datasets are not disclosed. +- Limits, timeouts, cancellation, request IDs, logs, metrics, and readiness are + operational. +- New published datasets and columns require neither GraphQL regeneration nor + a Loom restart. +- Loom and frontend unit, integration, compile, and production builds pass. + +## Decisions required before Phase 1 closes + +1. Which `dataType` and pair of projects will be the first federation fixture? + +No other contract decision currently blocks Phase 1. Incompatible source +schemas reject the federation with a diagnostic rather than silently omitting +an authorized project. diff --git a/docs/FORMAL_GAP_ANALYSIS.md b/docs/FORMAL_GAP_ANALYSIS.md deleted file mode 100644 index 0522853..0000000 --- a/docs/FORMAL_GAP_ANALYSIS.md +++ /dev/null @@ -1,1544 +0,0 @@ -# Formal Gap Analysis: From Prototype to Development Service - -## 1. Purpose - -This document is the implementation plan for moving Loom from a working -dataframe prototype to a deployable development service that can: - -1. ingest an unfamiliar FHIR dataset into ArangoDB -2. analyze what the dataset actually contains -3. ask a non-technical user a small number of meaningful questions -4. create a versioned dataframe recipe -5. validate and explain the recipe -6. compile the recipe into performant, authorization-safe AQL -7. preview the result -8. export flat NDJSON or CSV -9. optionally load the same row stream into Elasticsearch - -This is a plan, not a claim that those capabilities already exist. - -The compiler is the first implementation priority. The detailed authoritative -program for FHIR semantic lowering, filters, grains, pivots, aggregates, -physical AQL generation, optimizer passes, and Arango performance is -[`COMPILER_FIRST_PLAN.md`](COMPILER_FIRST_PLAN.md). The service-oriented gaps -below are downstream work and must not distort or precede the compiler core. - -The intended implementer is an engineering agent working directly in this -repository. Every gap therefore includes concrete ownership, implementation -steps, tests, dependencies, and completion criteria. - -## 2. Product Boundary - -### 2.1 What "any FHIR" should mean - -The first production contract should be precise: - -> Loom can losslessly ingest any valid resource type represented by its active -> FHIR graph schema, preserve unknown profile extensions in the raw payload, -> discover populated fields and references, and offer dataframe recipes for -> query shapes supported by its planner. - -It should not initially mean: - -- every historical and future FHIR release without selecting a schema package -- arbitrary cross-resource graph queries -- automatic clinical interpretation of unknown extensions -- every possible FHIRPath function -- automatic production of a useful dataframe when the source data has no - stable identifiers or usable relationships - -FHIR version, graph schema, semantic vocabulary, and planner capability must be -visible metadata. Unsupported query shapes must be reported honestly. - -### 2.2 Product architecture - -The target flow is: - -```text -FHIR input - -> validated project load - -> dataset manifest and analysis snapshot - -> recipe templates filtered by observed data and planner support - -> short guided conversation - -> normalized versioned recipe - -> validation and cardinality explanation - -> logical plan - -> optimized AQL - -> preview row stream or asynchronous export row stream - -> NDJSON / CSV / Elasticsearch -``` - -The frontend must not build AQL, infer graph safety, calculate authorization -scope, or decide whether a traversal is supported. - -## 3. Current Baseline - -The repository already contains valuable production-shaped components: - -- the 14-resource development dataset under `META/` and smaller `META_SMALL/` -- the active graph schema at `schemas/graph-fhir.json` -- generated Go FHIR structs, validators, and edge extractors in `fhirstructs` -- generated FHIR field/traversal metadata in `fhirschema/generated.go` -- existing generation commands in `cmd/generate`, `Makefile`, and `gqlgen.yml` -- NDJSON and gzip discovery and scanning in `internal/ingest` -- generated and generic FHIR row builders -- one Arango collection per discovered resource type -- a shared `fhir_edge` graph -- load-time field profiling in `fhir_field_catalog` -- project and auth-resource-path scoping -- populated field and relationship discovery -- discovery caching with project invalidation -- generated FHIR schema metadata -- mechanically derived friendly `fieldRef` support in `dataframebuilder` -- explicit optimized traversal semantics in `dataframe/traversal_rules.go` -- GraphQL introspection and dataframe execution -- a logical request, lowering planner, optimized AQL compiler, and query runner -- a browser builder suitable for development diagnostics -- unit and integration tests for important compiler paths - -All implementation packages in this plan must extend those owners. The plan -does not authorize a parallel FHIR object model, handwritten copies of generated -validators/extractors, a replacement graph schema, or manual edits to -gqlgen-generated files. Use `META/` for baseline characterization and add -synthetic conformance fixtures only where the existing sample lacks a required -case. - -The current limiting facts are: - -- generated ingestion rejects resource types outside its generated switch -- generic ingestion still requires a class in the configured graph schema -- HTTP import is synchronous and accepts one staged resource file -- the field catalog stores bounded distinct samples but not a complete dataset - manifest, relationship coverage, fanout, or value frequencies -- product-level recipes and templates do not exist -- planner lowering requires a Patient root -- planner traversal support is a hardcoded list of recognized tuples -- simple structurally valid requests may be rejected if they do not match the - optimized lowering family -- input supports only a narrow filter/predicate surface -- preview applies a limit but accumulates all returned rows in memory -- cursor fields exist in the GraphQL input but are not an implemented paging - contract -- export handles, durable jobs, files, and Elasticsearch delivery do not exist -- `/healthz` is liveness only; readiness and dependency diagnostics are absent - -## 4. Delivery Strategy - -Implement the compiler program first, then build thin product and delivery -layers around it. Do not begin with a larger frontend. -For multi-worker execution, use -[`TERRA_ULTRA_EXECUTION_PLAN.md`](TERRA_ULTRA_EXECUTION_PLAN.md), which splits -the durable job work into an early substrate and later export recovery, defines -contract freezes, and prevents unsafe parallel edits to shared packages. - -| Milestone | Outcome | Work | -| --- | --- | --- | -| M0 | Compiler oracle and typed contracts | CP0-CP2 | -| M1 | FHIR grain, filters, pivots, and correct generic lowering | CP3-CP6 | -| M2 | Typed AQL generation and optimizer | CP7-CP8 | -| M3 | Arango evidence and compiler release gate | CP9 | -| M4 | Thin recipe/capability/preview/export layer | reduced G6-G14 | -| M5 | Development-service durability and delivery | reduced G2-G5, G15-G20 | - -Each milestone must leave the repository testable and internally consistent. -Do not merge a new public API before its service and contract tests exist. - ---- - -# Gap 1: No Executable Product Use-Case Contract - -## Current state - -The repository has examples and a working browser builder, but it does not have -a machine-readable set of user conversations defining what the product must -successfully create. - -Without those fixtures, planner expansion can become an unbounded attempt to -support all graph shapes, and frontend work can expose capabilities the backend -does not implement. - -## Target state - -The repository contains a versioned conformance corpus covering at least these -recipe families: - -- patient cohort -- specimen inventory -- file manifest -- diagnoses -- labs/observations -- study enrollment - -Each fixture declares the user's words, normalized intent, required dataset -features, expected recipe, expected row grain, output schema, and support state. - -## Implementation plan - -1. Create `conformance/recipes/`. -2. Define a JSON fixture schema in `conformance/recipes/schema.json`. -3. Give every fixture these fields: - - `id` - - `description` - - `conversation` - - `projectFixture` - - `requiredResources` - - `requiredRelationships` - - `expectedRecipe` - - `expectedColumns` - - `expectedGrain` - - `expectedWarnings` - - `expectedSupportState` -4. Add positive fixtures for all six recipe families. -5. Add ambiguity fixtures, for example "files by patient" without an explicit - row grain. -6. Add unsupported fixtures, including missing relationships and unknown - clinical concepts. -7. Reuse `META/` for baseline and representative end-to-end cases. Add small - NDJSON datasets under `conformance/data//` only for isolated, - missing, ambiguous, or failure cases that `META/` cannot express. -8. Create `conformance/run_conformance.py` as an orchestrator that can invoke Go - tests or a running server and produce JSON results. Keep correctness logic in - Go tests; the script should coordinate rather than reimplement Loom. -9. Add `make conformance`. -10. Document how to add a fixture. - -## Tests - -- JSON Schema validation for every fixture. -- A Go test that loads every fixture definition and rejects duplicate IDs. -- A conformance smoke test that runs at least one fixture end to end. -- CI must fail when a fixture marked `supported` does not pass. - -## Dependencies - -None. This is M0 and should be implemented first. - -## Exit criteria - -- At least 12 positive and 8 negative/ambiguous conversations exist. -- Every future planner or frontend change can name the fixtures it enables. -- `make conformance` produces a deterministic result summary. - ---- - -# Gap 2: FHIR Version and Schema Support Is Implicit - -## Current state - -The server defaults to `schemas/graph-fhir.json`. The generated loader supports -only generated resource cases, while the generic loader requires a matching -class in the graph schema. The loaded project does not persist which FHIR -version, schema digest, generator version, or ingestion mode produced it. - -## Target state - -Every project load is tied to an explicit, inspectable schema identity. Unknown -profiles and extensions remain in the raw payload. Unsupported resource types -fail during preflight with a complete report rather than partway through load. - -## Implementation plan - -1. Maintain `internal/graphschema` with: - - `FHIRVersion` - - `SchemaName` - - `SchemaVersion` - - `SchemaSHA256` - - `GeneratorVersion` - - `GeneratedResourceTypes` -2. Compute the schema digest at process startup. -3. Add a `loom_dataset` collection containing one document per project and - dataset generation. -4. Persist schema identity in the dataset document before ingestion begins. -5. Add ingestion preflight that scans filenames and the first bounded number of - records to identify resource types. -6. Compare discovered resource types with: - - graph-schema classes - - generated loader support -7. Select ingestion mode per resource type: - - generated when supported - - generic fallback when the graph schema contains the class - - unsupported otherwise -8. Remove the requirement that a whole load uses one global `UseGeneric` mode. - Retain an override for debugging and parity testing. -9. Return one structured preflight report listing every resource type and - selected mode before writing begins. -10. Add server endpoints/GraphQL fields to inspect active server schema identity - and dataset schema identity. -11. Reject queries when the dataset schema identity is incompatible with the - active semantic/schema metadata unless an explicit migration is run. - -## Files and packages - -- maintain `internal/graphschema/` -- modify `internal/ingest/load.go` -- modify `internal/ingest/row_builder.go` -- modify `internal/ingest/generated_load.go` or generation output contract -- modify `internal/ingest/backend.go` -- modify both command entrypoints - -## Tests - -- generated-supported resource selects generated mode -- schema-known but nongenerated resource selects generic mode -- schema-unknown resource fails preflight without writes -- extension data survives round-trip in `payload` -- schema digest mismatch is detected -- mixed generated/generic load produces graph-equivalent documents - -## Migration - -Existing projects have no dataset identity. Mark them `legacy-unversioned` and -require analysis rebuild before exposing them through the product API. - -## Exit criteria - -- A project always reports the schema under which it was loaded. -- Mixed-resource loads choose the safest available builder automatically. -- Unsupported resource types are reported before mutation. - ---- - -# Gap 3: Loads Are Not Atomic Dataset Operations - -## Current state - -CLI directory loads and synchronous single-file HTTP imports write directly to -live collections. The HTTP route stages one file and waits for completion. A -failed multi-resource load can leave partially updated data and discovery -metadata. - -## Target state - -A dataset load is a durable operation with preflight, generation identity, -progress, error accounting, finalization, and an explicit ready state. Queries -only use finalized generations. - -## Implementation plan - -1. Extend `loom_dataset` with states: - - `PREFLIGHT` - - `LOADING` - - `ANALYZING` - - `READY` - - `FAILED` - - `SUPERSEDED` -2. Assign every load a `dataset_generation` UUID. -3. Store generation on every vertex, edge, and catalog document. -4. Update all indexes and query filters to include project plus active - generation. -5. Introduce `internal/dataset` to own lifecycle transitions. -6. Split ingestion into: - - preflight - - bootstrap - - resource ingestion - - reference reconciliation - - catalog finalization - - analysis - - activation -7. Write into a new generation while the previous generation remains readable. -8. Atomically switch the project's active generation only after validation. -9. Retain the prior generation until a configured cleanup period expires. -10. Replace synchronous HTTP import as the primary deployment path with a load - job endpoint. Keep synchronous import only as a bounded development helper. -11. Persist per-resource counts, rejected rows, validation errors, inserted - edges, elapsed stages, and source checksums. -12. Add cancellation checks through the load loops. -13. Add a cleanup command for superseded/failed generations. - -## Tests - -- failed load never replaces active generation -- previous generation remains queryable during a new load -- activation switches all discovery/query reads consistently -- cancellation leaves a failed/cancelled generation, not a ready one -- cleanup removes only inactive generations -- partial resource failure has a durable error report - -## Dependencies - -Gap 2. - -## Exit criteria - -- A development operator can observe and retry a failed load. -- No query sees a mixture of old and new generations. -- Dataset readiness is a persisted state, not inferred from collection presence. - ---- - -# Gap 4: Reference Integrity Is Not Measured After Load - -## Current state - -Edges are generated during resource ingestion. The system counts edges but does -not publish a post-load integrity report covering dangling targets, duplicate -references, cross-project edges, or relationships that resolve only partially. - -## Target state - -Every finalized dataset has a reference-integrity report used by both operators -and recipe capability analysis. - -## Implementation plan - -1. Add `internal/analysis/referenceintegrity`. -2. Run bounded AQL analyses for every observed relationship tuple. -3. Record: - - total references - - resolved edges - - dangling `_from` and `_to` counts - - distinct sources and targets - - duplicate logical reference count - - cross-project/generation violations - - source coverage -4. Persist results in `loom_relationship_analysis` keyed by project, - generation, source type, label, and target type. -5. Classify relationship quality: - - `HEALTHY` - - `SPARSE` - - `PARTIAL` - - `BROKEN` -6. Block generation activation only for structural isolation violations. - Preserve sparse/partial datasets but surface warnings. -7. Feed relationship quality into recipe capability results. -8. Add a CLI inspection command for operators. - -## Tests - -- complete references produce healthy analysis -- dangling target is counted and warned -- cross-project edge fails activation -- sparse relationship remains usable with a warning - -## Dependencies - -Gap 3. - -## Exit criteria - -- Every active dataset has a reference-integrity summary. -- The product never recommends a path without reporting observed coverage. - ---- - -# Gap 5: Catalog Analysis Is Too Shallow for Product Decisions - -## Current state - -`fhir_field_catalog` records path, kind, document count, sample count, bounded -distinct values, and pivot metadata. Reference discovery reports tuple and edge -count. It does not provide resource counts, coverage denominators, value -frequencies, high-cardinality search, fanout percentiles, or freshness metadata. - -## Target state - -A finalized, generation-scoped analysis snapshot answers the questions needed -by recipe selection and guided filtering without expensive ad hoc scans on each -page load. - -## Implementation plan - -1. Create `internal/analysis` as the orchestration package. Keep catalog raw - reads in `internal/catalog`. -2. Add `loom_resource_analysis`: - - document count - - valid/rejected count - - distinct logical ID count - - profile/extension URL frequencies where available -3. Extend field analysis with: - - coverage numerator and denominator - - missing count - - scalar/repeated cardinality classification - - approximate distinct count - - bounded example values - - top values with counts for low/medium-cardinality scalar fields - - sensitivity/display classification hook -4. Extend relationship analysis with: - - distinct source/target counts - - source and target coverage - - average, p50, p95, and max fanout -5. Do not compute unbounded exact distinct values during every load. Introduce - configurable caps and approximate counts. -6. Add on-demand, paged value search for high-cardinality fields. Require a - minimum search prefix and enforce query timeouts. -7. Persist `analysis_version`, generation, start/end timestamps, and status. -8. Make analysis idempotent and restartable. -9. Extend cache keys with dataset generation and analysis version. -10. Invalidate cache on generation activation rather than individual resource - writes. -11. Add service methods for dataset summary, relationship inventory, candidate - fields, and value suggestions. - -## Suggested packages - -```text -internal/analysis/ - service.go - resource.go - relationships.go - fields.go - values.go - types.go -``` - -## Tests - -- coverage uses the correct resource denominator -- auth scope changes counts and values correctly -- fanout percentiles match a deterministic fixture -- truncated value sets explicitly report truncation -- on-demand value search cannot escape project/generation/auth scope -- analysis rerun replaces one snapshot without duplicate records - -## Dependencies - -Gaps 3 and 4. - -## Exit criteria - -- The frontend can render recipe availability, columns, examples, coverage, and - filters without issuing raw AQL. -- Analysis has a known version and freshness state. - ---- - -# Gap 6: No Versioned Product Recipe Model - -## Current state - -The public input is a compiler-oriented GraphQL tree of roots, selectors, -traversals, aggregates, pivots, and slices. There is no stable artifact that -captures user intent independently of current planner internals. - -## Target state - -The product owns a versioned recipe model that can be created by the frontend, -CLI, or future language interface and translated into the existing dataframe -builder. - -## Implementation plan - -1. Add `internal/recipe`. -2. Define V1 types: - - `Recipe` - - `TemplateID` - - `Grain` - - `ColumnSelection` - - `Filter` - - `Sort` only if required for deterministic slices - - `Destination` -3. Use stable semantic IDs, not raw AQL or display labels. -4. Include: - - recipe version - - template version - - project - - dataset generation constraint or compatibility policy - - row grain - - selected columns - - filters - - user-provided output names -5. Define JSON Schema and GraphQL input/output types. -6. Implement normalization: - - trim names - - canonicalize operators - - deduplicate columns - - apply explicit defaults - - produce deterministic serialization -7. Implement `Recipe -> dataframe.Builder` translation behind an interface. -8. Keep `FhirDataframeInput` as an advanced/developer contract during - migration; do not force the browser product to construct it. -9. Return typed error codes with field paths in addition to messages. -10. Add migration functions from V1 to future recipe versions. - -## Tests - -- JSON and GraphQL round trips -- deterministic normalization -- duplicate/output-name collision rejection -- V1 fixture translation into expected builder input -- unknown version fails with an actionable error - -## Dependencies - -Gap 1. It can begin in parallel with analysis but cannot be declared usable -until Gap 7 exists. - -## Exit criteria - -- User intent can be saved without storing GraphQL or AQL. -- Conformance fixtures use the real recipe types. - ---- - -# Gap 7: No Template and Semantic Vocabulary Registry - -## Current state - -Friendly field references and some traversal semantics exist, but there is no -product registry defining recipe families, grains, common columns, synonyms, -required relationships, or destination compatibility. - -## Target state - -Templates are versioned code/data definitions that map user concepts to FHIR -semantics and declare capability requirements without embedding canned AQL. - -## Implementation plan - -1. Add `internal/recipe/templates`. -2. Define `TemplateDefinition` with: - - stable ID/version - - title/description - - supported grains - - required and optional semantic relationships - - suggested/common/advanced columns - - common filters - - required planner capabilities - - stable identifier strategy - - supported destinations -3. Define `SemanticField` with: - - semantic ID - - labels and synonyms - - candidate resource types/profiles - - prioritized `fieldRef`/selector fallbacks - - data kind - - sensitivity classification -4. Define `SemanticRelationship` separately from physical edge labels. -5. Consolidate the mechanically derived field references in - `graphqlapi/dataframe/fieldrefs.go` and the optimized traversal roles in - `internal/dataframe/traversal_rules.go` behind these stable concepts. Avoid - leaving product semantics split across unrelated packages. -6. Implement initial template definitions for all six product families. -7. Add a capability evaluator that intersects: - - template requirements - - active dataset analysis - - planner capabilities -8. Return `AVAILABLE`, `DEGRADED`, or `UNAVAILABLE` with reasons. -9. Add human-readable fallback text, but keep reason codes stable. -10. Make registry validation run in tests and process startup. - -## Tests - -- every template references valid semantic fields/relationships -- every suggested column has at least one resolver -- capability evaluation handles missing resources, sparse links, and planner - limitations -- synonyms do not create ambiguous IDs - -## Dependencies - -Gaps 5 and 6. - -## Exit criteria - -- The UI can ask "What are you making?" using backend-returned templates. -- An unavailable template includes a concrete reason. - ---- - -# Gap 8: No Unified Capability API - -## Current state - -The current introspection operation returns fields and one-hop related resource -hints. Clients could combine those records, but they cannot ask whether a full -recipe is supported, what grain is viable, or what warnings apply. - -## Target state - -One backend service is authoritative for dataset summary, templates, recipe -options, value suggestions, validation, and explanation. - -## Implementation plan - -1. Add `internal/productapi` or extend `graphqlapi/dataframe` only if the - latter remains semantically accurate. -2. Implement service methods: - - `DatasetSummary` - - `ListRecipeTemplates` - - `RecipeOptions` - - `FieldValueSuggestions` - - `ValidateRecipe` - - `ExplainRecipe` -3. Add matching GraphQL queries while retaining current introspection for - advanced clients. -4. Every input must resolve project, active generation, and auth paths before - analysis reads. -5. Return stable support and warning codes. -6. Include analysis freshness and dataset generation in responses. -7. Add request cost limits for recursive option expansion and value search. -8. Cache read-only results by project, generation, auth scope, template, grain, - and analysis version. - -## Tests - -- GraphQL contract tests for every new operation -- cross-scope cache isolation -- unavailable template reason propagation -- stale analysis response behavior -- no raw selector/AQL required in the primary recipe-options flow - -## Dependencies - -Gaps 5-7. - -## Exit criteria - -- A thin frontend can implement the full conversation using documented API - calls. -- Backend capability responses never advertise a planner-unsupported path. - ---- - -# Gap 9: No Recipe Persistence and Ownership - -## Current state - -Recipes cannot be saved, named, versioned, cloned, or audited. - -## Target state - -Users can save reusable recipes while Loom preserves ownership, normalized -content, compatibility state, and execution history. - -## Implementation plan - -1. Add `loom_recipe` collection. -2. Store: - - recipe ID - - project - - owner subject - - name/description - - normalized recipe JSON - - recipe/template versions - - created/updated timestamps - - last validated generation - - optimistic concurrency revision -3. Implement create, read, update, clone, list, and archive operations. -4. Enforce project authorization and owner/admin mutation policy. -5. On load, revalidate against the active dataset generation and report: - - compatible - - compatible with warnings - - incompatible -6. Never silently rewrite a saved recipe after template changes. -7. Add explicit migration/upgrade operation producing a new revision. - -## Tests - -- owner and project authorization -- optimistic concurrency conflict -- generation revalidation -- template upgrade preserves the previous revision -- archived recipes do not appear by default - -## Dependencies - -Gaps 6-8. - -## Exit criteria - -- A recipe is a durable, auditable product artifact. -- Loading new data cannot silently change saved intent. - ---- - -# Gap 10: Planner Is Patient-Root and Hardcoded to One Family - -## Current state - -`internal/dataframe/planner.go` rejects non-Patient roots. Supported traversal -tuples are classified in a hardcoded switch in `traversal_rules.go`. The -lowered plan is optimized around patient/case/assay and document summary sets. - -## Target state - -The planner supports declared row grains for Patient, Specimen, -DocumentReference/File, Condition/Diagnosis, Observation, and ResearchSubject -or study enrollment. Optimization remains explicit, testable, and safe. - -## Implementation plan - -This gap is superseded by CP0-CP9 in -[`COMPILER_FIRST_PLAN.md`](COMPILER_FIRST_PLAN.md). The steps below remain a -summary only; do not assign Gap 10 as one worker packet. - -1. Refactor planning into three layers: - - semantic logical plan - - generic physical plan - - optional optimized physical rewrites -2. Introduce `LogicalPlan` types that explicitly represent: - - grain/root set - - joins/traversals - - projections - - filters - - grouping/aggregation - - pivots - - representative slices -3. Replace the Patient check with grain-specific root planning. -4. Implement a generic one-hop/n-hop traversal physical operator using - schema-validated relationship definitions. -5. Preserve current patient-case-assay logic as an optimization rule over the - generic plan, not the only compilable shape. -6. Replace the switch-only traversal registry with data-driven semantic rules - generated or validated against `fhirschema`. -7. Add planner capability descriptors so Gap 7 can ask what operations are - implemented for each grain/path. -8. Implement grains in this order: - - Patient - - Specimen - - DocumentReference/File - - Condition/Diagnosis - - Observation - - ResearchSubject/Study enrollment -9. For each grain, define stable row identity and duplicate semantics. -10. Reject cycles and cap traversal depth in V1. -11. Add plan normalization to ensure semantically equivalent recipes compile - consistently. -12. Add plan explain output separate from raw AQL. - -## Suggested file split - -```text -internal/dataframe/planner/ - logical.go - capabilities.go - validate.go - physical.go - generic.go - optimize_patient.go - explain.go -``` - -Perform the move incrementally; do not rewrite the compiler and planner in one -unreviewable change. - -## Tests - -- golden logical plans for every conformance recipe -- root-grain tests for all six grains -- no duplicate rows unless the recipe explicitly requests an exploding grain -- cycle/depth rejection -- old Patient queries compile to equivalent AQL/results -- generic and optimized plans have result parity - -## Dependencies - -Gaps 1, 6, and 7. - -## Exit criteria - -- Every initial template has at least one supported grain. -- Planner support is queryable rather than inferred from error strings. -- Current optimized Patient behavior remains covered by parity tests. - ---- - -# Gap 11: Filters and FHIR Selection Semantics Are Too Narrow - -## Current state - -Selectors support a constrained path grammar and predicates are largely -`contains`/equality-oriented. The recipe experience needs typed filters, -missing-value rules, terminology-aware code selection, numeric/date comparison, -and repeated-value semantics. - -## Target state - -Recipe filters use a typed, safe expression model compiled to bound AQL. The -supported subset is explicit and sufficient for the six initial templates. - -## Implementation plan - -1. Define typed recipe filter operators: - - `EQUALS` - - `NOT_EQUALS` - - `IN` - - `EXISTS` - - `MISSING` - - `CONTAINS_TEXT` - - `GT`, `GTE`, `LT`, `LTE` - - bounded date/range operators -2. Define repeated-value semantics: - - `ANY` - - `ALL` only if required - - `NONE` -3. Add data types to semantic fields and candidate-column responses. -4. Validate operator compatibility before planning. -5. Compile all values through bind variables; never interpolate user values. -6. Add terminology representation for code/system/display triples. Do not rely - only on display strings when a code is available. -7. Implement missing/null semantics consistently for absent path, null value, - and empty array. -8. Add timezone policy for FHIR date/dateTime/instant comparisons. -9. Expose filter capabilities per field through Recipe Options. -10. Add cost limits for large `IN` lists. - -## Tests - -- operator/type compatibility -- array `ANY` semantics -- missing versus empty versus null -- code/system matching -- date boundary behavior -- bind-variable injection safety -- auth filters remain present in every compiled traversal - -## Dependencies - -Gaps 6, 7, and 10. - -## Exit criteria - -- Every initial conversation fixture can express its filters without raw - FHIRPath or AQL. -- Filter semantics have result-based tests, not only query-string tests. - ---- - -# Gap 12: Cardinality and Query Cost Are Not Explained - -## Current state - -The compiler can aggregate and slice, but the product does not explain whether -a relationship multiplies rows, produces arrays, or creates a costly plan. -Users can request a technically valid shape that is surprising or expensive. - -## Target state - -Recipe validation returns row-grain, cardinality, null-coverage, and cost -warnings before preview or export. - -## Implementation plan - -1. Add `PlanAnalysis` to the planner result. -2. For every relationship classify: - - one-to-one observed - - optional-one observed - - one-to-many - - many-to-many/unknown -3. Use dataset fanout analysis from Gap 5. -4. Estimate base rows, output rows, scanned vertices, and maximum fanout. -5. Mark each selected column as: - - scalar - - repeated array - - aggregated - - pivoted - - row-expanding -6. Return stable warnings such as: - - `HIGH_FANOUT` - - `SPARSE_COLUMN` - - `ROW_EXPANSION` - - `HIGH_CARDINALITY_PIVOT` - - `EXPENSIVE_VALUE_FILTER` -7. Add configurable preview/export cost policies. -8. Require explicit acknowledgement or asynchronous export for plans above the - synchronous threshold. -9. Surface AQL optimizer/explain diagnostics in developer mode only. - -## Tests - -- deterministic cardinality estimates on fixture datasets -- warning thresholds -- high-fanout plan cannot run synchronously -- scalar versus array output classification -- developer explain contains no credentials or sensitive values - -## Dependencies - -Gaps 5 and 10. - -## Exit criteria - -- The user can understand what one row means before running the query. -- Loom blocks obviously unsafe synchronous plans. - ---- - -# Gap 13: Preview Is Not a Bounded Production API - -## Current state - -Preview uses a row limit but collects every returned row in a Go slice. Cursor -input is not a real paging implementation. There are no explicit query timeout, -concurrency, cancellation, response-size, or stable-order guarantees. - -## Target state - -Preview is a bounded, cancellable API with deterministic pagination and resource -limits. - -## Implementation plan - -1. Create a preview-specific service rather than overloading export behavior. -2. Define hard server limits: - - maximum requested rows - - maximum encoded bytes - - maximum query duration - - maximum concurrent previews per subject/project -3. Establish a stable ordering and cursor contract based on grain identity plus - a tie-breaker. -4. Compile cursor predicates rather than offset pagination. -5. Return `nextCursor`, truncated state, elapsed time, and warnings. -6. Cancel the Arango cursor when the request context ends. -7. Stop row iteration when encoded-byte or row limits are reached. -8. Reject preview for plans above the configured synchronous cost threshold. -9. Keep a small in-memory result only up to the enforced maximum. -10. Add request-level metrics and structured error codes. - -## Tests - -- stable next-page behavior with duplicate sort values -- cancellation closes the query cursor -- byte limit truncates safely -- maximum concurrency enforcement -- timeout maps to a stable API error -- authorization scope is identical across pages - -## Dependencies - -Gaps 10-12. - -## Exit criteria - -- Preview cannot consume unbounded Loom memory. -- Pagination neither skips nor duplicates rows for a stable dataset generation. - ---- - -# Gap 14: No Shared Streaming Export Runtime - -## Current state - -The row executor uses a callback, but the dataframe service materializes all -rows into `Result`. File export remains CLI/canned-query territory. - -## Target state - -One streaming runtime executes a compiled plan and feeds bounded destination -writers without materializing the full dataframe. - -## Implementation plan - -1. Define `RowStream`/`RowSink` interfaces in `internal/export` or - `internal/dataframe/stream`. -2. Make the stream provide: - - schema/columns before or with first row - - rows - - progress counters - - close/cancel/error semantics -3. Refactor query execution so preview and export share compilation but use - different consumers. -4. Implement NDJSON writer with one flat object per line. -5. Implement CSV writer with deterministic columns and configurable array - encoding policy. -6. Add checksum, byte count, row count, start/end timestamps, and recipe/plan - provenance. -7. Write to a temporary artifact and atomically finalize it. -8. Define an artifact-store interface. Implement local filesystem first, with - configuration that makes later object storage possible. -9. Add maximum artifact size and disk-space preflight. -10. Ensure cancellation deletes incomplete temporary artifacts or marks them - failed for cleanup. - -## Tests - -- million-row synthetic stream keeps memory bounded -- NDJSON is valid and newline-terminated -- CSV columns remain stable when rows omit values -- cancellation removes incomplete artifact -- checksum and counts match content -- preview/export result parity for the same recipe and generation - -## Dependencies - -Gaps 10-13. - -## Exit criteria - -- Export memory usage is independent of total row count. -- CSV and NDJSON use the same compiled plan and row semantics. - ---- - -# Gap 15: No Durable Job System - -## Current state - -Imports run synchronously and exports do not exist. There is no persistent -queue, lease, retry, cancellation, progress, or recovery after process restart. - -## Target state - -Load, analysis, export, and Elasticsearch delivery execute as durable jobs with -observable state. - -## Implementation plan - -Implement this gap in three packets: - -- **15A:** generic job substrate, required by Gap 3 dataset load jobs -- **15B:** export recovery and row-stream progress, after Gap 14 -- **15C:** retention and operational hardening - -1. Add `loom_job` collection and `internal/jobs` as 15A. -2. Define job types: - - `DATASET_LOAD` - - `DATASET_ANALYSIS` - - `DATAFRAME_EXPORT` - - `ELASTICSEARCH_LOAD` - - `GENERATION_CLEANUP` -3. Define states: - - `QUEUED` - - `RUNNING` - - `SUCCEEDED` - - `FAILED` - - `CANCELLING` - - `CANCELLED` -4. Persist payload reference, project, generation, owner, progress, attempts, - lease owner/expiry, timestamps, result, and structured error. -5. Implement an in-process worker using Arango-backed leasing first. Keep the - queue interface replaceable. -6. Use atomic compare/update for job claims. -7. Renew leases and reclaim expired jobs after restart. -8. Classify retryable versus terminal errors. -9. Make destination writes idempotent or resume-safe before enabling retries. -10. Add create/status/list/cancel API operations. -11. Enforce per-project and global concurrency. -12. Add retention and cleanup policies. - -## Tests - -- two workers cannot own one lease -- expired lease is reclaimed -- process restart resumes or safely retries work -- cancellation propagates to Arango query and sink -- terminal validation errors are not retried -- status authorization prevents cross-project access - -## Dependencies - -Gap 2 for identity fields. Gap 3 depends on 15A. Gap 14 is required for 15B -export behavior. Gap 18 consumes 15C operational behavior. - -## Exit criteria - -- A server restart does not erase job history or strand running work forever. -- Operators and users can see progress and failure reasons. - ---- - -# Gap 16: No Elasticsearch Destination - -## Current state - -Loom does not validate an Elasticsearch target, create bulk payloads, handle -partial bulk failures, or record delivery provenance. - -## Target state - -A validated recipe can stream flat documents into Elasticsearch through a -durable job with deterministic IDs, bounded batches, retry safety, and an -auditable result. - -## Implementation plan - -1. Add `internal/export/elasticsearch` implementing the shared row sink. -2. Define destination configuration by secret reference, never inline password: - - endpoint - - credential/secret reference - - index or alias - - TLS settings - - operation mode (`create` or `index`) -3. Add connection and permission preflight. -4. Require a deterministic document ID strategy. Default to a stable hash of: - - project - - dataset generation or logical dataset identity - - recipe ID/version - - row-grain identity -5. Validate that every recipe can produce the identity fields before enabling - Elasticsearch. -6. Implement NDJSON Bulk API encoding with action and source lines. -7. Batch by both document count and encoded bytes. -8. Parse every bulk item response. Treat HTTP success with item failures as a - partial failure. -9. Retry only retryable items with capped exponential backoff and jitter. -10. Persist success/failure counts and a bounded error sample. -11. Provide optional mapping preflight: - - infer Loom output types - - compare against existing mappings - - report conflicts before job start -12. Do not silently create or replace production index templates. Make index - provisioning an explicit policy/configuration. -13. Support alias-based promotion as a later, separate operation if full index - rebuild workflows require it. -14. Redact endpoint credentials and sensitive row values from logs. - -## Tests - -- exact bulk wire format and final newline -- byte/count batch boundaries -- deterministic IDs across retries -- 429/503 retry behavior -- mixed-success bulk response handling -- mapping conflict preflight -- cancellation and timeout -- secrets absent from logs and persisted job payloads - -## Dependencies - -Gaps 14 and 15. - -## Exit criteria - -- A retry cannot create uncontrolled duplicate documents. -- Partial failures are visible and attributable. -- Elasticsearch delivery and file export have identical row content. - ---- - -# Gap 17: Authorization Is Not Yet Proven End to End - -## Current state - -Project and auth-resource-path scoping exists across discovery and dataframe -queries. Production features will add analysis snapshots, saved recipes, jobs, -artifacts, and external destinations, all of which create new authorization -surfaces. - -## Target state - -The same resolved principal scope applies to discovery, validation, preview, -export, artifact access, and Elasticsearch delivery. No cached or persisted -derived data leaks information across scopes. - -## Implementation plan - -1. Write an authorization matrix covering every API operation and stored - collection. -2. Centralize project/scope resolution in a request-scoped service dependency. -3. Store the effective auth scope or an immutable scope fingerprint on jobs and - artifacts. -4. Decide whether saved recipes store requested scope, inherit runtime scope, or - both. Prefer runtime reauthorization. -5. Ensure analysis responses are either: - - computed per auth scope; or - - safely filtered from scope-partitioned aggregates -6. Include generation and auth scope in all cache keys. -7. Authorize artifact download independently from job creation. -8. Add SSRF protections for Elasticsearch endpoints: - - configured allowlist or administrator-managed destinations - - block arbitrary user-provided internal URLs -9. Add audit events for load, recipe mutation, export creation, artifact access, - and external delivery. -10. Add query and export rate limits by subject/project. - -## Tests - -- two auth paths in one project see different analysis and preview results -- cache cannot leak values between scopes -- job creator losing access cannot download artifact -- arbitrary Elasticsearch URL is rejected -- audit event contains identity and action but not sensitive data - -## Dependencies - -Cross-cutting. Add tests as each earlier gap lands; complete before M5. - -## Exit criteria - -- An end-to-end isolation suite covers discovery through destination delivery. -- Security-sensitive destinations are administrator-controlled. - ---- - -# Gap 18: Service Operations and Deployment Are Incomplete - -## Current state - -The server has a liveness endpoint and local flags. There is no readiness check, -graceful worker drain contract, formal configuration validation, deployment -manifest, or dependency health report. - -## Target state - -Loom can be deployed to a development server, restarted safely, configured by -environment/secret references, and inspected by an operator. - -## Implementation plan - -1. Separate liveness and readiness: - - `/healthz`: process alive - - `/readyz`: Arango reachable, schema compatible, migrations complete, - artifact store writable, worker ready -2. Add startup configuration validation and a sanitized configuration summary. -3. Add graceful shutdown: - - stop accepting new jobs - - cancel/finish bounded HTTP requests - - release or expire worker leases - - close Arango clients -4. Add explicit database migration/version collection. -5. Add container image and non-root runtime verification. -6. Add a development deployment example, preferably Helm values/manifests if - that matches the surrounding platform. -7. Move secrets to environment/file references. -8. Configure request body, preview, query, job, artifact, and retention limits. -9. Add backup/restore notes for Loom metadata collections separately from raw - reloadable FHIR data. -10. Add an operator runbook for: - - failed load - - stale analysis - - stuck job - - disk pressure - - Arango outage - - Elasticsearch partial failure - -## Tests - -- readiness fails when Arango is unavailable -- readiness fails for pending migration -- SIGTERM releases work safely -- invalid configuration fails before serving traffic -- container smoke test loads a small dataset and previews a recipe - -## Dependencies - -Gaps 2, 3, 14, and 15. - -## Exit criteria - -- A clean development environment can deploy Loom from documented artifacts. -- Restart and dependency failure behavior is documented and tested. - ---- - -# Gap 19: Observability and Performance Budgets Are Missing - -## Current state - -The repository emits logs and ingest progress events, but it does not define -service-level metrics or budgets for analysis, planning, preview, export, and -jobs. - -## Target state - -Operators can determine whether Loom is healthy and developers can detect -performance regressions before deployment. - -## Implementation plan - -1. Add Prometheus-compatible metrics for: - - request count/latency/error by operation - - active Arango queries - - planner duration and support failures - - preview rows/bytes/duration - - job queue depth and age - - job duration/retries/failures - - export rows/bytes/throughput - - Elasticsearch item retries/failures - - analysis age and duration - - cache hit/miss/eviction -2. Add structured fields: - - request ID - - job ID - - project - - generation - - recipe ID - - plan profile - Do not log PHI field values. -3. Define initial budgets: - - recipe options p95 - - validation/explain p95 - - preview p95 for fixture sizes - - maximum preview memory - - export throughput and memory -4. Add benchmark fixtures at small, medium, and development-scale sizes. -5. Capture Arango `EXPLAIN` output for golden performance cases and assert - critical indexes/plan properties without overfitting the full plan text. -6. Add load tests for concurrent previews and background exports. -7. Document measurement commands and expected ranges. - -## Tests - -- metrics endpoint smoke test -- sensitive value redaction -- benchmark memory assertions for streaming export -- performance regression thresholds in a nonflaky scheduled CI job - -## Dependencies - -Instrument each gap as implemented; finish after Gap 16. - -## Exit criteria - -- A slow preview or stalled export can be diagnosed without attaching a - debugger. -- Product-critical paths have written budgets and repeatable benchmarks. - ---- - -# Gap 20: Test Coverage Does Not Yet Prove Product Generality - -## Current state - -There are strong focused unit tests and some Arango integration tests, but no -matrix proving mixed-resource ingestion, analysis, multiple row grains, -authorization isolation, restart recovery, and delivery parity. - -## Target state - -CI and scheduled tests prove the supported product envelope. - -## Implementation plan - -1. Organize tests into: - - fast unit tests - - contract tests - - conformance tests - - Arango integration tests - - end-to-end deployment smoke tests - - scheduled performance tests -2. Build dataset fixtures representing: - - complete research dataset - - sparse dataset - - dangling references - - extension-heavy resources - - multiple auth paths - - high-fanout observations - - high-cardinality filter values -3. For every supported recipe, assert: - - availability - - normalized recipe - - logical plan - - output schema - - row identity/cardinality - - preview/export parity -4. Add generated/generic ingestion parity for every generated resource type. -5. Add fuzz tests for selector parsing, recipe normalization, cursor decoding, - and bulk response parsing. -6. Add failure injection for Arango cursor errors, filesystem full, worker - restart, and Elasticsearch partial failure. -7. Add compatibility tests for saved V1 recipes across future template/planner - changes. -8. Publish the supported capability matrix from conformance results. - -## Dependencies - -Gap 1 and all implementation gaps as they land. - -## Exit criteria - -- "Supported" means a passing end-to-end fixture, not an available UI control. -- The development deployment runs the same conformance suite used by CI. - ---- - -# 5. Thin Frontend Plan - -The frontend is intentionally not a numbered backend gap because it should be -built after M2 exposes honest capabilities. - -Implement it in this order: - -1. project/dataset readiness screen -2. backend-provided recipe gallery -3. "one row per" grain selection -4. suggested/searchable populated columns -5. guided filters using value suggestions -6. validation and cardinality warnings -7. bounded preview -8. save recipe -9. start/export job and status view -10. advanced developer diagnostics behind an explicit disclosure - -Frontend acceptance test: - -> A user who knows the desired data but does not know FHIRPath, GraphQL, AQL, -> or Loom can complete the supported conformance tasks without developer help. - -The former hard-coded `/builder` demo was removed: it encoded one GDC-shaped -graph editor and could not use the compiler-safe capability contract. Developer -diagnostics should use compiler explanations and conformance fixtures; the -product interface should be the guided recipe flow. - -# 6. Recommended Execution Order - -An implementation agent should use this dependency-aware sequence: - -1. CP0: compiler corpus, reference semantics, and baselines -2. CP1-CP2: semantic IR and generated FHIR graph semantics -3. CP3-CP4: row grain/cardinality and typed filters -4. CP5-CP6: correct generic lowering plus aggregate/pivot engine -5. CP7: typed AQL physical IR and code generation -6. CP8: explicit optimization passes -7. CP9: Arango EXPLAIN, cost, indexes, and performance gates -8. Reduced G6-G8: recipe presets and thin capability API over the compiler -9. Reduced G12-G14: explain, bounded preview, and row-stream sinks -10. G2-G5 as needed for durable arbitrary dataset lifecycle and statistics -11. G15-G20 for jobs, destinations, deployment, security, and release proof - -Recipe persistence and frontend expansion should follow compiler validation and -explain. They are not prerequisites for compiler implementation. - -For every gap, the implementing agent must: - -1. inspect the named current owners before editing -2. add or update public types before generated GraphQL artifacts -3. keep authorization and generation in every query -4. write unit tests before or with implementation -5. add integration evidence for database behavior -6. update the capability matrix -7. run `go test ./...` plus the relevant conformance target -8. update developer documentation when the runtime contract changes - -# 7. First Development-Server Release Gate - -The first deployable development release does not require every conceivable -FHIR query. It does require all of the following: - -- explicit FHIR/schema identity -- automatic generated/generic ingestion selection -- atomic dataset generation activation -- load and analysis jobs with persisted status -- dataset/resource/relationship/field analysis -- versioned recipes and six registered template families -- capability API that hides unsupported templates and paths -- at least one passing supported recipe for every advertised family -- Patient, Specimen, File, Diagnosis, Observation, and Study Enrollment grains -- typed filters required by conformance fixtures -- cardinality and cost warnings -- bounded, cancellable preview -- streaming NDJSON and CSV export -- saved recipes -- end-to-end authorization isolation tests -- readiness, graceful shutdown, metrics, and runbook -- a published capability matrix produced by tests - -Elasticsearch delivery should be part of the same release only if deterministic -row IDs, partial-failure handling, and retry safety are complete. Otherwise ship -file export first and keep Elasticsearch disabled rather than exposing an -unreliable destination. - -# 8. Definition of Done for the Product Direction - -Loom has moved beyond demoware when this scenario is routine: - -1. An operator uploads or mounts an unfamiliar, schema-compatible FHIR NDJSON - dataset. -2. Loom preflights every resource type and reports its ingestion strategy. -3. The load completes into a new immutable dataset generation. -4. Loom measures resources, fields, values, references, integrity, coverage, - and fanout. -5. The API offers only recipe templates supported by both the data and planner. -6. A non-technical user selects what they are making, one row per entity, - desired columns, and guided filters. -7. Loom returns a normalized recipe, warnings, and an understandable row-grain - explanation. -8. Preview is fast, bounded, and representative. -9. Export survives process restart, streams with bounded memory, and records - provenance. -10. The resulting flat data is identical whether written as NDJSON, CSV, or - delivered to Elasticsearch. -11. Operators can observe, cancel, retry, and diagnose every long-running step. -12. The advertised capability is backed by passing conformance tests. - -That is the production boundary this plan is designed to reach. diff --git a/docs/LUNA_COMPILER_FINALIZATION_PLAN.md b/docs/LUNA_COMPILER_FINALIZATION_PLAN.md deleted file mode 100644 index 25a85f1..0000000 --- a/docs/LUNA_COMPILER_FINALIZATION_PLAN.md +++ /dev/null @@ -1,646 +0,0 @@ -# Luna Execution Plan: Finish the Physical Compiler and Delete Compatibility Lowering - -## Outcome - -Complete the remaining physical compiler features and remove the compatibility -compiler. The final production request path must be: - -```text -GraphQL input - -> graphqlapi/dataframe input mapping - -> dataframe.Builder - -> BuildSemanticPlan - -> BuildPhysicalPlan - -> physical optimization - -> RenderPhysicalPlan - -> Arango execution -``` - -The following symbols must have no production definition or call site when the -plan is finished: - -- `Compile` -- `Lower` -- `lowerSemanticBuilder` -- `compileLowered` -- `NamedSet` -- `DerivedField` -- lowered-only `RepresentativeSlice` -- `compiler` and its string-oriented helper methods - -Do not copy old AQL string helpers into the physical renderer. Migrate their -semantic meaning into typed physical operations, prove execution parity, then -delete the old implementation. - -## Repository facts Luna must use - -This repository already contains the FHIR backbone. Do not infer FHIR paths or -relationships from fixture names. - -- `fhirstructs/`: generated FHIR Go structs, validation, and edge extraction. -- `fhirschema/`: generated resource, selector, traversal, and pivot metadata. -- `META/`: real sample FHIR NDJSON. -- `internal/dataframe/storage_route.go`: authoritative physical edge-direction - evidence. Unknown outbound/ANY routes remain compiler errors. -- `internal/dataframe/semantic_plan.go`: backend-independent request meaning. -- `internal/dataframe/physical_plan.go`: typed renderer-independent execution - IR. -- `internal/dataframe/generic_physical_plan.go`: current semantic-to-physical - lowering. -- `internal/dataframe/physical_render.go`: current physical AQL renderer. -- `graphqlapi/schema.graphqls`: public input/output contract. -- `conformance/compiler/fixtures/`: compiler fixtures. - -Use `fhirschema` validation for selector/pivot decisions and the populated -catalog for bounded pivot columns. Never add Patient-, GDC-, or fixture-specific -compiler branches. - -## Current physical coverage - -The physical route currently handles: - -- root fields and typed filters; -- optional child fields and typed child filters; -- required traversal matches; -- inbound and proven outbound storage routes; -- root and child aggregates: `COUNT`, `COUNT_DISTINCT`, `EXISTS`, - `DISTINCT_VALUES`, `MIN`, `MAX`, plus predicate-qualified reductions; -- root execution window and generation/auth/project scope. - -The remaining production fallback reasons should be limited to: - -- pivots; -- representative slices; -- nested optional shapes that exceed current physical-set nesting; -- any legacy-only `Builder.DerivedFields`/`Builder.Sets` request assembled - directly by Go callers; -- compatibility traversal-sharing representation. - -Before changing code, run and record: - -```bash -GOCACHE="$(pwd)/.gocache" GOTOOLCHAIN=auto go test ./... -count=1 -GOCACHE="$(pwd)/.gocache" GOTOOLCHAIN=auto go test ./conformance/compiler -bench BenchmarkCompilerOracle -benchmem -run '^$' -``` - -Preserve unrelated dirty-worktree changes. - -## Compiler invariants - -Every work package must retain these properties: - -1. One outer root loop, one returned object per root resource. -2. Required semi-joins execute before root `SORT`/`LIMIT`; optional child work - executes after the root preview window. -3. Every scanned root, child node, and edge is constrained by project, - dataset generation, and authorization scope. -4. Request values, resource types, edge labels, selectors, pivot keys, limits, - and output names never become unvalidated AQL source. -5. Empty-set, null, scalar, array, distinct-array, and fallback behavior is - explicit in physical IR and covered by result tests. -6. Nested optional traversal never becomes a top-level `FOR` and never - multiplies root rows. -7. Stable ordering is explicit. Do not rely on Arango traversal, `UNIQUE`, or - hash-map iteration order. -8. Unsupported routes and expressions fail before rendering. - -## Work package F0 — Inventory legacy meaning before migration - -**Goal:** prove which legacy concepts represent public semantics and which are -only compatibility IR. - -**Read-only inputs** - -- `internal/dataframe/lowered_types.go` -- `internal/dataframe/planner.go` -- `internal/dataframe/generic_lowering.go` -- `internal/dataframe/lowered_compile.go` -- `graphqlapi/dataframe/input_mapping.go` - -**Deliverable:** add a table-driven test or checked-in Markdown table mapping -every legacy operation: - -| Legacy operation | Physical owner | -| --- | --- | -| `ROOT_FIELD` | `PhysicalExtract` scalar | -| `FIRST_NON_NULL` | `PhysicalExtract` with fallbacks, scalar | -| `ALL` | `PhysicalExtract` array | -| `UNIQUE` | `PhysicalExtract` distinct array | -| `COUNT` | `PhysicalAggregate(COUNT)` | -| `COUNT_DISTINCT` | `PhysicalAggregate(COUNT_DISTINCT)` | -| `COUNT_WHERE` | predicate-qualified `PhysicalAggregate(COUNT)` | -| `ANY` | predicate-qualified `PhysicalAggregate(EXISTS)` | -| `MIN`/`MAX` | typed physical aggregates | -| `PIVOT` | `PhysicalPivotMap` | -| representative slice | `PhysicalSlice` | -| `CONST` | classify as public requirement or legacy-only artifact | - -`CONST` must not be added to the physical compiler unless a current GraphQL -input or production caller requires it. If it exists only in tests/manual -lowered builders, delete that support at final cutover. - -**Acceptance:** every `DerivedOp*` constant has an explicit migration or -deletion decision. No worker may create `PhysicalDerivedField` as a generic -replacement for the old union. - -## Work package F1 — Eliminate “derived fields” as a runtime concept - -**Goal:** ensure every public output currently represented as `DerivedField` -is produced directly from `SemanticNode` by typed physical expressions. - -**Files** - -- `internal/dataframe/semantic_plan.go` -- `internal/dataframe/selection_semantics.go` -- `internal/dataframe/generic_physical_plan.go` -- `internal/dataframe/physical_plan.go` -- new `internal/dataframe/physical_derived_parity_test.go` - -**Implementation** - -1. Enumerate all places where `planner.go` creates `DerivedField`. -2. For field value modes: - - `FIRST`/`AUTO` -> scalar `PhysicalExtract` with ordered fallbacks; - - `ALL` -> array `PhysicalExtract`; - - `DISTINCT` -> distinct-array `PhysicalExtract`. -3. For aggregate-derived outputs, use `SemanticAggregate` and - `PhysicalAggregate`; do not lower them through `DerivedField`. -4. For pivot-derived outputs, wait for F2 and lower from `SemanticPivot`. -5. For representative outputs, wait for F3 and lower from `SemanticSlice`. -6. Ensure `CompiledQuery.Columns` and `PivotFields` come from final physical - return metadata, not compatibility builder arrays. -7. Add a test that builds representative public GraphQL shapes, runs - `BuildSemanticPlan`, and proves the physical plan contains no dependency on - `NamedSet` or `DerivedField`. - -**Parity matrix** - -- root scalar/array/distinct/fallback field; -- child scalar/array/distinct/fallback field; -- count/count-where/any/min/max; -- empty child set and all-null selector values. - -**Deletion gate:** after parity, remove production reads of -`Builder.DerivedFields` from `CompileRequest` routing. Directly constructed -legacy builders may remain test-only until F7. - -## Work package F2 — Physical pivots - -**Goal:** lower root and child `SemanticPivot` directly to `PhysicalPivotMap` -and render a bounded object without request-derived AQL keys. - -**Files** - -- `internal/dataframe/physical_plan.go` -- `internal/dataframe/generic_physical_plan.go` -- `internal/dataframe/physical_render.go` -- `internal/dataframe/physical_execution.go` -- `internal/dataframe/pivots.go` -- new `internal/dataframe/physical_pivot_test.go` -- optional `internal/dataframe/physical_pivot_arango_integration_test.go` - -**Preconditions** - -- `Service.expandPivotColumns` has populated a non-empty bounded column list. -- `BuildSemanticPlan` has validated both selectors through - `fhirschema.ValidatePivotSelectors`. - -**IR changes** - -Review the existing `PhysicalPivotMap`. Extend it only if needed to encode: - -- source cardinality: root singleton vs physical child set; -- collision behavior; -- empty/absent column behavior; -- output column order; -- optional value distinctness if required by current behavior. - -Do not add a raw map/AQL expression field. - -**Lowering** - -1. Root pivot source is the root document payload. -2. Child pivot source is the owning `PhysicalSet` variable. -3. Copy parsed key/value selectors and bounded columns into typed IR. -4. Store the column list in a bind variable with a deterministic compiler key. -5. Add the pivot expression to the final return using the semantic name and - mark the projection as a pivot for `CompiledQuery.PivotFields`. - -**Rendering** - -Render a fixed subquery equivalent to: - -```aql -MERGE( - FOR item IN source - FOR key IN key_values - FILTER key IN @allowed_columns - LET values = value_values - COLLECT pivot_key = key INTO grouped - RETURN { [pivot_key]: stable_collision_value(grouped) } -) -``` - -Use the existing legacy tests to determine the exact collision value. Do not -choose `FIRST`, `LAST`, or array aggregation without parity evidence. - -**Tests** - -- root and child pivots; -- sparse column set; -- duplicate key with multiple values; -- array key selector and array value selector; -- empty result object; -- requested keys absent from data; -- bind-safety test proving column strings do not appear in AQL source; -- deterministic output/pivot-field ordering. - -**Live gate:** run a real Observation pivot over `META`, compare legacy and -physical JSON rows, and require no full collection scan plus traversal-edge -index use. - -**Deletion gate:** remove production calls to `compileRootPivot`, -`compilePivotField`, `compileDerivedPivotMapLets`, -`compilePivotMapExpr`, and `compilePivotMapProjection`. - -## Work package F3 — Physical representative slices - -**Goal:** lower `SemanticSlice` to a stable, bounded `PhysicalSlice` for root -and child sources. - -**Files** - -- `internal/dataframe/physical_plan.go` -- `internal/dataframe/generic_physical_plan.go` -- `internal/dataframe/physical_render.go` -- new `internal/dataframe/physical_slice_test.go` -- optional `internal/dataframe/physical_slice_arango_integration_test.go` - -**IR contract** - -`PhysicalSlice` must encode: - -- typed source; -- optional typed predicate; -- deterministic primary sort and `_key` tie breaker; -- positive bind-backed limit; -- ordered nested object projections; -- empty-set behavior (`[]`). - -If the current `Sort` field cannot express primary plus tie-break ordering, -replace it with an ordered `[]PhysicalSortKey`; do not rely on implicit order. - -**Lowering** - -1. Root slice source is the singleton root document. -2. Child slice source is the physical set for its semantic node. -3. Reuse the typed selector/filter lowering used by child filters and aggregate - predicates. -4. Lower each `SemanticSlice.Fields` entry via `ResolveSemanticField`. -5. Bind the limit. Reject zero/negative limits in semantic or physical - validation. - -**Rendering order** - -```aql -FOR item IN source - FILTER typed_predicate - SORT typed_primary, item._key - LIMIT @slice_limit - RETURN { bound_name: typed_projection, ... } -``` - -This subquery belongs inside the returned root object and must not affect the -outer root limit. - -**Tests** - -- zero/one/many source items; -- deterministic tie behavior; -- predicate with and without equality literal; -- scalar/fallback/array nested fields; -- root and child slice; -- auth/generation-scoped source; -- limit is a bind, not an AQL literal. - -**Deletion gate:** remove production calls to `compileRepresentativeSlice`, -`compileRootSlice`, `compileSetSlice`, `compileSliceProjection`, and their -predicate-string helpers after all slice fixtures use physical execution. - -## Work package F4 — Fully general nested optional shaping - -**Goal:** allow any supported field/filter/aggregate/pivot/slice at any proven -optional traversal depth without reopening traversal paths or multiplying root -rows. - -**Files** - -- `internal/dataframe/generic_physical_plan.go` -- `internal/dataframe/physical_plan.go` -- `internal/dataframe/physical_render.go` -- `internal/dataframe/physical_scope.go` -- new `internal/dataframe/physical_nested_shape_test.go` - -**Current limitation to remove** - -Delete every `genericPhysicalNodeUnavailableReason` or lowering rejection for a -child that simultaneously has nested children and selections/shaping. - -**Lowering model** - -1. Each optional semantic child owns exactly one `PhysicalSet`. -2. A nested set captures the parent set-element variable within the parent - subplan. It must not capture the entire parent set and re-flatten unrelated - rows. -3. The owning child output is a typed `PhysicalObject` containing fields, - aggregates, pivots, slices, and nested child objects/arrays. -4. Parent output cardinality is explicit: - - child array projection remains an array; - - scalar representative uses the semantic value mode; - - nested child results remain correlated to their parent item. -5. Route resolution happens once per semantic edge through - `resolveStorageRoute`. - -**Scope proof** - -Extend `PhysicalPlan.Validate` and `ValidateGenericPhysicalPlanScope` to walk -nested subplans recursively. Every traversal inside a subplan must prove edge -and node project/generation/auth predicates before its return or nested set. - -**Required tests** - -- Patient -> Specimen -> DocumentReference fields and aggregates; -- Patient -> Specimen -> Group -> DocumentReference; -- mixed optional and required branches; -- same alias at different depths does not collide; -- zero/many children preserve root count; -- nested outbound ResearchSubject -> ResearchStudy; -- invalid capture and out-of-scope variable rejection. - -**Acceptance:** the checked-in GDC-style dataframe runs physically with no -unsupported nested-shaping reason. - -## Work package F5 — Traversal-prefix sharing optimization - -**Goal:** optimize equivalent physical traversal prefixes after unoptimized -plans have full parity. - -**Files** - -- new `internal/dataframe/physical_optimize.go` -- new `internal/dataframe/physical_optimize_test.go` -- `internal/dataframe/physical_execution.go` -- physical explain/benchmark files - -**Placement** - -```text -SemanticPlan -> BuildPhysicalPlan -> OptimizePhysicalPlan -> RenderPhysicalPlan -``` - -The semantic lowerer must always be capable of producing a correct unshared -plan. Sharing is never performed while walking `SemanticNode`. - -**Equivalence key** - -Two set/traversal prefixes can share only when all match: - -- parent capture identity; -- depth; -- direction; -- edge collection; -- edge label; -- project bind; -- dataset-generation bind; -- authorization scope inputs; -- filter prefix before target-type specialization; -- target edge-type discriminator field. - -Do not include target resource type in the broad-prefix key. Instead, only -after sharing, create typed subsets with both edge discriminator and node -`resourceType` filters. - -**Rewrite** - -1. Hash eligible prefixes by the equivalence key. -2. Keep groups with at least two consumers and at least two target types. -3. Introduce one broad set with a compiler-generated variable. -4. Rewrite each consumer to a typed filtered subset. -5. Preserve consumer request order and source provenance. -6. Re-run physical validation after rewriting. - -**Tests** - -- sibling sharing at root and nested depth; -- differing labels/directions/scopes/filters never share; -- same label but one target type does not create useless broad set; -- optimized/unoptimized execution parity; -- stable deterministic plan across runs; -- no target-type leakage. - -**Performance gate:** live Explain estimated cost and measured warm execution -must be neutral or better on the GDC fixture. Disable the rewrite if it -regresses the representative query. - -**Deletion gate:** remove compatibility `NamedSet` traversal/filter/union and -generic sibling-sharing lowering only after physical sharing owns equivalent -cases. - -## Work package F6 — Result parity and cost benchmark matrix - -**Goal:** make compiler correctness and performance measurable before deleting -the oracle. - -**Files** - -- `internal/dataframe/physical_renderer_parity_test.go` -- new `internal/dataframe/physical_rich_arango_integration_test.go` -- `conformance/compiler/fixtures/` -- `conformance/compiler/benchmark_test.go` -- `cmd/dataframe-query/` -- `examples/` - -**Fixture families** - -1. root fields/filters; -2. child fields/filters; -3. required inbound and outbound match; -4. aggregates with empty/non-empty/predicate cases; -5. root and child pivots; -6. root and child slices; -7. three- and four-hop nested shaping; -8. shared sibling prefixes; -9. restricted, unrestricted, and restricted-empty auth scopes; -10. legacy-null and explicit dataset generations. - -**Result parity harness** - -For each fixture: - -1. compile through physical path; -2. compile through compatibility oracle while it still exists; -3. execute both against the same database and generation; -4. canonicalize only JSON object-key ordering; -5. compare row count, row order, columns, pivot fields, scalar/null/array/object - values, and duplicate behavior; -6. print the first semantic difference with fixture ID and physical operator - provenance. - -Do not sort returned rows unless the public contract declares order irrelevant. - -**Explain gates** - -- no full collection scan; -- scoped root persistent index selected; -- `fhir_edge` edge index selected for every traversal; -- no warning; -- estimated item count does not explode between optional nested stages; -- shared plan estimated cost is neutral or better than unshared. - -**Human-readable benchmark output** - -`cmd/dataframe-query` must report separately: - -- HTTP/server total; -- compile time if exposed by response/diagnostics; -- Arango execution time if exposed; -- rows; -- response bytes; -- rows/second; -- cold first request and warm subsequent request timings. - -Check in a proper GDC-style request under `examples/` that includes fields, -child files, aggregates, observation pivot, representative slices, and nested -groups. - -## Work package F7 — Remove production fallback - -**Preconditions** - -- all supported GraphQL shapes compile physically; -- `genericPhysicalPlanUnavailableReason` has no supported-shape branch; -- F6 parity and Explain matrix is green; -- the GDC-style request executes physically. - -**Implementation** - -1. Replace `CompileRequest` with a single path: - - ```go - semantic, err := BuildSemanticPlan(builder) - if err != nil { return CompiledQuery{}, err } - physical, err := BuildPhysicalPlan(semantic) - if err != nil { return CompiledQuery{}, err } - physical, err = OptimizePhysicalPlan(physical) - if err != nil { return CompiledQuery{}, err } - return compilePhysicalExecution(physical, semantic, limit) - ``` - -2. Remove `compileRequestPlans` and every runtime fallback call. -3. Change unsupported semantics to explicit errors from semantic or physical - validation, not compatibility execution. -4. Retain the compatibility compiler only long enough for F6 tests; do not - expose it through runtime/service code. - -**Acceptance:** `rg` finds no production path from service/GraphQL to -`compileLowered`, `Lower`, or lowered builder types. - -## Work package F8 — Delete compatibility implementation - -Perform this as one focused deletion after F7 is green. - -**Delete or empty by deadcode evidence** - -- `internal/dataframe/lowered_compile.go` -- `internal/dataframe/lowered_types.go` -- compatibility sections of `planner.go`, `generic_lowering.go`, - `compile_fields.go`, relationship-match compilation, and storage-route - adapters; -- old `compiler` struct and AQL string helpers; -- legacy named-set optimization tests; -- compatibility-only fixtures. - -**Remove fields from `Builder` and `TraversalStep`** - -- `Sets` -- `DerivedFields` -- `RepresentativeSlices` -- compatibility plan hints that no longer describe public diagnostics. - -Do not remove public GraphQL fields, aggregates, pivots, or slices. Remove only -the lowered duplicates. - -**Test migration** - -- rewrite tests that assert legacy variable names or AQL substrings to assert - physical operators, bind safety, result parity, or Explain behavior; -- keep precise AQL tests only at renderer boundaries; -- remove direct `Lower -> Compile` conformance calls and use - `CompileRequest`/physical plan fixtures. - -**Final commands** - -```bash -GOCACHE="$(pwd)/.gocache" GOTOOLCHAIN=auto go test ./... -count=1 -GOCACHE="$(pwd)/.gocache" deadcode -test ./... -git diff --check -``` - -With local Arango: - -```bash -LOOM_COMPILER_ARANGO_INTEGRATION=1 \ -GOCACHE="$(pwd)/.gocache" GOTOOLCHAIN=auto \ -go test ./internal/dataframe -run 'Test.*Arango' -count=1 -v - -make dataframe-demo -``` - -**Final source audit** - -```bash -rg -n 'compileLowered|lowerSemanticBuilder|type NamedSet|type DerivedField|Builder\.Sets|Builder\.DerivedFields' . -``` - -Expected result: no production definitions or call sites. Test migration notes -may mention removed symbol names only in historical documentation. - -## Luna merge order and worker ownership - -The compiler hotspots are: - -- `physical_plan.go` -- `generic_physical_plan.go` -- `physical_render.go` -- `physical_execution.go` -- `compile.go` - -Only one integration owner edits those files per wave. - -| Wave | Primary implementation | Parallel safe work | Integration owner | -| --- | --- | --- | --- | -| 0 | F0 inventory | fixture enumeration | compiler core | -| 1 | F1 semantic derived migration | parity cases | physical lowerer | -| 2 | F2 pivots | pivot fixtures/live data inspection | pivot worker | -| 3 | F3 slices | slice fixtures | slice worker | -| 4 | F4 nested shaping | nested parity fixtures | physical lowerer | -| 5 | F5 sharing optimizer | Explain/cost harness | optimizer worker | -| 6 | F6 matrix | benchmark CLI/example | test owner | -| 7 | F7 cutover | deletion inventory | compiler core | -| 8 | F8 deletion | docs cleanup | deletion owner | - -Workers contributing fixtures must not edit compiler hotspot files. F7 and F8 -are serialized: do not delete the oracle while a parity worker still uses it. - -## Definition of done - -- Pivots and slices execute through typed physical expressions. -- Any supported shape can nest at any proven traversal depth without changing - root grain. -- Physical traversal sharing is validated and benchmark-backed. -- Rich-shape parity and Explain gates pass against `META`. -- A human-readable GDC dataframe request runs physically and reports useful - timing/row statistics. -- Production contains one compiler path. -- Compatibility lowering, named sets, derived-field IR, and string compiler - helpers are deleted. diff --git a/docs/LUNA_FRONTEND_ENABLEMENT_PART_5.md b/docs/LUNA_FRONTEND_ENABLEMENT_PART_5.md deleted file mode 100644 index b9b9448..0000000 --- a/docs/LUNA_FRONTEND_ENABLEMENT_PART_5.md +++ /dev/null @@ -1,1103 +0,0 @@ -# Part 5: Luna frontend-enablement execution plan - -## Mission - -Build the thin product API needed for a nontechnical frontend without moving -FHIR knowledge, graph safety, AQL generation, authorization, or dataset -generation logic into the browser. - -This part starts from a working compiler and execution service. It does not -redesign the physical compiler. It adds six frontend-facing capabilities: - -1. dataset and root-resource discovery; -2. guided dataframe templates; -3. compile/validate without execution; -4. structured frontend errors; -5. stable preview paging; -6. streaming NDJSON and CSV export. - -The resulting primary flow is: - -```text -list datasets - -> choose a guided template - -> inspect available fields, filters, traversals, and pivots - -> validate and normalize the request - -> preview bounded pages - -> stream NDJSON or CSV -``` - -Saved recipes, durable jobs, Elasticsearch delivery, a large frontend, and -additional AQL optimization tournaments are outside Part 5. - -## Repository facts Luna must preserve - -Read these files before changing code: - -- `README.md` -- `docs/QUICKSTART.md` -- `docs/DEVELOPER_ARCHITECTURE.md` -- `docs/FORMAL_GAP_ANALYSIS.md` -- `graphqlapi/schema.graphqls` -- `graphqlapi/handler.go` -- `graphqlapi/schema.resolvers.go` -- `graphqlapi/output_mapping.go` -- `graphqlapi/dataframe/service.go` -- `graphqlapi/dataframe/introspection.go` -- `graphqlapi/dataframe/input_resolution.go` -- `graphqlapi/dataframe/input_mapping.go` -- `internal/dataframe/service.go` -- `internal/dataframe/execution.go` -- `internal/dataframe/compile.go` -- `internal/dataframe/builder_types.go` -- `internal/catalog/types.go` -- `internal/catalog/read_fields.go` -- `internal/catalog/read_references.go` -- `internal/dataset/active_resolver.go` -- `internal/dataset/manifest.go` -- `internal/httpapi/server.go` -- `internal/httpapi/routes.go` -- `cmd/arango-fhir-server/main.go` -- `fhirschema/` -- `examples/meta_gdc_case_matrix.graphql` -- `examples/meta_gdc_case_matrix.variables.json` - -Current, real foundations: - -- `dataframeBuilderIntrospection` exposes populated fields, references, - distinct values, pivot candidates, and relationship cardinality. -- `runFhirDataframe` resolves `fieldRef` values, validates the request, lowers - through the physical compiler, executes AQL, and returns flattened rows. -- `dataframe.Service.Stream` already delivers flattened rows without retaining - the entire result in Loom memory. -- active immutable dataset generation resolution is available through - `dataset.ActiveManifestResolver`. -- request principal and authorization-path resolution already exist in - `internal/authscope`. -- the compiler owns FHIR selectors and routes through `fhirschema` and - `resolveStorageRoute`. - -Do not duplicate any of these foundations in a new product package. - -## Global contracts - -Every work package must preserve the following: - -1. The browser never constructs AQL and never decides whether a FHIR route is - physically safe. -2. Public field choices use stable `fieldRef` values. Raw selectors may be - returned for diagnostics but are not required by the primary UI. -3. Project, active generation, and resolved authorization scope are selected - once per request and propagated into every catalog, validation, preview, - and export operation. -4. A restricted caller with no surviving auth-resource paths receives no data; - an empty list must never be reinterpreted as unrestricted. -5. Only READY active generations are advertised in generation-aware mode. -6. Legacy mutable META mode remains supported deliberately. Its generation is - represented consistently as absent/null, not invented. -7. Requests and cursor tokens never embed raw AQL, credentials, or unvalidated - collection names. -8. Output row semantics, column names, pivot flattening, stable root ordering, - and null/array behavior remain identical between preview and export. -9. All lists returned to the frontend have deterministic ordering. -10. No Patient-, Observation-, GDC-, fixture-, or edge-label-specific branch is - allowed in compiler, catalog, pagination, or export code. -11. Template definitions may name FHIR resource types and semantic field - preferences because they are product metadata, but availability must be - proven from the current catalog and `fhirschema`. -12. GraphQL generated files are generated, never manually edited. -13. Preserve unrelated dirty-worktree changes. Stop if an owned file changes - concurrently. - -## Part 5 public API target - -The coordinator should freeze this semantic API before workers begin. Exact -GraphQL naming can change during schema review, but the information and -behavior must remain stable. - -```graphql -type Query { - dataframeDatasets: [DataframeDataset!]! - - dataframeTemplates( - input: DataframeTemplateOptionsInput! - ): [DataframeTemplateAvailability!]! - - dataframeBuilderIntrospection( - input: DataframeBuilderIntrospectionInput! - ): DataframeBuilderIntrospection! - - validateFhirDataframe( - input: FhirDataframeInput! - ): FhirDataframeValidation! -} - -type Mutation { - previewFhirDataframe( - input: FhirDataframeInput! - first: Int = 25 - after: String - ): FhirDataframePage! -} -``` - -`runFhirDataframe` may remain temporarily as the compatibility name during -Part 5, but it must call the same preview service. Do not maintain two -compilation or execution implementations. - -Streaming export is an authenticated HTTP endpoint because GraphQL JSON should -not buffer large row sets: - -```text -POST /api/v1/dataframes/export -Content-Type: application/json -Accept: application/x-ndjson | text/csv -``` - -The request body contains one `FhirDataframeInput`-equivalent request plus an -explicit format. The HTTP adapter must call the same preparation and streaming -service as GraphQL. - -## Shared-file ownership and coordination - -Only the Part 5 coordinator may edit or regenerate: - -- `graphqlapi/schema.graphqls` -- `graphqlapi/generated.go` -- `graphqlapi/model/models.go` -- `graphqlapi/schema.resolvers.go` -- `graphqlapi/resolver.go` -- `graphqlapi/handler.go` -- `graphqlapi/output_mapping.go` -- `cmd/arango-fhir-server/main.go` -- `internal/httpapi/server.go` -- `internal/httpapi/routes.go` -- `README.md` -- `docs/QUICKSTART.md` - -Workers implement typed Go services behind coordinator-frozen interfaces and -submit schema wiring requirements as handoff notes. Do not allow multiple -workers to run `make generate-graphql` concurrently. - -The coordinator performs GraphQL generation once after WP1-WP5 service -contracts are integrated, then again only if the schema changes during final -review. - -## Baseline required before implementation - -Run and record: - -```bash -GOCACHE="$(pwd)/.gocache" GOTOOLCHAIN=auto go test ./graphqlapi ./graphqlapi/dataframe ./internal/dataframe ./internal/catalog ./internal/dataset/... ./internal/httpapi -count=1 -GOCACHE="$(pwd)/.gocache" GOTOOLCHAIN=auto go test ./conformance/compiler -count=1 -make dataframe-demo DATAFRAME_LIMIT=25 DATAFRAME_REPEAT=1 DATAFRAME_PRINT_RESPONSE=false -``` - -Record the loaded database, project, generation mode, result hash, row count, -and current GraphQL schema hash. A failing baseline is a coordinator decision; -workers must not silently fix unrelated failures. - ---- - -## WP1 — Dataset and root-resource discovery - -### Goal - -Let a frontend discover what can be queried before it knows a project or root -resource type. Return only datasets and resource types visible to the current -principal. - -### Owner and files - -**Owner:** dataset-discovery worker. - -Owned production files: - -- new `internal/catalog/read_datasets.go` -- new `internal/catalog/read_datasets_test.go` -- additive types in `internal/catalog/types.go` -- new `graphqlapi/dataframe/datasets.go` -- new `graphqlapi/dataframe/datasets_test.go` -- additive service dependencies in `graphqlapi/dataframe/service.go` -- additive response types in `graphqlapi/dataframe/types.go` - -Coordinator-owned integration: - -- GraphQL schema, generated output, resolver, output mapping, server wiring. - -### Contract - -Define persistence-neutral read types: - -```go -type DatasetSummaryOptions struct { - arangostore.ConnectionOptions - ProjectAllowlist []string - CursorBatch int -} - -type DatasetSummary struct { - Project string - DatasetGeneration string - State string - ResourceTypes []ResourceTypeSummary -} - -type ResourceTypeSummary struct { - ResourceType string - DocumentCount int64 - PopulatedFieldCount int - PivotCandidateCount int -} -``` - -Names may be adjusted to match repository conventions, but do not expose -Arango documents or collection names publicly. - -### Implementation - -1. Determine the caller's allowed projects from the existing principal and - authorizer contract. Never discover all projects and filter them only in the - browser. -2. In generation-aware mode, resolve each allowed project's active READY - manifest. Exclude projects without a valid active READY generation and - report them only through server logs/diagnostics. -3. In legacy mode, aggregate dataset summaries from catalog rows whose - generation is null/absent. -4. Obtain resource types from `fhir_field_catalog`, not Arango collection names. - This avoids advertising system, edge, lifecycle, or generated collections. -5. Aggregate per resource type: - - maximum or otherwise semantically correct document count from catalog - facts; - - populated field count; - - pivot-candidate count. -6. Apply generation and authorization scope before aggregation. A restricted - user must not infer resource types that exist only outside their scope. -7. Sort datasets by project and resource types by resource type. -8. Add a cache keyed by principal scope, project, active generation, and - analysis/catalog version only if profiling proves the query needs it. Do not - key only by project. -9. Provide a GraphQL service method that returns DTOs without exposing catalog - persistence types. -10. Keep project selection explicit for deployments whose authorization layer - cannot enumerate projects. In that case accept a configured project list; - do not fall back to unrestricted catalog scanning. - -### Tests - -- legacy null-generation dataset; -- READY active generation; -- project with no active generation; -- active pointer naming a non-READY generation; -- two resource types with deterministic ordering; -- restricted scope hiding one resource type entirely; -- restricted-empty scope returns no resources; -- no leakage between projects or generations; -- catalog contains duplicate auth-path rows but counts are not multiplied; -- no field catalog rows returns an empty list, not null. - -### Acceptance - -- A caller can reach the first frontend screen without hardcoded root types. -- Every advertised resource type has at least one visible populated catalog - fact. -- No raw collection discovery or hardcoded FHIR resource list is used. -- Focused unit, GraphQL integration, and live META tests pass. -- Live query completes below 250 ms warm for the current META fixture. - -### Handoff - -Report exact DTOs, authorization behavior, AQL hash, selected indexes, live -latency, and GraphQL fields required from the coordinator. - ---- - -## WP2 — Guided dataframe template registry - -### Goal - -Implement the backend answer to “What are you making?” without building a -general saved-recipe system. - -Initial template families: - -- patient cohort; -- specimen inventory; -- file manifest; -- diagnoses; -- labs/observations; -- study enrollment. - -### Owner and files - -**Owner:** template worker. - -Owned files: - -- new `internal/dataframe/template/types.go` -- new `internal/dataframe/template/registry.go` -- new `internal/dataframe/template/availability.go` -- new `internal/dataframe/template/registry_test.go` -- new `internal/dataframe/template/availability_test.go` -- new `graphqlapi/dataframe/templates.go` -- new `graphqlapi/dataframe/templates_test.go` - -Read-only dependencies: - -- `fhirschema/` -- `internal/catalog` -- existing dataframe input/semantic types. - -Coordinator owns GraphQL schema and mappings. - -### Template definition - -Each template is immutable product metadata: - -```go -type Definition struct { - ID string - Version int - Label string - Description string - RootCandidates []string - SuggestedColumns []ColumnSuggestion - SuggestedTraversals []TraversalSuggestion - SuggestedPivots []PivotSuggestion -} -``` - -Suggestions must use semantic preferences rather than raw AQL. A column may -contain ordered `fieldRef` alternatives so the registry can adapt to different -FHIR encodings: - -```go -type ColumnSuggestion struct { - ID string - Label string - FieldRefAlternatives []string - DefaultSelected bool - Advanced bool -} -``` - -Traversal suggestions name FHIR source/target types and a semantic route -preference. Availability resolution must match them against discovered -references and `fhirschema`; it must not invent edge labels. - -### Implementation - -1. Freeze stable IDs and version `1` for all six families. -2. Keep definitions deterministic and side-effect free. No database calls from - `registry.go`. -3. Resolve availability against the WP1 dataset summary and existing builder - introspection: - - viable root types; - - available suggested columns; - - unavailable optional suggestions; - - required missing capabilities; - - viable traversals and pivots. -4. Return one of `AVAILABLE`, `PARTIAL`, or `UNAVAILABLE` with machine-readable - reasons. -5. Return `commonColumns` before `advancedColumns` and preserve registry order. -6. Never claim a field is available because it exists in the FHIR schema alone; - it must be populated and visible in the current dataset catalog. -7. Produce a starter `FhirDataframeInput`-equivalent DTO containing only - suggestions proven available. Do not persist it. -8. Use the existing `fieldRef` resolution path when materializing the starter - request; do not copy selector parsing. -9. Keep synonyms and friendly labels in the template package. The compiler - remains unaware of “file manifest” or “patient cohort.” -10. Document how a seventh template can be added without editing GraphQL or the - compiler. - -### Minimum semantic intent - -The exact available fields vary by dataset, but definitions should express: - -- patient cohort: Patient grain, identifiers/demographics, optional Condition - and enrollment relationships; -- specimen inventory: Specimen grain, identifiers, specimen type, subject and - container/collection facts when populated; -- file manifest: DocumentReference grain or a viable root leading to it, - attachment name/URL/size/content type and related subject/specimen IDs; -- diagnoses: Condition grain or Patient cohort with Condition aggregation; -- labs/observations: Observation grain or subject grain with bounded - Observation pivots; -- study enrollment: ResearchSubject grain with proven ResearchStudy and - subject relationships. - -These are product preferences, not assumptions that every dataset contains the -same paths. - -### Tests - -- all six IDs unique and stable; -- deterministic registry order; -- complete, partial, and unavailable datasets; -- alternative fieldRef selection; -- suggested traversal absent from catalog; -- schema-known but unpopulated field remains unavailable; -- pivot template with zero, one, and many bounded columns; -- restricted scope changes availability without leaking hidden facts; -- generated starter input passes the real WP3 validator; -- no template source imports Arango or renderer packages. - -### Acceptance - -- The frontend can render six choices without understanding FHIR routes. -- Every enabled suggestion is accepted by real input preparation. -- Unavailable templates explain which capability is missing. -- Adding a template requires registry data and tests, not compiler branches. - -### Handoff - -Report template IDs/version, availability DTO, starter request DTO, missing -capability reason codes, and coordinator GraphQL requirements. - ---- - -## WP3 — Compile and validate without execution - -### Goal - -Validate, normalize, and compile a dataframe request without opening an Arango -result cursor. Give the frontend actionable output before Preview. - -### Owner and files - -**Owner:** validation worker. - -Owned files: - -- new `internal/dataframe/validation.go` -- new `internal/dataframe/validation_test.go` -- additive types in `internal/dataframe/builder_types.go` -- additive service methods in `internal/dataframe/service.go` -- new `graphqlapi/dataframe/validation.go` -- new `graphqlapi/dataframe/validation_test.go` - -Coordinator owns GraphQL schema, resolver, and output mapping. - -### Internal contract - -Add one service operation that shares preparation and compilation with Run: - -```go -func (s *Service) Validate(ctx context.Context, req ValidateRequest) (ValidationResult, error) -``` - -The result should include: - -- `Valid`; -- normalized public request or normalized builder DTO; -- root row grain and stable root identity field; -- ordered output columns; -- bounded pivot-expanded columns when known; -- warnings; -- compiler plan diagnostics; -- request fingerprint used by WP5 cursor paging; -- limits/capabilities such as preview allowed and export allowed. - -Do not expose raw rendered AQL by default. A development-only diagnostic may -return its hash and plan facts. - -### Implementation - -1. Refactor the existing `prepareSpec` and `CompileRequest` call sequence into a - shared internal method used by Validate, Run, and Stream. -2. Validation must perform the same: - - project authorization; - - active generation resolution; - - auth-resource-path intersection; - - populated field/reference validation; - - pivot-column expansion and bounding; - - semantic and physical compilation. -3. Validation must never call `ExecuteQueryRows`, create an Arango cursor, or - profile the query. -4. Return the exact ordered columns that preview/export will produce. Include - flattened pivot columns based on bounded columns rather than waiting to see - runtime object keys. -5. Define warning codes for valid but potentially surprising shapes: - - no selected data columns; - - template suggestion unavailable; - - high traversal fanout; - - truncated distinct values; - - truncated pivot candidates; - - preview limit capped; - - export recommended instead of preview. -6. Define a deterministic request fingerprint from canonical normalized - semantics, project, generation, resolved scope mode/paths, output ordering, - and compiler-relevant request values. Do not hash arbitrary JSON map order. -7. The fingerprint must exclude the page cursor and page size, but include - filters and row grain. -8. Add configurable preview/export cost policy as a service dependency with - conservative defaults. Start with structural facts already available in - plan diagnostics; do not pretend Arango estimates are measured runtime. -9. Keep validation idempotent: validating an already-normalized request yields - the same normalized result and fingerprint. -10. `runFhirDataframe`, future preview, and export must fail if their prepared - fingerprint differs from a supplied validated fingerprint. - -### Tests - -- valid root-only request; -- nested traversal, aggregate, slice, and pivot request; -- unknown fieldRef and unavailable populated field; -- unsafe/unknown traversal; -- unbounded pivot; -- unauthorized project and restricted-empty scope; -- active generation changes between requests; -- Validate never invokes the execution dependency; -- Validate and Run produce identical columns and plan diagnostics; -- canonical equivalent inputs produce the same fingerprint; -- changed field, filter, scope, generation, traversal, or pivot column changes - the fingerprint; -- deterministic warning and column ordering. - -### Acceptance - -- The frontend can validate every editable request without executing AQL. -- Validation uses the production compiler, not a reduced validator. -- No execution cursor is opened. -- The GDC request validates in under 100 ms warm, excluding a deliberately cold - catalog cache. -- Existing Run and Stream behavior remains unchanged. - -### Handoff - -Report normalized DTO, fingerprint algorithm/version, warning schema, cost -policy defaults, benchmarks, and GraphQL requirements. - ---- - -## WP4 — Structured frontend error contract - -### Goal - -Give GraphQL, preview, and export one stable error taxonomy that the frontend -can map to controls and user messages. - -### Owner and files - -**Owner:** error-contract worker. - -Owned files: - -- new `internal/dataframe/errors.go` -- new `internal/dataframe/errors_test.go` -- new `graphqlapi/errors.go` -- new `graphqlapi/errors_test.go` -- additive reusable HTTP error mapping in `internal/httpapi/errors.go` -- new `internal/httpapi/errors_test.go` - -Coordinator integrates the GraphQL error presenter in `graphqlapi/handler.go` -and reconciles `internal/httpapi/server.go`. - -### Error shape - -Define an interface or typed error carrying: - -```go -type UserError interface { - error - Code() string - FieldPath() []string - Details() map[string]any - Retryable() bool -} -``` - -Do not expose internal errors, AQL text, bind variables, collection names, -filesystem paths, or stack traces through `Details`. - -Minimum stable codes: - -- `PROJECT_REQUIRED` -- `ROOT_RESOURCE_TYPE_REQUIRED` -- `UNAUTHORIZED_PROJECT` -- `UNKNOWN_FIELD` -- `FIELD_NOT_POPULATED` -- `INVALID_TRAVERSAL` -- `UNSAFE_TRAVERSAL_ROUTE` -- `INVALID_FILTER` -- `UNBOUNDED_PIVOT` -- `INVALID_PIVOT_COLUMN` -- `INVALID_SLICE` -- `PLAN_TOO_EXPENSIVE` -- `INVALID_CURSOR` -- `STALE_CURSOR` -- `DATASET_GENERATION_CHANGED` -- `UNSUPPORTED_EXPORT_FORMAT` -- `CLIENT_CANCELED` -- `BACKEND_UNAVAILABLE` -- `INTERNAL_ERROR` - -### Implementation - -1. Inventory public errors emitted by GraphQL input resolution, dataframe - preparation, catalog validation, semantic lowering, cursor decode, and - export adapters. -2. Wrap errors at their semantic owner. Do not classify by parsing error text - in the GraphQL layer. -3. Preserve `errors.Is`/`errors.As` behavior and root causes for server logs. -4. Return GraphQL errors with extensions: - - ```json - { - "code": "UNKNOWN_FIELD", - "fieldPath": ["rootFields", "2", "fieldRef"], - "retryable": false, - "details": {"fieldRef": "Patient.missing"}, - "requestId": "..." - } - ``` - -5. Reuse the same code/message/detail mapper for REST export errors before - response streaming begins. -6. Once streaming has started, log the structured error and terminate the - stream; do not attempt to replace a partial CSV/NDJSON body with JSON. -7. Map context cancellation separately from backend timeout/unavailability. -8. Unknown errors become `INTERNAL_ERROR` externally and retain the full cause - only in structured server logs. -9. Document which codes are user-correctable, retryable, or operator failures. -10. Add a compatibility test preventing accidental code renames. - -### Tests - -- every minimum code is unique and documented; -- GraphQL extensions contain code/path/request ID; -- REST envelope uses the same code; -- internal AQL and bind values are redacted; -- wrapped errors preserve `errors.Is`/`errors.As`; -- cancellation, deadline, and Arango connection failures map differently; -- validation locates root field, nested field, pivot, filter, and traversal - errors at the corresponding input path; -- unknown panic/error returns a generic external message. - -### Acceptance - -- The frontend never needs to parse an English message to identify an error. -- The same semantic failure has the same code across validation, preview, and - export. -- Existing request IDs remain available. -- No sensitive query or scope data leaks. - -### Handoff - -Report the frozen code registry, Go interface, GraphQL extension shape, REST -shape, redaction rules, and coordinator integration steps. - ---- - -## WP5 — Stable preview paging - -### Goal - -Replace the unused GraphQL cursor field with keyset paging over the stable root -grain. Preview pages must not use offsets and must not repeat or skip roots -within one immutable dataset generation. - -### Owner and files - -**Owner:** paging worker. - -Owned files: - -- new `internal/dataframe/cursor.go` -- new `internal/dataframe/cursor_test.go` -- new `internal/dataframe/paging.go` -- new `internal/dataframe/paging_test.go` -- additive semantic/physical paging fields in coordinator-approved dataframe - files only; -- new `graphqlapi/dataframe/preview.go` -- new `graphqlapi/dataframe/preview_test.go` - -The paging worker must stop and request coordinator approval before editing -shared physical files such as `physical_plan.go`, `physical_render.go`, or -`generic_physical_plan.go`. - -Coordinator owns GraphQL schema and generated files. - -### Cursor contract - -Use an opaque, versioned, authenticated token. Minimum payload: - -```go -type CursorV1 struct { - Version int - RequestFingerprint string - Project string - DatasetGeneration string - RootResourceType string - LastRootKey string -} -``` - -Sign the canonical payload with HMAC-SHA-256 using server configuration. Do not -accept unsigned base64 JSON in production. Provide an explicit insecure test -codec only in tests. - -### Implementation - -1. Add a preview request with `first` and `after`; cap `first` using configured - minimum/default/maximum values, initially 1/25/1,000. -2. Decode and verify the cursor before compiling AQL. -3. Recompute WP3's request fingerprint and reject a cursor from a different - request, root type, project, scope, or generation. -4. Add the root-key predicate before root `SORT` and `LIMIT`: - - ```aql - FILTER root._key > @after_root_key - SORT root._key ASC - LIMIT @page_fetch_limit - ``` - -5. Fetch `first + 1` rows to calculate `hasNextPage`, then return at most - `first` rows. -6. Construct `endCursor` from the last returned root `_key`; never derive it - from a child resource or output field selected by the user. -7. Keep `_key` available as hidden execution metadata even if the user does not - select it as an output column. Remove hidden metadata before delivering rows. -8. Preserve required-match filtering before paging. Optional child shaping - remains after the root window. -9. Do not implement backward paging or offset paging in Part 5. -10. Cancel and close the Arango cursor when the request context ends. Confirm - the current driver path does so; add explicit close behavior if required. -11. Return page info: - - `hasNextPage`; - - `endCursor` or null for an empty page; - - requested/effective page size; - - request fingerprint/version. -12. Keep `runFhirDataframe(limit:)` as a thin first-page compatibility adapter - during migration. - -### Tests - -- empty, one-row, exact-page, and page-plus-one datasets; -- multiple pages concatenate to the same ordered rows as one unpaged request; -- no duplicate or skipped root keys; -- required filter applied before page window; -- optional child fanout does not alter page boundaries; -- invalid signature, malformed token, unsupported version; -- cursor from another project, scope, generation, root type, or request; -- generation activation between pages produces `STALE_CURSOR` or - `DATASET_GENERATION_CHANGED`; -- page size zero, negative, and above maximum; -- cancellation closes cursor resources; -- root `_key` is not leaked unless requested as a column; -- live META paging for the GDC shape with exact concatenated result hash. - -### Acceptance - -- The GraphQL cursor field is no longer decorative. -- Page concatenation has exact parity with the equivalent stable unpaged - result. -- Explain uses the scoped root index and keyset predicate with no full scan. -- Page memory is bounded by effective page size, not total result size. -- Cursor tampering and stale generations fail with structured WP4 errors. - -### Handoff - -Report cursor wire version, key rotation/configuration decision, fingerprint -dependency, AQL/Explain evidence, parity hashes, and GraphQL page DTO. - ---- - -## WP6 — Streaming NDJSON and CSV export transport - -### Goal - -Expose the existing validated `dataframe.Service.Stream` path as an -authenticated, cancellation-aware HTTP export without buffering the full -dataframe in GraphQL or server memory. - -This is synchronous streaming export only. Durable jobs, artifact storage, -resume, and Elasticsearch are separate future work. - -### Owner and files - -**Owner:** export worker. - -Owned files: - -- new `internal/export/types.go` -- new `internal/export/ndjson.go` -- new `internal/export/csv.go` -- new `internal/export/ndjson_test.go` -- new `internal/export/csv_test.go` -- new `internal/httpapi/dataframe_export.go` -- new `internal/httpapi/dataframe_export_test.go` - -Coordinator owns shared HTTP config/routes and server command wiring. - -### Request and response contract - -Proposed request: - -```json -{ - "format": "ndjson", - "dataframe": { "...": "FhirDataframeInput-equivalent JSON" }, - "validatedFingerprint": "optional fingerprint returned by WP3" -} -``` - -Response behavior: - -- NDJSON: `Content-Type: application/x-ndjson; charset=utf-8`; -- CSV: `Content-Type: text/csv; charset=utf-8`; -- attachment filename is sanitized and contains project, root type, and a UTC - timestamp, never an auth path; -- `Cache-Control: no-store`; -- `X-Content-Type-Options: nosniff`; -- optional safe headers for request fingerprint and generation; -- no `Content-Length` requirement for streaming responses. - -### Implementation - -1. Define a small sink interface receiving ordered columns and flattened rows. -2. NDJSON writes one canonical JSON object and one newline per row. -3. CSV writes the header once using validated ordered columns, then writes every - value with RFC 4180-compatible quoting through `encoding/csv`. -4. Define deterministic CSV encoding: - - null -> empty field; - - string/number/bool -> scalar text; - - arrays/objects -> compact canonical JSON in one CSV cell; - - timestamps remain the compiler-delivered string representation. -5. Never derive CSV column order from Go map iteration. Use WP3's compiled - ordered columns, including bounded flattened pivot columns. -6. Call the same GraphQL input preparation/fieldRef resolution and - `dataframe.Service.Stream` path. Extract a transport-neutral request adapter - if necessary; do not duplicate GraphQL-only parsing rules. -7. Resolve authorization and active generation before writing HTTP headers so - validation failures can return a normal structured error response. -8. If `validatedFingerprint` is supplied, require exact equality with the - freshly prepared request. -9. Flush periodically or per configured row batch. Do not flush every cell. -10. Stop promptly when the request context is canceled or the client - disconnects; propagate cancellation to Arango. -11. Record rows and bytes written in structured logs. Do not log row contents. -12. Enforce configurable export row and duration limits for synchronous mode. - Return `PLAN_TOO_EXPENSIVE` before streaming if the request requires a - durable job that Part 5 does not provide. -13. Do not write temporary files, artifact metadata, job collections, or - Elasticsearch documents in this package. -14. Register `POST /api/v1/dataframes/export` behind the existing authentication - middleware and project authorization contract. - -### Tests - -- NDJSON exact rows and newline behavior; -- CSV header order, commas, quotes, newlines, Unicode, nulls, arrays, objects, - and pivot-expanded columns; -- preview and both export formats decode to identical logical rows; -- large fake stream proves bounded memory; -- writer failure and client cancellation stop upstream execution; -- error before headers returns WP4 JSON envelope; -- error after streaming starts terminates and logs without appending JSON error - material to CSV/NDJSON; -- unsupported `Accept`/format returns `UNSUPPORTED_EXPORT_FORMAT`; -- unauthorized project and restricted-empty scope; -- generation changes and validated-fingerprint mismatch; -- live META GDC export at 1,000 rows with row/hash parity; -- response headers prevent caching and content sniffing. - -### Acceptance - -- 100,000 generated test rows export with memory bounded independently of row - count. -- Live 1,000-row GDC NDJSON and CSV exports contain the same logical rows as - preview. -- Disconnect/cancellation stops Arango work. -- No GraphQL JSON buffering is used for export. -- No durable-job or Elasticsearch scaffolding is introduced. - -### Handoff - -Report sink interface, CSV value rules, endpoint request/headers, cancellation -evidence, memory benchmark, row hashes, and coordinator wiring steps. - ---- - -## Parallel execution plan - -### Phase 0 — Coordinator contract freeze - -One coordinator performs a read-only audit and freezes: - -- GraphQL semantic field names; -- shared DTOs between GraphQL and REST input adapters; -- WP4 error interface and initial code list; -- WP3 normalized-request and fingerprint boundary; -- shared-file hashes and worker ownership. - -No generated GraphQL edit occurs yet. - -### Phase 1 — Four parallel workers - -These packages can start concurrently after Phase 0: - -| Lane | Work | May edit | -| --- | --- | --- | -| A | WP1 catalog dataset summaries | catalog read files and dataset DTO/service files | -| B | WP2 pure template registry | `internal/dataframe/template` only initially | -| C | WP3 validation core | dataframe validation/service files | -| D | WP4 error taxonomy | new error files and tests | - -Coordination rules: - -- Lane B uses fake availability inputs until WP1's response interface lands. -- Lane C returns typed internal errors compatible with Lane D's frozen - interface. If the interface is insufficient, stop and request a coordinator - decision. -- No lane edits GraphQL schema/generated files. -- WP1 and WP3 must not independently refactor authorization or active generation - resolution; they reuse existing services. - -### Phase 2 — Two parallel workers plus integration - -After WP3's normalized request/fingerprint contract and WP4 errors are merged: - -| Lane | Work | Dependency | -| --- | --- | --- | -| E | WP5 cursor codec and paging | WP3 + WP4 | -| F | WP6 export sinks and handler | WP3 + WP4; may use current Stream immediately | -| Coordinator | WP1-WP4 GraphQL schema integration | stable service DTOs | - -WP5 and WP6 may run concurrently because paging owns compiler/page execution -and export owns streaming formats/HTTP adapter. They must not both edit shared -dataframe service files; any shared extraction is coordinator-owned. - -### Phase 3 — Single coordinator integration - -The coordinator: - -1. integrates service dependencies into the server command; -2. edits `graphqlapi/schema.graphqls` once for WP1-WP5; -3. runs `make generate-graphql` once; -4. wires resolvers and output mapping; -5. wires the WP6 HTTP route/configuration; -6. updates README and Quickstart examples; -7. resolves API naming and generated-code conflicts; -8. runs the complete Part 5 test matrix. - -### Phase 4 — Parallel verification - -After integration, three read-mostly verification workers may run concurrently: - -- authorization/generation isolation across all six operations; -- GraphQL/REST contract and error compatibility; -- live META performance, paging parity, streaming memory, and cancellation. - -Only the coordinator fixes shared production files. Verification workers add -focused tests/evidence or propose patches. - -## Dependency graph - -```text -Phase 0 contract freeze - ├── WP1 dataset discovery ───────┐ - ├── WP2 template registry ───────┼── GraphQL integration - ├── WP3 validate/fingerprint ─┬──┤ - └── WP4 structured errors ────┤ │ - ├── WP5 preview paging ─┐ - └── WP6 export ─────────┼── final integration - └── system verification -``` - -WP2's availability adapter depends on WP1 and existing introspection, but its -registry can be built independently. WP5 and WP6 require the final WP3/WP4 -contracts. Export does not depend on paging. - -## Coordinator merge order - -Merge in this order even if workers finish differently: - -1. WP4 internal error contract; -2. WP3 validation and fingerprint contract; -3. WP1 dataset summary service; -4. WP2 template registry and availability adapter; -5. WP5 paging; -6. WP6 export; -7. GraphQL generation and server wiring; -8. docs and system evidence. - -This order prevents WP3/WP5/WP6 from inventing incompatible error or -fingerprint representations. - -## Required system tests - -Add a Part 5 system suite covering one complete user flow: - -1. load or use the checked-in META dataset; -2. list visible datasets and resource types; -3. list templates; -4. select an available template and starter request; -5. introspect fields and distinct filter values; -6. validate the request; -7. preview at least two pages; -8. concatenate pages and verify row identity/order; -9. export NDJSON and CSV; -10. decode both exports and prove logical row parity; -11. repeat with restricted and restricted-empty scopes; -12. activate a different generation and prove the old cursor/fingerprint is - rejected. - -The system suite must include: - -- Patient-root GDC matrix with nested traversals, aggregates, slices, and - Observation pivot; -- a non-Patient root; -- a dataset where at least one of the six templates is partial/unavailable; -- malformed input and cursor cases; -- cancellation during streaming export. - -## Final acceptance gate - -Part 5 is complete when: - -1. The frontend needs no hardcoded project resource types for the first screen. -2. Six guided template families are returned with data-backed availability. -3. Every starter request is accepted by the production validator or explicitly - marked unavailable. -4. Validation opens no result cursor and returns stable columns, warnings, - diagnostics, and fingerprint. -5. Every user-correctable failure has a stable structured code and input path. -6. Preview cursor paging has exact concatenation parity and stable root order. -7. NDJSON and CSV stream through the production dataframe service with bounded - memory and cancellation. -8. Preview/export preserve project, generation, and auth-scope isolation. -9. Existing GDC result hashes and compiler conformance remain unchanged. -10. No frontend-specific FHIR route or AQL logic enters the compiler. -11. No saved-recipe, durable-job, artifact, or Elasticsearch scaffolding is - added. -12. README and Quickstart contain copy-pasteable calls for dataset discovery, - template selection, validation, paged preview, and both export formats. - -## Luna worker prompt template - -```text -Execute WP from docs/LUNA_FRONTEND_ENABLEMENT_PART_5.md. - -Read the entire Part 5 plan, then every file listed under that WP. Own only the -exact paths assigned to the WP. Do not edit GraphQL schema/generated files, -server wiring, shared physical compiler files, README, or Quickstart unless you -are the designated coordinator. - -Preserve all global contracts, especially project/generation/auth isolation, -stable fieldRef behavior, exact preview/export row semantics, deterministic -ordering, and no FHIR/AQL logic in the frontend layer. Reuse fhirschema, -resolveStorageRoute, catalog discovery, dataframe.Service, and -dataset.ActiveManifestResolver. Never hardcode a fixture route in generic code. - -Record baseline hashes. Implement only this package, run the named unit/live -tests, and produce the required handoff. Report changed files, test commands, -API/type decisions, hashes, latency/memory metrics, rejected approaches, and -coordinator decisions required. - -Stop rather than guess if a shared contract must change, an owned file changed -concurrently, authorization semantics differ, active-generation semantics are -unclear, result parity fails, or a generated/shared file edit is required. -``` - diff --git a/docs/LUNA_FRONTEND_ENABLEMENT_PART_5_PHASE0.md b/docs/LUNA_FRONTEND_ENABLEMENT_PART_5_PHASE0.md deleted file mode 100644 index e4271fb..0000000 --- a/docs/LUNA_FRONTEND_ENABLEMENT_PART_5_PHASE0.md +++ /dev/null @@ -1,30 +0,0 @@ -# Part 5 Phase 0 contract freeze - -This file records the coordinator freeze used for Phase 1. It is intentionally -an internal-service contract; GraphQL schema changes and generated code remain -coordinator-owned and are deferred until the service lanes have completed. - -## Frozen boundaries - -- Catalog discovery returns persistence-neutral dataset summaries and never - scans without an explicit project allowlist. -- Template availability consumes a catalog-backed capability snapshot and - returns semantic starter intent; it does not emit AQL or collection names. -- `dataframe.Service.Validate` shares the exact preparation and compiler - boundary with `Run`, returns a normalized builder, a request fingerprint, - plan metadata, warnings, and timing diagnostics, and never executes rows. -- User-facing errors use the stable code registry in - `internal/dataframe/errors.go`. GraphQL and HTTP adapters map the same - semantic error without exposing backend details. -- Project, active generation, and authorization scope are resolved once by - the request adapter and propagated to every catalog and compiler call. -- Generated GraphQL files, schema, route registration, and server wiring are - coordinator-owned. Phase 1 workers must report those wiring requirements - rather than editing shared files. - -## Phase 1 acceptance gate - -Each lane must provide focused unit tests, deterministic ordering and defensive -copy behavior where applicable, `git diff --check` output, and a list of -coordinator wiring decisions. No lane may introduce a FHIR-type-, project-, -fixture-, or edge-label-specific production branch. diff --git a/docs/LUNA_RICH_PHYSICAL_RENDERER_EXECUTION.md b/docs/LUNA_RICH_PHYSICAL_RENDERER_EXECUTION.md deleted file mode 100644 index 1d58773..0000000 --- a/docs/LUNA_RICH_PHYSICAL_RENDERER_EXECUTION.md +++ /dev/null @@ -1,519 +0,0 @@ -# Luna Execution Plan: Remove the Lowered AQL Compiler - -## Mission - -Make the physical compiler the only runtime execution path for supported -dataframe requests. The final runtime pipeline is: - -```text -GraphQL input - -> graphqlapi/dataframe - -> dataframe.Builder - -> dataframe.BuildSemanticPlan - -> dataframe.BuildPhysicalPlan - -> dataframe.RenderPhysicalPlan - -> Arango AQL -``` - -Delete the runtime compatibility path (`lowerSemanticBuilder`, `Compile`, and -`compileLowered`) only after the physical renderer covers the same supported -semantic operations and has result parity. Do not preserve old AQL helpers for -backward compatibility once their physical replacement is enabled. - -This is an implementation plan, not a request to invent FHIR behavior. The -repository already contains the FHIR facts and generated backbone: - -- `fhirstructs/`: generated FHIR structs, validators, and edge extraction; -- `fhirschema/`: generated resource metadata, selectors, and traversals; -- `META/`: loaded sample FHIR NDJSON data; -- `graphqlapi/schema.graphqls`: current public GraphQL contract; -- `internal/dataframe/storage_route.go`: the only authority for physically - proven FHIR edge directions; -- `internal/dataframe/semantic_plan.go`: semantic request meaning; -- `internal/dataframe/physical_plan.go`: renderer-independent physical IR. - -Never add a hard-coded resource-specific FHIR route or field just to make a -fixture pass. Resolve fields through `fhirschema` and routes through -`resolveStorageRoute`. - -## Current baseline (verify before editing) - -The physical runtime currently owns: - -- root scan plus project, dataset-generation, and authorization scope; -- root sort and preview limit; -- root field extraction, selector fallbacks, scalar/array/distinct-array - values, and root typed filters; -- optional navigation-only traversal sets; -- optional child traversal sets with child field projections and typed child - filters, including nested child sets; -- root and child aggregates (COUNT, COUNT_DISTINCT, EXISTS, DISTINCT_VALUES, - MIN, MAX, and predicate-qualified reductions); -- required traversal matches as scoped correlated `EXISTS` subplans; -- proven inbound routes and the explicit outbound - `ResearchSubject -> ResearchStudy` route. - -The compatibility compiler still owns: - -- derived fields; -- pivots; -- representative slices; -- legacy named-set traversal sharing. - -Before work, run: - -```bash -GOCACHE="$(pwd)/.gocache" GOTOOLCHAIN=auto go test ./... -count=1 -``` - -If the checkout has unrelated dirty changes, preserve them. Do not reset or -reformat unrelated code. - -## Non-negotiable compiler invariants - -Every work package must maintain these invariants: - -1. One outer `FOR root` produces at most one row per root resource. -2. `SORT root._key` and optional `LIMIT @limit` occur before optional child - traversal work. Required semi-joins occur before that window. -3. Every participating root, child node, and `fhir_edge` document has project, - dataset-generation, and authorization predicates. -4. AQL contains only compiler-owned syntax. Request values, field names, - resource types, labels, projection names, and pivot keys are bind values or - generated metadata; never concatenate request data into AQL source. -5. Physical plan validation rejects unsafe bind keys, selector paths, variable - capture/scope violations, untyped collection binds, and missing return - values before rendering. -6. Fallback/value-mode behavior is part of the output contract. Preserve - scalar `null`, empty arrays, array order, and distinct semantics as proven - by existing compatibility tests. -7. Unknown outbound or `ANY` routes fail through `resolveStorageRoute`; do not - make them work by guessing edge direction. - -## Physical IR rules - -Use the existing typed IR. Add fields only when a semantic concept cannot be -expressed safely with the current structures: - -- `PhysicalSet` / `PhysicalSubplan`: correlated array-valued child set; -- `PhysicalExpression`: value, extract, aggregate, pivot map, slice, object; -- `PhysicalPredicateExpression`: comparison, all, any, not, exists; -- `PhysicalExtract`: parsed selector plus fallbacks and explicit cardinality; -- `PhysicalAggregate`, `PhysicalPivotMap`, `PhysicalSlice`. - -Do not reintroduce `NamedSet`, string expressions, or an AQL snippet field to -the physical plan. If an operation cannot be rendered without a raw string, -the IR is incomplete and must be extended first. - -## Work package 1 — Make physical subplans fully executable - -**Files owned** - -- `internal/dataframe/physical_plan.go` -- `internal/dataframe/physical_render.go` -- `internal/dataframe/physical_helpers.go` -- `internal/dataframe/physical_plan_test.go` - -**Goal** - -Finish the generic support for `PhysicalSet` and nested `PhysicalSubplan` -before semantic lowering starts producing them for real child fields. - -**Implementation steps** - -1. Make `clonePhysicalPlan` deeply clone every rich IR payload: expressions, - predicate trees, sets, subplans, selectors, projections, and bind values. - `withGenericPhysicalExecutionWindow` must not mutate its input plan. -2. In `PhysicalPlan.Validate`, validate set captures against the parent lexical - scope. A subplan may reference only captures and variables it defines. -3. Reject root scans, outer returns, sort, and preview limits inside a child - subplan. A child subplan can contain traversal, scoped filters, derived lets, - nested sets, and a typed return expression. -4. Extend collection-bind discovery and render validation recursively through - sets, exists predicates, aggregate/slice/pivot expressions, and nested - object projections. -5. Implement deterministic AQL rendering for `PhysicalSet`: - - ```aql - LET child_set = ( - FOR child, edge IN 1..1 INBOUND parent @@edge_collection - ... mandatory scope filters ... - RETURN child - ) - ``` - - Apply `UNIQUE` only when `PhysicalSet.Unique` is true. Do not emit a top - level child loop. - -**Tests** - -- deep-clone mutation test for every rich payload; -- capture rejection, future-variable rejection, and nested-scope rejection; -- collection bind inside nested set/existence validation; -- renderer golden proving nested sets remain under root `LET` expressions. - -**Done when**: a hand-built nested child-set physical plan validates and renders -without raw AQL payloads, and `go test ./internal/dataframe -run TestPhysical` -passes. - -## Work package 2 — Lower optional child nodes to physical sets - -**Files owned** - -- `internal/dataframe/generic_physical_plan.go` -- `internal/dataframe/physical_lowering.go` -- `internal/dataframe/physical_helpers.go` -- new `internal/dataframe/physical_child_set_test.go` - -**Goal** - -Replace navigation-only child traversals with a physical set hierarchy that is -usable by fields, filters, aggregates, pivots, and slices. - -**Implementation steps** - -1. Add an internal lowering context with deterministic counters for set/node/ - edge variable names and bind keys. Do not derive names from user aliases. -2. For each optional `SemanticNode`, call: - - ```go - resolveStorageRoute(parent.ResourceType, child.EdgeLabel, child.ResourceType) - ``` - - Populate a `PhysicalSet` with a subplan that captures the parent variable - (or parent set-element variable for nesting). -3. Inside each set subplan, emit in this order: - - one-hop typed `PhysicalTraversal`; - - edge and child project filters; - - edge and child dataset-generation filters; - - edge and child authorization filters; - - child resource type and edge target-type discriminator filters; - - child typed filters (work package 4); - - nested child sets (recursive call); - - typed return object/value for the owning projection. -4. Preserve semantic request ordering. Grouping or traversal sharing is not - allowed in this package. -5. Update `genericPhysicalNodeUnavailableReason` only for the exact feature - implemented. Do not mark pivots, aggregates, slices, or derived fields as - physical merely because a child set exists. - -**Tests** - -- Patient -> Specimen field; -- Patient -> Specimen -> DocumentReference nested field; -- root row count remains one with zero/many children; -- inbound and ResearchSubject -> ResearchStudy outbound route rendering; -- unknown forward route remains rejected. - -**Done when**: optional child field-only requests select `BuildPhysicalPlan` -and no longer invoke `lowerSemanticBuilder` at runtime. - -## Work package 3 — Project fields from child sets - -**Files owned** - -- `internal/dataframe/generic_physical_plan.go` -- `internal/dataframe/physical_render.go` -- `internal/dataframe/selection_semantics.go` only if an existing semantic - contract is missing -- `internal/dataframe/physical_child_projection_test.go` - -**Goal** - -Emit exactly the current output shapes for traversed selections. - -**Implementation steps** - -1. Reuse `ResolveSemanticField(resourceType, nodeAlias, index, field)` for - every child `SemanticField`. Do not duplicate its selection semantics. -2. Lower a child field to `PhysicalExtract` with source equal to the child set - element payload and use explicit cardinality/null behavior. -3. For a child field output, render one typed subquery over the set: - - scalar/first: `FIRST(...)` according to semantic projection; - - array: ordered values; - - distinct array: physical distinct operation followed by required stable - ordering, matching existing tests. -4. Nested child fields are evaluated inside their parent child-set subplan, not - by reopening an uncorrelated root traversal. -5. Add projection names to `CompiledQuery.Columns` from the final physical - return operation. Do not reconstruct columns from `Builder` after lowering. - -**Parity fixtures** - -Use existing `META` resource types and add a physical fixture that selects: - -- specimen type from a patient-linked specimen; -- DocumentReference attachment title from a specimen-linked file; -- an array-valued field and a fallback field. - -Compare legacy and physical rows after canonical JSON key ordering. Compare -columns and row values, not raw AQL text. - -**Done when**: child field outputs have live Arango result parity and all -field-only traversal requests run physically. - -## Work package 4 — Child filters and required-match filters - -**Files owned** - -- `internal/dataframe/generic_physical_plan.go` -- `internal/dataframe/physical_required_match.go` -- `internal/dataframe/physical_render.go` -- `internal/dataframe/physical_child_filter_test.go` -- `internal/dataframe/physical_required_match_test.go` - -**Goal** - -Use one typed predicate lowering path for root filters, child set filters, and -required-match subplan filters. - -**Implementation steps** - -1. Extract root predicate construction from `appendRootPhysicalFilters` into - `lowerPhysicalFilters(resourceType, sourcePayload, filters, bindVars, - bindPrefix)`. It returns typed `PhysicalPredicateExpression`s and owns all - bind keys. -2. Invoke the helper in child set subplans immediately after mandatory scope - filters and before subplan return. -3. Invoke the same helper in `buildRequiredTraversalExists`; remove its current - rejection of `node.Filters`. -4. Retain exact existing `TypedFilter` behavior: `EQUALS`, `NOT_EQUALS`, `IN`, - `CONTAINS_TEXT`, `GT`, `GTE`, `LT`, `LTE`, `EXISTS`, `MISSING`, and - ALL/ANY/NONE. Use parsed selectors and bind literals only. -5. For date/datetime comparison, preserve existing `DATE_TIMESTAMP` behavior - from the compatibility compiler. - -**Tests** - -- every filter operation at child depth one; -- nested child filter; -- required traversal filter in both inbound and proven outbound directions; -- scope test where a matching document is out of auth scope or wrong dataset - generation; -- live `EXPLAIN` test checks a traversal edge index and no full collection scan. - -**Done when**: `compileTypedFilters` has no production call site outside the -compatibility oracle and all field/filter traversal requests run physically. - -## Work package 5 — Aggregates and derived fields - -**Files owned** - -- `internal/dataframe/physical_plan.go` -- `internal/dataframe/generic_physical_plan.go` -- `internal/dataframe/physical_render.go` -- new `internal/dataframe/physical_aggregate_test.go` - -**Goal** - -Lower `SemanticAggregate` and supported derived fields over typed physical -sets, with no use of `Builder.Sets` or `DerivedField` in runtime execution. - -**Implementation steps** - -1. Map every `SemanticAggregate.Operation` currently accepted by validation to - a `PhysicalAggregateOperation`. Before coding, enumerate the accepted - operations from `semantic_validation.go` and write a table-driven test that - rejects an unmapped operation. -2. Lower aggregate selectors and optional predicates with the shared helpers - from work packages 3 and 4. -3. Specify output behavior from existing compatibility tests before rendering: - - count/distinct count: `0` on empty set; - - values/distinct values: `[]` on empty set; - - first/min/max: `null` on empty set unless current test behavior differs. -4. Render fixed aggregate AQL templates only. Do not store function names in - request data. Ensure distinct arrays retain required ordering. -5. Lower derived count/values/first operations as typed expressions referencing - a physical set variable or prior physical expression. Do not permit an AQL - variable name from `Builder` to cross the boundary. - -**Tests** - -- zero/one/many child documents; -- repeated values and distinct values; -- predicate-qualified count; -- `META` GDC case -> specimen -> file counts/values; -- physical-vs-compat execution parity. - -**Deletion gate** - -Delete the corresponding production branches in `compileRootAggregateExpr` and -`compileDerivedField` after all aggregate fixtures select physical execution. - -## Work package 6 — Pivots - -**Files owned** - -- `internal/dataframe/physical_plan.go` -- `internal/dataframe/generic_physical_plan.go` -- `internal/dataframe/physical_render.go` -- new `internal/dataframe/physical_pivot_test.go` - -**Goal** - -Make root and child `SemanticPivot` physical without string-generated object -keys. - -**Implementation steps** - -1. Lower each pivot to `PhysicalPivotMap` with typed source, resource type, - parsed key/value selectors, and a bind-backed permitted column list. -2. Establish collision behavior by executing the existing compatibility pivot - tests; encode it as IR/renderer behavior and test duplicates explicitly. -3. Render a fixed AQL pivot subquery that filters values to permitted columns. - Requested output column names are bind values. -4. Set `CompiledQuery.PivotFields` from physical return projection metadata, - not from the old builder. - -**Tests** - -- root and child pivot; -- sparse values, duplicate key, absent permitted key, nested array selector; -- real observation pivot fixture from `META`; -- bind-safety test: no requested key appears interpolated in AQL source. - -**Deletion gate** - -Remove `compileRootPivot`, `compileDerivedPivotMapLets`, and their production -call sites after parity is green. - -## Work package 7 — Representative slices - -**Files owned** - -- `internal/dataframe/generic_physical_plan.go` -- `internal/dataframe/physical_render.go` -- `internal/dataframe/physical_plan.go` only if required -- new `internal/dataframe/physical_slice_test.go` - -**Goal** - -Lower `SemanticSlice` to a bounded typed local subquery. - -**Implementation steps** - -1. Lower the source child set, optional typed predicate, sort expression, - positive bind-backed limit, and nested projection to `PhysicalSlice`. -2. Require an explicit deterministic sort with a tie breaker. Do not inherit - incidental collection or `UNIQUE` order. -3. Render `FILTER`, `SORT`, `LIMIT`, and object projection inside the slice - subquery. Never alter the outer root preview window. -4. Validate limit type/positivity in physical plan validation. - -**Tests** - -- zero/one/many children; -- equal primary sort values prove tie stability; -- predicate + slice interaction; -- nested field object projection; -- output parity with existing representative slice fixture. - -**Deletion gate** - -Delete `compileRepresentativeSlice`, `compileRootSlice`, and slice-only legacy -tests once all slice fixtures are physical. - -## Work package 8 — Traversal sharing optimization - -**Dependencies**: work packages 2–7 must already have unshared physical parity. - -**Files owned** - -- new `internal/dataframe/physical_optimize.go` -- `internal/dataframe/physical_plan.go` -- `internal/dataframe/physical_render.go` only if an existing set form cannot - render the optimized plan -- new `internal/dataframe/physical_optimize_test.go` - -**Goal** - -Recreate generic sibling-prefix sharing as a physical-plan optimization, -without changing semantic results. - -**Implementation steps** - -1. Match only sets with identical parent capture, direction, edge collection, - label, project/generation/auth scopes, and traversal depth. -2. Materialize one broad shared set only when children differ solely by target - resource type. Create typed filtered subsets for each target type. -3. Run this optimizer after semantic lowering and before rendering. Keep an - unoptimized plan available for deterministic test comparison. -4. Record sharing count and set count in physical explain metadata; do not - expose raw AQL variable names to GraphQL clients. - -**Tests and performance gate** - -- optimized/unoptimized result parity; -- no target type broadening; -- `EXPLAIN` confirms edge traversal index use; -- benchmark must be cost-neutral or better before enabling optimization. - -## Work package 9 — Production cutover and deletion - -**Preconditions** - -- all supported semantic features have physical parity; -- full `META` fixture matrix executes through `CompileRequest` without - `lowerSemanticBuilder`; -- live Arango explain/cost coverage is green; -- one real GDC-style GraphQL request produces a useful dataframe (fields, - child fields, counts, pivots, slices) through the physical path. - -**Implementation steps** - -1. Change `CompileRequest` to exactly: - - ```go - semantic, err := BuildSemanticPlan(builder) - if err != nil { return CompiledQuery{}, err } - physical, err := BuildPhysicalPlan(semantic) - if err != nil { return CompiledQuery{}, err } - return compilePhysicalExecution(physical, semantic, limit) - ``` - -2. Move compatibility execution comparison into test-only helpers. It may be - retained temporarily in a test package as an oracle, but it must have no - production caller. -3. Delete production `Compile`, `Lower`, `lowerSemanticBuilder`, - `compileLowered`, `NamedSet`, and lowered-only helpers/types when deadcode - proves no production consumers remain. -4. Delete obsolete compatibility-only fixtures and replace them with physical - plan plus execution-parity fixtures. -5. Run: - - ```bash - GOCACHE="$(pwd)/.gocache" GOTOOLCHAIN=auto go test ./... -count=1 - GOCACHE="$(pwd)/.gocache" deadcode -test ./... - git diff --check - ``` - -6. With local Arango available, run the named Explain tests plus the - human-readable dataframe command: - - ```bash - LOOM_COMPILER_ARANGO_INTEGRATION=1 \ - GOCACHE="$(pwd)/.gocache" GOTOOLCHAIN=auto \ - go test ./internal/dataframe -run 'Test.*Explain.*Arango' -count=1 -v - - make dataframe-demo - ``` - -## Parallelization rules for Luna - -Use one integration owner per wave. `physical_plan.go`, -`physical_render.go`, `generic_physical_plan.go`, and `compile.go` are shared -hotspots and must not be edited concurrently by multiple workers. - -| Wave | Worker A | Worker B | Integration owner | -| --- | --- | --- | --- | -| 1 | WP1 IR/subplan validation | fixtures and parity harness | WP1 owner | -| 2 | WP2 child set lowering | WP3 child projection tests | WP2 owner | -| 3 | WP4 predicates/required filters | live Explain tests | WP4 owner | -| 4 | WP5 aggregates/derived fields | aggregate parity fixtures | WP5 owner | -| 5 | WP6 pivots | pivot fixtures/benchmark reporting | WP6 owner | -| 6 | WP7 slices | slice fixtures | WP7 owner | -| 7 | WP8 sharing/perf | WP9 deletion audit | compiler core owner | - -Workers must not write `compile.go` until work package 9. Each feature worker -reports the exact supported semantic shapes and leaves unsupported shapes -explicitly rejected by `BuildPhysicalPlan`; do not add a new fallback. diff --git a/docs/PHYSICAL_RENDERER_REPLACEMENT_PLAN.md b/docs/PHYSICAL_RENDERER_REPLACEMENT_PLAN.md deleted file mode 100644 index 736d813..0000000 --- a/docs/PHYSICAL_RENDERER_REPLACEMENT_PLAN.md +++ /dev/null @@ -1,251 +0,0 @@ -# Physical Renderer Replacement Plan - -## Objective - -Replace the compatibility `compileLowered(Builder, limit)` AQL string compiler -with one executable pipeline: - -```text -GraphQL Builder - -> SemanticPlan - -> PhysicalPlan - -> RenderPhysicalPlan - -> parameterized AQL -``` - -The physical plan is the only place where AQL execution strategy is chosen. -It must preserve the semantic plan exactly, including root row grain, selected -columns, filters, required relationship matches, pivots, aggregates, -representative slices, authorization scope, and dataset generation scope. - -`compileLowered` remains only as a test oracle during the migration. Do not -move its string-building helpers into the new renderer. - -## Non-negotiable invariants - -- No raw user-controlled AQL fragments enter the physical plan or renderer. -- Every root, traversed node, and edge is constrained by project, dataset - generation, and authorization scope. -- A physical plan validates lexical variable scope and bind-variable ownership - before rendering. -- The root execution window (`SORT root._key`, then optional `LIMIT`) occurs - before optional traversal work, unless a semantic ordering contract says - otherwise. -- Output column names and array/scalar/null semantics match the current - compiler exactly. -- A new route is enabled only after deterministic result parity and live - Arango `EXPLAIN` coverage prove it is correct and index-backed. - -## Target physical IR - -Keep the existing root scan, traversal, filter, derived-let, sort, limit, and -return operations, but replace the current navigation-only subset with typed -operations that can describe every semantic feature. - -| Capability | Required physical operation/value | -| --- | --- | -| FHIR selector extraction | `Extract` value: root value, selector path, fallback paths, value mode | -| Typed filters | `Predicate` tree: all/any/not, comparison, exists, membership, string match; typed bind values only | -| Relationship existence | `Exists` expression with a bounded typed traversal subplan and `LIMIT 1` | -| Optional child traversal | `Set`/subquery operation with traversal, resource-type filter, predicate, uniqueness, and stable sort | -| Shared traversal prefixes | `Set` operation can feed typed filtered subsets; lowering records shared-source provenance | -| Aggregates | `Aggregate` expression: count, distinct count, values, first/representative, and predicate guard | -| Pivots | `PivotMap` expression: key selector, value selector, permitted columns, collision/value mode | -| Representative slices | `Slice` expression: typed source set, predicate, stable sort, limit, nested projection | -| Derived fields | `Derived` expression: count, values, first, pivot map, and references to prior typed sets only | -| Projection | projection values can be scalar, array, object, aggregate, pivot, slice, or derived expression | - -Do not add one physical operation per historical AQL helper. Prefer a compact, -validated expression tree plus explicit set-producing operations. The renderer -owns only serialization of that tree; semantic interpretation belongs in the -lowerer. - -## Work packages - -### P1 — Freeze the executable IR contract - -**Owner:** compiler core. **Blocks:** all renderer work. - -1. Introduce `PhysicalExpression`, `PhysicalSet`, and typed predicate nodes in - `internal/dataframe/physical_plan.go`; make operation payloads a closed, - validated union. -2. Add explicit value cardinality and null behavior to expressions. Do not - infer scalar-versus-array behavior from renderer context. -3. Add `PhysicalSubplan`/set scope rules so a subquery cannot reference a - variable introduced later or outside its parent scope. -4. Version the physical plan and provide deterministic JSON fixtures for it. -5. Extend `PhysicalPlan.Validate` with bind type, selector-path, collection - bind, provenance, and mandatory-scope checks. - -**Acceptance:** invalid plans fail before AQL rendering; a fixture can express -every current semantic selection without a raw AQL string. - -### P2 — Direct semantic-to-physical lowering - -**Owner:** compiler core. **Blocks:** P3–P7. - -1. Add `BuildPhysicalPlan(SemanticPlan, ExecutionOptions) (PhysicalPlan, - error)`. -2. Stop using `lowerSemanticBuilder` in this new route. It may remain only for - compatibility-oracle tests until P9. -3. Lower root scan, root project/generation/auth scope, deterministic root - window, and `_key` projection directly from the semantic plan. -4. Lower each semantic node using the proven `storage_route` contract; - unsupported outbound/ANY routes must remain explicit compiler errors. -5. Carry semantic aliases and source fields into `PhysicalSource` for errors, - explain output, and goldens. - -**Acceptance:** navigation-only `CompileRequest` uses this route with no -`Builder.Sets` dependency and remains byte-stable where practical. - -### P3 — Typed selector extraction and root filters - -**Can run after P1/P2.** - -1. Lower `SemanticField` selectors, fallbacks, and value modes to `Extract`. -2. Lower `TypedFilter` to physical predicate trees; encode literals in bind - vars and retain existing FHIR payload traversal semantics. -3. Render extraction/predicate trees as fixed AQL templates, including nested - array iteration and null behavior. -4. Add parity fixtures for each filter operator and every supported value mode. - -**Acceptance:** root-only field/filter requests use the physical renderer and -match the compatibility query’s rows, JSON values, columns, and bind safety. - -### P4 — Required traversal matches and scoped relationship filters - -**Can run in parallel with P3 after P1/P2.** - -1. Lower `TraversalMatchMode` and required relationship filters into typed - `Exists` subplans instead of generated `LENGTH(FOR ...)` strings. -2. Render a bounded `LIMIT 1` correlated traversal with edge/node project, - generation, auth, label, target edge-type, and resource-type predicates. -3. Reuse the existing proven `storage_route` direction evidence; add explicit - tests for ResearchSubject -> ResearchStudy outbound traversal. - -**Acceptance:** required-match parity, no unscoped edge/node in rendered AQL, -and live Explain coverage for inbound and proven outbound routes. - -### P5 — Optional traversal sets and traversal-sharing - -**Can start once P1/P2 are stable; completes after P3/P4.** - -1. Lower child `SemanticNode`s into typed set subplans, preserving optional - traversal semantics and root row grain. -2. Materialize sibling-prefix sharing only when route, source, label, scope, - and traversal direction are equivalent; create typed filtered subsets for - distinct target resource types. -3. Implement dedupe and deterministic ordering as physical semantics, not - renderer-side textual rewrites. -4. Add plan goldens proving shared prefixes do not broaden target types or - alter result order. - -**Acceptance:** nested optional traversal fields execute through physical -plans; no regression in root row count; shared-prefix Explain/cost coverage. - -### P6 — Aggregates, derived fields, and representative slices - -**Depends on P3/P5.** - -1. Lower `SemanticAggregate`, `SemanticSlice`, and derived field operations - directly from semantic nodes/typed sets. -2. Specify empty-set behavior for every aggregate and derive it from current - result fixtures before implementation. -3. Render only fixed aggregate functions; reject unknown operations during - physical validation. -4. Ensure every representative slice has stable sort, bounded limit, and - nested typed projection. - -**Acceptance:** GDC-style counts, value lists, representative samples/files, -and derived fields have exact result parity on META fixtures. - -### P7 — Pivots and shaped object projections - -**Depends on P3/P5; can overlap P6.** - -1. Lower semantic pivots to `PivotMap` expressions with validated permitted - columns, key/value selectors, and collision semantics. -2. Render pivot maps with bind-backed column names; never synthesize AQL keys - from data or request strings. -3. Compose fields, aggregates, pivots, slices, and child-derived values into a - typed return object in requested column order. - -**Acceptance:** complex GDC dataframe fixtures with samples/files/groups/ -observations and pivots execute only through the physical renderer and retain -column shape exactly. - -### P8 — Performance, Explain, and observability gates - -**Runs continuously after P3; owns final performance sign-off.** - -1. Add a named real-world GDC dataframe fixture under `examples/` or - `conformance/compiler/`, with human-readable GraphQL input and expected - NDJSON/column contract. -2. Add cold/warm benchmark commands that report compile time, Arango query - execution time, rows, bytes, and rows/second separately. -3. For each physical feature, require live Arango `EXPLAIN` assertions: - no full collection scans, scoped root index use, traversal edge index use, - and bounded estimated cost relative to fixture cardinality. -4. Add a physical-plan explanation that reports operator counts, traversals, - shared prefixes, and selected indexes without exposing raw AQL internals. - -**Acceptance:** documented benchmark baseline and regression threshold for the -representative GDC dataframe; failures identify compiler phase versus Arango -execution phase. - -### P9 — Shadow parity and cutover - -**Depends on P3–P8.** - -1. Build a test-only harness that compiles each fixture through both the - physical path and `compileLowered`, executes both against the same Arango - generation, canonicalizes row-object key order, and compares rows/columns. -2. Cover root fields, nested fields, all filters, required matches, inbound and - outbound routes, aggregates, slices, pivots, auth scopes, empty results, - generation scope, and preview limits. -3. Make `CompileRequest` select the physical route for one capability family at - a time only after that family has parity and Explain gates. -4. Remove the fallback condition only when the full conformance matrix uses - physical plans. - -**Acceptance:** zero compatibility fallbacks in conformance; physical path is -the default for every supported GraphQL dataframe request. - -### P10 — Delete the compatibility compiler - -**Depends on P9. Must be one focused deletion PR.** - -1. Delete `compileLowered`, `compiler`, `NamedSet`, `DerivedField`, and other - lowered-only types/helpers that have no non-test consumer. -2. Delete `lowerSemanticBuilder` and old lowered-query goldens. -3. Simplify `CompileRequest` to semantic validation -> physical lowering -> - rendering. Preserve the public GraphQL input/output contract. -4. Run deadcode with tests, full Go tests, compiler conformance, integration - Explain tests, and benchmark gate. - -**Acceptance:** no production reference to `Builder.Sets`, `compileLowered`, -or lowered AQL string helpers; one compiler execution path remains. - -## Parallelization and merge order - -| Wave | Work packages | Shared files / merge owner | -| --- | --- | --- | -| 0 | P1, fixture/benchmark design from P8 | `physical_plan.go`: P1 owner | -| 1 | P2, P3, P4 | `physical_lowering.go`: P2 owner; P3/P4 add isolated feature files | -| 2 | P5, P6, P7, P8 | `physical_render.go`: renderer owner serializes reviewed IR additions | -| 3 | P9 | `compile.go`: cutover owner | -| 4 | P10 | deletion owner, after green matrix only | - -No worker edits `compile.go` while P1–P8 are in flight. Feature workers add -IR/lowering tests and renderer tests; the renderer owner integrates accepted -operation kinds. This prevents one feature from silently reintroducing -string-oriented lowering. - -## Explicit non-goals - -- Do not add unproven FHIR traversal directions merely to increase coverage. -- Do not optimize by materializing new collections before physical plans can -select and explain them. -- Do not alter GraphQL input/output names during this migration. -- Do not delete the compatibility compiler before live result parity, not just - query-string similarity, is proven. diff --git a/docs/QUICKSTART.md b/docs/QUICKSTART.md index ab781fc..55d22bd 100644 --- a/docs/QUICKSTART.md +++ b/docs/QUICKSTART.md @@ -84,7 +84,7 @@ For local work, run with `--no-auth`: Useful endpoints: - Apollo Sandbox: [http://127.0.0.1:8080/apollo](http://127.0.0.1:8080/apollo) -- GraphQL endpoint: [http://127.0.0.1:8080/graphql](http://127.0.0.1:8080/graphql) +- GraphQL endpoint: [http://127.0.0.1:8080/graphql/graph](http://127.0.0.1:8080/graphql/graph) - Health check: [http://127.0.0.1:8080/healthz](http://127.0.0.1:8080/healthz) On macOS you can jump straight to Apollo with: diff --git a/docs/RICH_PHYSICAL_RENDERER_PLAN.md b/docs/RICH_PHYSICAL_RENDERER_PLAN.md deleted file mode 100644 index a58a19c..0000000 --- a/docs/RICH_PHYSICAL_RENDERER_PLAN.md +++ /dev/null @@ -1,257 +0,0 @@ -# Rich Physical Renderer Plan - -## Goal - -Finish the physical renderer for the dataframe features that still execute -through `compileLowered` today: - -1. optional child traversal projections and filters; -2. aggregates and derived values over those child sets; -3. pivots; -4. representative slices; -5. deletion of the corresponding compatibility compiler code. - -The runtime rule at completion is simple: - -```text -Builder -> SemanticPlan -> PhysicalPlan -> RenderPhysicalPlan -> AQL -``` - -`compileLowered` is an oracle during implementation only. It must not receive -new product features or remain a production fallback after its physical -equivalent is available. - -## Already physical - -- root scan, project/generation/auth scope, root sort/limit; -- root fields, selector fallback/value modes, and root typed filters; -- optional navigation-only traversal sets; -- required traversal matches as correlated, scoped `EXISTS` subplans; -- proven inbound routes and ResearchSubject -> ResearchStudy outbound route. - -## Semantic contract to preserve - -For every work package, preserve all of the following before cutover: - -- one output row per root resource; -- root sort and preview limit happen before optional traversal work; -- child traversals never multiply root rows; -- every node and edge has project, dataset-generation, and auth predicates; -- selector fallback, scalar/array/distinct-array, null, and empty-array - behavior match the existing output contract; -- output column names and request order are stable; -- no request-derived text is serialized as AQL source. - -## W1 — Child traversal sets and child field projections - -### Physical IR - -Use the existing `PhysicalSet` plus `PhysicalSubplan` for every optional child -node. A child set captures its parent variable, contains its typed traversal -and scope operations, and returns the child node. Add a typed set projection -expression that accepts: - -- source set variable; -- child resource type; -- `PhysicalExtract` expression(s); -- scalar/array/distinct-array value mode; -- explicit null/empty behavior. - -Nested traversal sets capture their parent set element rather than the root. -They must be defined inside the parent set subplan, not as a top-level AQL -`FOR`, so root row grain cannot change. - -### Lowering - -1. Lower each optional `SemanticNode` to a `PhysicalSet` in request order. -2. Resolve the route exclusively with `storage_route.go`; reject unknown - outbound/ANY routes. -3. Lower each child `SemanticField` through `ResolveSemanticField`, exactly as - root fields are lowered today. -4. Add child output expressions to the owning root/parent projection. -5. Lower sibling-prefix sharing only after unshared child projections have - exact parity. Sharing is a physical optimization, not a semantic shortcut. - -### Renderer - -Render a child set as `LET set = (FOR node, edge IN ... RETURN node)` with all -scope filters in the subquery. Render child projections by iterating only over -the set variable. Never expand a child `FOR` in the outer root query. - -### Tests and deletion - -- unit goldens: one child field, nested child field, array and fallback field; -- result parity on META for patient -> specimen -> file; -- Explain: root scoped index + `fhir_edge` traversal index; -- cut over child-field requests; -- delete the matching `compileTraversal` field-projection branch once all - child-field fixtures use physical plans. - -## W2 — Typed child filters - -### Physical IR and lowering - -Reuse `PhysicalPredicateExpression` and `PhysicalExtract`; do not add a -second filter language. Lower `SemanticNode.Filters` into the child set -subplan immediately after traversal scope filters and before its return. - -Required-match filters use the same predicate lowering inside their `EXISTS` -subplan. This removes the last special-case relationship filter renderer. - -### Renderer - -Render selector predicates through the existing fixed templates for equality, -membership, text matching, date comparisons, EXISTS/MISSING, and ALL/ANY/NONE -quantifiers. Literal values remain bind variables. - -### Tests and deletion - -- parity matrix for every filter operator on root, child, nested child, and - required-match routes; -- auth and dataset-generation tests with a matching row outside scope; -- live inbound and proven-outbound Explain tests; -- remove `compileTypedFilters` use from traversal and required-match helpers. - -## W3 — Aggregates and derived values - -### Physical IR - -Use `PhysicalAggregate` and typed expressions over a set source. Support the -currently exposed semantic operations in this order: - -1. count and distinct count; -2. value arrays and distinct value arrays; -3. first/representative value; -4. min/max; -5. predicate-qualified aggregates. - -Define empty-set output explicitly from current GraphQL behavior before each -operation: count is `0`; value arrays are `[]`; scalar representative/min/max -are `null` unless existing tests prove otherwise. - -Derived fields are expressions over previously defined typed sets or typed -aggregate values. They may not reference a textual AQL variable name. - -### Renderer - -Render aggregate expressions as bounded AQL subqueries over a set variable. -Use `UNIQUE`, `SORTED_UNIQUE`, `FIRST`, `MIN`, `MAX`, and `LENGTH` only for -their validated typed operations. Preserve deterministic representative sort. - -### Tests and deletion - -- META GDC case/file counts, values, and null/empty behavior; -- aggregation parity for no child, one child, repeated values, and scoped-out - child documents; -- delete `compileRootAggregateExpr`, aggregate portions of - `compileDerivedField`, and aggregate-only named-set helpers after cutover. - -## W4 — Pivots - -### Physical IR - -Lower each `SemanticPivot` to `PhysicalPivotMap` with: - -- a typed source set or root payload source; -- resource type and parsed key/value selectors; -- requested columns stored as a bound list; -- explicit collision rule and absent-column null behavior. - -The pivot’s output object is a typed projection expression. Dynamic data values -must never become AQL object-key source. - -### Renderer - -Build the map through a fixed subquery that filters permitted columns and uses -bound column keys. Maintain the existing output naming and key order contract. - -### Tests and deletion - -- root and child pivots; -- sparse values, repeated keys, unknown requested column, and nested selector; -- real GDC observation pivot fixture with output-shape parity; -- delete `compileRootPivot`, `compileDerivedPivotMapLets`, and pivot-specific - string compiler helpers once every pivot route is physical. - -## W5 — Representative slices - -### Physical IR - -Lower `SemanticSlice` to `PhysicalSlice` over a typed child set with: - -- optional typed predicate; -- explicit stable sort expression; -- bind-backed positive limit; -- nested typed object projection. - -### Renderer - -Render a local set subquery: filter, stable sort, limit, then return the typed -object. It must not alter the outer root window or expose an unbounded child -collection. - -### Tests and deletion - -- zero, one, and many child records; -- tie-breaking stability; -- filter + slice interaction and nested projected field behavior; -- delete `compileRepresentativeSlice`, `compileRootSlice`, and slice-only - lowered helpers after physical cutover. - -## W6 — Traversal sharing optimization - -Do this only after W1–W5 have result parity without sharing. - -1. Identify sibling child sets with identical parent set, route direction, - edge label, scope, and traversal depth. -2. Materialize one typed broad traversal set, then typed resource-filtered - subsets. Never share across differing target-type semantics. -3. Add a physical-plan optimization pass that rewrites only equivalent set - subplans and records provenance/count in explain output. -4. Require parity and Explain cost improvement or neutrality; otherwise retain - the unshared physical plan. - -Delete generic compatibility named-set sharing only after this pass owns every -currently supported sharing case. - -## W7 — Cutover and deletion - -### Per-feature gate - -A feature family may leave compatibility only when all are true: - -1. semantic-to-physical lowering has deterministic plan goldens; -2. physical and compatibility execution have row/column/value parity on META; -3. physical AQL has scoped root and traversal index Explain coverage; -4. no runtime request for that family calls `compileLowered`. - -### Final deletion PR - -After W1–W6 pass, delete in one focused change: - -- `Compile`, `Lower`, `lowerSemanticBuilder`, and `compileLowered`; -- `Builder.Sets`, `NamedSet`, lowered derived-field types, and old AQL helper - methods that no longer have physical consumers; -- compatibility-only conformance fixtures, replacing them with physical-plan - and execution-parity fixtures. - -At that point `CompileRequest` is reduced to: - -```go -semantic := BuildSemanticPlan(builder) -physical := BuildPhysicalPlan(semantic) -return RenderPhysicalPlan(physical) -``` - -## Work ownership and order - -| Wave | Work | Shared-file owner | -| --- | --- | --- | -| 1 | W1 child sets and W2 predicates | physical lowerer owner | -| 2 | W3 aggregates and W4 pivots | physical renderer owner | -| 3 | W5 slices | physical renderer owner | -| 4 | W6 sharing + benchmark/Explain | optimizer owner | -| 5 | W7 deletion | compiler core owner | - -Only the owner for a wave edits `physical_plan.go`, `physical_render.go`, and -`compile.go`. Other workers contribute isolated feature tests and fixtures. diff --git a/docs/TERRA_ULTRA_EXECUTION_PLAN.md b/docs/TERRA_ULTRA_EXECUTION_PLAN.md deleted file mode 100644 index 3db0b6e..0000000 --- a/docs/TERRA_ULTRA_EXECUTION_PLAN.md +++ /dev/null @@ -1,1107 +0,0 @@ -# Terra Ultra Parallel Execution Plan - -## 1. Purpose - -This document converts the 20 gaps in -[`FORMAL_GAP_ANALYSIS.md`](FORMAL_GAP_ANALYSIS.md) into a safe multi-worker -execution program for a long-running Terra Ultra session. - -The compiler-first schedule in -[`COMPILER_FIRST_PLAN.md`](COMPILER_FIRST_PLAN.md) is now authoritative. Run -CP0-CP9 before the service-heavy waves in this document. The package ownership, -worktree, contract-freeze, handoff, and merge rules here still apply to compiler -workers. - -The objective is not to maximize the number of active workers. It is to -maximize completed, mergeable work while preventing workers from implementing -against contracts that another worker is still changing. - -The governing rule is: - -> Parallelize implementations behind frozen interfaces. Do not parallelize the -> definition of the same interface. - -Twenty gaps must not become twenty simultaneous branches. Dataset generation, -analysis, recipes, planner semantics, row identity, GraphQL, jobs, and generated -files are shared contracts. Uncoordinated work on them would create substantial -rework. - -## 1.1 Existing FHIR Backbone Is a Frozen Starting Asset - -Workers must begin from the implementation that is already present. They must -not design a replacement FHIR model, parser, graph schema, GraphQL framework, -or sample-data convention unless a specific failing conformance fixture proves -that the current owner cannot be extended. - -The baseline includes: - -- `META/`: the primary 14-resource sample dataset used for development and - end-to-end characterization -- `META_SMALL/`: the smaller sample dataset for faster local tests -- `schemas/graph-fhir.json`: the active graph/FHIR schema used by generation, - generic validation, and edge extraction -- `cmd/generate/main.go`: the existing FHIR generator -- `fhirstructs/model.go`: generated Go FHIR structs -- `fhirstructs/validate.go`: generated validation methods -- `fhirstructs/extract.go`: generated graph-edge extraction -- `fhirschema/generated.go`: generated field and traversal metadata -- `fhirschema/schema.go`: the handwritten lookup, selector, and pivot - logic over generated metadata -- `internal/ingest/generated_load.go`: generated fast-path resource dispatch -- `internal/ingest/row_builder.go`: generated/generic row-builder boundary -- `internal/catalog/`: existing populated-field, distinct-value, pivot, and - populated-reference profiling/discovery -- `graphqlapi/schema.graphqls`: the handwritten GraphQL contract -- `gqlgen.yml` and generated gqlgen artifacts under `graphqlapi/` -- `Makefile` targets `generate-fhir`, `generate-graphql`, `graphql-check`, and - `test` - -The generator already emits more than structs. It emits validation, edge -extraction, generated load dispatch, and `fhirschema` definitions/traversals. -A worker must inspect the relevant generator output and generator source before -adding handwritten resource-specific equivalents. - -The required extension policy is: - -1. use `META/` to characterize current behavior before creating synthetic data -2. add minimal synthetic fixtures only for cases not represented in `META/` -3. extend `schemas/graph-fhir.json` or `cmd/generate` when the missing behavior - is schema/generation-owned -4. regenerate with the existing Make targets -5. keep handwritten behavior in the existing owner package when it is not - generated -6. prove generated/generic parity when changing ingestion semantics -7. never copy generated FHIR types into a new recipe, analysis, or planner - package -8. keep GraphQL changes in `schema.graphqls` and regenerate through gqlgen; - never hand-edit generated GraphQL artifacts as the source of truth - -"Support any FHIR" therefore means extending and generalizing the existing -schema-driven backbone, not discarding it. - -## 2. Orchestrator Responsibilities - -Terra Ultra acts as the integration owner. It must: - -1. maintain one green integration branch -2. create worker packets from a known integration commit -3. assign exclusive package and shared-file ownership -4. enforce contract freeze gates -5. merge foundational contracts before launching their consumers -6. stop or rebase workers when an upstream contract changes -7. run integration and conformance tests after every merge -8. publish a handoff manifest for each completed packet -9. keep unsupported capabilities explicit -10. prevent workers from weakening validation merely to make fixtures pass -11. reject work that duplicates existing generated FHIR or gqlgen ownership -12. require a baseline run against `META/` for every ingestion, catalog, - planner, or analysis packet - -Terra should use spare workers for review, tests, fixtures, performance -measurement, and threat analysis rather than assigning two workers to the same -hotspot. - -## 3. Corrected Dependency Graph - -The formal gap analysis lists a mostly sequential execution order. For parallel -delivery, Gap 15 must be split: - -- **15A Job substrate:** durable state machine, leasing, cancellation, status, - concurrency, handler interface -- **15B Export recovery:** row-stream progress, retries, resume/idempotency, - artifact results -- **15C Job hardening:** retention, cleanup, operational recovery - -Gap 3 requires 15A. Gap 14 can consume the job handler contract. Gap 15B cannot -finish until the streaming runtime exists. - -```text -G1 Conformance corpus - ├──> G6 Recipe V1 ───────────────────────────────────────────┐ - └──> shared product fixtures │ - │ -G2 Schema identity ──> G3 Dataset generations ──> G4 Integrity ──> G5 Analysis - │ ^ │ - │ │ v - └──────────────> G15A Job substrate G7 Templates - │ -G6 Recipe V1 ─────────────────────────────────────────────────────┤ - v - G8 Capability API ──> G9 Persistence - │ -G6 Recipe V1 ──> G10 Planner ──> G11 Filters │ - │ │ - └──────────> G12 Cost/cardinality <───────────┘ - │ - v - G13 Preview - │ - v - G14 Row stream - │ - ┌────────────┴────────────┐ - v v - G15B Jobs G16 Elasticsearch - -G17 Authorization: continuous lane, final audit after G16 -G18 Operations: skeleton after G2/G3, finish after G14/G15 -G19 Observability: primitives early, budgets after G16 -G20 Testing: framework first, continuous evidence, final matrix last -``` - -## 4. Contract Freeze Gates - -No downstream worker starts before the applicable freeze has been merged into -the integration branch. - -### C0: Conformance vocabulary - -Freeze: - -- recipe-family IDs -- row-grain IDs -- fixture schema and fixture IDs -- support states -- warning/error-code naming rules -- conformance result format - -Required evidence: - -- fixture schema tests -- duplicate-ID test -- deterministic conformance smoke result - -### C1: Identity envelope - -Freeze: - -- project identity -- schema identity -- dataset generation ID and states -- active-generation resolution -- legacy dataset behavior -- analysis version placeholder -- authorization-scope fingerprint representation - -Canonical concepts consumed by all later work: - -```text -project -datasetGeneration -schemaIdentity -analysisVersion -authScopeFingerprint -``` - -Required evidence: - -- schema digest tests -- generation activation tests -- no mixed-generation reads - -### C2: Recipe V1 - -Freeze: - -- semantic IDs -- grain representation -- column selections -- filter expression serialization -- destination representation -- normalized JSON -- typed error paths/codes -- recipe/template version fields - -Required evidence: - -- golden serialization tests -- deterministic normalization -- fixture round trips - -Gap 11 may add operator implementations later, but it must not invent a second -serialized filter model. - -### C3: Job contract - -Freeze: - -- job types and states -- claim/lease rules -- cancellation contract -- progress, result, and error envelopes -- idempotency key -- handler interface -- restart recovery behavior - -Required evidence: - -- two-worker lease test -- expired-lease reclaim test -- cancellation state test - -### C4: Analysis snapshot - -Freeze: - -- resource, field, relationship, and value-analysis document keys -- project/generation/scope fields -- coverage denominators -- fanout representation -- truncation markers -- snapshot version/freshness -- relationship quality states - -Required evidence: - -- deterministic snapshot for fixture data -- auth-scope isolation test -- generation replacement test - -### C5: Planner IR - -Freeze: - -- logical operators -- grain/root representation -- traversal representation -- projection and filter representation -- row identity -- duplicate/expansion semantics -- capability descriptors -- explain result shape - -Required evidence: - -- existing Patient result parity -- logical-plan goldens -- generic-versus-optimized parity - -Only after C5 may separate workers add grain modules. - -### C6: Product capability API - -Freeze: - -- support decision and reason codes -- dataset summary -- template availability -- recipe options -- validation result -- plan/cardinality explanation -- cache identity - -Required evidence: - -- capability and planner agreement for every fixture -- stale-analysis behavior -- GraphQL contract tests - -### C7: Row-stream contract - -Freeze: - -- output column schema -- row representation -- missing-value policy -- array encoding policy -- cancellation/error semantics -- progress counters -- provenance -- checksum and row-count rules - -Required evidence: - -- preview/stream row parity -- bounded-memory test -- cancellation test - -### C8: Artifact and destination contract - -Freeze: - -- artifact lifecycle -- temporary/finalization semantics -- destination secret references -- deterministic Elasticsearch document IDs -- retry classification -- partial-failure reporting - -Required evidence: - -- interrupted-artifact cleanup -- deterministic-ID test -- partial bulk response test - -## 5. Package Ownership Lanes - -Assign workers to durable package lanes rather than one worker per gap. - -### Lane A: Dataset loading and lifecycle - -Owns: - -- G2 schema identity -- G3 dataset generations -- load integration with G15A - -Primary files: - -- `internal/graphschema/` -- new `internal/dataset/` -- `internal/ingest/` -- generation-aware ingestion storage - -Exclusive hotspots during its integration window: - -- `internal/ingest/load.go` -- `internal/ingest/backend.go` -- `internal/ingest/row_builder.go` -- command entrypoints for load wiring - -Published boundary: - -```go -type DatasetRef struct { - Project string - Generation string -} - -type Resolver interface { - Active(ctx context.Context, project string) (DatasetRef, error) -} -``` - -Exact names may change during C1, but only one canonical resolver may survive. - -### Lane B: Analysis and catalog - -Owns: - -- G4 reference integrity -- G5 analysis snapshots - -Primary files: - -- new `internal/analysis/` -- new `internal/analysis/referenceintegrity/` -- `internal/catalog/` -- analysis cache behavior - -Consumes Lane A's finalized `DatasetRef`. It must not invent generation -lifecycle or query inactive data. - -Lane A invokes an analysis/finalization interface; Lane B implements it. The two -lanes must not both edit load orchestration. - -### Lane C: Recipe domain and templates - -Owns: - -- G6 Recipe V1 -- G7 semantic vocabulary and templates - -Primary files: - -- new `internal/recipe/` -- new `internal/recipe/templates/` -- recipe JSON Schema - -During C2, consume current field and traversal behavior through adapters. Do -not immediately move: - -- `graphqlapi/dataframe/fieldrefs.go` -- `internal/dataframe/traversal_rules.go` - -Consolidation occurs after recipe and planner contracts agree. - -### Lane D: Planner and filters - -Owns: - -- G10 planner -- G11 filter compiler - -Primary files: - -- `internal/dataframe/` -- eventual `internal/dataframe/planner/` - -This is the highest-collision lane. It has one lead until C5. After C5, additive -workers may implement: - -- Patient parity/optimizations -- Specimen and DocumentReference/File -- Condition and Observation -- ResearchSubject/Study enrollment - -Only the planner lead edits central dispatch and shared IR. - -### Lane E: Product API and recipe persistence - -Owns: - -- G8 capability orchestration -- G9 recipe persistence -- product GraphQL integration - -Primary files: - -- new `internal/productapi/` -- recipe storage implementation -- GraphQL product operations - -This lane is the exclusive GraphQL owner. Other workers return Go services and -types; they do not independently change public GraphQL. - -### Lane F: Plan analysis and preview - -Owns: - -- G12 cardinality/cost analysis -- G13 bounded preview - -Primary files: - -- planner analysis/cost model -- preview service -- cursor codec - -It consumes C4 and C5. It does not redefine the planner IR or edit GraphQL. - -### Lane G: Streaming export and artifacts - -Owns: - -- G14 row stream -- NDJSON/CSV sinks -- artifact-store abstraction - -Primary files: - -- new `internal/export/` -- new `internal/export/artifact/` - -It consumes C7 and does not modify planner semantics. - -### Lane H: Durable jobs - -Owns: - -- G15A substrate early -- G15B export recovery -- G15C hardening - -Primary files: - -- new `internal/jobs/` - -Implement the core with fake handlers. Lane A and Lane G add real handlers only -through the frozen job interface. - -### Lane I: Elasticsearch - -Owns: - -- G16 bulk sink -- destination/mapping preflight - -Primary files: - -- new `internal/export/elasticsearch/` - -Most tests use an HTTP test server. Retry scheduling remains owned by Lane H; -the Elasticsearch package classifies item errors and reports retryable items. - -### Lane J: Security, operations, and observability - -G17-G19 are continuous concerns, not permission to edit every package. - -Own additive infrastructure: - -- authorization test helpers and matrix -- scope fingerprint helper -- readiness/configuration package -- audit-event interface -- metrics registry -- deployment manifests and runbook - -Every feature owner authorizes and instruments its own path. A final Lane J -worker audits the integrated system. - -### Lane K: Conformance and system tests - -Owns: - -- G1 -- continuous G20 evidence -- `conformance/` -- capability matrix publication - -It must not add production shortcuts merely to pass fixtures. - -## 6. Shared-File Hotspots - -These files have one integration owner per wave: - -- `graphqlapi/schema.graphqls` -- `graphqlapi/schema.resolvers.go` -- `graphqlapi/generated.go` -- `graphqlapi/model/models.go` -- `internal/httpapi/routes.go` -- `internal/httpapi/service.go` -- `internal/dataframe/service.go` -- current `internal/dataframe/planner.go` -- `internal/dataframe/lowered_types.go` -- `internal/dataframe/lowered_compile.go` -- `internal/ingest/load.go` -- `internal/ingest/backend.go` -- `cmd/arango-fhir-server/main.go` -- `cmd/arango-fhir-proto/main.go` -- `Makefile` -- `go.mod` -- `go.sum` -- top-level README and capability matrix - -Generated GraphQL artifacts are regenerated once by Lane E or the integration -owner after service contracts merge. Parallel workers must not commit their own -independently generated copies. - -If a worker needs a shared-file change it does not own, its handoff includes a -contract-change request or minimal integration patch description. - -## 7. Parallel Execution Waves - -The waves below assume four to six implementation workers plus one integrator. - -### Wave 0: Product oracle and baseline - -Start all workers from the same current green commit. - -#### W0-A: Conformance corpus - -Implements G1: - -- fixture schema -- conversation fixtures -- fixture datasets -- runner -- deterministic results - -Owns `conformance/`. - -#### W0-B: Baseline characterization - -Adds tests only for current: - -- generated/generic ingestion -- planner support/rejections -- auth scoping -- GraphQL contracts -- preview memory/result behavior -- the resource types and link tuples observed in `META/` -- generated versus generic behavior for the `META/` resource types -- current field/pivot catalog output produced from `META/` -- current generator and gqlgen reproducibility - -Must not improve production behavior in this packet. - -#### W0-C: Architecture verification - -Read-only review producing: - -- package/caller map -- current collection/index map -- public API map -- hotspot confirmation - -#### Gate C0 - -Merge fixtures and baseline tests. Freeze vocabulary before downstream work. - -### Wave 1: Independent foundational contracts - -#### W1-A: Schema identity and ingestion preflight - -Implements G2 without dataset generation activation. - -#### W1-B: Recipe V1 contract - -Implements G6 types, JSON Schema, normalization, and error model. Does not yet -translate into live analysis/templates. - -#### W1-C: Job substrate - -Implements G15A with fake handlers. - -#### W1-D: Security/test primitives - -Adds: - -- authorization matrix -- scope fingerprint proposal/helper -- reusable negative-test helpers -- test taxonomy - -#### W1-E: Metrics/config primitives - -Adds additive metrics and configuration packages only. Does not instrument all -features yet. - -#### Gates C1-C3 - -The integrator aligns and freezes the shared identity, recipe, and job -contracts. Dependent worktrees are recreated or rebased from this merge. - -### Wave 2: Atomic ingestion and semantic foundations - -#### W2-A: Dataset generations - -Implements G3 using C1 and C3. - -#### W2-B: Template registry structure - -Implements G7 registry mechanics with fake analysis/planner capability sources. -Do not finalize template availability yet. - -#### W2-C: Planner foundation - -Implements only: - -- logical IR -- generic physical interfaces -- capability descriptor interface -- Patient parity path - -This worker owns planner hotspots. - -#### W2-D: Operational skeleton - -Implements: - -- migration registry -- startup configuration validation -- readiness framework -- graceful worker shutdown interfaces - -#### W2-E: Generation and auth tests - -Expands conformance/system tests without editing production owners' packages. - -#### Gate - -Dataset generation storage/query behavior must be frozen before analysis begins. -This is the most consequential repository-wide merge point. - -### Wave 3: Dataset intelligence and planner expansion - -#### W3-A: Reference integrity - -Implements G4 against active-generation helpers. - -#### W3-B: Resource and field analysis - -Implements the G5 resource/field half using the analysis schema owner. - -#### W3-C: Relationship, fanout, and value analysis - -Implements the G5 relationship/value half. It consumes the same shared analysis -document types as W3-B. - -One of W3-B/W3-C owns collection registration and migrations; the other may -only add analyzers and tests. - -#### W3-D through W3-F: Grain modules - -After C5 is merged, assign additive planner modules: - -- Specimen and File -- Condition and Observation -- ResearchSubject and Study enrollment - -The planner lead remains the only central dispatch owner. - -#### W3-G: Typed filter implementation - -Implements G11 against C2 and C5. It may add operators but cannot change Recipe -V1 serialization without a contract amendment. - -#### Gate C4-C5 - -Freeze deterministic analysis snapshots and planner IR. Require fixture-based -result parity before product APIs consume them. - -### Wave 4: Product capability plane - -#### W4-A: Final templates and capability evaluator - -Completes G7 using real analysis and planner descriptors. - -Template-family entries may be split among workers only after registry schema -is frozen. Those workers add definitions and fixtures, not mechanics. - -#### W4-B: Capability service and GraphQL - -Implements G8 and exclusively owns GraphQL changes/generated artifacts. - -#### W4-C: Recipe storage - -Implements G9 CRUD and ownership. Compatibility/revalidation integrates after -the capability service lands. - -#### W4-D: Cardinality and cost - -Implements G12 against C4/C5. - -#### W4-E: Security integration tests - -Tests analysis, capability caching, recipes, and plan explanation across scopes. - -#### W4-F: Thin frontend mock - -May build against the proposed C6 contract using a faithful mock. It must not -invent missing fields or use raw catalog records. - -#### Gate C6 - -Capability API and planner must agree on every conformance fixture. Only then -connect the frontend to live services. - -### Wave 5: Execution plane - -#### W5-A: Bounded preview - -Implements G13. - -#### W5-B: Row-stream foundation and executor seam - -Defines C7 and performs the one shared execution refactor. This is the only -worker editing existing dataframe execution entrypoints during the wave. - -#### W5-C: NDJSON sink - -Starts after C7, owns additive export files. - -#### W5-D: CSV and artifact store - -Starts after C7, owns additive serializer/artifact files. - -#### W5-E: Export job integration - -Implements G15B against C3/C7. - -#### W5-F: Execution verification - -Owns: - -- preview/export parity -- cancellation propagation -- stable paging -- bounded-memory evidence -- worker restart tests - -#### Gates C7-C8 - -Freeze stream and artifact contracts before external delivery work. - -### Wave 6: External delivery and release hardening - -#### W6-A: Elasticsearch bulk transport - -Implements encoding, batching, response parsing, and retry classification. - -#### W6-B: Elasticsearch preflight - -Implements destination configuration, permission/mapping checks, deterministic -ID verification, and secret handling. - -#### W6-C: Final security audit - -Completes G17, including cache isolation, artifact authorization, SSRF controls, -and audit events. - -#### W6-D: Deployment and runbook - -Completes G18. - -#### W6-E: Metrics and performance - -Completes G19 with budgets, load tests, dashboards, and Arango plan evidence. - -#### W6-F: Release conformance - -Completes G20, failure injection, and generated capability matrix. - -#### Release gate - -A clean development deployment must pass the same conformance suite as CI, -including restart, authorization isolation, preview/export parity, and any -advertised Elasticsearch capability. - -## 8. False Parallelism to Prohibit - -Terra must not launch these combinations without the named freeze: - -- G2 and G3 both editing ingestion orchestration before C1 -- G3 and G5 independently defining catalog/generation keys -- G4 and G5 independently defining relationship-analysis documents -- G6 and G11 independently defining filter serialization -- G7 and G10 independently defining semantic relationships -- multiple grain workers before C5 -- G8 before real G5/G7/G10 capability sources exist -- G9 compatibility logic before template versioning -- G12 before C4/C5 -- G13 and G14 both refactoring dataframe execution -- all of G15 deferred until export -- G16 before deterministic row identity and C8 -- G17, G19, or G20 deferred as final cleanup -- frontend work against low-level catalog records -- multiple workers regenerating GraphQL artifacts - -## 9. Worker Packet Template - -Every Terra worker receives a packet using this exact structure. - -```markdown -# Packet W-- - -## Identity -- Objective: -- Gap(s): -- Packet type: contract | implementation | integration | verification -- Base integration commit: -- Integration owner: - -## Required reading -- Repository instructions -- Exact gap sections -- Applicable conformance fixture IDs -- Frozen contract documents/examples -- Current owner files and callers -- Existing backbone files listed in section 1.1 when the packet touches FHIR, - ingestion, schema metadata, GraphQL, catalog, or planner behavior - -## Frozen contracts consumed -- Contract name/version: -- Types and serialization that must not change: -- Dataset-generation invariant: -- Authorization invariant: - -## Scope -- Owned packages/files: -- Read-only packages/files: -- Shared files requiring integrator changes: -- Generated files allowed: yes/no -- Migrations allowed: yes/no - -## Non-goals -- Explicitly excluded behavior -- Existing generated or schema-driven behavior that must be reused - -## Deliverables -- Production types/implementation -- Unit tests -- Integration/conformance evidence -- Migration/compatibility note -- API/operator documentation -- Handoff manifest - -## Required verification -- Baseline command/result against `META/` when applicable -- Targeted package tests -- `go test ./...` -- race tests where applicable -- fixture subset -- integration command -- `git diff --check` -- generated-artifact check -- required negative/cancellation/performance tests - -## Stop conditions -- Frozen contract is insufficient or contradictory -- Required fixture or upstream commit is absent -- Another active worker owns a required file -- Generation or authorization invariants cannot be preserved -- Destructive migration is required -- Public/generated contract must change -- Baseline tests fail for unrelated reasons -- Performance evidence disproves the design -- The proposed implementation would duplicate generated FHIR structs, - validators, extractors, traversal metadata, or gqlgen-owned artifacts - -## Handoff -- Structured manifest required -``` - -Do not issue a packet whose objective is simply "implement Gap N." Name the -exact packages, types, endpoints, collections, fixtures, and observable result. - -## 10. Handoff Manifest - -Every worker returns a machine-readable handoff: - -```yaml -packet: W3-D-specimen-file-grains -base_commit: -result_commit: -status: complete | blocked | partial -contracts_consumed: - - recipe/v1 - - analysis-snapshot/v1 - - logical-plan/v1 -contracts_added: [] -files_changed: [] -migrations: [] -fixtures_enabled: [] -tests: - - command: go test ./internal/dataframe/... - result: pass -known_limits: [] -downstream_actions: [] -contract_change_requests: [] -``` - -Blocked workers must include evidence, affected packet IDs, and the smallest -proposed contract amendment. They must not silently redesign an upstream -contract. - -## 11. Worktree and Branch Strategy - -Use a separate Git worktree per implementation worker. Workers sharing one -checkout would observe and overwrite incomplete files, especially generated -GraphQL artifacts. - -`META/` and `META_SMALL/` are currently local workspace data rather than files -that a new Git worktree can be assumed to contain. Terra must make them -available explicitly to workers that need them, for example through a -read-only shared absolute path, a worktree-local symlink, or a configured -`META_DIR`. Workers must not silently fall back to invented data because their -worktree lacks `META/`. Do not commit or duplicate a large dataset merely to -solve worktree setup; keep small committed conformance fixtures separate. - -Suggested layout: - -```text -../loom-terra/ - integration/ - w0-conformance/ - w1-schema/ - w1-recipe/ - w1-jobs/ - w2-dataset/ - w3-analysis/ - w3-planner/ - w5-export/ -``` - -Branch naming: - -```text -codex/terra-w-- -``` - -Rules: - -1. Start every worker from the current wave's tagged green integration commit. -2. Provision and verify the shared sample-data path before launching any packet - that requires a `META/` baseline. -3. Do not start from another active worker branch. -4. Workers commit only owned files unless explicitly assigned an integration - patch. -5. The integrator merges or cherry-picks into `codex/terra-integration`. -6. After a contract merge, rebase or recreate dependent worktrees. -7. Split multi-day work into contract, implementation, and integration commits. -8. Regenerate shared generated code once on the integration branch. -9. Keep fixtures/generated data separate from production changes where - practical. -10. Do not maintain one long-lived mega-branch per original gap. - -## 12. Merge Procedure - -For every packet: - -1. Verify the base commit matches the declared integration gate. -2. Review the handoff manifest. -3. Reject edits outside package ownership unless preapproved. -4. Run targeted tests. -5. Merge into integration. -6. Regenerate shared artifacts if the integration owner is responsible. -7. Run `go test ./...`. -8. Run the relevant conformance subset. -9. Run `git diff --check`. -10. Update the capability matrix from test results. -11. Tag or record the new green integration commit. -12. Rebase/recreate downstream workers before they continue. - -If integration exposes a contract defect: - -1. pause all consumers of that contract -2. publish a versioned amendment -3. merge the amendment alone with contract tests -4. rebase consumers -5. rerun their contract/conformance tests - -## 13. Required Integration Gates - -Every branch: - -```bash -rtk go test .//... -rtk go test ./... -rtk git diff --check -``` - -Additional gates: - -- FHIR backbone: existing `META/` resources still load; generator output is - reproducible; generated/generic parity is maintained -- GraphQL: generation is clean and HTTP contract tests pass -- persistence: migrations are idempotent; legacy startup behavior is explicit -- dataset: no read mixes generations -- analysis: every query contains project, generation, and authorization scope -- planner: Patient parity and logical-plan goldens pass -- preview/export: row/schema parity and cancellation pass -- jobs: lease race and restart recovery pass -- Elasticsearch: partial success and deterministic retry IDs pass -- security: cross-project and cross-scope negative tests pass -- release: `make conformance` passes against a clean deployment - -## 14. Terra Scheduling Rules - -Use these rules for long-running efficiency: - -- Keep one integrator slot available whenever a wave has more than three active - workers. -- Limit the planner lane to one core owner until C5. -- Limit GraphQL to one owner at all times. -- Limit collection/migration registration to one owner per wave. -- When a worker blocks on a contract, reassign it to fixtures/tests for the same - lane rather than allowing speculative implementation. -- Cancel or restart workers whose base commit predates a changed frozen - contract. -- Prefer short contract packets followed by long additive implementation - packets. -- Merge usable internal slices quickly; do not let foundational interfaces live - only on a multi-day worker branch. -- Enable a capability only when its conformance fixture passes on integration. - -## 15. First Terra Ultra Launch Set - -The safest initial launch is now the Compiler Wave A set: - -1. **CP0-A Compiler corpus and result comparison** -2. **CP0-B Current compiler/AQL/Arango baseline characterization** -3. **CP1-A Semantic IR contract design** -4. **CP2-A Generated FHIR semantic API inventory/design** - -After the compiler oracle and semantic IDs freeze, launch Compiler Wave B: - -1. **CP3-A Row grain/cardinality** -2. **CP4-A Typed expression/filter AST** -3. **CP6-A Aggregate semantics** -4. **CP6-B Pivot semantics** -5. **CP0-C Conformance fixture expansion**, if capacity remains - -Do not launch a large frontend, recipe persistence, Elasticsearch, or broad job -infrastructure before the compiler release gate. Waiting is cheaper than -building product machinery around unstable grain, filter, pivot, or row-identity -semantics. diff --git a/fhirschema/compiler_semantics.go b/fhirschema/compiler_semantics.go index 4827194..8f08533 100644 --- a/fhirschema/compiler_semantics.go +++ b/fhirschema/compiler_semantics.go @@ -77,6 +77,19 @@ func HasResource(resourceType string) bool { // ResourceExists is the explicit predicate form of HasResource. func ResourceExists(resourceType string) bool { return HasResource(resourceType) } +// DefinitionExists reports whether name is represented by the generated FHIR +// schema. Unlike HasResource, it also accepts backbone and choice definitions +// that are valid selector sources but are not graph collections. This is a +// constant-time predicate and does not materialize or clone flattened fields. +func DefinitionExists(name string) bool { + name = strings.TrimSpace(name) + if name == "" { + return false + } + _, ok := generatedDefinitions[name] + return ok +} + // ResolveFieldSemantics resolves a canonical path using generated schema // metadata and returns only stable, compiler-safe values. func ResolveFieldSemantics(resourceType, canonicalPath string) (FieldSemantics, bool) { diff --git a/fhirschema/compiler_semantics_test.go b/fhirschema/compiler_semantics_test.go index 699e02b..29a1219 100644 --- a/fhirschema/compiler_semantics_test.go +++ b/fhirschema/compiler_semantics_test.go @@ -18,6 +18,21 @@ func TestResourceTypesAreSortedDefensiveCopy(t *testing.T) { } } +func TestDefinitionExistsIncludesBackbonesWithoutTreatingThemAsResources(t *testing.T) { + if !DefinitionExists("Patient") { + t.Fatal("Patient should be a generated definition") + } + if !DefinitionExists("ObservationComponent") { + t.Fatal("ObservationComponent should be a generated backbone definition") + } + if HasResource("ObservationComponent") { + t.Fatal("ObservationComponent must not be treated as a graph resource") + } + if DefinitionExists("") || DefinitionExists("UnknownDefinition") { + t.Fatal("empty and unknown definitions must not exist") + } +} + func TestResolveFieldSemanticsFromGeneratedMetadata(t *testing.T) { tests := []struct { path string diff --git a/fhirschema/schema.go b/fhirschema/schema.go index f157162..36bda29 100644 --- a/fhirschema/schema.go +++ b/fhirschema/schema.go @@ -59,8 +59,13 @@ type ResolvedPath struct { type PivotSpec struct { Family string CatalogRootPath string - ColumnSelector FieldSelectorSpec - ValueSelector FieldSelectorSpec + // ItemSourcePath identifies the repeated item scope that owns key/value. + // Empty means the resource itself is one pivot item. + ItemSourcePath string + ItemResourceType string + ColumnSelector FieldSelectorSpec + ValueSelector FieldSelectorSpec + ValueSelectors []FieldSelectorSpec } type TraversalSpec struct { @@ -169,10 +174,11 @@ func ResolvesToCodeableConcept(resourceType, canonicalPath string) bool { return ok && resolved.PropertyRef == "CodeableConcept" } -func ObservationValueSelectorOptions(resourceType string) []FieldSelectorSpec { - if resourceType != "Observation" { - return []FieldSelectorSpec{} - } +// ChoiceValueSelectorOptions returns generated-schema-backed value[x] +// selectors for a resource or generated backbone definition. FHIR-specific +// knowledge stays in this schema package; compiler and renderer code consume +// the resulting ordered selectors generically. +func ChoiceValueSelectorOptions(resourceType string) []FieldSelectorSpec { candidates := []string{ "valueQuantity.value", "valueCodeableConcept.text", @@ -200,6 +206,58 @@ func ObservationValueSelectorOptions(resourceType string) []FieldSelectorSpec { return out } +func prependSelector(existing []FieldSelectorSpec, preferred FieldSelectorSpec) []FieldSelectorSpec { + preferredPath := CanonicalPath(preferred) + result := []FieldSelectorSpec{preferred} + for _, candidate := range existing { + if CanonicalPath(candidate) != preferredPath { + result = append(result, candidate) + } + } + return result +} + +func normalizePivotSelectors(resourceType, catalogRootPath string, column, value FieldSelectorSpec) (FieldSelectorSpec, FieldSelectorSpec, string, string) { + itemType := resourceType + itemSource := "" + if source, repeatedType, ok := repeatedPivotItemScope(resourceType, catalogRootPath); ok { + itemType = repeatedType + itemSource = source + column = relativeSelector(column, itemSource) + value = relativeSelector(value, itemSource) + } + return column, value, itemType, itemSource +} + +func repeatedPivotItemScope(resourceType, canonicalPath string) (string, string, bool) { + parts := strings.Split(CanonicalizePath(canonicalPath), ".") + for index := len(parts) - 1; index >= 0; index-- { + if !strings.HasSuffix(parts[index], "[]") { + continue + } + source := strings.Join(parts[:index+1], ".") + resolved, ok := ResolvePath(resourceType, source) + if !ok || resolved.Property.Kind != "array" || strings.TrimSpace(resolved.Property.ItemRef) == "" || index+1 >= len(parts) { + continue + } + itemType := resolved.Property.ItemRef + relative := strings.Join(parts[index+1:], ".") + if _, ok := ResolvePath(itemType, relative); ok { + return source, itemType, true + } + } + return "", "", false +} + +func relativeSelector(selector FieldSelectorSpec, prefix string) FieldSelectorSpec { + prefix = CanonicalizePath(prefix) + path := CanonicalPath(selector) + if strings.HasPrefix(path, prefix+".") { + return FieldSelectorSpecFromPath(strings.TrimPrefix(path, prefix+".")) + } + return selector +} + func FieldSelectorSpecFromPath(path string) FieldSelectorSpec { sourcePath, valuePath := selectorParts(CanonicalizePath(path)) return FieldSelectorSpec{ @@ -222,11 +280,15 @@ func ValidatePivotSelectors(resourceType string, column FieldSelectorSpec, value } if match, ok := resolvePivotFamily(resourceType, columnCanonical, valueCanonical); ok { + column, value, itemType, itemSource := normalizePivotSelectors(resourceType, match.catalogRootPath, column, value) return PivotSpec{ - Family: match.family, - CatalogRootPath: match.catalogRootPath, - ColumnSelector: normalizeSelectorSpec(column, columnExpr), - ValueSelector: normalizeSelectorSpec(value, valueExpr), + Family: match.family, + CatalogRootPath: match.catalogRootPath, + ItemSourcePath: itemSource, + ItemResourceType: itemType, + ColumnSelector: normalizeSelectorSpec(column, columnExpr), + ValueSelector: normalizeSelectorSpec(value, valueExpr), + ValueSelectors: []FieldSelectorSpec{normalizeSelectorSpec(value, valueExpr)}, }, nil } @@ -234,31 +296,72 @@ func ValidatePivotSelectors(resourceType string, column FieldSelectorSpec, value } func DefaultPivotSpec(resourceType, canonicalPath string, observedValuePath string) (PivotSpec, bool) { - if ResolvesToCodeableConcept(resourceType, canonicalPath) { - column := FieldSelectorSpecFromPath(canonicalPath + ".coding[].display") - value := column + canonicalPath = CanonicalizePath(canonicalPath) + if source, itemType, ok := repeatedPivotItemScope(resourceType, canonicalPath); ok { + relativeRoot := strings.TrimPrefix(canonicalPath, source+".") + if !ResolvesToCodeableConcept(itemType, relativeRoot) { + return PivotSpec{}, false + } + column := defaultCodeableColumnSelector(itemType, relativeRoot) + values := ChoiceValueSelectorOptions(itemType) if strings.TrimSpace(observedValuePath) != "" { - value = FieldSelectorSpecFromPath(observedValuePath) + values = prependSelector(values, FieldSelectorSpecFromPath(observedValuePath)) } - spec, err := ValidatePivotSelectors(resourceType, column, value) - return spec, err == nil + if len(values) == 0 { + return PivotSpec{}, false + } + return PivotSpec{ + Family: PivotFamilyCodeableConcept, CatalogRootPath: canonicalPath, + ItemSourcePath: source, ItemResourceType: itemType, + ColumnSelector: column, ValueSelector: values[0], ValueSelectors: values, + }, true } - if resourceType == "Observation" && canonicalPath == "code" { - column := FieldSelectorSpecFromPath("code.coding[].display") - value := FieldSelectorSpecFromPath(strings.TrimSpace(observedValuePath)) - if strings.TrimSpace(observedValuePath) == "" { - options := ObservationValueSelectorOptions(resourceType) - if len(options) == 0 { - return PivotSpec{}, false + if ResolvesToCodeableConcept(resourceType, canonicalPath) { + column := defaultCodeableColumnSelector(resourceType, canonicalPath) + // Observation-style code/value[x] pivots use the resource's choice + // value. Other CodeableConcept fields (for example + // valueCodeableConcept itself) pivot their own text/coding value and + // must not borrow unrelated sibling value[x] selectors. + values := []FieldSelectorSpec{column} + if canonicalPath == "code" { + options := ChoiceValueSelectorOptions(resourceType) + if strings.TrimSpace(observedValuePath) != "" { + values = prependSelector(options, FieldSelectorSpecFromPath(observedValuePath)) + } else if len(options) > 0 { + values = options } - value = options[0] } - spec, err := ValidatePivotSelectors(resourceType, column, value) + spec, err := ValidatePivotSelectors(resourceType, column, values[0]) + if err == nil { + spec.ValueSelectors = values + } return spec, err == nil } return PivotSpec{}, false } +// defaultCodeableColumnSelector prefers the human-facing CodeableConcept.text +// for the conventional `code` shape when the generated schema contains it. +// FHIR producers frequently use that text as the semantic pivot key while +// coding.display is only a broad category (for example +// Observation.code.coding.display == "Component" for many distinct component +// observations). Other CodeableConcept roots retain coding.display. +func defaultCodeableColumnSelector(resourceType, rootPath string) FieldSelectorSpec { + rootPath = strings.TrimSuffix(CanonicalizePath(rootPath), ".") + // The dataframer-compatible semantic-key convention applies to FHIR + // CodeableConcept fields named `code` (including repeated backbone + // component.code). Other CodeableConcept pivots retain coding.display as + // their stable key unless a recipe explicitly chooses another selector. + if rootPath != "code" { + return FieldSelectorSpecFromPath(rootPath + ".coding[].display") + } + textPath := rootPath + ".text" + if _, ok := LookupField(resourceType, textPath); ok { + return FieldSelectorSpecFromPath(textPath) + } + return FieldSelectorSpecFromPath(rootPath + ".coding[].display") +} + func SelectorFromField(field FieldSpec) FieldSelectorSpec { return FieldSelectorSpec{ SourcePath: field.SourcePath, @@ -554,7 +657,14 @@ func resolvePivotFamily(resourceType, columnCanonical, valueCanonical string) (p } func matchObservationCodeValuePivot(resourceType, columnCanonical, valueCanonical string) (pivotFamilyMatch, bool) { - if resourceType == "Observation" && isObservationCodeSelector(columnCanonical) && isObservationValueSelector(valueCanonical) { + // The code/value[x] shape is not exclusive to Observation. Generated + // FHIR backbone definitions (for example ObservationComponent) can expose + // the same correlated pair, so prove the shape from the active schema + // rather than branching on a resource name. + if ResolvesToCodeableConcept(resourceType, "code") && isObservationCodeSelector(columnCanonical) && isObservationValueSelector(valueCanonical) { + if _, valueExists := ResolvePath(resourceType, valueCanonical); !valueExists { + return pivotFamilyMatch{}, false + } return pivotFamilyMatch{ family: PivotFamilyObservationCodeValue, catalogRootPath: "code", diff --git a/fhirschema/schema_test.go b/fhirschema/schema_test.go index 67c425c..1ffe87c 100644 --- a/fhirschema/schema_test.go +++ b/fhirschema/schema_test.go @@ -56,8 +56,8 @@ func TestParseSelectorCanonicalizesIndexedPaths(t *testing.T) { } } -func TestObservationValueSelectorOptions(t *testing.T) { - options := ObservationValueSelectorOptions("Observation") +func TestChoiceValueSelectorOptions(t *testing.T) { + options := ChoiceValueSelectorOptions("Observation") if len(options) == 0 { t.Fatal("expected observation value selector options") } @@ -95,6 +95,20 @@ func TestValidatePivotSelectors(t *testing.T) { if obs.CatalogRootPath != "code" { t.Fatalf("unexpected observation catalog root: %q", obs.CatalogRootPath) } + component, ok := DefaultPivotSpec("Observation", "component[].code", "") + if !ok { + t.Fatal("component pivot was not discovered from generated schema") + } + if component.ItemSourcePath != "component[]" || component.ItemResourceType != "ObservationComponent" { + t.Fatalf("unexpected component item scope: %#v", component) + } + if CanonicalPath(component.ColumnSelector) != "code.text" || len(component.ValueSelectors) == 0 { + t.Fatalf("unexpected component selectors: %#v", component) + } + observation, ok := DefaultPivotSpec("Observation", "code", "valueString") + if !ok || CanonicalPath(observation.ColumnSelector) != "code.text" || CanonicalPath(observation.ValueSelector) != "valueString" { + t.Fatalf("unexpected observation pivot selectors: %#v", observation) + } } func TestLookupTraversal(t *testing.T) { diff --git a/go.mod b/go.mod index 61e667e..2345317 100644 --- a/go.mod +++ b/go.mod @@ -1,74 +1,64 @@ module github.com/calypr/loom -go 1.25.0 +go 1.26.5 require ( - github.com/99designs/gqlgen v0.17.66 + github.com/99designs/gqlgen v0.17.94 github.com/ClickHouse/clickhouse-go/v2 v2.47.0 github.com/arangodb/go-driver/v2 v2.3.1 - github.com/bmeg/grip v0.0.0-20250915204302-93cb1e8117c8 github.com/bmeg/jsonschema/v6 v6.0.5 - github.com/bmeg/jsonschemagraph v0.0.4 + github.com/bmeg/jsonschemagraph v0.0.6 github.com/bytedance/sonic v1.15.2 - github.com/gofiber/fiber/v3 v3.3.0 + github.com/gofiber/fiber/v3 v3.4.0 github.com/google/uuid v1.6.0 - github.com/vektah/gqlparser/v2 v2.5.22 + github.com/vektah/gqlparser/v2 v2.5.36 + go.yaml.in/yaml/v3 v3.0.4 ) require ( github.com/ClickHouse/ch-go v0.73.0 // indirect - github.com/agnivade/levenshtein v1.2.0 // indirect - github.com/andybalholm/brotli v1.2.1 // indirect + github.com/agnivade/levenshtein v1.2.1 // indirect + github.com/andybalholm/brotli v1.2.2 // indirect github.com/arangodb/go-velocypack v0.0.0-20200318135517-5af53c29c67e // indirect - github.com/bytedance/gopkg v0.1.3 // indirect + github.com/bytedance/gopkg v0.1.4 // indirect github.com/bytedance/sonic/loader v0.5.1 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect - github.com/cloudwego/base64x v0.1.6 // indirect + github.com/cloudwego/base64x v0.1.7 // indirect + github.com/coder/websocket v1.8.15 // indirect github.com/dchest/siphash v1.2.3 // indirect github.com/go-faster/city v1.0.1 // indirect github.com/go-faster/errors v0.7.1 // indirect - github.com/go-viper/mapstructure/v2 v2.2.1 // indirect - github.com/gofiber/schema v1.7.1 // indirect - github.com/gofiber/utils/v2 v2.0.6 // indirect - github.com/gorilla/websocket v1.5.3 // indirect - github.com/grpc-ecosystem/go-grpc-middleware v1.4.0 // indirect - github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3 // indirect + github.com/go-viper/mapstructure/v2 v2.5.0 // indirect + github.com/gofiber/schema v1.8.2 // indirect + github.com/gofiber/utils/v2 v2.2.0 // indirect github.com/hashicorp/errwrap v1.1.0 // indirect github.com/hashicorp/go-multierror v1.1.1 // indirect github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect github.com/kkdai/maglev v0.2.0 // indirect - github.com/klauspost/compress v1.18.6 // indirect - github.com/klauspost/cpuid/v2 v2.2.9 // indirect - github.com/kr/pretty v0.3.1 // indirect - github.com/kr/text v0.2.0 // indirect - github.com/logrusorgru/aurora v2.0.3+incompatible // indirect - github.com/mattn/go-colorable v0.1.14 // indirect + github.com/klauspost/compress v1.19.0 // indirect + github.com/klauspost/cpuid/v2 v2.4.0 // indirect + github.com/mattn/go-colorable v0.1.15 // indirect github.com/mattn/go-isatty v0.0.22 // indirect github.com/paulmach/orb v0.13.0 // indirect github.com/philhofer/fwd v1.2.0 // indirect github.com/pierrec/lz4/v4 v4.1.27 // indirect github.com/pkg/errors v0.9.1 // indirect - github.com/rogpeppe/go-internal v1.12.0 // indirect - github.com/rs/zerolog v1.34.0 // indirect + github.com/rs/zerolog v1.35.1 // indirect github.com/segmentio/asm v1.2.1 // indirect github.com/shopspring/decimal v1.4.0 // indirect - github.com/sirupsen/logrus v1.9.4 // indirect - github.com/sosodev/duration v1.3.1 // indirect + github.com/sosodev/duration v1.4.0 // indirect github.com/tinylib/msgp v1.6.4 // indirect github.com/twitchyliquid64/golang-asm v0.15.1 // indirect github.com/valyala/bytebufferpool v1.0.0 // indirect - github.com/valyala/fasthttp v1.71.0 // indirect + github.com/valyala/fasthttp v1.72.0 // indirect go.opentelemetry.io/otel v1.44.0 // indirect go.opentelemetry.io/otel/trace v1.44.0 // indirect - go.yaml.in/yaml/v3 v3.0.4 // indirect - golang.org/x/arch v0.0.0-20210923205945-b76863e36670 // indirect - golang.org/x/crypto v0.53.0 // indirect - golang.org/x/net v0.56.0 // indirect - golang.org/x/sync v0.21.0 // indirect - golang.org/x/sys v0.46.0 // indirect - golang.org/x/term v0.44.0 // indirect - golang.org/x/text v0.38.0 // indirect - google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1 // indirect - google.golang.org/grpc v1.71.0 // indirect - google.golang.org/protobuf v1.36.7 // indirect + golang.org/x/arch v0.29.0 // indirect + golang.org/x/crypto v0.54.0 // indirect + golang.org/x/net v0.57.0 // indirect + golang.org/x/sync v0.22.0 // indirect + golang.org/x/sys v0.47.0 // indirect + golang.org/x/text v0.40.0 // indirect + google.golang.org/protobuf v1.36.11 // indirect + gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect ) diff --git a/go.sum b/go.sum index ee0905a..8c9e953 100644 --- a/go.sum +++ b/go.sum @@ -1,51 +1,35 @@ -cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= -github.com/99designs/gqlgen v0.17.66 h1:2/SRc+h3115fCOZeTtsqrB5R5gTGm+8qCAwcrZa+CXA= -github.com/99designs/gqlgen v0.17.66/go.mod h1:gucrb5jK5pgCKzAGuOMMVU9C8PnReecHEHd2UxLQwCg= -github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/99designs/gqlgen v0.17.94 h1:+3EUDVgX/8gDyDL+7NUqCo4cy2ylylwW0GvR1dGiEsA= +github.com/99designs/gqlgen v0.17.94/go.mod h1:o+XaAMpPA/AX4rqeiK03tZUb/5T+WCgpRDD4aujgdas= github.com/ClickHouse/ch-go v0.73.0 h1:jsHiGRbQ3sz+gekvDFJF29LWDo5dzbJm5s1h8TWVP2M= github.com/ClickHouse/ch-go v0.73.0/go.mod h1:wkFIxrqlXeRJ9cn3r5Fz5Qen9jl5aTMPuGZeuJpANNY= github.com/ClickHouse/clickhouse-go/v2 v2.47.0 h1:ZDAzrnKSOPTIsm4tdUNfrii2yc8dk4SVRLC77BR7Z5Q= github.com/ClickHouse/clickhouse-go/v2 v2.47.0/go.mod h1:sPj7C7UYQ2MWHcfX+4eGN6nwnCqwUKfgO6PcwKpd6K8= -github.com/PuerkitoBio/goquery v1.9.3 h1:mpJr/ikUA9/GNJB/DBZcGeFDXUtosHRyRrwh7KGdTG0= -github.com/PuerkitoBio/goquery v1.9.3/go.mod h1:1ndLHPdTz+DyQPICCWYlYQMPl0oXZj0G6D4LCYA6u4U= -github.com/agnivade/levenshtein v1.2.0 h1:U9L4IOT0Y3i0TIlUIDJ7rVUziKi/zPbrJGaFrtYH3SY= -github.com/agnivade/levenshtein v1.2.0/go.mod h1:QVVI16kDrtSuwcpd0p1+xMC6Z/VfhtCyDIjcwga4/DU= -github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883 h1:bvNMNQO63//z+xNgfBlViaCIJKLlCJ6/fmUseuG0wVQ= -github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883/go.mod h1:rCTlJbsFo29Kk6CurOXKm700vrz8f0KW0JNfpkRJY/8= -github.com/andybalholm/brotli v1.2.1 h1:R+f5xP285VArJDRgowrfb9DqL18yVK0gKAW/F+eTWro= -github.com/andybalholm/brotli v1.2.1/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY= -github.com/andybalholm/cascadia v1.3.2 h1:3Xi6Dw5lHF15JtdcmAHD3i1+T8plmv7BQ/nsViSLyss= -github.com/andybalholm/cascadia v1.3.2/go.mod h1:7gtRlve5FxPPgIgX36uWBX58OdBsSS6lUvCFb+h7KvU= +github.com/agnivade/levenshtein v1.2.1 h1:EHBY3UOn1gwdy/VbFwgo4cxecRznFk7fKWN1KOX7eoM= +github.com/agnivade/levenshtein v1.2.1/go.mod h1:QVVI16kDrtSuwcpd0p1+xMC6Z/VfhtCyDIjcwga4/DU= +github.com/andybalholm/brotli v1.2.2 h1:HzTuoo2ErYQqf5qvcJInB8uvqSVxRttzkFexPWtnceM= +github.com/andybalholm/brotli v1.2.2/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY= github.com/arangodb/go-driver/v2 v2.3.1 h1:km44FjBl6Uh+rD+0mPLLCkOv7QNOTl460Eid7jwQFMQ= github.com/arangodb/go-driver/v2 v2.3.1/go.mod h1:Mi++s/SLvrrXKmLpAy84ivZ3fuyUBPu7ydmyJo3PMlk= github.com/arangodb/go-velocypack v0.0.0-20200318135517-5af53c29c67e h1:Xg+hGrY2LcQBbxd0ZFdbGSyRKTYMZCfBbw/pMJFOk1g= github.com/arangodb/go-velocypack v0.0.0-20200318135517-5af53c29c67e/go.mod h1:mq7Shfa/CaixoDxiyAAc5jZ6CVBAyPaNQCGS7mkj4Ho= github.com/arbovm/levenshtein v0.0.0-20160628152529-48b4e1c0c4d0 h1:jfIu9sQUG6Ig+0+Ap1h4unLjW6YQJpKZVmUzxsD4E/Q= github.com/arbovm/levenshtein v0.0.0-20160628152529-48b4e1c0c4d0/go.mod h1:t2tdKJDJF9BV14lnkjHmOQgcvEKgtqs5a1N3LNdJhGE= -github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= -github.com/bmeg/grip v0.0.0-20250915204302-93cb1e8117c8 h1:UhgcpVbQuvqg8KJ0ioB4i/KaQw1Zel34oHKjSENPEIA= -github.com/bmeg/grip v0.0.0-20250915204302-93cb1e8117c8/go.mod h1:mIN98ALRqqjqRHqopIuZ9iDJ97w4gAzXqdKE6Enuu7Y= github.com/bmeg/jsonschema/v6 v6.0.5 h1:GnyWf8badsqBE0KytRwiuev240HtH89aj04+CzOd6SQ= github.com/bmeg/jsonschema/v6 v6.0.5/go.mod h1:g21s8u35PcH2eGtPBjavAPmXWTwBLd40lHNyq72+ihs= -github.com/bmeg/jsonschemagraph v0.0.4 h1:/uL7TkE1bae4yR9jaJdk38wySu1ScVAg7x6gRKsz58g= -github.com/bmeg/jsonschemagraph v0.0.4/go.mod h1:PS6Pay0bC/7fGUbqNocpUCWhVJY5xivlXwzX6vbY7JM= -github.com/bytedance/gopkg v0.1.3 h1:TPBSwH8RsouGCBcMBktLt1AymVo2TVsBVCY4b6TnZ/M= -github.com/bytedance/gopkg v0.1.3/go.mod h1:576VvJ+eJgyCzdjS+c4+77QF3p7ubbtiKARP3TxducM= +github.com/bmeg/jsonschemagraph v0.0.6 h1:0lbnSPNdtRmcVLAYzbRN1YuYVDGs2qcOg2kmYW09kRo= +github.com/bmeg/jsonschemagraph v0.0.6/go.mod h1:+nYbW3A/1rqhoYc+9ihtHZH0/vzpcqg151sw7lrTHfQ= +github.com/bytedance/gopkg v0.1.4 h1:oZnQwnX82KAIWb7033bEwtxvTqXcYMxDBaQxo5JJHWM= +github.com/bytedance/gopkg v0.1.4/go.mod h1:v1zWfPm21Fb+OsyXN2VAHdL6TBb2L88anLQgdyje6R4= github.com/bytedance/sonic v1.15.2 h1:90H+rcF/FwLXwfB1cudOLq/je83n683Utf4Cbp0xHCo= github.com/bytedance/sonic v1.15.2/go.mod h1:mT2NbXunuaEbnZ+mRIX/vYqKISmgEuHFDI4UzmKx2SA= github.com/bytedance/sonic/loader v0.5.1 h1:Ygpfa9zwRCCKSlrp5bBP/b/Xzc3VxsAW+5NIYXrOOpI= github.com/bytedance/sonic/loader v0.5.1/go.mod h1:AR4NYCk5DdzZizZ5djGqQ92eEhCCcdf5x77udYiSJRo= -github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= -github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= -github.com/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M= -github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU= -github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= -github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= -github.com/cpuguy83/go-md2man/v2 v2.0.5 h1:ZtcqGrnekaHpVLArFSe4HK5DoKx1T0rq2DwVB0alcyc= -github.com/cpuguy83/go-md2man/v2 v2.0.5/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= -github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/cloudwego/base64x v0.1.7 h1:NppS+Fgzg5ovhn4NkUXaDT3x9jldgH5ToMCqzBSi2zI= +github.com/cloudwego/base64x v0.1.7/go.mod h1:Cu1PV9zfrSf7ET2tIbWbbEy7jO7HHJ13q4X2SQ8aWYg= +github.com/coder/websocket v1.8.15 h1:6B2JPeOGlpff2Uz6vOEH1Vzpi0iUz20A+lPVhPHtNUA= +github.com/coder/websocket v1.8.15/go.mod h1:NX3SzP+inril6yawo5CQXx8+fk145lPDC6pumgx0mVg= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -54,53 +38,26 @@ github.com/dchest/siphash v1.2.3 h1:QXwFc8cFOR2dSa/gE6o/HokBMWtLUaNDVd+22aKHeEA= github.com/dchest/siphash v1.2.3/go.mod h1:0NvQU092bT0ipiFN++/rXm69QG9tVxLAlQHIXMPAkHc= github.com/dgryski/trifles v0.0.0-20230903005119-f50d829f2e54 h1:SG7nF6SRlWhcT7cNTs5R6Hk4V2lcmLz2NsG2VnInyNo= github.com/dgryski/trifles v0.0.0-20230903005119-f50d829f2e54/go.mod h1:if7Fbed8SFyPtHLHbg49SI7NAdJiC5WIA09pe59rfAA= -github.com/dlclark/regexp2 v1.11.2 h1:/u628IuisSTwri5/UKloiIsH8+qF2Pu7xEQX+yIKg68= -github.com/dlclark/regexp2 v1.11.2/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= -github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= -github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= -github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= -github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= +github.com/dlclark/regexp2 v1.11.0 h1:G/nrcoOa7ZXlpoa/91N3X7mM3r8eIlMBBJZvsz/mxKI= +github.com/dlclark/regexp2 v1.11.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= github.com/fxamacker/cbor/v2 v2.9.2 h1:X4Ksno9+x3cz0TZv69ec1hxP/+tymuR8PXQJyDwfh78= github.com/fxamacker/cbor/v2 v2.9.2/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ= github.com/go-faster/city v1.0.1 h1:4WAxSZ3V2Ws4QRDrscLEDcibJY8uf41H6AhXDrNDcGw= github.com/go-faster/city v1.0.1/go.mod h1:jKcUJId49qdW3L1qKHH/3wPeUstCVpVSXTM6vO3VcTw= github.com/go-faster/errors v0.7.1 h1:MkJTnDoEdi9pDabt1dpWf7AA8/BaSYZqibYyhZ20AYg= github.com/go-faster/errors v0.7.1/go.mod h1:5ySTjWFiphBs07IKuiL69nxdfd5+fzh1u7FPGZP2quo= -github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY= -github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A= -github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= -github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= -github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= -github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= -github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= -github.com/go-viper/mapstructure/v2 v2.2.1 h1:ZAaOCxANMuZx5RCeg0mBdEZk7DZasvvZIxtHqx8aGss= -github.com/go-viper/mapstructure/v2 v2.2.1/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= -github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= -github.com/gofiber/fiber/v3 v3.3.0 h1:QBd3sYCqdy6Qs5gJYzSw4I4SbqL204jPqpdub/ueiw8= -github.com/gofiber/fiber/v3 v3.3.0/go.mod h1:YH7/TAoRaU4kF8slDCtQuFJ1NzC+3MtxUI4KfvQtaIA= -github.com/gofiber/schema v1.7.1 h1:oSJBKdgP8JeIME4TQSAqlNKTU2iBB+2RNmKi8Nsc+TI= -github.com/gofiber/schema v1.7.1/go.mod h1:A/X5Ffyru4p9eBdp99qu+nzviHzQiZ7odLT+TwxWhbk= -github.com/gofiber/utils/v2 v2.0.6 h1:7fXYy7nSsyqbH0GQUMtK4Kwjy4J7R5742VM7JsZxzOs= -github.com/gofiber/utils/v2 v2.0.6/go.mod h1:p7mAHAk3+oUK10ZX2xTw9fZQixb4hCg8SKd4IH2xroU= -github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= -github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= -github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= -github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= -github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= -github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= -github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= +github.com/go-viper/mapstructure/v2 v2.5.0 h1:vM5IJoUAy3d7zRSVtIwQgBj7BiWtMPfmPEgAXnvj1Ro= +github.com/go-viper/mapstructure/v2 v2.5.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= +github.com/gofiber/fiber/v3 v3.4.0 h1:F0aND4vwZF7dR7cbvSwFQQEpBU902XHKWxrLsFBkVqw= +github.com/gofiber/fiber/v3 v3.4.0/go.mod h1:nAhJfdxUIJJph2tPWPmqWf8QDIN2iiqQiQf3lENZpdk= +github.com/gofiber/schema v1.8.2 h1:wq+LO2xEGlsqma/8Akp9PUebQ6vcsYmF0xYQ4F2ijvU= +github.com/gofiber/schema v1.8.2/go.mod h1:iyAMJztdyky7Pk2U7bwUP3EHjzMqUNjZ/hglE0nYO/g= +github.com/gofiber/utils/v2 v2.2.0 h1:YSSmCzQponq/f9uSOg2HtXC5qK1Dmor0o6DqaQVz8GE= +github.com/gofiber/utils/v2 v2.2.0/go.mod h1:Ieopk6sQh7rbhQ12aBNCJtJuG0gxAg0nz63sFCrrOmE= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= -github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= -github.com/grpc-ecosystem/go-grpc-middleware v1.4.0 h1:UH//fgunKIs4JdUbpDl1VZCDaL56wXCB/5+wF6uHfaI= -github.com/grpc-ecosystem/go-grpc-middleware v1.4.0/go.mod h1:g5qyo/la0ALbONm6Vbp88Yd8NsDy6rZz+RcrMPxvld8= -github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3 h1:5ZPtiqj0JL5oKWmcsq4VMaAW5ukBEgSGXEN89zeH1Jo= -github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3/go.mod h1:ndYquD05frm2vACXE1nsccT4oJzjhw2arTS2cpUD1PI= github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I= github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= @@ -108,76 +65,50 @@ github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+l github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k= github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM= -github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= -github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/kkdai/maglev v0.2.0 h1:w6DCW0kAA6fstZqXkrBrlgIC3jeIRXkjOYea/m6EK/Y= github.com/kkdai/maglev v0.2.0/go.mod h1:d+mt8Lmt3uqi9aRb/BnPjzD0fy+ETs1vVXiGRnqHVZ4= -github.com/klauspost/compress v1.18.6 h1:2jupLlAwFm95+YDR+NwD2MEfFO9d4z4Prjl1XXDjuao= -github.com/klauspost/compress v1.18.6/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ= -github.com/klauspost/cpuid/v2 v2.2.9 h1:66ze0taIn2H33fBvCkXuv9BmCwDfafmiIVpKV9kKGuY= -github.com/klauspost/cpuid/v2 v2.2.9/go.mod h1:rqkxqrZ1EhYM9G+hXH7YdowN5R5RGN6NK4QwQ3WMXF8= -github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= -github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/klauspost/compress v1.19.0 h1:sXLILfc9jV2QYWkzFOPWStmcUVH2RHEB1JCdY2oVvCQ= +github.com/klauspost/compress v1.19.0/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ= +github.com/klauspost/cpuid/v2 v2.4.0 h1:S6Hrbc7+ywsr0r+RLapfGBHfyefhCTwEh3A0tV913Dw= +github.com/klauspost/cpuid/v2 v2.4.0/go.mod h1:19jmZ9mjzoF//ddRSUsv0zfBTJWh3QJh9FNxZTMrGxU= +github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= -github.com/logrusorgru/aurora v2.0.3+incompatible h1:tOpm7WcpBTn4fjmVfgpQq0EfczGlG91VSDkswnjF5A8= -github.com/logrusorgru/aurora v2.0.3+incompatible/go.mod h1:7rIyQOR62GCctdiQpZ/zOJlFyk6y+94wXzv6RNZgaR4= -github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= -github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= -github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= -github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= -github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-colorable v0.1.15 h1:+u9SLTRGnXv73cEsnsmoZBom+dMU88B2M0aDcWy0/jY= +github.com/mattn/go-colorable v0.1.15/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= github.com/mattn/go-isatty v0.0.22 h1:j8l17JJ9i6VGPUFUYoTUKPSgKe/83EYU2zBC7YNKMw4= github.com/mattn/go-isatty v0.0.22/go.mod h1:ZXfXG4SQHsB/w3ZeOYbR0PrPwLy+n6xiMrJlRFqopa4= -github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o= github.com/paulmach/orb v0.13.0 h1:r7n7mQGGF+cj/CbcivEj9J3HGK+XR+yXnvzRdq9saIw= github.com/paulmach/orb v0.13.0/go.mod h1:6scRWINywA2Jf05dcjOfLfxrUIMECvTSG2MVbRLxu/k= github.com/philhofer/fwd v1.2.0 h1:e6DnBTl7vGY+Gz322/ASL4Gyp1FspeMvx1RNDoToZuM= github.com/philhofer/fwd v1.2.0/go.mod h1:RqIHx9QI14HlwKwm98g9Re5prTQ6LdeRQn+gXJFxsJM= github.com/pierrec/lz4/v4 v4.1.27 h1:+PhzhWDrjRj89TH2sw43nE3+4+W8lSxIuQadEHZyjUk= github.com/pierrec/lz4/v4 v4.1.27/go.mod h1:EoQMVJgeeEOMsCqCzqFm2O0cJvljX2nGZjcRIPL34O4= -github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= -github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= -github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= -github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0= -github.com/rs/zerolog v1.34.0 h1:k43nTLIwcTVQAncfCw4KZ2VY6ukYoZaBPNOE8txlOeY= -github.com/rs/zerolog v1.34.0/go.mod h1:bJsvje4Z08ROH4Nhs5iH600c3IkWhwp44iRc54W6wYQ= -github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= -github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/rs/zerolog v1.35.1 h1:m7xQeoiLIiV0BCEY4Hs+j2NG4Gp2o2KPKmhnnLiazKI= +github.com/rs/zerolog v1.35.1/go.mod h1:EjML9kdfa/RMA7h/6z6pYmq1ykOuA8/mjWaEvGI+jcw= github.com/segmentio/asm v1.2.1 h1:DTNbBqs57ioxAD4PrArqftgypG4/qNpXoJx8TVXxPR0= github.com/segmentio/asm v1.2.1/go.mod h1:BqMnlJP91P8d+4ibuonYZw9mfnzI9HfxselHZr5aAcs= -github.com/sergi/go-diff v1.3.1 h1:xkr+Oxo4BOQKmkn/B9eMK0g5Kg/983T9DqqPHwYqD+8= -github.com/sergi/go-diff v1.3.1/go.mod h1:aMJSSKb2lpPvRNec0+w3fl7LP9IOFzdc9Pa4NFbPK1I= -github.com/shamaton/msgpack/v3 v3.1.2 h1:d5gWAIyMU4M0WgDjz6IFSCuXJUA2dFwRHBpDclE8CLw= -github.com/shamaton/msgpack/v3 v3.1.2/go.mod h1:DcQG8jrdrQCIxr3HlMYkiXdMhK+KfN2CitkyzsQV4uc= +github.com/shamaton/msgpack/v3 v3.2.0 h1:1q2Ms+MWmuRju+PuDMSFDB7p7621npeX4zprJN5Zck8= +github.com/shamaton/msgpack/v3 v3.2.0/go.mod h1:sgBYvEiyz8JR1NC3yGRoPVME9xXovpnh3l/plW1nfRo= github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k= github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME= -github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= -github.com/sirupsen/logrus v1.9.4 h1:TsZE7l11zFCLZnZ+teH4Umoq5BhEIfIzfRDZ1Uzql2w= -github.com/sirupsen/logrus v1.9.4/go.mod h1:ftWc9WdOfJ0a92nsE2jF5u5ZwH8Bv2zdeOC42RjbV2g= -github.com/sosodev/duration v1.3.1 h1:qtHBDMQ6lvMQsL15g4aopM4HEfOaYuhWBw3NPTtlqq4= -github.com/sosodev/duration v1.3.1/go.mod h1:RQIBBX0+fMLc/D9+Jb/fwvVmo0eZvDDEERAikUR6SDg= +github.com/sosodev/duration v1.4.0 h1:35ed0KiVFriGHHzZZJaZLgmTEEICIyt8Sx0RQfj9IjE= +github.com/sosodev/duration v1.4.0/go.mod h1:RQIBBX0+fMLc/D9+Jb/fwvVmo0eZvDDEERAikUR6SDg= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= -github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= -github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= -github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= -github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= @@ -188,131 +119,40 @@ github.com/tinylib/msgp v1.6.4 h1:mOwYbyYDLPj35mkA2BjjYejgJk9BuHxDdvRnb6v2ZcQ= github.com/tinylib/msgp v1.6.4/go.mod h1:RSp0LW9oSxFut3KzESt5Voq4GVWyS+PSulT77roAqEA= github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= -github.com/urfave/cli/v2 v2.27.5 h1:WoHEJLdsXr6dDWoJgMq/CboDmyY/8HMMH1fTECbih+w= -github.com/urfave/cli/v2 v2.27.5/go.mod h1:3Sevf16NykTbInEnD0yKkjDAeZDS0A6bzhBH5hrMvTQ= github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= -github.com/valyala/fasthttp v1.71.0 h1:tepR7H+Guh9VUqxxcPggYi8R3lGUu2Rsdh+z7/FCY3k= -github.com/valyala/fasthttp v1.71.0/go.mod h1:z1sDUvOShhXq/C9mwH/fSm1Vb71tUJwmQdgkBrBNwnA= -github.com/vektah/gqlparser/v2 v2.5.22 h1:yaaeJ0fu+nv1vUMW0Hl+aS1eiv1vMfapBNjpffAda1I= -github.com/vektah/gqlparser/v2 v2.5.22/go.mod h1:xMl+ta8a5M1Yo1A1Iwt/k7gSpscwSnHZdw7tfhEGfTM= +github.com/valyala/fasthttp v1.72.0 h1:R7kYdoWhn1ye1fVpP+cDHDJwYm3NkwLliwgzJ/Abg7M= +github.com/valyala/fasthttp v1.72.0/go.mod h1:zsbLTYqcpIktdQytlVBwIjY9La5d6bs990nBxWg8efk= +github.com/vektah/gqlparser/v2 v2.5.36 h1:CN9mKVHgMkc+XftdOWIhb4HEL8wKSYkFAqhf8booa7s= +github.com/vektah/gqlparser/v2 v2.5.36/go.mod h1:cAJ9qwVgPaUkWv6Gn8vn0mqOE0Ui5Pn56wNy5396XWo= github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= -github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 h1:gEOO8jv9F4OT7lGCjxCBTO/36wtF6j2nSip77qHd4x4= -github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1/go.mod h1:Ohn+xnUBiLI6FVj/9LpzZWtj1/D6lUovWYBkxHVV3aM= github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU= github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E= -github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= -go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= go.opentelemetry.io/otel v1.44.0 h1:JjwHmHpA4iZ3wBxluu2fbbE7j4kqlE8jXyAyPXH7HqU= go.opentelemetry.io/otel v1.44.0/go.mod h1:BMgjTHL9WPRlRjL2oZCBTL4whCGtXch2H4BhOPIAyYc= -go.opentelemetry.io/otel/metric v1.44.0 h1:1w0gILTcHdr3YI+ixLyjemwrVnsMURbTZFrSYCdDdmc= -go.opentelemetry.io/otel/metric v1.44.0/go.mod h1:8O7hanEPBNgEMmybD3s2VBKcgWOCsA6tzHBPODAiquo= -go.opentelemetry.io/otel/sdk v1.44.0 h1:nHYwb9lK+fJPU/dnT6s7W7Z8itMWyqrnVfbheVYrZ58= -go.opentelemetry.io/otel/sdk v1.44.0/go.mod h1:Osuydd3Se74nqjAKxid74N5eC+jfEqfTegHRnq58oK0= -go.opentelemetry.io/otel/sdk/metric v1.34.0 h1:5CeK9ujjbFVL5c1PhLuStg1wxA7vQv7ce1EK0Gyvahk= -go.opentelemetry.io/otel/sdk/metric v1.34.0/go.mod h1:jQ/r8Ze28zRKoNRdkjCZxfs6YvBTG1+YIqyFVFYec5w= go.opentelemetry.io/otel/trace v1.44.0 h1:jxF5CsGYCe74MCRx2X4g7WsY/VBKRqqpNvXlX/6gtIk= go.opentelemetry.io/otel/trace v1.44.0/go.mod h1:oLl1jrMQAVo6v3GAggN+1VH9VIz9iUSvW53sW1Q8PIE= -go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= -go.uber.org/goleak v1.1.10/go.mod h1:8a7PlsEVH3e/a/GLqe5IIrQx6GzcnRmZEufDUTk4A7A= -go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= -go.uber.org/zap v1.18.1/go.mod h1:xg/QME4nWcxGxrpdeYfq7UvYrLh66cuVKdrbD1XF/NI= go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= -golang.org/x/arch v0.0.0-20210923205945-b76863e36670 h1:18EFjUmQOcUvxNYSkA6jO9VAiXCnxFY6NyDX0bHDmkU= -golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= -golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.53.0 h1:QZ4Muo8THX6CizN2vPPd5fBGHyogrdK9fG4wLPFUsto= -golang.org/x/crypto v0.53.0/go.mod h1:DNLU434OwVakk9PzuwV8w62mAJpRJL3vsgcfp4Qnsio= -golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= -golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= -golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= -golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= -golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= -golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.36.0 h1:JJjpVx6myfUsUdAzZuOSTTmRE0PfZeNWzzvKrP7amb4= -golang.org/x/mod v0.36.0/go.mod h1:moc6ELqsWcOw5Ef3xVprK5ul/MvtVvkIXLziUOICjUQ= -golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.56.0 h1:Rw8j/hFzGvJUZwNBXnAtf5sVDVt+65SK2C7IxCxZt5o= -golang.org/x/net v0.56.0/go.mod h1:D3Ku6r+V6JROoZK144D2XfMHFcMq/0zSfLelVTCFKec= -golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= -golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.21.0 h1:HLII4xRRTtCRkxYp4HNFF0Js/Og6q2i++KXbg0gHCwM= -golang.org/x/sync v0.21.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= -golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20211025201205-69cdffdb9359/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.46.0 h1:noSf2Fq6F8DBgS+LysIkx7rIExoNHJsxOAtPp4rthXw= -golang.org/x/sys v0.46.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= -golang.org/x/term v0.44.0 h1:0rLvDRCtNj0gZkyIXhCyOb2OAzEhLVqc4B+hrsBhrmc= -golang.org/x/term v0.44.0/go.mod h1:7ze4MdzUzLXpSAoFP1H0bOI9aXDqveSvatT5vKcFh2Y= -golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.38.0 h1:sXmwo9DwP3OK9EZ7PqAdaooSGozfl/3a6/xJcbzPRhE= -golang.org/x/text v0.38.0/go.mod h1:YXZt3QhHUKYT53r2lLKFIVi6Ao1jdzrTR/KQ09qyxF4= -golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= -golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= -golang.org/x/tools v0.0.0-20191108193012-7d206e10da11/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.45.0 h1:18qN3FAooORvApf5XjCXgsuayZOEtXf6JK18I3+ONa8= -golang.org/x/tools v0.45.0/go.mod h1:LuUGqqaXcXMEFEruIVJVm5mgDD8vww/z/SR1gQ4uE/0= -golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= -google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= -google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= -google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= -google.golang.org/genproto v0.0.0-20200423170343-7949de9c1215/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1 h1:KpwkzHKEF7B9Zxg18WzOa7djJ+Ha5DzthMyZYQfEn2A= -google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1/go.mod h1:nKE/iIaLqn2bQwXBg8f1g2Ylh6r5MN5CmZvuzZCgsCU= -google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= -google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= -google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= -google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= -google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk= -google.golang.org/grpc v1.71.0 h1:kF77BGdPTQ4/JZWMlb9VpJ5pa25aqvVqogsxNHHdeBg= -google.golang.org/grpc v1.71.0/go.mod h1:H0GRtasmQOh9LkFoCPDu3ZrwUtD1YGE+b2vYBYd/8Ec= -google.golang.org/protobuf v1.36.7 h1:IgrO7UwFQGJdRNXH/sQux4R1Dj1WAKcLElzeeRaXV2A= -google.golang.org/protobuf v1.36.7/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY= +golang.org/x/arch v0.29.0 h1:8sSET5wB0+exBm0FGmOtdHMqjlRdV2DRD3/IV6OZgho= +golang.org/x/arch v0.29.0/go.mod h1:0X+GdSIP+kL5wPmpK7sdkEVTt2XoYP0cSjQSbZBwOi8= +golang.org/x/crypto v0.54.0 h1:YLIA59K4fiNzHzjnZt2tUJQjQtUWfWbeHBqKtk3eScw= +golang.org/x/crypto v0.54.0/go.mod h1:KWL8ny2AZdGR2cWmzeHrp2azQPGogOv+HeQaVEXC2dk= +golang.org/x/net v0.57.0 h1:K5+3DljvIuDG9/Jv9rvyMywYNFCQ9RSUY6OOTTkT+tE= +golang.org/x/net v0.57.0/go.mod h1:KpXc8iv+r3XplLAG/f7Jsf9RPszJzdR0f58q9vGOuEU= +golang.org/x/sync v0.22.0 h1:SZjpbeLmrCk4xhRSZFNZW5gFUeCeFgjekvI/+gfScek= +golang.org/x/sync v0.22.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= +golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs= +golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +golang.org/x/text v0.40.0 h1:Ub2Z6/xjgF1WrYQz2nuITOEegKFtiIy+rieRJ5lHZKs= +golang.org/x/text v0.40.0/go.mod h1:hpnzDAfGV753zIKo+wk3u1bVKCGPbrnF7+7LBF/UHVY= +google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= +google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= diff --git a/gqlgen.yml b/gqlgen.yml index 5e09e35..b0f13f8 100644 --- a/gqlgen.yml +++ b/gqlgen.yml @@ -15,6 +15,10 @@ resolver: package: graphqlapi skip_mod_tidy: true +# The generated JSON scalar unmarshaller needs the post-generation correction +# in cmd/gqlgenfix. Skip gqlgen's pre-generation package validation so that +# correction can be applied before the package is compiled. +skip_validation: true models: JSON: diff --git a/graphqlapi/clickhouse/generated.go b/graphqlapi/clickhouse/generated.go new file mode 100644 index 0000000..74ca41c --- /dev/null +++ b/graphqlapi/clickhouse/generated.go @@ -0,0 +1,4366 @@ +// Code generated by github.com/99designs/gqlgen, DO NOT EDIT. + +package clickhouse + +import ( + "bytes" + "context" + "embed" + "encoding/json" + "errors" + "fmt" + "math" + "strconv" + "sync/atomic" + + "github.com/99designs/gqlgen/graphql" + "github.com/99designs/gqlgen/graphql/introspection" + "github.com/calypr/loom/graphqlapi/model" + gqlparser "github.com/vektah/gqlparser/v2" + "github.com/vektah/gqlparser/v2/ast" +) + +// region ***************************** api!.gotpl ***************************** + +// NewExecutableSchema creates an ExecutableSchema from the ResolverRoot interface. +func NewExecutableSchema(cfg Config) graphql.ExecutableSchema { + return &executableSchema{SchemaData: cfg.Schema, Resolvers: cfg.Resolvers, Directives: cfg.Directives, ComplexityRoot: cfg.Complexity} +} + +type Config = graphql.Config[ResolverRoot, DirectiveRoot, ComplexityRoot] + +type ResolverRoot interface { + Query() QueryResolver +} + +type DirectiveRoot struct { +} + +type ComplexityRoot struct { + DataframeAggregateResult struct { + Columns func(childComplexity int) int + Materialization func(childComplexity int) int + Rows func(childComplexity int) int + } + + DataframeColumn struct { + Aggregatable func(childComplexity int) int + ClickhouseType func(childComplexity int) int + Filterable func(childComplexity int) int + LogicalType func(childComplexity int) int + Name func(childComplexity int) int + Nullable func(childComplexity int) int + Repeated func(childComplexity int) int + Sortable func(childComplexity int) int + } + + DataframeMaterialization struct { + Columns func(childComplexity int) int + CreatedAt func(childComplexity int) int + Error func(childComplexity int) int + ID func(childComplexity int) int + Name func(childComplexity int) int + ReadyAt func(childComplexity int) int + Revision func(childComplexity int) int + RowCount func(childComplexity int) int + State func(childComplexity int) int + } + + DataframePageInfo struct { + EndCursor func(childComplexity int) int + HasNextPage func(childComplexity int) int + } + + DataframeRowConnection struct { + Columns func(childComplexity int) int + Materialization func(childComplexity int) int + PageInfo func(childComplexity int) int + Rows func(childComplexity int) int + TotalCount func(childComplexity int) int + } + + Query struct { + DataframeAggregate func(childComplexity int, input model.DataframeAggregateInput) int + DataframeDataset func(childComplexity int, input model.DataframeDatasetInput) int + DataframeDatasets func(childComplexity int) int + DataframeRows func(childComplexity int, input model.DataframeRowsInput) int + } +} + +// endregion ***************************** api!.gotpl ***************************** + +// region ************************** generated!.gotpl ************************** + +type QueryResolver interface { + DataframeDatasets(ctx context.Context) ([]*model.DataframeMaterialization, error) + DataframeDataset(ctx context.Context, input model.DataframeDatasetInput) (*model.DataframeMaterialization, error) + DataframeRows(ctx context.Context, input model.DataframeRowsInput) (*model.DataframeRowConnection, error) + DataframeAggregate(ctx context.Context, input model.DataframeAggregateInput) (*model.DataframeAggregateResult, error) +} + +// endregion ************************** generated!.gotpl ************************** + +// region ************************** internal!.gotpl *************************** + +type executableSchema graphql.ExecutableSchemaState[ResolverRoot, DirectiveRoot, ComplexityRoot] + +func (e *executableSchema) Schema() *ast.Schema { + if e.SchemaData != nil { + return e.SchemaData + } + return parsedSchema +} + +func (e *executableSchema) Complexity(ctx context.Context, typeName, field string, childComplexity int, rawArgs map[string]any) (int, bool) { + ec := newExecutionContext(nil, e, nil) + _ = ec + switch typeName + "." + field { + + case "DataframeAggregateResult.columns": + if e.ComplexityRoot.DataframeAggregateResult.Columns == nil { + break + } + + return e.ComplexityRoot.DataframeAggregateResult.Columns(childComplexity), true + case "DataframeAggregateResult.materialization": + if e.ComplexityRoot.DataframeAggregateResult.Materialization == nil { + break + } + + return e.ComplexityRoot.DataframeAggregateResult.Materialization(childComplexity), true + case "DataframeAggregateResult.rows": + if e.ComplexityRoot.DataframeAggregateResult.Rows == nil { + break + } + + return e.ComplexityRoot.DataframeAggregateResult.Rows(childComplexity), true + + case "DataframeColumn.aggregatable": + if e.ComplexityRoot.DataframeColumn.Aggregatable == nil { + break + } + + return e.ComplexityRoot.DataframeColumn.Aggregatable(childComplexity), true + case "DataframeColumn.clickhouseType": + if e.ComplexityRoot.DataframeColumn.ClickhouseType == nil { + break + } + + return e.ComplexityRoot.DataframeColumn.ClickhouseType(childComplexity), true + case "DataframeColumn.filterable": + if e.ComplexityRoot.DataframeColumn.Filterable == nil { + break + } + + return e.ComplexityRoot.DataframeColumn.Filterable(childComplexity), true + case "DataframeColumn.logicalType": + if e.ComplexityRoot.DataframeColumn.LogicalType == nil { + break + } + + return e.ComplexityRoot.DataframeColumn.LogicalType(childComplexity), true + case "DataframeColumn.name": + if e.ComplexityRoot.DataframeColumn.Name == nil { + break + } + + return e.ComplexityRoot.DataframeColumn.Name(childComplexity), true + case "DataframeColumn.nullable": + if e.ComplexityRoot.DataframeColumn.Nullable == nil { + break + } + + return e.ComplexityRoot.DataframeColumn.Nullable(childComplexity), true + case "DataframeColumn.repeated": + if e.ComplexityRoot.DataframeColumn.Repeated == nil { + break + } + + return e.ComplexityRoot.DataframeColumn.Repeated(childComplexity), true + case "DataframeColumn.sortable": + if e.ComplexityRoot.DataframeColumn.Sortable == nil { + break + } + + return e.ComplexityRoot.DataframeColumn.Sortable(childComplexity), true + + case "DataframeMaterialization.columns": + if e.ComplexityRoot.DataframeMaterialization.Columns == nil { + break + } + + return e.ComplexityRoot.DataframeMaterialization.Columns(childComplexity), true + case "DataframeMaterialization.createdAt": + if e.ComplexityRoot.DataframeMaterialization.CreatedAt == nil { + break + } + + return e.ComplexityRoot.DataframeMaterialization.CreatedAt(childComplexity), true + case "DataframeMaterialization.error": + if e.ComplexityRoot.DataframeMaterialization.Error == nil { + break + } + + return e.ComplexityRoot.DataframeMaterialization.Error(childComplexity), true + case "DataframeMaterialization.id": + if e.ComplexityRoot.DataframeMaterialization.ID == nil { + break + } + + return e.ComplexityRoot.DataframeMaterialization.ID(childComplexity), true + case "DataframeMaterialization.name": + if e.ComplexityRoot.DataframeMaterialization.Name == nil { + break + } + + return e.ComplexityRoot.DataframeMaterialization.Name(childComplexity), true + case "DataframeMaterialization.readyAt": + if e.ComplexityRoot.DataframeMaterialization.ReadyAt == nil { + break + } + + return e.ComplexityRoot.DataframeMaterialization.ReadyAt(childComplexity), true + case "DataframeMaterialization.revision": + if e.ComplexityRoot.DataframeMaterialization.Revision == nil { + break + } + + return e.ComplexityRoot.DataframeMaterialization.Revision(childComplexity), true + case "DataframeMaterialization.rowCount": + if e.ComplexityRoot.DataframeMaterialization.RowCount == nil { + break + } + + return e.ComplexityRoot.DataframeMaterialization.RowCount(childComplexity), true + case "DataframeMaterialization.state": + if e.ComplexityRoot.DataframeMaterialization.State == nil { + break + } + + return e.ComplexityRoot.DataframeMaterialization.State(childComplexity), true + + case "DataframePageInfo.endCursor": + if e.ComplexityRoot.DataframePageInfo.EndCursor == nil { + break + } + + return e.ComplexityRoot.DataframePageInfo.EndCursor(childComplexity), true + case "DataframePageInfo.hasNextPage": + if e.ComplexityRoot.DataframePageInfo.HasNextPage == nil { + break + } + + return e.ComplexityRoot.DataframePageInfo.HasNextPage(childComplexity), true + + case "DataframeRowConnection.columns": + if e.ComplexityRoot.DataframeRowConnection.Columns == nil { + break + } + + return e.ComplexityRoot.DataframeRowConnection.Columns(childComplexity), true + case "DataframeRowConnection.materialization": + if e.ComplexityRoot.DataframeRowConnection.Materialization == nil { + break + } + + return e.ComplexityRoot.DataframeRowConnection.Materialization(childComplexity), true + case "DataframeRowConnection.pageInfo": + if e.ComplexityRoot.DataframeRowConnection.PageInfo == nil { + break + } + + return e.ComplexityRoot.DataframeRowConnection.PageInfo(childComplexity), true + case "DataframeRowConnection.rows": + if e.ComplexityRoot.DataframeRowConnection.Rows == nil { + break + } + + return e.ComplexityRoot.DataframeRowConnection.Rows(childComplexity), true + case "DataframeRowConnection.totalCount": + if e.ComplexityRoot.DataframeRowConnection.TotalCount == nil { + break + } + + return e.ComplexityRoot.DataframeRowConnection.TotalCount(childComplexity), true + + case "Query.dataframeAggregate": + if e.ComplexityRoot.Query.DataframeAggregate == nil { + break + } + + args, err := ec.field_Query_dataframeAggregate_args(ctx, rawArgs) + if err != nil { + return 0, false + } + + return e.ComplexityRoot.Query.DataframeAggregate(childComplexity, args["input"].(model.DataframeAggregateInput)), true + case "Query.dataframeDataset": + if e.ComplexityRoot.Query.DataframeDataset == nil { + break + } + + args, err := ec.field_Query_dataframeDataset_args(ctx, rawArgs) + if err != nil { + return 0, false + } + + return e.ComplexityRoot.Query.DataframeDataset(childComplexity, args["input"].(model.DataframeDatasetInput)), true + case "Query.dataframeDatasets": + if e.ComplexityRoot.Query.DataframeDatasets == nil { + break + } + + return e.ComplexityRoot.Query.DataframeDatasets(childComplexity), true + case "Query.dataframeRows": + if e.ComplexityRoot.Query.DataframeRows == nil { + break + } + + args, err := ec.field_Query_dataframeRows_args(ctx, rawArgs) + if err != nil { + return 0, false + } + + return e.ComplexityRoot.Query.DataframeRows(childComplexity, args["input"].(model.DataframeRowsInput)), true + + } + return 0, false +} + +func (e *executableSchema) Exec(ctx context.Context) graphql.ResponseHandler { + opCtx := graphql.GetOperationContext(ctx) + ec := newExecutionContext(opCtx, e, make(chan graphql.DeferredResult)) + inputUnmarshalMap := graphql.BuildUnmarshalerMap( + ec.unmarshalInputDataframeAggregateInput, + ec.unmarshalInputDataframeDatasetInput, + ec.unmarshalInputDataframeFilterInput, + ec.unmarshalInputDataframeRowsInput, + ec.unmarshalInputDataframeSortInput, + ) + first := true + + switch opCtx.Operation.Operation { + case ast.Query: + return func(ctx context.Context) *graphql.Response { + var response graphql.Response + var data graphql.Marshaler + if first { + first = false + ctx = graphql.WithUnmarshalerMap(ctx, inputUnmarshalMap) + data = ec._Query(ctx, opCtx.Operation.SelectionSet) + } else { + if atomic.LoadInt32(&ec.PendingDeferred) > 0 { + result := <-ec.DeferredResults + atomic.AddInt32(&ec.PendingDeferred, -1) + data = result.Result + response.Path = result.Path + response.Label = result.Label + response.Errors = result.Errors + } else { + return nil + } + } + var buf bytes.Buffer + data.MarshalGQL(&buf) + response.Data = buf.Bytes() + if atomic.LoadInt32(&ec.Deferred) > 0 { + hasNext := atomic.LoadInt32(&ec.PendingDeferred) > 0 + response.HasNext = &hasNext + } + + return &response + } + + default: + return graphql.OneShot(graphql.ErrorResponse(ctx, "unsupported GraphQL operation")) + } +} + +type executionContext struct { + *graphql.ExecutionContextState[ResolverRoot, DirectiveRoot, ComplexityRoot] +} + +func newExecutionContext( + opCtx *graphql.OperationContext, + execSchema *executableSchema, + deferredResults chan graphql.DeferredResult, +) *executionContext { + return &executionContext{ + ExecutionContextState: graphql.NewExecutionContextState[ResolverRoot, DirectiveRoot, ComplexityRoot]( + opCtx, + (*graphql.ExecutableSchemaState[ResolverRoot, DirectiveRoot, ComplexityRoot])(execSchema), + parsedSchema, + deferredResults, + ), + } +} + +//go:embed "schema.graphqls" +var sourcesFS embed.FS + +func sourceData(filename string) string { + data, err := sourcesFS.ReadFile(filename) + if err != nil { + panic(fmt.Sprintf("codegen problem: %s not available", filename)) + } + return string(data) +} + +var sources = []*ast.Source{ + {Name: "schema.graphqls", Input: sourceData("schema.graphqls"), BuiltIn: false}, +} +var parsedSchema = gqlparser.MustLoadSchema(sources...) + +// childFields_* functions provide shared child field context lookups. +// Each function is generated once per unique object type, deduplicating the +// switch statements that were previously inlined in every fieldContext_* function. + +func (ec *executionContext) childFields_DataframeAggregateResult(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + switch field.Name { + case "materialization": + return ec.fieldContext_DataframeAggregateResult_materialization(ctx, field) + case "columns": + return ec.fieldContext_DataframeAggregateResult_columns(ctx, field) + case "rows": + return ec.fieldContext_DataframeAggregateResult_rows(ctx, field) + } + return nil, fmt.Errorf("no field named %q was found under type DataframeAggregateResult", field.Name) +} + +func (ec *executionContext) childFields_DataframeColumn(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + switch field.Name { + case "name": + return ec.fieldContext_DataframeColumn_name(ctx, field) + case "clickhouseType": + return ec.fieldContext_DataframeColumn_clickhouseType(ctx, field) + case "logicalType": + return ec.fieldContext_DataframeColumn_logicalType(ctx, field) + case "nullable": + return ec.fieldContext_DataframeColumn_nullable(ctx, field) + case "repeated": + return ec.fieldContext_DataframeColumn_repeated(ctx, field) + case "filterable": + return ec.fieldContext_DataframeColumn_filterable(ctx, field) + case "sortable": + return ec.fieldContext_DataframeColumn_sortable(ctx, field) + case "aggregatable": + return ec.fieldContext_DataframeColumn_aggregatable(ctx, field) + } + return nil, fmt.Errorf("no field named %q was found under type DataframeColumn", field.Name) +} + +func (ec *executionContext) childFields_DataframeMaterialization(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + switch field.Name { + case "id": + return ec.fieldContext_DataframeMaterialization_id(ctx, field) + case "name": + return ec.fieldContext_DataframeMaterialization_name(ctx, field) + case "revision": + return ec.fieldContext_DataframeMaterialization_revision(ctx, field) + case "state": + return ec.fieldContext_DataframeMaterialization_state(ctx, field) + case "columns": + return ec.fieldContext_DataframeMaterialization_columns(ctx, field) + case "rowCount": + return ec.fieldContext_DataframeMaterialization_rowCount(ctx, field) + case "createdAt": + return ec.fieldContext_DataframeMaterialization_createdAt(ctx, field) + case "readyAt": + return ec.fieldContext_DataframeMaterialization_readyAt(ctx, field) + case "error": + return ec.fieldContext_DataframeMaterialization_error(ctx, field) + } + return nil, fmt.Errorf("no field named %q was found under type DataframeMaterialization", field.Name) +} + +func (ec *executionContext) childFields_DataframePageInfo(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + switch field.Name { + case "hasNextPage": + return ec.fieldContext_DataframePageInfo_hasNextPage(ctx, field) + case "endCursor": + return ec.fieldContext_DataframePageInfo_endCursor(ctx, field) + } + return nil, fmt.Errorf("no field named %q was found under type DataframePageInfo", field.Name) +} + +func (ec *executionContext) childFields_DataframeRowConnection(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + switch field.Name { + case "materialization": + return ec.fieldContext_DataframeRowConnection_materialization(ctx, field) + case "columns": + return ec.fieldContext_DataframeRowConnection_columns(ctx, field) + case "rows": + return ec.fieldContext_DataframeRowConnection_rows(ctx, field) + case "totalCount": + return ec.fieldContext_DataframeRowConnection_totalCount(ctx, field) + case "pageInfo": + return ec.fieldContext_DataframeRowConnection_pageInfo(ctx, field) + } + return nil, fmt.Errorf("no field named %q was found under type DataframeRowConnection", field.Name) +} + +func (ec *executionContext) childFields___Directive(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + switch field.Name { + case "name": + return ec.fieldContext___Directive_name(ctx, field) + case "description": + return ec.fieldContext___Directive_description(ctx, field) + case "isRepeatable": + return ec.fieldContext___Directive_isRepeatable(ctx, field) + case "locations": + return ec.fieldContext___Directive_locations(ctx, field) + case "args": + return ec.fieldContext___Directive_args(ctx, field) + } + return nil, fmt.Errorf("no field named %q was found under type __Directive", field.Name) +} + +func (ec *executionContext) childFields___EnumValue(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + switch field.Name { + case "name": + return ec.fieldContext___EnumValue_name(ctx, field) + case "description": + return ec.fieldContext___EnumValue_description(ctx, field) + case "isDeprecated": + return ec.fieldContext___EnumValue_isDeprecated(ctx, field) + case "deprecationReason": + return ec.fieldContext___EnumValue_deprecationReason(ctx, field) + } + return nil, fmt.Errorf("no field named %q was found under type __EnumValue", field.Name) +} + +func (ec *executionContext) childFields___Field(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + switch field.Name { + case "name": + return ec.fieldContext___Field_name(ctx, field) + case "description": + return ec.fieldContext___Field_description(ctx, field) + case "args": + return ec.fieldContext___Field_args(ctx, field) + case "type": + return ec.fieldContext___Field_type(ctx, field) + case "isDeprecated": + return ec.fieldContext___Field_isDeprecated(ctx, field) + case "deprecationReason": + return ec.fieldContext___Field_deprecationReason(ctx, field) + } + return nil, fmt.Errorf("no field named %q was found under type __Field", field.Name) +} + +func (ec *executionContext) childFields___InputValue(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + switch field.Name { + case "name": + return ec.fieldContext___InputValue_name(ctx, field) + case "description": + return ec.fieldContext___InputValue_description(ctx, field) + case "type": + return ec.fieldContext___InputValue_type(ctx, field) + case "defaultValue": + return ec.fieldContext___InputValue_defaultValue(ctx, field) + case "isDeprecated": + return ec.fieldContext___InputValue_isDeprecated(ctx, field) + case "deprecationReason": + return ec.fieldContext___InputValue_deprecationReason(ctx, field) + } + return nil, fmt.Errorf("no field named %q was found under type __InputValue", field.Name) +} + +func (ec *executionContext) childFields___Schema(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + switch field.Name { + case "description": + return ec.fieldContext___Schema_description(ctx, field) + case "types": + return ec.fieldContext___Schema_types(ctx, field) + case "queryType": + return ec.fieldContext___Schema_queryType(ctx, field) + case "mutationType": + return ec.fieldContext___Schema_mutationType(ctx, field) + case "subscriptionType": + return ec.fieldContext___Schema_subscriptionType(ctx, field) + case "directives": + return ec.fieldContext___Schema_directives(ctx, field) + } + return nil, fmt.Errorf("no field named %q was found under type __Schema", field.Name) +} + +func (ec *executionContext) childFields___Type(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + switch field.Name { + case "kind": + return ec.fieldContext___Type_kind(ctx, field) + case "name": + return ec.fieldContext___Type_name(ctx, field) + case "description": + return ec.fieldContext___Type_description(ctx, field) + case "specifiedByURL": + return ec.fieldContext___Type_specifiedByURL(ctx, field) + case "fields": + return ec.fieldContext___Type_fields(ctx, field) + case "interfaces": + return ec.fieldContext___Type_interfaces(ctx, field) + case "possibleTypes": + return ec.fieldContext___Type_possibleTypes(ctx, field) + case "enumValues": + return ec.fieldContext___Type_enumValues(ctx, field) + case "inputFields": + return ec.fieldContext___Type_inputFields(ctx, field) + case "ofType": + return ec.fieldContext___Type_ofType(ctx, field) + case "isOneOf": + return ec.fieldContext___Type_isOneOf(ctx, field) + } + return nil, fmt.Errorf("no field named %q was found under type __Type", field.Name) +} + +// endregion ************************** internal!.gotpl *************************** + +// region ***************************** args.gotpl ***************************** + +func (ec *executionContext) field_Query___type_args(ctx context.Context, rawArgs map[string]any) (map[string]any, error) { + var err error + args := map[string]any{} + arg0, err := graphql.ProcessArgField(ctx, rawArgs, "name", + func(ctx context.Context, v any) (string, error) { + return ec.unmarshalNString2string(ctx, v) + }) + if err != nil { + return nil, err + } + args["name"] = arg0 + return args, nil +} + +func (ec *executionContext) field_Query_dataframeAggregate_args(ctx context.Context, rawArgs map[string]any) (map[string]any, error) { + var err error + args := map[string]any{} + arg0, err := graphql.ProcessArgField(ctx, rawArgs, "input", + func(ctx context.Context, v any) (model.DataframeAggregateInput, error) { + return ec.unmarshalNDataframeAggregateInput2githubᚗcomᚋcalyprᚋloomᚋgraphqlapiᚋmodelᚐDataframeAggregateInput(ctx, v) + }) + if err != nil { + return nil, err + } + args["input"] = arg0 + return args, nil +} + +func (ec *executionContext) field_Query_dataframeDataset_args(ctx context.Context, rawArgs map[string]any) (map[string]any, error) { + var err error + args := map[string]any{} + arg0, err := graphql.ProcessArgField(ctx, rawArgs, "input", + func(ctx context.Context, v any) (model.DataframeDatasetInput, error) { + return ec.unmarshalNDataframeDatasetInput2githubᚗcomᚋcalyprᚋloomᚋgraphqlapiᚋmodelᚐDataframeDatasetInput(ctx, v) + }) + if err != nil { + return nil, err + } + args["input"] = arg0 + return args, nil +} + +func (ec *executionContext) field_Query_dataframeRows_args(ctx context.Context, rawArgs map[string]any) (map[string]any, error) { + var err error + args := map[string]any{} + arg0, err := graphql.ProcessArgField(ctx, rawArgs, "input", + func(ctx context.Context, v any) (model.DataframeRowsInput, error) { + return ec.unmarshalNDataframeRowsInput2githubᚗcomᚋcalyprᚋloomᚋgraphqlapiᚋmodelᚐDataframeRowsInput(ctx, v) + }) + if err != nil { + return nil, err + } + args["input"] = arg0 + return args, nil +} + +func (ec *executionContext) field___Directive_args_args(ctx context.Context, rawArgs map[string]any) (map[string]any, error) { + var err error + args := map[string]any{} + arg0, err := graphql.ProcessArgField(ctx, rawArgs, "includeDeprecated", + func(ctx context.Context, v any) (*bool, error) { + return ec.unmarshalOBoolean2ᚖbool(ctx, v) + }) + if err != nil { + return nil, err + } + args["includeDeprecated"] = arg0 + return args, nil +} + +func (ec *executionContext) field___Field_args_args(ctx context.Context, rawArgs map[string]any) (map[string]any, error) { + var err error + args := map[string]any{} + arg0, err := graphql.ProcessArgField(ctx, rawArgs, "includeDeprecated", + func(ctx context.Context, v any) (*bool, error) { + return ec.unmarshalOBoolean2ᚖbool(ctx, v) + }) + if err != nil { + return nil, err + } + args["includeDeprecated"] = arg0 + return args, nil +} + +func (ec *executionContext) field___Type_enumValues_args(ctx context.Context, rawArgs map[string]any) (map[string]any, error) { + var err error + args := map[string]any{} + arg0, err := graphql.ProcessArgField(ctx, rawArgs, "includeDeprecated", + func(ctx context.Context, v any) (bool, error) { + return ec.unmarshalOBoolean2bool(ctx, v) + }) + if err != nil { + return nil, err + } + args["includeDeprecated"] = arg0 + return args, nil +} + +func (ec *executionContext) field___Type_fields_args(ctx context.Context, rawArgs map[string]any) (map[string]any, error) { + var err error + args := map[string]any{} + arg0, err := graphql.ProcessArgField(ctx, rawArgs, "includeDeprecated", + func(ctx context.Context, v any) (bool, error) { + return ec.unmarshalOBoolean2bool(ctx, v) + }) + if err != nil { + return nil, err + } + args["includeDeprecated"] = arg0 + return args, nil +} + +// endregion ***************************** args.gotpl ***************************** + +// region **************************** field.gotpl ***************************** + +func (ec *executionContext) _DataframeAggregateResult_materialization(ctx context.Context, field graphql.CollectedField, obj *model.DataframeAggregateResult) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_DataframeAggregateResult_materialization(ctx, field) + }, + func(ctx context.Context) (any, error) { + return obj.Materialization, nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v *model.DataframeMaterialization) graphql.Marshaler { + return ec.marshalNDataframeMaterialization2ᚖgithubᚗcomᚋcalyprᚋloomᚋgraphqlapiᚋmodelᚐDataframeMaterialization(ctx, selections, v) + }, + true, + true, + ) +} +func (ec *executionContext) fieldContext_DataframeAggregateResult_materialization(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "DataframeAggregateResult", + Field: field, + IsMethod: false, + IsResolver: false, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.childFields_DataframeMaterialization(ctx, field) + }, + } + return fc, nil +} + +func (ec *executionContext) _DataframeAggregateResult_columns(ctx context.Context, field graphql.CollectedField, obj *model.DataframeAggregateResult) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_DataframeAggregateResult_columns(ctx, field) + }, + func(ctx context.Context) (any, error) { + return obj.Columns, nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v []string) graphql.Marshaler { + return ec.marshalNString2ᚕstringᚄ(ctx, selections, v) + }, + true, + true, + ) +} +func (ec *executionContext) fieldContext_DataframeAggregateResult_columns(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + return graphql.NewScalarFieldContext("DataframeAggregateResult", field, false, false, errors.New("field of type String does not have child fields")) +} + +func (ec *executionContext) _DataframeAggregateResult_rows(ctx context.Context, field graphql.CollectedField, obj *model.DataframeAggregateResult) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_DataframeAggregateResult_rows(ctx, field) + }, + func(ctx context.Context) (any, error) { + return obj.Rows, nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v json.RawMessage) graphql.Marshaler { + return ec.marshalNJSON2encodingᚋjsonᚐRawMessage(ctx, selections, v) + }, + true, + true, + ) +} +func (ec *executionContext) fieldContext_DataframeAggregateResult_rows(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + return graphql.NewScalarFieldContext("DataframeAggregateResult", field, false, false, errors.New("field of type JSON does not have child fields")) +} + +func (ec *executionContext) _DataframeColumn_name(ctx context.Context, field graphql.CollectedField, obj *model.DataframeColumn) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_DataframeColumn_name(ctx, field) + }, + func(ctx context.Context) (any, error) { + return obj.Name, nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v string) graphql.Marshaler { + return ec.marshalNString2string(ctx, selections, v) + }, + true, + true, + ) +} +func (ec *executionContext) fieldContext_DataframeColumn_name(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + return graphql.NewScalarFieldContext("DataframeColumn", field, false, false, errors.New("field of type String does not have child fields")) +} + +func (ec *executionContext) _DataframeColumn_clickhouseType(ctx context.Context, field graphql.CollectedField, obj *model.DataframeColumn) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_DataframeColumn_clickhouseType(ctx, field) + }, + func(ctx context.Context) (any, error) { + return obj.ClickhouseType, nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v string) graphql.Marshaler { + return ec.marshalNString2string(ctx, selections, v) + }, + true, + true, + ) +} +func (ec *executionContext) fieldContext_DataframeColumn_clickhouseType(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + return graphql.NewScalarFieldContext("DataframeColumn", field, false, false, errors.New("field of type String does not have child fields")) +} + +func (ec *executionContext) _DataframeColumn_logicalType(ctx context.Context, field graphql.CollectedField, obj *model.DataframeColumn) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_DataframeColumn_logicalType(ctx, field) + }, + func(ctx context.Context) (any, error) { + return obj.LogicalType, nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v string) graphql.Marshaler { + return ec.marshalNString2string(ctx, selections, v) + }, + true, + true, + ) +} +func (ec *executionContext) fieldContext_DataframeColumn_logicalType(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + return graphql.NewScalarFieldContext("DataframeColumn", field, false, false, errors.New("field of type String does not have child fields")) +} + +func (ec *executionContext) _DataframeColumn_nullable(ctx context.Context, field graphql.CollectedField, obj *model.DataframeColumn) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_DataframeColumn_nullable(ctx, field) + }, + func(ctx context.Context) (any, error) { + return obj.Nullable, nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v bool) graphql.Marshaler { + return ec.marshalNBoolean2bool(ctx, selections, v) + }, + true, + true, + ) +} +func (ec *executionContext) fieldContext_DataframeColumn_nullable(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + return graphql.NewScalarFieldContext("DataframeColumn", field, false, false, errors.New("field of type Boolean does not have child fields")) +} + +func (ec *executionContext) _DataframeColumn_repeated(ctx context.Context, field graphql.CollectedField, obj *model.DataframeColumn) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_DataframeColumn_repeated(ctx, field) + }, + func(ctx context.Context) (any, error) { + return obj.Repeated, nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v bool) graphql.Marshaler { + return ec.marshalNBoolean2bool(ctx, selections, v) + }, + true, + true, + ) +} +func (ec *executionContext) fieldContext_DataframeColumn_repeated(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + return graphql.NewScalarFieldContext("DataframeColumn", field, false, false, errors.New("field of type Boolean does not have child fields")) +} + +func (ec *executionContext) _DataframeColumn_filterable(ctx context.Context, field graphql.CollectedField, obj *model.DataframeColumn) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_DataframeColumn_filterable(ctx, field) + }, + func(ctx context.Context) (any, error) { + return obj.Filterable, nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v bool) graphql.Marshaler { + return ec.marshalNBoolean2bool(ctx, selections, v) + }, + true, + true, + ) +} +func (ec *executionContext) fieldContext_DataframeColumn_filterable(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + return graphql.NewScalarFieldContext("DataframeColumn", field, false, false, errors.New("field of type Boolean does not have child fields")) +} + +func (ec *executionContext) _DataframeColumn_sortable(ctx context.Context, field graphql.CollectedField, obj *model.DataframeColumn) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_DataframeColumn_sortable(ctx, field) + }, + func(ctx context.Context) (any, error) { + return obj.Sortable, nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v bool) graphql.Marshaler { + return ec.marshalNBoolean2bool(ctx, selections, v) + }, + true, + true, + ) +} +func (ec *executionContext) fieldContext_DataframeColumn_sortable(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + return graphql.NewScalarFieldContext("DataframeColumn", field, false, false, errors.New("field of type Boolean does not have child fields")) +} + +func (ec *executionContext) _DataframeColumn_aggregatable(ctx context.Context, field graphql.CollectedField, obj *model.DataframeColumn) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_DataframeColumn_aggregatable(ctx, field) + }, + func(ctx context.Context) (any, error) { + return obj.Aggregatable, nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v bool) graphql.Marshaler { + return ec.marshalNBoolean2bool(ctx, selections, v) + }, + true, + true, + ) +} +func (ec *executionContext) fieldContext_DataframeColumn_aggregatable(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + return graphql.NewScalarFieldContext("DataframeColumn", field, false, false, errors.New("field of type Boolean does not have child fields")) +} + +func (ec *executionContext) _DataframeMaterialization_id(ctx context.Context, field graphql.CollectedField, obj *model.DataframeMaterialization) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_DataframeMaterialization_id(ctx, field) + }, + func(ctx context.Context) (any, error) { + return obj.ID, nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v string) graphql.Marshaler { + return ec.marshalNID2string(ctx, selections, v) + }, + true, + true, + ) +} +func (ec *executionContext) fieldContext_DataframeMaterialization_id(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + return graphql.NewScalarFieldContext("DataframeMaterialization", field, false, false, errors.New("field of type ID does not have child fields")) +} + +func (ec *executionContext) _DataframeMaterialization_name(ctx context.Context, field graphql.CollectedField, obj *model.DataframeMaterialization) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_DataframeMaterialization_name(ctx, field) + }, + func(ctx context.Context) (any, error) { + return obj.Name, nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v string) graphql.Marshaler { + return ec.marshalNString2string(ctx, selections, v) + }, + true, + true, + ) +} +func (ec *executionContext) fieldContext_DataframeMaterialization_name(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + return graphql.NewScalarFieldContext("DataframeMaterialization", field, false, false, errors.New("field of type String does not have child fields")) +} + +func (ec *executionContext) _DataframeMaterialization_revision(ctx context.Context, field graphql.CollectedField, obj *model.DataframeMaterialization) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_DataframeMaterialization_revision(ctx, field) + }, + func(ctx context.Context) (any, error) { + return obj.Revision, nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v string) graphql.Marshaler { + return ec.marshalNString2string(ctx, selections, v) + }, + true, + true, + ) +} +func (ec *executionContext) fieldContext_DataframeMaterialization_revision(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + return graphql.NewScalarFieldContext("DataframeMaterialization", field, false, false, errors.New("field of type String does not have child fields")) +} + +func (ec *executionContext) _DataframeMaterialization_state(ctx context.Context, field graphql.CollectedField, obj *model.DataframeMaterialization) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_DataframeMaterialization_state(ctx, field) + }, + func(ctx context.Context) (any, error) { + return obj.State, nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v model.DataframeMaterializationState) graphql.Marshaler { + return ec.marshalNDataframeMaterializationState2githubᚗcomᚋcalyprᚋloomᚋgraphqlapiᚋmodelᚐDataframeMaterializationState(ctx, selections, v) + }, + true, + true, + ) +} +func (ec *executionContext) fieldContext_DataframeMaterialization_state(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + return graphql.NewScalarFieldContext("DataframeMaterialization", field, false, false, errors.New("field of type DataframeMaterializationState does not have child fields")) +} + +func (ec *executionContext) _DataframeMaterialization_columns(ctx context.Context, field graphql.CollectedField, obj *model.DataframeMaterialization) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_DataframeMaterialization_columns(ctx, field) + }, + func(ctx context.Context) (any, error) { + return obj.Columns, nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v []*model.DataframeColumn) graphql.Marshaler { + return ec.marshalNDataframeColumn2ᚕᚖgithubᚗcomᚋcalyprᚋloomᚋgraphqlapiᚋmodelᚐDataframeColumnᚄ(ctx, selections, v) + }, + true, + true, + ) +} +func (ec *executionContext) fieldContext_DataframeMaterialization_columns(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "DataframeMaterialization", + Field: field, + IsMethod: false, + IsResolver: false, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.childFields_DataframeColumn(ctx, field) + }, + } + return fc, nil +} + +func (ec *executionContext) _DataframeMaterialization_rowCount(ctx context.Context, field graphql.CollectedField, obj *model.DataframeMaterialization) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_DataframeMaterialization_rowCount(ctx, field) + }, + func(ctx context.Context) (any, error) { + return obj.RowCount, nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v int) graphql.Marshaler { + return ec.marshalNInt2int(ctx, selections, v) + }, + true, + true, + ) +} +func (ec *executionContext) fieldContext_DataframeMaterialization_rowCount(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + return graphql.NewScalarFieldContext("DataframeMaterialization", field, false, false, errors.New("field of type Int does not have child fields")) +} + +func (ec *executionContext) _DataframeMaterialization_createdAt(ctx context.Context, field graphql.CollectedField, obj *model.DataframeMaterialization) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_DataframeMaterialization_createdAt(ctx, field) + }, + func(ctx context.Context) (any, error) { + return obj.CreatedAt, nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v string) graphql.Marshaler { + return ec.marshalNString2string(ctx, selections, v) + }, + true, + true, + ) +} +func (ec *executionContext) fieldContext_DataframeMaterialization_createdAt(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + return graphql.NewScalarFieldContext("DataframeMaterialization", field, false, false, errors.New("field of type String does not have child fields")) +} + +func (ec *executionContext) _DataframeMaterialization_readyAt(ctx context.Context, field graphql.CollectedField, obj *model.DataframeMaterialization) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_DataframeMaterialization_readyAt(ctx, field) + }, + func(ctx context.Context) (any, error) { + return obj.ReadyAt, nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v *string) graphql.Marshaler { + return ec.marshalOString2ᚖstring(ctx, selections, v) + }, + true, + false, + ) +} +func (ec *executionContext) fieldContext_DataframeMaterialization_readyAt(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + return graphql.NewScalarFieldContext("DataframeMaterialization", field, false, false, errors.New("field of type String does not have child fields")) +} + +func (ec *executionContext) _DataframeMaterialization_error(ctx context.Context, field graphql.CollectedField, obj *model.DataframeMaterialization) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_DataframeMaterialization_error(ctx, field) + }, + func(ctx context.Context) (any, error) { + return obj.Error, nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v *string) graphql.Marshaler { + return ec.marshalOString2ᚖstring(ctx, selections, v) + }, + true, + false, + ) +} +func (ec *executionContext) fieldContext_DataframeMaterialization_error(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + return graphql.NewScalarFieldContext("DataframeMaterialization", field, false, false, errors.New("field of type String does not have child fields")) +} + +func (ec *executionContext) _DataframePageInfo_hasNextPage(ctx context.Context, field graphql.CollectedField, obj *model.DataframePageInfo) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_DataframePageInfo_hasNextPage(ctx, field) + }, + func(ctx context.Context) (any, error) { + return obj.HasNextPage, nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v bool) graphql.Marshaler { + return ec.marshalNBoolean2bool(ctx, selections, v) + }, + true, + true, + ) +} +func (ec *executionContext) fieldContext_DataframePageInfo_hasNextPage(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + return graphql.NewScalarFieldContext("DataframePageInfo", field, false, false, errors.New("field of type Boolean does not have child fields")) +} + +func (ec *executionContext) _DataframePageInfo_endCursor(ctx context.Context, field graphql.CollectedField, obj *model.DataframePageInfo) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_DataframePageInfo_endCursor(ctx, field) + }, + func(ctx context.Context) (any, error) { + return obj.EndCursor, nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v *string) graphql.Marshaler { + return ec.marshalOString2ᚖstring(ctx, selections, v) + }, + true, + false, + ) +} +func (ec *executionContext) fieldContext_DataframePageInfo_endCursor(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + return graphql.NewScalarFieldContext("DataframePageInfo", field, false, false, errors.New("field of type String does not have child fields")) +} + +func (ec *executionContext) _DataframeRowConnection_materialization(ctx context.Context, field graphql.CollectedField, obj *model.DataframeRowConnection) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_DataframeRowConnection_materialization(ctx, field) + }, + func(ctx context.Context) (any, error) { + return obj.Materialization, nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v *model.DataframeMaterialization) graphql.Marshaler { + return ec.marshalNDataframeMaterialization2ᚖgithubᚗcomᚋcalyprᚋloomᚋgraphqlapiᚋmodelᚐDataframeMaterialization(ctx, selections, v) + }, + true, + true, + ) +} +func (ec *executionContext) fieldContext_DataframeRowConnection_materialization(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "DataframeRowConnection", + Field: field, + IsMethod: false, + IsResolver: false, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.childFields_DataframeMaterialization(ctx, field) + }, + } + return fc, nil +} + +func (ec *executionContext) _DataframeRowConnection_columns(ctx context.Context, field graphql.CollectedField, obj *model.DataframeRowConnection) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_DataframeRowConnection_columns(ctx, field) + }, + func(ctx context.Context) (any, error) { + return obj.Columns, nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v []string) graphql.Marshaler { + return ec.marshalNString2ᚕstringᚄ(ctx, selections, v) + }, + true, + true, + ) +} +func (ec *executionContext) fieldContext_DataframeRowConnection_columns(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + return graphql.NewScalarFieldContext("DataframeRowConnection", field, false, false, errors.New("field of type String does not have child fields")) +} + +func (ec *executionContext) _DataframeRowConnection_rows(ctx context.Context, field graphql.CollectedField, obj *model.DataframeRowConnection) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_DataframeRowConnection_rows(ctx, field) + }, + func(ctx context.Context) (any, error) { + return obj.Rows, nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v json.RawMessage) graphql.Marshaler { + return ec.marshalNJSON2encodingᚋjsonᚐRawMessage(ctx, selections, v) + }, + true, + true, + ) +} +func (ec *executionContext) fieldContext_DataframeRowConnection_rows(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + return graphql.NewScalarFieldContext("DataframeRowConnection", field, false, false, errors.New("field of type JSON does not have child fields")) +} + +func (ec *executionContext) _DataframeRowConnection_totalCount(ctx context.Context, field graphql.CollectedField, obj *model.DataframeRowConnection) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_DataframeRowConnection_totalCount(ctx, field) + }, + func(ctx context.Context) (any, error) { + return obj.TotalCount, nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v *int) graphql.Marshaler { + return ec.marshalOInt2ᚖint(ctx, selections, v) + }, + true, + false, + ) +} +func (ec *executionContext) fieldContext_DataframeRowConnection_totalCount(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + return graphql.NewScalarFieldContext("DataframeRowConnection", field, false, false, errors.New("field of type Int does not have child fields")) +} + +func (ec *executionContext) _DataframeRowConnection_pageInfo(ctx context.Context, field graphql.CollectedField, obj *model.DataframeRowConnection) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_DataframeRowConnection_pageInfo(ctx, field) + }, + func(ctx context.Context) (any, error) { + return obj.PageInfo, nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v *model.DataframePageInfo) graphql.Marshaler { + return ec.marshalNDataframePageInfo2ᚖgithubᚗcomᚋcalyprᚋloomᚋgraphqlapiᚋmodelᚐDataframePageInfo(ctx, selections, v) + }, + true, + true, + ) +} +func (ec *executionContext) fieldContext_DataframeRowConnection_pageInfo(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "DataframeRowConnection", + Field: field, + IsMethod: false, + IsResolver: false, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.childFields_DataframePageInfo(ctx, field) + }, + } + return fc, nil +} + +func (ec *executionContext) _Query_dataframeDatasets(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_Query_dataframeDatasets(ctx, field) + }, + func(ctx context.Context) (any, error) { + return ec.Resolvers.Query().DataframeDatasets(ctx) + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v []*model.DataframeMaterialization) graphql.Marshaler { + return ec.marshalNDataframeMaterialization2ᚕᚖgithubᚗcomᚋcalyprᚋloomᚋgraphqlapiᚋmodelᚐDataframeMaterializationᚄ(ctx, selections, v) + }, + true, + true, + ) +} +func (ec *executionContext) fieldContext_Query_dataframeDatasets(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "Query", + Field: field, + IsMethod: true, + IsResolver: true, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.childFields_DataframeMaterialization(ctx, field) + }, + } + return fc, nil +} + +func (ec *executionContext) _Query_dataframeDataset(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_Query_dataframeDataset(ctx, field) + }, + func(ctx context.Context) (any, error) { + fc := graphql.GetFieldContext(ctx) + return ec.Resolvers.Query().DataframeDataset(ctx, fc.Args["input"].(model.DataframeDatasetInput)) + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v *model.DataframeMaterialization) graphql.Marshaler { + return ec.marshalODataframeMaterialization2ᚖgithubᚗcomᚋcalyprᚋloomᚋgraphqlapiᚋmodelᚐDataframeMaterialization(ctx, selections, v) + }, + true, + false, + ) +} +func (ec *executionContext) fieldContext_Query_dataframeDataset(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "Query", + Field: field, + IsMethod: true, + IsResolver: true, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.childFields_DataframeMaterialization(ctx, field) + }, + } + defer func() { + if r := recover(); r != nil { + err = ec.Recover(ctx, r) + ec.Error(ctx, err) + } + }() + ctx = graphql.WithFieldContext(ctx, fc) + if fc.Args, err = ec.field_Query_dataframeDataset_args(ctx, field.ArgumentMap(ec.Variables)); err != nil { + ec.Error(ctx, err) + return fc, err + } + return fc, nil +} + +func (ec *executionContext) _Query_dataframeRows(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_Query_dataframeRows(ctx, field) + }, + func(ctx context.Context) (any, error) { + fc := graphql.GetFieldContext(ctx) + return ec.Resolvers.Query().DataframeRows(ctx, fc.Args["input"].(model.DataframeRowsInput)) + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v *model.DataframeRowConnection) graphql.Marshaler { + return ec.marshalNDataframeRowConnection2ᚖgithubᚗcomᚋcalyprᚋloomᚋgraphqlapiᚋmodelᚐDataframeRowConnection(ctx, selections, v) + }, + true, + true, + ) +} +func (ec *executionContext) fieldContext_Query_dataframeRows(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "Query", + Field: field, + IsMethod: true, + IsResolver: true, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.childFields_DataframeRowConnection(ctx, field) + }, + } + defer func() { + if r := recover(); r != nil { + err = ec.Recover(ctx, r) + ec.Error(ctx, err) + } + }() + ctx = graphql.WithFieldContext(ctx, fc) + if fc.Args, err = ec.field_Query_dataframeRows_args(ctx, field.ArgumentMap(ec.Variables)); err != nil { + ec.Error(ctx, err) + return fc, err + } + return fc, nil +} + +func (ec *executionContext) _Query_dataframeAggregate(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_Query_dataframeAggregate(ctx, field) + }, + func(ctx context.Context) (any, error) { + fc := graphql.GetFieldContext(ctx) + return ec.Resolvers.Query().DataframeAggregate(ctx, fc.Args["input"].(model.DataframeAggregateInput)) + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v *model.DataframeAggregateResult) graphql.Marshaler { + return ec.marshalNDataframeAggregateResult2ᚖgithubᚗcomᚋcalyprᚋloomᚋgraphqlapiᚋmodelᚐDataframeAggregateResult(ctx, selections, v) + }, + true, + true, + ) +} +func (ec *executionContext) fieldContext_Query_dataframeAggregate(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "Query", + Field: field, + IsMethod: true, + IsResolver: true, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.childFields_DataframeAggregateResult(ctx, field) + }, + } + defer func() { + if r := recover(); r != nil { + err = ec.Recover(ctx, r) + ec.Error(ctx, err) + } + }() + ctx = graphql.WithFieldContext(ctx, fc) + if fc.Args, err = ec.field_Query_dataframeAggregate_args(ctx, field.ArgumentMap(ec.Variables)); err != nil { + ec.Error(ctx, err) + return fc, err + } + return fc, nil +} + +func (ec *executionContext) _Query___type(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_Query___type(ctx, field) + }, + func(ctx context.Context) (any, error) { + fc := graphql.GetFieldContext(ctx) + return ec.IntrospectType(fc.Args["name"].(string)) + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v *introspection.Type) graphql.Marshaler { + return ec.marshalO__Type2ᚖgithubᚗcomᚋ99designsᚋgqlgenᚋgraphqlᚋintrospectionᚐType(ctx, selections, v) + }, + true, + false, + ) +} +func (ec *executionContext) fieldContext_Query___type(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "Query", + Field: field, + IsMethod: true, + IsResolver: false, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.childFields___Type(ctx, field) + }, + } + defer func() { + if r := recover(); r != nil { + err = ec.Recover(ctx, r) + ec.Error(ctx, err) + } + }() + ctx = graphql.WithFieldContext(ctx, fc) + if fc.Args, err = ec.field_Query___type_args(ctx, field.ArgumentMap(ec.Variables)); err != nil { + ec.Error(ctx, err) + return fc, err + } + return fc, nil +} + +func (ec *executionContext) _Query___schema(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_Query___schema(ctx, field) + }, + func(ctx context.Context) (any, error) { + return ec.IntrospectSchema() + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v *introspection.Schema) graphql.Marshaler { + return ec.marshalO__Schema2ᚖgithubᚗcomᚋ99designsᚋgqlgenᚋgraphqlᚋintrospectionᚐSchema(ctx, selections, v) + }, + true, + false, + ) +} +func (ec *executionContext) fieldContext_Query___schema(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "Query", + Field: field, + IsMethod: true, + IsResolver: false, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.childFields___Schema(ctx, field) + }, + } + return fc, nil +} + +func (ec *executionContext) ___Directive_name(ctx context.Context, field graphql.CollectedField, obj *introspection.Directive) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext___Directive_name(ctx, field) + }, + func(ctx context.Context) (any, error) { + return obj.Name, nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v string) graphql.Marshaler { + return ec.marshalNString2string(ctx, selections, v) + }, + true, + true, + ) +} +func (ec *executionContext) fieldContext___Directive_name(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + return graphql.NewScalarFieldContext("__Directive", field, false, false, errors.New("field of type String does not have child fields")) +} + +func (ec *executionContext) ___Directive_description(ctx context.Context, field graphql.CollectedField, obj *introspection.Directive) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext___Directive_description(ctx, field) + }, + func(ctx context.Context) (any, error) { + return obj.Description(), nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v *string) graphql.Marshaler { + return ec.marshalOString2ᚖstring(ctx, selections, v) + }, + true, + false, + ) +} +func (ec *executionContext) fieldContext___Directive_description(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + return graphql.NewScalarFieldContext("__Directive", field, true, false, errors.New("field of type String does not have child fields")) +} + +func (ec *executionContext) ___Directive_isRepeatable(ctx context.Context, field graphql.CollectedField, obj *introspection.Directive) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext___Directive_isRepeatable(ctx, field) + }, + func(ctx context.Context) (any, error) { + return obj.IsRepeatable, nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v bool) graphql.Marshaler { + return ec.marshalNBoolean2bool(ctx, selections, v) + }, + true, + true, + ) +} +func (ec *executionContext) fieldContext___Directive_isRepeatable(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + return graphql.NewScalarFieldContext("__Directive", field, false, false, errors.New("field of type Boolean does not have child fields")) +} + +func (ec *executionContext) ___Directive_locations(ctx context.Context, field graphql.CollectedField, obj *introspection.Directive) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext___Directive_locations(ctx, field) + }, + func(ctx context.Context) (any, error) { + return obj.Locations, nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v []string) graphql.Marshaler { + return ec.marshalN__DirectiveLocation2ᚕstringᚄ(ctx, selections, v) + }, + true, + true, + ) +} +func (ec *executionContext) fieldContext___Directive_locations(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + return graphql.NewScalarFieldContext("__Directive", field, false, false, errors.New("field of type __DirectiveLocation does not have child fields")) +} + +func (ec *executionContext) ___Directive_args(ctx context.Context, field graphql.CollectedField, obj *introspection.Directive) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext___Directive_args(ctx, field) + }, + func(ctx context.Context) (any, error) { + return obj.Args, nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v []introspection.InputValue) graphql.Marshaler { + return ec.marshalN__InputValue2ᚕgithubᚗcomᚋ99designsᚋgqlgenᚋgraphqlᚋintrospectionᚐInputValueᚄ(ctx, selections, v) + }, + true, + true, + ) +} +func (ec *executionContext) fieldContext___Directive_args(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "__Directive", + Field: field, + IsMethod: false, + IsResolver: false, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.childFields___InputValue(ctx, field) + }, + } + defer func() { + if r := recover(); r != nil { + err = ec.Recover(ctx, r) + ec.Error(ctx, err) + } + }() + ctx = graphql.WithFieldContext(ctx, fc) + if fc.Args, err = ec.field___Directive_args_args(ctx, field.ArgumentMap(ec.Variables)); err != nil { + ec.Error(ctx, err) + return fc, err + } + return fc, nil +} + +func (ec *executionContext) ___EnumValue_name(ctx context.Context, field graphql.CollectedField, obj *introspection.EnumValue) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext___EnumValue_name(ctx, field) + }, + func(ctx context.Context) (any, error) { + return obj.Name, nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v string) graphql.Marshaler { + return ec.marshalNString2string(ctx, selections, v) + }, + true, + true, + ) +} +func (ec *executionContext) fieldContext___EnumValue_name(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + return graphql.NewScalarFieldContext("__EnumValue", field, false, false, errors.New("field of type String does not have child fields")) +} + +func (ec *executionContext) ___EnumValue_description(ctx context.Context, field graphql.CollectedField, obj *introspection.EnumValue) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext___EnumValue_description(ctx, field) + }, + func(ctx context.Context) (any, error) { + return obj.Description(), nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v *string) graphql.Marshaler { + return ec.marshalOString2ᚖstring(ctx, selections, v) + }, + true, + false, + ) +} +func (ec *executionContext) fieldContext___EnumValue_description(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + return graphql.NewScalarFieldContext("__EnumValue", field, true, false, errors.New("field of type String does not have child fields")) +} + +func (ec *executionContext) ___EnumValue_isDeprecated(ctx context.Context, field graphql.CollectedField, obj *introspection.EnumValue) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext___EnumValue_isDeprecated(ctx, field) + }, + func(ctx context.Context) (any, error) { + return obj.IsDeprecated(), nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v bool) graphql.Marshaler { + return ec.marshalNBoolean2bool(ctx, selections, v) + }, + true, + true, + ) +} +func (ec *executionContext) fieldContext___EnumValue_isDeprecated(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + return graphql.NewScalarFieldContext("__EnumValue", field, true, false, errors.New("field of type Boolean does not have child fields")) +} + +func (ec *executionContext) ___EnumValue_deprecationReason(ctx context.Context, field graphql.CollectedField, obj *introspection.EnumValue) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext___EnumValue_deprecationReason(ctx, field) + }, + func(ctx context.Context) (any, error) { + return obj.DeprecationReason(), nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v *string) graphql.Marshaler { + return ec.marshalOString2ᚖstring(ctx, selections, v) + }, + true, + false, + ) +} +func (ec *executionContext) fieldContext___EnumValue_deprecationReason(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + return graphql.NewScalarFieldContext("__EnumValue", field, true, false, errors.New("field of type String does not have child fields")) +} + +func (ec *executionContext) ___Field_name(ctx context.Context, field graphql.CollectedField, obj *introspection.Field) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext___Field_name(ctx, field) + }, + func(ctx context.Context) (any, error) { + return obj.Name, nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v string) graphql.Marshaler { + return ec.marshalNString2string(ctx, selections, v) + }, + true, + true, + ) +} +func (ec *executionContext) fieldContext___Field_name(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + return graphql.NewScalarFieldContext("__Field", field, false, false, errors.New("field of type String does not have child fields")) +} + +func (ec *executionContext) ___Field_description(ctx context.Context, field graphql.CollectedField, obj *introspection.Field) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext___Field_description(ctx, field) + }, + func(ctx context.Context) (any, error) { + return obj.Description(), nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v *string) graphql.Marshaler { + return ec.marshalOString2ᚖstring(ctx, selections, v) + }, + true, + false, + ) +} +func (ec *executionContext) fieldContext___Field_description(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + return graphql.NewScalarFieldContext("__Field", field, true, false, errors.New("field of type String does not have child fields")) +} + +func (ec *executionContext) ___Field_args(ctx context.Context, field graphql.CollectedField, obj *introspection.Field) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext___Field_args(ctx, field) + }, + func(ctx context.Context) (any, error) { + return obj.Args, nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v []introspection.InputValue) graphql.Marshaler { + return ec.marshalN__InputValue2ᚕgithubᚗcomᚋ99designsᚋgqlgenᚋgraphqlᚋintrospectionᚐInputValueᚄ(ctx, selections, v) + }, + true, + true, + ) +} +func (ec *executionContext) fieldContext___Field_args(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "__Field", + Field: field, + IsMethod: false, + IsResolver: false, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.childFields___InputValue(ctx, field) + }, + } + defer func() { + if r := recover(); r != nil { + err = ec.Recover(ctx, r) + ec.Error(ctx, err) + } + }() + ctx = graphql.WithFieldContext(ctx, fc) + if fc.Args, err = ec.field___Field_args_args(ctx, field.ArgumentMap(ec.Variables)); err != nil { + ec.Error(ctx, err) + return fc, err + } + return fc, nil +} + +func (ec *executionContext) ___Field_type(ctx context.Context, field graphql.CollectedField, obj *introspection.Field) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext___Field_type(ctx, field) + }, + func(ctx context.Context) (any, error) { + return obj.Type, nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v *introspection.Type) graphql.Marshaler { + return ec.marshalN__Type2ᚖgithubᚗcomᚋ99designsᚋgqlgenᚋgraphqlᚋintrospectionᚐType(ctx, selections, v) + }, + true, + true, + ) +} +func (ec *executionContext) fieldContext___Field_type(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "__Field", + Field: field, + IsMethod: false, + IsResolver: false, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.childFields___Type(ctx, field) + }, + } + return fc, nil +} + +func (ec *executionContext) ___Field_isDeprecated(ctx context.Context, field graphql.CollectedField, obj *introspection.Field) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext___Field_isDeprecated(ctx, field) + }, + func(ctx context.Context) (any, error) { + return obj.IsDeprecated(), nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v bool) graphql.Marshaler { + return ec.marshalNBoolean2bool(ctx, selections, v) + }, + true, + true, + ) +} +func (ec *executionContext) fieldContext___Field_isDeprecated(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + return graphql.NewScalarFieldContext("__Field", field, true, false, errors.New("field of type Boolean does not have child fields")) +} + +func (ec *executionContext) ___Field_deprecationReason(ctx context.Context, field graphql.CollectedField, obj *introspection.Field) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext___Field_deprecationReason(ctx, field) + }, + func(ctx context.Context) (any, error) { + return obj.DeprecationReason(), nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v *string) graphql.Marshaler { + return ec.marshalOString2ᚖstring(ctx, selections, v) + }, + true, + false, + ) +} +func (ec *executionContext) fieldContext___Field_deprecationReason(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + return graphql.NewScalarFieldContext("__Field", field, true, false, errors.New("field of type String does not have child fields")) +} + +func (ec *executionContext) ___InputValue_name(ctx context.Context, field graphql.CollectedField, obj *introspection.InputValue) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext___InputValue_name(ctx, field) + }, + func(ctx context.Context) (any, error) { + return obj.Name, nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v string) graphql.Marshaler { + return ec.marshalNString2string(ctx, selections, v) + }, + true, + true, + ) +} +func (ec *executionContext) fieldContext___InputValue_name(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + return graphql.NewScalarFieldContext("__InputValue", field, false, false, errors.New("field of type String does not have child fields")) +} + +func (ec *executionContext) ___InputValue_description(ctx context.Context, field graphql.CollectedField, obj *introspection.InputValue) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext___InputValue_description(ctx, field) + }, + func(ctx context.Context) (any, error) { + return obj.Description(), nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v *string) graphql.Marshaler { + return ec.marshalOString2ᚖstring(ctx, selections, v) + }, + true, + false, + ) +} +func (ec *executionContext) fieldContext___InputValue_description(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + return graphql.NewScalarFieldContext("__InputValue", field, true, false, errors.New("field of type String does not have child fields")) +} + +func (ec *executionContext) ___InputValue_type(ctx context.Context, field graphql.CollectedField, obj *introspection.InputValue) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext___InputValue_type(ctx, field) + }, + func(ctx context.Context) (any, error) { + return obj.Type, nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v *introspection.Type) graphql.Marshaler { + return ec.marshalN__Type2ᚖgithubᚗcomᚋ99designsᚋgqlgenᚋgraphqlᚋintrospectionᚐType(ctx, selections, v) + }, + true, + true, + ) +} +func (ec *executionContext) fieldContext___InputValue_type(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "__InputValue", + Field: field, + IsMethod: false, + IsResolver: false, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.childFields___Type(ctx, field) + }, + } + return fc, nil +} + +func (ec *executionContext) ___InputValue_defaultValue(ctx context.Context, field graphql.CollectedField, obj *introspection.InputValue) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext___InputValue_defaultValue(ctx, field) + }, + func(ctx context.Context) (any, error) { + return obj.DefaultValue, nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v *string) graphql.Marshaler { + return ec.marshalOString2ᚖstring(ctx, selections, v) + }, + true, + false, + ) +} +func (ec *executionContext) fieldContext___InputValue_defaultValue(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + return graphql.NewScalarFieldContext("__InputValue", field, false, false, errors.New("field of type String does not have child fields")) +} + +func (ec *executionContext) ___InputValue_isDeprecated(ctx context.Context, field graphql.CollectedField, obj *introspection.InputValue) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext___InputValue_isDeprecated(ctx, field) + }, + func(ctx context.Context) (any, error) { + return obj.IsDeprecated(), nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v bool) graphql.Marshaler { + return ec.marshalNBoolean2bool(ctx, selections, v) + }, + true, + true, + ) +} +func (ec *executionContext) fieldContext___InputValue_isDeprecated(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + return graphql.NewScalarFieldContext("__InputValue", field, true, false, errors.New("field of type Boolean does not have child fields")) +} + +func (ec *executionContext) ___InputValue_deprecationReason(ctx context.Context, field graphql.CollectedField, obj *introspection.InputValue) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext___InputValue_deprecationReason(ctx, field) + }, + func(ctx context.Context) (any, error) { + return obj.DeprecationReason(), nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v *string) graphql.Marshaler { + return ec.marshalOString2ᚖstring(ctx, selections, v) + }, + true, + false, + ) +} +func (ec *executionContext) fieldContext___InputValue_deprecationReason(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + return graphql.NewScalarFieldContext("__InputValue", field, true, false, errors.New("field of type String does not have child fields")) +} + +func (ec *executionContext) ___Schema_description(ctx context.Context, field graphql.CollectedField, obj *introspection.Schema) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext___Schema_description(ctx, field) + }, + func(ctx context.Context) (any, error) { + return obj.Description(), nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v *string) graphql.Marshaler { + return ec.marshalOString2ᚖstring(ctx, selections, v) + }, + true, + false, + ) +} +func (ec *executionContext) fieldContext___Schema_description(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + return graphql.NewScalarFieldContext("__Schema", field, true, false, errors.New("field of type String does not have child fields")) +} + +func (ec *executionContext) ___Schema_types(ctx context.Context, field graphql.CollectedField, obj *introspection.Schema) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext___Schema_types(ctx, field) + }, + func(ctx context.Context) (any, error) { + return obj.Types(), nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v []introspection.Type) graphql.Marshaler { + return ec.marshalN__Type2ᚕgithubᚗcomᚋ99designsᚋgqlgenᚋgraphqlᚋintrospectionᚐTypeᚄ(ctx, selections, v) + }, + true, + true, + ) +} +func (ec *executionContext) fieldContext___Schema_types(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "__Schema", + Field: field, + IsMethod: true, + IsResolver: false, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.childFields___Type(ctx, field) + }, + } + return fc, nil +} + +func (ec *executionContext) ___Schema_queryType(ctx context.Context, field graphql.CollectedField, obj *introspection.Schema) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext___Schema_queryType(ctx, field) + }, + func(ctx context.Context) (any, error) { + return obj.QueryType(), nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v *introspection.Type) graphql.Marshaler { + return ec.marshalN__Type2ᚖgithubᚗcomᚋ99designsᚋgqlgenᚋgraphqlᚋintrospectionᚐType(ctx, selections, v) + }, + true, + true, + ) +} +func (ec *executionContext) fieldContext___Schema_queryType(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "__Schema", + Field: field, + IsMethod: true, + IsResolver: false, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.childFields___Type(ctx, field) + }, + } + return fc, nil +} + +func (ec *executionContext) ___Schema_mutationType(ctx context.Context, field graphql.CollectedField, obj *introspection.Schema) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext___Schema_mutationType(ctx, field) + }, + func(ctx context.Context) (any, error) { + return obj.MutationType(), nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v *introspection.Type) graphql.Marshaler { + return ec.marshalO__Type2ᚖgithubᚗcomᚋ99designsᚋgqlgenᚋgraphqlᚋintrospectionᚐType(ctx, selections, v) + }, + true, + false, + ) +} +func (ec *executionContext) fieldContext___Schema_mutationType(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "__Schema", + Field: field, + IsMethod: true, + IsResolver: false, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.childFields___Type(ctx, field) + }, + } + return fc, nil +} + +func (ec *executionContext) ___Schema_subscriptionType(ctx context.Context, field graphql.CollectedField, obj *introspection.Schema) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext___Schema_subscriptionType(ctx, field) + }, + func(ctx context.Context) (any, error) { + return obj.SubscriptionType(), nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v *introspection.Type) graphql.Marshaler { + return ec.marshalO__Type2ᚖgithubᚗcomᚋ99designsᚋgqlgenᚋgraphqlᚋintrospectionᚐType(ctx, selections, v) + }, + true, + false, + ) +} +func (ec *executionContext) fieldContext___Schema_subscriptionType(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "__Schema", + Field: field, + IsMethod: true, + IsResolver: false, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.childFields___Type(ctx, field) + }, + } + return fc, nil +} + +func (ec *executionContext) ___Schema_directives(ctx context.Context, field graphql.CollectedField, obj *introspection.Schema) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext___Schema_directives(ctx, field) + }, + func(ctx context.Context) (any, error) { + return obj.Directives(), nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v []introspection.Directive) graphql.Marshaler { + return ec.marshalN__Directive2ᚕgithubᚗcomᚋ99designsᚋgqlgenᚋgraphqlᚋintrospectionᚐDirectiveᚄ(ctx, selections, v) + }, + true, + true, + ) +} +func (ec *executionContext) fieldContext___Schema_directives(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "__Schema", + Field: field, + IsMethod: true, + IsResolver: false, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.childFields___Directive(ctx, field) + }, + } + return fc, nil +} + +func (ec *executionContext) ___Type_kind(ctx context.Context, field graphql.CollectedField, obj *introspection.Type) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext___Type_kind(ctx, field) + }, + func(ctx context.Context) (any, error) { + return obj.Kind(), nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v string) graphql.Marshaler { + return ec.marshalN__TypeKind2string(ctx, selections, v) + }, + true, + true, + ) +} +func (ec *executionContext) fieldContext___Type_kind(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + return graphql.NewScalarFieldContext("__Type", field, true, false, errors.New("field of type __TypeKind does not have child fields")) +} + +func (ec *executionContext) ___Type_name(ctx context.Context, field graphql.CollectedField, obj *introspection.Type) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext___Type_name(ctx, field) + }, + func(ctx context.Context) (any, error) { + return obj.Name(), nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v *string) graphql.Marshaler { + return ec.marshalOString2ᚖstring(ctx, selections, v) + }, + true, + false, + ) +} +func (ec *executionContext) fieldContext___Type_name(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + return graphql.NewScalarFieldContext("__Type", field, true, false, errors.New("field of type String does not have child fields")) +} + +func (ec *executionContext) ___Type_description(ctx context.Context, field graphql.CollectedField, obj *introspection.Type) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext___Type_description(ctx, field) + }, + func(ctx context.Context) (any, error) { + return obj.Description(), nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v *string) graphql.Marshaler { + return ec.marshalOString2ᚖstring(ctx, selections, v) + }, + true, + false, + ) +} +func (ec *executionContext) fieldContext___Type_description(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + return graphql.NewScalarFieldContext("__Type", field, true, false, errors.New("field of type String does not have child fields")) +} + +func (ec *executionContext) ___Type_specifiedByURL(ctx context.Context, field graphql.CollectedField, obj *introspection.Type) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext___Type_specifiedByURL(ctx, field) + }, + func(ctx context.Context) (any, error) { + return obj.SpecifiedByURL(), nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v *string) graphql.Marshaler { + return ec.marshalOString2ᚖstring(ctx, selections, v) + }, + true, + false, + ) +} +func (ec *executionContext) fieldContext___Type_specifiedByURL(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + return graphql.NewScalarFieldContext("__Type", field, true, false, errors.New("field of type String does not have child fields")) +} + +func (ec *executionContext) ___Type_fields(ctx context.Context, field graphql.CollectedField, obj *introspection.Type) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext___Type_fields(ctx, field) + }, + func(ctx context.Context) (any, error) { + fc := graphql.GetFieldContext(ctx) + return obj.Fields(fc.Args["includeDeprecated"].(bool)), nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v []introspection.Field) graphql.Marshaler { + return ec.marshalO__Field2ᚕgithubᚗcomᚋ99designsᚋgqlgenᚋgraphqlᚋintrospectionᚐFieldᚄ(ctx, selections, v) + }, + true, + false, + ) +} +func (ec *executionContext) fieldContext___Type_fields(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "__Type", + Field: field, + IsMethod: true, + IsResolver: false, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.childFields___Field(ctx, field) + }, + } + defer func() { + if r := recover(); r != nil { + err = ec.Recover(ctx, r) + ec.Error(ctx, err) + } + }() + ctx = graphql.WithFieldContext(ctx, fc) + if fc.Args, err = ec.field___Type_fields_args(ctx, field.ArgumentMap(ec.Variables)); err != nil { + ec.Error(ctx, err) + return fc, err + } + return fc, nil +} + +func (ec *executionContext) ___Type_interfaces(ctx context.Context, field graphql.CollectedField, obj *introspection.Type) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext___Type_interfaces(ctx, field) + }, + func(ctx context.Context) (any, error) { + return obj.Interfaces(), nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v []introspection.Type) graphql.Marshaler { + return ec.marshalO__Type2ᚕgithubᚗcomᚋ99designsᚋgqlgenᚋgraphqlᚋintrospectionᚐTypeᚄ(ctx, selections, v) + }, + true, + false, + ) +} +func (ec *executionContext) fieldContext___Type_interfaces(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "__Type", + Field: field, + IsMethod: true, + IsResolver: false, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.childFields___Type(ctx, field) + }, + } + return fc, nil +} + +func (ec *executionContext) ___Type_possibleTypes(ctx context.Context, field graphql.CollectedField, obj *introspection.Type) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext___Type_possibleTypes(ctx, field) + }, + func(ctx context.Context) (any, error) { + return obj.PossibleTypes(), nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v []introspection.Type) graphql.Marshaler { + return ec.marshalO__Type2ᚕgithubᚗcomᚋ99designsᚋgqlgenᚋgraphqlᚋintrospectionᚐTypeᚄ(ctx, selections, v) + }, + true, + false, + ) +} +func (ec *executionContext) fieldContext___Type_possibleTypes(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "__Type", + Field: field, + IsMethod: true, + IsResolver: false, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.childFields___Type(ctx, field) + }, + } + return fc, nil +} + +func (ec *executionContext) ___Type_enumValues(ctx context.Context, field graphql.CollectedField, obj *introspection.Type) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext___Type_enumValues(ctx, field) + }, + func(ctx context.Context) (any, error) { + fc := graphql.GetFieldContext(ctx) + return obj.EnumValues(fc.Args["includeDeprecated"].(bool)), nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v []introspection.EnumValue) graphql.Marshaler { + return ec.marshalO__EnumValue2ᚕgithubᚗcomᚋ99designsᚋgqlgenᚋgraphqlᚋintrospectionᚐEnumValueᚄ(ctx, selections, v) + }, + true, + false, + ) +} +func (ec *executionContext) fieldContext___Type_enumValues(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "__Type", + Field: field, + IsMethod: true, + IsResolver: false, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.childFields___EnumValue(ctx, field) + }, + } + defer func() { + if r := recover(); r != nil { + err = ec.Recover(ctx, r) + ec.Error(ctx, err) + } + }() + ctx = graphql.WithFieldContext(ctx, fc) + if fc.Args, err = ec.field___Type_enumValues_args(ctx, field.ArgumentMap(ec.Variables)); err != nil { + ec.Error(ctx, err) + return fc, err + } + return fc, nil +} + +func (ec *executionContext) ___Type_inputFields(ctx context.Context, field graphql.CollectedField, obj *introspection.Type) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext___Type_inputFields(ctx, field) + }, + func(ctx context.Context) (any, error) { + return obj.InputFields(), nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v []introspection.InputValue) graphql.Marshaler { + return ec.marshalO__InputValue2ᚕgithubᚗcomᚋ99designsᚋgqlgenᚋgraphqlᚋintrospectionᚐInputValueᚄ(ctx, selections, v) + }, + true, + false, + ) +} +func (ec *executionContext) fieldContext___Type_inputFields(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "__Type", + Field: field, + IsMethod: true, + IsResolver: false, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.childFields___InputValue(ctx, field) + }, + } + return fc, nil +} + +func (ec *executionContext) ___Type_ofType(ctx context.Context, field graphql.CollectedField, obj *introspection.Type) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext___Type_ofType(ctx, field) + }, + func(ctx context.Context) (any, error) { + return obj.OfType(), nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v *introspection.Type) graphql.Marshaler { + return ec.marshalO__Type2ᚖgithubᚗcomᚋ99designsᚋgqlgenᚋgraphqlᚋintrospectionᚐType(ctx, selections, v) + }, + true, + false, + ) +} +func (ec *executionContext) fieldContext___Type_ofType(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "__Type", + Field: field, + IsMethod: true, + IsResolver: false, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.childFields___Type(ctx, field) + }, + } + return fc, nil +} + +func (ec *executionContext) ___Type_isOneOf(ctx context.Context, field graphql.CollectedField, obj *introspection.Type) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext___Type_isOneOf(ctx, field) + }, + func(ctx context.Context) (any, error) { + return obj.IsOneOf(), nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v bool) graphql.Marshaler { + return ec.marshalOBoolean2bool(ctx, selections, v) + }, + true, + false, + ) +} +func (ec *executionContext) fieldContext___Type_isOneOf(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + return graphql.NewScalarFieldContext("__Type", field, true, false, errors.New("field of type Boolean does not have child fields")) +} + +// endregion **************************** field.gotpl ***************************** + +// region **************************** input.gotpl ***************************** + +func (ec *executionContext) unmarshalInputDataframeAggregateInput(ctx context.Context, obj any) (model.DataframeAggregateInput, error) { + var it model.DataframeAggregateInput + if obj == nil { + return it, nil + } + + asMap := map[string]any{} + for k, v := range obj.(map[string]any) { + asMap[k] = v + } + + fieldsInOrder := [...]string{"dataType", "groupBy", "filters", "operation", "column"} + for _, k := range fieldsInOrder { + v, ok := asMap[k] + if !ok { + continue + } + switch k { + case "dataType": + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("dataType")) + data, err := ec.unmarshalNString2string(ctx, v) + if err != nil { + return it, err + } + it.DataType = data + case "groupBy": + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("groupBy")) + data, err := ec.unmarshalOString2ᚕstringᚄ(ctx, v) + if err != nil { + return it, err + } + it.GroupBy = data + case "filters": + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("filters")) + data, err := ec.unmarshalODataframeFilterInput2ᚕᚖgithubᚗcomᚋcalyprᚋloomᚋgraphqlapiᚋmodelᚐDataframeFilterInputᚄ(ctx, v) + if err != nil { + return it, err + } + it.Filters = data + case "operation": + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("operation")) + data, err := ec.unmarshalNString2string(ctx, v) + if err != nil { + return it, err + } + it.Operation = data + case "column": + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("column")) + data, err := ec.unmarshalOString2ᚖstring(ctx, v) + if err != nil { + return it, err + } + it.Column = data + } + } + return it, nil +} + +func (ec *executionContext) unmarshalInputDataframeDatasetInput(ctx context.Context, obj any) (model.DataframeDatasetInput, error) { + var it model.DataframeDatasetInput + if obj == nil { + return it, nil + } + + asMap := map[string]any{} + for k, v := range obj.(map[string]any) { + asMap[k] = v + } + + fieldsInOrder := [...]string{"dataType"} + for _, k := range fieldsInOrder { + v, ok := asMap[k] + if !ok { + continue + } + switch k { + case "dataType": + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("dataType")) + data, err := ec.unmarshalNString2string(ctx, v) + if err != nil { + return it, err + } + it.DataType = data + } + } + return it, nil +} + +func (ec *executionContext) unmarshalInputDataframeFilterInput(ctx context.Context, obj any) (model.DataframeFilterInput, error) { + var it model.DataframeFilterInput + if obj == nil { + return it, nil + } + + asMap := map[string]any{} + for k, v := range obj.(map[string]any) { + asMap[k] = v + } + + fieldsInOrder := [...]string{"column", "op", "value"} + for _, k := range fieldsInOrder { + v, ok := asMap[k] + if !ok { + continue + } + switch k { + case "column": + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("column")) + data, err := ec.unmarshalNString2string(ctx, v) + if err != nil { + return it, err + } + it.Column = data + case "op": + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("op")) + data, err := ec.unmarshalNString2string(ctx, v) + if err != nil { + return it, err + } + it.Op = data + case "value": + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("value")) + data, err := ec.unmarshalNJSON2encodingᚋjsonᚐRawMessage(ctx, v) + if err != nil { + return it, err + } + it.Value = data + } + } + return it, nil +} + +func (ec *executionContext) unmarshalInputDataframeRowsInput(ctx context.Context, obj any) (model.DataframeRowsInput, error) { + var it model.DataframeRowsInput + if obj == nil { + return it, nil + } + + asMap := map[string]any{} + for k, v := range obj.(map[string]any) { + asMap[k] = v + } + + if _, present := asMap["first"]; !present { + asMap["first"] = 100 + } + + fieldsInOrder := [...]string{"dataType", "columns", "filters", "sort", "first", "after"} + for _, k := range fieldsInOrder { + v, ok := asMap[k] + if !ok { + continue + } + switch k { + case "dataType": + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("dataType")) + data, err := ec.unmarshalNString2string(ctx, v) + if err != nil { + return it, err + } + it.DataType = data + case "columns": + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("columns")) + data, err := ec.unmarshalOString2ᚕstringᚄ(ctx, v) + if err != nil { + return it, err + } + it.Columns = data + case "filters": + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("filters")) + data, err := ec.unmarshalODataframeFilterInput2ᚕᚖgithubᚗcomᚋcalyprᚋloomᚋgraphqlapiᚋmodelᚐDataframeFilterInputᚄ(ctx, v) + if err != nil { + return it, err + } + it.Filters = data + case "sort": + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("sort")) + data, err := ec.unmarshalODataframeSortInput2ᚖgithubᚗcomᚋcalyprᚋloomᚋgraphqlapiᚋmodelᚐDataframeSortInput(ctx, v) + if err != nil { + return it, err + } + it.Sort = data + case "first": + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("first")) + data, err := ec.unmarshalOInt2ᚖint(ctx, v) + if err != nil { + return it, err + } + it.First = data + case "after": + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("after")) + data, err := ec.unmarshalOString2ᚖstring(ctx, v) + if err != nil { + return it, err + } + it.After = data + } + } + return it, nil +} + +func (ec *executionContext) unmarshalInputDataframeSortInput(ctx context.Context, obj any) (model.DataframeSortInput, error) { + var it model.DataframeSortInput + if obj == nil { + return it, nil + } + + asMap := map[string]any{} + for k, v := range obj.(map[string]any) { + asMap[k] = v + } + + if _, present := asMap["desc"]; !present { + asMap["desc"] = false + } + + fieldsInOrder := [...]string{"column", "desc"} + for _, k := range fieldsInOrder { + v, ok := asMap[k] + if !ok { + continue + } + switch k { + case "column": + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("column")) + data, err := ec.unmarshalNString2string(ctx, v) + if err != nil { + return it, err + } + it.Column = data + case "desc": + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("desc")) + data, err := ec.unmarshalOBoolean2ᚖbool(ctx, v) + if err != nil { + return it, err + } + it.Desc = data + } + } + return it, nil +} + +// endregion **************************** input.gotpl ***************************** + +// region ************************** interface.gotpl *************************** + +// endregion ************************** interface.gotpl *************************** + +// region **************************** object.gotpl **************************** + +var dataframeAggregateResultImplementors = []string{"DataframeAggregateResult"} + +func (ec *executionContext) _DataframeAggregateResult(ctx context.Context, sel ast.SelectionSet, obj *model.DataframeAggregateResult) graphql.Marshaler { + fields := graphql.CollectFields(ec.OperationContext, sel, dataframeAggregateResultImplementors) + + out := graphql.NewFieldSet(fields) + deferredFieldSet := graphql.NewFieldSet(nil) + deferLabelToView := make(map[string]*graphql.FieldSetView) + for i, field := range fields { + switch field.Name { + case "__typename": + out.Values[i] = graphql.MarshalString("DataframeAggregateResult") + case "materialization": + out.Values[i] = ec._DataframeAggregateResult_materialization(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ + } + case "columns": + out.Values[i] = ec._DataframeAggregateResult_columns(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ + } + case "rows": + out.Values[i] = ec._DataframeAggregateResult_rows(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ + } + default: + panic("unknown field " + strconv.Quote(field.Name)) + } + } + out.Dispatch(ctx) + if out.Invalids > 0 { + return graphql.Null + } + + atomic.AddInt32(&ec.Deferred, int32(min(len(deferLabelToView), math.MaxInt32))) + + ec.ProcessDeferredGroup(graphql.DeferredGroup{ + Defers: deferLabelToView, + Path: graphql.GetPath(ctx), + FieldSet: deferredFieldSet, + Context: ctx, + }) + + return out +} + +var dataframeColumnImplementors = []string{"DataframeColumn"} + +func (ec *executionContext) _DataframeColumn(ctx context.Context, sel ast.SelectionSet, obj *model.DataframeColumn) graphql.Marshaler { + fields := graphql.CollectFields(ec.OperationContext, sel, dataframeColumnImplementors) + + out := graphql.NewFieldSet(fields) + deferredFieldSet := graphql.NewFieldSet(nil) + deferLabelToView := make(map[string]*graphql.FieldSetView) + for i, field := range fields { + switch field.Name { + case "__typename": + out.Values[i] = graphql.MarshalString("DataframeColumn") + case "name": + out.Values[i] = ec._DataframeColumn_name(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ + } + case "clickhouseType": + out.Values[i] = ec._DataframeColumn_clickhouseType(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ + } + case "logicalType": + out.Values[i] = ec._DataframeColumn_logicalType(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ + } + case "nullable": + out.Values[i] = ec._DataframeColumn_nullable(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ + } + case "repeated": + out.Values[i] = ec._DataframeColumn_repeated(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ + } + case "filterable": + out.Values[i] = ec._DataframeColumn_filterable(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ + } + case "sortable": + out.Values[i] = ec._DataframeColumn_sortable(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ + } + case "aggregatable": + out.Values[i] = ec._DataframeColumn_aggregatable(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ + } + default: + panic("unknown field " + strconv.Quote(field.Name)) + } + } + out.Dispatch(ctx) + if out.Invalids > 0 { + return graphql.Null + } + + atomic.AddInt32(&ec.Deferred, int32(min(len(deferLabelToView), math.MaxInt32))) + + ec.ProcessDeferredGroup(graphql.DeferredGroup{ + Defers: deferLabelToView, + Path: graphql.GetPath(ctx), + FieldSet: deferredFieldSet, + Context: ctx, + }) + + return out +} + +var dataframeMaterializationImplementors = []string{"DataframeMaterialization"} + +func (ec *executionContext) _DataframeMaterialization(ctx context.Context, sel ast.SelectionSet, obj *model.DataframeMaterialization) graphql.Marshaler { + fields := graphql.CollectFields(ec.OperationContext, sel, dataframeMaterializationImplementors) + + out := graphql.NewFieldSet(fields) + deferredFieldSet := graphql.NewFieldSet(nil) + deferLabelToView := make(map[string]*graphql.FieldSetView) + for i, field := range fields { + switch field.Name { + case "__typename": + out.Values[i] = graphql.MarshalString("DataframeMaterialization") + case "id": + out.Values[i] = ec._DataframeMaterialization_id(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ + } + case "name": + out.Values[i] = ec._DataframeMaterialization_name(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ + } + case "revision": + out.Values[i] = ec._DataframeMaterialization_revision(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ + } + case "state": + out.Values[i] = ec._DataframeMaterialization_state(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ + } + case "columns": + out.Values[i] = ec._DataframeMaterialization_columns(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ + } + case "rowCount": + out.Values[i] = ec._DataframeMaterialization_rowCount(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ + } + case "createdAt": + out.Values[i] = ec._DataframeMaterialization_createdAt(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ + } + case "readyAt": + out.Values[i] = ec._DataframeMaterialization_readyAt(ctx, field, obj) + if out.Values[i] == graphql.RequiredNull { + out.Invalids++ + } + case "error": + out.Values[i] = ec._DataframeMaterialization_error(ctx, field, obj) + if out.Values[i] == graphql.RequiredNull { + out.Invalids++ + } + default: + panic("unknown field " + strconv.Quote(field.Name)) + } + } + out.Dispatch(ctx) + if out.Invalids > 0 { + return graphql.Null + } + + atomic.AddInt32(&ec.Deferred, int32(min(len(deferLabelToView), math.MaxInt32))) + + ec.ProcessDeferredGroup(graphql.DeferredGroup{ + Defers: deferLabelToView, + Path: graphql.GetPath(ctx), + FieldSet: deferredFieldSet, + Context: ctx, + }) + + return out +} + +var dataframePageInfoImplementors = []string{"DataframePageInfo"} + +func (ec *executionContext) _DataframePageInfo(ctx context.Context, sel ast.SelectionSet, obj *model.DataframePageInfo) graphql.Marshaler { + fields := graphql.CollectFields(ec.OperationContext, sel, dataframePageInfoImplementors) + + out := graphql.NewFieldSet(fields) + deferredFieldSet := graphql.NewFieldSet(nil) + deferLabelToView := make(map[string]*graphql.FieldSetView) + for i, field := range fields { + switch field.Name { + case "__typename": + out.Values[i] = graphql.MarshalString("DataframePageInfo") + case "hasNextPage": + out.Values[i] = ec._DataframePageInfo_hasNextPage(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ + } + case "endCursor": + out.Values[i] = ec._DataframePageInfo_endCursor(ctx, field, obj) + if out.Values[i] == graphql.RequiredNull { + out.Invalids++ + } + default: + panic("unknown field " + strconv.Quote(field.Name)) + } + } + out.Dispatch(ctx) + if out.Invalids > 0 { + return graphql.Null + } + + atomic.AddInt32(&ec.Deferred, int32(min(len(deferLabelToView), math.MaxInt32))) + + ec.ProcessDeferredGroup(graphql.DeferredGroup{ + Defers: deferLabelToView, + Path: graphql.GetPath(ctx), + FieldSet: deferredFieldSet, + Context: ctx, + }) + + return out +} + +var dataframeRowConnectionImplementors = []string{"DataframeRowConnection"} + +func (ec *executionContext) _DataframeRowConnection(ctx context.Context, sel ast.SelectionSet, obj *model.DataframeRowConnection) graphql.Marshaler { + fields := graphql.CollectFields(ec.OperationContext, sel, dataframeRowConnectionImplementors) + + out := graphql.NewFieldSet(fields) + deferredFieldSet := graphql.NewFieldSet(nil) + deferLabelToView := make(map[string]*graphql.FieldSetView) + for i, field := range fields { + switch field.Name { + case "__typename": + out.Values[i] = graphql.MarshalString("DataframeRowConnection") + case "materialization": + out.Values[i] = ec._DataframeRowConnection_materialization(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ + } + case "columns": + out.Values[i] = ec._DataframeRowConnection_columns(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ + } + case "rows": + out.Values[i] = ec._DataframeRowConnection_rows(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ + } + case "totalCount": + out.Values[i] = ec._DataframeRowConnection_totalCount(ctx, field, obj) + if out.Values[i] == graphql.RequiredNull { + out.Invalids++ + } + case "pageInfo": + out.Values[i] = ec._DataframeRowConnection_pageInfo(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ + } + default: + panic("unknown field " + strconv.Quote(field.Name)) + } + } + out.Dispatch(ctx) + if out.Invalids > 0 { + return graphql.Null + } + + atomic.AddInt32(&ec.Deferred, int32(min(len(deferLabelToView), math.MaxInt32))) + + ec.ProcessDeferredGroup(graphql.DeferredGroup{ + Defers: deferLabelToView, + Path: graphql.GetPath(ctx), + FieldSet: deferredFieldSet, + Context: ctx, + }) + + return out +} + +var queryImplementors = []string{"Query"} + +func (ec *executionContext) _Query(ctx context.Context, sel ast.SelectionSet) graphql.Marshaler { + fields := graphql.CollectFields(ec.OperationContext, sel, queryImplementors) + ctx = graphql.WithFieldContext(ctx, &graphql.FieldContext{ + Object: "Query", + }) + + out := graphql.NewFieldSet(fields) + deferredFieldSet := graphql.NewFieldSet(nil) + deferLabelToView := make(map[string]*graphql.FieldSetView) + for i, field := range fields { + innerCtx := graphql.WithRootFieldContext(ctx, &graphql.RootFieldContext{ + Object: field.Name, + Field: field, + }) + + switch field.Name { + case "__typename": + out.Values[i] = graphql.MarshalString("Query") + case "dataframeDatasets": + field := field + + innerFunc := func(ctx context.Context, fs *graphql.FieldSet) (res graphql.Marshaler) { + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + } + }() + res = ec._Query_dataframeDatasets(ctx, field) + if res == graphql.Null { + atomic.AddUint32(&fs.Invalids, 1) + } + return res + } + + rrm := func(ctx context.Context) graphql.Marshaler { + return ec.OperationContext.RootResolverMiddleware(ctx, + func(ctx context.Context) graphql.Marshaler { return innerFunc(ctx, out) }) + } + + out.Concurrently(i, func(ctx context.Context) graphql.Marshaler { return rrm(innerCtx) }) + case "dataframeDataset": + field := field + + innerFunc := func(ctx context.Context, fs *graphql.FieldSet) (res graphql.Marshaler) { + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + } + }() + res = ec._Query_dataframeDataset(ctx, field) + if res == graphql.RequiredNull { + atomic.AddUint32(&fs.Invalids, 1) + } + return res + } + + rrm := func(ctx context.Context) graphql.Marshaler { + return ec.OperationContext.RootResolverMiddleware(ctx, + func(ctx context.Context) graphql.Marshaler { return innerFunc(ctx, out) }) + } + + out.Concurrently(i, func(ctx context.Context) graphql.Marshaler { return rrm(innerCtx) }) + case "dataframeRows": + field := field + + innerFunc := func(ctx context.Context, fs *graphql.FieldSet) (res graphql.Marshaler) { + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + } + }() + res = ec._Query_dataframeRows(ctx, field) + if res == graphql.Null { + atomic.AddUint32(&fs.Invalids, 1) + } + return res + } + + rrm := func(ctx context.Context) graphql.Marshaler { + return ec.OperationContext.RootResolverMiddleware(ctx, + func(ctx context.Context) graphql.Marshaler { return innerFunc(ctx, out) }) + } + + out.Concurrently(i, func(ctx context.Context) graphql.Marshaler { return rrm(innerCtx) }) + case "dataframeAggregate": + field := field + + innerFunc := func(ctx context.Context, fs *graphql.FieldSet) (res graphql.Marshaler) { + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + } + }() + res = ec._Query_dataframeAggregate(ctx, field) + if res == graphql.Null { + atomic.AddUint32(&fs.Invalids, 1) + } + return res + } + + rrm := func(ctx context.Context) graphql.Marshaler { + return ec.OperationContext.RootResolverMiddleware(ctx, + func(ctx context.Context) graphql.Marshaler { return innerFunc(ctx, out) }) + } + + out.Concurrently(i, func(ctx context.Context) graphql.Marshaler { return rrm(innerCtx) }) + case "__type": + out.Values[i] = ec.OperationContext.RootResolverMiddleware(innerCtx, func(ctx context.Context) (res graphql.Marshaler) { + return ec._Query___type(ctx, field) + }) + if out.Values[i] == graphql.RequiredNull { + atomic.AddUint32(&out.Invalids, 1) + } + case "__schema": + out.Values[i] = ec.OperationContext.RootResolverMiddleware(innerCtx, func(ctx context.Context) (res graphql.Marshaler) { + return ec._Query___schema(ctx, field) + }) + if out.Values[i] == graphql.RequiredNull { + atomic.AddUint32(&out.Invalids, 1) + } + default: + panic("unknown field " + strconv.Quote(field.Name)) + } + } + out.Dispatch(ctx) + if out.Invalids > 0 { + return graphql.Null + } + + atomic.AddInt32(&ec.Deferred, int32(min(len(deferLabelToView), math.MaxInt32))) + + ec.ProcessDeferredGroup(graphql.DeferredGroup{ + Defers: deferLabelToView, + Path: graphql.GetPath(ctx), + FieldSet: deferredFieldSet, + Context: ctx, + }) + + return out +} + +var __DirectiveImplementors = []string{"__Directive"} + +func (ec *executionContext) ___Directive(ctx context.Context, sel ast.SelectionSet, obj *introspection.Directive) graphql.Marshaler { + fields := graphql.CollectFields(ec.OperationContext, sel, __DirectiveImplementors) + + out := graphql.NewFieldSet(fields) + deferredFieldSet := graphql.NewFieldSet(nil) + deferLabelToView := make(map[string]*graphql.FieldSetView) + for i, field := range fields { + switch field.Name { + case "__typename": + out.Values[i] = graphql.MarshalString("__Directive") + case "name": + out.Values[i] = ec.___Directive_name(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ + } + case "description": + out.Values[i] = ec.___Directive_description(ctx, field, obj) + if out.Values[i] == graphql.RequiredNull { + out.Invalids++ + } + case "isRepeatable": + out.Values[i] = ec.___Directive_isRepeatable(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ + } + case "locations": + out.Values[i] = ec.___Directive_locations(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ + } + case "args": + out.Values[i] = ec.___Directive_args(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ + } + default: + panic("unknown field " + strconv.Quote(field.Name)) + } + } + out.Dispatch(ctx) + if out.Invalids > 0 { + return graphql.Null + } + + atomic.AddInt32(&ec.Deferred, int32(min(len(deferLabelToView), math.MaxInt32))) + + ec.ProcessDeferredGroup(graphql.DeferredGroup{ + Defers: deferLabelToView, + Path: graphql.GetPath(ctx), + FieldSet: deferredFieldSet, + Context: ctx, + }) + + return out +} + +var __EnumValueImplementors = []string{"__EnumValue"} + +func (ec *executionContext) ___EnumValue(ctx context.Context, sel ast.SelectionSet, obj *introspection.EnumValue) graphql.Marshaler { + fields := graphql.CollectFields(ec.OperationContext, sel, __EnumValueImplementors) + + out := graphql.NewFieldSet(fields) + deferredFieldSet := graphql.NewFieldSet(nil) + deferLabelToView := make(map[string]*graphql.FieldSetView) + for i, field := range fields { + switch field.Name { + case "__typename": + out.Values[i] = graphql.MarshalString("__EnumValue") + case "name": + out.Values[i] = ec.___EnumValue_name(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ + } + case "description": + out.Values[i] = ec.___EnumValue_description(ctx, field, obj) + if out.Values[i] == graphql.RequiredNull { + out.Invalids++ + } + case "isDeprecated": + out.Values[i] = ec.___EnumValue_isDeprecated(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ + } + case "deprecationReason": + out.Values[i] = ec.___EnumValue_deprecationReason(ctx, field, obj) + if out.Values[i] == graphql.RequiredNull { + out.Invalids++ + } + default: + panic("unknown field " + strconv.Quote(field.Name)) + } + } + out.Dispatch(ctx) + if out.Invalids > 0 { + return graphql.Null + } + + atomic.AddInt32(&ec.Deferred, int32(min(len(deferLabelToView), math.MaxInt32))) + + ec.ProcessDeferredGroup(graphql.DeferredGroup{ + Defers: deferLabelToView, + Path: graphql.GetPath(ctx), + FieldSet: deferredFieldSet, + Context: ctx, + }) + + return out +} + +var __FieldImplementors = []string{"__Field"} + +func (ec *executionContext) ___Field(ctx context.Context, sel ast.SelectionSet, obj *introspection.Field) graphql.Marshaler { + fields := graphql.CollectFields(ec.OperationContext, sel, __FieldImplementors) + + out := graphql.NewFieldSet(fields) + deferredFieldSet := graphql.NewFieldSet(nil) + deferLabelToView := make(map[string]*graphql.FieldSetView) + for i, field := range fields { + switch field.Name { + case "__typename": + out.Values[i] = graphql.MarshalString("__Field") + case "name": + out.Values[i] = ec.___Field_name(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ + } + case "description": + out.Values[i] = ec.___Field_description(ctx, field, obj) + if out.Values[i] == graphql.RequiredNull { + out.Invalids++ + } + case "args": + out.Values[i] = ec.___Field_args(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ + } + case "type": + out.Values[i] = ec.___Field_type(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ + } + case "isDeprecated": + out.Values[i] = ec.___Field_isDeprecated(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ + } + case "deprecationReason": + out.Values[i] = ec.___Field_deprecationReason(ctx, field, obj) + if out.Values[i] == graphql.RequiredNull { + out.Invalids++ + } + default: + panic("unknown field " + strconv.Quote(field.Name)) + } + } + out.Dispatch(ctx) + if out.Invalids > 0 { + return graphql.Null + } + + atomic.AddInt32(&ec.Deferred, int32(min(len(deferLabelToView), math.MaxInt32))) + + ec.ProcessDeferredGroup(graphql.DeferredGroup{ + Defers: deferLabelToView, + Path: graphql.GetPath(ctx), + FieldSet: deferredFieldSet, + Context: ctx, + }) + + return out +} + +var __InputValueImplementors = []string{"__InputValue"} + +func (ec *executionContext) ___InputValue(ctx context.Context, sel ast.SelectionSet, obj *introspection.InputValue) graphql.Marshaler { + fields := graphql.CollectFields(ec.OperationContext, sel, __InputValueImplementors) + + out := graphql.NewFieldSet(fields) + deferredFieldSet := graphql.NewFieldSet(nil) + deferLabelToView := make(map[string]*graphql.FieldSetView) + for i, field := range fields { + switch field.Name { + case "__typename": + out.Values[i] = graphql.MarshalString("__InputValue") + case "name": + out.Values[i] = ec.___InputValue_name(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ + } + case "description": + out.Values[i] = ec.___InputValue_description(ctx, field, obj) + if out.Values[i] == graphql.RequiredNull { + out.Invalids++ + } + case "type": + out.Values[i] = ec.___InputValue_type(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ + } + case "defaultValue": + out.Values[i] = ec.___InputValue_defaultValue(ctx, field, obj) + if out.Values[i] == graphql.RequiredNull { + out.Invalids++ + } + case "isDeprecated": + out.Values[i] = ec.___InputValue_isDeprecated(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ + } + case "deprecationReason": + out.Values[i] = ec.___InputValue_deprecationReason(ctx, field, obj) + if out.Values[i] == graphql.RequiredNull { + out.Invalids++ + } + default: + panic("unknown field " + strconv.Quote(field.Name)) + } + } + out.Dispatch(ctx) + if out.Invalids > 0 { + return graphql.Null + } + + atomic.AddInt32(&ec.Deferred, int32(min(len(deferLabelToView), math.MaxInt32))) + + ec.ProcessDeferredGroup(graphql.DeferredGroup{ + Defers: deferLabelToView, + Path: graphql.GetPath(ctx), + FieldSet: deferredFieldSet, + Context: ctx, + }) + + return out +} + +var __SchemaImplementors = []string{"__Schema"} + +func (ec *executionContext) ___Schema(ctx context.Context, sel ast.SelectionSet, obj *introspection.Schema) graphql.Marshaler { + fields := graphql.CollectFields(ec.OperationContext, sel, __SchemaImplementors) + + out := graphql.NewFieldSet(fields) + deferredFieldSet := graphql.NewFieldSet(nil) + deferLabelToView := make(map[string]*graphql.FieldSetView) + for i, field := range fields { + switch field.Name { + case "__typename": + out.Values[i] = graphql.MarshalString("__Schema") + case "description": + out.Values[i] = ec.___Schema_description(ctx, field, obj) + if out.Values[i] == graphql.RequiredNull { + out.Invalids++ + } + case "types": + out.Values[i] = ec.___Schema_types(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ + } + case "queryType": + out.Values[i] = ec.___Schema_queryType(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ + } + case "mutationType": + out.Values[i] = ec.___Schema_mutationType(ctx, field, obj) + if out.Values[i] == graphql.RequiredNull { + out.Invalids++ + } + case "subscriptionType": + out.Values[i] = ec.___Schema_subscriptionType(ctx, field, obj) + if out.Values[i] == graphql.RequiredNull { + out.Invalids++ + } + case "directives": + out.Values[i] = ec.___Schema_directives(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ + } + default: + panic("unknown field " + strconv.Quote(field.Name)) + } + } + out.Dispatch(ctx) + if out.Invalids > 0 { + return graphql.Null + } + + atomic.AddInt32(&ec.Deferred, int32(min(len(deferLabelToView), math.MaxInt32))) + + ec.ProcessDeferredGroup(graphql.DeferredGroup{ + Defers: deferLabelToView, + Path: graphql.GetPath(ctx), + FieldSet: deferredFieldSet, + Context: ctx, + }) + + return out +} + +var __TypeImplementors = []string{"__Type"} + +func (ec *executionContext) ___Type(ctx context.Context, sel ast.SelectionSet, obj *introspection.Type) graphql.Marshaler { + fields := graphql.CollectFields(ec.OperationContext, sel, __TypeImplementors) + + out := graphql.NewFieldSet(fields) + deferredFieldSet := graphql.NewFieldSet(nil) + deferLabelToView := make(map[string]*graphql.FieldSetView) + for i, field := range fields { + switch field.Name { + case "__typename": + out.Values[i] = graphql.MarshalString("__Type") + case "kind": + out.Values[i] = ec.___Type_kind(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ + } + case "name": + out.Values[i] = ec.___Type_name(ctx, field, obj) + if out.Values[i] == graphql.RequiredNull { + out.Invalids++ + } + case "description": + out.Values[i] = ec.___Type_description(ctx, field, obj) + if out.Values[i] == graphql.RequiredNull { + out.Invalids++ + } + case "specifiedByURL": + out.Values[i] = ec.___Type_specifiedByURL(ctx, field, obj) + if out.Values[i] == graphql.RequiredNull { + out.Invalids++ + } + case "fields": + out.Values[i] = ec.___Type_fields(ctx, field, obj) + if out.Values[i] == graphql.RequiredNull { + out.Invalids++ + } + case "interfaces": + out.Values[i] = ec.___Type_interfaces(ctx, field, obj) + if out.Values[i] == graphql.RequiredNull { + out.Invalids++ + } + case "possibleTypes": + out.Values[i] = ec.___Type_possibleTypes(ctx, field, obj) + if out.Values[i] == graphql.RequiredNull { + out.Invalids++ + } + case "enumValues": + out.Values[i] = ec.___Type_enumValues(ctx, field, obj) + if out.Values[i] == graphql.RequiredNull { + out.Invalids++ + } + case "inputFields": + out.Values[i] = ec.___Type_inputFields(ctx, field, obj) + if out.Values[i] == graphql.RequiredNull { + out.Invalids++ + } + case "ofType": + out.Values[i] = ec.___Type_ofType(ctx, field, obj) + if out.Values[i] == graphql.RequiredNull { + out.Invalids++ + } + case "isOneOf": + out.Values[i] = ec.___Type_isOneOf(ctx, field, obj) + if out.Values[i] == graphql.RequiredNull { + out.Invalids++ + } + default: + panic("unknown field " + strconv.Quote(field.Name)) + } + } + out.Dispatch(ctx) + if out.Invalids > 0 { + return graphql.Null + } + + atomic.AddInt32(&ec.Deferred, int32(min(len(deferLabelToView), math.MaxInt32))) + + ec.ProcessDeferredGroup(graphql.DeferredGroup{ + Defers: deferLabelToView, + Path: graphql.GetPath(ctx), + FieldSet: deferredFieldSet, + Context: ctx, + }) + + return out +} + +// endregion **************************** object.gotpl **************************** + +// region ***************************** type.gotpl ***************************** + +func (ec *executionContext) unmarshalNBoolean2bool(ctx context.Context, v any) (bool, error) { + res, err := graphql.UnmarshalBoolean(v) + return res, graphql.ErrorOnPath(ctx, err) +} + +func (ec *executionContext) marshalNBoolean2bool(ctx context.Context, sel ast.SelectionSet, v bool) graphql.Marshaler { + _ = sel + res := graphql.MarshalBoolean(v) + if res == graphql.Null { + if !graphql.HasFieldError(ctx, graphql.GetFieldContext(ctx)) { + graphql.AddErrorf(ctx, "the requested element is null which the schema does not allow") + } + } + return res +} + +func (ec *executionContext) unmarshalNDataframeAggregateInput2githubᚗcomᚋcalyprᚋloomᚋgraphqlapiᚋmodelᚐDataframeAggregateInput(ctx context.Context, v any) (model.DataframeAggregateInput, error) { + res, err := ec.unmarshalInputDataframeAggregateInput(ctx, v) + return res, graphql.ErrorOnPath(ctx, err) +} + +func (ec *executionContext) marshalNDataframeAggregateResult2githubᚗcomᚋcalyprᚋloomᚋgraphqlapiᚋmodelᚐDataframeAggregateResult(ctx context.Context, sel ast.SelectionSet, v model.DataframeAggregateResult) graphql.Marshaler { + return ec._DataframeAggregateResult(ctx, sel, &v) +} + +func (ec *executionContext) marshalNDataframeAggregateResult2ᚖgithubᚗcomᚋcalyprᚋloomᚋgraphqlapiᚋmodelᚐDataframeAggregateResult(ctx context.Context, sel ast.SelectionSet, v *model.DataframeAggregateResult) graphql.Marshaler { + if v == nil { + if !graphql.HasFieldError(ctx, graphql.GetFieldContext(ctx)) { + graphql.AddErrorf(ctx, "the requested element is null which the schema does not allow") + } + return graphql.Null + } + return ec._DataframeAggregateResult(ctx, sel, v) +} + +func (ec *executionContext) marshalNDataframeColumn2ᚕᚖgithubᚗcomᚋcalyprᚋloomᚋgraphqlapiᚋmodelᚐDataframeColumnᚄ(ctx context.Context, sel ast.SelectionSet, v []*model.DataframeColumn) graphql.Marshaler { + ret := graphql.MarshalSliceConcurrently(ctx, len(v), 0, false, func(ctx context.Context, i int) graphql.Marshaler { + fc := graphql.GetFieldContext(ctx) + fc.Result = &v[i] + return ec.marshalNDataframeColumn2ᚖgithubᚗcomᚋcalyprᚋloomᚋgraphqlapiᚋmodelᚐDataframeColumn(ctx, sel, v[i]) + }) + + for _, e := range ret { + if e == graphql.Null { + return graphql.Null + } + } + + return ret +} + +func (ec *executionContext) marshalNDataframeColumn2ᚖgithubᚗcomᚋcalyprᚋloomᚋgraphqlapiᚋmodelᚐDataframeColumn(ctx context.Context, sel ast.SelectionSet, v *model.DataframeColumn) graphql.Marshaler { + if v == nil { + if !graphql.HasFieldError(ctx, graphql.GetFieldContext(ctx)) { + graphql.AddErrorf(ctx, "the requested element is null which the schema does not allow") + } + return graphql.Null + } + return ec._DataframeColumn(ctx, sel, v) +} + +func (ec *executionContext) unmarshalNDataframeDatasetInput2githubᚗcomᚋcalyprᚋloomᚋgraphqlapiᚋmodelᚐDataframeDatasetInput(ctx context.Context, v any) (model.DataframeDatasetInput, error) { + res, err := ec.unmarshalInputDataframeDatasetInput(ctx, v) + return res, graphql.ErrorOnPath(ctx, err) +} + +func (ec *executionContext) unmarshalNDataframeFilterInput2ᚖgithubᚗcomᚋcalyprᚋloomᚋgraphqlapiᚋmodelᚐDataframeFilterInput(ctx context.Context, v any) (*model.DataframeFilterInput, error) { + res, err := ec.unmarshalInputDataframeFilterInput(ctx, v) + return &res, graphql.ErrorOnPath(ctx, err) +} + +func (ec *executionContext) marshalNDataframeMaterialization2ᚕᚖgithubᚗcomᚋcalyprᚋloomᚋgraphqlapiᚋmodelᚐDataframeMaterializationᚄ(ctx context.Context, sel ast.SelectionSet, v []*model.DataframeMaterialization) graphql.Marshaler { + ret := graphql.MarshalSliceConcurrently(ctx, len(v), 0, false, func(ctx context.Context, i int) graphql.Marshaler { + fc := graphql.GetFieldContext(ctx) + fc.Result = &v[i] + return ec.marshalNDataframeMaterialization2ᚖgithubᚗcomᚋcalyprᚋloomᚋgraphqlapiᚋmodelᚐDataframeMaterialization(ctx, sel, v[i]) + }) + + for _, e := range ret { + if e == graphql.Null { + return graphql.Null + } + } + + return ret +} + +func (ec *executionContext) marshalNDataframeMaterialization2ᚖgithubᚗcomᚋcalyprᚋloomᚋgraphqlapiᚋmodelᚐDataframeMaterialization(ctx context.Context, sel ast.SelectionSet, v *model.DataframeMaterialization) graphql.Marshaler { + if v == nil { + if !graphql.HasFieldError(ctx, graphql.GetFieldContext(ctx)) { + graphql.AddErrorf(ctx, "the requested element is null which the schema does not allow") + } + return graphql.Null + } + return ec._DataframeMaterialization(ctx, sel, v) +} + +func (ec *executionContext) unmarshalNDataframeMaterializationState2githubᚗcomᚋcalyprᚋloomᚋgraphqlapiᚋmodelᚐDataframeMaterializationState(ctx context.Context, v any) (model.DataframeMaterializationState, error) { + var res model.DataframeMaterializationState + err := res.UnmarshalGQL(v) + return res, graphql.ErrorOnPath(ctx, err) +} + +func (ec *executionContext) marshalNDataframeMaterializationState2githubᚗcomᚋcalyprᚋloomᚋgraphqlapiᚋmodelᚐDataframeMaterializationState(ctx context.Context, sel ast.SelectionSet, v model.DataframeMaterializationState) graphql.Marshaler { + return v +} + +func (ec *executionContext) marshalNDataframePageInfo2ᚖgithubᚗcomᚋcalyprᚋloomᚋgraphqlapiᚋmodelᚐDataframePageInfo(ctx context.Context, sel ast.SelectionSet, v *model.DataframePageInfo) graphql.Marshaler { + if v == nil { + if !graphql.HasFieldError(ctx, graphql.GetFieldContext(ctx)) { + graphql.AddErrorf(ctx, "the requested element is null which the schema does not allow") + } + return graphql.Null + } + return ec._DataframePageInfo(ctx, sel, v) +} + +func (ec *executionContext) marshalNDataframeRowConnection2githubᚗcomᚋcalyprᚋloomᚋgraphqlapiᚋmodelᚐDataframeRowConnection(ctx context.Context, sel ast.SelectionSet, v model.DataframeRowConnection) graphql.Marshaler { + return ec._DataframeRowConnection(ctx, sel, &v) +} + +func (ec *executionContext) marshalNDataframeRowConnection2ᚖgithubᚗcomᚋcalyprᚋloomᚋgraphqlapiᚋmodelᚐDataframeRowConnection(ctx context.Context, sel ast.SelectionSet, v *model.DataframeRowConnection) graphql.Marshaler { + if v == nil { + if !graphql.HasFieldError(ctx, graphql.GetFieldContext(ctx)) { + graphql.AddErrorf(ctx, "the requested element is null which the schema does not allow") + } + return graphql.Null + } + return ec._DataframeRowConnection(ctx, sel, v) +} + +func (ec *executionContext) unmarshalNDataframeRowsInput2githubᚗcomᚋcalyprᚋloomᚋgraphqlapiᚋmodelᚐDataframeRowsInput(ctx context.Context, v any) (model.DataframeRowsInput, error) { + res, err := ec.unmarshalInputDataframeRowsInput(ctx, v) + return res, graphql.ErrorOnPath(ctx, err) +} + +func (ec *executionContext) unmarshalNID2string(ctx context.Context, v any) (string, error) { + res, err := graphql.UnmarshalID(v) + return res, graphql.ErrorOnPath(ctx, err) +} + +func (ec *executionContext) marshalNID2string(ctx context.Context, sel ast.SelectionSet, v string) graphql.Marshaler { + _ = sel + res := graphql.MarshalID(v) + if res == graphql.Null { + if !graphql.HasFieldError(ctx, graphql.GetFieldContext(ctx)) { + graphql.AddErrorf(ctx, "the requested element is null which the schema does not allow") + } + } + return res +} + +func (ec *executionContext) unmarshalNInt2int(ctx context.Context, v any) (int, error) { + res, err := graphql.UnmarshalInt(v) + return res, graphql.ErrorOnPath(ctx, err) +} + +func (ec *executionContext) marshalNInt2int(ctx context.Context, sel ast.SelectionSet, v int) graphql.Marshaler { + _ = sel + res := graphql.MarshalInt(v) + if res == graphql.Null { + if !graphql.HasFieldError(ctx, graphql.GetFieldContext(ctx)) { + graphql.AddErrorf(ctx, "the requested element is null which the schema does not allow") + } + } + return res +} + +func (ec *executionContext) unmarshalNJSON2encodingᚋjsonᚐRawMessage(ctx context.Context, v any) (json.RawMessage, error) { + res, err := ec.unmarshalInputJSON(ctx, v) + return res, graphql.ErrorOnPath(ctx, err) +} + +func (ec *executionContext) marshalNJSON2encodingᚋjsonᚐRawMessage(ctx context.Context, sel ast.SelectionSet, v json.RawMessage) graphql.Marshaler { + if v == nil { + if !graphql.HasFieldError(ctx, graphql.GetFieldContext(ctx)) { + graphql.AddErrorf(ctx, "the requested element is null which the schema does not allow") + } + return graphql.Null + } + return ec._JSON(ctx, sel, v) +} + +func (ec *executionContext) unmarshalNString2string(ctx context.Context, v any) (string, error) { + res, err := graphql.UnmarshalString(v) + return res, graphql.ErrorOnPath(ctx, err) +} + +func (ec *executionContext) marshalNString2string(ctx context.Context, sel ast.SelectionSet, v string) graphql.Marshaler { + _ = sel + res := graphql.MarshalString(v) + if res == graphql.Null { + if !graphql.HasFieldError(ctx, graphql.GetFieldContext(ctx)) { + graphql.AddErrorf(ctx, "the requested element is null which the schema does not allow") + } + } + return res +} + +func (ec *executionContext) unmarshalNString2ᚕstringᚄ(ctx context.Context, v any) ([]string, error) { + vSlice := graphql.CoerceList(v) + var err error + res := make([]string, len(vSlice)) + for i := range vSlice { + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithIndex(i)) + res[i], err = ec.unmarshalNString2string(ctx, vSlice[i]) + if err != nil { + return nil, err + } + } + return res, nil +} + +func (ec *executionContext) marshalNString2ᚕstringᚄ(ctx context.Context, sel ast.SelectionSet, v []string) graphql.Marshaler { + ret := make(graphql.Array, len(v)) + for i := range v { + ret[i] = ec.marshalNString2string(ctx, sel, v[i]) + } + + for _, e := range ret { + if e == graphql.Null { + return graphql.Null + } + } + + return ret +} + +func (ec *executionContext) marshalN__Directive2githubᚗcomᚋ99designsᚋgqlgenᚋgraphqlᚋintrospectionᚐDirective(ctx context.Context, sel ast.SelectionSet, v introspection.Directive) graphql.Marshaler { + return ec.___Directive(ctx, sel, &v) +} + +func (ec *executionContext) marshalN__Directive2ᚕgithubᚗcomᚋ99designsᚋgqlgenᚋgraphqlᚋintrospectionᚐDirectiveᚄ(ctx context.Context, sel ast.SelectionSet, v []introspection.Directive) graphql.Marshaler { + ret := graphql.MarshalSliceConcurrently(ctx, len(v), 0, false, func(ctx context.Context, i int) graphql.Marshaler { + fc := graphql.GetFieldContext(ctx) + fc.Result = &v[i] + return ec.marshalN__Directive2githubᚗcomᚋ99designsᚋgqlgenᚋgraphqlᚋintrospectionᚐDirective(ctx, sel, v[i]) + }) + + for _, e := range ret { + if e == graphql.Null { + return graphql.Null + } + } + + return ret +} + +func (ec *executionContext) unmarshalN__DirectiveLocation2string(ctx context.Context, v any) (string, error) { + res, err := graphql.UnmarshalString(v) + return res, graphql.ErrorOnPath(ctx, err) +} + +func (ec *executionContext) marshalN__DirectiveLocation2string(ctx context.Context, sel ast.SelectionSet, v string) graphql.Marshaler { + _ = sel + res := graphql.MarshalString(v) + if res == graphql.Null { + if !graphql.HasFieldError(ctx, graphql.GetFieldContext(ctx)) { + graphql.AddErrorf(ctx, "the requested element is null which the schema does not allow") + } + } + return res +} + +func (ec *executionContext) unmarshalN__DirectiveLocation2ᚕstringᚄ(ctx context.Context, v any) ([]string, error) { + vSlice := graphql.CoerceList(v) + var err error + res := make([]string, len(vSlice)) + for i := range vSlice { + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithIndex(i)) + res[i], err = ec.unmarshalN__DirectiveLocation2string(ctx, vSlice[i]) + if err != nil { + return nil, err + } + } + return res, nil +} + +func (ec *executionContext) marshalN__DirectiveLocation2ᚕstringᚄ(ctx context.Context, sel ast.SelectionSet, v []string) graphql.Marshaler { + ret := graphql.MarshalSliceConcurrently(ctx, len(v), 0, false, func(ctx context.Context, i int) graphql.Marshaler { + fc := graphql.GetFieldContext(ctx) + fc.Result = &v[i] + return ec.marshalN__DirectiveLocation2string(ctx, sel, v[i]) + }) + + for _, e := range ret { + if e == graphql.Null { + return graphql.Null + } + } + + return ret +} + +func (ec *executionContext) marshalN__EnumValue2githubᚗcomᚋ99designsᚋgqlgenᚋgraphqlᚋintrospectionᚐEnumValue(ctx context.Context, sel ast.SelectionSet, v introspection.EnumValue) graphql.Marshaler { + return ec.___EnumValue(ctx, sel, &v) +} + +func (ec *executionContext) marshalN__Field2githubᚗcomᚋ99designsᚋgqlgenᚋgraphqlᚋintrospectionᚐField(ctx context.Context, sel ast.SelectionSet, v introspection.Field) graphql.Marshaler { + return ec.___Field(ctx, sel, &v) +} + +func (ec *executionContext) marshalN__InputValue2githubᚗcomᚋ99designsᚋgqlgenᚋgraphqlᚋintrospectionᚐInputValue(ctx context.Context, sel ast.SelectionSet, v introspection.InputValue) graphql.Marshaler { + return ec.___InputValue(ctx, sel, &v) +} + +func (ec *executionContext) marshalN__InputValue2ᚕgithubᚗcomᚋ99designsᚋgqlgenᚋgraphqlᚋintrospectionᚐInputValueᚄ(ctx context.Context, sel ast.SelectionSet, v []introspection.InputValue) graphql.Marshaler { + ret := graphql.MarshalSliceConcurrently(ctx, len(v), 0, false, func(ctx context.Context, i int) graphql.Marshaler { + fc := graphql.GetFieldContext(ctx) + fc.Result = &v[i] + return ec.marshalN__InputValue2githubᚗcomᚋ99designsᚋgqlgenᚋgraphqlᚋintrospectionᚐInputValue(ctx, sel, v[i]) + }) + + for _, e := range ret { + if e == graphql.Null { + return graphql.Null + } + } + + return ret +} + +func (ec *executionContext) marshalN__Type2githubᚗcomᚋ99designsᚋgqlgenᚋgraphqlᚋintrospectionᚐType(ctx context.Context, sel ast.SelectionSet, v introspection.Type) graphql.Marshaler { + return ec.___Type(ctx, sel, &v) +} + +func (ec *executionContext) marshalN__Type2ᚕgithubᚗcomᚋ99designsᚋgqlgenᚋgraphqlᚋintrospectionᚐTypeᚄ(ctx context.Context, sel ast.SelectionSet, v []introspection.Type) graphql.Marshaler { + ret := graphql.MarshalSliceConcurrently(ctx, len(v), 0, false, func(ctx context.Context, i int) graphql.Marshaler { + fc := graphql.GetFieldContext(ctx) + fc.Result = &v[i] + return ec.marshalN__Type2githubᚗcomᚋ99designsᚋgqlgenᚋgraphqlᚋintrospectionᚐType(ctx, sel, v[i]) + }) + + for _, e := range ret { + if e == graphql.Null { + return graphql.Null + } + } + + return ret +} + +func (ec *executionContext) marshalN__Type2ᚖgithubᚗcomᚋ99designsᚋgqlgenᚋgraphqlᚋintrospectionᚐType(ctx context.Context, sel ast.SelectionSet, v *introspection.Type) graphql.Marshaler { + if v == nil { + if !graphql.HasFieldError(ctx, graphql.GetFieldContext(ctx)) { + graphql.AddErrorf(ctx, "the requested element is null which the schema does not allow") + } + return graphql.Null + } + return ec.___Type(ctx, sel, v) +} + +func (ec *executionContext) unmarshalN__TypeKind2string(ctx context.Context, v any) (string, error) { + res, err := graphql.UnmarshalString(v) + return res, graphql.ErrorOnPath(ctx, err) +} + +func (ec *executionContext) marshalN__TypeKind2string(ctx context.Context, sel ast.SelectionSet, v string) graphql.Marshaler { + _ = sel + res := graphql.MarshalString(v) + if res == graphql.Null { + if !graphql.HasFieldError(ctx, graphql.GetFieldContext(ctx)) { + graphql.AddErrorf(ctx, "the requested element is null which the schema does not allow") + } + } + return res +} + +func (ec *executionContext) unmarshalOBoolean2bool(ctx context.Context, v any) (bool, error) { + res, err := graphql.UnmarshalBoolean(v) + return res, graphql.ErrorOnPath(ctx, err) +} + +func (ec *executionContext) marshalOBoolean2bool(ctx context.Context, sel ast.SelectionSet, v bool) graphql.Marshaler { + _ = sel + _ = ctx + res := graphql.MarshalBoolean(v) + return res +} + +func (ec *executionContext) unmarshalOBoolean2ᚖbool(ctx context.Context, v any) (*bool, error) { + if v == nil { + return nil, nil + } + res, err := graphql.UnmarshalBoolean(v) + return &res, graphql.ErrorOnPath(ctx, err) +} + +func (ec *executionContext) marshalOBoolean2ᚖbool(ctx context.Context, sel ast.SelectionSet, v *bool) graphql.Marshaler { + if v == nil { + return graphql.Null + } + _ = sel + _ = ctx + res := graphql.MarshalBoolean(*v) + return res +} + +func (ec *executionContext) unmarshalODataframeFilterInput2ᚕᚖgithubᚗcomᚋcalyprᚋloomᚋgraphqlapiᚋmodelᚐDataframeFilterInputᚄ(ctx context.Context, v any) ([]*model.DataframeFilterInput, error) { + if v == nil { + return nil, nil + } + vSlice := graphql.CoerceList(v) + var err error + res := make([]*model.DataframeFilterInput, len(vSlice)) + for i := range vSlice { + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithIndex(i)) + res[i], err = ec.unmarshalNDataframeFilterInput2ᚖgithubᚗcomᚋcalyprᚋloomᚋgraphqlapiᚋmodelᚐDataframeFilterInput(ctx, vSlice[i]) + if err != nil { + return nil, err + } + } + return res, nil +} + +func (ec *executionContext) marshalODataframeMaterialization2ᚖgithubᚗcomᚋcalyprᚋloomᚋgraphqlapiᚋmodelᚐDataframeMaterialization(ctx context.Context, sel ast.SelectionSet, v *model.DataframeMaterialization) graphql.Marshaler { + if v == nil { + return graphql.Null + } + return ec._DataframeMaterialization(ctx, sel, v) +} + +func (ec *executionContext) unmarshalODataframeSortInput2ᚖgithubᚗcomᚋcalyprᚋloomᚋgraphqlapiᚋmodelᚐDataframeSortInput(ctx context.Context, v any) (*model.DataframeSortInput, error) { + if v == nil { + return nil, nil + } + res, err := ec.unmarshalInputDataframeSortInput(ctx, v) + return &res, graphql.ErrorOnPath(ctx, err) +} + +func (ec *executionContext) unmarshalOInt2ᚖint(ctx context.Context, v any) (*int, error) { + if v == nil { + return nil, nil + } + res, err := graphql.UnmarshalInt(v) + return &res, graphql.ErrorOnPath(ctx, err) +} + +func (ec *executionContext) marshalOInt2ᚖint(ctx context.Context, sel ast.SelectionSet, v *int) graphql.Marshaler { + if v == nil { + return graphql.Null + } + _ = sel + _ = ctx + res := graphql.MarshalInt(*v) + return res +} + +func (ec *executionContext) unmarshalOString2ᚕstringᚄ(ctx context.Context, v any) ([]string, error) { + if v == nil { + return nil, nil + } + vSlice := graphql.CoerceList(v) + var err error + res := make([]string, len(vSlice)) + for i := range vSlice { + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithIndex(i)) + res[i], err = ec.unmarshalNString2string(ctx, vSlice[i]) + if err != nil { + return nil, err + } + } + return res, nil +} + +func (ec *executionContext) marshalOString2ᚕstringᚄ(ctx context.Context, sel ast.SelectionSet, v []string) graphql.Marshaler { + if v == nil { + return graphql.Null + } + ret := make(graphql.Array, len(v)) + for i := range v { + ret[i] = ec.marshalNString2string(ctx, sel, v[i]) + } + + for _, e := range ret { + if e == graphql.Null { + return graphql.Null + } + } + + return ret +} + +func (ec *executionContext) unmarshalOString2ᚖstring(ctx context.Context, v any) (*string, error) { + if v == nil { + return nil, nil + } + res, err := graphql.UnmarshalString(v) + return &res, graphql.ErrorOnPath(ctx, err) +} + +func (ec *executionContext) marshalOString2ᚖstring(ctx context.Context, sel ast.SelectionSet, v *string) graphql.Marshaler { + if v == nil { + return graphql.Null + } + _ = sel + _ = ctx + res := graphql.MarshalString(*v) + return res +} + +func (ec *executionContext) marshalO__EnumValue2ᚕgithubᚗcomᚋ99designsᚋgqlgenᚋgraphqlᚋintrospectionᚐEnumValueᚄ(ctx context.Context, sel ast.SelectionSet, v []introspection.EnumValue) graphql.Marshaler { + if v == nil { + return graphql.Null + } + ret := graphql.MarshalSliceConcurrently(ctx, len(v), 0, false, func(ctx context.Context, i int) graphql.Marshaler { + fc := graphql.GetFieldContext(ctx) + fc.Result = &v[i] + return ec.marshalN__EnumValue2githubᚗcomᚋ99designsᚋgqlgenᚋgraphqlᚋintrospectionᚐEnumValue(ctx, sel, v[i]) + }) + + for _, e := range ret { + if e == graphql.Null { + return graphql.Null + } + } + + return ret +} + +func (ec *executionContext) marshalO__Field2ᚕgithubᚗcomᚋ99designsᚋgqlgenᚋgraphqlᚋintrospectionᚐFieldᚄ(ctx context.Context, sel ast.SelectionSet, v []introspection.Field) graphql.Marshaler { + if v == nil { + return graphql.Null + } + ret := graphql.MarshalSliceConcurrently(ctx, len(v), 0, false, func(ctx context.Context, i int) graphql.Marshaler { + fc := graphql.GetFieldContext(ctx) + fc.Result = &v[i] + return ec.marshalN__Field2githubᚗcomᚋ99designsᚋgqlgenᚋgraphqlᚋintrospectionᚐField(ctx, sel, v[i]) + }) + + for _, e := range ret { + if e == graphql.Null { + return graphql.Null + } + } + + return ret +} + +func (ec *executionContext) marshalO__InputValue2ᚕgithubᚗcomᚋ99designsᚋgqlgenᚋgraphqlᚋintrospectionᚐInputValueᚄ(ctx context.Context, sel ast.SelectionSet, v []introspection.InputValue) graphql.Marshaler { + if v == nil { + return graphql.Null + } + ret := graphql.MarshalSliceConcurrently(ctx, len(v), 0, false, func(ctx context.Context, i int) graphql.Marshaler { + fc := graphql.GetFieldContext(ctx) + fc.Result = &v[i] + return ec.marshalN__InputValue2githubᚗcomᚋ99designsᚋgqlgenᚋgraphqlᚋintrospectionᚐInputValue(ctx, sel, v[i]) + }) + + for _, e := range ret { + if e == graphql.Null { + return graphql.Null + } + } + + return ret +} + +func (ec *executionContext) marshalO__Schema2ᚖgithubᚗcomᚋ99designsᚋgqlgenᚋgraphqlᚋintrospectionᚐSchema(ctx context.Context, sel ast.SelectionSet, v *introspection.Schema) graphql.Marshaler { + if v == nil { + return graphql.Null + } + return ec.___Schema(ctx, sel, v) +} + +func (ec *executionContext) marshalO__Type2ᚕgithubᚗcomᚋ99designsᚋgqlgenᚋgraphqlᚋintrospectionᚐTypeᚄ(ctx context.Context, sel ast.SelectionSet, v []introspection.Type) graphql.Marshaler { + if v == nil { + return graphql.Null + } + ret := graphql.MarshalSliceConcurrently(ctx, len(v), 0, false, func(ctx context.Context, i int) graphql.Marshaler { + fc := graphql.GetFieldContext(ctx) + fc.Result = &v[i] + return ec.marshalN__Type2githubᚗcomᚋ99designsᚋgqlgenᚋgraphqlᚋintrospectionᚐType(ctx, sel, v[i]) + }) + + for _, e := range ret { + if e == graphql.Null { + return graphql.Null + } + } + + return ret +} + +func (ec *executionContext) marshalO__Type2ᚖgithubᚗcomᚋ99designsᚋgqlgenᚋgraphqlᚋintrospectionᚐType(ctx context.Context, sel ast.SelectionSet, v *introspection.Type) graphql.Marshaler { + if v == nil { + return graphql.Null + } + return ec.___Type(ctx, sel, v) +} + +// endregion ***************************** type.gotpl ***************************** diff --git a/graphqlapi/clickhouse/gqlgen.yml b/graphqlapi/clickhouse/gqlgen.yml new file mode 100644 index 0000000..9f6e5fd --- /dev/null +++ b/graphqlapi/clickhouse/gqlgen.yml @@ -0,0 +1,56 @@ +schema: + - graphqlapi/clickhouse/schema.graphqls + +exec: + filename: graphqlapi/clickhouse/generated.go + package: clickhouse + +model: + filename: graphqlapi/clickhouse/model.go + package: clickhouse + +resolver: + layout: follow-schema + dir: graphqlapi/clickhouse + package: clickhouse + +skip_mod_tidy: true +skip_validation: true + +models: + JSON: + model: + - encoding/json.RawMessage + DataframeMaterialization: + model: + - github.com/calypr/loom/graphqlapi/model.DataframeMaterialization + DataframeMaterializationState: + model: + - github.com/calypr/loom/graphqlapi/model.DataframeMaterializationState + DataframeColumn: + model: + - github.com/calypr/loom/graphqlapi/model.DataframeColumn + DataframeDatasetInput: + model: + - github.com/calypr/loom/graphqlapi/model.DataframeDatasetInput + DataframeFilterInput: + model: + - github.com/calypr/loom/graphqlapi/model.DataframeFilterInput + DataframeSortInput: + model: + - github.com/calypr/loom/graphqlapi/model.DataframeSortInput + DataframeRowsInput: + model: + - github.com/calypr/loom/graphqlapi/model.DataframeRowsInput + DataframePageInfo: + model: + - github.com/calypr/loom/graphqlapi/model.DataframePageInfo + DataframeRowConnection: + model: + - github.com/calypr/loom/graphqlapi/model.DataframeRowConnection + DataframeAggregateInput: + model: + - github.com/calypr/loom/graphqlapi/model.DataframeAggregateInput + DataframeAggregateResult: + model: + - github.com/calypr/loom/graphqlapi/model.DataframeAggregateResult diff --git a/graphqlapi/clickhouse/handler.go b/graphqlapi/clickhouse/handler.go new file mode 100644 index 0000000..2239604 --- /dev/null +++ b/graphqlapi/clickhouse/handler.go @@ -0,0 +1,15 @@ +package clickhouse + +import ( + "net/http" + + gqlhandler "github.com/99designs/gqlgen/graphql/handler" + "github.com/calypr/loom/graphqlapi/materialization" +) + +func NewHandler(service *materializationapi.Service) http.Handler { + server := gqlhandler.NewDefaultServer(NewExecutableSchema(Config{ + Resolvers: NewResolver(service), + })) + return server +} diff --git a/graphqlapi/clickhouse/model.go b/graphqlapi/clickhouse/model.go new file mode 100644 index 0000000..db9a29e --- /dev/null +++ b/graphqlapi/clickhouse/model.go @@ -0,0 +1,6 @@ +// Code generated by github.com/99designs/gqlgen, DO NOT EDIT. + +package clickhouse + +type Query struct { +} diff --git a/graphqlapi/clickhouse/resolver.go b/graphqlapi/clickhouse/resolver.go new file mode 100644 index 0000000..3b06ffe --- /dev/null +++ b/graphqlapi/clickhouse/resolver.go @@ -0,0 +1,75 @@ +package clickhouse + +import ( + "context" + "encoding/json" + + materializationapi "github.com/calypr/loom/graphqlapi/materialization" + "github.com/calypr/loom/graphqlapi/model" + "github.com/calypr/loom/internal/dataframe/materialization" +) + +type Resolver struct { + service *materializationapi.Service +} + +func NewResolver(service *materializationapi.Service) *Resolver { + return &Resolver{service: service} +} + +func (r *Resolver) DataframeDatasets(ctx context.Context) ([]*model.DataframeMaterialization, error) { + values, err := r.service.Datasets(ctx) + if err != nil { + return nil, err + } + result := make([]*model.DataframeMaterialization, 0, len(values)) + for _, value := range values { + result = append(result, materializationapi.Model(value)) + } + return result, nil +} + +func (r *Resolver) DataframeDataset(ctx context.Context, input model.DataframeDatasetInput) (*model.DataframeMaterialization, error) { + value, err := r.service.Dataset(ctx, input) + if err != nil { + return nil, err + } + return materializationapi.Model(*value), nil +} + +func (r *Resolver) DataframeRows(ctx context.Context, input model.DataframeRowsInput) (*model.DataframeRowConnection, error) { + page, err := r.service.Rows(ctx, input) + if err != nil { + return nil, err + } + rows, err := json.Marshal(page.Rows) + if err != nil { + return nil, err + } + var cursor *string + if page.NextCursor != "" { + cursor = &page.NextCursor + } + total := int(page.TotalCount) + return &model.DataframeRowConnection{ + Materialization: materializationapi.Model(page.Materialization), + Columns: page.Columns, + Rows: rows, + TotalCount: &total, + PageInfo: &model.DataframePageInfo{HasNextPage: page.HasNext, EndCursor: cursor}, + }, nil +} + +func (r *Resolver) DataframeAggregate(ctx context.Context, input model.DataframeAggregateInput) (*model.DataframeAggregateResult, error) { + result, err := r.service.AggregateInput(ctx, input) + if err != nil { + return nil, err + } + return &model.DataframeAggregateResult{ + Materialization: materializationapi.Model(result.Materialization), + Columns: result.Columns, + Rows: materializationapi.AggregateRows(result.Rows), + }, nil +} + +var _ = materialization.Page{} diff --git a/graphqlapi/clickhouse/scalars.go b/graphqlapi/clickhouse/scalars.go new file mode 100644 index 0000000..b65bcc4 --- /dev/null +++ b/graphqlapi/clickhouse/scalars.go @@ -0,0 +1,24 @@ +package clickhouse + +import ( + "context" + "encoding/json" + "io" + + "github.com/99designs/gqlgen/graphql" + "github.com/vektah/gqlparser/v2/ast" +) + +func MarshalJSON(v json.RawMessage) graphql.Marshaler { + return graphql.WriterFunc(func(w io.Writer) { + _, _ = w.Write(v) + }) +} + +func (ec *executionContext) unmarshalInputJSON(_ context.Context, v any) (json.RawMessage, error) { + return json.Marshal(v) +} + +func (ec *executionContext) _JSON(_ context.Context, _ ast.SelectionSet, v json.RawMessage) graphql.Marshaler { + return MarshalJSON(v) +} diff --git a/graphqlapi/clickhouse/schema.graphqls b/graphqlapi/clickhouse/schema.graphqls new file mode 100644 index 0000000..aacb2e7 --- /dev/null +++ b/graphqlapi/clickhouse/schema.graphqls @@ -0,0 +1,89 @@ +scalar JSON + +type Query { + dataframeDatasets: [DataframeMaterialization!]! + dataframeDataset(input: DataframeDatasetInput!): DataframeMaterialization + dataframeRows(input: DataframeRowsInput!): DataframeRowConnection! + dataframeAggregate(input: DataframeAggregateInput!): DataframeAggregateResult! +} + +type DataframeMaterialization { + id: ID! + name: String! + revision: String! + state: DataframeMaterializationState! + columns: [DataframeColumn!]! + rowCount: Int! + createdAt: String! + readyAt: String + error: String +} + +enum DataframeMaterializationState { + PENDING + LOADING + READY + FAILED +} + +type DataframeColumn { + name: String! + clickhouseType: String! + logicalType: String! + nullable: Boolean! + repeated: Boolean! + filterable: Boolean! + sortable: Boolean! + aggregatable: Boolean! +} + +input DataframeDatasetInput { + dataType: String! +} + +input DataframeFilterInput { + column: String! + op: String! + value: JSON! +} + +input DataframeSortInput { + column: String! + desc: Boolean = false +} + +input DataframeRowsInput { + dataType: String! + columns: [String!] + filters: [DataframeFilterInput!] + sort: DataframeSortInput + first: Int = 100 + after: String +} + +type DataframePageInfo { + hasNextPage: Boolean! + endCursor: String +} + +type DataframeRowConnection { + materialization: DataframeMaterialization! + columns: [String!]! + rows: JSON! + totalCount: Int + pageInfo: DataframePageInfo! +} + +input DataframeAggregateInput { + dataType: String! + groupBy: [String!] + filters: [DataframeFilterInput!] + operation: String! + column: String +} + +type DataframeAggregateResult { + materialization: DataframeMaterialization! + columns: [String!]! + rows: JSON! +} diff --git a/graphqlapi/clickhouse/schema.resolvers.go b/graphqlapi/clickhouse/schema.resolvers.go new file mode 100644 index 0000000..55ef959 --- /dev/null +++ b/graphqlapi/clickhouse/schema.resolvers.go @@ -0,0 +1,38 @@ +package clickhouse + +// This file will be automatically regenerated based on the schema, any resolver +// implementations +// will be copied through when generating and any unknown code will be moved to the end. +// Code generated by github.com/99designs/gqlgen version v0.17.94 + +import ( + "context" + "fmt" + + "github.com/calypr/loom/graphqlapi/model" +) + +// DataframeDatasets is the resolver for the dataframeDatasets field. +func (r *queryResolver) DataframeDatasets(ctx context.Context) ([]*model.DataframeMaterialization, error) { + panic(fmt.Errorf("not implemented: DataframeDatasets - dataframeDatasets")) +} + +// DataframeDataset is the resolver for the dataframeDataset field. +func (r *queryResolver) DataframeDataset(ctx context.Context, input model.DataframeDatasetInput) (*model.DataframeMaterialization, error) { + panic(fmt.Errorf("not implemented: DataframeDataset - dataframeDataset")) +} + +// DataframeRows is the resolver for the dataframeRows field. +func (r *queryResolver) DataframeRows(ctx context.Context, input model.DataframeRowsInput) (*model.DataframeRowConnection, error) { + panic(fmt.Errorf("not implemented: DataframeRows - dataframeRows")) +} + +// DataframeAggregate is the resolver for the dataframeAggregate field. +func (r *queryResolver) DataframeAggregate(ctx context.Context, input model.DataframeAggregateInput) (*model.DataframeAggregateResult, error) { + panic(fmt.Errorf("not implemented: DataframeAggregate - dataframeAggregate")) +} + +// Query returns QueryResolver implementation. +func (r *Resolver) Query() QueryResolver { return &queryResolver{r} } + +type queryResolver struct{ *Resolver } diff --git a/graphqlapi/errors.go b/graphqlapi/errors.go index f01edff..57b122f 100644 --- a/graphqlapi/errors.go +++ b/graphqlapi/errors.go @@ -1,8 +1,6 @@ package graphqlapi import ( - "errors" - "github.com/calypr/loom/internal/dataframe" "github.com/vektah/gqlparser/v2/gqlerror" ) @@ -47,10 +45,3 @@ func ExtensionsForError(err error, requestID string) map[string]any { } return extensions } - -// IsUserError is useful to adapters that need to preserve the semantic error -// while adding transport context. -func IsUserError(err error) bool { - var userErr dataframe.UserError - return errors.As(err, &userErr) -} diff --git a/graphqlapi/generated.go b/graphqlapi/generated.go index c51413f..60efc05 100644 --- a/graphqlapi/generated.go +++ b/graphqlapi/generated.go @@ -9,8 +9,8 @@ import ( "encoding/json" "errors" "fmt" + "math" "strconv" - "sync" "sync/atomic" "github.com/99designs/gqlgen/graphql" @@ -20,24 +20,14 @@ import ( "github.com/vektah/gqlparser/v2/ast" ) -// region ************************** generated!.gotpl ************************** +// region ***************************** api!.gotpl ***************************** // NewExecutableSchema creates an ExecutableSchema from the ResolverRoot interface. func NewExecutableSchema(cfg Config) graphql.ExecutableSchema { - return &executableSchema{ - schema: cfg.Schema, - resolvers: cfg.Resolvers, - directives: cfg.Directives, - complexity: cfg.Complexity, - } + return &executableSchema{SchemaData: cfg.Schema, Resolvers: cfg.Resolvers, Directives: cfg.Directives, ComplexityRoot: cfg.Complexity} } -type Config struct { - Schema *ast.Schema - Resolvers ResolverRoot - Directives DirectiveRoot - Complexity ComplexityRoot -} +type Config = graphql.Config[ResolverRoot, DirectiveRoot, ComplexityRoot] type ResolverRoot interface { Mutation() MutationResolver @@ -66,8 +56,14 @@ type ComplexityRoot struct { } DataframeColumn struct { + Aggregatable func(childComplexity int) int ClickhouseType func(childComplexity int) int + Filterable func(childComplexity int) int + LogicalType func(childComplexity int) int Name func(childComplexity int) int + Nullable func(childComplexity int) int + Repeated func(childComplexity int) int + Sortable func(childComplexity int) int } DataframeCompilerPlanDiagnostics struct { @@ -114,16 +110,15 @@ type ComplexityRoot struct { } DataframeMaterialization struct { - Columns func(childComplexity int) int - CreatedAt func(childComplexity int) int - DatasetGeneration func(childComplexity int) int - Error func(childComplexity int) int - ID func(childComplexity int) int - Name func(childComplexity int) int - Project func(childComplexity int) int - ReadyAt func(childComplexity int) int - RowCount func(childComplexity int) int - State func(childComplexity int) int + Columns func(childComplexity int) int + CreatedAt func(childComplexity int) int + Error func(childComplexity int) int + ID func(childComplexity int) int + Name func(childComplexity int) int + ReadyAt func(childComplexity int) int + Revision func(childComplexity int) int + RowCount func(childComplexity int) int + State func(childComplexity int) int } DataframeOptimizationDecision struct { @@ -159,6 +154,202 @@ type ComplexityRoot struct { TotalMs func(childComplexity int) int } + DataframeRecipeArangoAssessment struct { + AppliedOptimizerRules func(childComplexity int) int + FullCollectionScans func(childComplexity int) int + Indexes func(childComplexity int) int + Plans func(childComplexity int) int + Warnings func(childComplexity int) int + } + + DataframeRecipeColumn struct { + DynamicName func(childComplexity int) int + LogicalType func(childComplexity int) int + Name func(childComplexity int) int + Nullable func(childComplexity int) int + Output func(childComplexity int) int + Repeated func(childComplexity int) int + } + + DataframeRecipeExecution struct { + Error func(childComplexity int) int + ID func(childComplexity int) int + Name func(childComplexity int) int + Outputs func(childComplexity int) int + RecipeDigest func(childComplexity int) int + ResolvedSchemaDigest func(childComplexity int) int + SourceGeneration func(childComplexity int) int + State func(childComplexity int) int + } + + DataframeRecipeExecutionOutput struct { + Error func(childComplexity int) int + Name func(childComplexity int) int + RowCount func(childComplexity int) int + State func(childComplexity int) int + } + + DataframeRecipeExpansionExplanation struct { + Alias func(childComplexity int) int + SourcePath func(childComplexity int) int + } + + DataframeRecipeExplainCollectionScan struct { + Collection func(childComplexity int) int + NodeID func(childComplexity int) int + Plan func(childComplexity int) int + } + + DataframeRecipeExplainIndexLocation struct { + NodeID func(childComplexity int) int + Plan func(childComplexity int) int + } + + DataframeRecipeExplainIndexSummary struct { + Collection func(childComplexity int) int + Fields func(childComplexity int) int + ID func(childComplexity int) int + Name func(childComplexity int) int + Type func(childComplexity int) int + Uses func(childComplexity int) int + } + + DataframeRecipeExplainPlanEstimate struct { + EstimatedCost func(childComplexity int) int + EstimatedNrItems func(childComplexity int) int + Plan func(childComplexity int) int + } + + DataframeRecipeExplainWarning struct { + Code func(childComplexity int) int + Message func(childComplexity int) int + } + + DataframeRecipeExplanation struct { + Name func(childComplexity int) int + Outputs func(childComplexity int) int + Physical func(childComplexity int) int + RecipeDigest func(childComplexity int) int + TranslationVersion func(childComplexity int) int + } + + DataframeRecipeExpressionExplanation struct { + Context func(childComplexity int) int + Kind func(childComplexity int) int + Nullable func(childComplexity int) int + Repeated func(childComplexity int) int + SourcePath func(childComplexity int) int + ValueType func(childComplexity int) int + } + + DataframeRecipeOptimizationDecision struct { + CandidateSets func(childComplexity int) int + Enabled func(childComplexity int) int + EstimatedBaselineWork func(childComplexity int) int + EstimatedOptimizedWork func(childComplexity int) int + EstimatedSavings func(childComplexity int) int + Reason func(childComplexity int) int + Rule func(childComplexity int) int + } + + DataframeRecipeOptimizationExplanation struct { + Decisions func(childComplexity int) int + Enabled func(childComplexity int) int + MinimumSavings func(childComplexity int) int + Policy func(childComplexity int) int + Rules func(childComplexity int) int + } + + DataframeRecipeOptimizationRuleState struct { + Enabled func(childComplexity int) int + Reason func(childComplexity int) int + Rule func(childComplexity int) int + } + + DataframeRecipeOutputExplanation struct { + CatalogProjections func(childComplexity int) int + DynamicMaps func(childComplexity int) int + Expansion func(childComplexity int) int + Fields func(childComplexity int) int + Identity func(childComplexity int) int + Name func(childComplexity int) int + RootResourceType func(childComplexity int) int + RowGrain func(childComplexity int) int + } + + DataframeRecipeOutputValidation struct { + DynamicColumns func(childComplexity int) int + FieldNames func(childComplexity int) int + Name func(childComplexity int) int + RootResourceType func(childComplexity int) int + RowGrain func(childComplexity int) int + } + + DataframeRecipePhysicalExplanation struct { + Outputs func(childComplexity int) int + } + + DataframeRecipePhysicalOutputExplanation struct { + Columns func(childComplexity int) int + EndpointTraversalCount func(childComplexity int) int + Live func(childComplexity int) int + Name func(childComplexity int) int + NativeTraversalCount func(childComplexity int) int + Optimization func(childComplexity int) int + PlanFingerprint func(childComplexity int) int + RequiredMatchReuseCount func(childComplexity int) int + SharedTraversalCount func(childComplexity int) int + TraversalSets func(childComplexity int) int + } + + DataframeRecipePreflight struct { + Columns func(childComplexity int) int + Name func(childComplexity int) int + RecipeDigest func(childComplexity int) int + ResolvedSchemaDigest func(childComplexity int) int + ScopeDigest func(childComplexity int) int + SourceGeneration func(childComplexity int) int + } + + DataframeRecipePreview struct { + Name func(childComplexity int) int + Outputs func(childComplexity int) int + RecipeDigest func(childComplexity int) int + ResolvedSchemaDigest func(childComplexity int) int + SourceGeneration func(childComplexity int) int + } + + DataframeRecipePreviewOutput struct { + CSV func(childComplexity int) int + Columns func(childComplexity int) int + Name func(childComplexity int) int + RowCount func(childComplexity int) int + Rows func(childComplexity int) int + } + + DataframeRecipeResult struct { + Name func(childComplexity int) int + Outputs func(childComplexity int) int + RecipeDigest func(childComplexity int) int + ResolvedSchemaDigest func(childComplexity int) int + SourceGeneration func(childComplexity int) int + } + + DataframeRecipeResultOutput struct { + CSV func(childComplexity int) int + Columns func(childComplexity int) int + Name func(childComplexity int) int + RowCount func(childComplexity int) int + Rows func(childComplexity int) int + } + + DataframeRecipeValidation struct { + Name func(childComplexity int) int + Outputs func(childComplexity int) int + RecipeDigest func(childComplexity int) int + TranslationVersion func(childComplexity int) int + } + DataframeRelatedResourceHints struct { EdgeCount func(childComplexity int) int Target func(childComplexity int) int @@ -185,6 +376,7 @@ type ComplexityRoot struct { Materialization func(childComplexity int) int PageInfo func(childComplexity int) int Rows func(childComplexity int) int + TotalCount func(childComplexity int) int } DataframeTraversalHint struct { @@ -202,7932 +394,8660 @@ type ComplexityRoot struct { } Mutation struct { - RunFhirDataframe func(childComplexity int, input model.FhirDataframeInput, limit *int) int + MaterializeDataframeRecipeBundle func(childComplexity int, input model.MaterializeDataframeRecipeInput) int + PreviewDataframeRecipe func(childComplexity int, input model.PreviewDataframeRecipeInput) int + RunDataframeRecipe func(childComplexity int, input model.RunDataframeRecipeInput) int + RunFhirDataframe func(childComplexity int, input model.FhirDataframeInput, limit *int) int + ValidateDataframeRecipe func(childComplexity int, input model.ValidateDataframeRecipeInput) int } Query struct { DataframeAggregate func(childComplexity int, input model.DataframeAggregateInput) int DataframeBuilderIntrospection func(childComplexity int, input model.DataframeBuilderIntrospectionInput) int + DataframeDataset func(childComplexity int, input model.DataframeDatasetInput) int + DataframeDatasets func(childComplexity int) int DataframeMaterialization func(childComplexity int, id string) int + DataframeRecipeExecution func(childComplexity int, id string) int DataframeRows func(childComplexity int, input model.DataframeRowsInput) int + ExplainDataframeRecipe func(childComplexity int, input model.ExplainDataframeRecipeInput) int + PreflightDataframeRecipe func(childComplexity int, input model.PreflightDataframeRecipeInput) int } } +// endregion ***************************** api!.gotpl ***************************** + +// region ************************** generated!.gotpl ************************** + type MutationResolver interface { RunFhirDataframe(ctx context.Context, input model.FhirDataframeInput, limit *int) (*model.FhirDataframeResult, error) + RunDataframeRecipe(ctx context.Context, input model.RunDataframeRecipeInput) (*model.DataframeRecipeResult, error) + ValidateDataframeRecipe(ctx context.Context, input model.ValidateDataframeRecipeInput) (*model.DataframeRecipeValidation, error) + PreviewDataframeRecipe(ctx context.Context, input model.PreviewDataframeRecipeInput) (*model.DataframeRecipePreview, error) + MaterializeDataframeRecipeBundle(ctx context.Context, input model.MaterializeDataframeRecipeInput) (*model.DataframeRecipeExecution, error) } type QueryResolver interface { DataframeBuilderIntrospection(ctx context.Context, input model.DataframeBuilderIntrospectionInput) (*model.DataframeBuilderIntrospection, error) DataframeMaterialization(ctx context.Context, id string) (*model.DataframeMaterialization, error) + DataframeDatasets(ctx context.Context) ([]*model.DataframeMaterialization, error) + DataframeDataset(ctx context.Context, input model.DataframeDatasetInput) (*model.DataframeMaterialization, error) DataframeRows(ctx context.Context, input model.DataframeRowsInput) (*model.DataframeRowConnection, error) DataframeAggregate(ctx context.Context, input model.DataframeAggregateInput) (*model.DataframeAggregateResult, error) + DataframeRecipeExecution(ctx context.Context, id string) (*model.DataframeRecipeExecution, error) + ExplainDataframeRecipe(ctx context.Context, input model.ExplainDataframeRecipeInput) (*model.DataframeRecipeExplanation, error) + PreflightDataframeRecipe(ctx context.Context, input model.PreflightDataframeRecipeInput) (*model.DataframeRecipePreflight, error) } -type executableSchema struct { - schema *ast.Schema - resolvers ResolverRoot - directives DirectiveRoot - complexity ComplexityRoot -} +// endregion ************************** generated!.gotpl ************************** + +// region ************************** internal!.gotpl *************************** + +type executableSchema graphql.ExecutableSchemaState[ResolverRoot, DirectiveRoot, ComplexityRoot] func (e *executableSchema) Schema() *ast.Schema { - if e.schema != nil { - return e.schema + if e.SchemaData != nil { + return e.SchemaData } return parsedSchema } -func (e *executableSchema) Complexity(typeName, field string, childComplexity int, rawArgs map[string]any) (int, bool) { - ec := executionContext{nil, e, 0, 0, nil} +func (e *executableSchema) Complexity(ctx context.Context, typeName, field string, childComplexity int, rawArgs map[string]any) (int, bool) { + ec := newExecutionContext(nil, e, nil) _ = ec switch typeName + "." + field { case "DataframeAggregateResult.columns": - if e.complexity.DataframeAggregateResult.Columns == nil { + if e.ComplexityRoot.DataframeAggregateResult.Columns == nil { break } - return e.complexity.DataframeAggregateResult.Columns(childComplexity), true - + return e.ComplexityRoot.DataframeAggregateResult.Columns(childComplexity), true case "DataframeAggregateResult.materialization": - if e.complexity.DataframeAggregateResult.Materialization == nil { + if e.ComplexityRoot.DataframeAggregateResult.Materialization == nil { break } - return e.complexity.DataframeAggregateResult.Materialization(childComplexity), true - + return e.ComplexityRoot.DataframeAggregateResult.Materialization(childComplexity), true case "DataframeAggregateResult.rows": - if e.complexity.DataframeAggregateResult.Rows == nil { + if e.ComplexityRoot.DataframeAggregateResult.Rows == nil { break } - return e.complexity.DataframeAggregateResult.Rows(childComplexity), true + return e.ComplexityRoot.DataframeAggregateResult.Rows(childComplexity), true case "DataframeBuilderIntrospection.authResourcePaths": - if e.complexity.DataframeBuilderIntrospection.AuthResourcePaths == nil { + if e.ComplexityRoot.DataframeBuilderIntrospection.AuthResourcePaths == nil { break } - return e.complexity.DataframeBuilderIntrospection.AuthResourcePaths(childComplexity), true - + return e.ComplexityRoot.DataframeBuilderIntrospection.AuthResourcePaths(childComplexity), true case "DataframeBuilderIntrospection.fields": - if e.complexity.DataframeBuilderIntrospection.Fields == nil { + if e.ComplexityRoot.DataframeBuilderIntrospection.Fields == nil { break } - return e.complexity.DataframeBuilderIntrospection.Fields(childComplexity), true - + return e.ComplexityRoot.DataframeBuilderIntrospection.Fields(childComplexity), true case "DataframeBuilderIntrospection.pivotFields": - if e.complexity.DataframeBuilderIntrospection.PivotFields == nil { + if e.ComplexityRoot.DataframeBuilderIntrospection.PivotFields == nil { break } - return e.complexity.DataframeBuilderIntrospection.PivotFields(childComplexity), true - + return e.ComplexityRoot.DataframeBuilderIntrospection.PivotFields(childComplexity), true case "DataframeBuilderIntrospection.project": - if e.complexity.DataframeBuilderIntrospection.Project == nil { + if e.ComplexityRoot.DataframeBuilderIntrospection.Project == nil { break } - return e.complexity.DataframeBuilderIntrospection.Project(childComplexity), true - + return e.ComplexityRoot.DataframeBuilderIntrospection.Project(childComplexity), true case "DataframeBuilderIntrospection.relatedResources": - if e.complexity.DataframeBuilderIntrospection.RelatedResources == nil { + if e.ComplexityRoot.DataframeBuilderIntrospection.RelatedResources == nil { break } - return e.complexity.DataframeBuilderIntrospection.RelatedResources(childComplexity), true - + return e.ComplexityRoot.DataframeBuilderIntrospection.RelatedResources(childComplexity), true case "DataframeBuilderIntrospection.root": - if e.complexity.DataframeBuilderIntrospection.Root == nil { + if e.ComplexityRoot.DataframeBuilderIntrospection.Root == nil { break } - return e.complexity.DataframeBuilderIntrospection.Root(childComplexity), true - + return e.ComplexityRoot.DataframeBuilderIntrospection.Root(childComplexity), true case "DataframeBuilderIntrospection.rootResourceType": - if e.complexity.DataframeBuilderIntrospection.RootResourceType == nil { + if e.ComplexityRoot.DataframeBuilderIntrospection.RootResourceType == nil { break } - return e.complexity.DataframeBuilderIntrospection.RootResourceType(childComplexity), true - + return e.ComplexityRoot.DataframeBuilderIntrospection.RootResourceType(childComplexity), true case "DataframeBuilderIntrospection.traversals": - if e.complexity.DataframeBuilderIntrospection.Traversals == nil { + if e.ComplexityRoot.DataframeBuilderIntrospection.Traversals == nil { break } - return e.complexity.DataframeBuilderIntrospection.Traversals(childComplexity), true + return e.ComplexityRoot.DataframeBuilderIntrospection.Traversals(childComplexity), true + + case "DataframeColumn.aggregatable": + if e.ComplexityRoot.DataframeColumn.Aggregatable == nil { + break + } + return e.ComplexityRoot.DataframeColumn.Aggregatable(childComplexity), true case "DataframeColumn.clickhouseType": - if e.complexity.DataframeColumn.ClickhouseType == nil { + if e.ComplexityRoot.DataframeColumn.ClickhouseType == nil { + break + } + + return e.ComplexityRoot.DataframeColumn.ClickhouseType(childComplexity), true + case "DataframeColumn.filterable": + if e.ComplexityRoot.DataframeColumn.Filterable == nil { break } - return e.complexity.DataframeColumn.ClickhouseType(childComplexity), true + return e.ComplexityRoot.DataframeColumn.Filterable(childComplexity), true + case "DataframeColumn.logicalType": + if e.ComplexityRoot.DataframeColumn.LogicalType == nil { + break + } + return e.ComplexityRoot.DataframeColumn.LogicalType(childComplexity), true case "DataframeColumn.name": - if e.complexity.DataframeColumn.Name == nil { + if e.ComplexityRoot.DataframeColumn.Name == nil { break } - return e.complexity.DataframeColumn.Name(childComplexity), true + return e.ComplexityRoot.DataframeColumn.Name(childComplexity), true + case "DataframeColumn.nullable": + if e.ComplexityRoot.DataframeColumn.Nullable == nil { + break + } - case "DataframeCompilerPlanDiagnostics.optimizationPolicy": - if e.complexity.DataframeCompilerPlanDiagnostics.OptimizationPolicy == nil { + return e.ComplexityRoot.DataframeColumn.Nullable(childComplexity), true + case "DataframeColumn.repeated": + if e.ComplexityRoot.DataframeColumn.Repeated == nil { break } - return e.complexity.DataframeCompilerPlanDiagnostics.OptimizationPolicy(childComplexity), true + return e.ComplexityRoot.DataframeColumn.Repeated(childComplexity), true + case "DataframeColumn.sortable": + if e.ComplexityRoot.DataframeColumn.Sortable == nil { + break + } - case "DataframeCompilerPlanDiagnostics.potentialSharingOpportunityGroups": - if e.complexity.DataframeCompilerPlanDiagnostics.PotentialSharingOpportunityGroups == nil { + return e.ComplexityRoot.DataframeColumn.Sortable(childComplexity), true + + case "DataframeCompilerPlanDiagnostics.optimizationPolicy": + if e.ComplexityRoot.DataframeCompilerPlanDiagnostics.OptimizationPolicy == nil { break } - return e.complexity.DataframeCompilerPlanDiagnostics.PotentialSharingOpportunityGroups(childComplexity), true + return e.ComplexityRoot.DataframeCompilerPlanDiagnostics.OptimizationPolicy(childComplexity), true + case "DataframeCompilerPlanDiagnostics.potentialSharingOpportunityGroups": + if e.ComplexityRoot.DataframeCompilerPlanDiagnostics.PotentialSharingOpportunityGroups == nil { + break + } + return e.ComplexityRoot.DataframeCompilerPlanDiagnostics.PotentialSharingOpportunityGroups(childComplexity), true case "DataframeCompilerPlanDiagnostics.potentialSharingOpportunitySets": - if e.complexity.DataframeCompilerPlanDiagnostics.PotentialSharingOpportunitySets == nil { + if e.ComplexityRoot.DataframeCompilerPlanDiagnostics.PotentialSharingOpportunitySets == nil { break } - return e.complexity.DataframeCompilerPlanDiagnostics.PotentialSharingOpportunitySets(childComplexity), true - + return e.ComplexityRoot.DataframeCompilerPlanDiagnostics.PotentialSharingOpportunitySets(childComplexity), true case "DataframeCompilerPlanDiagnostics.requiredMatchReuseCount": - if e.complexity.DataframeCompilerPlanDiagnostics.RequiredMatchReuseCount == nil { + if e.ComplexityRoot.DataframeCompilerPlanDiagnostics.RequiredMatchReuseCount == nil { break } - return e.complexity.DataframeCompilerPlanDiagnostics.RequiredMatchReuseCount(childComplexity), true - + return e.ComplexityRoot.DataframeCompilerPlanDiagnostics.RequiredMatchReuseCount(childComplexity), true case "DataframeCompilerPlanDiagnostics.richSourceReuse": - if e.complexity.DataframeCompilerPlanDiagnostics.RichSourceReuse == nil { + if e.ComplexityRoot.DataframeCompilerPlanDiagnostics.RichSourceReuse == nil { break } - return e.complexity.DataframeCompilerPlanDiagnostics.RichSourceReuse(childComplexity), true - + return e.ComplexityRoot.DataframeCompilerPlanDiagnostics.RichSourceReuse(childComplexity), true case "DataframeCompilerPlanDiagnostics.scopedSharingCandidateGroups": - if e.complexity.DataframeCompilerPlanDiagnostics.ScopedSharingCandidateGroups == nil { + if e.ComplexityRoot.DataframeCompilerPlanDiagnostics.ScopedSharingCandidateGroups == nil { break } - return e.complexity.DataframeCompilerPlanDiagnostics.ScopedSharingCandidateGroups(childComplexity), true - + return e.ComplexityRoot.DataframeCompilerPlanDiagnostics.ScopedSharingCandidateGroups(childComplexity), true case "DataframeCompilerPlanDiagnostics.scopedSharingCandidateSets": - if e.complexity.DataframeCompilerPlanDiagnostics.ScopedSharingCandidateSets == nil { + if e.ComplexityRoot.DataframeCompilerPlanDiagnostics.ScopedSharingCandidateSets == nil { break } - return e.complexity.DataframeCompilerPlanDiagnostics.ScopedSharingCandidateSets(childComplexity), true - + return e.ComplexityRoot.DataframeCompilerPlanDiagnostics.ScopedSharingCandidateSets(childComplexity), true case "DataframeCompilerPlanDiagnostics.sharedTraversalCount": - if e.complexity.DataframeCompilerPlanDiagnostics.SharedTraversalCount == nil { + if e.ComplexityRoot.DataframeCompilerPlanDiagnostics.SharedTraversalCount == nil { break } - return e.complexity.DataframeCompilerPlanDiagnostics.SharedTraversalCount(childComplexity), true - + return e.ComplexityRoot.DataframeCompilerPlanDiagnostics.SharedTraversalCount(childComplexity), true case "DataframeCompilerPlanDiagnostics.traversalSets": - if e.complexity.DataframeCompilerPlanDiagnostics.TraversalSets == nil { + if e.ComplexityRoot.DataframeCompilerPlanDiagnostics.TraversalSets == nil { break } - return e.complexity.DataframeCompilerPlanDiagnostics.TraversalSets(childComplexity), true + return e.ComplexityRoot.DataframeCompilerPlanDiagnostics.TraversalSets(childComplexity), true case "DataframeFieldHint.defaultPivotColumnSelector": - if e.complexity.DataframeFieldHint.DefaultPivotColumnSelector == nil { + if e.ComplexityRoot.DataframeFieldHint.DefaultPivotColumnSelector == nil { break } - return e.complexity.DataframeFieldHint.DefaultPivotColumnSelector(childComplexity), true - + return e.ComplexityRoot.DataframeFieldHint.DefaultPivotColumnSelector(childComplexity), true case "DataframeFieldHint.defaultPivotValueSelector": - if e.complexity.DataframeFieldHint.DefaultPivotValueSelector == nil { + if e.ComplexityRoot.DataframeFieldHint.DefaultPivotValueSelector == nil { break } - return e.complexity.DataframeFieldHint.DefaultPivotValueSelector(childComplexity), true - + return e.ComplexityRoot.DataframeFieldHint.DefaultPivotValueSelector(childComplexity), true case "DataframeFieldHint.distinctTruncated": - if e.complexity.DataframeFieldHint.DistinctTruncated == nil { + if e.ComplexityRoot.DataframeFieldHint.DistinctTruncated == nil { break } - return e.complexity.DataframeFieldHint.DistinctTruncated(childComplexity), true - + return e.ComplexityRoot.DataframeFieldHint.DistinctTruncated(childComplexity), true case "DataframeFieldHint.distinctValues": - if e.complexity.DataframeFieldHint.DistinctValues == nil { + if e.ComplexityRoot.DataframeFieldHint.DistinctValues == nil { break } - return e.complexity.DataframeFieldHint.DistinctValues(childComplexity), true - + return e.ComplexityRoot.DataframeFieldHint.DistinctValues(childComplexity), true case "DataframeFieldHint.docCount": - if e.complexity.DataframeFieldHint.DocCount == nil { + if e.ComplexityRoot.DataframeFieldHint.DocCount == nil { break } - return e.complexity.DataframeFieldHint.DocCount(childComplexity), true - + return e.ComplexityRoot.DataframeFieldHint.DocCount(childComplexity), true case "DataframeFieldHint.fieldRef": - if e.complexity.DataframeFieldHint.FieldRef == nil { + if e.ComplexityRoot.DataframeFieldHint.FieldRef == nil { break } - return e.complexity.DataframeFieldHint.FieldRef(childComplexity), true - + return e.ComplexityRoot.DataframeFieldHint.FieldRef(childComplexity), true case "DataframeFieldHint.kind": - if e.complexity.DataframeFieldHint.Kind == nil { + if e.ComplexityRoot.DataframeFieldHint.Kind == nil { break } - return e.complexity.DataframeFieldHint.Kind(childComplexity), true - + return e.ComplexityRoot.DataframeFieldHint.Kind(childComplexity), true case "DataframeFieldHint.label": - if e.complexity.DataframeFieldHint.Label == nil { + if e.ComplexityRoot.DataframeFieldHint.Label == nil { break } - return e.complexity.DataframeFieldHint.Label(childComplexity), true - + return e.ComplexityRoot.DataframeFieldHint.Label(childComplexity), true case "DataframeFieldHint.path": - if e.complexity.DataframeFieldHint.Path == nil { + if e.ComplexityRoot.DataframeFieldHint.Path == nil { break } - return e.complexity.DataframeFieldHint.Path(childComplexity), true - + return e.ComplexityRoot.DataframeFieldHint.Path(childComplexity), true case "DataframeFieldHint.pivotCandidate": - if e.complexity.DataframeFieldHint.PivotCandidate == nil { + if e.ComplexityRoot.DataframeFieldHint.PivotCandidate == nil { break } - return e.complexity.DataframeFieldHint.PivotCandidate(childComplexity), true - + return e.ComplexityRoot.DataframeFieldHint.PivotCandidate(childComplexity), true case "DataframeFieldHint.pivotColumns": - if e.complexity.DataframeFieldHint.PivotColumns == nil { + if e.ComplexityRoot.DataframeFieldHint.PivotColumns == nil { break } - return e.complexity.DataframeFieldHint.PivotColumns(childComplexity), true - + return e.ComplexityRoot.DataframeFieldHint.PivotColumns(childComplexity), true case "DataframeFieldHint.pivotFamily": - if e.complexity.DataframeFieldHint.PivotFamily == nil { + if e.ComplexityRoot.DataframeFieldHint.PivotFamily == nil { break } - return e.complexity.DataframeFieldHint.PivotFamily(childComplexity), true - + return e.ComplexityRoot.DataframeFieldHint.PivotFamily(childComplexity), true case "DataframeFieldHint.pivotKind": - if e.complexity.DataframeFieldHint.PivotKind == nil { + if e.ComplexityRoot.DataframeFieldHint.PivotKind == nil { break } - return e.complexity.DataframeFieldHint.PivotKind(childComplexity), true - + return e.ComplexityRoot.DataframeFieldHint.PivotKind(childComplexity), true case "DataframeFieldHint.resourceType": - if e.complexity.DataframeFieldHint.ResourceType == nil { + if e.ComplexityRoot.DataframeFieldHint.ResourceType == nil { break } - return e.complexity.DataframeFieldHint.ResourceType(childComplexity), true - + return e.ComplexityRoot.DataframeFieldHint.ResourceType(childComplexity), true case "DataframeFieldHint.sampleCount": - if e.complexity.DataframeFieldHint.SampleCount == nil { + if e.ComplexityRoot.DataframeFieldHint.SampleCount == nil { break } - return e.complexity.DataframeFieldHint.SampleCount(childComplexity), true - + return e.ComplexityRoot.DataframeFieldHint.SampleCount(childComplexity), true case "DataframeFieldHint.selector": - if e.complexity.DataframeFieldHint.Selector == nil { + if e.ComplexityRoot.DataframeFieldHint.Selector == nil { break } - return e.complexity.DataframeFieldHint.Selector(childComplexity), true + return e.ComplexityRoot.DataframeFieldHint.Selector(childComplexity), true case "DataframeFieldPredicate.op": - if e.complexity.DataframeFieldPredicate.Op == nil { + if e.ComplexityRoot.DataframeFieldPredicate.Op == nil { break } - return e.complexity.DataframeFieldPredicate.Op(childComplexity), true - + return e.ComplexityRoot.DataframeFieldPredicate.Op(childComplexity), true case "DataframeFieldPredicate.path": - if e.complexity.DataframeFieldPredicate.Path == nil { + if e.ComplexityRoot.DataframeFieldPredicate.Path == nil { break } - return e.complexity.DataframeFieldPredicate.Path(childComplexity), true - + return e.ComplexityRoot.DataframeFieldPredicate.Path(childComplexity), true case "DataframeFieldPredicate.value": - if e.complexity.DataframeFieldPredicate.Value == nil { + if e.ComplexityRoot.DataframeFieldPredicate.Value == nil { break } - return e.complexity.DataframeFieldPredicate.Value(childComplexity), true + return e.ComplexityRoot.DataframeFieldPredicate.Value(childComplexity), true case "DataframeFieldSelector.sourcePath": - if e.complexity.DataframeFieldSelector.SourcePath == nil { + if e.ComplexityRoot.DataframeFieldSelector.SourcePath == nil { break } - return e.complexity.DataframeFieldSelector.SourcePath(childComplexity), true - + return e.ComplexityRoot.DataframeFieldSelector.SourcePath(childComplexity), true case "DataframeFieldSelector.valuePath": - if e.complexity.DataframeFieldSelector.ValuePath == nil { + if e.ComplexityRoot.DataframeFieldSelector.ValuePath == nil { break } - return e.complexity.DataframeFieldSelector.ValuePath(childComplexity), true - + return e.ComplexityRoot.DataframeFieldSelector.ValuePath(childComplexity), true case "DataframeFieldSelector.where": - if e.complexity.DataframeFieldSelector.Where == nil { + if e.ComplexityRoot.DataframeFieldSelector.Where == nil { break } - return e.complexity.DataframeFieldSelector.Where(childComplexity), true + return e.ComplexityRoot.DataframeFieldSelector.Where(childComplexity), true case "DataframeMaterialization.columns": - if e.complexity.DataframeMaterialization.Columns == nil { + if e.ComplexityRoot.DataframeMaterialization.Columns == nil { break } - return e.complexity.DataframeMaterialization.Columns(childComplexity), true - + return e.ComplexityRoot.DataframeMaterialization.Columns(childComplexity), true case "DataframeMaterialization.createdAt": - if e.complexity.DataframeMaterialization.CreatedAt == nil { - break - } - - return e.complexity.DataframeMaterialization.CreatedAt(childComplexity), true - - case "DataframeMaterialization.datasetGeneration": - if e.complexity.DataframeMaterialization.DatasetGeneration == nil { + if e.ComplexityRoot.DataframeMaterialization.CreatedAt == nil { break } - return e.complexity.DataframeMaterialization.DatasetGeneration(childComplexity), true - + return e.ComplexityRoot.DataframeMaterialization.CreatedAt(childComplexity), true case "DataframeMaterialization.error": - if e.complexity.DataframeMaterialization.Error == nil { + if e.ComplexityRoot.DataframeMaterialization.Error == nil { break } - return e.complexity.DataframeMaterialization.Error(childComplexity), true - + return e.ComplexityRoot.DataframeMaterialization.Error(childComplexity), true case "DataframeMaterialization.id": - if e.complexity.DataframeMaterialization.ID == nil { + if e.ComplexityRoot.DataframeMaterialization.ID == nil { break } - return e.complexity.DataframeMaterialization.ID(childComplexity), true - + return e.ComplexityRoot.DataframeMaterialization.ID(childComplexity), true case "DataframeMaterialization.name": - if e.complexity.DataframeMaterialization.Name == nil { + if e.ComplexityRoot.DataframeMaterialization.Name == nil { break } - return e.complexity.DataframeMaterialization.Name(childComplexity), true - - case "DataframeMaterialization.project": - if e.complexity.DataframeMaterialization.Project == nil { + return e.ComplexityRoot.DataframeMaterialization.Name(childComplexity), true + case "DataframeMaterialization.readyAt": + if e.ComplexityRoot.DataframeMaterialization.ReadyAt == nil { break } - return e.complexity.DataframeMaterialization.Project(childComplexity), true - - case "DataframeMaterialization.readyAt": - if e.complexity.DataframeMaterialization.ReadyAt == nil { + return e.ComplexityRoot.DataframeMaterialization.ReadyAt(childComplexity), true + case "DataframeMaterialization.revision": + if e.ComplexityRoot.DataframeMaterialization.Revision == nil { break } - return e.complexity.DataframeMaterialization.ReadyAt(childComplexity), true - + return e.ComplexityRoot.DataframeMaterialization.Revision(childComplexity), true case "DataframeMaterialization.rowCount": - if e.complexity.DataframeMaterialization.RowCount == nil { + if e.ComplexityRoot.DataframeMaterialization.RowCount == nil { break } - return e.complexity.DataframeMaterialization.RowCount(childComplexity), true - + return e.ComplexityRoot.DataframeMaterialization.RowCount(childComplexity), true case "DataframeMaterialization.state": - if e.complexity.DataframeMaterialization.State == nil { + if e.ComplexityRoot.DataframeMaterialization.State == nil { break } - return e.complexity.DataframeMaterialization.State(childComplexity), true + return e.ComplexityRoot.DataframeMaterialization.State(childComplexity), true case "DataframeOptimizationDecision.candidateSets": - if e.complexity.DataframeOptimizationDecision.CandidateSets == nil { + if e.ComplexityRoot.DataframeOptimizationDecision.CandidateSets == nil { break } - return e.complexity.DataframeOptimizationDecision.CandidateSets(childComplexity), true - + return e.ComplexityRoot.DataframeOptimizationDecision.CandidateSets(childComplexity), true case "DataframeOptimizationDecision.enabled": - if e.complexity.DataframeOptimizationDecision.Enabled == nil { + if e.ComplexityRoot.DataframeOptimizationDecision.Enabled == nil { break } - return e.complexity.DataframeOptimizationDecision.Enabled(childComplexity), true - + return e.ComplexityRoot.DataframeOptimizationDecision.Enabled(childComplexity), true case "DataframeOptimizationDecision.estimatedBaselineWork": - if e.complexity.DataframeOptimizationDecision.EstimatedBaselineWork == nil { + if e.ComplexityRoot.DataframeOptimizationDecision.EstimatedBaselineWork == nil { break } - return e.complexity.DataframeOptimizationDecision.EstimatedBaselineWork(childComplexity), true - + return e.ComplexityRoot.DataframeOptimizationDecision.EstimatedBaselineWork(childComplexity), true case "DataframeOptimizationDecision.estimatedOptimizedWork": - if e.complexity.DataframeOptimizationDecision.EstimatedOptimizedWork == nil { + if e.ComplexityRoot.DataframeOptimizationDecision.EstimatedOptimizedWork == nil { break } - return e.complexity.DataframeOptimizationDecision.EstimatedOptimizedWork(childComplexity), true - + return e.ComplexityRoot.DataframeOptimizationDecision.EstimatedOptimizedWork(childComplexity), true case "DataframeOptimizationDecision.estimatedSavings": - if e.complexity.DataframeOptimizationDecision.EstimatedSavings == nil { + if e.ComplexityRoot.DataframeOptimizationDecision.EstimatedSavings == nil { break } - return e.complexity.DataframeOptimizationDecision.EstimatedSavings(childComplexity), true - + return e.ComplexityRoot.DataframeOptimizationDecision.EstimatedSavings(childComplexity), true case "DataframeOptimizationDecision.reason": - if e.complexity.DataframeOptimizationDecision.Reason == nil { + if e.ComplexityRoot.DataframeOptimizationDecision.Reason == nil { break } - return e.complexity.DataframeOptimizationDecision.Reason(childComplexity), true - + return e.ComplexityRoot.DataframeOptimizationDecision.Reason(childComplexity), true case "DataframeOptimizationDecision.rule": - if e.complexity.DataframeOptimizationDecision.Rule == nil { + if e.ComplexityRoot.DataframeOptimizationDecision.Rule == nil { break } - return e.complexity.DataframeOptimizationDecision.Rule(childComplexity), true + return e.ComplexityRoot.DataframeOptimizationDecision.Rule(childComplexity), true case "DataframeOptimizationPolicy.decisions": - if e.complexity.DataframeOptimizationPolicy.Decisions == nil { + if e.ComplexityRoot.DataframeOptimizationPolicy.Decisions == nil { break } - return e.complexity.DataframeOptimizationPolicy.Decisions(childComplexity), true - + return e.ComplexityRoot.DataframeOptimizationPolicy.Decisions(childComplexity), true case "DataframeOptimizationPolicy.enabled": - if e.complexity.DataframeOptimizationPolicy.Enabled == nil { + if e.ComplexityRoot.DataframeOptimizationPolicy.Enabled == nil { break } - return e.complexity.DataframeOptimizationPolicy.Enabled(childComplexity), true - + return e.ComplexityRoot.DataframeOptimizationPolicy.Enabled(childComplexity), true case "DataframeOptimizationPolicy.minimumSavings": - if e.complexity.DataframeOptimizationPolicy.MinimumSavings == nil { + if e.ComplexityRoot.DataframeOptimizationPolicy.MinimumSavings == nil { break } - return e.complexity.DataframeOptimizationPolicy.MinimumSavings(childComplexity), true - + return e.ComplexityRoot.DataframeOptimizationPolicy.MinimumSavings(childComplexity), true case "DataframeOptimizationPolicy.name": - if e.complexity.DataframeOptimizationPolicy.Name == nil { + if e.ComplexityRoot.DataframeOptimizationPolicy.Name == nil { break } - return e.complexity.DataframeOptimizationPolicy.Name(childComplexity), true + return e.ComplexityRoot.DataframeOptimizationPolicy.Name(childComplexity), true case "DataframePageInfo.endCursor": - if e.complexity.DataframePageInfo.EndCursor == nil { + if e.ComplexityRoot.DataframePageInfo.EndCursor == nil { break } - return e.complexity.DataframePageInfo.EndCursor(childComplexity), true - + return e.ComplexityRoot.DataframePageInfo.EndCursor(childComplexity), true case "DataframePageInfo.hasNextPage": - if e.complexity.DataframePageInfo.HasNextPage == nil { + if e.ComplexityRoot.DataframePageInfo.HasNextPage == nil { break } - return e.complexity.DataframePageInfo.HasNextPage(childComplexity), true + return e.ComplexityRoot.DataframePageInfo.HasNextPage(childComplexity), true case "DataframeQueryDiagnostics.arangoQueryMs": - if e.complexity.DataframeQueryDiagnostics.ArangoQueryMs == nil { + if e.ComplexityRoot.DataframeQueryDiagnostics.ArangoQueryMs == nil { break } - return e.complexity.DataframeQueryDiagnostics.ArangoQueryMs(childComplexity), true - + return e.ComplexityRoot.DataframeQueryDiagnostics.ArangoQueryMs(childComplexity), true case "DataframeQueryDiagnostics.compilationMs": - if e.complexity.DataframeQueryDiagnostics.CompilationMs == nil { + if e.ComplexityRoot.DataframeQueryDiagnostics.CompilationMs == nil { break } - return e.complexity.DataframeQueryDiagnostics.CompilationMs(childComplexity), true - + return e.ComplexityRoot.DataframeQueryDiagnostics.CompilationMs(childComplexity), true case "DataframeQueryDiagnostics.inputResolutionMs": - if e.complexity.DataframeQueryDiagnostics.InputResolutionMs == nil { + if e.ComplexityRoot.DataframeQueryDiagnostics.InputResolutionMs == nil { break } - return e.complexity.DataframeQueryDiagnostics.InputResolutionMs(childComplexity), true - + return e.ComplexityRoot.DataframeQueryDiagnostics.InputResolutionMs(childComplexity), true case "DataframeQueryDiagnostics.plan": - if e.complexity.DataframeQueryDiagnostics.Plan == nil { + if e.ComplexityRoot.DataframeQueryDiagnostics.Plan == nil { break } - return e.complexity.DataframeQueryDiagnostics.Plan(childComplexity), true - + return e.ComplexityRoot.DataframeQueryDiagnostics.Plan(childComplexity), true case "DataframeQueryDiagnostics.requestPreparationMs": - if e.complexity.DataframeQueryDiagnostics.RequestPreparationMs == nil { + if e.ComplexityRoot.DataframeQueryDiagnostics.RequestPreparationMs == nil { break } - return e.complexity.DataframeQueryDiagnostics.RequestPreparationMs(childComplexity), true - + return e.ComplexityRoot.DataframeQueryDiagnostics.RequestPreparationMs(childComplexity), true case "DataframeQueryDiagnostics.resultAssemblyMs": - if e.complexity.DataframeQueryDiagnostics.ResultAssemblyMs == nil { + if e.ComplexityRoot.DataframeQueryDiagnostics.ResultAssemblyMs == nil { break } - return e.complexity.DataframeQueryDiagnostics.ResultAssemblyMs(childComplexity), true - + return e.ComplexityRoot.DataframeQueryDiagnostics.ResultAssemblyMs(childComplexity), true case "DataframeQueryDiagnostics.rowMaterializationMs": - if e.complexity.DataframeQueryDiagnostics.RowMaterializationMs == nil { + if e.ComplexityRoot.DataframeQueryDiagnostics.RowMaterializationMs == nil { break } - return e.complexity.DataframeQueryDiagnostics.RowMaterializationMs(childComplexity), true - + return e.ComplexityRoot.DataframeQueryDiagnostics.RowMaterializationMs(childComplexity), true case "DataframeQueryDiagnostics.totalMs": - if e.complexity.DataframeQueryDiagnostics.TotalMs == nil { + if e.ComplexityRoot.DataframeQueryDiagnostics.TotalMs == nil { break } - return e.complexity.DataframeQueryDiagnostics.TotalMs(childComplexity), true + return e.ComplexityRoot.DataframeQueryDiagnostics.TotalMs(childComplexity), true - case "DataframeRelatedResourceHints.edgeCount": - if e.complexity.DataframeRelatedResourceHints.EdgeCount == nil { + case "DataframeRecipeArangoAssessment.appliedOptimizerRules": + if e.ComplexityRoot.DataframeRecipeArangoAssessment.AppliedOptimizerRules == nil { break } - return e.complexity.DataframeRelatedResourceHints.EdgeCount(childComplexity), true + return e.ComplexityRoot.DataframeRecipeArangoAssessment.AppliedOptimizerRules(childComplexity), true + case "DataframeRecipeArangoAssessment.fullCollectionScans": + if e.ComplexityRoot.DataframeRecipeArangoAssessment.FullCollectionScans == nil { + break + } - case "DataframeRelatedResourceHints.target": - if e.complexity.DataframeRelatedResourceHints.Target == nil { + return e.ComplexityRoot.DataframeRecipeArangoAssessment.FullCollectionScans(childComplexity), true + case "DataframeRecipeArangoAssessment.indexes": + if e.ComplexityRoot.DataframeRecipeArangoAssessment.Indexes == nil { break } - return e.complexity.DataframeRelatedResourceHints.Target(childComplexity), true + return e.ComplexityRoot.DataframeRecipeArangoAssessment.Indexes(childComplexity), true + case "DataframeRecipeArangoAssessment.plans": + if e.ComplexityRoot.DataframeRecipeArangoAssessment.Plans == nil { + break + } - case "DataframeRelatedResourceHints.viaLabel": - if e.complexity.DataframeRelatedResourceHints.ViaLabel == nil { + return e.ComplexityRoot.DataframeRecipeArangoAssessment.Plans(childComplexity), true + case "DataframeRecipeArangoAssessment.warnings": + if e.ComplexityRoot.DataframeRecipeArangoAssessment.Warnings == nil { break } - return e.complexity.DataframeRelatedResourceHints.ViaLabel(childComplexity), true + return e.ComplexityRoot.DataframeRecipeArangoAssessment.Warnings(childComplexity), true - case "DataframeResourceHints.fields": - if e.complexity.DataframeResourceHints.Fields == nil { + case "DataframeRecipeColumn.dynamicName": + if e.ComplexityRoot.DataframeRecipeColumn.DynamicName == nil { break } - return e.complexity.DataframeResourceHints.Fields(childComplexity), true - - case "DataframeResourceHints.pivotFields": - if e.complexity.DataframeResourceHints.PivotFields == nil { + return e.ComplexityRoot.DataframeRecipeColumn.DynamicName(childComplexity), true + case "DataframeRecipeColumn.logicalType": + if e.ComplexityRoot.DataframeRecipeColumn.LogicalType == nil { break } - return e.complexity.DataframeResourceHints.PivotFields(childComplexity), true + return e.ComplexityRoot.DataframeRecipeColumn.LogicalType(childComplexity), true + case "DataframeRecipeColumn.name": + if e.ComplexityRoot.DataframeRecipeColumn.Name == nil { + break + } - case "DataframeResourceHints.resourceType": - if e.complexity.DataframeResourceHints.ResourceType == nil { + return e.ComplexityRoot.DataframeRecipeColumn.Name(childComplexity), true + case "DataframeRecipeColumn.nullable": + if e.ComplexityRoot.DataframeRecipeColumn.Nullable == nil { break } - return e.complexity.DataframeResourceHints.ResourceType(childComplexity), true + return e.ComplexityRoot.DataframeRecipeColumn.Nullable(childComplexity), true + case "DataframeRecipeColumn.output": + if e.ComplexityRoot.DataframeRecipeColumn.Output == nil { + break + } - case "DataframeResourceHints.traversals": - if e.complexity.DataframeResourceHints.Traversals == nil { + return e.ComplexityRoot.DataframeRecipeColumn.Output(childComplexity), true + case "DataframeRecipeColumn.repeated": + if e.ComplexityRoot.DataframeRecipeColumn.Repeated == nil { break } - return e.complexity.DataframeResourceHints.Traversals(childComplexity), true + return e.ComplexityRoot.DataframeRecipeColumn.Repeated(childComplexity), true - case "DataframeRichSourceReuse.aggregateConsumers": - if e.complexity.DataframeRichSourceReuse.AggregateConsumers == nil { + case "DataframeRecipeExecution.error": + if e.ComplexityRoot.DataframeRecipeExecution.Error == nil { break } - return e.complexity.DataframeRichSourceReuse.AggregateConsumers(childComplexity), true - - case "DataframeRichSourceReuse.pivotConsumers": - if e.complexity.DataframeRichSourceReuse.PivotConsumers == nil { + return e.ComplexityRoot.DataframeRecipeExecution.Error(childComplexity), true + case "DataframeRecipeExecution.id": + if e.ComplexityRoot.DataframeRecipeExecution.ID == nil { break } - return e.complexity.DataframeRichSourceReuse.PivotConsumers(childComplexity), true + return e.ComplexityRoot.DataframeRecipeExecution.ID(childComplexity), true + case "DataframeRecipeExecution.name": + if e.ComplexityRoot.DataframeRecipeExecution.Name == nil { + break + } - case "DataframeRichSourceReuse.sliceConsumers": - if e.complexity.DataframeRichSourceReuse.SliceConsumers == nil { + return e.ComplexityRoot.DataframeRecipeExecution.Name(childComplexity), true + case "DataframeRecipeExecution.outputs": + if e.ComplexityRoot.DataframeRecipeExecution.Outputs == nil { break } - return e.complexity.DataframeRichSourceReuse.SliceConsumers(childComplexity), true + return e.ComplexityRoot.DataframeRecipeExecution.Outputs(childComplexity), true + case "DataframeRecipeExecution.recipeDigest": + if e.ComplexityRoot.DataframeRecipeExecution.RecipeDigest == nil { + break + } - case "DataframeRichSourceReuse.sourceSet": - if e.complexity.DataframeRichSourceReuse.SourceSet == nil { + return e.ComplexityRoot.DataframeRecipeExecution.RecipeDigest(childComplexity), true + case "DataframeRecipeExecution.resolvedSchemaDigest": + if e.ComplexityRoot.DataframeRecipeExecution.ResolvedSchemaDigest == nil { break } - return e.complexity.DataframeRichSourceReuse.SourceSet(childComplexity), true + return e.ComplexityRoot.DataframeRecipeExecution.ResolvedSchemaDigest(childComplexity), true + case "DataframeRecipeExecution.sourceGeneration": + if e.ComplexityRoot.DataframeRecipeExecution.SourceGeneration == nil { + break + } - case "DataframeRichSourceReuse.totalConsumers": - if e.complexity.DataframeRichSourceReuse.TotalConsumers == nil { + return e.ComplexityRoot.DataframeRecipeExecution.SourceGeneration(childComplexity), true + case "DataframeRecipeExecution.state": + if e.ComplexityRoot.DataframeRecipeExecution.State == nil { break } - return e.complexity.DataframeRichSourceReuse.TotalConsumers(childComplexity), true + return e.ComplexityRoot.DataframeRecipeExecution.State(childComplexity), true - case "DataframeRowConnection.columns": - if e.complexity.DataframeRowConnection.Columns == nil { + case "DataframeRecipeExecutionOutput.error": + if e.ComplexityRoot.DataframeRecipeExecutionOutput.Error == nil { break } - return e.complexity.DataframeRowConnection.Columns(childComplexity), true + return e.ComplexityRoot.DataframeRecipeExecutionOutput.Error(childComplexity), true + case "DataframeRecipeExecutionOutput.name": + if e.ComplexityRoot.DataframeRecipeExecutionOutput.Name == nil { + break + } - case "DataframeRowConnection.materialization": - if e.complexity.DataframeRowConnection.Materialization == nil { + return e.ComplexityRoot.DataframeRecipeExecutionOutput.Name(childComplexity), true + case "DataframeRecipeExecutionOutput.rowCount": + if e.ComplexityRoot.DataframeRecipeExecutionOutput.RowCount == nil { break } - return e.complexity.DataframeRowConnection.Materialization(childComplexity), true + return e.ComplexityRoot.DataframeRecipeExecutionOutput.RowCount(childComplexity), true + case "DataframeRecipeExecutionOutput.state": + if e.ComplexityRoot.DataframeRecipeExecutionOutput.State == nil { + break + } - case "DataframeRowConnection.pageInfo": - if e.complexity.DataframeRowConnection.PageInfo == nil { + return e.ComplexityRoot.DataframeRecipeExecutionOutput.State(childComplexity), true + + case "DataframeRecipeExpansionExplanation.alias": + if e.ComplexityRoot.DataframeRecipeExpansionExplanation.Alias == nil { break } - return e.complexity.DataframeRowConnection.PageInfo(childComplexity), true + return e.ComplexityRoot.DataframeRecipeExpansionExplanation.Alias(childComplexity), true + case "DataframeRecipeExpansionExplanation.sourcePath": + if e.ComplexityRoot.DataframeRecipeExpansionExplanation.SourcePath == nil { + break + } - case "DataframeRowConnection.rows": - if e.complexity.DataframeRowConnection.Rows == nil { + return e.ComplexityRoot.DataframeRecipeExpansionExplanation.SourcePath(childComplexity), true + + case "DataframeRecipeExplainCollectionScan.collection": + if e.ComplexityRoot.DataframeRecipeExplainCollectionScan.Collection == nil { break } - return e.complexity.DataframeRowConnection.Rows(childComplexity), true + return e.ComplexityRoot.DataframeRecipeExplainCollectionScan.Collection(childComplexity), true + case "DataframeRecipeExplainCollectionScan.nodeId": + if e.ComplexityRoot.DataframeRecipeExplainCollectionScan.NodeID == nil { + break + } - case "DataframeTraversalHint.edgeCount": - if e.complexity.DataframeTraversalHint.EdgeCount == nil { + return e.ComplexityRoot.DataframeRecipeExplainCollectionScan.NodeID(childComplexity), true + case "DataframeRecipeExplainCollectionScan.plan": + if e.ComplexityRoot.DataframeRecipeExplainCollectionScan.Plan == nil { break } - return e.complexity.DataframeTraversalHint.EdgeCount(childComplexity), true + return e.ComplexityRoot.DataframeRecipeExplainCollectionScan.Plan(childComplexity), true - case "DataframeTraversalHint.fromType": - if e.complexity.DataframeTraversalHint.FromType == nil { + case "DataframeRecipeExplainIndexLocation.nodeId": + if e.ComplexityRoot.DataframeRecipeExplainIndexLocation.NodeID == nil { break } - return e.complexity.DataframeTraversalHint.FromType(childComplexity), true + return e.ComplexityRoot.DataframeRecipeExplainIndexLocation.NodeID(childComplexity), true + case "DataframeRecipeExplainIndexLocation.plan": + if e.ComplexityRoot.DataframeRecipeExplainIndexLocation.Plan == nil { + break + } - case "DataframeTraversalHint.label": - if e.complexity.DataframeTraversalHint.Label == nil { + return e.ComplexityRoot.DataframeRecipeExplainIndexLocation.Plan(childComplexity), true + + case "DataframeRecipeExplainIndexSummary.collection": + if e.ComplexityRoot.DataframeRecipeExplainIndexSummary.Collection == nil { break } - return e.complexity.DataframeTraversalHint.Label(childComplexity), true + return e.ComplexityRoot.DataframeRecipeExplainIndexSummary.Collection(childComplexity), true + case "DataframeRecipeExplainIndexSummary.fields": + if e.ComplexityRoot.DataframeRecipeExplainIndexSummary.Fields == nil { + break + } - case "DataframeTraversalHint.toType": - if e.complexity.DataframeTraversalHint.ToType == nil { + return e.ComplexityRoot.DataframeRecipeExplainIndexSummary.Fields(childComplexity), true + case "DataframeRecipeExplainIndexSummary.id": + if e.ComplexityRoot.DataframeRecipeExplainIndexSummary.ID == nil { break } - return e.complexity.DataframeTraversalHint.ToType(childComplexity), true + return e.ComplexityRoot.DataframeRecipeExplainIndexSummary.ID(childComplexity), true + case "DataframeRecipeExplainIndexSummary.name": + if e.ComplexityRoot.DataframeRecipeExplainIndexSummary.Name == nil { + break + } - case "FhirDataframeResult.columns": - if e.complexity.FhirDataframeResult.Columns == nil { + return e.ComplexityRoot.DataframeRecipeExplainIndexSummary.Name(childComplexity), true + case "DataframeRecipeExplainIndexSummary.type": + if e.ComplexityRoot.DataframeRecipeExplainIndexSummary.Type == nil { + break + } + + return e.ComplexityRoot.DataframeRecipeExplainIndexSummary.Type(childComplexity), true + case "DataframeRecipeExplainIndexSummary.uses": + if e.ComplexityRoot.DataframeRecipeExplainIndexSummary.Uses == nil { break } - return e.complexity.FhirDataframeResult.Columns(childComplexity), true + return e.ComplexityRoot.DataframeRecipeExplainIndexSummary.Uses(childComplexity), true - case "FhirDataframeResult.diagnostics": - if e.complexity.FhirDataframeResult.Diagnostics == nil { + case "DataframeRecipeExplainPlanEstimate.estimatedCost": + if e.ComplexityRoot.DataframeRecipeExplainPlanEstimate.EstimatedCost == nil { break } - return e.complexity.FhirDataframeResult.Diagnostics(childComplexity), true + return e.ComplexityRoot.DataframeRecipeExplainPlanEstimate.EstimatedCost(childComplexity), true + case "DataframeRecipeExplainPlanEstimate.estimatedNrItems": + if e.ComplexityRoot.DataframeRecipeExplainPlanEstimate.EstimatedNrItems == nil { + break + } - case "FhirDataframeResult.rowCount": - if e.complexity.FhirDataframeResult.RowCount == nil { + return e.ComplexityRoot.DataframeRecipeExplainPlanEstimate.EstimatedNrItems(childComplexity), true + case "DataframeRecipeExplainPlanEstimate.plan": + if e.ComplexityRoot.DataframeRecipeExplainPlanEstimate.Plan == nil { break } - return e.complexity.FhirDataframeResult.RowCount(childComplexity), true + return e.ComplexityRoot.DataframeRecipeExplainPlanEstimate.Plan(childComplexity), true - case "FhirDataframeResult.rows": - if e.complexity.FhirDataframeResult.Rows == nil { + case "DataframeRecipeExplainWarning.code": + if e.ComplexityRoot.DataframeRecipeExplainWarning.Code == nil { + break + } + + return e.ComplexityRoot.DataframeRecipeExplainWarning.Code(childComplexity), true + case "DataframeRecipeExplainWarning.message": + if e.ComplexityRoot.DataframeRecipeExplainWarning.Message == nil { break } - return e.complexity.FhirDataframeResult.Rows(childComplexity), true + return e.ComplexityRoot.DataframeRecipeExplainWarning.Message(childComplexity), true - case "Mutation.runFhirDataframe": - if e.complexity.Mutation.RunFhirDataframe == nil { + case "DataframeRecipeExplanation.name": + if e.ComplexityRoot.DataframeRecipeExplanation.Name == nil { break } - args, err := ec.field_Mutation_runFhirDataframe_args(context.TODO(), rawArgs) - if err != nil { - return 0, false + return e.ComplexityRoot.DataframeRecipeExplanation.Name(childComplexity), true + case "DataframeRecipeExplanation.outputs": + if e.ComplexityRoot.DataframeRecipeExplanation.Outputs == nil { + break } - return e.complexity.Mutation.RunFhirDataframe(childComplexity, args["input"].(model.FhirDataframeInput), args["limit"].(*int)), true + return e.ComplexityRoot.DataframeRecipeExplanation.Outputs(childComplexity), true + case "DataframeRecipeExplanation.physical": + if e.ComplexityRoot.DataframeRecipeExplanation.Physical == nil { + break + } - case "Query.dataframeAggregate": - if e.complexity.Query.DataframeAggregate == nil { + return e.ComplexityRoot.DataframeRecipeExplanation.Physical(childComplexity), true + case "DataframeRecipeExplanation.recipeDigest": + if e.ComplexityRoot.DataframeRecipeExplanation.RecipeDigest == nil { break } - args, err := ec.field_Query_dataframeAggregate_args(context.TODO(), rawArgs) - if err != nil { - return 0, false + return e.ComplexityRoot.DataframeRecipeExplanation.RecipeDigest(childComplexity), true + case "DataframeRecipeExplanation.translationVersion": + if e.ComplexityRoot.DataframeRecipeExplanation.TranslationVersion == nil { + break } - return e.complexity.Query.DataframeAggregate(childComplexity, args["input"].(model.DataframeAggregateInput)), true + return e.ComplexityRoot.DataframeRecipeExplanation.TranslationVersion(childComplexity), true - case "Query.dataframeBuilderIntrospection": - if e.complexity.Query.DataframeBuilderIntrospection == nil { + case "DataframeRecipeExpressionExplanation.context": + if e.ComplexityRoot.DataframeRecipeExpressionExplanation.Context == nil { break } - args, err := ec.field_Query_dataframeBuilderIntrospection_args(context.TODO(), rawArgs) - if err != nil { - return 0, false + return e.ComplexityRoot.DataframeRecipeExpressionExplanation.Context(childComplexity), true + case "DataframeRecipeExpressionExplanation.kind": + if e.ComplexityRoot.DataframeRecipeExpressionExplanation.Kind == nil { + break } - return e.complexity.Query.DataframeBuilderIntrospection(childComplexity, args["input"].(model.DataframeBuilderIntrospectionInput)), true + return e.ComplexityRoot.DataframeRecipeExpressionExplanation.Kind(childComplexity), true + case "DataframeRecipeExpressionExplanation.nullable": + if e.ComplexityRoot.DataframeRecipeExpressionExplanation.Nullable == nil { + break + } - case "Query.dataframeMaterialization": - if e.complexity.Query.DataframeMaterialization == nil { + return e.ComplexityRoot.DataframeRecipeExpressionExplanation.Nullable(childComplexity), true + case "DataframeRecipeExpressionExplanation.repeated": + if e.ComplexityRoot.DataframeRecipeExpressionExplanation.Repeated == nil { break } - args, err := ec.field_Query_dataframeMaterialization_args(context.TODO(), rawArgs) - if err != nil { - return 0, false + return e.ComplexityRoot.DataframeRecipeExpressionExplanation.Repeated(childComplexity), true + case "DataframeRecipeExpressionExplanation.sourcePath": + if e.ComplexityRoot.DataframeRecipeExpressionExplanation.SourcePath == nil { + break + } + + return e.ComplexityRoot.DataframeRecipeExpressionExplanation.SourcePath(childComplexity), true + case "DataframeRecipeExpressionExplanation.valueType": + if e.ComplexityRoot.DataframeRecipeExpressionExplanation.ValueType == nil { + break } - return e.complexity.Query.DataframeMaterialization(childComplexity, args["id"].(string)), true + return e.ComplexityRoot.DataframeRecipeExpressionExplanation.ValueType(childComplexity), true - case "Query.dataframeRows": - if e.complexity.Query.DataframeRows == nil { + case "DataframeRecipeOptimizationDecision.candidateSets": + if e.ComplexityRoot.DataframeRecipeOptimizationDecision.CandidateSets == nil { break } - args, err := ec.field_Query_dataframeRows_args(context.TODO(), rawArgs) - if err != nil { - return 0, false + return e.ComplexityRoot.DataframeRecipeOptimizationDecision.CandidateSets(childComplexity), true + case "DataframeRecipeOptimizationDecision.enabled": + if e.ComplexityRoot.DataframeRecipeOptimizationDecision.Enabled == nil { + break } - return e.complexity.Query.DataframeRows(childComplexity, args["input"].(model.DataframeRowsInput)), true + return e.ComplexityRoot.DataframeRecipeOptimizationDecision.Enabled(childComplexity), true + case "DataframeRecipeOptimizationDecision.estimatedBaselineWork": + if e.ComplexityRoot.DataframeRecipeOptimizationDecision.EstimatedBaselineWork == nil { + break + } - } - return 0, false -} + return e.ComplexityRoot.DataframeRecipeOptimizationDecision.EstimatedBaselineWork(childComplexity), true + case "DataframeRecipeOptimizationDecision.estimatedOptimizedWork": + if e.ComplexityRoot.DataframeRecipeOptimizationDecision.EstimatedOptimizedWork == nil { + break + } -func (e *executableSchema) Exec(ctx context.Context) graphql.ResponseHandler { - opCtx := graphql.GetOperationContext(ctx) - ec := executionContext{opCtx, e, 0, 0, make(chan graphql.DeferredResult)} - inputUnmarshalMap := graphql.BuildUnmarshalerMap( - ec.unmarshalInputDataframeAggregateInput, - ec.unmarshalInputDataframeBuilderIntrospectionInput, - ec.unmarshalInputDataframeFilterInput, - ec.unmarshalInputDataframeRowsInput, - ec.unmarshalInputDataframeSortInput, - ec.unmarshalInputFhirAggregateInput, - ec.unmarshalInputFhirDataframeInput, - ec.unmarshalInputFhirFieldPredicateInput, - ec.unmarshalInputFhirFieldSelectInput, - ec.unmarshalInputFhirFieldSelectorInput, - ec.unmarshalInputFhirPivotInput, - ec.unmarshalInputFhirRepresentativeSliceInput, - ec.unmarshalInputFhirTraversalStepInput, - ) - first := true + return e.ComplexityRoot.DataframeRecipeOptimizationDecision.EstimatedOptimizedWork(childComplexity), true + case "DataframeRecipeOptimizationDecision.estimatedSavings": + if e.ComplexityRoot.DataframeRecipeOptimizationDecision.EstimatedSavings == nil { + break + } - switch opCtx.Operation.Operation { - case ast.Query: - return func(ctx context.Context) *graphql.Response { - var response graphql.Response - var data graphql.Marshaler - if first { - first = false - ctx = graphql.WithUnmarshalerMap(ctx, inputUnmarshalMap) - data = ec._Query(ctx, opCtx.Operation.SelectionSet) - } else { - if atomic.LoadInt32(&ec.pendingDeferred) > 0 { - result := <-ec.deferredResults - atomic.AddInt32(&ec.pendingDeferred, -1) - data = result.Result - response.Path = result.Path - response.Label = result.Label - response.Errors = result.Errors - } else { - return nil - } - } - var buf bytes.Buffer - data.MarshalGQL(&buf) - response.Data = buf.Bytes() - if atomic.LoadInt32(&ec.deferred) > 0 { - hasNext := atomic.LoadInt32(&ec.pendingDeferred) > 0 - response.HasNext = &hasNext - } + return e.ComplexityRoot.DataframeRecipeOptimizationDecision.EstimatedSavings(childComplexity), true + case "DataframeRecipeOptimizationDecision.reason": + if e.ComplexityRoot.DataframeRecipeOptimizationDecision.Reason == nil { + break + } - return &response + return e.ComplexityRoot.DataframeRecipeOptimizationDecision.Reason(childComplexity), true + case "DataframeRecipeOptimizationDecision.rule": + if e.ComplexityRoot.DataframeRecipeOptimizationDecision.Rule == nil { + break } - case ast.Mutation: - return func(ctx context.Context) *graphql.Response { - if !first { - return nil - } - first = false - ctx = graphql.WithUnmarshalerMap(ctx, inputUnmarshalMap) - data := ec._Mutation(ctx, opCtx.Operation.SelectionSet) - var buf bytes.Buffer - data.MarshalGQL(&buf) - return &graphql.Response{ - Data: buf.Bytes(), - } + return e.ComplexityRoot.DataframeRecipeOptimizationDecision.Rule(childComplexity), true + + case "DataframeRecipeOptimizationExplanation.decisions": + if e.ComplexityRoot.DataframeRecipeOptimizationExplanation.Decisions == nil { + break } - default: - return graphql.OneShot(graphql.ErrorResponse(ctx, "unsupported GraphQL operation")) - } -} + return e.ComplexityRoot.DataframeRecipeOptimizationExplanation.Decisions(childComplexity), true + case "DataframeRecipeOptimizationExplanation.enabled": + if e.ComplexityRoot.DataframeRecipeOptimizationExplanation.Enabled == nil { + break + } -type executionContext struct { - *graphql.OperationContext - *executableSchema - deferred int32 - pendingDeferred int32 - deferredResults chan graphql.DeferredResult -} - -func (ec *executionContext) processDeferredGroup(dg graphql.DeferredGroup) { - atomic.AddInt32(&ec.pendingDeferred, 1) - go func() { - ctx := graphql.WithFreshResponseContext(dg.Context) - dg.FieldSet.Dispatch(ctx) - ds := graphql.DeferredResult{ - Path: dg.Path, - Label: dg.Label, - Result: dg.FieldSet, - Errors: graphql.GetErrors(ctx), - } - // null fields should bubble up - if dg.FieldSet.Invalids > 0 { - ds.Result = graphql.Null - } - ec.deferredResults <- ds - }() -} + return e.ComplexityRoot.DataframeRecipeOptimizationExplanation.Enabled(childComplexity), true + case "DataframeRecipeOptimizationExplanation.minimumSavings": + if e.ComplexityRoot.DataframeRecipeOptimizationExplanation.MinimumSavings == nil { + break + } -func (ec *executionContext) introspectSchema() (*introspection.Schema, error) { - if ec.DisableIntrospection { - return nil, errors.New("introspection disabled") - } - return introspection.WrapSchema(ec.Schema()), nil -} + return e.ComplexityRoot.DataframeRecipeOptimizationExplanation.MinimumSavings(childComplexity), true + case "DataframeRecipeOptimizationExplanation.policy": + if e.ComplexityRoot.DataframeRecipeOptimizationExplanation.Policy == nil { + break + } -func (ec *executionContext) introspectType(name string) (*introspection.Type, error) { - if ec.DisableIntrospection { - return nil, errors.New("introspection disabled") - } - return introspection.WrapTypeFromDef(ec.Schema(), ec.Schema().Types[name]), nil -} + return e.ComplexityRoot.DataframeRecipeOptimizationExplanation.Policy(childComplexity), true + case "DataframeRecipeOptimizationExplanation.rules": + if e.ComplexityRoot.DataframeRecipeOptimizationExplanation.Rules == nil { + break + } -//go:embed "schema.graphqls" -var sourcesFS embed.FS + return e.ComplexityRoot.DataframeRecipeOptimizationExplanation.Rules(childComplexity), true -func sourceData(filename string) string { - data, err := sourcesFS.ReadFile(filename) - if err != nil { - panic(fmt.Sprintf("codegen problem: %s not available", filename)) - } - return string(data) -} + case "DataframeRecipeOptimizationRuleState.enabled": + if e.ComplexityRoot.DataframeRecipeOptimizationRuleState.Enabled == nil { + break + } -var sources = []*ast.Source{ - {Name: "schema.graphqls", Input: sourceData("schema.graphqls"), BuiltIn: false}, -} -var parsedSchema = gqlparser.MustLoadSchema(sources...) + return e.ComplexityRoot.DataframeRecipeOptimizationRuleState.Enabled(childComplexity), true + case "DataframeRecipeOptimizationRuleState.reason": + if e.ComplexityRoot.DataframeRecipeOptimizationRuleState.Reason == nil { + break + } -// endregion ************************** generated!.gotpl ************************** + return e.ComplexityRoot.DataframeRecipeOptimizationRuleState.Reason(childComplexity), true + case "DataframeRecipeOptimizationRuleState.rule": + if e.ComplexityRoot.DataframeRecipeOptimizationRuleState.Rule == nil { + break + } -// region ***************************** args.gotpl ***************************** + return e.ComplexityRoot.DataframeRecipeOptimizationRuleState.Rule(childComplexity), true -func (ec *executionContext) field_Mutation_runFhirDataframe_args(ctx context.Context, rawArgs map[string]any) (map[string]any, error) { - var err error - args := map[string]any{} - arg0, err := ec.field_Mutation_runFhirDataframe_argsInput(ctx, rawArgs) - if err != nil { - return nil, err - } - args["input"] = arg0 - arg1, err := ec.field_Mutation_runFhirDataframe_argsLimit(ctx, rawArgs) - if err != nil { - return nil, err - } - args["limit"] = arg1 - return args, nil -} -func (ec *executionContext) field_Mutation_runFhirDataframe_argsInput( - ctx context.Context, - rawArgs map[string]any, -) (model.FhirDataframeInput, error) { - if _, ok := rawArgs["input"]; !ok { - var zeroVal model.FhirDataframeInput - return zeroVal, nil - } + case "DataframeRecipeOutputExplanation.catalogProjections": + if e.ComplexityRoot.DataframeRecipeOutputExplanation.CatalogProjections == nil { + break + } - ctx = graphql.WithPathContext(ctx, graphql.NewPathWithField("input")) - if tmp, ok := rawArgs["input"]; ok { - return ec.unmarshalNFhirDataframeInput2githubᚗcomᚋcalyprᚋloomᚋgraphqlapiᚋmodelᚐFhirDataframeInput(ctx, tmp) - } + return e.ComplexityRoot.DataframeRecipeOutputExplanation.CatalogProjections(childComplexity), true + case "DataframeRecipeOutputExplanation.dynamicMaps": + if e.ComplexityRoot.DataframeRecipeOutputExplanation.DynamicMaps == nil { + break + } - var zeroVal model.FhirDataframeInput - return zeroVal, nil -} + return e.ComplexityRoot.DataframeRecipeOutputExplanation.DynamicMaps(childComplexity), true + case "DataframeRecipeOutputExplanation.expansion": + if e.ComplexityRoot.DataframeRecipeOutputExplanation.Expansion == nil { + break + } -func (ec *executionContext) field_Mutation_runFhirDataframe_argsLimit( - ctx context.Context, - rawArgs map[string]any, -) (*int, error) { - if _, ok := rawArgs["limit"]; !ok { - var zeroVal *int - return zeroVal, nil - } + return e.ComplexityRoot.DataframeRecipeOutputExplanation.Expansion(childComplexity), true + case "DataframeRecipeOutputExplanation.fields": + if e.ComplexityRoot.DataframeRecipeOutputExplanation.Fields == nil { + break + } - ctx = graphql.WithPathContext(ctx, graphql.NewPathWithField("limit")) - if tmp, ok := rawArgs["limit"]; ok { - return ec.unmarshalOInt2ᚖint(ctx, tmp) - } + return e.ComplexityRoot.DataframeRecipeOutputExplanation.Fields(childComplexity), true + case "DataframeRecipeOutputExplanation.identity": + if e.ComplexityRoot.DataframeRecipeOutputExplanation.Identity == nil { + break + } - var zeroVal *int - return zeroVal, nil -} + return e.ComplexityRoot.DataframeRecipeOutputExplanation.Identity(childComplexity), true + case "DataframeRecipeOutputExplanation.name": + if e.ComplexityRoot.DataframeRecipeOutputExplanation.Name == nil { + break + } -func (ec *executionContext) field_Query___type_args(ctx context.Context, rawArgs map[string]any) (map[string]any, error) { - var err error - args := map[string]any{} - arg0, err := ec.field_Query___type_argsName(ctx, rawArgs) - if err != nil { - return nil, err - } - args["name"] = arg0 - return args, nil -} -func (ec *executionContext) field_Query___type_argsName( - ctx context.Context, - rawArgs map[string]any, -) (string, error) { - if _, ok := rawArgs["name"]; !ok { - var zeroVal string - return zeroVal, nil - } + return e.ComplexityRoot.DataframeRecipeOutputExplanation.Name(childComplexity), true + case "DataframeRecipeOutputExplanation.rootResourceType": + if e.ComplexityRoot.DataframeRecipeOutputExplanation.RootResourceType == nil { + break + } - ctx = graphql.WithPathContext(ctx, graphql.NewPathWithField("name")) - if tmp, ok := rawArgs["name"]; ok { - return ec.unmarshalNString2string(ctx, tmp) - } + return e.ComplexityRoot.DataframeRecipeOutputExplanation.RootResourceType(childComplexity), true + case "DataframeRecipeOutputExplanation.rowGrain": + if e.ComplexityRoot.DataframeRecipeOutputExplanation.RowGrain == nil { + break + } - var zeroVal string - return zeroVal, nil -} + return e.ComplexityRoot.DataframeRecipeOutputExplanation.RowGrain(childComplexity), true -func (ec *executionContext) field_Query_dataframeAggregate_args(ctx context.Context, rawArgs map[string]any) (map[string]any, error) { - var err error - args := map[string]any{} - arg0, err := ec.field_Query_dataframeAggregate_argsInput(ctx, rawArgs) - if err != nil { - return nil, err - } - args["input"] = arg0 - return args, nil -} -func (ec *executionContext) field_Query_dataframeAggregate_argsInput( - ctx context.Context, - rawArgs map[string]any, -) (model.DataframeAggregateInput, error) { - if _, ok := rawArgs["input"]; !ok { - var zeroVal model.DataframeAggregateInput - return zeroVal, nil - } + case "DataframeRecipeOutputValidation.dynamicColumns": + if e.ComplexityRoot.DataframeRecipeOutputValidation.DynamicColumns == nil { + break + } - ctx = graphql.WithPathContext(ctx, graphql.NewPathWithField("input")) - if tmp, ok := rawArgs["input"]; ok { - return ec.unmarshalNDataframeAggregateInput2githubᚗcomᚋcalyprᚋloomᚋgraphqlapiᚋmodelᚐDataframeAggregateInput(ctx, tmp) - } + return e.ComplexityRoot.DataframeRecipeOutputValidation.DynamicColumns(childComplexity), true + case "DataframeRecipeOutputValidation.fieldNames": + if e.ComplexityRoot.DataframeRecipeOutputValidation.FieldNames == nil { + break + } - var zeroVal model.DataframeAggregateInput - return zeroVal, nil -} + return e.ComplexityRoot.DataframeRecipeOutputValidation.FieldNames(childComplexity), true + case "DataframeRecipeOutputValidation.name": + if e.ComplexityRoot.DataframeRecipeOutputValidation.Name == nil { + break + } -func (ec *executionContext) field_Query_dataframeBuilderIntrospection_args(ctx context.Context, rawArgs map[string]any) (map[string]any, error) { - var err error - args := map[string]any{} - arg0, err := ec.field_Query_dataframeBuilderIntrospection_argsInput(ctx, rawArgs) - if err != nil { - return nil, err - } - args["input"] = arg0 - return args, nil -} -func (ec *executionContext) field_Query_dataframeBuilderIntrospection_argsInput( - ctx context.Context, - rawArgs map[string]any, -) (model.DataframeBuilderIntrospectionInput, error) { - if _, ok := rawArgs["input"]; !ok { - var zeroVal model.DataframeBuilderIntrospectionInput - return zeroVal, nil - } + return e.ComplexityRoot.DataframeRecipeOutputValidation.Name(childComplexity), true + case "DataframeRecipeOutputValidation.rootResourceType": + if e.ComplexityRoot.DataframeRecipeOutputValidation.RootResourceType == nil { + break + } - ctx = graphql.WithPathContext(ctx, graphql.NewPathWithField("input")) - if tmp, ok := rawArgs["input"]; ok { - return ec.unmarshalNDataframeBuilderIntrospectionInput2githubᚗcomᚋcalyprᚋloomᚋgraphqlapiᚋmodelᚐDataframeBuilderIntrospectionInput(ctx, tmp) - } + return e.ComplexityRoot.DataframeRecipeOutputValidation.RootResourceType(childComplexity), true + case "DataframeRecipeOutputValidation.rowGrain": + if e.ComplexityRoot.DataframeRecipeOutputValidation.RowGrain == nil { + break + } - var zeroVal model.DataframeBuilderIntrospectionInput - return zeroVal, nil -} + return e.ComplexityRoot.DataframeRecipeOutputValidation.RowGrain(childComplexity), true -func (ec *executionContext) field_Query_dataframeMaterialization_args(ctx context.Context, rawArgs map[string]any) (map[string]any, error) { - var err error - args := map[string]any{} - arg0, err := ec.field_Query_dataframeMaterialization_argsID(ctx, rawArgs) - if err != nil { - return nil, err - } - args["id"] = arg0 - return args, nil -} -func (ec *executionContext) field_Query_dataframeMaterialization_argsID( - ctx context.Context, - rawArgs map[string]any, -) (string, error) { - if _, ok := rawArgs["id"]; !ok { - var zeroVal string - return zeroVal, nil - } + case "DataframeRecipePhysicalExplanation.outputs": + if e.ComplexityRoot.DataframeRecipePhysicalExplanation.Outputs == nil { + break + } - ctx = graphql.WithPathContext(ctx, graphql.NewPathWithField("id")) - if tmp, ok := rawArgs["id"]; ok { - return ec.unmarshalNID2string(ctx, tmp) - } + return e.ComplexityRoot.DataframeRecipePhysicalExplanation.Outputs(childComplexity), true - var zeroVal string - return zeroVal, nil -} + case "DataframeRecipePhysicalOutputExplanation.columns": + if e.ComplexityRoot.DataframeRecipePhysicalOutputExplanation.Columns == nil { + break + } -func (ec *executionContext) field_Query_dataframeRows_args(ctx context.Context, rawArgs map[string]any) (map[string]any, error) { - var err error - args := map[string]any{} - arg0, err := ec.field_Query_dataframeRows_argsInput(ctx, rawArgs) - if err != nil { - return nil, err - } - args["input"] = arg0 - return args, nil -} -func (ec *executionContext) field_Query_dataframeRows_argsInput( - ctx context.Context, - rawArgs map[string]any, -) (model.DataframeRowsInput, error) { - if _, ok := rawArgs["input"]; !ok { - var zeroVal model.DataframeRowsInput - return zeroVal, nil - } + return e.ComplexityRoot.DataframeRecipePhysicalOutputExplanation.Columns(childComplexity), true + case "DataframeRecipePhysicalOutputExplanation.endpointTraversalCount": + if e.ComplexityRoot.DataframeRecipePhysicalOutputExplanation.EndpointTraversalCount == nil { + break + } - ctx = graphql.WithPathContext(ctx, graphql.NewPathWithField("input")) - if tmp, ok := rawArgs["input"]; ok { - return ec.unmarshalNDataframeRowsInput2githubᚗcomᚋcalyprᚋloomᚋgraphqlapiᚋmodelᚐDataframeRowsInput(ctx, tmp) - } + return e.ComplexityRoot.DataframeRecipePhysicalOutputExplanation.EndpointTraversalCount(childComplexity), true + case "DataframeRecipePhysicalOutputExplanation.live": + if e.ComplexityRoot.DataframeRecipePhysicalOutputExplanation.Live == nil { + break + } - var zeroVal model.DataframeRowsInput - return zeroVal, nil -} + return e.ComplexityRoot.DataframeRecipePhysicalOutputExplanation.Live(childComplexity), true + case "DataframeRecipePhysicalOutputExplanation.name": + if e.ComplexityRoot.DataframeRecipePhysicalOutputExplanation.Name == nil { + break + } -func (ec *executionContext) field___Directive_args_args(ctx context.Context, rawArgs map[string]any) (map[string]any, error) { - var err error - args := map[string]any{} - arg0, err := ec.field___Directive_args_argsIncludeDeprecated(ctx, rawArgs) - if err != nil { - return nil, err - } - args["includeDeprecated"] = arg0 - return args, nil -} -func (ec *executionContext) field___Directive_args_argsIncludeDeprecated( - ctx context.Context, - rawArgs map[string]any, -) (*bool, error) { - if _, ok := rawArgs["includeDeprecated"]; !ok { - var zeroVal *bool - return zeroVal, nil - } + return e.ComplexityRoot.DataframeRecipePhysicalOutputExplanation.Name(childComplexity), true + case "DataframeRecipePhysicalOutputExplanation.nativeTraversalCount": + if e.ComplexityRoot.DataframeRecipePhysicalOutputExplanation.NativeTraversalCount == nil { + break + } - ctx = graphql.WithPathContext(ctx, graphql.NewPathWithField("includeDeprecated")) - if tmp, ok := rawArgs["includeDeprecated"]; ok { - return ec.unmarshalOBoolean2ᚖbool(ctx, tmp) - } + return e.ComplexityRoot.DataframeRecipePhysicalOutputExplanation.NativeTraversalCount(childComplexity), true + case "DataframeRecipePhysicalOutputExplanation.optimization": + if e.ComplexityRoot.DataframeRecipePhysicalOutputExplanation.Optimization == nil { + break + } - var zeroVal *bool - return zeroVal, nil -} + return e.ComplexityRoot.DataframeRecipePhysicalOutputExplanation.Optimization(childComplexity), true + case "DataframeRecipePhysicalOutputExplanation.planFingerprint": + if e.ComplexityRoot.DataframeRecipePhysicalOutputExplanation.PlanFingerprint == nil { + break + } -func (ec *executionContext) field___Field_args_args(ctx context.Context, rawArgs map[string]any) (map[string]any, error) { - var err error - args := map[string]any{} - arg0, err := ec.field___Field_args_argsIncludeDeprecated(ctx, rawArgs) - if err != nil { - return nil, err - } - args["includeDeprecated"] = arg0 - return args, nil -} -func (ec *executionContext) field___Field_args_argsIncludeDeprecated( - ctx context.Context, - rawArgs map[string]any, -) (*bool, error) { - if _, ok := rawArgs["includeDeprecated"]; !ok { - var zeroVal *bool - return zeroVal, nil - } + return e.ComplexityRoot.DataframeRecipePhysicalOutputExplanation.PlanFingerprint(childComplexity), true + case "DataframeRecipePhysicalOutputExplanation.requiredMatchReuseCount": + if e.ComplexityRoot.DataframeRecipePhysicalOutputExplanation.RequiredMatchReuseCount == nil { + break + } - ctx = graphql.WithPathContext(ctx, graphql.NewPathWithField("includeDeprecated")) - if tmp, ok := rawArgs["includeDeprecated"]; ok { - return ec.unmarshalOBoolean2ᚖbool(ctx, tmp) - } + return e.ComplexityRoot.DataframeRecipePhysicalOutputExplanation.RequiredMatchReuseCount(childComplexity), true + case "DataframeRecipePhysicalOutputExplanation.sharedTraversalCount": + if e.ComplexityRoot.DataframeRecipePhysicalOutputExplanation.SharedTraversalCount == nil { + break + } - var zeroVal *bool - return zeroVal, nil -} + return e.ComplexityRoot.DataframeRecipePhysicalOutputExplanation.SharedTraversalCount(childComplexity), true + case "DataframeRecipePhysicalOutputExplanation.traversalSets": + if e.ComplexityRoot.DataframeRecipePhysicalOutputExplanation.TraversalSets == nil { + break + } -func (ec *executionContext) field___Type_enumValues_args(ctx context.Context, rawArgs map[string]any) (map[string]any, error) { - var err error - args := map[string]any{} - arg0, err := ec.field___Type_enumValues_argsIncludeDeprecated(ctx, rawArgs) - if err != nil { - return nil, err - } - args["includeDeprecated"] = arg0 - return args, nil -} -func (ec *executionContext) field___Type_enumValues_argsIncludeDeprecated( - ctx context.Context, - rawArgs map[string]any, -) (bool, error) { - if _, ok := rawArgs["includeDeprecated"]; !ok { - var zeroVal bool - return zeroVal, nil - } + return e.ComplexityRoot.DataframeRecipePhysicalOutputExplanation.TraversalSets(childComplexity), true - ctx = graphql.WithPathContext(ctx, graphql.NewPathWithField("includeDeprecated")) - if tmp, ok := rawArgs["includeDeprecated"]; ok { - return ec.unmarshalOBoolean2bool(ctx, tmp) - } + case "DataframeRecipePreflight.columns": + if e.ComplexityRoot.DataframeRecipePreflight.Columns == nil { + break + } - var zeroVal bool - return zeroVal, nil -} + return e.ComplexityRoot.DataframeRecipePreflight.Columns(childComplexity), true + case "DataframeRecipePreflight.name": + if e.ComplexityRoot.DataframeRecipePreflight.Name == nil { + break + } -func (ec *executionContext) field___Type_fields_args(ctx context.Context, rawArgs map[string]any) (map[string]any, error) { - var err error - args := map[string]any{} - arg0, err := ec.field___Type_fields_argsIncludeDeprecated(ctx, rawArgs) - if err != nil { - return nil, err - } - args["includeDeprecated"] = arg0 - return args, nil -} -func (ec *executionContext) field___Type_fields_argsIncludeDeprecated( - ctx context.Context, - rawArgs map[string]any, -) (bool, error) { - if _, ok := rawArgs["includeDeprecated"]; !ok { - var zeroVal bool - return zeroVal, nil - } + return e.ComplexityRoot.DataframeRecipePreflight.Name(childComplexity), true + case "DataframeRecipePreflight.recipeDigest": + if e.ComplexityRoot.DataframeRecipePreflight.RecipeDigest == nil { + break + } - ctx = graphql.WithPathContext(ctx, graphql.NewPathWithField("includeDeprecated")) - if tmp, ok := rawArgs["includeDeprecated"]; ok { - return ec.unmarshalOBoolean2bool(ctx, tmp) - } + return e.ComplexityRoot.DataframeRecipePreflight.RecipeDigest(childComplexity), true + case "DataframeRecipePreflight.resolvedSchemaDigest": + if e.ComplexityRoot.DataframeRecipePreflight.ResolvedSchemaDigest == nil { + break + } - var zeroVal bool - return zeroVal, nil -} + return e.ComplexityRoot.DataframeRecipePreflight.ResolvedSchemaDigest(childComplexity), true + case "DataframeRecipePreflight.scopeDigest": + if e.ComplexityRoot.DataframeRecipePreflight.ScopeDigest == nil { + break + } -// endregion ***************************** args.gotpl ***************************** + return e.ComplexityRoot.DataframeRecipePreflight.ScopeDigest(childComplexity), true + case "DataframeRecipePreflight.sourceGeneration": + if e.ComplexityRoot.DataframeRecipePreflight.SourceGeneration == nil { + break + } -// region ************************** directives.gotpl ************************** + return e.ComplexityRoot.DataframeRecipePreflight.SourceGeneration(childComplexity), true -// endregion ************************** directives.gotpl ************************** + case "DataframeRecipePreview.name": + if e.ComplexityRoot.DataframeRecipePreview.Name == nil { + break + } -// region **************************** field.gotpl ***************************** + return e.ComplexityRoot.DataframeRecipePreview.Name(childComplexity), true + case "DataframeRecipePreview.outputs": + if e.ComplexityRoot.DataframeRecipePreview.Outputs == nil { + break + } -func (ec *executionContext) _DataframeAggregateResult_materialization(ctx context.Context, field graphql.CollectedField, obj *model.DataframeAggregateResult) (ret graphql.Marshaler) { - fc, err := ec.fieldContext_DataframeAggregateResult_materialization(ctx, field) - if err != nil { - return graphql.Null - } - ctx = graphql.WithFieldContext(ctx, fc) - defer func() { - if r := recover(); r != nil { - ec.Error(ctx, ec.Recover(ctx, r)) - ret = graphql.Null + return e.ComplexityRoot.DataframeRecipePreview.Outputs(childComplexity), true + case "DataframeRecipePreview.recipeDigest": + if e.ComplexityRoot.DataframeRecipePreview.RecipeDigest == nil { + break } - }() - resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { - ctx = rctx // use context from middleware stack in children - return obj.Materialization, nil - }) - if err != nil { - ec.Error(ctx, err) - return graphql.Null - } - if resTmp == nil { - if !graphql.HasFieldError(ctx, fc) { - ec.Errorf(ctx, "must not be null") + + return e.ComplexityRoot.DataframeRecipePreview.RecipeDigest(childComplexity), true + case "DataframeRecipePreview.resolvedSchemaDigest": + if e.ComplexityRoot.DataframeRecipePreview.ResolvedSchemaDigest == nil { + break } - return graphql.Null - } - res := resTmp.(*model.DataframeMaterialization) - fc.Result = res - return ec.marshalNDataframeMaterialization2ᚖgithubᚗcomᚋcalyprᚋloomᚋgraphqlapiᚋmodelᚐDataframeMaterialization(ctx, field.Selections, res) -} -func (ec *executionContext) fieldContext_DataframeAggregateResult_materialization(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { - fc = &graphql.FieldContext{ - Object: "DataframeAggregateResult", - Field: field, - IsMethod: false, - IsResolver: false, - Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { - switch field.Name { - case "id": - return ec.fieldContext_DataframeMaterialization_id(ctx, field) - case "name": - return ec.fieldContext_DataframeMaterialization_name(ctx, field) - case "project": - return ec.fieldContext_DataframeMaterialization_project(ctx, field) - case "datasetGeneration": - return ec.fieldContext_DataframeMaterialization_datasetGeneration(ctx, field) - case "state": - return ec.fieldContext_DataframeMaterialization_state(ctx, field) - case "columns": - return ec.fieldContext_DataframeMaterialization_columns(ctx, field) - case "rowCount": - return ec.fieldContext_DataframeMaterialization_rowCount(ctx, field) - case "createdAt": - return ec.fieldContext_DataframeMaterialization_createdAt(ctx, field) - case "readyAt": - return ec.fieldContext_DataframeMaterialization_readyAt(ctx, field) - case "error": - return ec.fieldContext_DataframeMaterialization_error(ctx, field) - } - return nil, fmt.Errorf("no field named %q was found under type DataframeMaterialization", field.Name) - }, - } - return fc, nil -} + return e.ComplexityRoot.DataframeRecipePreview.ResolvedSchemaDigest(childComplexity), true + case "DataframeRecipePreview.sourceGeneration": + if e.ComplexityRoot.DataframeRecipePreview.SourceGeneration == nil { + break + } -func (ec *executionContext) _DataframeAggregateResult_columns(ctx context.Context, field graphql.CollectedField, obj *model.DataframeAggregateResult) (ret graphql.Marshaler) { - fc, err := ec.fieldContext_DataframeAggregateResult_columns(ctx, field) - if err != nil { - return graphql.Null - } - ctx = graphql.WithFieldContext(ctx, fc) - defer func() { - if r := recover(); r != nil { - ec.Error(ctx, ec.Recover(ctx, r)) - ret = graphql.Null + return e.ComplexityRoot.DataframeRecipePreview.SourceGeneration(childComplexity), true + + case "DataframeRecipePreviewOutput.csv": + if e.ComplexityRoot.DataframeRecipePreviewOutput.CSV == nil { + break } - }() - resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { - ctx = rctx // use context from middleware stack in children - return obj.Columns, nil - }) - if err != nil { - ec.Error(ctx, err) - return graphql.Null - } - if resTmp == nil { - if !graphql.HasFieldError(ctx, fc) { - ec.Errorf(ctx, "must not be null") + + return e.ComplexityRoot.DataframeRecipePreviewOutput.CSV(childComplexity), true + case "DataframeRecipePreviewOutput.columns": + if e.ComplexityRoot.DataframeRecipePreviewOutput.Columns == nil { + break } - return graphql.Null - } - res := resTmp.([]string) - fc.Result = res - return ec.marshalNString2ᚕstringᚄ(ctx, field.Selections, res) -} -func (ec *executionContext) fieldContext_DataframeAggregateResult_columns(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { - fc = &graphql.FieldContext{ - Object: "DataframeAggregateResult", - Field: field, - IsMethod: false, - IsResolver: false, - Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { - return nil, errors.New("field of type String does not have child fields") - }, - } - return fc, nil -} + return e.ComplexityRoot.DataframeRecipePreviewOutput.Columns(childComplexity), true + case "DataframeRecipePreviewOutput.name": + if e.ComplexityRoot.DataframeRecipePreviewOutput.Name == nil { + break + } -func (ec *executionContext) _DataframeAggregateResult_rows(ctx context.Context, field graphql.CollectedField, obj *model.DataframeAggregateResult) (ret graphql.Marshaler) { - fc, err := ec.fieldContext_DataframeAggregateResult_rows(ctx, field) - if err != nil { - return graphql.Null - } - ctx = graphql.WithFieldContext(ctx, fc) - defer func() { - if r := recover(); r != nil { - ec.Error(ctx, ec.Recover(ctx, r)) - ret = graphql.Null + return e.ComplexityRoot.DataframeRecipePreviewOutput.Name(childComplexity), true + case "DataframeRecipePreviewOutput.rowCount": + if e.ComplexityRoot.DataframeRecipePreviewOutput.RowCount == nil { + break } - }() - resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { - ctx = rctx // use context from middleware stack in children - return obj.Rows, nil - }) - if err != nil { - ec.Error(ctx, err) - return graphql.Null - } - if resTmp == nil { - if !graphql.HasFieldError(ctx, fc) { - ec.Errorf(ctx, "must not be null") + + return e.ComplexityRoot.DataframeRecipePreviewOutput.RowCount(childComplexity), true + case "DataframeRecipePreviewOutput.rows": + if e.ComplexityRoot.DataframeRecipePreviewOutput.Rows == nil { + break } - return graphql.Null - } - res := resTmp.(json.RawMessage) - fc.Result = res - return ec.marshalNJSON2encodingᚋjsonᚐRawMessage(ctx, field.Selections, res) -} -func (ec *executionContext) fieldContext_DataframeAggregateResult_rows(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { - fc = &graphql.FieldContext{ - Object: "DataframeAggregateResult", - Field: field, - IsMethod: false, - IsResolver: false, - Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { - return nil, errors.New("field of type JSON does not have child fields") - }, - } - return fc, nil -} + return e.ComplexityRoot.DataframeRecipePreviewOutput.Rows(childComplexity), true -func (ec *executionContext) _DataframeBuilderIntrospection_project(ctx context.Context, field graphql.CollectedField, obj *model.DataframeBuilderIntrospection) (ret graphql.Marshaler) { - fc, err := ec.fieldContext_DataframeBuilderIntrospection_project(ctx, field) - if err != nil { - return graphql.Null - } - ctx = graphql.WithFieldContext(ctx, fc) - defer func() { - if r := recover(); r != nil { - ec.Error(ctx, ec.Recover(ctx, r)) - ret = graphql.Null + case "DataframeRecipeResult.name": + if e.ComplexityRoot.DataframeRecipeResult.Name == nil { + break } - }() - resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { - ctx = rctx // use context from middleware stack in children - return obj.Project, nil - }) - if err != nil { - ec.Error(ctx, err) - return graphql.Null - } - if resTmp == nil { - if !graphql.HasFieldError(ctx, fc) { - ec.Errorf(ctx, "must not be null") + + return e.ComplexityRoot.DataframeRecipeResult.Name(childComplexity), true + case "DataframeRecipeResult.outputs": + if e.ComplexityRoot.DataframeRecipeResult.Outputs == nil { + break } - return graphql.Null - } - res := resTmp.(string) - fc.Result = res - return ec.marshalNString2string(ctx, field.Selections, res) -} -func (ec *executionContext) fieldContext_DataframeBuilderIntrospection_project(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { - fc = &graphql.FieldContext{ - Object: "DataframeBuilderIntrospection", - Field: field, - IsMethod: false, - IsResolver: false, - Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { - return nil, errors.New("field of type String does not have child fields") - }, - } - return fc, nil -} + return e.ComplexityRoot.DataframeRecipeResult.Outputs(childComplexity), true + case "DataframeRecipeResult.recipeDigest": + if e.ComplexityRoot.DataframeRecipeResult.RecipeDigest == nil { + break + } -func (ec *executionContext) _DataframeBuilderIntrospection_rootResourceType(ctx context.Context, field graphql.CollectedField, obj *model.DataframeBuilderIntrospection) (ret graphql.Marshaler) { - fc, err := ec.fieldContext_DataframeBuilderIntrospection_rootResourceType(ctx, field) - if err != nil { - return graphql.Null - } - ctx = graphql.WithFieldContext(ctx, fc) - defer func() { - if r := recover(); r != nil { - ec.Error(ctx, ec.Recover(ctx, r)) - ret = graphql.Null + return e.ComplexityRoot.DataframeRecipeResult.RecipeDigest(childComplexity), true + case "DataframeRecipeResult.resolvedSchemaDigest": + if e.ComplexityRoot.DataframeRecipeResult.ResolvedSchemaDigest == nil { + break } - }() - resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { - ctx = rctx // use context from middleware stack in children - return obj.RootResourceType, nil - }) - if err != nil { - ec.Error(ctx, err) - return graphql.Null - } - if resTmp == nil { - if !graphql.HasFieldError(ctx, fc) { - ec.Errorf(ctx, "must not be null") + + return e.ComplexityRoot.DataframeRecipeResult.ResolvedSchemaDigest(childComplexity), true + case "DataframeRecipeResult.sourceGeneration": + if e.ComplexityRoot.DataframeRecipeResult.SourceGeneration == nil { + break } - return graphql.Null - } - res := resTmp.(string) - fc.Result = res - return ec.marshalNString2string(ctx, field.Selections, res) -} -func (ec *executionContext) fieldContext_DataframeBuilderIntrospection_rootResourceType(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { - fc = &graphql.FieldContext{ - Object: "DataframeBuilderIntrospection", - Field: field, - IsMethod: false, - IsResolver: false, - Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { - return nil, errors.New("field of type String does not have child fields") - }, - } - return fc, nil -} + return e.ComplexityRoot.DataframeRecipeResult.SourceGeneration(childComplexity), true -func (ec *executionContext) _DataframeBuilderIntrospection_authResourcePaths(ctx context.Context, field graphql.CollectedField, obj *model.DataframeBuilderIntrospection) (ret graphql.Marshaler) { - fc, err := ec.fieldContext_DataframeBuilderIntrospection_authResourcePaths(ctx, field) - if err != nil { - return graphql.Null - } - ctx = graphql.WithFieldContext(ctx, fc) - defer func() { - if r := recover(); r != nil { - ec.Error(ctx, ec.Recover(ctx, r)) - ret = graphql.Null + case "DataframeRecipeResultOutput.csv": + if e.ComplexityRoot.DataframeRecipeResultOutput.CSV == nil { + break } - }() - resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { - ctx = rctx // use context from middleware stack in children - return obj.AuthResourcePaths, nil - }) - if err != nil { - ec.Error(ctx, err) - return graphql.Null - } - if resTmp == nil { - if !graphql.HasFieldError(ctx, fc) { - ec.Errorf(ctx, "must not be null") + + return e.ComplexityRoot.DataframeRecipeResultOutput.CSV(childComplexity), true + case "DataframeRecipeResultOutput.columns": + if e.ComplexityRoot.DataframeRecipeResultOutput.Columns == nil { + break } - return graphql.Null - } - res := resTmp.([]string) - fc.Result = res - return ec.marshalNString2ᚕstringᚄ(ctx, field.Selections, res) -} -func (ec *executionContext) fieldContext_DataframeBuilderIntrospection_authResourcePaths(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { - fc = &graphql.FieldContext{ - Object: "DataframeBuilderIntrospection", - Field: field, - IsMethod: false, - IsResolver: false, - Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { - return nil, errors.New("field of type String does not have child fields") - }, - } - return fc, nil -} + return e.ComplexityRoot.DataframeRecipeResultOutput.Columns(childComplexity), true + case "DataframeRecipeResultOutput.name": + if e.ComplexityRoot.DataframeRecipeResultOutput.Name == nil { + break + } -func (ec *executionContext) _DataframeBuilderIntrospection_root(ctx context.Context, field graphql.CollectedField, obj *model.DataframeBuilderIntrospection) (ret graphql.Marshaler) { - fc, err := ec.fieldContext_DataframeBuilderIntrospection_root(ctx, field) - if err != nil { - return graphql.Null - } - ctx = graphql.WithFieldContext(ctx, fc) - defer func() { - if r := recover(); r != nil { - ec.Error(ctx, ec.Recover(ctx, r)) - ret = graphql.Null + return e.ComplexityRoot.DataframeRecipeResultOutput.Name(childComplexity), true + case "DataframeRecipeResultOutput.rowCount": + if e.ComplexityRoot.DataframeRecipeResultOutput.RowCount == nil { + break } - }() - resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { - ctx = rctx // use context from middleware stack in children - return obj.Root, nil - }) - if err != nil { - ec.Error(ctx, err) - return graphql.Null - } - if resTmp == nil { - if !graphql.HasFieldError(ctx, fc) { - ec.Errorf(ctx, "must not be null") + + return e.ComplexityRoot.DataframeRecipeResultOutput.RowCount(childComplexity), true + case "DataframeRecipeResultOutput.rows": + if e.ComplexityRoot.DataframeRecipeResultOutput.Rows == nil { + break } - return graphql.Null - } - res := resTmp.(*model.DataframeResourceHints) - fc.Result = res - return ec.marshalNDataframeResourceHints2ᚖgithubᚗcomᚋcalyprᚋloomᚋgraphqlapiᚋmodelᚐDataframeResourceHints(ctx, field.Selections, res) -} -func (ec *executionContext) fieldContext_DataframeBuilderIntrospection_root(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { - fc = &graphql.FieldContext{ - Object: "DataframeBuilderIntrospection", - Field: field, - IsMethod: false, - IsResolver: false, - Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { - switch field.Name { - case "resourceType": - return ec.fieldContext_DataframeResourceHints_resourceType(ctx, field) - case "fields": - return ec.fieldContext_DataframeResourceHints_fields(ctx, field) - case "pivotFields": - return ec.fieldContext_DataframeResourceHints_pivotFields(ctx, field) - case "traversals": - return ec.fieldContext_DataframeResourceHints_traversals(ctx, field) - } - return nil, fmt.Errorf("no field named %q was found under type DataframeResourceHints", field.Name) - }, - } - return fc, nil -} + return e.ComplexityRoot.DataframeRecipeResultOutput.Rows(childComplexity), true -func (ec *executionContext) _DataframeBuilderIntrospection_relatedResources(ctx context.Context, field graphql.CollectedField, obj *model.DataframeBuilderIntrospection) (ret graphql.Marshaler) { - fc, err := ec.fieldContext_DataframeBuilderIntrospection_relatedResources(ctx, field) - if err != nil { - return graphql.Null - } - ctx = graphql.WithFieldContext(ctx, fc) - defer func() { - if r := recover(); r != nil { - ec.Error(ctx, ec.Recover(ctx, r)) - ret = graphql.Null - } - }() - resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { - ctx = rctx // use context from middleware stack in children - return obj.RelatedResources, nil - }) - if err != nil { - ec.Error(ctx, err) - return graphql.Null - } - if resTmp == nil { - if !graphql.HasFieldError(ctx, fc) { - ec.Errorf(ctx, "must not be null") + case "DataframeRecipeValidation.name": + if e.ComplexityRoot.DataframeRecipeValidation.Name == nil { + break } - return graphql.Null - } - res := resTmp.([]*model.DataframeRelatedResourceHints) - fc.Result = res - return ec.marshalNDataframeRelatedResourceHints2ᚕᚖgithubᚗcomᚋcalyprᚋloomᚋgraphqlapiᚋmodelᚐDataframeRelatedResourceHintsᚄ(ctx, field.Selections, res) -} -func (ec *executionContext) fieldContext_DataframeBuilderIntrospection_relatedResources(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { - fc = &graphql.FieldContext{ - Object: "DataframeBuilderIntrospection", - Field: field, - IsMethod: false, - IsResolver: false, - Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { - switch field.Name { - case "viaLabel": - return ec.fieldContext_DataframeRelatedResourceHints_viaLabel(ctx, field) - case "edgeCount": - return ec.fieldContext_DataframeRelatedResourceHints_edgeCount(ctx, field) - case "target": - return ec.fieldContext_DataframeRelatedResourceHints_target(ctx, field) - } - return nil, fmt.Errorf("no field named %q was found under type DataframeRelatedResourceHints", field.Name) - }, - } - return fc, nil -} + return e.ComplexityRoot.DataframeRecipeValidation.Name(childComplexity), true + case "DataframeRecipeValidation.outputs": + if e.ComplexityRoot.DataframeRecipeValidation.Outputs == nil { + break + } -func (ec *executionContext) _DataframeBuilderIntrospection_traversals(ctx context.Context, field graphql.CollectedField, obj *model.DataframeBuilderIntrospection) (ret graphql.Marshaler) { - fc, err := ec.fieldContext_DataframeBuilderIntrospection_traversals(ctx, field) - if err != nil { - return graphql.Null - } - ctx = graphql.WithFieldContext(ctx, fc) - defer func() { - if r := recover(); r != nil { - ec.Error(ctx, ec.Recover(ctx, r)) - ret = graphql.Null + return e.ComplexityRoot.DataframeRecipeValidation.Outputs(childComplexity), true + case "DataframeRecipeValidation.recipeDigest": + if e.ComplexityRoot.DataframeRecipeValidation.RecipeDigest == nil { + break } - }() - resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { - ctx = rctx // use context from middleware stack in children - return obj.Traversals, nil - }) - if err != nil { - ec.Error(ctx, err) - return graphql.Null - } - if resTmp == nil { - if !graphql.HasFieldError(ctx, fc) { - ec.Errorf(ctx, "must not be null") + + return e.ComplexityRoot.DataframeRecipeValidation.RecipeDigest(childComplexity), true + case "DataframeRecipeValidation.translationVersion": + if e.ComplexityRoot.DataframeRecipeValidation.TranslationVersion == nil { + break } - return graphql.Null - } - res := resTmp.([]*model.DataframeTraversalHint) - fc.Result = res - return ec.marshalNDataframeTraversalHint2ᚕᚖgithubᚗcomᚋcalyprᚋloomᚋgraphqlapiᚋmodelᚐDataframeTraversalHintᚄ(ctx, field.Selections, res) -} -func (ec *executionContext) fieldContext_DataframeBuilderIntrospection_traversals(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { - fc = &graphql.FieldContext{ - Object: "DataframeBuilderIntrospection", - Field: field, - IsMethod: false, - IsResolver: false, - Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { - switch field.Name { - case "fromType": - return ec.fieldContext_DataframeTraversalHint_fromType(ctx, field) - case "label": - return ec.fieldContext_DataframeTraversalHint_label(ctx, field) - case "toType": - return ec.fieldContext_DataframeTraversalHint_toType(ctx, field) - case "edgeCount": - return ec.fieldContext_DataframeTraversalHint_edgeCount(ctx, field) - } - return nil, fmt.Errorf("no field named %q was found under type DataframeTraversalHint", field.Name) - }, - } - return fc, nil -} + return e.ComplexityRoot.DataframeRecipeValidation.TranslationVersion(childComplexity), true -func (ec *executionContext) _DataframeBuilderIntrospection_fields(ctx context.Context, field graphql.CollectedField, obj *model.DataframeBuilderIntrospection) (ret graphql.Marshaler) { - fc, err := ec.fieldContext_DataframeBuilderIntrospection_fields(ctx, field) - if err != nil { - return graphql.Null - } - ctx = graphql.WithFieldContext(ctx, fc) - defer func() { - if r := recover(); r != nil { - ec.Error(ctx, ec.Recover(ctx, r)) - ret = graphql.Null - } - }() - resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { - ctx = rctx // use context from middleware stack in children - return obj.Fields, nil - }) - if err != nil { - ec.Error(ctx, err) - return graphql.Null - } - if resTmp == nil { - if !graphql.HasFieldError(ctx, fc) { - ec.Errorf(ctx, "must not be null") + case "DataframeRelatedResourceHints.edgeCount": + if e.ComplexityRoot.DataframeRelatedResourceHints.EdgeCount == nil { + break } - return graphql.Null - } - res := resTmp.([]*model.DataframeFieldHint) - fc.Result = res - return ec.marshalNDataframeFieldHint2ᚕᚖgithubᚗcomᚋcalyprᚋloomᚋgraphqlapiᚋmodelᚐDataframeFieldHintᚄ(ctx, field.Selections, res) -} -func (ec *executionContext) fieldContext_DataframeBuilderIntrospection_fields(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { - fc = &graphql.FieldContext{ - Object: "DataframeBuilderIntrospection", - Field: field, - IsMethod: false, - IsResolver: false, - Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { - switch field.Name { - case "resourceType": - return ec.fieldContext_DataframeFieldHint_resourceType(ctx, field) - case "fieldRef": - return ec.fieldContext_DataframeFieldHint_fieldRef(ctx, field) - case "label": - return ec.fieldContext_DataframeFieldHint_label(ctx, field) - case "path": - return ec.fieldContext_DataframeFieldHint_path(ctx, field) - case "selector": - return ec.fieldContext_DataframeFieldHint_selector(ctx, field) - case "kind": - return ec.fieldContext_DataframeFieldHint_kind(ctx, field) - case "docCount": - return ec.fieldContext_DataframeFieldHint_docCount(ctx, field) - case "sampleCount": - return ec.fieldContext_DataframeFieldHint_sampleCount(ctx, field) - case "distinctValues": - return ec.fieldContext_DataframeFieldHint_distinctValues(ctx, field) - case "distinctTruncated": - return ec.fieldContext_DataframeFieldHint_distinctTruncated(ctx, field) - case "pivotCandidate": - return ec.fieldContext_DataframeFieldHint_pivotCandidate(ctx, field) - case "pivotKind": - return ec.fieldContext_DataframeFieldHint_pivotKind(ctx, field) - case "pivotColumns": - return ec.fieldContext_DataframeFieldHint_pivotColumns(ctx, field) - case "pivotFamily": - return ec.fieldContext_DataframeFieldHint_pivotFamily(ctx, field) - case "defaultPivotColumnSelector": - return ec.fieldContext_DataframeFieldHint_defaultPivotColumnSelector(ctx, field) - case "defaultPivotValueSelector": - return ec.fieldContext_DataframeFieldHint_defaultPivotValueSelector(ctx, field) - } - return nil, fmt.Errorf("no field named %q was found under type DataframeFieldHint", field.Name) - }, - } - return fc, nil -} + return e.ComplexityRoot.DataframeRelatedResourceHints.EdgeCount(childComplexity), true + case "DataframeRelatedResourceHints.target": + if e.ComplexityRoot.DataframeRelatedResourceHints.Target == nil { + break + } -func (ec *executionContext) _DataframeBuilderIntrospection_pivotFields(ctx context.Context, field graphql.CollectedField, obj *model.DataframeBuilderIntrospection) (ret graphql.Marshaler) { - fc, err := ec.fieldContext_DataframeBuilderIntrospection_pivotFields(ctx, field) - if err != nil { - return graphql.Null - } - ctx = graphql.WithFieldContext(ctx, fc) - defer func() { - if r := recover(); r != nil { - ec.Error(ctx, ec.Recover(ctx, r)) - ret = graphql.Null + return e.ComplexityRoot.DataframeRelatedResourceHints.Target(childComplexity), true + case "DataframeRelatedResourceHints.viaLabel": + if e.ComplexityRoot.DataframeRelatedResourceHints.ViaLabel == nil { + break } - }() - resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { - ctx = rctx // use context from middleware stack in children - return obj.PivotFields, nil - }) - if err != nil { - ec.Error(ctx, err) - return graphql.Null - } - if resTmp == nil { - if !graphql.HasFieldError(ctx, fc) { - ec.Errorf(ctx, "must not be null") + + return e.ComplexityRoot.DataframeRelatedResourceHints.ViaLabel(childComplexity), true + + case "DataframeResourceHints.fields": + if e.ComplexityRoot.DataframeResourceHints.Fields == nil { + break } - return graphql.Null - } - res := resTmp.([]*model.DataframeFieldHint) - fc.Result = res - return ec.marshalNDataframeFieldHint2ᚕᚖgithubᚗcomᚋcalyprᚋloomᚋgraphqlapiᚋmodelᚐDataframeFieldHintᚄ(ctx, field.Selections, res) -} -func (ec *executionContext) fieldContext_DataframeBuilderIntrospection_pivotFields(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { - fc = &graphql.FieldContext{ - Object: "DataframeBuilderIntrospection", - Field: field, - IsMethod: false, - IsResolver: false, - Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { - switch field.Name { - case "resourceType": - return ec.fieldContext_DataframeFieldHint_resourceType(ctx, field) - case "fieldRef": - return ec.fieldContext_DataframeFieldHint_fieldRef(ctx, field) - case "label": - return ec.fieldContext_DataframeFieldHint_label(ctx, field) - case "path": - return ec.fieldContext_DataframeFieldHint_path(ctx, field) - case "selector": - return ec.fieldContext_DataframeFieldHint_selector(ctx, field) - case "kind": - return ec.fieldContext_DataframeFieldHint_kind(ctx, field) - case "docCount": - return ec.fieldContext_DataframeFieldHint_docCount(ctx, field) - case "sampleCount": - return ec.fieldContext_DataframeFieldHint_sampleCount(ctx, field) - case "distinctValues": - return ec.fieldContext_DataframeFieldHint_distinctValues(ctx, field) - case "distinctTruncated": - return ec.fieldContext_DataframeFieldHint_distinctTruncated(ctx, field) - case "pivotCandidate": - return ec.fieldContext_DataframeFieldHint_pivotCandidate(ctx, field) - case "pivotKind": - return ec.fieldContext_DataframeFieldHint_pivotKind(ctx, field) - case "pivotColumns": - return ec.fieldContext_DataframeFieldHint_pivotColumns(ctx, field) - case "pivotFamily": - return ec.fieldContext_DataframeFieldHint_pivotFamily(ctx, field) - case "defaultPivotColumnSelector": - return ec.fieldContext_DataframeFieldHint_defaultPivotColumnSelector(ctx, field) - case "defaultPivotValueSelector": - return ec.fieldContext_DataframeFieldHint_defaultPivotValueSelector(ctx, field) - } - return nil, fmt.Errorf("no field named %q was found under type DataframeFieldHint", field.Name) - }, - } - return fc, nil -} + return e.ComplexityRoot.DataframeResourceHints.Fields(childComplexity), true + case "DataframeResourceHints.pivotFields": + if e.ComplexityRoot.DataframeResourceHints.PivotFields == nil { + break + } -func (ec *executionContext) _DataframeColumn_name(ctx context.Context, field graphql.CollectedField, obj *model.DataframeColumn) (ret graphql.Marshaler) { - fc, err := ec.fieldContext_DataframeColumn_name(ctx, field) - if err != nil { - return graphql.Null - } - ctx = graphql.WithFieldContext(ctx, fc) - defer func() { - if r := recover(); r != nil { - ec.Error(ctx, ec.Recover(ctx, r)) - ret = graphql.Null + return e.ComplexityRoot.DataframeResourceHints.PivotFields(childComplexity), true + case "DataframeResourceHints.resourceType": + if e.ComplexityRoot.DataframeResourceHints.ResourceType == nil { + break } - }() - resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { - ctx = rctx // use context from middleware stack in children - return obj.Name, nil - }) - if err != nil { - ec.Error(ctx, err) - return graphql.Null - } - if resTmp == nil { - if !graphql.HasFieldError(ctx, fc) { - ec.Errorf(ctx, "must not be null") + + return e.ComplexityRoot.DataframeResourceHints.ResourceType(childComplexity), true + case "DataframeResourceHints.traversals": + if e.ComplexityRoot.DataframeResourceHints.Traversals == nil { + break } - return graphql.Null - } - res := resTmp.(string) - fc.Result = res - return ec.marshalNString2string(ctx, field.Selections, res) -} -func (ec *executionContext) fieldContext_DataframeColumn_name(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { - fc = &graphql.FieldContext{ - Object: "DataframeColumn", - Field: field, - IsMethod: false, - IsResolver: false, - Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { - return nil, errors.New("field of type String does not have child fields") - }, - } - return fc, nil -} + return e.ComplexityRoot.DataframeResourceHints.Traversals(childComplexity), true -func (ec *executionContext) _DataframeColumn_clickhouseType(ctx context.Context, field graphql.CollectedField, obj *model.DataframeColumn) (ret graphql.Marshaler) { - fc, err := ec.fieldContext_DataframeColumn_clickhouseType(ctx, field) - if err != nil { - return graphql.Null - } - ctx = graphql.WithFieldContext(ctx, fc) - defer func() { - if r := recover(); r != nil { - ec.Error(ctx, ec.Recover(ctx, r)) - ret = graphql.Null + case "DataframeRichSourceReuse.aggregateConsumers": + if e.ComplexityRoot.DataframeRichSourceReuse.AggregateConsumers == nil { + break } - }() - resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { - ctx = rctx // use context from middleware stack in children - return obj.ClickhouseType, nil - }) - if err != nil { - ec.Error(ctx, err) - return graphql.Null - } - if resTmp == nil { - if !graphql.HasFieldError(ctx, fc) { - ec.Errorf(ctx, "must not be null") + + return e.ComplexityRoot.DataframeRichSourceReuse.AggregateConsumers(childComplexity), true + case "DataframeRichSourceReuse.pivotConsumers": + if e.ComplexityRoot.DataframeRichSourceReuse.PivotConsumers == nil { + break } - return graphql.Null - } - res := resTmp.(string) - fc.Result = res - return ec.marshalNString2string(ctx, field.Selections, res) -} -func (ec *executionContext) fieldContext_DataframeColumn_clickhouseType(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { - fc = &graphql.FieldContext{ - Object: "DataframeColumn", - Field: field, - IsMethod: false, - IsResolver: false, - Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { - return nil, errors.New("field of type String does not have child fields") - }, - } - return fc, nil -} + return e.ComplexityRoot.DataframeRichSourceReuse.PivotConsumers(childComplexity), true + case "DataframeRichSourceReuse.sliceConsumers": + if e.ComplexityRoot.DataframeRichSourceReuse.SliceConsumers == nil { + break + } -func (ec *executionContext) _DataframeCompilerPlanDiagnostics_traversalSets(ctx context.Context, field graphql.CollectedField, obj *model.DataframeCompilerPlanDiagnostics) (ret graphql.Marshaler) { - fc, err := ec.fieldContext_DataframeCompilerPlanDiagnostics_traversalSets(ctx, field) - if err != nil { - return graphql.Null - } - ctx = graphql.WithFieldContext(ctx, fc) - defer func() { - if r := recover(); r != nil { - ec.Error(ctx, ec.Recover(ctx, r)) - ret = graphql.Null + return e.ComplexityRoot.DataframeRichSourceReuse.SliceConsumers(childComplexity), true + case "DataframeRichSourceReuse.sourceSet": + if e.ComplexityRoot.DataframeRichSourceReuse.SourceSet == nil { + break } - }() - resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { - ctx = rctx // use context from middleware stack in children - return obj.TraversalSets, nil - }) - if err != nil { - ec.Error(ctx, err) - return graphql.Null - } - if resTmp == nil { - if !graphql.HasFieldError(ctx, fc) { - ec.Errorf(ctx, "must not be null") + + return e.ComplexityRoot.DataframeRichSourceReuse.SourceSet(childComplexity), true + case "DataframeRichSourceReuse.totalConsumers": + if e.ComplexityRoot.DataframeRichSourceReuse.TotalConsumers == nil { + break } - return graphql.Null - } - res := resTmp.(int) - fc.Result = res - return ec.marshalNInt2int(ctx, field.Selections, res) -} -func (ec *executionContext) fieldContext_DataframeCompilerPlanDiagnostics_traversalSets(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { - fc = &graphql.FieldContext{ - Object: "DataframeCompilerPlanDiagnostics", - Field: field, - IsMethod: false, - IsResolver: false, - Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { - return nil, errors.New("field of type Int does not have child fields") - }, - } - return fc, nil -} + return e.ComplexityRoot.DataframeRichSourceReuse.TotalConsumers(childComplexity), true -func (ec *executionContext) _DataframeCompilerPlanDiagnostics_sharedTraversalCount(ctx context.Context, field graphql.CollectedField, obj *model.DataframeCompilerPlanDiagnostics) (ret graphql.Marshaler) { - fc, err := ec.fieldContext_DataframeCompilerPlanDiagnostics_sharedTraversalCount(ctx, field) - if err != nil { - return graphql.Null - } - ctx = graphql.WithFieldContext(ctx, fc) - defer func() { - if r := recover(); r != nil { - ec.Error(ctx, ec.Recover(ctx, r)) - ret = graphql.Null + case "DataframeRowConnection.columns": + if e.ComplexityRoot.DataframeRowConnection.Columns == nil { + break } - }() - resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { - ctx = rctx // use context from middleware stack in children - return obj.SharedTraversalCount, nil - }) - if err != nil { - ec.Error(ctx, err) - return graphql.Null - } - if resTmp == nil { - if !graphql.HasFieldError(ctx, fc) { - ec.Errorf(ctx, "must not be null") + + return e.ComplexityRoot.DataframeRowConnection.Columns(childComplexity), true + case "DataframeRowConnection.materialization": + if e.ComplexityRoot.DataframeRowConnection.Materialization == nil { + break } - return graphql.Null - } - res := resTmp.(int) - fc.Result = res - return ec.marshalNInt2int(ctx, field.Selections, res) -} -func (ec *executionContext) fieldContext_DataframeCompilerPlanDiagnostics_sharedTraversalCount(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { - fc = &graphql.FieldContext{ - Object: "DataframeCompilerPlanDiagnostics", - Field: field, - IsMethod: false, - IsResolver: false, - Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { - return nil, errors.New("field of type Int does not have child fields") - }, - } - return fc, nil -} + return e.ComplexityRoot.DataframeRowConnection.Materialization(childComplexity), true + case "DataframeRowConnection.pageInfo": + if e.ComplexityRoot.DataframeRowConnection.PageInfo == nil { + break + } -func (ec *executionContext) _DataframeCompilerPlanDiagnostics_requiredMatchReuseCount(ctx context.Context, field graphql.CollectedField, obj *model.DataframeCompilerPlanDiagnostics) (ret graphql.Marshaler) { - fc, err := ec.fieldContext_DataframeCompilerPlanDiagnostics_requiredMatchReuseCount(ctx, field) - if err != nil { - return graphql.Null - } - ctx = graphql.WithFieldContext(ctx, fc) - defer func() { - if r := recover(); r != nil { - ec.Error(ctx, ec.Recover(ctx, r)) - ret = graphql.Null + return e.ComplexityRoot.DataframeRowConnection.PageInfo(childComplexity), true + case "DataframeRowConnection.rows": + if e.ComplexityRoot.DataframeRowConnection.Rows == nil { + break } - }() - resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { - ctx = rctx // use context from middleware stack in children - return obj.RequiredMatchReuseCount, nil - }) - if err != nil { - ec.Error(ctx, err) - return graphql.Null - } - if resTmp == nil { - if !graphql.HasFieldError(ctx, fc) { - ec.Errorf(ctx, "must not be null") + + return e.ComplexityRoot.DataframeRowConnection.Rows(childComplexity), true + case "DataframeRowConnection.totalCount": + if e.ComplexityRoot.DataframeRowConnection.TotalCount == nil { + break } - return graphql.Null - } - res := resTmp.(int) - fc.Result = res - return ec.marshalNInt2int(ctx, field.Selections, res) -} -func (ec *executionContext) fieldContext_DataframeCompilerPlanDiagnostics_requiredMatchReuseCount(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { - fc = &graphql.FieldContext{ - Object: "DataframeCompilerPlanDiagnostics", - Field: field, - IsMethod: false, - IsResolver: false, - Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { - return nil, errors.New("field of type Int does not have child fields") - }, - } - return fc, nil -} + return e.ComplexityRoot.DataframeRowConnection.TotalCount(childComplexity), true -func (ec *executionContext) _DataframeCompilerPlanDiagnostics_scopedSharingCandidateGroups(ctx context.Context, field graphql.CollectedField, obj *model.DataframeCompilerPlanDiagnostics) (ret graphql.Marshaler) { - fc, err := ec.fieldContext_DataframeCompilerPlanDiagnostics_scopedSharingCandidateGroups(ctx, field) - if err != nil { - return graphql.Null - } - ctx = graphql.WithFieldContext(ctx, fc) - defer func() { - if r := recover(); r != nil { - ec.Error(ctx, ec.Recover(ctx, r)) - ret = graphql.Null - } - }() - resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { - ctx = rctx // use context from middleware stack in children - return obj.ScopedSharingCandidateGroups, nil - }) - if err != nil { - ec.Error(ctx, err) - return graphql.Null - } - if resTmp == nil { - if !graphql.HasFieldError(ctx, fc) { - ec.Errorf(ctx, "must not be null") + case "DataframeTraversalHint.edgeCount": + if e.ComplexityRoot.DataframeTraversalHint.EdgeCount == nil { + break } - return graphql.Null - } - res := resTmp.(int) - fc.Result = res - return ec.marshalNInt2int(ctx, field.Selections, res) -} -func (ec *executionContext) fieldContext_DataframeCompilerPlanDiagnostics_scopedSharingCandidateGroups(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { - fc = &graphql.FieldContext{ - Object: "DataframeCompilerPlanDiagnostics", - Field: field, - IsMethod: false, - IsResolver: false, - Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { - return nil, errors.New("field of type Int does not have child fields") - }, - } - return fc, nil -} + return e.ComplexityRoot.DataframeTraversalHint.EdgeCount(childComplexity), true + case "DataframeTraversalHint.fromType": + if e.ComplexityRoot.DataframeTraversalHint.FromType == nil { + break + } -func (ec *executionContext) _DataframeCompilerPlanDiagnostics_scopedSharingCandidateSets(ctx context.Context, field graphql.CollectedField, obj *model.DataframeCompilerPlanDiagnostics) (ret graphql.Marshaler) { - fc, err := ec.fieldContext_DataframeCompilerPlanDiagnostics_scopedSharingCandidateSets(ctx, field) - if err != nil { - return graphql.Null - } - ctx = graphql.WithFieldContext(ctx, fc) - defer func() { - if r := recover(); r != nil { - ec.Error(ctx, ec.Recover(ctx, r)) - ret = graphql.Null + return e.ComplexityRoot.DataframeTraversalHint.FromType(childComplexity), true + case "DataframeTraversalHint.label": + if e.ComplexityRoot.DataframeTraversalHint.Label == nil { + break } - }() - resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { - ctx = rctx // use context from middleware stack in children - return obj.ScopedSharingCandidateSets, nil - }) - if err != nil { - ec.Error(ctx, err) - return graphql.Null - } - if resTmp == nil { - if !graphql.HasFieldError(ctx, fc) { - ec.Errorf(ctx, "must not be null") + + return e.ComplexityRoot.DataframeTraversalHint.Label(childComplexity), true + case "DataframeTraversalHint.toType": + if e.ComplexityRoot.DataframeTraversalHint.ToType == nil { + break } - return graphql.Null - } - res := resTmp.(int) - fc.Result = res - return ec.marshalNInt2int(ctx, field.Selections, res) -} -func (ec *executionContext) fieldContext_DataframeCompilerPlanDiagnostics_scopedSharingCandidateSets(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { - fc = &graphql.FieldContext{ - Object: "DataframeCompilerPlanDiagnostics", - Field: field, - IsMethod: false, - IsResolver: false, - Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { - return nil, errors.New("field of type Int does not have child fields") - }, - } - return fc, nil -} + return e.ComplexityRoot.DataframeTraversalHint.ToType(childComplexity), true -func (ec *executionContext) _DataframeCompilerPlanDiagnostics_potentialSharingOpportunityGroups(ctx context.Context, field graphql.CollectedField, obj *model.DataframeCompilerPlanDiagnostics) (ret graphql.Marshaler) { - fc, err := ec.fieldContext_DataframeCompilerPlanDiagnostics_potentialSharingOpportunityGroups(ctx, field) - if err != nil { - return graphql.Null - } - ctx = graphql.WithFieldContext(ctx, fc) - defer func() { - if r := recover(); r != nil { - ec.Error(ctx, ec.Recover(ctx, r)) - ret = graphql.Null + case "FhirDataframeResult.columns": + if e.ComplexityRoot.FhirDataframeResult.Columns == nil { + break } - }() - resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { - ctx = rctx // use context from middleware stack in children - return obj.PotentialSharingOpportunityGroups, nil - }) - if err != nil { - ec.Error(ctx, err) - return graphql.Null - } - if resTmp == nil { - if !graphql.HasFieldError(ctx, fc) { - ec.Errorf(ctx, "must not be null") + + return e.ComplexityRoot.FhirDataframeResult.Columns(childComplexity), true + case "FhirDataframeResult.diagnostics": + if e.ComplexityRoot.FhirDataframeResult.Diagnostics == nil { + break } - return graphql.Null - } - res := resTmp.(int) - fc.Result = res - return ec.marshalNInt2int(ctx, field.Selections, res) -} -func (ec *executionContext) fieldContext_DataframeCompilerPlanDiagnostics_potentialSharingOpportunityGroups(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { - fc = &graphql.FieldContext{ - Object: "DataframeCompilerPlanDiagnostics", - Field: field, - IsMethod: false, - IsResolver: false, - Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { - return nil, errors.New("field of type Int does not have child fields") - }, - } - return fc, nil -} + return e.ComplexityRoot.FhirDataframeResult.Diagnostics(childComplexity), true + case "FhirDataframeResult.rowCount": + if e.ComplexityRoot.FhirDataframeResult.RowCount == nil { + break + } -func (ec *executionContext) _DataframeCompilerPlanDiagnostics_potentialSharingOpportunitySets(ctx context.Context, field graphql.CollectedField, obj *model.DataframeCompilerPlanDiagnostics) (ret graphql.Marshaler) { - fc, err := ec.fieldContext_DataframeCompilerPlanDiagnostics_potentialSharingOpportunitySets(ctx, field) - if err != nil { - return graphql.Null - } - ctx = graphql.WithFieldContext(ctx, fc) - defer func() { - if r := recover(); r != nil { - ec.Error(ctx, ec.Recover(ctx, r)) - ret = graphql.Null + return e.ComplexityRoot.FhirDataframeResult.RowCount(childComplexity), true + case "FhirDataframeResult.rows": + if e.ComplexityRoot.FhirDataframeResult.Rows == nil { + break } - }() - resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { - ctx = rctx // use context from middleware stack in children - return obj.PotentialSharingOpportunitySets, nil - }) - if err != nil { - ec.Error(ctx, err) - return graphql.Null - } - if resTmp == nil { - if !graphql.HasFieldError(ctx, fc) { - ec.Errorf(ctx, "must not be null") + + return e.ComplexityRoot.FhirDataframeResult.Rows(childComplexity), true + + case "Mutation.materializeDataframeRecipeBundle": + if e.ComplexityRoot.Mutation.MaterializeDataframeRecipeBundle == nil { + break } - return graphql.Null - } - res := resTmp.(int) - fc.Result = res - return ec.marshalNInt2int(ctx, field.Selections, res) -} -func (ec *executionContext) fieldContext_DataframeCompilerPlanDiagnostics_potentialSharingOpportunitySets(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { - fc = &graphql.FieldContext{ - Object: "DataframeCompilerPlanDiagnostics", - Field: field, - IsMethod: false, - IsResolver: false, - Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { - return nil, errors.New("field of type Int does not have child fields") - }, - } - return fc, nil -} + args, err := ec.field_Mutation_materializeDataframeRecipeBundle_args(ctx, rawArgs) + if err != nil { + return 0, false + } -func (ec *executionContext) _DataframeCompilerPlanDiagnostics_optimizationPolicy(ctx context.Context, field graphql.CollectedField, obj *model.DataframeCompilerPlanDiagnostics) (ret graphql.Marshaler) { - fc, err := ec.fieldContext_DataframeCompilerPlanDiagnostics_optimizationPolicy(ctx, field) - if err != nil { - return graphql.Null - } - ctx = graphql.WithFieldContext(ctx, fc) - defer func() { - if r := recover(); r != nil { - ec.Error(ctx, ec.Recover(ctx, r)) - ret = graphql.Null + return e.ComplexityRoot.Mutation.MaterializeDataframeRecipeBundle(childComplexity, args["input"].(model.MaterializeDataframeRecipeInput)), true + case "Mutation.previewDataframeRecipe": + if e.ComplexityRoot.Mutation.PreviewDataframeRecipe == nil { + break } - }() - resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { - ctx = rctx // use context from middleware stack in children - return obj.OptimizationPolicy, nil - }) - if err != nil { - ec.Error(ctx, err) - return graphql.Null - } - if resTmp == nil { - if !graphql.HasFieldError(ctx, fc) { - ec.Errorf(ctx, "must not be null") + + args, err := ec.field_Mutation_previewDataframeRecipe_args(ctx, rawArgs) + if err != nil { + return 0, false } - return graphql.Null - } - res := resTmp.(*model.DataframeOptimizationPolicy) - fc.Result = res - return ec.marshalNDataframeOptimizationPolicy2ᚖgithubᚗcomᚋcalyprᚋloomᚋgraphqlapiᚋmodelᚐDataframeOptimizationPolicy(ctx, field.Selections, res) -} -func (ec *executionContext) fieldContext_DataframeCompilerPlanDiagnostics_optimizationPolicy(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { - fc = &graphql.FieldContext{ - Object: "DataframeCompilerPlanDiagnostics", - Field: field, - IsMethod: false, - IsResolver: false, - Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { - switch field.Name { - case "name": - return ec.fieldContext_DataframeOptimizationPolicy_name(ctx, field) - case "enabled": - return ec.fieldContext_DataframeOptimizationPolicy_enabled(ctx, field) - case "minimumSavings": - return ec.fieldContext_DataframeOptimizationPolicy_minimumSavings(ctx, field) - case "decisions": - return ec.fieldContext_DataframeOptimizationPolicy_decisions(ctx, field) - } - return nil, fmt.Errorf("no field named %q was found under type DataframeOptimizationPolicy", field.Name) - }, - } - return fc, nil -} + return e.ComplexityRoot.Mutation.PreviewDataframeRecipe(childComplexity, args["input"].(model.PreviewDataframeRecipeInput)), true + case "Mutation.runDataframeRecipe": + if e.ComplexityRoot.Mutation.RunDataframeRecipe == nil { + break + } -func (ec *executionContext) _DataframeCompilerPlanDiagnostics_richSourceReuse(ctx context.Context, field graphql.CollectedField, obj *model.DataframeCompilerPlanDiagnostics) (ret graphql.Marshaler) { - fc, err := ec.fieldContext_DataframeCompilerPlanDiagnostics_richSourceReuse(ctx, field) - if err != nil { - return graphql.Null - } - ctx = graphql.WithFieldContext(ctx, fc) - defer func() { - if r := recover(); r != nil { - ec.Error(ctx, ec.Recover(ctx, r)) - ret = graphql.Null + args, err := ec.field_Mutation_runDataframeRecipe_args(ctx, rawArgs) + if err != nil { + return 0, false } - }() - resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { - ctx = rctx // use context from middleware stack in children - return obj.RichSourceReuse, nil - }) - if err != nil { - ec.Error(ctx, err) - return graphql.Null - } - if resTmp == nil { - if !graphql.HasFieldError(ctx, fc) { - ec.Errorf(ctx, "must not be null") + + return e.ComplexityRoot.Mutation.RunDataframeRecipe(childComplexity, args["input"].(model.RunDataframeRecipeInput)), true + case "Mutation.runFhirDataframe": + if e.ComplexityRoot.Mutation.RunFhirDataframe == nil { + break } - return graphql.Null - } - res := resTmp.([]*model.DataframeRichSourceReuse) - fc.Result = res - return ec.marshalNDataframeRichSourceReuse2ᚕᚖgithubᚗcomᚋcalyprᚋloomᚋgraphqlapiᚋmodelᚐDataframeRichSourceReuseᚄ(ctx, field.Selections, res) -} -func (ec *executionContext) fieldContext_DataframeCompilerPlanDiagnostics_richSourceReuse(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { - fc = &graphql.FieldContext{ - Object: "DataframeCompilerPlanDiagnostics", - Field: field, - IsMethod: false, - IsResolver: false, - Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { - switch field.Name { - case "sourceSet": - return ec.fieldContext_DataframeRichSourceReuse_sourceSet(ctx, field) - case "aggregateConsumers": - return ec.fieldContext_DataframeRichSourceReuse_aggregateConsumers(ctx, field) - case "pivotConsumers": - return ec.fieldContext_DataframeRichSourceReuse_pivotConsumers(ctx, field) - case "sliceConsumers": - return ec.fieldContext_DataframeRichSourceReuse_sliceConsumers(ctx, field) - case "totalConsumers": - return ec.fieldContext_DataframeRichSourceReuse_totalConsumers(ctx, field) - } - return nil, fmt.Errorf("no field named %q was found under type DataframeRichSourceReuse", field.Name) - }, - } - return fc, nil -} + args, err := ec.field_Mutation_runFhirDataframe_args(ctx, rawArgs) + if err != nil { + return 0, false + } -func (ec *executionContext) _DataframeFieldHint_resourceType(ctx context.Context, field graphql.CollectedField, obj *model.DataframeFieldHint) (ret graphql.Marshaler) { - fc, err := ec.fieldContext_DataframeFieldHint_resourceType(ctx, field) - if err != nil { - return graphql.Null - } - ctx = graphql.WithFieldContext(ctx, fc) - defer func() { - if r := recover(); r != nil { - ec.Error(ctx, ec.Recover(ctx, r)) - ret = graphql.Null + return e.ComplexityRoot.Mutation.RunFhirDataframe(childComplexity, args["input"].(model.FhirDataframeInput), args["limit"].(*int)), true + case "Mutation.validateDataframeRecipe": + if e.ComplexityRoot.Mutation.ValidateDataframeRecipe == nil { + break } - }() - resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { - ctx = rctx // use context from middleware stack in children - return obj.ResourceType, nil - }) - if err != nil { - ec.Error(ctx, err) - return graphql.Null - } - if resTmp == nil { - if !graphql.HasFieldError(ctx, fc) { - ec.Errorf(ctx, "must not be null") + + args, err := ec.field_Mutation_validateDataframeRecipe_args(ctx, rawArgs) + if err != nil { + return 0, false } - return graphql.Null - } - res := resTmp.(string) - fc.Result = res - return ec.marshalNString2string(ctx, field.Selections, res) -} -func (ec *executionContext) fieldContext_DataframeFieldHint_resourceType(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { - fc = &graphql.FieldContext{ - Object: "DataframeFieldHint", - Field: field, - IsMethod: false, - IsResolver: false, - Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { - return nil, errors.New("field of type String does not have child fields") - }, - } - return fc, nil -} + return e.ComplexityRoot.Mutation.ValidateDataframeRecipe(childComplexity, args["input"].(model.ValidateDataframeRecipeInput)), true -func (ec *executionContext) _DataframeFieldHint_fieldRef(ctx context.Context, field graphql.CollectedField, obj *model.DataframeFieldHint) (ret graphql.Marshaler) { - fc, err := ec.fieldContext_DataframeFieldHint_fieldRef(ctx, field) - if err != nil { - return graphql.Null - } - ctx = graphql.WithFieldContext(ctx, fc) - defer func() { - if r := recover(); r != nil { - ec.Error(ctx, ec.Recover(ctx, r)) - ret = graphql.Null - } - }() - resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { - ctx = rctx // use context from middleware stack in children - return obj.FieldRef, nil - }) - if err != nil { - ec.Error(ctx, err) - return graphql.Null - } - if resTmp == nil { - if !graphql.HasFieldError(ctx, fc) { - ec.Errorf(ctx, "must not be null") + case "Query.dataframeAggregate": + if e.ComplexityRoot.Query.DataframeAggregate == nil { + break } - return graphql.Null - } - res := resTmp.(string) - fc.Result = res - return ec.marshalNString2string(ctx, field.Selections, res) -} - -func (ec *executionContext) fieldContext_DataframeFieldHint_fieldRef(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { - fc = &graphql.FieldContext{ - Object: "DataframeFieldHint", - Field: field, - IsMethod: false, - IsResolver: false, - Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { - return nil, errors.New("field of type String does not have child fields") - }, - } - return fc, nil -} -func (ec *executionContext) _DataframeFieldHint_label(ctx context.Context, field graphql.CollectedField, obj *model.DataframeFieldHint) (ret graphql.Marshaler) { - fc, err := ec.fieldContext_DataframeFieldHint_label(ctx, field) - if err != nil { - return graphql.Null - } - ctx = graphql.WithFieldContext(ctx, fc) - defer func() { - if r := recover(); r != nil { - ec.Error(ctx, ec.Recover(ctx, r)) - ret = graphql.Null + args, err := ec.field_Query_dataframeAggregate_args(ctx, rawArgs) + if err != nil { + return 0, false } - }() - resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { - ctx = rctx // use context from middleware stack in children - return obj.Label, nil - }) - if err != nil { - ec.Error(ctx, err) - return graphql.Null - } - if resTmp == nil { - if !graphql.HasFieldError(ctx, fc) { - ec.Errorf(ctx, "must not be null") + + return e.ComplexityRoot.Query.DataframeAggregate(childComplexity, args["input"].(model.DataframeAggregateInput)), true + case "Query.dataframeBuilderIntrospection": + if e.ComplexityRoot.Query.DataframeBuilderIntrospection == nil { + break } - return graphql.Null - } - res := resTmp.(string) - fc.Result = res - return ec.marshalNString2string(ctx, field.Selections, res) -} -func (ec *executionContext) fieldContext_DataframeFieldHint_label(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { - fc = &graphql.FieldContext{ - Object: "DataframeFieldHint", - Field: field, - IsMethod: false, - IsResolver: false, - Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { - return nil, errors.New("field of type String does not have child fields") - }, - } - return fc, nil -} + args, err := ec.field_Query_dataframeBuilderIntrospection_args(ctx, rawArgs) + if err != nil { + return 0, false + } -func (ec *executionContext) _DataframeFieldHint_path(ctx context.Context, field graphql.CollectedField, obj *model.DataframeFieldHint) (ret graphql.Marshaler) { - fc, err := ec.fieldContext_DataframeFieldHint_path(ctx, field) - if err != nil { - return graphql.Null - } - ctx = graphql.WithFieldContext(ctx, fc) - defer func() { - if r := recover(); r != nil { - ec.Error(ctx, ec.Recover(ctx, r)) - ret = graphql.Null + return e.ComplexityRoot.Query.DataframeBuilderIntrospection(childComplexity, args["input"].(model.DataframeBuilderIntrospectionInput)), true + case "Query.dataframeDataset": + if e.ComplexityRoot.Query.DataframeDataset == nil { + break } - }() - resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { - ctx = rctx // use context from middleware stack in children - return obj.Path, nil - }) - if err != nil { - ec.Error(ctx, err) - return graphql.Null - } - if resTmp == nil { - if !graphql.HasFieldError(ctx, fc) { - ec.Errorf(ctx, "must not be null") + + args, err := ec.field_Query_dataframeDataset_args(ctx, rawArgs) + if err != nil { + return 0, false } - return graphql.Null - } - res := resTmp.(string) - fc.Result = res - return ec.marshalNString2string(ctx, field.Selections, res) -} -func (ec *executionContext) fieldContext_DataframeFieldHint_path(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { - fc = &graphql.FieldContext{ - Object: "DataframeFieldHint", - Field: field, - IsMethod: false, - IsResolver: false, - Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { - return nil, errors.New("field of type String does not have child fields") - }, - } - return fc, nil -} + return e.ComplexityRoot.Query.DataframeDataset(childComplexity, args["input"].(model.DataframeDatasetInput)), true + case "Query.dataframeDatasets": + if e.ComplexityRoot.Query.DataframeDatasets == nil { + break + } -func (ec *executionContext) _DataframeFieldHint_selector(ctx context.Context, field graphql.CollectedField, obj *model.DataframeFieldHint) (ret graphql.Marshaler) { - fc, err := ec.fieldContext_DataframeFieldHint_selector(ctx, field) - if err != nil { - return graphql.Null - } - ctx = graphql.WithFieldContext(ctx, fc) - defer func() { - if r := recover(); r != nil { - ec.Error(ctx, ec.Recover(ctx, r)) - ret = graphql.Null + return e.ComplexityRoot.Query.DataframeDatasets(childComplexity), true + case "Query.dataframeMaterialization": + if e.ComplexityRoot.Query.DataframeMaterialization == nil { + break } - }() - resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { - ctx = rctx // use context from middleware stack in children - return obj.Selector, nil - }) - if err != nil { - ec.Error(ctx, err) - return graphql.Null - } - if resTmp == nil { - if !graphql.HasFieldError(ctx, fc) { - ec.Errorf(ctx, "must not be null") + + args, err := ec.field_Query_dataframeMaterialization_args(ctx, rawArgs) + if err != nil { + return 0, false } - return graphql.Null - } - res := resTmp.(*model.DataframeFieldSelector) - fc.Result = res - return ec.marshalNDataframeFieldSelector2ᚖgithubᚗcomᚋcalyprᚋloomᚋgraphqlapiᚋmodelᚐDataframeFieldSelector(ctx, field.Selections, res) -} -func (ec *executionContext) fieldContext_DataframeFieldHint_selector(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { - fc = &graphql.FieldContext{ - Object: "DataframeFieldHint", - Field: field, - IsMethod: false, - IsResolver: false, - Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { - switch field.Name { - case "sourcePath": - return ec.fieldContext_DataframeFieldSelector_sourcePath(ctx, field) - case "where": - return ec.fieldContext_DataframeFieldSelector_where(ctx, field) - case "valuePath": - return ec.fieldContext_DataframeFieldSelector_valuePath(ctx, field) - } - return nil, fmt.Errorf("no field named %q was found under type DataframeFieldSelector", field.Name) - }, - } - return fc, nil -} + return e.ComplexityRoot.Query.DataframeMaterialization(childComplexity, args["id"].(string)), true + case "Query.dataframeRecipeExecution": + if e.ComplexityRoot.Query.DataframeRecipeExecution == nil { + break + } -func (ec *executionContext) _DataframeFieldHint_kind(ctx context.Context, field graphql.CollectedField, obj *model.DataframeFieldHint) (ret graphql.Marshaler) { - fc, err := ec.fieldContext_DataframeFieldHint_kind(ctx, field) - if err != nil { - return graphql.Null - } - ctx = graphql.WithFieldContext(ctx, fc) - defer func() { - if r := recover(); r != nil { - ec.Error(ctx, ec.Recover(ctx, r)) - ret = graphql.Null + args, err := ec.field_Query_dataframeRecipeExecution_args(ctx, rawArgs) + if err != nil { + return 0, false } - }() - resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { - ctx = rctx // use context from middleware stack in children - return obj.Kind, nil - }) - if err != nil { - ec.Error(ctx, err) - return graphql.Null - } - if resTmp == nil { - if !graphql.HasFieldError(ctx, fc) { - ec.Errorf(ctx, "must not be null") + + return e.ComplexityRoot.Query.DataframeRecipeExecution(childComplexity, args["id"].(string)), true + case "Query.dataframeRows": + if e.ComplexityRoot.Query.DataframeRows == nil { + break } - return graphql.Null - } - res := resTmp.(string) - fc.Result = res - return ec.marshalNString2string(ctx, field.Selections, res) -} -func (ec *executionContext) fieldContext_DataframeFieldHint_kind(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { - fc = &graphql.FieldContext{ - Object: "DataframeFieldHint", - Field: field, - IsMethod: false, - IsResolver: false, - Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { - return nil, errors.New("field of type String does not have child fields") - }, - } - return fc, nil -} + args, err := ec.field_Query_dataframeRows_args(ctx, rawArgs) + if err != nil { + return 0, false + } -func (ec *executionContext) _DataframeFieldHint_docCount(ctx context.Context, field graphql.CollectedField, obj *model.DataframeFieldHint) (ret graphql.Marshaler) { - fc, err := ec.fieldContext_DataframeFieldHint_docCount(ctx, field) - if err != nil { - return graphql.Null - } - ctx = graphql.WithFieldContext(ctx, fc) - defer func() { - if r := recover(); r != nil { - ec.Error(ctx, ec.Recover(ctx, r)) - ret = graphql.Null + return e.ComplexityRoot.Query.DataframeRows(childComplexity, args["input"].(model.DataframeRowsInput)), true + case "Query.explainDataframeRecipe": + if e.ComplexityRoot.Query.ExplainDataframeRecipe == nil { + break } - }() - resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { - ctx = rctx // use context from middleware stack in children - return obj.DocCount, nil - }) - if err != nil { - ec.Error(ctx, err) - return graphql.Null - } - if resTmp == nil { - if !graphql.HasFieldError(ctx, fc) { - ec.Errorf(ctx, "must not be null") + + args, err := ec.field_Query_explainDataframeRecipe_args(ctx, rawArgs) + if err != nil { + return 0, false } - return graphql.Null - } - res := resTmp.(int) - fc.Result = res - return ec.marshalNInt2int(ctx, field.Selections, res) -} -func (ec *executionContext) fieldContext_DataframeFieldHint_docCount(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { - fc = &graphql.FieldContext{ - Object: "DataframeFieldHint", - Field: field, - IsMethod: false, - IsResolver: false, - Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { - return nil, errors.New("field of type Int does not have child fields") - }, - } - return fc, nil -} + return e.ComplexityRoot.Query.ExplainDataframeRecipe(childComplexity, args["input"].(model.ExplainDataframeRecipeInput)), true -func (ec *executionContext) _DataframeFieldHint_sampleCount(ctx context.Context, field graphql.CollectedField, obj *model.DataframeFieldHint) (ret graphql.Marshaler) { - fc, err := ec.fieldContext_DataframeFieldHint_sampleCount(ctx, field) - if err != nil { - return graphql.Null - } - ctx = graphql.WithFieldContext(ctx, fc) - defer func() { - if r := recover(); r != nil { - ec.Error(ctx, ec.Recover(ctx, r)) - ret = graphql.Null + case "Query.preflightDataframeRecipe": + if e.ComplexityRoot.Query.PreflightDataframeRecipe == nil { + break } - }() - resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { - ctx = rctx // use context from middleware stack in children - return obj.SampleCount, nil - }) - if err != nil { - ec.Error(ctx, err) - return graphql.Null - } - if resTmp == nil { - if !graphql.HasFieldError(ctx, fc) { - ec.Errorf(ctx, "must not be null") + + args, err := ec.field_Query_preflightDataframeRecipe_args(ctx, rawArgs) + if err != nil { + return 0, false } - return graphql.Null - } - res := resTmp.(int) - fc.Result = res - return ec.marshalNInt2int(ctx, field.Selections, res) -} -func (ec *executionContext) fieldContext_DataframeFieldHint_sampleCount(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { - fc = &graphql.FieldContext{ - Object: "DataframeFieldHint", - Field: field, - IsMethod: false, - IsResolver: false, - Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { - return nil, errors.New("field of type Int does not have child fields") - }, + return e.ComplexityRoot.Query.PreflightDataframeRecipe(childComplexity, args["input"].(model.PreflightDataframeRecipeInput)), true + } - return fc, nil + return 0, false } -func (ec *executionContext) _DataframeFieldHint_distinctValues(ctx context.Context, field graphql.CollectedField, obj *model.DataframeFieldHint) (ret graphql.Marshaler) { - fc, err := ec.fieldContext_DataframeFieldHint_distinctValues(ctx, field) - if err != nil { - return graphql.Null - } - ctx = graphql.WithFieldContext(ctx, fc) - defer func() { - if r := recover(); r != nil { - ec.Error(ctx, ec.Recover(ctx, r)) - ret = graphql.Null +func (e *executableSchema) Exec(ctx context.Context) graphql.ResponseHandler { + opCtx := graphql.GetOperationContext(ctx) + ec := newExecutionContext(opCtx, e, make(chan graphql.DeferredResult)) + inputUnmarshalMap := graphql.BuildUnmarshalerMap( + ec.unmarshalInputDataframeAggregateInput, + ec.unmarshalInputDataframeBuilderIntrospectionInput, + ec.unmarshalInputDataframeDatasetInput, + ec.unmarshalInputDataframeFilterInput, + ec.unmarshalInputDataframeRecipeBindingsInput, + ec.unmarshalInputDataframeRowsInput, + ec.unmarshalInputDataframeSortInput, + ec.unmarshalInputExplainDataframeRecipeInput, + ec.unmarshalInputFhirAggregateInput, + ec.unmarshalInputFhirCatalogProjectionInput, + ec.unmarshalInputFhirCodeValueInput, + ec.unmarshalInputFhirDataframeInput, + ec.unmarshalInputFhirFieldPredicateInput, + ec.unmarshalInputFhirFieldSelectInput, + ec.unmarshalInputFhirFieldSelectorInput, + ec.unmarshalInputFhirFilterInput, + ec.unmarshalInputFhirFilterValueInput, + ec.unmarshalInputFhirPivotDiscoveryInput, + ec.unmarshalInputFhirPivotInput, + ec.unmarshalInputFhirRepresentativeSliceInput, + ec.unmarshalInputFhirTraversalStepInput, + ec.unmarshalInputMaterializeDataframeRecipeInput, + ec.unmarshalInputPreflightDataframeRecipeInput, + ec.unmarshalInputPreviewDataframeRecipeInput, + ec.unmarshalInputRunDataframeRecipeInput, + ec.unmarshalInputValidateDataframeRecipeInput, + ) + first := true + + switch opCtx.Operation.Operation { + case ast.Query: + return func(ctx context.Context) *graphql.Response { + var response graphql.Response + var data graphql.Marshaler + if first { + first = false + ctx = graphql.WithUnmarshalerMap(ctx, inputUnmarshalMap) + data = ec._Query(ctx, opCtx.Operation.SelectionSet) + } else { + if atomic.LoadInt32(&ec.PendingDeferred) > 0 { + result := <-ec.DeferredResults + atomic.AddInt32(&ec.PendingDeferred, -1) + data = result.Result + response.Path = result.Path + response.Label = result.Label + response.Errors = result.Errors + } else { + return nil + } + } + var buf bytes.Buffer + data.MarshalGQL(&buf) + response.Data = buf.Bytes() + if atomic.LoadInt32(&ec.Deferred) > 0 { + hasNext := atomic.LoadInt32(&ec.PendingDeferred) > 0 + response.HasNext = &hasNext + } + + return &response } - }() - resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { - ctx = rctx // use context from middleware stack in children - return obj.DistinctValues, nil - }) - if err != nil { - ec.Error(ctx, err) - return graphql.Null - } - if resTmp == nil { - if !graphql.HasFieldError(ctx, fc) { - ec.Errorf(ctx, "must not be null") + case ast.Mutation: + return func(ctx context.Context) *graphql.Response { + if !first { + return nil + } + first = false + ctx = graphql.WithUnmarshalerMap(ctx, inputUnmarshalMap) + data := ec._Mutation(ctx, opCtx.Operation.SelectionSet) + var buf bytes.Buffer + data.MarshalGQL(&buf) + + return &graphql.Response{ + Data: buf.Bytes(), + } } - return graphql.Null + + default: + return graphql.OneShot(graphql.ErrorResponse(ctx, "unsupported GraphQL operation")) } - res := resTmp.([]string) - fc.Result = res - return ec.marshalNString2ᚕstringᚄ(ctx, field.Selections, res) } -func (ec *executionContext) fieldContext_DataframeFieldHint_distinctValues(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { - fc = &graphql.FieldContext{ - Object: "DataframeFieldHint", - Field: field, - IsMethod: false, - IsResolver: false, - Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { - return nil, errors.New("field of type String does not have child fields") - }, - } - return fc, nil +type executionContext struct { + *graphql.ExecutionContextState[ResolverRoot, DirectiveRoot, ComplexityRoot] } -func (ec *executionContext) _DataframeFieldHint_distinctTruncated(ctx context.Context, field graphql.CollectedField, obj *model.DataframeFieldHint) (ret graphql.Marshaler) { - fc, err := ec.fieldContext_DataframeFieldHint_distinctTruncated(ctx, field) - if err != nil { - return graphql.Null +func newExecutionContext( + opCtx *graphql.OperationContext, + execSchema *executableSchema, + deferredResults chan graphql.DeferredResult, +) *executionContext { + return &executionContext{ + ExecutionContextState: graphql.NewExecutionContextState[ResolverRoot, DirectiveRoot, ComplexityRoot]( + opCtx, + (*graphql.ExecutableSchemaState[ResolverRoot, DirectiveRoot, ComplexityRoot])(execSchema), + parsedSchema, + deferredResults, + ), } - ctx = graphql.WithFieldContext(ctx, fc) - defer func() { - if r := recover(); r != nil { - ec.Error(ctx, ec.Recover(ctx, r)) - ret = graphql.Null - } - }() - resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { - ctx = rctx // use context from middleware stack in children - return obj.DistinctTruncated, nil - }) +} + +//go:embed "schema.graphqls" +var sourcesFS embed.FS + +func sourceData(filename string) string { + data, err := sourcesFS.ReadFile(filename) if err != nil { - ec.Error(ctx, err) - return graphql.Null - } - if resTmp == nil { - if !graphql.HasFieldError(ctx, fc) { - ec.Errorf(ctx, "must not be null") - } - return graphql.Null + panic(fmt.Sprintf("codegen problem: %s not available", filename)) } - res := resTmp.(bool) - fc.Result = res - return ec.marshalNBoolean2bool(ctx, field.Selections, res) + return string(data) } -func (ec *executionContext) fieldContext_DataframeFieldHint_distinctTruncated(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { - fc = &graphql.FieldContext{ - Object: "DataframeFieldHint", - Field: field, - IsMethod: false, - IsResolver: false, - Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { - return nil, errors.New("field of type Boolean does not have child fields") - }, - } - return fc, nil +var sources = []*ast.Source{ + {Name: "schema.graphqls", Input: sourceData("schema.graphqls"), BuiltIn: false}, } +var parsedSchema = gqlparser.MustLoadSchema(sources...) -func (ec *executionContext) _DataframeFieldHint_pivotCandidate(ctx context.Context, field graphql.CollectedField, obj *model.DataframeFieldHint) (ret graphql.Marshaler) { - fc, err := ec.fieldContext_DataframeFieldHint_pivotCandidate(ctx, field) +// childFields_* functions provide shared child field context lookups. +// Each function is generated once per unique object type, deduplicating the +// switch statements that were previously inlined in every fieldContext_* function. + +func (ec *executionContext) childFields_DataframeAggregateResult(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + switch field.Name { + case "materialization": + return ec.fieldContext_DataframeAggregateResult_materialization(ctx, field) + case "columns": + return ec.fieldContext_DataframeAggregateResult_columns(ctx, field) + case "rows": + return ec.fieldContext_DataframeAggregateResult_rows(ctx, field) + } + return nil, fmt.Errorf("no field named %q was found under type DataframeAggregateResult", field.Name) +} + +func (ec *executionContext) childFields_DataframeBuilderIntrospection(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + switch field.Name { + case "project": + return ec.fieldContext_DataframeBuilderIntrospection_project(ctx, field) + case "rootResourceType": + return ec.fieldContext_DataframeBuilderIntrospection_rootResourceType(ctx, field) + case "authResourcePaths": + return ec.fieldContext_DataframeBuilderIntrospection_authResourcePaths(ctx, field) + case "root": + return ec.fieldContext_DataframeBuilderIntrospection_root(ctx, field) + case "relatedResources": + return ec.fieldContext_DataframeBuilderIntrospection_relatedResources(ctx, field) + case "traversals": + return ec.fieldContext_DataframeBuilderIntrospection_traversals(ctx, field) + case "fields": + return ec.fieldContext_DataframeBuilderIntrospection_fields(ctx, field) + case "pivotFields": + return ec.fieldContext_DataframeBuilderIntrospection_pivotFields(ctx, field) + } + return nil, fmt.Errorf("no field named %q was found under type DataframeBuilderIntrospection", field.Name) +} + +func (ec *executionContext) childFields_DataframeColumn(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + switch field.Name { + case "name": + return ec.fieldContext_DataframeColumn_name(ctx, field) + case "clickhouseType": + return ec.fieldContext_DataframeColumn_clickhouseType(ctx, field) + case "logicalType": + return ec.fieldContext_DataframeColumn_logicalType(ctx, field) + case "nullable": + return ec.fieldContext_DataframeColumn_nullable(ctx, field) + case "repeated": + return ec.fieldContext_DataframeColumn_repeated(ctx, field) + case "filterable": + return ec.fieldContext_DataframeColumn_filterable(ctx, field) + case "sortable": + return ec.fieldContext_DataframeColumn_sortable(ctx, field) + case "aggregatable": + return ec.fieldContext_DataframeColumn_aggregatable(ctx, field) + } + return nil, fmt.Errorf("no field named %q was found under type DataframeColumn", field.Name) +} + +func (ec *executionContext) childFields_DataframeCompilerPlanDiagnostics(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + switch field.Name { + case "traversalSets": + return ec.fieldContext_DataframeCompilerPlanDiagnostics_traversalSets(ctx, field) + case "sharedTraversalCount": + return ec.fieldContext_DataframeCompilerPlanDiagnostics_sharedTraversalCount(ctx, field) + case "requiredMatchReuseCount": + return ec.fieldContext_DataframeCompilerPlanDiagnostics_requiredMatchReuseCount(ctx, field) + case "scopedSharingCandidateGroups": + return ec.fieldContext_DataframeCompilerPlanDiagnostics_scopedSharingCandidateGroups(ctx, field) + case "scopedSharingCandidateSets": + return ec.fieldContext_DataframeCompilerPlanDiagnostics_scopedSharingCandidateSets(ctx, field) + case "potentialSharingOpportunityGroups": + return ec.fieldContext_DataframeCompilerPlanDiagnostics_potentialSharingOpportunityGroups(ctx, field) + case "potentialSharingOpportunitySets": + return ec.fieldContext_DataframeCompilerPlanDiagnostics_potentialSharingOpportunitySets(ctx, field) + case "optimizationPolicy": + return ec.fieldContext_DataframeCompilerPlanDiagnostics_optimizationPolicy(ctx, field) + case "richSourceReuse": + return ec.fieldContext_DataframeCompilerPlanDiagnostics_richSourceReuse(ctx, field) + } + return nil, fmt.Errorf("no field named %q was found under type DataframeCompilerPlanDiagnostics", field.Name) +} + +func (ec *executionContext) childFields_DataframeFieldHint(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + switch field.Name { + case "resourceType": + return ec.fieldContext_DataframeFieldHint_resourceType(ctx, field) + case "fieldRef": + return ec.fieldContext_DataframeFieldHint_fieldRef(ctx, field) + case "label": + return ec.fieldContext_DataframeFieldHint_label(ctx, field) + case "path": + return ec.fieldContext_DataframeFieldHint_path(ctx, field) + case "selector": + return ec.fieldContext_DataframeFieldHint_selector(ctx, field) + case "kind": + return ec.fieldContext_DataframeFieldHint_kind(ctx, field) + case "docCount": + return ec.fieldContext_DataframeFieldHint_docCount(ctx, field) + case "sampleCount": + return ec.fieldContext_DataframeFieldHint_sampleCount(ctx, field) + case "distinctValues": + return ec.fieldContext_DataframeFieldHint_distinctValues(ctx, field) + case "distinctTruncated": + return ec.fieldContext_DataframeFieldHint_distinctTruncated(ctx, field) + case "pivotCandidate": + return ec.fieldContext_DataframeFieldHint_pivotCandidate(ctx, field) + case "pivotKind": + return ec.fieldContext_DataframeFieldHint_pivotKind(ctx, field) + case "pivotColumns": + return ec.fieldContext_DataframeFieldHint_pivotColumns(ctx, field) + case "pivotFamily": + return ec.fieldContext_DataframeFieldHint_pivotFamily(ctx, field) + case "defaultPivotColumnSelector": + return ec.fieldContext_DataframeFieldHint_defaultPivotColumnSelector(ctx, field) + case "defaultPivotValueSelector": + return ec.fieldContext_DataframeFieldHint_defaultPivotValueSelector(ctx, field) + } + return nil, fmt.Errorf("no field named %q was found under type DataframeFieldHint", field.Name) +} + +func (ec *executionContext) childFields_DataframeFieldPredicate(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + switch field.Name { + case "path": + return ec.fieldContext_DataframeFieldPredicate_path(ctx, field) + case "op": + return ec.fieldContext_DataframeFieldPredicate_op(ctx, field) + case "value": + return ec.fieldContext_DataframeFieldPredicate_value(ctx, field) + } + return nil, fmt.Errorf("no field named %q was found under type DataframeFieldPredicate", field.Name) +} + +func (ec *executionContext) childFields_DataframeFieldSelector(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + switch field.Name { + case "sourcePath": + return ec.fieldContext_DataframeFieldSelector_sourcePath(ctx, field) + case "where": + return ec.fieldContext_DataframeFieldSelector_where(ctx, field) + case "valuePath": + return ec.fieldContext_DataframeFieldSelector_valuePath(ctx, field) + } + return nil, fmt.Errorf("no field named %q was found under type DataframeFieldSelector", field.Name) +} + +func (ec *executionContext) childFields_DataframeMaterialization(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + switch field.Name { + case "id": + return ec.fieldContext_DataframeMaterialization_id(ctx, field) + case "name": + return ec.fieldContext_DataframeMaterialization_name(ctx, field) + case "revision": + return ec.fieldContext_DataframeMaterialization_revision(ctx, field) + case "state": + return ec.fieldContext_DataframeMaterialization_state(ctx, field) + case "columns": + return ec.fieldContext_DataframeMaterialization_columns(ctx, field) + case "rowCount": + return ec.fieldContext_DataframeMaterialization_rowCount(ctx, field) + case "createdAt": + return ec.fieldContext_DataframeMaterialization_createdAt(ctx, field) + case "readyAt": + return ec.fieldContext_DataframeMaterialization_readyAt(ctx, field) + case "error": + return ec.fieldContext_DataframeMaterialization_error(ctx, field) + } + return nil, fmt.Errorf("no field named %q was found under type DataframeMaterialization", field.Name) +} + +func (ec *executionContext) childFields_DataframeOptimizationDecision(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + switch field.Name { + case "rule": + return ec.fieldContext_DataframeOptimizationDecision_rule(ctx, field) + case "enabled": + return ec.fieldContext_DataframeOptimizationDecision_enabled(ctx, field) + case "candidateSets": + return ec.fieldContext_DataframeOptimizationDecision_candidateSets(ctx, field) + case "estimatedBaselineWork": + return ec.fieldContext_DataframeOptimizationDecision_estimatedBaselineWork(ctx, field) + case "estimatedOptimizedWork": + return ec.fieldContext_DataframeOptimizationDecision_estimatedOptimizedWork(ctx, field) + case "estimatedSavings": + return ec.fieldContext_DataframeOptimizationDecision_estimatedSavings(ctx, field) + case "reason": + return ec.fieldContext_DataframeOptimizationDecision_reason(ctx, field) + } + return nil, fmt.Errorf("no field named %q was found under type DataframeOptimizationDecision", field.Name) +} + +func (ec *executionContext) childFields_DataframeOptimizationPolicy(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + switch field.Name { + case "name": + return ec.fieldContext_DataframeOptimizationPolicy_name(ctx, field) + case "enabled": + return ec.fieldContext_DataframeOptimizationPolicy_enabled(ctx, field) + case "minimumSavings": + return ec.fieldContext_DataframeOptimizationPolicy_minimumSavings(ctx, field) + case "decisions": + return ec.fieldContext_DataframeOptimizationPolicy_decisions(ctx, field) + } + return nil, fmt.Errorf("no field named %q was found under type DataframeOptimizationPolicy", field.Name) +} + +func (ec *executionContext) childFields_DataframePageInfo(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + switch field.Name { + case "hasNextPage": + return ec.fieldContext_DataframePageInfo_hasNextPage(ctx, field) + case "endCursor": + return ec.fieldContext_DataframePageInfo_endCursor(ctx, field) + } + return nil, fmt.Errorf("no field named %q was found under type DataframePageInfo", field.Name) +} + +func (ec *executionContext) childFields_DataframeQueryDiagnostics(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + switch field.Name { + case "inputResolutionMs": + return ec.fieldContext_DataframeQueryDiagnostics_inputResolutionMs(ctx, field) + case "requestPreparationMs": + return ec.fieldContext_DataframeQueryDiagnostics_requestPreparationMs(ctx, field) + case "compilationMs": + return ec.fieldContext_DataframeQueryDiagnostics_compilationMs(ctx, field) + case "arangoQueryMs": + return ec.fieldContext_DataframeQueryDiagnostics_arangoQueryMs(ctx, field) + case "rowMaterializationMs": + return ec.fieldContext_DataframeQueryDiagnostics_rowMaterializationMs(ctx, field) + case "resultAssemblyMs": + return ec.fieldContext_DataframeQueryDiagnostics_resultAssemblyMs(ctx, field) + case "totalMs": + return ec.fieldContext_DataframeQueryDiagnostics_totalMs(ctx, field) + case "plan": + return ec.fieldContext_DataframeQueryDiagnostics_plan(ctx, field) + } + return nil, fmt.Errorf("no field named %q was found under type DataframeQueryDiagnostics", field.Name) +} + +func (ec *executionContext) childFields_DataframeRecipeArangoAssessment(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + switch field.Name { + case "plans": + return ec.fieldContext_DataframeRecipeArangoAssessment_plans(ctx, field) + case "fullCollectionScans": + return ec.fieldContext_DataframeRecipeArangoAssessment_fullCollectionScans(ctx, field) + case "indexes": + return ec.fieldContext_DataframeRecipeArangoAssessment_indexes(ctx, field) + case "warnings": + return ec.fieldContext_DataframeRecipeArangoAssessment_warnings(ctx, field) + case "appliedOptimizerRules": + return ec.fieldContext_DataframeRecipeArangoAssessment_appliedOptimizerRules(ctx, field) + } + return nil, fmt.Errorf("no field named %q was found under type DataframeRecipeArangoAssessment", field.Name) +} + +func (ec *executionContext) childFields_DataframeRecipeColumn(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + switch field.Name { + case "output": + return ec.fieldContext_DataframeRecipeColumn_output(ctx, field) + case "dynamicName": + return ec.fieldContext_DataframeRecipeColumn_dynamicName(ctx, field) + case "name": + return ec.fieldContext_DataframeRecipeColumn_name(ctx, field) + case "logicalType": + return ec.fieldContext_DataframeRecipeColumn_logicalType(ctx, field) + case "repeated": + return ec.fieldContext_DataframeRecipeColumn_repeated(ctx, field) + case "nullable": + return ec.fieldContext_DataframeRecipeColumn_nullable(ctx, field) + } + return nil, fmt.Errorf("no field named %q was found under type DataframeRecipeColumn", field.Name) +} + +func (ec *executionContext) childFields_DataframeRecipeExecution(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + switch field.Name { + case "id": + return ec.fieldContext_DataframeRecipeExecution_id(ctx, field) + case "name": + return ec.fieldContext_DataframeRecipeExecution_name(ctx, field) + case "recipeDigest": + return ec.fieldContext_DataframeRecipeExecution_recipeDigest(ctx, field) + case "resolvedSchemaDigest": + return ec.fieldContext_DataframeRecipeExecution_resolvedSchemaDigest(ctx, field) + case "sourceGeneration": + return ec.fieldContext_DataframeRecipeExecution_sourceGeneration(ctx, field) + case "state": + return ec.fieldContext_DataframeRecipeExecution_state(ctx, field) + case "outputs": + return ec.fieldContext_DataframeRecipeExecution_outputs(ctx, field) + case "error": + return ec.fieldContext_DataframeRecipeExecution_error(ctx, field) + } + return nil, fmt.Errorf("no field named %q was found under type DataframeRecipeExecution", field.Name) +} + +func (ec *executionContext) childFields_DataframeRecipeExecutionOutput(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + switch field.Name { + case "name": + return ec.fieldContext_DataframeRecipeExecutionOutput_name(ctx, field) + case "state": + return ec.fieldContext_DataframeRecipeExecutionOutput_state(ctx, field) + case "rowCount": + return ec.fieldContext_DataframeRecipeExecutionOutput_rowCount(ctx, field) + case "error": + return ec.fieldContext_DataframeRecipeExecutionOutput_error(ctx, field) + } + return nil, fmt.Errorf("no field named %q was found under type DataframeRecipeExecutionOutput", field.Name) +} + +func (ec *executionContext) childFields_DataframeRecipeExpansionExplanation(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + switch field.Name { + case "sourcePath": + return ec.fieldContext_DataframeRecipeExpansionExplanation_sourcePath(ctx, field) + case "alias": + return ec.fieldContext_DataframeRecipeExpansionExplanation_alias(ctx, field) + } + return nil, fmt.Errorf("no field named %q was found under type DataframeRecipeExpansionExplanation", field.Name) +} + +func (ec *executionContext) childFields_DataframeRecipeExplainCollectionScan(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + switch field.Name { + case "plan": + return ec.fieldContext_DataframeRecipeExplainCollectionScan_plan(ctx, field) + case "nodeId": + return ec.fieldContext_DataframeRecipeExplainCollectionScan_nodeId(ctx, field) + case "collection": + return ec.fieldContext_DataframeRecipeExplainCollectionScan_collection(ctx, field) + } + return nil, fmt.Errorf("no field named %q was found under type DataframeRecipeExplainCollectionScan", field.Name) +} + +func (ec *executionContext) childFields_DataframeRecipeExplainIndexLocation(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + switch field.Name { + case "plan": + return ec.fieldContext_DataframeRecipeExplainIndexLocation_plan(ctx, field) + case "nodeId": + return ec.fieldContext_DataframeRecipeExplainIndexLocation_nodeId(ctx, field) + } + return nil, fmt.Errorf("no field named %q was found under type DataframeRecipeExplainIndexLocation", field.Name) +} + +func (ec *executionContext) childFields_DataframeRecipeExplainIndexSummary(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + switch field.Name { + case "collection": + return ec.fieldContext_DataframeRecipeExplainIndexSummary_collection(ctx, field) + case "id": + return ec.fieldContext_DataframeRecipeExplainIndexSummary_id(ctx, field) + case "name": + return ec.fieldContext_DataframeRecipeExplainIndexSummary_name(ctx, field) + case "type": + return ec.fieldContext_DataframeRecipeExplainIndexSummary_type(ctx, field) + case "fields": + return ec.fieldContext_DataframeRecipeExplainIndexSummary_fields(ctx, field) + case "uses": + return ec.fieldContext_DataframeRecipeExplainIndexSummary_uses(ctx, field) + } + return nil, fmt.Errorf("no field named %q was found under type DataframeRecipeExplainIndexSummary", field.Name) +} + +func (ec *executionContext) childFields_DataframeRecipeExplainPlanEstimate(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + switch field.Name { + case "plan": + return ec.fieldContext_DataframeRecipeExplainPlanEstimate_plan(ctx, field) + case "estimatedCost": + return ec.fieldContext_DataframeRecipeExplainPlanEstimate_estimatedCost(ctx, field) + case "estimatedNrItems": + return ec.fieldContext_DataframeRecipeExplainPlanEstimate_estimatedNrItems(ctx, field) + } + return nil, fmt.Errorf("no field named %q was found under type DataframeRecipeExplainPlanEstimate", field.Name) +} + +func (ec *executionContext) childFields_DataframeRecipeExplainWarning(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + switch field.Name { + case "code": + return ec.fieldContext_DataframeRecipeExplainWarning_code(ctx, field) + case "message": + return ec.fieldContext_DataframeRecipeExplainWarning_message(ctx, field) + } + return nil, fmt.Errorf("no field named %q was found under type DataframeRecipeExplainWarning", field.Name) +} + +func (ec *executionContext) childFields_DataframeRecipeExplanation(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + switch field.Name { + case "name": + return ec.fieldContext_DataframeRecipeExplanation_name(ctx, field) + case "recipeDigest": + return ec.fieldContext_DataframeRecipeExplanation_recipeDigest(ctx, field) + case "translationVersion": + return ec.fieldContext_DataframeRecipeExplanation_translationVersion(ctx, field) + case "outputs": + return ec.fieldContext_DataframeRecipeExplanation_outputs(ctx, field) + case "physical": + return ec.fieldContext_DataframeRecipeExplanation_physical(ctx, field) + } + return nil, fmt.Errorf("no field named %q was found under type DataframeRecipeExplanation", field.Name) +} + +func (ec *executionContext) childFields_DataframeRecipeExpressionExplanation(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + switch field.Name { + case "sourcePath": + return ec.fieldContext_DataframeRecipeExpressionExplanation_sourcePath(ctx, field) + case "context": + return ec.fieldContext_DataframeRecipeExpressionExplanation_context(ctx, field) + case "kind": + return ec.fieldContext_DataframeRecipeExpressionExplanation_kind(ctx, field) + case "valueType": + return ec.fieldContext_DataframeRecipeExpressionExplanation_valueType(ctx, field) + case "repeated": + return ec.fieldContext_DataframeRecipeExpressionExplanation_repeated(ctx, field) + case "nullable": + return ec.fieldContext_DataframeRecipeExpressionExplanation_nullable(ctx, field) + } + return nil, fmt.Errorf("no field named %q was found under type DataframeRecipeExpressionExplanation", field.Name) +} + +func (ec *executionContext) childFields_DataframeRecipeOptimizationDecision(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + switch field.Name { + case "rule": + return ec.fieldContext_DataframeRecipeOptimizationDecision_rule(ctx, field) + case "enabled": + return ec.fieldContext_DataframeRecipeOptimizationDecision_enabled(ctx, field) + case "candidateSets": + return ec.fieldContext_DataframeRecipeOptimizationDecision_candidateSets(ctx, field) + case "estimatedBaselineWork": + return ec.fieldContext_DataframeRecipeOptimizationDecision_estimatedBaselineWork(ctx, field) + case "estimatedOptimizedWork": + return ec.fieldContext_DataframeRecipeOptimizationDecision_estimatedOptimizedWork(ctx, field) + case "estimatedSavings": + return ec.fieldContext_DataframeRecipeOptimizationDecision_estimatedSavings(ctx, field) + case "reason": + return ec.fieldContext_DataframeRecipeOptimizationDecision_reason(ctx, field) + } + return nil, fmt.Errorf("no field named %q was found under type DataframeRecipeOptimizationDecision", field.Name) +} + +func (ec *executionContext) childFields_DataframeRecipeOptimizationExplanation(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + switch field.Name { + case "policy": + return ec.fieldContext_DataframeRecipeOptimizationExplanation_policy(ctx, field) + case "enabled": + return ec.fieldContext_DataframeRecipeOptimizationExplanation_enabled(ctx, field) + case "minimumSavings": + return ec.fieldContext_DataframeRecipeOptimizationExplanation_minimumSavings(ctx, field) + case "rules": + return ec.fieldContext_DataframeRecipeOptimizationExplanation_rules(ctx, field) + case "decisions": + return ec.fieldContext_DataframeRecipeOptimizationExplanation_decisions(ctx, field) + } + return nil, fmt.Errorf("no field named %q was found under type DataframeRecipeOptimizationExplanation", field.Name) +} + +func (ec *executionContext) childFields_DataframeRecipeOptimizationRuleState(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + switch field.Name { + case "rule": + return ec.fieldContext_DataframeRecipeOptimizationRuleState_rule(ctx, field) + case "enabled": + return ec.fieldContext_DataframeRecipeOptimizationRuleState_enabled(ctx, field) + case "reason": + return ec.fieldContext_DataframeRecipeOptimizationRuleState_reason(ctx, field) + } + return nil, fmt.Errorf("no field named %q was found under type DataframeRecipeOptimizationRuleState", field.Name) +} + +func (ec *executionContext) childFields_DataframeRecipeOutputExplanation(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + switch field.Name { + case "name": + return ec.fieldContext_DataframeRecipeOutputExplanation_name(ctx, field) + case "rootResourceType": + return ec.fieldContext_DataframeRecipeOutputExplanation_rootResourceType(ctx, field) + case "rowGrain": + return ec.fieldContext_DataframeRecipeOutputExplanation_rowGrain(ctx, field) + case "fields": + return ec.fieldContext_DataframeRecipeOutputExplanation_fields(ctx, field) + case "identity": + return ec.fieldContext_DataframeRecipeOutputExplanation_identity(ctx, field) + case "expansion": + return ec.fieldContext_DataframeRecipeOutputExplanation_expansion(ctx, field) + case "dynamicMaps": + return ec.fieldContext_DataframeRecipeOutputExplanation_dynamicMaps(ctx, field) + case "catalogProjections": + return ec.fieldContext_DataframeRecipeOutputExplanation_catalogProjections(ctx, field) + } + return nil, fmt.Errorf("no field named %q was found under type DataframeRecipeOutputExplanation", field.Name) +} + +func (ec *executionContext) childFields_DataframeRecipeOutputValidation(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + switch field.Name { + case "name": + return ec.fieldContext_DataframeRecipeOutputValidation_name(ctx, field) + case "rootResourceType": + return ec.fieldContext_DataframeRecipeOutputValidation_rootResourceType(ctx, field) + case "rowGrain": + return ec.fieldContext_DataframeRecipeOutputValidation_rowGrain(ctx, field) + case "fieldNames": + return ec.fieldContext_DataframeRecipeOutputValidation_fieldNames(ctx, field) + case "dynamicColumns": + return ec.fieldContext_DataframeRecipeOutputValidation_dynamicColumns(ctx, field) + } + return nil, fmt.Errorf("no field named %q was found under type DataframeRecipeOutputValidation", field.Name) +} + +func (ec *executionContext) childFields_DataframeRecipePhysicalExplanation(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + switch field.Name { + case "outputs": + return ec.fieldContext_DataframeRecipePhysicalExplanation_outputs(ctx, field) + } + return nil, fmt.Errorf("no field named %q was found under type DataframeRecipePhysicalExplanation", field.Name) +} + +func (ec *executionContext) childFields_DataframeRecipePhysicalOutputExplanation(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + switch field.Name { + case "name": + return ec.fieldContext_DataframeRecipePhysicalOutputExplanation_name(ctx, field) + case "planFingerprint": + return ec.fieldContext_DataframeRecipePhysicalOutputExplanation_planFingerprint(ctx, field) + case "columns": + return ec.fieldContext_DataframeRecipePhysicalOutputExplanation_columns(ctx, field) + case "traversalSets": + return ec.fieldContext_DataframeRecipePhysicalOutputExplanation_traversalSets(ctx, field) + case "endpointTraversalCount": + return ec.fieldContext_DataframeRecipePhysicalOutputExplanation_endpointTraversalCount(ctx, field) + case "nativeTraversalCount": + return ec.fieldContext_DataframeRecipePhysicalOutputExplanation_nativeTraversalCount(ctx, field) + case "sharedTraversalCount": + return ec.fieldContext_DataframeRecipePhysicalOutputExplanation_sharedTraversalCount(ctx, field) + case "requiredMatchReuseCount": + return ec.fieldContext_DataframeRecipePhysicalOutputExplanation_requiredMatchReuseCount(ctx, field) + case "optimization": + return ec.fieldContext_DataframeRecipePhysicalOutputExplanation_optimization(ctx, field) + case "live": + return ec.fieldContext_DataframeRecipePhysicalOutputExplanation_live(ctx, field) + } + return nil, fmt.Errorf("no field named %q was found under type DataframeRecipePhysicalOutputExplanation", field.Name) +} + +func (ec *executionContext) childFields_DataframeRecipePreflight(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + switch field.Name { + case "name": + return ec.fieldContext_DataframeRecipePreflight_name(ctx, field) + case "recipeDigest": + return ec.fieldContext_DataframeRecipePreflight_recipeDigest(ctx, field) + case "resolvedSchemaDigest": + return ec.fieldContext_DataframeRecipePreflight_resolvedSchemaDigest(ctx, field) + case "sourceGeneration": + return ec.fieldContext_DataframeRecipePreflight_sourceGeneration(ctx, field) + case "scopeDigest": + return ec.fieldContext_DataframeRecipePreflight_scopeDigest(ctx, field) + case "columns": + return ec.fieldContext_DataframeRecipePreflight_columns(ctx, field) + } + return nil, fmt.Errorf("no field named %q was found under type DataframeRecipePreflight", field.Name) +} + +func (ec *executionContext) childFields_DataframeRecipePreview(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + switch field.Name { + case "name": + return ec.fieldContext_DataframeRecipePreview_name(ctx, field) + case "recipeDigest": + return ec.fieldContext_DataframeRecipePreview_recipeDigest(ctx, field) + case "resolvedSchemaDigest": + return ec.fieldContext_DataframeRecipePreview_resolvedSchemaDigest(ctx, field) + case "sourceGeneration": + return ec.fieldContext_DataframeRecipePreview_sourceGeneration(ctx, field) + case "outputs": + return ec.fieldContext_DataframeRecipePreview_outputs(ctx, field) + } + return nil, fmt.Errorf("no field named %q was found under type DataframeRecipePreview", field.Name) +} + +func (ec *executionContext) childFields_DataframeRecipePreviewOutput(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + switch field.Name { + case "name": + return ec.fieldContext_DataframeRecipePreviewOutput_name(ctx, field) + case "columns": + return ec.fieldContext_DataframeRecipePreviewOutput_columns(ctx, field) + case "rows": + return ec.fieldContext_DataframeRecipePreviewOutput_rows(ctx, field) + case "csv": + return ec.fieldContext_DataframeRecipePreviewOutput_csv(ctx, field) + case "rowCount": + return ec.fieldContext_DataframeRecipePreviewOutput_rowCount(ctx, field) + } + return nil, fmt.Errorf("no field named %q was found under type DataframeRecipePreviewOutput", field.Name) +} + +func (ec *executionContext) childFields_DataframeRecipeResult(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + switch field.Name { + case "name": + return ec.fieldContext_DataframeRecipeResult_name(ctx, field) + case "recipeDigest": + return ec.fieldContext_DataframeRecipeResult_recipeDigest(ctx, field) + case "resolvedSchemaDigest": + return ec.fieldContext_DataframeRecipeResult_resolvedSchemaDigest(ctx, field) + case "sourceGeneration": + return ec.fieldContext_DataframeRecipeResult_sourceGeneration(ctx, field) + case "outputs": + return ec.fieldContext_DataframeRecipeResult_outputs(ctx, field) + } + return nil, fmt.Errorf("no field named %q was found under type DataframeRecipeResult", field.Name) +} + +func (ec *executionContext) childFields_DataframeRecipeResultOutput(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + switch field.Name { + case "name": + return ec.fieldContext_DataframeRecipeResultOutput_name(ctx, field) + case "columns": + return ec.fieldContext_DataframeRecipeResultOutput_columns(ctx, field) + case "rows": + return ec.fieldContext_DataframeRecipeResultOutput_rows(ctx, field) + case "csv": + return ec.fieldContext_DataframeRecipeResultOutput_csv(ctx, field) + case "rowCount": + return ec.fieldContext_DataframeRecipeResultOutput_rowCount(ctx, field) + } + return nil, fmt.Errorf("no field named %q was found under type DataframeRecipeResultOutput", field.Name) +} + +func (ec *executionContext) childFields_DataframeRecipeValidation(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + switch field.Name { + case "name": + return ec.fieldContext_DataframeRecipeValidation_name(ctx, field) + case "recipeDigest": + return ec.fieldContext_DataframeRecipeValidation_recipeDigest(ctx, field) + case "translationVersion": + return ec.fieldContext_DataframeRecipeValidation_translationVersion(ctx, field) + case "outputs": + return ec.fieldContext_DataframeRecipeValidation_outputs(ctx, field) + } + return nil, fmt.Errorf("no field named %q was found under type DataframeRecipeValidation", field.Name) +} + +func (ec *executionContext) childFields_DataframeRelatedResourceHints(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + switch field.Name { + case "viaLabel": + return ec.fieldContext_DataframeRelatedResourceHints_viaLabel(ctx, field) + case "edgeCount": + return ec.fieldContext_DataframeRelatedResourceHints_edgeCount(ctx, field) + case "target": + return ec.fieldContext_DataframeRelatedResourceHints_target(ctx, field) + } + return nil, fmt.Errorf("no field named %q was found under type DataframeRelatedResourceHints", field.Name) +} + +func (ec *executionContext) childFields_DataframeResourceHints(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + switch field.Name { + case "resourceType": + return ec.fieldContext_DataframeResourceHints_resourceType(ctx, field) + case "fields": + return ec.fieldContext_DataframeResourceHints_fields(ctx, field) + case "pivotFields": + return ec.fieldContext_DataframeResourceHints_pivotFields(ctx, field) + case "traversals": + return ec.fieldContext_DataframeResourceHints_traversals(ctx, field) + } + return nil, fmt.Errorf("no field named %q was found under type DataframeResourceHints", field.Name) +} + +func (ec *executionContext) childFields_DataframeRichSourceReuse(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + switch field.Name { + case "sourceSet": + return ec.fieldContext_DataframeRichSourceReuse_sourceSet(ctx, field) + case "aggregateConsumers": + return ec.fieldContext_DataframeRichSourceReuse_aggregateConsumers(ctx, field) + case "pivotConsumers": + return ec.fieldContext_DataframeRichSourceReuse_pivotConsumers(ctx, field) + case "sliceConsumers": + return ec.fieldContext_DataframeRichSourceReuse_sliceConsumers(ctx, field) + case "totalConsumers": + return ec.fieldContext_DataframeRichSourceReuse_totalConsumers(ctx, field) + } + return nil, fmt.Errorf("no field named %q was found under type DataframeRichSourceReuse", field.Name) +} + +func (ec *executionContext) childFields_DataframeRowConnection(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + switch field.Name { + case "materialization": + return ec.fieldContext_DataframeRowConnection_materialization(ctx, field) + case "columns": + return ec.fieldContext_DataframeRowConnection_columns(ctx, field) + case "rows": + return ec.fieldContext_DataframeRowConnection_rows(ctx, field) + case "totalCount": + return ec.fieldContext_DataframeRowConnection_totalCount(ctx, field) + case "pageInfo": + return ec.fieldContext_DataframeRowConnection_pageInfo(ctx, field) + } + return nil, fmt.Errorf("no field named %q was found under type DataframeRowConnection", field.Name) +} + +func (ec *executionContext) childFields_DataframeTraversalHint(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + switch field.Name { + case "fromType": + return ec.fieldContext_DataframeTraversalHint_fromType(ctx, field) + case "label": + return ec.fieldContext_DataframeTraversalHint_label(ctx, field) + case "toType": + return ec.fieldContext_DataframeTraversalHint_toType(ctx, field) + case "edgeCount": + return ec.fieldContext_DataframeTraversalHint_edgeCount(ctx, field) + } + return nil, fmt.Errorf("no field named %q was found under type DataframeTraversalHint", field.Name) +} + +func (ec *executionContext) childFields_FhirDataframeResult(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + switch field.Name { + case "columns": + return ec.fieldContext_FhirDataframeResult_columns(ctx, field) + case "rows": + return ec.fieldContext_FhirDataframeResult_rows(ctx, field) + case "rowCount": + return ec.fieldContext_FhirDataframeResult_rowCount(ctx, field) + case "diagnostics": + return ec.fieldContext_FhirDataframeResult_diagnostics(ctx, field) + } + return nil, fmt.Errorf("no field named %q was found under type FhirDataframeResult", field.Name) +} + +func (ec *executionContext) childFields___Directive(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + switch field.Name { + case "name": + return ec.fieldContext___Directive_name(ctx, field) + case "description": + return ec.fieldContext___Directive_description(ctx, field) + case "isRepeatable": + return ec.fieldContext___Directive_isRepeatable(ctx, field) + case "locations": + return ec.fieldContext___Directive_locations(ctx, field) + case "args": + return ec.fieldContext___Directive_args(ctx, field) + } + return nil, fmt.Errorf("no field named %q was found under type __Directive", field.Name) +} + +func (ec *executionContext) childFields___EnumValue(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + switch field.Name { + case "name": + return ec.fieldContext___EnumValue_name(ctx, field) + case "description": + return ec.fieldContext___EnumValue_description(ctx, field) + case "isDeprecated": + return ec.fieldContext___EnumValue_isDeprecated(ctx, field) + case "deprecationReason": + return ec.fieldContext___EnumValue_deprecationReason(ctx, field) + } + return nil, fmt.Errorf("no field named %q was found under type __EnumValue", field.Name) +} + +func (ec *executionContext) childFields___Field(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + switch field.Name { + case "name": + return ec.fieldContext___Field_name(ctx, field) + case "description": + return ec.fieldContext___Field_description(ctx, field) + case "args": + return ec.fieldContext___Field_args(ctx, field) + case "type": + return ec.fieldContext___Field_type(ctx, field) + case "isDeprecated": + return ec.fieldContext___Field_isDeprecated(ctx, field) + case "deprecationReason": + return ec.fieldContext___Field_deprecationReason(ctx, field) + } + return nil, fmt.Errorf("no field named %q was found under type __Field", field.Name) +} + +func (ec *executionContext) childFields___InputValue(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + switch field.Name { + case "name": + return ec.fieldContext___InputValue_name(ctx, field) + case "description": + return ec.fieldContext___InputValue_description(ctx, field) + case "type": + return ec.fieldContext___InputValue_type(ctx, field) + case "defaultValue": + return ec.fieldContext___InputValue_defaultValue(ctx, field) + case "isDeprecated": + return ec.fieldContext___InputValue_isDeprecated(ctx, field) + case "deprecationReason": + return ec.fieldContext___InputValue_deprecationReason(ctx, field) + } + return nil, fmt.Errorf("no field named %q was found under type __InputValue", field.Name) +} + +func (ec *executionContext) childFields___Schema(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + switch field.Name { + case "description": + return ec.fieldContext___Schema_description(ctx, field) + case "types": + return ec.fieldContext___Schema_types(ctx, field) + case "queryType": + return ec.fieldContext___Schema_queryType(ctx, field) + case "mutationType": + return ec.fieldContext___Schema_mutationType(ctx, field) + case "subscriptionType": + return ec.fieldContext___Schema_subscriptionType(ctx, field) + case "directives": + return ec.fieldContext___Schema_directives(ctx, field) + } + return nil, fmt.Errorf("no field named %q was found under type __Schema", field.Name) +} + +func (ec *executionContext) childFields___Type(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + switch field.Name { + case "kind": + return ec.fieldContext___Type_kind(ctx, field) + case "name": + return ec.fieldContext___Type_name(ctx, field) + case "description": + return ec.fieldContext___Type_description(ctx, field) + case "specifiedByURL": + return ec.fieldContext___Type_specifiedByURL(ctx, field) + case "fields": + return ec.fieldContext___Type_fields(ctx, field) + case "interfaces": + return ec.fieldContext___Type_interfaces(ctx, field) + case "possibleTypes": + return ec.fieldContext___Type_possibleTypes(ctx, field) + case "enumValues": + return ec.fieldContext___Type_enumValues(ctx, field) + case "inputFields": + return ec.fieldContext___Type_inputFields(ctx, field) + case "ofType": + return ec.fieldContext___Type_ofType(ctx, field) + case "isOneOf": + return ec.fieldContext___Type_isOneOf(ctx, field) + } + return nil, fmt.Errorf("no field named %q was found under type __Type", field.Name) +} + +// endregion ************************** internal!.gotpl *************************** + +// region ***************************** args.gotpl ***************************** + +func (ec *executionContext) field_Mutation_materializeDataframeRecipeBundle_args(ctx context.Context, rawArgs map[string]any) (map[string]any, error) { + var err error + args := map[string]any{} + arg0, err := graphql.ProcessArgField(ctx, rawArgs, "input", + func(ctx context.Context, v any) (model.MaterializeDataframeRecipeInput, error) { + return ec.unmarshalNMaterializeDataframeRecipeInput2githubᚗcomᚋcalyprᚋloomᚋgraphqlapiᚋmodelᚐMaterializeDataframeRecipeInput(ctx, v) + }) if err != nil { - return graphql.Null + return nil, err } - ctx = graphql.WithFieldContext(ctx, fc) - defer func() { - if r := recover(); r != nil { - ec.Error(ctx, ec.Recover(ctx, r)) - ret = graphql.Null - } - }() - resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { - ctx = rctx // use context from middleware stack in children - return obj.PivotCandidate, nil - }) + args["input"] = arg0 + return args, nil +} + +func (ec *executionContext) field_Mutation_previewDataframeRecipe_args(ctx context.Context, rawArgs map[string]any) (map[string]any, error) { + var err error + args := map[string]any{} + arg0, err := graphql.ProcessArgField(ctx, rawArgs, "input", + func(ctx context.Context, v any) (model.PreviewDataframeRecipeInput, error) { + return ec.unmarshalNPreviewDataframeRecipeInput2githubᚗcomᚋcalyprᚋloomᚋgraphqlapiᚋmodelᚐPreviewDataframeRecipeInput(ctx, v) + }) if err != nil { - ec.Error(ctx, err) - return graphql.Null - } - if resTmp == nil { - if !graphql.HasFieldError(ctx, fc) { - ec.Errorf(ctx, "must not be null") - } - return graphql.Null + return nil, err } - res := resTmp.(bool) - fc.Result = res - return ec.marshalNBoolean2bool(ctx, field.Selections, res) + args["input"] = arg0 + return args, nil } -func (ec *executionContext) fieldContext_DataframeFieldHint_pivotCandidate(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { - fc = &graphql.FieldContext{ - Object: "DataframeFieldHint", - Field: field, - IsMethod: false, - IsResolver: false, - Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { - return nil, errors.New("field of type Boolean does not have child fields") - }, +func (ec *executionContext) field_Mutation_runDataframeRecipe_args(ctx context.Context, rawArgs map[string]any) (map[string]any, error) { + var err error + args := map[string]any{} + arg0, err := graphql.ProcessArgField(ctx, rawArgs, "input", + func(ctx context.Context, v any) (model.RunDataframeRecipeInput, error) { + return ec.unmarshalNRunDataframeRecipeInput2githubᚗcomᚋcalyprᚋloomᚋgraphqlapiᚋmodelᚐRunDataframeRecipeInput(ctx, v) + }) + if err != nil { + return nil, err } - return fc, nil + args["input"] = arg0 + return args, nil } -func (ec *executionContext) _DataframeFieldHint_pivotKind(ctx context.Context, field graphql.CollectedField, obj *model.DataframeFieldHint) (ret graphql.Marshaler) { - fc, err := ec.fieldContext_DataframeFieldHint_pivotKind(ctx, field) +func (ec *executionContext) field_Mutation_runFhirDataframe_args(ctx context.Context, rawArgs map[string]any) (map[string]any, error) { + var err error + args := map[string]any{} + arg0, err := graphql.ProcessArgField(ctx, rawArgs, "input", + func(ctx context.Context, v any) (model.FhirDataframeInput, error) { + return ec.unmarshalNFhirDataframeInput2githubᚗcomᚋcalyprᚋloomᚋgraphqlapiᚋmodelᚐFhirDataframeInput(ctx, v) + }) if err != nil { - return graphql.Null + return nil, err } - ctx = graphql.WithFieldContext(ctx, fc) - defer func() { - if r := recover(); r != nil { - ec.Error(ctx, ec.Recover(ctx, r)) - ret = graphql.Null - } - }() - resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { - ctx = rctx // use context from middleware stack in children - return obj.PivotKind, nil - }) + args["input"] = arg0 + arg1, err := graphql.ProcessArgField(ctx, rawArgs, "limit", + func(ctx context.Context, v any) (*int, error) { + return ec.unmarshalOInt2ᚖint(ctx, v) + }) if err != nil { - ec.Error(ctx, err) - return graphql.Null - } - if resTmp == nil { - return graphql.Null + return nil, err } - res := resTmp.(*string) - fc.Result = res - return ec.marshalOString2ᚖstring(ctx, field.Selections, res) + args["limit"] = arg1 + return args, nil } -func (ec *executionContext) fieldContext_DataframeFieldHint_pivotKind(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { - fc = &graphql.FieldContext{ - Object: "DataframeFieldHint", - Field: field, - IsMethod: false, - IsResolver: false, - Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { - return nil, errors.New("field of type String does not have child fields") - }, +func (ec *executionContext) field_Mutation_validateDataframeRecipe_args(ctx context.Context, rawArgs map[string]any) (map[string]any, error) { + var err error + args := map[string]any{} + arg0, err := graphql.ProcessArgField(ctx, rawArgs, "input", + func(ctx context.Context, v any) (model.ValidateDataframeRecipeInput, error) { + return ec.unmarshalNValidateDataframeRecipeInput2githubᚗcomᚋcalyprᚋloomᚋgraphqlapiᚋmodelᚐValidateDataframeRecipeInput(ctx, v) + }) + if err != nil { + return nil, err } - return fc, nil + args["input"] = arg0 + return args, nil } -func (ec *executionContext) _DataframeFieldHint_pivotColumns(ctx context.Context, field graphql.CollectedField, obj *model.DataframeFieldHint) (ret graphql.Marshaler) { - fc, err := ec.fieldContext_DataframeFieldHint_pivotColumns(ctx, field) +func (ec *executionContext) field_Query___type_args(ctx context.Context, rawArgs map[string]any) (map[string]any, error) { + var err error + args := map[string]any{} + arg0, err := graphql.ProcessArgField(ctx, rawArgs, "name", + func(ctx context.Context, v any) (string, error) { + return ec.unmarshalNString2string(ctx, v) + }) if err != nil { - return graphql.Null + return nil, err } - ctx = graphql.WithFieldContext(ctx, fc) - defer func() { - if r := recover(); r != nil { - ec.Error(ctx, ec.Recover(ctx, r)) - ret = graphql.Null - } - }() - resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { - ctx = rctx // use context from middleware stack in children - return obj.PivotColumns, nil - }) + args["name"] = arg0 + return args, nil +} + +func (ec *executionContext) field_Query_dataframeAggregate_args(ctx context.Context, rawArgs map[string]any) (map[string]any, error) { + var err error + args := map[string]any{} + arg0, err := graphql.ProcessArgField(ctx, rawArgs, "input", + func(ctx context.Context, v any) (model.DataframeAggregateInput, error) { + return ec.unmarshalNDataframeAggregateInput2githubᚗcomᚋcalyprᚋloomᚋgraphqlapiᚋmodelᚐDataframeAggregateInput(ctx, v) + }) if err != nil { - ec.Error(ctx, err) - return graphql.Null - } - if resTmp == nil { - if !graphql.HasFieldError(ctx, fc) { - ec.Errorf(ctx, "must not be null") - } - return graphql.Null + return nil, err } - res := resTmp.([]string) - fc.Result = res - return ec.marshalNString2ᚕstringᚄ(ctx, field.Selections, res) + args["input"] = arg0 + return args, nil } -func (ec *executionContext) fieldContext_DataframeFieldHint_pivotColumns(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { - fc = &graphql.FieldContext{ - Object: "DataframeFieldHint", - Field: field, - IsMethod: false, - IsResolver: false, - Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { - return nil, errors.New("field of type String does not have child fields") - }, +func (ec *executionContext) field_Query_dataframeBuilderIntrospection_args(ctx context.Context, rawArgs map[string]any) (map[string]any, error) { + var err error + args := map[string]any{} + arg0, err := graphql.ProcessArgField(ctx, rawArgs, "input", + func(ctx context.Context, v any) (model.DataframeBuilderIntrospectionInput, error) { + return ec.unmarshalNDataframeBuilderIntrospectionInput2githubᚗcomᚋcalyprᚋloomᚋgraphqlapiᚋmodelᚐDataframeBuilderIntrospectionInput(ctx, v) + }) + if err != nil { + return nil, err } - return fc, nil + args["input"] = arg0 + return args, nil } -func (ec *executionContext) _DataframeFieldHint_pivotFamily(ctx context.Context, field graphql.CollectedField, obj *model.DataframeFieldHint) (ret graphql.Marshaler) { - fc, err := ec.fieldContext_DataframeFieldHint_pivotFamily(ctx, field) +func (ec *executionContext) field_Query_dataframeDataset_args(ctx context.Context, rawArgs map[string]any) (map[string]any, error) { + var err error + args := map[string]any{} + arg0, err := graphql.ProcessArgField(ctx, rawArgs, "input", + func(ctx context.Context, v any) (model.DataframeDatasetInput, error) { + return ec.unmarshalNDataframeDatasetInput2githubᚗcomᚋcalyprᚋloomᚋgraphqlapiᚋmodelᚐDataframeDatasetInput(ctx, v) + }) if err != nil { - return graphql.Null + return nil, err } - ctx = graphql.WithFieldContext(ctx, fc) - defer func() { - if r := recover(); r != nil { - ec.Error(ctx, ec.Recover(ctx, r)) - ret = graphql.Null - } - }() - resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { - ctx = rctx // use context from middleware stack in children - return obj.PivotFamily, nil - }) - if err != nil { - ec.Error(ctx, err) - return graphql.Null - } - if resTmp == nil { - return graphql.Null - } - res := resTmp.(*model.FhirPivotFamily) - fc.Result = res - return ec.marshalOFhirPivotFamily2ᚖgithubᚗcomᚋcalyprᚋloomᚋgraphqlapiᚋmodelᚐFhirPivotFamily(ctx, field.Selections, res) + args["input"] = arg0 + return args, nil } -func (ec *executionContext) fieldContext_DataframeFieldHint_pivotFamily(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { - fc = &graphql.FieldContext{ - Object: "DataframeFieldHint", - Field: field, - IsMethod: false, - IsResolver: false, - Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { - return nil, errors.New("field of type FhirPivotFamily does not have child fields") - }, +func (ec *executionContext) field_Query_dataframeMaterialization_args(ctx context.Context, rawArgs map[string]any) (map[string]any, error) { + var err error + args := map[string]any{} + arg0, err := graphql.ProcessArgField(ctx, rawArgs, "id", + func(ctx context.Context, v any) (string, error) { + return ec.unmarshalNID2string(ctx, v) + }) + if err != nil { + return nil, err } - return fc, nil + args["id"] = arg0 + return args, nil } -func (ec *executionContext) _DataframeFieldHint_defaultPivotColumnSelector(ctx context.Context, field graphql.CollectedField, obj *model.DataframeFieldHint) (ret graphql.Marshaler) { - fc, err := ec.fieldContext_DataframeFieldHint_defaultPivotColumnSelector(ctx, field) +func (ec *executionContext) field_Query_dataframeRecipeExecution_args(ctx context.Context, rawArgs map[string]any) (map[string]any, error) { + var err error + args := map[string]any{} + arg0, err := graphql.ProcessArgField(ctx, rawArgs, "id", + func(ctx context.Context, v any) (string, error) { + return ec.unmarshalNID2string(ctx, v) + }) if err != nil { - return graphql.Null + return nil, err } - ctx = graphql.WithFieldContext(ctx, fc) - defer func() { - if r := recover(); r != nil { - ec.Error(ctx, ec.Recover(ctx, r)) - ret = graphql.Null - } - }() - resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { - ctx = rctx // use context from middleware stack in children - return obj.DefaultPivotColumnSelector, nil - }) + args["id"] = arg0 + return args, nil +} + +func (ec *executionContext) field_Query_dataframeRows_args(ctx context.Context, rawArgs map[string]any) (map[string]any, error) { + var err error + args := map[string]any{} + arg0, err := graphql.ProcessArgField(ctx, rawArgs, "input", + func(ctx context.Context, v any) (model.DataframeRowsInput, error) { + return ec.unmarshalNDataframeRowsInput2githubᚗcomᚋcalyprᚋloomᚋgraphqlapiᚋmodelᚐDataframeRowsInput(ctx, v) + }) if err != nil { - ec.Error(ctx, err) - return graphql.Null - } - if resTmp == nil { - return graphql.Null + return nil, err } - res := resTmp.(*model.DataframeFieldSelector) - fc.Result = res - return ec.marshalODataframeFieldSelector2ᚖgithubᚗcomᚋcalyprᚋloomᚋgraphqlapiᚋmodelᚐDataframeFieldSelector(ctx, field.Selections, res) + args["input"] = arg0 + return args, nil } -func (ec *executionContext) fieldContext_DataframeFieldHint_defaultPivotColumnSelector(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { - fc = &graphql.FieldContext{ - Object: "DataframeFieldHint", - Field: field, - IsMethod: false, - IsResolver: false, - Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { - switch field.Name { - case "sourcePath": - return ec.fieldContext_DataframeFieldSelector_sourcePath(ctx, field) - case "where": - return ec.fieldContext_DataframeFieldSelector_where(ctx, field) - case "valuePath": - return ec.fieldContext_DataframeFieldSelector_valuePath(ctx, field) - } - return nil, fmt.Errorf("no field named %q was found under type DataframeFieldSelector", field.Name) - }, +func (ec *executionContext) field_Query_explainDataframeRecipe_args(ctx context.Context, rawArgs map[string]any) (map[string]any, error) { + var err error + args := map[string]any{} + arg0, err := graphql.ProcessArgField(ctx, rawArgs, "input", + func(ctx context.Context, v any) (model.ExplainDataframeRecipeInput, error) { + return ec.unmarshalNExplainDataframeRecipeInput2githubᚗcomᚋcalyprᚋloomᚋgraphqlapiᚋmodelᚐExplainDataframeRecipeInput(ctx, v) + }) + if err != nil { + return nil, err } - return fc, nil + args["input"] = arg0 + return args, nil } -func (ec *executionContext) _DataframeFieldHint_defaultPivotValueSelector(ctx context.Context, field graphql.CollectedField, obj *model.DataframeFieldHint) (ret graphql.Marshaler) { - fc, err := ec.fieldContext_DataframeFieldHint_defaultPivotValueSelector(ctx, field) +func (ec *executionContext) field_Query_preflightDataframeRecipe_args(ctx context.Context, rawArgs map[string]any) (map[string]any, error) { + var err error + args := map[string]any{} + arg0, err := graphql.ProcessArgField(ctx, rawArgs, "input", + func(ctx context.Context, v any) (model.PreflightDataframeRecipeInput, error) { + return ec.unmarshalNPreflightDataframeRecipeInput2githubᚗcomᚋcalyprᚋloomᚋgraphqlapiᚋmodelᚐPreflightDataframeRecipeInput(ctx, v) + }) if err != nil { - return graphql.Null + return nil, err } - ctx = graphql.WithFieldContext(ctx, fc) - defer func() { - if r := recover(); r != nil { - ec.Error(ctx, ec.Recover(ctx, r)) - ret = graphql.Null - } - }() - resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { - ctx = rctx // use context from middleware stack in children - return obj.DefaultPivotValueSelector, nil - }) + args["input"] = arg0 + return args, nil +} + +func (ec *executionContext) field___Directive_args_args(ctx context.Context, rawArgs map[string]any) (map[string]any, error) { + var err error + args := map[string]any{} + arg0, err := graphql.ProcessArgField(ctx, rawArgs, "includeDeprecated", + func(ctx context.Context, v any) (*bool, error) { + return ec.unmarshalOBoolean2ᚖbool(ctx, v) + }) if err != nil { - ec.Error(ctx, err) - return graphql.Null - } - if resTmp == nil { - return graphql.Null + return nil, err } - res := resTmp.(*model.DataframeFieldSelector) - fc.Result = res - return ec.marshalODataframeFieldSelector2ᚖgithubᚗcomᚋcalyprᚋloomᚋgraphqlapiᚋmodelᚐDataframeFieldSelector(ctx, field.Selections, res) + args["includeDeprecated"] = arg0 + return args, nil } -func (ec *executionContext) fieldContext_DataframeFieldHint_defaultPivotValueSelector(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { - fc = &graphql.FieldContext{ - Object: "DataframeFieldHint", - Field: field, - IsMethod: false, - IsResolver: false, - Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { - switch field.Name { - case "sourcePath": - return ec.fieldContext_DataframeFieldSelector_sourcePath(ctx, field) - case "where": - return ec.fieldContext_DataframeFieldSelector_where(ctx, field) - case "valuePath": - return ec.fieldContext_DataframeFieldSelector_valuePath(ctx, field) - } - return nil, fmt.Errorf("no field named %q was found under type DataframeFieldSelector", field.Name) - }, +func (ec *executionContext) field___Field_args_args(ctx context.Context, rawArgs map[string]any) (map[string]any, error) { + var err error + args := map[string]any{} + arg0, err := graphql.ProcessArgField(ctx, rawArgs, "includeDeprecated", + func(ctx context.Context, v any) (*bool, error) { + return ec.unmarshalOBoolean2ᚖbool(ctx, v) + }) + if err != nil { + return nil, err } - return fc, nil + args["includeDeprecated"] = arg0 + return args, nil } -func (ec *executionContext) _DataframeFieldPredicate_path(ctx context.Context, field graphql.CollectedField, obj *model.DataframeFieldPredicate) (ret graphql.Marshaler) { - fc, err := ec.fieldContext_DataframeFieldPredicate_path(ctx, field) +func (ec *executionContext) field___Type_enumValues_args(ctx context.Context, rawArgs map[string]any) (map[string]any, error) { + var err error + args := map[string]any{} + arg0, err := graphql.ProcessArgField(ctx, rawArgs, "includeDeprecated", + func(ctx context.Context, v any) (bool, error) { + return ec.unmarshalOBoolean2bool(ctx, v) + }) if err != nil { - return graphql.Null + return nil, err } - ctx = graphql.WithFieldContext(ctx, fc) - defer func() { - if r := recover(); r != nil { - ec.Error(ctx, ec.Recover(ctx, r)) - ret = graphql.Null - } - }() - resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { - ctx = rctx // use context from middleware stack in children - return obj.Path, nil - }) + args["includeDeprecated"] = arg0 + return args, nil +} + +func (ec *executionContext) field___Type_fields_args(ctx context.Context, rawArgs map[string]any) (map[string]any, error) { + var err error + args := map[string]any{} + arg0, err := graphql.ProcessArgField(ctx, rawArgs, "includeDeprecated", + func(ctx context.Context, v any) (bool, error) { + return ec.unmarshalOBoolean2bool(ctx, v) + }) if err != nil { - ec.Error(ctx, err) - return graphql.Null - } - if resTmp == nil { - if !graphql.HasFieldError(ctx, fc) { - ec.Errorf(ctx, "must not be null") - } - return graphql.Null + return nil, err } - res := resTmp.(string) - fc.Result = res - return ec.marshalNString2string(ctx, field.Selections, res) + args["includeDeprecated"] = arg0 + return args, nil } -func (ec *executionContext) fieldContext_DataframeFieldPredicate_path(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { +// endregion ***************************** args.gotpl ***************************** + +// region **************************** field.gotpl ***************************** + +func (ec *executionContext) _DataframeAggregateResult_materialization(ctx context.Context, field graphql.CollectedField, obj *model.DataframeAggregateResult) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_DataframeAggregateResult_materialization(ctx, field) + }, + func(ctx context.Context) (any, error) { + return obj.Materialization, nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v *model.DataframeMaterialization) graphql.Marshaler { + return ec.marshalNDataframeMaterialization2ᚖgithubᚗcomᚋcalyprᚋloomᚋgraphqlapiᚋmodelᚐDataframeMaterialization(ctx, selections, v) + }, + true, + true, + ) +} +func (ec *executionContext) fieldContext_DataframeAggregateResult_materialization(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { fc = &graphql.FieldContext{ - Object: "DataframeFieldPredicate", + Object: "DataframeAggregateResult", Field: field, IsMethod: false, IsResolver: false, Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { - return nil, errors.New("field of type String does not have child fields") + return ec.childFields_DataframeMaterialization(ctx, field) }, } return fc, nil } -func (ec *executionContext) _DataframeFieldPredicate_op(ctx context.Context, field graphql.CollectedField, obj *model.DataframeFieldPredicate) (ret graphql.Marshaler) { - fc, err := ec.fieldContext_DataframeFieldPredicate_op(ctx, field) - if err != nil { - return graphql.Null - } - ctx = graphql.WithFieldContext(ctx, fc) - defer func() { - if r := recover(); r != nil { - ec.Error(ctx, ec.Recover(ctx, r)) - ret = graphql.Null - } - }() - resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { - ctx = rctx // use context from middleware stack in children - return obj.Op, nil - }) - if err != nil { - ec.Error(ctx, err) - return graphql.Null - } - if resTmp == nil { - if !graphql.HasFieldError(ctx, fc) { - ec.Errorf(ctx, "must not be null") - } - return graphql.Null - } - res := resTmp.(model.FhirFieldPredicateOperation) - fc.Result = res - return ec.marshalNFhirFieldPredicateOperation2githubᚗcomᚋcalyprᚋloomᚋgraphqlapiᚋmodelᚐFhirFieldPredicateOperation(ctx, field.Selections, res) +func (ec *executionContext) _DataframeAggregateResult_columns(ctx context.Context, field graphql.CollectedField, obj *model.DataframeAggregateResult) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_DataframeAggregateResult_columns(ctx, field) + }, + func(ctx context.Context) (any, error) { + return obj.Columns, nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v []string) graphql.Marshaler { + return ec.marshalNString2ᚕstringᚄ(ctx, selections, v) + }, + true, + true, + ) +} +func (ec *executionContext) fieldContext_DataframeAggregateResult_columns(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + return graphql.NewScalarFieldContext("DataframeAggregateResult", field, false, false, errors.New("field of type String does not have child fields")) } -func (ec *executionContext) fieldContext_DataframeFieldPredicate_op(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { +func (ec *executionContext) _DataframeAggregateResult_rows(ctx context.Context, field graphql.CollectedField, obj *model.DataframeAggregateResult) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_DataframeAggregateResult_rows(ctx, field) + }, + func(ctx context.Context) (any, error) { + return obj.Rows, nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v json.RawMessage) graphql.Marshaler { + return ec.marshalNJSON2encodingᚋjsonᚐRawMessage(ctx, selections, v) + }, + true, + true, + ) +} +func (ec *executionContext) fieldContext_DataframeAggregateResult_rows(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + return graphql.NewScalarFieldContext("DataframeAggregateResult", field, false, false, errors.New("field of type JSON does not have child fields")) +} + +func (ec *executionContext) _DataframeBuilderIntrospection_project(ctx context.Context, field graphql.CollectedField, obj *model.DataframeBuilderIntrospection) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_DataframeBuilderIntrospection_project(ctx, field) + }, + func(ctx context.Context) (any, error) { + return obj.Project, nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v string) graphql.Marshaler { + return ec.marshalNString2string(ctx, selections, v) + }, + true, + true, + ) +} +func (ec *executionContext) fieldContext_DataframeBuilderIntrospection_project(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + return graphql.NewScalarFieldContext("DataframeBuilderIntrospection", field, false, false, errors.New("field of type String does not have child fields")) +} + +func (ec *executionContext) _DataframeBuilderIntrospection_rootResourceType(ctx context.Context, field graphql.CollectedField, obj *model.DataframeBuilderIntrospection) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_DataframeBuilderIntrospection_rootResourceType(ctx, field) + }, + func(ctx context.Context) (any, error) { + return obj.RootResourceType, nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v string) graphql.Marshaler { + return ec.marshalNString2string(ctx, selections, v) + }, + true, + true, + ) +} +func (ec *executionContext) fieldContext_DataframeBuilderIntrospection_rootResourceType(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + return graphql.NewScalarFieldContext("DataframeBuilderIntrospection", field, false, false, errors.New("field of type String does not have child fields")) +} + +func (ec *executionContext) _DataframeBuilderIntrospection_authResourcePaths(ctx context.Context, field graphql.CollectedField, obj *model.DataframeBuilderIntrospection) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_DataframeBuilderIntrospection_authResourcePaths(ctx, field) + }, + func(ctx context.Context) (any, error) { + return obj.AuthResourcePaths, nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v []string) graphql.Marshaler { + return ec.marshalNString2ᚕstringᚄ(ctx, selections, v) + }, + true, + true, + ) +} +func (ec *executionContext) fieldContext_DataframeBuilderIntrospection_authResourcePaths(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + return graphql.NewScalarFieldContext("DataframeBuilderIntrospection", field, false, false, errors.New("field of type String does not have child fields")) +} + +func (ec *executionContext) _DataframeBuilderIntrospection_root(ctx context.Context, field graphql.CollectedField, obj *model.DataframeBuilderIntrospection) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_DataframeBuilderIntrospection_root(ctx, field) + }, + func(ctx context.Context) (any, error) { + return obj.Root, nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v *model.DataframeResourceHints) graphql.Marshaler { + return ec.marshalNDataframeResourceHints2ᚖgithubᚗcomᚋcalyprᚋloomᚋgraphqlapiᚋmodelᚐDataframeResourceHints(ctx, selections, v) + }, + true, + true, + ) +} +func (ec *executionContext) fieldContext_DataframeBuilderIntrospection_root(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { fc = &graphql.FieldContext{ - Object: "DataframeFieldPredicate", + Object: "DataframeBuilderIntrospection", Field: field, IsMethod: false, IsResolver: false, Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { - return nil, errors.New("field of type FhirFieldPredicateOperation does not have child fields") + return ec.childFields_DataframeResourceHints(ctx, field) }, } return fc, nil } -func (ec *executionContext) _DataframeFieldPredicate_value(ctx context.Context, field graphql.CollectedField, obj *model.DataframeFieldPredicate) (ret graphql.Marshaler) { - fc, err := ec.fieldContext_DataframeFieldPredicate_value(ctx, field) - if err != nil { - return graphql.Null - } - ctx = graphql.WithFieldContext(ctx, fc) - defer func() { - if r := recover(); r != nil { - ec.Error(ctx, ec.Recover(ctx, r)) - ret = graphql.Null - } - }() - resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { - ctx = rctx // use context from middleware stack in children - return obj.Value, nil - }) - if err != nil { - ec.Error(ctx, err) - return graphql.Null - } - if resTmp == nil { - if !graphql.HasFieldError(ctx, fc) { - ec.Errorf(ctx, "must not be null") - } - return graphql.Null - } - res := resTmp.(string) - fc.Result = res - return ec.marshalNString2string(ctx, field.Selections, res) +func (ec *executionContext) _DataframeBuilderIntrospection_relatedResources(ctx context.Context, field graphql.CollectedField, obj *model.DataframeBuilderIntrospection) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_DataframeBuilderIntrospection_relatedResources(ctx, field) + }, + func(ctx context.Context) (any, error) { + return obj.RelatedResources, nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v []*model.DataframeRelatedResourceHints) graphql.Marshaler { + return ec.marshalNDataframeRelatedResourceHints2ᚕᚖgithubᚗcomᚋcalyprᚋloomᚋgraphqlapiᚋmodelᚐDataframeRelatedResourceHintsᚄ(ctx, selections, v) + }, + true, + true, + ) } - -func (ec *executionContext) fieldContext_DataframeFieldPredicate_value(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { +func (ec *executionContext) fieldContext_DataframeBuilderIntrospection_relatedResources(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { fc = &graphql.FieldContext{ - Object: "DataframeFieldPredicate", + Object: "DataframeBuilderIntrospection", Field: field, IsMethod: false, IsResolver: false, Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { - return nil, errors.New("field of type String does not have child fields") + return ec.childFields_DataframeRelatedResourceHints(ctx, field) }, } return fc, nil } -func (ec *executionContext) _DataframeFieldSelector_sourcePath(ctx context.Context, field graphql.CollectedField, obj *model.DataframeFieldSelector) (ret graphql.Marshaler) { - fc, err := ec.fieldContext_DataframeFieldSelector_sourcePath(ctx, field) - if err != nil { - return graphql.Null - } - ctx = graphql.WithFieldContext(ctx, fc) - defer func() { - if r := recover(); r != nil { - ec.Error(ctx, ec.Recover(ctx, r)) - ret = graphql.Null - } - }() - resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { - ctx = rctx // use context from middleware stack in children - return obj.SourcePath, nil - }) - if err != nil { - ec.Error(ctx, err) - return graphql.Null - } - if resTmp == nil { - return graphql.Null - } - res := resTmp.(*string) - fc.Result = res - return ec.marshalOString2ᚖstring(ctx, field.Selections, res) +func (ec *executionContext) _DataframeBuilderIntrospection_traversals(ctx context.Context, field graphql.CollectedField, obj *model.DataframeBuilderIntrospection) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_DataframeBuilderIntrospection_traversals(ctx, field) + }, + func(ctx context.Context) (any, error) { + return obj.Traversals, nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v []*model.DataframeTraversalHint) graphql.Marshaler { + return ec.marshalNDataframeTraversalHint2ᚕᚖgithubᚗcomᚋcalyprᚋloomᚋgraphqlapiᚋmodelᚐDataframeTraversalHintᚄ(ctx, selections, v) + }, + true, + true, + ) } - -func (ec *executionContext) fieldContext_DataframeFieldSelector_sourcePath(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { +func (ec *executionContext) fieldContext_DataframeBuilderIntrospection_traversals(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { fc = &graphql.FieldContext{ - Object: "DataframeFieldSelector", + Object: "DataframeBuilderIntrospection", Field: field, IsMethod: false, IsResolver: false, Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { - return nil, errors.New("field of type String does not have child fields") + return ec.childFields_DataframeTraversalHint(ctx, field) }, } return fc, nil } -func (ec *executionContext) _DataframeFieldSelector_where(ctx context.Context, field graphql.CollectedField, obj *model.DataframeFieldSelector) (ret graphql.Marshaler) { - fc, err := ec.fieldContext_DataframeFieldSelector_where(ctx, field) - if err != nil { - return graphql.Null - } - ctx = graphql.WithFieldContext(ctx, fc) - defer func() { - if r := recover(); r != nil { - ec.Error(ctx, ec.Recover(ctx, r)) - ret = graphql.Null - } - }() - resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { - ctx = rctx // use context from middleware stack in children - return obj.Where, nil - }) - if err != nil { - ec.Error(ctx, err) - return graphql.Null - } - if resTmp == nil { - return graphql.Null - } - res := resTmp.(*model.DataframeFieldPredicate) - fc.Result = res - return ec.marshalODataframeFieldPredicate2ᚖgithubᚗcomᚋcalyprᚋloomᚋgraphqlapiᚋmodelᚐDataframeFieldPredicate(ctx, field.Selections, res) +func (ec *executionContext) _DataframeBuilderIntrospection_fields(ctx context.Context, field graphql.CollectedField, obj *model.DataframeBuilderIntrospection) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_DataframeBuilderIntrospection_fields(ctx, field) + }, + func(ctx context.Context) (any, error) { + return obj.Fields, nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v []*model.DataframeFieldHint) graphql.Marshaler { + return ec.marshalNDataframeFieldHint2ᚕᚖgithubᚗcomᚋcalyprᚋloomᚋgraphqlapiᚋmodelᚐDataframeFieldHintᚄ(ctx, selections, v) + }, + true, + true, + ) } - -func (ec *executionContext) fieldContext_DataframeFieldSelector_where(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { +func (ec *executionContext) fieldContext_DataframeBuilderIntrospection_fields(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { fc = &graphql.FieldContext{ - Object: "DataframeFieldSelector", + Object: "DataframeBuilderIntrospection", Field: field, IsMethod: false, IsResolver: false, Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { - switch field.Name { - case "path": - return ec.fieldContext_DataframeFieldPredicate_path(ctx, field) - case "op": - return ec.fieldContext_DataframeFieldPredicate_op(ctx, field) - case "value": - return ec.fieldContext_DataframeFieldPredicate_value(ctx, field) - } - return nil, fmt.Errorf("no field named %q was found under type DataframeFieldPredicate", field.Name) + return ec.childFields_DataframeFieldHint(ctx, field) }, } return fc, nil } -func (ec *executionContext) _DataframeFieldSelector_valuePath(ctx context.Context, field graphql.CollectedField, obj *model.DataframeFieldSelector) (ret graphql.Marshaler) { - fc, err := ec.fieldContext_DataframeFieldSelector_valuePath(ctx, field) - if err != nil { - return graphql.Null - } - ctx = graphql.WithFieldContext(ctx, fc) - defer func() { - if r := recover(); r != nil { - ec.Error(ctx, ec.Recover(ctx, r)) - ret = graphql.Null - } - }() - resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { - ctx = rctx // use context from middleware stack in children - return obj.ValuePath, nil - }) - if err != nil { - ec.Error(ctx, err) - return graphql.Null - } - if resTmp == nil { - if !graphql.HasFieldError(ctx, fc) { - ec.Errorf(ctx, "must not be null") - } - return graphql.Null - } - res := resTmp.(string) - fc.Result = res - return ec.marshalNString2string(ctx, field.Selections, res) +func (ec *executionContext) _DataframeBuilderIntrospection_pivotFields(ctx context.Context, field graphql.CollectedField, obj *model.DataframeBuilderIntrospection) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_DataframeBuilderIntrospection_pivotFields(ctx, field) + }, + func(ctx context.Context) (any, error) { + return obj.PivotFields, nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v []*model.DataframeFieldHint) graphql.Marshaler { + return ec.marshalNDataframeFieldHint2ᚕᚖgithubᚗcomᚋcalyprᚋloomᚋgraphqlapiᚋmodelᚐDataframeFieldHintᚄ(ctx, selections, v) + }, + true, + true, + ) } - -func (ec *executionContext) fieldContext_DataframeFieldSelector_valuePath(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { +func (ec *executionContext) fieldContext_DataframeBuilderIntrospection_pivotFields(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { fc = &graphql.FieldContext{ - Object: "DataframeFieldSelector", + Object: "DataframeBuilderIntrospection", Field: field, IsMethod: false, IsResolver: false, Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { - return nil, errors.New("field of type String does not have child fields") + return ec.childFields_DataframeFieldHint(ctx, field) }, } return fc, nil } -func (ec *executionContext) _DataframeMaterialization_id(ctx context.Context, field graphql.CollectedField, obj *model.DataframeMaterialization) (ret graphql.Marshaler) { - fc, err := ec.fieldContext_DataframeMaterialization_id(ctx, field) - if err != nil { - return graphql.Null - } - ctx = graphql.WithFieldContext(ctx, fc) - defer func() { - if r := recover(); r != nil { - ec.Error(ctx, ec.Recover(ctx, r)) - ret = graphql.Null - } - }() - resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { - ctx = rctx // use context from middleware stack in children - return obj.ID, nil - }) - if err != nil { - ec.Error(ctx, err) - return graphql.Null - } - if resTmp == nil { - if !graphql.HasFieldError(ctx, fc) { - ec.Errorf(ctx, "must not be null") - } - return graphql.Null - } - res := resTmp.(string) - fc.Result = res - return ec.marshalNID2string(ctx, field.Selections, res) +func (ec *executionContext) _DataframeColumn_name(ctx context.Context, field graphql.CollectedField, obj *model.DataframeColumn) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_DataframeColumn_name(ctx, field) + }, + func(ctx context.Context) (any, error) { + return obj.Name, nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v string) graphql.Marshaler { + return ec.marshalNString2string(ctx, selections, v) + }, + true, + true, + ) +} +func (ec *executionContext) fieldContext_DataframeColumn_name(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + return graphql.NewScalarFieldContext("DataframeColumn", field, false, false, errors.New("field of type String does not have child fields")) } -func (ec *executionContext) fieldContext_DataframeMaterialization_id(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { - fc = &graphql.FieldContext{ - Object: "DataframeMaterialization", - Field: field, - IsMethod: false, - IsResolver: false, - Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { - return nil, errors.New("field of type ID does not have child fields") +func (ec *executionContext) _DataframeColumn_clickhouseType(ctx context.Context, field graphql.CollectedField, obj *model.DataframeColumn) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_DataframeColumn_clickhouseType(ctx, field) }, - } - return fc, nil + func(ctx context.Context) (any, error) { + return obj.ClickhouseType, nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v string) graphql.Marshaler { + return ec.marshalNString2string(ctx, selections, v) + }, + true, + true, + ) } - -func (ec *executionContext) _DataframeMaterialization_name(ctx context.Context, field graphql.CollectedField, obj *model.DataframeMaterialization) (ret graphql.Marshaler) { - fc, err := ec.fieldContext_DataframeMaterialization_name(ctx, field) - if err != nil { - return graphql.Null - } - ctx = graphql.WithFieldContext(ctx, fc) - defer func() { - if r := recover(); r != nil { - ec.Error(ctx, ec.Recover(ctx, r)) - ret = graphql.Null - } - }() - resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { - ctx = rctx // use context from middleware stack in children - return obj.Name, nil - }) - if err != nil { - ec.Error(ctx, err) - return graphql.Null - } - if resTmp == nil { - if !graphql.HasFieldError(ctx, fc) { - ec.Errorf(ctx, "must not be null") - } - return graphql.Null - } - res := resTmp.(string) - fc.Result = res - return ec.marshalNString2string(ctx, field.Selections, res) +func (ec *executionContext) fieldContext_DataframeColumn_clickhouseType(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + return graphql.NewScalarFieldContext("DataframeColumn", field, false, false, errors.New("field of type String does not have child fields")) } -func (ec *executionContext) fieldContext_DataframeMaterialization_name(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { - fc = &graphql.FieldContext{ - Object: "DataframeMaterialization", - Field: field, - IsMethod: false, - IsResolver: false, - Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { - return nil, errors.New("field of type String does not have child fields") +func (ec *executionContext) _DataframeColumn_logicalType(ctx context.Context, field graphql.CollectedField, obj *model.DataframeColumn) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_DataframeColumn_logicalType(ctx, field) }, - } - return fc, nil + func(ctx context.Context) (any, error) { + return obj.LogicalType, nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v string) graphql.Marshaler { + return ec.marshalNString2string(ctx, selections, v) + }, + true, + true, + ) +} +func (ec *executionContext) fieldContext_DataframeColumn_logicalType(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + return graphql.NewScalarFieldContext("DataframeColumn", field, false, false, errors.New("field of type String does not have child fields")) } -func (ec *executionContext) _DataframeMaterialization_project(ctx context.Context, field graphql.CollectedField, obj *model.DataframeMaterialization) (ret graphql.Marshaler) { - fc, err := ec.fieldContext_DataframeMaterialization_project(ctx, field) - if err != nil { - return graphql.Null - } - ctx = graphql.WithFieldContext(ctx, fc) - defer func() { - if r := recover(); r != nil { - ec.Error(ctx, ec.Recover(ctx, r)) - ret = graphql.Null - } - }() - resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { - ctx = rctx // use context from middleware stack in children - return obj.Project, nil - }) - if err != nil { - ec.Error(ctx, err) - return graphql.Null - } - if resTmp == nil { - if !graphql.HasFieldError(ctx, fc) { - ec.Errorf(ctx, "must not be null") - } - return graphql.Null - } - res := resTmp.(string) - fc.Result = res - return ec.marshalNString2string(ctx, field.Selections, res) +func (ec *executionContext) _DataframeColumn_nullable(ctx context.Context, field graphql.CollectedField, obj *model.DataframeColumn) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_DataframeColumn_nullable(ctx, field) + }, + func(ctx context.Context) (any, error) { + return obj.Nullable, nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v bool) graphql.Marshaler { + return ec.marshalNBoolean2bool(ctx, selections, v) + }, + true, + true, + ) +} +func (ec *executionContext) fieldContext_DataframeColumn_nullable(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + return graphql.NewScalarFieldContext("DataframeColumn", field, false, false, errors.New("field of type Boolean does not have child fields")) } -func (ec *executionContext) fieldContext_DataframeMaterialization_project(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { - fc = &graphql.FieldContext{ - Object: "DataframeMaterialization", - Field: field, - IsMethod: false, - IsResolver: false, - Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { - return nil, errors.New("field of type String does not have child fields") +func (ec *executionContext) _DataframeColumn_repeated(ctx context.Context, field graphql.CollectedField, obj *model.DataframeColumn) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_DataframeColumn_repeated(ctx, field) }, - } - return fc, nil + func(ctx context.Context) (any, error) { + return obj.Repeated, nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v bool) graphql.Marshaler { + return ec.marshalNBoolean2bool(ctx, selections, v) + }, + true, + true, + ) +} +func (ec *executionContext) fieldContext_DataframeColumn_repeated(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + return graphql.NewScalarFieldContext("DataframeColumn", field, false, false, errors.New("field of type Boolean does not have child fields")) } -func (ec *executionContext) _DataframeMaterialization_datasetGeneration(ctx context.Context, field graphql.CollectedField, obj *model.DataframeMaterialization) (ret graphql.Marshaler) { - fc, err := ec.fieldContext_DataframeMaterialization_datasetGeneration(ctx, field) - if err != nil { - return graphql.Null - } - ctx = graphql.WithFieldContext(ctx, fc) - defer func() { - if r := recover(); r != nil { - ec.Error(ctx, ec.Recover(ctx, r)) - ret = graphql.Null - } - }() - resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { - ctx = rctx // use context from middleware stack in children - return obj.DatasetGeneration, nil - }) - if err != nil { - ec.Error(ctx, err) - return graphql.Null - } - if resTmp == nil { - if !graphql.HasFieldError(ctx, fc) { - ec.Errorf(ctx, "must not be null") - } - return graphql.Null - } - res := resTmp.(string) - fc.Result = res - return ec.marshalNString2string(ctx, field.Selections, res) +func (ec *executionContext) _DataframeColumn_filterable(ctx context.Context, field graphql.CollectedField, obj *model.DataframeColumn) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_DataframeColumn_filterable(ctx, field) + }, + func(ctx context.Context) (any, error) { + return obj.Filterable, nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v bool) graphql.Marshaler { + return ec.marshalNBoolean2bool(ctx, selections, v) + }, + true, + true, + ) +} +func (ec *executionContext) fieldContext_DataframeColumn_filterable(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + return graphql.NewScalarFieldContext("DataframeColumn", field, false, false, errors.New("field of type Boolean does not have child fields")) } -func (ec *executionContext) fieldContext_DataframeMaterialization_datasetGeneration(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { - fc = &graphql.FieldContext{ - Object: "DataframeMaterialization", - Field: field, - IsMethod: false, - IsResolver: false, - Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { - return nil, errors.New("field of type String does not have child fields") +func (ec *executionContext) _DataframeColumn_sortable(ctx context.Context, field graphql.CollectedField, obj *model.DataframeColumn) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_DataframeColumn_sortable(ctx, field) }, - } - return fc, nil + func(ctx context.Context) (any, error) { + return obj.Sortable, nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v bool) graphql.Marshaler { + return ec.marshalNBoolean2bool(ctx, selections, v) + }, + true, + true, + ) +} +func (ec *executionContext) fieldContext_DataframeColumn_sortable(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + return graphql.NewScalarFieldContext("DataframeColumn", field, false, false, errors.New("field of type Boolean does not have child fields")) } -func (ec *executionContext) _DataframeMaterialization_state(ctx context.Context, field graphql.CollectedField, obj *model.DataframeMaterialization) (ret graphql.Marshaler) { - fc, err := ec.fieldContext_DataframeMaterialization_state(ctx, field) - if err != nil { - return graphql.Null - } - ctx = graphql.WithFieldContext(ctx, fc) - defer func() { - if r := recover(); r != nil { - ec.Error(ctx, ec.Recover(ctx, r)) - ret = graphql.Null - } - }() - resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { - ctx = rctx // use context from middleware stack in children - return obj.State, nil - }) - if err != nil { - ec.Error(ctx, err) - return graphql.Null - } - if resTmp == nil { - if !graphql.HasFieldError(ctx, fc) { - ec.Errorf(ctx, "must not be null") - } - return graphql.Null - } - res := resTmp.(model.DataframeMaterializationState) - fc.Result = res - return ec.marshalNDataframeMaterializationState2githubᚗcomᚋcalyprᚋloomᚋgraphqlapiᚋmodelᚐDataframeMaterializationState(ctx, field.Selections, res) +func (ec *executionContext) _DataframeColumn_aggregatable(ctx context.Context, field graphql.CollectedField, obj *model.DataframeColumn) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_DataframeColumn_aggregatable(ctx, field) + }, + func(ctx context.Context) (any, error) { + return obj.Aggregatable, nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v bool) graphql.Marshaler { + return ec.marshalNBoolean2bool(ctx, selections, v) + }, + true, + true, + ) +} +func (ec *executionContext) fieldContext_DataframeColumn_aggregatable(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + return graphql.NewScalarFieldContext("DataframeColumn", field, false, false, errors.New("field of type Boolean does not have child fields")) } -func (ec *executionContext) fieldContext_DataframeMaterialization_state(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { - fc = &graphql.FieldContext{ - Object: "DataframeMaterialization", - Field: field, - IsMethod: false, - IsResolver: false, - Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { - return nil, errors.New("field of type DataframeMaterializationState does not have child fields") +func (ec *executionContext) _DataframeCompilerPlanDiagnostics_traversalSets(ctx context.Context, field graphql.CollectedField, obj *model.DataframeCompilerPlanDiagnostics) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_DataframeCompilerPlanDiagnostics_traversalSets(ctx, field) }, - } - return fc, nil + func(ctx context.Context) (any, error) { + return obj.TraversalSets, nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v int) graphql.Marshaler { + return ec.marshalNInt2int(ctx, selections, v) + }, + true, + true, + ) +} +func (ec *executionContext) fieldContext_DataframeCompilerPlanDiagnostics_traversalSets(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + return graphql.NewScalarFieldContext("DataframeCompilerPlanDiagnostics", field, false, false, errors.New("field of type Int does not have child fields")) } -func (ec *executionContext) _DataframeMaterialization_columns(ctx context.Context, field graphql.CollectedField, obj *model.DataframeMaterialization) (ret graphql.Marshaler) { - fc, err := ec.fieldContext_DataframeMaterialization_columns(ctx, field) - if err != nil { - return graphql.Null - } - ctx = graphql.WithFieldContext(ctx, fc) - defer func() { - if r := recover(); r != nil { - ec.Error(ctx, ec.Recover(ctx, r)) - ret = graphql.Null - } - }() - resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { - ctx = rctx // use context from middleware stack in children - return obj.Columns, nil - }) - if err != nil { - ec.Error(ctx, err) - return graphql.Null - } - if resTmp == nil { - if !graphql.HasFieldError(ctx, fc) { - ec.Errorf(ctx, "must not be null") - } - return graphql.Null - } - res := resTmp.([]*model.DataframeColumn) - fc.Result = res - return ec.marshalNDataframeColumn2ᚕᚖgithubᚗcomᚋcalyprᚋloomᚋgraphqlapiᚋmodelᚐDataframeColumnᚄ(ctx, field.Selections, res) +func (ec *executionContext) _DataframeCompilerPlanDiagnostics_sharedTraversalCount(ctx context.Context, field graphql.CollectedField, obj *model.DataframeCompilerPlanDiagnostics) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_DataframeCompilerPlanDiagnostics_sharedTraversalCount(ctx, field) + }, + func(ctx context.Context) (any, error) { + return obj.SharedTraversalCount, nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v int) graphql.Marshaler { + return ec.marshalNInt2int(ctx, selections, v) + }, + true, + true, + ) +} +func (ec *executionContext) fieldContext_DataframeCompilerPlanDiagnostics_sharedTraversalCount(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + return graphql.NewScalarFieldContext("DataframeCompilerPlanDiagnostics", field, false, false, errors.New("field of type Int does not have child fields")) } -func (ec *executionContext) fieldContext_DataframeMaterialization_columns(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { - fc = &graphql.FieldContext{ - Object: "DataframeMaterialization", - Field: field, - IsMethod: false, - IsResolver: false, - Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { - switch field.Name { - case "name": - return ec.fieldContext_DataframeColumn_name(ctx, field) - case "clickhouseType": - return ec.fieldContext_DataframeColumn_clickhouseType(ctx, field) - } - return nil, fmt.Errorf("no field named %q was found under type DataframeColumn", field.Name) +func (ec *executionContext) _DataframeCompilerPlanDiagnostics_requiredMatchReuseCount(ctx context.Context, field graphql.CollectedField, obj *model.DataframeCompilerPlanDiagnostics) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_DataframeCompilerPlanDiagnostics_requiredMatchReuseCount(ctx, field) }, - } - return fc, nil + func(ctx context.Context) (any, error) { + return obj.RequiredMatchReuseCount, nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v int) graphql.Marshaler { + return ec.marshalNInt2int(ctx, selections, v) + }, + true, + true, + ) } - -func (ec *executionContext) _DataframeMaterialization_rowCount(ctx context.Context, field graphql.CollectedField, obj *model.DataframeMaterialization) (ret graphql.Marshaler) { - fc, err := ec.fieldContext_DataframeMaterialization_rowCount(ctx, field) - if err != nil { - return graphql.Null - } - ctx = graphql.WithFieldContext(ctx, fc) - defer func() { - if r := recover(); r != nil { - ec.Error(ctx, ec.Recover(ctx, r)) - ret = graphql.Null - } - }() - resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { - ctx = rctx // use context from middleware stack in children - return obj.RowCount, nil - }) - if err != nil { - ec.Error(ctx, err) - return graphql.Null - } - if resTmp == nil { - if !graphql.HasFieldError(ctx, fc) { - ec.Errorf(ctx, "must not be null") - } - return graphql.Null - } - res := resTmp.(int) - fc.Result = res - return ec.marshalNInt2int(ctx, field.Selections, res) +func (ec *executionContext) fieldContext_DataframeCompilerPlanDiagnostics_requiredMatchReuseCount(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + return graphql.NewScalarFieldContext("DataframeCompilerPlanDiagnostics", field, false, false, errors.New("field of type Int does not have child fields")) } -func (ec *executionContext) fieldContext_DataframeMaterialization_rowCount(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { - fc = &graphql.FieldContext{ - Object: "DataframeMaterialization", - Field: field, - IsMethod: false, - IsResolver: false, - Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { - return nil, errors.New("field of type Int does not have child fields") +func (ec *executionContext) _DataframeCompilerPlanDiagnostics_scopedSharingCandidateGroups(ctx context.Context, field graphql.CollectedField, obj *model.DataframeCompilerPlanDiagnostics) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_DataframeCompilerPlanDiagnostics_scopedSharingCandidateGroups(ctx, field) }, - } - return fc, nil + func(ctx context.Context) (any, error) { + return obj.ScopedSharingCandidateGroups, nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v int) graphql.Marshaler { + return ec.marshalNInt2int(ctx, selections, v) + }, + true, + true, + ) +} +func (ec *executionContext) fieldContext_DataframeCompilerPlanDiagnostics_scopedSharingCandidateGroups(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + return graphql.NewScalarFieldContext("DataframeCompilerPlanDiagnostics", field, false, false, errors.New("field of type Int does not have child fields")) } -func (ec *executionContext) _DataframeMaterialization_createdAt(ctx context.Context, field graphql.CollectedField, obj *model.DataframeMaterialization) (ret graphql.Marshaler) { - fc, err := ec.fieldContext_DataframeMaterialization_createdAt(ctx, field) - if err != nil { - return graphql.Null - } - ctx = graphql.WithFieldContext(ctx, fc) - defer func() { - if r := recover(); r != nil { - ec.Error(ctx, ec.Recover(ctx, r)) - ret = graphql.Null - } - }() - resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { - ctx = rctx // use context from middleware stack in children - return obj.CreatedAt, nil - }) - if err != nil { - ec.Error(ctx, err) - return graphql.Null - } - if resTmp == nil { - if !graphql.HasFieldError(ctx, fc) { - ec.Errorf(ctx, "must not be null") - } - return graphql.Null - } - res := resTmp.(string) - fc.Result = res - return ec.marshalNString2string(ctx, field.Selections, res) +func (ec *executionContext) _DataframeCompilerPlanDiagnostics_scopedSharingCandidateSets(ctx context.Context, field graphql.CollectedField, obj *model.DataframeCompilerPlanDiagnostics) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_DataframeCompilerPlanDiagnostics_scopedSharingCandidateSets(ctx, field) + }, + func(ctx context.Context) (any, error) { + return obj.ScopedSharingCandidateSets, nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v int) graphql.Marshaler { + return ec.marshalNInt2int(ctx, selections, v) + }, + true, + true, + ) +} +func (ec *executionContext) fieldContext_DataframeCompilerPlanDiagnostics_scopedSharingCandidateSets(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + return graphql.NewScalarFieldContext("DataframeCompilerPlanDiagnostics", field, false, false, errors.New("field of type Int does not have child fields")) } -func (ec *executionContext) fieldContext_DataframeMaterialization_createdAt(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { - fc = &graphql.FieldContext{ - Object: "DataframeMaterialization", - Field: field, - IsMethod: false, - IsResolver: false, - Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { - return nil, errors.New("field of type String does not have child fields") +func (ec *executionContext) _DataframeCompilerPlanDiagnostics_potentialSharingOpportunityGroups(ctx context.Context, field graphql.CollectedField, obj *model.DataframeCompilerPlanDiagnostics) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_DataframeCompilerPlanDiagnostics_potentialSharingOpportunityGroups(ctx, field) }, - } - return fc, nil + func(ctx context.Context) (any, error) { + return obj.PotentialSharingOpportunityGroups, nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v int) graphql.Marshaler { + return ec.marshalNInt2int(ctx, selections, v) + }, + true, + true, + ) +} +func (ec *executionContext) fieldContext_DataframeCompilerPlanDiagnostics_potentialSharingOpportunityGroups(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + return graphql.NewScalarFieldContext("DataframeCompilerPlanDiagnostics", field, false, false, errors.New("field of type Int does not have child fields")) } -func (ec *executionContext) _DataframeMaterialization_readyAt(ctx context.Context, field graphql.CollectedField, obj *model.DataframeMaterialization) (ret graphql.Marshaler) { - fc, err := ec.fieldContext_DataframeMaterialization_readyAt(ctx, field) - if err != nil { - return graphql.Null - } - ctx = graphql.WithFieldContext(ctx, fc) - defer func() { - if r := recover(); r != nil { - ec.Error(ctx, ec.Recover(ctx, r)) - ret = graphql.Null - } - }() - resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { - ctx = rctx // use context from middleware stack in children - return obj.ReadyAt, nil - }) - if err != nil { - ec.Error(ctx, err) - return graphql.Null - } - if resTmp == nil { - return graphql.Null - } - res := resTmp.(*string) - fc.Result = res - return ec.marshalOString2ᚖstring(ctx, field.Selections, res) +func (ec *executionContext) _DataframeCompilerPlanDiagnostics_potentialSharingOpportunitySets(ctx context.Context, field graphql.CollectedField, obj *model.DataframeCompilerPlanDiagnostics) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_DataframeCompilerPlanDiagnostics_potentialSharingOpportunitySets(ctx, field) + }, + func(ctx context.Context) (any, error) { + return obj.PotentialSharingOpportunitySets, nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v int) graphql.Marshaler { + return ec.marshalNInt2int(ctx, selections, v) + }, + true, + true, + ) +} +func (ec *executionContext) fieldContext_DataframeCompilerPlanDiagnostics_potentialSharingOpportunitySets(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + return graphql.NewScalarFieldContext("DataframeCompilerPlanDiagnostics", field, false, false, errors.New("field of type Int does not have child fields")) } -func (ec *executionContext) fieldContext_DataframeMaterialization_readyAt(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { +func (ec *executionContext) _DataframeCompilerPlanDiagnostics_optimizationPolicy(ctx context.Context, field graphql.CollectedField, obj *model.DataframeCompilerPlanDiagnostics) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_DataframeCompilerPlanDiagnostics_optimizationPolicy(ctx, field) + }, + func(ctx context.Context) (any, error) { + return obj.OptimizationPolicy, nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v *model.DataframeOptimizationPolicy) graphql.Marshaler { + return ec.marshalNDataframeOptimizationPolicy2ᚖgithubᚗcomᚋcalyprᚋloomᚋgraphqlapiᚋmodelᚐDataframeOptimizationPolicy(ctx, selections, v) + }, + true, + true, + ) +} +func (ec *executionContext) fieldContext_DataframeCompilerPlanDiagnostics_optimizationPolicy(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { fc = &graphql.FieldContext{ - Object: "DataframeMaterialization", + Object: "DataframeCompilerPlanDiagnostics", Field: field, IsMethod: false, IsResolver: false, Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { - return nil, errors.New("field of type String does not have child fields") + return ec.childFields_DataframeOptimizationPolicy(ctx, field) }, } return fc, nil } -func (ec *executionContext) _DataframeMaterialization_error(ctx context.Context, field graphql.CollectedField, obj *model.DataframeMaterialization) (ret graphql.Marshaler) { - fc, err := ec.fieldContext_DataframeMaterialization_error(ctx, field) - if err != nil { - return graphql.Null - } - ctx = graphql.WithFieldContext(ctx, fc) - defer func() { - if r := recover(); r != nil { - ec.Error(ctx, ec.Recover(ctx, r)) - ret = graphql.Null - } - }() - resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { - ctx = rctx // use context from middleware stack in children - return obj.Error, nil - }) - if err != nil { - ec.Error(ctx, err) - return graphql.Null - } - if resTmp == nil { - return graphql.Null - } - res := resTmp.(*string) - fc.Result = res - return ec.marshalOString2ᚖstring(ctx, field.Selections, res) +func (ec *executionContext) _DataframeCompilerPlanDiagnostics_richSourceReuse(ctx context.Context, field graphql.CollectedField, obj *model.DataframeCompilerPlanDiagnostics) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_DataframeCompilerPlanDiagnostics_richSourceReuse(ctx, field) + }, + func(ctx context.Context) (any, error) { + return obj.RichSourceReuse, nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v []*model.DataframeRichSourceReuse) graphql.Marshaler { + return ec.marshalNDataframeRichSourceReuse2ᚕᚖgithubᚗcomᚋcalyprᚋloomᚋgraphqlapiᚋmodelᚐDataframeRichSourceReuseᚄ(ctx, selections, v) + }, + true, + true, + ) } - -func (ec *executionContext) fieldContext_DataframeMaterialization_error(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { +func (ec *executionContext) fieldContext_DataframeCompilerPlanDiagnostics_richSourceReuse(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { fc = &graphql.FieldContext{ - Object: "DataframeMaterialization", + Object: "DataframeCompilerPlanDiagnostics", Field: field, IsMethod: false, IsResolver: false, Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { - return nil, errors.New("field of type String does not have child fields") + return ec.childFields_DataframeRichSourceReuse(ctx, field) }, } return fc, nil } -func (ec *executionContext) _DataframeOptimizationDecision_rule(ctx context.Context, field graphql.CollectedField, obj *model.DataframeOptimizationDecision) (ret graphql.Marshaler) { - fc, err := ec.fieldContext_DataframeOptimizationDecision_rule(ctx, field) - if err != nil { - return graphql.Null - } - ctx = graphql.WithFieldContext(ctx, fc) - defer func() { - if r := recover(); r != nil { - ec.Error(ctx, ec.Recover(ctx, r)) - ret = graphql.Null - } - }() - resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { - ctx = rctx // use context from middleware stack in children - return obj.Rule, nil - }) - if err != nil { - ec.Error(ctx, err) - return graphql.Null - } - if resTmp == nil { - if !graphql.HasFieldError(ctx, fc) { - ec.Errorf(ctx, "must not be null") - } - return graphql.Null - } - res := resTmp.(string) - fc.Result = res - return ec.marshalNString2string(ctx, field.Selections, res) +func (ec *executionContext) _DataframeFieldHint_resourceType(ctx context.Context, field graphql.CollectedField, obj *model.DataframeFieldHint) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_DataframeFieldHint_resourceType(ctx, field) + }, + func(ctx context.Context) (any, error) { + return obj.ResourceType, nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v string) graphql.Marshaler { + return ec.marshalNString2string(ctx, selections, v) + }, + true, + true, + ) +} +func (ec *executionContext) fieldContext_DataframeFieldHint_resourceType(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + return graphql.NewScalarFieldContext("DataframeFieldHint", field, false, false, errors.New("field of type String does not have child fields")) } -func (ec *executionContext) fieldContext_DataframeOptimizationDecision_rule(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { - fc = &graphql.FieldContext{ - Object: "DataframeOptimizationDecision", - Field: field, - IsMethod: false, - IsResolver: false, - Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { - return nil, errors.New("field of type String does not have child fields") +func (ec *executionContext) _DataframeFieldHint_fieldRef(ctx context.Context, field graphql.CollectedField, obj *model.DataframeFieldHint) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_DataframeFieldHint_fieldRef(ctx, field) }, - } - return fc, nil + func(ctx context.Context) (any, error) { + return obj.FieldRef, nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v string) graphql.Marshaler { + return ec.marshalNString2string(ctx, selections, v) + }, + true, + true, + ) +} +func (ec *executionContext) fieldContext_DataframeFieldHint_fieldRef(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + return graphql.NewScalarFieldContext("DataframeFieldHint", field, false, false, errors.New("field of type String does not have child fields")) } -func (ec *executionContext) _DataframeOptimizationDecision_enabled(ctx context.Context, field graphql.CollectedField, obj *model.DataframeOptimizationDecision) (ret graphql.Marshaler) { - fc, err := ec.fieldContext_DataframeOptimizationDecision_enabled(ctx, field) - if err != nil { - return graphql.Null - } - ctx = graphql.WithFieldContext(ctx, fc) - defer func() { - if r := recover(); r != nil { - ec.Error(ctx, ec.Recover(ctx, r)) - ret = graphql.Null - } - }() - resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { - ctx = rctx // use context from middleware stack in children - return obj.Enabled, nil - }) - if err != nil { - ec.Error(ctx, err) - return graphql.Null - } - if resTmp == nil { - if !graphql.HasFieldError(ctx, fc) { - ec.Errorf(ctx, "must not be null") - } - return graphql.Null - } - res := resTmp.(bool) - fc.Result = res - return ec.marshalNBoolean2bool(ctx, field.Selections, res) +func (ec *executionContext) _DataframeFieldHint_label(ctx context.Context, field graphql.CollectedField, obj *model.DataframeFieldHint) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_DataframeFieldHint_label(ctx, field) + }, + func(ctx context.Context) (any, error) { + return obj.Label, nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v string) graphql.Marshaler { + return ec.marshalNString2string(ctx, selections, v) + }, + true, + true, + ) +} +func (ec *executionContext) fieldContext_DataframeFieldHint_label(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + return graphql.NewScalarFieldContext("DataframeFieldHint", field, false, false, errors.New("field of type String does not have child fields")) } -func (ec *executionContext) fieldContext_DataframeOptimizationDecision_enabled(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { +func (ec *executionContext) _DataframeFieldHint_path(ctx context.Context, field graphql.CollectedField, obj *model.DataframeFieldHint) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_DataframeFieldHint_path(ctx, field) + }, + func(ctx context.Context) (any, error) { + return obj.Path, nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v string) graphql.Marshaler { + return ec.marshalNString2string(ctx, selections, v) + }, + true, + true, + ) +} +func (ec *executionContext) fieldContext_DataframeFieldHint_path(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + return graphql.NewScalarFieldContext("DataframeFieldHint", field, false, false, errors.New("field of type String does not have child fields")) +} + +func (ec *executionContext) _DataframeFieldHint_selector(ctx context.Context, field graphql.CollectedField, obj *model.DataframeFieldHint) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_DataframeFieldHint_selector(ctx, field) + }, + func(ctx context.Context) (any, error) { + return obj.Selector, nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v *model.DataframeFieldSelector) graphql.Marshaler { + return ec.marshalNDataframeFieldSelector2ᚖgithubᚗcomᚋcalyprᚋloomᚋgraphqlapiᚋmodelᚐDataframeFieldSelector(ctx, selections, v) + }, + true, + true, + ) +} +func (ec *executionContext) fieldContext_DataframeFieldHint_selector(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { fc = &graphql.FieldContext{ - Object: "DataframeOptimizationDecision", + Object: "DataframeFieldHint", Field: field, IsMethod: false, IsResolver: false, Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { - return nil, errors.New("field of type Boolean does not have child fields") + return ec.childFields_DataframeFieldSelector(ctx, field) }, } return fc, nil } -func (ec *executionContext) _DataframeOptimizationDecision_candidateSets(ctx context.Context, field graphql.CollectedField, obj *model.DataframeOptimizationDecision) (ret graphql.Marshaler) { - fc, err := ec.fieldContext_DataframeOptimizationDecision_candidateSets(ctx, field) - if err != nil { - return graphql.Null - } - ctx = graphql.WithFieldContext(ctx, fc) - defer func() { - if r := recover(); r != nil { - ec.Error(ctx, ec.Recover(ctx, r)) - ret = graphql.Null - } - }() - resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { - ctx = rctx // use context from middleware stack in children - return obj.CandidateSets, nil - }) - if err != nil { - ec.Error(ctx, err) - return graphql.Null - } - if resTmp == nil { - if !graphql.HasFieldError(ctx, fc) { - ec.Errorf(ctx, "must not be null") - } - return graphql.Null - } - res := resTmp.(int) - fc.Result = res - return ec.marshalNInt2int(ctx, field.Selections, res) +func (ec *executionContext) _DataframeFieldHint_kind(ctx context.Context, field graphql.CollectedField, obj *model.DataframeFieldHint) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_DataframeFieldHint_kind(ctx, field) + }, + func(ctx context.Context) (any, error) { + return obj.Kind, nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v string) graphql.Marshaler { + return ec.marshalNString2string(ctx, selections, v) + }, + true, + true, + ) +} +func (ec *executionContext) fieldContext_DataframeFieldHint_kind(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + return graphql.NewScalarFieldContext("DataframeFieldHint", field, false, false, errors.New("field of type String does not have child fields")) } -func (ec *executionContext) fieldContext_DataframeOptimizationDecision_candidateSets(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { - fc = &graphql.FieldContext{ - Object: "DataframeOptimizationDecision", - Field: field, - IsMethod: false, - IsResolver: false, - Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { - return nil, errors.New("field of type Int does not have child fields") +func (ec *executionContext) _DataframeFieldHint_docCount(ctx context.Context, field graphql.CollectedField, obj *model.DataframeFieldHint) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_DataframeFieldHint_docCount(ctx, field) }, - } - return fc, nil + func(ctx context.Context) (any, error) { + return obj.DocCount, nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v int) graphql.Marshaler { + return ec.marshalNInt2int(ctx, selections, v) + }, + true, + true, + ) +} +func (ec *executionContext) fieldContext_DataframeFieldHint_docCount(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + return graphql.NewScalarFieldContext("DataframeFieldHint", field, false, false, errors.New("field of type Int does not have child fields")) } -func (ec *executionContext) _DataframeOptimizationDecision_estimatedBaselineWork(ctx context.Context, field graphql.CollectedField, obj *model.DataframeOptimizationDecision) (ret graphql.Marshaler) { - fc, err := ec.fieldContext_DataframeOptimizationDecision_estimatedBaselineWork(ctx, field) - if err != nil { - return graphql.Null - } - ctx = graphql.WithFieldContext(ctx, fc) - defer func() { - if r := recover(); r != nil { - ec.Error(ctx, ec.Recover(ctx, r)) - ret = graphql.Null - } - }() - resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { - ctx = rctx // use context from middleware stack in children - return obj.EstimatedBaselineWork, nil - }) - if err != nil { - ec.Error(ctx, err) - return graphql.Null - } - if resTmp == nil { - if !graphql.HasFieldError(ctx, fc) { - ec.Errorf(ctx, "must not be null") - } - return graphql.Null - } - res := resTmp.(int) - fc.Result = res - return ec.marshalNInt2int(ctx, field.Selections, res) +func (ec *executionContext) _DataframeFieldHint_sampleCount(ctx context.Context, field graphql.CollectedField, obj *model.DataframeFieldHint) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_DataframeFieldHint_sampleCount(ctx, field) + }, + func(ctx context.Context) (any, error) { + return obj.SampleCount, nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v int) graphql.Marshaler { + return ec.marshalNInt2int(ctx, selections, v) + }, + true, + true, + ) +} +func (ec *executionContext) fieldContext_DataframeFieldHint_sampleCount(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + return graphql.NewScalarFieldContext("DataframeFieldHint", field, false, false, errors.New("field of type Int does not have child fields")) } -func (ec *executionContext) fieldContext_DataframeOptimizationDecision_estimatedBaselineWork(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { - fc = &graphql.FieldContext{ - Object: "DataframeOptimizationDecision", - Field: field, - IsMethod: false, - IsResolver: false, - Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { - return nil, errors.New("field of type Int does not have child fields") +func (ec *executionContext) _DataframeFieldHint_distinctValues(ctx context.Context, field graphql.CollectedField, obj *model.DataframeFieldHint) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_DataframeFieldHint_distinctValues(ctx, field) }, - } - return fc, nil + func(ctx context.Context) (any, error) { + return obj.DistinctValues, nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v []string) graphql.Marshaler { + return ec.marshalNString2ᚕstringᚄ(ctx, selections, v) + }, + true, + true, + ) +} +func (ec *executionContext) fieldContext_DataframeFieldHint_distinctValues(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + return graphql.NewScalarFieldContext("DataframeFieldHint", field, false, false, errors.New("field of type String does not have child fields")) } -func (ec *executionContext) _DataframeOptimizationDecision_estimatedOptimizedWork(ctx context.Context, field graphql.CollectedField, obj *model.DataframeOptimizationDecision) (ret graphql.Marshaler) { - fc, err := ec.fieldContext_DataframeOptimizationDecision_estimatedOptimizedWork(ctx, field) - if err != nil { - return graphql.Null - } - ctx = graphql.WithFieldContext(ctx, fc) - defer func() { - if r := recover(); r != nil { - ec.Error(ctx, ec.Recover(ctx, r)) - ret = graphql.Null - } - }() - resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { - ctx = rctx // use context from middleware stack in children - return obj.EstimatedOptimizedWork, nil - }) - if err != nil { - ec.Error(ctx, err) - return graphql.Null - } - if resTmp == nil { - if !graphql.HasFieldError(ctx, fc) { - ec.Errorf(ctx, "must not be null") - } - return graphql.Null - } - res := resTmp.(int) - fc.Result = res - return ec.marshalNInt2int(ctx, field.Selections, res) +func (ec *executionContext) _DataframeFieldHint_distinctTruncated(ctx context.Context, field graphql.CollectedField, obj *model.DataframeFieldHint) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_DataframeFieldHint_distinctTruncated(ctx, field) + }, + func(ctx context.Context) (any, error) { + return obj.DistinctTruncated, nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v bool) graphql.Marshaler { + return ec.marshalNBoolean2bool(ctx, selections, v) + }, + true, + true, + ) +} +func (ec *executionContext) fieldContext_DataframeFieldHint_distinctTruncated(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + return graphql.NewScalarFieldContext("DataframeFieldHint", field, false, false, errors.New("field of type Boolean does not have child fields")) } -func (ec *executionContext) fieldContext_DataframeOptimizationDecision_estimatedOptimizedWork(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { - fc = &graphql.FieldContext{ - Object: "DataframeOptimizationDecision", - Field: field, - IsMethod: false, - IsResolver: false, - Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { - return nil, errors.New("field of type Int does not have child fields") +func (ec *executionContext) _DataframeFieldHint_pivotCandidate(ctx context.Context, field graphql.CollectedField, obj *model.DataframeFieldHint) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_DataframeFieldHint_pivotCandidate(ctx, field) }, - } - return fc, nil + func(ctx context.Context) (any, error) { + return obj.PivotCandidate, nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v bool) graphql.Marshaler { + return ec.marshalNBoolean2bool(ctx, selections, v) + }, + true, + true, + ) +} +func (ec *executionContext) fieldContext_DataframeFieldHint_pivotCandidate(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + return graphql.NewScalarFieldContext("DataframeFieldHint", field, false, false, errors.New("field of type Boolean does not have child fields")) } -func (ec *executionContext) _DataframeOptimizationDecision_estimatedSavings(ctx context.Context, field graphql.CollectedField, obj *model.DataframeOptimizationDecision) (ret graphql.Marshaler) { - fc, err := ec.fieldContext_DataframeOptimizationDecision_estimatedSavings(ctx, field) - if err != nil { - return graphql.Null - } - ctx = graphql.WithFieldContext(ctx, fc) - defer func() { - if r := recover(); r != nil { - ec.Error(ctx, ec.Recover(ctx, r)) - ret = graphql.Null - } - }() - resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { - ctx = rctx // use context from middleware stack in children - return obj.EstimatedSavings, nil - }) - if err != nil { - ec.Error(ctx, err) - return graphql.Null - } - if resTmp == nil { - if !graphql.HasFieldError(ctx, fc) { - ec.Errorf(ctx, "must not be null") - } - return graphql.Null - } - res := resTmp.(int) - fc.Result = res - return ec.marshalNInt2int(ctx, field.Selections, res) +func (ec *executionContext) _DataframeFieldHint_pivotKind(ctx context.Context, field graphql.CollectedField, obj *model.DataframeFieldHint) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_DataframeFieldHint_pivotKind(ctx, field) + }, + func(ctx context.Context) (any, error) { + return obj.PivotKind, nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v *string) graphql.Marshaler { + return ec.marshalOString2ᚖstring(ctx, selections, v) + }, + true, + false, + ) +} +func (ec *executionContext) fieldContext_DataframeFieldHint_pivotKind(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + return graphql.NewScalarFieldContext("DataframeFieldHint", field, false, false, errors.New("field of type String does not have child fields")) } -func (ec *executionContext) fieldContext_DataframeOptimizationDecision_estimatedSavings(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { +func (ec *executionContext) _DataframeFieldHint_pivotColumns(ctx context.Context, field graphql.CollectedField, obj *model.DataframeFieldHint) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_DataframeFieldHint_pivotColumns(ctx, field) + }, + func(ctx context.Context) (any, error) { + return obj.PivotColumns, nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v []string) graphql.Marshaler { + return ec.marshalNString2ᚕstringᚄ(ctx, selections, v) + }, + true, + true, + ) +} +func (ec *executionContext) fieldContext_DataframeFieldHint_pivotColumns(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + return graphql.NewScalarFieldContext("DataframeFieldHint", field, false, false, errors.New("field of type String does not have child fields")) +} + +func (ec *executionContext) _DataframeFieldHint_pivotFamily(ctx context.Context, field graphql.CollectedField, obj *model.DataframeFieldHint) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_DataframeFieldHint_pivotFamily(ctx, field) + }, + func(ctx context.Context) (any, error) { + return obj.PivotFamily, nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v *model.FhirPivotFamily) graphql.Marshaler { + return ec.marshalOFhirPivotFamily2ᚖgithubᚗcomᚋcalyprᚋloomᚋgraphqlapiᚋmodelᚐFhirPivotFamily(ctx, selections, v) + }, + true, + false, + ) +} +func (ec *executionContext) fieldContext_DataframeFieldHint_pivotFamily(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + return graphql.NewScalarFieldContext("DataframeFieldHint", field, false, false, errors.New("field of type FhirPivotFamily does not have child fields")) +} + +func (ec *executionContext) _DataframeFieldHint_defaultPivotColumnSelector(ctx context.Context, field graphql.CollectedField, obj *model.DataframeFieldHint) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_DataframeFieldHint_defaultPivotColumnSelector(ctx, field) + }, + func(ctx context.Context) (any, error) { + return obj.DefaultPivotColumnSelector, nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v *model.DataframeFieldSelector) graphql.Marshaler { + return ec.marshalODataframeFieldSelector2ᚖgithubᚗcomᚋcalyprᚋloomᚋgraphqlapiᚋmodelᚐDataframeFieldSelector(ctx, selections, v) + }, + true, + false, + ) +} +func (ec *executionContext) fieldContext_DataframeFieldHint_defaultPivotColumnSelector(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { fc = &graphql.FieldContext{ - Object: "DataframeOptimizationDecision", + Object: "DataframeFieldHint", Field: field, IsMethod: false, IsResolver: false, Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { - return nil, errors.New("field of type Int does not have child fields") + return ec.childFields_DataframeFieldSelector(ctx, field) }, } return fc, nil } -func (ec *executionContext) _DataframeOptimizationDecision_reason(ctx context.Context, field graphql.CollectedField, obj *model.DataframeOptimizationDecision) (ret graphql.Marshaler) { - fc, err := ec.fieldContext_DataframeOptimizationDecision_reason(ctx, field) - if err != nil { - return graphql.Null - } - ctx = graphql.WithFieldContext(ctx, fc) - defer func() { - if r := recover(); r != nil { - ec.Error(ctx, ec.Recover(ctx, r)) - ret = graphql.Null - } - }() - resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { - ctx = rctx // use context from middleware stack in children - return obj.Reason, nil - }) - if err != nil { - ec.Error(ctx, err) - return graphql.Null - } - if resTmp == nil { - if !graphql.HasFieldError(ctx, fc) { - ec.Errorf(ctx, "must not be null") - } - return graphql.Null - } - res := resTmp.(string) - fc.Result = res - return ec.marshalNString2string(ctx, field.Selections, res) +func (ec *executionContext) _DataframeFieldHint_defaultPivotValueSelector(ctx context.Context, field graphql.CollectedField, obj *model.DataframeFieldHint) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_DataframeFieldHint_defaultPivotValueSelector(ctx, field) + }, + func(ctx context.Context) (any, error) { + return obj.DefaultPivotValueSelector, nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v *model.DataframeFieldSelector) graphql.Marshaler { + return ec.marshalODataframeFieldSelector2ᚖgithubᚗcomᚋcalyprᚋloomᚋgraphqlapiᚋmodelᚐDataframeFieldSelector(ctx, selections, v) + }, + true, + false, + ) } - -func (ec *executionContext) fieldContext_DataframeOptimizationDecision_reason(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { +func (ec *executionContext) fieldContext_DataframeFieldHint_defaultPivotValueSelector(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { fc = &graphql.FieldContext{ - Object: "DataframeOptimizationDecision", + Object: "DataframeFieldHint", Field: field, IsMethod: false, IsResolver: false, Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { - return nil, errors.New("field of type String does not have child fields") + return ec.childFields_DataframeFieldSelector(ctx, field) }, } return fc, nil } -func (ec *executionContext) _DataframeOptimizationPolicy_name(ctx context.Context, field graphql.CollectedField, obj *model.DataframeOptimizationPolicy) (ret graphql.Marshaler) { - fc, err := ec.fieldContext_DataframeOptimizationPolicy_name(ctx, field) - if err != nil { - return graphql.Null - } - ctx = graphql.WithFieldContext(ctx, fc) - defer func() { - if r := recover(); r != nil { - ec.Error(ctx, ec.Recover(ctx, r)) - ret = graphql.Null - } - }() - resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { - ctx = rctx // use context from middleware stack in children - return obj.Name, nil - }) - if err != nil { - ec.Error(ctx, err) - return graphql.Null - } - if resTmp == nil { - if !graphql.HasFieldError(ctx, fc) { - ec.Errorf(ctx, "must not be null") - } - return graphql.Null - } - res := resTmp.(string) - fc.Result = res - return ec.marshalNString2string(ctx, field.Selections, res) +func (ec *executionContext) _DataframeFieldPredicate_path(ctx context.Context, field graphql.CollectedField, obj *model.DataframeFieldPredicate) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_DataframeFieldPredicate_path(ctx, field) + }, + func(ctx context.Context) (any, error) { + return obj.Path, nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v string) graphql.Marshaler { + return ec.marshalNString2string(ctx, selections, v) + }, + true, + true, + ) +} +func (ec *executionContext) fieldContext_DataframeFieldPredicate_path(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + return graphql.NewScalarFieldContext("DataframeFieldPredicate", field, false, false, errors.New("field of type String does not have child fields")) } -func (ec *executionContext) fieldContext_DataframeOptimizationPolicy_name(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { +func (ec *executionContext) _DataframeFieldPredicate_op(ctx context.Context, field graphql.CollectedField, obj *model.DataframeFieldPredicate) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_DataframeFieldPredicate_op(ctx, field) + }, + func(ctx context.Context) (any, error) { + return obj.Op, nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v model.FhirFieldPredicateOperation) graphql.Marshaler { + return ec.marshalNFhirFieldPredicateOperation2githubᚗcomᚋcalyprᚋloomᚋgraphqlapiᚋmodelᚐFhirFieldPredicateOperation(ctx, selections, v) + }, + true, + true, + ) +} +func (ec *executionContext) fieldContext_DataframeFieldPredicate_op(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + return graphql.NewScalarFieldContext("DataframeFieldPredicate", field, false, false, errors.New("field of type FhirFieldPredicateOperation does not have child fields")) +} + +func (ec *executionContext) _DataframeFieldPredicate_value(ctx context.Context, field graphql.CollectedField, obj *model.DataframeFieldPredicate) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_DataframeFieldPredicate_value(ctx, field) + }, + func(ctx context.Context) (any, error) { + return obj.Value, nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v string) graphql.Marshaler { + return ec.marshalNString2string(ctx, selections, v) + }, + true, + true, + ) +} +func (ec *executionContext) fieldContext_DataframeFieldPredicate_value(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + return graphql.NewScalarFieldContext("DataframeFieldPredicate", field, false, false, errors.New("field of type String does not have child fields")) +} + +func (ec *executionContext) _DataframeFieldSelector_sourcePath(ctx context.Context, field graphql.CollectedField, obj *model.DataframeFieldSelector) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_DataframeFieldSelector_sourcePath(ctx, field) + }, + func(ctx context.Context) (any, error) { + return obj.SourcePath, nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v *string) graphql.Marshaler { + return ec.marshalOString2ᚖstring(ctx, selections, v) + }, + true, + false, + ) +} +func (ec *executionContext) fieldContext_DataframeFieldSelector_sourcePath(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + return graphql.NewScalarFieldContext("DataframeFieldSelector", field, false, false, errors.New("field of type String does not have child fields")) +} + +func (ec *executionContext) _DataframeFieldSelector_where(ctx context.Context, field graphql.CollectedField, obj *model.DataframeFieldSelector) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_DataframeFieldSelector_where(ctx, field) + }, + func(ctx context.Context) (any, error) { + return obj.Where, nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v *model.DataframeFieldPredicate) graphql.Marshaler { + return ec.marshalODataframeFieldPredicate2ᚖgithubᚗcomᚋcalyprᚋloomᚋgraphqlapiᚋmodelᚐDataframeFieldPredicate(ctx, selections, v) + }, + true, + false, + ) +} +func (ec *executionContext) fieldContext_DataframeFieldSelector_where(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { fc = &graphql.FieldContext{ - Object: "DataframeOptimizationPolicy", + Object: "DataframeFieldSelector", Field: field, IsMethod: false, IsResolver: false, Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { - return nil, errors.New("field of type String does not have child fields") + return ec.childFields_DataframeFieldPredicate(ctx, field) }, } return fc, nil } -func (ec *executionContext) _DataframeOptimizationPolicy_enabled(ctx context.Context, field graphql.CollectedField, obj *model.DataframeOptimizationPolicy) (ret graphql.Marshaler) { - fc, err := ec.fieldContext_DataframeOptimizationPolicy_enabled(ctx, field) - if err != nil { - return graphql.Null - } - ctx = graphql.WithFieldContext(ctx, fc) - defer func() { - if r := recover(); r != nil { - ec.Error(ctx, ec.Recover(ctx, r)) - ret = graphql.Null - } - }() - resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { - ctx = rctx // use context from middleware stack in children - return obj.Enabled, nil - }) - if err != nil { - ec.Error(ctx, err) - return graphql.Null - } - if resTmp == nil { - if !graphql.HasFieldError(ctx, fc) { - ec.Errorf(ctx, "must not be null") - } - return graphql.Null - } - res := resTmp.(bool) - fc.Result = res - return ec.marshalNBoolean2bool(ctx, field.Selections, res) +func (ec *executionContext) _DataframeFieldSelector_valuePath(ctx context.Context, field graphql.CollectedField, obj *model.DataframeFieldSelector) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_DataframeFieldSelector_valuePath(ctx, field) + }, + func(ctx context.Context) (any, error) { + return obj.ValuePath, nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v string) graphql.Marshaler { + return ec.marshalNString2string(ctx, selections, v) + }, + true, + true, + ) +} +func (ec *executionContext) fieldContext_DataframeFieldSelector_valuePath(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + return graphql.NewScalarFieldContext("DataframeFieldSelector", field, false, false, errors.New("field of type String does not have child fields")) } -func (ec *executionContext) fieldContext_DataframeOptimizationPolicy_enabled(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { - fc = &graphql.FieldContext{ - Object: "DataframeOptimizationPolicy", - Field: field, - IsMethod: false, - IsResolver: false, - Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { - return nil, errors.New("field of type Boolean does not have child fields") +func (ec *executionContext) _DataframeMaterialization_id(ctx context.Context, field graphql.CollectedField, obj *model.DataframeMaterialization) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_DataframeMaterialization_id(ctx, field) }, - } - return fc, nil + func(ctx context.Context) (any, error) { + return obj.ID, nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v string) graphql.Marshaler { + return ec.marshalNID2string(ctx, selections, v) + }, + true, + true, + ) } - -func (ec *executionContext) _DataframeOptimizationPolicy_minimumSavings(ctx context.Context, field graphql.CollectedField, obj *model.DataframeOptimizationPolicy) (ret graphql.Marshaler) { - fc, err := ec.fieldContext_DataframeOptimizationPolicy_minimumSavings(ctx, field) - if err != nil { - return graphql.Null - } - ctx = graphql.WithFieldContext(ctx, fc) - defer func() { - if r := recover(); r != nil { - ec.Error(ctx, ec.Recover(ctx, r)) - ret = graphql.Null - } - }() - resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { - ctx = rctx // use context from middleware stack in children - return obj.MinimumSavings, nil - }) - if err != nil { - ec.Error(ctx, err) - return graphql.Null - } - if resTmp == nil { - if !graphql.HasFieldError(ctx, fc) { - ec.Errorf(ctx, "must not be null") - } - return graphql.Null - } - res := resTmp.(int) - fc.Result = res - return ec.marshalNInt2int(ctx, field.Selections, res) +func (ec *executionContext) fieldContext_DataframeMaterialization_id(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + return graphql.NewScalarFieldContext("DataframeMaterialization", field, false, false, errors.New("field of type ID does not have child fields")) } -func (ec *executionContext) fieldContext_DataframeOptimizationPolicy_minimumSavings(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { - fc = &graphql.FieldContext{ - Object: "DataframeOptimizationPolicy", - Field: field, - IsMethod: false, - IsResolver: false, - Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { - return nil, errors.New("field of type Int does not have child fields") +func (ec *executionContext) _DataframeMaterialization_name(ctx context.Context, field graphql.CollectedField, obj *model.DataframeMaterialization) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_DataframeMaterialization_name(ctx, field) }, - } - return fc, nil + func(ctx context.Context) (any, error) { + return obj.Name, nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v string) graphql.Marshaler { + return ec.marshalNString2string(ctx, selections, v) + }, + true, + true, + ) } - -func (ec *executionContext) _DataframeOptimizationPolicy_decisions(ctx context.Context, field graphql.CollectedField, obj *model.DataframeOptimizationPolicy) (ret graphql.Marshaler) { - fc, err := ec.fieldContext_DataframeOptimizationPolicy_decisions(ctx, field) - if err != nil { - return graphql.Null - } - ctx = graphql.WithFieldContext(ctx, fc) - defer func() { - if r := recover(); r != nil { - ec.Error(ctx, ec.Recover(ctx, r)) - ret = graphql.Null - } - }() - resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { - ctx = rctx // use context from middleware stack in children - return obj.Decisions, nil - }) - if err != nil { - ec.Error(ctx, err) - return graphql.Null - } - if resTmp == nil { - if !graphql.HasFieldError(ctx, fc) { - ec.Errorf(ctx, "must not be null") - } - return graphql.Null - } - res := resTmp.([]*model.DataframeOptimizationDecision) - fc.Result = res - return ec.marshalNDataframeOptimizationDecision2ᚕᚖgithubᚗcomᚋcalyprᚋloomᚋgraphqlapiᚋmodelᚐDataframeOptimizationDecisionᚄ(ctx, field.Selections, res) +func (ec *executionContext) fieldContext_DataframeMaterialization_name(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + return graphql.NewScalarFieldContext("DataframeMaterialization", field, false, false, errors.New("field of type String does not have child fields")) } -func (ec *executionContext) fieldContext_DataframeOptimizationPolicy_decisions(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { - fc = &graphql.FieldContext{ - Object: "DataframeOptimizationPolicy", - Field: field, - IsMethod: false, - IsResolver: false, - Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { - switch field.Name { - case "rule": - return ec.fieldContext_DataframeOptimizationDecision_rule(ctx, field) - case "enabled": - return ec.fieldContext_DataframeOptimizationDecision_enabled(ctx, field) - case "candidateSets": - return ec.fieldContext_DataframeOptimizationDecision_candidateSets(ctx, field) - case "estimatedBaselineWork": - return ec.fieldContext_DataframeOptimizationDecision_estimatedBaselineWork(ctx, field) - case "estimatedOptimizedWork": - return ec.fieldContext_DataframeOptimizationDecision_estimatedOptimizedWork(ctx, field) - case "estimatedSavings": - return ec.fieldContext_DataframeOptimizationDecision_estimatedSavings(ctx, field) - case "reason": - return ec.fieldContext_DataframeOptimizationDecision_reason(ctx, field) - } - return nil, fmt.Errorf("no field named %q was found under type DataframeOptimizationDecision", field.Name) +func (ec *executionContext) _DataframeMaterialization_revision(ctx context.Context, field graphql.CollectedField, obj *model.DataframeMaterialization) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_DataframeMaterialization_revision(ctx, field) }, - } - return fc, nil + func(ctx context.Context) (any, error) { + return obj.Revision, nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v string) graphql.Marshaler { + return ec.marshalNString2string(ctx, selections, v) + }, + true, + true, + ) } - -func (ec *executionContext) _DataframePageInfo_hasNextPage(ctx context.Context, field graphql.CollectedField, obj *model.DataframePageInfo) (ret graphql.Marshaler) { - fc, err := ec.fieldContext_DataframePageInfo_hasNextPage(ctx, field) - if err != nil { - return graphql.Null - } - ctx = graphql.WithFieldContext(ctx, fc) - defer func() { - if r := recover(); r != nil { - ec.Error(ctx, ec.Recover(ctx, r)) - ret = graphql.Null - } - }() - resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { - ctx = rctx // use context from middleware stack in children - return obj.HasNextPage, nil - }) - if err != nil { - ec.Error(ctx, err) - return graphql.Null - } - if resTmp == nil { - if !graphql.HasFieldError(ctx, fc) { - ec.Errorf(ctx, "must not be null") - } - return graphql.Null - } - res := resTmp.(bool) - fc.Result = res - return ec.marshalNBoolean2bool(ctx, field.Selections, res) +func (ec *executionContext) fieldContext_DataframeMaterialization_revision(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + return graphql.NewScalarFieldContext("DataframeMaterialization", field, false, false, errors.New("field of type String does not have child fields")) } -func (ec *executionContext) fieldContext_DataframePageInfo_hasNextPage(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { - fc = &graphql.FieldContext{ - Object: "DataframePageInfo", - Field: field, - IsMethod: false, - IsResolver: false, - Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { - return nil, errors.New("field of type Boolean does not have child fields") +func (ec *executionContext) _DataframeMaterialization_state(ctx context.Context, field graphql.CollectedField, obj *model.DataframeMaterialization) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_DataframeMaterialization_state(ctx, field) }, - } - return fc, nil + func(ctx context.Context) (any, error) { + return obj.State, nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v model.DataframeMaterializationState) graphql.Marshaler { + return ec.marshalNDataframeMaterializationState2githubᚗcomᚋcalyprᚋloomᚋgraphqlapiᚋmodelᚐDataframeMaterializationState(ctx, selections, v) + }, + true, + true, + ) } - -func (ec *executionContext) _DataframePageInfo_endCursor(ctx context.Context, field graphql.CollectedField, obj *model.DataframePageInfo) (ret graphql.Marshaler) { - fc, err := ec.fieldContext_DataframePageInfo_endCursor(ctx, field) - if err != nil { - return graphql.Null - } - ctx = graphql.WithFieldContext(ctx, fc) - defer func() { - if r := recover(); r != nil { - ec.Error(ctx, ec.Recover(ctx, r)) - ret = graphql.Null - } - }() - resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { - ctx = rctx // use context from middleware stack in children - return obj.EndCursor, nil - }) - if err != nil { - ec.Error(ctx, err) - return graphql.Null - } - if resTmp == nil { - return graphql.Null - } - res := resTmp.(*string) - fc.Result = res - return ec.marshalOString2ᚖstring(ctx, field.Selections, res) +func (ec *executionContext) fieldContext_DataframeMaterialization_state(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + return graphql.NewScalarFieldContext("DataframeMaterialization", field, false, false, errors.New("field of type DataframeMaterializationState does not have child fields")) } -func (ec *executionContext) fieldContext_DataframePageInfo_endCursor(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { +func (ec *executionContext) _DataframeMaterialization_columns(ctx context.Context, field graphql.CollectedField, obj *model.DataframeMaterialization) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_DataframeMaterialization_columns(ctx, field) + }, + func(ctx context.Context) (any, error) { + return obj.Columns, nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v []*model.DataframeColumn) graphql.Marshaler { + return ec.marshalNDataframeColumn2ᚕᚖgithubᚗcomᚋcalyprᚋloomᚋgraphqlapiᚋmodelᚐDataframeColumnᚄ(ctx, selections, v) + }, + true, + true, + ) +} +func (ec *executionContext) fieldContext_DataframeMaterialization_columns(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { fc = &graphql.FieldContext{ - Object: "DataframePageInfo", + Object: "DataframeMaterialization", Field: field, IsMethod: false, IsResolver: false, Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { - return nil, errors.New("field of type String does not have child fields") + return ec.childFields_DataframeColumn(ctx, field) }, } return fc, nil } -func (ec *executionContext) _DataframeQueryDiagnostics_inputResolutionMs(ctx context.Context, field graphql.CollectedField, obj *model.DataframeQueryDiagnostics) (ret graphql.Marshaler) { - fc, err := ec.fieldContext_DataframeQueryDiagnostics_inputResolutionMs(ctx, field) - if err != nil { - return graphql.Null - } - ctx = graphql.WithFieldContext(ctx, fc) - defer func() { - if r := recover(); r != nil { - ec.Error(ctx, ec.Recover(ctx, r)) - ret = graphql.Null - } - }() - resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { - ctx = rctx // use context from middleware stack in children - return obj.InputResolutionMs, nil - }) - if err != nil { - ec.Error(ctx, err) - return graphql.Null - } - if resTmp == nil { - if !graphql.HasFieldError(ctx, fc) { - ec.Errorf(ctx, "must not be null") - } - return graphql.Null - } - res := resTmp.(float64) - fc.Result = res - return ec.marshalNFloat2float64(ctx, field.Selections, res) +func (ec *executionContext) _DataframeMaterialization_rowCount(ctx context.Context, field graphql.CollectedField, obj *model.DataframeMaterialization) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_DataframeMaterialization_rowCount(ctx, field) + }, + func(ctx context.Context) (any, error) { + return obj.RowCount, nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v int) graphql.Marshaler { + return ec.marshalNInt2int(ctx, selections, v) + }, + true, + true, + ) +} +func (ec *executionContext) fieldContext_DataframeMaterialization_rowCount(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + return graphql.NewScalarFieldContext("DataframeMaterialization", field, false, false, errors.New("field of type Int does not have child fields")) } -func (ec *executionContext) fieldContext_DataframeQueryDiagnostics_inputResolutionMs(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { - fc = &graphql.FieldContext{ - Object: "DataframeQueryDiagnostics", - Field: field, - IsMethod: false, - IsResolver: false, - Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { - return nil, errors.New("field of type Float does not have child fields") +func (ec *executionContext) _DataframeMaterialization_createdAt(ctx context.Context, field graphql.CollectedField, obj *model.DataframeMaterialization) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_DataframeMaterialization_createdAt(ctx, field) }, - } - return fc, nil + func(ctx context.Context) (any, error) { + return obj.CreatedAt, nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v string) graphql.Marshaler { + return ec.marshalNString2string(ctx, selections, v) + }, + true, + true, + ) } - -func (ec *executionContext) _DataframeQueryDiagnostics_requestPreparationMs(ctx context.Context, field graphql.CollectedField, obj *model.DataframeQueryDiagnostics) (ret graphql.Marshaler) { - fc, err := ec.fieldContext_DataframeQueryDiagnostics_requestPreparationMs(ctx, field) - if err != nil { - return graphql.Null - } - ctx = graphql.WithFieldContext(ctx, fc) - defer func() { - if r := recover(); r != nil { - ec.Error(ctx, ec.Recover(ctx, r)) - ret = graphql.Null - } - }() - resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { - ctx = rctx // use context from middleware stack in children - return obj.RequestPreparationMs, nil - }) - if err != nil { - ec.Error(ctx, err) - return graphql.Null - } - if resTmp == nil { - if !graphql.HasFieldError(ctx, fc) { - ec.Errorf(ctx, "must not be null") - } - return graphql.Null - } - res := resTmp.(float64) - fc.Result = res - return ec.marshalNFloat2float64(ctx, field.Selections, res) +func (ec *executionContext) fieldContext_DataframeMaterialization_createdAt(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + return graphql.NewScalarFieldContext("DataframeMaterialization", field, false, false, errors.New("field of type String does not have child fields")) } -func (ec *executionContext) fieldContext_DataframeQueryDiagnostics_requestPreparationMs(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { - fc = &graphql.FieldContext{ - Object: "DataframeQueryDiagnostics", - Field: field, - IsMethod: false, - IsResolver: false, - Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { - return nil, errors.New("field of type Float does not have child fields") +func (ec *executionContext) _DataframeMaterialization_readyAt(ctx context.Context, field graphql.CollectedField, obj *model.DataframeMaterialization) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_DataframeMaterialization_readyAt(ctx, field) }, - } - return fc, nil + func(ctx context.Context) (any, error) { + return obj.ReadyAt, nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v *string) graphql.Marshaler { + return ec.marshalOString2ᚖstring(ctx, selections, v) + }, + true, + false, + ) +} +func (ec *executionContext) fieldContext_DataframeMaterialization_readyAt(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + return graphql.NewScalarFieldContext("DataframeMaterialization", field, false, false, errors.New("field of type String does not have child fields")) } -func (ec *executionContext) _DataframeQueryDiagnostics_compilationMs(ctx context.Context, field graphql.CollectedField, obj *model.DataframeQueryDiagnostics) (ret graphql.Marshaler) { - fc, err := ec.fieldContext_DataframeQueryDiagnostics_compilationMs(ctx, field) - if err != nil { - return graphql.Null - } - ctx = graphql.WithFieldContext(ctx, fc) - defer func() { - if r := recover(); r != nil { - ec.Error(ctx, ec.Recover(ctx, r)) - ret = graphql.Null - } - }() - resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { - ctx = rctx // use context from middleware stack in children - return obj.CompilationMs, nil - }) - if err != nil { - ec.Error(ctx, err) - return graphql.Null - } - if resTmp == nil { - if !graphql.HasFieldError(ctx, fc) { - ec.Errorf(ctx, "must not be null") - } - return graphql.Null - } - res := resTmp.(float64) - fc.Result = res - return ec.marshalNFloat2float64(ctx, field.Selections, res) +func (ec *executionContext) _DataframeMaterialization_error(ctx context.Context, field graphql.CollectedField, obj *model.DataframeMaterialization) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_DataframeMaterialization_error(ctx, field) + }, + func(ctx context.Context) (any, error) { + return obj.Error, nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v *string) graphql.Marshaler { + return ec.marshalOString2ᚖstring(ctx, selections, v) + }, + true, + false, + ) +} +func (ec *executionContext) fieldContext_DataframeMaterialization_error(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + return graphql.NewScalarFieldContext("DataframeMaterialization", field, false, false, errors.New("field of type String does not have child fields")) } -func (ec *executionContext) fieldContext_DataframeQueryDiagnostics_compilationMs(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { - fc = &graphql.FieldContext{ - Object: "DataframeQueryDiagnostics", - Field: field, - IsMethod: false, - IsResolver: false, - Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { - return nil, errors.New("field of type Float does not have child fields") +func (ec *executionContext) _DataframeOptimizationDecision_rule(ctx context.Context, field graphql.CollectedField, obj *model.DataframeOptimizationDecision) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_DataframeOptimizationDecision_rule(ctx, field) }, - } - return fc, nil + func(ctx context.Context) (any, error) { + return obj.Rule, nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v string) graphql.Marshaler { + return ec.marshalNString2string(ctx, selections, v) + }, + true, + true, + ) +} +func (ec *executionContext) fieldContext_DataframeOptimizationDecision_rule(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + return graphql.NewScalarFieldContext("DataframeOptimizationDecision", field, false, false, errors.New("field of type String does not have child fields")) } -func (ec *executionContext) _DataframeQueryDiagnostics_arangoQueryMs(ctx context.Context, field graphql.CollectedField, obj *model.DataframeQueryDiagnostics) (ret graphql.Marshaler) { - fc, err := ec.fieldContext_DataframeQueryDiagnostics_arangoQueryMs(ctx, field) - if err != nil { - return graphql.Null - } - ctx = graphql.WithFieldContext(ctx, fc) - defer func() { - if r := recover(); r != nil { - ec.Error(ctx, ec.Recover(ctx, r)) - ret = graphql.Null - } - }() - resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { - ctx = rctx // use context from middleware stack in children - return obj.ArangoQueryMs, nil - }) - if err != nil { - ec.Error(ctx, err) - return graphql.Null - } - if resTmp == nil { - if !graphql.HasFieldError(ctx, fc) { - ec.Errorf(ctx, "must not be null") - } - return graphql.Null - } - res := resTmp.(float64) - fc.Result = res - return ec.marshalNFloat2float64(ctx, field.Selections, res) +func (ec *executionContext) _DataframeOptimizationDecision_enabled(ctx context.Context, field graphql.CollectedField, obj *model.DataframeOptimizationDecision) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_DataframeOptimizationDecision_enabled(ctx, field) + }, + func(ctx context.Context) (any, error) { + return obj.Enabled, nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v bool) graphql.Marshaler { + return ec.marshalNBoolean2bool(ctx, selections, v) + }, + true, + true, + ) +} +func (ec *executionContext) fieldContext_DataframeOptimizationDecision_enabled(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + return graphql.NewScalarFieldContext("DataframeOptimizationDecision", field, false, false, errors.New("field of type Boolean does not have child fields")) } -func (ec *executionContext) fieldContext_DataframeQueryDiagnostics_arangoQueryMs(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { +func (ec *executionContext) _DataframeOptimizationDecision_candidateSets(ctx context.Context, field graphql.CollectedField, obj *model.DataframeOptimizationDecision) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_DataframeOptimizationDecision_candidateSets(ctx, field) + }, + func(ctx context.Context) (any, error) { + return obj.CandidateSets, nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v int) graphql.Marshaler { + return ec.marshalNInt2int(ctx, selections, v) + }, + true, + true, + ) +} +func (ec *executionContext) fieldContext_DataframeOptimizationDecision_candidateSets(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + return graphql.NewScalarFieldContext("DataframeOptimizationDecision", field, false, false, errors.New("field of type Int does not have child fields")) +} + +func (ec *executionContext) _DataframeOptimizationDecision_estimatedBaselineWork(ctx context.Context, field graphql.CollectedField, obj *model.DataframeOptimizationDecision) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_DataframeOptimizationDecision_estimatedBaselineWork(ctx, field) + }, + func(ctx context.Context) (any, error) { + return obj.EstimatedBaselineWork, nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v int) graphql.Marshaler { + return ec.marshalNInt2int(ctx, selections, v) + }, + true, + true, + ) +} +func (ec *executionContext) fieldContext_DataframeOptimizationDecision_estimatedBaselineWork(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + return graphql.NewScalarFieldContext("DataframeOptimizationDecision", field, false, false, errors.New("field of type Int does not have child fields")) +} + +func (ec *executionContext) _DataframeOptimizationDecision_estimatedOptimizedWork(ctx context.Context, field graphql.CollectedField, obj *model.DataframeOptimizationDecision) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_DataframeOptimizationDecision_estimatedOptimizedWork(ctx, field) + }, + func(ctx context.Context) (any, error) { + return obj.EstimatedOptimizedWork, nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v int) graphql.Marshaler { + return ec.marshalNInt2int(ctx, selections, v) + }, + true, + true, + ) +} +func (ec *executionContext) fieldContext_DataframeOptimizationDecision_estimatedOptimizedWork(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + return graphql.NewScalarFieldContext("DataframeOptimizationDecision", field, false, false, errors.New("field of type Int does not have child fields")) +} + +func (ec *executionContext) _DataframeOptimizationDecision_estimatedSavings(ctx context.Context, field graphql.CollectedField, obj *model.DataframeOptimizationDecision) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_DataframeOptimizationDecision_estimatedSavings(ctx, field) + }, + func(ctx context.Context) (any, error) { + return obj.EstimatedSavings, nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v int) graphql.Marshaler { + return ec.marshalNInt2int(ctx, selections, v) + }, + true, + true, + ) +} +func (ec *executionContext) fieldContext_DataframeOptimizationDecision_estimatedSavings(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + return graphql.NewScalarFieldContext("DataframeOptimizationDecision", field, false, false, errors.New("field of type Int does not have child fields")) +} + +func (ec *executionContext) _DataframeOptimizationDecision_reason(ctx context.Context, field graphql.CollectedField, obj *model.DataframeOptimizationDecision) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_DataframeOptimizationDecision_reason(ctx, field) + }, + func(ctx context.Context) (any, error) { + return obj.Reason, nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v string) graphql.Marshaler { + return ec.marshalNString2string(ctx, selections, v) + }, + true, + true, + ) +} +func (ec *executionContext) fieldContext_DataframeOptimizationDecision_reason(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + return graphql.NewScalarFieldContext("DataframeOptimizationDecision", field, false, false, errors.New("field of type String does not have child fields")) +} + +func (ec *executionContext) _DataframeOptimizationPolicy_name(ctx context.Context, field graphql.CollectedField, obj *model.DataframeOptimizationPolicy) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_DataframeOptimizationPolicy_name(ctx, field) + }, + func(ctx context.Context) (any, error) { + return obj.Name, nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v string) graphql.Marshaler { + return ec.marshalNString2string(ctx, selections, v) + }, + true, + true, + ) +} +func (ec *executionContext) fieldContext_DataframeOptimizationPolicy_name(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + return graphql.NewScalarFieldContext("DataframeOptimizationPolicy", field, false, false, errors.New("field of type String does not have child fields")) +} + +func (ec *executionContext) _DataframeOptimizationPolicy_enabled(ctx context.Context, field graphql.CollectedField, obj *model.DataframeOptimizationPolicy) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_DataframeOptimizationPolicy_enabled(ctx, field) + }, + func(ctx context.Context) (any, error) { + return obj.Enabled, nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v bool) graphql.Marshaler { + return ec.marshalNBoolean2bool(ctx, selections, v) + }, + true, + true, + ) +} +func (ec *executionContext) fieldContext_DataframeOptimizationPolicy_enabled(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + return graphql.NewScalarFieldContext("DataframeOptimizationPolicy", field, false, false, errors.New("field of type Boolean does not have child fields")) +} + +func (ec *executionContext) _DataframeOptimizationPolicy_minimumSavings(ctx context.Context, field graphql.CollectedField, obj *model.DataframeOptimizationPolicy) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_DataframeOptimizationPolicy_minimumSavings(ctx, field) + }, + func(ctx context.Context) (any, error) { + return obj.MinimumSavings, nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v int) graphql.Marshaler { + return ec.marshalNInt2int(ctx, selections, v) + }, + true, + true, + ) +} +func (ec *executionContext) fieldContext_DataframeOptimizationPolicy_minimumSavings(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + return graphql.NewScalarFieldContext("DataframeOptimizationPolicy", field, false, false, errors.New("field of type Int does not have child fields")) +} + +func (ec *executionContext) _DataframeOptimizationPolicy_decisions(ctx context.Context, field graphql.CollectedField, obj *model.DataframeOptimizationPolicy) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_DataframeOptimizationPolicy_decisions(ctx, field) + }, + func(ctx context.Context) (any, error) { + return obj.Decisions, nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v []*model.DataframeOptimizationDecision) graphql.Marshaler { + return ec.marshalNDataframeOptimizationDecision2ᚕᚖgithubᚗcomᚋcalyprᚋloomᚋgraphqlapiᚋmodelᚐDataframeOptimizationDecisionᚄ(ctx, selections, v) + }, + true, + true, + ) +} +func (ec *executionContext) fieldContext_DataframeOptimizationPolicy_decisions(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { fc = &graphql.FieldContext{ - Object: "DataframeQueryDiagnostics", + Object: "DataframeOptimizationPolicy", Field: field, IsMethod: false, IsResolver: false, Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { - return nil, errors.New("field of type Float does not have child fields") + return ec.childFields_DataframeOptimizationDecision(ctx, field) }, } return fc, nil } -func (ec *executionContext) _DataframeQueryDiagnostics_rowMaterializationMs(ctx context.Context, field graphql.CollectedField, obj *model.DataframeQueryDiagnostics) (ret graphql.Marshaler) { - fc, err := ec.fieldContext_DataframeQueryDiagnostics_rowMaterializationMs(ctx, field) - if err != nil { - return graphql.Null - } - ctx = graphql.WithFieldContext(ctx, fc) - defer func() { - if r := recover(); r != nil { - ec.Error(ctx, ec.Recover(ctx, r)) - ret = graphql.Null - } - }() - resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { - ctx = rctx // use context from middleware stack in children - return obj.RowMaterializationMs, nil - }) - if err != nil { - ec.Error(ctx, err) - return graphql.Null - } - if resTmp == nil { - if !graphql.HasFieldError(ctx, fc) { - ec.Errorf(ctx, "must not be null") - } - return graphql.Null - } - res := resTmp.(float64) - fc.Result = res - return ec.marshalNFloat2float64(ctx, field.Selections, res) +func (ec *executionContext) _DataframePageInfo_hasNextPage(ctx context.Context, field graphql.CollectedField, obj *model.DataframePageInfo) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_DataframePageInfo_hasNextPage(ctx, field) + }, + func(ctx context.Context) (any, error) { + return obj.HasNextPage, nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v bool) graphql.Marshaler { + return ec.marshalNBoolean2bool(ctx, selections, v) + }, + true, + true, + ) +} +func (ec *executionContext) fieldContext_DataframePageInfo_hasNextPage(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + return graphql.NewScalarFieldContext("DataframePageInfo", field, false, false, errors.New("field of type Boolean does not have child fields")) +} + +func (ec *executionContext) _DataframePageInfo_endCursor(ctx context.Context, field graphql.CollectedField, obj *model.DataframePageInfo) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_DataframePageInfo_endCursor(ctx, field) + }, + func(ctx context.Context) (any, error) { + return obj.EndCursor, nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v *string) graphql.Marshaler { + return ec.marshalOString2ᚖstring(ctx, selections, v) + }, + true, + false, + ) +} +func (ec *executionContext) fieldContext_DataframePageInfo_endCursor(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + return graphql.NewScalarFieldContext("DataframePageInfo", field, false, false, errors.New("field of type String does not have child fields")) +} + +func (ec *executionContext) _DataframeQueryDiagnostics_inputResolutionMs(ctx context.Context, field graphql.CollectedField, obj *model.DataframeQueryDiagnostics) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_DataframeQueryDiagnostics_inputResolutionMs(ctx, field) + }, + func(ctx context.Context) (any, error) { + return obj.InputResolutionMs, nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v float64) graphql.Marshaler { + return ec.marshalNFloat2float64(ctx, selections, v) + }, + true, + true, + ) +} +func (ec *executionContext) fieldContext_DataframeQueryDiagnostics_inputResolutionMs(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + return graphql.NewScalarFieldContext("DataframeQueryDiagnostics", field, false, false, errors.New("field of type Float does not have child fields")) +} + +func (ec *executionContext) _DataframeQueryDiagnostics_requestPreparationMs(ctx context.Context, field graphql.CollectedField, obj *model.DataframeQueryDiagnostics) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_DataframeQueryDiagnostics_requestPreparationMs(ctx, field) + }, + func(ctx context.Context) (any, error) { + return obj.RequestPreparationMs, nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v float64) graphql.Marshaler { + return ec.marshalNFloat2float64(ctx, selections, v) + }, + true, + true, + ) +} +func (ec *executionContext) fieldContext_DataframeQueryDiagnostics_requestPreparationMs(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + return graphql.NewScalarFieldContext("DataframeQueryDiagnostics", field, false, false, errors.New("field of type Float does not have child fields")) +} + +func (ec *executionContext) _DataframeQueryDiagnostics_compilationMs(ctx context.Context, field graphql.CollectedField, obj *model.DataframeQueryDiagnostics) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_DataframeQueryDiagnostics_compilationMs(ctx, field) + }, + func(ctx context.Context) (any, error) { + return obj.CompilationMs, nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v float64) graphql.Marshaler { + return ec.marshalNFloat2float64(ctx, selections, v) + }, + true, + true, + ) +} +func (ec *executionContext) fieldContext_DataframeQueryDiagnostics_compilationMs(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + return graphql.NewScalarFieldContext("DataframeQueryDiagnostics", field, false, false, errors.New("field of type Float does not have child fields")) +} + +func (ec *executionContext) _DataframeQueryDiagnostics_arangoQueryMs(ctx context.Context, field graphql.CollectedField, obj *model.DataframeQueryDiagnostics) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_DataframeQueryDiagnostics_arangoQueryMs(ctx, field) + }, + func(ctx context.Context) (any, error) { + return obj.ArangoQueryMs, nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v float64) graphql.Marshaler { + return ec.marshalNFloat2float64(ctx, selections, v) + }, + true, + true, + ) +} +func (ec *executionContext) fieldContext_DataframeQueryDiagnostics_arangoQueryMs(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + return graphql.NewScalarFieldContext("DataframeQueryDiagnostics", field, false, false, errors.New("field of type Float does not have child fields")) } +func (ec *executionContext) _DataframeQueryDiagnostics_rowMaterializationMs(ctx context.Context, field graphql.CollectedField, obj *model.DataframeQueryDiagnostics) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_DataframeQueryDiagnostics_rowMaterializationMs(ctx, field) + }, + func(ctx context.Context) (any, error) { + return obj.RowMaterializationMs, nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v float64) graphql.Marshaler { + return ec.marshalNFloat2float64(ctx, selections, v) + }, + true, + true, + ) +} func (ec *executionContext) fieldContext_DataframeQueryDiagnostics_rowMaterializationMs(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + return graphql.NewScalarFieldContext("DataframeQueryDiagnostics", field, false, false, errors.New("field of type Float does not have child fields")) +} + +func (ec *executionContext) _DataframeQueryDiagnostics_resultAssemblyMs(ctx context.Context, field graphql.CollectedField, obj *model.DataframeQueryDiagnostics) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_DataframeQueryDiagnostics_resultAssemblyMs(ctx, field) + }, + func(ctx context.Context) (any, error) { + return obj.ResultAssemblyMs, nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v float64) graphql.Marshaler { + return ec.marshalNFloat2float64(ctx, selections, v) + }, + true, + true, + ) +} +func (ec *executionContext) fieldContext_DataframeQueryDiagnostics_resultAssemblyMs(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + return graphql.NewScalarFieldContext("DataframeQueryDiagnostics", field, false, false, errors.New("field of type Float does not have child fields")) +} + +func (ec *executionContext) _DataframeQueryDiagnostics_totalMs(ctx context.Context, field graphql.CollectedField, obj *model.DataframeQueryDiagnostics) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_DataframeQueryDiagnostics_totalMs(ctx, field) + }, + func(ctx context.Context) (any, error) { + return obj.TotalMs, nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v float64) graphql.Marshaler { + return ec.marshalNFloat2float64(ctx, selections, v) + }, + true, + true, + ) +} +func (ec *executionContext) fieldContext_DataframeQueryDiagnostics_totalMs(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + return graphql.NewScalarFieldContext("DataframeQueryDiagnostics", field, false, false, errors.New("field of type Float does not have child fields")) +} + +func (ec *executionContext) _DataframeQueryDiagnostics_plan(ctx context.Context, field graphql.CollectedField, obj *model.DataframeQueryDiagnostics) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_DataframeQueryDiagnostics_plan(ctx, field) + }, + func(ctx context.Context) (any, error) { + return obj.Plan, nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v *model.DataframeCompilerPlanDiagnostics) graphql.Marshaler { + return ec.marshalNDataframeCompilerPlanDiagnostics2ᚖgithubᚗcomᚋcalyprᚋloomᚋgraphqlapiᚋmodelᚐDataframeCompilerPlanDiagnostics(ctx, selections, v) + }, + true, + true, + ) +} +func (ec *executionContext) fieldContext_DataframeQueryDiagnostics_plan(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { fc = &graphql.FieldContext{ Object: "DataframeQueryDiagnostics", Field: field, IsMethod: false, IsResolver: false, Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { - return nil, errors.New("field of type Float does not have child fields") + return ec.childFields_DataframeCompilerPlanDiagnostics(ctx, field) }, } return fc, nil } -func (ec *executionContext) _DataframeQueryDiagnostics_resultAssemblyMs(ctx context.Context, field graphql.CollectedField, obj *model.DataframeQueryDiagnostics) (ret graphql.Marshaler) { - fc, err := ec.fieldContext_DataframeQueryDiagnostics_resultAssemblyMs(ctx, field) - if err != nil { - return graphql.Null - } - ctx = graphql.WithFieldContext(ctx, fc) - defer func() { - if r := recover(); r != nil { - ec.Error(ctx, ec.Recover(ctx, r)) - ret = graphql.Null - } - }() - resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { - ctx = rctx // use context from middleware stack in children - return obj.ResultAssemblyMs, nil - }) - if err != nil { - ec.Error(ctx, err) - return graphql.Null - } - if resTmp == nil { - if !graphql.HasFieldError(ctx, fc) { - ec.Errorf(ctx, "must not be null") - } - return graphql.Null - } - res := resTmp.(float64) - fc.Result = res - return ec.marshalNFloat2float64(ctx, field.Selections, res) +func (ec *executionContext) _DataframeRecipeArangoAssessment_plans(ctx context.Context, field graphql.CollectedField, obj *model.DataframeRecipeArangoAssessment) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_DataframeRecipeArangoAssessment_plans(ctx, field) + }, + func(ctx context.Context) (any, error) { + return obj.Plans, nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v []*model.DataframeRecipeExplainPlanEstimate) graphql.Marshaler { + return ec.marshalNDataframeRecipeExplainPlanEstimate2ᚕᚖgithubᚗcomᚋcalyprᚋloomᚋgraphqlapiᚋmodelᚐDataframeRecipeExplainPlanEstimateᚄ(ctx, selections, v) + }, + true, + true, + ) } - -func (ec *executionContext) fieldContext_DataframeQueryDiagnostics_resultAssemblyMs(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { +func (ec *executionContext) fieldContext_DataframeRecipeArangoAssessment_plans(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { fc = &graphql.FieldContext{ - Object: "DataframeQueryDiagnostics", + Object: "DataframeRecipeArangoAssessment", Field: field, IsMethod: false, IsResolver: false, Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { - return nil, errors.New("field of type Float does not have child fields") + return ec.childFields_DataframeRecipeExplainPlanEstimate(ctx, field) }, } return fc, nil } -func (ec *executionContext) _DataframeQueryDiagnostics_totalMs(ctx context.Context, field graphql.CollectedField, obj *model.DataframeQueryDiagnostics) (ret graphql.Marshaler) { - fc, err := ec.fieldContext_DataframeQueryDiagnostics_totalMs(ctx, field) - if err != nil { - return graphql.Null - } - ctx = graphql.WithFieldContext(ctx, fc) - defer func() { - if r := recover(); r != nil { - ec.Error(ctx, ec.Recover(ctx, r)) - ret = graphql.Null - } - }() - resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { - ctx = rctx // use context from middleware stack in children - return obj.TotalMs, nil - }) - if err != nil { - ec.Error(ctx, err) - return graphql.Null - } - if resTmp == nil { - if !graphql.HasFieldError(ctx, fc) { - ec.Errorf(ctx, "must not be null") - } - return graphql.Null - } - res := resTmp.(float64) - fc.Result = res - return ec.marshalNFloat2float64(ctx, field.Selections, res) +func (ec *executionContext) _DataframeRecipeArangoAssessment_fullCollectionScans(ctx context.Context, field graphql.CollectedField, obj *model.DataframeRecipeArangoAssessment) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_DataframeRecipeArangoAssessment_fullCollectionScans(ctx, field) + }, + func(ctx context.Context) (any, error) { + return obj.FullCollectionScans, nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v []*model.DataframeRecipeExplainCollectionScan) graphql.Marshaler { + return ec.marshalNDataframeRecipeExplainCollectionScan2ᚕᚖgithubᚗcomᚋcalyprᚋloomᚋgraphqlapiᚋmodelᚐDataframeRecipeExplainCollectionScanᚄ(ctx, selections, v) + }, + true, + true, + ) } - -func (ec *executionContext) fieldContext_DataframeQueryDiagnostics_totalMs(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { +func (ec *executionContext) fieldContext_DataframeRecipeArangoAssessment_fullCollectionScans(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { fc = &graphql.FieldContext{ - Object: "DataframeQueryDiagnostics", + Object: "DataframeRecipeArangoAssessment", Field: field, IsMethod: false, IsResolver: false, Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { - return nil, errors.New("field of type Float does not have child fields") + return ec.childFields_DataframeRecipeExplainCollectionScan(ctx, field) }, } return fc, nil } -func (ec *executionContext) _DataframeQueryDiagnostics_plan(ctx context.Context, field graphql.CollectedField, obj *model.DataframeQueryDiagnostics) (ret graphql.Marshaler) { - fc, err := ec.fieldContext_DataframeQueryDiagnostics_plan(ctx, field) - if err != nil { - return graphql.Null - } - ctx = graphql.WithFieldContext(ctx, fc) - defer func() { - if r := recover(); r != nil { - ec.Error(ctx, ec.Recover(ctx, r)) - ret = graphql.Null - } - }() - resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { - ctx = rctx // use context from middleware stack in children - return obj.Plan, nil - }) - if err != nil { - ec.Error(ctx, err) - return graphql.Null - } - if resTmp == nil { - if !graphql.HasFieldError(ctx, fc) { - ec.Errorf(ctx, "must not be null") - } - return graphql.Null - } - res := resTmp.(*model.DataframeCompilerPlanDiagnostics) - fc.Result = res - return ec.marshalNDataframeCompilerPlanDiagnostics2ᚖgithubᚗcomᚋcalyprᚋloomᚋgraphqlapiᚋmodelᚐDataframeCompilerPlanDiagnostics(ctx, field.Selections, res) +func (ec *executionContext) _DataframeRecipeArangoAssessment_indexes(ctx context.Context, field graphql.CollectedField, obj *model.DataframeRecipeArangoAssessment) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_DataframeRecipeArangoAssessment_indexes(ctx, field) + }, + func(ctx context.Context) (any, error) { + return obj.Indexes, nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v []*model.DataframeRecipeExplainIndexSummary) graphql.Marshaler { + return ec.marshalNDataframeRecipeExplainIndexSummary2ᚕᚖgithubᚗcomᚋcalyprᚋloomᚋgraphqlapiᚋmodelᚐDataframeRecipeExplainIndexSummaryᚄ(ctx, selections, v) + }, + true, + true, + ) } - -func (ec *executionContext) fieldContext_DataframeQueryDiagnostics_plan(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { +func (ec *executionContext) fieldContext_DataframeRecipeArangoAssessment_indexes(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { fc = &graphql.FieldContext{ - Object: "DataframeQueryDiagnostics", + Object: "DataframeRecipeArangoAssessment", Field: field, IsMethod: false, IsResolver: false, Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { - switch field.Name { - case "traversalSets": - return ec.fieldContext_DataframeCompilerPlanDiagnostics_traversalSets(ctx, field) - case "sharedTraversalCount": - return ec.fieldContext_DataframeCompilerPlanDiagnostics_sharedTraversalCount(ctx, field) - case "requiredMatchReuseCount": - return ec.fieldContext_DataframeCompilerPlanDiagnostics_requiredMatchReuseCount(ctx, field) - case "scopedSharingCandidateGroups": - return ec.fieldContext_DataframeCompilerPlanDiagnostics_scopedSharingCandidateGroups(ctx, field) - case "scopedSharingCandidateSets": - return ec.fieldContext_DataframeCompilerPlanDiagnostics_scopedSharingCandidateSets(ctx, field) - case "potentialSharingOpportunityGroups": - return ec.fieldContext_DataframeCompilerPlanDiagnostics_potentialSharingOpportunityGroups(ctx, field) - case "potentialSharingOpportunitySets": - return ec.fieldContext_DataframeCompilerPlanDiagnostics_potentialSharingOpportunitySets(ctx, field) - case "optimizationPolicy": - return ec.fieldContext_DataframeCompilerPlanDiagnostics_optimizationPolicy(ctx, field) - case "richSourceReuse": - return ec.fieldContext_DataframeCompilerPlanDiagnostics_richSourceReuse(ctx, field) - } - return nil, fmt.Errorf("no field named %q was found under type DataframeCompilerPlanDiagnostics", field.Name) + return ec.childFields_DataframeRecipeExplainIndexSummary(ctx, field) }, } return fc, nil } -func (ec *executionContext) _DataframeRelatedResourceHints_viaLabel(ctx context.Context, field graphql.CollectedField, obj *model.DataframeRelatedResourceHints) (ret graphql.Marshaler) { - fc, err := ec.fieldContext_DataframeRelatedResourceHints_viaLabel(ctx, field) - if err != nil { - return graphql.Null - } - ctx = graphql.WithFieldContext(ctx, fc) - defer func() { - if r := recover(); r != nil { - ec.Error(ctx, ec.Recover(ctx, r)) - ret = graphql.Null - } - }() - resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { - ctx = rctx // use context from middleware stack in children - return obj.ViaLabel, nil - }) - if err != nil { - ec.Error(ctx, err) - return graphql.Null - } - if resTmp == nil { - if !graphql.HasFieldError(ctx, fc) { - ec.Errorf(ctx, "must not be null") - } - return graphql.Null - } - res := resTmp.(string) - fc.Result = res - return ec.marshalNString2string(ctx, field.Selections, res) +func (ec *executionContext) _DataframeRecipeArangoAssessment_warnings(ctx context.Context, field graphql.CollectedField, obj *model.DataframeRecipeArangoAssessment) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_DataframeRecipeArangoAssessment_warnings(ctx, field) + }, + func(ctx context.Context) (any, error) { + return obj.Warnings, nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v []*model.DataframeRecipeExplainWarning) graphql.Marshaler { + return ec.marshalNDataframeRecipeExplainWarning2ᚕᚖgithubᚗcomᚋcalyprᚋloomᚋgraphqlapiᚋmodelᚐDataframeRecipeExplainWarningᚄ(ctx, selections, v) + }, + true, + true, + ) } - -func (ec *executionContext) fieldContext_DataframeRelatedResourceHints_viaLabel(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { +func (ec *executionContext) fieldContext_DataframeRecipeArangoAssessment_warnings(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { fc = &graphql.FieldContext{ - Object: "DataframeRelatedResourceHints", + Object: "DataframeRecipeArangoAssessment", Field: field, IsMethod: false, IsResolver: false, Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { - return nil, errors.New("field of type String does not have child fields") + return ec.childFields_DataframeRecipeExplainWarning(ctx, field) }, } return fc, nil } -func (ec *executionContext) _DataframeRelatedResourceHints_edgeCount(ctx context.Context, field graphql.CollectedField, obj *model.DataframeRelatedResourceHints) (ret graphql.Marshaler) { - fc, err := ec.fieldContext_DataframeRelatedResourceHints_edgeCount(ctx, field) - if err != nil { - return graphql.Null - } - ctx = graphql.WithFieldContext(ctx, fc) - defer func() { - if r := recover(); r != nil { - ec.Error(ctx, ec.Recover(ctx, r)) - ret = graphql.Null - } - }() - resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { - ctx = rctx // use context from middleware stack in children - return obj.EdgeCount, nil - }) - if err != nil { - ec.Error(ctx, err) - return graphql.Null - } - if resTmp == nil { - if !graphql.HasFieldError(ctx, fc) { - ec.Errorf(ctx, "must not be null") - } - return graphql.Null - } - res := resTmp.(int) - fc.Result = res - return ec.marshalNInt2int(ctx, field.Selections, res) +func (ec *executionContext) _DataframeRecipeArangoAssessment_appliedOptimizerRules(ctx context.Context, field graphql.CollectedField, obj *model.DataframeRecipeArangoAssessment) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_DataframeRecipeArangoAssessment_appliedOptimizerRules(ctx, field) + }, + func(ctx context.Context) (any, error) { + return obj.AppliedOptimizerRules, nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v []string) graphql.Marshaler { + return ec.marshalNString2ᚕstringᚄ(ctx, selections, v) + }, + true, + true, + ) +} +func (ec *executionContext) fieldContext_DataframeRecipeArangoAssessment_appliedOptimizerRules(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + return graphql.NewScalarFieldContext("DataframeRecipeArangoAssessment", field, false, false, errors.New("field of type String does not have child fields")) } -func (ec *executionContext) fieldContext_DataframeRelatedResourceHints_edgeCount(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { - fc = &graphql.FieldContext{ - Object: "DataframeRelatedResourceHints", - Field: field, - IsMethod: false, - IsResolver: false, - Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { - return nil, errors.New("field of type Int does not have child fields") +func (ec *executionContext) _DataframeRecipeColumn_output(ctx context.Context, field graphql.CollectedField, obj *model.DataframeRecipeColumn) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_DataframeRecipeColumn_output(ctx, field) }, - } - return fc, nil + func(ctx context.Context) (any, error) { + return obj.Output, nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v string) graphql.Marshaler { + return ec.marshalNString2string(ctx, selections, v) + }, + true, + true, + ) +} +func (ec *executionContext) fieldContext_DataframeRecipeColumn_output(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + return graphql.NewScalarFieldContext("DataframeRecipeColumn", field, false, false, errors.New("field of type String does not have child fields")) } -func (ec *executionContext) _DataframeRelatedResourceHints_target(ctx context.Context, field graphql.CollectedField, obj *model.DataframeRelatedResourceHints) (ret graphql.Marshaler) { - fc, err := ec.fieldContext_DataframeRelatedResourceHints_target(ctx, field) - if err != nil { - return graphql.Null - } - ctx = graphql.WithFieldContext(ctx, fc) - defer func() { - if r := recover(); r != nil { - ec.Error(ctx, ec.Recover(ctx, r)) - ret = graphql.Null - } - }() - resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { - ctx = rctx // use context from middleware stack in children - return obj.Target, nil - }) - if err != nil { - ec.Error(ctx, err) - return graphql.Null - } - if resTmp == nil { - if !graphql.HasFieldError(ctx, fc) { - ec.Errorf(ctx, "must not be null") - } - return graphql.Null - } - res := resTmp.(*model.DataframeResourceHints) - fc.Result = res - return ec.marshalNDataframeResourceHints2ᚖgithubᚗcomᚋcalyprᚋloomᚋgraphqlapiᚋmodelᚐDataframeResourceHints(ctx, field.Selections, res) +func (ec *executionContext) _DataframeRecipeColumn_dynamicName(ctx context.Context, field graphql.CollectedField, obj *model.DataframeRecipeColumn) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_DataframeRecipeColumn_dynamicName(ctx, field) + }, + func(ctx context.Context) (any, error) { + return obj.DynamicName, nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v string) graphql.Marshaler { + return ec.marshalNString2string(ctx, selections, v) + }, + true, + true, + ) +} +func (ec *executionContext) fieldContext_DataframeRecipeColumn_dynamicName(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + return graphql.NewScalarFieldContext("DataframeRecipeColumn", field, false, false, errors.New("field of type String does not have child fields")) } -func (ec *executionContext) fieldContext_DataframeRelatedResourceHints_target(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { - fc = &graphql.FieldContext{ - Object: "DataframeRelatedResourceHints", - Field: field, - IsMethod: false, - IsResolver: false, - Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { - switch field.Name { - case "resourceType": - return ec.fieldContext_DataframeResourceHints_resourceType(ctx, field) - case "fields": - return ec.fieldContext_DataframeResourceHints_fields(ctx, field) - case "pivotFields": - return ec.fieldContext_DataframeResourceHints_pivotFields(ctx, field) - case "traversals": - return ec.fieldContext_DataframeResourceHints_traversals(ctx, field) - } - return nil, fmt.Errorf("no field named %q was found under type DataframeResourceHints", field.Name) +func (ec *executionContext) _DataframeRecipeColumn_name(ctx context.Context, field graphql.CollectedField, obj *model.DataframeRecipeColumn) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_DataframeRecipeColumn_name(ctx, field) }, - } - return fc, nil + func(ctx context.Context) (any, error) { + return obj.Name, nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v string) graphql.Marshaler { + return ec.marshalNString2string(ctx, selections, v) + }, + true, + true, + ) +} +func (ec *executionContext) fieldContext_DataframeRecipeColumn_name(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + return graphql.NewScalarFieldContext("DataframeRecipeColumn", field, false, false, errors.New("field of type String does not have child fields")) } -func (ec *executionContext) _DataframeResourceHints_resourceType(ctx context.Context, field graphql.CollectedField, obj *model.DataframeResourceHints) (ret graphql.Marshaler) { - fc, err := ec.fieldContext_DataframeResourceHints_resourceType(ctx, field) - if err != nil { - return graphql.Null - } - ctx = graphql.WithFieldContext(ctx, fc) - defer func() { - if r := recover(); r != nil { - ec.Error(ctx, ec.Recover(ctx, r)) - ret = graphql.Null - } - }() - resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { - ctx = rctx // use context from middleware stack in children - return obj.ResourceType, nil - }) - if err != nil { - ec.Error(ctx, err) - return graphql.Null - } - if resTmp == nil { - if !graphql.HasFieldError(ctx, fc) { - ec.Errorf(ctx, "must not be null") - } - return graphql.Null - } - res := resTmp.(string) - fc.Result = res - return ec.marshalNString2string(ctx, field.Selections, res) +func (ec *executionContext) _DataframeRecipeColumn_logicalType(ctx context.Context, field graphql.CollectedField, obj *model.DataframeRecipeColumn) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_DataframeRecipeColumn_logicalType(ctx, field) + }, + func(ctx context.Context) (any, error) { + return obj.LogicalType, nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v string) graphql.Marshaler { + return ec.marshalNString2string(ctx, selections, v) + }, + true, + true, + ) +} +func (ec *executionContext) fieldContext_DataframeRecipeColumn_logicalType(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + return graphql.NewScalarFieldContext("DataframeRecipeColumn", field, false, false, errors.New("field of type String does not have child fields")) } -func (ec *executionContext) fieldContext_DataframeResourceHints_resourceType(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { - fc = &graphql.FieldContext{ - Object: "DataframeResourceHints", - Field: field, - IsMethod: false, - IsResolver: false, - Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { - return nil, errors.New("field of type String does not have child fields") +func (ec *executionContext) _DataframeRecipeColumn_repeated(ctx context.Context, field graphql.CollectedField, obj *model.DataframeRecipeColumn) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_DataframeRecipeColumn_repeated(ctx, field) }, - } - return fc, nil + func(ctx context.Context) (any, error) { + return obj.Repeated, nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v bool) graphql.Marshaler { + return ec.marshalNBoolean2bool(ctx, selections, v) + }, + true, + true, + ) +} +func (ec *executionContext) fieldContext_DataframeRecipeColumn_repeated(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + return graphql.NewScalarFieldContext("DataframeRecipeColumn", field, false, false, errors.New("field of type Boolean does not have child fields")) } -func (ec *executionContext) _DataframeResourceHints_fields(ctx context.Context, field graphql.CollectedField, obj *model.DataframeResourceHints) (ret graphql.Marshaler) { - fc, err := ec.fieldContext_DataframeResourceHints_fields(ctx, field) - if err != nil { - return graphql.Null - } - ctx = graphql.WithFieldContext(ctx, fc) - defer func() { - if r := recover(); r != nil { - ec.Error(ctx, ec.Recover(ctx, r)) - ret = graphql.Null - } - }() - resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { - ctx = rctx // use context from middleware stack in children - return obj.Fields, nil - }) - if err != nil { - ec.Error(ctx, err) - return graphql.Null - } - if resTmp == nil { - if !graphql.HasFieldError(ctx, fc) { - ec.Errorf(ctx, "must not be null") - } - return graphql.Null - } - res := resTmp.([]*model.DataframeFieldHint) - fc.Result = res - return ec.marshalNDataframeFieldHint2ᚕᚖgithubᚗcomᚋcalyprᚋloomᚋgraphqlapiᚋmodelᚐDataframeFieldHintᚄ(ctx, field.Selections, res) +func (ec *executionContext) _DataframeRecipeColumn_nullable(ctx context.Context, field graphql.CollectedField, obj *model.DataframeRecipeColumn) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_DataframeRecipeColumn_nullable(ctx, field) + }, + func(ctx context.Context) (any, error) { + return obj.Nullable, nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v bool) graphql.Marshaler { + return ec.marshalNBoolean2bool(ctx, selections, v) + }, + true, + true, + ) +} +func (ec *executionContext) fieldContext_DataframeRecipeColumn_nullable(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + return graphql.NewScalarFieldContext("DataframeRecipeColumn", field, false, false, errors.New("field of type Boolean does not have child fields")) } -func (ec *executionContext) fieldContext_DataframeResourceHints_fields(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { - fc = &graphql.FieldContext{ - Object: "DataframeResourceHints", - Field: field, - IsMethod: false, - IsResolver: false, - Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { - switch field.Name { - case "resourceType": - return ec.fieldContext_DataframeFieldHint_resourceType(ctx, field) - case "fieldRef": - return ec.fieldContext_DataframeFieldHint_fieldRef(ctx, field) - case "label": - return ec.fieldContext_DataframeFieldHint_label(ctx, field) - case "path": - return ec.fieldContext_DataframeFieldHint_path(ctx, field) - case "selector": - return ec.fieldContext_DataframeFieldHint_selector(ctx, field) - case "kind": - return ec.fieldContext_DataframeFieldHint_kind(ctx, field) - case "docCount": - return ec.fieldContext_DataframeFieldHint_docCount(ctx, field) - case "sampleCount": - return ec.fieldContext_DataframeFieldHint_sampleCount(ctx, field) - case "distinctValues": - return ec.fieldContext_DataframeFieldHint_distinctValues(ctx, field) - case "distinctTruncated": - return ec.fieldContext_DataframeFieldHint_distinctTruncated(ctx, field) - case "pivotCandidate": - return ec.fieldContext_DataframeFieldHint_pivotCandidate(ctx, field) - case "pivotKind": - return ec.fieldContext_DataframeFieldHint_pivotKind(ctx, field) - case "pivotColumns": - return ec.fieldContext_DataframeFieldHint_pivotColumns(ctx, field) - case "pivotFamily": - return ec.fieldContext_DataframeFieldHint_pivotFamily(ctx, field) - case "defaultPivotColumnSelector": - return ec.fieldContext_DataframeFieldHint_defaultPivotColumnSelector(ctx, field) - case "defaultPivotValueSelector": - return ec.fieldContext_DataframeFieldHint_defaultPivotValueSelector(ctx, field) - } - return nil, fmt.Errorf("no field named %q was found under type DataframeFieldHint", field.Name) +func (ec *executionContext) _DataframeRecipeExecution_id(ctx context.Context, field graphql.CollectedField, obj *model.DataframeRecipeExecution) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_DataframeRecipeExecution_id(ctx, field) }, - } - return fc, nil + func(ctx context.Context) (any, error) { + return obj.ID, nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v string) graphql.Marshaler { + return ec.marshalNID2string(ctx, selections, v) + }, + true, + true, + ) +} +func (ec *executionContext) fieldContext_DataframeRecipeExecution_id(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + return graphql.NewScalarFieldContext("DataframeRecipeExecution", field, false, false, errors.New("field of type ID does not have child fields")) } -func (ec *executionContext) _DataframeResourceHints_pivotFields(ctx context.Context, field graphql.CollectedField, obj *model.DataframeResourceHints) (ret graphql.Marshaler) { - fc, err := ec.fieldContext_DataframeResourceHints_pivotFields(ctx, field) - if err != nil { - return graphql.Null - } - ctx = graphql.WithFieldContext(ctx, fc) - defer func() { - if r := recover(); r != nil { - ec.Error(ctx, ec.Recover(ctx, r)) - ret = graphql.Null - } - }() - resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { - ctx = rctx // use context from middleware stack in children - return obj.PivotFields, nil - }) - if err != nil { - ec.Error(ctx, err) - return graphql.Null - } - if resTmp == nil { - if !graphql.HasFieldError(ctx, fc) { - ec.Errorf(ctx, "must not be null") - } - return graphql.Null - } - res := resTmp.([]*model.DataframeFieldHint) - fc.Result = res - return ec.marshalNDataframeFieldHint2ᚕᚖgithubᚗcomᚋcalyprᚋloomᚋgraphqlapiᚋmodelᚐDataframeFieldHintᚄ(ctx, field.Selections, res) +func (ec *executionContext) _DataframeRecipeExecution_name(ctx context.Context, field graphql.CollectedField, obj *model.DataframeRecipeExecution) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_DataframeRecipeExecution_name(ctx, field) + }, + func(ctx context.Context) (any, error) { + return obj.Name, nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v string) graphql.Marshaler { + return ec.marshalNString2string(ctx, selections, v) + }, + true, + true, + ) +} +func (ec *executionContext) fieldContext_DataframeRecipeExecution_name(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + return graphql.NewScalarFieldContext("DataframeRecipeExecution", field, false, false, errors.New("field of type String does not have child fields")) } -func (ec *executionContext) fieldContext_DataframeResourceHints_pivotFields(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { +func (ec *executionContext) _DataframeRecipeExecution_recipeDigest(ctx context.Context, field graphql.CollectedField, obj *model.DataframeRecipeExecution) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_DataframeRecipeExecution_recipeDigest(ctx, field) + }, + func(ctx context.Context) (any, error) { + return obj.RecipeDigest, nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v string) graphql.Marshaler { + return ec.marshalNString2string(ctx, selections, v) + }, + true, + true, + ) +} +func (ec *executionContext) fieldContext_DataframeRecipeExecution_recipeDigest(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + return graphql.NewScalarFieldContext("DataframeRecipeExecution", field, false, false, errors.New("field of type String does not have child fields")) +} + +func (ec *executionContext) _DataframeRecipeExecution_resolvedSchemaDigest(ctx context.Context, field graphql.CollectedField, obj *model.DataframeRecipeExecution) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_DataframeRecipeExecution_resolvedSchemaDigest(ctx, field) + }, + func(ctx context.Context) (any, error) { + return obj.ResolvedSchemaDigest, nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v string) graphql.Marshaler { + return ec.marshalNString2string(ctx, selections, v) + }, + true, + true, + ) +} +func (ec *executionContext) fieldContext_DataframeRecipeExecution_resolvedSchemaDigest(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + return graphql.NewScalarFieldContext("DataframeRecipeExecution", field, false, false, errors.New("field of type String does not have child fields")) +} + +func (ec *executionContext) _DataframeRecipeExecution_sourceGeneration(ctx context.Context, field graphql.CollectedField, obj *model.DataframeRecipeExecution) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_DataframeRecipeExecution_sourceGeneration(ctx, field) + }, + func(ctx context.Context) (any, error) { + return obj.SourceGeneration, nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v string) graphql.Marshaler { + return ec.marshalNString2string(ctx, selections, v) + }, + true, + true, + ) +} +func (ec *executionContext) fieldContext_DataframeRecipeExecution_sourceGeneration(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + return graphql.NewScalarFieldContext("DataframeRecipeExecution", field, false, false, errors.New("field of type String does not have child fields")) +} + +func (ec *executionContext) _DataframeRecipeExecution_state(ctx context.Context, field graphql.CollectedField, obj *model.DataframeRecipeExecution) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_DataframeRecipeExecution_state(ctx, field) + }, + func(ctx context.Context) (any, error) { + return obj.State, nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v model.DataframeRecipeExecutionState) graphql.Marshaler { + return ec.marshalNDataframeRecipeExecutionState2githubᚗcomᚋcalyprᚋloomᚋgraphqlapiᚋmodelᚐDataframeRecipeExecutionState(ctx, selections, v) + }, + true, + true, + ) +} +func (ec *executionContext) fieldContext_DataframeRecipeExecution_state(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + return graphql.NewScalarFieldContext("DataframeRecipeExecution", field, false, false, errors.New("field of type DataframeRecipeExecutionState does not have child fields")) +} + +func (ec *executionContext) _DataframeRecipeExecution_outputs(ctx context.Context, field graphql.CollectedField, obj *model.DataframeRecipeExecution) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_DataframeRecipeExecution_outputs(ctx, field) + }, + func(ctx context.Context) (any, error) { + return obj.Outputs, nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v []*model.DataframeRecipeExecutionOutput) graphql.Marshaler { + return ec.marshalNDataframeRecipeExecutionOutput2ᚕᚖgithubᚗcomᚋcalyprᚋloomᚋgraphqlapiᚋmodelᚐDataframeRecipeExecutionOutputᚄ(ctx, selections, v) + }, + true, + true, + ) +} +func (ec *executionContext) fieldContext_DataframeRecipeExecution_outputs(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { fc = &graphql.FieldContext{ - Object: "DataframeResourceHints", + Object: "DataframeRecipeExecution", Field: field, IsMethod: false, IsResolver: false, Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { - switch field.Name { - case "resourceType": - return ec.fieldContext_DataframeFieldHint_resourceType(ctx, field) - case "fieldRef": - return ec.fieldContext_DataframeFieldHint_fieldRef(ctx, field) - case "label": - return ec.fieldContext_DataframeFieldHint_label(ctx, field) - case "path": - return ec.fieldContext_DataframeFieldHint_path(ctx, field) - case "selector": - return ec.fieldContext_DataframeFieldHint_selector(ctx, field) - case "kind": - return ec.fieldContext_DataframeFieldHint_kind(ctx, field) - case "docCount": - return ec.fieldContext_DataframeFieldHint_docCount(ctx, field) - case "sampleCount": - return ec.fieldContext_DataframeFieldHint_sampleCount(ctx, field) - case "distinctValues": - return ec.fieldContext_DataframeFieldHint_distinctValues(ctx, field) - case "distinctTruncated": - return ec.fieldContext_DataframeFieldHint_distinctTruncated(ctx, field) - case "pivotCandidate": - return ec.fieldContext_DataframeFieldHint_pivotCandidate(ctx, field) - case "pivotKind": - return ec.fieldContext_DataframeFieldHint_pivotKind(ctx, field) - case "pivotColumns": - return ec.fieldContext_DataframeFieldHint_pivotColumns(ctx, field) - case "pivotFamily": - return ec.fieldContext_DataframeFieldHint_pivotFamily(ctx, field) - case "defaultPivotColumnSelector": - return ec.fieldContext_DataframeFieldHint_defaultPivotColumnSelector(ctx, field) - case "defaultPivotValueSelector": - return ec.fieldContext_DataframeFieldHint_defaultPivotValueSelector(ctx, field) - } - return nil, fmt.Errorf("no field named %q was found under type DataframeFieldHint", field.Name) + return ec.childFields_DataframeRecipeExecutionOutput(ctx, field) }, } return fc, nil } -func (ec *executionContext) _DataframeResourceHints_traversals(ctx context.Context, field graphql.CollectedField, obj *model.DataframeResourceHints) (ret graphql.Marshaler) { - fc, err := ec.fieldContext_DataframeResourceHints_traversals(ctx, field) - if err != nil { - return graphql.Null - } - ctx = graphql.WithFieldContext(ctx, fc) - defer func() { - if r := recover(); r != nil { - ec.Error(ctx, ec.Recover(ctx, r)) - ret = graphql.Null - } - }() - resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { - ctx = rctx // use context from middleware stack in children - return obj.Traversals, nil - }) - if err != nil { - ec.Error(ctx, err) - return graphql.Null - } - if resTmp == nil { - if !graphql.HasFieldError(ctx, fc) { - ec.Errorf(ctx, "must not be null") - } - return graphql.Null - } - res := resTmp.([]*model.DataframeTraversalHint) - fc.Result = res - return ec.marshalNDataframeTraversalHint2ᚕᚖgithubᚗcomᚋcalyprᚋloomᚋgraphqlapiᚋmodelᚐDataframeTraversalHintᚄ(ctx, field.Selections, res) +func (ec *executionContext) _DataframeRecipeExecution_error(ctx context.Context, field graphql.CollectedField, obj *model.DataframeRecipeExecution) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_DataframeRecipeExecution_error(ctx, field) + }, + func(ctx context.Context) (any, error) { + return obj.Error, nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v *string) graphql.Marshaler { + return ec.marshalOString2ᚖstring(ctx, selections, v) + }, + true, + false, + ) +} +func (ec *executionContext) fieldContext_DataframeRecipeExecution_error(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + return graphql.NewScalarFieldContext("DataframeRecipeExecution", field, false, false, errors.New("field of type String does not have child fields")) } -func (ec *executionContext) fieldContext_DataframeResourceHints_traversals(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { - fc = &graphql.FieldContext{ - Object: "DataframeResourceHints", - Field: field, - IsMethod: false, - IsResolver: false, - Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { - switch field.Name { - case "fromType": - return ec.fieldContext_DataframeTraversalHint_fromType(ctx, field) - case "label": - return ec.fieldContext_DataframeTraversalHint_label(ctx, field) - case "toType": - return ec.fieldContext_DataframeTraversalHint_toType(ctx, field) - case "edgeCount": - return ec.fieldContext_DataframeTraversalHint_edgeCount(ctx, field) - } - return nil, fmt.Errorf("no field named %q was found under type DataframeTraversalHint", field.Name) +func (ec *executionContext) _DataframeRecipeExecutionOutput_name(ctx context.Context, field graphql.CollectedField, obj *model.DataframeRecipeExecutionOutput) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_DataframeRecipeExecutionOutput_name(ctx, field) }, - } - return fc, nil + func(ctx context.Context) (any, error) { + return obj.Name, nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v string) graphql.Marshaler { + return ec.marshalNString2string(ctx, selections, v) + }, + true, + true, + ) +} +func (ec *executionContext) fieldContext_DataframeRecipeExecutionOutput_name(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + return graphql.NewScalarFieldContext("DataframeRecipeExecutionOutput", field, false, false, errors.New("field of type String does not have child fields")) } -func (ec *executionContext) _DataframeRichSourceReuse_sourceSet(ctx context.Context, field graphql.CollectedField, obj *model.DataframeRichSourceReuse) (ret graphql.Marshaler) { - fc, err := ec.fieldContext_DataframeRichSourceReuse_sourceSet(ctx, field) - if err != nil { - return graphql.Null - } - ctx = graphql.WithFieldContext(ctx, fc) - defer func() { - if r := recover(); r != nil { - ec.Error(ctx, ec.Recover(ctx, r)) - ret = graphql.Null - } - }() - resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { - ctx = rctx // use context from middleware stack in children - return obj.SourceSet, nil - }) - if err != nil { - ec.Error(ctx, err) - return graphql.Null - } - if resTmp == nil { - if !graphql.HasFieldError(ctx, fc) { - ec.Errorf(ctx, "must not be null") - } - return graphql.Null - } - res := resTmp.(string) - fc.Result = res - return ec.marshalNString2string(ctx, field.Selections, res) +func (ec *executionContext) _DataframeRecipeExecutionOutput_state(ctx context.Context, field graphql.CollectedField, obj *model.DataframeRecipeExecutionOutput) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_DataframeRecipeExecutionOutput_state(ctx, field) + }, + func(ctx context.Context) (any, error) { + return obj.State, nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v model.DataframeRecipeExecutionState) graphql.Marshaler { + return ec.marshalNDataframeRecipeExecutionState2githubᚗcomᚋcalyprᚋloomᚋgraphqlapiᚋmodelᚐDataframeRecipeExecutionState(ctx, selections, v) + }, + true, + true, + ) +} +func (ec *executionContext) fieldContext_DataframeRecipeExecutionOutput_state(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + return graphql.NewScalarFieldContext("DataframeRecipeExecutionOutput", field, false, false, errors.New("field of type DataframeRecipeExecutionState does not have child fields")) } -func (ec *executionContext) fieldContext_DataframeRichSourceReuse_sourceSet(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { - fc = &graphql.FieldContext{ - Object: "DataframeRichSourceReuse", - Field: field, - IsMethod: false, - IsResolver: false, - Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { - return nil, errors.New("field of type String does not have child fields") +func (ec *executionContext) _DataframeRecipeExecutionOutput_rowCount(ctx context.Context, field graphql.CollectedField, obj *model.DataframeRecipeExecutionOutput) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_DataframeRecipeExecutionOutput_rowCount(ctx, field) }, - } - return fc, nil + func(ctx context.Context) (any, error) { + return obj.RowCount, nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v *int) graphql.Marshaler { + return ec.marshalOInt2ᚖint(ctx, selections, v) + }, + true, + false, + ) +} +func (ec *executionContext) fieldContext_DataframeRecipeExecutionOutput_rowCount(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + return graphql.NewScalarFieldContext("DataframeRecipeExecutionOutput", field, false, false, errors.New("field of type Int does not have child fields")) } -func (ec *executionContext) _DataframeRichSourceReuse_aggregateConsumers(ctx context.Context, field graphql.CollectedField, obj *model.DataframeRichSourceReuse) (ret graphql.Marshaler) { - fc, err := ec.fieldContext_DataframeRichSourceReuse_aggregateConsumers(ctx, field) - if err != nil { - return graphql.Null - } - ctx = graphql.WithFieldContext(ctx, fc) - defer func() { - if r := recover(); r != nil { - ec.Error(ctx, ec.Recover(ctx, r)) - ret = graphql.Null - } - }() - resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { - ctx = rctx // use context from middleware stack in children - return obj.AggregateConsumers, nil - }) - if err != nil { - ec.Error(ctx, err) - return graphql.Null - } - if resTmp == nil { - if !graphql.HasFieldError(ctx, fc) { - ec.Errorf(ctx, "must not be null") - } - return graphql.Null - } - res := resTmp.(int) - fc.Result = res - return ec.marshalNInt2int(ctx, field.Selections, res) +func (ec *executionContext) _DataframeRecipeExecutionOutput_error(ctx context.Context, field graphql.CollectedField, obj *model.DataframeRecipeExecutionOutput) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_DataframeRecipeExecutionOutput_error(ctx, field) + }, + func(ctx context.Context) (any, error) { + return obj.Error, nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v *string) graphql.Marshaler { + return ec.marshalOString2ᚖstring(ctx, selections, v) + }, + true, + false, + ) +} +func (ec *executionContext) fieldContext_DataframeRecipeExecutionOutput_error(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + return graphql.NewScalarFieldContext("DataframeRecipeExecutionOutput", field, false, false, errors.New("field of type String does not have child fields")) } -func (ec *executionContext) fieldContext_DataframeRichSourceReuse_aggregateConsumers(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { - fc = &graphql.FieldContext{ - Object: "DataframeRichSourceReuse", - Field: field, - IsMethod: false, - IsResolver: false, - Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { - return nil, errors.New("field of type Int does not have child fields") +func (ec *executionContext) _DataframeRecipeExpansionExplanation_sourcePath(ctx context.Context, field graphql.CollectedField, obj *model.DataframeRecipeExpansionExplanation) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_DataframeRecipeExpansionExplanation_sourcePath(ctx, field) }, - } - return fc, nil + func(ctx context.Context) (any, error) { + return obj.SourcePath, nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v string) graphql.Marshaler { + return ec.marshalNString2string(ctx, selections, v) + }, + true, + true, + ) +} +func (ec *executionContext) fieldContext_DataframeRecipeExpansionExplanation_sourcePath(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + return graphql.NewScalarFieldContext("DataframeRecipeExpansionExplanation", field, false, false, errors.New("field of type String does not have child fields")) } -func (ec *executionContext) _DataframeRichSourceReuse_pivotConsumers(ctx context.Context, field graphql.CollectedField, obj *model.DataframeRichSourceReuse) (ret graphql.Marshaler) { - fc, err := ec.fieldContext_DataframeRichSourceReuse_pivotConsumers(ctx, field) - if err != nil { - return graphql.Null - } - ctx = graphql.WithFieldContext(ctx, fc) - defer func() { - if r := recover(); r != nil { - ec.Error(ctx, ec.Recover(ctx, r)) - ret = graphql.Null - } - }() - resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { - ctx = rctx // use context from middleware stack in children - return obj.PivotConsumers, nil - }) - if err != nil { - ec.Error(ctx, err) - return graphql.Null - } - if resTmp == nil { - if !graphql.HasFieldError(ctx, fc) { - ec.Errorf(ctx, "must not be null") - } - return graphql.Null - } - res := resTmp.(int) - fc.Result = res - return ec.marshalNInt2int(ctx, field.Selections, res) +func (ec *executionContext) _DataframeRecipeExpansionExplanation_alias(ctx context.Context, field graphql.CollectedField, obj *model.DataframeRecipeExpansionExplanation) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_DataframeRecipeExpansionExplanation_alias(ctx, field) + }, + func(ctx context.Context) (any, error) { + return obj.Alias, nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v string) graphql.Marshaler { + return ec.marshalNString2string(ctx, selections, v) + }, + true, + true, + ) +} +func (ec *executionContext) fieldContext_DataframeRecipeExpansionExplanation_alias(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + return graphql.NewScalarFieldContext("DataframeRecipeExpansionExplanation", field, false, false, errors.New("field of type String does not have child fields")) } -func (ec *executionContext) fieldContext_DataframeRichSourceReuse_pivotConsumers(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { - fc = &graphql.FieldContext{ - Object: "DataframeRichSourceReuse", - Field: field, - IsMethod: false, - IsResolver: false, - Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { - return nil, errors.New("field of type Int does not have child fields") +func (ec *executionContext) _DataframeRecipeExplainCollectionScan_plan(ctx context.Context, field graphql.CollectedField, obj *model.DataframeRecipeExplainCollectionScan) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_DataframeRecipeExplainCollectionScan_plan(ctx, field) }, - } - return fc, nil + func(ctx context.Context) (any, error) { + return obj.Plan, nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v int) graphql.Marshaler { + return ec.marshalNInt2int(ctx, selections, v) + }, + true, + true, + ) +} +func (ec *executionContext) fieldContext_DataframeRecipeExplainCollectionScan_plan(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + return graphql.NewScalarFieldContext("DataframeRecipeExplainCollectionScan", field, false, false, errors.New("field of type Int does not have child fields")) } -func (ec *executionContext) _DataframeRichSourceReuse_sliceConsumers(ctx context.Context, field graphql.CollectedField, obj *model.DataframeRichSourceReuse) (ret graphql.Marshaler) { - fc, err := ec.fieldContext_DataframeRichSourceReuse_sliceConsumers(ctx, field) - if err != nil { - return graphql.Null - } - ctx = graphql.WithFieldContext(ctx, fc) - defer func() { - if r := recover(); r != nil { - ec.Error(ctx, ec.Recover(ctx, r)) - ret = graphql.Null - } - }() - resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { - ctx = rctx // use context from middleware stack in children - return obj.SliceConsumers, nil - }) - if err != nil { - ec.Error(ctx, err) - return graphql.Null - } - if resTmp == nil { - if !graphql.HasFieldError(ctx, fc) { - ec.Errorf(ctx, "must not be null") - } - return graphql.Null - } - res := resTmp.(int) - fc.Result = res - return ec.marshalNInt2int(ctx, field.Selections, res) +func (ec *executionContext) _DataframeRecipeExplainCollectionScan_nodeId(ctx context.Context, field graphql.CollectedField, obj *model.DataframeRecipeExplainCollectionScan) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_DataframeRecipeExplainCollectionScan_nodeId(ctx, field) + }, + func(ctx context.Context) (any, error) { + return obj.NodeID, nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v int) graphql.Marshaler { + return ec.marshalNInt2int(ctx, selections, v) + }, + true, + true, + ) +} +func (ec *executionContext) fieldContext_DataframeRecipeExplainCollectionScan_nodeId(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + return graphql.NewScalarFieldContext("DataframeRecipeExplainCollectionScan", field, false, false, errors.New("field of type Int does not have child fields")) } -func (ec *executionContext) fieldContext_DataframeRichSourceReuse_sliceConsumers(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { - fc = &graphql.FieldContext{ - Object: "DataframeRichSourceReuse", - Field: field, - IsMethod: false, - IsResolver: false, - Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { - return nil, errors.New("field of type Int does not have child fields") +func (ec *executionContext) _DataframeRecipeExplainCollectionScan_collection(ctx context.Context, field graphql.CollectedField, obj *model.DataframeRecipeExplainCollectionScan) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_DataframeRecipeExplainCollectionScan_collection(ctx, field) }, - } - return fc, nil + func(ctx context.Context) (any, error) { + return obj.Collection, nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v string) graphql.Marshaler { + return ec.marshalNString2string(ctx, selections, v) + }, + true, + true, + ) +} +func (ec *executionContext) fieldContext_DataframeRecipeExplainCollectionScan_collection(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + return graphql.NewScalarFieldContext("DataframeRecipeExplainCollectionScan", field, false, false, errors.New("field of type String does not have child fields")) } -func (ec *executionContext) _DataframeRichSourceReuse_totalConsumers(ctx context.Context, field graphql.CollectedField, obj *model.DataframeRichSourceReuse) (ret graphql.Marshaler) { - fc, err := ec.fieldContext_DataframeRichSourceReuse_totalConsumers(ctx, field) - if err != nil { - return graphql.Null - } - ctx = graphql.WithFieldContext(ctx, fc) - defer func() { - if r := recover(); r != nil { - ec.Error(ctx, ec.Recover(ctx, r)) - ret = graphql.Null - } - }() - resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { - ctx = rctx // use context from middleware stack in children - return obj.TotalConsumers, nil - }) - if err != nil { - ec.Error(ctx, err) - return graphql.Null - } - if resTmp == nil { - if !graphql.HasFieldError(ctx, fc) { - ec.Errorf(ctx, "must not be null") - } - return graphql.Null - } - res := resTmp.(int) - fc.Result = res - return ec.marshalNInt2int(ctx, field.Selections, res) +func (ec *executionContext) _DataframeRecipeExplainIndexLocation_plan(ctx context.Context, field graphql.CollectedField, obj *model.DataframeRecipeExplainIndexLocation) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_DataframeRecipeExplainIndexLocation_plan(ctx, field) + }, + func(ctx context.Context) (any, error) { + return obj.Plan, nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v int) graphql.Marshaler { + return ec.marshalNInt2int(ctx, selections, v) + }, + true, + true, + ) +} +func (ec *executionContext) fieldContext_DataframeRecipeExplainIndexLocation_plan(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + return graphql.NewScalarFieldContext("DataframeRecipeExplainIndexLocation", field, false, false, errors.New("field of type Int does not have child fields")) } -func (ec *executionContext) fieldContext_DataframeRichSourceReuse_totalConsumers(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { - fc = &graphql.FieldContext{ - Object: "DataframeRichSourceReuse", - Field: field, - IsMethod: false, - IsResolver: false, - Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { - return nil, errors.New("field of type Int does not have child fields") +func (ec *executionContext) _DataframeRecipeExplainIndexLocation_nodeId(ctx context.Context, field graphql.CollectedField, obj *model.DataframeRecipeExplainIndexLocation) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_DataframeRecipeExplainIndexLocation_nodeId(ctx, field) }, - } - return fc, nil + func(ctx context.Context) (any, error) { + return obj.NodeID, nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v int) graphql.Marshaler { + return ec.marshalNInt2int(ctx, selections, v) + }, + true, + true, + ) +} +func (ec *executionContext) fieldContext_DataframeRecipeExplainIndexLocation_nodeId(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + return graphql.NewScalarFieldContext("DataframeRecipeExplainIndexLocation", field, false, false, errors.New("field of type Int does not have child fields")) } -func (ec *executionContext) _DataframeRowConnection_materialization(ctx context.Context, field graphql.CollectedField, obj *model.DataframeRowConnection) (ret graphql.Marshaler) { - fc, err := ec.fieldContext_DataframeRowConnection_materialization(ctx, field) - if err != nil { - return graphql.Null - } - ctx = graphql.WithFieldContext(ctx, fc) - defer func() { - if r := recover(); r != nil { - ec.Error(ctx, ec.Recover(ctx, r)) - ret = graphql.Null - } - }() - resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { - ctx = rctx // use context from middleware stack in children - return obj.Materialization, nil - }) - if err != nil { - ec.Error(ctx, err) - return graphql.Null - } - if resTmp == nil { - if !graphql.HasFieldError(ctx, fc) { - ec.Errorf(ctx, "must not be null") - } - return graphql.Null - } - res := resTmp.(*model.DataframeMaterialization) - fc.Result = res - return ec.marshalNDataframeMaterialization2ᚖgithubᚗcomᚋcalyprᚋloomᚋgraphqlapiᚋmodelᚐDataframeMaterialization(ctx, field.Selections, res) +func (ec *executionContext) _DataframeRecipeExplainIndexSummary_collection(ctx context.Context, field graphql.CollectedField, obj *model.DataframeRecipeExplainIndexSummary) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_DataframeRecipeExplainIndexSummary_collection(ctx, field) + }, + func(ctx context.Context) (any, error) { + return obj.Collection, nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v string) graphql.Marshaler { + return ec.marshalNString2string(ctx, selections, v) + }, + true, + true, + ) +} +func (ec *executionContext) fieldContext_DataframeRecipeExplainIndexSummary_collection(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + return graphql.NewScalarFieldContext("DataframeRecipeExplainIndexSummary", field, false, false, errors.New("field of type String does not have child fields")) } -func (ec *executionContext) fieldContext_DataframeRowConnection_materialization(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { +func (ec *executionContext) _DataframeRecipeExplainIndexSummary_id(ctx context.Context, field graphql.CollectedField, obj *model.DataframeRecipeExplainIndexSummary) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_DataframeRecipeExplainIndexSummary_id(ctx, field) + }, + func(ctx context.Context) (any, error) { + return obj.ID, nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v string) graphql.Marshaler { + return ec.marshalNString2string(ctx, selections, v) + }, + true, + true, + ) +} +func (ec *executionContext) fieldContext_DataframeRecipeExplainIndexSummary_id(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + return graphql.NewScalarFieldContext("DataframeRecipeExplainIndexSummary", field, false, false, errors.New("field of type String does not have child fields")) +} + +func (ec *executionContext) _DataframeRecipeExplainIndexSummary_name(ctx context.Context, field graphql.CollectedField, obj *model.DataframeRecipeExplainIndexSummary) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_DataframeRecipeExplainIndexSummary_name(ctx, field) + }, + func(ctx context.Context) (any, error) { + return obj.Name, nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v string) graphql.Marshaler { + return ec.marshalNString2string(ctx, selections, v) + }, + true, + true, + ) +} +func (ec *executionContext) fieldContext_DataframeRecipeExplainIndexSummary_name(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + return graphql.NewScalarFieldContext("DataframeRecipeExplainIndexSummary", field, false, false, errors.New("field of type String does not have child fields")) +} + +func (ec *executionContext) _DataframeRecipeExplainIndexSummary_type(ctx context.Context, field graphql.CollectedField, obj *model.DataframeRecipeExplainIndexSummary) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_DataframeRecipeExplainIndexSummary_type(ctx, field) + }, + func(ctx context.Context) (any, error) { + return obj.Type, nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v string) graphql.Marshaler { + return ec.marshalNString2string(ctx, selections, v) + }, + true, + true, + ) +} +func (ec *executionContext) fieldContext_DataframeRecipeExplainIndexSummary_type(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + return graphql.NewScalarFieldContext("DataframeRecipeExplainIndexSummary", field, false, false, errors.New("field of type String does not have child fields")) +} + +func (ec *executionContext) _DataframeRecipeExplainIndexSummary_fields(ctx context.Context, field graphql.CollectedField, obj *model.DataframeRecipeExplainIndexSummary) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_DataframeRecipeExplainIndexSummary_fields(ctx, field) + }, + func(ctx context.Context) (any, error) { + return obj.Fields, nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v []string) graphql.Marshaler { + return ec.marshalNString2ᚕstringᚄ(ctx, selections, v) + }, + true, + true, + ) +} +func (ec *executionContext) fieldContext_DataframeRecipeExplainIndexSummary_fields(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + return graphql.NewScalarFieldContext("DataframeRecipeExplainIndexSummary", field, false, false, errors.New("field of type String does not have child fields")) +} + +func (ec *executionContext) _DataframeRecipeExplainIndexSummary_uses(ctx context.Context, field graphql.CollectedField, obj *model.DataframeRecipeExplainIndexSummary) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_DataframeRecipeExplainIndexSummary_uses(ctx, field) + }, + func(ctx context.Context) (any, error) { + return obj.Uses, nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v []*model.DataframeRecipeExplainIndexLocation) graphql.Marshaler { + return ec.marshalNDataframeRecipeExplainIndexLocation2ᚕᚖgithubᚗcomᚋcalyprᚋloomᚋgraphqlapiᚋmodelᚐDataframeRecipeExplainIndexLocationᚄ(ctx, selections, v) + }, + true, + true, + ) +} +func (ec *executionContext) fieldContext_DataframeRecipeExplainIndexSummary_uses(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { fc = &graphql.FieldContext{ - Object: "DataframeRowConnection", + Object: "DataframeRecipeExplainIndexSummary", Field: field, IsMethod: false, IsResolver: false, Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { - switch field.Name { - case "id": - return ec.fieldContext_DataframeMaterialization_id(ctx, field) - case "name": - return ec.fieldContext_DataframeMaterialization_name(ctx, field) - case "project": - return ec.fieldContext_DataframeMaterialization_project(ctx, field) - case "datasetGeneration": - return ec.fieldContext_DataframeMaterialization_datasetGeneration(ctx, field) - case "state": - return ec.fieldContext_DataframeMaterialization_state(ctx, field) - case "columns": - return ec.fieldContext_DataframeMaterialization_columns(ctx, field) - case "rowCount": - return ec.fieldContext_DataframeMaterialization_rowCount(ctx, field) - case "createdAt": - return ec.fieldContext_DataframeMaterialization_createdAt(ctx, field) - case "readyAt": - return ec.fieldContext_DataframeMaterialization_readyAt(ctx, field) - case "error": - return ec.fieldContext_DataframeMaterialization_error(ctx, field) - } - return nil, fmt.Errorf("no field named %q was found under type DataframeMaterialization", field.Name) + return ec.childFields_DataframeRecipeExplainIndexLocation(ctx, field) }, } return fc, nil } -func (ec *executionContext) _DataframeRowConnection_columns(ctx context.Context, field graphql.CollectedField, obj *model.DataframeRowConnection) (ret graphql.Marshaler) { - fc, err := ec.fieldContext_DataframeRowConnection_columns(ctx, field) - if err != nil { - return graphql.Null - } - ctx = graphql.WithFieldContext(ctx, fc) - defer func() { - if r := recover(); r != nil { - ec.Error(ctx, ec.Recover(ctx, r)) - ret = graphql.Null - } - }() - resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { - ctx = rctx // use context from middleware stack in children - return obj.Columns, nil - }) - if err != nil { - ec.Error(ctx, err) - return graphql.Null - } - if resTmp == nil { - if !graphql.HasFieldError(ctx, fc) { - ec.Errorf(ctx, "must not be null") - } - return graphql.Null - } - res := resTmp.([]string) - fc.Result = res - return ec.marshalNString2ᚕstringᚄ(ctx, field.Selections, res) +func (ec *executionContext) _DataframeRecipeExplainPlanEstimate_plan(ctx context.Context, field graphql.CollectedField, obj *model.DataframeRecipeExplainPlanEstimate) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_DataframeRecipeExplainPlanEstimate_plan(ctx, field) + }, + func(ctx context.Context) (any, error) { + return obj.Plan, nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v int) graphql.Marshaler { + return ec.marshalNInt2int(ctx, selections, v) + }, + true, + true, + ) +} +func (ec *executionContext) fieldContext_DataframeRecipeExplainPlanEstimate_plan(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + return graphql.NewScalarFieldContext("DataframeRecipeExplainPlanEstimate", field, false, false, errors.New("field of type Int does not have child fields")) } -func (ec *executionContext) fieldContext_DataframeRowConnection_columns(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { - fc = &graphql.FieldContext{ - Object: "DataframeRowConnection", - Field: field, - IsMethod: false, - IsResolver: false, - Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { - return nil, errors.New("field of type String does not have child fields") +func (ec *executionContext) _DataframeRecipeExplainPlanEstimate_estimatedCost(ctx context.Context, field graphql.CollectedField, obj *model.DataframeRecipeExplainPlanEstimate) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_DataframeRecipeExplainPlanEstimate_estimatedCost(ctx, field) }, - } - return fc, nil + func(ctx context.Context) (any, error) { + return obj.EstimatedCost, nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v float64) graphql.Marshaler { + return ec.marshalNFloat2float64(ctx, selections, v) + }, + true, + true, + ) } - -func (ec *executionContext) _DataframeRowConnection_rows(ctx context.Context, field graphql.CollectedField, obj *model.DataframeRowConnection) (ret graphql.Marshaler) { - fc, err := ec.fieldContext_DataframeRowConnection_rows(ctx, field) - if err != nil { - return graphql.Null - } - ctx = graphql.WithFieldContext(ctx, fc) - defer func() { - if r := recover(); r != nil { - ec.Error(ctx, ec.Recover(ctx, r)) - ret = graphql.Null - } - }() - resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { - ctx = rctx // use context from middleware stack in children - return obj.Rows, nil - }) - if err != nil { - ec.Error(ctx, err) - return graphql.Null - } - if resTmp == nil { - if !graphql.HasFieldError(ctx, fc) { - ec.Errorf(ctx, "must not be null") - } - return graphql.Null - } - res := resTmp.(json.RawMessage) - fc.Result = res - return ec.marshalNJSON2encodingᚋjsonᚐRawMessage(ctx, field.Selections, res) +func (ec *executionContext) fieldContext_DataframeRecipeExplainPlanEstimate_estimatedCost(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + return graphql.NewScalarFieldContext("DataframeRecipeExplainPlanEstimate", field, false, false, errors.New("field of type Float does not have child fields")) } -func (ec *executionContext) fieldContext_DataframeRowConnection_rows(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { - fc = &graphql.FieldContext{ - Object: "DataframeRowConnection", - Field: field, - IsMethod: false, - IsResolver: false, - Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { - return nil, errors.New("field of type JSON does not have child fields") +func (ec *executionContext) _DataframeRecipeExplainPlanEstimate_estimatedNrItems(ctx context.Context, field graphql.CollectedField, obj *model.DataframeRecipeExplainPlanEstimate) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_DataframeRecipeExplainPlanEstimate_estimatedNrItems(ctx, field) }, - } - return fc, nil + func(ctx context.Context) (any, error) { + return obj.EstimatedNrItems, nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v float64) graphql.Marshaler { + return ec.marshalNFloat2float64(ctx, selections, v) + }, + true, + true, + ) +} +func (ec *executionContext) fieldContext_DataframeRecipeExplainPlanEstimate_estimatedNrItems(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + return graphql.NewScalarFieldContext("DataframeRecipeExplainPlanEstimate", field, false, false, errors.New("field of type Float does not have child fields")) } -func (ec *executionContext) _DataframeRowConnection_pageInfo(ctx context.Context, field graphql.CollectedField, obj *model.DataframeRowConnection) (ret graphql.Marshaler) { - fc, err := ec.fieldContext_DataframeRowConnection_pageInfo(ctx, field) - if err != nil { - return graphql.Null - } - ctx = graphql.WithFieldContext(ctx, fc) - defer func() { - if r := recover(); r != nil { - ec.Error(ctx, ec.Recover(ctx, r)) - ret = graphql.Null - } - }() - resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { - ctx = rctx // use context from middleware stack in children - return obj.PageInfo, nil - }) - if err != nil { - ec.Error(ctx, err) - return graphql.Null - } - if resTmp == nil { - if !graphql.HasFieldError(ctx, fc) { - ec.Errorf(ctx, "must not be null") - } - return graphql.Null - } - res := resTmp.(*model.DataframePageInfo) - fc.Result = res - return ec.marshalNDataframePageInfo2ᚖgithubᚗcomᚋcalyprᚋloomᚋgraphqlapiᚋmodelᚐDataframePageInfo(ctx, field.Selections, res) +func (ec *executionContext) _DataframeRecipeExplainWarning_code(ctx context.Context, field graphql.CollectedField, obj *model.DataframeRecipeExplainWarning) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_DataframeRecipeExplainWarning_code(ctx, field) + }, + func(ctx context.Context) (any, error) { + return obj.Code, nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v int) graphql.Marshaler { + return ec.marshalNInt2int(ctx, selections, v) + }, + true, + true, + ) +} +func (ec *executionContext) fieldContext_DataframeRecipeExplainWarning_code(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + return graphql.NewScalarFieldContext("DataframeRecipeExplainWarning", field, false, false, errors.New("field of type Int does not have child fields")) } -func (ec *executionContext) fieldContext_DataframeRowConnection_pageInfo(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { - fc = &graphql.FieldContext{ - Object: "DataframeRowConnection", - Field: field, - IsMethod: false, - IsResolver: false, - Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { - switch field.Name { - case "hasNextPage": - return ec.fieldContext_DataframePageInfo_hasNextPage(ctx, field) - case "endCursor": - return ec.fieldContext_DataframePageInfo_endCursor(ctx, field) - } - return nil, fmt.Errorf("no field named %q was found under type DataframePageInfo", field.Name) +func (ec *executionContext) _DataframeRecipeExplainWarning_message(ctx context.Context, field graphql.CollectedField, obj *model.DataframeRecipeExplainWarning) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_DataframeRecipeExplainWarning_message(ctx, field) }, - } - return fc, nil + func(ctx context.Context) (any, error) { + return obj.Message, nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v string) graphql.Marshaler { + return ec.marshalNString2string(ctx, selections, v) + }, + true, + true, + ) +} +func (ec *executionContext) fieldContext_DataframeRecipeExplainWarning_message(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + return graphql.NewScalarFieldContext("DataframeRecipeExplainWarning", field, false, false, errors.New("field of type String does not have child fields")) } -func (ec *executionContext) _DataframeTraversalHint_fromType(ctx context.Context, field graphql.CollectedField, obj *model.DataframeTraversalHint) (ret graphql.Marshaler) { - fc, err := ec.fieldContext_DataframeTraversalHint_fromType(ctx, field) - if err != nil { - return graphql.Null - } - ctx = graphql.WithFieldContext(ctx, fc) - defer func() { - if r := recover(); r != nil { - ec.Error(ctx, ec.Recover(ctx, r)) - ret = graphql.Null - } - }() - resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { - ctx = rctx // use context from middleware stack in children - return obj.FromType, nil - }) - if err != nil { - ec.Error(ctx, err) - return graphql.Null - } - if resTmp == nil { - if !graphql.HasFieldError(ctx, fc) { - ec.Errorf(ctx, "must not be null") - } - return graphql.Null - } - res := resTmp.(string) - fc.Result = res - return ec.marshalNString2string(ctx, field.Selections, res) +func (ec *executionContext) _DataframeRecipeExplanation_name(ctx context.Context, field graphql.CollectedField, obj *model.DataframeRecipeExplanation) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_DataframeRecipeExplanation_name(ctx, field) + }, + func(ctx context.Context) (any, error) { + return obj.Name, nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v string) graphql.Marshaler { + return ec.marshalNString2string(ctx, selections, v) + }, + true, + true, + ) +} +func (ec *executionContext) fieldContext_DataframeRecipeExplanation_name(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + return graphql.NewScalarFieldContext("DataframeRecipeExplanation", field, false, false, errors.New("field of type String does not have child fields")) } -func (ec *executionContext) fieldContext_DataframeTraversalHint_fromType(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { - fc = &graphql.FieldContext{ - Object: "DataframeTraversalHint", - Field: field, - IsMethod: false, - IsResolver: false, - Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { - return nil, errors.New("field of type String does not have child fields") +func (ec *executionContext) _DataframeRecipeExplanation_recipeDigest(ctx context.Context, field graphql.CollectedField, obj *model.DataframeRecipeExplanation) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_DataframeRecipeExplanation_recipeDigest(ctx, field) }, - } - return fc, nil + func(ctx context.Context) (any, error) { + return obj.RecipeDigest, nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v string) graphql.Marshaler { + return ec.marshalNString2string(ctx, selections, v) + }, + true, + true, + ) +} +func (ec *executionContext) fieldContext_DataframeRecipeExplanation_recipeDigest(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + return graphql.NewScalarFieldContext("DataframeRecipeExplanation", field, false, false, errors.New("field of type String does not have child fields")) } -func (ec *executionContext) _DataframeTraversalHint_label(ctx context.Context, field graphql.CollectedField, obj *model.DataframeTraversalHint) (ret graphql.Marshaler) { - fc, err := ec.fieldContext_DataframeTraversalHint_label(ctx, field) - if err != nil { - return graphql.Null - } - ctx = graphql.WithFieldContext(ctx, fc) - defer func() { - if r := recover(); r != nil { - ec.Error(ctx, ec.Recover(ctx, r)) - ret = graphql.Null - } - }() - resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { - ctx = rctx // use context from middleware stack in children - return obj.Label, nil - }) - if err != nil { - ec.Error(ctx, err) - return graphql.Null - } - if resTmp == nil { - if !graphql.HasFieldError(ctx, fc) { - ec.Errorf(ctx, "must not be null") - } - return graphql.Null - } - res := resTmp.(string) - fc.Result = res - return ec.marshalNString2string(ctx, field.Selections, res) +func (ec *executionContext) _DataframeRecipeExplanation_translationVersion(ctx context.Context, field graphql.CollectedField, obj *model.DataframeRecipeExplanation) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_DataframeRecipeExplanation_translationVersion(ctx, field) + }, + func(ctx context.Context) (any, error) { + return obj.TranslationVersion, nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v string) graphql.Marshaler { + return ec.marshalNString2string(ctx, selections, v) + }, + true, + true, + ) +} +func (ec *executionContext) fieldContext_DataframeRecipeExplanation_translationVersion(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + return graphql.NewScalarFieldContext("DataframeRecipeExplanation", field, false, false, errors.New("field of type String does not have child fields")) } -func (ec *executionContext) fieldContext_DataframeTraversalHint_label(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { +func (ec *executionContext) _DataframeRecipeExplanation_outputs(ctx context.Context, field graphql.CollectedField, obj *model.DataframeRecipeExplanation) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_DataframeRecipeExplanation_outputs(ctx, field) + }, + func(ctx context.Context) (any, error) { + return obj.Outputs, nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v []*model.DataframeRecipeOutputExplanation) graphql.Marshaler { + return ec.marshalNDataframeRecipeOutputExplanation2ᚕᚖgithubᚗcomᚋcalyprᚋloomᚋgraphqlapiᚋmodelᚐDataframeRecipeOutputExplanationᚄ(ctx, selections, v) + }, + true, + true, + ) +} +func (ec *executionContext) fieldContext_DataframeRecipeExplanation_outputs(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { fc = &graphql.FieldContext{ - Object: "DataframeTraversalHint", + Object: "DataframeRecipeExplanation", Field: field, IsMethod: false, IsResolver: false, Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { - return nil, errors.New("field of type String does not have child fields") + return ec.childFields_DataframeRecipeOutputExplanation(ctx, field) }, } return fc, nil } -func (ec *executionContext) _DataframeTraversalHint_toType(ctx context.Context, field graphql.CollectedField, obj *model.DataframeTraversalHint) (ret graphql.Marshaler) { - fc, err := ec.fieldContext_DataframeTraversalHint_toType(ctx, field) - if err != nil { - return graphql.Null - } - ctx = graphql.WithFieldContext(ctx, fc) - defer func() { - if r := recover(); r != nil { - ec.Error(ctx, ec.Recover(ctx, r)) - ret = graphql.Null - } - }() - resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { - ctx = rctx // use context from middleware stack in children - return obj.ToType, nil - }) - if err != nil { - ec.Error(ctx, err) - return graphql.Null - } - if resTmp == nil { - if !graphql.HasFieldError(ctx, fc) { - ec.Errorf(ctx, "must not be null") - } - return graphql.Null - } - res := resTmp.(string) - fc.Result = res - return ec.marshalNString2string(ctx, field.Selections, res) +func (ec *executionContext) _DataframeRecipeExplanation_physical(ctx context.Context, field graphql.CollectedField, obj *model.DataframeRecipeExplanation) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_DataframeRecipeExplanation_physical(ctx, field) + }, + func(ctx context.Context) (any, error) { + return obj.Physical, nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v *model.DataframeRecipePhysicalExplanation) graphql.Marshaler { + return ec.marshalODataframeRecipePhysicalExplanation2ᚖgithubᚗcomᚋcalyprᚋloomᚋgraphqlapiᚋmodelᚐDataframeRecipePhysicalExplanation(ctx, selections, v) + }, + true, + false, + ) } - -func (ec *executionContext) fieldContext_DataframeTraversalHint_toType(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { +func (ec *executionContext) fieldContext_DataframeRecipeExplanation_physical(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { fc = &graphql.FieldContext{ - Object: "DataframeTraversalHint", + Object: "DataframeRecipeExplanation", Field: field, IsMethod: false, IsResolver: false, Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { - return nil, errors.New("field of type String does not have child fields") + return ec.childFields_DataframeRecipePhysicalExplanation(ctx, field) }, } return fc, nil } -func (ec *executionContext) _DataframeTraversalHint_edgeCount(ctx context.Context, field graphql.CollectedField, obj *model.DataframeTraversalHint) (ret graphql.Marshaler) { - fc, err := ec.fieldContext_DataframeTraversalHint_edgeCount(ctx, field) - if err != nil { - return graphql.Null - } - ctx = graphql.WithFieldContext(ctx, fc) - defer func() { - if r := recover(); r != nil { - ec.Error(ctx, ec.Recover(ctx, r)) - ret = graphql.Null - } - }() - resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { - ctx = rctx // use context from middleware stack in children - return obj.EdgeCount, nil - }) - if err != nil { - ec.Error(ctx, err) - return graphql.Null - } - if resTmp == nil { - if !graphql.HasFieldError(ctx, fc) { - ec.Errorf(ctx, "must not be null") - } - return graphql.Null - } - res := resTmp.(int) - fc.Result = res - return ec.marshalNInt2int(ctx, field.Selections, res) +func (ec *executionContext) _DataframeRecipeExpressionExplanation_sourcePath(ctx context.Context, field graphql.CollectedField, obj *model.DataframeRecipeExpressionExplanation) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_DataframeRecipeExpressionExplanation_sourcePath(ctx, field) + }, + func(ctx context.Context) (any, error) { + return obj.SourcePath, nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v string) graphql.Marshaler { + return ec.marshalNString2string(ctx, selections, v) + }, + true, + true, + ) +} +func (ec *executionContext) fieldContext_DataframeRecipeExpressionExplanation_sourcePath(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + return graphql.NewScalarFieldContext("DataframeRecipeExpressionExplanation", field, false, false, errors.New("field of type String does not have child fields")) } -func (ec *executionContext) fieldContext_DataframeTraversalHint_edgeCount(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { - fc = &graphql.FieldContext{ - Object: "DataframeTraversalHint", - Field: field, - IsMethod: false, - IsResolver: false, - Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { - return nil, errors.New("field of type Int does not have child fields") +func (ec *executionContext) _DataframeRecipeExpressionExplanation_context(ctx context.Context, field graphql.CollectedField, obj *model.DataframeRecipeExpressionExplanation) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_DataframeRecipeExpressionExplanation_context(ctx, field) }, - } - return fc, nil + func(ctx context.Context) (any, error) { + return obj.Context, nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v string) graphql.Marshaler { + return ec.marshalNString2string(ctx, selections, v) + }, + true, + true, + ) +} +func (ec *executionContext) fieldContext_DataframeRecipeExpressionExplanation_context(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + return graphql.NewScalarFieldContext("DataframeRecipeExpressionExplanation", field, false, false, errors.New("field of type String does not have child fields")) } -func (ec *executionContext) _FhirDataframeResult_columns(ctx context.Context, field graphql.CollectedField, obj *model.FhirDataframeResult) (ret graphql.Marshaler) { - fc, err := ec.fieldContext_FhirDataframeResult_columns(ctx, field) - if err != nil { - return graphql.Null - } - ctx = graphql.WithFieldContext(ctx, fc) - defer func() { - if r := recover(); r != nil { - ec.Error(ctx, ec.Recover(ctx, r)) - ret = graphql.Null - } - }() - resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { - ctx = rctx // use context from middleware stack in children - return obj.Columns, nil - }) - if err != nil { - ec.Error(ctx, err) - return graphql.Null - } - if resTmp == nil { - if !graphql.HasFieldError(ctx, fc) { - ec.Errorf(ctx, "must not be null") - } - return graphql.Null - } - res := resTmp.([]string) - fc.Result = res - return ec.marshalNString2ᚕstringᚄ(ctx, field.Selections, res) +func (ec *executionContext) _DataframeRecipeExpressionExplanation_kind(ctx context.Context, field graphql.CollectedField, obj *model.DataframeRecipeExpressionExplanation) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_DataframeRecipeExpressionExplanation_kind(ctx, field) + }, + func(ctx context.Context) (any, error) { + return obj.Kind, nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v string) graphql.Marshaler { + return ec.marshalNString2string(ctx, selections, v) + }, + true, + true, + ) +} +func (ec *executionContext) fieldContext_DataframeRecipeExpressionExplanation_kind(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + return graphql.NewScalarFieldContext("DataframeRecipeExpressionExplanation", field, false, false, errors.New("field of type String does not have child fields")) } -func (ec *executionContext) fieldContext_FhirDataframeResult_columns(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { - fc = &graphql.FieldContext{ - Object: "FhirDataframeResult", - Field: field, - IsMethod: false, - IsResolver: false, - Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { - return nil, errors.New("field of type String does not have child fields") +func (ec *executionContext) _DataframeRecipeExpressionExplanation_valueType(ctx context.Context, field graphql.CollectedField, obj *model.DataframeRecipeExpressionExplanation) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_DataframeRecipeExpressionExplanation_valueType(ctx, field) }, - } - return fc, nil + func(ctx context.Context) (any, error) { + return obj.ValueType, nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v string) graphql.Marshaler { + return ec.marshalNString2string(ctx, selections, v) + }, + true, + true, + ) +} +func (ec *executionContext) fieldContext_DataframeRecipeExpressionExplanation_valueType(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + return graphql.NewScalarFieldContext("DataframeRecipeExpressionExplanation", field, false, false, errors.New("field of type String does not have child fields")) } -func (ec *executionContext) _FhirDataframeResult_rows(ctx context.Context, field graphql.CollectedField, obj *model.FhirDataframeResult) (ret graphql.Marshaler) { - fc, err := ec.fieldContext_FhirDataframeResult_rows(ctx, field) - if err != nil { - return graphql.Null - } - ctx = graphql.WithFieldContext(ctx, fc) - defer func() { - if r := recover(); r != nil { - ec.Error(ctx, ec.Recover(ctx, r)) - ret = graphql.Null - } - }() - resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { - ctx = rctx // use context from middleware stack in children - return obj.Rows, nil - }) - if err != nil { - ec.Error(ctx, err) - return graphql.Null - } - if resTmp == nil { - if !graphql.HasFieldError(ctx, fc) { - ec.Errorf(ctx, "must not be null") - } - return graphql.Null - } - res := resTmp.(json.RawMessage) - fc.Result = res - return ec.marshalNJSON2encodingᚋjsonᚐRawMessage(ctx, field.Selections, res) +func (ec *executionContext) _DataframeRecipeExpressionExplanation_repeated(ctx context.Context, field graphql.CollectedField, obj *model.DataframeRecipeExpressionExplanation) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_DataframeRecipeExpressionExplanation_repeated(ctx, field) + }, + func(ctx context.Context) (any, error) { + return obj.Repeated, nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v bool) graphql.Marshaler { + return ec.marshalNBoolean2bool(ctx, selections, v) + }, + true, + true, + ) +} +func (ec *executionContext) fieldContext_DataframeRecipeExpressionExplanation_repeated(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + return graphql.NewScalarFieldContext("DataframeRecipeExpressionExplanation", field, false, false, errors.New("field of type Boolean does not have child fields")) } -func (ec *executionContext) fieldContext_FhirDataframeResult_rows(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { +func (ec *executionContext) _DataframeRecipeExpressionExplanation_nullable(ctx context.Context, field graphql.CollectedField, obj *model.DataframeRecipeExpressionExplanation) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_DataframeRecipeExpressionExplanation_nullable(ctx, field) + }, + func(ctx context.Context) (any, error) { + return obj.Nullable, nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v bool) graphql.Marshaler { + return ec.marshalNBoolean2bool(ctx, selections, v) + }, + true, + true, + ) +} +func (ec *executionContext) fieldContext_DataframeRecipeExpressionExplanation_nullable(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + return graphql.NewScalarFieldContext("DataframeRecipeExpressionExplanation", field, false, false, errors.New("field of type Boolean does not have child fields")) +} + +func (ec *executionContext) _DataframeRecipeOptimizationDecision_rule(ctx context.Context, field graphql.CollectedField, obj *model.DataframeRecipeOptimizationDecision) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_DataframeRecipeOptimizationDecision_rule(ctx, field) + }, + func(ctx context.Context) (any, error) { + return obj.Rule, nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v string) graphql.Marshaler { + return ec.marshalNString2string(ctx, selections, v) + }, + true, + true, + ) +} +func (ec *executionContext) fieldContext_DataframeRecipeOptimizationDecision_rule(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + return graphql.NewScalarFieldContext("DataframeRecipeOptimizationDecision", field, false, false, errors.New("field of type String does not have child fields")) +} + +func (ec *executionContext) _DataframeRecipeOptimizationDecision_enabled(ctx context.Context, field graphql.CollectedField, obj *model.DataframeRecipeOptimizationDecision) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_DataframeRecipeOptimizationDecision_enabled(ctx, field) + }, + func(ctx context.Context) (any, error) { + return obj.Enabled, nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v bool) graphql.Marshaler { + return ec.marshalNBoolean2bool(ctx, selections, v) + }, + true, + true, + ) +} +func (ec *executionContext) fieldContext_DataframeRecipeOptimizationDecision_enabled(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + return graphql.NewScalarFieldContext("DataframeRecipeOptimizationDecision", field, false, false, errors.New("field of type Boolean does not have child fields")) +} + +func (ec *executionContext) _DataframeRecipeOptimizationDecision_candidateSets(ctx context.Context, field graphql.CollectedField, obj *model.DataframeRecipeOptimizationDecision) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_DataframeRecipeOptimizationDecision_candidateSets(ctx, field) + }, + func(ctx context.Context) (any, error) { + return obj.CandidateSets, nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v int) graphql.Marshaler { + return ec.marshalNInt2int(ctx, selections, v) + }, + true, + true, + ) +} +func (ec *executionContext) fieldContext_DataframeRecipeOptimizationDecision_candidateSets(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + return graphql.NewScalarFieldContext("DataframeRecipeOptimizationDecision", field, false, false, errors.New("field of type Int does not have child fields")) +} + +func (ec *executionContext) _DataframeRecipeOptimizationDecision_estimatedBaselineWork(ctx context.Context, field graphql.CollectedField, obj *model.DataframeRecipeOptimizationDecision) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_DataframeRecipeOptimizationDecision_estimatedBaselineWork(ctx, field) + }, + func(ctx context.Context) (any, error) { + return obj.EstimatedBaselineWork, nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v int) graphql.Marshaler { + return ec.marshalNInt2int(ctx, selections, v) + }, + true, + true, + ) +} +func (ec *executionContext) fieldContext_DataframeRecipeOptimizationDecision_estimatedBaselineWork(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + return graphql.NewScalarFieldContext("DataframeRecipeOptimizationDecision", field, false, false, errors.New("field of type Int does not have child fields")) +} + +func (ec *executionContext) _DataframeRecipeOptimizationDecision_estimatedOptimizedWork(ctx context.Context, field graphql.CollectedField, obj *model.DataframeRecipeOptimizationDecision) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_DataframeRecipeOptimizationDecision_estimatedOptimizedWork(ctx, field) + }, + func(ctx context.Context) (any, error) { + return obj.EstimatedOptimizedWork, nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v int) graphql.Marshaler { + return ec.marshalNInt2int(ctx, selections, v) + }, + true, + true, + ) +} +func (ec *executionContext) fieldContext_DataframeRecipeOptimizationDecision_estimatedOptimizedWork(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + return graphql.NewScalarFieldContext("DataframeRecipeOptimizationDecision", field, false, false, errors.New("field of type Int does not have child fields")) +} + +func (ec *executionContext) _DataframeRecipeOptimizationDecision_estimatedSavings(ctx context.Context, field graphql.CollectedField, obj *model.DataframeRecipeOptimizationDecision) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_DataframeRecipeOptimizationDecision_estimatedSavings(ctx, field) + }, + func(ctx context.Context) (any, error) { + return obj.EstimatedSavings, nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v int) graphql.Marshaler { + return ec.marshalNInt2int(ctx, selections, v) + }, + true, + true, + ) +} +func (ec *executionContext) fieldContext_DataframeRecipeOptimizationDecision_estimatedSavings(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + return graphql.NewScalarFieldContext("DataframeRecipeOptimizationDecision", field, false, false, errors.New("field of type Int does not have child fields")) +} + +func (ec *executionContext) _DataframeRecipeOptimizationDecision_reason(ctx context.Context, field graphql.CollectedField, obj *model.DataframeRecipeOptimizationDecision) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_DataframeRecipeOptimizationDecision_reason(ctx, field) + }, + func(ctx context.Context) (any, error) { + return obj.Reason, nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v string) graphql.Marshaler { + return ec.marshalNString2string(ctx, selections, v) + }, + true, + true, + ) +} +func (ec *executionContext) fieldContext_DataframeRecipeOptimizationDecision_reason(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + return graphql.NewScalarFieldContext("DataframeRecipeOptimizationDecision", field, false, false, errors.New("field of type String does not have child fields")) +} + +func (ec *executionContext) _DataframeRecipeOptimizationExplanation_policy(ctx context.Context, field graphql.CollectedField, obj *model.DataframeRecipeOptimizationExplanation) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_DataframeRecipeOptimizationExplanation_policy(ctx, field) + }, + func(ctx context.Context) (any, error) { + return obj.Policy, nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v string) graphql.Marshaler { + return ec.marshalNString2string(ctx, selections, v) + }, + true, + true, + ) +} +func (ec *executionContext) fieldContext_DataframeRecipeOptimizationExplanation_policy(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + return graphql.NewScalarFieldContext("DataframeRecipeOptimizationExplanation", field, false, false, errors.New("field of type String does not have child fields")) +} + +func (ec *executionContext) _DataframeRecipeOptimizationExplanation_enabled(ctx context.Context, field graphql.CollectedField, obj *model.DataframeRecipeOptimizationExplanation) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_DataframeRecipeOptimizationExplanation_enabled(ctx, field) + }, + func(ctx context.Context) (any, error) { + return obj.Enabled, nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v bool) graphql.Marshaler { + return ec.marshalNBoolean2bool(ctx, selections, v) + }, + true, + true, + ) +} +func (ec *executionContext) fieldContext_DataframeRecipeOptimizationExplanation_enabled(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + return graphql.NewScalarFieldContext("DataframeRecipeOptimizationExplanation", field, false, false, errors.New("field of type Boolean does not have child fields")) +} + +func (ec *executionContext) _DataframeRecipeOptimizationExplanation_minimumSavings(ctx context.Context, field graphql.CollectedField, obj *model.DataframeRecipeOptimizationExplanation) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_DataframeRecipeOptimizationExplanation_minimumSavings(ctx, field) + }, + func(ctx context.Context) (any, error) { + return obj.MinimumSavings, nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v int) graphql.Marshaler { + return ec.marshalNInt2int(ctx, selections, v) + }, + true, + true, + ) +} +func (ec *executionContext) fieldContext_DataframeRecipeOptimizationExplanation_minimumSavings(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + return graphql.NewScalarFieldContext("DataframeRecipeOptimizationExplanation", field, false, false, errors.New("field of type Int does not have child fields")) +} + +func (ec *executionContext) _DataframeRecipeOptimizationExplanation_rules(ctx context.Context, field graphql.CollectedField, obj *model.DataframeRecipeOptimizationExplanation) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_DataframeRecipeOptimizationExplanation_rules(ctx, field) + }, + func(ctx context.Context) (any, error) { + return obj.Rules, nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v []*model.DataframeRecipeOptimizationRuleState) graphql.Marshaler { + return ec.marshalNDataframeRecipeOptimizationRuleState2ᚕᚖgithubᚗcomᚋcalyprᚋloomᚋgraphqlapiᚋmodelᚐDataframeRecipeOptimizationRuleStateᚄ(ctx, selections, v) + }, + true, + true, + ) +} +func (ec *executionContext) fieldContext_DataframeRecipeOptimizationExplanation_rules(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { fc = &graphql.FieldContext{ - Object: "FhirDataframeResult", + Object: "DataframeRecipeOptimizationExplanation", Field: field, IsMethod: false, IsResolver: false, Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { - return nil, errors.New("field of type JSON does not have child fields") + return ec.childFields_DataframeRecipeOptimizationRuleState(ctx, field) }, } return fc, nil } -func (ec *executionContext) _FhirDataframeResult_rowCount(ctx context.Context, field graphql.CollectedField, obj *model.FhirDataframeResult) (ret graphql.Marshaler) { - fc, err := ec.fieldContext_FhirDataframeResult_rowCount(ctx, field) - if err != nil { - return graphql.Null - } - ctx = graphql.WithFieldContext(ctx, fc) - defer func() { - if r := recover(); r != nil { - ec.Error(ctx, ec.Recover(ctx, r)) - ret = graphql.Null - } - }() - resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { - ctx = rctx // use context from middleware stack in children - return obj.RowCount, nil - }) - if err != nil { - ec.Error(ctx, err) - return graphql.Null - } - if resTmp == nil { - if !graphql.HasFieldError(ctx, fc) { - ec.Errorf(ctx, "must not be null") - } - return graphql.Null - } - res := resTmp.(int) - fc.Result = res - return ec.marshalNInt2int(ctx, field.Selections, res) +func (ec *executionContext) _DataframeRecipeOptimizationExplanation_decisions(ctx context.Context, field graphql.CollectedField, obj *model.DataframeRecipeOptimizationExplanation) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_DataframeRecipeOptimizationExplanation_decisions(ctx, field) + }, + func(ctx context.Context) (any, error) { + return obj.Decisions, nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v []*model.DataframeRecipeOptimizationDecision) graphql.Marshaler { + return ec.marshalNDataframeRecipeOptimizationDecision2ᚕᚖgithubᚗcomᚋcalyprᚋloomᚋgraphqlapiᚋmodelᚐDataframeRecipeOptimizationDecisionᚄ(ctx, selections, v) + }, + true, + true, + ) } - -func (ec *executionContext) fieldContext_FhirDataframeResult_rowCount(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { +func (ec *executionContext) fieldContext_DataframeRecipeOptimizationExplanation_decisions(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { fc = &graphql.FieldContext{ - Object: "FhirDataframeResult", + Object: "DataframeRecipeOptimizationExplanation", Field: field, IsMethod: false, IsResolver: false, Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { - return nil, errors.New("field of type Int does not have child fields") + return ec.childFields_DataframeRecipeOptimizationDecision(ctx, field) }, } return fc, nil } -func (ec *executionContext) _FhirDataframeResult_diagnostics(ctx context.Context, field graphql.CollectedField, obj *model.FhirDataframeResult) (ret graphql.Marshaler) { - fc, err := ec.fieldContext_FhirDataframeResult_diagnostics(ctx, field) - if err != nil { - return graphql.Null - } - ctx = graphql.WithFieldContext(ctx, fc) - defer func() { - if r := recover(); r != nil { - ec.Error(ctx, ec.Recover(ctx, r)) - ret = graphql.Null - } - }() - resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { - ctx = rctx // use context from middleware stack in children - return obj.Diagnostics, nil - }) - if err != nil { - ec.Error(ctx, err) - return graphql.Null - } - if resTmp == nil { - if !graphql.HasFieldError(ctx, fc) { - ec.Errorf(ctx, "must not be null") - } - return graphql.Null - } - res := resTmp.(*model.DataframeQueryDiagnostics) - fc.Result = res - return ec.marshalNDataframeQueryDiagnostics2ᚖgithubᚗcomᚋcalyprᚋloomᚋgraphqlapiᚋmodelᚐDataframeQueryDiagnostics(ctx, field.Selections, res) +func (ec *executionContext) _DataframeRecipeOptimizationRuleState_rule(ctx context.Context, field graphql.CollectedField, obj *model.DataframeRecipeOptimizationRuleState) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_DataframeRecipeOptimizationRuleState_rule(ctx, field) + }, + func(ctx context.Context) (any, error) { + return obj.Rule, nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v string) graphql.Marshaler { + return ec.marshalNString2string(ctx, selections, v) + }, + true, + true, + ) +} +func (ec *executionContext) fieldContext_DataframeRecipeOptimizationRuleState_rule(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + return graphql.NewScalarFieldContext("DataframeRecipeOptimizationRuleState", field, false, false, errors.New("field of type String does not have child fields")) } -func (ec *executionContext) fieldContext_FhirDataframeResult_diagnostics(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { +func (ec *executionContext) _DataframeRecipeOptimizationRuleState_enabled(ctx context.Context, field graphql.CollectedField, obj *model.DataframeRecipeOptimizationRuleState) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_DataframeRecipeOptimizationRuleState_enabled(ctx, field) + }, + func(ctx context.Context) (any, error) { + return obj.Enabled, nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v bool) graphql.Marshaler { + return ec.marshalNBoolean2bool(ctx, selections, v) + }, + true, + true, + ) +} +func (ec *executionContext) fieldContext_DataframeRecipeOptimizationRuleState_enabled(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + return graphql.NewScalarFieldContext("DataframeRecipeOptimizationRuleState", field, false, false, errors.New("field of type Boolean does not have child fields")) +} + +func (ec *executionContext) _DataframeRecipeOptimizationRuleState_reason(ctx context.Context, field graphql.CollectedField, obj *model.DataframeRecipeOptimizationRuleState) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_DataframeRecipeOptimizationRuleState_reason(ctx, field) + }, + func(ctx context.Context) (any, error) { + return obj.Reason, nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v string) graphql.Marshaler { + return ec.marshalNString2string(ctx, selections, v) + }, + true, + true, + ) +} +func (ec *executionContext) fieldContext_DataframeRecipeOptimizationRuleState_reason(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + return graphql.NewScalarFieldContext("DataframeRecipeOptimizationRuleState", field, false, false, errors.New("field of type String does not have child fields")) +} + +func (ec *executionContext) _DataframeRecipeOutputExplanation_name(ctx context.Context, field graphql.CollectedField, obj *model.DataframeRecipeOutputExplanation) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_DataframeRecipeOutputExplanation_name(ctx, field) + }, + func(ctx context.Context) (any, error) { + return obj.Name, nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v string) graphql.Marshaler { + return ec.marshalNString2string(ctx, selections, v) + }, + true, + true, + ) +} +func (ec *executionContext) fieldContext_DataframeRecipeOutputExplanation_name(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + return graphql.NewScalarFieldContext("DataframeRecipeOutputExplanation", field, false, false, errors.New("field of type String does not have child fields")) +} + +func (ec *executionContext) _DataframeRecipeOutputExplanation_rootResourceType(ctx context.Context, field graphql.CollectedField, obj *model.DataframeRecipeOutputExplanation) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_DataframeRecipeOutputExplanation_rootResourceType(ctx, field) + }, + func(ctx context.Context) (any, error) { + return obj.RootResourceType, nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v string) graphql.Marshaler { + return ec.marshalNString2string(ctx, selections, v) + }, + true, + true, + ) +} +func (ec *executionContext) fieldContext_DataframeRecipeOutputExplanation_rootResourceType(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + return graphql.NewScalarFieldContext("DataframeRecipeOutputExplanation", field, false, false, errors.New("field of type String does not have child fields")) +} + +func (ec *executionContext) _DataframeRecipeOutputExplanation_rowGrain(ctx context.Context, field graphql.CollectedField, obj *model.DataframeRecipeOutputExplanation) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_DataframeRecipeOutputExplanation_rowGrain(ctx, field) + }, + func(ctx context.Context) (any, error) { + return obj.RowGrain, nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v string) graphql.Marshaler { + return ec.marshalNString2string(ctx, selections, v) + }, + true, + true, + ) +} +func (ec *executionContext) fieldContext_DataframeRecipeOutputExplanation_rowGrain(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + return graphql.NewScalarFieldContext("DataframeRecipeOutputExplanation", field, false, false, errors.New("field of type String does not have child fields")) +} + +func (ec *executionContext) _DataframeRecipeOutputExplanation_fields(ctx context.Context, field graphql.CollectedField, obj *model.DataframeRecipeOutputExplanation) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_DataframeRecipeOutputExplanation_fields(ctx, field) + }, + func(ctx context.Context) (any, error) { + return obj.Fields, nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v []*model.DataframeRecipeExpressionExplanation) graphql.Marshaler { + return ec.marshalNDataframeRecipeExpressionExplanation2ᚕᚖgithubᚗcomᚋcalyprᚋloomᚋgraphqlapiᚋmodelᚐDataframeRecipeExpressionExplanationᚄ(ctx, selections, v) + }, + true, + true, + ) +} +func (ec *executionContext) fieldContext_DataframeRecipeOutputExplanation_fields(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { fc = &graphql.FieldContext{ - Object: "FhirDataframeResult", + Object: "DataframeRecipeOutputExplanation", Field: field, IsMethod: false, IsResolver: false, Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { - switch field.Name { - case "inputResolutionMs": - return ec.fieldContext_DataframeQueryDiagnostics_inputResolutionMs(ctx, field) - case "requestPreparationMs": - return ec.fieldContext_DataframeQueryDiagnostics_requestPreparationMs(ctx, field) - case "compilationMs": - return ec.fieldContext_DataframeQueryDiagnostics_compilationMs(ctx, field) - case "arangoQueryMs": - return ec.fieldContext_DataframeQueryDiagnostics_arangoQueryMs(ctx, field) - case "rowMaterializationMs": - return ec.fieldContext_DataframeQueryDiagnostics_rowMaterializationMs(ctx, field) - case "resultAssemblyMs": - return ec.fieldContext_DataframeQueryDiagnostics_resultAssemblyMs(ctx, field) - case "totalMs": - return ec.fieldContext_DataframeQueryDiagnostics_totalMs(ctx, field) - case "plan": - return ec.fieldContext_DataframeQueryDiagnostics_plan(ctx, field) - } - return nil, fmt.Errorf("no field named %q was found under type DataframeQueryDiagnostics", field.Name) + return ec.childFields_DataframeRecipeExpressionExplanation(ctx, field) }, } return fc, nil } -func (ec *executionContext) _Mutation_runFhirDataframe(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) { - fc, err := ec.fieldContext_Mutation_runFhirDataframe(ctx, field) - if err != nil { - return graphql.Null - } - ctx = graphql.WithFieldContext(ctx, fc) - defer func() { - if r := recover(); r != nil { - ec.Error(ctx, ec.Recover(ctx, r)) - ret = graphql.Null - } - }() - resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { - ctx = rctx // use context from middleware stack in children - return ec.resolvers.Mutation().RunFhirDataframe(rctx, fc.Args["input"].(model.FhirDataframeInput), fc.Args["limit"].(*int)) - }) - if err != nil { - ec.Error(ctx, err) - return graphql.Null - } - if resTmp == nil { - if !graphql.HasFieldError(ctx, fc) { - ec.Errorf(ctx, "must not be null") - } - return graphql.Null - } - res := resTmp.(*model.FhirDataframeResult) - fc.Result = res - return ec.marshalNFhirDataframeResult2ᚖgithubᚗcomᚋcalyprᚋloomᚋgraphqlapiᚋmodelᚐFhirDataframeResult(ctx, field.Selections, res) +func (ec *executionContext) _DataframeRecipeOutputExplanation_identity(ctx context.Context, field graphql.CollectedField, obj *model.DataframeRecipeOutputExplanation) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_DataframeRecipeOutputExplanation_identity(ctx, field) + }, + func(ctx context.Context) (any, error) { + return obj.Identity, nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v *model.DataframeRecipeExpressionExplanation) graphql.Marshaler { + return ec.marshalODataframeRecipeExpressionExplanation2ᚖgithubᚗcomᚋcalyprᚋloomᚋgraphqlapiᚋmodelᚐDataframeRecipeExpressionExplanation(ctx, selections, v) + }, + true, + false, + ) } - -func (ec *executionContext) fieldContext_Mutation_runFhirDataframe(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { +func (ec *executionContext) fieldContext_DataframeRecipeOutputExplanation_identity(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { fc = &graphql.FieldContext{ - Object: "Mutation", + Object: "DataframeRecipeOutputExplanation", Field: field, - IsMethod: true, - IsResolver: true, + IsMethod: false, + IsResolver: false, Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { - switch field.Name { - case "columns": - return ec.fieldContext_FhirDataframeResult_columns(ctx, field) - case "rows": - return ec.fieldContext_FhirDataframeResult_rows(ctx, field) - case "rowCount": - return ec.fieldContext_FhirDataframeResult_rowCount(ctx, field) - case "diagnostics": - return ec.fieldContext_FhirDataframeResult_diagnostics(ctx, field) - } - return nil, fmt.Errorf("no field named %q was found under type FhirDataframeResult", field.Name) + return ec.childFields_DataframeRecipeExpressionExplanation(ctx, field) }, } - defer func() { - if r := recover(); r != nil { - err = ec.Recover(ctx, r) - ec.Error(ctx, err) - } - }() - ctx = graphql.WithFieldContext(ctx, fc) - if fc.Args, err = ec.field_Mutation_runFhirDataframe_args(ctx, field.ArgumentMap(ec.Variables)); err != nil { - ec.Error(ctx, err) - return fc, err - } return fc, nil } -func (ec *executionContext) _Query_dataframeBuilderIntrospection(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) { - fc, err := ec.fieldContext_Query_dataframeBuilderIntrospection(ctx, field) - if err != nil { - return graphql.Null - } - ctx = graphql.WithFieldContext(ctx, fc) - defer func() { - if r := recover(); r != nil { - ec.Error(ctx, ec.Recover(ctx, r)) - ret = graphql.Null - } - }() - resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { - ctx = rctx // use context from middleware stack in children - return ec.resolvers.Query().DataframeBuilderIntrospection(rctx, fc.Args["input"].(model.DataframeBuilderIntrospectionInput)) - }) - if err != nil { - ec.Error(ctx, err) - return graphql.Null - } - if resTmp == nil { - if !graphql.HasFieldError(ctx, fc) { - ec.Errorf(ctx, "must not be null") - } - return graphql.Null - } - res := resTmp.(*model.DataframeBuilderIntrospection) - fc.Result = res - return ec.marshalNDataframeBuilderIntrospection2ᚖgithubᚗcomᚋcalyprᚋloomᚋgraphqlapiᚋmodelᚐDataframeBuilderIntrospection(ctx, field.Selections, res) +func (ec *executionContext) _DataframeRecipeOutputExplanation_expansion(ctx context.Context, field graphql.CollectedField, obj *model.DataframeRecipeOutputExplanation) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_DataframeRecipeOutputExplanation_expansion(ctx, field) + }, + func(ctx context.Context) (any, error) { + return obj.Expansion, nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v *model.DataframeRecipeExpansionExplanation) graphql.Marshaler { + return ec.marshalODataframeRecipeExpansionExplanation2ᚖgithubᚗcomᚋcalyprᚋloomᚋgraphqlapiᚋmodelᚐDataframeRecipeExpansionExplanation(ctx, selections, v) + }, + true, + false, + ) } - -func (ec *executionContext) fieldContext_Query_dataframeBuilderIntrospection(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { +func (ec *executionContext) fieldContext_DataframeRecipeOutputExplanation_expansion(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { fc = &graphql.FieldContext{ - Object: "Query", + Object: "DataframeRecipeOutputExplanation", Field: field, - IsMethod: true, - IsResolver: true, + IsMethod: false, + IsResolver: false, Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { - switch field.Name { - case "project": - return ec.fieldContext_DataframeBuilderIntrospection_project(ctx, field) - case "rootResourceType": - return ec.fieldContext_DataframeBuilderIntrospection_rootResourceType(ctx, field) - case "authResourcePaths": - return ec.fieldContext_DataframeBuilderIntrospection_authResourcePaths(ctx, field) - case "root": - return ec.fieldContext_DataframeBuilderIntrospection_root(ctx, field) - case "relatedResources": - return ec.fieldContext_DataframeBuilderIntrospection_relatedResources(ctx, field) - case "traversals": - return ec.fieldContext_DataframeBuilderIntrospection_traversals(ctx, field) - case "fields": - return ec.fieldContext_DataframeBuilderIntrospection_fields(ctx, field) - case "pivotFields": - return ec.fieldContext_DataframeBuilderIntrospection_pivotFields(ctx, field) - } - return nil, fmt.Errorf("no field named %q was found under type DataframeBuilderIntrospection", field.Name) + return ec.childFields_DataframeRecipeExpansionExplanation(ctx, field) }, } - defer func() { - if r := recover(); r != nil { - err = ec.Recover(ctx, r) - ec.Error(ctx, err) - } - }() - ctx = graphql.WithFieldContext(ctx, fc) - if fc.Args, err = ec.field_Query_dataframeBuilderIntrospection_args(ctx, field.ArgumentMap(ec.Variables)); err != nil { - ec.Error(ctx, err) - return fc, err - } return fc, nil } -func (ec *executionContext) _Query_dataframeMaterialization(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) { - fc, err := ec.fieldContext_Query_dataframeMaterialization(ctx, field) - if err != nil { - return graphql.Null - } - ctx = graphql.WithFieldContext(ctx, fc) - defer func() { - if r := recover(); r != nil { - ec.Error(ctx, ec.Recover(ctx, r)) - ret = graphql.Null - } - }() - resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { - ctx = rctx // use context from middleware stack in children - return ec.resolvers.Query().DataframeMaterialization(rctx, fc.Args["id"].(string)) - }) - if err != nil { - ec.Error(ctx, err) - return graphql.Null - } - if resTmp == nil { - return graphql.Null - } - res := resTmp.(*model.DataframeMaterialization) - fc.Result = res - return ec.marshalODataframeMaterialization2ᚖgithubᚗcomᚋcalyprᚋloomᚋgraphqlapiᚋmodelᚐDataframeMaterialization(ctx, field.Selections, res) +func (ec *executionContext) _DataframeRecipeOutputExplanation_dynamicMaps(ctx context.Context, field graphql.CollectedField, obj *model.DataframeRecipeOutputExplanation) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_DataframeRecipeOutputExplanation_dynamicMaps(ctx, field) + }, + func(ctx context.Context) (any, error) { + return obj.DynamicMaps, nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v []string) graphql.Marshaler { + return ec.marshalNString2ᚕstringᚄ(ctx, selections, v) + }, + true, + true, + ) +} +func (ec *executionContext) fieldContext_DataframeRecipeOutputExplanation_dynamicMaps(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + return graphql.NewScalarFieldContext("DataframeRecipeOutputExplanation", field, false, false, errors.New("field of type String does not have child fields")) } -func (ec *executionContext) fieldContext_Query_dataframeMaterialization(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { - fc = &graphql.FieldContext{ - Object: "Query", - Field: field, - IsMethod: true, - IsResolver: true, - Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { - switch field.Name { - case "id": - return ec.fieldContext_DataframeMaterialization_id(ctx, field) - case "name": - return ec.fieldContext_DataframeMaterialization_name(ctx, field) - case "project": - return ec.fieldContext_DataframeMaterialization_project(ctx, field) - case "datasetGeneration": - return ec.fieldContext_DataframeMaterialization_datasetGeneration(ctx, field) - case "state": - return ec.fieldContext_DataframeMaterialization_state(ctx, field) - case "columns": - return ec.fieldContext_DataframeMaterialization_columns(ctx, field) - case "rowCount": - return ec.fieldContext_DataframeMaterialization_rowCount(ctx, field) - case "createdAt": - return ec.fieldContext_DataframeMaterialization_createdAt(ctx, field) - case "readyAt": - return ec.fieldContext_DataframeMaterialization_readyAt(ctx, field) - case "error": - return ec.fieldContext_DataframeMaterialization_error(ctx, field) - } - return nil, fmt.Errorf("no field named %q was found under type DataframeMaterialization", field.Name) +func (ec *executionContext) _DataframeRecipeOutputExplanation_catalogProjections(ctx context.Context, field graphql.CollectedField, obj *model.DataframeRecipeOutputExplanation) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_DataframeRecipeOutputExplanation_catalogProjections(ctx, field) }, - } - defer func() { - if r := recover(); r != nil { - err = ec.Recover(ctx, r) - ec.Error(ctx, err) - } - }() - ctx = graphql.WithFieldContext(ctx, fc) - if fc.Args, err = ec.field_Query_dataframeMaterialization_args(ctx, field.ArgumentMap(ec.Variables)); err != nil { - ec.Error(ctx, err) - return fc, err - } - return fc, nil + func(ctx context.Context) (any, error) { + return obj.CatalogProjections, nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v []string) graphql.Marshaler { + return ec.marshalNString2ᚕstringᚄ(ctx, selections, v) + }, + true, + true, + ) +} +func (ec *executionContext) fieldContext_DataframeRecipeOutputExplanation_catalogProjections(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + return graphql.NewScalarFieldContext("DataframeRecipeOutputExplanation", field, false, false, errors.New("field of type String does not have child fields")) } -func (ec *executionContext) _Query_dataframeRows(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) { - fc, err := ec.fieldContext_Query_dataframeRows(ctx, field) - if err != nil { - return graphql.Null - } - ctx = graphql.WithFieldContext(ctx, fc) - defer func() { - if r := recover(); r != nil { - ec.Error(ctx, ec.Recover(ctx, r)) - ret = graphql.Null - } - }() - resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { - ctx = rctx // use context from middleware stack in children - return ec.resolvers.Query().DataframeRows(rctx, fc.Args["input"].(model.DataframeRowsInput)) - }) - if err != nil { - ec.Error(ctx, err) - return graphql.Null - } - if resTmp == nil { - if !graphql.HasFieldError(ctx, fc) { - ec.Errorf(ctx, "must not be null") - } - return graphql.Null - } - res := resTmp.(*model.DataframeRowConnection) - fc.Result = res - return ec.marshalNDataframeRowConnection2ᚖgithubᚗcomᚋcalyprᚋloomᚋgraphqlapiᚋmodelᚐDataframeRowConnection(ctx, field.Selections, res) +func (ec *executionContext) _DataframeRecipeOutputValidation_name(ctx context.Context, field graphql.CollectedField, obj *model.DataframeRecipeOutputValidation) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_DataframeRecipeOutputValidation_name(ctx, field) + }, + func(ctx context.Context) (any, error) { + return obj.Name, nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v string) graphql.Marshaler { + return ec.marshalNString2string(ctx, selections, v) + }, + true, + true, + ) +} +func (ec *executionContext) fieldContext_DataframeRecipeOutputValidation_name(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + return graphql.NewScalarFieldContext("DataframeRecipeOutputValidation", field, false, false, errors.New("field of type String does not have child fields")) } -func (ec *executionContext) fieldContext_Query_dataframeRows(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { - fc = &graphql.FieldContext{ - Object: "Query", - Field: field, - IsMethod: true, - IsResolver: true, - Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { - switch field.Name { - case "materialization": - return ec.fieldContext_DataframeRowConnection_materialization(ctx, field) - case "columns": - return ec.fieldContext_DataframeRowConnection_columns(ctx, field) - case "rows": - return ec.fieldContext_DataframeRowConnection_rows(ctx, field) - case "pageInfo": - return ec.fieldContext_DataframeRowConnection_pageInfo(ctx, field) - } - return nil, fmt.Errorf("no field named %q was found under type DataframeRowConnection", field.Name) +func (ec *executionContext) _DataframeRecipeOutputValidation_rootResourceType(ctx context.Context, field graphql.CollectedField, obj *model.DataframeRecipeOutputValidation) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_DataframeRecipeOutputValidation_rootResourceType(ctx, field) }, - } - defer func() { - if r := recover(); r != nil { - err = ec.Recover(ctx, r) - ec.Error(ctx, err) - } - }() - ctx = graphql.WithFieldContext(ctx, fc) - if fc.Args, err = ec.field_Query_dataframeRows_args(ctx, field.ArgumentMap(ec.Variables)); err != nil { - ec.Error(ctx, err) - return fc, err - } - return fc, nil + func(ctx context.Context) (any, error) { + return obj.RootResourceType, nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v string) graphql.Marshaler { + return ec.marshalNString2string(ctx, selections, v) + }, + true, + true, + ) +} +func (ec *executionContext) fieldContext_DataframeRecipeOutputValidation_rootResourceType(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + return graphql.NewScalarFieldContext("DataframeRecipeOutputValidation", field, false, false, errors.New("field of type String does not have child fields")) } -func (ec *executionContext) _Query_dataframeAggregate(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) { - fc, err := ec.fieldContext_Query_dataframeAggregate(ctx, field) - if err != nil { - return graphql.Null - } - ctx = graphql.WithFieldContext(ctx, fc) - defer func() { - if r := recover(); r != nil { - ec.Error(ctx, ec.Recover(ctx, r)) - ret = graphql.Null - } - }() - resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { - ctx = rctx // use context from middleware stack in children - return ec.resolvers.Query().DataframeAggregate(rctx, fc.Args["input"].(model.DataframeAggregateInput)) - }) - if err != nil { - ec.Error(ctx, err) - return graphql.Null - } - if resTmp == nil { - if !graphql.HasFieldError(ctx, fc) { - ec.Errorf(ctx, "must not be null") - } - return graphql.Null - } - res := resTmp.(*model.DataframeAggregateResult) - fc.Result = res - return ec.marshalNDataframeAggregateResult2ᚖgithubᚗcomᚋcalyprᚋloomᚋgraphqlapiᚋmodelᚐDataframeAggregateResult(ctx, field.Selections, res) +func (ec *executionContext) _DataframeRecipeOutputValidation_rowGrain(ctx context.Context, field graphql.CollectedField, obj *model.DataframeRecipeOutputValidation) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_DataframeRecipeOutputValidation_rowGrain(ctx, field) + }, + func(ctx context.Context) (any, error) { + return obj.RowGrain, nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v string) graphql.Marshaler { + return ec.marshalNString2string(ctx, selections, v) + }, + true, + true, + ) +} +func (ec *executionContext) fieldContext_DataframeRecipeOutputValidation_rowGrain(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + return graphql.NewScalarFieldContext("DataframeRecipeOutputValidation", field, false, false, errors.New("field of type String does not have child fields")) } -func (ec *executionContext) fieldContext_Query_dataframeAggregate(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { - fc = &graphql.FieldContext{ - Object: "Query", - Field: field, - IsMethod: true, - IsResolver: true, - Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { - switch field.Name { - case "materialization": - return ec.fieldContext_DataframeAggregateResult_materialization(ctx, field) - case "columns": - return ec.fieldContext_DataframeAggregateResult_columns(ctx, field) - case "rows": - return ec.fieldContext_DataframeAggregateResult_rows(ctx, field) - } - return nil, fmt.Errorf("no field named %q was found under type DataframeAggregateResult", field.Name) +func (ec *executionContext) _DataframeRecipeOutputValidation_fieldNames(ctx context.Context, field graphql.CollectedField, obj *model.DataframeRecipeOutputValidation) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_DataframeRecipeOutputValidation_fieldNames(ctx, field) }, - } - defer func() { - if r := recover(); r != nil { - err = ec.Recover(ctx, r) - ec.Error(ctx, err) - } - }() - ctx = graphql.WithFieldContext(ctx, fc) - if fc.Args, err = ec.field_Query_dataframeAggregate_args(ctx, field.ArgumentMap(ec.Variables)); err != nil { - ec.Error(ctx, err) - return fc, err - } - return fc, nil + func(ctx context.Context) (any, error) { + return obj.FieldNames, nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v []string) graphql.Marshaler { + return ec.marshalNString2ᚕstringᚄ(ctx, selections, v) + }, + true, + true, + ) +} +func (ec *executionContext) fieldContext_DataframeRecipeOutputValidation_fieldNames(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + return graphql.NewScalarFieldContext("DataframeRecipeOutputValidation", field, false, false, errors.New("field of type String does not have child fields")) } -func (ec *executionContext) _Query___type(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) { - fc, err := ec.fieldContext_Query___type(ctx, field) - if err != nil { - return graphql.Null - } - ctx = graphql.WithFieldContext(ctx, fc) - defer func() { - if r := recover(); r != nil { - ec.Error(ctx, ec.Recover(ctx, r)) - ret = graphql.Null - } - }() - resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { - ctx = rctx // use context from middleware stack in children - return ec.introspectType(fc.Args["name"].(string)) - }) - if err != nil { - ec.Error(ctx, err) - return graphql.Null - } - if resTmp == nil { - return graphql.Null - } - res := resTmp.(*introspection.Type) - fc.Result = res - return ec.marshalO__Type2ᚖgithubᚗcomᚋ99designsᚋgqlgenᚋgraphqlᚋintrospectionᚐType(ctx, field.Selections, res) +func (ec *executionContext) _DataframeRecipeOutputValidation_dynamicColumns(ctx context.Context, field graphql.CollectedField, obj *model.DataframeRecipeOutputValidation) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_DataframeRecipeOutputValidation_dynamicColumns(ctx, field) + }, + func(ctx context.Context) (any, error) { + return obj.DynamicColumns, nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v []string) graphql.Marshaler { + return ec.marshalNString2ᚕstringᚄ(ctx, selections, v) + }, + true, + true, + ) +} +func (ec *executionContext) fieldContext_DataframeRecipeOutputValidation_dynamicColumns(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + return graphql.NewScalarFieldContext("DataframeRecipeOutputValidation", field, false, false, errors.New("field of type String does not have child fields")) } -func (ec *executionContext) fieldContext_Query___type(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { +func (ec *executionContext) _DataframeRecipePhysicalExplanation_outputs(ctx context.Context, field graphql.CollectedField, obj *model.DataframeRecipePhysicalExplanation) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_DataframeRecipePhysicalExplanation_outputs(ctx, field) + }, + func(ctx context.Context) (any, error) { + return obj.Outputs, nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v []*model.DataframeRecipePhysicalOutputExplanation) graphql.Marshaler { + return ec.marshalNDataframeRecipePhysicalOutputExplanation2ᚕᚖgithubᚗcomᚋcalyprᚋloomᚋgraphqlapiᚋmodelᚐDataframeRecipePhysicalOutputExplanationᚄ(ctx, selections, v) + }, + true, + true, + ) +} +func (ec *executionContext) fieldContext_DataframeRecipePhysicalExplanation_outputs(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { fc = &graphql.FieldContext{ - Object: "Query", + Object: "DataframeRecipePhysicalExplanation", Field: field, - IsMethod: true, + IsMethod: false, IsResolver: false, Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { - switch field.Name { - case "kind": - return ec.fieldContext___Type_kind(ctx, field) - case "name": - return ec.fieldContext___Type_name(ctx, field) - case "description": - return ec.fieldContext___Type_description(ctx, field) - case "fields": - return ec.fieldContext___Type_fields(ctx, field) - case "interfaces": - return ec.fieldContext___Type_interfaces(ctx, field) - case "possibleTypes": - return ec.fieldContext___Type_possibleTypes(ctx, field) - case "enumValues": - return ec.fieldContext___Type_enumValues(ctx, field) - case "inputFields": - return ec.fieldContext___Type_inputFields(ctx, field) - case "ofType": - return ec.fieldContext___Type_ofType(ctx, field) - case "specifiedByURL": - return ec.fieldContext___Type_specifiedByURL(ctx, field) - case "isOneOf": - return ec.fieldContext___Type_isOneOf(ctx, field) - } - return nil, fmt.Errorf("no field named %q was found under type __Type", field.Name) + return ec.childFields_DataframeRecipePhysicalOutputExplanation(ctx, field) }, } - defer func() { - if r := recover(); r != nil { - err = ec.Recover(ctx, r) - ec.Error(ctx, err) - } - }() - ctx = graphql.WithFieldContext(ctx, fc) - if fc.Args, err = ec.field_Query___type_args(ctx, field.ArgumentMap(ec.Variables)); err != nil { - ec.Error(ctx, err) - return fc, err - } return fc, nil } -func (ec *executionContext) _Query___schema(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) { - fc, err := ec.fieldContext_Query___schema(ctx, field) - if err != nil { - return graphql.Null - } - ctx = graphql.WithFieldContext(ctx, fc) - defer func() { - if r := recover(); r != nil { - ec.Error(ctx, ec.Recover(ctx, r)) - ret = graphql.Null - } - }() - resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { - ctx = rctx // use context from middleware stack in children - return ec.introspectSchema() - }) - if err != nil { - ec.Error(ctx, err) - return graphql.Null - } - if resTmp == nil { - return graphql.Null - } - res := resTmp.(*introspection.Schema) - fc.Result = res - return ec.marshalO__Schema2ᚖgithubᚗcomᚋ99designsᚋgqlgenᚋgraphqlᚋintrospectionᚐSchema(ctx, field.Selections, res) +func (ec *executionContext) _DataframeRecipePhysicalOutputExplanation_name(ctx context.Context, field graphql.CollectedField, obj *model.DataframeRecipePhysicalOutputExplanation) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_DataframeRecipePhysicalOutputExplanation_name(ctx, field) + }, + func(ctx context.Context) (any, error) { + return obj.Name, nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v string) graphql.Marshaler { + return ec.marshalNString2string(ctx, selections, v) + }, + true, + true, + ) +} +func (ec *executionContext) fieldContext_DataframeRecipePhysicalOutputExplanation_name(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + return graphql.NewScalarFieldContext("DataframeRecipePhysicalOutputExplanation", field, false, false, errors.New("field of type String does not have child fields")) } -func (ec *executionContext) fieldContext_Query___schema(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { - fc = &graphql.FieldContext{ - Object: "Query", - Field: field, - IsMethod: true, - IsResolver: false, - Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { - switch field.Name { - case "description": - return ec.fieldContext___Schema_description(ctx, field) - case "types": - return ec.fieldContext___Schema_types(ctx, field) - case "queryType": - return ec.fieldContext___Schema_queryType(ctx, field) - case "mutationType": - return ec.fieldContext___Schema_mutationType(ctx, field) - case "subscriptionType": - return ec.fieldContext___Schema_subscriptionType(ctx, field) - case "directives": - return ec.fieldContext___Schema_directives(ctx, field) - } - return nil, fmt.Errorf("no field named %q was found under type __Schema", field.Name) +func (ec *executionContext) _DataframeRecipePhysicalOutputExplanation_planFingerprint(ctx context.Context, field graphql.CollectedField, obj *model.DataframeRecipePhysicalOutputExplanation) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_DataframeRecipePhysicalOutputExplanation_planFingerprint(ctx, field) }, - } - return fc, nil + func(ctx context.Context) (any, error) { + return obj.PlanFingerprint, nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v string) graphql.Marshaler { + return ec.marshalNString2string(ctx, selections, v) + }, + true, + true, + ) } - -func (ec *executionContext) ___Directive_name(ctx context.Context, field graphql.CollectedField, obj *introspection.Directive) (ret graphql.Marshaler) { - fc, err := ec.fieldContext___Directive_name(ctx, field) - if err != nil { - return graphql.Null - } - ctx = graphql.WithFieldContext(ctx, fc) - defer func() { - if r := recover(); r != nil { - ec.Error(ctx, ec.Recover(ctx, r)) - ret = graphql.Null - } - }() - resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { - ctx = rctx // use context from middleware stack in children - return obj.Name, nil - }) - if err != nil { - ec.Error(ctx, err) - return graphql.Null - } - if resTmp == nil { - if !graphql.HasFieldError(ctx, fc) { - ec.Errorf(ctx, "must not be null") - } - return graphql.Null - } - res := resTmp.(string) - fc.Result = res - return ec.marshalNString2string(ctx, field.Selections, res) +func (ec *executionContext) fieldContext_DataframeRecipePhysicalOutputExplanation_planFingerprint(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + return graphql.NewScalarFieldContext("DataframeRecipePhysicalOutputExplanation", field, false, false, errors.New("field of type String does not have child fields")) } -func (ec *executionContext) fieldContext___Directive_name(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { - fc = &graphql.FieldContext{ - Object: "__Directive", - Field: field, - IsMethod: false, - IsResolver: false, - Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { - return nil, errors.New("field of type String does not have child fields") +func (ec *executionContext) _DataframeRecipePhysicalOutputExplanation_columns(ctx context.Context, field graphql.CollectedField, obj *model.DataframeRecipePhysicalOutputExplanation) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_DataframeRecipePhysicalOutputExplanation_columns(ctx, field) }, - } - return fc, nil + func(ctx context.Context) (any, error) { + return obj.Columns, nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v []string) graphql.Marshaler { + return ec.marshalNString2ᚕstringᚄ(ctx, selections, v) + }, + true, + true, + ) +} +func (ec *executionContext) fieldContext_DataframeRecipePhysicalOutputExplanation_columns(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + return graphql.NewScalarFieldContext("DataframeRecipePhysicalOutputExplanation", field, false, false, errors.New("field of type String does not have child fields")) } -func (ec *executionContext) ___Directive_description(ctx context.Context, field graphql.CollectedField, obj *introspection.Directive) (ret graphql.Marshaler) { - fc, err := ec.fieldContext___Directive_description(ctx, field) - if err != nil { - return graphql.Null - } - ctx = graphql.WithFieldContext(ctx, fc) - defer func() { - if r := recover(); r != nil { - ec.Error(ctx, ec.Recover(ctx, r)) - ret = graphql.Null - } - }() - resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { - ctx = rctx // use context from middleware stack in children - return obj.Description(), nil - }) - if err != nil { - ec.Error(ctx, err) - return graphql.Null - } - if resTmp == nil { - return graphql.Null - } - res := resTmp.(*string) - fc.Result = res - return ec.marshalOString2ᚖstring(ctx, field.Selections, res) +func (ec *executionContext) _DataframeRecipePhysicalOutputExplanation_traversalSets(ctx context.Context, field graphql.CollectedField, obj *model.DataframeRecipePhysicalOutputExplanation) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_DataframeRecipePhysicalOutputExplanation_traversalSets(ctx, field) + }, + func(ctx context.Context) (any, error) { + return obj.TraversalSets, nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v int) graphql.Marshaler { + return ec.marshalNInt2int(ctx, selections, v) + }, + true, + true, + ) +} +func (ec *executionContext) fieldContext_DataframeRecipePhysicalOutputExplanation_traversalSets(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + return graphql.NewScalarFieldContext("DataframeRecipePhysicalOutputExplanation", field, false, false, errors.New("field of type Int does not have child fields")) } -func (ec *executionContext) fieldContext___Directive_description(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { - fc = &graphql.FieldContext{ - Object: "__Directive", - Field: field, - IsMethod: true, - IsResolver: false, - Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { - return nil, errors.New("field of type String does not have child fields") +func (ec *executionContext) _DataframeRecipePhysicalOutputExplanation_endpointTraversalCount(ctx context.Context, field graphql.CollectedField, obj *model.DataframeRecipePhysicalOutputExplanation) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_DataframeRecipePhysicalOutputExplanation_endpointTraversalCount(ctx, field) }, - } - return fc, nil + func(ctx context.Context) (any, error) { + return obj.EndpointTraversalCount, nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v int) graphql.Marshaler { + return ec.marshalNInt2int(ctx, selections, v) + }, + true, + true, + ) } - -func (ec *executionContext) ___Directive_locations(ctx context.Context, field graphql.CollectedField, obj *introspection.Directive) (ret graphql.Marshaler) { - fc, err := ec.fieldContext___Directive_locations(ctx, field) - if err != nil { - return graphql.Null - } - ctx = graphql.WithFieldContext(ctx, fc) - defer func() { - if r := recover(); r != nil { - ec.Error(ctx, ec.Recover(ctx, r)) - ret = graphql.Null - } - }() - resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { - ctx = rctx // use context from middleware stack in children - return obj.Locations, nil - }) - if err != nil { - ec.Error(ctx, err) - return graphql.Null - } - if resTmp == nil { - if !graphql.HasFieldError(ctx, fc) { - ec.Errorf(ctx, "must not be null") - } - return graphql.Null - } - res := resTmp.([]string) - fc.Result = res - return ec.marshalN__DirectiveLocation2ᚕstringᚄ(ctx, field.Selections, res) +func (ec *executionContext) fieldContext_DataframeRecipePhysicalOutputExplanation_endpointTraversalCount(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + return graphql.NewScalarFieldContext("DataframeRecipePhysicalOutputExplanation", field, false, false, errors.New("field of type Int does not have child fields")) } -func (ec *executionContext) fieldContext___Directive_locations(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { - fc = &graphql.FieldContext{ - Object: "__Directive", - Field: field, - IsMethod: false, - IsResolver: false, - Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { - return nil, errors.New("field of type __DirectiveLocation does not have child fields") +func (ec *executionContext) _DataframeRecipePhysicalOutputExplanation_nativeTraversalCount(ctx context.Context, field graphql.CollectedField, obj *model.DataframeRecipePhysicalOutputExplanation) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_DataframeRecipePhysicalOutputExplanation_nativeTraversalCount(ctx, field) }, - } - return fc, nil + func(ctx context.Context) (any, error) { + return obj.NativeTraversalCount, nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v int) graphql.Marshaler { + return ec.marshalNInt2int(ctx, selections, v) + }, + true, + true, + ) } - -func (ec *executionContext) ___Directive_args(ctx context.Context, field graphql.CollectedField, obj *introspection.Directive) (ret graphql.Marshaler) { - fc, err := ec.fieldContext___Directive_args(ctx, field) - if err != nil { - return graphql.Null - } - ctx = graphql.WithFieldContext(ctx, fc) - defer func() { - if r := recover(); r != nil { - ec.Error(ctx, ec.Recover(ctx, r)) - ret = graphql.Null - } - }() - resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { - ctx = rctx // use context from middleware stack in children - return obj.Args, nil - }) - if err != nil { - ec.Error(ctx, err) - return graphql.Null - } - if resTmp == nil { - if !graphql.HasFieldError(ctx, fc) { - ec.Errorf(ctx, "must not be null") - } - return graphql.Null - } - res := resTmp.([]introspection.InputValue) - fc.Result = res - return ec.marshalN__InputValue2ᚕgithubᚗcomᚋ99designsᚋgqlgenᚋgraphqlᚋintrospectionᚐInputValueᚄ(ctx, field.Selections, res) +func (ec *executionContext) fieldContext_DataframeRecipePhysicalOutputExplanation_nativeTraversalCount(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + return graphql.NewScalarFieldContext("DataframeRecipePhysicalOutputExplanation", field, false, false, errors.New("field of type Int does not have child fields")) } -func (ec *executionContext) fieldContext___Directive_args(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { - fc = &graphql.FieldContext{ - Object: "__Directive", - Field: field, - IsMethod: false, - IsResolver: false, - Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { - switch field.Name { - case "name": - return ec.fieldContext___InputValue_name(ctx, field) - case "description": - return ec.fieldContext___InputValue_description(ctx, field) - case "type": - return ec.fieldContext___InputValue_type(ctx, field) - case "defaultValue": - return ec.fieldContext___InputValue_defaultValue(ctx, field) - case "isDeprecated": - return ec.fieldContext___InputValue_isDeprecated(ctx, field) - case "deprecationReason": - return ec.fieldContext___InputValue_deprecationReason(ctx, field) - } - return nil, fmt.Errorf("no field named %q was found under type __InputValue", field.Name) +func (ec *executionContext) _DataframeRecipePhysicalOutputExplanation_sharedTraversalCount(ctx context.Context, field graphql.CollectedField, obj *model.DataframeRecipePhysicalOutputExplanation) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_DataframeRecipePhysicalOutputExplanation_sharedTraversalCount(ctx, field) }, - } - defer func() { - if r := recover(); r != nil { - err = ec.Recover(ctx, r) - ec.Error(ctx, err) - } - }() - ctx = graphql.WithFieldContext(ctx, fc) - if fc.Args, err = ec.field___Directive_args_args(ctx, field.ArgumentMap(ec.Variables)); err != nil { - ec.Error(ctx, err) - return fc, err - } - return fc, nil + func(ctx context.Context) (any, error) { + return obj.SharedTraversalCount, nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v int) graphql.Marshaler { + return ec.marshalNInt2int(ctx, selections, v) + }, + true, + true, + ) +} +func (ec *executionContext) fieldContext_DataframeRecipePhysicalOutputExplanation_sharedTraversalCount(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + return graphql.NewScalarFieldContext("DataframeRecipePhysicalOutputExplanation", field, false, false, errors.New("field of type Int does not have child fields")) } -func (ec *executionContext) ___Directive_isRepeatable(ctx context.Context, field graphql.CollectedField, obj *introspection.Directive) (ret graphql.Marshaler) { - fc, err := ec.fieldContext___Directive_isRepeatable(ctx, field) - if err != nil { - return graphql.Null - } - ctx = graphql.WithFieldContext(ctx, fc) - defer func() { - if r := recover(); r != nil { - ec.Error(ctx, ec.Recover(ctx, r)) - ret = graphql.Null - } - }() - resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { - ctx = rctx // use context from middleware stack in children - return obj.IsRepeatable, nil - }) - if err != nil { - ec.Error(ctx, err) - return graphql.Null - } - if resTmp == nil { - if !graphql.HasFieldError(ctx, fc) { - ec.Errorf(ctx, "must not be null") - } - return graphql.Null - } - res := resTmp.(bool) - fc.Result = res - return ec.marshalNBoolean2bool(ctx, field.Selections, res) +func (ec *executionContext) _DataframeRecipePhysicalOutputExplanation_requiredMatchReuseCount(ctx context.Context, field graphql.CollectedField, obj *model.DataframeRecipePhysicalOutputExplanation) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_DataframeRecipePhysicalOutputExplanation_requiredMatchReuseCount(ctx, field) + }, + func(ctx context.Context) (any, error) { + return obj.RequiredMatchReuseCount, nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v int) graphql.Marshaler { + return ec.marshalNInt2int(ctx, selections, v) + }, + true, + true, + ) +} +func (ec *executionContext) fieldContext_DataframeRecipePhysicalOutputExplanation_requiredMatchReuseCount(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + return graphql.NewScalarFieldContext("DataframeRecipePhysicalOutputExplanation", field, false, false, errors.New("field of type Int does not have child fields")) } -func (ec *executionContext) fieldContext___Directive_isRepeatable(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { +func (ec *executionContext) _DataframeRecipePhysicalOutputExplanation_optimization(ctx context.Context, field graphql.CollectedField, obj *model.DataframeRecipePhysicalOutputExplanation) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_DataframeRecipePhysicalOutputExplanation_optimization(ctx, field) + }, + func(ctx context.Context) (any, error) { + return obj.Optimization, nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v *model.DataframeRecipeOptimizationExplanation) graphql.Marshaler { + return ec.marshalNDataframeRecipeOptimizationExplanation2ᚖgithubᚗcomᚋcalyprᚋloomᚋgraphqlapiᚋmodelᚐDataframeRecipeOptimizationExplanation(ctx, selections, v) + }, + true, + true, + ) +} +func (ec *executionContext) fieldContext_DataframeRecipePhysicalOutputExplanation_optimization(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { fc = &graphql.FieldContext{ - Object: "__Directive", + Object: "DataframeRecipePhysicalOutputExplanation", Field: field, IsMethod: false, IsResolver: false, Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { - return nil, errors.New("field of type Boolean does not have child fields") + return ec.childFields_DataframeRecipeOptimizationExplanation(ctx, field) }, } return fc, nil } -func (ec *executionContext) ___EnumValue_name(ctx context.Context, field graphql.CollectedField, obj *introspection.EnumValue) (ret graphql.Marshaler) { - fc, err := ec.fieldContext___EnumValue_name(ctx, field) - if err != nil { - return graphql.Null - } - ctx = graphql.WithFieldContext(ctx, fc) - defer func() { - if r := recover(); r != nil { - ec.Error(ctx, ec.Recover(ctx, r)) - ret = graphql.Null - } - }() - resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { - ctx = rctx // use context from middleware stack in children - return obj.Name, nil - }) - if err != nil { - ec.Error(ctx, err) - return graphql.Null - } - if resTmp == nil { - if !graphql.HasFieldError(ctx, fc) { - ec.Errorf(ctx, "must not be null") - } - return graphql.Null - } - res := resTmp.(string) - fc.Result = res - return ec.marshalNString2string(ctx, field.Selections, res) +func (ec *executionContext) _DataframeRecipePhysicalOutputExplanation_live(ctx context.Context, field graphql.CollectedField, obj *model.DataframeRecipePhysicalOutputExplanation) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_DataframeRecipePhysicalOutputExplanation_live(ctx, field) + }, + func(ctx context.Context) (any, error) { + return obj.Live, nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v *model.DataframeRecipeArangoAssessment) graphql.Marshaler { + return ec.marshalODataframeRecipeArangoAssessment2ᚖgithubᚗcomᚋcalyprᚋloomᚋgraphqlapiᚋmodelᚐDataframeRecipeArangoAssessment(ctx, selections, v) + }, + true, + false, + ) } - -func (ec *executionContext) fieldContext___EnumValue_name(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { +func (ec *executionContext) fieldContext_DataframeRecipePhysicalOutputExplanation_live(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { fc = &graphql.FieldContext{ - Object: "__EnumValue", + Object: "DataframeRecipePhysicalOutputExplanation", Field: field, IsMethod: false, IsResolver: false, Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { - return nil, errors.New("field of type String does not have child fields") + return ec.childFields_DataframeRecipeArangoAssessment(ctx, field) }, } return fc, nil } -func (ec *executionContext) ___EnumValue_description(ctx context.Context, field graphql.CollectedField, obj *introspection.EnumValue) (ret graphql.Marshaler) { - fc, err := ec.fieldContext___EnumValue_description(ctx, field) - if err != nil { - return graphql.Null - } - ctx = graphql.WithFieldContext(ctx, fc) - defer func() { - if r := recover(); r != nil { - ec.Error(ctx, ec.Recover(ctx, r)) - ret = graphql.Null - } - }() - resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { - ctx = rctx // use context from middleware stack in children - return obj.Description(), nil - }) - if err != nil { - ec.Error(ctx, err) - return graphql.Null - } - if resTmp == nil { - return graphql.Null - } - res := resTmp.(*string) - fc.Result = res - return ec.marshalOString2ᚖstring(ctx, field.Selections, res) +func (ec *executionContext) _DataframeRecipePreflight_name(ctx context.Context, field graphql.CollectedField, obj *model.DataframeRecipePreflight) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_DataframeRecipePreflight_name(ctx, field) + }, + func(ctx context.Context) (any, error) { + return obj.Name, nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v string) graphql.Marshaler { + return ec.marshalNString2string(ctx, selections, v) + }, + true, + true, + ) +} +func (ec *executionContext) fieldContext_DataframeRecipePreflight_name(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + return graphql.NewScalarFieldContext("DataframeRecipePreflight", field, false, false, errors.New("field of type String does not have child fields")) } -func (ec *executionContext) fieldContext___EnumValue_description(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { - fc = &graphql.FieldContext{ - Object: "__EnumValue", - Field: field, - IsMethod: true, - IsResolver: false, - Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { - return nil, errors.New("field of type String does not have child fields") +func (ec *executionContext) _DataframeRecipePreflight_recipeDigest(ctx context.Context, field graphql.CollectedField, obj *model.DataframeRecipePreflight) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_DataframeRecipePreflight_recipeDigest(ctx, field) }, - } - return fc, nil + func(ctx context.Context) (any, error) { + return obj.RecipeDigest, nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v string) graphql.Marshaler { + return ec.marshalNString2string(ctx, selections, v) + }, + true, + true, + ) +} +func (ec *executionContext) fieldContext_DataframeRecipePreflight_recipeDigest(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + return graphql.NewScalarFieldContext("DataframeRecipePreflight", field, false, false, errors.New("field of type String does not have child fields")) } -func (ec *executionContext) ___EnumValue_isDeprecated(ctx context.Context, field graphql.CollectedField, obj *introspection.EnumValue) (ret graphql.Marshaler) { - fc, err := ec.fieldContext___EnumValue_isDeprecated(ctx, field) - if err != nil { - return graphql.Null - } - ctx = graphql.WithFieldContext(ctx, fc) - defer func() { - if r := recover(); r != nil { - ec.Error(ctx, ec.Recover(ctx, r)) - ret = graphql.Null - } - }() - resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { - ctx = rctx // use context from middleware stack in children - return obj.IsDeprecated(), nil - }) - if err != nil { - ec.Error(ctx, err) - return graphql.Null - } - if resTmp == nil { - if !graphql.HasFieldError(ctx, fc) { - ec.Errorf(ctx, "must not be null") - } - return graphql.Null - } - res := resTmp.(bool) - fc.Result = res - return ec.marshalNBoolean2bool(ctx, field.Selections, res) +func (ec *executionContext) _DataframeRecipePreflight_resolvedSchemaDigest(ctx context.Context, field graphql.CollectedField, obj *model.DataframeRecipePreflight) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_DataframeRecipePreflight_resolvedSchemaDigest(ctx, field) + }, + func(ctx context.Context) (any, error) { + return obj.ResolvedSchemaDigest, nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v string) graphql.Marshaler { + return ec.marshalNString2string(ctx, selections, v) + }, + true, + true, + ) +} +func (ec *executionContext) fieldContext_DataframeRecipePreflight_resolvedSchemaDigest(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + return graphql.NewScalarFieldContext("DataframeRecipePreflight", field, false, false, errors.New("field of type String does not have child fields")) } -func (ec *executionContext) fieldContext___EnumValue_isDeprecated(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { - fc = &graphql.FieldContext{ - Object: "__EnumValue", - Field: field, - IsMethod: true, - IsResolver: false, - Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { - return nil, errors.New("field of type Boolean does not have child fields") +func (ec *executionContext) _DataframeRecipePreflight_sourceGeneration(ctx context.Context, field graphql.CollectedField, obj *model.DataframeRecipePreflight) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_DataframeRecipePreflight_sourceGeneration(ctx, field) }, - } - return fc, nil + func(ctx context.Context) (any, error) { + return obj.SourceGeneration, nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v string) graphql.Marshaler { + return ec.marshalNString2string(ctx, selections, v) + }, + true, + true, + ) +} +func (ec *executionContext) fieldContext_DataframeRecipePreflight_sourceGeneration(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + return graphql.NewScalarFieldContext("DataframeRecipePreflight", field, false, false, errors.New("field of type String does not have child fields")) } -func (ec *executionContext) ___EnumValue_deprecationReason(ctx context.Context, field graphql.CollectedField, obj *introspection.EnumValue) (ret graphql.Marshaler) { - fc, err := ec.fieldContext___EnumValue_deprecationReason(ctx, field) - if err != nil { - return graphql.Null - } - ctx = graphql.WithFieldContext(ctx, fc) - defer func() { - if r := recover(); r != nil { - ec.Error(ctx, ec.Recover(ctx, r)) - ret = graphql.Null - } - }() - resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { - ctx = rctx // use context from middleware stack in children - return obj.DeprecationReason(), nil - }) - if err != nil { - ec.Error(ctx, err) - return graphql.Null - } - if resTmp == nil { - return graphql.Null - } - res := resTmp.(*string) - fc.Result = res - return ec.marshalOString2ᚖstring(ctx, field.Selections, res) +func (ec *executionContext) _DataframeRecipePreflight_scopeDigest(ctx context.Context, field graphql.CollectedField, obj *model.DataframeRecipePreflight) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_DataframeRecipePreflight_scopeDigest(ctx, field) + }, + func(ctx context.Context) (any, error) { + return obj.ScopeDigest, nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v string) graphql.Marshaler { + return ec.marshalNString2string(ctx, selections, v) + }, + true, + true, + ) +} +func (ec *executionContext) fieldContext_DataframeRecipePreflight_scopeDigest(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + return graphql.NewScalarFieldContext("DataframeRecipePreflight", field, false, false, errors.New("field of type String does not have child fields")) } -func (ec *executionContext) fieldContext___EnumValue_deprecationReason(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { +func (ec *executionContext) _DataframeRecipePreflight_columns(ctx context.Context, field graphql.CollectedField, obj *model.DataframeRecipePreflight) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_DataframeRecipePreflight_columns(ctx, field) + }, + func(ctx context.Context) (any, error) { + return obj.Columns, nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v []*model.DataframeRecipeColumn) graphql.Marshaler { + return ec.marshalNDataframeRecipeColumn2ᚕᚖgithubᚗcomᚋcalyprᚋloomᚋgraphqlapiᚋmodelᚐDataframeRecipeColumnᚄ(ctx, selections, v) + }, + true, + true, + ) +} +func (ec *executionContext) fieldContext_DataframeRecipePreflight_columns(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { fc = &graphql.FieldContext{ - Object: "__EnumValue", + Object: "DataframeRecipePreflight", Field: field, - IsMethod: true, + IsMethod: false, IsResolver: false, Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { - return nil, errors.New("field of type String does not have child fields") + return ec.childFields_DataframeRecipeColumn(ctx, field) }, } return fc, nil } -func (ec *executionContext) ___Field_name(ctx context.Context, field graphql.CollectedField, obj *introspection.Field) (ret graphql.Marshaler) { - fc, err := ec.fieldContext___Field_name(ctx, field) - if err != nil { - return graphql.Null - } - ctx = graphql.WithFieldContext(ctx, fc) - defer func() { - if r := recover(); r != nil { - ec.Error(ctx, ec.Recover(ctx, r)) - ret = graphql.Null - } - }() - resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { - ctx = rctx // use context from middleware stack in children - return obj.Name, nil - }) - if err != nil { - ec.Error(ctx, err) - return graphql.Null - } - if resTmp == nil { - if !graphql.HasFieldError(ctx, fc) { - ec.Errorf(ctx, "must not be null") - } - return graphql.Null - } - res := resTmp.(string) - fc.Result = res - return ec.marshalNString2string(ctx, field.Selections, res) +func (ec *executionContext) _DataframeRecipePreview_name(ctx context.Context, field graphql.CollectedField, obj *model.DataframeRecipePreview) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_DataframeRecipePreview_name(ctx, field) + }, + func(ctx context.Context) (any, error) { + return obj.Name, nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v string) graphql.Marshaler { + return ec.marshalNString2string(ctx, selections, v) + }, + true, + true, + ) +} +func (ec *executionContext) fieldContext_DataframeRecipePreview_name(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + return graphql.NewScalarFieldContext("DataframeRecipePreview", field, false, false, errors.New("field of type String does not have child fields")) } -func (ec *executionContext) fieldContext___Field_name(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { - fc = &graphql.FieldContext{ - Object: "__Field", - Field: field, - IsMethod: false, - IsResolver: false, - Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { - return nil, errors.New("field of type String does not have child fields") +func (ec *executionContext) _DataframeRecipePreview_recipeDigest(ctx context.Context, field graphql.CollectedField, obj *model.DataframeRecipePreview) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_DataframeRecipePreview_recipeDigest(ctx, field) }, - } - return fc, nil + func(ctx context.Context) (any, error) { + return obj.RecipeDigest, nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v string) graphql.Marshaler { + return ec.marshalNString2string(ctx, selections, v) + }, + true, + true, + ) } - -func (ec *executionContext) ___Field_description(ctx context.Context, field graphql.CollectedField, obj *introspection.Field) (ret graphql.Marshaler) { - fc, err := ec.fieldContext___Field_description(ctx, field) - if err != nil { - return graphql.Null - } - ctx = graphql.WithFieldContext(ctx, fc) - defer func() { - if r := recover(); r != nil { - ec.Error(ctx, ec.Recover(ctx, r)) - ret = graphql.Null - } - }() - resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { - ctx = rctx // use context from middleware stack in children - return obj.Description(), nil - }) - if err != nil { - ec.Error(ctx, err) - return graphql.Null - } - if resTmp == nil { - return graphql.Null - } - res := resTmp.(*string) - fc.Result = res - return ec.marshalOString2ᚖstring(ctx, field.Selections, res) +func (ec *executionContext) fieldContext_DataframeRecipePreview_recipeDigest(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + return graphql.NewScalarFieldContext("DataframeRecipePreview", field, false, false, errors.New("field of type String does not have child fields")) } -func (ec *executionContext) fieldContext___Field_description(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { - fc = &graphql.FieldContext{ - Object: "__Field", - Field: field, - IsMethod: true, - IsResolver: false, - Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { - return nil, errors.New("field of type String does not have child fields") +func (ec *executionContext) _DataframeRecipePreview_resolvedSchemaDigest(ctx context.Context, field graphql.CollectedField, obj *model.DataframeRecipePreview) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_DataframeRecipePreview_resolvedSchemaDigest(ctx, field) }, - } - return fc, nil + func(ctx context.Context) (any, error) { + return obj.ResolvedSchemaDigest, nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v string) graphql.Marshaler { + return ec.marshalNString2string(ctx, selections, v) + }, + true, + true, + ) +} +func (ec *executionContext) fieldContext_DataframeRecipePreview_resolvedSchemaDigest(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + return graphql.NewScalarFieldContext("DataframeRecipePreview", field, false, false, errors.New("field of type String does not have child fields")) } -func (ec *executionContext) ___Field_args(ctx context.Context, field graphql.CollectedField, obj *introspection.Field) (ret graphql.Marshaler) { - fc, err := ec.fieldContext___Field_args(ctx, field) - if err != nil { - return graphql.Null - } - ctx = graphql.WithFieldContext(ctx, fc) - defer func() { - if r := recover(); r != nil { - ec.Error(ctx, ec.Recover(ctx, r)) - ret = graphql.Null - } - }() - resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { - ctx = rctx // use context from middleware stack in children - return obj.Args, nil - }) - if err != nil { - ec.Error(ctx, err) - return graphql.Null - } - if resTmp == nil { - if !graphql.HasFieldError(ctx, fc) { - ec.Errorf(ctx, "must not be null") - } - return graphql.Null - } - res := resTmp.([]introspection.InputValue) - fc.Result = res - return ec.marshalN__InputValue2ᚕgithubᚗcomᚋ99designsᚋgqlgenᚋgraphqlᚋintrospectionᚐInputValueᚄ(ctx, field.Selections, res) +func (ec *executionContext) _DataframeRecipePreview_sourceGeneration(ctx context.Context, field graphql.CollectedField, obj *model.DataframeRecipePreview) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_DataframeRecipePreview_sourceGeneration(ctx, field) + }, + func(ctx context.Context) (any, error) { + return obj.SourceGeneration, nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v string) graphql.Marshaler { + return ec.marshalNString2string(ctx, selections, v) + }, + true, + true, + ) +} +func (ec *executionContext) fieldContext_DataframeRecipePreview_sourceGeneration(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + return graphql.NewScalarFieldContext("DataframeRecipePreview", field, false, false, errors.New("field of type String does not have child fields")) } -func (ec *executionContext) fieldContext___Field_args(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { +func (ec *executionContext) _DataframeRecipePreview_outputs(ctx context.Context, field graphql.CollectedField, obj *model.DataframeRecipePreview) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_DataframeRecipePreview_outputs(ctx, field) + }, + func(ctx context.Context) (any, error) { + return obj.Outputs, nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v []*model.DataframeRecipePreviewOutput) graphql.Marshaler { + return ec.marshalNDataframeRecipePreviewOutput2ᚕᚖgithubᚗcomᚋcalyprᚋloomᚋgraphqlapiᚋmodelᚐDataframeRecipePreviewOutputᚄ(ctx, selections, v) + }, + true, + true, + ) +} +func (ec *executionContext) fieldContext_DataframeRecipePreview_outputs(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { fc = &graphql.FieldContext{ - Object: "__Field", + Object: "DataframeRecipePreview", Field: field, IsMethod: false, IsResolver: false, Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { - switch field.Name { - case "name": - return ec.fieldContext___InputValue_name(ctx, field) - case "description": - return ec.fieldContext___InputValue_description(ctx, field) - case "type": - return ec.fieldContext___InputValue_type(ctx, field) - case "defaultValue": - return ec.fieldContext___InputValue_defaultValue(ctx, field) - case "isDeprecated": - return ec.fieldContext___InputValue_isDeprecated(ctx, field) - case "deprecationReason": - return ec.fieldContext___InputValue_deprecationReason(ctx, field) - } - return nil, fmt.Errorf("no field named %q was found under type __InputValue", field.Name) + return ec.childFields_DataframeRecipePreviewOutput(ctx, field) }, } - defer func() { - if r := recover(); r != nil { - err = ec.Recover(ctx, r) - ec.Error(ctx, err) - } - }() - ctx = graphql.WithFieldContext(ctx, fc) - if fc.Args, err = ec.field___Field_args_args(ctx, field.ArgumentMap(ec.Variables)); err != nil { - ec.Error(ctx, err) - return fc, err - } return fc, nil } -func (ec *executionContext) ___Field_type(ctx context.Context, field graphql.CollectedField, obj *introspection.Field) (ret graphql.Marshaler) { - fc, err := ec.fieldContext___Field_type(ctx, field) - if err != nil { - return graphql.Null - } - ctx = graphql.WithFieldContext(ctx, fc) - defer func() { - if r := recover(); r != nil { - ec.Error(ctx, ec.Recover(ctx, r)) - ret = graphql.Null - } - }() - resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { - ctx = rctx // use context from middleware stack in children - return obj.Type, nil - }) - if err != nil { - ec.Error(ctx, err) - return graphql.Null - } - if resTmp == nil { - if !graphql.HasFieldError(ctx, fc) { - ec.Errorf(ctx, "must not be null") - } - return graphql.Null - } - res := resTmp.(*introspection.Type) - fc.Result = res - return ec.marshalN__Type2ᚖgithubᚗcomᚋ99designsᚋgqlgenᚋgraphqlᚋintrospectionᚐType(ctx, field.Selections, res) +func (ec *executionContext) _DataframeRecipePreviewOutput_name(ctx context.Context, field graphql.CollectedField, obj *model.DataframeRecipePreviewOutput) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_DataframeRecipePreviewOutput_name(ctx, field) + }, + func(ctx context.Context) (any, error) { + return obj.Name, nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v string) graphql.Marshaler { + return ec.marshalNString2string(ctx, selections, v) + }, + true, + true, + ) +} +func (ec *executionContext) fieldContext_DataframeRecipePreviewOutput_name(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + return graphql.NewScalarFieldContext("DataframeRecipePreviewOutput", field, false, false, errors.New("field of type String does not have child fields")) } -func (ec *executionContext) fieldContext___Field_type(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { - fc = &graphql.FieldContext{ - Object: "__Field", - Field: field, - IsMethod: false, - IsResolver: false, - Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { - switch field.Name { - case "kind": - return ec.fieldContext___Type_kind(ctx, field) - case "name": - return ec.fieldContext___Type_name(ctx, field) - case "description": - return ec.fieldContext___Type_description(ctx, field) - case "fields": - return ec.fieldContext___Type_fields(ctx, field) - case "interfaces": - return ec.fieldContext___Type_interfaces(ctx, field) - case "possibleTypes": - return ec.fieldContext___Type_possibleTypes(ctx, field) - case "enumValues": - return ec.fieldContext___Type_enumValues(ctx, field) - case "inputFields": - return ec.fieldContext___Type_inputFields(ctx, field) - case "ofType": - return ec.fieldContext___Type_ofType(ctx, field) - case "specifiedByURL": - return ec.fieldContext___Type_specifiedByURL(ctx, field) - case "isOneOf": - return ec.fieldContext___Type_isOneOf(ctx, field) - } - return nil, fmt.Errorf("no field named %q was found under type __Type", field.Name) +func (ec *executionContext) _DataframeRecipePreviewOutput_columns(ctx context.Context, field graphql.CollectedField, obj *model.DataframeRecipePreviewOutput) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_DataframeRecipePreviewOutput_columns(ctx, field) }, - } - return fc, nil + func(ctx context.Context) (any, error) { + return obj.Columns, nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v []string) graphql.Marshaler { + return ec.marshalNString2ᚕstringᚄ(ctx, selections, v) + }, + true, + true, + ) +} +func (ec *executionContext) fieldContext_DataframeRecipePreviewOutput_columns(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + return graphql.NewScalarFieldContext("DataframeRecipePreviewOutput", field, false, false, errors.New("field of type String does not have child fields")) } -func (ec *executionContext) ___Field_isDeprecated(ctx context.Context, field graphql.CollectedField, obj *introspection.Field) (ret graphql.Marshaler) { - fc, err := ec.fieldContext___Field_isDeprecated(ctx, field) - if err != nil { - return graphql.Null - } - ctx = graphql.WithFieldContext(ctx, fc) - defer func() { - if r := recover(); r != nil { - ec.Error(ctx, ec.Recover(ctx, r)) - ret = graphql.Null - } - }() - resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { - ctx = rctx // use context from middleware stack in children - return obj.IsDeprecated(), nil - }) - if err != nil { - ec.Error(ctx, err) - return graphql.Null - } - if resTmp == nil { - if !graphql.HasFieldError(ctx, fc) { - ec.Errorf(ctx, "must not be null") - } - return graphql.Null - } - res := resTmp.(bool) - fc.Result = res - return ec.marshalNBoolean2bool(ctx, field.Selections, res) +func (ec *executionContext) _DataframeRecipePreviewOutput_rows(ctx context.Context, field graphql.CollectedField, obj *model.DataframeRecipePreviewOutput) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_DataframeRecipePreviewOutput_rows(ctx, field) + }, + func(ctx context.Context) (any, error) { + return obj.Rows, nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v json.RawMessage) graphql.Marshaler { + return ec.marshalNJSON2encodingᚋjsonᚐRawMessage(ctx, selections, v) + }, + true, + true, + ) +} +func (ec *executionContext) fieldContext_DataframeRecipePreviewOutput_rows(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + return graphql.NewScalarFieldContext("DataframeRecipePreviewOutput", field, false, false, errors.New("field of type JSON does not have child fields")) } -func (ec *executionContext) fieldContext___Field_isDeprecated(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { - fc = &graphql.FieldContext{ - Object: "__Field", - Field: field, - IsMethod: true, - IsResolver: false, - Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { - return nil, errors.New("field of type Boolean does not have child fields") +func (ec *executionContext) _DataframeRecipePreviewOutput_csv(ctx context.Context, field graphql.CollectedField, obj *model.DataframeRecipePreviewOutput) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_DataframeRecipePreviewOutput_csv(ctx, field) }, - } - return fc, nil + func(ctx context.Context) (any, error) { + return obj.CSV, nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v *string) graphql.Marshaler { + return ec.marshalOString2ᚖstring(ctx, selections, v) + }, + true, + false, + ) +} +func (ec *executionContext) fieldContext_DataframeRecipePreviewOutput_csv(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + return graphql.NewScalarFieldContext("DataframeRecipePreviewOutput", field, false, false, errors.New("field of type String does not have child fields")) } -func (ec *executionContext) ___Field_deprecationReason(ctx context.Context, field graphql.CollectedField, obj *introspection.Field) (ret graphql.Marshaler) { - fc, err := ec.fieldContext___Field_deprecationReason(ctx, field) - if err != nil { - return graphql.Null - } - ctx = graphql.WithFieldContext(ctx, fc) - defer func() { - if r := recover(); r != nil { - ec.Error(ctx, ec.Recover(ctx, r)) - ret = graphql.Null - } - }() - resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { - ctx = rctx // use context from middleware stack in children - return obj.DeprecationReason(), nil - }) - if err != nil { - ec.Error(ctx, err) - return graphql.Null - } - if resTmp == nil { - return graphql.Null - } - res := resTmp.(*string) - fc.Result = res - return ec.marshalOString2ᚖstring(ctx, field.Selections, res) +func (ec *executionContext) _DataframeRecipePreviewOutput_rowCount(ctx context.Context, field graphql.CollectedField, obj *model.DataframeRecipePreviewOutput) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_DataframeRecipePreviewOutput_rowCount(ctx, field) + }, + func(ctx context.Context) (any, error) { + return obj.RowCount, nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v int) graphql.Marshaler { + return ec.marshalNInt2int(ctx, selections, v) + }, + true, + true, + ) +} +func (ec *executionContext) fieldContext_DataframeRecipePreviewOutput_rowCount(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + return graphql.NewScalarFieldContext("DataframeRecipePreviewOutput", field, false, false, errors.New("field of type Int does not have child fields")) } -func (ec *executionContext) fieldContext___Field_deprecationReason(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { - fc = &graphql.FieldContext{ - Object: "__Field", - Field: field, - IsMethod: true, - IsResolver: false, - Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { - return nil, errors.New("field of type String does not have child fields") +func (ec *executionContext) _DataframeRecipeResult_name(ctx context.Context, field graphql.CollectedField, obj *model.DataframeRecipeResult) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_DataframeRecipeResult_name(ctx, field) }, - } - return fc, nil + func(ctx context.Context) (any, error) { + return obj.Name, nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v string) graphql.Marshaler { + return ec.marshalNString2string(ctx, selections, v) + }, + true, + true, + ) +} +func (ec *executionContext) fieldContext_DataframeRecipeResult_name(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + return graphql.NewScalarFieldContext("DataframeRecipeResult", field, false, false, errors.New("field of type String does not have child fields")) } -func (ec *executionContext) ___InputValue_name(ctx context.Context, field graphql.CollectedField, obj *introspection.InputValue) (ret graphql.Marshaler) { - fc, err := ec.fieldContext___InputValue_name(ctx, field) - if err != nil { - return graphql.Null - } - ctx = graphql.WithFieldContext(ctx, fc) - defer func() { - if r := recover(); r != nil { - ec.Error(ctx, ec.Recover(ctx, r)) - ret = graphql.Null - } - }() - resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { - ctx = rctx // use context from middleware stack in children - return obj.Name, nil - }) - if err != nil { - ec.Error(ctx, err) - return graphql.Null - } - if resTmp == nil { - if !graphql.HasFieldError(ctx, fc) { - ec.Errorf(ctx, "must not be null") - } - return graphql.Null - } - res := resTmp.(string) - fc.Result = res - return ec.marshalNString2string(ctx, field.Selections, res) +func (ec *executionContext) _DataframeRecipeResult_recipeDigest(ctx context.Context, field graphql.CollectedField, obj *model.DataframeRecipeResult) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_DataframeRecipeResult_recipeDigest(ctx, field) + }, + func(ctx context.Context) (any, error) { + return obj.RecipeDigest, nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v string) graphql.Marshaler { + return ec.marshalNString2string(ctx, selections, v) + }, + true, + true, + ) +} +func (ec *executionContext) fieldContext_DataframeRecipeResult_recipeDigest(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + return graphql.NewScalarFieldContext("DataframeRecipeResult", field, false, false, errors.New("field of type String does not have child fields")) } -func (ec *executionContext) fieldContext___InputValue_name(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { +func (ec *executionContext) _DataframeRecipeResult_resolvedSchemaDigest(ctx context.Context, field graphql.CollectedField, obj *model.DataframeRecipeResult) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_DataframeRecipeResult_resolvedSchemaDigest(ctx, field) + }, + func(ctx context.Context) (any, error) { + return obj.ResolvedSchemaDigest, nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v string) graphql.Marshaler { + return ec.marshalNString2string(ctx, selections, v) + }, + true, + true, + ) +} +func (ec *executionContext) fieldContext_DataframeRecipeResult_resolvedSchemaDigest(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + return graphql.NewScalarFieldContext("DataframeRecipeResult", field, false, false, errors.New("field of type String does not have child fields")) +} + +func (ec *executionContext) _DataframeRecipeResult_sourceGeneration(ctx context.Context, field graphql.CollectedField, obj *model.DataframeRecipeResult) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_DataframeRecipeResult_sourceGeneration(ctx, field) + }, + func(ctx context.Context) (any, error) { + return obj.SourceGeneration, nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v string) graphql.Marshaler { + return ec.marshalNString2string(ctx, selections, v) + }, + true, + true, + ) +} +func (ec *executionContext) fieldContext_DataframeRecipeResult_sourceGeneration(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + return graphql.NewScalarFieldContext("DataframeRecipeResult", field, false, false, errors.New("field of type String does not have child fields")) +} + +func (ec *executionContext) _DataframeRecipeResult_outputs(ctx context.Context, field graphql.CollectedField, obj *model.DataframeRecipeResult) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_DataframeRecipeResult_outputs(ctx, field) + }, + func(ctx context.Context) (any, error) { + return obj.Outputs, nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v []*model.DataframeRecipeResultOutput) graphql.Marshaler { + return ec.marshalNDataframeRecipeResultOutput2ᚕᚖgithubᚗcomᚋcalyprᚋloomᚋgraphqlapiᚋmodelᚐDataframeRecipeResultOutputᚄ(ctx, selections, v) + }, + true, + true, + ) +} +func (ec *executionContext) fieldContext_DataframeRecipeResult_outputs(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { fc = &graphql.FieldContext{ - Object: "__InputValue", + Object: "DataframeRecipeResult", Field: field, IsMethod: false, IsResolver: false, Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { - return nil, errors.New("field of type String does not have child fields") + return ec.childFields_DataframeRecipeResultOutput(ctx, field) }, } return fc, nil } -func (ec *executionContext) ___InputValue_description(ctx context.Context, field graphql.CollectedField, obj *introspection.InputValue) (ret graphql.Marshaler) { - fc, err := ec.fieldContext___InputValue_description(ctx, field) - if err != nil { - return graphql.Null - } - ctx = graphql.WithFieldContext(ctx, fc) - defer func() { - if r := recover(); r != nil { - ec.Error(ctx, ec.Recover(ctx, r)) - ret = graphql.Null - } - }() - resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { - ctx = rctx // use context from middleware stack in children - return obj.Description(), nil - }) - if err != nil { - ec.Error(ctx, err) - return graphql.Null - } - if resTmp == nil { - return graphql.Null - } - res := resTmp.(*string) - fc.Result = res - return ec.marshalOString2ᚖstring(ctx, field.Selections, res) +func (ec *executionContext) _DataframeRecipeResultOutput_name(ctx context.Context, field graphql.CollectedField, obj *model.DataframeRecipeResultOutput) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_DataframeRecipeResultOutput_name(ctx, field) + }, + func(ctx context.Context) (any, error) { + return obj.Name, nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v string) graphql.Marshaler { + return ec.marshalNString2string(ctx, selections, v) + }, + true, + true, + ) +} +func (ec *executionContext) fieldContext_DataframeRecipeResultOutput_name(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + return graphql.NewScalarFieldContext("DataframeRecipeResultOutput", field, false, false, errors.New("field of type String does not have child fields")) } -func (ec *executionContext) fieldContext___InputValue_description(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { - fc = &graphql.FieldContext{ - Object: "__InputValue", - Field: field, - IsMethod: true, - IsResolver: false, - Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { - return nil, errors.New("field of type String does not have child fields") +func (ec *executionContext) _DataframeRecipeResultOutput_columns(ctx context.Context, field graphql.CollectedField, obj *model.DataframeRecipeResultOutput) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_DataframeRecipeResultOutput_columns(ctx, field) }, - } - return fc, nil + func(ctx context.Context) (any, error) { + return obj.Columns, nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v []string) graphql.Marshaler { + return ec.marshalNString2ᚕstringᚄ(ctx, selections, v) + }, + true, + true, + ) +} +func (ec *executionContext) fieldContext_DataframeRecipeResultOutput_columns(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + return graphql.NewScalarFieldContext("DataframeRecipeResultOutput", field, false, false, errors.New("field of type String does not have child fields")) } -func (ec *executionContext) ___InputValue_type(ctx context.Context, field graphql.CollectedField, obj *introspection.InputValue) (ret graphql.Marshaler) { - fc, err := ec.fieldContext___InputValue_type(ctx, field) - if err != nil { - return graphql.Null - } - ctx = graphql.WithFieldContext(ctx, fc) - defer func() { - if r := recover(); r != nil { - ec.Error(ctx, ec.Recover(ctx, r)) - ret = graphql.Null - } - }() - resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { - ctx = rctx // use context from middleware stack in children - return obj.Type, nil - }) - if err != nil { - ec.Error(ctx, err) - return graphql.Null - } - if resTmp == nil { - if !graphql.HasFieldError(ctx, fc) { - ec.Errorf(ctx, "must not be null") - } - return graphql.Null - } - res := resTmp.(*introspection.Type) - fc.Result = res - return ec.marshalN__Type2ᚖgithubᚗcomᚋ99designsᚋgqlgenᚋgraphqlᚋintrospectionᚐType(ctx, field.Selections, res) +func (ec *executionContext) _DataframeRecipeResultOutput_rows(ctx context.Context, field graphql.CollectedField, obj *model.DataframeRecipeResultOutput) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_DataframeRecipeResultOutput_rows(ctx, field) + }, + func(ctx context.Context) (any, error) { + return obj.Rows, nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v json.RawMessage) graphql.Marshaler { + return ec.marshalNJSON2encodingᚋjsonᚐRawMessage(ctx, selections, v) + }, + true, + true, + ) +} +func (ec *executionContext) fieldContext_DataframeRecipeResultOutput_rows(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + return graphql.NewScalarFieldContext("DataframeRecipeResultOutput", field, false, false, errors.New("field of type JSON does not have child fields")) } -func (ec *executionContext) fieldContext___InputValue_type(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { - fc = &graphql.FieldContext{ - Object: "__InputValue", - Field: field, - IsMethod: false, - IsResolver: false, - Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { - switch field.Name { - case "kind": - return ec.fieldContext___Type_kind(ctx, field) - case "name": - return ec.fieldContext___Type_name(ctx, field) - case "description": - return ec.fieldContext___Type_description(ctx, field) - case "fields": - return ec.fieldContext___Type_fields(ctx, field) - case "interfaces": - return ec.fieldContext___Type_interfaces(ctx, field) - case "possibleTypes": - return ec.fieldContext___Type_possibleTypes(ctx, field) - case "enumValues": - return ec.fieldContext___Type_enumValues(ctx, field) - case "inputFields": - return ec.fieldContext___Type_inputFields(ctx, field) - case "ofType": - return ec.fieldContext___Type_ofType(ctx, field) - case "specifiedByURL": - return ec.fieldContext___Type_specifiedByURL(ctx, field) - case "isOneOf": - return ec.fieldContext___Type_isOneOf(ctx, field) - } - return nil, fmt.Errorf("no field named %q was found under type __Type", field.Name) +func (ec *executionContext) _DataframeRecipeResultOutput_csv(ctx context.Context, field graphql.CollectedField, obj *model.DataframeRecipeResultOutput) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_DataframeRecipeResultOutput_csv(ctx, field) }, - } - return fc, nil + func(ctx context.Context) (any, error) { + return obj.CSV, nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v *string) graphql.Marshaler { + return ec.marshalOString2ᚖstring(ctx, selections, v) + }, + true, + false, + ) +} +func (ec *executionContext) fieldContext_DataframeRecipeResultOutput_csv(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + return graphql.NewScalarFieldContext("DataframeRecipeResultOutput", field, false, false, errors.New("field of type String does not have child fields")) } -func (ec *executionContext) ___InputValue_defaultValue(ctx context.Context, field graphql.CollectedField, obj *introspection.InputValue) (ret graphql.Marshaler) { - fc, err := ec.fieldContext___InputValue_defaultValue(ctx, field) - if err != nil { - return graphql.Null - } - ctx = graphql.WithFieldContext(ctx, fc) - defer func() { - if r := recover(); r != nil { - ec.Error(ctx, ec.Recover(ctx, r)) - ret = graphql.Null - } - }() - resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { - ctx = rctx // use context from middleware stack in children - return obj.DefaultValue, nil - }) - if err != nil { - ec.Error(ctx, err) - return graphql.Null - } - if resTmp == nil { - return graphql.Null - } - res := resTmp.(*string) - fc.Result = res - return ec.marshalOString2ᚖstring(ctx, field.Selections, res) +func (ec *executionContext) _DataframeRecipeResultOutput_rowCount(ctx context.Context, field graphql.CollectedField, obj *model.DataframeRecipeResultOutput) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_DataframeRecipeResultOutput_rowCount(ctx, field) + }, + func(ctx context.Context) (any, error) { + return obj.RowCount, nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v int) graphql.Marshaler { + return ec.marshalNInt2int(ctx, selections, v) + }, + true, + true, + ) +} +func (ec *executionContext) fieldContext_DataframeRecipeResultOutput_rowCount(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + return graphql.NewScalarFieldContext("DataframeRecipeResultOutput", field, false, false, errors.New("field of type Int does not have child fields")) } -func (ec *executionContext) fieldContext___InputValue_defaultValue(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { +func (ec *executionContext) _DataframeRecipeValidation_name(ctx context.Context, field graphql.CollectedField, obj *model.DataframeRecipeValidation) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_DataframeRecipeValidation_name(ctx, field) + }, + func(ctx context.Context) (any, error) { + return obj.Name, nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v string) graphql.Marshaler { + return ec.marshalNString2string(ctx, selections, v) + }, + true, + true, + ) +} +func (ec *executionContext) fieldContext_DataframeRecipeValidation_name(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + return graphql.NewScalarFieldContext("DataframeRecipeValidation", field, false, false, errors.New("field of type String does not have child fields")) +} + +func (ec *executionContext) _DataframeRecipeValidation_recipeDigest(ctx context.Context, field graphql.CollectedField, obj *model.DataframeRecipeValidation) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_DataframeRecipeValidation_recipeDigest(ctx, field) + }, + func(ctx context.Context) (any, error) { + return obj.RecipeDigest, nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v string) graphql.Marshaler { + return ec.marshalNString2string(ctx, selections, v) + }, + true, + true, + ) +} +func (ec *executionContext) fieldContext_DataframeRecipeValidation_recipeDigest(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + return graphql.NewScalarFieldContext("DataframeRecipeValidation", field, false, false, errors.New("field of type String does not have child fields")) +} + +func (ec *executionContext) _DataframeRecipeValidation_translationVersion(ctx context.Context, field graphql.CollectedField, obj *model.DataframeRecipeValidation) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_DataframeRecipeValidation_translationVersion(ctx, field) + }, + func(ctx context.Context) (any, error) { + return obj.TranslationVersion, nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v string) graphql.Marshaler { + return ec.marshalNString2string(ctx, selections, v) + }, + true, + true, + ) +} +func (ec *executionContext) fieldContext_DataframeRecipeValidation_translationVersion(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + return graphql.NewScalarFieldContext("DataframeRecipeValidation", field, false, false, errors.New("field of type String does not have child fields")) +} + +func (ec *executionContext) _DataframeRecipeValidation_outputs(ctx context.Context, field graphql.CollectedField, obj *model.DataframeRecipeValidation) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_DataframeRecipeValidation_outputs(ctx, field) + }, + func(ctx context.Context) (any, error) { + return obj.Outputs, nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v []*model.DataframeRecipeOutputValidation) graphql.Marshaler { + return ec.marshalNDataframeRecipeOutputValidation2ᚕᚖgithubᚗcomᚋcalyprᚋloomᚋgraphqlapiᚋmodelᚐDataframeRecipeOutputValidationᚄ(ctx, selections, v) + }, + true, + true, + ) +} +func (ec *executionContext) fieldContext_DataframeRecipeValidation_outputs(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { fc = &graphql.FieldContext{ - Object: "__InputValue", + Object: "DataframeRecipeValidation", Field: field, IsMethod: false, IsResolver: false, Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { - return nil, errors.New("field of type String does not have child fields") + return ec.childFields_DataframeRecipeOutputValidation(ctx, field) }, } return fc, nil } -func (ec *executionContext) ___InputValue_isDeprecated(ctx context.Context, field graphql.CollectedField, obj *introspection.InputValue) (ret graphql.Marshaler) { - fc, err := ec.fieldContext___InputValue_isDeprecated(ctx, field) - if err != nil { - return graphql.Null - } - ctx = graphql.WithFieldContext(ctx, fc) - defer func() { - if r := recover(); r != nil { - ec.Error(ctx, ec.Recover(ctx, r)) - ret = graphql.Null - } - }() - resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { - ctx = rctx // use context from middleware stack in children - return obj.IsDeprecated(), nil - }) - if err != nil { - ec.Error(ctx, err) - return graphql.Null - } - if resTmp == nil { - if !graphql.HasFieldError(ctx, fc) { - ec.Errorf(ctx, "must not be null") - } - return graphql.Null - } - res := resTmp.(bool) - fc.Result = res - return ec.marshalNBoolean2bool(ctx, field.Selections, res) +func (ec *executionContext) _DataframeRelatedResourceHints_viaLabel(ctx context.Context, field graphql.CollectedField, obj *model.DataframeRelatedResourceHints) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_DataframeRelatedResourceHints_viaLabel(ctx, field) + }, + func(ctx context.Context) (any, error) { + return obj.ViaLabel, nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v string) graphql.Marshaler { + return ec.marshalNString2string(ctx, selections, v) + }, + true, + true, + ) +} +func (ec *executionContext) fieldContext_DataframeRelatedResourceHints_viaLabel(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + return graphql.NewScalarFieldContext("DataframeRelatedResourceHints", field, false, false, errors.New("field of type String does not have child fields")) } -func (ec *executionContext) fieldContext___InputValue_isDeprecated(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { +func (ec *executionContext) _DataframeRelatedResourceHints_edgeCount(ctx context.Context, field graphql.CollectedField, obj *model.DataframeRelatedResourceHints) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_DataframeRelatedResourceHints_edgeCount(ctx, field) + }, + func(ctx context.Context) (any, error) { + return obj.EdgeCount, nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v int) graphql.Marshaler { + return ec.marshalNInt2int(ctx, selections, v) + }, + true, + true, + ) +} +func (ec *executionContext) fieldContext_DataframeRelatedResourceHints_edgeCount(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + return graphql.NewScalarFieldContext("DataframeRelatedResourceHints", field, false, false, errors.New("field of type Int does not have child fields")) +} + +func (ec *executionContext) _DataframeRelatedResourceHints_target(ctx context.Context, field graphql.CollectedField, obj *model.DataframeRelatedResourceHints) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_DataframeRelatedResourceHints_target(ctx, field) + }, + func(ctx context.Context) (any, error) { + return obj.Target, nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v *model.DataframeResourceHints) graphql.Marshaler { + return ec.marshalNDataframeResourceHints2ᚖgithubᚗcomᚋcalyprᚋloomᚋgraphqlapiᚋmodelᚐDataframeResourceHints(ctx, selections, v) + }, + true, + true, + ) +} +func (ec *executionContext) fieldContext_DataframeRelatedResourceHints_target(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { fc = &graphql.FieldContext{ - Object: "__InputValue", + Object: "DataframeRelatedResourceHints", Field: field, - IsMethod: true, + IsMethod: false, IsResolver: false, Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { - return nil, errors.New("field of type Boolean does not have child fields") + return ec.childFields_DataframeResourceHints(ctx, field) }, } return fc, nil } -func (ec *executionContext) ___InputValue_deprecationReason(ctx context.Context, field graphql.CollectedField, obj *introspection.InputValue) (ret graphql.Marshaler) { - fc, err := ec.fieldContext___InputValue_deprecationReason(ctx, field) - if err != nil { - return graphql.Null - } - ctx = graphql.WithFieldContext(ctx, fc) - defer func() { - if r := recover(); r != nil { - ec.Error(ctx, ec.Recover(ctx, r)) - ret = graphql.Null - } - }() - resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { - ctx = rctx // use context from middleware stack in children - return obj.DeprecationReason(), nil - }) - if err != nil { - ec.Error(ctx, err) - return graphql.Null - } - if resTmp == nil { - return graphql.Null - } - res := resTmp.(*string) - fc.Result = res - return ec.marshalOString2ᚖstring(ctx, field.Selections, res) +func (ec *executionContext) _DataframeResourceHints_resourceType(ctx context.Context, field graphql.CollectedField, obj *model.DataframeResourceHints) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_DataframeResourceHints_resourceType(ctx, field) + }, + func(ctx context.Context) (any, error) { + return obj.ResourceType, nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v string) graphql.Marshaler { + return ec.marshalNString2string(ctx, selections, v) + }, + true, + true, + ) +} +func (ec *executionContext) fieldContext_DataframeResourceHints_resourceType(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + return graphql.NewScalarFieldContext("DataframeResourceHints", field, false, false, errors.New("field of type String does not have child fields")) } -func (ec *executionContext) fieldContext___InputValue_deprecationReason(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { +func (ec *executionContext) _DataframeResourceHints_fields(ctx context.Context, field graphql.CollectedField, obj *model.DataframeResourceHints) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_DataframeResourceHints_fields(ctx, field) + }, + func(ctx context.Context) (any, error) { + return obj.Fields, nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v []*model.DataframeFieldHint) graphql.Marshaler { + return ec.marshalNDataframeFieldHint2ᚕᚖgithubᚗcomᚋcalyprᚋloomᚋgraphqlapiᚋmodelᚐDataframeFieldHintᚄ(ctx, selections, v) + }, + true, + true, + ) +} +func (ec *executionContext) fieldContext_DataframeResourceHints_fields(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { fc = &graphql.FieldContext{ - Object: "__InputValue", + Object: "DataframeResourceHints", Field: field, - IsMethod: true, + IsMethod: false, IsResolver: false, Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { - return nil, errors.New("field of type String does not have child fields") + return ec.childFields_DataframeFieldHint(ctx, field) }, } return fc, nil } -func (ec *executionContext) ___Schema_description(ctx context.Context, field graphql.CollectedField, obj *introspection.Schema) (ret graphql.Marshaler) { - fc, err := ec.fieldContext___Schema_description(ctx, field) - if err != nil { - return graphql.Null - } - ctx = graphql.WithFieldContext(ctx, fc) - defer func() { - if r := recover(); r != nil { - ec.Error(ctx, ec.Recover(ctx, r)) - ret = graphql.Null - } - }() - resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { - ctx = rctx // use context from middleware stack in children - return obj.Description(), nil - }) - if err != nil { - ec.Error(ctx, err) - return graphql.Null - } - if resTmp == nil { - return graphql.Null - } - res := resTmp.(*string) - fc.Result = res - return ec.marshalOString2ᚖstring(ctx, field.Selections, res) +func (ec *executionContext) _DataframeResourceHints_pivotFields(ctx context.Context, field graphql.CollectedField, obj *model.DataframeResourceHints) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_DataframeResourceHints_pivotFields(ctx, field) + }, + func(ctx context.Context) (any, error) { + return obj.PivotFields, nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v []*model.DataframeFieldHint) graphql.Marshaler { + return ec.marshalNDataframeFieldHint2ᚕᚖgithubᚗcomᚋcalyprᚋloomᚋgraphqlapiᚋmodelᚐDataframeFieldHintᚄ(ctx, selections, v) + }, + true, + true, + ) } - -func (ec *executionContext) fieldContext___Schema_description(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { +func (ec *executionContext) fieldContext_DataframeResourceHints_pivotFields(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { fc = &graphql.FieldContext{ - Object: "__Schema", + Object: "DataframeResourceHints", Field: field, - IsMethod: true, + IsMethod: false, IsResolver: false, Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { - return nil, errors.New("field of type String does not have child fields") + return ec.childFields_DataframeFieldHint(ctx, field) }, } return fc, nil } -func (ec *executionContext) ___Schema_types(ctx context.Context, field graphql.CollectedField, obj *introspection.Schema) (ret graphql.Marshaler) { - fc, err := ec.fieldContext___Schema_types(ctx, field) - if err != nil { - return graphql.Null - } - ctx = graphql.WithFieldContext(ctx, fc) - defer func() { - if r := recover(); r != nil { - ec.Error(ctx, ec.Recover(ctx, r)) - ret = graphql.Null - } - }() - resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { - ctx = rctx // use context from middleware stack in children - return obj.Types(), nil - }) - if err != nil { - ec.Error(ctx, err) - return graphql.Null - } - if resTmp == nil { - if !graphql.HasFieldError(ctx, fc) { - ec.Errorf(ctx, "must not be null") - } - return graphql.Null - } - res := resTmp.([]introspection.Type) - fc.Result = res - return ec.marshalN__Type2ᚕgithubᚗcomᚋ99designsᚋgqlgenᚋgraphqlᚋintrospectionᚐTypeᚄ(ctx, field.Selections, res) +func (ec *executionContext) _DataframeResourceHints_traversals(ctx context.Context, field graphql.CollectedField, obj *model.DataframeResourceHints) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_DataframeResourceHints_traversals(ctx, field) + }, + func(ctx context.Context) (any, error) { + return obj.Traversals, nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v []*model.DataframeTraversalHint) graphql.Marshaler { + return ec.marshalNDataframeTraversalHint2ᚕᚖgithubᚗcomᚋcalyprᚋloomᚋgraphqlapiᚋmodelᚐDataframeTraversalHintᚄ(ctx, selections, v) + }, + true, + true, + ) } - -func (ec *executionContext) fieldContext___Schema_types(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { +func (ec *executionContext) fieldContext_DataframeResourceHints_traversals(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { fc = &graphql.FieldContext{ - Object: "__Schema", + Object: "DataframeResourceHints", Field: field, - IsMethod: true, + IsMethod: false, IsResolver: false, Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { - switch field.Name { - case "kind": - return ec.fieldContext___Type_kind(ctx, field) - case "name": - return ec.fieldContext___Type_name(ctx, field) - case "description": - return ec.fieldContext___Type_description(ctx, field) - case "fields": - return ec.fieldContext___Type_fields(ctx, field) - case "interfaces": - return ec.fieldContext___Type_interfaces(ctx, field) - case "possibleTypes": - return ec.fieldContext___Type_possibleTypes(ctx, field) - case "enumValues": - return ec.fieldContext___Type_enumValues(ctx, field) - case "inputFields": - return ec.fieldContext___Type_inputFields(ctx, field) - case "ofType": - return ec.fieldContext___Type_ofType(ctx, field) - case "specifiedByURL": - return ec.fieldContext___Type_specifiedByURL(ctx, field) - case "isOneOf": - return ec.fieldContext___Type_isOneOf(ctx, field) - } - return nil, fmt.Errorf("no field named %q was found under type __Type", field.Name) + return ec.childFields_DataframeTraversalHint(ctx, field) }, } return fc, nil } -func (ec *executionContext) ___Schema_queryType(ctx context.Context, field graphql.CollectedField, obj *introspection.Schema) (ret graphql.Marshaler) { - fc, err := ec.fieldContext___Schema_queryType(ctx, field) - if err != nil { - return graphql.Null - } - ctx = graphql.WithFieldContext(ctx, fc) - defer func() { - if r := recover(); r != nil { - ec.Error(ctx, ec.Recover(ctx, r)) - ret = graphql.Null - } - }() - resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { - ctx = rctx // use context from middleware stack in children - return obj.QueryType(), nil - }) - if err != nil { - ec.Error(ctx, err) - return graphql.Null - } - if resTmp == nil { - if !graphql.HasFieldError(ctx, fc) { - ec.Errorf(ctx, "must not be null") - } - return graphql.Null - } - res := resTmp.(*introspection.Type) - fc.Result = res - return ec.marshalN__Type2ᚖgithubᚗcomᚋ99designsᚋgqlgenᚋgraphqlᚋintrospectionᚐType(ctx, field.Selections, res) +func (ec *executionContext) _DataframeRichSourceReuse_sourceSet(ctx context.Context, field graphql.CollectedField, obj *model.DataframeRichSourceReuse) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_DataframeRichSourceReuse_sourceSet(ctx, field) + }, + func(ctx context.Context) (any, error) { + return obj.SourceSet, nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v string) graphql.Marshaler { + return ec.marshalNString2string(ctx, selections, v) + }, + true, + true, + ) +} +func (ec *executionContext) fieldContext_DataframeRichSourceReuse_sourceSet(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + return graphql.NewScalarFieldContext("DataframeRichSourceReuse", field, false, false, errors.New("field of type String does not have child fields")) } -func (ec *executionContext) fieldContext___Schema_queryType(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { +func (ec *executionContext) _DataframeRichSourceReuse_aggregateConsumers(ctx context.Context, field graphql.CollectedField, obj *model.DataframeRichSourceReuse) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_DataframeRichSourceReuse_aggregateConsumers(ctx, field) + }, + func(ctx context.Context) (any, error) { + return obj.AggregateConsumers, nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v int) graphql.Marshaler { + return ec.marshalNInt2int(ctx, selections, v) + }, + true, + true, + ) +} +func (ec *executionContext) fieldContext_DataframeRichSourceReuse_aggregateConsumers(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + return graphql.NewScalarFieldContext("DataframeRichSourceReuse", field, false, false, errors.New("field of type Int does not have child fields")) +} + +func (ec *executionContext) _DataframeRichSourceReuse_pivotConsumers(ctx context.Context, field graphql.CollectedField, obj *model.DataframeRichSourceReuse) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_DataframeRichSourceReuse_pivotConsumers(ctx, field) + }, + func(ctx context.Context) (any, error) { + return obj.PivotConsumers, nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v int) graphql.Marshaler { + return ec.marshalNInt2int(ctx, selections, v) + }, + true, + true, + ) +} +func (ec *executionContext) fieldContext_DataframeRichSourceReuse_pivotConsumers(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + return graphql.NewScalarFieldContext("DataframeRichSourceReuse", field, false, false, errors.New("field of type Int does not have child fields")) +} + +func (ec *executionContext) _DataframeRichSourceReuse_sliceConsumers(ctx context.Context, field graphql.CollectedField, obj *model.DataframeRichSourceReuse) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_DataframeRichSourceReuse_sliceConsumers(ctx, field) + }, + func(ctx context.Context) (any, error) { + return obj.SliceConsumers, nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v int) graphql.Marshaler { + return ec.marshalNInt2int(ctx, selections, v) + }, + true, + true, + ) +} +func (ec *executionContext) fieldContext_DataframeRichSourceReuse_sliceConsumers(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + return graphql.NewScalarFieldContext("DataframeRichSourceReuse", field, false, false, errors.New("field of type Int does not have child fields")) +} + +func (ec *executionContext) _DataframeRichSourceReuse_totalConsumers(ctx context.Context, field graphql.CollectedField, obj *model.DataframeRichSourceReuse) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_DataframeRichSourceReuse_totalConsumers(ctx, field) + }, + func(ctx context.Context) (any, error) { + return obj.TotalConsumers, nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v int) graphql.Marshaler { + return ec.marshalNInt2int(ctx, selections, v) + }, + true, + true, + ) +} +func (ec *executionContext) fieldContext_DataframeRichSourceReuse_totalConsumers(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + return graphql.NewScalarFieldContext("DataframeRichSourceReuse", field, false, false, errors.New("field of type Int does not have child fields")) +} + +func (ec *executionContext) _DataframeRowConnection_materialization(ctx context.Context, field graphql.CollectedField, obj *model.DataframeRowConnection) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_DataframeRowConnection_materialization(ctx, field) + }, + func(ctx context.Context) (any, error) { + return obj.Materialization, nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v *model.DataframeMaterialization) graphql.Marshaler { + return ec.marshalNDataframeMaterialization2ᚖgithubᚗcomᚋcalyprᚋloomᚋgraphqlapiᚋmodelᚐDataframeMaterialization(ctx, selections, v) + }, + true, + true, + ) +} +func (ec *executionContext) fieldContext_DataframeRowConnection_materialization(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { fc = &graphql.FieldContext{ - Object: "__Schema", + Object: "DataframeRowConnection", Field: field, - IsMethod: true, + IsMethod: false, IsResolver: false, Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { - switch field.Name { - case "kind": - return ec.fieldContext___Type_kind(ctx, field) - case "name": - return ec.fieldContext___Type_name(ctx, field) - case "description": - return ec.fieldContext___Type_description(ctx, field) - case "fields": - return ec.fieldContext___Type_fields(ctx, field) - case "interfaces": - return ec.fieldContext___Type_interfaces(ctx, field) - case "possibleTypes": - return ec.fieldContext___Type_possibleTypes(ctx, field) - case "enumValues": - return ec.fieldContext___Type_enumValues(ctx, field) - case "inputFields": - return ec.fieldContext___Type_inputFields(ctx, field) - case "ofType": - return ec.fieldContext___Type_ofType(ctx, field) - case "specifiedByURL": - return ec.fieldContext___Type_specifiedByURL(ctx, field) - case "isOneOf": - return ec.fieldContext___Type_isOneOf(ctx, field) - } - return nil, fmt.Errorf("no field named %q was found under type __Type", field.Name) + return ec.childFields_DataframeMaterialization(ctx, field) }, } return fc, nil } -func (ec *executionContext) ___Schema_mutationType(ctx context.Context, field graphql.CollectedField, obj *introspection.Schema) (ret graphql.Marshaler) { - fc, err := ec.fieldContext___Schema_mutationType(ctx, field) - if err != nil { - return graphql.Null - } - ctx = graphql.WithFieldContext(ctx, fc) - defer func() { - if r := recover(); r != nil { - ec.Error(ctx, ec.Recover(ctx, r)) - ret = graphql.Null - } - }() - resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { - ctx = rctx // use context from middleware stack in children - return obj.MutationType(), nil - }) - if err != nil { - ec.Error(ctx, err) - return graphql.Null - } - if resTmp == nil { - return graphql.Null - } - res := resTmp.(*introspection.Type) - fc.Result = res - return ec.marshalO__Type2ᚖgithubᚗcomᚋ99designsᚋgqlgenᚋgraphqlᚋintrospectionᚐType(ctx, field.Selections, res) +func (ec *executionContext) _DataframeRowConnection_columns(ctx context.Context, field graphql.CollectedField, obj *model.DataframeRowConnection) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_DataframeRowConnection_columns(ctx, field) + }, + func(ctx context.Context) (any, error) { + return obj.Columns, nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v []string) graphql.Marshaler { + return ec.marshalNString2ᚕstringᚄ(ctx, selections, v) + }, + true, + true, + ) +} +func (ec *executionContext) fieldContext_DataframeRowConnection_columns(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + return graphql.NewScalarFieldContext("DataframeRowConnection", field, false, false, errors.New("field of type String does not have child fields")) } -func (ec *executionContext) fieldContext___Schema_mutationType(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { +func (ec *executionContext) _DataframeRowConnection_rows(ctx context.Context, field graphql.CollectedField, obj *model.DataframeRowConnection) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_DataframeRowConnection_rows(ctx, field) + }, + func(ctx context.Context) (any, error) { + return obj.Rows, nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v json.RawMessage) graphql.Marshaler { + return ec.marshalNJSON2encodingᚋjsonᚐRawMessage(ctx, selections, v) + }, + true, + true, + ) +} +func (ec *executionContext) fieldContext_DataframeRowConnection_rows(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + return graphql.NewScalarFieldContext("DataframeRowConnection", field, false, false, errors.New("field of type JSON does not have child fields")) +} + +func (ec *executionContext) _DataframeRowConnection_totalCount(ctx context.Context, field graphql.CollectedField, obj *model.DataframeRowConnection) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_DataframeRowConnection_totalCount(ctx, field) + }, + func(ctx context.Context) (any, error) { + return obj.TotalCount, nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v *int) graphql.Marshaler { + return ec.marshalOInt2ᚖint(ctx, selections, v) + }, + true, + false, + ) +} +func (ec *executionContext) fieldContext_DataframeRowConnection_totalCount(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + return graphql.NewScalarFieldContext("DataframeRowConnection", field, false, false, errors.New("field of type Int does not have child fields")) +} + +func (ec *executionContext) _DataframeRowConnection_pageInfo(ctx context.Context, field graphql.CollectedField, obj *model.DataframeRowConnection) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_DataframeRowConnection_pageInfo(ctx, field) + }, + func(ctx context.Context) (any, error) { + return obj.PageInfo, nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v *model.DataframePageInfo) graphql.Marshaler { + return ec.marshalNDataframePageInfo2ᚖgithubᚗcomᚋcalyprᚋloomᚋgraphqlapiᚋmodelᚐDataframePageInfo(ctx, selections, v) + }, + true, + true, + ) +} +func (ec *executionContext) fieldContext_DataframeRowConnection_pageInfo(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { fc = &graphql.FieldContext{ - Object: "__Schema", + Object: "DataframeRowConnection", Field: field, - IsMethod: true, + IsMethod: false, IsResolver: false, Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { - switch field.Name { - case "kind": - return ec.fieldContext___Type_kind(ctx, field) - case "name": - return ec.fieldContext___Type_name(ctx, field) - case "description": - return ec.fieldContext___Type_description(ctx, field) - case "fields": - return ec.fieldContext___Type_fields(ctx, field) - case "interfaces": - return ec.fieldContext___Type_interfaces(ctx, field) - case "possibleTypes": - return ec.fieldContext___Type_possibleTypes(ctx, field) - case "enumValues": - return ec.fieldContext___Type_enumValues(ctx, field) - case "inputFields": - return ec.fieldContext___Type_inputFields(ctx, field) - case "ofType": - return ec.fieldContext___Type_ofType(ctx, field) - case "specifiedByURL": - return ec.fieldContext___Type_specifiedByURL(ctx, field) - case "isOneOf": - return ec.fieldContext___Type_isOneOf(ctx, field) - } - return nil, fmt.Errorf("no field named %q was found under type __Type", field.Name) + return ec.childFields_DataframePageInfo(ctx, field) }, } return fc, nil } -func (ec *executionContext) ___Schema_subscriptionType(ctx context.Context, field graphql.CollectedField, obj *introspection.Schema) (ret graphql.Marshaler) { - fc, err := ec.fieldContext___Schema_subscriptionType(ctx, field) - if err != nil { - return graphql.Null - } - ctx = graphql.WithFieldContext(ctx, fc) - defer func() { - if r := recover(); r != nil { - ec.Error(ctx, ec.Recover(ctx, r)) - ret = graphql.Null - } - }() - resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { - ctx = rctx // use context from middleware stack in children - return obj.SubscriptionType(), nil - }) - if err != nil { - ec.Error(ctx, err) - return graphql.Null - } - if resTmp == nil { - return graphql.Null - } - res := resTmp.(*introspection.Type) - fc.Result = res - return ec.marshalO__Type2ᚖgithubᚗcomᚋ99designsᚋgqlgenᚋgraphqlᚋintrospectionᚐType(ctx, field.Selections, res) +func (ec *executionContext) _DataframeTraversalHint_fromType(ctx context.Context, field graphql.CollectedField, obj *model.DataframeTraversalHint) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_DataframeTraversalHint_fromType(ctx, field) + }, + func(ctx context.Context) (any, error) { + return obj.FromType, nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v string) graphql.Marshaler { + return ec.marshalNString2string(ctx, selections, v) + }, + true, + true, + ) +} +func (ec *executionContext) fieldContext_DataframeTraversalHint_fromType(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + return graphql.NewScalarFieldContext("DataframeTraversalHint", field, false, false, errors.New("field of type String does not have child fields")) } -func (ec *executionContext) fieldContext___Schema_subscriptionType(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { +func (ec *executionContext) _DataframeTraversalHint_label(ctx context.Context, field graphql.CollectedField, obj *model.DataframeTraversalHint) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_DataframeTraversalHint_label(ctx, field) + }, + func(ctx context.Context) (any, error) { + return obj.Label, nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v string) graphql.Marshaler { + return ec.marshalNString2string(ctx, selections, v) + }, + true, + true, + ) +} +func (ec *executionContext) fieldContext_DataframeTraversalHint_label(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + return graphql.NewScalarFieldContext("DataframeTraversalHint", field, false, false, errors.New("field of type String does not have child fields")) +} + +func (ec *executionContext) _DataframeTraversalHint_toType(ctx context.Context, field graphql.CollectedField, obj *model.DataframeTraversalHint) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_DataframeTraversalHint_toType(ctx, field) + }, + func(ctx context.Context) (any, error) { + return obj.ToType, nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v string) graphql.Marshaler { + return ec.marshalNString2string(ctx, selections, v) + }, + true, + true, + ) +} +func (ec *executionContext) fieldContext_DataframeTraversalHint_toType(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + return graphql.NewScalarFieldContext("DataframeTraversalHint", field, false, false, errors.New("field of type String does not have child fields")) +} + +func (ec *executionContext) _DataframeTraversalHint_edgeCount(ctx context.Context, field graphql.CollectedField, obj *model.DataframeTraversalHint) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_DataframeTraversalHint_edgeCount(ctx, field) + }, + func(ctx context.Context) (any, error) { + return obj.EdgeCount, nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v int) graphql.Marshaler { + return ec.marshalNInt2int(ctx, selections, v) + }, + true, + true, + ) +} +func (ec *executionContext) fieldContext_DataframeTraversalHint_edgeCount(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + return graphql.NewScalarFieldContext("DataframeTraversalHint", field, false, false, errors.New("field of type Int does not have child fields")) +} + +func (ec *executionContext) _FhirDataframeResult_columns(ctx context.Context, field graphql.CollectedField, obj *model.FhirDataframeResult) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_FhirDataframeResult_columns(ctx, field) + }, + func(ctx context.Context) (any, error) { + return obj.Columns, nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v []string) graphql.Marshaler { + return ec.marshalNString2ᚕstringᚄ(ctx, selections, v) + }, + true, + true, + ) +} +func (ec *executionContext) fieldContext_FhirDataframeResult_columns(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + return graphql.NewScalarFieldContext("FhirDataframeResult", field, false, false, errors.New("field of type String does not have child fields")) +} + +func (ec *executionContext) _FhirDataframeResult_rows(ctx context.Context, field graphql.CollectedField, obj *model.FhirDataframeResult) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_FhirDataframeResult_rows(ctx, field) + }, + func(ctx context.Context) (any, error) { + return obj.Rows, nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v json.RawMessage) graphql.Marshaler { + return ec.marshalNJSON2encodingᚋjsonᚐRawMessage(ctx, selections, v) + }, + true, + true, + ) +} +func (ec *executionContext) fieldContext_FhirDataframeResult_rows(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + return graphql.NewScalarFieldContext("FhirDataframeResult", field, false, false, errors.New("field of type JSON does not have child fields")) +} + +func (ec *executionContext) _FhirDataframeResult_rowCount(ctx context.Context, field graphql.CollectedField, obj *model.FhirDataframeResult) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_FhirDataframeResult_rowCount(ctx, field) + }, + func(ctx context.Context) (any, error) { + return obj.RowCount, nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v int) graphql.Marshaler { + return ec.marshalNInt2int(ctx, selections, v) + }, + true, + true, + ) +} +func (ec *executionContext) fieldContext_FhirDataframeResult_rowCount(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + return graphql.NewScalarFieldContext("FhirDataframeResult", field, false, false, errors.New("field of type Int does not have child fields")) +} + +func (ec *executionContext) _FhirDataframeResult_diagnostics(ctx context.Context, field graphql.CollectedField, obj *model.FhirDataframeResult) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_FhirDataframeResult_diagnostics(ctx, field) + }, + func(ctx context.Context) (any, error) { + return obj.Diagnostics, nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v *model.DataframeQueryDiagnostics) graphql.Marshaler { + return ec.marshalNDataframeQueryDiagnostics2ᚖgithubᚗcomᚋcalyprᚋloomᚋgraphqlapiᚋmodelᚐDataframeQueryDiagnostics(ctx, selections, v) + }, + true, + true, + ) +} +func (ec *executionContext) fieldContext_FhirDataframeResult_diagnostics(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { fc = &graphql.FieldContext{ - Object: "__Schema", + Object: "FhirDataframeResult", Field: field, - IsMethod: true, + IsMethod: false, IsResolver: false, Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { - switch field.Name { - case "kind": - return ec.fieldContext___Type_kind(ctx, field) - case "name": - return ec.fieldContext___Type_name(ctx, field) - case "description": - return ec.fieldContext___Type_description(ctx, field) - case "fields": - return ec.fieldContext___Type_fields(ctx, field) - case "interfaces": - return ec.fieldContext___Type_interfaces(ctx, field) - case "possibleTypes": - return ec.fieldContext___Type_possibleTypes(ctx, field) - case "enumValues": - return ec.fieldContext___Type_enumValues(ctx, field) - case "inputFields": - return ec.fieldContext___Type_inputFields(ctx, field) - case "ofType": - return ec.fieldContext___Type_ofType(ctx, field) - case "specifiedByURL": - return ec.fieldContext___Type_specifiedByURL(ctx, field) - case "isOneOf": - return ec.fieldContext___Type_isOneOf(ctx, field) - } - return nil, fmt.Errorf("no field named %q was found under type __Type", field.Name) + return ec.childFields_DataframeQueryDiagnostics(ctx, field) }, } return fc, nil } -func (ec *executionContext) ___Schema_directives(ctx context.Context, field graphql.CollectedField, obj *introspection.Schema) (ret graphql.Marshaler) { - fc, err := ec.fieldContext___Schema_directives(ctx, field) - if err != nil { - return graphql.Null +func (ec *executionContext) _Mutation_runFhirDataframe(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_Mutation_runFhirDataframe(ctx, field) + }, + func(ctx context.Context) (any, error) { + fc := graphql.GetFieldContext(ctx) + return ec.Resolvers.Mutation().RunFhirDataframe(ctx, fc.Args["input"].(model.FhirDataframeInput), fc.Args["limit"].(*int)) + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v *model.FhirDataframeResult) graphql.Marshaler { + return ec.marshalNFhirDataframeResult2ᚖgithubᚗcomᚋcalyprᚋloomᚋgraphqlapiᚋmodelᚐFhirDataframeResult(ctx, selections, v) + }, + true, + true, + ) +} +func (ec *executionContext) fieldContext_Mutation_runFhirDataframe(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "Mutation", + Field: field, + IsMethod: true, + IsResolver: true, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.childFields_FhirDataframeResult(ctx, field) + }, } - ctx = graphql.WithFieldContext(ctx, fc) defer func() { if r := recover(); r != nil { - ec.Error(ctx, ec.Recover(ctx, r)) - ret = graphql.Null + err = ec.Recover(ctx, r) + ec.Error(ctx, err) } }() - resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { - ctx = rctx // use context from middleware stack in children - return obj.Directives(), nil - }) - if err != nil { + ctx = graphql.WithFieldContext(ctx, fc) + if fc.Args, err = ec.field_Mutation_runFhirDataframe_args(ctx, field.ArgumentMap(ec.Variables)); err != nil { ec.Error(ctx, err) - return graphql.Null - } - if resTmp == nil { - if !graphql.HasFieldError(ctx, fc) { - ec.Errorf(ctx, "must not be null") - } - return graphql.Null + return fc, err } - res := resTmp.([]introspection.Directive) - fc.Result = res - return ec.marshalN__Directive2ᚕgithubᚗcomᚋ99designsᚋgqlgenᚋgraphqlᚋintrospectionᚐDirectiveᚄ(ctx, field.Selections, res) + return fc, nil } -func (ec *executionContext) fieldContext___Schema_directives(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { +func (ec *executionContext) _Mutation_runDataframeRecipe(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_Mutation_runDataframeRecipe(ctx, field) + }, + func(ctx context.Context) (any, error) { + fc := graphql.GetFieldContext(ctx) + return ec.Resolvers.Mutation().RunDataframeRecipe(ctx, fc.Args["input"].(model.RunDataframeRecipeInput)) + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v *model.DataframeRecipeResult) graphql.Marshaler { + return ec.marshalNDataframeRecipeResult2ᚖgithubᚗcomᚋcalyprᚋloomᚋgraphqlapiᚋmodelᚐDataframeRecipeResult(ctx, selections, v) + }, + true, + true, + ) +} +func (ec *executionContext) fieldContext_Mutation_runDataframeRecipe(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { fc = &graphql.FieldContext{ - Object: "__Schema", + Object: "Mutation", Field: field, IsMethod: true, - IsResolver: false, + IsResolver: true, Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { - switch field.Name { - case "name": - return ec.fieldContext___Directive_name(ctx, field) - case "description": - return ec.fieldContext___Directive_description(ctx, field) - case "locations": - return ec.fieldContext___Directive_locations(ctx, field) - case "args": - return ec.fieldContext___Directive_args(ctx, field) - case "isRepeatable": - return ec.fieldContext___Directive_isRepeatable(ctx, field) - } - return nil, fmt.Errorf("no field named %q was found under type __Directive", field.Name) + return ec.childFields_DataframeRecipeResult(ctx, field) }, } - return fc, nil -} - -func (ec *executionContext) ___Type_kind(ctx context.Context, field graphql.CollectedField, obj *introspection.Type) (ret graphql.Marshaler) { - fc, err := ec.fieldContext___Type_kind(ctx, field) - if err != nil { - return graphql.Null - } - ctx = graphql.WithFieldContext(ctx, fc) defer func() { if r := recover(); r != nil { - ec.Error(ctx, ec.Recover(ctx, r)) - ret = graphql.Null + err = ec.Recover(ctx, r) + ec.Error(ctx, err) } }() - resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { - ctx = rctx // use context from middleware stack in children - return obj.Kind(), nil - }) - if err != nil { + ctx = graphql.WithFieldContext(ctx, fc) + if fc.Args, err = ec.field_Mutation_runDataframeRecipe_args(ctx, field.ArgumentMap(ec.Variables)); err != nil { ec.Error(ctx, err) - return graphql.Null - } - if resTmp == nil { - if !graphql.HasFieldError(ctx, fc) { - ec.Errorf(ctx, "must not be null") - } - return graphql.Null + return fc, err } - res := resTmp.(string) - fc.Result = res - return ec.marshalN__TypeKind2string(ctx, field.Selections, res) + return fc, nil } -func (ec *executionContext) fieldContext___Type_kind(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { +func (ec *executionContext) _Mutation_validateDataframeRecipe(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_Mutation_validateDataframeRecipe(ctx, field) + }, + func(ctx context.Context) (any, error) { + fc := graphql.GetFieldContext(ctx) + return ec.Resolvers.Mutation().ValidateDataframeRecipe(ctx, fc.Args["input"].(model.ValidateDataframeRecipeInput)) + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v *model.DataframeRecipeValidation) graphql.Marshaler { + return ec.marshalNDataframeRecipeValidation2ᚖgithubᚗcomᚋcalyprᚋloomᚋgraphqlapiᚋmodelᚐDataframeRecipeValidation(ctx, selections, v) + }, + true, + true, + ) +} +func (ec *executionContext) fieldContext_Mutation_validateDataframeRecipe(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { fc = &graphql.FieldContext{ - Object: "__Type", + Object: "Mutation", Field: field, IsMethod: true, - IsResolver: false, + IsResolver: true, Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { - return nil, errors.New("field of type __TypeKind does not have child fields") + return ec.childFields_DataframeRecipeValidation(ctx, field) }, } - return fc, nil -} - -func (ec *executionContext) ___Type_name(ctx context.Context, field graphql.CollectedField, obj *introspection.Type) (ret graphql.Marshaler) { - fc, err := ec.fieldContext___Type_name(ctx, field) - if err != nil { - return graphql.Null - } - ctx = graphql.WithFieldContext(ctx, fc) defer func() { if r := recover(); r != nil { - ec.Error(ctx, ec.Recover(ctx, r)) - ret = graphql.Null + err = ec.Recover(ctx, r) + ec.Error(ctx, err) } }() - resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { - ctx = rctx // use context from middleware stack in children - return obj.Name(), nil - }) - if err != nil { + ctx = graphql.WithFieldContext(ctx, fc) + if fc.Args, err = ec.field_Mutation_validateDataframeRecipe_args(ctx, field.ArgumentMap(ec.Variables)); err != nil { ec.Error(ctx, err) - return graphql.Null - } - if resTmp == nil { - return graphql.Null + return fc, err } - res := resTmp.(*string) - fc.Result = res - return ec.marshalOString2ᚖstring(ctx, field.Selections, res) + return fc, nil } -func (ec *executionContext) fieldContext___Type_name(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { +func (ec *executionContext) _Mutation_previewDataframeRecipe(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_Mutation_previewDataframeRecipe(ctx, field) + }, + func(ctx context.Context) (any, error) { + fc := graphql.GetFieldContext(ctx) + return ec.Resolvers.Mutation().PreviewDataframeRecipe(ctx, fc.Args["input"].(model.PreviewDataframeRecipeInput)) + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v *model.DataframeRecipePreview) graphql.Marshaler { + return ec.marshalNDataframeRecipePreview2ᚖgithubᚗcomᚋcalyprᚋloomᚋgraphqlapiᚋmodelᚐDataframeRecipePreview(ctx, selections, v) + }, + true, + true, + ) +} +func (ec *executionContext) fieldContext_Mutation_previewDataframeRecipe(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { fc = &graphql.FieldContext{ - Object: "__Type", + Object: "Mutation", Field: field, IsMethod: true, - IsResolver: false, + IsResolver: true, Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { - return nil, errors.New("field of type String does not have child fields") + return ec.childFields_DataframeRecipePreview(ctx, field) }, } - return fc, nil -} - -func (ec *executionContext) ___Type_description(ctx context.Context, field graphql.CollectedField, obj *introspection.Type) (ret graphql.Marshaler) { - fc, err := ec.fieldContext___Type_description(ctx, field) - if err != nil { - return graphql.Null - } - ctx = graphql.WithFieldContext(ctx, fc) defer func() { if r := recover(); r != nil { - ec.Error(ctx, ec.Recover(ctx, r)) - ret = graphql.Null + err = ec.Recover(ctx, r) + ec.Error(ctx, err) } }() - resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { - ctx = rctx // use context from middleware stack in children - return obj.Description(), nil - }) - if err != nil { + ctx = graphql.WithFieldContext(ctx, fc) + if fc.Args, err = ec.field_Mutation_previewDataframeRecipe_args(ctx, field.ArgumentMap(ec.Variables)); err != nil { ec.Error(ctx, err) - return graphql.Null - } - if resTmp == nil { - return graphql.Null + return fc, err } - res := resTmp.(*string) - fc.Result = res - return ec.marshalOString2ᚖstring(ctx, field.Selections, res) + return fc, nil } -func (ec *executionContext) fieldContext___Type_description(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { +func (ec *executionContext) _Mutation_materializeDataframeRecipeBundle(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_Mutation_materializeDataframeRecipeBundle(ctx, field) + }, + func(ctx context.Context) (any, error) { + fc := graphql.GetFieldContext(ctx) + return ec.Resolvers.Mutation().MaterializeDataframeRecipeBundle(ctx, fc.Args["input"].(model.MaterializeDataframeRecipeInput)) + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v *model.DataframeRecipeExecution) graphql.Marshaler { + return ec.marshalNDataframeRecipeExecution2ᚖgithubᚗcomᚋcalyprᚋloomᚋgraphqlapiᚋmodelᚐDataframeRecipeExecution(ctx, selections, v) + }, + true, + true, + ) +} +func (ec *executionContext) fieldContext_Mutation_materializeDataframeRecipeBundle(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { fc = &graphql.FieldContext{ - Object: "__Type", + Object: "Mutation", Field: field, IsMethod: true, - IsResolver: false, + IsResolver: true, Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { - return nil, errors.New("field of type String does not have child fields") + return ec.childFields_DataframeRecipeExecution(ctx, field) }, } - return fc, nil -} - -func (ec *executionContext) ___Type_fields(ctx context.Context, field graphql.CollectedField, obj *introspection.Type) (ret graphql.Marshaler) { - fc, err := ec.fieldContext___Type_fields(ctx, field) - if err != nil { - return graphql.Null - } - ctx = graphql.WithFieldContext(ctx, fc) defer func() { if r := recover(); r != nil { - ec.Error(ctx, ec.Recover(ctx, r)) - ret = graphql.Null + err = ec.Recover(ctx, r) + ec.Error(ctx, err) } }() - resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { - ctx = rctx // use context from middleware stack in children - return obj.Fields(fc.Args["includeDeprecated"].(bool)), nil - }) - if err != nil { + ctx = graphql.WithFieldContext(ctx, fc) + if fc.Args, err = ec.field_Mutation_materializeDataframeRecipeBundle_args(ctx, field.ArgumentMap(ec.Variables)); err != nil { ec.Error(ctx, err) - return graphql.Null - } - if resTmp == nil { - return graphql.Null + return fc, err } - res := resTmp.([]introspection.Field) - fc.Result = res - return ec.marshalO__Field2ᚕgithubᚗcomᚋ99designsᚋgqlgenᚋgraphqlᚋintrospectionᚐFieldᚄ(ctx, field.Selections, res) + return fc, nil } -func (ec *executionContext) fieldContext___Type_fields(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { +func (ec *executionContext) _Query_dataframeBuilderIntrospection(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_Query_dataframeBuilderIntrospection(ctx, field) + }, + func(ctx context.Context) (any, error) { + fc := graphql.GetFieldContext(ctx) + return ec.Resolvers.Query().DataframeBuilderIntrospection(ctx, fc.Args["input"].(model.DataframeBuilderIntrospectionInput)) + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v *model.DataframeBuilderIntrospection) graphql.Marshaler { + return ec.marshalNDataframeBuilderIntrospection2ᚖgithubᚗcomᚋcalyprᚋloomᚋgraphqlapiᚋmodelᚐDataframeBuilderIntrospection(ctx, selections, v) + }, + true, + true, + ) +} +func (ec *executionContext) fieldContext_Query_dataframeBuilderIntrospection(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { fc = &graphql.FieldContext{ - Object: "__Type", + Object: "Query", Field: field, IsMethod: true, - IsResolver: false, + IsResolver: true, Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { - switch field.Name { - case "name": - return ec.fieldContext___Field_name(ctx, field) - case "description": - return ec.fieldContext___Field_description(ctx, field) - case "args": - return ec.fieldContext___Field_args(ctx, field) - case "type": - return ec.fieldContext___Field_type(ctx, field) - case "isDeprecated": - return ec.fieldContext___Field_isDeprecated(ctx, field) - case "deprecationReason": - return ec.fieldContext___Field_deprecationReason(ctx, field) - } - return nil, fmt.Errorf("no field named %q was found under type __Field", field.Name) + return ec.childFields_DataframeBuilderIntrospection(ctx, field) }, } defer func() { @@ -8137,189 +9057,249 @@ func (ec *executionContext) fieldContext___Type_fields(ctx context.Context, fiel } }() ctx = graphql.WithFieldContext(ctx, fc) - if fc.Args, err = ec.field___Type_fields_args(ctx, field.ArgumentMap(ec.Variables)); err != nil { + if fc.Args, err = ec.field_Query_dataframeBuilderIntrospection_args(ctx, field.ArgumentMap(ec.Variables)); err != nil { ec.Error(ctx, err) return fc, err } return fc, nil } -func (ec *executionContext) ___Type_interfaces(ctx context.Context, field graphql.CollectedField, obj *introspection.Type) (ret graphql.Marshaler) { - fc, err := ec.fieldContext___Type_interfaces(ctx, field) - if err != nil { - return graphql.Null +func (ec *executionContext) _Query_dataframeMaterialization(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_Query_dataframeMaterialization(ctx, field) + }, + func(ctx context.Context) (any, error) { + fc := graphql.GetFieldContext(ctx) + return ec.Resolvers.Query().DataframeMaterialization(ctx, fc.Args["id"].(string)) + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v *model.DataframeMaterialization) graphql.Marshaler { + return ec.marshalODataframeMaterialization2ᚖgithubᚗcomᚋcalyprᚋloomᚋgraphqlapiᚋmodelᚐDataframeMaterialization(ctx, selections, v) + }, + true, + false, + ) +} +func (ec *executionContext) fieldContext_Query_dataframeMaterialization(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "Query", + Field: field, + IsMethod: true, + IsResolver: true, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.childFields_DataframeMaterialization(ctx, field) + }, } - ctx = graphql.WithFieldContext(ctx, fc) defer func() { if r := recover(); r != nil { - ec.Error(ctx, ec.Recover(ctx, r)) - ret = graphql.Null + err = ec.Recover(ctx, r) + ec.Error(ctx, err) } }() - resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { - ctx = rctx // use context from middleware stack in children - return obj.Interfaces(), nil - }) - if err != nil { + ctx = graphql.WithFieldContext(ctx, fc) + if fc.Args, err = ec.field_Query_dataframeMaterialization_args(ctx, field.ArgumentMap(ec.Variables)); err != nil { ec.Error(ctx, err) - return graphql.Null - } - if resTmp == nil { - return graphql.Null + return fc, err } - res := resTmp.([]introspection.Type) - fc.Result = res - return ec.marshalO__Type2ᚕgithubᚗcomᚋ99designsᚋgqlgenᚋgraphqlᚋintrospectionᚐTypeᚄ(ctx, field.Selections, res) + return fc, nil } -func (ec *executionContext) fieldContext___Type_interfaces(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { +func (ec *executionContext) _Query_dataframeDatasets(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_Query_dataframeDatasets(ctx, field) + }, + func(ctx context.Context) (any, error) { + return ec.Resolvers.Query().DataframeDatasets(ctx) + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v []*model.DataframeMaterialization) graphql.Marshaler { + return ec.marshalNDataframeMaterialization2ᚕᚖgithubᚗcomᚋcalyprᚋloomᚋgraphqlapiᚋmodelᚐDataframeMaterializationᚄ(ctx, selections, v) + }, + true, + true, + ) +} +func (ec *executionContext) fieldContext_Query_dataframeDatasets(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { fc = &graphql.FieldContext{ - Object: "__Type", + Object: "Query", Field: field, IsMethod: true, - IsResolver: false, + IsResolver: true, Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { - switch field.Name { - case "kind": - return ec.fieldContext___Type_kind(ctx, field) - case "name": - return ec.fieldContext___Type_name(ctx, field) - case "description": - return ec.fieldContext___Type_description(ctx, field) - case "fields": - return ec.fieldContext___Type_fields(ctx, field) - case "interfaces": - return ec.fieldContext___Type_interfaces(ctx, field) - case "possibleTypes": - return ec.fieldContext___Type_possibleTypes(ctx, field) - case "enumValues": - return ec.fieldContext___Type_enumValues(ctx, field) - case "inputFields": - return ec.fieldContext___Type_inputFields(ctx, field) - case "ofType": - return ec.fieldContext___Type_ofType(ctx, field) - case "specifiedByURL": - return ec.fieldContext___Type_specifiedByURL(ctx, field) - case "isOneOf": - return ec.fieldContext___Type_isOneOf(ctx, field) - } - return nil, fmt.Errorf("no field named %q was found under type __Type", field.Name) + return ec.childFields_DataframeMaterialization(ctx, field) }, } return fc, nil } -func (ec *executionContext) ___Type_possibleTypes(ctx context.Context, field graphql.CollectedField, obj *introspection.Type) (ret graphql.Marshaler) { - fc, err := ec.fieldContext___Type_possibleTypes(ctx, field) - if err != nil { - return graphql.Null +func (ec *executionContext) _Query_dataframeDataset(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_Query_dataframeDataset(ctx, field) + }, + func(ctx context.Context) (any, error) { + fc := graphql.GetFieldContext(ctx) + return ec.Resolvers.Query().DataframeDataset(ctx, fc.Args["input"].(model.DataframeDatasetInput)) + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v *model.DataframeMaterialization) graphql.Marshaler { + return ec.marshalODataframeMaterialization2ᚖgithubᚗcomᚋcalyprᚋloomᚋgraphqlapiᚋmodelᚐDataframeMaterialization(ctx, selections, v) + }, + true, + false, + ) +} +func (ec *executionContext) fieldContext_Query_dataframeDataset(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "Query", + Field: field, + IsMethod: true, + IsResolver: true, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.childFields_DataframeMaterialization(ctx, field) + }, } - ctx = graphql.WithFieldContext(ctx, fc) defer func() { if r := recover(); r != nil { - ec.Error(ctx, ec.Recover(ctx, r)) - ret = graphql.Null + err = ec.Recover(ctx, r) + ec.Error(ctx, err) } }() - resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { - ctx = rctx // use context from middleware stack in children - return obj.PossibleTypes(), nil - }) - if err != nil { + ctx = graphql.WithFieldContext(ctx, fc) + if fc.Args, err = ec.field_Query_dataframeDataset_args(ctx, field.ArgumentMap(ec.Variables)); err != nil { ec.Error(ctx, err) - return graphql.Null - } - if resTmp == nil { - return graphql.Null + return fc, err } - res := resTmp.([]introspection.Type) - fc.Result = res - return ec.marshalO__Type2ᚕgithubᚗcomᚋ99designsᚋgqlgenᚋgraphqlᚋintrospectionᚐTypeᚄ(ctx, field.Selections, res) + return fc, nil } -func (ec *executionContext) fieldContext___Type_possibleTypes(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { +func (ec *executionContext) _Query_dataframeRows(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_Query_dataframeRows(ctx, field) + }, + func(ctx context.Context) (any, error) { + fc := graphql.GetFieldContext(ctx) + return ec.Resolvers.Query().DataframeRows(ctx, fc.Args["input"].(model.DataframeRowsInput)) + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v *model.DataframeRowConnection) graphql.Marshaler { + return ec.marshalNDataframeRowConnection2ᚖgithubᚗcomᚋcalyprᚋloomᚋgraphqlapiᚋmodelᚐDataframeRowConnection(ctx, selections, v) + }, + true, + true, + ) +} +func (ec *executionContext) fieldContext_Query_dataframeRows(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { fc = &graphql.FieldContext{ - Object: "__Type", + Object: "Query", Field: field, IsMethod: true, - IsResolver: false, + IsResolver: true, Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { - switch field.Name { - case "kind": - return ec.fieldContext___Type_kind(ctx, field) - case "name": - return ec.fieldContext___Type_name(ctx, field) - case "description": - return ec.fieldContext___Type_description(ctx, field) - case "fields": - return ec.fieldContext___Type_fields(ctx, field) - case "interfaces": - return ec.fieldContext___Type_interfaces(ctx, field) - case "possibleTypes": - return ec.fieldContext___Type_possibleTypes(ctx, field) - case "enumValues": - return ec.fieldContext___Type_enumValues(ctx, field) - case "inputFields": - return ec.fieldContext___Type_inputFields(ctx, field) - case "ofType": - return ec.fieldContext___Type_ofType(ctx, field) - case "specifiedByURL": - return ec.fieldContext___Type_specifiedByURL(ctx, field) - case "isOneOf": - return ec.fieldContext___Type_isOneOf(ctx, field) - } - return nil, fmt.Errorf("no field named %q was found under type __Type", field.Name) + return ec.childFields_DataframeRowConnection(ctx, field) }, } + defer func() { + if r := recover(); r != nil { + err = ec.Recover(ctx, r) + ec.Error(ctx, err) + } + }() + ctx = graphql.WithFieldContext(ctx, fc) + if fc.Args, err = ec.field_Query_dataframeRows_args(ctx, field.ArgumentMap(ec.Variables)); err != nil { + ec.Error(ctx, err) + return fc, err + } return fc, nil } -func (ec *executionContext) ___Type_enumValues(ctx context.Context, field graphql.CollectedField, obj *introspection.Type) (ret graphql.Marshaler) { - fc, err := ec.fieldContext___Type_enumValues(ctx, field) - if err != nil { - return graphql.Null +func (ec *executionContext) _Query_dataframeAggregate(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_Query_dataframeAggregate(ctx, field) + }, + func(ctx context.Context) (any, error) { + fc := graphql.GetFieldContext(ctx) + return ec.Resolvers.Query().DataframeAggregate(ctx, fc.Args["input"].(model.DataframeAggregateInput)) + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v *model.DataframeAggregateResult) graphql.Marshaler { + return ec.marshalNDataframeAggregateResult2ᚖgithubᚗcomᚋcalyprᚋloomᚋgraphqlapiᚋmodelᚐDataframeAggregateResult(ctx, selections, v) + }, + true, + true, + ) +} +func (ec *executionContext) fieldContext_Query_dataframeAggregate(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "Query", + Field: field, + IsMethod: true, + IsResolver: true, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.childFields_DataframeAggregateResult(ctx, field) + }, } - ctx = graphql.WithFieldContext(ctx, fc) defer func() { if r := recover(); r != nil { - ec.Error(ctx, ec.Recover(ctx, r)) - ret = graphql.Null + err = ec.Recover(ctx, r) + ec.Error(ctx, err) } }() - resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { - ctx = rctx // use context from middleware stack in children - return obj.EnumValues(fc.Args["includeDeprecated"].(bool)), nil - }) - if err != nil { + ctx = graphql.WithFieldContext(ctx, fc) + if fc.Args, err = ec.field_Query_dataframeAggregate_args(ctx, field.ArgumentMap(ec.Variables)); err != nil { ec.Error(ctx, err) - return graphql.Null - } - if resTmp == nil { - return graphql.Null + return fc, err } - res := resTmp.([]introspection.EnumValue) - fc.Result = res - return ec.marshalO__EnumValue2ᚕgithubᚗcomᚋ99designsᚋgqlgenᚋgraphqlᚋintrospectionᚐEnumValueᚄ(ctx, field.Selections, res) + return fc, nil } -func (ec *executionContext) fieldContext___Type_enumValues(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { +func (ec *executionContext) _Query_dataframeRecipeExecution(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_Query_dataframeRecipeExecution(ctx, field) + }, + func(ctx context.Context) (any, error) { + fc := graphql.GetFieldContext(ctx) + return ec.Resolvers.Query().DataframeRecipeExecution(ctx, fc.Args["id"].(string)) + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v *model.DataframeRecipeExecution) graphql.Marshaler { + return ec.marshalODataframeRecipeExecution2ᚖgithubᚗcomᚋcalyprᚋloomᚋgraphqlapiᚋmodelᚐDataframeRecipeExecution(ctx, selections, v) + }, + true, + false, + ) +} +func (ec *executionContext) fieldContext_Query_dataframeRecipeExecution(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { fc = &graphql.FieldContext{ - Object: "__Type", + Object: "Query", Field: field, IsMethod: true, - IsResolver: false, + IsResolver: true, Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { - switch field.Name { - case "name": - return ec.fieldContext___EnumValue_name(ctx, field) - case "description": - return ec.fieldContext___EnumValue_description(ctx, field) - case "isDeprecated": - return ec.fieldContext___EnumValue_isDeprecated(ctx, field) - case "deprecationReason": - return ec.fieldContext___EnumValue_deprecationReason(ctx, field) - } - return nil, fmt.Errorf("no field named %q was found under type __EnumValue", field.Name) + return ec.childFields_DataframeRecipeExecution(ctx, field) }, } defer func() { @@ -8329,1061 +9309,4562 @@ func (ec *executionContext) fieldContext___Type_enumValues(ctx context.Context, } }() ctx = graphql.WithFieldContext(ctx, fc) - if fc.Args, err = ec.field___Type_enumValues_args(ctx, field.ArgumentMap(ec.Variables)); err != nil { + if fc.Args, err = ec.field_Query_dataframeRecipeExecution_args(ctx, field.ArgumentMap(ec.Variables)); err != nil { ec.Error(ctx, err) return fc, err } return fc, nil } -func (ec *executionContext) ___Type_inputFields(ctx context.Context, field graphql.CollectedField, obj *introspection.Type) (ret graphql.Marshaler) { - fc, err := ec.fieldContext___Type_inputFields(ctx, field) - if err != nil { - return graphql.Null +func (ec *executionContext) _Query_explainDataframeRecipe(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_Query_explainDataframeRecipe(ctx, field) + }, + func(ctx context.Context) (any, error) { + fc := graphql.GetFieldContext(ctx) + return ec.Resolvers.Query().ExplainDataframeRecipe(ctx, fc.Args["input"].(model.ExplainDataframeRecipeInput)) + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v *model.DataframeRecipeExplanation) graphql.Marshaler { + return ec.marshalNDataframeRecipeExplanation2ᚖgithubᚗcomᚋcalyprᚋloomᚋgraphqlapiᚋmodelᚐDataframeRecipeExplanation(ctx, selections, v) + }, + true, + true, + ) +} +func (ec *executionContext) fieldContext_Query_explainDataframeRecipe(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "Query", + Field: field, + IsMethod: true, + IsResolver: true, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.childFields_DataframeRecipeExplanation(ctx, field) + }, } - ctx = graphql.WithFieldContext(ctx, fc) defer func() { if r := recover(); r != nil { - ec.Error(ctx, ec.Recover(ctx, r)) - ret = graphql.Null + err = ec.Recover(ctx, r) + ec.Error(ctx, err) + } + }() + ctx = graphql.WithFieldContext(ctx, fc) + if fc.Args, err = ec.field_Query_explainDataframeRecipe_args(ctx, field.ArgumentMap(ec.Variables)); err != nil { + ec.Error(ctx, err) + return fc, err + } + return fc, nil +} + +func (ec *executionContext) _Query_preflightDataframeRecipe(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_Query_preflightDataframeRecipe(ctx, field) + }, + func(ctx context.Context) (any, error) { + fc := graphql.GetFieldContext(ctx) + return ec.Resolvers.Query().PreflightDataframeRecipe(ctx, fc.Args["input"].(model.PreflightDataframeRecipeInput)) + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v *model.DataframeRecipePreflight) graphql.Marshaler { + return ec.marshalNDataframeRecipePreflight2ᚖgithubᚗcomᚋcalyprᚋloomᚋgraphqlapiᚋmodelᚐDataframeRecipePreflight(ctx, selections, v) + }, + true, + true, + ) +} +func (ec *executionContext) fieldContext_Query_preflightDataframeRecipe(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "Query", + Field: field, + IsMethod: true, + IsResolver: true, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.childFields_DataframeRecipePreflight(ctx, field) + }, + } + defer func() { + if r := recover(); r != nil { + err = ec.Recover(ctx, r) + ec.Error(ctx, err) + } + }() + ctx = graphql.WithFieldContext(ctx, fc) + if fc.Args, err = ec.field_Query_preflightDataframeRecipe_args(ctx, field.ArgumentMap(ec.Variables)); err != nil { + ec.Error(ctx, err) + return fc, err + } + return fc, nil +} + +func (ec *executionContext) _Query___type(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_Query___type(ctx, field) + }, + func(ctx context.Context) (any, error) { + fc := graphql.GetFieldContext(ctx) + return ec.IntrospectType(fc.Args["name"].(string)) + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v *introspection.Type) graphql.Marshaler { + return ec.marshalO__Type2ᚖgithubᚗcomᚋ99designsᚋgqlgenᚋgraphqlᚋintrospectionᚐType(ctx, selections, v) + }, + true, + false, + ) +} +func (ec *executionContext) fieldContext_Query___type(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "Query", + Field: field, + IsMethod: true, + IsResolver: false, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.childFields___Type(ctx, field) + }, + } + defer func() { + if r := recover(); r != nil { + err = ec.Recover(ctx, r) + ec.Error(ctx, err) + } + }() + ctx = graphql.WithFieldContext(ctx, fc) + if fc.Args, err = ec.field_Query___type_args(ctx, field.ArgumentMap(ec.Variables)); err != nil { + ec.Error(ctx, err) + return fc, err + } + return fc, nil +} + +func (ec *executionContext) _Query___schema(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_Query___schema(ctx, field) + }, + func(ctx context.Context) (any, error) { + return ec.IntrospectSchema() + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v *introspection.Schema) graphql.Marshaler { + return ec.marshalO__Schema2ᚖgithubᚗcomᚋ99designsᚋgqlgenᚋgraphqlᚋintrospectionᚐSchema(ctx, selections, v) + }, + true, + false, + ) +} +func (ec *executionContext) fieldContext_Query___schema(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "Query", + Field: field, + IsMethod: true, + IsResolver: false, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.childFields___Schema(ctx, field) + }, + } + return fc, nil +} + +func (ec *executionContext) ___Directive_name(ctx context.Context, field graphql.CollectedField, obj *introspection.Directive) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext___Directive_name(ctx, field) + }, + func(ctx context.Context) (any, error) { + return obj.Name, nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v string) graphql.Marshaler { + return ec.marshalNString2string(ctx, selections, v) + }, + true, + true, + ) +} +func (ec *executionContext) fieldContext___Directive_name(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + return graphql.NewScalarFieldContext("__Directive", field, false, false, errors.New("field of type String does not have child fields")) +} + +func (ec *executionContext) ___Directive_description(ctx context.Context, field graphql.CollectedField, obj *introspection.Directive) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext___Directive_description(ctx, field) + }, + func(ctx context.Context) (any, error) { + return obj.Description(), nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v *string) graphql.Marshaler { + return ec.marshalOString2ᚖstring(ctx, selections, v) + }, + true, + false, + ) +} +func (ec *executionContext) fieldContext___Directive_description(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + return graphql.NewScalarFieldContext("__Directive", field, true, false, errors.New("field of type String does not have child fields")) +} + +func (ec *executionContext) ___Directive_isRepeatable(ctx context.Context, field graphql.CollectedField, obj *introspection.Directive) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext___Directive_isRepeatable(ctx, field) + }, + func(ctx context.Context) (any, error) { + return obj.IsRepeatable, nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v bool) graphql.Marshaler { + return ec.marshalNBoolean2bool(ctx, selections, v) + }, + true, + true, + ) +} +func (ec *executionContext) fieldContext___Directive_isRepeatable(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + return graphql.NewScalarFieldContext("__Directive", field, false, false, errors.New("field of type Boolean does not have child fields")) +} + +func (ec *executionContext) ___Directive_locations(ctx context.Context, field graphql.CollectedField, obj *introspection.Directive) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext___Directive_locations(ctx, field) + }, + func(ctx context.Context) (any, error) { + return obj.Locations, nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v []string) graphql.Marshaler { + return ec.marshalN__DirectiveLocation2ᚕstringᚄ(ctx, selections, v) + }, + true, + true, + ) +} +func (ec *executionContext) fieldContext___Directive_locations(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + return graphql.NewScalarFieldContext("__Directive", field, false, false, errors.New("field of type __DirectiveLocation does not have child fields")) +} + +func (ec *executionContext) ___Directive_args(ctx context.Context, field graphql.CollectedField, obj *introspection.Directive) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext___Directive_args(ctx, field) + }, + func(ctx context.Context) (any, error) { + return obj.Args, nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v []introspection.InputValue) graphql.Marshaler { + return ec.marshalN__InputValue2ᚕgithubᚗcomᚋ99designsᚋgqlgenᚋgraphqlᚋintrospectionᚐInputValueᚄ(ctx, selections, v) + }, + true, + true, + ) +} +func (ec *executionContext) fieldContext___Directive_args(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "__Directive", + Field: field, + IsMethod: false, + IsResolver: false, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.childFields___InputValue(ctx, field) + }, + } + defer func() { + if r := recover(); r != nil { + err = ec.Recover(ctx, r) + ec.Error(ctx, err) + } + }() + ctx = graphql.WithFieldContext(ctx, fc) + if fc.Args, err = ec.field___Directive_args_args(ctx, field.ArgumentMap(ec.Variables)); err != nil { + ec.Error(ctx, err) + return fc, err + } + return fc, nil +} + +func (ec *executionContext) ___EnumValue_name(ctx context.Context, field graphql.CollectedField, obj *introspection.EnumValue) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext___EnumValue_name(ctx, field) + }, + func(ctx context.Context) (any, error) { + return obj.Name, nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v string) graphql.Marshaler { + return ec.marshalNString2string(ctx, selections, v) + }, + true, + true, + ) +} +func (ec *executionContext) fieldContext___EnumValue_name(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + return graphql.NewScalarFieldContext("__EnumValue", field, false, false, errors.New("field of type String does not have child fields")) +} + +func (ec *executionContext) ___EnumValue_description(ctx context.Context, field graphql.CollectedField, obj *introspection.EnumValue) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext___EnumValue_description(ctx, field) + }, + func(ctx context.Context) (any, error) { + return obj.Description(), nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v *string) graphql.Marshaler { + return ec.marshalOString2ᚖstring(ctx, selections, v) + }, + true, + false, + ) +} +func (ec *executionContext) fieldContext___EnumValue_description(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + return graphql.NewScalarFieldContext("__EnumValue", field, true, false, errors.New("field of type String does not have child fields")) +} + +func (ec *executionContext) ___EnumValue_isDeprecated(ctx context.Context, field graphql.CollectedField, obj *introspection.EnumValue) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext___EnumValue_isDeprecated(ctx, field) + }, + func(ctx context.Context) (any, error) { + return obj.IsDeprecated(), nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v bool) graphql.Marshaler { + return ec.marshalNBoolean2bool(ctx, selections, v) + }, + true, + true, + ) +} +func (ec *executionContext) fieldContext___EnumValue_isDeprecated(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + return graphql.NewScalarFieldContext("__EnumValue", field, true, false, errors.New("field of type Boolean does not have child fields")) +} + +func (ec *executionContext) ___EnumValue_deprecationReason(ctx context.Context, field graphql.CollectedField, obj *introspection.EnumValue) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext___EnumValue_deprecationReason(ctx, field) + }, + func(ctx context.Context) (any, error) { + return obj.DeprecationReason(), nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v *string) graphql.Marshaler { + return ec.marshalOString2ᚖstring(ctx, selections, v) + }, + true, + false, + ) +} +func (ec *executionContext) fieldContext___EnumValue_deprecationReason(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + return graphql.NewScalarFieldContext("__EnumValue", field, true, false, errors.New("field of type String does not have child fields")) +} + +func (ec *executionContext) ___Field_name(ctx context.Context, field graphql.CollectedField, obj *introspection.Field) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext___Field_name(ctx, field) + }, + func(ctx context.Context) (any, error) { + return obj.Name, nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v string) graphql.Marshaler { + return ec.marshalNString2string(ctx, selections, v) + }, + true, + true, + ) +} +func (ec *executionContext) fieldContext___Field_name(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + return graphql.NewScalarFieldContext("__Field", field, false, false, errors.New("field of type String does not have child fields")) +} + +func (ec *executionContext) ___Field_description(ctx context.Context, field graphql.CollectedField, obj *introspection.Field) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext___Field_description(ctx, field) + }, + func(ctx context.Context) (any, error) { + return obj.Description(), nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v *string) graphql.Marshaler { + return ec.marshalOString2ᚖstring(ctx, selections, v) + }, + true, + false, + ) +} +func (ec *executionContext) fieldContext___Field_description(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + return graphql.NewScalarFieldContext("__Field", field, true, false, errors.New("field of type String does not have child fields")) +} + +func (ec *executionContext) ___Field_args(ctx context.Context, field graphql.CollectedField, obj *introspection.Field) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext___Field_args(ctx, field) + }, + func(ctx context.Context) (any, error) { + return obj.Args, nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v []introspection.InputValue) graphql.Marshaler { + return ec.marshalN__InputValue2ᚕgithubᚗcomᚋ99designsᚋgqlgenᚋgraphqlᚋintrospectionᚐInputValueᚄ(ctx, selections, v) + }, + true, + true, + ) +} +func (ec *executionContext) fieldContext___Field_args(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "__Field", + Field: field, + IsMethod: false, + IsResolver: false, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.childFields___InputValue(ctx, field) + }, + } + defer func() { + if r := recover(); r != nil { + err = ec.Recover(ctx, r) + ec.Error(ctx, err) + } + }() + ctx = graphql.WithFieldContext(ctx, fc) + if fc.Args, err = ec.field___Field_args_args(ctx, field.ArgumentMap(ec.Variables)); err != nil { + ec.Error(ctx, err) + return fc, err + } + return fc, nil +} + +func (ec *executionContext) ___Field_type(ctx context.Context, field graphql.CollectedField, obj *introspection.Field) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext___Field_type(ctx, field) + }, + func(ctx context.Context) (any, error) { + return obj.Type, nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v *introspection.Type) graphql.Marshaler { + return ec.marshalN__Type2ᚖgithubᚗcomᚋ99designsᚋgqlgenᚋgraphqlᚋintrospectionᚐType(ctx, selections, v) + }, + true, + true, + ) +} +func (ec *executionContext) fieldContext___Field_type(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "__Field", + Field: field, + IsMethod: false, + IsResolver: false, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.childFields___Type(ctx, field) + }, + } + return fc, nil +} + +func (ec *executionContext) ___Field_isDeprecated(ctx context.Context, field graphql.CollectedField, obj *introspection.Field) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext___Field_isDeprecated(ctx, field) + }, + func(ctx context.Context) (any, error) { + return obj.IsDeprecated(), nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v bool) graphql.Marshaler { + return ec.marshalNBoolean2bool(ctx, selections, v) + }, + true, + true, + ) +} +func (ec *executionContext) fieldContext___Field_isDeprecated(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + return graphql.NewScalarFieldContext("__Field", field, true, false, errors.New("field of type Boolean does not have child fields")) +} + +func (ec *executionContext) ___Field_deprecationReason(ctx context.Context, field graphql.CollectedField, obj *introspection.Field) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext___Field_deprecationReason(ctx, field) + }, + func(ctx context.Context) (any, error) { + return obj.DeprecationReason(), nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v *string) graphql.Marshaler { + return ec.marshalOString2ᚖstring(ctx, selections, v) + }, + true, + false, + ) +} +func (ec *executionContext) fieldContext___Field_deprecationReason(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + return graphql.NewScalarFieldContext("__Field", field, true, false, errors.New("field of type String does not have child fields")) +} + +func (ec *executionContext) ___InputValue_name(ctx context.Context, field graphql.CollectedField, obj *introspection.InputValue) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext___InputValue_name(ctx, field) + }, + func(ctx context.Context) (any, error) { + return obj.Name, nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v string) graphql.Marshaler { + return ec.marshalNString2string(ctx, selections, v) + }, + true, + true, + ) +} +func (ec *executionContext) fieldContext___InputValue_name(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + return graphql.NewScalarFieldContext("__InputValue", field, false, false, errors.New("field of type String does not have child fields")) +} + +func (ec *executionContext) ___InputValue_description(ctx context.Context, field graphql.CollectedField, obj *introspection.InputValue) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext___InputValue_description(ctx, field) + }, + func(ctx context.Context) (any, error) { + return obj.Description(), nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v *string) graphql.Marshaler { + return ec.marshalOString2ᚖstring(ctx, selections, v) + }, + true, + false, + ) +} +func (ec *executionContext) fieldContext___InputValue_description(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + return graphql.NewScalarFieldContext("__InputValue", field, true, false, errors.New("field of type String does not have child fields")) +} + +func (ec *executionContext) ___InputValue_type(ctx context.Context, field graphql.CollectedField, obj *introspection.InputValue) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext___InputValue_type(ctx, field) + }, + func(ctx context.Context) (any, error) { + return obj.Type, nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v *introspection.Type) graphql.Marshaler { + return ec.marshalN__Type2ᚖgithubᚗcomᚋ99designsᚋgqlgenᚋgraphqlᚋintrospectionᚐType(ctx, selections, v) + }, + true, + true, + ) +} +func (ec *executionContext) fieldContext___InputValue_type(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "__InputValue", + Field: field, + IsMethod: false, + IsResolver: false, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.childFields___Type(ctx, field) + }, + } + return fc, nil +} + +func (ec *executionContext) ___InputValue_defaultValue(ctx context.Context, field graphql.CollectedField, obj *introspection.InputValue) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext___InputValue_defaultValue(ctx, field) + }, + func(ctx context.Context) (any, error) { + return obj.DefaultValue, nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v *string) graphql.Marshaler { + return ec.marshalOString2ᚖstring(ctx, selections, v) + }, + true, + false, + ) +} +func (ec *executionContext) fieldContext___InputValue_defaultValue(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + return graphql.NewScalarFieldContext("__InputValue", field, false, false, errors.New("field of type String does not have child fields")) +} + +func (ec *executionContext) ___InputValue_isDeprecated(ctx context.Context, field graphql.CollectedField, obj *introspection.InputValue) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext___InputValue_isDeprecated(ctx, field) + }, + func(ctx context.Context) (any, error) { + return obj.IsDeprecated(), nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v bool) graphql.Marshaler { + return ec.marshalNBoolean2bool(ctx, selections, v) + }, + true, + true, + ) +} +func (ec *executionContext) fieldContext___InputValue_isDeprecated(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + return graphql.NewScalarFieldContext("__InputValue", field, true, false, errors.New("field of type Boolean does not have child fields")) +} + +func (ec *executionContext) ___InputValue_deprecationReason(ctx context.Context, field graphql.CollectedField, obj *introspection.InputValue) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext___InputValue_deprecationReason(ctx, field) + }, + func(ctx context.Context) (any, error) { + return obj.DeprecationReason(), nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v *string) graphql.Marshaler { + return ec.marshalOString2ᚖstring(ctx, selections, v) + }, + true, + false, + ) +} +func (ec *executionContext) fieldContext___InputValue_deprecationReason(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + return graphql.NewScalarFieldContext("__InputValue", field, true, false, errors.New("field of type String does not have child fields")) +} + +func (ec *executionContext) ___Schema_description(ctx context.Context, field graphql.CollectedField, obj *introspection.Schema) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext___Schema_description(ctx, field) + }, + func(ctx context.Context) (any, error) { + return obj.Description(), nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v *string) graphql.Marshaler { + return ec.marshalOString2ᚖstring(ctx, selections, v) + }, + true, + false, + ) +} +func (ec *executionContext) fieldContext___Schema_description(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + return graphql.NewScalarFieldContext("__Schema", field, true, false, errors.New("field of type String does not have child fields")) +} + +func (ec *executionContext) ___Schema_types(ctx context.Context, field graphql.CollectedField, obj *introspection.Schema) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext___Schema_types(ctx, field) + }, + func(ctx context.Context) (any, error) { + return obj.Types(), nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v []introspection.Type) graphql.Marshaler { + return ec.marshalN__Type2ᚕgithubᚗcomᚋ99designsᚋgqlgenᚋgraphqlᚋintrospectionᚐTypeᚄ(ctx, selections, v) + }, + true, + true, + ) +} +func (ec *executionContext) fieldContext___Schema_types(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "__Schema", + Field: field, + IsMethod: true, + IsResolver: false, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.childFields___Type(ctx, field) + }, + } + return fc, nil +} + +func (ec *executionContext) ___Schema_queryType(ctx context.Context, field graphql.CollectedField, obj *introspection.Schema) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext___Schema_queryType(ctx, field) + }, + func(ctx context.Context) (any, error) { + return obj.QueryType(), nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v *introspection.Type) graphql.Marshaler { + return ec.marshalN__Type2ᚖgithubᚗcomᚋ99designsᚋgqlgenᚋgraphqlᚋintrospectionᚐType(ctx, selections, v) + }, + true, + true, + ) +} +func (ec *executionContext) fieldContext___Schema_queryType(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "__Schema", + Field: field, + IsMethod: true, + IsResolver: false, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.childFields___Type(ctx, field) + }, + } + return fc, nil +} + +func (ec *executionContext) ___Schema_mutationType(ctx context.Context, field graphql.CollectedField, obj *introspection.Schema) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext___Schema_mutationType(ctx, field) + }, + func(ctx context.Context) (any, error) { + return obj.MutationType(), nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v *introspection.Type) graphql.Marshaler { + return ec.marshalO__Type2ᚖgithubᚗcomᚋ99designsᚋgqlgenᚋgraphqlᚋintrospectionᚐType(ctx, selections, v) + }, + true, + false, + ) +} +func (ec *executionContext) fieldContext___Schema_mutationType(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "__Schema", + Field: field, + IsMethod: true, + IsResolver: false, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.childFields___Type(ctx, field) + }, + } + return fc, nil +} + +func (ec *executionContext) ___Schema_subscriptionType(ctx context.Context, field graphql.CollectedField, obj *introspection.Schema) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext___Schema_subscriptionType(ctx, field) + }, + func(ctx context.Context) (any, error) { + return obj.SubscriptionType(), nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v *introspection.Type) graphql.Marshaler { + return ec.marshalO__Type2ᚖgithubᚗcomᚋ99designsᚋgqlgenᚋgraphqlᚋintrospectionᚐType(ctx, selections, v) + }, + true, + false, + ) +} +func (ec *executionContext) fieldContext___Schema_subscriptionType(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "__Schema", + Field: field, + IsMethod: true, + IsResolver: false, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.childFields___Type(ctx, field) + }, + } + return fc, nil +} + +func (ec *executionContext) ___Schema_directives(ctx context.Context, field graphql.CollectedField, obj *introspection.Schema) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext___Schema_directives(ctx, field) + }, + func(ctx context.Context) (any, error) { + return obj.Directives(), nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v []introspection.Directive) graphql.Marshaler { + return ec.marshalN__Directive2ᚕgithubᚗcomᚋ99designsᚋgqlgenᚋgraphqlᚋintrospectionᚐDirectiveᚄ(ctx, selections, v) + }, + true, + true, + ) +} +func (ec *executionContext) fieldContext___Schema_directives(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "__Schema", + Field: field, + IsMethod: true, + IsResolver: false, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.childFields___Directive(ctx, field) + }, + } + return fc, nil +} + +func (ec *executionContext) ___Type_kind(ctx context.Context, field graphql.CollectedField, obj *introspection.Type) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext___Type_kind(ctx, field) + }, + func(ctx context.Context) (any, error) { + return obj.Kind(), nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v string) graphql.Marshaler { + return ec.marshalN__TypeKind2string(ctx, selections, v) + }, + true, + true, + ) +} +func (ec *executionContext) fieldContext___Type_kind(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + return graphql.NewScalarFieldContext("__Type", field, true, false, errors.New("field of type __TypeKind does not have child fields")) +} + +func (ec *executionContext) ___Type_name(ctx context.Context, field graphql.CollectedField, obj *introspection.Type) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext___Type_name(ctx, field) + }, + func(ctx context.Context) (any, error) { + return obj.Name(), nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v *string) graphql.Marshaler { + return ec.marshalOString2ᚖstring(ctx, selections, v) + }, + true, + false, + ) +} +func (ec *executionContext) fieldContext___Type_name(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + return graphql.NewScalarFieldContext("__Type", field, true, false, errors.New("field of type String does not have child fields")) +} + +func (ec *executionContext) ___Type_description(ctx context.Context, field graphql.CollectedField, obj *introspection.Type) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext___Type_description(ctx, field) + }, + func(ctx context.Context) (any, error) { + return obj.Description(), nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v *string) graphql.Marshaler { + return ec.marshalOString2ᚖstring(ctx, selections, v) + }, + true, + false, + ) +} +func (ec *executionContext) fieldContext___Type_description(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + return graphql.NewScalarFieldContext("__Type", field, true, false, errors.New("field of type String does not have child fields")) +} + +func (ec *executionContext) ___Type_specifiedByURL(ctx context.Context, field graphql.CollectedField, obj *introspection.Type) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext___Type_specifiedByURL(ctx, field) + }, + func(ctx context.Context) (any, error) { + return obj.SpecifiedByURL(), nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v *string) graphql.Marshaler { + return ec.marshalOString2ᚖstring(ctx, selections, v) + }, + true, + false, + ) +} +func (ec *executionContext) fieldContext___Type_specifiedByURL(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + return graphql.NewScalarFieldContext("__Type", field, true, false, errors.New("field of type String does not have child fields")) +} + +func (ec *executionContext) ___Type_fields(ctx context.Context, field graphql.CollectedField, obj *introspection.Type) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext___Type_fields(ctx, field) + }, + func(ctx context.Context) (any, error) { + fc := graphql.GetFieldContext(ctx) + return obj.Fields(fc.Args["includeDeprecated"].(bool)), nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v []introspection.Field) graphql.Marshaler { + return ec.marshalO__Field2ᚕgithubᚗcomᚋ99designsᚋgqlgenᚋgraphqlᚋintrospectionᚐFieldᚄ(ctx, selections, v) + }, + true, + false, + ) +} +func (ec *executionContext) fieldContext___Type_fields(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "__Type", + Field: field, + IsMethod: true, + IsResolver: false, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.childFields___Field(ctx, field) + }, + } + defer func() { + if r := recover(); r != nil { + err = ec.Recover(ctx, r) + ec.Error(ctx, err) + } + }() + ctx = graphql.WithFieldContext(ctx, fc) + if fc.Args, err = ec.field___Type_fields_args(ctx, field.ArgumentMap(ec.Variables)); err != nil { + ec.Error(ctx, err) + return fc, err + } + return fc, nil +} + +func (ec *executionContext) ___Type_interfaces(ctx context.Context, field graphql.CollectedField, obj *introspection.Type) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext___Type_interfaces(ctx, field) + }, + func(ctx context.Context) (any, error) { + return obj.Interfaces(), nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v []introspection.Type) graphql.Marshaler { + return ec.marshalO__Type2ᚕgithubᚗcomᚋ99designsᚋgqlgenᚋgraphqlᚋintrospectionᚐTypeᚄ(ctx, selections, v) + }, + true, + false, + ) +} +func (ec *executionContext) fieldContext___Type_interfaces(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "__Type", + Field: field, + IsMethod: true, + IsResolver: false, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.childFields___Type(ctx, field) + }, + } + return fc, nil +} + +func (ec *executionContext) ___Type_possibleTypes(ctx context.Context, field graphql.CollectedField, obj *introspection.Type) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext___Type_possibleTypes(ctx, field) + }, + func(ctx context.Context) (any, error) { + return obj.PossibleTypes(), nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v []introspection.Type) graphql.Marshaler { + return ec.marshalO__Type2ᚕgithubᚗcomᚋ99designsᚋgqlgenᚋgraphqlᚋintrospectionᚐTypeᚄ(ctx, selections, v) + }, + true, + false, + ) +} +func (ec *executionContext) fieldContext___Type_possibleTypes(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "__Type", + Field: field, + IsMethod: true, + IsResolver: false, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.childFields___Type(ctx, field) + }, + } + return fc, nil +} + +func (ec *executionContext) ___Type_enumValues(ctx context.Context, field graphql.CollectedField, obj *introspection.Type) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext___Type_enumValues(ctx, field) + }, + func(ctx context.Context) (any, error) { + fc := graphql.GetFieldContext(ctx) + return obj.EnumValues(fc.Args["includeDeprecated"].(bool)), nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v []introspection.EnumValue) graphql.Marshaler { + return ec.marshalO__EnumValue2ᚕgithubᚗcomᚋ99designsᚋgqlgenᚋgraphqlᚋintrospectionᚐEnumValueᚄ(ctx, selections, v) + }, + true, + false, + ) +} +func (ec *executionContext) fieldContext___Type_enumValues(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "__Type", + Field: field, + IsMethod: true, + IsResolver: false, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.childFields___EnumValue(ctx, field) + }, + } + defer func() { + if r := recover(); r != nil { + err = ec.Recover(ctx, r) + ec.Error(ctx, err) + } + }() + ctx = graphql.WithFieldContext(ctx, fc) + if fc.Args, err = ec.field___Type_enumValues_args(ctx, field.ArgumentMap(ec.Variables)); err != nil { + ec.Error(ctx, err) + return fc, err + } + return fc, nil +} + +func (ec *executionContext) ___Type_inputFields(ctx context.Context, field graphql.CollectedField, obj *introspection.Type) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext___Type_inputFields(ctx, field) + }, + func(ctx context.Context) (any, error) { + return obj.InputFields(), nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v []introspection.InputValue) graphql.Marshaler { + return ec.marshalO__InputValue2ᚕgithubᚗcomᚋ99designsᚋgqlgenᚋgraphqlᚋintrospectionᚐInputValueᚄ(ctx, selections, v) + }, + true, + false, + ) +} +func (ec *executionContext) fieldContext___Type_inputFields(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "__Type", + Field: field, + IsMethod: true, + IsResolver: false, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.childFields___InputValue(ctx, field) + }, + } + return fc, nil +} + +func (ec *executionContext) ___Type_ofType(ctx context.Context, field graphql.CollectedField, obj *introspection.Type) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext___Type_ofType(ctx, field) + }, + func(ctx context.Context) (any, error) { + return obj.OfType(), nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v *introspection.Type) graphql.Marshaler { + return ec.marshalO__Type2ᚖgithubᚗcomᚋ99designsᚋgqlgenᚋgraphqlᚋintrospectionᚐType(ctx, selections, v) + }, + true, + false, + ) +} +func (ec *executionContext) fieldContext___Type_ofType(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "__Type", + Field: field, + IsMethod: true, + IsResolver: false, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.childFields___Type(ctx, field) + }, + } + return fc, nil +} + +func (ec *executionContext) ___Type_isOneOf(ctx context.Context, field graphql.CollectedField, obj *introspection.Type) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext___Type_isOneOf(ctx, field) + }, + func(ctx context.Context) (any, error) { + return obj.IsOneOf(), nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v bool) graphql.Marshaler { + return ec.marshalOBoolean2bool(ctx, selections, v) + }, + true, + false, + ) +} +func (ec *executionContext) fieldContext___Type_isOneOf(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + return graphql.NewScalarFieldContext("__Type", field, true, false, errors.New("field of type Boolean does not have child fields")) +} + +// endregion **************************** field.gotpl ***************************** + +// region **************************** input.gotpl ***************************** + +func (ec *executionContext) unmarshalInputDataframeAggregateInput(ctx context.Context, obj any) (model.DataframeAggregateInput, error) { + var it model.DataframeAggregateInput + if obj == nil { + return it, nil + } + + asMap := map[string]any{} + for k, v := range obj.(map[string]any) { + asMap[k] = v + } + + fieldsInOrder := [...]string{"materializationId", "dataType", "groupBy", "filters", "operation", "column"} + for _, k := range fieldsInOrder { + v, ok := asMap[k] + if !ok { + continue + } + switch k { + case "materializationId": + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("materializationId")) + data, err := ec.unmarshalOID2ᚖstring(ctx, v) + if err != nil { + return it, err + } + it.MaterializationID = data + case "dataType": + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("dataType")) + data, err := ec.unmarshalNString2string(ctx, v) + if err != nil { + return it, err + } + it.DataType = data + case "groupBy": + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("groupBy")) + data, err := ec.unmarshalOString2ᚕstringᚄ(ctx, v) + if err != nil { + return it, err + } + it.GroupBy = data + case "filters": + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("filters")) + data, err := ec.unmarshalODataframeFilterInput2ᚕᚖgithubᚗcomᚋcalyprᚋloomᚋgraphqlapiᚋmodelᚐDataframeFilterInputᚄ(ctx, v) + if err != nil { + return it, err + } + it.Filters = data + case "operation": + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("operation")) + data, err := ec.unmarshalNString2string(ctx, v) + if err != nil { + return it, err + } + it.Operation = data + case "column": + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("column")) + data, err := ec.unmarshalOString2ᚖstring(ctx, v) + if err != nil { + return it, err + } + it.Column = data + } + } + return it, nil +} + +func (ec *executionContext) unmarshalInputDataframeBuilderIntrospectionInput(ctx context.Context, obj any) (model.DataframeBuilderIntrospectionInput, error) { + var it model.DataframeBuilderIntrospectionInput + if obj == nil { + return it, nil + } + + asMap := map[string]any{} + for k, v := range obj.(map[string]any) { + asMap[k] = v + } + + if _, present := asMap["includePivotOnlyFields"]; !present { + asMap["includePivotOnlyFields"] = true + } + + fieldsInOrder := [...]string{"project", "rootResourceType", "authResourcePaths", "includePivotOnlyFields"} + for _, k := range fieldsInOrder { + v, ok := asMap[k] + if !ok { + continue + } + switch k { + case "project": + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("project")) + data, err := ec.unmarshalNString2string(ctx, v) + if err != nil { + return it, err + } + it.Project = data + case "rootResourceType": + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("rootResourceType")) + data, err := ec.unmarshalNString2string(ctx, v) + if err != nil { + return it, err + } + it.RootResourceType = data + case "authResourcePaths": + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("authResourcePaths")) + data, err := ec.unmarshalOString2ᚕstringᚄ(ctx, v) + if err != nil { + return it, err + } + it.AuthResourcePaths = data + case "includePivotOnlyFields": + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("includePivotOnlyFields")) + data, err := ec.unmarshalOBoolean2ᚖbool(ctx, v) + if err != nil { + return it, err + } + it.IncludePivotOnlyFields = data + } + } + return it, nil +} + +func (ec *executionContext) unmarshalInputDataframeDatasetInput(ctx context.Context, obj any) (model.DataframeDatasetInput, error) { + var it model.DataframeDatasetInput + if obj == nil { + return it, nil + } + + asMap := map[string]any{} + for k, v := range obj.(map[string]any) { + asMap[k] = v + } + + fieldsInOrder := [...]string{"dataType"} + for _, k := range fieldsInOrder { + v, ok := asMap[k] + if !ok { + continue + } + switch k { + case "dataType": + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("dataType")) + data, err := ec.unmarshalNString2string(ctx, v) + if err != nil { + return it, err + } + it.DataType = data + } + } + return it, nil +} + +func (ec *executionContext) unmarshalInputDataframeFilterInput(ctx context.Context, obj any) (model.DataframeFilterInput, error) { + var it model.DataframeFilterInput + if obj == nil { + return it, nil + } + + asMap := map[string]any{} + for k, v := range obj.(map[string]any) { + asMap[k] = v + } + + fieldsInOrder := [...]string{"column", "op", "value"} + for _, k := range fieldsInOrder { + v, ok := asMap[k] + if !ok { + continue + } + switch k { + case "column": + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("column")) + data, err := ec.unmarshalNString2string(ctx, v) + if err != nil { + return it, err + } + it.Column = data + case "op": + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("op")) + data, err := ec.unmarshalNString2string(ctx, v) + if err != nil { + return it, err + } + it.Op = data + case "value": + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("value")) + data, err := ec.unmarshalNJSON2encodingᚋjsonᚐRawMessage(ctx, v) + if err != nil { + return it, err + } + it.Value = data + } + } + return it, nil +} + +func (ec *executionContext) unmarshalInputDataframeRecipeBindingsInput(ctx context.Context, obj any) (model.DataframeRecipeBindingsInput, error) { + var it model.DataframeRecipeBindingsInput + if obj == nil { + return it, nil + } + + asMap := map[string]any{} + for k, v := range obj.(map[string]any) { + asMap[k] = v + } + + if _, present := asMap["previewLimit"]; !present { + asMap["previewLimit"] = 25 + } + + fieldsInOrder := [...]string{"project", "datasetGeneration", "authResourcePaths", "previewLimit"} + for _, k := range fieldsInOrder { + v, ok := asMap[k] + if !ok { + continue + } + switch k { + case "project": + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("project")) + data, err := ec.unmarshalNString2string(ctx, v) + if err != nil { + return it, err + } + it.Project = data + case "datasetGeneration": + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("datasetGeneration")) + data, err := ec.unmarshalOString2ᚖstring(ctx, v) + if err != nil { + return it, err + } + it.DatasetGeneration = data + case "authResourcePaths": + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("authResourcePaths")) + data, err := ec.unmarshalOString2ᚕstringᚄ(ctx, v) + if err != nil { + return it, err + } + it.AuthResourcePaths = data + case "previewLimit": + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("previewLimit")) + data, err := ec.unmarshalOInt2ᚖint(ctx, v) + if err != nil { + return it, err + } + it.PreviewLimit = data + } + } + return it, nil +} + +func (ec *executionContext) unmarshalInputDataframeRowsInput(ctx context.Context, obj any) (model.DataframeRowsInput, error) { + var it model.DataframeRowsInput + if obj == nil { + return it, nil + } + + asMap := map[string]any{} + for k, v := range obj.(map[string]any) { + asMap[k] = v + } + + if _, present := asMap["first"]; !present { + asMap["first"] = 100 + } + + fieldsInOrder := [...]string{"materializationId", "dataType", "columns", "filters", "sort", "first", "after"} + for _, k := range fieldsInOrder { + v, ok := asMap[k] + if !ok { + continue + } + switch k { + case "materializationId": + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("materializationId")) + data, err := ec.unmarshalOID2ᚖstring(ctx, v) + if err != nil { + return it, err + } + it.MaterializationID = data + case "dataType": + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("dataType")) + data, err := ec.unmarshalNString2string(ctx, v) + if err != nil { + return it, err + } + it.DataType = data + case "columns": + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("columns")) + data, err := ec.unmarshalOString2ᚕstringᚄ(ctx, v) + if err != nil { + return it, err + } + it.Columns = data + case "filters": + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("filters")) + data, err := ec.unmarshalODataframeFilterInput2ᚕᚖgithubᚗcomᚋcalyprᚋloomᚋgraphqlapiᚋmodelᚐDataframeFilterInputᚄ(ctx, v) + if err != nil { + return it, err + } + it.Filters = data + case "sort": + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("sort")) + data, err := ec.unmarshalODataframeSortInput2ᚖgithubᚗcomᚋcalyprᚋloomᚋgraphqlapiᚋmodelᚐDataframeSortInput(ctx, v) + if err != nil { + return it, err + } + it.Sort = data + case "first": + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("first")) + data, err := ec.unmarshalOInt2ᚖint(ctx, v) + if err != nil { + return it, err + } + it.First = data + case "after": + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("after")) + data, err := ec.unmarshalOString2ᚖstring(ctx, v) + if err != nil { + return it, err + } + it.After = data + } + } + return it, nil +} + +func (ec *executionContext) unmarshalInputDataframeSortInput(ctx context.Context, obj any) (model.DataframeSortInput, error) { + var it model.DataframeSortInput + if obj == nil { + return it, nil + } + + asMap := map[string]any{} + for k, v := range obj.(map[string]any) { + asMap[k] = v + } + + if _, present := asMap["desc"]; !present { + asMap["desc"] = false + } + + fieldsInOrder := [...]string{"column", "desc"} + for _, k := range fieldsInOrder { + v, ok := asMap[k] + if !ok { + continue + } + switch k { + case "column": + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("column")) + data, err := ec.unmarshalNString2string(ctx, v) + if err != nil { + return it, err + } + it.Column = data + case "desc": + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("desc")) + data, err := ec.unmarshalOBoolean2ᚖbool(ctx, v) + if err != nil { + return it, err + } + it.Desc = data + } + } + return it, nil +} + +func (ec *executionContext) unmarshalInputExplainDataframeRecipeInput(ctx context.Context, obj any) (model.ExplainDataframeRecipeInput, error) { + var it model.ExplainDataframeRecipeInput + if obj == nil { + return it, nil + } + + asMap := map[string]any{} + for k, v := range obj.(map[string]any) { + asMap[k] = v + } + + if _, present := asMap["live"]; !present { + asMap["live"] = false + } + + fieldsInOrder := [...]string{"name", "bindings", "live"} + for _, k := range fieldsInOrder { + v, ok := asMap[k] + if !ok { + continue + } + switch k { + case "name": + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("name")) + data, err := ec.unmarshalNString2string(ctx, v) + if err != nil { + return it, err + } + it.Name = data + case "bindings": + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("bindings")) + data, err := ec.unmarshalNDataframeRecipeBindingsInput2ᚖgithubᚗcomᚋcalyprᚋloomᚋgraphqlapiᚋmodelᚐDataframeRecipeBindingsInput(ctx, v) + if err != nil { + return it, err + } + it.Bindings = data + case "live": + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("live")) + data, err := ec.unmarshalOBoolean2ᚖbool(ctx, v) + if err != nil { + return it, err + } + it.Live = data + } + } + return it, nil +} + +func (ec *executionContext) unmarshalInputFhirAggregateInput(ctx context.Context, obj any) (model.FhirAggregateInput, error) { + var it model.FhirAggregateInput + if obj == nil { + return it, nil + } + + asMap := map[string]any{} + for k, v := range obj.(map[string]any) { + asMap[k] = v + } + + if _, present := asMap["valueMode"]; !present { + asMap["valueMode"] = "AUTO" + } + + fieldsInOrder := [...]string{"name", "operation", "fieldRef", "fhirPath", "predicateFieldRef", "predicatePath", "predicateEquals", "valueMode"} + for _, k := range fieldsInOrder { + v, ok := asMap[k] + if !ok { + continue + } + switch k { + case "name": + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("name")) + data, err := ec.unmarshalNString2string(ctx, v) + if err != nil { + return it, err + } + it.Name = data + case "operation": + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("operation")) + data, err := ec.unmarshalNFhirAggregateOperation2githubᚗcomᚋcalyprᚋloomᚋgraphqlapiᚋmodelᚐFhirAggregateOperation(ctx, v) + if err != nil { + return it, err + } + it.Operation = data + case "fieldRef": + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("fieldRef")) + data, err := ec.unmarshalOString2ᚖstring(ctx, v) + if err != nil { + return it, err + } + it.FieldRef = data + case "fhirPath": + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("fhirPath")) + data, err := ec.unmarshalOString2ᚖstring(ctx, v) + if err != nil { + return it, err + } + it.FhirPath = data + case "predicateFieldRef": + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("predicateFieldRef")) + data, err := ec.unmarshalOString2ᚖstring(ctx, v) + if err != nil { + return it, err + } + it.PredicateFieldRef = data + case "predicatePath": + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("predicatePath")) + data, err := ec.unmarshalOString2ᚖstring(ctx, v) + if err != nil { + return it, err + } + it.PredicatePath = data + case "predicateEquals": + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("predicateEquals")) + data, err := ec.unmarshalOString2ᚖstring(ctx, v) + if err != nil { + return it, err + } + it.PredicateEquals = data + case "valueMode": + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("valueMode")) + data, err := ec.unmarshalNFhirValueMode2githubᚗcomᚋcalyprᚋloomᚋgraphqlapiᚋmodelᚐFhirValueMode(ctx, v) + if err != nil { + return it, err + } + it.ValueMode = data + } + } + return it, nil +} + +func (ec *executionContext) unmarshalInputFhirCatalogProjectionInput(ctx context.Context, obj any) (model.FhirCatalogProjectionInput, error) { + var it model.FhirCatalogProjectionInput + if obj == nil { + return it, nil + } + + asMap := map[string]any{} + for k, v := range obj.(map[string]any) { + asMap[k] = v + } + + if _, present := asMap["includePaths"]; !present { + asMap["includePaths"] = []any{} + } + if _, present := asMap["excludePaths"]; !present { + asMap["excludePaths"] = []any{} + } + if _, present := asMap["kinds"]; !present { + asMap["kinds"] = []any{} + } + if _, present := asMap["naming"]; !present { + asMap["naming"] = "PATH" + } + if _, present := asMap["valueMode"]; !present { + asMap["valueMode"] = "FIRST" + } + + fieldsInOrder := [...]string{"name", "includePaths", "excludePaths", "kinds", "naming", "valueMode", "maxColumns"} + for _, k := range fieldsInOrder { + v, ok := asMap[k] + if !ok { + continue + } + switch k { + case "name": + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("name")) + data, err := ec.unmarshalNString2string(ctx, v) + if err != nil { + return it, err + } + it.Name = data + case "includePaths": + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("includePaths")) + data, err := ec.unmarshalNString2ᚕstringᚄ(ctx, v) + if err != nil { + return it, err + } + it.IncludePaths = data + case "excludePaths": + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("excludePaths")) + data, err := ec.unmarshalNString2ᚕstringᚄ(ctx, v) + if err != nil { + return it, err + } + it.ExcludePaths = data + case "kinds": + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("kinds")) + data, err := ec.unmarshalNString2ᚕstringᚄ(ctx, v) + if err != nil { + return it, err + } + it.Kinds = data + case "naming": + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("naming")) + data, err := ec.unmarshalNFhirColumnNaming2githubᚗcomᚋcalyprᚋloomᚋgraphqlapiᚋmodelᚐFhirColumnNaming(ctx, v) + if err != nil { + return it, err + } + it.Naming = data + case "valueMode": + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("valueMode")) + data, err := ec.unmarshalNFhirValueMode2githubᚗcomᚋcalyprᚋloomᚋgraphqlapiᚋmodelᚐFhirValueMode(ctx, v) + if err != nil { + return it, err + } + it.ValueMode = data + case "maxColumns": + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("maxColumns")) + data, err := ec.unmarshalNInt2int(ctx, v) + if err != nil { + return it, err + } + it.MaxColumns = data + } + } + return it, nil +} + +func (ec *executionContext) unmarshalInputFhirCodeValueInput(ctx context.Context, obj any) (model.FhirCodeValueInput, error) { + var it model.FhirCodeValueInput + if obj == nil { + return it, nil + } + + asMap := map[string]any{} + for k, v := range obj.(map[string]any) { + asMap[k] = v + } + + fieldsInOrder := [...]string{"system", "code", "display"} + for _, k := range fieldsInOrder { + v, ok := asMap[k] + if !ok { + continue + } + switch k { + case "system": + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("system")) + data, err := ec.unmarshalOString2ᚖstring(ctx, v) + if err != nil { + return it, err + } + it.System = data + case "code": + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("code")) + data, err := ec.unmarshalNString2string(ctx, v) + if err != nil { + return it, err + } + it.Code = data + case "display": + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("display")) + data, err := ec.unmarshalOString2ᚖstring(ctx, v) + if err != nil { + return it, err + } + it.Display = data + } + } + return it, nil +} + +func (ec *executionContext) unmarshalInputFhirDataframeInput(ctx context.Context, obj any) (model.FhirDataframeInput, error) { + var it model.FhirDataframeInput + if obj == nil { + return it, nil + } + + asMap := map[string]any{} + for k, v := range obj.(map[string]any) { + asMap[k] = v + } + + fieldsInOrder := [...]string{"project", "authResourcePath", "authResourcePaths", "rootResourceType", "rowGrain", "rootFields", "rootFilters", "rootPivots", "rootAggregates", "rootSlices", "rootCatalogProjections", "traverse", "limit", "cursor"} + for _, k := range fieldsInOrder { + v, ok := asMap[k] + if !ok { + continue + } + switch k { + case "project": + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("project")) + data, err := ec.unmarshalNString2string(ctx, v) + if err != nil { + return it, err + } + it.Project = data + case "authResourcePath": + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("authResourcePath")) + data, err := ec.unmarshalOString2ᚖstring(ctx, v) + if err != nil { + return it, err + } + it.AuthResourcePath = data + case "authResourcePaths": + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("authResourcePaths")) + data, err := ec.unmarshalOString2ᚕstringᚄ(ctx, v) + if err != nil { + return it, err + } + it.AuthResourcePaths = data + case "rootResourceType": + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("rootResourceType")) + data, err := ec.unmarshalNString2string(ctx, v) + if err != nil { + return it, err + } + it.RootResourceType = data + case "rowGrain": + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("rowGrain")) + data, err := ec.unmarshalOString2ᚖstring(ctx, v) + if err != nil { + return it, err + } + it.RowGrain = data + case "rootFields": + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("rootFields")) + data, err := ec.unmarshalOFhirFieldSelectInput2ᚕᚖgithubᚗcomᚋcalyprᚋloomᚋgraphqlapiᚋmodelᚐFhirFieldSelectInputᚄ(ctx, v) + if err != nil { + return it, err + } + it.RootFields = data + case "rootFilters": + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("rootFilters")) + data, err := ec.unmarshalOFhirFilterInput2ᚕᚖgithubᚗcomᚋcalyprᚋloomᚋgraphqlapiᚋmodelᚐFhirFilterInputᚄ(ctx, v) + if err != nil { + return it, err + } + it.RootFilters = data + case "rootPivots": + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("rootPivots")) + data, err := ec.unmarshalOFhirPivotInput2ᚕᚖgithubᚗcomᚋcalyprᚋloomᚋgraphqlapiᚋmodelᚐFhirPivotInputᚄ(ctx, v) + if err != nil { + return it, err + } + it.RootPivots = data + case "rootAggregates": + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("rootAggregates")) + data, err := ec.unmarshalOFhirAggregateInput2ᚕᚖgithubᚗcomᚋcalyprᚋloomᚋgraphqlapiᚋmodelᚐFhirAggregateInputᚄ(ctx, v) + if err != nil { + return it, err + } + it.RootAggregates = data + case "rootSlices": + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("rootSlices")) + data, err := ec.unmarshalOFhirRepresentativeSliceInput2ᚕᚖgithubᚗcomᚋcalyprᚋloomᚋgraphqlapiᚋmodelᚐFhirRepresentativeSliceInputᚄ(ctx, v) + if err != nil { + return it, err + } + it.RootSlices = data + case "rootCatalogProjections": + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("rootCatalogProjections")) + data, err := ec.unmarshalOFhirCatalogProjectionInput2ᚕᚖgithubᚗcomᚋcalyprᚋloomᚋgraphqlapiᚋmodelᚐFhirCatalogProjectionInputᚄ(ctx, v) + if err != nil { + return it, err + } + it.RootCatalogProjections = data + case "traverse": + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("traverse")) + data, err := ec.unmarshalOFhirTraversalStepInput2ᚕᚖgithubᚗcomᚋcalyprᚋloomᚋgraphqlapiᚋmodelᚐFhirTraversalStepInputᚄ(ctx, v) + if err != nil { + return it, err + } + it.Traverse = data + case "limit": + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("limit")) + data, err := ec.unmarshalOInt2ᚖint(ctx, v) + if err != nil { + return it, err + } + it.Limit = data + case "cursor": + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("cursor")) + data, err := ec.unmarshalOString2ᚖstring(ctx, v) + if err != nil { + return it, err + } + it.Cursor = data + } + } + return it, nil +} + +func (ec *executionContext) unmarshalInputFhirFieldPredicateInput(ctx context.Context, obj any) (model.FhirFieldPredicateInput, error) { + var it model.FhirFieldPredicateInput + if obj == nil { + return it, nil + } + + asMap := map[string]any{} + for k, v := range obj.(map[string]any) { + asMap[k] = v + } + + if _, present := asMap["op"]; !present { + asMap["op"] = "CONTAINS" + } + + fieldsInOrder := [...]string{"path", "op", "value"} + for _, k := range fieldsInOrder { + v, ok := asMap[k] + if !ok { + continue + } + switch k { + case "path": + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("path")) + data, err := ec.unmarshalNString2string(ctx, v) + if err != nil { + return it, err + } + it.Path = data + case "op": + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("op")) + data, err := ec.unmarshalNFhirFieldPredicateOperation2githubᚗcomᚋcalyprᚋloomᚋgraphqlapiᚋmodelᚐFhirFieldPredicateOperation(ctx, v) + if err != nil { + return it, err + } + it.Op = data + case "value": + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("value")) + data, err := ec.unmarshalNString2string(ctx, v) + if err != nil { + return it, err + } + it.Value = data + } + } + return it, nil +} + +func (ec *executionContext) unmarshalInputFhirFieldSelectInput(ctx context.Context, obj any) (model.FhirFieldSelectInput, error) { + var it model.FhirFieldSelectInput + if obj == nil { + return it, nil + } + + asMap := map[string]any{} + for k, v := range obj.(map[string]any) { + asMap[k] = v + } + + if _, present := asMap["fallbackFieldRefs"]; !present { + asMap["fallbackFieldRefs"] = []any{} + } + if _, present := asMap["fallbackSelectors"]; !present { + asMap["fallbackSelectors"] = []any{} + } + if _, present := asMap["valueMode"]; !present { + asMap["valueMode"] = "AUTO" + } + + fieldsInOrder := [...]string{"name", "fieldRef", "selector", "selectionHint", "fallbackFieldRefs", "fallbackSelectors", "valueMode"} + for _, k := range fieldsInOrder { + v, ok := asMap[k] + if !ok { + continue + } + switch k { + case "name": + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("name")) + data, err := ec.unmarshalNString2string(ctx, v) + if err != nil { + return it, err + } + it.Name = data + case "fieldRef": + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("fieldRef")) + data, err := ec.unmarshalOString2ᚖstring(ctx, v) + if err != nil { + return it, err + } + it.FieldRef = data + case "selector": + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("selector")) + data, err := ec.unmarshalOFhirFieldSelectorInput2ᚖgithubᚗcomᚋcalyprᚋloomᚋgraphqlapiᚋmodelᚐFhirFieldSelectorInput(ctx, v) + if err != nil { + return it, err + } + it.Selector = data + case "selectionHint": + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("selectionHint")) + data, err := ec.unmarshalOString2ᚖstring(ctx, v) + if err != nil { + return it, err + } + it.SelectionHint = data + case "fallbackFieldRefs": + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("fallbackFieldRefs")) + data, err := ec.unmarshalNString2ᚕstringᚄ(ctx, v) + if err != nil { + return it, err + } + it.FallbackFieldRefs = data + case "fallbackSelectors": + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("fallbackSelectors")) + data, err := ec.unmarshalNFhirFieldSelectorInput2ᚕᚖgithubᚗcomᚋcalyprᚋloomᚋgraphqlapiᚋmodelᚐFhirFieldSelectorInputᚄ(ctx, v) + if err != nil { + return it, err + } + it.FallbackSelectors = data + case "valueMode": + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("valueMode")) + data, err := ec.unmarshalNFhirValueMode2githubᚗcomᚋcalyprᚋloomᚋgraphqlapiᚋmodelᚐFhirValueMode(ctx, v) + if err != nil { + return it, err + } + it.ValueMode = data + } + } + return it, nil +} + +func (ec *executionContext) unmarshalInputFhirFieldSelectorInput(ctx context.Context, obj any) (model.FhirFieldSelectorInput, error) { + var it model.FhirFieldSelectorInput + if obj == nil { + return it, nil + } + + asMap := map[string]any{} + for k, v := range obj.(map[string]any) { + asMap[k] = v + } + + fieldsInOrder := [...]string{"sourcePath", "where", "valuePath"} + for _, k := range fieldsInOrder { + v, ok := asMap[k] + if !ok { + continue + } + switch k { + case "sourcePath": + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("sourcePath")) + data, err := ec.unmarshalOString2ᚖstring(ctx, v) + if err != nil { + return it, err + } + it.SourcePath = data + case "where": + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("where")) + data, err := ec.unmarshalOFhirFieldPredicateInput2ᚖgithubᚗcomᚋcalyprᚋloomᚋgraphqlapiᚋmodelᚐFhirFieldPredicateInput(ctx, v) + if err != nil { + return it, err + } + it.Where = data + case "valuePath": + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("valuePath")) + data, err := ec.unmarshalNString2string(ctx, v) + if err != nil { + return it, err + } + it.ValuePath = data + } + } + return it, nil +} + +func (ec *executionContext) unmarshalInputFhirFilterInput(ctx context.Context, obj any) (model.FhirFilterInput, error) { + var it model.FhirFilterInput + if obj == nil { + return it, nil + } + + asMap := map[string]any{} + for k, v := range obj.(map[string]any) { + asMap[k] = v + } + + if _, present := asMap["quantifier"]; !present { + asMap["quantifier"] = "ANY" + } + if _, present := asMap["values"]; !present { + asMap["values"] = []any{} + } + + fieldsInOrder := [...]string{"select", "fieldRef", "operator", "quantifier", "values"} + for _, k := range fieldsInOrder { + v, ok := asMap[k] + if !ok { + continue + } + switch k { + case "select": + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("select")) + data, err := ec.unmarshalNString2string(ctx, v) + if err != nil { + return it, err + } + it.Select = data + case "fieldRef": + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("fieldRef")) + data, err := ec.unmarshalOString2ᚖstring(ctx, v) + if err != nil { + return it, err + } + it.FieldRef = data + case "operator": + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("operator")) + data, err := ec.unmarshalNFhirFilterOperator2githubᚗcomᚋcalyprᚋloomᚋgraphqlapiᚋmodelᚐFhirFilterOperator(ctx, v) + if err != nil { + return it, err + } + it.Operator = data + case "quantifier": + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("quantifier")) + data, err := ec.unmarshalOFhirFilterQuantifier2ᚖgithubᚗcomᚋcalyprᚋloomᚋgraphqlapiᚋmodelᚐFhirFilterQuantifier(ctx, v) + if err != nil { + return it, err + } + it.Quantifier = data + case "values": + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("values")) + data, err := ec.unmarshalNFhirFilterValueInput2ᚕᚖgithubᚗcomᚋcalyprᚋloomᚋgraphqlapiᚋmodelᚐFhirFilterValueInputᚄ(ctx, v) + if err != nil { + return it, err + } + it.Values = data + } + } + return it, nil +} + +func (ec *executionContext) unmarshalInputFhirFilterValueInput(ctx context.Context, obj any) (model.FhirFilterValueInput, error) { + var it model.FhirFilterValueInput + if obj == nil { + return it, nil + } + + asMap := map[string]any{} + for k, v := range obj.(map[string]any) { + asMap[k] = v + } + + fieldsInOrder := [...]string{"kind", "string", "code", "boolean", "integer", "decimal", "date", "dateTime"} + for _, k := range fieldsInOrder { + v, ok := asMap[k] + if !ok { + continue + } + switch k { + case "kind": + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("kind")) + data, err := ec.unmarshalNFhirFilterValueKind2githubᚗcomᚋcalyprᚋloomᚋgraphqlapiᚋmodelᚐFhirFilterValueKind(ctx, v) + if err != nil { + return it, err + } + it.Kind = data + case "string": + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("string")) + data, err := ec.unmarshalOString2ᚖstring(ctx, v) + if err != nil { + return it, err + } + it.String = data + case "code": + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("code")) + data, err := ec.unmarshalOFhirCodeValueInput2ᚖgithubᚗcomᚋcalyprᚋloomᚋgraphqlapiᚋmodelᚐFhirCodeValueInput(ctx, v) + if err != nil { + return it, err + } + it.Code = data + case "boolean": + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("boolean")) + data, err := ec.unmarshalOBoolean2ᚖbool(ctx, v) + if err != nil { + return it, err + } + it.Boolean = data + case "integer": + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("integer")) + data, err := ec.unmarshalOInt2ᚖint(ctx, v) + if err != nil { + return it, err + } + it.Integer = data + case "decimal": + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("decimal")) + data, err := ec.unmarshalOFloat2ᚖfloat64(ctx, v) + if err != nil { + return it, err + } + it.Decimal = data + case "date": + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("date")) + data, err := ec.unmarshalOString2ᚖstring(ctx, v) + if err != nil { + return it, err + } + it.Date = data + case "dateTime": + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("dateTime")) + data, err := ec.unmarshalOString2ᚖstring(ctx, v) + if err != nil { + return it, err + } + it.DateTime = data + } + } + return it, nil +} + +func (ec *executionContext) unmarshalInputFhirPivotDiscoveryInput(ctx context.Context, obj any) (model.FhirPivotDiscoveryInput, error) { + var it model.FhirPivotDiscoveryInput + if obj == nil { + return it, nil + } + + asMap := map[string]any{} + for k, v := range obj.(map[string]any) { + asMap[k] = v + } + + fieldsInOrder := [...]string{"family", "path", "maxColumns"} + for _, k := range fieldsInOrder { + v, ok := asMap[k] + if !ok { + continue + } + switch k { + case "family": + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("family")) + data, err := ec.unmarshalOString2ᚖstring(ctx, v) + if err != nil { + return it, err + } + it.Family = data + case "path": + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("path")) + data, err := ec.unmarshalOString2ᚖstring(ctx, v) + if err != nil { + return it, err + } + it.Path = data + case "maxColumns": + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("maxColumns")) + data, err := ec.unmarshalNInt2int(ctx, v) + if err != nil { + return it, err + } + it.MaxColumns = data + } + } + return it, nil +} + +func (ec *executionContext) unmarshalInputFhirPivotInput(ctx context.Context, obj any) (model.FhirPivotInput, error) { + var it model.FhirPivotInput + if obj == nil { + return it, nil + } + + asMap := map[string]any{} + for k, v := range obj.(map[string]any) { + asMap[k] = v + } + + if _, present := asMap["columns"]; !present { + asMap["columns"] = []any{} + } + + fieldsInOrder := [...]string{"name", "fieldRef", "columnSelector", "valueSelector", "columns", "discovery"} + for _, k := range fieldsInOrder { + v, ok := asMap[k] + if !ok { + continue + } + switch k { + case "name": + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("name")) + data, err := ec.unmarshalNString2string(ctx, v) + if err != nil { + return it, err + } + it.Name = data + case "fieldRef": + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("fieldRef")) + data, err := ec.unmarshalOString2ᚖstring(ctx, v) + if err != nil { + return it, err + } + it.FieldRef = data + case "columnSelector": + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("columnSelector")) + data, err := ec.unmarshalOFhirFieldSelectorInput2ᚖgithubᚗcomᚋcalyprᚋloomᚋgraphqlapiᚋmodelᚐFhirFieldSelectorInput(ctx, v) + if err != nil { + return it, err + } + it.ColumnSelector = data + case "valueSelector": + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("valueSelector")) + data, err := ec.unmarshalOFhirFieldSelectorInput2ᚖgithubᚗcomᚋcalyprᚋloomᚋgraphqlapiᚋmodelᚐFhirFieldSelectorInput(ctx, v) + if err != nil { + return it, err + } + it.ValueSelector = data + case "columns": + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("columns")) + data, err := ec.unmarshalNString2ᚕstringᚄ(ctx, v) + if err != nil { + return it, err + } + it.Columns = data + case "discovery": + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("discovery")) + data, err := ec.unmarshalOFhirPivotDiscoveryInput2ᚖgithubᚗcomᚋcalyprᚋloomᚋgraphqlapiᚋmodelᚐFhirPivotDiscoveryInput(ctx, v) + if err != nil { + return it, err + } + it.Discovery = data + } + } + return it, nil +} + +func (ec *executionContext) unmarshalInputFhirRepresentativeSliceInput(ctx context.Context, obj any) (model.FhirRepresentativeSliceInput, error) { + var it model.FhirRepresentativeSliceInput + if obj == nil { + return it, nil + } + + asMap := map[string]any{} + for k, v := range obj.(map[string]any) { + asMap[k] = v + } + + if _, present := asMap["fields"]; !present { + asMap["fields"] = []any{} + } + + fieldsInOrder := [...]string{"name", "limit", "whereFieldRef", "wherePath", "whereEquals", "fields"} + for _, k := range fieldsInOrder { + v, ok := asMap[k] + if !ok { + continue + } + switch k { + case "name": + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("name")) + data, err := ec.unmarshalNString2string(ctx, v) + if err != nil { + return it, err + } + it.Name = data + case "limit": + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("limit")) + data, err := ec.unmarshalNInt2int(ctx, v) + if err != nil { + return it, err + } + it.Limit = data + case "whereFieldRef": + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("whereFieldRef")) + data, err := ec.unmarshalOString2ᚖstring(ctx, v) + if err != nil { + return it, err + } + it.WhereFieldRef = data + case "wherePath": + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("wherePath")) + data, err := ec.unmarshalOString2ᚖstring(ctx, v) + if err != nil { + return it, err + } + it.WherePath = data + case "whereEquals": + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("whereEquals")) + data, err := ec.unmarshalOString2ᚖstring(ctx, v) + if err != nil { + return it, err + } + it.WhereEquals = data + case "fields": + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("fields")) + data, err := ec.unmarshalNFhirFieldSelectInput2ᚕᚖgithubᚗcomᚋcalyprᚋloomᚋgraphqlapiᚋmodelᚐFhirFieldSelectInputᚄ(ctx, v) + if err != nil { + return it, err + } + it.Fields = data + } + } + return it, nil +} + +func (ec *executionContext) unmarshalInputFhirTraversalStepInput(ctx context.Context, obj any) (model.FhirTraversalStepInput, error) { + var it model.FhirTraversalStepInput + if obj == nil { + return it, nil + } + + asMap := map[string]any{} + for k, v := range obj.(map[string]any) { + asMap[k] = v + } + + if _, present := asMap["matchMode"]; !present { + asMap["matchMode"] = "OPTIONAL" + } + if _, present := asMap["fields"]; !present { + asMap["fields"] = []any{} + } + if _, present := asMap["filters"]; !present { + asMap["filters"] = []any{} + } + if _, present := asMap["pivots"]; !present { + asMap["pivots"] = []any{} + } + if _, present := asMap["aggregates"]; !present { + asMap["aggregates"] = []any{} + } + if _, present := asMap["slices"]; !present { + asMap["slices"] = []any{} + } + if _, present := asMap["catalogProjections"]; !present { + asMap["catalogProjections"] = []any{} + } + if _, present := asMap["traverse"]; !present { + asMap["traverse"] = []any{} + } + + fieldsInOrder := [...]string{"edgeLabel", "toResourceType", "alias", "matchMode", "fields", "filters", "pivots", "aggregates", "slices", "catalogProjections", "traverse"} + for _, k := range fieldsInOrder { + v, ok := asMap[k] + if !ok { + continue + } + switch k { + case "edgeLabel": + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("edgeLabel")) + data, err := ec.unmarshalNString2string(ctx, v) + if err != nil { + return it, err + } + it.EdgeLabel = data + case "toResourceType": + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("toResourceType")) + data, err := ec.unmarshalNString2string(ctx, v) + if err != nil { + return it, err + } + it.ToResourceType = data + case "alias": + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("alias")) + data, err := ec.unmarshalNString2string(ctx, v) + if err != nil { + return it, err + } + it.Alias = data + case "matchMode": + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("matchMode")) + data, err := ec.unmarshalOFhirTraversalMatchMode2ᚖgithubᚗcomᚋcalyprᚋloomᚋgraphqlapiᚋmodelᚐFhirTraversalMatchMode(ctx, v) + if err != nil { + return it, err + } + it.MatchMode = data + case "fields": + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("fields")) + data, err := ec.unmarshalNFhirFieldSelectInput2ᚕᚖgithubᚗcomᚋcalyprᚋloomᚋgraphqlapiᚋmodelᚐFhirFieldSelectInputᚄ(ctx, v) + if err != nil { + return it, err + } + it.Fields = data + case "filters": + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("filters")) + data, err := ec.unmarshalOFhirFilterInput2ᚕᚖgithubᚗcomᚋcalyprᚋloomᚋgraphqlapiᚋmodelᚐFhirFilterInputᚄ(ctx, v) + if err != nil { + return it, err + } + it.Filters = data + case "pivots": + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("pivots")) + data, err := ec.unmarshalOFhirPivotInput2ᚕᚖgithubᚗcomᚋcalyprᚋloomᚋgraphqlapiᚋmodelᚐFhirPivotInputᚄ(ctx, v) + if err != nil { + return it, err + } + it.Pivots = data + case "aggregates": + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("aggregates")) + data, err := ec.unmarshalOFhirAggregateInput2ᚕᚖgithubᚗcomᚋcalyprᚋloomᚋgraphqlapiᚋmodelᚐFhirAggregateInputᚄ(ctx, v) + if err != nil { + return it, err + } + it.Aggregates = data + case "slices": + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("slices")) + data, err := ec.unmarshalOFhirRepresentativeSliceInput2ᚕᚖgithubᚗcomᚋcalyprᚋloomᚋgraphqlapiᚋmodelᚐFhirRepresentativeSliceInputᚄ(ctx, v) + if err != nil { + return it, err + } + it.Slices = data + case "catalogProjections": + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("catalogProjections")) + data, err := ec.unmarshalOFhirCatalogProjectionInput2ᚕᚖgithubᚗcomᚋcalyprᚋloomᚋgraphqlapiᚋmodelᚐFhirCatalogProjectionInputᚄ(ctx, v) + if err != nil { + return it, err + } + it.CatalogProjections = data + case "traverse": + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("traverse")) + data, err := ec.unmarshalOFhirTraversalStepInput2ᚕᚖgithubᚗcomᚋcalyprᚋloomᚋgraphqlapiᚋmodelᚐFhirTraversalStepInputᚄ(ctx, v) + if err != nil { + return it, err + } + it.Traverse = data + } + } + return it, nil +} + +func (ec *executionContext) unmarshalInputMaterializeDataframeRecipeInput(ctx context.Context, obj any) (model.MaterializeDataframeRecipeInput, error) { + var it model.MaterializeDataframeRecipeInput + if obj == nil { + return it, nil + } + + asMap := map[string]any{} + for k, v := range obj.(map[string]any) { + asMap[k] = v + } + + fieldsInOrder := [...]string{"name", "bindings"} + for _, k := range fieldsInOrder { + v, ok := asMap[k] + if !ok { + continue + } + switch k { + case "name": + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("name")) + data, err := ec.unmarshalNString2string(ctx, v) + if err != nil { + return it, err + } + it.Name = data + case "bindings": + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("bindings")) + data, err := ec.unmarshalNDataframeRecipeBindingsInput2ᚖgithubᚗcomᚋcalyprᚋloomᚋgraphqlapiᚋmodelᚐDataframeRecipeBindingsInput(ctx, v) + if err != nil { + return it, err + } + it.Bindings = data + } + } + return it, nil +} + +func (ec *executionContext) unmarshalInputPreflightDataframeRecipeInput(ctx context.Context, obj any) (model.PreflightDataframeRecipeInput, error) { + var it model.PreflightDataframeRecipeInput + if obj == nil { + return it, nil + } + + asMap := map[string]any{} + for k, v := range obj.(map[string]any) { + asMap[k] = v + } + + fieldsInOrder := [...]string{"name", "bindings"} + for _, k := range fieldsInOrder { + v, ok := asMap[k] + if !ok { + continue + } + switch k { + case "name": + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("name")) + data, err := ec.unmarshalNString2string(ctx, v) + if err != nil { + return it, err + } + it.Name = data + case "bindings": + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("bindings")) + data, err := ec.unmarshalNDataframeRecipeBindingsInput2ᚖgithubᚗcomᚋcalyprᚋloomᚋgraphqlapiᚋmodelᚐDataframeRecipeBindingsInput(ctx, v) + if err != nil { + return it, err + } + it.Bindings = data + } + } + return it, nil +} + +func (ec *executionContext) unmarshalInputPreviewDataframeRecipeInput(ctx context.Context, obj any) (model.PreviewDataframeRecipeInput, error) { + var it model.PreviewDataframeRecipeInput + if obj == nil { + return it, nil + } + + asMap := map[string]any{} + for k, v := range obj.(map[string]any) { + asMap[k] = v + } + + if _, present := asMap["limit"]; !present { + asMap["limit"] = 25 + } + + fieldsInOrder := [...]string{"name", "bindings", "limit", "outputs"} + for _, k := range fieldsInOrder { + v, ok := asMap[k] + if !ok { + continue + } + switch k { + case "name": + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("name")) + data, err := ec.unmarshalNString2string(ctx, v) + if err != nil { + return it, err + } + it.Name = data + case "bindings": + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("bindings")) + data, err := ec.unmarshalNDataframeRecipeBindingsInput2ᚖgithubᚗcomᚋcalyprᚋloomᚋgraphqlapiᚋmodelᚐDataframeRecipeBindingsInput(ctx, v) + if err != nil { + return it, err + } + it.Bindings = data + case "limit": + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("limit")) + data, err := ec.unmarshalOInt2ᚖint(ctx, v) + if err != nil { + return it, err + } + it.Limit = data + case "outputs": + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("outputs")) + data, err := ec.unmarshalOString2ᚕstringᚄ(ctx, v) + if err != nil { + return it, err + } + it.Outputs = data + } + } + return it, nil +} + +func (ec *executionContext) unmarshalInputRunDataframeRecipeInput(ctx context.Context, obj any) (model.RunDataframeRecipeInput, error) { + var it model.RunDataframeRecipeInput + if obj == nil { + return it, nil + } + + asMap := map[string]any{} + for k, v := range obj.(map[string]any) { + asMap[k] = v + } + + fieldsInOrder := [...]string{"name", "bindings", "outputs"} + for _, k := range fieldsInOrder { + v, ok := asMap[k] + if !ok { + continue + } + switch k { + case "name": + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("name")) + data, err := ec.unmarshalNString2string(ctx, v) + if err != nil { + return it, err + } + it.Name = data + case "bindings": + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("bindings")) + data, err := ec.unmarshalNDataframeRecipeBindingsInput2ᚖgithubᚗcomᚋcalyprᚋloomᚋgraphqlapiᚋmodelᚐDataframeRecipeBindingsInput(ctx, v) + if err != nil { + return it, err + } + it.Bindings = data + case "outputs": + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("outputs")) + data, err := ec.unmarshalOString2ᚕstringᚄ(ctx, v) + if err != nil { + return it, err + } + it.Outputs = data + } + } + return it, nil +} + +func (ec *executionContext) unmarshalInputValidateDataframeRecipeInput(ctx context.Context, obj any) (model.ValidateDataframeRecipeInput, error) { + var it model.ValidateDataframeRecipeInput + if obj == nil { + return it, nil + } + + asMap := map[string]any{} + for k, v := range obj.(map[string]any) { + asMap[k] = v + } + + fieldsInOrder := [...]string{"name", "bindings"} + for _, k := range fieldsInOrder { + v, ok := asMap[k] + if !ok { + continue + } + switch k { + case "name": + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("name")) + data, err := ec.unmarshalNString2string(ctx, v) + if err != nil { + return it, err + } + it.Name = data + case "bindings": + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("bindings")) + data, err := ec.unmarshalNDataframeRecipeBindingsInput2ᚖgithubᚗcomᚋcalyprᚋloomᚋgraphqlapiᚋmodelᚐDataframeRecipeBindingsInput(ctx, v) + if err != nil { + return it, err + } + it.Bindings = data + } + } + return it, nil +} + +// endregion **************************** input.gotpl ***************************** + +// region ************************** interface.gotpl *************************** + +// endregion ************************** interface.gotpl *************************** + +// region **************************** object.gotpl **************************** + +var dataframeAggregateResultImplementors = []string{"DataframeAggregateResult"} + +func (ec *executionContext) _DataframeAggregateResult(ctx context.Context, sel ast.SelectionSet, obj *model.DataframeAggregateResult) graphql.Marshaler { + fields := graphql.CollectFields(ec.OperationContext, sel, dataframeAggregateResultImplementors) + + out := graphql.NewFieldSet(fields) + deferredFieldSet := graphql.NewFieldSet(nil) + deferLabelToView := make(map[string]*graphql.FieldSetView) + for i, field := range fields { + switch field.Name { + case "__typename": + out.Values[i] = graphql.MarshalString("DataframeAggregateResult") + case "materialization": + out.Values[i] = ec._DataframeAggregateResult_materialization(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ + } + case "columns": + out.Values[i] = ec._DataframeAggregateResult_columns(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ + } + case "rows": + out.Values[i] = ec._DataframeAggregateResult_rows(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ + } + default: + panic("unknown field " + strconv.Quote(field.Name)) + } + } + out.Dispatch(ctx) + if out.Invalids > 0 { + return graphql.Null + } + + atomic.AddInt32(&ec.Deferred, int32(min(len(deferLabelToView), math.MaxInt32))) + + ec.ProcessDeferredGroup(graphql.DeferredGroup{ + Defers: deferLabelToView, + Path: graphql.GetPath(ctx), + FieldSet: deferredFieldSet, + Context: ctx, + }) + + return out +} + +var dataframeBuilderIntrospectionImplementors = []string{"DataframeBuilderIntrospection"} + +func (ec *executionContext) _DataframeBuilderIntrospection(ctx context.Context, sel ast.SelectionSet, obj *model.DataframeBuilderIntrospection) graphql.Marshaler { + fields := graphql.CollectFields(ec.OperationContext, sel, dataframeBuilderIntrospectionImplementors) + + out := graphql.NewFieldSet(fields) + deferredFieldSet := graphql.NewFieldSet(nil) + deferLabelToView := make(map[string]*graphql.FieldSetView) + for i, field := range fields { + switch field.Name { + case "__typename": + out.Values[i] = graphql.MarshalString("DataframeBuilderIntrospection") + case "project": + out.Values[i] = ec._DataframeBuilderIntrospection_project(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ + } + case "rootResourceType": + out.Values[i] = ec._DataframeBuilderIntrospection_rootResourceType(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ + } + case "authResourcePaths": + out.Values[i] = ec._DataframeBuilderIntrospection_authResourcePaths(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ + } + case "root": + out.Values[i] = ec._DataframeBuilderIntrospection_root(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ + } + case "relatedResources": + out.Values[i] = ec._DataframeBuilderIntrospection_relatedResources(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ + } + case "traversals": + out.Values[i] = ec._DataframeBuilderIntrospection_traversals(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ + } + case "fields": + out.Values[i] = ec._DataframeBuilderIntrospection_fields(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ + } + case "pivotFields": + out.Values[i] = ec._DataframeBuilderIntrospection_pivotFields(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ + } + default: + panic("unknown field " + strconv.Quote(field.Name)) + } + } + out.Dispatch(ctx) + if out.Invalids > 0 { + return graphql.Null + } + + atomic.AddInt32(&ec.Deferred, int32(min(len(deferLabelToView), math.MaxInt32))) + + ec.ProcessDeferredGroup(graphql.DeferredGroup{ + Defers: deferLabelToView, + Path: graphql.GetPath(ctx), + FieldSet: deferredFieldSet, + Context: ctx, + }) + + return out +} + +var dataframeColumnImplementors = []string{"DataframeColumn"} + +func (ec *executionContext) _DataframeColumn(ctx context.Context, sel ast.SelectionSet, obj *model.DataframeColumn) graphql.Marshaler { + fields := graphql.CollectFields(ec.OperationContext, sel, dataframeColumnImplementors) + + out := graphql.NewFieldSet(fields) + deferredFieldSet := graphql.NewFieldSet(nil) + deferLabelToView := make(map[string]*graphql.FieldSetView) + for i, field := range fields { + switch field.Name { + case "__typename": + out.Values[i] = graphql.MarshalString("DataframeColumn") + case "name": + out.Values[i] = ec._DataframeColumn_name(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ + } + case "clickhouseType": + out.Values[i] = ec._DataframeColumn_clickhouseType(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ + } + case "logicalType": + out.Values[i] = ec._DataframeColumn_logicalType(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ + } + case "nullable": + out.Values[i] = ec._DataframeColumn_nullable(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ + } + case "repeated": + out.Values[i] = ec._DataframeColumn_repeated(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ + } + case "filterable": + out.Values[i] = ec._DataframeColumn_filterable(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ + } + case "sortable": + out.Values[i] = ec._DataframeColumn_sortable(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ + } + case "aggregatable": + out.Values[i] = ec._DataframeColumn_aggregatable(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ + } + default: + panic("unknown field " + strconv.Quote(field.Name)) + } + } + out.Dispatch(ctx) + if out.Invalids > 0 { + return graphql.Null + } + + atomic.AddInt32(&ec.Deferred, int32(min(len(deferLabelToView), math.MaxInt32))) + + ec.ProcessDeferredGroup(graphql.DeferredGroup{ + Defers: deferLabelToView, + Path: graphql.GetPath(ctx), + FieldSet: deferredFieldSet, + Context: ctx, + }) + + return out +} + +var dataframeCompilerPlanDiagnosticsImplementors = []string{"DataframeCompilerPlanDiagnostics"} + +func (ec *executionContext) _DataframeCompilerPlanDiagnostics(ctx context.Context, sel ast.SelectionSet, obj *model.DataframeCompilerPlanDiagnostics) graphql.Marshaler { + fields := graphql.CollectFields(ec.OperationContext, sel, dataframeCompilerPlanDiagnosticsImplementors) + + out := graphql.NewFieldSet(fields) + deferredFieldSet := graphql.NewFieldSet(nil) + deferLabelToView := make(map[string]*graphql.FieldSetView) + for i, field := range fields { + switch field.Name { + case "__typename": + out.Values[i] = graphql.MarshalString("DataframeCompilerPlanDiagnostics") + case "traversalSets": + out.Values[i] = ec._DataframeCompilerPlanDiagnostics_traversalSets(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ + } + case "sharedTraversalCount": + out.Values[i] = ec._DataframeCompilerPlanDiagnostics_sharedTraversalCount(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ + } + case "requiredMatchReuseCount": + out.Values[i] = ec._DataframeCompilerPlanDiagnostics_requiredMatchReuseCount(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ + } + case "scopedSharingCandidateGroups": + out.Values[i] = ec._DataframeCompilerPlanDiagnostics_scopedSharingCandidateGroups(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ + } + case "scopedSharingCandidateSets": + out.Values[i] = ec._DataframeCompilerPlanDiagnostics_scopedSharingCandidateSets(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ + } + case "potentialSharingOpportunityGroups": + out.Values[i] = ec._DataframeCompilerPlanDiagnostics_potentialSharingOpportunityGroups(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ + } + case "potentialSharingOpportunitySets": + out.Values[i] = ec._DataframeCompilerPlanDiagnostics_potentialSharingOpportunitySets(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ + } + case "optimizationPolicy": + out.Values[i] = ec._DataframeCompilerPlanDiagnostics_optimizationPolicy(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ + } + case "richSourceReuse": + out.Values[i] = ec._DataframeCompilerPlanDiagnostics_richSourceReuse(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ + } + default: + panic("unknown field " + strconv.Quote(field.Name)) + } + } + out.Dispatch(ctx) + if out.Invalids > 0 { + return graphql.Null + } + + atomic.AddInt32(&ec.Deferred, int32(min(len(deferLabelToView), math.MaxInt32))) + + ec.ProcessDeferredGroup(graphql.DeferredGroup{ + Defers: deferLabelToView, + Path: graphql.GetPath(ctx), + FieldSet: deferredFieldSet, + Context: ctx, + }) + + return out +} + +var dataframeFieldHintImplementors = []string{"DataframeFieldHint"} + +func (ec *executionContext) _DataframeFieldHint(ctx context.Context, sel ast.SelectionSet, obj *model.DataframeFieldHint) graphql.Marshaler { + fields := graphql.CollectFields(ec.OperationContext, sel, dataframeFieldHintImplementors) + + out := graphql.NewFieldSet(fields) + deferredFieldSet := graphql.NewFieldSet(nil) + deferLabelToView := make(map[string]*graphql.FieldSetView) + for i, field := range fields { + switch field.Name { + case "__typename": + out.Values[i] = graphql.MarshalString("DataframeFieldHint") + case "resourceType": + out.Values[i] = ec._DataframeFieldHint_resourceType(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ + } + case "fieldRef": + out.Values[i] = ec._DataframeFieldHint_fieldRef(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ + } + case "label": + out.Values[i] = ec._DataframeFieldHint_label(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ + } + case "path": + out.Values[i] = ec._DataframeFieldHint_path(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ + } + case "selector": + out.Values[i] = ec._DataframeFieldHint_selector(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ + } + case "kind": + out.Values[i] = ec._DataframeFieldHint_kind(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ + } + case "docCount": + out.Values[i] = ec._DataframeFieldHint_docCount(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ + } + case "sampleCount": + out.Values[i] = ec._DataframeFieldHint_sampleCount(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ + } + case "distinctValues": + out.Values[i] = ec._DataframeFieldHint_distinctValues(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ + } + case "distinctTruncated": + out.Values[i] = ec._DataframeFieldHint_distinctTruncated(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ + } + case "pivotCandidate": + out.Values[i] = ec._DataframeFieldHint_pivotCandidate(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ + } + case "pivotKind": + out.Values[i] = ec._DataframeFieldHint_pivotKind(ctx, field, obj) + if out.Values[i] == graphql.RequiredNull { + out.Invalids++ + } + case "pivotColumns": + out.Values[i] = ec._DataframeFieldHint_pivotColumns(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ + } + case "pivotFamily": + out.Values[i] = ec._DataframeFieldHint_pivotFamily(ctx, field, obj) + if out.Values[i] == graphql.RequiredNull { + out.Invalids++ + } + case "defaultPivotColumnSelector": + out.Values[i] = ec._DataframeFieldHint_defaultPivotColumnSelector(ctx, field, obj) + if out.Values[i] == graphql.RequiredNull { + out.Invalids++ + } + case "defaultPivotValueSelector": + out.Values[i] = ec._DataframeFieldHint_defaultPivotValueSelector(ctx, field, obj) + if out.Values[i] == graphql.RequiredNull { + out.Invalids++ + } + default: + panic("unknown field " + strconv.Quote(field.Name)) + } + } + out.Dispatch(ctx) + if out.Invalids > 0 { + return graphql.Null + } + + atomic.AddInt32(&ec.Deferred, int32(min(len(deferLabelToView), math.MaxInt32))) + + ec.ProcessDeferredGroup(graphql.DeferredGroup{ + Defers: deferLabelToView, + Path: graphql.GetPath(ctx), + FieldSet: deferredFieldSet, + Context: ctx, + }) + + return out +} + +var dataframeFieldPredicateImplementors = []string{"DataframeFieldPredicate"} + +func (ec *executionContext) _DataframeFieldPredicate(ctx context.Context, sel ast.SelectionSet, obj *model.DataframeFieldPredicate) graphql.Marshaler { + fields := graphql.CollectFields(ec.OperationContext, sel, dataframeFieldPredicateImplementors) + + out := graphql.NewFieldSet(fields) + deferredFieldSet := graphql.NewFieldSet(nil) + deferLabelToView := make(map[string]*graphql.FieldSetView) + for i, field := range fields { + switch field.Name { + case "__typename": + out.Values[i] = graphql.MarshalString("DataframeFieldPredicate") + case "path": + out.Values[i] = ec._DataframeFieldPredicate_path(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ + } + case "op": + out.Values[i] = ec._DataframeFieldPredicate_op(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ + } + case "value": + out.Values[i] = ec._DataframeFieldPredicate_value(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ + } + default: + panic("unknown field " + strconv.Quote(field.Name)) + } + } + out.Dispatch(ctx) + if out.Invalids > 0 { + return graphql.Null + } + + atomic.AddInt32(&ec.Deferred, int32(min(len(deferLabelToView), math.MaxInt32))) + + ec.ProcessDeferredGroup(graphql.DeferredGroup{ + Defers: deferLabelToView, + Path: graphql.GetPath(ctx), + FieldSet: deferredFieldSet, + Context: ctx, + }) + + return out +} + +var dataframeFieldSelectorImplementors = []string{"DataframeFieldSelector"} + +func (ec *executionContext) _DataframeFieldSelector(ctx context.Context, sel ast.SelectionSet, obj *model.DataframeFieldSelector) graphql.Marshaler { + fields := graphql.CollectFields(ec.OperationContext, sel, dataframeFieldSelectorImplementors) + + out := graphql.NewFieldSet(fields) + deferredFieldSet := graphql.NewFieldSet(nil) + deferLabelToView := make(map[string]*graphql.FieldSetView) + for i, field := range fields { + switch field.Name { + case "__typename": + out.Values[i] = graphql.MarshalString("DataframeFieldSelector") + case "sourcePath": + out.Values[i] = ec._DataframeFieldSelector_sourcePath(ctx, field, obj) + if out.Values[i] == graphql.RequiredNull { + out.Invalids++ + } + case "where": + out.Values[i] = ec._DataframeFieldSelector_where(ctx, field, obj) + if out.Values[i] == graphql.RequiredNull { + out.Invalids++ + } + case "valuePath": + out.Values[i] = ec._DataframeFieldSelector_valuePath(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ + } + default: + panic("unknown field " + strconv.Quote(field.Name)) + } + } + out.Dispatch(ctx) + if out.Invalids > 0 { + return graphql.Null + } + + atomic.AddInt32(&ec.Deferred, int32(min(len(deferLabelToView), math.MaxInt32))) + + ec.ProcessDeferredGroup(graphql.DeferredGroup{ + Defers: deferLabelToView, + Path: graphql.GetPath(ctx), + FieldSet: deferredFieldSet, + Context: ctx, + }) + + return out +} + +var dataframeMaterializationImplementors = []string{"DataframeMaterialization"} + +func (ec *executionContext) _DataframeMaterialization(ctx context.Context, sel ast.SelectionSet, obj *model.DataframeMaterialization) graphql.Marshaler { + fields := graphql.CollectFields(ec.OperationContext, sel, dataframeMaterializationImplementors) + + out := graphql.NewFieldSet(fields) + deferredFieldSet := graphql.NewFieldSet(nil) + deferLabelToView := make(map[string]*graphql.FieldSetView) + for i, field := range fields { + switch field.Name { + case "__typename": + out.Values[i] = graphql.MarshalString("DataframeMaterialization") + case "id": + out.Values[i] = ec._DataframeMaterialization_id(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ + } + case "name": + out.Values[i] = ec._DataframeMaterialization_name(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ + } + case "revision": + out.Values[i] = ec._DataframeMaterialization_revision(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ + } + case "state": + out.Values[i] = ec._DataframeMaterialization_state(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ + } + case "columns": + out.Values[i] = ec._DataframeMaterialization_columns(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ + } + case "rowCount": + out.Values[i] = ec._DataframeMaterialization_rowCount(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ + } + case "createdAt": + out.Values[i] = ec._DataframeMaterialization_createdAt(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ + } + case "readyAt": + out.Values[i] = ec._DataframeMaterialization_readyAt(ctx, field, obj) + if out.Values[i] == graphql.RequiredNull { + out.Invalids++ + } + case "error": + out.Values[i] = ec._DataframeMaterialization_error(ctx, field, obj) + if out.Values[i] == graphql.RequiredNull { + out.Invalids++ + } + default: + panic("unknown field " + strconv.Quote(field.Name)) + } + } + out.Dispatch(ctx) + if out.Invalids > 0 { + return graphql.Null + } + + atomic.AddInt32(&ec.Deferred, int32(min(len(deferLabelToView), math.MaxInt32))) + + ec.ProcessDeferredGroup(graphql.DeferredGroup{ + Defers: deferLabelToView, + Path: graphql.GetPath(ctx), + FieldSet: deferredFieldSet, + Context: ctx, + }) + + return out +} + +var dataframeOptimizationDecisionImplementors = []string{"DataframeOptimizationDecision"} + +func (ec *executionContext) _DataframeOptimizationDecision(ctx context.Context, sel ast.SelectionSet, obj *model.DataframeOptimizationDecision) graphql.Marshaler { + fields := graphql.CollectFields(ec.OperationContext, sel, dataframeOptimizationDecisionImplementors) + + out := graphql.NewFieldSet(fields) + deferredFieldSet := graphql.NewFieldSet(nil) + deferLabelToView := make(map[string]*graphql.FieldSetView) + for i, field := range fields { + switch field.Name { + case "__typename": + out.Values[i] = graphql.MarshalString("DataframeOptimizationDecision") + case "rule": + out.Values[i] = ec._DataframeOptimizationDecision_rule(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ + } + case "enabled": + out.Values[i] = ec._DataframeOptimizationDecision_enabled(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ + } + case "candidateSets": + out.Values[i] = ec._DataframeOptimizationDecision_candidateSets(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ + } + case "estimatedBaselineWork": + out.Values[i] = ec._DataframeOptimizationDecision_estimatedBaselineWork(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ + } + case "estimatedOptimizedWork": + out.Values[i] = ec._DataframeOptimizationDecision_estimatedOptimizedWork(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ + } + case "estimatedSavings": + out.Values[i] = ec._DataframeOptimizationDecision_estimatedSavings(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ + } + case "reason": + out.Values[i] = ec._DataframeOptimizationDecision_reason(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ + } + default: + panic("unknown field " + strconv.Quote(field.Name)) + } + } + out.Dispatch(ctx) + if out.Invalids > 0 { + return graphql.Null + } + + atomic.AddInt32(&ec.Deferred, int32(min(len(deferLabelToView), math.MaxInt32))) + + ec.ProcessDeferredGroup(graphql.DeferredGroup{ + Defers: deferLabelToView, + Path: graphql.GetPath(ctx), + FieldSet: deferredFieldSet, + Context: ctx, + }) + + return out +} + +var dataframeOptimizationPolicyImplementors = []string{"DataframeOptimizationPolicy"} + +func (ec *executionContext) _DataframeOptimizationPolicy(ctx context.Context, sel ast.SelectionSet, obj *model.DataframeOptimizationPolicy) graphql.Marshaler { + fields := graphql.CollectFields(ec.OperationContext, sel, dataframeOptimizationPolicyImplementors) + + out := graphql.NewFieldSet(fields) + deferredFieldSet := graphql.NewFieldSet(nil) + deferLabelToView := make(map[string]*graphql.FieldSetView) + for i, field := range fields { + switch field.Name { + case "__typename": + out.Values[i] = graphql.MarshalString("DataframeOptimizationPolicy") + case "name": + out.Values[i] = ec._DataframeOptimizationPolicy_name(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ + } + case "enabled": + out.Values[i] = ec._DataframeOptimizationPolicy_enabled(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ + } + case "minimumSavings": + out.Values[i] = ec._DataframeOptimizationPolicy_minimumSavings(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ + } + case "decisions": + out.Values[i] = ec._DataframeOptimizationPolicy_decisions(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ + } + default: + panic("unknown field " + strconv.Quote(field.Name)) } - }() - resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { - ctx = rctx // use context from middleware stack in children - return obj.InputFields(), nil - }) - if err != nil { - ec.Error(ctx, err) - return graphql.Null } - if resTmp == nil { + out.Dispatch(ctx) + if out.Invalids > 0 { return graphql.Null } - res := resTmp.([]introspection.InputValue) - fc.Result = res - return ec.marshalO__InputValue2ᚕgithubᚗcomᚋ99designsᚋgqlgenᚋgraphqlᚋintrospectionᚐInputValueᚄ(ctx, field.Selections, res) -} -func (ec *executionContext) fieldContext___Type_inputFields(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { - fc = &graphql.FieldContext{ - Object: "__Type", - Field: field, - IsMethod: true, - IsResolver: false, - Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { - switch field.Name { - case "name": - return ec.fieldContext___InputValue_name(ctx, field) - case "description": - return ec.fieldContext___InputValue_description(ctx, field) - case "type": - return ec.fieldContext___InputValue_type(ctx, field) - case "defaultValue": - return ec.fieldContext___InputValue_defaultValue(ctx, field) - case "isDeprecated": - return ec.fieldContext___InputValue_isDeprecated(ctx, field) - case "deprecationReason": - return ec.fieldContext___InputValue_deprecationReason(ctx, field) - } - return nil, fmt.Errorf("no field named %q was found under type __InputValue", field.Name) - }, - } - return fc, nil + atomic.AddInt32(&ec.Deferred, int32(min(len(deferLabelToView), math.MaxInt32))) + + ec.ProcessDeferredGroup(graphql.DeferredGroup{ + Defers: deferLabelToView, + Path: graphql.GetPath(ctx), + FieldSet: deferredFieldSet, + Context: ctx, + }) + + return out } -func (ec *executionContext) ___Type_ofType(ctx context.Context, field graphql.CollectedField, obj *introspection.Type) (ret graphql.Marshaler) { - fc, err := ec.fieldContext___Type_ofType(ctx, field) - if err != nil { - return graphql.Null - } - ctx = graphql.WithFieldContext(ctx, fc) - defer func() { - if r := recover(); r != nil { - ec.Error(ctx, ec.Recover(ctx, r)) - ret = graphql.Null +var dataframePageInfoImplementors = []string{"DataframePageInfo"} + +func (ec *executionContext) _DataframePageInfo(ctx context.Context, sel ast.SelectionSet, obj *model.DataframePageInfo) graphql.Marshaler { + fields := graphql.CollectFields(ec.OperationContext, sel, dataframePageInfoImplementors) + + out := graphql.NewFieldSet(fields) + deferredFieldSet := graphql.NewFieldSet(nil) + deferLabelToView := make(map[string]*graphql.FieldSetView) + for i, field := range fields { + switch field.Name { + case "__typename": + out.Values[i] = graphql.MarshalString("DataframePageInfo") + case "hasNextPage": + out.Values[i] = ec._DataframePageInfo_hasNextPage(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ + } + case "endCursor": + out.Values[i] = ec._DataframePageInfo_endCursor(ctx, field, obj) + if out.Values[i] == graphql.RequiredNull { + out.Invalids++ + } + default: + panic("unknown field " + strconv.Quote(field.Name)) } - }() - resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { - ctx = rctx // use context from middleware stack in children - return obj.OfType(), nil - }) - if err != nil { - ec.Error(ctx, err) - return graphql.Null } - if resTmp == nil { + out.Dispatch(ctx) + if out.Invalids > 0 { return graphql.Null } - res := resTmp.(*introspection.Type) - fc.Result = res - return ec.marshalO__Type2ᚖgithubᚗcomᚋ99designsᚋgqlgenᚋgraphqlᚋintrospectionᚐType(ctx, field.Selections, res) -} -func (ec *executionContext) fieldContext___Type_ofType(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { - fc = &graphql.FieldContext{ - Object: "__Type", - Field: field, - IsMethod: true, - IsResolver: false, - Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { - switch field.Name { - case "kind": - return ec.fieldContext___Type_kind(ctx, field) - case "name": - return ec.fieldContext___Type_name(ctx, field) - case "description": - return ec.fieldContext___Type_description(ctx, field) - case "fields": - return ec.fieldContext___Type_fields(ctx, field) - case "interfaces": - return ec.fieldContext___Type_interfaces(ctx, field) - case "possibleTypes": - return ec.fieldContext___Type_possibleTypes(ctx, field) - case "enumValues": - return ec.fieldContext___Type_enumValues(ctx, field) - case "inputFields": - return ec.fieldContext___Type_inputFields(ctx, field) - case "ofType": - return ec.fieldContext___Type_ofType(ctx, field) - case "specifiedByURL": - return ec.fieldContext___Type_specifiedByURL(ctx, field) - case "isOneOf": - return ec.fieldContext___Type_isOneOf(ctx, field) - } - return nil, fmt.Errorf("no field named %q was found under type __Type", field.Name) - }, - } - return fc, nil + atomic.AddInt32(&ec.Deferred, int32(min(len(deferLabelToView), math.MaxInt32))) + + ec.ProcessDeferredGroup(graphql.DeferredGroup{ + Defers: deferLabelToView, + Path: graphql.GetPath(ctx), + FieldSet: deferredFieldSet, + Context: ctx, + }) + + return out } -func (ec *executionContext) ___Type_specifiedByURL(ctx context.Context, field graphql.CollectedField, obj *introspection.Type) (ret graphql.Marshaler) { - fc, err := ec.fieldContext___Type_specifiedByURL(ctx, field) - if err != nil { - return graphql.Null - } - ctx = graphql.WithFieldContext(ctx, fc) - defer func() { - if r := recover(); r != nil { - ec.Error(ctx, ec.Recover(ctx, r)) - ret = graphql.Null +var dataframeQueryDiagnosticsImplementors = []string{"DataframeQueryDiagnostics"} + +func (ec *executionContext) _DataframeQueryDiagnostics(ctx context.Context, sel ast.SelectionSet, obj *model.DataframeQueryDiagnostics) graphql.Marshaler { + fields := graphql.CollectFields(ec.OperationContext, sel, dataframeQueryDiagnosticsImplementors) + + out := graphql.NewFieldSet(fields) + deferredFieldSet := graphql.NewFieldSet(nil) + deferLabelToView := make(map[string]*graphql.FieldSetView) + for i, field := range fields { + switch field.Name { + case "__typename": + out.Values[i] = graphql.MarshalString("DataframeQueryDiagnostics") + case "inputResolutionMs": + out.Values[i] = ec._DataframeQueryDiagnostics_inputResolutionMs(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ + } + case "requestPreparationMs": + out.Values[i] = ec._DataframeQueryDiagnostics_requestPreparationMs(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ + } + case "compilationMs": + out.Values[i] = ec._DataframeQueryDiagnostics_compilationMs(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ + } + case "arangoQueryMs": + out.Values[i] = ec._DataframeQueryDiagnostics_arangoQueryMs(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ + } + case "rowMaterializationMs": + out.Values[i] = ec._DataframeQueryDiagnostics_rowMaterializationMs(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ + } + case "resultAssemblyMs": + out.Values[i] = ec._DataframeQueryDiagnostics_resultAssemblyMs(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ + } + case "totalMs": + out.Values[i] = ec._DataframeQueryDiagnostics_totalMs(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ + } + case "plan": + out.Values[i] = ec._DataframeQueryDiagnostics_plan(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ + } + default: + panic("unknown field " + strconv.Quote(field.Name)) } - }() - resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { - ctx = rctx // use context from middleware stack in children - return obj.SpecifiedByURL(), nil - }) - if err != nil { - ec.Error(ctx, err) - return graphql.Null } - if resTmp == nil { + out.Dispatch(ctx) + if out.Invalids > 0 { return graphql.Null } - res := resTmp.(*string) - fc.Result = res - return ec.marshalOString2ᚖstring(ctx, field.Selections, res) -} -func (ec *executionContext) fieldContext___Type_specifiedByURL(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { - fc = &graphql.FieldContext{ - Object: "__Type", - Field: field, - IsMethod: true, - IsResolver: false, - Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { - return nil, errors.New("field of type String does not have child fields") - }, - } - return fc, nil + atomic.AddInt32(&ec.Deferred, int32(min(len(deferLabelToView), math.MaxInt32))) + + ec.ProcessDeferredGroup(graphql.DeferredGroup{ + Defers: deferLabelToView, + Path: graphql.GetPath(ctx), + FieldSet: deferredFieldSet, + Context: ctx, + }) + + return out } -func (ec *executionContext) ___Type_isOneOf(ctx context.Context, field graphql.CollectedField, obj *introspection.Type) (ret graphql.Marshaler) { - fc, err := ec.fieldContext___Type_isOneOf(ctx, field) - if err != nil { - return graphql.Null - } - ctx = graphql.WithFieldContext(ctx, fc) - defer func() { - if r := recover(); r != nil { - ec.Error(ctx, ec.Recover(ctx, r)) - ret = graphql.Null +var dataframeRecipeArangoAssessmentImplementors = []string{"DataframeRecipeArangoAssessment"} + +func (ec *executionContext) _DataframeRecipeArangoAssessment(ctx context.Context, sel ast.SelectionSet, obj *model.DataframeRecipeArangoAssessment) graphql.Marshaler { + fields := graphql.CollectFields(ec.OperationContext, sel, dataframeRecipeArangoAssessmentImplementors) + + out := graphql.NewFieldSet(fields) + deferredFieldSet := graphql.NewFieldSet(nil) + deferLabelToView := make(map[string]*graphql.FieldSetView) + for i, field := range fields { + switch field.Name { + case "__typename": + out.Values[i] = graphql.MarshalString("DataframeRecipeArangoAssessment") + case "plans": + out.Values[i] = ec._DataframeRecipeArangoAssessment_plans(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ + } + case "fullCollectionScans": + out.Values[i] = ec._DataframeRecipeArangoAssessment_fullCollectionScans(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ + } + case "indexes": + out.Values[i] = ec._DataframeRecipeArangoAssessment_indexes(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ + } + case "warnings": + out.Values[i] = ec._DataframeRecipeArangoAssessment_warnings(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ + } + case "appliedOptimizerRules": + out.Values[i] = ec._DataframeRecipeArangoAssessment_appliedOptimizerRules(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ + } + default: + panic("unknown field " + strconv.Quote(field.Name)) } - }() - resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { - ctx = rctx // use context from middleware stack in children - return obj.IsOneOf(), nil - }) - if err != nil { - ec.Error(ctx, err) - return graphql.Null } - if resTmp == nil { + out.Dispatch(ctx) + if out.Invalids > 0 { return graphql.Null } - res := resTmp.(bool) - fc.Result = res - return ec.marshalOBoolean2bool(ctx, field.Selections, res) -} -func (ec *executionContext) fieldContext___Type_isOneOf(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { - fc = &graphql.FieldContext{ - Object: "__Type", - Field: field, - IsMethod: true, - IsResolver: false, - Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { - return nil, errors.New("field of type Boolean does not have child fields") - }, - } - return fc, nil -} + atomic.AddInt32(&ec.Deferred, int32(min(len(deferLabelToView), math.MaxInt32))) -// endregion **************************** field.gotpl ***************************** + ec.ProcessDeferredGroup(graphql.DeferredGroup{ + Defers: deferLabelToView, + Path: graphql.GetPath(ctx), + FieldSet: deferredFieldSet, + Context: ctx, + }) -// region **************************** input.gotpl ***************************** + return out +} -func (ec *executionContext) unmarshalInputDataframeAggregateInput(ctx context.Context, obj any) (model.DataframeAggregateInput, error) { - var it model.DataframeAggregateInput - asMap := map[string]any{} - for k, v := range obj.(map[string]any) { - asMap[k] = v - } +var dataframeRecipeColumnImplementors = []string{"DataframeRecipeColumn"} - fieldsInOrder := [...]string{"materializationId", "groupBy", "filters", "operation", "column"} - for _, k := range fieldsInOrder { - v, ok := asMap[k] - if !ok { - continue - } - switch k { - case "materializationId": - ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("materializationId")) - data, err := ec.unmarshalNID2string(ctx, v) - if err != nil { - return it, err +func (ec *executionContext) _DataframeRecipeColumn(ctx context.Context, sel ast.SelectionSet, obj *model.DataframeRecipeColumn) graphql.Marshaler { + fields := graphql.CollectFields(ec.OperationContext, sel, dataframeRecipeColumnImplementors) + + out := graphql.NewFieldSet(fields) + deferredFieldSet := graphql.NewFieldSet(nil) + deferLabelToView := make(map[string]*graphql.FieldSetView) + for i, field := range fields { + switch field.Name { + case "__typename": + out.Values[i] = graphql.MarshalString("DataframeRecipeColumn") + case "output": + out.Values[i] = ec._DataframeRecipeColumn_output(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ } - it.MaterializationID = data - case "groupBy": - ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("groupBy")) - data, err := ec.unmarshalOString2ᚕstringᚄ(ctx, v) - if err != nil { - return it, err + case "dynamicName": + out.Values[i] = ec._DataframeRecipeColumn_dynamicName(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ } - it.GroupBy = data - case "filters": - ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("filters")) - data, err := ec.unmarshalODataframeFilterInput2ᚕᚖgithubᚗcomᚋcalyprᚋloomᚋgraphqlapiᚋmodelᚐDataframeFilterInputᚄ(ctx, v) - if err != nil { - return it, err + case "name": + out.Values[i] = ec._DataframeRecipeColumn_name(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ } - it.Filters = data - case "operation": - ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("operation")) - data, err := ec.unmarshalNString2string(ctx, v) - if err != nil { - return it, err + case "logicalType": + out.Values[i] = ec._DataframeRecipeColumn_logicalType(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ } - it.Operation = data - case "column": - ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("column")) - data, err := ec.unmarshalOString2ᚖstring(ctx, v) - if err != nil { - return it, err + case "repeated": + out.Values[i] = ec._DataframeRecipeColumn_repeated(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ } - it.Column = data + case "nullable": + out.Values[i] = ec._DataframeRecipeColumn_nullable(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ + } + default: + panic("unknown field " + strconv.Quote(field.Name)) } } + out.Dispatch(ctx) + if out.Invalids > 0 { + return graphql.Null + } - return it, nil + atomic.AddInt32(&ec.Deferred, int32(min(len(deferLabelToView), math.MaxInt32))) + + ec.ProcessDeferredGroup(graphql.DeferredGroup{ + Defers: deferLabelToView, + Path: graphql.GetPath(ctx), + FieldSet: deferredFieldSet, + Context: ctx, + }) + + return out } -func (ec *executionContext) unmarshalInputDataframeBuilderIntrospectionInput(ctx context.Context, obj any) (model.DataframeBuilderIntrospectionInput, error) { - var it model.DataframeBuilderIntrospectionInput - asMap := map[string]any{} - for k, v := range obj.(map[string]any) { - asMap[k] = v - } +var dataframeRecipeExecutionImplementors = []string{"DataframeRecipeExecution"} - if _, present := asMap["includePivotOnlyFields"]; !present { - asMap["includePivotOnlyFields"] = true - } +func (ec *executionContext) _DataframeRecipeExecution(ctx context.Context, sel ast.SelectionSet, obj *model.DataframeRecipeExecution) graphql.Marshaler { + fields := graphql.CollectFields(ec.OperationContext, sel, dataframeRecipeExecutionImplementors) - fieldsInOrder := [...]string{"project", "rootResourceType", "authResourcePaths", "includePivotOnlyFields"} - for _, k := range fieldsInOrder { - v, ok := asMap[k] - if !ok { - continue - } - switch k { - case "project": - ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("project")) - data, err := ec.unmarshalNString2string(ctx, v) - if err != nil { - return it, err + out := graphql.NewFieldSet(fields) + deferredFieldSet := graphql.NewFieldSet(nil) + deferLabelToView := make(map[string]*graphql.FieldSetView) + for i, field := range fields { + switch field.Name { + case "__typename": + out.Values[i] = graphql.MarshalString("DataframeRecipeExecution") + case "id": + out.Values[i] = ec._DataframeRecipeExecution_id(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ } - it.Project = data - case "rootResourceType": - ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("rootResourceType")) - data, err := ec.unmarshalNString2string(ctx, v) - if err != nil { - return it, err + case "name": + out.Values[i] = ec._DataframeRecipeExecution_name(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ } - it.RootResourceType = data - case "authResourcePaths": - ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("authResourcePaths")) - data, err := ec.unmarshalOString2ᚕstringᚄ(ctx, v) - if err != nil { - return it, err + case "recipeDigest": + out.Values[i] = ec._DataframeRecipeExecution_recipeDigest(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ } - it.AuthResourcePaths = data - case "includePivotOnlyFields": - ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("includePivotOnlyFields")) - data, err := ec.unmarshalOBoolean2ᚖbool(ctx, v) - if err != nil { - return it, err + case "resolvedSchemaDigest": + out.Values[i] = ec._DataframeRecipeExecution_resolvedSchemaDigest(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ } - it.IncludePivotOnlyFields = data + case "sourceGeneration": + out.Values[i] = ec._DataframeRecipeExecution_sourceGeneration(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ + } + case "state": + out.Values[i] = ec._DataframeRecipeExecution_state(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ + } + case "outputs": + out.Values[i] = ec._DataframeRecipeExecution_outputs(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ + } + case "error": + out.Values[i] = ec._DataframeRecipeExecution_error(ctx, field, obj) + if out.Values[i] == graphql.RequiredNull { + out.Invalids++ + } + default: + panic("unknown field " + strconv.Quote(field.Name)) } } + out.Dispatch(ctx) + if out.Invalids > 0 { + return graphql.Null + } - return it, nil + atomic.AddInt32(&ec.Deferred, int32(min(len(deferLabelToView), math.MaxInt32))) + + ec.ProcessDeferredGroup(graphql.DeferredGroup{ + Defers: deferLabelToView, + Path: graphql.GetPath(ctx), + FieldSet: deferredFieldSet, + Context: ctx, + }) + + return out } -func (ec *executionContext) unmarshalInputDataframeFilterInput(ctx context.Context, obj any) (model.DataframeFilterInput, error) { - var it model.DataframeFilterInput - asMap := map[string]any{} - for k, v := range obj.(map[string]any) { - asMap[k] = v - } +var dataframeRecipeExecutionOutputImplementors = []string{"DataframeRecipeExecutionOutput"} - fieldsInOrder := [...]string{"column", "op", "value"} - for _, k := range fieldsInOrder { - v, ok := asMap[k] - if !ok { - continue - } - switch k { - case "column": - ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("column")) - data, err := ec.unmarshalNString2string(ctx, v) - if err != nil { - return it, err +func (ec *executionContext) _DataframeRecipeExecutionOutput(ctx context.Context, sel ast.SelectionSet, obj *model.DataframeRecipeExecutionOutput) graphql.Marshaler { + fields := graphql.CollectFields(ec.OperationContext, sel, dataframeRecipeExecutionOutputImplementors) + + out := graphql.NewFieldSet(fields) + deferredFieldSet := graphql.NewFieldSet(nil) + deferLabelToView := make(map[string]*graphql.FieldSetView) + for i, field := range fields { + switch field.Name { + case "__typename": + out.Values[i] = graphql.MarshalString("DataframeRecipeExecutionOutput") + case "name": + out.Values[i] = ec._DataframeRecipeExecutionOutput_name(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ } - it.Column = data - case "op": - ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("op")) - data, err := ec.unmarshalNString2string(ctx, v) - if err != nil { - return it, err + case "state": + out.Values[i] = ec._DataframeRecipeExecutionOutput_state(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ } - it.Op = data - case "value": - ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("value")) - data, err := ec.unmarshalNJSON2encodingᚋjsonᚐRawMessage(ctx, v) - if err != nil { - return it, err + case "rowCount": + out.Values[i] = ec._DataframeRecipeExecutionOutput_rowCount(ctx, field, obj) + if out.Values[i] == graphql.RequiredNull { + out.Invalids++ } - it.Value = data + case "error": + out.Values[i] = ec._DataframeRecipeExecutionOutput_error(ctx, field, obj) + if out.Values[i] == graphql.RequiredNull { + out.Invalids++ + } + default: + panic("unknown field " + strconv.Quote(field.Name)) } } + out.Dispatch(ctx) + if out.Invalids > 0 { + return graphql.Null + } - return it, nil + atomic.AddInt32(&ec.Deferred, int32(min(len(deferLabelToView), math.MaxInt32))) + + ec.ProcessDeferredGroup(graphql.DeferredGroup{ + Defers: deferLabelToView, + Path: graphql.GetPath(ctx), + FieldSet: deferredFieldSet, + Context: ctx, + }) + + return out } -func (ec *executionContext) unmarshalInputDataframeRowsInput(ctx context.Context, obj any) (model.DataframeRowsInput, error) { - var it model.DataframeRowsInput - asMap := map[string]any{} - for k, v := range obj.(map[string]any) { - asMap[k] = v - } +var dataframeRecipeExpansionExplanationImplementors = []string{"DataframeRecipeExpansionExplanation"} - if _, present := asMap["first"]; !present { - asMap["first"] = 100 - } +func (ec *executionContext) _DataframeRecipeExpansionExplanation(ctx context.Context, sel ast.SelectionSet, obj *model.DataframeRecipeExpansionExplanation) graphql.Marshaler { + fields := graphql.CollectFields(ec.OperationContext, sel, dataframeRecipeExpansionExplanationImplementors) - fieldsInOrder := [...]string{"materializationId", "columns", "filters", "sort", "first", "after"} - for _, k := range fieldsInOrder { - v, ok := asMap[k] - if !ok { - continue - } - switch k { - case "materializationId": - ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("materializationId")) - data, err := ec.unmarshalNID2string(ctx, v) - if err != nil { - return it, err - } - it.MaterializationID = data - case "columns": - ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("columns")) - data, err := ec.unmarshalOString2ᚕstringᚄ(ctx, v) - if err != nil { - return it, err - } - it.Columns = data - case "filters": - ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("filters")) - data, err := ec.unmarshalODataframeFilterInput2ᚕᚖgithubᚗcomᚋcalyprᚋloomᚋgraphqlapiᚋmodelᚐDataframeFilterInputᚄ(ctx, v) - if err != nil { - return it, err + out := graphql.NewFieldSet(fields) + deferredFieldSet := graphql.NewFieldSet(nil) + deferLabelToView := make(map[string]*graphql.FieldSetView) + for i, field := range fields { + switch field.Name { + case "__typename": + out.Values[i] = graphql.MarshalString("DataframeRecipeExpansionExplanation") + case "sourcePath": + out.Values[i] = ec._DataframeRecipeExpansionExplanation_sourcePath(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ } - it.Filters = data - case "sort": - ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("sort")) - data, err := ec.unmarshalODataframeSortInput2ᚖgithubᚗcomᚋcalyprᚋloomᚋgraphqlapiᚋmodelᚐDataframeSortInput(ctx, v) - if err != nil { - return it, err + case "alias": + out.Values[i] = ec._DataframeRecipeExpansionExplanation_alias(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ } - it.Sort = data - case "first": - ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("first")) - data, err := ec.unmarshalOInt2ᚖint(ctx, v) - if err != nil { - return it, err + default: + panic("unknown field " + strconv.Quote(field.Name)) + } + } + out.Dispatch(ctx) + if out.Invalids > 0 { + return graphql.Null + } + + atomic.AddInt32(&ec.Deferred, int32(min(len(deferLabelToView), math.MaxInt32))) + + ec.ProcessDeferredGroup(graphql.DeferredGroup{ + Defers: deferLabelToView, + Path: graphql.GetPath(ctx), + FieldSet: deferredFieldSet, + Context: ctx, + }) + + return out +} + +var dataframeRecipeExplainCollectionScanImplementors = []string{"DataframeRecipeExplainCollectionScan"} + +func (ec *executionContext) _DataframeRecipeExplainCollectionScan(ctx context.Context, sel ast.SelectionSet, obj *model.DataframeRecipeExplainCollectionScan) graphql.Marshaler { + fields := graphql.CollectFields(ec.OperationContext, sel, dataframeRecipeExplainCollectionScanImplementors) + + out := graphql.NewFieldSet(fields) + deferredFieldSet := graphql.NewFieldSet(nil) + deferLabelToView := make(map[string]*graphql.FieldSetView) + for i, field := range fields { + switch field.Name { + case "__typename": + out.Values[i] = graphql.MarshalString("DataframeRecipeExplainCollectionScan") + case "plan": + out.Values[i] = ec._DataframeRecipeExplainCollectionScan_plan(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ } - it.First = data - case "after": - ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("after")) - data, err := ec.unmarshalOString2ᚖstring(ctx, v) - if err != nil { - return it, err + case "nodeId": + out.Values[i] = ec._DataframeRecipeExplainCollectionScan_nodeId(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ } - it.After = data + case "collection": + out.Values[i] = ec._DataframeRecipeExplainCollectionScan_collection(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ + } + default: + panic("unknown field " + strconv.Quote(field.Name)) } } + out.Dispatch(ctx) + if out.Invalids > 0 { + return graphql.Null + } - return it, nil + atomic.AddInt32(&ec.Deferred, int32(min(len(deferLabelToView), math.MaxInt32))) + + ec.ProcessDeferredGroup(graphql.DeferredGroup{ + Defers: deferLabelToView, + Path: graphql.GetPath(ctx), + FieldSet: deferredFieldSet, + Context: ctx, + }) + + return out } -func (ec *executionContext) unmarshalInputDataframeSortInput(ctx context.Context, obj any) (model.DataframeSortInput, error) { - var it model.DataframeSortInput - asMap := map[string]any{} - for k, v := range obj.(map[string]any) { - asMap[k] = v - } +var dataframeRecipeExplainIndexLocationImplementors = []string{"DataframeRecipeExplainIndexLocation"} - if _, present := asMap["desc"]; !present { - asMap["desc"] = false - } +func (ec *executionContext) _DataframeRecipeExplainIndexLocation(ctx context.Context, sel ast.SelectionSet, obj *model.DataframeRecipeExplainIndexLocation) graphql.Marshaler { + fields := graphql.CollectFields(ec.OperationContext, sel, dataframeRecipeExplainIndexLocationImplementors) - fieldsInOrder := [...]string{"column", "desc"} - for _, k := range fieldsInOrder { - v, ok := asMap[k] - if !ok { - continue - } - switch k { - case "column": - ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("column")) - data, err := ec.unmarshalNString2string(ctx, v) - if err != nil { - return it, err + out := graphql.NewFieldSet(fields) + deferredFieldSet := graphql.NewFieldSet(nil) + deferLabelToView := make(map[string]*graphql.FieldSetView) + for i, field := range fields { + switch field.Name { + case "__typename": + out.Values[i] = graphql.MarshalString("DataframeRecipeExplainIndexLocation") + case "plan": + out.Values[i] = ec._DataframeRecipeExplainIndexLocation_plan(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ } - it.Column = data - case "desc": - ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("desc")) - data, err := ec.unmarshalOBoolean2ᚖbool(ctx, v) - if err != nil { - return it, err + case "nodeId": + out.Values[i] = ec._DataframeRecipeExplainIndexLocation_nodeId(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ } - it.Desc = data + default: + panic("unknown field " + strconv.Quote(field.Name)) } } + out.Dispatch(ctx) + if out.Invalids > 0 { + return graphql.Null + } - return it, nil + atomic.AddInt32(&ec.Deferred, int32(min(len(deferLabelToView), math.MaxInt32))) + + ec.ProcessDeferredGroup(graphql.DeferredGroup{ + Defers: deferLabelToView, + Path: graphql.GetPath(ctx), + FieldSet: deferredFieldSet, + Context: ctx, + }) + + return out } -func (ec *executionContext) unmarshalInputFhirAggregateInput(ctx context.Context, obj any) (model.FhirAggregateInput, error) { - var it model.FhirAggregateInput - asMap := map[string]any{} - for k, v := range obj.(map[string]any) { - asMap[k] = v - } +var dataframeRecipeExplainIndexSummaryImplementors = []string{"DataframeRecipeExplainIndexSummary"} - if _, present := asMap["valueMode"]; !present { - asMap["valueMode"] = "AUTO" - } +func (ec *executionContext) _DataframeRecipeExplainIndexSummary(ctx context.Context, sel ast.SelectionSet, obj *model.DataframeRecipeExplainIndexSummary) graphql.Marshaler { + fields := graphql.CollectFields(ec.OperationContext, sel, dataframeRecipeExplainIndexSummaryImplementors) - fieldsInOrder := [...]string{"name", "operation", "fieldRef", "fhirPath", "predicateFieldRef", "predicatePath", "predicateEquals", "valueMode"} - for _, k := range fieldsInOrder { - v, ok := asMap[k] - if !ok { - continue - } - switch k { - case "name": - ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("name")) - data, err := ec.unmarshalNString2string(ctx, v) - if err != nil { - return it, err - } - it.Name = data - case "operation": - ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("operation")) - data, err := ec.unmarshalNFhirAggregateOperation2githubᚗcomᚋcalyprᚋloomᚋgraphqlapiᚋmodelᚐFhirAggregateOperation(ctx, v) - if err != nil { - return it, err - } - it.Operation = data - case "fieldRef": - ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("fieldRef")) - data, err := ec.unmarshalOString2ᚖstring(ctx, v) - if err != nil { - return it, err + out := graphql.NewFieldSet(fields) + deferredFieldSet := graphql.NewFieldSet(nil) + deferLabelToView := make(map[string]*graphql.FieldSetView) + for i, field := range fields { + switch field.Name { + case "__typename": + out.Values[i] = graphql.MarshalString("DataframeRecipeExplainIndexSummary") + case "collection": + out.Values[i] = ec._DataframeRecipeExplainIndexSummary_collection(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ } - it.FieldRef = data - case "fhirPath": - ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("fhirPath")) - data, err := ec.unmarshalOString2ᚖstring(ctx, v) - if err != nil { - return it, err + case "id": + out.Values[i] = ec._DataframeRecipeExplainIndexSummary_id(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ } - it.FhirPath = data - case "predicateFieldRef": - ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("predicateFieldRef")) - data, err := ec.unmarshalOString2ᚖstring(ctx, v) - if err != nil { - return it, err + case "name": + out.Values[i] = ec._DataframeRecipeExplainIndexSummary_name(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ } - it.PredicateFieldRef = data - case "predicatePath": - ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("predicatePath")) - data, err := ec.unmarshalOString2ᚖstring(ctx, v) - if err != nil { - return it, err + case "type": + out.Values[i] = ec._DataframeRecipeExplainIndexSummary_type(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ } - it.PredicatePath = data - case "predicateEquals": - ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("predicateEquals")) - data, err := ec.unmarshalOString2ᚖstring(ctx, v) - if err != nil { - return it, err + case "fields": + out.Values[i] = ec._DataframeRecipeExplainIndexSummary_fields(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ } - it.PredicateEquals = data - case "valueMode": - ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("valueMode")) - data, err := ec.unmarshalNFhirValueMode2githubᚗcomᚋcalyprᚋloomᚋgraphqlapiᚋmodelᚐFhirValueMode(ctx, v) - if err != nil { - return it, err + case "uses": + out.Values[i] = ec._DataframeRecipeExplainIndexSummary_uses(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ } - it.ValueMode = data + default: + panic("unknown field " + strconv.Quote(field.Name)) } } + out.Dispatch(ctx) + if out.Invalids > 0 { + return graphql.Null + } - return it, nil + atomic.AddInt32(&ec.Deferred, int32(min(len(deferLabelToView), math.MaxInt32))) + + ec.ProcessDeferredGroup(graphql.DeferredGroup{ + Defers: deferLabelToView, + Path: graphql.GetPath(ctx), + FieldSet: deferredFieldSet, + Context: ctx, + }) + + return out } -func (ec *executionContext) unmarshalInputFhirDataframeInput(ctx context.Context, obj any) (model.FhirDataframeInput, error) { - var it model.FhirDataframeInput - asMap := map[string]any{} - for k, v := range obj.(map[string]any) { - asMap[k] = v - } +var dataframeRecipeExplainPlanEstimateImplementors = []string{"DataframeRecipeExplainPlanEstimate"} - fieldsInOrder := [...]string{"project", "authResourcePath", "authResourcePaths", "rootResourceType", "rootFields", "rootPivots", "rootAggregates", "rootSlices", "traverse", "limit", "cursor"} - for _, k := range fieldsInOrder { - v, ok := asMap[k] - if !ok { - continue - } - switch k { - case "project": - ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("project")) - data, err := ec.unmarshalNString2string(ctx, v) - if err != nil { - return it, err - } - it.Project = data - case "authResourcePath": - ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("authResourcePath")) - data, err := ec.unmarshalOString2ᚖstring(ctx, v) - if err != nil { - return it, err - } - it.AuthResourcePath = data - case "authResourcePaths": - ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("authResourcePaths")) - data, err := ec.unmarshalOString2ᚕstringᚄ(ctx, v) - if err != nil { - return it, err - } - it.AuthResourcePaths = data - case "rootResourceType": - ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("rootResourceType")) - data, err := ec.unmarshalNString2string(ctx, v) - if err != nil { - return it, err - } - it.RootResourceType = data - case "rootFields": - ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("rootFields")) - data, err := ec.unmarshalOFhirFieldSelectInput2ᚕᚖgithubᚗcomᚋcalyprᚋloomᚋgraphqlapiᚋmodelᚐFhirFieldSelectInputᚄ(ctx, v) - if err != nil { - return it, err - } - it.RootFields = data - case "rootPivots": - ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("rootPivots")) - data, err := ec.unmarshalOFhirPivotInput2ᚕᚖgithubᚗcomᚋcalyprᚋloomᚋgraphqlapiᚋmodelᚐFhirPivotInputᚄ(ctx, v) - if err != nil { - return it, err - } - it.RootPivots = data - case "rootAggregates": - ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("rootAggregates")) - data, err := ec.unmarshalOFhirAggregateInput2ᚕᚖgithubᚗcomᚋcalyprᚋloomᚋgraphqlapiᚋmodelᚐFhirAggregateInputᚄ(ctx, v) - if err != nil { - return it, err - } - it.RootAggregates = data - case "rootSlices": - ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("rootSlices")) - data, err := ec.unmarshalOFhirRepresentativeSliceInput2ᚕᚖgithubᚗcomᚋcalyprᚋloomᚋgraphqlapiᚋmodelᚐFhirRepresentativeSliceInputᚄ(ctx, v) - if err != nil { - return it, err - } - it.RootSlices = data - case "traverse": - ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("traverse")) - data, err := ec.unmarshalOFhirTraversalStepInput2ᚕᚖgithubᚗcomᚋcalyprᚋloomᚋgraphqlapiᚋmodelᚐFhirTraversalStepInputᚄ(ctx, v) - if err != nil { - return it, err +func (ec *executionContext) _DataframeRecipeExplainPlanEstimate(ctx context.Context, sel ast.SelectionSet, obj *model.DataframeRecipeExplainPlanEstimate) graphql.Marshaler { + fields := graphql.CollectFields(ec.OperationContext, sel, dataframeRecipeExplainPlanEstimateImplementors) + + out := graphql.NewFieldSet(fields) + deferredFieldSet := graphql.NewFieldSet(nil) + deferLabelToView := make(map[string]*graphql.FieldSetView) + for i, field := range fields { + switch field.Name { + case "__typename": + out.Values[i] = graphql.MarshalString("DataframeRecipeExplainPlanEstimate") + case "plan": + out.Values[i] = ec._DataframeRecipeExplainPlanEstimate_plan(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ } - it.Traverse = data - case "limit": - ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("limit")) - data, err := ec.unmarshalOInt2ᚖint(ctx, v) - if err != nil { - return it, err + case "estimatedCost": + out.Values[i] = ec._DataframeRecipeExplainPlanEstimate_estimatedCost(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ } - it.Limit = data - case "cursor": - ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("cursor")) - data, err := ec.unmarshalOString2ᚖstring(ctx, v) - if err != nil { - return it, err + case "estimatedNrItems": + out.Values[i] = ec._DataframeRecipeExplainPlanEstimate_estimatedNrItems(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ } - it.Cursor = data + default: + panic("unknown field " + strconv.Quote(field.Name)) } } + out.Dispatch(ctx) + if out.Invalids > 0 { + return graphql.Null + } - return it, nil + atomic.AddInt32(&ec.Deferred, int32(min(len(deferLabelToView), math.MaxInt32))) + + ec.ProcessDeferredGroup(graphql.DeferredGroup{ + Defers: deferLabelToView, + Path: graphql.GetPath(ctx), + FieldSet: deferredFieldSet, + Context: ctx, + }) + + return out } -func (ec *executionContext) unmarshalInputFhirFieldPredicateInput(ctx context.Context, obj any) (model.FhirFieldPredicateInput, error) { - var it model.FhirFieldPredicateInput - asMap := map[string]any{} - for k, v := range obj.(map[string]any) { - asMap[k] = v - } +var dataframeRecipeExplainWarningImplementors = []string{"DataframeRecipeExplainWarning"} - if _, present := asMap["op"]; !present { - asMap["op"] = "CONTAINS" - } +func (ec *executionContext) _DataframeRecipeExplainWarning(ctx context.Context, sel ast.SelectionSet, obj *model.DataframeRecipeExplainWarning) graphql.Marshaler { + fields := graphql.CollectFields(ec.OperationContext, sel, dataframeRecipeExplainWarningImplementors) - fieldsInOrder := [...]string{"path", "op", "value"} - for _, k := range fieldsInOrder { - v, ok := asMap[k] - if !ok { - continue - } - switch k { - case "path": - ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("path")) - data, err := ec.unmarshalNString2string(ctx, v) - if err != nil { - return it, err - } - it.Path = data - case "op": - ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("op")) - data, err := ec.unmarshalNFhirFieldPredicateOperation2githubᚗcomᚋcalyprᚋloomᚋgraphqlapiᚋmodelᚐFhirFieldPredicateOperation(ctx, v) - if err != nil { - return it, err + out := graphql.NewFieldSet(fields) + deferredFieldSet := graphql.NewFieldSet(nil) + deferLabelToView := make(map[string]*graphql.FieldSetView) + for i, field := range fields { + switch field.Name { + case "__typename": + out.Values[i] = graphql.MarshalString("DataframeRecipeExplainWarning") + case "code": + out.Values[i] = ec._DataframeRecipeExplainWarning_code(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ } - it.Op = data - case "value": - ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("value")) - data, err := ec.unmarshalNString2string(ctx, v) - if err != nil { - return it, err + case "message": + out.Values[i] = ec._DataframeRecipeExplainWarning_message(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ } - it.Value = data + default: + panic("unknown field " + strconv.Quote(field.Name)) } } + out.Dispatch(ctx) + if out.Invalids > 0 { + return graphql.Null + } - return it, nil + atomic.AddInt32(&ec.Deferred, int32(min(len(deferLabelToView), math.MaxInt32))) + + ec.ProcessDeferredGroup(graphql.DeferredGroup{ + Defers: deferLabelToView, + Path: graphql.GetPath(ctx), + FieldSet: deferredFieldSet, + Context: ctx, + }) + + return out } -func (ec *executionContext) unmarshalInputFhirFieldSelectInput(ctx context.Context, obj any) (model.FhirFieldSelectInput, error) { - var it model.FhirFieldSelectInput - asMap := map[string]any{} - for k, v := range obj.(map[string]any) { - asMap[k] = v - } +var dataframeRecipeExplanationImplementors = []string{"DataframeRecipeExplanation"} - if _, present := asMap["fallbackFieldRefs"]; !present { - asMap["fallbackFieldRefs"] = []any{} - } - if _, present := asMap["fallbackSelectors"]; !present { - asMap["fallbackSelectors"] = []any{} - } - if _, present := asMap["valueMode"]; !present { - asMap["valueMode"] = "AUTO" - } +func (ec *executionContext) _DataframeRecipeExplanation(ctx context.Context, sel ast.SelectionSet, obj *model.DataframeRecipeExplanation) graphql.Marshaler { + fields := graphql.CollectFields(ec.OperationContext, sel, dataframeRecipeExplanationImplementors) - fieldsInOrder := [...]string{"name", "fieldRef", "selector", "selectionHint", "fallbackFieldRefs", "fallbackSelectors", "valueMode"} - for _, k := range fieldsInOrder { - v, ok := asMap[k] - if !ok { - continue - } - switch k { + out := graphql.NewFieldSet(fields) + deferredFieldSet := graphql.NewFieldSet(nil) + deferLabelToView := make(map[string]*graphql.FieldSetView) + for i, field := range fields { + switch field.Name { + case "__typename": + out.Values[i] = graphql.MarshalString("DataframeRecipeExplanation") case "name": - ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("name")) - data, err := ec.unmarshalNString2string(ctx, v) - if err != nil { - return it, err + out.Values[i] = ec._DataframeRecipeExplanation_name(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ } - it.Name = data - case "fieldRef": - ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("fieldRef")) - data, err := ec.unmarshalOString2ᚖstring(ctx, v) - if err != nil { - return it, err + case "recipeDigest": + out.Values[i] = ec._DataframeRecipeExplanation_recipeDigest(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ } - it.FieldRef = data - case "selector": - ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("selector")) - data, err := ec.unmarshalOFhirFieldSelectorInput2ᚖgithubᚗcomᚋcalyprᚋloomᚋgraphqlapiᚋmodelᚐFhirFieldSelectorInput(ctx, v) - if err != nil { - return it, err + case "translationVersion": + out.Values[i] = ec._DataframeRecipeExplanation_translationVersion(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ } - it.Selector = data - case "selectionHint": - ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("selectionHint")) - data, err := ec.unmarshalOString2ᚖstring(ctx, v) - if err != nil { - return it, err + case "outputs": + out.Values[i] = ec._DataframeRecipeExplanation_outputs(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ } - it.SelectionHint = data - case "fallbackFieldRefs": - ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("fallbackFieldRefs")) - data, err := ec.unmarshalNString2ᚕstringᚄ(ctx, v) - if err != nil { - return it, err + case "physical": + out.Values[i] = ec._DataframeRecipeExplanation_physical(ctx, field, obj) + if out.Values[i] == graphql.RequiredNull { + out.Invalids++ } - it.FallbackFieldRefs = data - case "fallbackSelectors": - ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("fallbackSelectors")) - data, err := ec.unmarshalNFhirFieldSelectorInput2ᚕᚖgithubᚗcomᚋcalyprᚋloomᚋgraphqlapiᚋmodelᚐFhirFieldSelectorInputᚄ(ctx, v) - if err != nil { - return it, err + default: + panic("unknown field " + strconv.Quote(field.Name)) + } + } + out.Dispatch(ctx) + if out.Invalids > 0 { + return graphql.Null + } + + atomic.AddInt32(&ec.Deferred, int32(min(len(deferLabelToView), math.MaxInt32))) + + ec.ProcessDeferredGroup(graphql.DeferredGroup{ + Defers: deferLabelToView, + Path: graphql.GetPath(ctx), + FieldSet: deferredFieldSet, + Context: ctx, + }) + + return out +} + +var dataframeRecipeExpressionExplanationImplementors = []string{"DataframeRecipeExpressionExplanation"} + +func (ec *executionContext) _DataframeRecipeExpressionExplanation(ctx context.Context, sel ast.SelectionSet, obj *model.DataframeRecipeExpressionExplanation) graphql.Marshaler { + fields := graphql.CollectFields(ec.OperationContext, sel, dataframeRecipeExpressionExplanationImplementors) + + out := graphql.NewFieldSet(fields) + deferredFieldSet := graphql.NewFieldSet(nil) + deferLabelToView := make(map[string]*graphql.FieldSetView) + for i, field := range fields { + switch field.Name { + case "__typename": + out.Values[i] = graphql.MarshalString("DataframeRecipeExpressionExplanation") + case "sourcePath": + out.Values[i] = ec._DataframeRecipeExpressionExplanation_sourcePath(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ } - it.FallbackSelectors = data - case "valueMode": - ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("valueMode")) - data, err := ec.unmarshalNFhirValueMode2githubᚗcomᚋcalyprᚋloomᚋgraphqlapiᚋmodelᚐFhirValueMode(ctx, v) - if err != nil { - return it, err + case "context": + out.Values[i] = ec._DataframeRecipeExpressionExplanation_context(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ } - it.ValueMode = data + case "kind": + out.Values[i] = ec._DataframeRecipeExpressionExplanation_kind(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ + } + case "valueType": + out.Values[i] = ec._DataframeRecipeExpressionExplanation_valueType(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ + } + case "repeated": + out.Values[i] = ec._DataframeRecipeExpressionExplanation_repeated(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ + } + case "nullable": + out.Values[i] = ec._DataframeRecipeExpressionExplanation_nullable(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ + } + default: + panic("unknown field " + strconv.Quote(field.Name)) } } + out.Dispatch(ctx) + if out.Invalids > 0 { + return graphql.Null + } - return it, nil + atomic.AddInt32(&ec.Deferred, int32(min(len(deferLabelToView), math.MaxInt32))) + + ec.ProcessDeferredGroup(graphql.DeferredGroup{ + Defers: deferLabelToView, + Path: graphql.GetPath(ctx), + FieldSet: deferredFieldSet, + Context: ctx, + }) + + return out } -func (ec *executionContext) unmarshalInputFhirFieldSelectorInput(ctx context.Context, obj any) (model.FhirFieldSelectorInput, error) { - var it model.FhirFieldSelectorInput - asMap := map[string]any{} - for k, v := range obj.(map[string]any) { - asMap[k] = v - } +var dataframeRecipeOptimizationDecisionImplementors = []string{"DataframeRecipeOptimizationDecision"} - fieldsInOrder := [...]string{"sourcePath", "where", "valuePath"} - for _, k := range fieldsInOrder { - v, ok := asMap[k] - if !ok { - continue - } - switch k { - case "sourcePath": - ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("sourcePath")) - data, err := ec.unmarshalOString2ᚖstring(ctx, v) - if err != nil { - return it, err +func (ec *executionContext) _DataframeRecipeOptimizationDecision(ctx context.Context, sel ast.SelectionSet, obj *model.DataframeRecipeOptimizationDecision) graphql.Marshaler { + fields := graphql.CollectFields(ec.OperationContext, sel, dataframeRecipeOptimizationDecisionImplementors) + + out := graphql.NewFieldSet(fields) + deferredFieldSet := graphql.NewFieldSet(nil) + deferLabelToView := make(map[string]*graphql.FieldSetView) + for i, field := range fields { + switch field.Name { + case "__typename": + out.Values[i] = graphql.MarshalString("DataframeRecipeOptimizationDecision") + case "rule": + out.Values[i] = ec._DataframeRecipeOptimizationDecision_rule(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ } - it.SourcePath = data - case "where": - ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("where")) - data, err := ec.unmarshalOFhirFieldPredicateInput2ᚖgithubᚗcomᚋcalyprᚋloomᚋgraphqlapiᚋmodelᚐFhirFieldPredicateInput(ctx, v) - if err != nil { - return it, err + case "enabled": + out.Values[i] = ec._DataframeRecipeOptimizationDecision_enabled(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ } - it.Where = data - case "valuePath": - ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("valuePath")) - data, err := ec.unmarshalNString2string(ctx, v) - if err != nil { - return it, err + case "candidateSets": + out.Values[i] = ec._DataframeRecipeOptimizationDecision_candidateSets(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ } - it.ValuePath = data + case "estimatedBaselineWork": + out.Values[i] = ec._DataframeRecipeOptimizationDecision_estimatedBaselineWork(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ + } + case "estimatedOptimizedWork": + out.Values[i] = ec._DataframeRecipeOptimizationDecision_estimatedOptimizedWork(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ + } + case "estimatedSavings": + out.Values[i] = ec._DataframeRecipeOptimizationDecision_estimatedSavings(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ + } + case "reason": + out.Values[i] = ec._DataframeRecipeOptimizationDecision_reason(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ + } + default: + panic("unknown field " + strconv.Quote(field.Name)) } } + out.Dispatch(ctx) + if out.Invalids > 0 { + return graphql.Null + } - return it, nil + atomic.AddInt32(&ec.Deferred, int32(min(len(deferLabelToView), math.MaxInt32))) + + ec.ProcessDeferredGroup(graphql.DeferredGroup{ + Defers: deferLabelToView, + Path: graphql.GetPath(ctx), + FieldSet: deferredFieldSet, + Context: ctx, + }) + + return out } -func (ec *executionContext) unmarshalInputFhirPivotInput(ctx context.Context, obj any) (model.FhirPivotInput, error) { - var it model.FhirPivotInput - asMap := map[string]any{} - for k, v := range obj.(map[string]any) { - asMap[k] = v - } +var dataframeRecipeOptimizationExplanationImplementors = []string{"DataframeRecipeOptimizationExplanation"} - if _, present := asMap["columns"]; !present { - asMap["columns"] = []any{} - } +func (ec *executionContext) _DataframeRecipeOptimizationExplanation(ctx context.Context, sel ast.SelectionSet, obj *model.DataframeRecipeOptimizationExplanation) graphql.Marshaler { + fields := graphql.CollectFields(ec.OperationContext, sel, dataframeRecipeOptimizationExplanationImplementors) - fieldsInOrder := [...]string{"name", "fieldRef", "columnSelector", "valueSelector", "columns"} - for _, k := range fieldsInOrder { - v, ok := asMap[k] - if !ok { - continue - } - switch k { - case "name": - ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("name")) - data, err := ec.unmarshalNString2string(ctx, v) - if err != nil { - return it, err + out := graphql.NewFieldSet(fields) + deferredFieldSet := graphql.NewFieldSet(nil) + deferLabelToView := make(map[string]*graphql.FieldSetView) + for i, field := range fields { + switch field.Name { + case "__typename": + out.Values[i] = graphql.MarshalString("DataframeRecipeOptimizationExplanation") + case "policy": + out.Values[i] = ec._DataframeRecipeOptimizationExplanation_policy(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ } - it.Name = data - case "fieldRef": - ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("fieldRef")) - data, err := ec.unmarshalOString2ᚖstring(ctx, v) - if err != nil { - return it, err + case "enabled": + out.Values[i] = ec._DataframeRecipeOptimizationExplanation_enabled(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ } - it.FieldRef = data - case "columnSelector": - ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("columnSelector")) - data, err := ec.unmarshalOFhirFieldSelectorInput2ᚖgithubᚗcomᚋcalyprᚋloomᚋgraphqlapiᚋmodelᚐFhirFieldSelectorInput(ctx, v) - if err != nil { - return it, err + case "minimumSavings": + out.Values[i] = ec._DataframeRecipeOptimizationExplanation_minimumSavings(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ } - it.ColumnSelector = data - case "valueSelector": - ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("valueSelector")) - data, err := ec.unmarshalOFhirFieldSelectorInput2ᚖgithubᚗcomᚋcalyprᚋloomᚋgraphqlapiᚋmodelᚐFhirFieldSelectorInput(ctx, v) - if err != nil { - return it, err + case "rules": + out.Values[i] = ec._DataframeRecipeOptimizationExplanation_rules(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ } - it.ValueSelector = data - case "columns": - ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("columns")) - data, err := ec.unmarshalNString2ᚕstringᚄ(ctx, v) - if err != nil { - return it, err + case "decisions": + out.Values[i] = ec._DataframeRecipeOptimizationExplanation_decisions(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ } - it.Columns = data + default: + panic("unknown field " + strconv.Quote(field.Name)) } } + out.Dispatch(ctx) + if out.Invalids > 0 { + return graphql.Null + } - return it, nil + atomic.AddInt32(&ec.Deferred, int32(min(len(deferLabelToView), math.MaxInt32))) + + ec.ProcessDeferredGroup(graphql.DeferredGroup{ + Defers: deferLabelToView, + Path: graphql.GetPath(ctx), + FieldSet: deferredFieldSet, + Context: ctx, + }) + + return out } -func (ec *executionContext) unmarshalInputFhirRepresentativeSliceInput(ctx context.Context, obj any) (model.FhirRepresentativeSliceInput, error) { - var it model.FhirRepresentativeSliceInput - asMap := map[string]any{} - for k, v := range obj.(map[string]any) { - asMap[k] = v - } +var dataframeRecipeOptimizationRuleStateImplementors = []string{"DataframeRecipeOptimizationRuleState"} - if _, present := asMap["fields"]; !present { - asMap["fields"] = []any{} - } +func (ec *executionContext) _DataframeRecipeOptimizationRuleState(ctx context.Context, sel ast.SelectionSet, obj *model.DataframeRecipeOptimizationRuleState) graphql.Marshaler { + fields := graphql.CollectFields(ec.OperationContext, sel, dataframeRecipeOptimizationRuleStateImplementors) - fieldsInOrder := [...]string{"name", "limit", "whereFieldRef", "wherePath", "whereEquals", "fields"} - for _, k := range fieldsInOrder { - v, ok := asMap[k] - if !ok { - continue - } - switch k { - case "name": - ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("name")) - data, err := ec.unmarshalNString2string(ctx, v) - if err != nil { - return it, err - } - it.Name = data - case "limit": - ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("limit")) - data, err := ec.unmarshalNInt2int(ctx, v) - if err != nil { - return it, err - } - it.Limit = data - case "whereFieldRef": - ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("whereFieldRef")) - data, err := ec.unmarshalOString2ᚖstring(ctx, v) - if err != nil { - return it, err - } - it.WhereFieldRef = data - case "wherePath": - ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("wherePath")) - data, err := ec.unmarshalOString2ᚖstring(ctx, v) - if err != nil { - return it, err + out := graphql.NewFieldSet(fields) + deferredFieldSet := graphql.NewFieldSet(nil) + deferLabelToView := make(map[string]*graphql.FieldSetView) + for i, field := range fields { + switch field.Name { + case "__typename": + out.Values[i] = graphql.MarshalString("DataframeRecipeOptimizationRuleState") + case "rule": + out.Values[i] = ec._DataframeRecipeOptimizationRuleState_rule(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ } - it.WherePath = data - case "whereEquals": - ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("whereEquals")) - data, err := ec.unmarshalOString2ᚖstring(ctx, v) - if err != nil { - return it, err + case "enabled": + out.Values[i] = ec._DataframeRecipeOptimizationRuleState_enabled(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ } - it.WhereEquals = data - case "fields": - ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("fields")) - data, err := ec.unmarshalNFhirFieldSelectInput2ᚕᚖgithubᚗcomᚋcalyprᚋloomᚋgraphqlapiᚋmodelᚐFhirFieldSelectInputᚄ(ctx, v) - if err != nil { - return it, err + case "reason": + out.Values[i] = ec._DataframeRecipeOptimizationRuleState_reason(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ } - it.Fields = data + default: + panic("unknown field " + strconv.Quote(field.Name)) } } + out.Dispatch(ctx) + if out.Invalids > 0 { + return graphql.Null + } - return it, nil + atomic.AddInt32(&ec.Deferred, int32(min(len(deferLabelToView), math.MaxInt32))) + + ec.ProcessDeferredGroup(graphql.DeferredGroup{ + Defers: deferLabelToView, + Path: graphql.GetPath(ctx), + FieldSet: deferredFieldSet, + Context: ctx, + }) + + return out } -func (ec *executionContext) unmarshalInputFhirTraversalStepInput(ctx context.Context, obj any) (model.FhirTraversalStepInput, error) { - var it model.FhirTraversalStepInput - asMap := map[string]any{} - for k, v := range obj.(map[string]any) { - asMap[k] = v - } +var dataframeRecipeOutputExplanationImplementors = []string{"DataframeRecipeOutputExplanation"} - if _, present := asMap["fields"]; !present { - asMap["fields"] = []any{} - } - if _, present := asMap["pivots"]; !present { - asMap["pivots"] = []any{} - } - if _, present := asMap["aggregates"]; !present { - asMap["aggregates"] = []any{} - } - if _, present := asMap["slices"]; !present { - asMap["slices"] = []any{} - } - if _, present := asMap["traverse"]; !present { - asMap["traverse"] = []any{} - } +func (ec *executionContext) _DataframeRecipeOutputExplanation(ctx context.Context, sel ast.SelectionSet, obj *model.DataframeRecipeOutputExplanation) graphql.Marshaler { + fields := graphql.CollectFields(ec.OperationContext, sel, dataframeRecipeOutputExplanationImplementors) - fieldsInOrder := [...]string{"edgeLabel", "toResourceType", "alias", "fields", "pivots", "aggregates", "slices", "traverse"} - for _, k := range fieldsInOrder { - v, ok := asMap[k] - if !ok { - continue - } - switch k { - case "edgeLabel": - ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("edgeLabel")) - data, err := ec.unmarshalNString2string(ctx, v) - if err != nil { - return it, err + out := graphql.NewFieldSet(fields) + deferredFieldSet := graphql.NewFieldSet(nil) + deferLabelToView := make(map[string]*graphql.FieldSetView) + for i, field := range fields { + switch field.Name { + case "__typename": + out.Values[i] = graphql.MarshalString("DataframeRecipeOutputExplanation") + case "name": + out.Values[i] = ec._DataframeRecipeOutputExplanation_name(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ } - it.EdgeLabel = data - case "toResourceType": - ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("toResourceType")) - data, err := ec.unmarshalNString2string(ctx, v) - if err != nil { - return it, err + case "rootResourceType": + out.Values[i] = ec._DataframeRecipeOutputExplanation_rootResourceType(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ } - it.ToResourceType = data - case "alias": - ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("alias")) - data, err := ec.unmarshalNString2string(ctx, v) - if err != nil { - return it, err + case "rowGrain": + out.Values[i] = ec._DataframeRecipeOutputExplanation_rowGrain(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ } - it.Alias = data case "fields": - ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("fields")) - data, err := ec.unmarshalNFhirFieldSelectInput2ᚕᚖgithubᚗcomᚋcalyprᚋloomᚋgraphqlapiᚋmodelᚐFhirFieldSelectInputᚄ(ctx, v) - if err != nil { - return it, err + out.Values[i] = ec._DataframeRecipeOutputExplanation_fields(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ } - it.Fields = data - case "pivots": - ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("pivots")) - data, err := ec.unmarshalOFhirPivotInput2ᚕᚖgithubᚗcomᚋcalyprᚋloomᚋgraphqlapiᚋmodelᚐFhirPivotInputᚄ(ctx, v) - if err != nil { - return it, err + case "identity": + out.Values[i] = ec._DataframeRecipeOutputExplanation_identity(ctx, field, obj) + if out.Values[i] == graphql.RequiredNull { + out.Invalids++ } - it.Pivots = data - case "aggregates": - ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("aggregates")) - data, err := ec.unmarshalOFhirAggregateInput2ᚕᚖgithubᚗcomᚋcalyprᚋloomᚋgraphqlapiᚋmodelᚐFhirAggregateInputᚄ(ctx, v) - if err != nil { - return it, err + case "expansion": + out.Values[i] = ec._DataframeRecipeOutputExplanation_expansion(ctx, field, obj) + if out.Values[i] == graphql.RequiredNull { + out.Invalids++ } - it.Aggregates = data - case "slices": - ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("slices")) - data, err := ec.unmarshalOFhirRepresentativeSliceInput2ᚕᚖgithubᚗcomᚋcalyprᚋloomᚋgraphqlapiᚋmodelᚐFhirRepresentativeSliceInputᚄ(ctx, v) - if err != nil { - return it, err + case "dynamicMaps": + out.Values[i] = ec._DataframeRecipeOutputExplanation_dynamicMaps(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ } - it.Slices = data - case "traverse": - ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("traverse")) - data, err := ec.unmarshalOFhirTraversalStepInput2ᚕᚖgithubᚗcomᚋcalyprᚋloomᚋgraphqlapiᚋmodelᚐFhirTraversalStepInputᚄ(ctx, v) - if err != nil { - return it, err + case "catalogProjections": + out.Values[i] = ec._DataframeRecipeOutputExplanation_catalogProjections(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ } - it.Traverse = data + default: + panic("unknown field " + strconv.Quote(field.Name)) } } + out.Dispatch(ctx) + if out.Invalids > 0 { + return graphql.Null + } - return it, nil -} - -// endregion **************************** input.gotpl ***************************** - -// region ************************** interface.gotpl *************************** + atomic.AddInt32(&ec.Deferred, int32(min(len(deferLabelToView), math.MaxInt32))) -// endregion ************************** interface.gotpl *************************** + ec.ProcessDeferredGroup(graphql.DeferredGroup{ + Defers: deferLabelToView, + Path: graphql.GetPath(ctx), + FieldSet: deferredFieldSet, + Context: ctx, + }) -// region **************************** object.gotpl **************************** + return out +} -var dataframeAggregateResultImplementors = []string{"DataframeAggregateResult"} +var dataframeRecipeOutputValidationImplementors = []string{"DataframeRecipeOutputValidation"} -func (ec *executionContext) _DataframeAggregateResult(ctx context.Context, sel ast.SelectionSet, obj *model.DataframeAggregateResult) graphql.Marshaler { - fields := graphql.CollectFields(ec.OperationContext, sel, dataframeAggregateResultImplementors) +func (ec *executionContext) _DataframeRecipeOutputValidation(ctx context.Context, sel ast.SelectionSet, obj *model.DataframeRecipeOutputValidation) graphql.Marshaler { + fields := graphql.CollectFields(ec.OperationContext, sel, dataframeRecipeOutputValidationImplementors) out := graphql.NewFieldSet(fields) - deferred := make(map[string]*graphql.FieldSet) + deferredFieldSet := graphql.NewFieldSet(nil) + deferLabelToView := make(map[string]*graphql.FieldSetView) for i, field := range fields { switch field.Name { case "__typename": - out.Values[i] = graphql.MarshalString("DataframeAggregateResult") - case "materialization": - out.Values[i] = ec._DataframeAggregateResult_materialization(ctx, field, obj) + out.Values[i] = graphql.MarshalString("DataframeRecipeOutputValidation") + case "name": + out.Values[i] = ec._DataframeRecipeOutputValidation_name(ctx, field, obj) if out.Values[i] == graphql.Null { out.Invalids++ } - case "columns": - out.Values[i] = ec._DataframeAggregateResult_columns(ctx, field, obj) + case "rootResourceType": + out.Values[i] = ec._DataframeRecipeOutputValidation_rootResourceType(ctx, field, obj) if out.Values[i] == graphql.Null { out.Invalids++ } - case "rows": - out.Values[i] = ec._DataframeAggregateResult_rows(ctx, field, obj) + case "rowGrain": + out.Values[i] = ec._DataframeRecipeOutputValidation_rowGrain(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ + } + case "fieldNames": + out.Values[i] = ec._DataframeRecipeOutputValidation_fieldNames(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ + } + case "dynamicColumns": + out.Values[i] = ec._DataframeRecipeOutputValidation_dynamicColumns(ctx, field, obj) if out.Values[i] == graphql.Null { out.Invalids++ } @@ -9396,71 +13877,118 @@ func (ec *executionContext) _DataframeAggregateResult(ctx context.Context, sel a return graphql.Null } - atomic.AddInt32(&ec.deferred, int32(len(deferred))) + atomic.AddInt32(&ec.Deferred, int32(min(len(deferLabelToView), math.MaxInt32))) - for label, dfs := range deferred { - ec.processDeferredGroup(graphql.DeferredGroup{ - Label: label, - Path: graphql.GetPath(ctx), - FieldSet: dfs, - Context: ctx, - }) + ec.ProcessDeferredGroup(graphql.DeferredGroup{ + Defers: deferLabelToView, + Path: graphql.GetPath(ctx), + FieldSet: deferredFieldSet, + Context: ctx, + }) + + return out +} + +var dataframeRecipePhysicalExplanationImplementors = []string{"DataframeRecipePhysicalExplanation"} + +func (ec *executionContext) _DataframeRecipePhysicalExplanation(ctx context.Context, sel ast.SelectionSet, obj *model.DataframeRecipePhysicalExplanation) graphql.Marshaler { + fields := graphql.CollectFields(ec.OperationContext, sel, dataframeRecipePhysicalExplanationImplementors) + + out := graphql.NewFieldSet(fields) + deferredFieldSet := graphql.NewFieldSet(nil) + deferLabelToView := make(map[string]*graphql.FieldSetView) + for i, field := range fields { + switch field.Name { + case "__typename": + out.Values[i] = graphql.MarshalString("DataframeRecipePhysicalExplanation") + case "outputs": + out.Values[i] = ec._DataframeRecipePhysicalExplanation_outputs(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ + } + default: + panic("unknown field " + strconv.Quote(field.Name)) + } + } + out.Dispatch(ctx) + if out.Invalids > 0 { + return graphql.Null } + atomic.AddInt32(&ec.Deferred, int32(min(len(deferLabelToView), math.MaxInt32))) + + ec.ProcessDeferredGroup(graphql.DeferredGroup{ + Defers: deferLabelToView, + Path: graphql.GetPath(ctx), + FieldSet: deferredFieldSet, + Context: ctx, + }) + return out } -var dataframeBuilderIntrospectionImplementors = []string{"DataframeBuilderIntrospection"} +var dataframeRecipePhysicalOutputExplanationImplementors = []string{"DataframeRecipePhysicalOutputExplanation"} -func (ec *executionContext) _DataframeBuilderIntrospection(ctx context.Context, sel ast.SelectionSet, obj *model.DataframeBuilderIntrospection) graphql.Marshaler { - fields := graphql.CollectFields(ec.OperationContext, sel, dataframeBuilderIntrospectionImplementors) +func (ec *executionContext) _DataframeRecipePhysicalOutputExplanation(ctx context.Context, sel ast.SelectionSet, obj *model.DataframeRecipePhysicalOutputExplanation) graphql.Marshaler { + fields := graphql.CollectFields(ec.OperationContext, sel, dataframeRecipePhysicalOutputExplanationImplementors) out := graphql.NewFieldSet(fields) - deferred := make(map[string]*graphql.FieldSet) + deferredFieldSet := graphql.NewFieldSet(nil) + deferLabelToView := make(map[string]*graphql.FieldSetView) for i, field := range fields { switch field.Name { case "__typename": - out.Values[i] = graphql.MarshalString("DataframeBuilderIntrospection") - case "project": - out.Values[i] = ec._DataframeBuilderIntrospection_project(ctx, field, obj) + out.Values[i] = graphql.MarshalString("DataframeRecipePhysicalOutputExplanation") + case "name": + out.Values[i] = ec._DataframeRecipePhysicalOutputExplanation_name(ctx, field, obj) if out.Values[i] == graphql.Null { out.Invalids++ } - case "rootResourceType": - out.Values[i] = ec._DataframeBuilderIntrospection_rootResourceType(ctx, field, obj) + case "planFingerprint": + out.Values[i] = ec._DataframeRecipePhysicalOutputExplanation_planFingerprint(ctx, field, obj) if out.Values[i] == graphql.Null { out.Invalids++ } - case "authResourcePaths": - out.Values[i] = ec._DataframeBuilderIntrospection_authResourcePaths(ctx, field, obj) + case "columns": + out.Values[i] = ec._DataframeRecipePhysicalOutputExplanation_columns(ctx, field, obj) if out.Values[i] == graphql.Null { out.Invalids++ } - case "root": - out.Values[i] = ec._DataframeBuilderIntrospection_root(ctx, field, obj) + case "traversalSets": + out.Values[i] = ec._DataframeRecipePhysicalOutputExplanation_traversalSets(ctx, field, obj) if out.Values[i] == graphql.Null { out.Invalids++ } - case "relatedResources": - out.Values[i] = ec._DataframeBuilderIntrospection_relatedResources(ctx, field, obj) + case "endpointTraversalCount": + out.Values[i] = ec._DataframeRecipePhysicalOutputExplanation_endpointTraversalCount(ctx, field, obj) if out.Values[i] == graphql.Null { out.Invalids++ } - case "traversals": - out.Values[i] = ec._DataframeBuilderIntrospection_traversals(ctx, field, obj) + case "nativeTraversalCount": + out.Values[i] = ec._DataframeRecipePhysicalOutputExplanation_nativeTraversalCount(ctx, field, obj) if out.Values[i] == graphql.Null { out.Invalids++ } - case "fields": - out.Values[i] = ec._DataframeBuilderIntrospection_fields(ctx, field, obj) + case "sharedTraversalCount": + out.Values[i] = ec._DataframeRecipePhysicalOutputExplanation_sharedTraversalCount(ctx, field, obj) if out.Values[i] == graphql.Null { out.Invalids++ } - case "pivotFields": - out.Values[i] = ec._DataframeBuilderIntrospection_pivotFields(ctx, field, obj) + case "requiredMatchReuseCount": + out.Values[i] = ec._DataframeRecipePhysicalOutputExplanation_requiredMatchReuseCount(ctx, field, obj) if out.Values[i] == graphql.Null { out.Invalids++ } + case "optimization": + out.Values[i] = ec._DataframeRecipePhysicalOutputExplanation_optimization(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ + } + case "live": + out.Values[i] = ec._DataframeRecipePhysicalOutputExplanation_live(ctx, field, obj) + if out.Values[i] == graphql.RequiredNull { + out.Invalids++ + } default: panic("unknown field " + strconv.Quote(field.Name)) } @@ -9470,38 +13998,57 @@ func (ec *executionContext) _DataframeBuilderIntrospection(ctx context.Context, return graphql.Null } - atomic.AddInt32(&ec.deferred, int32(len(deferred))) + atomic.AddInt32(&ec.Deferred, int32(min(len(deferLabelToView), math.MaxInt32))) - for label, dfs := range deferred { - ec.processDeferredGroup(graphql.DeferredGroup{ - Label: label, - Path: graphql.GetPath(ctx), - FieldSet: dfs, - Context: ctx, - }) - } + ec.ProcessDeferredGroup(graphql.DeferredGroup{ + Defers: deferLabelToView, + Path: graphql.GetPath(ctx), + FieldSet: deferredFieldSet, + Context: ctx, + }) return out } -var dataframeColumnImplementors = []string{"DataframeColumn"} +var dataframeRecipePreflightImplementors = []string{"DataframeRecipePreflight"} -func (ec *executionContext) _DataframeColumn(ctx context.Context, sel ast.SelectionSet, obj *model.DataframeColumn) graphql.Marshaler { - fields := graphql.CollectFields(ec.OperationContext, sel, dataframeColumnImplementors) +func (ec *executionContext) _DataframeRecipePreflight(ctx context.Context, sel ast.SelectionSet, obj *model.DataframeRecipePreflight) graphql.Marshaler { + fields := graphql.CollectFields(ec.OperationContext, sel, dataframeRecipePreflightImplementors) out := graphql.NewFieldSet(fields) - deferred := make(map[string]*graphql.FieldSet) + deferredFieldSet := graphql.NewFieldSet(nil) + deferLabelToView := make(map[string]*graphql.FieldSetView) for i, field := range fields { switch field.Name { case "__typename": - out.Values[i] = graphql.MarshalString("DataframeColumn") + out.Values[i] = graphql.MarshalString("DataframeRecipePreflight") case "name": - out.Values[i] = ec._DataframeColumn_name(ctx, field, obj) + out.Values[i] = ec._DataframeRecipePreflight_name(ctx, field, obj) if out.Values[i] == graphql.Null { out.Invalids++ } - case "clickhouseType": - out.Values[i] = ec._DataframeColumn_clickhouseType(ctx, field, obj) + case "recipeDigest": + out.Values[i] = ec._DataframeRecipePreflight_recipeDigest(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ + } + case "resolvedSchemaDigest": + out.Values[i] = ec._DataframeRecipePreflight_resolvedSchemaDigest(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ + } + case "sourceGeneration": + out.Values[i] = ec._DataframeRecipePreflight_sourceGeneration(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ + } + case "scopeDigest": + out.Values[i] = ec._DataframeRecipePreflight_scopeDigest(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ + } + case "columns": + out.Values[i] = ec._DataframeRecipePreflight_columns(ctx, field, obj) if out.Values[i] == graphql.Null { out.Invalids++ } @@ -9514,73 +14061,110 @@ func (ec *executionContext) _DataframeColumn(ctx context.Context, sel ast.Select return graphql.Null } - atomic.AddInt32(&ec.deferred, int32(len(deferred))) + atomic.AddInt32(&ec.Deferred, int32(min(len(deferLabelToView), math.MaxInt32))) - for label, dfs := range deferred { - ec.processDeferredGroup(graphql.DeferredGroup{ - Label: label, - Path: graphql.GetPath(ctx), - FieldSet: dfs, - Context: ctx, - }) - } + ec.ProcessDeferredGroup(graphql.DeferredGroup{ + Defers: deferLabelToView, + Path: graphql.GetPath(ctx), + FieldSet: deferredFieldSet, + Context: ctx, + }) return out } -var dataframeCompilerPlanDiagnosticsImplementors = []string{"DataframeCompilerPlanDiagnostics"} +var dataframeRecipePreviewImplementors = []string{"DataframeRecipePreview"} -func (ec *executionContext) _DataframeCompilerPlanDiagnostics(ctx context.Context, sel ast.SelectionSet, obj *model.DataframeCompilerPlanDiagnostics) graphql.Marshaler { - fields := graphql.CollectFields(ec.OperationContext, sel, dataframeCompilerPlanDiagnosticsImplementors) +func (ec *executionContext) _DataframeRecipePreview(ctx context.Context, sel ast.SelectionSet, obj *model.DataframeRecipePreview) graphql.Marshaler { + fields := graphql.CollectFields(ec.OperationContext, sel, dataframeRecipePreviewImplementors) out := graphql.NewFieldSet(fields) - deferred := make(map[string]*graphql.FieldSet) + deferredFieldSet := graphql.NewFieldSet(nil) + deferLabelToView := make(map[string]*graphql.FieldSetView) for i, field := range fields { switch field.Name { case "__typename": - out.Values[i] = graphql.MarshalString("DataframeCompilerPlanDiagnostics") - case "traversalSets": - out.Values[i] = ec._DataframeCompilerPlanDiagnostics_traversalSets(ctx, field, obj) + out.Values[i] = graphql.MarshalString("DataframeRecipePreview") + case "name": + out.Values[i] = ec._DataframeRecipePreview_name(ctx, field, obj) if out.Values[i] == graphql.Null { out.Invalids++ } - case "sharedTraversalCount": - out.Values[i] = ec._DataframeCompilerPlanDiagnostics_sharedTraversalCount(ctx, field, obj) + case "recipeDigest": + out.Values[i] = ec._DataframeRecipePreview_recipeDigest(ctx, field, obj) if out.Values[i] == graphql.Null { out.Invalids++ } - case "requiredMatchReuseCount": - out.Values[i] = ec._DataframeCompilerPlanDiagnostics_requiredMatchReuseCount(ctx, field, obj) + case "resolvedSchemaDigest": + out.Values[i] = ec._DataframeRecipePreview_resolvedSchemaDigest(ctx, field, obj) if out.Values[i] == graphql.Null { out.Invalids++ } - case "scopedSharingCandidateGroups": - out.Values[i] = ec._DataframeCompilerPlanDiagnostics_scopedSharingCandidateGroups(ctx, field, obj) + case "sourceGeneration": + out.Values[i] = ec._DataframeRecipePreview_sourceGeneration(ctx, field, obj) if out.Values[i] == graphql.Null { out.Invalids++ } - case "scopedSharingCandidateSets": - out.Values[i] = ec._DataframeCompilerPlanDiagnostics_scopedSharingCandidateSets(ctx, field, obj) + case "outputs": + out.Values[i] = ec._DataframeRecipePreview_outputs(ctx, field, obj) if out.Values[i] == graphql.Null { out.Invalids++ } - case "potentialSharingOpportunityGroups": - out.Values[i] = ec._DataframeCompilerPlanDiagnostics_potentialSharingOpportunityGroups(ctx, field, obj) + default: + panic("unknown field " + strconv.Quote(field.Name)) + } + } + out.Dispatch(ctx) + if out.Invalids > 0 { + return graphql.Null + } + + atomic.AddInt32(&ec.Deferred, int32(min(len(deferLabelToView), math.MaxInt32))) + + ec.ProcessDeferredGroup(graphql.DeferredGroup{ + Defers: deferLabelToView, + Path: graphql.GetPath(ctx), + FieldSet: deferredFieldSet, + Context: ctx, + }) + + return out +} + +var dataframeRecipePreviewOutputImplementors = []string{"DataframeRecipePreviewOutput"} + +func (ec *executionContext) _DataframeRecipePreviewOutput(ctx context.Context, sel ast.SelectionSet, obj *model.DataframeRecipePreviewOutput) graphql.Marshaler { + fields := graphql.CollectFields(ec.OperationContext, sel, dataframeRecipePreviewOutputImplementors) + + out := graphql.NewFieldSet(fields) + deferredFieldSet := graphql.NewFieldSet(nil) + deferLabelToView := make(map[string]*graphql.FieldSetView) + for i, field := range fields { + switch field.Name { + case "__typename": + out.Values[i] = graphql.MarshalString("DataframeRecipePreviewOutput") + case "name": + out.Values[i] = ec._DataframeRecipePreviewOutput_name(ctx, field, obj) if out.Values[i] == graphql.Null { out.Invalids++ } - case "potentialSharingOpportunitySets": - out.Values[i] = ec._DataframeCompilerPlanDiagnostics_potentialSharingOpportunitySets(ctx, field, obj) + case "columns": + out.Values[i] = ec._DataframeRecipePreviewOutput_columns(ctx, field, obj) if out.Values[i] == graphql.Null { out.Invalids++ } - case "optimizationPolicy": - out.Values[i] = ec._DataframeCompilerPlanDiagnostics_optimizationPolicy(ctx, field, obj) + case "rows": + out.Values[i] = ec._DataframeRecipePreviewOutput_rows(ctx, field, obj) if out.Values[i] == graphql.Null { out.Invalids++ } - case "richSourceReuse": - out.Values[i] = ec._DataframeCompilerPlanDiagnostics_richSourceReuse(ctx, field, obj) + case "csv": + out.Values[i] = ec._DataframeRecipePreviewOutput_csv(ctx, field, obj) + if out.Values[i] == graphql.RequiredNull { + out.Invalids++ + } + case "rowCount": + out.Values[i] = ec._DataframeRecipePreviewOutput_rowCount(ctx, field, obj) if out.Values[i] == graphql.Null { out.Invalids++ } @@ -9593,99 +14177,166 @@ func (ec *executionContext) _DataframeCompilerPlanDiagnostics(ctx context.Contex return graphql.Null } - atomic.AddInt32(&ec.deferred, int32(len(deferred))) + atomic.AddInt32(&ec.Deferred, int32(min(len(deferLabelToView), math.MaxInt32))) - for label, dfs := range deferred { - ec.processDeferredGroup(graphql.DeferredGroup{ - Label: label, - Path: graphql.GetPath(ctx), - FieldSet: dfs, - Context: ctx, - }) - } + ec.ProcessDeferredGroup(graphql.DeferredGroup{ + Defers: deferLabelToView, + Path: graphql.GetPath(ctx), + FieldSet: deferredFieldSet, + Context: ctx, + }) return out } -var dataframeFieldHintImplementors = []string{"DataframeFieldHint"} +var dataframeRecipeResultImplementors = []string{"DataframeRecipeResult"} -func (ec *executionContext) _DataframeFieldHint(ctx context.Context, sel ast.SelectionSet, obj *model.DataframeFieldHint) graphql.Marshaler { - fields := graphql.CollectFields(ec.OperationContext, sel, dataframeFieldHintImplementors) +func (ec *executionContext) _DataframeRecipeResult(ctx context.Context, sel ast.SelectionSet, obj *model.DataframeRecipeResult) graphql.Marshaler { + fields := graphql.CollectFields(ec.OperationContext, sel, dataframeRecipeResultImplementors) out := graphql.NewFieldSet(fields) - deferred := make(map[string]*graphql.FieldSet) + deferredFieldSet := graphql.NewFieldSet(nil) + deferLabelToView := make(map[string]*graphql.FieldSetView) for i, field := range fields { switch field.Name { case "__typename": - out.Values[i] = graphql.MarshalString("DataframeFieldHint") - case "resourceType": - out.Values[i] = ec._DataframeFieldHint_resourceType(ctx, field, obj) + out.Values[i] = graphql.MarshalString("DataframeRecipeResult") + case "name": + out.Values[i] = ec._DataframeRecipeResult_name(ctx, field, obj) if out.Values[i] == graphql.Null { out.Invalids++ } - case "fieldRef": - out.Values[i] = ec._DataframeFieldHint_fieldRef(ctx, field, obj) + case "recipeDigest": + out.Values[i] = ec._DataframeRecipeResult_recipeDigest(ctx, field, obj) if out.Values[i] == graphql.Null { out.Invalids++ } - case "label": - out.Values[i] = ec._DataframeFieldHint_label(ctx, field, obj) + case "resolvedSchemaDigest": + out.Values[i] = ec._DataframeRecipeResult_resolvedSchemaDigest(ctx, field, obj) if out.Values[i] == graphql.Null { out.Invalids++ } - case "path": - out.Values[i] = ec._DataframeFieldHint_path(ctx, field, obj) + case "sourceGeneration": + out.Values[i] = ec._DataframeRecipeResult_sourceGeneration(ctx, field, obj) if out.Values[i] == graphql.Null { out.Invalids++ } - case "selector": - out.Values[i] = ec._DataframeFieldHint_selector(ctx, field, obj) + case "outputs": + out.Values[i] = ec._DataframeRecipeResult_outputs(ctx, field, obj) if out.Values[i] == graphql.Null { out.Invalids++ } - case "kind": - out.Values[i] = ec._DataframeFieldHint_kind(ctx, field, obj) + default: + panic("unknown field " + strconv.Quote(field.Name)) + } + } + out.Dispatch(ctx) + if out.Invalids > 0 { + return graphql.Null + } + + atomic.AddInt32(&ec.Deferred, int32(min(len(deferLabelToView), math.MaxInt32))) + + ec.ProcessDeferredGroup(graphql.DeferredGroup{ + Defers: deferLabelToView, + Path: graphql.GetPath(ctx), + FieldSet: deferredFieldSet, + Context: ctx, + }) + + return out +} + +var dataframeRecipeResultOutputImplementors = []string{"DataframeRecipeResultOutput"} + +func (ec *executionContext) _DataframeRecipeResultOutput(ctx context.Context, sel ast.SelectionSet, obj *model.DataframeRecipeResultOutput) graphql.Marshaler { + fields := graphql.CollectFields(ec.OperationContext, sel, dataframeRecipeResultOutputImplementors) + + out := graphql.NewFieldSet(fields) + deferredFieldSet := graphql.NewFieldSet(nil) + deferLabelToView := make(map[string]*graphql.FieldSetView) + for i, field := range fields { + switch field.Name { + case "__typename": + out.Values[i] = graphql.MarshalString("DataframeRecipeResultOutput") + case "name": + out.Values[i] = ec._DataframeRecipeResultOutput_name(ctx, field, obj) if out.Values[i] == graphql.Null { out.Invalids++ } - case "docCount": - out.Values[i] = ec._DataframeFieldHint_docCount(ctx, field, obj) + case "columns": + out.Values[i] = ec._DataframeRecipeResultOutput_columns(ctx, field, obj) if out.Values[i] == graphql.Null { out.Invalids++ } - case "sampleCount": - out.Values[i] = ec._DataframeFieldHint_sampleCount(ctx, field, obj) + case "rows": + out.Values[i] = ec._DataframeRecipeResultOutput_rows(ctx, field, obj) if out.Values[i] == graphql.Null { out.Invalids++ } - case "distinctValues": - out.Values[i] = ec._DataframeFieldHint_distinctValues(ctx, field, obj) + case "csv": + out.Values[i] = ec._DataframeRecipeResultOutput_csv(ctx, field, obj) + if out.Values[i] == graphql.RequiredNull { + out.Invalids++ + } + case "rowCount": + out.Values[i] = ec._DataframeRecipeResultOutput_rowCount(ctx, field, obj) if out.Values[i] == graphql.Null { out.Invalids++ } - case "distinctTruncated": - out.Values[i] = ec._DataframeFieldHint_distinctTruncated(ctx, field, obj) + default: + panic("unknown field " + strconv.Quote(field.Name)) + } + } + out.Dispatch(ctx) + if out.Invalids > 0 { + return graphql.Null + } + + atomic.AddInt32(&ec.Deferred, int32(min(len(deferLabelToView), math.MaxInt32))) + + ec.ProcessDeferredGroup(graphql.DeferredGroup{ + Defers: deferLabelToView, + Path: graphql.GetPath(ctx), + FieldSet: deferredFieldSet, + Context: ctx, + }) + + return out +} + +var dataframeRecipeValidationImplementors = []string{"DataframeRecipeValidation"} + +func (ec *executionContext) _DataframeRecipeValidation(ctx context.Context, sel ast.SelectionSet, obj *model.DataframeRecipeValidation) graphql.Marshaler { + fields := graphql.CollectFields(ec.OperationContext, sel, dataframeRecipeValidationImplementors) + + out := graphql.NewFieldSet(fields) + deferredFieldSet := graphql.NewFieldSet(nil) + deferLabelToView := make(map[string]*graphql.FieldSetView) + for i, field := range fields { + switch field.Name { + case "__typename": + out.Values[i] = graphql.MarshalString("DataframeRecipeValidation") + case "name": + out.Values[i] = ec._DataframeRecipeValidation_name(ctx, field, obj) if out.Values[i] == graphql.Null { out.Invalids++ } - case "pivotCandidate": - out.Values[i] = ec._DataframeFieldHint_pivotCandidate(ctx, field, obj) + case "recipeDigest": + out.Values[i] = ec._DataframeRecipeValidation_recipeDigest(ctx, field, obj) if out.Values[i] == graphql.Null { out.Invalids++ } - case "pivotKind": - out.Values[i] = ec._DataframeFieldHint_pivotKind(ctx, field, obj) - case "pivotColumns": - out.Values[i] = ec._DataframeFieldHint_pivotColumns(ctx, field, obj) + case "translationVersion": + out.Values[i] = ec._DataframeRecipeValidation_translationVersion(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ + } + case "outputs": + out.Values[i] = ec._DataframeRecipeValidation_outputs(ctx, field, obj) if out.Values[i] == graphql.Null { out.Invalids++ } - case "pivotFamily": - out.Values[i] = ec._DataframeFieldHint_pivotFamily(ctx, field, obj) - case "defaultPivotColumnSelector": - out.Values[i] = ec._DataframeFieldHint_defaultPivotColumnSelector(ctx, field, obj) - case "defaultPivotValueSelector": - out.Values[i] = ec._DataframeFieldHint_defaultPivotValueSelector(ctx, field, obj) default: panic("unknown field " + strconv.Quote(field.Name)) } @@ -9695,43 +14346,42 @@ func (ec *executionContext) _DataframeFieldHint(ctx context.Context, sel ast.Sel return graphql.Null } - atomic.AddInt32(&ec.deferred, int32(len(deferred))) + atomic.AddInt32(&ec.Deferred, int32(min(len(deferLabelToView), math.MaxInt32))) - for label, dfs := range deferred { - ec.processDeferredGroup(graphql.DeferredGroup{ - Label: label, - Path: graphql.GetPath(ctx), - FieldSet: dfs, - Context: ctx, - }) - } + ec.ProcessDeferredGroup(graphql.DeferredGroup{ + Defers: deferLabelToView, + Path: graphql.GetPath(ctx), + FieldSet: deferredFieldSet, + Context: ctx, + }) return out } -var dataframeFieldPredicateImplementors = []string{"DataframeFieldPredicate"} +var dataframeRelatedResourceHintsImplementors = []string{"DataframeRelatedResourceHints"} -func (ec *executionContext) _DataframeFieldPredicate(ctx context.Context, sel ast.SelectionSet, obj *model.DataframeFieldPredicate) graphql.Marshaler { - fields := graphql.CollectFields(ec.OperationContext, sel, dataframeFieldPredicateImplementors) +func (ec *executionContext) _DataframeRelatedResourceHints(ctx context.Context, sel ast.SelectionSet, obj *model.DataframeRelatedResourceHints) graphql.Marshaler { + fields := graphql.CollectFields(ec.OperationContext, sel, dataframeRelatedResourceHintsImplementors) out := graphql.NewFieldSet(fields) - deferred := make(map[string]*graphql.FieldSet) + deferredFieldSet := graphql.NewFieldSet(nil) + deferLabelToView := make(map[string]*graphql.FieldSetView) for i, field := range fields { switch field.Name { case "__typename": - out.Values[i] = graphql.MarshalString("DataframeFieldPredicate") - case "path": - out.Values[i] = ec._DataframeFieldPredicate_path(ctx, field, obj) + out.Values[i] = graphql.MarshalString("DataframeRelatedResourceHints") + case "viaLabel": + out.Values[i] = ec._DataframeRelatedResourceHints_viaLabel(ctx, field, obj) if out.Values[i] == graphql.Null { out.Invalids++ } - case "op": - out.Values[i] = ec._DataframeFieldPredicate_op(ctx, field, obj) + case "edgeCount": + out.Values[i] = ec._DataframeRelatedResourceHints_edgeCount(ctx, field, obj) if out.Values[i] == graphql.Null { out.Invalids++ } - case "value": - out.Values[i] = ec._DataframeFieldPredicate_value(ctx, field, obj) + case "target": + out.Values[i] = ec._DataframeRelatedResourceHints_target(ctx, field, obj) if out.Values[i] == graphql.Null { out.Invalids++ } @@ -9744,37 +14394,47 @@ func (ec *executionContext) _DataframeFieldPredicate(ctx context.Context, sel as return graphql.Null } - atomic.AddInt32(&ec.deferred, int32(len(deferred))) + atomic.AddInt32(&ec.Deferred, int32(min(len(deferLabelToView), math.MaxInt32))) - for label, dfs := range deferred { - ec.processDeferredGroup(graphql.DeferredGroup{ - Label: label, - Path: graphql.GetPath(ctx), - FieldSet: dfs, - Context: ctx, - }) - } + ec.ProcessDeferredGroup(graphql.DeferredGroup{ + Defers: deferLabelToView, + Path: graphql.GetPath(ctx), + FieldSet: deferredFieldSet, + Context: ctx, + }) return out } -var dataframeFieldSelectorImplementors = []string{"DataframeFieldSelector"} +var dataframeResourceHintsImplementors = []string{"DataframeResourceHints"} -func (ec *executionContext) _DataframeFieldSelector(ctx context.Context, sel ast.SelectionSet, obj *model.DataframeFieldSelector) graphql.Marshaler { - fields := graphql.CollectFields(ec.OperationContext, sel, dataframeFieldSelectorImplementors) +func (ec *executionContext) _DataframeResourceHints(ctx context.Context, sel ast.SelectionSet, obj *model.DataframeResourceHints) graphql.Marshaler { + fields := graphql.CollectFields(ec.OperationContext, sel, dataframeResourceHintsImplementors) out := graphql.NewFieldSet(fields) - deferred := make(map[string]*graphql.FieldSet) + deferredFieldSet := graphql.NewFieldSet(nil) + deferLabelToView := make(map[string]*graphql.FieldSetView) for i, field := range fields { switch field.Name { case "__typename": - out.Values[i] = graphql.MarshalString("DataframeFieldSelector") - case "sourcePath": - out.Values[i] = ec._DataframeFieldSelector_sourcePath(ctx, field, obj) - case "where": - out.Values[i] = ec._DataframeFieldSelector_where(ctx, field, obj) - case "valuePath": - out.Values[i] = ec._DataframeFieldSelector_valuePath(ctx, field, obj) + out.Values[i] = graphql.MarshalString("DataframeResourceHints") + case "resourceType": + out.Values[i] = ec._DataframeResourceHints_resourceType(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ + } + case "fields": + out.Values[i] = ec._DataframeResourceHints_fields(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ + } + case "pivotFields": + out.Values[i] = ec._DataframeResourceHints_pivotFields(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ + } + case "traversals": + out.Values[i] = ec._DataframeResourceHints_traversals(ctx, field, obj) if out.Values[i] == graphql.Null { out.Invalids++ } @@ -9787,75 +14447,55 @@ func (ec *executionContext) _DataframeFieldSelector(ctx context.Context, sel ast return graphql.Null } - atomic.AddInt32(&ec.deferred, int32(len(deferred))) + atomic.AddInt32(&ec.Deferred, int32(min(len(deferLabelToView), math.MaxInt32))) - for label, dfs := range deferred { - ec.processDeferredGroup(graphql.DeferredGroup{ - Label: label, - Path: graphql.GetPath(ctx), - FieldSet: dfs, - Context: ctx, - }) - } + ec.ProcessDeferredGroup(graphql.DeferredGroup{ + Defers: deferLabelToView, + Path: graphql.GetPath(ctx), + FieldSet: deferredFieldSet, + Context: ctx, + }) return out } -var dataframeMaterializationImplementors = []string{"DataframeMaterialization"} +var dataframeRichSourceReuseImplementors = []string{"DataframeRichSourceReuse"} -func (ec *executionContext) _DataframeMaterialization(ctx context.Context, sel ast.SelectionSet, obj *model.DataframeMaterialization) graphql.Marshaler { - fields := graphql.CollectFields(ec.OperationContext, sel, dataframeMaterializationImplementors) +func (ec *executionContext) _DataframeRichSourceReuse(ctx context.Context, sel ast.SelectionSet, obj *model.DataframeRichSourceReuse) graphql.Marshaler { + fields := graphql.CollectFields(ec.OperationContext, sel, dataframeRichSourceReuseImplementors) out := graphql.NewFieldSet(fields) - deferred := make(map[string]*graphql.FieldSet) + deferredFieldSet := graphql.NewFieldSet(nil) + deferLabelToView := make(map[string]*graphql.FieldSetView) for i, field := range fields { switch field.Name { case "__typename": - out.Values[i] = graphql.MarshalString("DataframeMaterialization") - case "id": - out.Values[i] = ec._DataframeMaterialization_id(ctx, field, obj) - if out.Values[i] == graphql.Null { - out.Invalids++ - } - case "name": - out.Values[i] = ec._DataframeMaterialization_name(ctx, field, obj) - if out.Values[i] == graphql.Null { - out.Invalids++ - } - case "project": - out.Values[i] = ec._DataframeMaterialization_project(ctx, field, obj) - if out.Values[i] == graphql.Null { - out.Invalids++ - } - case "datasetGeneration": - out.Values[i] = ec._DataframeMaterialization_datasetGeneration(ctx, field, obj) + out.Values[i] = graphql.MarshalString("DataframeRichSourceReuse") + case "sourceSet": + out.Values[i] = ec._DataframeRichSourceReuse_sourceSet(ctx, field, obj) if out.Values[i] == graphql.Null { out.Invalids++ } - case "state": - out.Values[i] = ec._DataframeMaterialization_state(ctx, field, obj) + case "aggregateConsumers": + out.Values[i] = ec._DataframeRichSourceReuse_aggregateConsumers(ctx, field, obj) if out.Values[i] == graphql.Null { out.Invalids++ } - case "columns": - out.Values[i] = ec._DataframeMaterialization_columns(ctx, field, obj) + case "pivotConsumers": + out.Values[i] = ec._DataframeRichSourceReuse_pivotConsumers(ctx, field, obj) if out.Values[i] == graphql.Null { out.Invalids++ } - case "rowCount": - out.Values[i] = ec._DataframeMaterialization_rowCount(ctx, field, obj) + case "sliceConsumers": + out.Values[i] = ec._DataframeRichSourceReuse_sliceConsumers(ctx, field, obj) if out.Values[i] == graphql.Null { out.Invalids++ } - case "createdAt": - out.Values[i] = ec._DataframeMaterialization_createdAt(ctx, field, obj) + case "totalConsumers": + out.Values[i] = ec._DataframeRichSourceReuse_totalConsumers(ctx, field, obj) if out.Values[i] == graphql.Null { out.Invalids++ } - case "readyAt": - out.Values[i] = ec._DataframeMaterialization_readyAt(ctx, field, obj) - case "error": - out.Values[i] = ec._DataframeMaterialization_error(ctx, field, obj) default: panic("unknown field " + strconv.Quote(field.Name)) } @@ -9865,63 +14505,52 @@ func (ec *executionContext) _DataframeMaterialization(ctx context.Context, sel a return graphql.Null } - atomic.AddInt32(&ec.deferred, int32(len(deferred))) + atomic.AddInt32(&ec.Deferred, int32(min(len(deferLabelToView), math.MaxInt32))) - for label, dfs := range deferred { - ec.processDeferredGroup(graphql.DeferredGroup{ - Label: label, - Path: graphql.GetPath(ctx), - FieldSet: dfs, - Context: ctx, - }) - } + ec.ProcessDeferredGroup(graphql.DeferredGroup{ + Defers: deferLabelToView, + Path: graphql.GetPath(ctx), + FieldSet: deferredFieldSet, + Context: ctx, + }) return out } -var dataframeOptimizationDecisionImplementors = []string{"DataframeOptimizationDecision"} +var dataframeRowConnectionImplementors = []string{"DataframeRowConnection"} -func (ec *executionContext) _DataframeOptimizationDecision(ctx context.Context, sel ast.SelectionSet, obj *model.DataframeOptimizationDecision) graphql.Marshaler { - fields := graphql.CollectFields(ec.OperationContext, sel, dataframeOptimizationDecisionImplementors) +func (ec *executionContext) _DataframeRowConnection(ctx context.Context, sel ast.SelectionSet, obj *model.DataframeRowConnection) graphql.Marshaler { + fields := graphql.CollectFields(ec.OperationContext, sel, dataframeRowConnectionImplementors) out := graphql.NewFieldSet(fields) - deferred := make(map[string]*graphql.FieldSet) + deferredFieldSet := graphql.NewFieldSet(nil) + deferLabelToView := make(map[string]*graphql.FieldSetView) for i, field := range fields { switch field.Name { case "__typename": - out.Values[i] = graphql.MarshalString("DataframeOptimizationDecision") - case "rule": - out.Values[i] = ec._DataframeOptimizationDecision_rule(ctx, field, obj) - if out.Values[i] == graphql.Null { - out.Invalids++ - } - case "enabled": - out.Values[i] = ec._DataframeOptimizationDecision_enabled(ctx, field, obj) - if out.Values[i] == graphql.Null { - out.Invalids++ - } - case "candidateSets": - out.Values[i] = ec._DataframeOptimizationDecision_candidateSets(ctx, field, obj) + out.Values[i] = graphql.MarshalString("DataframeRowConnection") + case "materialization": + out.Values[i] = ec._DataframeRowConnection_materialization(ctx, field, obj) if out.Values[i] == graphql.Null { out.Invalids++ } - case "estimatedBaselineWork": - out.Values[i] = ec._DataframeOptimizationDecision_estimatedBaselineWork(ctx, field, obj) + case "columns": + out.Values[i] = ec._DataframeRowConnection_columns(ctx, field, obj) if out.Values[i] == graphql.Null { out.Invalids++ } - case "estimatedOptimizedWork": - out.Values[i] = ec._DataframeOptimizationDecision_estimatedOptimizedWork(ctx, field, obj) + case "rows": + out.Values[i] = ec._DataframeRowConnection_rows(ctx, field, obj) if out.Values[i] == graphql.Null { out.Invalids++ } - case "estimatedSavings": - out.Values[i] = ec._DataframeOptimizationDecision_estimatedSavings(ctx, field, obj) - if out.Values[i] == graphql.Null { + case "totalCount": + out.Values[i] = ec._DataframeRowConnection_totalCount(ctx, field, obj) + if out.Values[i] == graphql.RequiredNull { out.Invalids++ } - case "reason": - out.Values[i] = ec._DataframeOptimizationDecision_reason(ctx, field, obj) + case "pageInfo": + out.Values[i] = ec._DataframeRowConnection_pageInfo(ctx, field, obj) if out.Values[i] == graphql.Null { out.Invalids++ } @@ -9934,48 +14563,47 @@ func (ec *executionContext) _DataframeOptimizationDecision(ctx context.Context, return graphql.Null } - atomic.AddInt32(&ec.deferred, int32(len(deferred))) + atomic.AddInt32(&ec.Deferred, int32(min(len(deferLabelToView), math.MaxInt32))) - for label, dfs := range deferred { - ec.processDeferredGroup(graphql.DeferredGroup{ - Label: label, - Path: graphql.GetPath(ctx), - FieldSet: dfs, - Context: ctx, - }) - } + ec.ProcessDeferredGroup(graphql.DeferredGroup{ + Defers: deferLabelToView, + Path: graphql.GetPath(ctx), + FieldSet: deferredFieldSet, + Context: ctx, + }) return out } -var dataframeOptimizationPolicyImplementors = []string{"DataframeOptimizationPolicy"} +var dataframeTraversalHintImplementors = []string{"DataframeTraversalHint"} -func (ec *executionContext) _DataframeOptimizationPolicy(ctx context.Context, sel ast.SelectionSet, obj *model.DataframeOptimizationPolicy) graphql.Marshaler { - fields := graphql.CollectFields(ec.OperationContext, sel, dataframeOptimizationPolicyImplementors) +func (ec *executionContext) _DataframeTraversalHint(ctx context.Context, sel ast.SelectionSet, obj *model.DataframeTraversalHint) graphql.Marshaler { + fields := graphql.CollectFields(ec.OperationContext, sel, dataframeTraversalHintImplementors) out := graphql.NewFieldSet(fields) - deferred := make(map[string]*graphql.FieldSet) + deferredFieldSet := graphql.NewFieldSet(nil) + deferLabelToView := make(map[string]*graphql.FieldSetView) for i, field := range fields { switch field.Name { case "__typename": - out.Values[i] = graphql.MarshalString("DataframeOptimizationPolicy") - case "name": - out.Values[i] = ec._DataframeOptimizationPolicy_name(ctx, field, obj) + out.Values[i] = graphql.MarshalString("DataframeTraversalHint") + case "fromType": + out.Values[i] = ec._DataframeTraversalHint_fromType(ctx, field, obj) if out.Values[i] == graphql.Null { out.Invalids++ } - case "enabled": - out.Values[i] = ec._DataframeOptimizationPolicy_enabled(ctx, field, obj) + case "label": + out.Values[i] = ec._DataframeTraversalHint_label(ctx, field, obj) if out.Values[i] == graphql.Null { out.Invalids++ } - case "minimumSavings": - out.Values[i] = ec._DataframeOptimizationPolicy_minimumSavings(ctx, field, obj) + case "toType": + out.Values[i] = ec._DataframeTraversalHint_toType(ctx, field, obj) if out.Values[i] == graphql.Null { out.Invalids++ } - case "decisions": - out.Values[i] = ec._DataframeOptimizationPolicy_decisions(ctx, field, obj) + case "edgeCount": + out.Values[i] = ec._DataframeTraversalHint_edgeCount(ctx, field, obj) if out.Values[i] == graphql.Null { out.Invalids++ } @@ -9988,38 +14616,50 @@ func (ec *executionContext) _DataframeOptimizationPolicy(ctx context.Context, se return graphql.Null } - atomic.AddInt32(&ec.deferred, int32(len(deferred))) + atomic.AddInt32(&ec.Deferred, int32(min(len(deferLabelToView), math.MaxInt32))) - for label, dfs := range deferred { - ec.processDeferredGroup(graphql.DeferredGroup{ - Label: label, - Path: graphql.GetPath(ctx), - FieldSet: dfs, - Context: ctx, - }) - } + ec.ProcessDeferredGroup(graphql.DeferredGroup{ + Defers: deferLabelToView, + Path: graphql.GetPath(ctx), + FieldSet: deferredFieldSet, + Context: ctx, + }) return out } -var dataframePageInfoImplementors = []string{"DataframePageInfo"} +var fhirDataframeResultImplementors = []string{"FhirDataframeResult"} -func (ec *executionContext) _DataframePageInfo(ctx context.Context, sel ast.SelectionSet, obj *model.DataframePageInfo) graphql.Marshaler { - fields := graphql.CollectFields(ec.OperationContext, sel, dataframePageInfoImplementors) +func (ec *executionContext) _FhirDataframeResult(ctx context.Context, sel ast.SelectionSet, obj *model.FhirDataframeResult) graphql.Marshaler { + fields := graphql.CollectFields(ec.OperationContext, sel, fhirDataframeResultImplementors) out := graphql.NewFieldSet(fields) - deferred := make(map[string]*graphql.FieldSet) + deferredFieldSet := graphql.NewFieldSet(nil) + deferLabelToView := make(map[string]*graphql.FieldSetView) for i, field := range fields { switch field.Name { case "__typename": - out.Values[i] = graphql.MarshalString("DataframePageInfo") - case "hasNextPage": - out.Values[i] = ec._DataframePageInfo_hasNextPage(ctx, field, obj) + out.Values[i] = graphql.MarshalString("FhirDataframeResult") + case "columns": + out.Values[i] = ec._FhirDataframeResult_columns(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ + } + case "rows": + out.Values[i] = ec._FhirDataframeResult_rows(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ + } + case "rowCount": + out.Values[i] = ec._FhirDataframeResult_rowCount(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ + } + case "diagnostics": + out.Values[i] = ec._FhirDataframeResult_diagnostics(ctx, field, obj) if out.Values[i] == graphql.Null { out.Invalids++ } - case "endCursor": - out.Values[i] = ec._DataframePageInfo_endCursor(ctx, field, obj) default: panic("unknown field " + strconv.Quote(field.Name)) } @@ -10029,68 +14669,70 @@ func (ec *executionContext) _DataframePageInfo(ctx context.Context, sel ast.Sele return graphql.Null } - atomic.AddInt32(&ec.deferred, int32(len(deferred))) + atomic.AddInt32(&ec.Deferred, int32(min(len(deferLabelToView), math.MaxInt32))) - for label, dfs := range deferred { - ec.processDeferredGroup(graphql.DeferredGroup{ - Label: label, - Path: graphql.GetPath(ctx), - FieldSet: dfs, - Context: ctx, - }) - } + ec.ProcessDeferredGroup(graphql.DeferredGroup{ + Defers: deferLabelToView, + Path: graphql.GetPath(ctx), + FieldSet: deferredFieldSet, + Context: ctx, + }) return out } -var dataframeQueryDiagnosticsImplementors = []string{"DataframeQueryDiagnostics"} +var mutationImplementors = []string{"Mutation"} -func (ec *executionContext) _DataframeQueryDiagnostics(ctx context.Context, sel ast.SelectionSet, obj *model.DataframeQueryDiagnostics) graphql.Marshaler { - fields := graphql.CollectFields(ec.OperationContext, sel, dataframeQueryDiagnosticsImplementors) +func (ec *executionContext) _Mutation(ctx context.Context, sel ast.SelectionSet) graphql.Marshaler { + fields := graphql.CollectFields(ec.OperationContext, sel, mutationImplementors) + ctx = graphql.WithFieldContext(ctx, &graphql.FieldContext{ + Object: "Mutation", + }) out := graphql.NewFieldSet(fields) - deferred := make(map[string]*graphql.FieldSet) + deferredFieldSet := graphql.NewFieldSet(nil) + deferLabelToView := make(map[string]*graphql.FieldSetView) for i, field := range fields { + innerCtx := graphql.WithRootFieldContext(ctx, &graphql.RootFieldContext{ + Object: field.Name, + Field: field, + }) + switch field.Name { case "__typename": - out.Values[i] = graphql.MarshalString("DataframeQueryDiagnostics") - case "inputResolutionMs": - out.Values[i] = ec._DataframeQueryDiagnostics_inputResolutionMs(ctx, field, obj) - if out.Values[i] == graphql.Null { - out.Invalids++ - } - case "requestPreparationMs": - out.Values[i] = ec._DataframeQueryDiagnostics_requestPreparationMs(ctx, field, obj) - if out.Values[i] == graphql.Null { - out.Invalids++ - } - case "compilationMs": - out.Values[i] = ec._DataframeQueryDiagnostics_compilationMs(ctx, field, obj) - if out.Values[i] == graphql.Null { - out.Invalids++ - } - case "arangoQueryMs": - out.Values[i] = ec._DataframeQueryDiagnostics_arangoQueryMs(ctx, field, obj) + out.Values[i] = graphql.MarshalString("Mutation") + case "runFhirDataframe": + out.Values[i] = ec.OperationContext.RootResolverMiddleware(innerCtx, func(ctx context.Context) (res graphql.Marshaler) { + return ec._Mutation_runFhirDataframe(ctx, field) + }) if out.Values[i] == graphql.Null { out.Invalids++ } - case "rowMaterializationMs": - out.Values[i] = ec._DataframeQueryDiagnostics_rowMaterializationMs(ctx, field, obj) + case "runDataframeRecipe": + out.Values[i] = ec.OperationContext.RootResolverMiddleware(innerCtx, func(ctx context.Context) (res graphql.Marshaler) { + return ec._Mutation_runDataframeRecipe(ctx, field) + }) if out.Values[i] == graphql.Null { out.Invalids++ } - case "resultAssemblyMs": - out.Values[i] = ec._DataframeQueryDiagnostics_resultAssemblyMs(ctx, field, obj) + case "validateDataframeRecipe": + out.Values[i] = ec.OperationContext.RootResolverMiddleware(innerCtx, func(ctx context.Context) (res graphql.Marshaler) { + return ec._Mutation_validateDataframeRecipe(ctx, field) + }) if out.Values[i] == graphql.Null { out.Invalids++ - } - case "totalMs": - out.Values[i] = ec._DataframeQueryDiagnostics_totalMs(ctx, field, obj) + } + case "previewDataframeRecipe": + out.Values[i] = ec.OperationContext.RootResolverMiddleware(innerCtx, func(ctx context.Context) (res graphql.Marshaler) { + return ec._Mutation_previewDataframeRecipe(ctx, field) + }) if out.Values[i] == graphql.Null { out.Invalids++ } - case "plan": - out.Values[i] = ec._DataframeQueryDiagnostics_plan(ctx, field, obj) + case "materializeDataframeRecipeBundle": + out.Values[i] = ec.OperationContext.RootResolverMiddleware(innerCtx, func(ctx context.Context) (res graphql.Marshaler) { + return ec._Mutation_materializeDataframeRecipeBundle(ctx, field) + }) if out.Values[i] == graphql.Null { out.Invalids++ } @@ -10103,99 +14745,249 @@ func (ec *executionContext) _DataframeQueryDiagnostics(ctx context.Context, sel return graphql.Null } - atomic.AddInt32(&ec.deferred, int32(len(deferred))) + atomic.AddInt32(&ec.Deferred, int32(min(len(deferLabelToView), math.MaxInt32))) - for label, dfs := range deferred { - ec.processDeferredGroup(graphql.DeferredGroup{ - Label: label, - Path: graphql.GetPath(ctx), - FieldSet: dfs, - Context: ctx, - }) - } + ec.ProcessDeferredGroup(graphql.DeferredGroup{ + Defers: deferLabelToView, + Path: graphql.GetPath(ctx), + FieldSet: deferredFieldSet, + Context: ctx, + }) return out } -var dataframeRelatedResourceHintsImplementors = []string{"DataframeRelatedResourceHints"} +var queryImplementors = []string{"Query"} -func (ec *executionContext) _DataframeRelatedResourceHints(ctx context.Context, sel ast.SelectionSet, obj *model.DataframeRelatedResourceHints) graphql.Marshaler { - fields := graphql.CollectFields(ec.OperationContext, sel, dataframeRelatedResourceHintsImplementors) +func (ec *executionContext) _Query(ctx context.Context, sel ast.SelectionSet) graphql.Marshaler { + fields := graphql.CollectFields(ec.OperationContext, sel, queryImplementors) + ctx = graphql.WithFieldContext(ctx, &graphql.FieldContext{ + Object: "Query", + }) out := graphql.NewFieldSet(fields) - deferred := make(map[string]*graphql.FieldSet) + deferredFieldSet := graphql.NewFieldSet(nil) + deferLabelToView := make(map[string]*graphql.FieldSetView) for i, field := range fields { + innerCtx := graphql.WithRootFieldContext(ctx, &graphql.RootFieldContext{ + Object: field.Name, + Field: field, + }) + switch field.Name { case "__typename": - out.Values[i] = graphql.MarshalString("DataframeRelatedResourceHints") - case "viaLabel": - out.Values[i] = ec._DataframeRelatedResourceHints_viaLabel(ctx, field, obj) - if out.Values[i] == graphql.Null { - out.Invalids++ + out.Values[i] = graphql.MarshalString("Query") + case "dataframeBuilderIntrospection": + field := field + + innerFunc := func(ctx context.Context, fs *graphql.FieldSet) (res graphql.Marshaler) { + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + } + }() + res = ec._Query_dataframeBuilderIntrospection(ctx, field) + if res == graphql.Null { + atomic.AddUint32(&fs.Invalids, 1) + } + return res } - case "edgeCount": - out.Values[i] = ec._DataframeRelatedResourceHints_edgeCount(ctx, field, obj) - if out.Values[i] == graphql.Null { - out.Invalids++ + + rrm := func(ctx context.Context) graphql.Marshaler { + return ec.OperationContext.RootResolverMiddleware(ctx, + func(ctx context.Context) graphql.Marshaler { return innerFunc(ctx, out) }) } - case "target": - out.Values[i] = ec._DataframeRelatedResourceHints_target(ctx, field, obj) - if out.Values[i] == graphql.Null { - out.Invalids++ + + out.Concurrently(i, func(ctx context.Context) graphql.Marshaler { return rrm(innerCtx) }) + case "dataframeMaterialization": + field := field + + innerFunc := func(ctx context.Context, fs *graphql.FieldSet) (res graphql.Marshaler) { + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + } + }() + res = ec._Query_dataframeMaterialization(ctx, field) + if res == graphql.RequiredNull { + atomic.AddUint32(&fs.Invalids, 1) + } + return res } - default: - panic("unknown field " + strconv.Quote(field.Name)) - } - } - out.Dispatch(ctx) - if out.Invalids > 0 { - return graphql.Null - } - atomic.AddInt32(&ec.deferred, int32(len(deferred))) + rrm := func(ctx context.Context) graphql.Marshaler { + return ec.OperationContext.RootResolverMiddleware(ctx, + func(ctx context.Context) graphql.Marshaler { return innerFunc(ctx, out) }) + } - for label, dfs := range deferred { - ec.processDeferredGroup(graphql.DeferredGroup{ - Label: label, - Path: graphql.GetPath(ctx), - FieldSet: dfs, - Context: ctx, - }) - } + out.Concurrently(i, func(ctx context.Context) graphql.Marshaler { return rrm(innerCtx) }) + case "dataframeDatasets": + field := field - return out -} + innerFunc := func(ctx context.Context, fs *graphql.FieldSet) (res graphql.Marshaler) { + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + } + }() + res = ec._Query_dataframeDatasets(ctx, field) + if res == graphql.Null { + atomic.AddUint32(&fs.Invalids, 1) + } + return res + } -var dataframeResourceHintsImplementors = []string{"DataframeResourceHints"} + rrm := func(ctx context.Context) graphql.Marshaler { + return ec.OperationContext.RootResolverMiddleware(ctx, + func(ctx context.Context) graphql.Marshaler { return innerFunc(ctx, out) }) + } -func (ec *executionContext) _DataframeResourceHints(ctx context.Context, sel ast.SelectionSet, obj *model.DataframeResourceHints) graphql.Marshaler { - fields := graphql.CollectFields(ec.OperationContext, sel, dataframeResourceHintsImplementors) + out.Concurrently(i, func(ctx context.Context) graphql.Marshaler { return rrm(innerCtx) }) + case "dataframeDataset": + field := field - out := graphql.NewFieldSet(fields) - deferred := make(map[string]*graphql.FieldSet) - for i, field := range fields { - switch field.Name { - case "__typename": - out.Values[i] = graphql.MarshalString("DataframeResourceHints") - case "resourceType": - out.Values[i] = ec._DataframeResourceHints_resourceType(ctx, field, obj) - if out.Values[i] == graphql.Null { - out.Invalids++ + innerFunc := func(ctx context.Context, fs *graphql.FieldSet) (res graphql.Marshaler) { + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + } + }() + res = ec._Query_dataframeDataset(ctx, field) + if res == graphql.RequiredNull { + atomic.AddUint32(&fs.Invalids, 1) + } + return res } - case "fields": - out.Values[i] = ec._DataframeResourceHints_fields(ctx, field, obj) - if out.Values[i] == graphql.Null { - out.Invalids++ + + rrm := func(ctx context.Context) graphql.Marshaler { + return ec.OperationContext.RootResolverMiddleware(ctx, + func(ctx context.Context) graphql.Marshaler { return innerFunc(ctx, out) }) } - case "pivotFields": - out.Values[i] = ec._DataframeResourceHints_pivotFields(ctx, field, obj) - if out.Values[i] == graphql.Null { - out.Invalids++ + + out.Concurrently(i, func(ctx context.Context) graphql.Marshaler { return rrm(innerCtx) }) + case "dataframeRows": + field := field + + innerFunc := func(ctx context.Context, fs *graphql.FieldSet) (res graphql.Marshaler) { + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + } + }() + res = ec._Query_dataframeRows(ctx, field) + if res == graphql.Null { + atomic.AddUint32(&fs.Invalids, 1) + } + return res } - case "traversals": - out.Values[i] = ec._DataframeResourceHints_traversals(ctx, field, obj) - if out.Values[i] == graphql.Null { - out.Invalids++ + + rrm := func(ctx context.Context) graphql.Marshaler { + return ec.OperationContext.RootResolverMiddleware(ctx, + func(ctx context.Context) graphql.Marshaler { return innerFunc(ctx, out) }) + } + + out.Concurrently(i, func(ctx context.Context) graphql.Marshaler { return rrm(innerCtx) }) + case "dataframeAggregate": + field := field + + innerFunc := func(ctx context.Context, fs *graphql.FieldSet) (res graphql.Marshaler) { + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + } + }() + res = ec._Query_dataframeAggregate(ctx, field) + if res == graphql.Null { + atomic.AddUint32(&fs.Invalids, 1) + } + return res + } + + rrm := func(ctx context.Context) graphql.Marshaler { + return ec.OperationContext.RootResolverMiddleware(ctx, + func(ctx context.Context) graphql.Marshaler { return innerFunc(ctx, out) }) + } + + out.Concurrently(i, func(ctx context.Context) graphql.Marshaler { return rrm(innerCtx) }) + case "dataframeRecipeExecution": + field := field + + innerFunc := func(ctx context.Context, fs *graphql.FieldSet) (res graphql.Marshaler) { + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + } + }() + res = ec._Query_dataframeRecipeExecution(ctx, field) + if res == graphql.RequiredNull { + atomic.AddUint32(&fs.Invalids, 1) + } + return res + } + + rrm := func(ctx context.Context) graphql.Marshaler { + return ec.OperationContext.RootResolverMiddleware(ctx, + func(ctx context.Context) graphql.Marshaler { return innerFunc(ctx, out) }) + } + + out.Concurrently(i, func(ctx context.Context) graphql.Marshaler { return rrm(innerCtx) }) + case "explainDataframeRecipe": + field := field + + innerFunc := func(ctx context.Context, fs *graphql.FieldSet) (res graphql.Marshaler) { + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + } + }() + res = ec._Query_explainDataframeRecipe(ctx, field) + if res == graphql.Null { + atomic.AddUint32(&fs.Invalids, 1) + } + return res + } + + rrm := func(ctx context.Context) graphql.Marshaler { + return ec.OperationContext.RootResolverMiddleware(ctx, + func(ctx context.Context) graphql.Marshaler { return innerFunc(ctx, out) }) + } + + out.Concurrently(i, func(ctx context.Context) graphql.Marshaler { return rrm(innerCtx) }) + case "preflightDataframeRecipe": + field := field + + innerFunc := func(ctx context.Context, fs *graphql.FieldSet) (res graphql.Marshaler) { + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + } + }() + res = ec._Query_preflightDataframeRecipe(ctx, field) + if res == graphql.Null { + atomic.AddUint32(&fs.Invalids, 1) + } + return res + } + + rrm := func(ctx context.Context) graphql.Marshaler { + return ec.OperationContext.RootResolverMiddleware(ctx, + func(ctx context.Context) graphql.Marshaler { return innerFunc(ctx, out) }) + } + + out.Concurrently(i, func(ctx context.Context) graphql.Marshaler { return rrm(innerCtx) }) + case "__type": + out.Values[i] = ec.OperationContext.RootResolverMiddleware(innerCtx, func(ctx context.Context) (res graphql.Marshaler) { + return ec._Query___type(ctx, field) + }) + if out.Values[i] == graphql.RequiredNull { + atomic.AddUint32(&out.Invalids, 1) + } + case "__schema": + out.Values[i] = ec.OperationContext.RootResolverMiddleware(innerCtx, func(ctx context.Context) (res graphql.Marshaler) { + return ec._Query___schema(ctx, field) + }) + if out.Values[i] == graphql.RequiredNull { + atomic.AddUint32(&out.Invalids, 1) } default: panic("unknown field " + strconv.Quote(field.Name)) @@ -10206,53 +14998,52 @@ func (ec *executionContext) _DataframeResourceHints(ctx context.Context, sel ast return graphql.Null } - atomic.AddInt32(&ec.deferred, int32(len(deferred))) + atomic.AddInt32(&ec.Deferred, int32(min(len(deferLabelToView), math.MaxInt32))) - for label, dfs := range deferred { - ec.processDeferredGroup(graphql.DeferredGroup{ - Label: label, - Path: graphql.GetPath(ctx), - FieldSet: dfs, - Context: ctx, - }) - } + ec.ProcessDeferredGroup(graphql.DeferredGroup{ + Defers: deferLabelToView, + Path: graphql.GetPath(ctx), + FieldSet: deferredFieldSet, + Context: ctx, + }) return out } -var dataframeRichSourceReuseImplementors = []string{"DataframeRichSourceReuse"} +var __DirectiveImplementors = []string{"__Directive"} -func (ec *executionContext) _DataframeRichSourceReuse(ctx context.Context, sel ast.SelectionSet, obj *model.DataframeRichSourceReuse) graphql.Marshaler { - fields := graphql.CollectFields(ec.OperationContext, sel, dataframeRichSourceReuseImplementors) +func (ec *executionContext) ___Directive(ctx context.Context, sel ast.SelectionSet, obj *introspection.Directive) graphql.Marshaler { + fields := graphql.CollectFields(ec.OperationContext, sel, __DirectiveImplementors) out := graphql.NewFieldSet(fields) - deferred := make(map[string]*graphql.FieldSet) + deferredFieldSet := graphql.NewFieldSet(nil) + deferLabelToView := make(map[string]*graphql.FieldSetView) for i, field := range fields { switch field.Name { case "__typename": - out.Values[i] = graphql.MarshalString("DataframeRichSourceReuse") - case "sourceSet": - out.Values[i] = ec._DataframeRichSourceReuse_sourceSet(ctx, field, obj) + out.Values[i] = graphql.MarshalString("__Directive") + case "name": + out.Values[i] = ec.___Directive_name(ctx, field, obj) if out.Values[i] == graphql.Null { out.Invalids++ } - case "aggregateConsumers": - out.Values[i] = ec._DataframeRichSourceReuse_aggregateConsumers(ctx, field, obj) - if out.Values[i] == graphql.Null { + case "description": + out.Values[i] = ec.___Directive_description(ctx, field, obj) + if out.Values[i] == graphql.RequiredNull { out.Invalids++ } - case "pivotConsumers": - out.Values[i] = ec._DataframeRichSourceReuse_pivotConsumers(ctx, field, obj) + case "isRepeatable": + out.Values[i] = ec.___Directive_isRepeatable(ctx, field, obj) if out.Values[i] == graphql.Null { out.Invalids++ } - case "sliceConsumers": - out.Values[i] = ec._DataframeRichSourceReuse_sliceConsumers(ctx, field, obj) + case "locations": + out.Values[i] = ec.___Directive_locations(ctx, field, obj) if out.Values[i] == graphql.Null { out.Invalids++ } - case "totalConsumers": - out.Values[i] = ec._DataframeRichSourceReuse_totalConsumers(ctx, field, obj) + case "args": + out.Values[i] = ec.___Directive_args(ctx, field, obj) if out.Values[i] == graphql.Null { out.Invalids++ } @@ -10265,49 +15056,48 @@ func (ec *executionContext) _DataframeRichSourceReuse(ctx context.Context, sel a return graphql.Null } - atomic.AddInt32(&ec.deferred, int32(len(deferred))) + atomic.AddInt32(&ec.Deferred, int32(min(len(deferLabelToView), math.MaxInt32))) - for label, dfs := range deferred { - ec.processDeferredGroup(graphql.DeferredGroup{ - Label: label, - Path: graphql.GetPath(ctx), - FieldSet: dfs, - Context: ctx, - }) - } + ec.ProcessDeferredGroup(graphql.DeferredGroup{ + Defers: deferLabelToView, + Path: graphql.GetPath(ctx), + FieldSet: deferredFieldSet, + Context: ctx, + }) return out } -var dataframeRowConnectionImplementors = []string{"DataframeRowConnection"} +var __EnumValueImplementors = []string{"__EnumValue"} -func (ec *executionContext) _DataframeRowConnection(ctx context.Context, sel ast.SelectionSet, obj *model.DataframeRowConnection) graphql.Marshaler { - fields := graphql.CollectFields(ec.OperationContext, sel, dataframeRowConnectionImplementors) +func (ec *executionContext) ___EnumValue(ctx context.Context, sel ast.SelectionSet, obj *introspection.EnumValue) graphql.Marshaler { + fields := graphql.CollectFields(ec.OperationContext, sel, __EnumValueImplementors) out := graphql.NewFieldSet(fields) - deferred := make(map[string]*graphql.FieldSet) + deferredFieldSet := graphql.NewFieldSet(nil) + deferLabelToView := make(map[string]*graphql.FieldSetView) for i, field := range fields { switch field.Name { case "__typename": - out.Values[i] = graphql.MarshalString("DataframeRowConnection") - case "materialization": - out.Values[i] = ec._DataframeRowConnection_materialization(ctx, field, obj) + out.Values[i] = graphql.MarshalString("__EnumValue") + case "name": + out.Values[i] = ec.___EnumValue_name(ctx, field, obj) if out.Values[i] == graphql.Null { out.Invalids++ } - case "columns": - out.Values[i] = ec._DataframeRowConnection_columns(ctx, field, obj) - if out.Values[i] == graphql.Null { + case "description": + out.Values[i] = ec.___EnumValue_description(ctx, field, obj) + if out.Values[i] == graphql.RequiredNull { out.Invalids++ } - case "rows": - out.Values[i] = ec._DataframeRowConnection_rows(ctx, field, obj) + case "isDeprecated": + out.Values[i] = ec.___EnumValue_isDeprecated(ctx, field, obj) if out.Values[i] == graphql.Null { out.Invalids++ } - case "pageInfo": - out.Values[i] = ec._DataframeRowConnection_pageInfo(ctx, field, obj) - if out.Values[i] == graphql.Null { + case "deprecationReason": + out.Values[i] = ec.___EnumValue_deprecationReason(ctx, field, obj) + if out.Values[i] == graphql.RequiredNull { out.Invalids++ } default: @@ -10319,51 +15109,60 @@ func (ec *executionContext) _DataframeRowConnection(ctx context.Context, sel ast return graphql.Null } - atomic.AddInt32(&ec.deferred, int32(len(deferred))) + atomic.AddInt32(&ec.Deferred, int32(min(len(deferLabelToView), math.MaxInt32))) - for label, dfs := range deferred { - ec.processDeferredGroup(graphql.DeferredGroup{ - Label: label, - Path: graphql.GetPath(ctx), - FieldSet: dfs, - Context: ctx, - }) - } + ec.ProcessDeferredGroup(graphql.DeferredGroup{ + Defers: deferLabelToView, + Path: graphql.GetPath(ctx), + FieldSet: deferredFieldSet, + Context: ctx, + }) return out } -var dataframeTraversalHintImplementors = []string{"DataframeTraversalHint"} +var __FieldImplementors = []string{"__Field"} -func (ec *executionContext) _DataframeTraversalHint(ctx context.Context, sel ast.SelectionSet, obj *model.DataframeTraversalHint) graphql.Marshaler { - fields := graphql.CollectFields(ec.OperationContext, sel, dataframeTraversalHintImplementors) +func (ec *executionContext) ___Field(ctx context.Context, sel ast.SelectionSet, obj *introspection.Field) graphql.Marshaler { + fields := graphql.CollectFields(ec.OperationContext, sel, __FieldImplementors) out := graphql.NewFieldSet(fields) - deferred := make(map[string]*graphql.FieldSet) + deferredFieldSet := graphql.NewFieldSet(nil) + deferLabelToView := make(map[string]*graphql.FieldSetView) for i, field := range fields { switch field.Name { case "__typename": - out.Values[i] = graphql.MarshalString("DataframeTraversalHint") - case "fromType": - out.Values[i] = ec._DataframeTraversalHint_fromType(ctx, field, obj) + out.Values[i] = graphql.MarshalString("__Field") + case "name": + out.Values[i] = ec.___Field_name(ctx, field, obj) if out.Values[i] == graphql.Null { out.Invalids++ } - case "label": - out.Values[i] = ec._DataframeTraversalHint_label(ctx, field, obj) + case "description": + out.Values[i] = ec.___Field_description(ctx, field, obj) + if out.Values[i] == graphql.RequiredNull { + out.Invalids++ + } + case "args": + out.Values[i] = ec.___Field_args(ctx, field, obj) if out.Values[i] == graphql.Null { out.Invalids++ } - case "toType": - out.Values[i] = ec._DataframeTraversalHint_toType(ctx, field, obj) + case "type": + out.Values[i] = ec.___Field_type(ctx, field, obj) if out.Values[i] == graphql.Null { out.Invalids++ } - case "edgeCount": - out.Values[i] = ec._DataframeTraversalHint_edgeCount(ctx, field, obj) + case "isDeprecated": + out.Values[i] = ec.___Field_isDeprecated(ctx, field, obj) if out.Values[i] == graphql.Null { out.Invalids++ } + case "deprecationReason": + out.Values[i] = ec.___Field_deprecationReason(ctx, field, obj) + if out.Values[i] == graphql.RequiredNull { + out.Invalids++ + } default: panic("unknown field " + strconv.Quote(field.Name)) } @@ -10373,100 +15172,60 @@ func (ec *executionContext) _DataframeTraversalHint(ctx context.Context, sel ast return graphql.Null } - atomic.AddInt32(&ec.deferred, int32(len(deferred))) + atomic.AddInt32(&ec.Deferred, int32(min(len(deferLabelToView), math.MaxInt32))) - for label, dfs := range deferred { - ec.processDeferredGroup(graphql.DeferredGroup{ - Label: label, - Path: graphql.GetPath(ctx), - FieldSet: dfs, - Context: ctx, - }) - } + ec.ProcessDeferredGroup(graphql.DeferredGroup{ + Defers: deferLabelToView, + Path: graphql.GetPath(ctx), + FieldSet: deferredFieldSet, + Context: ctx, + }) return out } -var fhirDataframeResultImplementors = []string{"FhirDataframeResult"} +var __InputValueImplementors = []string{"__InputValue"} -func (ec *executionContext) _FhirDataframeResult(ctx context.Context, sel ast.SelectionSet, obj *model.FhirDataframeResult) graphql.Marshaler { - fields := graphql.CollectFields(ec.OperationContext, sel, fhirDataframeResultImplementors) +func (ec *executionContext) ___InputValue(ctx context.Context, sel ast.SelectionSet, obj *introspection.InputValue) graphql.Marshaler { + fields := graphql.CollectFields(ec.OperationContext, sel, __InputValueImplementors) out := graphql.NewFieldSet(fields) - deferred := make(map[string]*graphql.FieldSet) + deferredFieldSet := graphql.NewFieldSet(nil) + deferLabelToView := make(map[string]*graphql.FieldSetView) for i, field := range fields { switch field.Name { case "__typename": - out.Values[i] = graphql.MarshalString("FhirDataframeResult") - case "columns": - out.Values[i] = ec._FhirDataframeResult_columns(ctx, field, obj) + out.Values[i] = graphql.MarshalString("__InputValue") + case "name": + out.Values[i] = ec.___InputValue_name(ctx, field, obj) if out.Values[i] == graphql.Null { out.Invalids++ } - case "rows": - out.Values[i] = ec._FhirDataframeResult_rows(ctx, field, obj) - if out.Values[i] == graphql.Null { + case "description": + out.Values[i] = ec.___InputValue_description(ctx, field, obj) + if out.Values[i] == graphql.RequiredNull { out.Invalids++ } - case "rowCount": - out.Values[i] = ec._FhirDataframeResult_rowCount(ctx, field, obj) + case "type": + out.Values[i] = ec.___InputValue_type(ctx, field, obj) if out.Values[i] == graphql.Null { out.Invalids++ } - case "diagnostics": - out.Values[i] = ec._FhirDataframeResult_diagnostics(ctx, field, obj) - if out.Values[i] == graphql.Null { + case "defaultValue": + out.Values[i] = ec.___InputValue_defaultValue(ctx, field, obj) + if out.Values[i] == graphql.RequiredNull { out.Invalids++ } - default: - panic("unknown field " + strconv.Quote(field.Name)) - } - } - out.Dispatch(ctx) - if out.Invalids > 0 { - return graphql.Null - } - - atomic.AddInt32(&ec.deferred, int32(len(deferred))) - - for label, dfs := range deferred { - ec.processDeferredGroup(graphql.DeferredGroup{ - Label: label, - Path: graphql.GetPath(ctx), - FieldSet: dfs, - Context: ctx, - }) - } - - return out -} - -var mutationImplementors = []string{"Mutation"} - -func (ec *executionContext) _Mutation(ctx context.Context, sel ast.SelectionSet) graphql.Marshaler { - fields := graphql.CollectFields(ec.OperationContext, sel, mutationImplementors) - ctx = graphql.WithFieldContext(ctx, &graphql.FieldContext{ - Object: "Mutation", - }) - - out := graphql.NewFieldSet(fields) - deferred := make(map[string]*graphql.FieldSet) - for i, field := range fields { - innerCtx := graphql.WithRootFieldContext(ctx, &graphql.RootFieldContext{ - Object: field.Name, - Field: field, - }) - - switch field.Name { - case "__typename": - out.Values[i] = graphql.MarshalString("Mutation") - case "runFhirDataframe": - out.Values[i] = ec.OperationContext.RootResolverMiddleware(innerCtx, func(ctx context.Context) (res graphql.Marshaler) { - return ec._Mutation_runFhirDataframe(ctx, field) - }) + case "isDeprecated": + out.Values[i] = ec.___InputValue_isDeprecated(ctx, field, obj) if out.Values[i] == graphql.Null { out.Invalids++ } + case "deprecationReason": + out.Values[i] = ec.___InputValue_deprecationReason(ctx, field, obj) + if out.Values[i] == graphql.RequiredNull { + out.Invalids++ + } default: panic("unknown field " + strconv.Quote(field.Name)) } @@ -10476,132 +15235,60 @@ func (ec *executionContext) _Mutation(ctx context.Context, sel ast.SelectionSet) return graphql.Null } - atomic.AddInt32(&ec.deferred, int32(len(deferred))) + atomic.AddInt32(&ec.Deferred, int32(min(len(deferLabelToView), math.MaxInt32))) - for label, dfs := range deferred { - ec.processDeferredGroup(graphql.DeferredGroup{ - Label: label, - Path: graphql.GetPath(ctx), - FieldSet: dfs, - Context: ctx, - }) - } + ec.ProcessDeferredGroup(graphql.DeferredGroup{ + Defers: deferLabelToView, + Path: graphql.GetPath(ctx), + FieldSet: deferredFieldSet, + Context: ctx, + }) return out } -var queryImplementors = []string{"Query"} +var __SchemaImplementors = []string{"__Schema"} -func (ec *executionContext) _Query(ctx context.Context, sel ast.SelectionSet) graphql.Marshaler { - fields := graphql.CollectFields(ec.OperationContext, sel, queryImplementors) - ctx = graphql.WithFieldContext(ctx, &graphql.FieldContext{ - Object: "Query", - }) +func (ec *executionContext) ___Schema(ctx context.Context, sel ast.SelectionSet, obj *introspection.Schema) graphql.Marshaler { + fields := graphql.CollectFields(ec.OperationContext, sel, __SchemaImplementors) out := graphql.NewFieldSet(fields) - deferred := make(map[string]*graphql.FieldSet) + deferredFieldSet := graphql.NewFieldSet(nil) + deferLabelToView := make(map[string]*graphql.FieldSetView) for i, field := range fields { - innerCtx := graphql.WithRootFieldContext(ctx, &graphql.RootFieldContext{ - Object: field.Name, - Field: field, - }) - switch field.Name { case "__typename": - out.Values[i] = graphql.MarshalString("Query") - case "dataframeBuilderIntrospection": - field := field - - innerFunc := func(ctx context.Context, fs *graphql.FieldSet) (res graphql.Marshaler) { - defer func() { - if r := recover(); r != nil { - ec.Error(ctx, ec.Recover(ctx, r)) - } - }() - res = ec._Query_dataframeBuilderIntrospection(ctx, field) - if res == graphql.Null { - atomic.AddUint32(&fs.Invalids, 1) - } - return res - } - - rrm := func(ctx context.Context) graphql.Marshaler { - return ec.OperationContext.RootResolverMiddleware(ctx, - func(ctx context.Context) graphql.Marshaler { return innerFunc(ctx, out) }) - } - - out.Concurrently(i, func(ctx context.Context) graphql.Marshaler { return rrm(innerCtx) }) - case "dataframeMaterialization": - field := field - - innerFunc := func(ctx context.Context, _ *graphql.FieldSet) (res graphql.Marshaler) { - defer func() { - if r := recover(); r != nil { - ec.Error(ctx, ec.Recover(ctx, r)) - } - }() - res = ec._Query_dataframeMaterialization(ctx, field) - return res - } - - rrm := func(ctx context.Context) graphql.Marshaler { - return ec.OperationContext.RootResolverMiddleware(ctx, - func(ctx context.Context) graphql.Marshaler { return innerFunc(ctx, out) }) + out.Values[i] = graphql.MarshalString("__Schema") + case "description": + out.Values[i] = ec.___Schema_description(ctx, field, obj) + if out.Values[i] == graphql.RequiredNull { + out.Invalids++ } - - out.Concurrently(i, func(ctx context.Context) graphql.Marshaler { return rrm(innerCtx) }) - case "dataframeRows": - field := field - - innerFunc := func(ctx context.Context, fs *graphql.FieldSet) (res graphql.Marshaler) { - defer func() { - if r := recover(); r != nil { - ec.Error(ctx, ec.Recover(ctx, r)) - } - }() - res = ec._Query_dataframeRows(ctx, field) - if res == graphql.Null { - atomic.AddUint32(&fs.Invalids, 1) - } - return res + case "types": + out.Values[i] = ec.___Schema_types(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ } - - rrm := func(ctx context.Context) graphql.Marshaler { - return ec.OperationContext.RootResolverMiddleware(ctx, - func(ctx context.Context) graphql.Marshaler { return innerFunc(ctx, out) }) + case "queryType": + out.Values[i] = ec.___Schema_queryType(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ } - - out.Concurrently(i, func(ctx context.Context) graphql.Marshaler { return rrm(innerCtx) }) - case "dataframeAggregate": - field := field - - innerFunc := func(ctx context.Context, fs *graphql.FieldSet) (res graphql.Marshaler) { - defer func() { - if r := recover(); r != nil { - ec.Error(ctx, ec.Recover(ctx, r)) - } - }() - res = ec._Query_dataframeAggregate(ctx, field) - if res == graphql.Null { - atomic.AddUint32(&fs.Invalids, 1) - } - return res + case "mutationType": + out.Values[i] = ec.___Schema_mutationType(ctx, field, obj) + if out.Values[i] == graphql.RequiredNull { + out.Invalids++ } - - rrm := func(ctx context.Context) graphql.Marshaler { - return ec.OperationContext.RootResolverMiddleware(ctx, - func(ctx context.Context) graphql.Marshaler { return innerFunc(ctx, out) }) + case "subscriptionType": + out.Values[i] = ec.___Schema_subscriptionType(ctx, field, obj) + if out.Values[i] == graphql.RequiredNull { + out.Invalids++ + } + case "directives": + out.Values[i] = ec.___Schema_directives(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ } - - out.Concurrently(i, func(ctx context.Context) graphql.Marshaler { return rrm(innerCtx) }) - case "__type": - out.Values[i] = ec.OperationContext.RootResolverMiddleware(innerCtx, func(ctx context.Context) (res graphql.Marshaler) { - return ec._Query___type(ctx, field) - }) - case "__schema": - out.Values[i] = ec.OperationContext.RootResolverMiddleware(innerCtx, func(ctx context.Context) (res graphql.Marshaler) { - return ec._Query___schema(ctx, field) - }) default: panic("unknown field " + strconv.Quote(field.Name)) } @@ -10611,51 +15298,83 @@ func (ec *executionContext) _Query(ctx context.Context, sel ast.SelectionSet) gr return graphql.Null } - atomic.AddInt32(&ec.deferred, int32(len(deferred))) + atomic.AddInt32(&ec.Deferred, int32(min(len(deferLabelToView), math.MaxInt32))) - for label, dfs := range deferred { - ec.processDeferredGroup(graphql.DeferredGroup{ - Label: label, - Path: graphql.GetPath(ctx), - FieldSet: dfs, - Context: ctx, - }) - } + ec.ProcessDeferredGroup(graphql.DeferredGroup{ + Defers: deferLabelToView, + Path: graphql.GetPath(ctx), + FieldSet: deferredFieldSet, + Context: ctx, + }) return out } -var __DirectiveImplementors = []string{"__Directive"} +var __TypeImplementors = []string{"__Type"} -func (ec *executionContext) ___Directive(ctx context.Context, sel ast.SelectionSet, obj *introspection.Directive) graphql.Marshaler { - fields := graphql.CollectFields(ec.OperationContext, sel, __DirectiveImplementors) +func (ec *executionContext) ___Type(ctx context.Context, sel ast.SelectionSet, obj *introspection.Type) graphql.Marshaler { + fields := graphql.CollectFields(ec.OperationContext, sel, __TypeImplementors) out := graphql.NewFieldSet(fields) - deferred := make(map[string]*graphql.FieldSet) + deferredFieldSet := graphql.NewFieldSet(nil) + deferLabelToView := make(map[string]*graphql.FieldSetView) for i, field := range fields { switch field.Name { case "__typename": - out.Values[i] = graphql.MarshalString("__Directive") - case "name": - out.Values[i] = ec.___Directive_name(ctx, field, obj) + out.Values[i] = graphql.MarshalString("__Type") + case "kind": + out.Values[i] = ec.___Type_kind(ctx, field, obj) if out.Values[i] == graphql.Null { out.Invalids++ } + case "name": + out.Values[i] = ec.___Type_name(ctx, field, obj) + if out.Values[i] == graphql.RequiredNull { + out.Invalids++ + } case "description": - out.Values[i] = ec.___Directive_description(ctx, field, obj) - case "locations": - out.Values[i] = ec.___Directive_locations(ctx, field, obj) - if out.Values[i] == graphql.Null { + out.Values[i] = ec.___Type_description(ctx, field, obj) + if out.Values[i] == graphql.RequiredNull { out.Invalids++ } - case "args": - out.Values[i] = ec.___Directive_args(ctx, field, obj) - if out.Values[i] == graphql.Null { + case "specifiedByURL": + out.Values[i] = ec.___Type_specifiedByURL(ctx, field, obj) + if out.Values[i] == graphql.RequiredNull { out.Invalids++ } - case "isRepeatable": - out.Values[i] = ec.___Directive_isRepeatable(ctx, field, obj) - if out.Values[i] == graphql.Null { + case "fields": + out.Values[i] = ec.___Type_fields(ctx, field, obj) + if out.Values[i] == graphql.RequiredNull { + out.Invalids++ + } + case "interfaces": + out.Values[i] = ec.___Type_interfaces(ctx, field, obj) + if out.Values[i] == graphql.RequiredNull { + out.Invalids++ + } + case "possibleTypes": + out.Values[i] = ec.___Type_possibleTypes(ctx, field, obj) + if out.Values[i] == graphql.RequiredNull { + out.Invalids++ + } + case "enumValues": + out.Values[i] = ec.___Type_enumValues(ctx, field, obj) + if out.Values[i] == graphql.RequiredNull { + out.Invalids++ + } + case "inputFields": + out.Values[i] = ec.___Type_inputFields(ctx, field, obj) + if out.Values[i] == graphql.RequiredNull { + out.Invalids++ + } + case "ofType": + out.Values[i] = ec.___Type_ofType(ctx, field, obj) + if out.Values[i] == graphql.RequiredNull { + out.Invalids++ + } + case "isOneOf": + out.Values[i] = ec.___Type_isOneOf(ctx, field, obj) + if out.Values[i] == graphql.RequiredNull { out.Invalids++ } default: @@ -10667,386 +15386,507 @@ func (ec *executionContext) ___Directive(ctx context.Context, sel ast.SelectionS return graphql.Null } - atomic.AddInt32(&ec.deferred, int32(len(deferred))) + atomic.AddInt32(&ec.Deferred, int32(min(len(deferLabelToView), math.MaxInt32))) + + ec.ProcessDeferredGroup(graphql.DeferredGroup{ + Defers: deferLabelToView, + Path: graphql.GetPath(ctx), + FieldSet: deferredFieldSet, + Context: ctx, + }) + + return out +} + +// endregion **************************** object.gotpl **************************** + +// region ***************************** type.gotpl ***************************** + +func (ec *executionContext) unmarshalNBoolean2bool(ctx context.Context, v any) (bool, error) { + res, err := graphql.UnmarshalBoolean(v) + return res, graphql.ErrorOnPath(ctx, err) +} + +func (ec *executionContext) marshalNBoolean2bool(ctx context.Context, sel ast.SelectionSet, v bool) graphql.Marshaler { + _ = sel + res := graphql.MarshalBoolean(v) + if res == graphql.Null { + if !graphql.HasFieldError(ctx, graphql.GetFieldContext(ctx)) { + graphql.AddErrorf(ctx, "the requested element is null which the schema does not allow") + } + } + return res +} + +func (ec *executionContext) unmarshalNDataframeAggregateInput2githubᚗcomᚋcalyprᚋloomᚋgraphqlapiᚋmodelᚐDataframeAggregateInput(ctx context.Context, v any) (model.DataframeAggregateInput, error) { + res, err := ec.unmarshalInputDataframeAggregateInput(ctx, v) + return res, graphql.ErrorOnPath(ctx, err) +} + +func (ec *executionContext) marshalNDataframeAggregateResult2githubᚗcomᚋcalyprᚋloomᚋgraphqlapiᚋmodelᚐDataframeAggregateResult(ctx context.Context, sel ast.SelectionSet, v model.DataframeAggregateResult) graphql.Marshaler { + return ec._DataframeAggregateResult(ctx, sel, &v) +} + +func (ec *executionContext) marshalNDataframeAggregateResult2ᚖgithubᚗcomᚋcalyprᚋloomᚋgraphqlapiᚋmodelᚐDataframeAggregateResult(ctx context.Context, sel ast.SelectionSet, v *model.DataframeAggregateResult) graphql.Marshaler { + if v == nil { + if !graphql.HasFieldError(ctx, graphql.GetFieldContext(ctx)) { + graphql.AddErrorf(ctx, "the requested element is null which the schema does not allow") + } + return graphql.Null + } + return ec._DataframeAggregateResult(ctx, sel, v) +} + +func (ec *executionContext) marshalNDataframeBuilderIntrospection2githubᚗcomᚋcalyprᚋloomᚋgraphqlapiᚋmodelᚐDataframeBuilderIntrospection(ctx context.Context, sel ast.SelectionSet, v model.DataframeBuilderIntrospection) graphql.Marshaler { + return ec._DataframeBuilderIntrospection(ctx, sel, &v) +} + +func (ec *executionContext) marshalNDataframeBuilderIntrospection2ᚖgithubᚗcomᚋcalyprᚋloomᚋgraphqlapiᚋmodelᚐDataframeBuilderIntrospection(ctx context.Context, sel ast.SelectionSet, v *model.DataframeBuilderIntrospection) graphql.Marshaler { + if v == nil { + if !graphql.HasFieldError(ctx, graphql.GetFieldContext(ctx)) { + graphql.AddErrorf(ctx, "the requested element is null which the schema does not allow") + } + return graphql.Null + } + return ec._DataframeBuilderIntrospection(ctx, sel, v) +} + +func (ec *executionContext) unmarshalNDataframeBuilderIntrospectionInput2githubᚗcomᚋcalyprᚋloomᚋgraphqlapiᚋmodelᚐDataframeBuilderIntrospectionInput(ctx context.Context, v any) (model.DataframeBuilderIntrospectionInput, error) { + res, err := ec.unmarshalInputDataframeBuilderIntrospectionInput(ctx, v) + return res, graphql.ErrorOnPath(ctx, err) +} + +func (ec *executionContext) marshalNDataframeColumn2ᚕᚖgithubᚗcomᚋcalyprᚋloomᚋgraphqlapiᚋmodelᚐDataframeColumnᚄ(ctx context.Context, sel ast.SelectionSet, v []*model.DataframeColumn) graphql.Marshaler { + ret := graphql.MarshalSliceConcurrently(ctx, len(v), 0, false, func(ctx context.Context, i int) graphql.Marshaler { + fc := graphql.GetFieldContext(ctx) + fc.Result = &v[i] + return ec.marshalNDataframeColumn2ᚖgithubᚗcomᚋcalyprᚋloomᚋgraphqlapiᚋmodelᚐDataframeColumn(ctx, sel, v[i]) + }) + + for _, e := range ret { + if e == graphql.Null { + return graphql.Null + } + } + + return ret +} + +func (ec *executionContext) marshalNDataframeColumn2ᚖgithubᚗcomᚋcalyprᚋloomᚋgraphqlapiᚋmodelᚐDataframeColumn(ctx context.Context, sel ast.SelectionSet, v *model.DataframeColumn) graphql.Marshaler { + if v == nil { + if !graphql.HasFieldError(ctx, graphql.GetFieldContext(ctx)) { + graphql.AddErrorf(ctx, "the requested element is null which the schema does not allow") + } + return graphql.Null + } + return ec._DataframeColumn(ctx, sel, v) +} + +func (ec *executionContext) marshalNDataframeCompilerPlanDiagnostics2ᚖgithubᚗcomᚋcalyprᚋloomᚋgraphqlapiᚋmodelᚐDataframeCompilerPlanDiagnostics(ctx context.Context, sel ast.SelectionSet, v *model.DataframeCompilerPlanDiagnostics) graphql.Marshaler { + if v == nil { + if !graphql.HasFieldError(ctx, graphql.GetFieldContext(ctx)) { + graphql.AddErrorf(ctx, "the requested element is null which the schema does not allow") + } + return graphql.Null + } + return ec._DataframeCompilerPlanDiagnostics(ctx, sel, v) +} + +func (ec *executionContext) unmarshalNDataframeDatasetInput2githubᚗcomᚋcalyprᚋloomᚋgraphqlapiᚋmodelᚐDataframeDatasetInput(ctx context.Context, v any) (model.DataframeDatasetInput, error) { + res, err := ec.unmarshalInputDataframeDatasetInput(ctx, v) + return res, graphql.ErrorOnPath(ctx, err) +} + +func (ec *executionContext) marshalNDataframeFieldHint2ᚕᚖgithubᚗcomᚋcalyprᚋloomᚋgraphqlapiᚋmodelᚐDataframeFieldHintᚄ(ctx context.Context, sel ast.SelectionSet, v []*model.DataframeFieldHint) graphql.Marshaler { + ret := graphql.MarshalSliceConcurrently(ctx, len(v), 0, false, func(ctx context.Context, i int) graphql.Marshaler { + fc := graphql.GetFieldContext(ctx) + fc.Result = &v[i] + return ec.marshalNDataframeFieldHint2ᚖgithubᚗcomᚋcalyprᚋloomᚋgraphqlapiᚋmodelᚐDataframeFieldHint(ctx, sel, v[i]) + }) + + for _, e := range ret { + if e == graphql.Null { + return graphql.Null + } + } + + return ret +} + +func (ec *executionContext) marshalNDataframeFieldHint2ᚖgithubᚗcomᚋcalyprᚋloomᚋgraphqlapiᚋmodelᚐDataframeFieldHint(ctx context.Context, sel ast.SelectionSet, v *model.DataframeFieldHint) graphql.Marshaler { + if v == nil { + if !graphql.HasFieldError(ctx, graphql.GetFieldContext(ctx)) { + graphql.AddErrorf(ctx, "the requested element is null which the schema does not allow") + } + return graphql.Null + } + return ec._DataframeFieldHint(ctx, sel, v) +} + +func (ec *executionContext) marshalNDataframeFieldSelector2ᚖgithubᚗcomᚋcalyprᚋloomᚋgraphqlapiᚋmodelᚐDataframeFieldSelector(ctx context.Context, sel ast.SelectionSet, v *model.DataframeFieldSelector) graphql.Marshaler { + if v == nil { + if !graphql.HasFieldError(ctx, graphql.GetFieldContext(ctx)) { + graphql.AddErrorf(ctx, "the requested element is null which the schema does not allow") + } + return graphql.Null + } + return ec._DataframeFieldSelector(ctx, sel, v) +} + +func (ec *executionContext) unmarshalNDataframeFilterInput2ᚖgithubᚗcomᚋcalyprᚋloomᚋgraphqlapiᚋmodelᚐDataframeFilterInput(ctx context.Context, v any) (*model.DataframeFilterInput, error) { + res, err := ec.unmarshalInputDataframeFilterInput(ctx, v) + return &res, graphql.ErrorOnPath(ctx, err) +} + +func (ec *executionContext) marshalNDataframeMaterialization2ᚕᚖgithubᚗcomᚋcalyprᚋloomᚋgraphqlapiᚋmodelᚐDataframeMaterializationᚄ(ctx context.Context, sel ast.SelectionSet, v []*model.DataframeMaterialization) graphql.Marshaler { + ret := graphql.MarshalSliceConcurrently(ctx, len(v), 0, false, func(ctx context.Context, i int) graphql.Marshaler { + fc := graphql.GetFieldContext(ctx) + fc.Result = &v[i] + return ec.marshalNDataframeMaterialization2ᚖgithubᚗcomᚋcalyprᚋloomᚋgraphqlapiᚋmodelᚐDataframeMaterialization(ctx, sel, v[i]) + }) + + for _, e := range ret { + if e == graphql.Null { + return graphql.Null + } + } + + return ret +} + +func (ec *executionContext) marshalNDataframeMaterialization2ᚖgithubᚗcomᚋcalyprᚋloomᚋgraphqlapiᚋmodelᚐDataframeMaterialization(ctx context.Context, sel ast.SelectionSet, v *model.DataframeMaterialization) graphql.Marshaler { + if v == nil { + if !graphql.HasFieldError(ctx, graphql.GetFieldContext(ctx)) { + graphql.AddErrorf(ctx, "the requested element is null which the schema does not allow") + } + return graphql.Null + } + return ec._DataframeMaterialization(ctx, sel, v) +} + +func (ec *executionContext) unmarshalNDataframeMaterializationState2githubᚗcomᚋcalyprᚋloomᚋgraphqlapiᚋmodelᚐDataframeMaterializationState(ctx context.Context, v any) (model.DataframeMaterializationState, error) { + var res model.DataframeMaterializationState + err := res.UnmarshalGQL(v) + return res, graphql.ErrorOnPath(ctx, err) +} + +func (ec *executionContext) marshalNDataframeMaterializationState2githubᚗcomᚋcalyprᚋloomᚋgraphqlapiᚋmodelᚐDataframeMaterializationState(ctx context.Context, sel ast.SelectionSet, v model.DataframeMaterializationState) graphql.Marshaler { + return v +} + +func (ec *executionContext) marshalNDataframeOptimizationDecision2ᚕᚖgithubᚗcomᚋcalyprᚋloomᚋgraphqlapiᚋmodelᚐDataframeOptimizationDecisionᚄ(ctx context.Context, sel ast.SelectionSet, v []*model.DataframeOptimizationDecision) graphql.Marshaler { + ret := graphql.MarshalSliceConcurrently(ctx, len(v), 0, false, func(ctx context.Context, i int) graphql.Marshaler { + fc := graphql.GetFieldContext(ctx) + fc.Result = &v[i] + return ec.marshalNDataframeOptimizationDecision2ᚖgithubᚗcomᚋcalyprᚋloomᚋgraphqlapiᚋmodelᚐDataframeOptimizationDecision(ctx, sel, v[i]) + }) - for label, dfs := range deferred { - ec.processDeferredGroup(graphql.DeferredGroup{ - Label: label, - Path: graphql.GetPath(ctx), - FieldSet: dfs, - Context: ctx, - }) + for _, e := range ret { + if e == graphql.Null { + return graphql.Null + } } - return out + return ret } -var __EnumValueImplementors = []string{"__EnumValue"} - -func (ec *executionContext) ___EnumValue(ctx context.Context, sel ast.SelectionSet, obj *introspection.EnumValue) graphql.Marshaler { - fields := graphql.CollectFields(ec.OperationContext, sel, __EnumValueImplementors) - - out := graphql.NewFieldSet(fields) - deferred := make(map[string]*graphql.FieldSet) - for i, field := range fields { - switch field.Name { - case "__typename": - out.Values[i] = graphql.MarshalString("__EnumValue") - case "name": - out.Values[i] = ec.___EnumValue_name(ctx, field, obj) - if out.Values[i] == graphql.Null { - out.Invalids++ - } - case "description": - out.Values[i] = ec.___EnumValue_description(ctx, field, obj) - case "isDeprecated": - out.Values[i] = ec.___EnumValue_isDeprecated(ctx, field, obj) - if out.Values[i] == graphql.Null { - out.Invalids++ - } - case "deprecationReason": - out.Values[i] = ec.___EnumValue_deprecationReason(ctx, field, obj) - default: - panic("unknown field " + strconv.Quote(field.Name)) +func (ec *executionContext) marshalNDataframeOptimizationDecision2ᚖgithubᚗcomᚋcalyprᚋloomᚋgraphqlapiᚋmodelᚐDataframeOptimizationDecision(ctx context.Context, sel ast.SelectionSet, v *model.DataframeOptimizationDecision) graphql.Marshaler { + if v == nil { + if !graphql.HasFieldError(ctx, graphql.GetFieldContext(ctx)) { + graphql.AddErrorf(ctx, "the requested element is null which the schema does not allow") } - } - out.Dispatch(ctx) - if out.Invalids > 0 { return graphql.Null } + return ec._DataframeOptimizationDecision(ctx, sel, v) +} - atomic.AddInt32(&ec.deferred, int32(len(deferred))) - - for label, dfs := range deferred { - ec.processDeferredGroup(graphql.DeferredGroup{ - Label: label, - Path: graphql.GetPath(ctx), - FieldSet: dfs, - Context: ctx, - }) +func (ec *executionContext) marshalNDataframeOptimizationPolicy2ᚖgithubᚗcomᚋcalyprᚋloomᚋgraphqlapiᚋmodelᚐDataframeOptimizationPolicy(ctx context.Context, sel ast.SelectionSet, v *model.DataframeOptimizationPolicy) graphql.Marshaler { + if v == nil { + if !graphql.HasFieldError(ctx, graphql.GetFieldContext(ctx)) { + graphql.AddErrorf(ctx, "the requested element is null which the schema does not allow") + } + return graphql.Null } - - return out + return ec._DataframeOptimizationPolicy(ctx, sel, v) } -var __FieldImplementors = []string{"__Field"} - -func (ec *executionContext) ___Field(ctx context.Context, sel ast.SelectionSet, obj *introspection.Field) graphql.Marshaler { - fields := graphql.CollectFields(ec.OperationContext, sel, __FieldImplementors) - - out := graphql.NewFieldSet(fields) - deferred := make(map[string]*graphql.FieldSet) - for i, field := range fields { - switch field.Name { - case "__typename": - out.Values[i] = graphql.MarshalString("__Field") - case "name": - out.Values[i] = ec.___Field_name(ctx, field, obj) - if out.Values[i] == graphql.Null { - out.Invalids++ - } - case "description": - out.Values[i] = ec.___Field_description(ctx, field, obj) - case "args": - out.Values[i] = ec.___Field_args(ctx, field, obj) - if out.Values[i] == graphql.Null { - out.Invalids++ - } - case "type": - out.Values[i] = ec.___Field_type(ctx, field, obj) - if out.Values[i] == graphql.Null { - out.Invalids++ - } - case "isDeprecated": - out.Values[i] = ec.___Field_isDeprecated(ctx, field, obj) - if out.Values[i] == graphql.Null { - out.Invalids++ - } - case "deprecationReason": - out.Values[i] = ec.___Field_deprecationReason(ctx, field, obj) - default: - panic("unknown field " + strconv.Quote(field.Name)) +func (ec *executionContext) marshalNDataframePageInfo2ᚖgithubᚗcomᚋcalyprᚋloomᚋgraphqlapiᚋmodelᚐDataframePageInfo(ctx context.Context, sel ast.SelectionSet, v *model.DataframePageInfo) graphql.Marshaler { + if v == nil { + if !graphql.HasFieldError(ctx, graphql.GetFieldContext(ctx)) { + graphql.AddErrorf(ctx, "the requested element is null which the schema does not allow") } + return graphql.Null } - out.Dispatch(ctx) - if out.Invalids > 0 { + return ec._DataframePageInfo(ctx, sel, v) +} + +func (ec *executionContext) marshalNDataframeQueryDiagnostics2ᚖgithubᚗcomᚋcalyprᚋloomᚋgraphqlapiᚋmodelᚐDataframeQueryDiagnostics(ctx context.Context, sel ast.SelectionSet, v *model.DataframeQueryDiagnostics) graphql.Marshaler { + if v == nil { + if !graphql.HasFieldError(ctx, graphql.GetFieldContext(ctx)) { + graphql.AddErrorf(ctx, "the requested element is null which the schema does not allow") + } return graphql.Null } + return ec._DataframeQueryDiagnostics(ctx, sel, v) +} - atomic.AddInt32(&ec.deferred, int32(len(deferred))) +func (ec *executionContext) unmarshalNDataframeRecipeBindingsInput2ᚖgithubᚗcomᚋcalyprᚋloomᚋgraphqlapiᚋmodelᚐDataframeRecipeBindingsInput(ctx context.Context, v any) (*model.DataframeRecipeBindingsInput, error) { + res, err := ec.unmarshalInputDataframeRecipeBindingsInput(ctx, v) + return &res, graphql.ErrorOnPath(ctx, err) +} - for label, dfs := range deferred { - ec.processDeferredGroup(graphql.DeferredGroup{ - Label: label, - Path: graphql.GetPath(ctx), - FieldSet: dfs, - Context: ctx, - }) +func (ec *executionContext) marshalNDataframeRecipeColumn2ᚕᚖgithubᚗcomᚋcalyprᚋloomᚋgraphqlapiᚋmodelᚐDataframeRecipeColumnᚄ(ctx context.Context, sel ast.SelectionSet, v []*model.DataframeRecipeColumn) graphql.Marshaler { + ret := graphql.MarshalSliceConcurrently(ctx, len(v), 0, false, func(ctx context.Context, i int) graphql.Marshaler { + fc := graphql.GetFieldContext(ctx) + fc.Result = &v[i] + return ec.marshalNDataframeRecipeColumn2ᚖgithubᚗcomᚋcalyprᚋloomᚋgraphqlapiᚋmodelᚐDataframeRecipeColumn(ctx, sel, v[i]) + }) + + for _, e := range ret { + if e == graphql.Null { + return graphql.Null + } } - return out + return ret } -var __InputValueImplementors = []string{"__InputValue"} +func (ec *executionContext) marshalNDataframeRecipeColumn2ᚖgithubᚗcomᚋcalyprᚋloomᚋgraphqlapiᚋmodelᚐDataframeRecipeColumn(ctx context.Context, sel ast.SelectionSet, v *model.DataframeRecipeColumn) graphql.Marshaler { + if v == nil { + if !graphql.HasFieldError(ctx, graphql.GetFieldContext(ctx)) { + graphql.AddErrorf(ctx, "the requested element is null which the schema does not allow") + } + return graphql.Null + } + return ec._DataframeRecipeColumn(ctx, sel, v) +} -func (ec *executionContext) ___InputValue(ctx context.Context, sel ast.SelectionSet, obj *introspection.InputValue) graphql.Marshaler { - fields := graphql.CollectFields(ec.OperationContext, sel, __InputValueImplementors) +func (ec *executionContext) marshalNDataframeRecipeExecution2githubᚗcomᚋcalyprᚋloomᚋgraphqlapiᚋmodelᚐDataframeRecipeExecution(ctx context.Context, sel ast.SelectionSet, v model.DataframeRecipeExecution) graphql.Marshaler { + return ec._DataframeRecipeExecution(ctx, sel, &v) +} - out := graphql.NewFieldSet(fields) - deferred := make(map[string]*graphql.FieldSet) - for i, field := range fields { - switch field.Name { - case "__typename": - out.Values[i] = graphql.MarshalString("__InputValue") - case "name": - out.Values[i] = ec.___InputValue_name(ctx, field, obj) - if out.Values[i] == graphql.Null { - out.Invalids++ - } - case "description": - out.Values[i] = ec.___InputValue_description(ctx, field, obj) - case "type": - out.Values[i] = ec.___InputValue_type(ctx, field, obj) - if out.Values[i] == graphql.Null { - out.Invalids++ - } - case "defaultValue": - out.Values[i] = ec.___InputValue_defaultValue(ctx, field, obj) - case "isDeprecated": - out.Values[i] = ec.___InputValue_isDeprecated(ctx, field, obj) - if out.Values[i] == graphql.Null { - out.Invalids++ - } - case "deprecationReason": - out.Values[i] = ec.___InputValue_deprecationReason(ctx, field, obj) - default: - panic("unknown field " + strconv.Quote(field.Name)) +func (ec *executionContext) marshalNDataframeRecipeExecution2ᚖgithubᚗcomᚋcalyprᚋloomᚋgraphqlapiᚋmodelᚐDataframeRecipeExecution(ctx context.Context, sel ast.SelectionSet, v *model.DataframeRecipeExecution) graphql.Marshaler { + if v == nil { + if !graphql.HasFieldError(ctx, graphql.GetFieldContext(ctx)) { + graphql.AddErrorf(ctx, "the requested element is null which the schema does not allow") } - } - out.Dispatch(ctx) - if out.Invalids > 0 { return graphql.Null } + return ec._DataframeRecipeExecution(ctx, sel, v) +} - atomic.AddInt32(&ec.deferred, int32(len(deferred))) +func (ec *executionContext) marshalNDataframeRecipeExecutionOutput2ᚕᚖgithubᚗcomᚋcalyprᚋloomᚋgraphqlapiᚋmodelᚐDataframeRecipeExecutionOutputᚄ(ctx context.Context, sel ast.SelectionSet, v []*model.DataframeRecipeExecutionOutput) graphql.Marshaler { + ret := graphql.MarshalSliceConcurrently(ctx, len(v), 0, false, func(ctx context.Context, i int) graphql.Marshaler { + fc := graphql.GetFieldContext(ctx) + fc.Result = &v[i] + return ec.marshalNDataframeRecipeExecutionOutput2ᚖgithubᚗcomᚋcalyprᚋloomᚋgraphqlapiᚋmodelᚐDataframeRecipeExecutionOutput(ctx, sel, v[i]) + }) - for label, dfs := range deferred { - ec.processDeferredGroup(graphql.DeferredGroup{ - Label: label, - Path: graphql.GetPath(ctx), - FieldSet: dfs, - Context: ctx, - }) + for _, e := range ret { + if e == graphql.Null { + return graphql.Null + } } - return out + return ret } -var __SchemaImplementors = []string{"__Schema"} - -func (ec *executionContext) ___Schema(ctx context.Context, sel ast.SelectionSet, obj *introspection.Schema) graphql.Marshaler { - fields := graphql.CollectFields(ec.OperationContext, sel, __SchemaImplementors) - - out := graphql.NewFieldSet(fields) - deferred := make(map[string]*graphql.FieldSet) - for i, field := range fields { - switch field.Name { - case "__typename": - out.Values[i] = graphql.MarshalString("__Schema") - case "description": - out.Values[i] = ec.___Schema_description(ctx, field, obj) - case "types": - out.Values[i] = ec.___Schema_types(ctx, field, obj) - if out.Values[i] == graphql.Null { - out.Invalids++ - } - case "queryType": - out.Values[i] = ec.___Schema_queryType(ctx, field, obj) - if out.Values[i] == graphql.Null { - out.Invalids++ - } - case "mutationType": - out.Values[i] = ec.___Schema_mutationType(ctx, field, obj) - case "subscriptionType": - out.Values[i] = ec.___Schema_subscriptionType(ctx, field, obj) - case "directives": - out.Values[i] = ec.___Schema_directives(ctx, field, obj) - if out.Values[i] == graphql.Null { - out.Invalids++ - } - default: - panic("unknown field " + strconv.Quote(field.Name)) +func (ec *executionContext) marshalNDataframeRecipeExecutionOutput2ᚖgithubᚗcomᚋcalyprᚋloomᚋgraphqlapiᚋmodelᚐDataframeRecipeExecutionOutput(ctx context.Context, sel ast.SelectionSet, v *model.DataframeRecipeExecutionOutput) graphql.Marshaler { + if v == nil { + if !graphql.HasFieldError(ctx, graphql.GetFieldContext(ctx)) { + graphql.AddErrorf(ctx, "the requested element is null which the schema does not allow") } - } - out.Dispatch(ctx) - if out.Invalids > 0 { return graphql.Null } + return ec._DataframeRecipeExecutionOutput(ctx, sel, v) +} - atomic.AddInt32(&ec.deferred, int32(len(deferred))) - - for label, dfs := range deferred { - ec.processDeferredGroup(graphql.DeferredGroup{ - Label: label, - Path: graphql.GetPath(ctx), - FieldSet: dfs, - Context: ctx, - }) - } +func (ec *executionContext) unmarshalNDataframeRecipeExecutionState2githubᚗcomᚋcalyprᚋloomᚋgraphqlapiᚋmodelᚐDataframeRecipeExecutionState(ctx context.Context, v any) (model.DataframeRecipeExecutionState, error) { + var res model.DataframeRecipeExecutionState + err := res.UnmarshalGQL(v) + return res, graphql.ErrorOnPath(ctx, err) +} - return out +func (ec *executionContext) marshalNDataframeRecipeExecutionState2githubᚗcomᚋcalyprᚋloomᚋgraphqlapiᚋmodelᚐDataframeRecipeExecutionState(ctx context.Context, sel ast.SelectionSet, v model.DataframeRecipeExecutionState) graphql.Marshaler { + return v } -var __TypeImplementors = []string{"__Type"} +func (ec *executionContext) marshalNDataframeRecipeExplainCollectionScan2ᚕᚖgithubᚗcomᚋcalyprᚋloomᚋgraphqlapiᚋmodelᚐDataframeRecipeExplainCollectionScanᚄ(ctx context.Context, sel ast.SelectionSet, v []*model.DataframeRecipeExplainCollectionScan) graphql.Marshaler { + ret := graphql.MarshalSliceConcurrently(ctx, len(v), 0, false, func(ctx context.Context, i int) graphql.Marshaler { + fc := graphql.GetFieldContext(ctx) + fc.Result = &v[i] + return ec.marshalNDataframeRecipeExplainCollectionScan2ᚖgithubᚗcomᚋcalyprᚋloomᚋgraphqlapiᚋmodelᚐDataframeRecipeExplainCollectionScan(ctx, sel, v[i]) + }) -func (ec *executionContext) ___Type(ctx context.Context, sel ast.SelectionSet, obj *introspection.Type) graphql.Marshaler { - fields := graphql.CollectFields(ec.OperationContext, sel, __TypeImplementors) + for _, e := range ret { + if e == graphql.Null { + return graphql.Null + } + } - out := graphql.NewFieldSet(fields) - deferred := make(map[string]*graphql.FieldSet) - for i, field := range fields { - switch field.Name { - case "__typename": - out.Values[i] = graphql.MarshalString("__Type") - case "kind": - out.Values[i] = ec.___Type_kind(ctx, field, obj) - if out.Values[i] == graphql.Null { - out.Invalids++ - } - case "name": - out.Values[i] = ec.___Type_name(ctx, field, obj) - case "description": - out.Values[i] = ec.___Type_description(ctx, field, obj) - case "fields": - out.Values[i] = ec.___Type_fields(ctx, field, obj) - case "interfaces": - out.Values[i] = ec.___Type_interfaces(ctx, field, obj) - case "possibleTypes": - out.Values[i] = ec.___Type_possibleTypes(ctx, field, obj) - case "enumValues": - out.Values[i] = ec.___Type_enumValues(ctx, field, obj) - case "inputFields": - out.Values[i] = ec.___Type_inputFields(ctx, field, obj) - case "ofType": - out.Values[i] = ec.___Type_ofType(ctx, field, obj) - case "specifiedByURL": - out.Values[i] = ec.___Type_specifiedByURL(ctx, field, obj) - case "isOneOf": - out.Values[i] = ec.___Type_isOneOf(ctx, field, obj) - default: - panic("unknown field " + strconv.Quote(field.Name)) + return ret +} + +func (ec *executionContext) marshalNDataframeRecipeExplainCollectionScan2ᚖgithubᚗcomᚋcalyprᚋloomᚋgraphqlapiᚋmodelᚐDataframeRecipeExplainCollectionScan(ctx context.Context, sel ast.SelectionSet, v *model.DataframeRecipeExplainCollectionScan) graphql.Marshaler { + if v == nil { + if !graphql.HasFieldError(ctx, graphql.GetFieldContext(ctx)) { + graphql.AddErrorf(ctx, "the requested element is null which the schema does not allow") } - } - out.Dispatch(ctx) - if out.Invalids > 0 { return graphql.Null } + return ec._DataframeRecipeExplainCollectionScan(ctx, sel, v) +} - atomic.AddInt32(&ec.deferred, int32(len(deferred))) +func (ec *executionContext) marshalNDataframeRecipeExplainIndexLocation2ᚕᚖgithubᚗcomᚋcalyprᚋloomᚋgraphqlapiᚋmodelᚐDataframeRecipeExplainIndexLocationᚄ(ctx context.Context, sel ast.SelectionSet, v []*model.DataframeRecipeExplainIndexLocation) graphql.Marshaler { + ret := graphql.MarshalSliceConcurrently(ctx, len(v), 0, false, func(ctx context.Context, i int) graphql.Marshaler { + fc := graphql.GetFieldContext(ctx) + fc.Result = &v[i] + return ec.marshalNDataframeRecipeExplainIndexLocation2ᚖgithubᚗcomᚋcalyprᚋloomᚋgraphqlapiᚋmodelᚐDataframeRecipeExplainIndexLocation(ctx, sel, v[i]) + }) - for label, dfs := range deferred { - ec.processDeferredGroup(graphql.DeferredGroup{ - Label: label, - Path: graphql.GetPath(ctx), - FieldSet: dfs, - Context: ctx, - }) + for _, e := range ret { + if e == graphql.Null { + return graphql.Null + } } - return out + return ret } -// endregion **************************** object.gotpl **************************** +func (ec *executionContext) marshalNDataframeRecipeExplainIndexLocation2ᚖgithubᚗcomᚋcalyprᚋloomᚋgraphqlapiᚋmodelᚐDataframeRecipeExplainIndexLocation(ctx context.Context, sel ast.SelectionSet, v *model.DataframeRecipeExplainIndexLocation) graphql.Marshaler { + if v == nil { + if !graphql.HasFieldError(ctx, graphql.GetFieldContext(ctx)) { + graphql.AddErrorf(ctx, "the requested element is null which the schema does not allow") + } + return graphql.Null + } + return ec._DataframeRecipeExplainIndexLocation(ctx, sel, v) +} -// region ***************************** type.gotpl ***************************** +func (ec *executionContext) marshalNDataframeRecipeExplainIndexSummary2ᚕᚖgithubᚗcomᚋcalyprᚋloomᚋgraphqlapiᚋmodelᚐDataframeRecipeExplainIndexSummaryᚄ(ctx context.Context, sel ast.SelectionSet, v []*model.DataframeRecipeExplainIndexSummary) graphql.Marshaler { + ret := graphql.MarshalSliceConcurrently(ctx, len(v), 0, false, func(ctx context.Context, i int) graphql.Marshaler { + fc := graphql.GetFieldContext(ctx) + fc.Result = &v[i] + return ec.marshalNDataframeRecipeExplainIndexSummary2ᚖgithubᚗcomᚋcalyprᚋloomᚋgraphqlapiᚋmodelᚐDataframeRecipeExplainIndexSummary(ctx, sel, v[i]) + }) -func (ec *executionContext) unmarshalNBoolean2bool(ctx context.Context, v any) (bool, error) { - res, err := graphql.UnmarshalBoolean(v) - return res, graphql.ErrorOnPath(ctx, err) + for _, e := range ret { + if e == graphql.Null { + return graphql.Null + } + } + + return ret } -func (ec *executionContext) marshalNBoolean2bool(ctx context.Context, sel ast.SelectionSet, v bool) graphql.Marshaler { - res := graphql.MarshalBoolean(v) - if res == graphql.Null { +func (ec *executionContext) marshalNDataframeRecipeExplainIndexSummary2ᚖgithubᚗcomᚋcalyprᚋloomᚋgraphqlapiᚋmodelᚐDataframeRecipeExplainIndexSummary(ctx context.Context, sel ast.SelectionSet, v *model.DataframeRecipeExplainIndexSummary) graphql.Marshaler { + if v == nil { if !graphql.HasFieldError(ctx, graphql.GetFieldContext(ctx)) { - ec.Errorf(ctx, "the requested element is null which the schema does not allow") + graphql.AddErrorf(ctx, "the requested element is null which the schema does not allow") } + return graphql.Null } - return res + return ec._DataframeRecipeExplainIndexSummary(ctx, sel, v) } -func (ec *executionContext) unmarshalNDataframeAggregateInput2githubᚗcomᚋcalyprᚋloomᚋgraphqlapiᚋmodelᚐDataframeAggregateInput(ctx context.Context, v any) (model.DataframeAggregateInput, error) { - res, err := ec.unmarshalInputDataframeAggregateInput(ctx, v) - return res, graphql.ErrorOnPath(ctx, err) -} +func (ec *executionContext) marshalNDataframeRecipeExplainPlanEstimate2ᚕᚖgithubᚗcomᚋcalyprᚋloomᚋgraphqlapiᚋmodelᚐDataframeRecipeExplainPlanEstimateᚄ(ctx context.Context, sel ast.SelectionSet, v []*model.DataframeRecipeExplainPlanEstimate) graphql.Marshaler { + ret := graphql.MarshalSliceConcurrently(ctx, len(v), 0, false, func(ctx context.Context, i int) graphql.Marshaler { + fc := graphql.GetFieldContext(ctx) + fc.Result = &v[i] + return ec.marshalNDataframeRecipeExplainPlanEstimate2ᚖgithubᚗcomᚋcalyprᚋloomᚋgraphqlapiᚋmodelᚐDataframeRecipeExplainPlanEstimate(ctx, sel, v[i]) + }) -func (ec *executionContext) marshalNDataframeAggregateResult2githubᚗcomᚋcalyprᚋloomᚋgraphqlapiᚋmodelᚐDataframeAggregateResult(ctx context.Context, sel ast.SelectionSet, v model.DataframeAggregateResult) graphql.Marshaler { - return ec._DataframeAggregateResult(ctx, sel, &v) + for _, e := range ret { + if e == graphql.Null { + return graphql.Null + } + } + + return ret } -func (ec *executionContext) marshalNDataframeAggregateResult2ᚖgithubᚗcomᚋcalyprᚋloomᚋgraphqlapiᚋmodelᚐDataframeAggregateResult(ctx context.Context, sel ast.SelectionSet, v *model.DataframeAggregateResult) graphql.Marshaler { +func (ec *executionContext) marshalNDataframeRecipeExplainPlanEstimate2ᚖgithubᚗcomᚋcalyprᚋloomᚋgraphqlapiᚋmodelᚐDataframeRecipeExplainPlanEstimate(ctx context.Context, sel ast.SelectionSet, v *model.DataframeRecipeExplainPlanEstimate) graphql.Marshaler { if v == nil { if !graphql.HasFieldError(ctx, graphql.GetFieldContext(ctx)) { - ec.Errorf(ctx, "the requested element is null which the schema does not allow") + graphql.AddErrorf(ctx, "the requested element is null which the schema does not allow") } return graphql.Null } - return ec._DataframeAggregateResult(ctx, sel, v) + return ec._DataframeRecipeExplainPlanEstimate(ctx, sel, v) } -func (ec *executionContext) marshalNDataframeBuilderIntrospection2githubᚗcomᚋcalyprᚋloomᚋgraphqlapiᚋmodelᚐDataframeBuilderIntrospection(ctx context.Context, sel ast.SelectionSet, v model.DataframeBuilderIntrospection) graphql.Marshaler { - return ec._DataframeBuilderIntrospection(ctx, sel, &v) +func (ec *executionContext) marshalNDataframeRecipeExplainWarning2ᚕᚖgithubᚗcomᚋcalyprᚋloomᚋgraphqlapiᚋmodelᚐDataframeRecipeExplainWarningᚄ(ctx context.Context, sel ast.SelectionSet, v []*model.DataframeRecipeExplainWarning) graphql.Marshaler { + ret := graphql.MarshalSliceConcurrently(ctx, len(v), 0, false, func(ctx context.Context, i int) graphql.Marshaler { + fc := graphql.GetFieldContext(ctx) + fc.Result = &v[i] + return ec.marshalNDataframeRecipeExplainWarning2ᚖgithubᚗcomᚋcalyprᚋloomᚋgraphqlapiᚋmodelᚐDataframeRecipeExplainWarning(ctx, sel, v[i]) + }) + + for _, e := range ret { + if e == graphql.Null { + return graphql.Null + } + } + + return ret } -func (ec *executionContext) marshalNDataframeBuilderIntrospection2ᚖgithubᚗcomᚋcalyprᚋloomᚋgraphqlapiᚋmodelᚐDataframeBuilderIntrospection(ctx context.Context, sel ast.SelectionSet, v *model.DataframeBuilderIntrospection) graphql.Marshaler { +func (ec *executionContext) marshalNDataframeRecipeExplainWarning2ᚖgithubᚗcomᚋcalyprᚋloomᚋgraphqlapiᚋmodelᚐDataframeRecipeExplainWarning(ctx context.Context, sel ast.SelectionSet, v *model.DataframeRecipeExplainWarning) graphql.Marshaler { if v == nil { if !graphql.HasFieldError(ctx, graphql.GetFieldContext(ctx)) { - ec.Errorf(ctx, "the requested element is null which the schema does not allow") + graphql.AddErrorf(ctx, "the requested element is null which the schema does not allow") } return graphql.Null } - return ec._DataframeBuilderIntrospection(ctx, sel, v) + return ec._DataframeRecipeExplainWarning(ctx, sel, v) } -func (ec *executionContext) unmarshalNDataframeBuilderIntrospectionInput2githubᚗcomᚋcalyprᚋloomᚋgraphqlapiᚋmodelᚐDataframeBuilderIntrospectionInput(ctx context.Context, v any) (model.DataframeBuilderIntrospectionInput, error) { - res, err := ec.unmarshalInputDataframeBuilderIntrospectionInput(ctx, v) - return res, graphql.ErrorOnPath(ctx, err) +func (ec *executionContext) marshalNDataframeRecipeExplanation2githubᚗcomᚋcalyprᚋloomᚋgraphqlapiᚋmodelᚐDataframeRecipeExplanation(ctx context.Context, sel ast.SelectionSet, v model.DataframeRecipeExplanation) graphql.Marshaler { + return ec._DataframeRecipeExplanation(ctx, sel, &v) } -func (ec *executionContext) marshalNDataframeColumn2ᚕᚖgithubᚗcomᚋcalyprᚋloomᚋgraphqlapiᚋmodelᚐDataframeColumnᚄ(ctx context.Context, sel ast.SelectionSet, v []*model.DataframeColumn) graphql.Marshaler { - ret := make(graphql.Array, len(v)) - var wg sync.WaitGroup - isLen1 := len(v) == 1 - if !isLen1 { - wg.Add(len(v)) - } - for i := range v { - i := i - fc := &graphql.FieldContext{ - Index: &i, - Result: &v[i], - } - ctx := graphql.WithFieldContext(ctx, fc) - f := func(i int) { - defer func() { - if r := recover(); r != nil { - ec.Error(ctx, ec.Recover(ctx, r)) - ret = nil - } - }() - if !isLen1 { - defer wg.Done() - } - ret[i] = ec.marshalNDataframeColumn2ᚖgithubᚗcomᚋcalyprᚋloomᚋgraphqlapiᚋmodelᚐDataframeColumn(ctx, sel, v[i]) +func (ec *executionContext) marshalNDataframeRecipeExplanation2ᚖgithubᚗcomᚋcalyprᚋloomᚋgraphqlapiᚋmodelᚐDataframeRecipeExplanation(ctx context.Context, sel ast.SelectionSet, v *model.DataframeRecipeExplanation) graphql.Marshaler { + if v == nil { + if !graphql.HasFieldError(ctx, graphql.GetFieldContext(ctx)) { + graphql.AddErrorf(ctx, "the requested element is null which the schema does not allow") } - if isLen1 { - f(i) - } else { - go f(i) + return graphql.Null + } + return ec._DataframeRecipeExplanation(ctx, sel, v) +} + +func (ec *executionContext) marshalNDataframeRecipeExpressionExplanation2ᚕᚖgithubᚗcomᚋcalyprᚋloomᚋgraphqlapiᚋmodelᚐDataframeRecipeExpressionExplanationᚄ(ctx context.Context, sel ast.SelectionSet, v []*model.DataframeRecipeExpressionExplanation) graphql.Marshaler { + ret := graphql.MarshalSliceConcurrently(ctx, len(v), 0, false, func(ctx context.Context, i int) graphql.Marshaler { + fc := graphql.GetFieldContext(ctx) + fc.Result = &v[i] + return ec.marshalNDataframeRecipeExpressionExplanation2ᚖgithubᚗcomᚋcalyprᚋloomᚋgraphqlapiᚋmodelᚐDataframeRecipeExpressionExplanation(ctx, sel, v[i]) + }) + + for _, e := range ret { + if e == graphql.Null { + return graphql.Null } + } + return ret +} + +func (ec *executionContext) marshalNDataframeRecipeExpressionExplanation2ᚖgithubᚗcomᚋcalyprᚋloomᚋgraphqlapiᚋmodelᚐDataframeRecipeExpressionExplanation(ctx context.Context, sel ast.SelectionSet, v *model.DataframeRecipeExpressionExplanation) graphql.Marshaler { + if v == nil { + if !graphql.HasFieldError(ctx, graphql.GetFieldContext(ctx)) { + graphql.AddErrorf(ctx, "the requested element is null which the schema does not allow") + } + return graphql.Null } - wg.Wait() + return ec._DataframeRecipeExpressionExplanation(ctx, sel, v) +} + +func (ec *executionContext) marshalNDataframeRecipeOptimizationDecision2ᚕᚖgithubᚗcomᚋcalyprᚋloomᚋgraphqlapiᚋmodelᚐDataframeRecipeOptimizationDecisionᚄ(ctx context.Context, sel ast.SelectionSet, v []*model.DataframeRecipeOptimizationDecision) graphql.Marshaler { + ret := graphql.MarshalSliceConcurrently(ctx, len(v), 0, false, func(ctx context.Context, i int) graphql.Marshaler { + fc := graphql.GetFieldContext(ctx) + fc.Result = &v[i] + return ec.marshalNDataframeRecipeOptimizationDecision2ᚖgithubᚗcomᚋcalyprᚋloomᚋgraphqlapiᚋmodelᚐDataframeRecipeOptimizationDecision(ctx, sel, v[i]) + }) for _, e := range ret { if e == graphql.Null { @@ -11057,60 +15897,58 @@ func (ec *executionContext) marshalNDataframeColumn2ᚕᚖgithubᚗcomᚋcalypr return ret } -func (ec *executionContext) marshalNDataframeColumn2ᚖgithubᚗcomᚋcalyprᚋloomᚋgraphqlapiᚋmodelᚐDataframeColumn(ctx context.Context, sel ast.SelectionSet, v *model.DataframeColumn) graphql.Marshaler { +func (ec *executionContext) marshalNDataframeRecipeOptimizationDecision2ᚖgithubᚗcomᚋcalyprᚋloomᚋgraphqlapiᚋmodelᚐDataframeRecipeOptimizationDecision(ctx context.Context, sel ast.SelectionSet, v *model.DataframeRecipeOptimizationDecision) graphql.Marshaler { if v == nil { if !graphql.HasFieldError(ctx, graphql.GetFieldContext(ctx)) { - ec.Errorf(ctx, "the requested element is null which the schema does not allow") + graphql.AddErrorf(ctx, "the requested element is null which the schema does not allow") } return graphql.Null } - return ec._DataframeColumn(ctx, sel, v) + return ec._DataframeRecipeOptimizationDecision(ctx, sel, v) } -func (ec *executionContext) marshalNDataframeCompilerPlanDiagnostics2ᚖgithubᚗcomᚋcalyprᚋloomᚋgraphqlapiᚋmodelᚐDataframeCompilerPlanDiagnostics(ctx context.Context, sel ast.SelectionSet, v *model.DataframeCompilerPlanDiagnostics) graphql.Marshaler { +func (ec *executionContext) marshalNDataframeRecipeOptimizationExplanation2ᚖgithubᚗcomᚋcalyprᚋloomᚋgraphqlapiᚋmodelᚐDataframeRecipeOptimizationExplanation(ctx context.Context, sel ast.SelectionSet, v *model.DataframeRecipeOptimizationExplanation) graphql.Marshaler { if v == nil { if !graphql.HasFieldError(ctx, graphql.GetFieldContext(ctx)) { - ec.Errorf(ctx, "the requested element is null which the schema does not allow") + graphql.AddErrorf(ctx, "the requested element is null which the schema does not allow") } return graphql.Null } - return ec._DataframeCompilerPlanDiagnostics(ctx, sel, v) + return ec._DataframeRecipeOptimizationExplanation(ctx, sel, v) } -func (ec *executionContext) marshalNDataframeFieldHint2ᚕᚖgithubᚗcomᚋcalyprᚋloomᚋgraphqlapiᚋmodelᚐDataframeFieldHintᚄ(ctx context.Context, sel ast.SelectionSet, v []*model.DataframeFieldHint) graphql.Marshaler { - ret := make(graphql.Array, len(v)) - var wg sync.WaitGroup - isLen1 := len(v) == 1 - if !isLen1 { - wg.Add(len(v)) - } - for i := range v { - i := i - fc := &graphql.FieldContext{ - Index: &i, - Result: &v[i], - } - ctx := graphql.WithFieldContext(ctx, fc) - f := func(i int) { - defer func() { - if r := recover(); r != nil { - ec.Error(ctx, ec.Recover(ctx, r)) - ret = nil - } - }() - if !isLen1 { - defer wg.Done() - } - ret[i] = ec.marshalNDataframeFieldHint2ᚖgithubᚗcomᚋcalyprᚋloomᚋgraphqlapiᚋmodelᚐDataframeFieldHint(ctx, sel, v[i]) - } - if isLen1 { - f(i) - } else { - go f(i) +func (ec *executionContext) marshalNDataframeRecipeOptimizationRuleState2ᚕᚖgithubᚗcomᚋcalyprᚋloomᚋgraphqlapiᚋmodelᚐDataframeRecipeOptimizationRuleStateᚄ(ctx context.Context, sel ast.SelectionSet, v []*model.DataframeRecipeOptimizationRuleState) graphql.Marshaler { + ret := graphql.MarshalSliceConcurrently(ctx, len(v), 0, false, func(ctx context.Context, i int) graphql.Marshaler { + fc := graphql.GetFieldContext(ctx) + fc.Result = &v[i] + return ec.marshalNDataframeRecipeOptimizationRuleState2ᚖgithubᚗcomᚋcalyprᚋloomᚋgraphqlapiᚋmodelᚐDataframeRecipeOptimizationRuleState(ctx, sel, v[i]) + }) + + for _, e := range ret { + if e == graphql.Null { + return graphql.Null } + } + + return ret +} +func (ec *executionContext) marshalNDataframeRecipeOptimizationRuleState2ᚖgithubᚗcomᚋcalyprᚋloomᚋgraphqlapiᚋmodelᚐDataframeRecipeOptimizationRuleState(ctx context.Context, sel ast.SelectionSet, v *model.DataframeRecipeOptimizationRuleState) graphql.Marshaler { + if v == nil { + if !graphql.HasFieldError(ctx, graphql.GetFieldContext(ctx)) { + graphql.AddErrorf(ctx, "the requested element is null which the schema does not allow") + } + return graphql.Null } - wg.Wait() + return ec._DataframeRecipeOptimizationRuleState(ctx, sel, v) +} + +func (ec *executionContext) marshalNDataframeRecipeOutputExplanation2ᚕᚖgithubᚗcomᚋcalyprᚋloomᚋgraphqlapiᚋmodelᚐDataframeRecipeOutputExplanationᚄ(ctx context.Context, sel ast.SelectionSet, v []*model.DataframeRecipeOutputExplanation) graphql.Marshaler { + ret := graphql.MarshalSliceConcurrently(ctx, len(v), 0, false, func(ctx context.Context, i int) graphql.Marshaler { + fc := graphql.GetFieldContext(ctx) + fc.Result = &v[i] + return ec.marshalNDataframeRecipeOutputExplanation2ᚖgithubᚗcomᚋcalyprᚋloomᚋgraphqlapiᚋmodelᚐDataframeRecipeOutputExplanation(ctx, sel, v[i]) + }) for _, e := range ret { if e == graphql.Null { @@ -11121,85 +15959,102 @@ func (ec *executionContext) marshalNDataframeFieldHint2ᚕᚖgithubᚗcomᚋcaly return ret } -func (ec *executionContext) marshalNDataframeFieldHint2ᚖgithubᚗcomᚋcalyprᚋloomᚋgraphqlapiᚋmodelᚐDataframeFieldHint(ctx context.Context, sel ast.SelectionSet, v *model.DataframeFieldHint) graphql.Marshaler { +func (ec *executionContext) marshalNDataframeRecipeOutputExplanation2ᚖgithubᚗcomᚋcalyprᚋloomᚋgraphqlapiᚋmodelᚐDataframeRecipeOutputExplanation(ctx context.Context, sel ast.SelectionSet, v *model.DataframeRecipeOutputExplanation) graphql.Marshaler { if v == nil { if !graphql.HasFieldError(ctx, graphql.GetFieldContext(ctx)) { - ec.Errorf(ctx, "the requested element is null which the schema does not allow") + graphql.AddErrorf(ctx, "the requested element is null which the schema does not allow") } return graphql.Null } - return ec._DataframeFieldHint(ctx, sel, v) + return ec._DataframeRecipeOutputExplanation(ctx, sel, v) } -func (ec *executionContext) marshalNDataframeFieldSelector2ᚖgithubᚗcomᚋcalyprᚋloomᚋgraphqlapiᚋmodelᚐDataframeFieldSelector(ctx context.Context, sel ast.SelectionSet, v *model.DataframeFieldSelector) graphql.Marshaler { +func (ec *executionContext) marshalNDataframeRecipeOutputValidation2ᚕᚖgithubᚗcomᚋcalyprᚋloomᚋgraphqlapiᚋmodelᚐDataframeRecipeOutputValidationᚄ(ctx context.Context, sel ast.SelectionSet, v []*model.DataframeRecipeOutputValidation) graphql.Marshaler { + ret := graphql.MarshalSliceConcurrently(ctx, len(v), 0, false, func(ctx context.Context, i int) graphql.Marshaler { + fc := graphql.GetFieldContext(ctx) + fc.Result = &v[i] + return ec.marshalNDataframeRecipeOutputValidation2ᚖgithubᚗcomᚋcalyprᚋloomᚋgraphqlapiᚋmodelᚐDataframeRecipeOutputValidation(ctx, sel, v[i]) + }) + + for _, e := range ret { + if e == graphql.Null { + return graphql.Null + } + } + + return ret +} + +func (ec *executionContext) marshalNDataframeRecipeOutputValidation2ᚖgithubᚗcomᚋcalyprᚋloomᚋgraphqlapiᚋmodelᚐDataframeRecipeOutputValidation(ctx context.Context, sel ast.SelectionSet, v *model.DataframeRecipeOutputValidation) graphql.Marshaler { if v == nil { if !graphql.HasFieldError(ctx, graphql.GetFieldContext(ctx)) { - ec.Errorf(ctx, "the requested element is null which the schema does not allow") + graphql.AddErrorf(ctx, "the requested element is null which the schema does not allow") } return graphql.Null } - return ec._DataframeFieldSelector(ctx, sel, v) + return ec._DataframeRecipeOutputValidation(ctx, sel, v) } -func (ec *executionContext) unmarshalNDataframeFilterInput2ᚖgithubᚗcomᚋcalyprᚋloomᚋgraphqlapiᚋmodelᚐDataframeFilterInput(ctx context.Context, v any) (*model.DataframeFilterInput, error) { - res, err := ec.unmarshalInputDataframeFilterInput(ctx, v) - return &res, graphql.ErrorOnPath(ctx, err) +func (ec *executionContext) marshalNDataframeRecipePhysicalOutputExplanation2ᚕᚖgithubᚗcomᚋcalyprᚋloomᚋgraphqlapiᚋmodelᚐDataframeRecipePhysicalOutputExplanationᚄ(ctx context.Context, sel ast.SelectionSet, v []*model.DataframeRecipePhysicalOutputExplanation) graphql.Marshaler { + ret := graphql.MarshalSliceConcurrently(ctx, len(v), 0, false, func(ctx context.Context, i int) graphql.Marshaler { + fc := graphql.GetFieldContext(ctx) + fc.Result = &v[i] + return ec.marshalNDataframeRecipePhysicalOutputExplanation2ᚖgithubᚗcomᚋcalyprᚋloomᚋgraphqlapiᚋmodelᚐDataframeRecipePhysicalOutputExplanation(ctx, sel, v[i]) + }) + + for _, e := range ret { + if e == graphql.Null { + return graphql.Null + } + } + + return ret } -func (ec *executionContext) marshalNDataframeMaterialization2ᚖgithubᚗcomᚋcalyprᚋloomᚋgraphqlapiᚋmodelᚐDataframeMaterialization(ctx context.Context, sel ast.SelectionSet, v *model.DataframeMaterialization) graphql.Marshaler { +func (ec *executionContext) marshalNDataframeRecipePhysicalOutputExplanation2ᚖgithubᚗcomᚋcalyprᚋloomᚋgraphqlapiᚋmodelᚐDataframeRecipePhysicalOutputExplanation(ctx context.Context, sel ast.SelectionSet, v *model.DataframeRecipePhysicalOutputExplanation) graphql.Marshaler { if v == nil { if !graphql.HasFieldError(ctx, graphql.GetFieldContext(ctx)) { - ec.Errorf(ctx, "the requested element is null which the schema does not allow") + graphql.AddErrorf(ctx, "the requested element is null which the schema does not allow") } return graphql.Null } - return ec._DataframeMaterialization(ctx, sel, v) + return ec._DataframeRecipePhysicalOutputExplanation(ctx, sel, v) } -func (ec *executionContext) unmarshalNDataframeMaterializationState2githubᚗcomᚋcalyprᚋloomᚋgraphqlapiᚋmodelᚐDataframeMaterializationState(ctx context.Context, v any) (model.DataframeMaterializationState, error) { - var res model.DataframeMaterializationState - err := res.UnmarshalGQL(v) - return res, graphql.ErrorOnPath(ctx, err) +func (ec *executionContext) marshalNDataframeRecipePreflight2githubᚗcomᚋcalyprᚋloomᚋgraphqlapiᚋmodelᚐDataframeRecipePreflight(ctx context.Context, sel ast.SelectionSet, v model.DataframeRecipePreflight) graphql.Marshaler { + return ec._DataframeRecipePreflight(ctx, sel, &v) } -func (ec *executionContext) marshalNDataframeMaterializationState2githubᚗcomᚋcalyprᚋloomᚋgraphqlapiᚋmodelᚐDataframeMaterializationState(ctx context.Context, sel ast.SelectionSet, v model.DataframeMaterializationState) graphql.Marshaler { - return v +func (ec *executionContext) marshalNDataframeRecipePreflight2ᚖgithubᚗcomᚋcalyprᚋloomᚋgraphqlapiᚋmodelᚐDataframeRecipePreflight(ctx context.Context, sel ast.SelectionSet, v *model.DataframeRecipePreflight) graphql.Marshaler { + if v == nil { + if !graphql.HasFieldError(ctx, graphql.GetFieldContext(ctx)) { + graphql.AddErrorf(ctx, "the requested element is null which the schema does not allow") + } + return graphql.Null + } + return ec._DataframeRecipePreflight(ctx, sel, v) } -func (ec *executionContext) marshalNDataframeOptimizationDecision2ᚕᚖgithubᚗcomᚋcalyprᚋloomᚋgraphqlapiᚋmodelᚐDataframeOptimizationDecisionᚄ(ctx context.Context, sel ast.SelectionSet, v []*model.DataframeOptimizationDecision) graphql.Marshaler { - ret := make(graphql.Array, len(v)) - var wg sync.WaitGroup - isLen1 := len(v) == 1 - if !isLen1 { - wg.Add(len(v)) - } - for i := range v { - i := i - fc := &graphql.FieldContext{ - Index: &i, - Result: &v[i], - } - ctx := graphql.WithFieldContext(ctx, fc) - f := func(i int) { - defer func() { - if r := recover(); r != nil { - ec.Error(ctx, ec.Recover(ctx, r)) - ret = nil - } - }() - if !isLen1 { - defer wg.Done() - } - ret[i] = ec.marshalNDataframeOptimizationDecision2ᚖgithubᚗcomᚋcalyprᚋloomᚋgraphqlapiᚋmodelᚐDataframeOptimizationDecision(ctx, sel, v[i]) - } - if isLen1 { - f(i) - } else { - go f(i) - } +func (ec *executionContext) marshalNDataframeRecipePreview2githubᚗcomᚋcalyprᚋloomᚋgraphqlapiᚋmodelᚐDataframeRecipePreview(ctx context.Context, sel ast.SelectionSet, v model.DataframeRecipePreview) graphql.Marshaler { + return ec._DataframeRecipePreview(ctx, sel, &v) +} +func (ec *executionContext) marshalNDataframeRecipePreview2ᚖgithubᚗcomᚋcalyprᚋloomᚋgraphqlapiᚋmodelᚐDataframeRecipePreview(ctx context.Context, sel ast.SelectionSet, v *model.DataframeRecipePreview) graphql.Marshaler { + if v == nil { + if !graphql.HasFieldError(ctx, graphql.GetFieldContext(ctx)) { + graphql.AddErrorf(ctx, "the requested element is null which the schema does not allow") + } + return graphql.Null } - wg.Wait() + return ec._DataframeRecipePreview(ctx, sel, v) +} + +func (ec *executionContext) marshalNDataframeRecipePreviewOutput2ᚕᚖgithubᚗcomᚋcalyprᚋloomᚋgraphqlapiᚋmodelᚐDataframeRecipePreviewOutputᚄ(ctx context.Context, sel ast.SelectionSet, v []*model.DataframeRecipePreviewOutput) graphql.Marshaler { + ret := graphql.MarshalSliceConcurrently(ctx, len(v), 0, false, func(ctx context.Context, i int) graphql.Marshaler { + fc := graphql.GetFieldContext(ctx) + fc.Result = &v[i] + return ec.marshalNDataframeRecipePreviewOutput2ᚖgithubᚗcomᚋcalyprᚋloomᚋgraphqlapiᚋmodelᚐDataframeRecipePreviewOutput(ctx, sel, v[i]) + }) for _, e := range ret { if e == graphql.Null { @@ -11210,80 +16065,76 @@ func (ec *executionContext) marshalNDataframeOptimizationDecision2ᚕᚖgithub return ret } -func (ec *executionContext) marshalNDataframeOptimizationDecision2ᚖgithubᚗcomᚋcalyprᚋloomᚋgraphqlapiᚋmodelᚐDataframeOptimizationDecision(ctx context.Context, sel ast.SelectionSet, v *model.DataframeOptimizationDecision) graphql.Marshaler { +func (ec *executionContext) marshalNDataframeRecipePreviewOutput2ᚖgithubᚗcomᚋcalyprᚋloomᚋgraphqlapiᚋmodelᚐDataframeRecipePreviewOutput(ctx context.Context, sel ast.SelectionSet, v *model.DataframeRecipePreviewOutput) graphql.Marshaler { if v == nil { if !graphql.HasFieldError(ctx, graphql.GetFieldContext(ctx)) { - ec.Errorf(ctx, "the requested element is null which the schema does not allow") + graphql.AddErrorf(ctx, "the requested element is null which the schema does not allow") } return graphql.Null } - return ec._DataframeOptimizationDecision(ctx, sel, v) + return ec._DataframeRecipePreviewOutput(ctx, sel, v) } -func (ec *executionContext) marshalNDataframeOptimizationPolicy2ᚖgithubᚗcomᚋcalyprᚋloomᚋgraphqlapiᚋmodelᚐDataframeOptimizationPolicy(ctx context.Context, sel ast.SelectionSet, v *model.DataframeOptimizationPolicy) graphql.Marshaler { +func (ec *executionContext) marshalNDataframeRecipeResult2githubᚗcomᚋcalyprᚋloomᚋgraphqlapiᚋmodelᚐDataframeRecipeResult(ctx context.Context, sel ast.SelectionSet, v model.DataframeRecipeResult) graphql.Marshaler { + return ec._DataframeRecipeResult(ctx, sel, &v) +} + +func (ec *executionContext) marshalNDataframeRecipeResult2ᚖgithubᚗcomᚋcalyprᚋloomᚋgraphqlapiᚋmodelᚐDataframeRecipeResult(ctx context.Context, sel ast.SelectionSet, v *model.DataframeRecipeResult) graphql.Marshaler { if v == nil { if !graphql.HasFieldError(ctx, graphql.GetFieldContext(ctx)) { - ec.Errorf(ctx, "the requested element is null which the schema does not allow") + graphql.AddErrorf(ctx, "the requested element is null which the schema does not allow") } return graphql.Null } - return ec._DataframeOptimizationPolicy(ctx, sel, v) + return ec._DataframeRecipeResult(ctx, sel, v) } -func (ec *executionContext) marshalNDataframePageInfo2ᚖgithubᚗcomᚋcalyprᚋloomᚋgraphqlapiᚋmodelᚐDataframePageInfo(ctx context.Context, sel ast.SelectionSet, v *model.DataframePageInfo) graphql.Marshaler { +func (ec *executionContext) marshalNDataframeRecipeResultOutput2ᚕᚖgithubᚗcomᚋcalyprᚋloomᚋgraphqlapiᚋmodelᚐDataframeRecipeResultOutputᚄ(ctx context.Context, sel ast.SelectionSet, v []*model.DataframeRecipeResultOutput) graphql.Marshaler { + ret := graphql.MarshalSliceConcurrently(ctx, len(v), 0, false, func(ctx context.Context, i int) graphql.Marshaler { + fc := graphql.GetFieldContext(ctx) + fc.Result = &v[i] + return ec.marshalNDataframeRecipeResultOutput2ᚖgithubᚗcomᚋcalyprᚋloomᚋgraphqlapiᚋmodelᚐDataframeRecipeResultOutput(ctx, sel, v[i]) + }) + + for _, e := range ret { + if e == graphql.Null { + return graphql.Null + } + } + + return ret +} + +func (ec *executionContext) marshalNDataframeRecipeResultOutput2ᚖgithubᚗcomᚋcalyprᚋloomᚋgraphqlapiᚋmodelᚐDataframeRecipeResultOutput(ctx context.Context, sel ast.SelectionSet, v *model.DataframeRecipeResultOutput) graphql.Marshaler { if v == nil { if !graphql.HasFieldError(ctx, graphql.GetFieldContext(ctx)) { - ec.Errorf(ctx, "the requested element is null which the schema does not allow") + graphql.AddErrorf(ctx, "the requested element is null which the schema does not allow") } return graphql.Null } - return ec._DataframePageInfo(ctx, sel, v) + return ec._DataframeRecipeResultOutput(ctx, sel, v) } -func (ec *executionContext) marshalNDataframeQueryDiagnostics2ᚖgithubᚗcomᚋcalyprᚋloomᚋgraphqlapiᚋmodelᚐDataframeQueryDiagnostics(ctx context.Context, sel ast.SelectionSet, v *model.DataframeQueryDiagnostics) graphql.Marshaler { +func (ec *executionContext) marshalNDataframeRecipeValidation2githubᚗcomᚋcalyprᚋloomᚋgraphqlapiᚋmodelᚐDataframeRecipeValidation(ctx context.Context, sel ast.SelectionSet, v model.DataframeRecipeValidation) graphql.Marshaler { + return ec._DataframeRecipeValidation(ctx, sel, &v) +} + +func (ec *executionContext) marshalNDataframeRecipeValidation2ᚖgithubᚗcomᚋcalyprᚋloomᚋgraphqlapiᚋmodelᚐDataframeRecipeValidation(ctx context.Context, sel ast.SelectionSet, v *model.DataframeRecipeValidation) graphql.Marshaler { if v == nil { if !graphql.HasFieldError(ctx, graphql.GetFieldContext(ctx)) { - ec.Errorf(ctx, "the requested element is null which the schema does not allow") + graphql.AddErrorf(ctx, "the requested element is null which the schema does not allow") } return graphql.Null } - return ec._DataframeQueryDiagnostics(ctx, sel, v) + return ec._DataframeRecipeValidation(ctx, sel, v) } func (ec *executionContext) marshalNDataframeRelatedResourceHints2ᚕᚖgithubᚗcomᚋcalyprᚋloomᚋgraphqlapiᚋmodelᚐDataframeRelatedResourceHintsᚄ(ctx context.Context, sel ast.SelectionSet, v []*model.DataframeRelatedResourceHints) graphql.Marshaler { - ret := make(graphql.Array, len(v)) - var wg sync.WaitGroup - isLen1 := len(v) == 1 - if !isLen1 { - wg.Add(len(v)) - } - for i := range v { - i := i - fc := &graphql.FieldContext{ - Index: &i, - Result: &v[i], - } - ctx := graphql.WithFieldContext(ctx, fc) - f := func(i int) { - defer func() { - if r := recover(); r != nil { - ec.Error(ctx, ec.Recover(ctx, r)) - ret = nil - } - }() - if !isLen1 { - defer wg.Done() - } - ret[i] = ec.marshalNDataframeRelatedResourceHints2ᚖgithubᚗcomᚋcalyprᚋloomᚋgraphqlapiᚋmodelᚐDataframeRelatedResourceHints(ctx, sel, v[i]) - } - if isLen1 { - f(i) - } else { - go f(i) - } - - } - wg.Wait() + ret := graphql.MarshalSliceConcurrently(ctx, len(v), 0, false, func(ctx context.Context, i int) graphql.Marshaler { + fc := graphql.GetFieldContext(ctx) + fc.Result = &v[i] + return ec.marshalNDataframeRelatedResourceHints2ᚖgithubᚗcomᚋcalyprᚋloomᚋgraphqlapiᚋmodelᚐDataframeRelatedResourceHints(ctx, sel, v[i]) + }) for _, e := range ret { if e == graphql.Null { @@ -11297,7 +16148,7 @@ func (ec *executionContext) marshalNDataframeRelatedResourceHints2ᚕᚖgithub func (ec *executionContext) marshalNDataframeRelatedResourceHints2ᚖgithubᚗcomᚋcalyprᚋloomᚋgraphqlapiᚋmodelᚐDataframeRelatedResourceHints(ctx context.Context, sel ast.SelectionSet, v *model.DataframeRelatedResourceHints) graphql.Marshaler { if v == nil { if !graphql.HasFieldError(ctx, graphql.GetFieldContext(ctx)) { - ec.Errorf(ctx, "the requested element is null which the schema does not allow") + graphql.AddErrorf(ctx, "the requested element is null which the schema does not allow") } return graphql.Null } @@ -11307,7 +16158,7 @@ func (ec *executionContext) marshalNDataframeRelatedResourceHints2ᚖgithubᚗco func (ec *executionContext) marshalNDataframeResourceHints2ᚖgithubᚗcomᚋcalyprᚋloomᚋgraphqlapiᚋmodelᚐDataframeResourceHints(ctx context.Context, sel ast.SelectionSet, v *model.DataframeResourceHints) graphql.Marshaler { if v == nil { if !graphql.HasFieldError(ctx, graphql.GetFieldContext(ctx)) { - ec.Errorf(ctx, "the requested element is null which the schema does not allow") + graphql.AddErrorf(ctx, "the requested element is null which the schema does not allow") } return graphql.Null } @@ -11315,39 +16166,11 @@ func (ec *executionContext) marshalNDataframeResourceHints2ᚖgithubᚗcomᚋcal } func (ec *executionContext) marshalNDataframeRichSourceReuse2ᚕᚖgithubᚗcomᚋcalyprᚋloomᚋgraphqlapiᚋmodelᚐDataframeRichSourceReuseᚄ(ctx context.Context, sel ast.SelectionSet, v []*model.DataframeRichSourceReuse) graphql.Marshaler { - ret := make(graphql.Array, len(v)) - var wg sync.WaitGroup - isLen1 := len(v) == 1 - if !isLen1 { - wg.Add(len(v)) - } - for i := range v { - i := i - fc := &graphql.FieldContext{ - Index: &i, - Result: &v[i], - } - ctx := graphql.WithFieldContext(ctx, fc) - f := func(i int) { - defer func() { - if r := recover(); r != nil { - ec.Error(ctx, ec.Recover(ctx, r)) - ret = nil - } - }() - if !isLen1 { - defer wg.Done() - } - ret[i] = ec.marshalNDataframeRichSourceReuse2ᚖgithubᚗcomᚋcalyprᚋloomᚋgraphqlapiᚋmodelᚐDataframeRichSourceReuse(ctx, sel, v[i]) - } - if isLen1 { - f(i) - } else { - go f(i) - } - - } - wg.Wait() + ret := graphql.MarshalSliceConcurrently(ctx, len(v), 0, false, func(ctx context.Context, i int) graphql.Marshaler { + fc := graphql.GetFieldContext(ctx) + fc.Result = &v[i] + return ec.marshalNDataframeRichSourceReuse2ᚖgithubᚗcomᚋcalyprᚋloomᚋgraphqlapiᚋmodelᚐDataframeRichSourceReuse(ctx, sel, v[i]) + }) for _, e := range ret { if e == graphql.Null { @@ -11361,7 +16184,7 @@ func (ec *executionContext) marshalNDataframeRichSourceReuse2ᚕᚖgithubᚗcom func (ec *executionContext) marshalNDataframeRichSourceReuse2ᚖgithubᚗcomᚋcalyprᚋloomᚋgraphqlapiᚋmodelᚐDataframeRichSourceReuse(ctx context.Context, sel ast.SelectionSet, v *model.DataframeRichSourceReuse) graphql.Marshaler { if v == nil { if !graphql.HasFieldError(ctx, graphql.GetFieldContext(ctx)) { - ec.Errorf(ctx, "the requested element is null which the schema does not allow") + graphql.AddErrorf(ctx, "the requested element is null which the schema does not allow") } return graphql.Null } @@ -11375,7 +16198,7 @@ func (ec *executionContext) marshalNDataframeRowConnection2githubᚗcomᚋcalypr func (ec *executionContext) marshalNDataframeRowConnection2ᚖgithubᚗcomᚋcalyprᚋloomᚋgraphqlapiᚋmodelᚐDataframeRowConnection(ctx context.Context, sel ast.SelectionSet, v *model.DataframeRowConnection) graphql.Marshaler { if v == nil { if !graphql.HasFieldError(ctx, graphql.GetFieldContext(ctx)) { - ec.Errorf(ctx, "the requested element is null which the schema does not allow") + graphql.AddErrorf(ctx, "the requested element is null which the schema does not allow") } return graphql.Null } @@ -11388,39 +16211,11 @@ func (ec *executionContext) unmarshalNDataframeRowsInput2githubᚗcomᚋcalypr } func (ec *executionContext) marshalNDataframeTraversalHint2ᚕᚖgithubᚗcomᚋcalyprᚋloomᚋgraphqlapiᚋmodelᚐDataframeTraversalHintᚄ(ctx context.Context, sel ast.SelectionSet, v []*model.DataframeTraversalHint) graphql.Marshaler { - ret := make(graphql.Array, len(v)) - var wg sync.WaitGroup - isLen1 := len(v) == 1 - if !isLen1 { - wg.Add(len(v)) - } - for i := range v { - i := i - fc := &graphql.FieldContext{ - Index: &i, - Result: &v[i], - } - ctx := graphql.WithFieldContext(ctx, fc) - f := func(i int) { - defer func() { - if r := recover(); r != nil { - ec.Error(ctx, ec.Recover(ctx, r)) - ret = nil - } - }() - if !isLen1 { - defer wg.Done() - } - ret[i] = ec.marshalNDataframeTraversalHint2ᚖgithubᚗcomᚋcalyprᚋloomᚋgraphqlapiᚋmodelᚐDataframeTraversalHint(ctx, sel, v[i]) - } - if isLen1 { - f(i) - } else { - go f(i) - } - - } - wg.Wait() + ret := graphql.MarshalSliceConcurrently(ctx, len(v), 0, false, func(ctx context.Context, i int) graphql.Marshaler { + fc := graphql.GetFieldContext(ctx) + fc.Result = &v[i] + return ec.marshalNDataframeTraversalHint2ᚖgithubᚗcomᚋcalyprᚋloomᚋgraphqlapiᚋmodelᚐDataframeTraversalHint(ctx, sel, v[i]) + }) for _, e := range ret { if e == graphql.Null { @@ -11434,25 +16229,45 @@ func (ec *executionContext) marshalNDataframeTraversalHint2ᚕᚖgithubᚗcomᚋ func (ec *executionContext) marshalNDataframeTraversalHint2ᚖgithubᚗcomᚋcalyprᚋloomᚋgraphqlapiᚋmodelᚐDataframeTraversalHint(ctx context.Context, sel ast.SelectionSet, v *model.DataframeTraversalHint) graphql.Marshaler { if v == nil { if !graphql.HasFieldError(ctx, graphql.GetFieldContext(ctx)) { - ec.Errorf(ctx, "the requested element is null which the schema does not allow") + graphql.AddErrorf(ctx, "the requested element is null which the schema does not allow") } return graphql.Null } return ec._DataframeTraversalHint(ctx, sel, v) } -func (ec *executionContext) unmarshalNFhirAggregateInput2ᚖgithubᚗcomᚋcalyprᚋloomᚋgraphqlapiᚋmodelᚐFhirAggregateInput(ctx context.Context, v any) (*model.FhirAggregateInput, error) { - res, err := ec.unmarshalInputFhirAggregateInput(ctx, v) +func (ec *executionContext) unmarshalNExplainDataframeRecipeInput2githubᚗcomᚋcalyprᚋloomᚋgraphqlapiᚋmodelᚐExplainDataframeRecipeInput(ctx context.Context, v any) (model.ExplainDataframeRecipeInput, error) { + res, err := ec.unmarshalInputExplainDataframeRecipeInput(ctx, v) + return res, graphql.ErrorOnPath(ctx, err) +} + +func (ec *executionContext) unmarshalNFhirAggregateInput2ᚖgithubᚗcomᚋcalyprᚋloomᚋgraphqlapiᚋmodelᚐFhirAggregateInput(ctx context.Context, v any) (*model.FhirAggregateInput, error) { + res, err := ec.unmarshalInputFhirAggregateInput(ctx, v) + return &res, graphql.ErrorOnPath(ctx, err) +} + +func (ec *executionContext) unmarshalNFhirAggregateOperation2githubᚗcomᚋcalyprᚋloomᚋgraphqlapiᚋmodelᚐFhirAggregateOperation(ctx context.Context, v any) (model.FhirAggregateOperation, error) { + var res model.FhirAggregateOperation + err := res.UnmarshalGQL(v) + return res, graphql.ErrorOnPath(ctx, err) +} + +func (ec *executionContext) marshalNFhirAggregateOperation2githubᚗcomᚋcalyprᚋloomᚋgraphqlapiᚋmodelᚐFhirAggregateOperation(ctx context.Context, sel ast.SelectionSet, v model.FhirAggregateOperation) graphql.Marshaler { + return v +} + +func (ec *executionContext) unmarshalNFhirCatalogProjectionInput2ᚖgithubᚗcomᚋcalyprᚋloomᚋgraphqlapiᚋmodelᚐFhirCatalogProjectionInput(ctx context.Context, v any) (*model.FhirCatalogProjectionInput, error) { + res, err := ec.unmarshalInputFhirCatalogProjectionInput(ctx, v) return &res, graphql.ErrorOnPath(ctx, err) } -func (ec *executionContext) unmarshalNFhirAggregateOperation2githubᚗcomᚋcalyprᚋloomᚋgraphqlapiᚋmodelᚐFhirAggregateOperation(ctx context.Context, v any) (model.FhirAggregateOperation, error) { - var res model.FhirAggregateOperation +func (ec *executionContext) unmarshalNFhirColumnNaming2githubᚗcomᚋcalyprᚋloomᚋgraphqlapiᚋmodelᚐFhirColumnNaming(ctx context.Context, v any) (model.FhirColumnNaming, error) { + var res model.FhirColumnNaming err := res.UnmarshalGQL(v) return res, graphql.ErrorOnPath(ctx, err) } -func (ec *executionContext) marshalNFhirAggregateOperation2githubᚗcomᚋcalyprᚋloomᚋgraphqlapiᚋmodelᚐFhirAggregateOperation(ctx context.Context, sel ast.SelectionSet, v model.FhirAggregateOperation) graphql.Marshaler { +func (ec *executionContext) marshalNFhirColumnNaming2githubᚗcomᚋcalyprᚋloomᚋgraphqlapiᚋmodelᚐFhirColumnNaming(ctx context.Context, sel ast.SelectionSet, v model.FhirColumnNaming) graphql.Marshaler { return v } @@ -11468,7 +16283,7 @@ func (ec *executionContext) marshalNFhirDataframeResult2githubᚗcomᚋcalyprᚋ func (ec *executionContext) marshalNFhirDataframeResult2ᚖgithubᚗcomᚋcalyprᚋloomᚋgraphqlapiᚋmodelᚐFhirDataframeResult(ctx context.Context, sel ast.SelectionSet, v *model.FhirDataframeResult) graphql.Marshaler { if v == nil { if !graphql.HasFieldError(ctx, graphql.GetFieldContext(ctx)) { - ec.Errorf(ctx, "the requested element is null which the schema does not allow") + graphql.AddErrorf(ctx, "the requested element is null which the schema does not allow") } return graphql.Null } @@ -11486,10 +16301,7 @@ func (ec *executionContext) marshalNFhirFieldPredicateOperation2githubᚗcomᚋc } func (ec *executionContext) unmarshalNFhirFieldSelectInput2ᚕᚖgithubᚗcomᚋcalyprᚋloomᚋgraphqlapiᚋmodelᚐFhirFieldSelectInputᚄ(ctx context.Context, v any) ([]*model.FhirFieldSelectInput, error) { - var vSlice []any - if v != nil { - vSlice = graphql.CoerceList(v) - } + vSlice := graphql.CoerceList(v) var err error res := make([]*model.FhirFieldSelectInput, len(vSlice)) for i := range vSlice { @@ -11508,10 +16320,7 @@ func (ec *executionContext) unmarshalNFhirFieldSelectInput2ᚖgithubᚗcomᚋcal } func (ec *executionContext) unmarshalNFhirFieldSelectorInput2ᚕᚖgithubᚗcomᚋcalyprᚋloomᚋgraphqlapiᚋmodelᚐFhirFieldSelectorInputᚄ(ctx context.Context, v any) ([]*model.FhirFieldSelectorInput, error) { - var vSlice []any - if v != nil { - vSlice = graphql.CoerceList(v) - } + vSlice := graphql.CoerceList(v) var err error res := make([]*model.FhirFieldSelectorInput, len(vSlice)) for i := range vSlice { @@ -11529,6 +16338,50 @@ func (ec *executionContext) unmarshalNFhirFieldSelectorInput2ᚖgithubᚗcomᚋc return &res, graphql.ErrorOnPath(ctx, err) } +func (ec *executionContext) unmarshalNFhirFilterInput2ᚖgithubᚗcomᚋcalyprᚋloomᚋgraphqlapiᚋmodelᚐFhirFilterInput(ctx context.Context, v any) (*model.FhirFilterInput, error) { + res, err := ec.unmarshalInputFhirFilterInput(ctx, v) + return &res, graphql.ErrorOnPath(ctx, err) +} + +func (ec *executionContext) unmarshalNFhirFilterOperator2githubᚗcomᚋcalyprᚋloomᚋgraphqlapiᚋmodelᚐFhirFilterOperator(ctx context.Context, v any) (model.FhirFilterOperator, error) { + var res model.FhirFilterOperator + err := res.UnmarshalGQL(v) + return res, graphql.ErrorOnPath(ctx, err) +} + +func (ec *executionContext) marshalNFhirFilterOperator2githubᚗcomᚋcalyprᚋloomᚋgraphqlapiᚋmodelᚐFhirFilterOperator(ctx context.Context, sel ast.SelectionSet, v model.FhirFilterOperator) graphql.Marshaler { + return v +} + +func (ec *executionContext) unmarshalNFhirFilterValueInput2ᚕᚖgithubᚗcomᚋcalyprᚋloomᚋgraphqlapiᚋmodelᚐFhirFilterValueInputᚄ(ctx context.Context, v any) ([]*model.FhirFilterValueInput, error) { + vSlice := graphql.CoerceList(v) + var err error + res := make([]*model.FhirFilterValueInput, len(vSlice)) + for i := range vSlice { + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithIndex(i)) + res[i], err = ec.unmarshalNFhirFilterValueInput2ᚖgithubᚗcomᚋcalyprᚋloomᚋgraphqlapiᚋmodelᚐFhirFilterValueInput(ctx, vSlice[i]) + if err != nil { + return nil, err + } + } + return res, nil +} + +func (ec *executionContext) unmarshalNFhirFilterValueInput2ᚖgithubᚗcomᚋcalyprᚋloomᚋgraphqlapiᚋmodelᚐFhirFilterValueInput(ctx context.Context, v any) (*model.FhirFilterValueInput, error) { + res, err := ec.unmarshalInputFhirFilterValueInput(ctx, v) + return &res, graphql.ErrorOnPath(ctx, err) +} + +func (ec *executionContext) unmarshalNFhirFilterValueKind2githubᚗcomᚋcalyprᚋloomᚋgraphqlapiᚋmodelᚐFhirFilterValueKind(ctx context.Context, v any) (model.FhirFilterValueKind, error) { + var res model.FhirFilterValueKind + err := res.UnmarshalGQL(v) + return res, graphql.ErrorOnPath(ctx, err) +} + +func (ec *executionContext) marshalNFhirFilterValueKind2githubᚗcomᚋcalyprᚋloomᚋgraphqlapiᚋmodelᚐFhirFilterValueKind(ctx context.Context, sel ast.SelectionSet, v model.FhirFilterValueKind) graphql.Marshaler { + return v +} + func (ec *executionContext) unmarshalNFhirPivotInput2ᚖgithubᚗcomᚋcalyprᚋloomᚋgraphqlapiᚋmodelᚐFhirPivotInput(ctx context.Context, v any) (*model.FhirPivotInput, error) { res, err := ec.unmarshalInputFhirPivotInput(ctx, v) return &res, graphql.ErrorOnPath(ctx, err) @@ -11560,10 +16413,11 @@ func (ec *executionContext) unmarshalNFloat2float64(ctx context.Context, v any) } func (ec *executionContext) marshalNFloat2float64(ctx context.Context, sel ast.SelectionSet, v float64) graphql.Marshaler { + _ = sel res := graphql.MarshalFloatContext(v) if res == graphql.Null { if !graphql.HasFieldError(ctx, graphql.GetFieldContext(ctx)) { - ec.Errorf(ctx, "the requested element is null which the schema does not allow") + graphql.AddErrorf(ctx, "the requested element is null which the schema does not allow") } } return graphql.WrapContextMarshaler(ctx, res) @@ -11575,10 +16429,11 @@ func (ec *executionContext) unmarshalNID2string(ctx context.Context, v any) (str } func (ec *executionContext) marshalNID2string(ctx context.Context, sel ast.SelectionSet, v string) graphql.Marshaler { + _ = sel res := graphql.MarshalID(v) if res == graphql.Null { if !graphql.HasFieldError(ctx, graphql.GetFieldContext(ctx)) { - ec.Errorf(ctx, "the requested element is null which the schema does not allow") + graphql.AddErrorf(ctx, "the requested element is null which the schema does not allow") } } return res @@ -11590,10 +16445,11 @@ func (ec *executionContext) unmarshalNInt2int(ctx context.Context, v any) (int, } func (ec *executionContext) marshalNInt2int(ctx context.Context, sel ast.SelectionSet, v int) graphql.Marshaler { + _ = sel res := graphql.MarshalInt(v) if res == graphql.Null { if !graphql.HasFieldError(ctx, graphql.GetFieldContext(ctx)) { - ec.Errorf(ctx, "the requested element is null which the schema does not allow") + graphql.AddErrorf(ctx, "the requested element is null which the schema does not allow") } } return res @@ -11607,33 +16463,51 @@ func (ec *executionContext) unmarshalNJSON2encodingᚋjsonᚐRawMessage(ctx cont func (ec *executionContext) marshalNJSON2encodingᚋjsonᚐRawMessage(ctx context.Context, sel ast.SelectionSet, v json.RawMessage) graphql.Marshaler { if v == nil { if !graphql.HasFieldError(ctx, graphql.GetFieldContext(ctx)) { - ec.Errorf(ctx, "the requested element is null which the schema does not allow") + graphql.AddErrorf(ctx, "the requested element is null which the schema does not allow") } return graphql.Null } return ec._JSON(ctx, sel, v) } +func (ec *executionContext) unmarshalNMaterializeDataframeRecipeInput2githubᚗcomᚋcalyprᚋloomᚋgraphqlapiᚋmodelᚐMaterializeDataframeRecipeInput(ctx context.Context, v any) (model.MaterializeDataframeRecipeInput, error) { + res, err := ec.unmarshalInputMaterializeDataframeRecipeInput(ctx, v) + return res, graphql.ErrorOnPath(ctx, err) +} + +func (ec *executionContext) unmarshalNPreflightDataframeRecipeInput2githubᚗcomᚋcalyprᚋloomᚋgraphqlapiᚋmodelᚐPreflightDataframeRecipeInput(ctx context.Context, v any) (model.PreflightDataframeRecipeInput, error) { + res, err := ec.unmarshalInputPreflightDataframeRecipeInput(ctx, v) + return res, graphql.ErrorOnPath(ctx, err) +} + +func (ec *executionContext) unmarshalNPreviewDataframeRecipeInput2githubᚗcomᚋcalyprᚋloomᚋgraphqlapiᚋmodelᚐPreviewDataframeRecipeInput(ctx context.Context, v any) (model.PreviewDataframeRecipeInput, error) { + res, err := ec.unmarshalInputPreviewDataframeRecipeInput(ctx, v) + return res, graphql.ErrorOnPath(ctx, err) +} + +func (ec *executionContext) unmarshalNRunDataframeRecipeInput2githubᚗcomᚋcalyprᚋloomᚋgraphqlapiᚋmodelᚐRunDataframeRecipeInput(ctx context.Context, v any) (model.RunDataframeRecipeInput, error) { + res, err := ec.unmarshalInputRunDataframeRecipeInput(ctx, v) + return res, graphql.ErrorOnPath(ctx, err) +} + func (ec *executionContext) unmarshalNString2string(ctx context.Context, v any) (string, error) { res, err := graphql.UnmarshalString(v) return res, graphql.ErrorOnPath(ctx, err) } func (ec *executionContext) marshalNString2string(ctx context.Context, sel ast.SelectionSet, v string) graphql.Marshaler { + _ = sel res := graphql.MarshalString(v) if res == graphql.Null { if !graphql.HasFieldError(ctx, graphql.GetFieldContext(ctx)) { - ec.Errorf(ctx, "the requested element is null which the schema does not allow") + graphql.AddErrorf(ctx, "the requested element is null which the schema does not allow") } } return res } func (ec *executionContext) unmarshalNString2ᚕstringᚄ(ctx context.Context, v any) ([]string, error) { - var vSlice []any - if v != nil { - vSlice = graphql.CoerceList(v) - } + vSlice := graphql.CoerceList(v) var err error res := make([]string, len(vSlice)) for i := range vSlice { @@ -11661,44 +16535,21 @@ func (ec *executionContext) marshalNString2ᚕstringᚄ(ctx context.Context, sel return ret } +func (ec *executionContext) unmarshalNValidateDataframeRecipeInput2githubᚗcomᚋcalyprᚋloomᚋgraphqlapiᚋmodelᚐValidateDataframeRecipeInput(ctx context.Context, v any) (model.ValidateDataframeRecipeInput, error) { + res, err := ec.unmarshalInputValidateDataframeRecipeInput(ctx, v) + return res, graphql.ErrorOnPath(ctx, err) +} + func (ec *executionContext) marshalN__Directive2githubᚗcomᚋ99designsᚋgqlgenᚋgraphqlᚋintrospectionᚐDirective(ctx context.Context, sel ast.SelectionSet, v introspection.Directive) graphql.Marshaler { return ec.___Directive(ctx, sel, &v) } func (ec *executionContext) marshalN__Directive2ᚕgithubᚗcomᚋ99designsᚋgqlgenᚋgraphqlᚋintrospectionᚐDirectiveᚄ(ctx context.Context, sel ast.SelectionSet, v []introspection.Directive) graphql.Marshaler { - ret := make(graphql.Array, len(v)) - var wg sync.WaitGroup - isLen1 := len(v) == 1 - if !isLen1 { - wg.Add(len(v)) - } - for i := range v { - i := i - fc := &graphql.FieldContext{ - Index: &i, - Result: &v[i], - } - ctx := graphql.WithFieldContext(ctx, fc) - f := func(i int) { - defer func() { - if r := recover(); r != nil { - ec.Error(ctx, ec.Recover(ctx, r)) - ret = nil - } - }() - if !isLen1 { - defer wg.Done() - } - ret[i] = ec.marshalN__Directive2githubᚗcomᚋ99designsᚋgqlgenᚋgraphqlᚋintrospectionᚐDirective(ctx, sel, v[i]) - } - if isLen1 { - f(i) - } else { - go f(i) - } - - } - wg.Wait() + ret := graphql.MarshalSliceConcurrently(ctx, len(v), 0, false, func(ctx context.Context, i int) graphql.Marshaler { + fc := graphql.GetFieldContext(ctx) + fc.Result = &v[i] + return ec.marshalN__Directive2githubᚗcomᚋ99designsᚋgqlgenᚋgraphqlᚋintrospectionᚐDirective(ctx, sel, v[i]) + }) for _, e := range ret { if e == graphql.Null { @@ -11715,20 +16566,18 @@ func (ec *executionContext) unmarshalN__DirectiveLocation2string(ctx context.Con } func (ec *executionContext) marshalN__DirectiveLocation2string(ctx context.Context, sel ast.SelectionSet, v string) graphql.Marshaler { + _ = sel res := graphql.MarshalString(v) if res == graphql.Null { if !graphql.HasFieldError(ctx, graphql.GetFieldContext(ctx)) { - ec.Errorf(ctx, "the requested element is null which the schema does not allow") + graphql.AddErrorf(ctx, "the requested element is null which the schema does not allow") } } return res } func (ec *executionContext) unmarshalN__DirectiveLocation2ᚕstringᚄ(ctx context.Context, v any) ([]string, error) { - var vSlice []any - if v != nil { - vSlice = graphql.CoerceList(v) - } + vSlice := graphql.CoerceList(v) var err error res := make([]string, len(vSlice)) for i := range vSlice { @@ -11742,39 +16591,11 @@ func (ec *executionContext) unmarshalN__DirectiveLocation2ᚕstringᚄ(ctx conte } func (ec *executionContext) marshalN__DirectiveLocation2ᚕstringᚄ(ctx context.Context, sel ast.SelectionSet, v []string) graphql.Marshaler { - ret := make(graphql.Array, len(v)) - var wg sync.WaitGroup - isLen1 := len(v) == 1 - if !isLen1 { - wg.Add(len(v)) - } - for i := range v { - i := i - fc := &graphql.FieldContext{ - Index: &i, - Result: &v[i], - } - ctx := graphql.WithFieldContext(ctx, fc) - f := func(i int) { - defer func() { - if r := recover(); r != nil { - ec.Error(ctx, ec.Recover(ctx, r)) - ret = nil - } - }() - if !isLen1 { - defer wg.Done() - } - ret[i] = ec.marshalN__DirectiveLocation2string(ctx, sel, v[i]) - } - if isLen1 { - f(i) - } else { - go f(i) - } - - } - wg.Wait() + ret := graphql.MarshalSliceConcurrently(ctx, len(v), 0, false, func(ctx context.Context, i int) graphql.Marshaler { + fc := graphql.GetFieldContext(ctx) + fc.Result = &v[i] + return ec.marshalN__DirectiveLocation2string(ctx, sel, v[i]) + }) for _, e := range ret { if e == graphql.Null { @@ -11798,39 +16619,11 @@ func (ec *executionContext) marshalN__InputValue2githubᚗcomᚋ99designsᚋgqlg } func (ec *executionContext) marshalN__InputValue2ᚕgithubᚗcomᚋ99designsᚋgqlgenᚋgraphqlᚋintrospectionᚐInputValueᚄ(ctx context.Context, sel ast.SelectionSet, v []introspection.InputValue) graphql.Marshaler { - ret := make(graphql.Array, len(v)) - var wg sync.WaitGroup - isLen1 := len(v) == 1 - if !isLen1 { - wg.Add(len(v)) - } - for i := range v { - i := i - fc := &graphql.FieldContext{ - Index: &i, - Result: &v[i], - } - ctx := graphql.WithFieldContext(ctx, fc) - f := func(i int) { - defer func() { - if r := recover(); r != nil { - ec.Error(ctx, ec.Recover(ctx, r)) - ret = nil - } - }() - if !isLen1 { - defer wg.Done() - } - ret[i] = ec.marshalN__InputValue2githubᚗcomᚋ99designsᚋgqlgenᚋgraphqlᚋintrospectionᚐInputValue(ctx, sel, v[i]) - } - if isLen1 { - f(i) - } else { - go f(i) - } - - } - wg.Wait() + ret := graphql.MarshalSliceConcurrently(ctx, len(v), 0, false, func(ctx context.Context, i int) graphql.Marshaler { + fc := graphql.GetFieldContext(ctx) + fc.Result = &v[i] + return ec.marshalN__InputValue2githubᚗcomᚋ99designsᚋgqlgenᚋgraphqlᚋintrospectionᚐInputValue(ctx, sel, v[i]) + }) for _, e := range ret { if e == graphql.Null { @@ -11846,39 +16639,11 @@ func (ec *executionContext) marshalN__Type2githubᚗcomᚋ99designsᚋgqlgenᚋg } func (ec *executionContext) marshalN__Type2ᚕgithubᚗcomᚋ99designsᚋgqlgenᚋgraphqlᚋintrospectionᚐTypeᚄ(ctx context.Context, sel ast.SelectionSet, v []introspection.Type) graphql.Marshaler { - ret := make(graphql.Array, len(v)) - var wg sync.WaitGroup - isLen1 := len(v) == 1 - if !isLen1 { - wg.Add(len(v)) - } - for i := range v { - i := i - fc := &graphql.FieldContext{ - Index: &i, - Result: &v[i], - } - ctx := graphql.WithFieldContext(ctx, fc) - f := func(i int) { - defer func() { - if r := recover(); r != nil { - ec.Error(ctx, ec.Recover(ctx, r)) - ret = nil - } - }() - if !isLen1 { - defer wg.Done() - } - ret[i] = ec.marshalN__Type2githubᚗcomᚋ99designsᚋgqlgenᚋgraphqlᚋintrospectionᚐType(ctx, sel, v[i]) - } - if isLen1 { - f(i) - } else { - go f(i) - } - - } - wg.Wait() + ret := graphql.MarshalSliceConcurrently(ctx, len(v), 0, false, func(ctx context.Context, i int) graphql.Marshaler { + fc := graphql.GetFieldContext(ctx) + fc.Result = &v[i] + return ec.marshalN__Type2githubᚗcomᚋ99designsᚋgqlgenᚋgraphqlᚋintrospectionᚐType(ctx, sel, v[i]) + }) for _, e := range ret { if e == graphql.Null { @@ -11892,7 +16657,7 @@ func (ec *executionContext) marshalN__Type2ᚕgithubᚗcomᚋ99designsᚋgqlgen func (ec *executionContext) marshalN__Type2ᚖgithubᚗcomᚋ99designsᚋgqlgenᚋgraphqlᚋintrospectionᚐType(ctx context.Context, sel ast.SelectionSet, v *introspection.Type) graphql.Marshaler { if v == nil { if !graphql.HasFieldError(ctx, graphql.GetFieldContext(ctx)) { - ec.Errorf(ctx, "the requested element is null which the schema does not allow") + graphql.AddErrorf(ctx, "the requested element is null which the schema does not allow") } return graphql.Null } @@ -11905,10 +16670,11 @@ func (ec *executionContext) unmarshalN__TypeKind2string(ctx context.Context, v a } func (ec *executionContext) marshalN__TypeKind2string(ctx context.Context, sel ast.SelectionSet, v string) graphql.Marshaler { + _ = sel res := graphql.MarshalString(v) if res == graphql.Null { if !graphql.HasFieldError(ctx, graphql.GetFieldContext(ctx)) { - ec.Errorf(ctx, "the requested element is null which the schema does not allow") + graphql.AddErrorf(ctx, "the requested element is null which the schema does not allow") } } return res @@ -11920,6 +16686,8 @@ func (ec *executionContext) unmarshalOBoolean2bool(ctx context.Context, v any) ( } func (ec *executionContext) marshalOBoolean2bool(ctx context.Context, sel ast.SelectionSet, v bool) graphql.Marshaler { + _ = sel + _ = ctx res := graphql.MarshalBoolean(v) return res } @@ -11936,6 +16704,8 @@ func (ec *executionContext) marshalOBoolean2ᚖbool(ctx context.Context, sel ast if v == nil { return graphql.Null } + _ = sel + _ = ctx res := graphql.MarshalBoolean(*v) return res } @@ -11958,10 +16728,7 @@ func (ec *executionContext) unmarshalODataframeFilterInput2ᚕᚖgithubᚗcomᚋ if v == nil { return nil, nil } - var vSlice []any - if v != nil { - vSlice = graphql.CoerceList(v) - } + vSlice := graphql.CoerceList(v) var err error res := make([]*model.DataframeFilterInput, len(vSlice)) for i := range vSlice { @@ -11981,6 +16748,41 @@ func (ec *executionContext) marshalODataframeMaterialization2ᚖgithubᚗcomᚋc return ec._DataframeMaterialization(ctx, sel, v) } +func (ec *executionContext) marshalODataframeRecipeArangoAssessment2ᚖgithubᚗcomᚋcalyprᚋloomᚋgraphqlapiᚋmodelᚐDataframeRecipeArangoAssessment(ctx context.Context, sel ast.SelectionSet, v *model.DataframeRecipeArangoAssessment) graphql.Marshaler { + if v == nil { + return graphql.Null + } + return ec._DataframeRecipeArangoAssessment(ctx, sel, v) +} + +func (ec *executionContext) marshalODataframeRecipeExecution2ᚖgithubᚗcomᚋcalyprᚋloomᚋgraphqlapiᚋmodelᚐDataframeRecipeExecution(ctx context.Context, sel ast.SelectionSet, v *model.DataframeRecipeExecution) graphql.Marshaler { + if v == nil { + return graphql.Null + } + return ec._DataframeRecipeExecution(ctx, sel, v) +} + +func (ec *executionContext) marshalODataframeRecipeExpansionExplanation2ᚖgithubᚗcomᚋcalyprᚋloomᚋgraphqlapiᚋmodelᚐDataframeRecipeExpansionExplanation(ctx context.Context, sel ast.SelectionSet, v *model.DataframeRecipeExpansionExplanation) graphql.Marshaler { + if v == nil { + return graphql.Null + } + return ec._DataframeRecipeExpansionExplanation(ctx, sel, v) +} + +func (ec *executionContext) marshalODataframeRecipeExpressionExplanation2ᚖgithubᚗcomᚋcalyprᚋloomᚋgraphqlapiᚋmodelᚐDataframeRecipeExpressionExplanation(ctx context.Context, sel ast.SelectionSet, v *model.DataframeRecipeExpressionExplanation) graphql.Marshaler { + if v == nil { + return graphql.Null + } + return ec._DataframeRecipeExpressionExplanation(ctx, sel, v) +} + +func (ec *executionContext) marshalODataframeRecipePhysicalExplanation2ᚖgithubᚗcomᚋcalyprᚋloomᚋgraphqlapiᚋmodelᚐDataframeRecipePhysicalExplanation(ctx context.Context, sel ast.SelectionSet, v *model.DataframeRecipePhysicalExplanation) graphql.Marshaler { + if v == nil { + return graphql.Null + } + return ec._DataframeRecipePhysicalExplanation(ctx, sel, v) +} + func (ec *executionContext) unmarshalODataframeSortInput2ᚖgithubᚗcomᚋcalyprᚋloomᚋgraphqlapiᚋmodelᚐDataframeSortInput(ctx context.Context, v any) (*model.DataframeSortInput, error) { if v == nil { return nil, nil @@ -11993,10 +16795,7 @@ func (ec *executionContext) unmarshalOFhirAggregateInput2ᚕᚖgithubᚗcomᚋca if v == nil { return nil, nil } - var vSlice []any - if v != nil { - vSlice = graphql.CoerceList(v) - } + vSlice := graphql.CoerceList(v) var err error res := make([]*model.FhirAggregateInput, len(vSlice)) for i := range vSlice { @@ -12009,6 +16808,31 @@ func (ec *executionContext) unmarshalOFhirAggregateInput2ᚕᚖgithubᚗcomᚋca return res, nil } +func (ec *executionContext) unmarshalOFhirCatalogProjectionInput2ᚕᚖgithubᚗcomᚋcalyprᚋloomᚋgraphqlapiᚋmodelᚐFhirCatalogProjectionInputᚄ(ctx context.Context, v any) ([]*model.FhirCatalogProjectionInput, error) { + if v == nil { + return nil, nil + } + vSlice := graphql.CoerceList(v) + var err error + res := make([]*model.FhirCatalogProjectionInput, len(vSlice)) + for i := range vSlice { + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithIndex(i)) + res[i], err = ec.unmarshalNFhirCatalogProjectionInput2ᚖgithubᚗcomᚋcalyprᚋloomᚋgraphqlapiᚋmodelᚐFhirCatalogProjectionInput(ctx, vSlice[i]) + if err != nil { + return nil, err + } + } + return res, nil +} + +func (ec *executionContext) unmarshalOFhirCodeValueInput2ᚖgithubᚗcomᚋcalyprᚋloomᚋgraphqlapiᚋmodelᚐFhirCodeValueInput(ctx context.Context, v any) (*model.FhirCodeValueInput, error) { + if v == nil { + return nil, nil + } + res, err := ec.unmarshalInputFhirCodeValueInput(ctx, v) + return &res, graphql.ErrorOnPath(ctx, err) +} + func (ec *executionContext) unmarshalOFhirFieldPredicateInput2ᚖgithubᚗcomᚋcalyprᚋloomᚋgraphqlapiᚋmodelᚐFhirFieldPredicateInput(ctx context.Context, v any) (*model.FhirFieldPredicateInput, error) { if v == nil { return nil, nil @@ -12021,10 +16845,7 @@ func (ec *executionContext) unmarshalOFhirFieldSelectInput2ᚕᚖgithubᚗcomᚋ if v == nil { return nil, nil } - var vSlice []any - if v != nil { - vSlice = graphql.CoerceList(v) - } + vSlice := graphql.CoerceList(v) var err error res := make([]*model.FhirFieldSelectInput, len(vSlice)) for i := range vSlice { @@ -12045,6 +16866,47 @@ func (ec *executionContext) unmarshalOFhirFieldSelectorInput2ᚖgithubᚗcomᚋc return &res, graphql.ErrorOnPath(ctx, err) } +func (ec *executionContext) unmarshalOFhirFilterInput2ᚕᚖgithubᚗcomᚋcalyprᚋloomᚋgraphqlapiᚋmodelᚐFhirFilterInputᚄ(ctx context.Context, v any) ([]*model.FhirFilterInput, error) { + if v == nil { + return nil, nil + } + vSlice := graphql.CoerceList(v) + var err error + res := make([]*model.FhirFilterInput, len(vSlice)) + for i := range vSlice { + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithIndex(i)) + res[i], err = ec.unmarshalNFhirFilterInput2ᚖgithubᚗcomᚋcalyprᚋloomᚋgraphqlapiᚋmodelᚐFhirFilterInput(ctx, vSlice[i]) + if err != nil { + return nil, err + } + } + return res, nil +} + +func (ec *executionContext) unmarshalOFhirFilterQuantifier2ᚖgithubᚗcomᚋcalyprᚋloomᚋgraphqlapiᚋmodelᚐFhirFilterQuantifier(ctx context.Context, v any) (*model.FhirFilterQuantifier, error) { + if v == nil { + return nil, nil + } + var res = new(model.FhirFilterQuantifier) + err := res.UnmarshalGQL(v) + return res, graphql.ErrorOnPath(ctx, err) +} + +func (ec *executionContext) marshalOFhirFilterQuantifier2ᚖgithubᚗcomᚋcalyprᚋloomᚋgraphqlapiᚋmodelᚐFhirFilterQuantifier(ctx context.Context, sel ast.SelectionSet, v *model.FhirFilterQuantifier) graphql.Marshaler { + if v == nil { + return graphql.Null + } + return v +} + +func (ec *executionContext) unmarshalOFhirPivotDiscoveryInput2ᚖgithubᚗcomᚋcalyprᚋloomᚋgraphqlapiᚋmodelᚐFhirPivotDiscoveryInput(ctx context.Context, v any) (*model.FhirPivotDiscoveryInput, error) { + if v == nil { + return nil, nil + } + res, err := ec.unmarshalInputFhirPivotDiscoveryInput(ctx, v) + return &res, graphql.ErrorOnPath(ctx, err) +} + func (ec *executionContext) unmarshalOFhirPivotFamily2ᚖgithubᚗcomᚋcalyprᚋloomᚋgraphqlapiᚋmodelᚐFhirPivotFamily(ctx context.Context, v any) (*model.FhirPivotFamily, error) { if v == nil { return nil, nil @@ -12065,10 +16927,7 @@ func (ec *executionContext) unmarshalOFhirPivotInput2ᚕᚖgithubᚗcomᚋcalypr if v == nil { return nil, nil } - var vSlice []any - if v != nil { - vSlice = graphql.CoerceList(v) - } + vSlice := graphql.CoerceList(v) var err error res := make([]*model.FhirPivotInput, len(vSlice)) for i := range vSlice { @@ -12085,10 +16944,7 @@ func (ec *executionContext) unmarshalOFhirRepresentativeSliceInput2ᚕᚖgithub if v == nil { return nil, nil } - var vSlice []any - if v != nil { - vSlice = graphql.CoerceList(v) - } + vSlice := graphql.CoerceList(v) var err error res := make([]*model.FhirRepresentativeSliceInput, len(vSlice)) for i := range vSlice { @@ -12101,14 +16957,27 @@ func (ec *executionContext) unmarshalOFhirRepresentativeSliceInput2ᚕᚖgithub return res, nil } -func (ec *executionContext) unmarshalOFhirTraversalStepInput2ᚕᚖgithubᚗcomᚋcalyprᚋloomᚋgraphqlapiᚋmodelᚐFhirTraversalStepInputᚄ(ctx context.Context, v any) ([]*model.FhirTraversalStepInput, error) { +func (ec *executionContext) unmarshalOFhirTraversalMatchMode2ᚖgithubᚗcomᚋcalyprᚋloomᚋgraphqlapiᚋmodelᚐFhirTraversalMatchMode(ctx context.Context, v any) (*model.FhirTraversalMatchMode, error) { if v == nil { return nil, nil } - var vSlice []any - if v != nil { - vSlice = graphql.CoerceList(v) + var res = new(model.FhirTraversalMatchMode) + err := res.UnmarshalGQL(v) + return res, graphql.ErrorOnPath(ctx, err) +} + +func (ec *executionContext) marshalOFhirTraversalMatchMode2ᚖgithubᚗcomᚋcalyprᚋloomᚋgraphqlapiᚋmodelᚐFhirTraversalMatchMode(ctx context.Context, sel ast.SelectionSet, v *model.FhirTraversalMatchMode) graphql.Marshaler { + if v == nil { + return graphql.Null + } + return v +} + +func (ec *executionContext) unmarshalOFhirTraversalStepInput2ᚕᚖgithubᚗcomᚋcalyprᚋloomᚋgraphqlapiᚋmodelᚐFhirTraversalStepInputᚄ(ctx context.Context, v any) ([]*model.FhirTraversalStepInput, error) { + if v == nil { + return nil, nil } + vSlice := graphql.CoerceList(v) var err error res := make([]*model.FhirTraversalStepInput, len(vSlice)) for i := range vSlice { @@ -12121,6 +16990,41 @@ func (ec *executionContext) unmarshalOFhirTraversalStepInput2ᚕᚖgithubᚗcom return res, nil } +func (ec *executionContext) unmarshalOFloat2ᚖfloat64(ctx context.Context, v any) (*float64, error) { + if v == nil { + return nil, nil + } + res, err := graphql.UnmarshalFloatContext(ctx, v) + return &res, graphql.ErrorOnPath(ctx, err) +} + +func (ec *executionContext) marshalOFloat2ᚖfloat64(ctx context.Context, sel ast.SelectionSet, v *float64) graphql.Marshaler { + if v == nil { + return graphql.Null + } + _ = sel + res := graphql.MarshalFloatContext(*v) + return graphql.WrapContextMarshaler(ctx, res) +} + +func (ec *executionContext) unmarshalOID2ᚖstring(ctx context.Context, v any) (*string, error) { + if v == nil { + return nil, nil + } + res, err := graphql.UnmarshalID(v) + return &res, graphql.ErrorOnPath(ctx, err) +} + +func (ec *executionContext) marshalOID2ᚖstring(ctx context.Context, sel ast.SelectionSet, v *string) graphql.Marshaler { + if v == nil { + return graphql.Null + } + _ = sel + _ = ctx + res := graphql.MarshalID(*v) + return res +} + func (ec *executionContext) unmarshalOInt2ᚖint(ctx context.Context, v any) (*int, error) { if v == nil { return nil, nil @@ -12133,6 +17037,8 @@ func (ec *executionContext) marshalOInt2ᚖint(ctx context.Context, sel ast.Sele if v == nil { return graphql.Null } + _ = sel + _ = ctx res := graphql.MarshalInt(*v) return res } @@ -12141,10 +17047,7 @@ func (ec *executionContext) unmarshalOString2ᚕstringᚄ(ctx context.Context, v if v == nil { return nil, nil } - var vSlice []any - if v != nil { - vSlice = graphql.CoerceList(v) - } + vSlice := graphql.CoerceList(v) var err error res := make([]string, len(vSlice)) for i := range vSlice { @@ -12187,6 +17090,8 @@ func (ec *executionContext) marshalOString2ᚖstring(ctx context.Context, sel as if v == nil { return graphql.Null } + _ = sel + _ = ctx res := graphql.MarshalString(*v) return res } @@ -12195,39 +17100,11 @@ func (ec *executionContext) marshalO__EnumValue2ᚕgithubᚗcomᚋ99designsᚋgq if v == nil { return graphql.Null } - ret := make(graphql.Array, len(v)) - var wg sync.WaitGroup - isLen1 := len(v) == 1 - if !isLen1 { - wg.Add(len(v)) - } - for i := range v { - i := i - fc := &graphql.FieldContext{ - Index: &i, - Result: &v[i], - } - ctx := graphql.WithFieldContext(ctx, fc) - f := func(i int) { - defer func() { - if r := recover(); r != nil { - ec.Error(ctx, ec.Recover(ctx, r)) - ret = nil - } - }() - if !isLen1 { - defer wg.Done() - } - ret[i] = ec.marshalN__EnumValue2githubᚗcomᚋ99designsᚋgqlgenᚋgraphqlᚋintrospectionᚐEnumValue(ctx, sel, v[i]) - } - if isLen1 { - f(i) - } else { - go f(i) - } - - } - wg.Wait() + ret := graphql.MarshalSliceConcurrently(ctx, len(v), 0, false, func(ctx context.Context, i int) graphql.Marshaler { + fc := graphql.GetFieldContext(ctx) + fc.Result = &v[i] + return ec.marshalN__EnumValue2githubᚗcomᚋ99designsᚋgqlgenᚋgraphqlᚋintrospectionᚐEnumValue(ctx, sel, v[i]) + }) for _, e := range ret { if e == graphql.Null { @@ -12242,39 +17119,11 @@ func (ec *executionContext) marshalO__Field2ᚕgithubᚗcomᚋ99designsᚋgqlgen if v == nil { return graphql.Null } - ret := make(graphql.Array, len(v)) - var wg sync.WaitGroup - isLen1 := len(v) == 1 - if !isLen1 { - wg.Add(len(v)) - } - for i := range v { - i := i - fc := &graphql.FieldContext{ - Index: &i, - Result: &v[i], - } - ctx := graphql.WithFieldContext(ctx, fc) - f := func(i int) { - defer func() { - if r := recover(); r != nil { - ec.Error(ctx, ec.Recover(ctx, r)) - ret = nil - } - }() - if !isLen1 { - defer wg.Done() - } - ret[i] = ec.marshalN__Field2githubᚗcomᚋ99designsᚋgqlgenᚋgraphqlᚋintrospectionᚐField(ctx, sel, v[i]) - } - if isLen1 { - f(i) - } else { - go f(i) - } - - } - wg.Wait() + ret := graphql.MarshalSliceConcurrently(ctx, len(v), 0, false, func(ctx context.Context, i int) graphql.Marshaler { + fc := graphql.GetFieldContext(ctx) + fc.Result = &v[i] + return ec.marshalN__Field2githubᚗcomᚋ99designsᚋgqlgenᚋgraphqlᚋintrospectionᚐField(ctx, sel, v[i]) + }) for _, e := range ret { if e == graphql.Null { @@ -12289,39 +17138,11 @@ func (ec *executionContext) marshalO__InputValue2ᚕgithubᚗcomᚋ99designsᚋg if v == nil { return graphql.Null } - ret := make(graphql.Array, len(v)) - var wg sync.WaitGroup - isLen1 := len(v) == 1 - if !isLen1 { - wg.Add(len(v)) - } - for i := range v { - i := i - fc := &graphql.FieldContext{ - Index: &i, - Result: &v[i], - } - ctx := graphql.WithFieldContext(ctx, fc) - f := func(i int) { - defer func() { - if r := recover(); r != nil { - ec.Error(ctx, ec.Recover(ctx, r)) - ret = nil - } - }() - if !isLen1 { - defer wg.Done() - } - ret[i] = ec.marshalN__InputValue2githubᚗcomᚋ99designsᚋgqlgenᚋgraphqlᚋintrospectionᚐInputValue(ctx, sel, v[i]) - } - if isLen1 { - f(i) - } else { - go f(i) - } - - } - wg.Wait() + ret := graphql.MarshalSliceConcurrently(ctx, len(v), 0, false, func(ctx context.Context, i int) graphql.Marshaler { + fc := graphql.GetFieldContext(ctx) + fc.Result = &v[i] + return ec.marshalN__InputValue2githubᚗcomᚋ99designsᚋgqlgenᚋgraphqlᚋintrospectionᚐInputValue(ctx, sel, v[i]) + }) for _, e := range ret { if e == graphql.Null { @@ -12343,39 +17164,11 @@ func (ec *executionContext) marshalO__Type2ᚕgithubᚗcomᚋ99designsᚋgqlgen if v == nil { return graphql.Null } - ret := make(graphql.Array, len(v)) - var wg sync.WaitGroup - isLen1 := len(v) == 1 - if !isLen1 { - wg.Add(len(v)) - } - for i := range v { - i := i - fc := &graphql.FieldContext{ - Index: &i, - Result: &v[i], - } - ctx := graphql.WithFieldContext(ctx, fc) - f := func(i int) { - defer func() { - if r := recover(); r != nil { - ec.Error(ctx, ec.Recover(ctx, r)) - ret = nil - } - }() - if !isLen1 { - defer wg.Done() - } - ret[i] = ec.marshalN__Type2githubᚗcomᚋ99designsᚋgqlgenᚋgraphqlᚋintrospectionᚐType(ctx, sel, v[i]) - } - if isLen1 { - f(i) - } else { - go f(i) - } - - } - wg.Wait() + ret := graphql.MarshalSliceConcurrently(ctx, len(v), 0, false, func(ctx context.Context, i int) graphql.Marshaler { + fc := graphql.GetFieldContext(ctx) + fc.Result = &v[i] + return ec.marshalN__Type2githubᚗcomᚋ99designsᚋgqlgenᚋgraphqlᚋintrospectionᚐType(ctx, sel, v[i]) + }) for _, e := range ret { if e == graphql.Null { diff --git a/graphqlapi/http_integration_test.go b/graphqlapi/http_integration_test.go index 44d3d65..a97aee3 100644 --- a/graphqlapi/http_integration_test.go +++ b/graphqlapi/http_integration_test.go @@ -70,15 +70,15 @@ func TestGraphQLIntrospectionEndpoint(t *testing.T) { Service: svc, Authenticator: authscope.StaticAuthenticator{Principal: authscope.Principal{Subject: "u1", Projects: []string{"P1"}, AuthResourcePaths: []string{"pathA"}}}, GraphQLHandler: graphqlapi.NewHandler(graphResolver), - GraphQLPlaygroundHandler: graphqlapi.NewPlaygroundHandler("/graphql"), - ApolloSandboxHandler: graphqlapi.NewApolloSandboxHandler("/graphql"), + GraphQLPlaygroundHandler: graphqlapi.NewPlaygroundHandler("/graphql/graph"), + ApolloSandboxHandler: graphqlapi.NewApolloSandboxHandler("/graphql/graph"), }) if err != nil { t.Fatal(err) } queryBody := `{"query":"query($input: DataframeBuilderIntrospectionInput!) { dataframeBuilderIntrospection(input: $input) { project rootResourceType authResourcePaths root { resourceType fields { resourceType fieldRef label path selector { sourcePath where { path op value } valuePath } kind } pivotFields { resourceType fieldRef path selector { sourcePath where { path op value } valuePath } pivotCandidate pivotKind pivotColumns } traversals { fromType label toType edgeCount } } relatedResources { viaLabel edgeCount target { resourceType fields { resourceType fieldRef path selector { sourcePath where { path op value } valuePath } kind } pivotFields { resourceType fieldRef path selector { sourcePath where { path op value } valuePath } pivotCandidate pivotKind pivotColumns } } } traversals { fromType label toType edgeCount } fields { resourceType fieldRef label path selector { sourcePath where { path op value } valuePath } kind docCount sampleCount distinctValues distinctTruncated pivotCandidate pivotKind pivotColumns } pivotFields { resourceType fieldRef path selector { sourcePath where { path op value } valuePath } pivotCandidate pivotKind pivotColumns } } }","variables":{"input":{"project":"P1","rootResourceType":"Patient"}}}` - req := httptest.NewRequest(http.MethodPost, "/graphql", strings.NewReader(queryBody)) + req := httptest.NewRequest(http.MethodPost, "/graphql/graph", strings.NewReader(queryBody)) req.Header.Set("Content-Type", "application/json") resp, err := server.App().Test(req) @@ -141,15 +141,15 @@ func TestGraphQLSchemaIntrospectionEndpoint(t *testing.T) { Service: svc, Authenticator: authscope.StaticAuthenticator{Principal: authscope.Principal{Subject: "u1"}}, GraphQLHandler: graphqlapi.NewHandler(graphResolver), - GraphQLPlaygroundHandler: graphqlapi.NewPlaygroundHandler("/graphql"), - ApolloSandboxHandler: graphqlapi.NewApolloSandboxHandler("/graphql"), + GraphQLPlaygroundHandler: graphqlapi.NewPlaygroundHandler("/graphql/graph"), + ApolloSandboxHandler: graphqlapi.NewApolloSandboxHandler("/graphql/graph"), }) if err != nil { t.Fatal(err) } queryBody := `{"query":"query { __schema { queryType { name } mutationType { name } } }"}` - req := httptest.NewRequest(http.MethodPost, "/graphql", strings.NewReader(queryBody)) + req := httptest.NewRequest(http.MethodPost, "/graphql/graph", strings.NewReader(queryBody)) req.Header.Set("Content-Type", "application/json") resp, err := server.App().Test(req) @@ -267,15 +267,15 @@ func TestGraphQLRunDataframeMutation(t *testing.T) { Service: svc, Authenticator: authscope.StaticAuthenticator{Principal: authscope.Principal{Subject: "u1", Projects: []string{"P1"}, AuthResourcePaths: []string{"pathA"}}}, GraphQLHandler: graphqlapi.NewHandler(graphResolver), - GraphQLPlaygroundHandler: graphqlapi.NewPlaygroundHandler("/graphql"), - ApolloSandboxHandler: graphqlapi.NewApolloSandboxHandler("/graphql"), + GraphQLPlaygroundHandler: graphqlapi.NewPlaygroundHandler("/graphql/graph"), + ApolloSandboxHandler: graphqlapi.NewApolloSandboxHandler("/graphql/graph"), }) if err != nil { t.Fatal(err) } queryBody := `{"query":"mutation($input: FhirDataframeInput!, $limit: Int) { runFhirDataframe(input: $input, limit: $limit) { columns rows rowCount diagnostics { inputResolutionMs requestPreparationMs compilationMs arangoQueryMs rowMaterializationMs resultAssemblyMs totalMs plan { traversalSets sharedTraversalCount scopedSharingCandidateGroups scopedSharingCandidateSets richSourceReuse { sourceSet aggregateConsumers pivotConsumers sliceConsumers totalConsumers } } } } }","variables":{"limit":25,"input":{"project":"P1","rootResourceType":"Patient","rootFields":[{"name":"gender","selector":{"valuePath":"gender"},"valueMode":"AUTO"}],"traverse":[{"edgeLabel":"subject_Patient","toResourceType":"Condition","alias":"condition","aggregates":[{"name":"condition_count","operation":"COUNT"}]},{"edgeLabel":"subject_Patient","toResourceType":"Specimen","alias":"specimen","aggregates":[{"name":"specimen_count","operation":"COUNT"}]}]}}}` - req := httptest.NewRequest(http.MethodPost, "/graphql", strings.NewReader(queryBody)) + req := httptest.NewRequest(http.MethodPost, "/graphql/graph", strings.NewReader(queryBody)) req.Header.Set("Content-Type", "application/json") resp, err := server.App().Test(req) @@ -406,15 +406,15 @@ func TestGraphQLRunDataframeTraversalBuilder(t *testing.T) { Service: svc, Authenticator: authscope.StaticAuthenticator{Principal: authscope.Principal{Subject: "u1", Projects: []string{"P1"}, AuthResourcePaths: []string{"pathA"}}}, GraphQLHandler: graphqlapi.NewHandler(graphResolver), - GraphQLPlaygroundHandler: graphqlapi.NewPlaygroundHandler("/graphql"), - ApolloSandboxHandler: graphqlapi.NewApolloSandboxHandler("/graphql"), + GraphQLPlaygroundHandler: graphqlapi.NewPlaygroundHandler("/graphql/graph"), + ApolloSandboxHandler: graphqlapi.NewApolloSandboxHandler("/graphql/graph"), }) if err != nil { t.Fatal(err) } queryBody := `{"query":"mutation($input: FhirDataframeInput!, $limit: Int) { runFhirDataframe(input: $input, limit: $limit) { columns rows rowCount } }","variables":{"limit":25,"input":{"project":"P1","rootResourceType":"Patient","rootFields":[{"name":"gender","selector":{"valuePath":"gender"},"valueMode":"AUTO"}],"traverse":[{"edgeLabel":"subject_Patient","toResourceType":"Specimen","alias":"specimen","aggregates":[{"name":"specimen_count","operation":"COUNT"},{"name":"specimen_types","operation":"DISTINCT_VALUES","fhirPath":"type[].coding[].display","valueMode":"AUTO"}]},{"edgeLabel":"subject_Patient","toResourceType":"Condition","alias":"condition","aggregates":[{"name":"condition_count","operation":"COUNT"}]}]}}}` - req := httptest.NewRequest(http.MethodPost, "/graphql", strings.NewReader(queryBody)) + req := httptest.NewRequest(http.MethodPost, "/graphql/graph", strings.NewReader(queryBody)) req.Header.Set("Content-Type", "application/json") resp, err := server.App().Test(req) diff --git a/graphqlapi/materialization/output.go b/graphqlapi/materialization/output.go index 8bdc3ba..f03ee5b 100644 --- a/graphqlapi/materialization/output.go +++ b/graphqlapi/materialization/output.go @@ -2,15 +2,21 @@ package materializationapi import ( "encoding/json" + "strings" "github.com/calypr/loom/graphqlapi/model" "github.com/calypr/loom/internal/dataframe/materialization" ) func Model(value materialization.Materialization) *model.DataframeMaterialization { + revision := value.Revision + if revision == "" { + revision = value.ID + } columns := make([]*model.DataframeColumn, 0, len(value.Columns)) for _, column := range value.Columns { - columns = append(columns, &model.DataframeColumn{Name: column.Name, ClickhouseType: column.ClickHouse}) + logical, nullable, repeated, filterable, sortable, aggregatable := columnCapabilities(column.ClickHouse) + columns = append(columns, &model.DataframeColumn{Name: column.Name, ClickhouseType: column.ClickHouse, LogicalType: logical, Nullable: nullable, Repeated: repeated, Filterable: filterable, Sortable: sortable, Aggregatable: aggregatable}) } var readyAt *string if value.ReadyAt != nil { @@ -22,15 +28,44 @@ func Model(value materialization.Materialization) *model.DataframeMaterializatio failure = &value.Error } return &model.DataframeMaterialization{ - ID: value.ID, Name: value.Name, Project: value.Project, - DatasetGeneration: value.DatasetGeneration, - State: model.DataframeMaterializationState(value.State), Columns: columns, + ID: value.ID, Name: value.Name, Revision: revision, + State: model.DataframeMaterializationState(value.State), Columns: columns, RowCount: int(value.RowCount), CreatedAt: value.CreatedAt.UTC().Format("2006-01-02T15:04:05.999Z07:00"), ReadyAt: readyAt, Error: failure, } } +func columnCapabilities(clickHouseType string) (logical string, nullable, repeated, filterable, sortable, aggregatable bool) { + typ := clickHouseType + if strings.HasPrefix(typ, "Nullable(") && strings.HasSuffix(typ, ")") { + nullable = true + typ = strings.TrimSuffix(strings.TrimPrefix(typ, "Nullable("), ")") + } + if strings.HasPrefix(typ, "Array(") && strings.HasSuffix(typ, ")") { + repeated = true + typ = strings.TrimSuffix(strings.TrimPrefix(typ, "Array("), ")") + } + switch { + case strings.HasPrefix(typ, "Bool"): + logical = "boolean" + case strings.HasPrefix(typ, "Int") || strings.HasPrefix(typ, "UInt"): + logical = "integer" + case strings.HasPrefix(typ, "Float") || strings.HasPrefix(typ, "Decimal"): + logical = "number" + case strings.HasPrefix(typ, "Date"): + logical = "date" + case strings.HasPrefix(typ, "String") || strings.HasPrefix(typ, "FixedString") || strings.HasPrefix(typ, "UUID"): + logical = "string" + default: + logical = "json" + } + filterable = true + sortable = !repeated + aggregatable = !repeated && logical != "json" + return logical, nullable, repeated, filterable, sortable, aggregatable +} + func AggregateRows(value []map[string]any) json.RawMessage { data, _ := json.Marshal(value) return data diff --git a/graphqlapi/materialization/service.go b/graphqlapi/materialization/service.go index b41603a..93e68ba 100644 --- a/graphqlapi/materialization/service.go +++ b/graphqlapi/materialization/service.go @@ -1,6 +1,6 @@ -// Package materializationapi adapts published dataframe materializations to -// GraphQL. It owns read authorization and ClickHouse-backed row/aggregate -// access; dataframe compilation remains in internal/dataframe. +// Package materializationapi adapts published ClickHouse dataframe outputs to +// GraphQL. Dataset discovery and reads are principal-scoped federations; the +// browser supplies only the logical dataType alias. package materializationapi import ( @@ -9,154 +9,280 @@ import ( "github.com/calypr/loom/graphqlapi/model" "github.com/calypr/loom/internal/authscope" - "github.com/calypr/loom/internal/dataframe/materialization" + dfmaterialization "github.com/calypr/loom/internal/dataframe/materialization" + "github.com/calypr/loom/internal/dataset" ) type Service struct { - reader *materialization.Reader - scopeResolver *authscope.ScopeResolver + reader *dfmaterialization.Reader + scopeResolver *authscope.ScopeResolver + activeGeneration dataset.ActiveManifestResolver } type Config struct { - Reader *materialization.Reader - ScopeResolver *authscope.ScopeResolver + Reader *dfmaterialization.Reader + ScopeResolver *authscope.ScopeResolver + ActiveManifestResolver dataset.ActiveManifestResolver } func NewService(cfg Config) *Service { - return &Service{reader: cfg.Reader, scopeResolver: cfg.ScopeResolver} + return &Service{reader: cfg.Reader, scopeResolver: cfg.ScopeResolver, activeGeneration: cfg.ActiveManifestResolver} } -func (s *Service) Get(ctx context.Context, id string) (*materialization.Materialization, error) { +func (s *Service) principal(ctx context.Context) (*authscope.Principal, error) { + principal, ok := authscope.PrincipalFromContext(ctx) + if !ok || principal == nil { + return nil, fmt.Errorf("an authenticated principal is required") + } + return principal, nil +} + +func (s *Service) projects(ctx context.Context, principal *authscope.Principal) ([]string, error) { + if len(principal.Projects) > 0 { + return append([]string(nil), principal.Projects...), nil + } + projects, err := s.reader.PublishedProjects(ctx) + if err != nil { + return nil, err + } + if len(projects) == 0 { + return nil, fmt.Errorf("principal has no authorized projects") + } + return projects, nil +} + +func (s *Service) Datasets(ctx context.Context) ([]dfmaterialization.Materialization, error) { if s.reader == nil { return nil, fmt.Errorf("dataframe materialization reads are not configured") } - value, err := s.reader.Registry.Get(ctx, id) + principal, err := s.principal(ctx) if err != nil { return nil, err } - if err := s.authorize(ctx, value); err != nil { + projects, err := s.projects(ctx, principal) + if err != nil { return nil, err } - return &value, nil + datasets, err := s.reader.FederatedDatasets(ctx, projects) + if err != nil { + return nil, err + } + result := make([]dfmaterialization.Materialization, 0, len(datasets)) + for _, dataset := range datasets { + result = append(result, federatedMaterialization(dataset)) + } + return result, nil } -func (s *Service) Rows(ctx context.Context, input model.DataframeRowsInput) (materialization.Page, error) { +func (s *Service) Dataset(ctx context.Context, input model.DataframeDatasetInput) (*dfmaterialization.Materialization, error) { if s.reader == nil { - return materialization.Page{}, fmt.Errorf("dataframe materialization reads are not configured") + return nil, fmt.Errorf("dataframe materialization reads are not configured") } - value, err := s.reader.Registry.Get(ctx, input.MaterializationID) + principal, err := s.principal(ctx) if err != nil { - return materialization.Page{}, err + return nil, err } - if err := s.authorize(ctx, value); err != nil { - return materialization.Page{}, err + projects, err := s.projects(ctx, principal) + if err != nil { + return nil, err } - filters := make([]materialization.Filter, 0, len(input.Filters)) - for _, filter := range input.Filters { - if filter != nil { - filters = append(filters, materialization.Filter{Column: filter.Column, Op: filter.Op, Value: filter.Value}) - } + dataset, err := s.reader.ResolveFederatedDataset(ctx, projects, input.DataType) + if err != nil { + return nil, err } - var sortInput *materialization.Sort - if input.Sort != nil { - sortInput = &materialization.Sort{Column: input.Sort.Column, Desc: input.Sort.Desc != nil && *input.Sort.Desc} - } - first := 0 - if input.First != nil { - first = *input.First - } - after := "" - if input.After != nil { - after = *input.After - } - return s.reader.Page(ctx, materialization.PageRequest{ - MaterializationID: input.MaterializationID, - Columns: input.Columns, - Filters: filters, - Sort: sortInput, - First: first, - After: after, - }) + value := federatedMaterialization(dataset) + return &value, nil } -func (s *Service) Aggregate(ctx context.Context, id string, groupBy []string, filters []*model.DataframeFilterInput, operation, column string) (materialization.AggregateResult, error) { +// Get remains available for internal callers holding a publication ID. It is +// not part of the projectless Explorer contract and still performs source +// authorization before returning metadata. +func (s *Service) Get(ctx context.Context, id string) (*dfmaterialization.Materialization, error) { if s.reader == nil { - return materialization.AggregateResult{}, fmt.Errorf("dataframe materialization reads are not configured") + return nil, fmt.Errorf("dataframe materialization reads are not configured") } - value, err := s.reader.Registry.Get(ctx, id) + value, err := s.reader.DatasetByPublishedID(ctx, id) if err != nil { - return materialization.AggregateResult{}, err + return nil, err } - if err := s.authorize(ctx, value); err != nil { - return materialization.AggregateResult{}, err + if err := s.authorizePublished(ctx, value); err != nil { + return nil, err } - converted := make([]materialization.Filter, 0, len(filters)) - for _, filter := range filters { - if filter != nil { - converted = append(converted, materialization.Filter{Column: filter.Column, Op: filter.Op, Value: filter.Value}) + return &value, nil +} + +func (s *Service) Rows(ctx context.Context, input model.DataframeRowsInput) (dfmaterialization.Page, error) { + if s.reader == nil { + return dfmaterialization.Page{}, fmt.Errorf("dataframe materialization reads are not configured") + } + if input.MaterializationID != nil && *input.MaterializationID != "" { + value, err := s.reader.DatasetByPublishedID(ctx, *input.MaterializationID) + if err != nil { + return dfmaterialization.Page{}, err } + if err := s.authorizePublished(ctx, value); err != nil { + return dfmaterialization.Page{}, err + } + return s.reader.PagePublishedID(ctx, value.ID, pageRequest(input)) + } + principal, err := s.principal(ctx) + if err != nil { + return dfmaterialization.Page{}, err } - return s.reader.Aggregate(ctx, materialization.AggregateRequest{ - MaterializationID: id, - GroupBy: groupBy, - Filters: converted, - Operation: operation, - Column: column, + dataset, projects, authPaths, unrestricted, err := s.authorizedFederation(ctx, principal, input.DataType) + if err != nil { + return dfmaterialization.Page{}, err + } + page, err := s.reader.PageFederated(ctx, projects, input.DataType, dfmaterialization.FederatedPageRequest{ + Columns: input.Columns, Filters: convertFilters(input.Filters), Sort: convertSort(input), First: intValue(input.First), After: stringValue(input.After), + AuthPathsByProject: authPaths, UnrestrictedByProject: unrestricted, }) + if err != nil { + return dfmaterialization.Page{}, err + } + return dfmaterialization.Page{Materialization: federatedMaterialization(dataset), Columns: page.Columns, Rows: page.Rows, TotalCount: page.TotalCount, HasNext: page.HasNext, NextCursor: page.NextCursor}, nil } -func (s *Service) authorize(ctx context.Context, value materialization.Materialization) error { - principal, _ := authscope.PrincipalFromContext(ctx) - if principal != nil && len(principal.Projects) > 0 { - allowed := false - for _, project := range principal.Projects { - if project == value.Project { - allowed = true - break - } +func (s *Service) AggregateInput(ctx context.Context, input model.DataframeAggregateInput) (dfmaterialization.AggregateResult, error) { + if s.reader == nil { + return dfmaterialization.AggregateResult{}, fmt.Errorf("dataframe materialization reads are not configured") + } + if input.MaterializationID != nil && *input.MaterializationID != "" { + value, err := s.reader.DatasetByPublishedID(ctx, *input.MaterializationID) + if err != nil { + return dfmaterialization.AggregateResult{}, err } - if !allowed { - return fmt.Errorf("principal is not authorized for project %q", value.Project) + if err := s.authorizePublished(ctx, value); err != nil { + return dfmaterialization.AggregateResult{}, err } + return s.reader.AggregatePublishedID(ctx, value.ID, dfmaterialization.AggregateRequest{MaterializationID: value.ID, GroupBy: input.GroupBy, Filters: convertFilters(input.Filters), Operation: input.Operation, Column: stringValue(input.Column)}) } - if value.AuthScopeMode == authscope.ReadScopeUnrestricted { + principal, err := s.principal(ctx) + if err != nil { + return dfmaterialization.AggregateResult{}, err + } + dataset, projects, authPaths, unrestricted, err := s.authorizedFederation(ctx, principal, input.DataType) + if err != nil { + return dfmaterialization.AggregateResult{}, err + } + result, err := s.reader.AggregateFederated(ctx, projects, input.DataType, dfmaterialization.FederatedAggregateRequest{ + GroupBy: input.GroupBy, Filters: convertFilters(input.Filters), Operation: input.Operation, Column: stringValue(input.Column), AuthPathsByProject: authPaths, UnrestrictedByProject: unrestricted, + }) + if err != nil { + return dfmaterialization.AggregateResult{}, err + } + result.Dataset = dataset + return dfmaterialization.AggregateResult{Materialization: federatedMaterialization(result.Dataset), Columns: result.Columns, Rows: result.Rows}, nil +} + +func (s *Service) authorizedFederation(ctx context.Context, principal *authscope.Principal, alias string) (dfmaterialization.FederatedDataset, []string, map[string][]string, map[string]bool, error) { + candidates, err := s.projects(ctx, principal) + if err != nil { + return dfmaterialization.FederatedDataset{}, nil, nil, nil, err + } + dataset, err := s.reader.ResolveFederatedDataset(ctx, candidates, alias) + if err != nil { + return dfmaterialization.FederatedDataset{}, nil, nil, nil, err + } + paths := make(map[string][]string, len(dataset.Sources)) + unrestricted := make(map[string]bool, len(dataset.Sources)) + authorizedProjects := make([]string, 0, len(dataset.Sources)) + for _, source := range dataset.Sources { if s.scopeResolver != nil { - scope, err := s.scopeResolver.ResolveReadScopeForGeneration(ctx, principal, value.Project, value.DatasetGeneration, nil) + scope, err := s.scopeResolver.ResolveReadScopeForGeneration(ctx, principal, source.Project, source.DatasetGeneration, nil) if err != nil { - return err + if len(principal.Projects) == 0 { + continue + } + return dfmaterialization.FederatedDataset{}, nil, nil, nil, err } - if !scope.Unrestricted() { - return fmt.Errorf("materialization %q requires unrestricted authorization", value.ID) - } - } else if principal != nil && len(principal.AuthResourcePaths) > 0 { - return fmt.Errorf("materialization %q requires unrestricted authorization", value.ID) + paths[source.Project] = append([]string(nil), scope.AuthResourcePaths...) + unrestricted[source.Project] = scope.Unrestricted() + authorizedProjects = append(authorizedProjects, source.Project) + continue + } + if source.AuthScopeMode == authscope.ReadScopeUnrestricted { + unrestricted[source.Project] = true + authorizedProjects = append(authorizedProjects, source.Project) + continue + } + paths[source.Project] = append([]string(nil), principal.AuthResourcePaths...) + authorizedProjects = append(authorizedProjects, source.Project) + } + if len(authorizedProjects) == 0 { + return dfmaterialization.FederatedDataset{}, nil, nil, nil, fmt.Errorf("published dataset %q is not authorized", alias) + } + if len(authorizedProjects) != len(dataset.Sources) { + dataset, err = s.reader.ResolveFederatedDataset(ctx, authorizedProjects, alias) + if err != nil { + return dfmaterialization.FederatedDataset{}, nil, nil, nil, err + } + } + return dataset, authorizedProjects, paths, unrestricted, nil +} + +func federatedMaterialization(dataset dfmaterialization.FederatedDataset) dfmaterialization.Materialization { + return dfmaterialization.Materialization{ID: "federated:" + dataset.Name, Name: dataset.Name, Revision: dataset.Revision, DatasetGeneration: "federated:" + dataset.Revision, State: dfmaterialization.StateReady, Columns: dataset.Columns, RowCount: dataset.RowCount} +} + +func (s *Service) authorizePublished(ctx context.Context, value dfmaterialization.Materialization) error { + principal, err := s.principal(ctx) + if err != nil { + return err + } + allowed := false + for _, project := range principal.Projects { + if project == value.Project { + allowed = true + break } - return nil + } + if !allowed { + return fmt.Errorf("principal is not authorized for published dataset") } if s.scopeResolver != nil { scope, err := s.scopeResolver.ResolveReadScopeForGeneration(ctx, principal, value.Project, value.DatasetGeneration, value.AuthResourcePaths) if err != nil { return err } - if len(scope.AuthResourcePaths) != len(value.AuthResourcePaths) { - return fmt.Errorf("materialization %q is outside caller scope", value.ID) + if value.AuthScopeMode != authscope.ReadScopeUnrestricted && scope.Unrestricted() { + return fmt.Errorf("published dataset is outside caller scope") + } + } + return nil +} + +func pageRequest(input model.DataframeRowsInput) dfmaterialization.PageRequest { + return dfmaterialization.PageRequest{Columns: input.Columns, Filters: convertFilters(input.Filters), Sort: convertSort(input), First: intValue(input.First), After: stringValue(input.After)} +} + +func convertFilters(filters []*model.DataframeFilterInput) []dfmaterialization.Filter { + converted := make([]dfmaterialization.Filter, 0, len(filters)) + for _, filter := range filters { + if filter != nil { + converted = append(converted, dfmaterialization.Filter{Column: filter.Column, Op: filter.Op, Value: filter.Value}) } + } + return converted +} + +func convertSort(input model.DataframeRowsInput) *dfmaterialization.Sort { + if input.Sort == nil { return nil } - if principal == nil { - return fmt.Errorf("materialization %q requires an authorized principal", value.ID) + return &dfmaterialization.Sort{Column: input.Sort.Column, Desc: input.Sort.Desc != nil && *input.Sort.Desc} +} + +func intValue(value *int) int { + if value == nil { + return 0 } - for _, requested := range value.AuthResourcePaths { - found := false - for _, allowed := range principal.AuthResourcePaths { - if requested == allowed { - found = true - break - } - } - if !found { - return fmt.Errorf("materialization %q is outside caller scope", value.ID) - } + return *value +} + +func stringValue(value *string) string { + if value == nil { + return "" } - return nil + return *value } diff --git a/graphqlapi/model/models.go b/graphqlapi/model/models.go index 449faab..a938d13 100644 --- a/graphqlapi/model/models.go +++ b/graphqlapi/model/models.go @@ -3,6 +3,7 @@ package model import ( + "bytes" "encoding/json" "fmt" "io" @@ -10,7 +11,8 @@ import ( ) type DataframeAggregateInput struct { - MaterializationID string `json:"materializationId"` + MaterializationID *string `json:"materializationId,omitempty"` + DataType string `json:"dataType"` GroupBy []string `json:"groupBy,omitempty"` Filters []*DataframeFilterInput `json:"filters,omitempty"` Operation string `json:"operation"` @@ -44,6 +46,12 @@ type DataframeBuilderIntrospectionInput struct { type DataframeColumn struct { Name string `json:"name"` ClickhouseType string `json:"clickhouseType"` + LogicalType string `json:"logicalType"` + Nullable bool `json:"nullable"` + Repeated bool `json:"repeated"` + Filterable bool `json:"filterable"` + Sortable bool `json:"sortable"` + Aggregatable bool `json:"aggregatable"` } type DataframeCompilerPlanDiagnostics struct { @@ -58,6 +66,10 @@ type DataframeCompilerPlanDiagnostics struct { RichSourceReuse []*DataframeRichSourceReuse `json:"richSourceReuse"` } +type DataframeDatasetInput struct { + DataType string `json:"dataType"` +} + type DataframeFieldHint struct { ResourceType string `json:"resourceType"` FieldRef string `json:"fieldRef"` @@ -96,16 +108,15 @@ type DataframeFilterInput struct { } type DataframeMaterialization struct { - ID string `json:"id"` - Name string `json:"name"` - Project string `json:"project"` - DatasetGeneration string `json:"datasetGeneration"` - State DataframeMaterializationState `json:"state"` - Columns []*DataframeColumn `json:"columns"` - RowCount int `json:"rowCount"` - CreatedAt string `json:"createdAt"` - ReadyAt *string `json:"readyAt,omitempty"` - Error *string `json:"error,omitempty"` + ID string `json:"id"` + Name string `json:"name"` + Revision string `json:"revision"` + State DataframeMaterializationState `json:"state"` + Columns []*DataframeColumn `json:"columns"` + RowCount int `json:"rowCount"` + CreatedAt string `json:"createdAt"` + ReadyAt *string `json:"readyAt,omitempty"` + Error *string `json:"error,omitempty"` } type DataframeOptimizationDecision struct { @@ -141,6 +152,209 @@ type DataframeQueryDiagnostics struct { Plan *DataframeCompilerPlanDiagnostics `json:"plan"` } +type DataframeRecipeArangoAssessment struct { + Plans []*DataframeRecipeExplainPlanEstimate `json:"plans"` + FullCollectionScans []*DataframeRecipeExplainCollectionScan `json:"fullCollectionScans"` + Indexes []*DataframeRecipeExplainIndexSummary `json:"indexes"` + Warnings []*DataframeRecipeExplainWarning `json:"warnings"` + AppliedOptimizerRules []string `json:"appliedOptimizerRules"` +} + +type DataframeRecipeBindingsInput struct { + Project string `json:"project"` + DatasetGeneration *string `json:"datasetGeneration,omitempty"` + AuthResourcePaths []string `json:"authResourcePaths,omitempty"` + PreviewLimit *int `json:"previewLimit,omitempty"` +} + +type DataframeRecipeColumn struct { + Output string `json:"output"` + DynamicName string `json:"dynamicName"` + Name string `json:"name"` + LogicalType string `json:"logicalType"` + Repeated bool `json:"repeated"` + Nullable bool `json:"nullable"` +} + +type DataframeRecipeExecution struct { + ID string `json:"id"` + Name string `json:"name"` + RecipeDigest string `json:"recipeDigest"` + ResolvedSchemaDigest string `json:"resolvedSchemaDigest"` + SourceGeneration string `json:"sourceGeneration"` + State DataframeRecipeExecutionState `json:"state"` + Outputs []*DataframeRecipeExecutionOutput `json:"outputs"` + Error *string `json:"error,omitempty"` +} + +type DataframeRecipeExecutionOutput struct { + Name string `json:"name"` + State DataframeRecipeExecutionState `json:"state"` + RowCount *int `json:"rowCount,omitempty"` + Error *string `json:"error,omitempty"` +} + +type DataframeRecipeExpansionExplanation struct { + SourcePath string `json:"sourcePath"` + Alias string `json:"alias"` +} + +type DataframeRecipeExplainCollectionScan struct { + Plan int `json:"plan"` + NodeID int `json:"nodeId"` + Collection string `json:"collection"` +} + +type DataframeRecipeExplainIndexLocation struct { + Plan int `json:"plan"` + NodeID int `json:"nodeId"` +} + +type DataframeRecipeExplainIndexSummary struct { + Collection string `json:"collection"` + ID string `json:"id"` + Name string `json:"name"` + Type string `json:"type"` + Fields []string `json:"fields"` + Uses []*DataframeRecipeExplainIndexLocation `json:"uses"` +} + +type DataframeRecipeExplainPlanEstimate struct { + Plan int `json:"plan"` + EstimatedCost float64 `json:"estimatedCost"` + EstimatedNrItems float64 `json:"estimatedNrItems"` +} + +type DataframeRecipeExplainWarning struct { + Code int `json:"code"` + Message string `json:"message"` +} + +type DataframeRecipeExplanation struct { + Name string `json:"name"` + RecipeDigest string `json:"recipeDigest"` + TranslationVersion string `json:"translationVersion"` + Outputs []*DataframeRecipeOutputExplanation `json:"outputs"` + Physical *DataframeRecipePhysicalExplanation `json:"physical,omitempty"` +} + +type DataframeRecipeExpressionExplanation struct { + SourcePath string `json:"sourcePath"` + Context string `json:"context"` + Kind string `json:"kind"` + ValueType string `json:"valueType"` + Repeated bool `json:"repeated"` + Nullable bool `json:"nullable"` +} + +type DataframeRecipeOptimizationDecision struct { + Rule string `json:"rule"` + Enabled bool `json:"enabled"` + CandidateSets int `json:"candidateSets"` + EstimatedBaselineWork int `json:"estimatedBaselineWork"` + EstimatedOptimizedWork int `json:"estimatedOptimizedWork"` + EstimatedSavings int `json:"estimatedSavings"` + Reason string `json:"reason"` +} + +type DataframeRecipeOptimizationExplanation struct { + Policy string `json:"policy"` + Enabled bool `json:"enabled"` + MinimumSavings int `json:"minimumSavings"` + Rules []*DataframeRecipeOptimizationRuleState `json:"rules"` + Decisions []*DataframeRecipeOptimizationDecision `json:"decisions"` +} + +type DataframeRecipeOptimizationRuleState struct { + Rule string `json:"rule"` + Enabled bool `json:"enabled"` + Reason string `json:"reason"` +} + +type DataframeRecipeOutputExplanation struct { + Name string `json:"name"` + RootResourceType string `json:"rootResourceType"` + RowGrain string `json:"rowGrain"` + Fields []*DataframeRecipeExpressionExplanation `json:"fields"` + Identity *DataframeRecipeExpressionExplanation `json:"identity,omitempty"` + Expansion *DataframeRecipeExpansionExplanation `json:"expansion,omitempty"` + DynamicMaps []string `json:"dynamicMaps"` + CatalogProjections []string `json:"catalogProjections"` +} + +type DataframeRecipeOutputValidation struct { + Name string `json:"name"` + RootResourceType string `json:"rootResourceType"` + RowGrain string `json:"rowGrain"` + FieldNames []string `json:"fieldNames"` + DynamicColumns []string `json:"dynamicColumns"` +} + +type DataframeRecipePhysicalExplanation struct { + Outputs []*DataframeRecipePhysicalOutputExplanation `json:"outputs"` +} + +type DataframeRecipePhysicalOutputExplanation struct { + Name string `json:"name"` + PlanFingerprint string `json:"planFingerprint"` + Columns []string `json:"columns"` + TraversalSets int `json:"traversalSets"` + EndpointTraversalCount int `json:"endpointTraversalCount"` + NativeTraversalCount int `json:"nativeTraversalCount"` + SharedTraversalCount int `json:"sharedTraversalCount"` + RequiredMatchReuseCount int `json:"requiredMatchReuseCount"` + Optimization *DataframeRecipeOptimizationExplanation `json:"optimization"` + Live *DataframeRecipeArangoAssessment `json:"live,omitempty"` +} + +type DataframeRecipePreflight struct { + Name string `json:"name"` + RecipeDigest string `json:"recipeDigest"` + ResolvedSchemaDigest string `json:"resolvedSchemaDigest"` + SourceGeneration string `json:"sourceGeneration"` + ScopeDigest string `json:"scopeDigest"` + Columns []*DataframeRecipeColumn `json:"columns"` +} + +type DataframeRecipePreview struct { + Name string `json:"name"` + RecipeDigest string `json:"recipeDigest"` + ResolvedSchemaDigest string `json:"resolvedSchemaDigest"` + SourceGeneration string `json:"sourceGeneration"` + Outputs []*DataframeRecipePreviewOutput `json:"outputs"` +} + +type DataframeRecipePreviewOutput struct { + Name string `json:"name"` + Columns []string `json:"columns"` + Rows json.RawMessage `json:"rows"` + CSV *string `json:"csv,omitempty"` + RowCount int `json:"rowCount"` +} + +type DataframeRecipeResult struct { + Name string `json:"name"` + RecipeDigest string `json:"recipeDigest"` + ResolvedSchemaDigest string `json:"resolvedSchemaDigest"` + SourceGeneration string `json:"sourceGeneration"` + Outputs []*DataframeRecipeResultOutput `json:"outputs"` +} + +type DataframeRecipeResultOutput struct { + Name string `json:"name"` + Columns []string `json:"columns"` + Rows json.RawMessage `json:"rows"` + CSV *string `json:"csv,omitempty"` + RowCount int `json:"rowCount"` +} + +type DataframeRecipeValidation struct { + Name string `json:"name"` + RecipeDigest string `json:"recipeDigest"` + TranslationVersion string `json:"translationVersion"` + Outputs []*DataframeRecipeOutputValidation `json:"outputs"` +} + type DataframeRelatedResourceHints struct { ViaLabel string `json:"viaLabel"` EdgeCount int `json:"edgeCount"` @@ -166,11 +380,13 @@ type DataframeRowConnection struct { Materialization *DataframeMaterialization `json:"materialization"` Columns []string `json:"columns"` Rows json.RawMessage `json:"rows"` + TotalCount *int `json:"totalCount,omitempty"` PageInfo *DataframePageInfo `json:"pageInfo"` } type DataframeRowsInput struct { - MaterializationID string `json:"materializationId"` + MaterializationID *string `json:"materializationId,omitempty"` + DataType string `json:"dataType"` Columns []string `json:"columns,omitempty"` Filters []*DataframeFilterInput `json:"filters,omitempty"` Sort *DataframeSortInput `json:"sort,omitempty"` @@ -190,6 +406,12 @@ type DataframeTraversalHint struct { EdgeCount int `json:"edgeCount"` } +type ExplainDataframeRecipeInput struct { + Name string `json:"name"` + Bindings *DataframeRecipeBindingsInput `json:"bindings"` + Live *bool `json:"live,omitempty"` +} + type FhirAggregateInput struct { Name string `json:"name"` Operation FhirAggregateOperation `json:"operation"` @@ -201,18 +423,37 @@ type FhirAggregateInput struct { ValueMode FhirValueMode `json:"valueMode"` } +type FhirCatalogProjectionInput struct { + Name string `json:"name"` + IncludePaths []string `json:"includePaths"` + ExcludePaths []string `json:"excludePaths"` + Kinds []string `json:"kinds"` + Naming FhirColumnNaming `json:"naming"` + ValueMode FhirValueMode `json:"valueMode"` + MaxColumns int `json:"maxColumns"` +} + +type FhirCodeValueInput struct { + System *string `json:"system,omitempty"` + Code string `json:"code"` + Display *string `json:"display,omitempty"` +} + type FhirDataframeInput struct { - Project string `json:"project"` - AuthResourcePath *string `json:"authResourcePath,omitempty"` - AuthResourcePaths []string `json:"authResourcePaths,omitempty"` - RootResourceType string `json:"rootResourceType"` - RootFields []*FhirFieldSelectInput `json:"rootFields,omitempty"` - RootPivots []*FhirPivotInput `json:"rootPivots,omitempty"` - RootAggregates []*FhirAggregateInput `json:"rootAggregates,omitempty"` - RootSlices []*FhirRepresentativeSliceInput `json:"rootSlices,omitempty"` - Traverse []*FhirTraversalStepInput `json:"traverse,omitempty"` - Limit *int `json:"limit,omitempty"` - Cursor *string `json:"cursor,omitempty"` + Project string `json:"project"` + AuthResourcePath *string `json:"authResourcePath,omitempty"` + AuthResourcePaths []string `json:"authResourcePaths,omitempty"` + RootResourceType string `json:"rootResourceType"` + RowGrain *string `json:"rowGrain,omitempty"` + RootFields []*FhirFieldSelectInput `json:"rootFields,omitempty"` + RootFilters []*FhirFilterInput `json:"rootFilters,omitempty"` + RootPivots []*FhirPivotInput `json:"rootPivots,omitempty"` + RootAggregates []*FhirAggregateInput `json:"rootAggregates,omitempty"` + RootSlices []*FhirRepresentativeSliceInput `json:"rootSlices,omitempty"` + RootCatalogProjections []*FhirCatalogProjectionInput `json:"rootCatalogProjections,omitempty"` + Traverse []*FhirTraversalStepInput `json:"traverse,omitempty"` + Limit *int `json:"limit,omitempty"` + Cursor *string `json:"cursor,omitempty"` } type FhirDataframeResult struct { @@ -244,12 +485,38 @@ type FhirFieldSelectorInput struct { ValuePath string `json:"valuePath"` } +type FhirFilterInput struct { + Select string `json:"select"` + FieldRef *string `json:"fieldRef,omitempty"` + Operator FhirFilterOperator `json:"operator"` + Quantifier *FhirFilterQuantifier `json:"quantifier,omitempty"` + Values []*FhirFilterValueInput `json:"values"` +} + +type FhirFilterValueInput struct { + Kind FhirFilterValueKind `json:"kind"` + String *string `json:"string,omitempty"` + Code *FhirCodeValueInput `json:"code,omitempty"` + Boolean *bool `json:"boolean,omitempty"` + Integer *int `json:"integer,omitempty"` + Decimal *float64 `json:"decimal,omitempty"` + Date *string `json:"date,omitempty"` + DateTime *string `json:"dateTime,omitempty"` +} + +type FhirPivotDiscoveryInput struct { + Family *string `json:"family,omitempty"` + Path *string `json:"path,omitempty"` + MaxColumns int `json:"maxColumns"` +} + type FhirPivotInput struct { - Name string `json:"name"` - FieldRef *string `json:"fieldRef,omitempty"` - ColumnSelector *FhirFieldSelectorInput `json:"columnSelector,omitempty"` - ValueSelector *FhirFieldSelectorInput `json:"valueSelector,omitempty"` - Columns []string `json:"columns"` + Name string `json:"name"` + FieldRef *string `json:"fieldRef,omitempty"` + ColumnSelector *FhirFieldSelectorInput `json:"columnSelector,omitempty"` + ValueSelector *FhirFieldSelectorInput `json:"valueSelector,omitempty"` + Columns []string `json:"columns"` + Discovery *FhirPivotDiscoveryInput `json:"discovery,omitempty"` } type FhirRepresentativeSliceInput struct { @@ -262,22 +529,53 @@ type FhirRepresentativeSliceInput struct { } type FhirTraversalStepInput struct { - EdgeLabel string `json:"edgeLabel"` - ToResourceType string `json:"toResourceType"` - Alias string `json:"alias"` - Fields []*FhirFieldSelectInput `json:"fields"` - Pivots []*FhirPivotInput `json:"pivots,omitempty"` - Aggregates []*FhirAggregateInput `json:"aggregates,omitempty"` - Slices []*FhirRepresentativeSliceInput `json:"slices,omitempty"` - Traverse []*FhirTraversalStepInput `json:"traverse,omitempty"` + EdgeLabel string `json:"edgeLabel"` + ToResourceType string `json:"toResourceType"` + Alias string `json:"alias"` + MatchMode *FhirTraversalMatchMode `json:"matchMode,omitempty"` + Fields []*FhirFieldSelectInput `json:"fields"` + Filters []*FhirFilterInput `json:"filters,omitempty"` + Pivots []*FhirPivotInput `json:"pivots,omitempty"` + Aggregates []*FhirAggregateInput `json:"aggregates,omitempty"` + Slices []*FhirRepresentativeSliceInput `json:"slices,omitempty"` + CatalogProjections []*FhirCatalogProjectionInput `json:"catalogProjections,omitempty"` + Traverse []*FhirTraversalStepInput `json:"traverse,omitempty"` +} + +type MaterializeDataframeRecipeInput struct { + Name string `json:"name"` + Bindings *DataframeRecipeBindingsInput `json:"bindings"` } type Mutation struct { } +type PreflightDataframeRecipeInput struct { + Name string `json:"name"` + Bindings *DataframeRecipeBindingsInput `json:"bindings"` +} + +type PreviewDataframeRecipeInput struct { + Name string `json:"name"` + Bindings *DataframeRecipeBindingsInput `json:"bindings"` + Limit *int `json:"limit,omitempty"` + Outputs []string `json:"outputs,omitempty"` +} + type Query struct { } +type RunDataframeRecipeInput struct { + Name string `json:"name"` + Bindings *DataframeRecipeBindingsInput `json:"bindings"` + Outputs []string `json:"outputs,omitempty"` +} + +type ValidateDataframeRecipeInput struct { + Name string `json:"name"` + Bindings *DataframeRecipeBindingsInput `json:"bindings"` +} + type DataframeMaterializationState string const ( @@ -323,6 +621,81 @@ func (e DataframeMaterializationState) MarshalGQL(w io.Writer) { fmt.Fprint(w, strconv.Quote(e.String())) } +func (e *DataframeMaterializationState) UnmarshalJSON(b []byte) error { + s, err := strconv.Unquote(string(b)) + if err != nil { + return err + } + return e.UnmarshalGQL(s) +} + +func (e DataframeMaterializationState) MarshalJSON() ([]byte, error) { + var buf bytes.Buffer + e.MarshalGQL(&buf) + return buf.Bytes(), nil +} + +type DataframeRecipeExecutionState string + +const ( + DataframeRecipeExecutionStateAccepted DataframeRecipeExecutionState = "ACCEPTED" + DataframeRecipeExecutionStateValidating DataframeRecipeExecutionState = "VALIDATING" + DataframeRecipeExecutionStateRunning DataframeRecipeExecutionState = "RUNNING" + DataframeRecipeExecutionStateReady DataframeRecipeExecutionState = "READY" + DataframeRecipeExecutionStateFailed DataframeRecipeExecutionState = "FAILED" +) + +var AllDataframeRecipeExecutionState = []DataframeRecipeExecutionState{ + DataframeRecipeExecutionStateAccepted, + DataframeRecipeExecutionStateValidating, + DataframeRecipeExecutionStateRunning, + DataframeRecipeExecutionStateReady, + DataframeRecipeExecutionStateFailed, +} + +func (e DataframeRecipeExecutionState) IsValid() bool { + switch e { + case DataframeRecipeExecutionStateAccepted, DataframeRecipeExecutionStateValidating, DataframeRecipeExecutionStateRunning, DataframeRecipeExecutionStateReady, DataframeRecipeExecutionStateFailed: + return true + } + return false +} + +func (e DataframeRecipeExecutionState) String() string { + return string(e) +} + +func (e *DataframeRecipeExecutionState) UnmarshalGQL(v any) error { + str, ok := v.(string) + if !ok { + return fmt.Errorf("enums must be strings") + } + + *e = DataframeRecipeExecutionState(str) + if !e.IsValid() { + return fmt.Errorf("%s is not a valid DataframeRecipeExecutionState", str) + } + return nil +} + +func (e DataframeRecipeExecutionState) MarshalGQL(w io.Writer) { + fmt.Fprint(w, strconv.Quote(e.String())) +} + +func (e *DataframeRecipeExecutionState) UnmarshalJSON(b []byte) error { + s, err := strconv.Unquote(string(b)) + if err != nil { + return err + } + return e.UnmarshalGQL(s) +} + +func (e DataframeRecipeExecutionState) MarshalJSON() ([]byte, error) { + var buf bytes.Buffer + e.MarshalGQL(&buf) + return buf.Bytes(), nil +} + type FhirAggregateOperation string const ( @@ -368,6 +741,75 @@ func (e FhirAggregateOperation) MarshalGQL(w io.Writer) { fmt.Fprint(w, strconv.Quote(e.String())) } +func (e *FhirAggregateOperation) UnmarshalJSON(b []byte) error { + s, err := strconv.Unquote(string(b)) + if err != nil { + return err + } + return e.UnmarshalGQL(s) +} + +func (e FhirAggregateOperation) MarshalJSON() ([]byte, error) { + var buf bytes.Buffer + e.MarshalGQL(&buf) + return buf.Bytes(), nil +} + +type FhirColumnNaming string + +const ( + FhirColumnNamingPath FhirColumnNaming = "PATH" + FhirColumnNamingPathSuffix FhirColumnNaming = "PATH_SUFFIX" +) + +var AllFhirColumnNaming = []FhirColumnNaming{ + FhirColumnNamingPath, + FhirColumnNamingPathSuffix, +} + +func (e FhirColumnNaming) IsValid() bool { + switch e { + case FhirColumnNamingPath, FhirColumnNamingPathSuffix: + return true + } + return false +} + +func (e FhirColumnNaming) String() string { + return string(e) +} + +func (e *FhirColumnNaming) UnmarshalGQL(v any) error { + str, ok := v.(string) + if !ok { + return fmt.Errorf("enums must be strings") + } + + *e = FhirColumnNaming(str) + if !e.IsValid() { + return fmt.Errorf("%s is not a valid FhirColumnNaming", str) + } + return nil +} + +func (e FhirColumnNaming) MarshalGQL(w io.Writer) { + fmt.Fprint(w, strconv.Quote(e.String())) +} + +func (e *FhirColumnNaming) UnmarshalJSON(b []byte) error { + s, err := strconv.Unquote(string(b)) + if err != nil { + return err + } + return e.UnmarshalGQL(s) +} + +func (e FhirColumnNaming) MarshalJSON() ([]byte, error) { + var buf bytes.Buffer + e.MarshalGQL(&buf) + return buf.Bytes(), nil +} + type FhirFieldPredicateOperation string const ( @@ -407,6 +849,213 @@ func (e FhirFieldPredicateOperation) MarshalGQL(w io.Writer) { fmt.Fprint(w, strconv.Quote(e.String())) } +func (e *FhirFieldPredicateOperation) UnmarshalJSON(b []byte) error { + s, err := strconv.Unquote(string(b)) + if err != nil { + return err + } + return e.UnmarshalGQL(s) +} + +func (e FhirFieldPredicateOperation) MarshalJSON() ([]byte, error) { + var buf bytes.Buffer + e.MarshalGQL(&buf) + return buf.Bytes(), nil +} + +type FhirFilterOperator string + +const ( + FhirFilterOperatorEquals FhirFilterOperator = "EQUALS" + FhirFilterOperatorNotEquals FhirFilterOperator = "NOT_EQUALS" + FhirFilterOperatorIn FhirFilterOperator = "IN" + FhirFilterOperatorExists FhirFilterOperator = "EXISTS" + FhirFilterOperatorMissing FhirFilterOperator = "MISSING" + FhirFilterOperatorContainsText FhirFilterOperator = "CONTAINS_TEXT" + FhirFilterOperatorGt FhirFilterOperator = "GT" + FhirFilterOperatorGte FhirFilterOperator = "GTE" + FhirFilterOperatorLt FhirFilterOperator = "LT" + FhirFilterOperatorLte FhirFilterOperator = "LTE" +) + +var AllFhirFilterOperator = []FhirFilterOperator{ + FhirFilterOperatorEquals, + FhirFilterOperatorNotEquals, + FhirFilterOperatorIn, + FhirFilterOperatorExists, + FhirFilterOperatorMissing, + FhirFilterOperatorContainsText, + FhirFilterOperatorGt, + FhirFilterOperatorGte, + FhirFilterOperatorLt, + FhirFilterOperatorLte, +} + +func (e FhirFilterOperator) IsValid() bool { + switch e { + case FhirFilterOperatorEquals, FhirFilterOperatorNotEquals, FhirFilterOperatorIn, FhirFilterOperatorExists, FhirFilterOperatorMissing, FhirFilterOperatorContainsText, FhirFilterOperatorGt, FhirFilterOperatorGte, FhirFilterOperatorLt, FhirFilterOperatorLte: + return true + } + return false +} + +func (e FhirFilterOperator) String() string { + return string(e) +} + +func (e *FhirFilterOperator) UnmarshalGQL(v any) error { + str, ok := v.(string) + if !ok { + return fmt.Errorf("enums must be strings") + } + + *e = FhirFilterOperator(str) + if !e.IsValid() { + return fmt.Errorf("%s is not a valid FhirFilterOperator", str) + } + return nil +} + +func (e FhirFilterOperator) MarshalGQL(w io.Writer) { + fmt.Fprint(w, strconv.Quote(e.String())) +} + +func (e *FhirFilterOperator) UnmarshalJSON(b []byte) error { + s, err := strconv.Unquote(string(b)) + if err != nil { + return err + } + return e.UnmarshalGQL(s) +} + +func (e FhirFilterOperator) MarshalJSON() ([]byte, error) { + var buf bytes.Buffer + e.MarshalGQL(&buf) + return buf.Bytes(), nil +} + +type FhirFilterQuantifier string + +const ( + FhirFilterQuantifierAny FhirFilterQuantifier = "ANY" + FhirFilterQuantifierAll FhirFilterQuantifier = "ALL" + FhirFilterQuantifierNone FhirFilterQuantifier = "NONE" +) + +var AllFhirFilterQuantifier = []FhirFilterQuantifier{ + FhirFilterQuantifierAny, + FhirFilterQuantifierAll, + FhirFilterQuantifierNone, +} + +func (e FhirFilterQuantifier) IsValid() bool { + switch e { + case FhirFilterQuantifierAny, FhirFilterQuantifierAll, FhirFilterQuantifierNone: + return true + } + return false +} + +func (e FhirFilterQuantifier) String() string { + return string(e) +} + +func (e *FhirFilterQuantifier) UnmarshalGQL(v any) error { + str, ok := v.(string) + if !ok { + return fmt.Errorf("enums must be strings") + } + + *e = FhirFilterQuantifier(str) + if !e.IsValid() { + return fmt.Errorf("%s is not a valid FhirFilterQuantifier", str) + } + return nil +} + +func (e FhirFilterQuantifier) MarshalGQL(w io.Writer) { + fmt.Fprint(w, strconv.Quote(e.String())) +} + +func (e *FhirFilterQuantifier) UnmarshalJSON(b []byte) error { + s, err := strconv.Unquote(string(b)) + if err != nil { + return err + } + return e.UnmarshalGQL(s) +} + +func (e FhirFilterQuantifier) MarshalJSON() ([]byte, error) { + var buf bytes.Buffer + e.MarshalGQL(&buf) + return buf.Bytes(), nil +} + +type FhirFilterValueKind string + +const ( + FhirFilterValueKindString FhirFilterValueKind = "STRING" + FhirFilterValueKindCode FhirFilterValueKind = "CODE" + FhirFilterValueKindBoolean FhirFilterValueKind = "BOOLEAN" + FhirFilterValueKindInteger FhirFilterValueKind = "INTEGER" + FhirFilterValueKindDecimal FhirFilterValueKind = "DECIMAL" + FhirFilterValueKindDate FhirFilterValueKind = "DATE" + FhirFilterValueKindDateTime FhirFilterValueKind = "DATE_TIME" +) + +var AllFhirFilterValueKind = []FhirFilterValueKind{ + FhirFilterValueKindString, + FhirFilterValueKindCode, + FhirFilterValueKindBoolean, + FhirFilterValueKindInteger, + FhirFilterValueKindDecimal, + FhirFilterValueKindDate, + FhirFilterValueKindDateTime, +} + +func (e FhirFilterValueKind) IsValid() bool { + switch e { + case FhirFilterValueKindString, FhirFilterValueKindCode, FhirFilterValueKindBoolean, FhirFilterValueKindInteger, FhirFilterValueKindDecimal, FhirFilterValueKindDate, FhirFilterValueKindDateTime: + return true + } + return false +} + +func (e FhirFilterValueKind) String() string { + return string(e) +} + +func (e *FhirFilterValueKind) UnmarshalGQL(v any) error { + str, ok := v.(string) + if !ok { + return fmt.Errorf("enums must be strings") + } + + *e = FhirFilterValueKind(str) + if !e.IsValid() { + return fmt.Errorf("%s is not a valid FhirFilterValueKind", str) + } + return nil +} + +func (e FhirFilterValueKind) MarshalGQL(w io.Writer) { + fmt.Fprint(w, strconv.Quote(e.String())) +} + +func (e *FhirFilterValueKind) UnmarshalJSON(b []byte) error { + s, err := strconv.Unquote(string(b)) + if err != nil { + return err + } + return e.UnmarshalGQL(s) +} + +func (e FhirFilterValueKind) MarshalJSON() ([]byte, error) { + var buf bytes.Buffer + e.MarshalGQL(&buf) + return buf.Bytes(), nil +} + type FhirPivotFamily string const ( @@ -448,6 +1097,75 @@ func (e FhirPivotFamily) MarshalGQL(w io.Writer) { fmt.Fprint(w, strconv.Quote(e.String())) } +func (e *FhirPivotFamily) UnmarshalJSON(b []byte) error { + s, err := strconv.Unquote(string(b)) + if err != nil { + return err + } + return e.UnmarshalGQL(s) +} + +func (e FhirPivotFamily) MarshalJSON() ([]byte, error) { + var buf bytes.Buffer + e.MarshalGQL(&buf) + return buf.Bytes(), nil +} + +type FhirTraversalMatchMode string + +const ( + FhirTraversalMatchModeOptional FhirTraversalMatchMode = "OPTIONAL" + FhirTraversalMatchModeRequired FhirTraversalMatchMode = "REQUIRED" +) + +var AllFhirTraversalMatchMode = []FhirTraversalMatchMode{ + FhirTraversalMatchModeOptional, + FhirTraversalMatchModeRequired, +} + +func (e FhirTraversalMatchMode) IsValid() bool { + switch e { + case FhirTraversalMatchModeOptional, FhirTraversalMatchModeRequired: + return true + } + return false +} + +func (e FhirTraversalMatchMode) String() string { + return string(e) +} + +func (e *FhirTraversalMatchMode) UnmarshalGQL(v any) error { + str, ok := v.(string) + if !ok { + return fmt.Errorf("enums must be strings") + } + + *e = FhirTraversalMatchMode(str) + if !e.IsValid() { + return fmt.Errorf("%s is not a valid FhirTraversalMatchMode", str) + } + return nil +} + +func (e FhirTraversalMatchMode) MarshalGQL(w io.Writer) { + fmt.Fprint(w, strconv.Quote(e.String())) +} + +func (e *FhirTraversalMatchMode) UnmarshalJSON(b []byte) error { + s, err := strconv.Unquote(string(b)) + if err != nil { + return err + } + return e.UnmarshalGQL(s) +} + +func (e FhirTraversalMatchMode) MarshalJSON() ([]byte, error) { + var buf bytes.Buffer + e.MarshalGQL(&buf) + return buf.Bytes(), nil +} + type FhirValueMode string const ( @@ -492,3 +1210,17 @@ func (e *FhirValueMode) UnmarshalGQL(v any) error { func (e FhirValueMode) MarshalGQL(w io.Writer) { fmt.Fprint(w, strconv.Quote(e.String())) } + +func (e *FhirValueMode) UnmarshalJSON(b []byte) error { + s, err := strconv.Unquote(string(b)) + if err != nil { + return err + } + return e.UnmarshalGQL(s) +} + +func (e FhirValueMode) MarshalJSON() ([]byte, error) { + var buf bytes.Buffer + e.MarshalGQL(&buf) + return buf.Bytes(), nil +} diff --git a/graphqlapi/query/active_generation_test.go b/graphqlapi/query/active_generation_test.go index c9bd53d..aa04b29 100644 --- a/graphqlapi/query/active_generation_test.go +++ b/graphqlapi/query/active_generation_test.go @@ -114,8 +114,8 @@ func TestActiveGenerationPropagatesThroughBuilderCatalogEntrypoints(t *testing.T }); err != nil { t.Fatalf("Introspect() error = %v", err) } - if len(active.projects) < 4 { - t.Fatalf("active resolver calls = %#v, want one selection for each live builder/dataframe entrypoint", active.projects) + if len(active.projects) < 3 { + t.Fatalf("active resolver calls = %#v, want one selection for each live dataframe entrypoint", active.projects) } if len(fieldCalls) == 0 || len(referenceCalls) == 0 { t.Fatalf("catalog calls fields=%d references=%d, want both", len(fieldCalls), len(referenceCalls)) diff --git a/graphqlapi/query/auth_scope_test.go b/graphqlapi/query/auth_scope_test.go index 441cf4c..4b755ec 100644 --- a/graphqlapi/query/auth_scope_test.go +++ b/graphqlapi/query/auth_scope_test.go @@ -34,14 +34,12 @@ func dataframeBuilderRestrictedEmptyContext() context.Context { func TestRunPreservesRestrictedEmptyScopeIntoDataframeService(t *testing.T) { resolver := dataframeBuilderRestrictedEmptyScopeResolver() preparedCatalogCalls := 0 - dataframeCatalogCalls := 0 dataframes := dataframe.NewService(dataframe.ServiceConfig{ - // This intentionally has no ScopeResolver. It proves the marker carried - // by dataframebuilder survives into a separately configured dataframe - // service instead of being reinterpreted as unrestricted. + // This intentionally has no ScopeResolver. The one-shot GraphQL recipe + // is already catalog- and scope-resolved before reaching execution, so + // the runtime must not perform a second recipe preparation pass. DiscoverFields: func(_ context.Context, options catalog.PopulatedFieldOptions) ([]catalog.PopulatedField, error) { - dataframeCatalogCalls++ assertRestrictedEmptyFieldScope(t, options) return []catalog.PopulatedField{}, nil }, @@ -76,8 +74,8 @@ func TestRunPreservesRestrictedEmptyScopeIntoDataframeService(t *testing.T) { if result.RowCount != 0 { t.Fatalf("result row count = %d, want no fake rows", result.RowCount) } - if preparedCatalogCalls == 0 || dataframeCatalogCalls == 0 { - t.Fatalf("catalog calls = prepared %d dataframe %d, want both paths", preparedCatalogCalls, dataframeCatalogCalls) + if preparedCatalogCalls == 0 { + t.Fatalf("prepared catalog calls = %d, want scoped preparation", preparedCatalogCalls) } } diff --git a/graphqlapi/query/helpers.go b/graphqlapi/query/helpers.go index 4c49d76..a7c7d4e 100644 --- a/graphqlapi/query/helpers.go +++ b/graphqlapi/query/helpers.go @@ -1,6 +1,37 @@ package queryapi -import "github.com/calypr/loom/internal/catalog" +import ( + "github.com/calypr/loom/graphqlapi/model" + "github.com/calypr/loom/internal/catalog" +) + +func derefString(value *string) string { + if value == nil { + return "" + } + return *value +} + +func predicatePathFromInput(input *model.FhirFieldPredicateInput) string { + if input == nil { + return "" + } + return input.Path +} + +func predicateOpFromInput(input *model.FhirFieldPredicateInput) string { + if input == nil { + return "" + } + return input.Op.String() +} + +func predicateValueFromInput(input *model.FhirFieldPredicateInput) string { + if input == nil { + return "" + } + return input.Value +} func cloneStrings(in []string) []string { if len(in) == 0 { diff --git a/graphqlapi/query/input_mapping.go b/graphqlapi/query/input_mapping.go deleted file mode 100644 index 959640f..0000000 --- a/graphqlapi/query/input_mapping.go +++ /dev/null @@ -1,197 +0,0 @@ -package queryapi - -import ( - "strings" - - "github.com/calypr/loom/graphqlapi/model" - "github.com/calypr/loom/internal/dataframe" -) - -func BuilderFromInput(in model.FhirDataframeInput) dataframe.Builder { - authResourcePaths := cloneStrings(in.AuthResourcePaths) - if len(authResourcePaths) == 0 && strings.TrimSpace(derefString(in.AuthResourcePath)) != "" { - authResourcePaths = []string{strings.TrimSpace(derefString(in.AuthResourcePath))} - } - return dataframe.Builder{ - Project: in.Project, - AuthResourcePaths: authResourcePaths, - RootResourceType: in.RootResourceType, - Fields: fieldSelectsFromModel(in.RootFields), - Pivots: pivotSelectsFromModel(in.RootPivots), - Aggregates: aggregateSelectsFromModel(in.RootAggregates), - Slices: sliceSelectsFromModel(in.RootSlices), - Traversals: traversalStepsFromModel(in.Traverse), - } -} - -func fieldSelectsFromModel(in []*model.FhirFieldSelectInput) []dataframe.FieldSelect { - if len(in) == 0 { - return []dataframe.FieldSelect{} - } - out := make([]dataframe.FieldSelect, 0, len(in)) - for _, item := range in { - if item == nil { - continue - } - selectText := "" - if item.Selector != nil { - selectText = composeSelector( - derefString(item.Selector.SourcePath), - predicatePathFromInput(item.Selector.Where), - predicateOpFromInput(item.Selector.Where), - predicateValueFromInput(item.Selector.Where), - item.Selector.ValuePath, - ) - } - fallbackSelectors := make([]string, 0, len(item.FallbackSelectors)) - for _, fallback := range item.FallbackSelectors { - if fallback == nil { - continue - } - fallbackSelectors = append(fallbackSelectors, composeSelector( - derefString(fallback.SourcePath), - predicatePathFromInput(fallback.Where), - predicateOpFromInput(fallback.Where), - predicateValueFromInput(fallback.Where), - fallback.ValuePath, - )) - } - out = append(out, dataframe.FieldSelect{ - Name: item.Name, - FieldRef: derefString(item.FieldRef), - Select: selectText, - FallbackFieldRefs: cloneStrings(item.FallbackFieldRefs), - FallbackSelects: fallbackSelectors, - ValueMode: item.ValueMode.String(), - }) - } - return out -} - -func pivotSelectsFromModel(in []*model.FhirPivotInput) []dataframe.PivotSelect { - if len(in) == 0 { - return []dataframe.PivotSelect{} - } - out := make([]dataframe.PivotSelect, 0, len(in)) - for _, item := range in { - if item == nil { - continue - } - out = append(out, dataframe.PivotSelect{ - Name: item.Name, - FieldRef: derefString(item.FieldRef), - ColumnSelect: composeSelectorFromInput(item.ColumnSelector), - ValueSelect: composeSelectorFromInput(item.ValueSelector), - Columns: cloneStrings(item.Columns), - }) - } - return out -} - -func composeSelectorFromInput(in *model.FhirFieldSelectorInput) string { - if in == nil { - return "" - } - return composeSelector( - derefString(in.SourcePath), - predicatePathFromInput(in.Where), - predicateOpFromInput(in.Where), - predicateValueFromInput(in.Where), - in.ValuePath, - ) -} - -func aggregateSelectsFromModel(in []*model.FhirAggregateInput) []dataframe.AggregateSelect { - if len(in) == 0 { - return []dataframe.AggregateSelect{} - } - out := make([]dataframe.AggregateSelect, 0, len(in)) - for _, item := range in { - if item == nil { - continue - } - out = append(out, dataframe.AggregateSelect{ - Name: item.Name, - Operation: item.Operation.String(), - FieldRef: strings.TrimSpace(derefString(item.FieldRef)), - Select: strings.TrimSpace(derefString(item.FhirPath)), - PredicateFieldRef: strings.TrimSpace(derefString(item.PredicateFieldRef)), - PredicatePath: strings.TrimSpace(derefString(item.PredicatePath)), - PredicateEquals: derefString(item.PredicateEquals), - ValueMode: item.ValueMode.String(), - }) - } - return out -} - -func sliceSelectsFromModel(in []*model.FhirRepresentativeSliceInput) []dataframe.RepresentativeSlice { - if len(in) == 0 { - return []dataframe.RepresentativeSlice{} - } - out := make([]dataframe.RepresentativeSlice, 0, len(in)) - for _, item := range in { - if item == nil { - continue - } - out = append(out, dataframe.RepresentativeSlice{ - Name: item.Name, - Limit: item.Limit, - PredicateFieldRef: strings.TrimSpace(derefString(item.WhereFieldRef)), - PredicatePath: strings.TrimSpace(derefString(item.WherePath)), - PredicateEquals: derefString(item.WhereEquals), - Fields: fieldSelectsFromModel(item.Fields), - }) - } - return out -} - -func traversalStepsFromModel(in []*model.FhirTraversalStepInput) []dataframe.TraversalStep { - if len(in) == 0 { - return []dataframe.TraversalStep{} - } - out := make([]dataframe.TraversalStep, 0, len(in)) - for _, item := range in { - if item == nil { - continue - } - out = append(out, dataframe.TraversalStep{ - Label: item.EdgeLabel, - ToResourceType: item.ToResourceType, - Alias: item.Alias, - Fields: fieldSelectsFromModel(item.Fields), - Pivots: pivotSelectsFromModel(item.Pivots), - Aggregates: aggregateSelectsFromModel(item.Aggregates), - Slices: sliceSelectsFromModel(item.Slices), - Traversals: traversalStepsFromModel(item.Traverse), - }) - } - return out -} - -func derefString(in *string) string { - if in == nil { - return "" - } - return *in -} - -func predicatePathFromInput(in *model.FhirFieldPredicateInput) string { - if in == nil { - return "" - } - return in.Path -} - -func predicateOpFromInput(in *model.FhirFieldPredicateInput) string { - if in == nil { - return "" - } - return in.Op.String() -} - -func predicateValueFromInput(in *model.FhirFieldPredicateInput) string { - if in == nil { - return "" - } - return in.Value -} diff --git a/graphqlapi/query/input_mapping_test.go b/graphqlapi/query/input_mapping_test.go deleted file mode 100644 index 6141bb0..0000000 --- a/graphqlapi/query/input_mapping_test.go +++ /dev/null @@ -1,75 +0,0 @@ -package queryapi - -import ( - "testing" - - "github.com/calypr/loom/graphqlapi/model" -) - -func TestBuilderFromInputMapsAggregatesSlicesAndFallbacks(t *testing.T) { - valueMode := model.FhirValueModeAuto - op := model.FhirAggregateOperationDistinctValues - contains := model.FhirFieldPredicateOperationContains - input := model.FhirDataframeInput{ - Project: "P1", - RootResourceType: "Patient", - RootFields: []*model.FhirFieldSelectInput{ - { - Name: "case_id", - Selector: &model.FhirFieldSelectorInput{ - SourcePath: strPtr("identifier[]"), - Where: &model.FhirFieldPredicateInput{Path: "system", Op: contains, Value: "case_id"}, - ValuePath: "value", - }, - FallbackSelectors: []*model.FhirFieldSelectorInput{ - { - SourcePath: strPtr("identifier[]"), - Where: &model.FhirFieldPredicateInput{Path: "system", Op: contains, Value: "submitter_id"}, - ValuePath: "value", - }, - }, - ValueMode: valueMode, - }, - }, - Traverse: []*model.FhirTraversalStepInput{ - { - EdgeLabel: "subject_Patient", - ToResourceType: "Specimen", - Alias: "specimen", - Aggregates: []*model.FhirAggregateInput{ - {Name: "specimen_types", Operation: op, FhirPath: strPtr("type.coding[].display"), ValueMode: valueMode}, - }, - Slices: []*model.FhirRepresentativeSliceInput{ - { - Name: "sample_slice", - Limit: 2, - WherePath: strPtr("type.coding[].display"), - WhereEquals: strPtr("Primary Tumor"), - Fields: []*model.FhirFieldSelectInput{ - {Name: "id", Selector: &model.FhirFieldSelectorInput{ValuePath: "id"}, ValueMode: valueMode}, - }, - }, - }, - }, - }, - } - - builder := BuilderFromInput(input) - if len(builder.Fields) != 1 || len(builder.Fields[0].FallbackSelects) != 1 { - t.Fatalf("unexpected root field mapping: %#v", builder.Fields) - } - if len(builder.Traversals) != 1 { - t.Fatalf("unexpected traversal mapping: %#v", builder.Traversals) - } - step := builder.Traversals[0] - if len(step.Aggregates) != 1 || step.Aggregates[0].Operation != "DISTINCT_VALUES" { - t.Fatalf("unexpected aggregate mapping: %#v", step.Aggregates) - } - if len(step.Slices) != 1 || step.Slices[0].PredicatePath != "type.coding[].display" || len(step.Slices[0].Fields) != 1 { - t.Fatalf("unexpected slice mapping: %#v", step.Slices) - } -} - -func strPtr(in string) *string { - return &in -} diff --git a/graphqlapi/query/input_resolution.go b/graphqlapi/query/input_resolution.go index 966d6c7..b33bef7 100644 --- a/graphqlapi/query/input_resolution.go +++ b/graphqlapi/query/input_resolution.go @@ -17,9 +17,8 @@ func (s *Service) PrepareRunInput(ctx context.Context, input model.FhirDataframe // prepareRunInput resolves field references and returns the effective scope // and selected generation alongside the GraphQL-shaped input. The public -// PrepareRunInput wrapper keeps its compatibility return type, while Run uses -// this private form so it can carry a restricted empty scope and generation -// into dataframe.Builder without exposing generation in GraphQL. +// PrepareRunInput keeps the GraphQL transport shape while Run carries the +// resolved scope and generation into recipe.RuntimeBindings. func (s *Service) prepareRunInput(ctx context.Context, input model.FhirDataframeInput) (model.FhirDataframeInput, authscope.ReadScope, string, error) { if input.Project == "" { return input, authscope.ReadScope{}, "", fmt.Errorf("project is required") @@ -45,7 +44,7 @@ func (s *Service) prepareRunInput(ctx context.Context, input model.FhirDataframe if len(input.AuthResourcePaths) == 0 { input.AuthResourcePaths = nil } - if err := s.resolveNodeInputRefs(ctx, input.Project, generation, scope, input.RootResourceType, input.RootFields, input.RootPivots, input.RootAggregates, input.RootSlices); err != nil { + if err := s.resolveNodeInputRefs(ctx, input.Project, generation, scope, input.RootResourceType, input.RootFields, input.RootFilters, input.RootPivots, input.RootAggregates, input.RootSlices); err != nil { return input, authscope.ReadScope{}, "", err } for _, step := range input.Traverse { @@ -60,7 +59,7 @@ func (s *Service) resolveTraversalInputRefs(ctx context.Context, project, datase if step == nil { return nil } - if err := s.resolveNodeInputRefs(ctx, project, datasetGeneration, scope, step.ToResourceType, step.Fields, step.Pivots, step.Aggregates, step.Slices); err != nil { + if err := s.resolveNodeInputRefs(ctx, project, datasetGeneration, scope, step.ToResourceType, step.Fields, step.Filters, step.Pivots, step.Aggregates, step.Slices); err != nil { return err } for _, child := range step.Traverse { @@ -71,7 +70,7 @@ func (s *Service) resolveTraversalInputRefs(ctx context.Context, project, datase return nil } -func (s *Service) resolveNodeInputRefs(ctx context.Context, project, datasetGeneration string, scope authscope.ReadScope, resourceType string, fields []*model.FhirFieldSelectInput, pivots []*model.FhirPivotInput, aggregates []*model.FhirAggregateInput, slices []*model.FhirRepresentativeSliceInput) error { +func (s *Service) resolveNodeInputRefs(ctx context.Context, project, datasetGeneration string, scope authscope.ReadScope, resourceType string, fields []*model.FhirFieldSelectInput, filters []*model.FhirFilterInput, pivots []*model.FhirPivotInput, aggregates []*model.FhirAggregateInput, slices []*model.FhirRepresentativeSliceInput) error { discovered, err := s.discoverFields(ctx, catalog.PopulatedFieldOptions{ ConnectionOptions: s.connOpts, Project: project, @@ -126,6 +125,17 @@ func (s *Service) resolveNodeInputRefs(ctx context.Context, project, datasetGene } } + for _, filter := range filters { + if filter == nil || strings.TrimSpace(derefString(filter.FieldRef)) == "" { + continue + } + selector, err := resolveFieldRef(resourceType, discovered, derefString(filter.FieldRef)) + if err != nil { + return err + } + filter.Select = selector + } + for _, aggregate := range aggregates { if aggregate == nil { continue diff --git a/graphqlapi/query/recipe_discovery.go b/graphqlapi/query/recipe_discovery.go new file mode 100644 index 0000000..629ad8a --- /dev/null +++ b/graphqlapi/query/recipe_discovery.go @@ -0,0 +1,51 @@ +package queryapi + +import ( + "context" + + "github.com/calypr/loom/internal/authscope" + "github.com/calypr/loom/internal/catalog" + "github.com/calypr/loom/internal/dataframe/recipe/schema" +) + +// recipeFieldDiscovery adapts the existing scoped field catalog to the +// backend-neutral recipe schema resolver. The GraphQL one-shot path and the +// stored-recipe server path therefore resolve catalog declarations with the +// same project, generation, and authorization semantics. +type recipeFieldDiscovery struct { + read func(context.Context, catalog.PopulatedFieldOptions) ([]catalog.PopulatedField, error) +} + +func (d recipeFieldDiscovery) Fields(ctx context.Context, scope schema.Scope, resourceType string) ([]schema.FieldCandidate, error) { + var unrestricted *bool + switch authscope.ReadScopeMode(scope.AuthScopeMode) { + case authscope.ReadScopeUnrestricted: + value := true + unrestricted = &value + case authscope.ReadScopeRestricted: + value := false + unrestricted = &value + } + read := d.read + if read == nil { + read = catalog.DiscoverPopulatedFields + } + fields, err := read(ctx, catalog.PopulatedFieldOptions{ + Project: scope.Project, DatasetGeneration: scope.DatasetGeneration, + AuthResourcePaths: append([]string(nil), scope.AuthResourcePaths...), + AuthResourcePathsUnrestricted: unrestricted, ResourceType: resourceType, + }) + if err != nil { + return nil, err + } + result := make([]schema.FieldCandidate, 0, len(fields)) + for _, field := range fields { + result = append(result, schema.FieldCandidate{ + ResourceType: field.ResourceType, Path: field.Path, Kind: field.Kind, + DistinctValues: append([]string(nil), field.DistinctValues...), DistinctTruncated: field.DistinctTruncated, PivotCandidate: field.PivotCandidate, + PivotFamily: field.PivotFamily, PivotColumns: append([]string(nil), field.PivotColumns...), + PivotColumnSelect: field.PivotColumnSelect, PivotValueSelect: field.PivotValueSelect, + }) + } + return result, nil +} diff --git a/graphqlapi/query/recipe_mapping.go b/graphqlapi/query/recipe_mapping.go new file mode 100644 index 0000000..f42f503 --- /dev/null +++ b/graphqlapi/query/recipe_mapping.go @@ -0,0 +1,295 @@ +package queryapi + +// This file is the transport-only GraphQL -> recipe boundary. It copies the +// already resolved GraphQL selections into the canonical recipe wire types; +// semantic validation and compilation remain in the recipe/semantic packages. + +import ( + "fmt" + "strings" + + "github.com/calypr/loom/graphqlapi/model" + "github.com/calypr/loom/internal/dataframe/recipe" + "github.com/calypr/loom/internal/dataframe/spec" +) + +// RecipeBundleFromInput creates the ephemeral one-output recipe used by a +// one-shot GraphQL dataframe request. Runtime scope (project, generation, +// authorization, and limit) is deliberately not copied into the bundle; the +// caller supplies it through recipe.RuntimeBindings. +// +// Field references must have been resolved by PrepareRunInput first. Keeping +// that boundary explicit prevents an unresolved catalog name from becoming an +// executable selector or an accidental storage-specific path. +func RecipeBundleFromInput(in model.FhirDataframeInput) (recipe.Bundle, error) { + if strings.TrimSpace(in.RootResourceType) == "" { + return recipe.Bundle{}, fmt.Errorf("rootResourceType is required") + } + grain, ok := spec.InferRowGrain(in.RootResourceType) + if in.RowGrain != nil { + grain = spec.RowGrain(strings.TrimSpace(*in.RowGrain)) + ok = true + } + if !ok || strings.TrimSpace(string(grain)) == "" { + return recipe.Bundle{}, fmt.Errorf("no row grain is available for root resource type %q", in.RootResourceType) + } + output, err := recipeOutputFromInput(in.RootResourceType, in.RootFields, in.RootFilters, in.RootPivots, in.RootAggregates, in.RootSlices, in.RootCatalogProjections, in.Traverse) + if err != nil { + return recipe.Bundle{}, err + } + output.Name = "dataframe" + output.RowGrain = string(grain) + bundle := recipe.Bundle{ + RecipeSchemaVersion: recipe.CurrentSchemaVersion, + Name: "graphql_request", + TranslationVersion: "graphql-request", + Outputs: []recipe.Output{output}, + } + if err := bundle.Validate(); err != nil { + return recipe.Bundle{}, err + } + return bundle, nil +} + +func recipeOutputFromInput(resourceType string, fields []*model.FhirFieldSelectInput, filters []*model.FhirFilterInput, pivots []*model.FhirPivotInput, aggregates []*model.FhirAggregateInput, slices []*model.FhirRepresentativeSliceInput, catalogProjections []*model.FhirCatalogProjectionInput, traversals []*model.FhirTraversalStepInput) (recipe.Output, error) { + rf, err := recipeFieldsFromInput(fields) + if err != nil { + return recipe.Output{}, fmt.Errorf("root fields: %w", err) + } + rfilt, err := recipeFiltersFromInput(filters) + if err != nil { + return recipe.Output{}, fmt.Errorf("root filters: %w", err) + } + rp, err := recipePivotsFromInput(pivots) + if err != nil { + return recipe.Output{}, fmt.Errorf("root pivots: %w", err) + } + ra, err := recipeAggregatesFromInput(aggregates) + if err != nil { + return recipe.Output{}, fmt.Errorf("root aggregates: %w", err) + } + rs, err := recipeSlicesFromInput(slices) + if err != nil { + return recipe.Output{}, fmt.Errorf("root slices: %w", err) + } + rcp, err := recipeCatalogProjectionsFromInput(catalogProjections) + if err != nil { + return recipe.Output{}, fmt.Errorf("catalog projections: %w", err) + } + rt, err := recipeTraversalsFromInput(traversals) + if err != nil { + return recipe.Output{}, fmt.Errorf("traversals: %w", err) + } + return recipe.Output{RootResourceType: resourceType, Fields: rf, Filters: rfilt, Pivots: rp, Aggregates: ra, Slices: rs, CatalogProjections: rcp, Traversals: rt}, nil +} + +func recipeCatalogProjectionsFromInput(in []*model.FhirCatalogProjectionInput) ([]recipe.CatalogProjection, error) { + out := make([]recipe.CatalogProjection, 0, len(in)) + for index, item := range in { + if item == nil { + continue + } + projection := recipe.CatalogProjection{ + Name: item.Name, IncludePaths: cloneStrings(item.IncludePaths), ExcludePaths: cloneStrings(item.ExcludePaths), + Kinds: cloneStrings(item.Kinds), Naming: recipe.ColumnNaming(item.Naming.String()), ValueMode: recipe.ValueMode(item.ValueMode.String()), MaxColumns: item.MaxColumns, + } + if err := projection.Validate(); err != nil { + return nil, fmt.Errorf("catalogProjections[%d] %q: %w", index, item.Name, err) + } + out = append(out, projection) + } + return out, nil +} + +func recipeFieldsFromInput(in []*model.FhirFieldSelectInput) ([]recipe.Field, error) { + out := make([]recipe.Field, 0, len(in)) + for index, item := range in { + if item == nil { + continue + } + expr, err := recipeSelectorExpression(item.Selector) + if err != nil { + return nil, fmt.Errorf("fields[%d] %q: %w", index, item.Name, err) + } + field := recipe.Field{Name: item.Name, FieldRef: strings.TrimSpace(derefString(item.FieldRef)), Expr: expr, ValueMode: recipe.ValueMode(item.ValueMode.String())} + for fallbackIndex, fallback := range item.FallbackSelectors { + fallbackExpr, err := recipeSelectorExpression(fallback) + if err != nil { + return nil, fmt.Errorf("fields[%d] %q fallback[%d]: %w", index, item.Name, fallbackIndex, err) + } + field.Fallbacks = append(field.Fallbacks, fallbackExpr) + } + if len(item.FallbackFieldRefs) > 0 && len(item.FallbackFieldRefs) != len(field.Fallbacks) { + return nil, fmt.Errorf("fields[%d] %q has unresolved fallback field references", index, item.Name) + } + out = append(out, field) + } + return out, nil +} + +func recipeFiltersFromInput(in []*model.FhirFilterInput) ([]recipe.Filter, error) { + out := make([]recipe.Filter, 0, len(in)) + for index, item := range in { + if item == nil { + continue + } + filter := recipe.Filter{ + Select: strings.TrimSpace(item.Select), + FieldRef: strings.TrimSpace(derefString(item.FieldRef)), + Operator: recipe.FilterOperator(item.Operator.String()), + } + if item.Quantifier != nil { + filter.Quantifier = recipe.ArrayQuantifier(item.Quantifier.String()) + } + for valueIndex, value := range item.Values { + converted, err := recipeFilterValue(value) + if err != nil { + return nil, fmt.Errorf("filters[%d].values[%d]: %w", index, valueIndex, err) + } + filter.Values = append(filter.Values, converted) + } + out = append(out, filter) + } + return out, nil +} + +func recipeFilterValue(input *model.FhirFilterValueInput) (recipe.FilterValue, error) { + if input == nil { + return recipe.FilterValue{}, fmt.Errorf("value is required") + } + value := recipe.FilterValue{Kind: recipe.FilterValueKind(input.Kind.String())} + switch input.Kind { + case model.FhirFilterValueKindString: + value.String = input.String + case model.FhirFilterValueKindCode: + if input.Code != nil { + value.Code = &recipe.CodeValue{System: derefString(input.Code.System), Code: input.Code.Code, Display: derefString(input.Code.Display)} + } + case model.FhirFilterValueKindBoolean: + value.Boolean = input.Boolean + case model.FhirFilterValueKindInteger: + if input.Integer != nil { + converted := int64(*input.Integer) + value.Integer = &converted + } + case model.FhirFilterValueKindDecimal: + value.Decimal = input.Decimal + case model.FhirFilterValueKindDate: + value.Date = input.Date + case model.FhirFilterValueKindDateTime: + value.DateTime = input.DateTime + default: + return recipe.FilterValue{}, fmt.Errorf("unsupported value kind %q", input.Kind) + } + if err := value.Validate(); err != nil { + return recipe.FilterValue{}, err + } + return value, nil +} + +func recipePivotsFromInput(in []*model.FhirPivotInput) ([]recipe.Pivot, error) { + out := make([]recipe.Pivot, 0, len(in)) + for index, item := range in { + if item == nil { + continue + } + column, err := recipeSelectorExpression(item.ColumnSelector) + if err != nil { + return nil, fmt.Errorf("pivots[%d] %q columnSelector: %w", index, item.Name, err) + } + value, err := recipeSelectorExpression(item.ValueSelector) + if err != nil { + return nil, fmt.Errorf("pivots[%d] %q valueSelector: %w", index, item.Name, err) + } + pivot := recipe.Pivot{Name: item.Name, FieldRef: strings.TrimSpace(derefString(item.FieldRef)), ColumnExpr: column, ValueExpr: value, Columns: cloneStrings(item.Columns)} + if item.Discovery != nil { + pivot.Discovery = &recipe.PivotDiscovery{Family: derefString(item.Discovery.Family), Path: derefString(item.Discovery.Path), MaxColumns: item.Discovery.MaxColumns} + } + out = append(out, pivot) + } + return out, nil +} + +func recipeAggregatesFromInput(in []*model.FhirAggregateInput) ([]recipe.Aggregate, error) { + out := make([]recipe.Aggregate, 0, len(in)) + for index, item := range in { + if item == nil { + continue + } + aggregate := recipe.Aggregate{Name: item.Name, Operation: recipe.AggregateOperation(item.Operation.String()), FieldRef: strings.TrimSpace(derefString(item.FieldRef)), ValueMode: recipe.ValueMode(item.ValueMode.String())} + path := strings.TrimSpace(derefString(item.FhirPath)) + if path != "" { + expr := recipe.Expression{Select: path} + aggregate.Expr = &expr + } + predicatePath := strings.TrimSpace(derefString(item.PredicatePath)) + predicateValue := derefString(item.PredicateEquals) + if predicatePath != "" || strings.TrimSpace(predicateValue) != "" { + if predicatePath == "" || strings.TrimSpace(predicateValue) == "" { + return nil, fmt.Errorf("aggregates[%d] %q has an incomplete predicate", index, item.Name) + } + aggregate.Where = recipeStringEqualityFilter(predicatePath, predicateValue) + } + out = append(out, aggregate) + } + return out, nil +} + +func recipeSlicesFromInput(in []*model.FhirRepresentativeSliceInput) ([]recipe.RepresentativeSlice, error) { + out := make([]recipe.RepresentativeSlice, 0, len(in)) + for index, item := range in { + if item == nil { + continue + } + fields, err := recipeFieldsFromInput(item.Fields) + if err != nil { + return nil, fmt.Errorf("slices[%d] %q fields: %w", index, item.Name, err) + } + slice := recipe.RepresentativeSlice{Name: item.Name, Limit: item.Limit, Fields: fields} + wherePath := strings.TrimSpace(derefString(item.WherePath)) + whereValue := derefString(item.WhereEquals) + if wherePath != "" || strings.TrimSpace(whereValue) != "" { + if wherePath == "" || strings.TrimSpace(whereValue) == "" { + return nil, fmt.Errorf("slices[%d] %q has an incomplete predicate", index, item.Name) + } + slice.Where = recipeStringEqualityFilter(wherePath, whereValue) + } + out = append(out, slice) + } + return out, nil +} + +func recipeTraversalsFromInput(in []*model.FhirTraversalStepInput) ([]recipe.Traversal, error) { + out := make([]recipe.Traversal, 0, len(in)) + for index, item := range in { + if item == nil { + continue + } + output, err := recipeOutputFromInput(item.ToResourceType, item.Fields, item.Filters, item.Pivots, item.Aggregates, item.Slices, item.CatalogProjections, item.Traverse) + if err != nil { + return nil, fmt.Errorf("traversals[%d] %q: %w", index, item.Alias, err) + } + matchMode := recipe.MatchOptional + if item.MatchMode != nil { + matchMode = recipe.TraversalMatchMode(item.MatchMode.String()) + } + out = append(out, recipe.Traversal{Name: item.EdgeLabel, ToResourceType: item.ToResourceType, Alias: item.Alias, MatchMode: matchMode, Fields: output.Fields, Filters: output.Filters, Pivots: output.Pivots, Aggregates: output.Aggregates, Slices: output.Slices, CatalogProjections: output.CatalogProjections, Traversals: output.Traversals}) + } + return out, nil +} + +func recipeSelectorExpression(in *model.FhirFieldSelectorInput) (recipe.Expression, error) { + if in == nil { + return recipe.Expression{}, fmt.Errorf("selector is required") + } + selectText := composeSelector(derefString(in.SourcePath), predicatePathFromInput(in.Where), predicateOpFromInput(in.Where), predicateValueFromInput(in.Where), in.ValuePath) + if strings.TrimSpace(selectText) == "" { + return recipe.Expression{}, fmt.Errorf("selector valuePath is required") + } + return recipe.Expression{Select: selectText}, nil +} + +func recipeStringEqualityFilter(path, value string) *recipe.Filter { + valueCopy := value + return &recipe.Filter{Select: path, Operator: recipe.FilterEquals, Values: []recipe.FilterValue{{Kind: recipe.FilterString, String: &valueCopy}}} +} diff --git a/graphqlapi/query/recipe_mapping_test.go b/graphqlapi/query/recipe_mapping_test.go new file mode 100644 index 0000000..8265698 --- /dev/null +++ b/graphqlapi/query/recipe_mapping_test.go @@ -0,0 +1,126 @@ +package queryapi + +import ( + "context" + "testing" + + "github.com/calypr/loom/graphqlapi/model" + "github.com/calypr/loom/internal/catalog" + "github.com/calypr/loom/internal/dataframe/recipe" + "github.com/calypr/loom/internal/dataframe/semantic" +) + +func TestRecipeBundleFromInputMapsRichGraphQLShape(t *testing.T) { + gender := "gender" + wantGender := "female" + input := model.FhirDataframeInput{ + Project: "P1", + RootResourceType: "Patient", + RootFields: []*model.FhirFieldSelectInput{{ + Name: "gender", FieldRef: &gender, + Selector: &model.FhirFieldSelectorInput{ValuePath: "gender"}, + }}, + RootFilters: []*model.FhirFilterInput{{ + Select: "gender", Operator: model.FhirFilterOperatorEquals, + Values: []*model.FhirFilterValueInput{{Kind: model.FhirFilterValueKindString, String: &wantGender}}, + }}, + RootAggregates: []*model.FhirAggregateInput{{ + Name: "condition_count", Operation: model.FhirAggregateOperationCount, + }}, + Traverse: []*model.FhirTraversalStepInput{{ + EdgeLabel: "subject_Patient", ToResourceType: "Condition", Alias: "condition", + MatchMode: func() *model.FhirTraversalMatchMode { value := model.FhirTraversalMatchModeRequired; return &value }(), + Filters: []*model.FhirFilterInput{{ + Select: "id", Operator: model.FhirFilterOperatorExists, + }}, + Aggregates: []*model.FhirAggregateInput{{Name: "count", Operation: model.FhirAggregateOperationCount}}, + }}, + } + bundle, err := RecipeBundleFromInput(input) + if err != nil { + t.Fatal(err) + } + if len(bundle.Outputs) != 1 || len(bundle.Outputs[0].Fields) != 1 || len(bundle.Outputs[0].Traversals) != 1 { + t.Fatalf("bundle shape = %#v", bundle) + } + if bundle.Outputs[0].RowGrain != "patient" || len(bundle.Outputs[0].Filters) != 1 || bundle.Outputs[0].Aggregates[0].Operation != recipe.AggregateCount { + t.Fatalf("bundle metadata = %#v", bundle.Outputs[0]) + } + if bundle.Outputs[0].Traversals[0].MatchMode != recipe.MatchRequired || len(bundle.Outputs[0].Traversals[0].Filters) != 1 { + t.Fatalf("traversal semantics = %#v", bundle.Outputs[0].Traversals[0]) + } + if _, err := semantic.BuildRecipePlan(bundle, recipe.RuntimeBindings{Project: "P1", DatasetGeneration: "generation"}); err != nil { + t.Fatalf("mapped bundle does not build semantically: %v", err) + } +} + +func TestRecipeBundleFromInputRejectsIncompleteAggregatePredicate(t *testing.T) { + path := "gender" + input := model.FhirDataframeInput{ + Project: "P1", RootResourceType: "Patient", + RootAggregates: []*model.FhirAggregateInput{{ + Name: "count", Operation: model.FhirAggregateOperationCount, + PredicatePath: &path, + }}, + } + if _, err := RecipeBundleFromInput(input); err == nil { + t.Fatal("expected incomplete aggregate predicate to fail") + } +} + +func TestRecipeBundleFromInputMapsCatalogResolutionContracts(t *testing.T) { + family := "observation_code_value" + path := "code.coding[].code" + input := model.FhirDataframeInput{ + Project: "P1", RootResourceType: "Patient", + RootCatalogProjections: []*model.FhirCatalogProjectionInput{{ + Name: "populated", IncludePaths: []string{"identifier.*"}, Kinds: []string{"scalar"}, + Naming: model.FhirColumnNamingPath, ValueMode: model.FhirValueModeFirst, MaxColumns: 32, + }}, + RootPivots: []*model.FhirPivotInput{{ + Name: "observations", ColumnSelector: &model.FhirFieldSelectorInput{ValuePath: "code"}, ValueSelector: &model.FhirFieldSelectorInput{ValuePath: "value"}, + Discovery: &model.FhirPivotDiscoveryInput{Family: &family, Path: &path, MaxColumns: 16}, + }}, + Traverse: []*model.FhirTraversalStepInput{{ + EdgeLabel: "subject_Patient", ToResourceType: "Observation", Alias: "observation", + CatalogProjections: []*model.FhirCatalogProjectionInput{{Name: "observation_fields", IncludePaths: []string{"value*"}, Naming: model.FhirColumnNamingPathSuffix, ValueMode: model.FhirValueModeFirst, MaxColumns: 8}}, + }}, + } + bundle, err := RecipeBundleFromInput(input) + if err != nil { + t.Fatal(err) + } + output := bundle.Outputs[0] + if len(output.CatalogProjections) != 1 || output.CatalogProjections[0].MaxColumns != 32 { + t.Fatalf("root catalog projection = %#v", output.CatalogProjections) + } + if output.Pivots[0].Discovery == nil || output.Pivots[0].Discovery.Family != family || output.Pivots[0].Discovery.Path != path { + t.Fatalf("pivot discovery = %#v", output.Pivots[0]) + } + if len(output.Traversals) != 1 || len(output.Traversals[0].CatalogProjections) != 1 { + t.Fatalf("traversal catalog projection = %#v", output.Traversals) + } +} + +func TestGraphQLRecipeResolutionUsesScopedCatalogBeforeSemanticTyping(t *testing.T) { + input := model.FhirDataframeInput{ + Project: "P1", RootResourceType: "Patient", + RootCatalogProjections: []*model.FhirCatalogProjectionInput{{Name: "populated", IncludePaths: []string{"gender"}, Kinds: []string{"scalar"}, Naming: model.FhirColumnNamingPath, ValueMode: model.FhirValueModeFirst, MaxColumns: 4}}, + } + bundle, err := RecipeBundleFromInput(input) + if err != nil { + t.Fatal(err) + } + var got catalog.PopulatedFieldOptions + service := NewService(Config{DiscoverFields: func(_ context.Context, options catalog.PopulatedFieldOptions) ([]catalog.PopulatedField, error) { + got = options + return []catalog.PopulatedField{{ResourceType: "Patient", Path: "gender", Kind: "scalar"}}, nil + }}) + resolved, err := service.resolveRecipeBundle(context.Background(), bundle, recipe.RuntimeBindings{Project: "P1", DatasetGeneration: "g", AuthResourcePaths: []string{"/controlled"}}) + if err != nil { + t.Fatal(err) + } + if got.Project != "P1" || got.DatasetGeneration != "g" || len(got.AuthResourcePaths) != 1 || len(resolved.Outputs[0].Fields) != 1 || resolved.Outputs[0].Fields[0].Name != "gender" { + t.Fatalf("scoped GraphQL resolution = options:%#v bundle:%#v", got, resolved) + } +} diff --git a/graphqlapi/query/recipe_resolve.go b/graphqlapi/query/recipe_resolve.go new file mode 100644 index 0000000..111a1d8 --- /dev/null +++ b/graphqlapi/query/recipe_resolve.go @@ -0,0 +1,20 @@ +package queryapi + +import ( + "context" + "fmt" + + "github.com/calypr/loom/internal/dataframe/recipe" + "github.com/calypr/loom/internal/dataframe/recipe/schema" +) + +func (s *Service) resolveRecipeBundle(ctx context.Context, bundle recipe.Bundle, bindings recipe.RuntimeBindings) (recipe.Bundle, error) { + resolved, err := schema.Resolve(ctx, bundle, schema.Scope{ + Project: bindings.Project, DatasetGeneration: bindings.DatasetGeneration, + AuthResourcePaths: append([]string(nil), bindings.AuthResourcePaths...), AuthScopeMode: string(bindings.AuthScopeMode), + }, recipeFieldDiscovery{read: s.discoverFields}) + if err != nil { + return recipe.Bundle{}, fmt.Errorf("resolve GraphQL recipe schema: %w", err) + } + return resolved.Bundle, nil +} diff --git a/graphqlapi/query/service.go b/graphqlapi/query/service.go index 603021f..3d8b280 100644 --- a/graphqlapi/query/service.go +++ b/graphqlapi/query/service.go @@ -2,12 +2,16 @@ package queryapi import ( "context" + "fmt" "time" "github.com/calypr/loom/graphqlapi/model" "github.com/calypr/loom/internal/authscope" "github.com/calypr/loom/internal/catalog" "github.com/calypr/loom/internal/dataframe" + "github.com/calypr/loom/internal/dataframe/compiler" + "github.com/calypr/loom/internal/dataframe/recipe" + "github.com/calypr/loom/internal/dataframe/semantic" "github.com/calypr/loom/internal/dataset" arangostore "github.com/calypr/loom/internal/store/arango" ) @@ -67,8 +71,6 @@ func NewService(cfg Config) *Service { } else { service.dataframes = dataframe.NewService(dataframe.ServiceConfig{ ConnectionOptions: cfg.ConnectionOptions, - DiscoverReferences: service.discoverReferences, - DiscoverFields: service.discoverFields, ScopeResolver: cfg.ScopeResolver, ActiveManifestResolver: cfg.ActiveManifestResolver, }) @@ -82,29 +84,56 @@ func (s *Service) Run(ctx context.Context, input model.FhirDataframeInput, limit if err != nil { return nil, err } + inputResolutionDuration := time.Since(started) rowLimit := 0 if limit != nil { rowLimit = *limit } else if normalizedInput.Limit != nil { rowLimit = *normalizedInput.Limit } - builder := BuilderFromInput(normalizedInput) - builder.DatasetGeneration = generation - // Preserve the scope mode that resolved the catalog references above. This - // matters when no catalog paths survive a restricted caller's intersection: - // an empty list alone would otherwise be legacy-unrestricted downstream. - builder.AuthScopeMode = scope.Mode - result, err := s.dataframes.Run(ctx, dataframe.RunRequest{ - Builder: builder, - Limit: rowLimit, - }) + if rowLimit <= 0 { + rowLimit = 25 + } + preparationStarted := time.Now() + bundle, err := RecipeBundleFromInput(normalizedInput) + if err != nil { + return nil, err + } + bindings := recipe.RuntimeBindings{ + Project: normalizedInput.Project, + DatasetGeneration: generation, + AuthResourcePaths: cloneStrings(scope.AuthResourcePaths), + AuthScopeMode: scope.Mode, + PreviewLimit: rowLimit, + } + bundle, err = s.resolveRecipeBundle(ctx, bundle, bindings) + if err != nil { + return nil, err + } + semanticPlan, err := semantic.BuildRecipePlan(bundle, bindings) if err != nil { return nil, err } - result.Diagnostics.InputResolution = time.Since(started) - result.Diagnostics.Total - if result.Diagnostics.InputResolution < 0 { - result.Diagnostics.InputResolution = 0 + resolved, err := semantic.ResolveRecipePlan(semanticPlan, "", generation) + if err != nil { + return nil, err + } + requestPreparationDuration := time.Since(preparationStarted) + compileStarted := time.Now() + queries, err := compiler.CompileResolvedRecipePlanWithPolicy(resolved, rowLimit, compiler.DefaultPhysicalOptimizationPolicy()) + if err != nil { + return nil, fmt.Errorf("compile GraphQL recipe: %w", err) + } + if len(queries) != 1 { + return nil, fmt.Errorf("GraphQL dataframe recipe produced %d outputs, want 1", len(queries)) + } + result, err := s.dataframes.RunCompiled(ctx, queries[0]) + if err != nil { + return nil, err } + result.Diagnostics.Compilation = time.Since(compileStarted) + result.Diagnostics.InputResolution = inputResolutionDuration + result.Diagnostics.RequestPreparation = requestPreparationDuration result.Diagnostics.Total = time.Since(started) return result, nil } diff --git a/graphqlapi/query/validation.go b/graphqlapi/query/validation.go index 683a982..522a011 100644 --- a/graphqlapi/query/validation.go +++ b/graphqlapi/query/validation.go @@ -2,9 +2,15 @@ package queryapi import ( "context" + "fmt" + "time" "github.com/calypr/loom/graphqlapi/model" "github.com/calypr/loom/internal/dataframe" + "github.com/calypr/loom/internal/dataframe/compiler" + "github.com/calypr/loom/internal/dataframe/recipe" + "github.com/calypr/loom/internal/dataframe/semantic" + dfspec "github.com/calypr/loom/internal/dataframe/spec" ) // ValidationResult is the transport-neutral dataframe adapter result used by @@ -32,41 +38,73 @@ type ValidationResult struct { // the same internal preparation/compiler boundary used by Run. It never opens // an Arango cursor or invokes row execution. func (s *Service) Validate(ctx context.Context, input model.FhirDataframeInput) (ValidationResult, error) { + started := time.Now() normalized, scope, generation, err := s.prepareRunInput(ctx, input) if err != nil { return ValidationResult{}, err } - builder := BuilderFromInput(normalized) - builder.DatasetGeneration = generation - builder.AuthScopeMode = scope.Mode limit := 0 if normalized.Limit != nil { limit = *normalized.Limit } - validated, err := s.dataframes.Validate(ctx, dataframe.ValidateRequest{Builder: builder, Limit: limit}) + if limit <= 0 { + limit = 25 + } + preparationStarted := time.Now() + bundle, err := RecipeBundleFromInput(normalized) + if err != nil { + return ValidationResult{}, err + } + bindings := recipe.RuntimeBindings{Project: normalized.Project, DatasetGeneration: generation, AuthResourcePaths: cloneStrings(scope.AuthResourcePaths), AuthScopeMode: scope.Mode, PreviewLimit: limit} + bundle, err = s.resolveRecipeBundle(ctx, bundle, bindings) if err != nil { return ValidationResult{}, err } - validatedWarnings := cloneValidationWarnings(validated.Warnings) - // The internal service owns the normalized builder. Keep the adapter's - // public normalized input as the fieldRef-resolved model so a caller can - // display or persist it without learning compiler selectors. + plan, err := semantic.BuildRecipePlan(bundle, bindings) + if err != nil { + return ValidationResult{}, err + } + resolved, err := semantic.ResolveRecipePlan(plan, "", generation) + if err != nil { + return ValidationResult{}, err + } + preparationDuration := time.Since(preparationStarted) + compileStarted := time.Now() + queries, err := compiler.CompileResolvedRecipePlanWithPolicy(resolved, limit, compiler.DefaultPhysicalOptimizationPolicy()) + if err != nil { + return ValidationResult{}, fmt.Errorf("compile GraphQL recipe: %w", err) + } + if len(queries) != 1 { + return ValidationResult{}, fmt.Errorf("GraphQL dataframe recipe produced %d outputs, want 1", len(queries)) + } + compiled := queries[0] + var identity *dataframe.RowIdentity + if value, ok := dfspec.DefaultRowIdentity(dfspec.RowGrain(resolved.SemanticPlan.Outputs[0].RowGrain)); ok { + identity = &value + } + warnings := make([]dataframe.ValidationWarning, 0, 1) + if len(normalized.RootFields) == 0 && len(normalized.RootPivots) == 0 && len(normalized.RootAggregates) == 0 && len(normalized.RootSlices) == 0 && len(normalized.Traverse) == 0 { + warnings = append(warnings, dataframe.ValidationWarning{Code: "NO_SELECTED_COLUMNS", Message: "No explicit fields, pivots, aggregates, slices, or traversals were selected; only the row identity will be returned."}) + } + compileDuration := time.Since(compileStarted) + // Keep the adapter's public normalized input as the fieldRef-resolved model + // so a caller can display or persist it without learning compiler selectors. return ValidationResult{ - Valid: validated.Valid, + Valid: true, NormalizedInput: normalized, - Project: validated.Project, - DatasetGeneration: validated.DatasetGeneration, - RootResourceType: validated.RootResourceType, - Limit: validated.Limit, - Columns: cloneStrings(validated.Columns), - PivotFields: cloneStrings(validated.PivotFields), - RowIdentity: cloneRowIdentity(validated.RowIdentity), - RequestFingerprint: validated.RequestFingerprint, - Warnings: validatedWarnings, - Plan: validated.Plan, - PreviewAllowed: validated.PreviewAllowed, - ExportAllowed: validated.ExportAllowed, - Diagnostics: validated.Diagnostics, + Project: compiled.Project, + DatasetGeneration: compiled.DatasetGeneration, + RootResourceType: compiled.RootResourceType, + Limit: compiled.Limit, + Columns: cloneStrings(compiled.Columns), + PivotFields: cloneStrings(compiled.PivotFields), + RowIdentity: cloneRowIdentity(identity), + RequestFingerprint: resolved.SemanticPlan.RecipeDigest, + Warnings: cloneValidationWarnings(warnings), + Plan: compiled.PlanDiagnostics, + PreviewAllowed: true, + ExportAllowed: true, + Diagnostics: dataframe.QueryDiagnostics{InputResolution: time.Since(started) - preparationDuration - compileDuration, RequestPreparation: preparationDuration, Compilation: compileDuration, Total: time.Since(started), Plan: compiled.PlanDiagnostics}, }, nil } diff --git a/graphqlapi/recipe_control_graphql.go b/graphqlapi/recipe_control_graphql.go new file mode 100644 index 0000000..280e85d --- /dev/null +++ b/graphqlapi/recipe_control_graphql.go @@ -0,0 +1,309 @@ +package graphqlapi + +import ( + "bytes" + "encoding/csv" + "encoding/json" + "fmt" + "sort" + "strconv" + + "github.com/calypr/loom/graphqlapi/model" + "github.com/calypr/loom/internal/dataframe/recipe" + "github.com/calypr/loom/internal/dataframe/recipe/control" + "github.com/calypr/loom/internal/dataframe/semantic" +) + +func requireRecipeControl(control RecipeControl) error { + if control == nil { + return fmt.Errorf("recipe control plane is not configured") + } + return nil +} + +func recipeGraphQLError(err error) error { + if err == nil { + return nil + } + return GraphQLError(err, "") +} + +func graphqlRecipeBindings(bindings *model.DataframeRecipeBindingsInput) (recipe.RuntimeBindings, error) { + if bindings == nil { + return recipe.RuntimeBindings{}, fmt.Errorf("recipe bindings are required") + } + value := recipeBindings(*bindings) + if value.PreviewLimit < 0 { + return recipe.RuntimeBindings{}, fmt.Errorf("preview limit must not be negative") + } + return value, nil +} + +func outputValidation(output semantic.OutputPlan) *model.DataframeRecipeOutputValidation { + fields := append([]string(nil), output.DeclaredOrder...) + if len(fields) == 0 { + for _, field := range output.Fields { + fields = append(fields, field.Name) + } + } + dynamic := make([]string, 0, len(output.DynamicMaps)) + for _, value := range output.DynamicMaps { + dynamic = append(dynamic, value.Name) + } + return &model.DataframeRecipeOutputValidation{ + Name: output.Name, RootResourceType: output.RootResourceType, + RowGrain: string(output.RowGrain), FieldNames: fields, DynamicColumns: dynamic, + } +} + +func planExplanation(plan semantic.RecipePlanExplanation, name string) *model.DataframeRecipeExplanation { + outputs := make([]*model.DataframeRecipeOutputExplanation, 0, len(plan.Outputs)) + for _, output := range plan.Outputs { + fields := make([]*model.DataframeRecipeExpressionExplanation, 0, len(output.Fields)) + for _, field := range output.Fields { + fields = append(fields, &model.DataframeRecipeExpressionExplanation{ + SourcePath: field.SourcePath, Context: field.Context, Kind: string(field.Kind), + ValueType: string(field.Type.Kind), Repeated: field.Type.Cardinality == "many", + Nullable: field.Type.Cardinality == "optional_one" || field.Type.Cardinality == "many", + }) + } + var identity *model.DataframeRecipeExpressionExplanation + if output.Identity != nil { + identity = &model.DataframeRecipeExpressionExplanation{ + SourcePath: output.Identity.SourcePath, Context: output.Identity.Context, Kind: string(output.Identity.Kind), + ValueType: string(output.Identity.Type.Kind), Repeated: output.Identity.Type.Cardinality == "many", + Nullable: output.Identity.Type.Cardinality == "optional_one" || output.Identity.Type.Cardinality == "many", + } + } + var expansion *model.DataframeRecipeExpansionExplanation + if output.Expansion != nil { + expansion = &model.DataframeRecipeExpansionExplanation{SourcePath: output.Expansion.SourcePath, Alias: output.Expansion.As} + } + outputs = append(outputs, &model.DataframeRecipeOutputExplanation{ + Name: output.Name, RootResourceType: output.Root, RowGrain: string(output.RowGrain), + Fields: fields, Identity: identity, Expansion: expansion, DynamicMaps: append([]string(nil), output.DynamicMap...), CatalogProjections: append([]string(nil), output.CatalogProjections...), + }) + } + return &model.DataframeRecipeExplanation{Name: name, RecipeDigest: plan.RecipeDigest, TranslationVersion: plan.TranslationVersion, Outputs: outputs} +} + +func physicalExplanation(value control.PhysicalExplanation) *model.DataframeRecipePhysicalExplanation { + outputs := make([]*model.DataframeRecipePhysicalOutputExplanation, 0, len(value.Outputs)) + for _, output := range value.Outputs { + diagnostics := output.Diagnostics + states := make([]*model.DataframeRecipeOptimizationRuleState, 0, len(diagnostics.OptimizationPolicy.RuleStates)) + for _, state := range diagnostics.OptimizationPolicy.RuleStates { + states = append(states, &model.DataframeRecipeOptimizationRuleState{Rule: string(state.Rule), Enabled: state.Enabled, Reason: state.Reason}) + } + decisions := make([]*model.DataframeRecipeOptimizationDecision, 0, len(diagnostics.OptimizationPolicy.Decisions)) + for _, decision := range diagnostics.OptimizationPolicy.Decisions { + decisions = append(decisions, &model.DataframeRecipeOptimizationDecision{ + Rule: decision.Rule, Enabled: decision.Enabled, CandidateSets: decision.CandidateSets, + EstimatedBaselineWork: decision.EstimatedBaselineWork, EstimatedOptimizedWork: decision.EstimatedOptimizedWork, + EstimatedSavings: decision.EstimatedSavings, Reason: decision.Reason, + }) + } + optimization := &model.DataframeRecipeOptimizationExplanation{ + Policy: diagnostics.OptimizationPolicy.Policy, Enabled: diagnostics.OptimizationPolicy.Enabled, + MinimumSavings: diagnostics.OptimizationPolicy.MinimumSavings, Rules: states, Decisions: decisions, + } + mapped := &model.DataframeRecipePhysicalOutputExplanation{ + Name: output.Name, PlanFingerprint: output.PlanFingerprint, Columns: append([]string(nil), output.Columns...), + TraversalSets: diagnostics.TraversalSets, EndpointTraversalCount: diagnostics.EndpointTraversalCount, + NativeTraversalCount: diagnostics.NativeTraversalCount, SharedTraversalCount: diagnostics.SharedTraversalCount, + RequiredMatchReuseCount: diagnostics.RequiredMatchReuseCount, Optimization: optimization, + } + if output.Live != nil { + mapped.Live = arangoAssessment(*output.Live) + } + outputs = append(outputs, mapped) + } + return &model.DataframeRecipePhysicalExplanation{Outputs: outputs} +} + +func arangoAssessment(value control.ExplainAssessment) *model.DataframeRecipeArangoAssessment { + result := &model.DataframeRecipeArangoAssessment{ + Plans: make([]*model.DataframeRecipeExplainPlanEstimate, 0, len(value.Plans)), + FullCollectionScans: make([]*model.DataframeRecipeExplainCollectionScan, 0, len(value.FullCollectionScans)), + Indexes: make([]*model.DataframeRecipeExplainIndexSummary, 0, len(value.Indexes)), + Warnings: make([]*model.DataframeRecipeExplainWarning, 0, len(value.Warnings)), + AppliedOptimizerRules: append([]string(nil), value.AppliedOptimizerRules...), + } + for _, plan := range value.Plans { + result.Plans = append(result.Plans, &model.DataframeRecipeExplainPlanEstimate{Plan: plan.Plan, EstimatedCost: plan.EstimatedCost, EstimatedNrItems: plan.EstimatedNrItems}) + } + for _, scan := range value.FullCollectionScans { + result.FullCollectionScans = append(result.FullCollectionScans, &model.DataframeRecipeExplainCollectionScan{Plan: scan.Plan, NodeID: int(scan.NodeID), Collection: scan.Collection}) + } + for _, index := range value.Indexes { + mapped := &model.DataframeRecipeExplainIndexSummary{Collection: index.Collection, ID: index.ID, Name: index.Name, Type: index.Type, Fields: append([]string(nil), index.Fields...), Uses: make([]*model.DataframeRecipeExplainIndexLocation, 0, len(index.Uses))} + for _, use := range index.Uses { + mapped.Uses = append(mapped.Uses, &model.DataframeRecipeExplainIndexLocation{Plan: use.Plan, NodeID: int(use.NodeID)}) + } + result.Indexes = append(result.Indexes, mapped) + } + for _, warning := range value.Warnings { + result.Warnings = append(result.Warnings, &model.DataframeRecipeExplainWarning{Code: warning.Code, Message: warning.Message}) + } + return result +} + +func preflightResult(plan semantic.ResolvedRecipePlan, name string) *model.DataframeRecipePreflight { + columns := make([]*model.DataframeRecipeColumn, 0) + for _, output := range plan.SemanticPlan.Outputs { + for _, field := range output.Fields { + columns = append(columns, &model.DataframeRecipeColumn{ + Output: output.Name, Name: field.Name, LogicalType: string(field.Expr.Type.Kind), + Repeated: field.Expr.Type.Cardinality == "many", + Nullable: field.Expr.Type.Cardinality == "optional_one" || field.Expr.Type.Cardinality == "many", + }) + } + } + keys := make([]string, 0, len(plan.ResolvedColumns)) + for key := range plan.ResolvedColumns { + keys = append(keys, key) + } + sort.Strings(keys) + for _, key := range keys { + for _, column := range plan.ResolvedColumns[key] { + columns = append(columns, &model.DataframeRecipeColumn{ + Output: column.Output, DynamicName: column.DynamicName, Name: column.Column.Name, + LogicalType: column.Column.ValueType, + }) + } + } + return &model.DataframeRecipePreflight{ + Name: name, RecipeDigest: plan.SemanticPlan.RecipeDigest, + ResolvedSchemaDigest: plan.ResolvedSchemaDigest, SourceGeneration: plan.SourceGeneration, + ScopeDigest: plan.ScopeDigest, Columns: columns, + } +} + +func previewResult(preview recipePreviewView, name string) (*model.DataframeRecipePreview, error) { + if len(preview.outputs) == 0 { + return nil, fmt.Errorf("compiler output schema is missing") + } + outputs := make([]*model.DataframeRecipePreviewOutput, 0, len(preview.outputs)) + for _, output := range preview.outputs { + columns := append([]string(nil), output.Columns...) + rows := logicalPreviewRows(columns, output.Rows) + encoded, err := json.Marshal(rows) + if err != nil { + return nil, fmt.Errorf("marshal preview output %q: %w", output.Name, err) + } + csvValue, err := dataframeCSV(columns, rows) + if err != nil { + return nil, fmt.Errorf("marshal preview CSV output %q: %w", output.Name, err) + } + outputs = append(outputs, &model.DataframeRecipePreviewOutput{Name: output.Name, Columns: columns, Rows: encoded, CSV: &csvValue, RowCount: len(rows)}) + } + return &model.DataframeRecipePreview{ + Name: name, RecipeDigest: preview.plan.SemanticPlan.RecipeDigest, + ResolvedSchemaDigest: preview.plan.ResolvedSchemaDigest, SourceGeneration: preview.plan.SourceGeneration, + Outputs: outputs, + }, nil +} + +func fullRecipeResult(result recipePreviewView, name string) (*model.DataframeRecipeResult, error) { + if len(result.outputs) == 0 { + return nil, fmt.Errorf("compiler output schema is missing") + } + outputs := make([]*model.DataframeRecipeResultOutput, 0, len(result.outputs)) + for _, output := range result.outputs { + columns := append([]string(nil), output.Columns...) + rows := logicalPreviewRows(columns, output.Rows) + encoded, err := json.Marshal(rows) + if err != nil { + return nil, fmt.Errorf("marshal dataframe output %q: %w", output.Name, err) + } + csvValue, err := dataframeCSV(columns, rows) + if err != nil { + return nil, fmt.Errorf("marshal dataframe CSV output %q: %w", output.Name, err) + } + outputs = append(outputs, &model.DataframeRecipeResultOutput{Name: output.Name, Columns: columns, Rows: encoded, CSV: &csvValue, RowCount: len(rows)}) + } + return &model.DataframeRecipeResult{ + Name: name, RecipeDigest: result.plan.SemanticPlan.RecipeDigest, + ResolvedSchemaDigest: result.plan.ResolvedSchemaDigest, SourceGeneration: result.plan.SourceGeneration, + Outputs: outputs, + }, nil +} + +// dataframeCSV serializes one logical output using the compiler-resolved +// column order. Repeated and object values are represented as JSON in one CSV +// cell so the flat export remains lossless. +func dataframeCSV(columns []string, rows []map[string]any) (string, error) { + var buffer bytes.Buffer + writer := csv.NewWriter(&buffer) + if err := writer.Write(columns); err != nil { + return "", err + } + for _, row := range rows { + record := make([]string, len(columns)) + for index, column := range columns { + value, ok := row[column] + if !ok || value == nil { + continue + } + switch typed := value.(type) { + case string: + record[index] = typed + case bool: + record[index] = strconv.FormatBool(typed) + case int: + record[index] = strconv.Itoa(typed) + default: + encoded, err := json.Marshal(typed) + if err != nil { + return "", err + } + record[index] = string(encoded) + } + } + if err := writer.Write(record); err != nil { + return "", err + } + } + writer.Flush() + if err := writer.Error(); err != nil { + return "", err + } + return buffer.String(), nil +} + +func logicalPreviewRows(columns []string, rows []map[string]any) []map[string]any { + result := make([]map[string]any, 0, len(rows)) + for _, row := range rows { + copy := make(map[string]any, len(columns)) + for _, column := range columns { + if value, ok := row[column]; ok { + copy[column] = value + } + } + result = append(result, copy) + } + return result +} + +type recipePreviewView struct { + plan semantic.ResolvedRecipePlan + outputs []control.OutputRows +} + +func executionModel(value RecipeExecution) *model.DataframeRecipeExecution { + outputs := make([]*model.DataframeRecipeExecutionOutput, 0, len(value.Outputs)) + for _, output := range value.Outputs { + var errorText *string + if output.Error != "" { + value := output.Error + errorText = &value + } + outputs = append(outputs, &model.DataframeRecipeExecutionOutput{Name: output.Name, State: model.DataframeRecipeExecutionState(output.State), RowCount: output.RowCount, Error: errorText}) + } + var errorText *string + if value.Error != "" { + copy := value.Error + errorText = © + } + return &model.DataframeRecipeExecution{ID: value.ID, Name: value.Name, RecipeDigest: value.RecipeDigest, ResolvedSchemaDigest: value.ResolvedSchemaDigest, SourceGeneration: value.SourceGeneration, State: model.DataframeRecipeExecutionState(value.State), Outputs: outputs, Error: errorText} +} diff --git a/graphqlapi/recipe_control_test.go b/graphqlapi/recipe_control_test.go new file mode 100644 index 0000000..dbe8ea4 --- /dev/null +++ b/graphqlapi/recipe_control_test.go @@ -0,0 +1,187 @@ +package graphqlapi + +import ( + "context" + "testing" + + "github.com/calypr/loom/graphqlapi/model" + "github.com/calypr/loom/internal/dataframe/materialization" + "github.com/calypr/loom/internal/dataframe/recipe" + "github.com/calypr/loom/internal/dataframe/recipe/control" + "github.com/calypr/loom/internal/dataframe/semantic" +) + +type fakeRecipeControl struct { + validation control.Validation + plan semantic.ResolvedRecipePlan + resolveCalls *int +} + +type fakeRecipeRunControl struct{ fakeRecipeControl } + +func (f fakeRecipeRunControl) Run(context.Context, string, recipe.RuntimeBindings) (control.Preview, error) { + rows := []map[string]any{{"id": "p1"}, {"id": "p2"}} + return control.Preview{Plan: f.plan, Rows: map[string][]map[string]any{"rows": rows}, Outputs: []control.OutputRows{{Name: "rows", Columns: []string{"id"}, Rows: rows}}}, nil +} + +func (f fakeRecipeControl) Validate(context.Context, string, recipe.RuntimeBindings) (control.Validation, error) { + return f.validation, nil +} +func (f fakeRecipeControl) Explain(context.Context, string, recipe.RuntimeBindings) (semantic.RecipePlanExplanation, error) { + return f.validation.Plan.Explain(), nil +} +func (f fakeRecipeControl) Resolve(context.Context, string, recipe.RuntimeBindings) (semantic.ResolvedRecipePlan, error) { + if f.resolveCalls != nil { + (*f.resolveCalls)++ + } + return f.plan, nil +} +func (f fakeRecipeControl) Preview(context.Context, string, recipe.RuntimeBindings, control.ExecuteFunc) (control.Preview, error) { + rows := []map[string]any{{"id": "p1"}} + return control.Preview{Plan: f.plan, Rows: map[string][]map[string]any{"rows": rows}, Outputs: []control.OutputRows{{Name: "rows", Columns: []string{"id"}, Rows: rows}}}, nil +} + +func testRecipeValidation() control.Validation { + plan := semantic.RecipePlan{Version: 1, RecipeDigest: "recipe", TranslationVersion: "legacy", Outputs: []semantic.OutputPlan{{ + Name: "rows", RootResourceType: "Patient", RowGrain: semantic.RowGrain("resource"), + DeclaredOrder: []string{"id"}, Fields: []semantic.SemanticProjection{{Name: "id", Expr: semantic.SemanticExpression{SourcePath: "$.outputs[0].fields[0]", Type: semanticTypeString().Type}}}, + }}} + return control.Validation{Plan: plan} +} + +func semanticTypeString() (value semantic.SemanticExpression) { + value.Type.Kind = "string" + value.Type.Cardinality = "required_one" + return value +} + +func TestRecipeControlResolverReturnsLogicalValidation(t *testing.T) { + validation := testRecipeValidation() + resolver := NewResolver(ResolverConfig{RecipeControl: fakeRecipeControl{validation: validation, plan: semantic.ResolvedRecipePlan{SemanticPlan: validation.Plan, ResolvedSchemaDigest: "schema", SourceGeneration: "g", ScopeDigest: "scope"}}}) + result, err := resolver.Mutation().ValidateDataframeRecipe(context.Background(), model.ValidateDataframeRecipeInput{ + Name: "default", Bindings: &model.DataframeRecipeBindingsInput{Project: "p"}, + }) + if err != nil { + t.Fatal(err) + } + if result.RecipeDigest != "recipe" || len(result.Outputs) != 1 || result.Outputs[0].FieldNames[0] != "id" { + t.Fatalf("unexpected validation result: %#v", result) + } +} + +func TestRecipeControlResolverRunsCompleteRecipe(t *testing.T) { + validation := testRecipeValidation() + plan := semantic.ResolvedRecipePlan{SemanticPlan: validation.Plan, ResolvedSchemaDigest: "schema", SourceGeneration: "g", ScopeDigest: "scope"} + resolver := NewResolver(ResolverConfig{RecipeControl: fakeRecipeRunControl{fakeRecipeControl{validation: validation, plan: plan}}}) + result, err := resolver.Mutation().RunDataframeRecipe(context.Background(), model.RunDataframeRecipeInput{ + Name: "default", Bindings: &model.DataframeRecipeBindingsInput{Project: "p"}, + }) + if err != nil { + t.Fatal(err) + } + if result.Name != "default" || len(result.Outputs) != 1 || result.Outputs[0].RowCount != 2 { + t.Fatalf("unexpected full recipe result: %#v", result) + } +} + +func TestDataframeCSVUsesResolvedColumnsAndEscapesValues(t *testing.T) { + value, err := dataframeCSV([]string{"id", "label", "tags"}, []map[string]any{{ + "id": "p1", "label": "a,b", "tags": []string{"x", "y"}, + }}) + if err != nil { + t.Fatal(err) + } + want := `id,label,tags +p1,"a,b","[""x"",""y""]" +` + if value != want { + t.Fatalf("unexpected CSV output: %q", value) + } +} + +func TestLogicalPreviewRowsKeepsNestedCompilerColumnsAndDropsInternalValues(t *testing.T) { + columns := []string{"id", "specimen__type", "specimen__observation__code"} + rows := logicalPreviewRows(columns, []map[string]any{{ + "id": "p1", "specimen__type": "tumor", "specimen__observation__code": "C1", + "__loom_row_id": "internal", "unplanned": "must not leak", + }}) + if len(rows) != 1 || rows[0]["specimen__observation__code"] != "C1" { + t.Fatalf("nested compiler column was lost: %#v", rows) + } + if _, ok := rows[0]["__loom_row_id"]; ok { + t.Fatalf("internal identity leaked: %#v", rows[0]) + } + if _, ok := rows[0]["unplanned"]; ok { + t.Fatalf("unplanned row key leaked: %#v", rows[0]) + } + csvValue, err := dataframeCSV(columns, rows) + if err != nil { + t.Fatal(err) + } + if csvValue != "id,specimen__type,specimen__observation__code\np1,tumor,C1\n" { + t.Fatalf("nested compiler column missing from CSV: %q", csvValue) + } +} + +func TestRecipeControlPreflightDoesNotExposePhysicalDetails(t *testing.T) { + validation := testRecipeValidation() + plan := semantic.ResolvedRecipePlan{SemanticPlan: validation.Plan, ResolvedSchemaDigest: "schema", SourceGeneration: "g", ScopeDigest: "scope"} + resolver := NewResolver(ResolverConfig{RecipeControl: fakeRecipeControl{validation: validation, plan: plan}}) + result, err := resolver.Query().PreflightDataframeRecipe(context.Background(), model.PreflightDataframeRecipeInput{ + Name: "default", Bindings: &model.DataframeRecipeBindingsInput{Project: "p"}, + }) + if err != nil { + t.Fatal(err) + } + if result.ResolvedSchemaDigest != "schema" || result.SourceGeneration != "g" || result.ScopeDigest != "scope" { + t.Fatalf("unexpected preflight result: %#v", result) + } +} + +type fakeRecipeExecutionStore struct { + execution materialization.BundleExecution +} + +func (s fakeRecipeExecutionStore) GetExecution(context.Context, string) (materialization.BundleExecution, error) { + return s.execution, nil +} + +func TestRecipeExecutionReaderMapsDurableBundleState(t *testing.T) { + reader := NewAuthorizedRecipeExecutionReader(fakeRecipeExecutionStore{execution: materialization.BundleExecution{ + ID: "execution-1", BundleIdentity: materialization.BundleIdentity{ + Name: "default", Project: "p", DatasetGeneration: "g", RecipeDigest: "recipe", SchemaDigest: "schema", + }, State: materialization.BundleReady, + Outputs: []materialization.BundleOutputRecord{{Name: "Patient", State: materialization.BundleReady, RowCount: 7}}, + }}, nil) + execution, err := reader(context.Background(), "execution-1") + if err != nil { + t.Fatal(err) + } + if execution.ID != "execution-1" || execution.State != "READY" || execution.ResolvedSchemaDigest != "schema" || len(execution.Outputs) != 1 || *execution.Outputs[0].RowCount != 7 { + t.Fatalf("unexpected execution: %#v", execution) + } +} + +func TestMaterializeRecipeDoesNotPreResolveInGraphQL(t *testing.T) { + validation := testRecipeValidation() + plan := semantic.ResolvedRecipePlan{SemanticPlan: validation.Plan, ResolvedSchemaDigest: "schema", SourceGeneration: "g", ScopeDigest: "scope"} + resolveCalls := 0 + materializeCalls := 0 + resolver := NewResolver(ResolverConfig{ + RecipeControl: fakeRecipeControl{validation: validation, plan: plan, resolveCalls: &resolveCalls}, + RecipeMaterialize: func(context.Context, string, recipe.RuntimeBindings) (RecipeExecution, error) { + materializeCalls++ + return RecipeExecution{ID: "execution-1", Name: "default", State: "READY"}, nil + }, + }) + if _, err := resolver.Mutation().MaterializeDataframeRecipeBundle(context.Background(), model.MaterializeDataframeRecipeInput{ + Name: "default", Bindings: &model.DataframeRecipeBindingsInput{Project: "p", DatasetGeneration: stringPtr("g")}, + }); err != nil { + t.Fatal(err) + } + if resolveCalls != 0 || materializeCalls != 1 { + t.Fatalf("GraphQL pre-resolve counts = resolve:%d materialize:%d", resolveCalls, materializeCalls) + } +} + +func stringPtr(value string) *string { return &value } diff --git a/graphqlapi/recipe_execution_reader.go b/graphqlapi/recipe_execution_reader.go new file mode 100644 index 0000000..847781c --- /dev/null +++ b/graphqlapi/recipe_execution_reader.go @@ -0,0 +1,84 @@ +package graphqlapi + +import ( + "context" + "fmt" + + "github.com/calypr/loom/internal/authscope" + "github.com/calypr/loom/internal/dataframe/materialization" +) + +// RecipeExecutionStore is the durable subset needed by the GraphQL execution +// status query. Keeping this interface narrower than BundleCatalog makes the +// transport adapter usable with both the production Arango registry and small +// test fakes. +type RecipeExecutionStore interface { + GetExecution(context.Context, string) (materialization.BundleExecution, error) +} + +// NewAuthorizedRecipeExecutionReader performs the project/scope check before +// exposing durable publication status. Execution IDs are opaque and are not +// themselves authorization credentials. +func NewAuthorizedRecipeExecutionReader(store RecipeExecutionStore, scopeResolver *authscope.ScopeResolver) RecipeExecutionReader { + return func(ctx context.Context, id string) (*RecipeExecution, error) { + if store == nil { + return nil, fmt.Errorf("recipe execution store is not configured") + } + execution, err := store.GetExecution(ctx, id) + if err != nil { + return nil, err + } + if scopeResolver != nil { + principal, _ := authscope.PrincipalFromContext(ctx) + scope, scopeErr := scopeResolver.ResolveReadScopeForGeneration(ctx, principal, execution.Project, execution.DatasetGeneration, execution.AuthResourcePaths) + if scopeErr != nil { + return nil, scopeErr + } + if scope.Mode == authscope.ReadScopeRestricted && len(scope.AuthResourcePaths) != len(execution.AuthResourcePaths) { + return nil, fmt.Errorf("recipe execution %q is outside caller scope", id) + } + } + state, err := graphQLRecipeExecutionState(execution.State) + if err != nil { + return nil, err + } + result := &RecipeExecution{ + ID: execution.ID, + Name: execution.Name, + RecipeDigest: execution.RecipeDigest, + ResolvedSchemaDigest: execution.SchemaDigest, + SourceGeneration: execution.DatasetGeneration, + State: state, + Error: execution.Error, + Outputs: make([]RecipeExecutionOutput, 0, len(execution.Outputs)), + } + for _, output := range execution.Outputs { + outputState, err := graphQLRecipeExecutionState(output.State) + if err != nil { + return nil, fmt.Errorf("output %q: %w", output.Name, err) + } + rowCount := int(output.RowCount) + result.Outputs = append(result.Outputs, RecipeExecutionOutput{ + Name: output.Name, State: outputState, RowCount: &rowCount, + }) + } + return result, nil + } +} + +func graphQLRecipeExecutionState(state materialization.BundleState) (string, error) { + switch state { + case materialization.BundlePending: + return "ACCEPTED", nil + case materialization.BundlePreflight, materialization.BundleValidating: + return "VALIDATING", nil + case materialization.BundleLoading: + return "RUNNING", nil + case materialization.BundleReady: + return "READY", nil + case materialization.BundleFailed: + return "FAILED", nil + default: + return "", fmt.Errorf("unsupported bundle execution state %q", state) + } +} diff --git a/graphqlapi/resolver.go b/graphqlapi/resolver.go index cb534ea..1953005 100644 --- a/graphqlapi/resolver.go +++ b/graphqlapi/resolver.go @@ -1,27 +1,142 @@ package graphqlapi import ( + "context" + materializationapi "github.com/calypr/loom/graphqlapi/materialization" + "github.com/calypr/loom/graphqlapi/model" queryapi "github.com/calypr/loom/graphqlapi/query" "github.com/calypr/loom/internal/dataframe/materialization" + "github.com/calypr/loom/internal/dataframe/recipe" + "github.com/calypr/loom/internal/dataframe/recipe/control" + "github.com/calypr/loom/internal/dataframe/semantic" ) +// RecipeControl is the transport-neutral semantic control plane used by the +// recipe GraphQL fields. It intentionally returns logical plans only; no +// AQL, SQL, table names, or credentials cross this boundary. +type RecipeControl interface { + Validate(context.Context, string, recipe.RuntimeBindings) (control.Validation, error) + Explain(context.Context, string, recipe.RuntimeBindings) (semantic.RecipePlanExplanation, error) + Resolve(context.Context, string, recipe.RuntimeBindings) (semantic.ResolvedRecipePlan, error) + Preview(context.Context, string, recipe.RuntimeBindings, control.ExecuteFunc) (control.Preview, error) +} + +// RecipeRunControl is an optional extension for the explicit full-data +// GraphQL operation. Preview and materialization remain separate contracts; +// this extension is only implemented by the canonical recipe engine. +type RecipeRunControl interface { + Run(context.Context, string, recipe.RuntimeBindings) (control.Preview, error) +} + +// RecipeExplainEvidenceControl is an optional extension implemented by the +// canonical recipe engine. Keeping it separate preserves compatibility with +// existing semantic-only controls and test doubles while allowing Explain to +// expose sanitized compiler/Arango evidence when configured. +type RecipeExplainEvidenceControl interface { + ExplainPhysical(context.Context, string, recipe.RuntimeBindings, bool) (control.PhysicalExplanation, error) +} + +// RecipeExecution is the stable logical execution record exposed by GraphQL. +// Implementations may persist additional physical metadata, but adapters must +// not return it here. +type RecipeExecution struct { + ID string + Name string + RecipeDigest string + ResolvedSchemaDigest string + SourceGeneration string + State string + Outputs []RecipeExecutionOutput + Error string +} + +type RecipeExecutionOutput struct { + Name string + State string + RowCount *int + Error string +} + +// RecipeMaterializeFunc owns the complete materialization operation. The +// callback resolves the recipe exactly once and publishes the resulting +// physical plan; GraphQL does not pre-resolve it and then ask the callback to +// resolve it again. +type RecipeMaterializeFunc func(context.Context, string, recipe.RuntimeBindings) (RecipeExecution, error) +type RecipeExecutionReader func(context.Context, string) (*RecipeExecution, error) + +// RecipeAuthorizer resolves request bindings against the caller's project +// grants. GraphQL operation type is intentionally irrelevant: preview/run are +// reads even though they are mutations, while publication is a write. +type RecipeAuthorizer interface { + AuthorizeRead(context.Context, recipe.RuntimeBindings) (recipe.RuntimeBindings, error) + AuthorizeWrite(context.Context, recipe.RuntimeBindings) (recipe.RuntimeBindings, error) +} + type Resolver struct { - query *queryapi.Service - materializations *materializationapi.Service + query *queryapi.Service + materializations *materializationapi.Service + recipeControl RecipeControl + previewExecute control.ExecuteFunc + recipeMaterialize RecipeMaterializeFunc + recipeExecutions RecipeExecutionReader + recipeAuthorizer RecipeAuthorizer } type ResolverConfig struct { DataframeQuery queryapi.Config MaterializationReader *materialization.Reader + RecipeControl RecipeControl + RecipePreviewExecute control.ExecuteFunc + RecipeMaterialize RecipeMaterializeFunc + RecipeExecutions RecipeExecutionReader + RecipeAuthorizer RecipeAuthorizer } func NewResolver(cfg ResolverConfig) *Resolver { return &Resolver{ query: queryapi.NewService(cfg.DataframeQuery), materializations: materializationapi.NewService(materializationapi.Config{ - Reader: cfg.MaterializationReader, - ScopeResolver: cfg.DataframeQuery.ScopeResolver, + Reader: cfg.MaterializationReader, + ScopeResolver: cfg.DataframeQuery.ScopeResolver, + ActiveManifestResolver: cfg.DataframeQuery.ActiveManifestResolver, }), + recipeControl: cfg.RecipeControl, + previewExecute: cfg.RecipePreviewExecute, + recipeMaterialize: cfg.RecipeMaterialize, + recipeExecutions: cfg.RecipeExecutions, + recipeAuthorizer: cfg.RecipeAuthorizer, + } +} + +func (r *Resolver) authorizeRecipeRead(ctx context.Context, bindings recipe.RuntimeBindings) (recipe.RuntimeBindings, error) { + if r.recipeAuthorizer == nil { + return bindings, nil + } + return r.recipeAuthorizer.AuthorizeRead(ctx, bindings) +} + +func (r *Resolver) authorizeRecipeWrite(ctx context.Context, bindings recipe.RuntimeBindings) (recipe.RuntimeBindings, error) { + if r.recipeAuthorizer == nil { + return bindings, nil + } + return r.recipeAuthorizer.AuthorizeWrite(ctx, bindings) +} + +func recipeBindings(input model.DataframeRecipeBindingsInput) recipe.RuntimeBindings { + limit := 25 + if input.PreviewLimit != nil { + limit = *input.PreviewLimit + } + return recipe.RuntimeBindings{ + Project: input.Project, DatasetGeneration: valueOrEmpty(input.DatasetGeneration), + AuthResourcePaths: append([]string(nil), input.AuthResourcePaths...), PreviewLimit: limit, + } +} + +func valueOrEmpty(value *string) string { + if value == nil { + return "" } + return *value } diff --git a/graphqlapi/schema.graphqls b/graphqlapi/schema.graphqls index 2a5ffb9..d4594ca 100644 --- a/graphqlapi/schema.graphqls +++ b/graphqlapi/schema.graphqls @@ -4,8 +4,13 @@ type Query { ): DataframeBuilderIntrospection! dataframeMaterialization(id: ID!): DataframeMaterialization + dataframeDatasets: [DataframeMaterialization!]! + dataframeDataset(input: DataframeDatasetInput!): DataframeMaterialization dataframeRows(input: DataframeRowsInput!): DataframeRowConnection! dataframeAggregate(input: DataframeAggregateInput!): DataframeAggregateResult! + dataframeRecipeExecution(id: ID!): DataframeRecipeExecution + explainDataframeRecipe(input: ExplainDataframeRecipeInput!): DataframeRecipeExplanation! + preflightDataframeRecipe(input: PreflightDataframeRecipeInput!): DataframeRecipePreflight! } scalar JSON @@ -15,6 +20,255 @@ type Mutation { input: FhirDataframeInput! limit: Int = 25 ): FhirDataframeResult! + runDataframeRecipe(input: RunDataframeRecipeInput!): DataframeRecipeResult! + validateDataframeRecipe(input: ValidateDataframeRecipeInput!): DataframeRecipeValidation! + previewDataframeRecipe(input: PreviewDataframeRecipeInput!): DataframeRecipePreview! + materializeDataframeRecipeBundle(input: MaterializeDataframeRecipeInput!): DataframeRecipeExecution! +} + +input DataframeRecipeBindingsInput { + project: String! + datasetGeneration: String + authResourcePaths: [String!] + previewLimit: Int = 25 +} + +input ValidateDataframeRecipeInput { + name: String! + bindings: DataframeRecipeBindingsInput! +} + +input ExplainDataframeRecipeInput { + name: String! + bindings: DataframeRecipeBindingsInput! + live: Boolean = false +} + +input PreflightDataframeRecipeInput { + name: String! + bindings: DataframeRecipeBindingsInput! +} + +input PreviewDataframeRecipeInput { + name: String! + bindings: DataframeRecipeBindingsInput! + limit: Int = 25 + outputs: [String!] +} + +input RunDataframeRecipeInput { + name: String! + bindings: DataframeRecipeBindingsInput! + outputs: [String!] +} + +input MaterializeDataframeRecipeInput { + name: String! + bindings: DataframeRecipeBindingsInput! +} + +type DataframeRecipeValidation { + name: String! + recipeDigest: String! + translationVersion: String! + outputs: [DataframeRecipeOutputValidation!]! +} + +type DataframeRecipeOutputValidation { + name: String! + rootResourceType: String! + rowGrain: String! + fieldNames: [String!]! + dynamicColumns: [String!]! +} + +type DataframeRecipeExplanation { + name: String! + recipeDigest: String! + translationVersion: String! + outputs: [DataframeRecipeOutputExplanation!]! + physical: DataframeRecipePhysicalExplanation +} + +type DataframeRecipePhysicalExplanation { + outputs: [DataframeRecipePhysicalOutputExplanation!]! +} + +type DataframeRecipePhysicalOutputExplanation { + name: String! + planFingerprint: String! + columns: [String!]! + traversalSets: Int! + endpointTraversalCount: Int! + nativeTraversalCount: Int! + sharedTraversalCount: Int! + requiredMatchReuseCount: Int! + optimization: DataframeRecipeOptimizationExplanation! + live: DataframeRecipeArangoAssessment +} + +type DataframeRecipeOptimizationExplanation { + policy: String! + enabled: Boolean! + minimumSavings: Int! + rules: [DataframeRecipeOptimizationRuleState!]! + decisions: [DataframeRecipeOptimizationDecision!]! +} + +type DataframeRecipeOptimizationRuleState { + rule: String! + enabled: Boolean! + reason: String! +} + +type DataframeRecipeOptimizationDecision { + rule: String! + enabled: Boolean! + candidateSets: Int! + estimatedBaselineWork: Int! + estimatedOptimizedWork: Int! + estimatedSavings: Int! + reason: String! +} + +type DataframeRecipeArangoAssessment { + plans: [DataframeRecipeExplainPlanEstimate!]! + fullCollectionScans: [DataframeRecipeExplainCollectionScan!]! + indexes: [DataframeRecipeExplainIndexSummary!]! + warnings: [DataframeRecipeExplainWarning!]! + appliedOptimizerRules: [String!]! +} + +type DataframeRecipeExplainPlanEstimate { + plan: Int! + estimatedCost: Float! + estimatedNrItems: Float! +} + +type DataframeRecipeExplainCollectionScan { + plan: Int! + nodeId: Int! + collection: String! +} + +type DataframeRecipeExplainIndexSummary { + collection: String! + id: String! + name: String! + type: String! + fields: [String!]! + uses: [DataframeRecipeExplainIndexLocation!]! +} + +type DataframeRecipeExplainIndexLocation { + plan: Int! + nodeId: Int! +} + +type DataframeRecipeExplainWarning { + code: Int! + message: String! +} + +type DataframeRecipeOutputExplanation { + name: String! + rootResourceType: String! + rowGrain: String! + fields: [DataframeRecipeExpressionExplanation!]! + identity: DataframeRecipeExpressionExplanation + expansion: DataframeRecipeExpansionExplanation + dynamicMaps: [String!]! + catalogProjections: [String!]! +} + +type DataframeRecipeExpressionExplanation { + sourcePath: String! + context: String! + kind: String! + valueType: String! + repeated: Boolean! + nullable: Boolean! +} + +type DataframeRecipeExpansionExplanation { + sourcePath: String! + alias: String! +} + +type DataframeRecipePreflight { + name: String! + recipeDigest: String! + resolvedSchemaDigest: String! + sourceGeneration: String! + scopeDigest: String! + columns: [DataframeRecipeColumn!]! +} + +type DataframeRecipeColumn { + output: String! + dynamicName: String! + name: String! + logicalType: String! + repeated: Boolean! + nullable: Boolean! +} + +type DataframeRecipePreview { + name: String! + recipeDigest: String! + resolvedSchemaDigest: String! + sourceGeneration: String! + outputs: [DataframeRecipePreviewOutput!]! +} + +type DataframeRecipePreviewOutput { + name: String! + columns: [String!]! + rows: JSON! + csv: String + rowCount: Int! +} + +type DataframeRecipeResult { + name: String! + recipeDigest: String! + resolvedSchemaDigest: String! + sourceGeneration: String! + outputs: [DataframeRecipeResultOutput!]! +} + +type DataframeRecipeResultOutput { + name: String! + columns: [String!]! + rows: JSON! + csv: String + rowCount: Int! +} + +enum DataframeRecipeExecutionState { + ACCEPTED + VALIDATING + RUNNING + READY + FAILED +} + +type DataframeRecipeExecution { + id: ID! + name: String! + recipeDigest: String! + resolvedSchemaDigest: String! + sourceGeneration: String! + state: DataframeRecipeExecutionState! + outputs: [DataframeRecipeExecutionOutput!]! + error: String +} + +type DataframeRecipeExecutionOutput { + name: String! + state: DataframeRecipeExecutionState! + rowCount: Int + error: String } enum DataframeMaterializationState { @@ -27,13 +281,18 @@ enum DataframeMaterializationState { type DataframeColumn { name: String! clickhouseType: String! + logicalType: String! + nullable: Boolean! + repeated: Boolean! + filterable: Boolean! + sortable: Boolean! + aggregatable: Boolean! } type DataframeMaterialization { id: ID! name: String! - project: String! - datasetGeneration: String! + revision: String! state: DataframeMaterializationState! columns: [DataframeColumn!]! rowCount: Int! @@ -42,6 +301,10 @@ type DataframeMaterialization { error: String } +input DataframeDatasetInput { + dataType: String! +} + input DataframeFilterInput { column: String! op: String! @@ -54,7 +317,8 @@ input DataframeSortInput { } input DataframeRowsInput { - materializationId: ID! + materializationId: ID + dataType: String! columns: [String!] filters: [DataframeFilterInput!] sort: DataframeSortInput @@ -71,11 +335,13 @@ type DataframeRowConnection { materialization: DataframeMaterialization! columns: [String!]! rows: JSON! + totalCount: Int pageInfo: DataframePageInfo! } input DataframeAggregateInput { - materializationId: ID! + materializationId: ID + dataType: String! groupBy: [String!] filters: [DataframeFilterInput!] operation: String! @@ -195,12 +461,92 @@ enum FhirValueMode { DISTINCT } +# Typed, storage-independent dataframe predicates. `select` is a FHIR path +# (or a resolved catalog selector), while `fieldRef` is optional frontend +# provenance used during scoped field resolution. Values are a strict tagged +# union; exactly one member matching `kind` is accepted by recipe validation. +enum FhirFilterOperator { + EQUALS + NOT_EQUALS + IN + EXISTS + MISSING + CONTAINS_TEXT + GT + GTE + LT + LTE +} + +enum FhirFilterQuantifier { + ANY + ALL + NONE +} + +enum FhirFilterValueKind { + STRING + CODE + BOOLEAN + INTEGER + DECIMAL + DATE + DATE_TIME +} + +input FhirCodeValueInput { + system: String + code: String! + display: String +} + +input FhirFilterValueInput { + kind: FhirFilterValueKind! + string: String + code: FhirCodeValueInput + boolean: Boolean + integer: Int + decimal: Float + date: String + dateTime: String +} + +input FhirFilterInput { + select: String! + fieldRef: String + operator: FhirFilterOperator! + quantifier: FhirFilterQuantifier = ANY + values: [FhirFilterValueInput!]! = [] +} + input FhirPivotInput { name: String! fieldRef: String columnSelector: FhirFieldSelectorInput valueSelector: FhirFieldSelectorInput columns: [String!]! = [] + discovery: FhirPivotDiscoveryInput +} + +input FhirPivotDiscoveryInput { + family: String + path: String + maxColumns: Int! +} + +enum FhirColumnNaming { + PATH + PATH_SUFFIX +} + +input FhirCatalogProjectionInput { + name: String! + includePaths: [String!]! = [] + excludePaths: [String!]! = [] + kinds: [String!]! = [] + naming: FhirColumnNaming! = PATH + valueMode: FhirValueMode! = FIRST + maxColumns: Int! } enum FhirAggregateOperation { @@ -234,22 +580,33 @@ input FhirTraversalStepInput { edgeLabel: String! toResourceType: String! alias: String! + matchMode: FhirTraversalMatchMode = OPTIONAL fields: [FhirFieldSelectInput!]! = [] + filters: [FhirFilterInput!] = [] pivots: [FhirPivotInput!] = [] aggregates: [FhirAggregateInput!] = [] slices: [FhirRepresentativeSliceInput!] = [] + catalogProjections: [FhirCatalogProjectionInput!] = [] traverse: [FhirTraversalStepInput!] = [] } +enum FhirTraversalMatchMode { + OPTIONAL + REQUIRED +} + input FhirDataframeInput { project: String! authResourcePath: String authResourcePaths: [String!] rootResourceType: String! + rowGrain: String rootFields: [FhirFieldSelectInput!] + rootFilters: [FhirFilterInput!] rootPivots: [FhirPivotInput!] rootAggregates: [FhirAggregateInput!] rootSlices: [FhirRepresentativeSliceInput!] + rootCatalogProjections: [FhirCatalogProjectionInput!] traverse: [FhirTraversalStepInput!] limit: Int cursor: String diff --git a/graphqlapi/schema.resolvers.go b/graphqlapi/schema.resolvers.go index 516b12d..302c921 100644 --- a/graphqlapi/schema.resolvers.go +++ b/graphqlapi/schema.resolvers.go @@ -1,12 +1,14 @@ package graphqlapi -// This file will be automatically regenerated based on the schema, any resolver implementations +// This file will be automatically regenerated based on the schema, any resolver +// implementations // will be copied through when generating and any unknown code will be moved to the end. -// Code generated by github.com/99designs/gqlgen version v0.17.66 +// Code generated by github.com/99designs/gqlgen version v0.17.94 import ( "context" "encoding/json" + "fmt" materializationapi "github.com/calypr/loom/graphqlapi/materialization" "github.com/calypr/loom/graphqlapi/model" @@ -27,6 +29,105 @@ func (r *mutationResolver) RunFhirDataframe(ctx context.Context, input model.Fhi }, nil } +// RunDataframeRecipe executes a registered recipe completely and returns all +// selected rows. This is intentionally separate from preview and from the +// streaming publication mutation; callers should use materialization for +// large production exports. +func (r *mutationResolver) RunDataframeRecipe(ctx context.Context, input model.RunDataframeRecipeInput) (*model.DataframeRecipeResult, error) { + bindings, err := graphqlRecipeBindings(input.Bindings) + if err != nil { + return nil, recipeGraphQLError(err) + } + bindings, err = r.authorizeRecipeRead(ctx, bindings) + if err != nil { + return nil, recipeGraphQLError(err) + } + bindings.OutputNames = append([]string(nil), input.Outputs...) + runner, ok := r.recipeControl.(RecipeRunControl) + if !ok { + return nil, recipeGraphQLError(fmt.Errorf("full recipe execution is not configured")) + } + result, err := runner.Run(ctx, input.Name, bindings) + if err != nil { + return nil, recipeGraphQLError(err) + } + return fullRecipeResult(recipePreviewView{plan: result.Plan, outputs: result.Outputs}, input.Name) +} + +// ValidateDataframeRecipe is the resolver for the validateDataframeRecipe field. +func (r *mutationResolver) ValidateDataframeRecipe(ctx context.Context, input model.ValidateDataframeRecipeInput) (*model.DataframeRecipeValidation, error) { + bindings, err := graphqlRecipeBindings(input.Bindings) + if err != nil { + return nil, recipeGraphQLError(err) + } + bindings, err = r.authorizeRecipeRead(ctx, bindings) + if err != nil { + return nil, recipeGraphQLError(err) + } + if err := requireRecipeControl(r.recipeControl); err != nil { + return nil, recipeGraphQLError(err) + } + validated, err := r.recipeControl.Validate(ctx, input.Name, bindings) + if err != nil { + return nil, recipeGraphQLError(err) + } + outputs := make([]*model.DataframeRecipeOutputValidation, 0, len(validated.Plan.Outputs)) + for _, output := range validated.Plan.Outputs { + outputs = append(outputs, outputValidation(output)) + } + return &model.DataframeRecipeValidation{Name: input.Name, RecipeDigest: validated.Plan.RecipeDigest, TranslationVersion: validated.Plan.TranslationVersion, Outputs: outputs}, nil +} + +// PreviewDataframeRecipe is the resolver for the previewDataframeRecipe field. +func (r *mutationResolver) PreviewDataframeRecipe(ctx context.Context, input model.PreviewDataframeRecipeInput) (*model.DataframeRecipePreview, error) { + bindings, err := graphqlRecipeBindings(input.Bindings) + if err != nil { + return nil, recipeGraphQLError(err) + } + bindings, err = r.authorizeRecipeRead(ctx, bindings) + if err != nil { + return nil, recipeGraphQLError(err) + } + if input.Limit != nil { + if *input.Limit < 0 { + return nil, recipeGraphQLError(fmt.Errorf("preview limit must not be negative")) + } + bindings.PreviewLimit = *input.Limit + } + bindings.OutputNames = append([]string(nil), input.Outputs...) + if err := requireRecipeControl(r.recipeControl); err != nil { + return nil, recipeGraphQLError(err) + } + preview, err := r.recipeControl.Preview(ctx, input.Name, bindings, r.previewExecute) + if err != nil { + return nil, recipeGraphQLError(err) + } + return previewResult(recipePreviewView{plan: preview.Plan, outputs: preview.Outputs}, input.Name) +} + +// MaterializeDataframeRecipeBundle is the resolver for the materializeDataframeRecipeBundle field. +func (r *mutationResolver) MaterializeDataframeRecipeBundle(ctx context.Context, input model.MaterializeDataframeRecipeInput) (*model.DataframeRecipeExecution, error) { + bindings, err := graphqlRecipeBindings(input.Bindings) + if err != nil { + return nil, recipeGraphQLError(err) + } + bindings, err = r.authorizeRecipeWrite(ctx, bindings) + if err != nil { + return nil, recipeGraphQLError(err) + } + if err := requireRecipeControl(r.recipeControl); err != nil { + return nil, recipeGraphQLError(err) + } + if r.recipeMaterialize == nil { + return nil, recipeGraphQLError(fmt.Errorf("recipe materializer is not configured")) + } + execution, err := r.recipeMaterialize(ctx, input.Name, bindings) + if err != nil { + return nil, recipeGraphQLError(err) + } + return executionModel(execution), nil +} + // DataframeBuilderIntrospection is the resolver for the dataframeBuilderIntrospection field. func (r *queryResolver) DataframeBuilderIntrospection(ctx context.Context, input model.DataframeBuilderIntrospectionInput) (*model.DataframeBuilderIntrospection, error) { includePivotOnlyFields := true @@ -63,6 +164,28 @@ func (r *queryResolver) DataframeMaterialization(ctx context.Context, id string) return materializationapi.Model(*value), nil } +// DataframeDatasets is the resolver for the dataframeDatasets field. +func (r *queryResolver) DataframeDatasets(ctx context.Context) ([]*model.DataframeMaterialization, error) { + values, err := r.materializations.Datasets(ctx) + if err != nil { + return nil, err + } + result := make([]*model.DataframeMaterialization, 0, len(values)) + for _, value := range values { + result = append(result, materializationapi.Model(value)) + } + return result, nil +} + +// DataframeDataset is the resolver for the dataframeDataset field. +func (r *queryResolver) DataframeDataset(ctx context.Context, input model.DataframeDatasetInput) (*model.DataframeMaterialization, error) { + value, err := r.materializations.Dataset(ctx, input) + if err != nil { + return nil, err + } + return materializationapi.Model(*value), nil +} + // DataframeRows is the resolver for the dataframeRows field. func (r *queryResolver) DataframeRows(ctx context.Context, input model.DataframeRowsInput) (*model.DataframeRowConnection, error) { page, err := r.materializations.Rows(ctx, input) @@ -77,32 +200,95 @@ func (r *queryResolver) DataframeRows(ctx context.Context, input model.Dataframe if page.NextCursor != "" { cursor = &page.NextCursor } + totalCount := int(page.TotalCount) return &model.DataframeRowConnection{ Materialization: materializationapi.Model(page.Materialization), - Columns: append([]string(nil), page.Columns...), Rows: rows, + Columns: append([]string(nil), page.Columns...), Rows: rows, TotalCount: &totalCount, PageInfo: &model.DataframePageInfo{HasNextPage: page.HasNext, EndCursor: cursor}, }, nil } // DataframeAggregate is the resolver for the dataframeAggregate field. func (r *queryResolver) DataframeAggregate(ctx context.Context, input model.DataframeAggregateInput) (*model.DataframeAggregateResult, error) { - groupBy := append([]string(nil), input.GroupBy...) - column := "" - if input.Column != nil { - column = *input.Column - } - result, err := r.materializations.Aggregate(ctx, input.MaterializationID, groupBy, input.Filters, input.Operation, column) + result, err := r.materializations.AggregateInput(ctx, input) if err != nil { return nil, err } return &model.DataframeAggregateResult{Materialization: materializationapi.Model(result.Materialization), Columns: result.Columns, Rows: materializationapi.AggregateRows(result.Rows)}, nil } +// DataframeRecipeExecution is the resolver for the dataframeRecipeExecution field. +func (r *queryResolver) DataframeRecipeExecution(ctx context.Context, id string) (*model.DataframeRecipeExecution, error) { + if r.recipeExecutions == nil { + return nil, recipeGraphQLError(fmt.Errorf("recipe execution reader is not configured")) + } + execution, err := r.recipeExecutions(ctx, id) + if err != nil { + return nil, recipeGraphQLError(err) + } + if execution == nil { + return nil, recipeGraphQLError(fmt.Errorf("recipe execution %q was not found", id)) + } + return executionModel(*execution), nil +} + +// ExplainDataframeRecipe is the resolver for the explainDataframeRecipe field. +func (r *queryResolver) ExplainDataframeRecipe(ctx context.Context, input model.ExplainDataframeRecipeInput) (*model.DataframeRecipeExplanation, error) { + bindings, err := graphqlRecipeBindings(input.Bindings) + if err != nil { + return nil, recipeGraphQLError(err) + } + bindings, err = r.authorizeRecipeRead(ctx, bindings) + if err != nil { + return nil, recipeGraphQLError(err) + } + if err := requireRecipeControl(r.recipeControl); err != nil { + return nil, recipeGraphQLError(err) + } + explanation, err := r.recipeControl.Explain(ctx, input.Name, bindings) + if err != nil { + return nil, recipeGraphQLError(err) + } + result := planExplanation(explanation, input.Name) + if physicalControl, ok := r.recipeControl.(RecipeExplainEvidenceControl); ok { + physical, err := physicalControl.ExplainPhysical(ctx, input.Name, bindings, input.Live != nil && *input.Live) + if err != nil { + return nil, recipeGraphQLError(err) + } + result.Physical = physicalExplanation(physical) + } else if input.Live != nil && *input.Live { + return nil, recipeGraphQLError(fmt.Errorf("physical recipe explainer is not configured")) + } + return result, nil +} + +// PreflightDataframeRecipe is the resolver for the preflightDataframeRecipe field. +func (r *queryResolver) PreflightDataframeRecipe(ctx context.Context, input model.PreflightDataframeRecipeInput) (*model.DataframeRecipePreflight, error) { + bindings, err := graphqlRecipeBindings(input.Bindings) + if err != nil { + return nil, recipeGraphQLError(err) + } + bindings, err = r.authorizeRecipeRead(ctx, bindings) + if err != nil { + return nil, recipeGraphQLError(err) + } + if err := requireRecipeControl(r.recipeControl); err != nil { + return nil, recipeGraphQLError(err) + } + plan, err := r.recipeControl.Resolve(ctx, input.Name, bindings) + if err != nil { + return nil, recipeGraphQLError(err) + } + return preflightResult(plan, input.Name), nil +} + // Mutation returns MutationResolver implementation. func (r *Resolver) Mutation() MutationResolver { return &mutationResolver{r} } // Query returns QueryResolver implementation. func (r *Resolver) Query() QueryResolver { return &queryResolver{r} } -type mutationResolver struct{ *Resolver } -type queryResolver struct{ *Resolver } +type ( + mutationResolver struct{ *Resolver } + queryResolver struct{ *Resolver } +) diff --git a/internal/authscope/authentication.go b/internal/authscope/authentication.go new file mode 100644 index 0000000..477c7d3 --- /dev/null +++ b/internal/authscope/authentication.go @@ -0,0 +1,69 @@ +package authscope + +import ( + "context" + "crypto/subtle" + "encoding/base64" + "fmt" + "net/http" + "strings" +) + +// BasicAuthenticator authenticates the standalone/operator mode. A basic +// principal is deliberately unrestricted: Basic is a process-level operator +// credential, not a substitute for a per-project Fence ACL. +type BasicAuthenticator struct { + Username string + Password string +} + +func (a BasicAuthenticator) Authenticate(_ context.Context, headers map[string][]string) (*Principal, error) { + raw := strings.TrimSpace(firstHeaderValue(headers, "Authorization")) + if raw == "" { + return nil, fmt.Errorf("authorization is required") + } + prefix, encoded, ok := strings.Cut(raw, " ") + if !ok || !strings.EqualFold(prefix, "basic") || strings.TrimSpace(encoded) == "" { + return nil, fmt.Errorf("authorization must use basic authentication") + } + decoded, err := base64.StdEncoding.DecodeString(strings.TrimSpace(encoded)) + if err != nil { + return nil, fmt.Errorf("invalid basic credentials") + } + username, password, ok := strings.Cut(string(decoded), ":") + if !ok || !constantTimeEqual(username, a.Username) || !constantTimeEqual(password, a.Password) { + return nil, fmt.Errorf("invalid basic credentials") + } + return &Principal{Subject: username}, nil +} + +func constantTimeEqual(left, right string) bool { + leftBytes, rightBytes := []byte(left), []byte(right) + if len(leftBytes) != len(rightBytes) { + // Still perform a comparison to avoid making the common length mismatch + // an immediately observable fast path. + _ = subtle.ConstantTimeCompare(leftBytes, rightBytes) + return false + } + return subtle.ConstantTimeCompare(leftBytes, rightBytes) == 1 +} + +// CalyprAuthenticator is the strict HTTP boundary for Fence-backed mode. It +// validates presence and shape here; the ScopeResolver performs the signed +// resource authorization lookup using the same bearer header. +type CalyprAuthenticator struct{} + +func (CalyprAuthenticator) Authenticate(_ context.Context, headers map[string][]string) (*Principal, error) { + auth := strings.TrimSpace(firstHeaderValue(headers, http.CanonicalHeaderKey("Authorization"))) + if auth == "" { + return nil, fmt.Errorf("authorization is required") + } + if _, err := validateAuthorizationHeader(auth); err != nil { + return nil, err + } + principal := &Principal{AuthorizationHeader: auth, Subject: "anonymous"} + if sub := subjectFromAuthorizationHeader(auth); sub != "" { + principal.Subject = sub + } + return principal, nil +} diff --git a/internal/authscope/authentication_test.go b/internal/authscope/authentication_test.go new file mode 100644 index 0000000..0c3eceb --- /dev/null +++ b/internal/authscope/authentication_test.go @@ -0,0 +1,69 @@ +package authscope + +import ( + "context" + "encoding/base64" + "fmt" + "io" + "net/http" + "strings" + "testing" + "time" +) + +func TestBasicAuthenticator(t *testing.T) { + encoded := base64.StdEncoding.EncodeToString([]byte("loom:secret")) + principal, err := (BasicAuthenticator{Username: "loom", Password: "secret"}).Authenticate(context.Background(), map[string][]string{"Authorization": {"Basic " + encoded}}) + if err != nil { + t.Fatalf("Authenticate() error = %v", err) + } + if principal.Subject != "loom" || principal.AuthorizationHeader != "" { + t.Fatalf("principal = %#v", principal) + } + if _, err := (BasicAuthenticator{Username: "loom", Password: "secret"}).Authenticate(context.Background(), map[string][]string{"Authorization": {"Basic " + base64.StdEncoding.EncodeToString([]byte("loom:wrong"))}}); err == nil { + t.Fatal("wrong credentials unexpectedly authenticated") + } +} + +func TestCalyprAuthenticatorRequiresBearer(t *testing.T) { + if _, err := (CalyprAuthenticator{}).Authenticate(context.Background(), nil); err == nil { + t.Fatal("missing authorization unexpectedly authenticated") + } + principal, err := (CalyprAuthenticator{}).Authenticate(context.Background(), map[string][]string{"Authorization": {"Bearer abc.def.ghi"}}) + if err != nil { + t.Fatalf("Bearer Authenticate() error = %v", err) + } + if principal.AuthorizationHeader != "Bearer abc.def.ghi" { + t.Fatalf("authorization header = %q", principal.AuthorizationHeader) + } +} + +func TestFenceUserAccessClientCachesParsedSnapshot(t *testing.T) { + calls := 0 + transport := roundTripFunc(func(r *http.Request) (*http.Response, error) { + calls++ + if r.URL.Path != "/user/user" { + return nil, fmt.Errorf("Fence path = %q", r.URL.Path) + } + if !strings.HasPrefix(r.Header.Get("Authorization"), "Bearer ") { + return nil, fmt.Errorf("authorization header was not forwarded") + } + return &http.Response{StatusCode: http.StatusOK, Body: io.NopCloser(strings.NewReader(`{"authz":{"/programs/example/projects/demo":[{"method":"read","service":"*"}]}}`)), Header: make(http.Header)}, nil + }) + iss := base64.RawURLEncoding.EncodeToString([]byte(`{"iss":"https://fence.example/user"}`)) + token := "Bearer e30." + iss + ".sig" + client := NewFenceUserAccessClientWithTTL(&http.Client{Transport: transport}, time.Minute) + for i := 0; i < 2; i++ { + resources, err := client.GetAllowedResources(context.Background(), token, "read", "*") + if err != nil || len(resources) != 1 { + t.Fatalf("GetAllowedResources() = %#v, %v", resources, err) + } + } + if calls != 1 { + t.Fatalf("Fence calls = %d, want one cached lookup", calls) + } +} + +type roundTripFunc func(*http.Request) (*http.Response, error) + +func (f roundTripFunc) RoundTrip(r *http.Request) (*http.Response, error) { return f(r) } diff --git a/internal/authscope/fence_resource_access.go b/internal/authscope/fence_resource_access.go new file mode 100644 index 0000000..67d7c65 --- /dev/null +++ b/internal/authscope/fence_resource_access.go @@ -0,0 +1,323 @@ +package authscope + +import ( + "context" + "crypto/sha256" + "encoding/base64" + "encoding/json" + "fmt" + "io" + "net/http" + "sort" + "strings" + "sync" + "time" +) + +type FenceUserAccessClient struct { + client *http.Client + cacheTTL time.Duration + mu sync.Mutex + cache map[string]cachedResourceAccess +} + +type cachedResourceAccess struct { + snapshot map[string][]resourceAccessRecord + expiresAt time.Time +} + +func NewFenceUserAccessClient(client *http.Client) *FenceUserAccessClient { + return NewFenceUserAccessClientWithTTL(client, 30*time.Second) +} + +func NewFenceUserAccessClientWithTTL(client *http.Client, cacheTTL time.Duration) *FenceUserAccessClient { + if client == nil { + client = http.DefaultClient + } + if cacheTTL <= 0 { + cacheTTL = 30 * time.Second + } + return &FenceUserAccessClient{client: client, cacheTTL: cacheTTL, cache: make(map[string]cachedResourceAccess)} +} + +func (c *FenceUserAccessClient) GetAllowedResources(ctx context.Context, authorizationHeader, method, service string) ([]string, error) { + snapshot, err := c.getResourceAccess(ctx, authorizationHeader) + if err != nil { + return nil, err + } + out := make([]string, 0, len(snapshot)) + for resource, records := range snapshot { + if resourceAccessAllows(records, method, service) { + out = append(out, resource) + } + } + sort.Strings(out) + return out, nil +} + +type resourceAccessRecord struct { + Method string + Service string +} + +func (c *FenceUserAccessClient) getResourceAccess(ctx context.Context, authorizationHeader string) (map[string][]resourceAccessRecord, error) { + sum := sha256.Sum256([]byte(authorizationHeader)) + key := fmt.Sprintf("%x", sum[:]) + now := time.Now() + c.mu.Lock() + if cached, ok := c.cache[key]; ok && now.Before(cached.expiresAt) { + snapshot := cloneResourceAccess(cached.snapshot) + c.mu.Unlock() + return snapshot, nil + } + c.mu.Unlock() + endpoint, err := fenceUserEndpoint(authorizationHeader) + if err != nil { + return nil, err + } + authHeader, err := validateAuthorizationHeader(authorizationHeader) + if err != nil { + return nil, err + } + req, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint, nil) + if err != nil { + return nil, fmt.Errorf("build authorization snapshot request: %w", err) + } + req.Header.Set("Authorization", authHeader) + resp, err := c.client.Do(req) + if err != nil { + return nil, fmt.Errorf("authorization snapshot request failed: %w", err) + } + defer resp.Body.Close() + body, err := io.ReadAll(resp.Body) + if err != nil { + return nil, fmt.Errorf("read authorization snapshot response: %w", err) + } + if resp.StatusCode >= 400 { + message := strings.TrimSpace(string(body)) + if message == "" { + message = fmt.Sprintf("authorization snapshot request failed with status %d", resp.StatusCode) + } + return nil, fmt.Errorf("%s", message) + } + var payload map[string]any + if err := json.Unmarshal(body, &payload); err != nil { + return nil, fmt.Errorf("invalid authorization snapshot response: %w", err) + } + snapshot, err := parseResourceAccessSnapshot(payload) + if err != nil { + return nil, err + } + c.mu.Lock() + c.cache[key] = cachedResourceAccess{snapshot: cloneResourceAccess(snapshot), expiresAt: time.Now().Add(c.cacheTTL)} + c.mu.Unlock() + return snapshot, nil +} + +func cloneResourceAccess(in map[string][]resourceAccessRecord) map[string][]resourceAccessRecord { + out := make(map[string][]resourceAccessRecord, len(in)) + for resource, records := range in { + out[resource] = append([]resourceAccessRecord(nil), records...) + } + return out +} + +func parseResourceAccessSnapshot(payload map[string]any) (map[string][]resourceAccessRecord, error) { + resourceAccess, ok := payload["authz"].(map[string]any) + if !ok || len(resourceAccess) == 0 { + resourceAccess, ok = payload["project_access"].(map[string]any) + if !ok { + return nil, fmt.Errorf("authorization snapshot response did not include authz/project_access") + } + } + out := make(map[string][]resourceAccessRecord, len(resourceAccess)) + for resource, raw := range resourceAccess { + entries, ok := raw.([]any) + if !ok { + continue + } + for _, entry := range entries { + record, ok := entry.(map[string]any) + if !ok { + continue + } + method, _ := record["method"].(string) + service, _ := record["service"].(string) + out[resource] = append(out[resource], resourceAccessRecord{Method: method, Service: service}) + } + } + return out, nil +} + +func resourceAccessAllows(records []resourceAccessRecord, method, service string) bool { + for _, record := range records { + if !strings.EqualFold(record.Method, method) && record.Method != "*" { + continue + } + if record.Service == service || record.Service == "*" || service == "*" { + return true + } + } + return false +} + +func NormalizeAuthResourcePath(value string) string { + value = strings.TrimSpace(value) + if value == "" { + return "" + } + if key, ok := calyprProjectKeyFromResourcePath(value); ok { + return key + } + return strings.Trim(value, "/") +} + +func authResourcePathsFromResources(resources []string) []string { + seen := make(map[string]struct{}) + out := make([]string, 0, len(resources)) + for _, resource := range resources { + if key, ok := calyprProjectKeyFromResourcePath(resource); ok { + if _, exists := seen[key]; exists { + continue + } + seen[key] = struct{}{} + out = append(out, key) + } + } + sort.Strings(out) + return out +} + +func normalizeAuthResourcePathList(paths []string) []string { + if paths == nil { + return nil + } + seen := make(map[string]struct{}, len(paths)) + out := make([]string, 0, len(paths)) + for _, path := range paths { + normalized := NormalizeAuthResourcePath(path) + if normalized == "" { + continue + } + if _, ok := seen[normalized]; ok { + continue + } + seen[normalized] = struct{}{} + out = append(out, normalized) + } + sort.Strings(out) + return out +} + +func firstHeaderValue(headers map[string][]string, key string) string { + for headerKey, values := range headers { + if !strings.EqualFold(headerKey, key) || len(values) == 0 { + continue + } + return values[0] + } + return "" +} + +func cloneStrings(in []string) []string { + if in == nil { + return nil + } + out := make([]string, len(in)) + copy(out, in) + return out +} + +func subjectFromAuthorizationHeader(authHeader string) string { + token := cleanAccessToken(authHeader) + if token == "" { + return "" + } + parts := strings.Split(token, ".") + if len(parts) < 2 { + return "" + } + payload, err := base64.RawURLEncoding.DecodeString(parts[1]) + if err != nil { + return "" + } + var claims map[string]any + if err := json.Unmarshal(payload, &claims); err != nil { + return "" + } + if sub, _ := claims["sub"].(string); sub != "" { + return sub + } + if user, _ := claims["user_name"].(string); user != "" { + return user + } + return "" +} + +func fenceUserEndpoint(authorizationHeader string) (string, error) { + token := cleanAccessToken(authorizationHeader) + if token == "" { + return "", fmt.Errorf("authorization header is required") + } + parts := strings.Split(token, ".") + if len(parts) < 2 { + return "", fmt.Errorf("authorization token does not look like a JWT") + } + payload, err := base64.RawURLEncoding.DecodeString(parts[1]) + if err != nil { + return "", fmt.Errorf("decode authorization token payload: %w", err) + } + var claims map[string]any + if err := json.Unmarshal(payload, &claims); err != nil { + return "", fmt.Errorf("parse authorization token payload: %w", err) + } + iss, _ := claims["iss"].(string) + iss = strings.TrimSpace(iss) + if iss == "" { + return "", fmt.Errorf("authorization token does not include iss") + } + return strings.TrimRight(iss, "/") + "/user", nil +} + +func validateAuthorizationHeader(raw string) (string, error) { + header := strings.TrimSpace(raw) + if header == "" { + return "", fmt.Errorf("authorization header is required") + } + if !strings.HasPrefix(strings.ToLower(header), "bearer ") { + return "", fmt.Errorf("authorization header must use bearer auth") + } + return header, nil +} + +func cleanAccessToken(raw string) string { + token := strings.TrimSpace(raw) + if strings.HasPrefix(strings.ToLower(token), "bearer ") { + token = strings.TrimSpace(token[len("bearer "):]) + } + return token +} + +func calyprProjectKeyFromResourcePath(resource string) (string, bool) { + normalized := normalizeResourcePath(resource) + parts := strings.Split(normalized, "/") + if len(parts) < 5 { + return "", false + } + if parts[1] != "programs" || parts[3] != "projects" { + return "", false + } + if parts[2] == "" || parts[4] == "" { + return "", false + } + return parts[2] + "-" + parts[4], true +} + +func normalizeResourcePath(resource string) string { + resource = strings.TrimSpace(resource) + if resource == "" { + return "" + } + resource = "/" + strings.Trim(resource, "/") + return strings.TrimSuffix(resource, "/") +} diff --git a/internal/authscope/principal.go b/internal/authscope/principal.go index 56db2e2..63d6949 100644 --- a/internal/authscope/principal.go +++ b/internal/authscope/principal.go @@ -2,6 +2,17 @@ package authscope import "context" +// Permission is the method recorded by Fence for a resource grant. +// Values are intentionally normalized at the authorization boundary so Loom +// accepts both the lowercase values emitted by current Gen3 services and +// uppercase values used by some deployments. +type Permission string + +const ( + PermissionRead Permission = "read" + PermissionWrite Permission = "write" +) + type Principal struct { Subject string `json:"subject"` Claims map[string]string `json:"claims,omitempty"` diff --git a/internal/authscope/scope.go b/internal/authscope/scope_resolver.go similarity index 60% rename from internal/authscope/scope.go rename to internal/authscope/scope_resolver.go index 0e4d2e3..ecd862f 100644 --- a/internal/authscope/scope.go +++ b/internal/authscope/scope_resolver.go @@ -2,12 +2,7 @@ package authscope import ( "context" - "encoding/base64" - "encoding/json" "fmt" - "io" - "net/http" - "sort" "strings" "sync" "time" @@ -149,7 +144,7 @@ func (r *ScopeResolver) ResolveReadScope(ctx context.Context, principal *Princip // every generation. Callers that have selected an active manifest must use // this method so the scope cache and catalog reads remain generation-aligned. func (r *ScopeResolver) ResolveReadScopeForGeneration(ctx context.Context, principal *Principal, project, datasetGeneration string, requested []string) (ReadScope, error) { - callerPaths, restricted, err := r.resolveCallerPaths(ctx, principal, "read", "*") + callerPaths, restricted, err := r.resolveCallerPaths(ctx, principal, PermissionRead, "*") if err != nil { return ReadScope{}, err } @@ -213,7 +208,7 @@ func (r *ScopeResolver) ResolveReadAuthResourcePathsForGeneration(ctx context.Co } func (r *ScopeResolver) AuthorizeWrite(ctx context.Context, principal *Principal, project, authResourcePath string) error { - callerPaths, restricted, err := r.resolveCallerPaths(ctx, principal, "read", "*") + callerPaths, restricted, err := r.resolveCallerPaths(ctx, principal, PermissionWrite, "*") if err != nil { return err } @@ -232,7 +227,55 @@ func (r *ScopeResolver) AuthorizeWrite(ctx context.Context, principal *Principal return fmt.Errorf("auth_resource_path %q is outside caller scope for project %q", normalized, project) } -func (r *ScopeResolver) resolveCallerPaths(ctx context.Context, principal *Principal, method, service string) ([]string, bool, error) { +// ResolveWriteScopeForGeneration proves that the caller may publish or mutate +// data for the selected project. It mirrors read-scope resolution but asks +// Fence for write permission and never treats an empty intersection as an +// unrestricted result. +func (r *ScopeResolver) ResolveWriteScopeForGeneration(ctx context.Context, principal *Principal, project, datasetGeneration string, requested []string) (ReadScope, error) { + callerPaths, restricted, err := r.resolveCallerPaths(ctx, principal, PermissionWrite, "*") + if err != nil { + return ReadScope{}, err + } + normalizedRequested := normalizeAuthResourcePathList(requested) + if !restricted { + if len(normalizedRequested) == 0 { + return ReadScope{Mode: ReadScopeUnrestricted}, nil + } + return ReadScope{AuthResourcePaths: normalizedRequested, Mode: ReadScopeRestricted}, nil + } + existingPaths, err := r.listExistingPaths(ctx, project, datasetGeneration) + if err != nil { + return ReadScope{}, err + } + allowed := make(map[string]struct{}, len(callerPaths)) + for _, path := range callerPaths { + allowed[path] = struct{}{} + } + effective := make([]string, 0, len(existingPaths)) + for _, path := range existingPaths { + if _, ok := allowed[path]; ok { + effective = append(effective, path) + } + } + if len(normalizedRequested) > 0 { + effectiveSet := make(map[string]struct{}, len(effective)) + for _, path := range effective { + effectiveSet[path] = struct{}{} + } + for _, path := range normalizedRequested { + if _, ok := effectiveSet[path]; !ok { + return ReadScope{}, fmt.Errorf("authResourcePath %q is outside caller write scope", path) + } + } + effective = normalizedRequested + } + if len(effective) == 0 { + return ReadScope{}, fmt.Errorf("caller has no write access to project %q", project) + } + return ReadScope{AuthResourcePaths: effective, Mode: ReadScopeRestricted}, nil +} + +func (r *ScopeResolver) resolveCallerPaths(ctx context.Context, principal *Principal, method Permission, service string) ([]string, bool, error) { if principal == nil { if r.resourceAccess != nil { return []string{}, true, nil @@ -240,7 +283,7 @@ func (r *ScopeResolver) resolveCallerPaths(ctx context.Context, principal *Princ return nil, false, nil } if auth := strings.TrimSpace(principal.AuthorizationHeader); auth != "" && r.resourceAccess != nil { - resources, err := r.resourceAccess.GetAllowedResources(ctx, auth, method, service) + resources, err := r.resourceAccess.GetAllowedResources(ctx, auth, string(method), service) if err != nil { return nil, true, err } @@ -319,271 +362,3 @@ func (r *ScopeResolver) InvalidateGeneration(project, datasetGeneration string) delete(r.cache, key) r.mu.Unlock() } - -type FenceUserAccessClient struct { - client *http.Client -} - -func NewFenceUserAccessClient(client *http.Client) *FenceUserAccessClient { - if client == nil { - client = http.DefaultClient - } - return &FenceUserAccessClient{client: client} -} - -func (c *FenceUserAccessClient) GetAllowedResources(ctx context.Context, authorizationHeader, method, service string) ([]string, error) { - snapshot, err := c.getResourceAccess(ctx, authorizationHeader) - if err != nil { - return nil, err - } - out := make([]string, 0, len(snapshot)) - for resource, records := range snapshot { - if resourceAccessAllows(records, method, service) { - out = append(out, resource) - } - } - sort.Strings(out) - return out, nil -} - -type resourceAccessRecord struct { - Method string - Service string -} - -func (c *FenceUserAccessClient) getResourceAccess(ctx context.Context, authorizationHeader string) (map[string][]resourceAccessRecord, error) { - endpoint, err := fenceUserEndpoint(authorizationHeader) - if err != nil { - return nil, err - } - authHeader, err := validateAuthorizationHeader(authorizationHeader) - if err != nil { - return nil, err - } - req, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint, nil) - if err != nil { - return nil, fmt.Errorf("build authorization snapshot request: %w", err) - } - req.Header.Set("Authorization", authHeader) - resp, err := c.client.Do(req) - if err != nil { - return nil, fmt.Errorf("authorization snapshot request failed: %w", err) - } - defer resp.Body.Close() - body, err := io.ReadAll(resp.Body) - if err != nil { - return nil, fmt.Errorf("read authorization snapshot response: %w", err) - } - if resp.StatusCode >= 400 { - message := strings.TrimSpace(string(body)) - if message == "" { - message = fmt.Sprintf("authorization snapshot request failed with status %d", resp.StatusCode) - } - return nil, fmt.Errorf("%s", message) - } - var payload map[string]any - if err := json.Unmarshal(body, &payload); err != nil { - return nil, fmt.Errorf("invalid authorization snapshot response: %w", err) - } - return parseResourceAccessSnapshot(payload) -} - -func parseResourceAccessSnapshot(payload map[string]any) (map[string][]resourceAccessRecord, error) { - resourceAccess, ok := payload["authz"].(map[string]any) - if !ok || len(resourceAccess) == 0 { - resourceAccess, ok = payload["project_access"].(map[string]any) - if !ok { - return nil, fmt.Errorf("authorization snapshot response did not include authz/project_access") - } - } - out := make(map[string][]resourceAccessRecord, len(resourceAccess)) - for resource, raw := range resourceAccess { - entries, ok := raw.([]any) - if !ok { - continue - } - for _, entry := range entries { - record, ok := entry.(map[string]any) - if !ok { - continue - } - method, _ := record["method"].(string) - service, _ := record["service"].(string) - out[resource] = append(out[resource], resourceAccessRecord{Method: method, Service: service}) - } - } - return out, nil -} - -func resourceAccessAllows(records []resourceAccessRecord, method, service string) bool { - for _, record := range records { - if record.Method != method && record.Method != "*" { - continue - } - if record.Service == service || record.Service == "*" || service == "*" { - return true - } - } - return false -} - -func NormalizeAuthResourcePath(value string) string { - value = strings.TrimSpace(value) - if value == "" { - return "" - } - if key, ok := calyprProjectKeyFromResourcePath(value); ok { - return key - } - return strings.Trim(value, "/") -} - -func authResourcePathsFromResources(resources []string) []string { - seen := make(map[string]struct{}) - out := make([]string, 0, len(resources)) - for _, resource := range resources { - if key, ok := calyprProjectKeyFromResourcePath(resource); ok { - if _, exists := seen[key]; exists { - continue - } - seen[key] = struct{}{} - out = append(out, key) - } - } - sort.Strings(out) - return out -} - -func normalizeAuthResourcePathList(paths []string) []string { - if paths == nil { - return nil - } - seen := make(map[string]struct{}, len(paths)) - out := make([]string, 0, len(paths)) - for _, path := range paths { - normalized := NormalizeAuthResourcePath(path) - if normalized == "" { - continue - } - if _, ok := seen[normalized]; ok { - continue - } - seen[normalized] = struct{}{} - out = append(out, normalized) - } - sort.Strings(out) - return out -} - -func firstHeaderValue(headers map[string][]string, key string) string { - for headerKey, values := range headers { - if !strings.EqualFold(headerKey, key) || len(values) == 0 { - continue - } - return values[0] - } - return "" -} - -func cloneStrings(in []string) []string { - if in == nil { - return nil - } - out := make([]string, len(in)) - copy(out, in) - return out -} - -func subjectFromAuthorizationHeader(authHeader string) string { - token := cleanAccessToken(authHeader) - if token == "" { - return "" - } - parts := strings.Split(token, ".") - if len(parts) < 2 { - return "" - } - payload, err := base64.RawURLEncoding.DecodeString(parts[1]) - if err != nil { - return "" - } - var claims map[string]any - if err := json.Unmarshal(payload, &claims); err != nil { - return "" - } - if sub, _ := claims["sub"].(string); sub != "" { - return sub - } - if user, _ := claims["user_name"].(string); user != "" { - return user - } - return "" -} - -func fenceUserEndpoint(authorizationHeader string) (string, error) { - token := cleanAccessToken(authorizationHeader) - if token == "" { - return "", fmt.Errorf("authorization header is required") - } - parts := strings.Split(token, ".") - if len(parts) < 2 { - return "", fmt.Errorf("authorization token does not look like a JWT") - } - payload, err := base64.RawURLEncoding.DecodeString(parts[1]) - if err != nil { - return "", fmt.Errorf("decode authorization token payload: %w", err) - } - var claims map[string]any - if err := json.Unmarshal(payload, &claims); err != nil { - return "", fmt.Errorf("parse authorization token payload: %w", err) - } - iss, _ := claims["iss"].(string) - iss = strings.TrimSpace(iss) - if iss == "" { - return "", fmt.Errorf("authorization token does not include iss") - } - return strings.TrimRight(iss, "/") + "/user", nil -} - -func validateAuthorizationHeader(raw string) (string, error) { - header := strings.TrimSpace(raw) - if header == "" { - return "", fmt.Errorf("authorization header is required") - } - if !strings.HasPrefix(strings.ToLower(header), "bearer ") { - return "", fmt.Errorf("authorization header must use bearer auth") - } - return header, nil -} - -func cleanAccessToken(raw string) string { - token := strings.TrimSpace(raw) - if strings.HasPrefix(strings.ToLower(token), "bearer ") { - token = strings.TrimSpace(token[len("bearer "):]) - } - return token -} - -func calyprProjectKeyFromResourcePath(resource string) (string, bool) { - normalized := normalizeResourcePath(resource) - parts := strings.Split(normalized, "/") - if len(parts) < 5 { - return "", false - } - if parts[1] != "programs" || parts[3] != "projects" { - return "", false - } - if parts[2] == "" || parts[4] == "" { - return "", false - } - return parts[2] + "-" + parts[4], true -} - -func normalizeResourcePath(resource string) string { - resource = strings.TrimSpace(resource) - if resource == "" { - return "" - } - resource = "/" + strings.Trim(resource, "/") - return strings.TrimSuffix(resource, "/") -} diff --git a/internal/authscope/scope_test.go b/internal/authscope/scope_test.go index 3608cdf..3ae3467 100644 --- a/internal/authscope/scope_test.go +++ b/internal/authscope/scope_test.go @@ -17,6 +17,16 @@ func (f fakeResourceAccessClient) GetAllowedResources(ctx context.Context, autho return append([]string(nil), f.resources...), nil } +type recordingResourceAccessClient struct { + resources []string + method string +} + +func (f *recordingResourceAccessClient) GetAllowedResources(ctx context.Context, authorizationHeader, method, service string) ([]string, error) { + f.method = method + return append([]string(nil), f.resources...), nil +} + func TestScopeResolverResolveReadAuthResourcePathsIntersectsDBPaths(t *testing.T) { resolver := NewScopeResolver(ScopeResolverConfig{ ConnectionOptions: arangostore.ConnectionOptions{}, @@ -198,6 +208,26 @@ func TestScopeAuthorizerRequiresScopedWritePath(t *testing.T) { } } +func TestScopeResolverUsesWritePermissionForWriteScope(t *testing.T) { + client := &recordingResourceAccessClient{resources: []string{"/programs/example/projects/allowed"}} + resolver := NewScopeResolver(ScopeResolverConfig{ + ResourceAccess: client, + ListExistingAuthResourcePaths: func(context.Context, catalog.AuthResourcePathOptions) ([]string, error) { + return []string{"example-allowed"}, nil + }, + }) + scope, err := resolver.ResolveWriteScopeForGeneration(context.Background(), &Principal{AuthorizationHeader: "Bearer token"}, "P1", "g", nil) + if err != nil { + t.Fatalf("ResolveWriteScopeForGeneration() error = %v", err) + } + if client.method != string(PermissionWrite) { + t.Fatalf("Fence method = %q, want %q", client.method, PermissionWrite) + } + if scope.Unrestricted() || len(scope.AuthResourcePaths) != 1 { + t.Fatalf("scope = %#v", scope) + } +} + func TestNormalizeAuthResourcePathAcceptsResourcePath(t *testing.T) { got := NormalizeAuthResourcePath("/programs/EllrottLab/projects/GDC_Data") if got != "EllrottLab-GDC_Data" { diff --git a/internal/catalog/read_fields.go b/internal/catalog/read_fields.go index 97bde76..0c334fa 100644 --- a/internal/catalog/read_fields.go +++ b/internal/catalog/read_fields.go @@ -31,7 +31,10 @@ FOR d IN fhir_field_catalog pivot_columns: d.pivot_columns, pivot_family: d.pivot_family, pivot_column_selector: d.pivot_column_selector, - pivot_value_selector: d.pivot_value_selector + pivot_value_selector: d.pivot_value_selector, + pivot_item_source: d.pivot_item_source, + pivot_item_resource_type: d.pivot_item_resource_type, + pivot_value_selectors: d.pivot_value_selectors } ` @@ -62,22 +65,25 @@ func DiscoverPopulatedFields(ctx context.Context, opts PopulatedFieldOptions) ([ results := make([]PopulatedField, 0, 64) err = client.QueryRows(ctx, populatedFieldsAQL, opts.CursorBatch, bindVars, func(row map[string]any) error { results = append(results, PopulatedField{ - Project: stringValue(row["project"]), - DatasetGeneration: stringValue(row["dataset_generation"]), - AuthResourcePath: stringValue(row["auth_resource_path"]), - ResourceType: stringValue(row["resource_type"]), - Path: stringValue(row["path"]), - Kind: stringValue(row["kind"]), - DocCount: int64Must(row["doc_count"]), - SampleCount: int(int64Must(row["sample_count"])), - DistinctValues: stringSliceValue(row["distinct_values"]), - DistinctTruncated: boolValue(row["distinct_truncated"]), - PivotCandidate: boolValue(row["pivot_candidate"]), - PivotKind: stringValue(row["pivot_kind"]), - PivotColumns: stringSliceValue(row["pivot_columns"]), - PivotFamily: stringValue(row["pivot_family"]), - PivotColumnSelect: stringValue(row["pivot_column_selector"]), - PivotValueSelect: stringValue(row["pivot_value_selector"]), + Project: stringValue(row["project"]), + DatasetGeneration: stringValue(row["dataset_generation"]), + AuthResourcePath: stringValue(row["auth_resource_path"]), + ResourceType: stringValue(row["resource_type"]), + Path: stringValue(row["path"]), + Kind: stringValue(row["kind"]), + DocCount: int64Must(row["doc_count"]), + SampleCount: int(int64Must(row["sample_count"])), + DistinctValues: stringSliceValue(row["distinct_values"]), + DistinctTruncated: boolValue(row["distinct_truncated"]), + PivotCandidate: boolValue(row["pivot_candidate"]), + PivotKind: stringValue(row["pivot_kind"]), + PivotColumns: stringSliceValue(row["pivot_columns"]), + PivotFamily: stringValue(row["pivot_family"]), + PivotColumnSelect: stringValue(row["pivot_column_selector"]), + PivotValueSelect: stringValue(row["pivot_value_selector"]), + PivotItemSource: stringValue(row["pivot_item_source"]), + PivotItemResourceType: stringValue(row["pivot_item_resource_type"]), + PivotValueSelectors: stringSliceValue(row["pivot_value_selectors"]), }) return nil }) diff --git a/internal/catalog/types.go b/internal/catalog/types.go index a582874..0b3d47b 100644 --- a/internal/catalog/types.go +++ b/internal/catalog/types.go @@ -33,21 +33,24 @@ type FieldCatalogDocument struct { // produced this catalog row. An omitted value is intentionally the legacy // dataset namespace; readers always bind either this exact value or null so // legacy and generation-qualified observations never mix. - DatasetGeneration string `json:"dataset_generation,omitempty"` - AuthResourcePath string `json:"auth_resource_path,omitempty"` - ResourceType string `json:"resource_type"` - Path string `json:"path"` - Kind string `json:"kind"` - DocCount int64 `json:"doc_count"` - SampleCount int `json:"sample_count"` - DistinctValues []string `json:"distinct_values,omitempty"` - DistinctTruncated bool `json:"distinct_truncated"` - PivotCandidate bool `json:"pivot_candidate"` - PivotKind string `json:"pivot_kind,omitempty"` - PivotColumns []string `json:"pivot_columns,omitempty"` - PivotFamily string `json:"pivot_family,omitempty"` - PivotColumnSelect string `json:"pivot_column_selector,omitempty"` - PivotValueSelect string `json:"pivot_value_selector,omitempty"` + DatasetGeneration string `json:"dataset_generation,omitempty"` + AuthResourcePath string `json:"auth_resource_path,omitempty"` + ResourceType string `json:"resource_type"` + Path string `json:"path"` + Kind string `json:"kind"` + DocCount int64 `json:"doc_count"` + SampleCount int `json:"sample_count"` + DistinctValues []string `json:"distinct_values,omitempty"` + DistinctTruncated bool `json:"distinct_truncated"` + PivotCandidate bool `json:"pivot_candidate"` + PivotKind string `json:"pivot_kind,omitempty"` + PivotColumns []string `json:"pivot_columns,omitempty"` + PivotFamily string `json:"pivot_family,omitempty"` + PivotColumnSelect string `json:"pivot_column_selector,omitempty"` + PivotValueSelect string `json:"pivot_value_selector,omitempty"` + PivotItemSource string `json:"pivot_item_source,omitempty"` + PivotItemResourceType string `json:"pivot_item_resource_type,omitempty"` + PivotValueSelectors []string `json:"pivot_value_selectors,omitempty"` } // Read-side field discovery request and response types. @@ -112,22 +115,25 @@ type ResourceTypeSummary struct { } type PopulatedField struct { - Project string `json:"project"` - DatasetGeneration string `json:"dataset_generation,omitempty"` - AuthResourcePath string `json:"auth_resource_path,omitempty"` - ResourceType string `json:"resource_type"` - Path string `json:"path"` - Kind string `json:"kind"` - DocCount int64 `json:"doc_count"` - SampleCount int `json:"sample_count"` - DistinctValues []string `json:"distinct_values,omitempty"` - DistinctTruncated bool `json:"distinct_truncated"` - PivotCandidate bool `json:"pivot_candidate"` - PivotKind string `json:"pivot_kind,omitempty"` - PivotColumns []string `json:"pivot_columns,omitempty"` - PivotFamily string `json:"pivot_family,omitempty"` - PivotColumnSelect string `json:"pivot_column_selector,omitempty"` - PivotValueSelect string `json:"pivot_value_selector,omitempty"` + Project string `json:"project"` + DatasetGeneration string `json:"dataset_generation,omitempty"` + AuthResourcePath string `json:"auth_resource_path,omitempty"` + ResourceType string `json:"resource_type"` + Path string `json:"path"` + Kind string `json:"kind"` + DocCount int64 `json:"doc_count"` + SampleCount int `json:"sample_count"` + DistinctValues []string `json:"distinct_values,omitempty"` + DistinctTruncated bool `json:"distinct_truncated"` + PivotCandidate bool `json:"pivot_candidate"` + PivotKind string `json:"pivot_kind,omitempty"` + PivotColumns []string `json:"pivot_columns,omitempty"` + PivotFamily string `json:"pivot_family,omitempty"` + PivotColumnSelect string `json:"pivot_column_selector,omitempty"` + PivotValueSelect string `json:"pivot_value_selector,omitempty"` + PivotItemSource string `json:"pivot_item_source,omitempty"` + PivotItemResourceType string `json:"pivot_item_resource_type,omitempty"` + PivotValueSelectors []string `json:"pivot_value_selectors,omitempty"` } // Read-side auth path discovery request type. @@ -197,19 +203,22 @@ type Profiler struct { } type fieldCatalogStats struct { - path string - kind string - docCount int64 - distinctValues []string - distinctSet map[string]struct{} - distinctTruncated bool - pivotCandidate bool - pivotKind string - pivotColumns []string - pivotColumnSet map[string]struct{} - pivotFamily string - pivotColumnSelect string - pivotValueSelect string + path string + kind string + docCount int64 + distinctValues []string + distinctSet map[string]struct{} + distinctTruncated bool + pivotCandidate bool + pivotKind string + pivotColumns []string + pivotColumnSet map[string]struct{} + pivotFamily string + pivotColumnSelect string + pivotValueSelect string + pivotItemSource string + pivotItemResourceType string + pivotValueSelectors []string } // Shared write-side shape planning cache. diff --git a/internal/catalog/write_profiler.go b/internal/catalog/write_profiler.go index fe4e80b..b5e6e83 100644 --- a/internal/catalog/write_profiler.go +++ b/internal/catalog/write_profiler.go @@ -131,21 +131,25 @@ func (p *Profiler) Merge(other *Profiler) error { stat, ok := p.stats[path] if !ok { stat = &fieldCatalogStats{ - path: otherStat.path, - kind: otherStat.kind, - pivotCandidate: otherStat.pivotCandidate, - pivotKind: otherStat.pivotKind, - pivotFamily: otherStat.pivotFamily, - pivotColumnSelect: otherStat.pivotColumnSelect, - pivotValueSelect: otherStat.pivotValueSelect, - distinctSet: make(map[string]struct{}), - pivotColumnSet: make(map[string]struct{}), + path: otherStat.path, + kind: otherStat.kind, + pivotCandidate: otherStat.pivotCandidate, + pivotKind: otherStat.pivotKind, + pivotFamily: otherStat.pivotFamily, + pivotColumnSelect: otherStat.pivotColumnSelect, + pivotValueSelect: otherStat.pivotValueSelect, + pivotItemSource: otherStat.pivotItemSource, + pivotItemResourceType: otherStat.pivotItemResourceType, + pivotValueSelectors: append([]string(nil), otherStat.pivotValueSelectors...), + distinctSet: make(map[string]struct{}), + pivotColumnSet: make(map[string]struct{}), } p.stats[path] = stat } stat.docCount += otherStat.docCount stat.distinctTruncated = stat.distinctTruncated || otherStat.distinctTruncated stat.setPivotDefaults(otherStat.pivotFamily, otherStat.pivotColumnSelect, otherStat.pivotValueSelect) + stat.setPivotScope(otherStat.pivotItemSource, otherStat.pivotItemResourceType, otherStat.pivotValueSelectors) for _, value := range otherStat.distinctValues { stat.addDistinct(value) } @@ -171,23 +175,26 @@ func (p *Profiler) Documents() []FieldCatalogDocument { slices.Sort(distinctValues) slices.Sort(pivotColumns) out = append(out, FieldCatalogDocument{ - Key: fieldCatalogKeyForGeneration(p.project, datasetGeneration, p.authResourcePath, p.resourceType, stat.path), - Project: p.project, - DatasetGeneration: datasetGeneration, - AuthResourcePath: p.authResourcePath, - ResourceType: p.resourceType, - Path: stat.path, - Kind: stat.kind, - DocCount: stat.docCount, - SampleCount: len(distinctValues), - DistinctValues: distinctValues, - DistinctTruncated: stat.distinctTruncated, - PivotCandidate: stat.pivotCandidate, - PivotKind: stat.pivotKind, - PivotColumns: pivotColumns, - PivotFamily: stat.pivotFamily, - PivotColumnSelect: stat.pivotColumnSelect, - PivotValueSelect: stat.pivotValueSelect, + Key: fieldCatalogKeyForGeneration(p.project, datasetGeneration, p.authResourcePath, p.resourceType, stat.path), + Project: p.project, + DatasetGeneration: datasetGeneration, + AuthResourcePath: p.authResourcePath, + ResourceType: p.resourceType, + Path: stat.path, + Kind: stat.kind, + DocCount: stat.docCount, + SampleCount: len(distinctValues), + DistinctValues: distinctValues, + DistinctTruncated: stat.distinctTruncated, + PivotCandidate: stat.pivotCandidate, + PivotKind: stat.pivotKind, + PivotColumns: pivotColumns, + PivotFamily: stat.pivotFamily, + PivotColumnSelect: stat.pivotColumnSelect, + PivotValueSelect: stat.pivotValueSelect, + PivotItemSource: stat.pivotItemSource, + PivotItemResourceType: stat.pivotItemResourceType, + PivotValueSelectors: append([]string(nil), stat.pivotValueSelectors...), }) } return out @@ -210,6 +217,9 @@ func (p *Profiler) ensureStat(field *fieldPlan) *fieldCatalogStats { stat.pivotFamily = spec.Family stat.pivotColumnSelect = fhirschema.SelectorExpression(spec.ColumnSelector) stat.pivotValueSelect = fhirschema.SelectorExpression(spec.ValueSelector) + stat.pivotItemSource = spec.ItemSourcePath + stat.pivotItemResourceType = spec.ItemResourceType + stat.pivotValueSelectors = selectorExpressions(spec.ValueSelectors) } } p.stats[field.Path] = stat @@ -260,6 +270,28 @@ func (s *fieldCatalogStats) setPivotDefaults(family string, columnSelector strin } } +func (s *fieldCatalogStats) setPivotScope(itemSource, itemResourceType string, valueSelectors []string) { + if strings.TrimSpace(itemSource) != "" { + s.pivotItemSource = itemSource + } + if strings.TrimSpace(itemResourceType) != "" { + s.pivotItemResourceType = itemResourceType + } + if len(valueSelectors) > 0 { + s.pivotValueSelectors = append([]string(nil), valueSelectors...) + } +} + +func selectorExpressions(selectors []fhirschema.FieldSelectorSpec) []string { + result := make([]string, 0, len(selectors)) + for _, selector := range selectors { + if value := strings.TrimSpace(fhirschema.SelectorExpression(selector)); value != "" { + result = append(result, value) + } + } + return result +} + func (c *ShapePlanCache) getOrBuild(fingerprint string, payload map[string]any) *shapePlan { c.mu.RLock() plan, ok := c.plans[fingerprint] @@ -412,7 +444,11 @@ func (p *Profiler) observeObservationCodePivot(payload map[string]any) { } stat.pivotCandidate = true stat.pivotKind = pivotKindObservation - stat.setPivotDefaults(fhirschema.PivotFamilyObservationCodeValue, "code.coding[].display", valueSelector) + columnSelector := "code.coding[].display" + if _, hasText := codeValue["text"]; hasText { + columnSelector = "code.text" + } + stat.setPivotDefaults(fhirschema.PivotFamilyObservationCodeValue, columnSelector, valueSelector) for _, col := range codeableConceptColumns(codeValue) { stat.addPivotColumn(col) } diff --git a/internal/dataframe/api.go b/internal/dataframe/api.go index acdd29c..1c32b06 100644 --- a/internal/dataframe/api.go +++ b/internal/dataframe/api.go @@ -1,13 +1,21 @@ -// Package dataframe is Loom's stable dataframe compatibility facade. +// Package dataframe is Loom's stable dataframe facade. // // Runtime orchestration, compiler contracts, and user errors are re-exported -// from their canonical packages here so existing GraphQL, CLI, and -// conformance callers do not depend on implementation layout. +// from their canonical packages here so GraphQL, CLI, and conformance callers +// share one recipe/compiler contract without depending on implementation +// layout. package dataframe import ( "github.com/calypr/loom/internal/dataframe/compiler" dataframeerrors "github.com/calypr/loom/internal/dataframe/errors" + "github.com/calypr/loom/internal/dataframe/expression" + "github.com/calypr/loom/internal/dataframe/materialization" + "github.com/calypr/loom/internal/dataframe/recipe" + "github.com/calypr/loom/internal/dataframe/recipe/control" + "github.com/calypr/loom/internal/dataframe/recipe/engine" + "github.com/calypr/loom/internal/dataframe/recipe/exec" + "github.com/calypr/loom/internal/dataframe/recipe/plan" "github.com/calypr/loom/internal/dataframe/runtime" ) @@ -23,12 +31,6 @@ type ( QueryDiagnostics = runtime.QueryDiagnostics StreamResult = runtime.StreamResult - Builder = compiler.Builder - TraversalStep = compiler.TraversalStep - RepresentativeSlice = compiler.RepresentativeSlice - FieldSelect = compiler.FieldSelect - PivotSelect = compiler.PivotSelect - AggregateSelect = compiler.AggregateSelect CompiledQuery = compiler.CompiledQuery SemanticPlan = compiler.SemanticPlan SemanticNode = compiler.SemanticNode @@ -54,6 +56,37 @@ type ( SelectorStep = compiler.SelectorStep ContainsFilter = compiler.ContainsFilter + RecipeBundle = recipe.Bundle + RecipeOutput = recipe.Output + RecipeRuntimeBindings = recipe.RuntimeBindings + RecipeExpression = recipe.Expression + RecipeRegistry = exec.Registry + RecipeRunner = exec.Runner + RecipeResult = exec.Result + RecipePlan = compiler.RecipePlan + RecipeOutputPlan = compiler.OutputPlan + RecipeResolvedPlan = compiler.ResolvedRecipePlan + RecipeResolvedColumn = compiler.ResolvedColumn + RecipeFrozenSchema = plan.FrozenSchema + RecipeDynamicSpec = plan.DynamicSpec + RecipeColumnCandidate = plan.Candidate + RecipePlanColumn = plan.Column + RecipeControlService = control.Service + RecipeValidation = control.Validation + RecipePreview = control.Preview + RecipeEngine = engine.Engine + RecipeEngineControl = engine.Control + RecipeEngineConfig = engine.Config + RecipeEngineResolved = engine.Resolved + RecipeOutputStream = engine.OutputStream + RecipeEngineStreamResult = engine.StreamResult + BundleOutput = materialization.BundleOutput + AtomicBundleStore = materialization.AtomicBundleStore + AtomicBundleTx = materialization.AtomicBundleTx + Expression = expression.Expression + ExpressionType = expression.Type + CheckedExpression = expression.CheckedExpression + PhysicalOptimizationPolicy = compiler.PhysicalOptimizationPolicy PhysicalOptimizationRule = compiler.PhysicalOptimizationRule PhysicalOptimizationDecision = compiler.PhysicalOptimizationDecision @@ -242,49 +275,56 @@ const ( ) var ( - NewService = runtime.NewService - ExecuteQueryRows = runtime.ExecuteQueryRows - ExplainCompiledQuery = runtime.ExplainCompiledQuery - ProfileCompiledQuery = runtime.ProfileCompiledQuery - DefaultPhysicalOptimizationPolicy = compiler.DefaultPhysicalOptimizationPolicy - CompileRequest = compiler.CompileRequest - CompileRequestWithPolicy = compiler.CompileRequestWithPolicy - BuildSemanticPlan = compiler.BuildSemanticPlan - ValidateSemanticGraph = compiler.ValidateSemanticGraph - BuildPhysicalPlan = compiler.BuildPhysicalPlan - BuildPhysicalPlanWithPolicy = compiler.BuildPhysicalPlanWithPolicy - BuildGenericPhysicalPlan = compiler.BuildGenericPhysicalPlan - BuildGenericPhysicalPlanWithPolicy = compiler.BuildGenericPhysicalPlanWithPolicy - OptimizePhysicalPlan = compiler.OptimizePhysicalPlan - OptimizePhysicalPlanWithPolicy = compiler.OptimizePhysicalPlanWithPolicy - RenderPhysicalPlan = compiler.RenderPhysicalPlan - ParseSelector = compiler.ParseSelector - ValidateTypedFilterForResource = compiler.ValidateTypedFilterForResource - NormalizeSelectionPlan = compiler.NormalizeSelectionPlan - ResolveSemanticField = compiler.ResolveSemanticField - InferRowGrain = compiler.InferRowGrain - RootResourceForGrain = compiler.RootResourceForGrain - ValidateRootGrain = compiler.ValidateRootGrain - DefaultRowIdentity = compiler.DefaultRowIdentity - ValidateProjection = compiler.ValidateProjection - OperatorSupportsKind = compiler.OperatorSupportsKind - ValidateGenericPhysicalPlanScope = compiler.ValidateGenericPhysicalPlanScope - DecomposePhysicalTraversalPrefix = compiler.DecomposePhysicalTraversalPrefix - ResolveStorageRoute = compiler.ResolveStorageRoute - AsUserError = dataframeerrors.AsUserError - Normalize = dataframeerrors.Normalize - PublicMessage = dataframeerrors.PublicMessage - NewError = dataframeerrors.NewError - Wrap = dataframeerrors.Wrap - WithFieldPath = dataframeerrors.WithFieldPath - WithDetails = dataframeerrors.WithDetails - WithRetryable = dataframeerrors.WithRetryable - WithCause = dataframeerrors.WithCause - IsUserCorrectable = dataframeerrors.IsUserCorrectable - IsRetryableCode = dataframeerrors.IsRetryableCode - IsOperatorFailure = dataframeerrors.IsOperatorFailure - Errorf = dataframeerrors.Errorf - AllErrorCodes = dataframeerrors.AllErrorCodes - ErrBackendUnavailable = dataframeerrors.ErrBackendUnavailable - ErrClientCanceled = dataframeerrors.ErrClientCanceled + NewService = runtime.NewService + ExecuteQueryRows = runtime.ExecuteQueryRows + ExplainCompiledQuery = runtime.ExplainCompiledQuery + ProfileCompiledQuery = runtime.ProfileCompiledQuery + DefaultPhysicalOptimizationPolicy = compiler.DefaultPhysicalOptimizationPolicy + ValidateSemanticGraph = compiler.ValidateSemanticGraph + BuildPhysicalPlan = compiler.BuildPhysicalPlan + BuildPhysicalPlanWithPolicy = compiler.BuildPhysicalPlanWithPolicy + BuildGenericPhysicalPlan = compiler.BuildGenericPhysicalPlan + BuildGenericPhysicalPlanWithPolicy = compiler.BuildGenericPhysicalPlanWithPolicy + OptimizePhysicalPlan = compiler.OptimizePhysicalPlan + OptimizePhysicalPlanWithPolicy = compiler.OptimizePhysicalPlanWithPolicy + RenderPhysicalPlan = compiler.RenderPhysicalPlan + ParseSelector = compiler.ParseSelector + ParseRecipe = recipe.Parse + BuildRecipePlan = compiler.BuildRecipePlan + CompileResolvedRecipePlanWithPolicy = compiler.CompileResolvedRecipePlanWithPolicy + ResolveRecipePlan = compiler.ResolveRecipePlan + NewRecipeControlService = func(registry control.Registry) control.Service { + return control.Service{Registry: registry} + } + NewRecipeEngine = engine.New + PublishRecipeBundle = materialization.PublishBundle + NewRecipeRegistry = exec.NewRegistry + ValidateTypedFilterForResource = compiler.ValidateTypedFilterForResource + NormalizeSelectionPlan = compiler.NormalizeSelectionPlan + ResolveSemanticField = compiler.ResolveSemanticField + InferRowGrain = compiler.InferRowGrain + RootResourceForGrain = compiler.RootResourceForGrain + ValidateRootGrain = compiler.ValidateRootGrain + DefaultRowIdentity = compiler.DefaultRowIdentity + ValidateProjection = compiler.ValidateProjection + OperatorSupportsKind = compiler.OperatorSupportsKind + ValidateGenericPhysicalPlanScope = compiler.ValidateGenericPhysicalPlanScope + DecomposePhysicalTraversalPrefix = compiler.DecomposePhysicalTraversalPrefix + ResolveStorageRoute = compiler.ResolveStorageRoute + AsUserError = dataframeerrors.AsUserError + Normalize = dataframeerrors.Normalize + PublicMessage = dataframeerrors.PublicMessage + NewError = dataframeerrors.NewError + Wrap = dataframeerrors.Wrap + WithFieldPath = dataframeerrors.WithFieldPath + WithDetails = dataframeerrors.WithDetails + WithRetryable = dataframeerrors.WithRetryable + WithCause = dataframeerrors.WithCause + IsUserCorrectable = dataframeerrors.IsUserCorrectable + IsRetryableCode = dataframeerrors.IsRetryableCode + IsOperatorFailure = dataframeerrors.IsOperatorFailure + Errorf = dataframeerrors.Errorf + AllErrorCodes = dataframeerrors.AllErrorCodes + ErrBackendUnavailable = dataframeerrors.ErrBackendUnavailable + ErrClientCanceled = dataframeerrors.ErrClientCanceled ) diff --git a/internal/dataframe/compat_test_helpers_test.go b/internal/dataframe/compat_test_helpers_test.go deleted file mode 100644 index abfade3..0000000 --- a/internal/dataframe/compat_test_helpers_test.go +++ /dev/null @@ -1,19 +0,0 @@ -package dataframe - -// These aliases keep package-local compiler tests readable while their -// implementations live in the canonical compiler/runtime packages. -type storageRoute = StorageRoute - -func resolveStorageRoute(fromType, label, toType string) (storageRoute, error) { - return ResolveStorageRoute(fromType, label, toType) -} - -const datasetGenerationBindKey = "dataset_generation" - -func isDatasetGenerationScopePredicate(predicate PhysicalPredicate, variable string) bool { - return predicate.Operator == "EQUALS" && - predicate.Left.Variable == variable && - len(predicate.Left.Path) == 1 && predicate.Left.Path[0] == "dataset_generation" && - predicate.Right != nil && predicate.Right.BindKey == datasetGenerationBindKey && - predicate.Right.Variable == "" && len(predicate.Right.Path) == 0 -} diff --git a/internal/dataframe/compiler/aggregate_compile_test.go b/internal/dataframe/compiler/aggregate_compile_test.go deleted file mode 100644 index 0eb9ad5..0000000 --- a/internal/dataframe/compiler/aggregate_compile_test.go +++ /dev/null @@ -1,53 +0,0 @@ -package compiler - -import ( - "strings" - "testing" -) - -func TestGenericExistsAggregateWithoutPredicateTestsSetNonEmptiness(t *testing.T) { - compiled, err := CompileRequest(Builder{ - Project: "P1", RootResourceType: "Specimen", - Traversals: []TraversalStep{{ - Label: "subject_Specimen", ToResourceType: "DocumentReference", Alias: "file", - Aggregates: []AggregateSelect{{Name: "has_file", Operation: "EXISTS"}}, - }}, - }, 1) - if err != nil { - t.Fatal(err) - } - if !strings.Contains(compiled.Query, "LENGTH(child_set_1) > 0") { - t.Fatalf("EXISTS aggregate did not compile as physical set non-emptiness:\n%s", compiled.Query) - } -} - -func TestSemanticPlanRejectsAggregateWithoutRequiredInput(t *testing.T) { - _, err := BuildSemanticPlan(Builder{ - Project: "P1", RootResourceType: "Specimen", - Aggregates: []AggregateSelect{{Name: "distinct", Operation: "COUNT_DISTINCT"}}, - }) - // COUNT_DISTINCT is accepted by the current public shape but requires an - // input before lowering; assert it never reaches a silent zero-value plan. - if err == nil { - t.Fatal("COUNT_DISTINCT without a selector unexpectedly reached semantic plan") - } -} - -func TestGenericMinAndMaxAggregatesLowerToTypedReductions(t *testing.T) { - compiled, err := CompileRequest(Builder{ - Project: "P1", RootResourceType: "Specimen", - Traversals: []TraversalStep{{ - Label: "subject_Specimen", ToResourceType: "DocumentReference", Alias: "file", - Aggregates: []AggregateSelect{ - {Name: "min_size", Operation: "MIN", Select: "content[].attachment.size"}, - {Name: "max_size", Operation: "MAX", Select: "content[].attachment.size"}, - }, - }}, - }, 1) - if err != nil { - t.Fatal(err) - } - if !strings.Contains(compiled.Query, "MIN(FLATTEN(") || !strings.Contains(compiled.Query, "MAX(FLATTEN(") { - t.Fatalf("MIN/MAX did not compile through physical aggregate reductions:\n%s", compiled.Query) - } -} diff --git a/internal/dataframe/compiler/clone_helpers.go b/internal/dataframe/compiler/clone_helpers.go index 76ab92a..9928bde 100644 --- a/internal/dataframe/compiler/clone_helpers.go +++ b/internal/dataframe/compiler/clone_helpers.go @@ -1,5 +1,9 @@ package compiler +import "github.com/calypr/loom/internal/dataframe/compiler/ir" + +func clonePhysicalPlan(plan PhysicalPlan) PhysicalPlan { return ir.ClonePhysicalPlan(plan) } + func cloneStrings(in []string) []string { if in == nil { return nil diff --git a/internal/dataframe/compiler/compile.go b/internal/dataframe/compiler/compile.go deleted file mode 100644 index 1314ade..0000000 --- a/internal/dataframe/compiler/compile.go +++ /dev/null @@ -1,55 +0,0 @@ -package compiler - -import "fmt" - -type CompiledQuery struct { - Project string - DatasetGeneration string - RootResourceType string - AuthResourcePaths []string - PlanMode string - PlanProfile string - TraversalCount int - FileSummaries bool - StudyLookup bool - OptimizationRules []string - // RowIdentity describes the stable resource identity behind each returned - // row. It is metadata for exporters and recipe consumers; the existing row - // object keeps its backwards-compatible _key column. - RowIdentity *RowIdentity - Query string - BindVars map[string]any - Columns []string - PivotFields []string - Limit int - PlanDiagnostics CompilerPlanDiagnostics -} - -// CompileRequest is the sole production compiler entrypoint for a dataframe -// request. It validates semantic meaning, lowers to typed physical operators, -// applies semantics-preserving physical rewrites, and renders parameterized -// AQL. Unsupported shapes fail explicitly. -func CompileRequest(builder Builder, limit int) (CompiledQuery, error) { - return CompileRequestWithPolicy(builder, limit, DefaultPhysicalOptimizationPolicy()) -} - -// CompileRequestWithPolicy is the explicit ablation entrypoint used by -// compiler tests and benchmark tooling. Normal service execution uses -// CompileRequest and the production default policy. -func CompileRequestWithPolicy(builder Builder, limit int, policy PhysicalOptimizationPolicy) (CompiledQuery, error) { - semantic, err := BuildSemanticPlan(builder) - if err != nil { - return CompiledQuery{}, err - } - // The physical route owns navigation-only requests directly from semantic - // meaning. - physical, err := BuildPhysicalPlanWithPolicy(semantic, policy) - if err != nil { - return CompiledQuery{}, fmt.Errorf("unsupported physical dataframe shape: %w", err) - } - physical, err = OptimizePhysicalPlanWithPolicy(physical, policy) - if err != nil { - return CompiledQuery{}, fmt.Errorf("optimize physical plan: %w", err) - } - return compilePhysicalExecution(physical, semantic, limit) -} diff --git a/internal/dataframe/compiler/compiled_query.go b/internal/dataframe/compiler/compiled_query.go new file mode 100644 index 0000000..de621fd --- /dev/null +++ b/internal/dataframe/compiler/compiled_query.go @@ -0,0 +1,30 @@ +package compiler + +// CompiledQuery is the executable result of the canonical recipe compiler. +// It contains parameterized AQL plus stable metadata for execution, export, +// and diagnostics; it does not expose a transport-specific request builder. +type CompiledQuery struct { + Project string + DatasetGeneration string + RootResourceType string + AuthResourcePaths []string + PlanMode string + PlanProfile string + TraversalCount int + FileSummaries bool + StudyLookup bool + OptimizationRules []string + RowIdentity *RowIdentity + Query string + BindVars map[string]any + Columns []string + // OutputSchema is the compiler-owned ordered schema for the finalized + // physical RETURN projections. PublicColumns is the transport-safe view; + // Columns remains the legacy execution metadata used by generic runtime + // callers and may include the stable physical row identity. + OutputSchema []CompiledOutputColumn + PublicColumns []string + PivotFields []string + Limit int + PlanDiagnostics CompilerPlanDiagnostics +} diff --git a/internal/dataframe/compiler/generic_physical_plan_test.go b/internal/dataframe/compiler/generic_physical_plan_test.go index 620274f..4ea7a92 100644 --- a/internal/dataframe/compiler/generic_physical_plan_test.go +++ b/internal/dataframe/compiler/generic_physical_plan_test.go @@ -239,16 +239,6 @@ func TestBuildAndRenderGenericPhysicalPlanAggregatePredicates(t *testing.T) { if rendered.BindVars["aggregate_root_female_count_predicate_equals"] != "female" || rendered.BindVars["aggregate_child_set_1_available_count_predicate_equals"] != "available" { t.Fatalf("predicate binds missing: %#v", rendered.BindVars) } - compiled, err := CompileRequest(Builder{ - Project: "p", RootResourceType: "Patient", - Aggregates: []AggregateSelect{{Name: "female_count", Operation: "COUNT", PredicatePath: "gender", PredicateEquals: "female"}}, - }, 1) - if err != nil { - t.Fatal(err) - } - if !strings.Contains(compiled.Query, "FOR __loom_physical_aggregate_item") { - t.Fatalf("aggregate predicate request did not use physical renderer:\n%s", compiled.Query) - } } func TestBuildGenericPhysicalPlanPreparesSelectorsAcrossRichConsumers(t *testing.T) { @@ -317,8 +307,8 @@ func TestBuildGenericPhysicalPlanPreparesSelectorsAcrossRichConsumers(t *testing } } } - if preparedRefs != 4 { - t.Fatalf("prepared rich consumer references = %d, want 4", preparedRefs) + if preparedRefs != 2 { + t.Fatalf("prepared rich consumer references = %d, want 2 (one shared pivot key/value pair)", preparedRefs) } renderedA, err := RenderPhysicalPlan(plan) if err != nil { @@ -393,13 +383,90 @@ func TestBuildAndRenderGenericPhysicalPlanPivots(t *testing.T) { if err != nil { t.Fatal(err) } - for _, want := range []string{"MERGE(", "COLLECT __pivot_key = __pair.key", "POSITION(@pivot_root_lab_values_columns", "lab_values"} { + for _, want := range []string{"FIRST(", "COLLECT __pivot_key = __pair.key", "__loom_pivot_root_lab_values[@pivot_root_lab_values_columns_female"} { if !strings.Contains(rendered.Query, want) && want != "lab_values" { t.Fatalf("pivot query missing %q:\n%s", want, rendered.Query) } } - if got := rendered.BindVars["pivot_root_lab_values_columns"]; got == nil { - t.Fatalf("pivot columns bind missing: %#v", rendered.BindVars) + for _, key := range []string{"pivot_root_lab_values_columns", "pivot_root_lab_values_columns_female", "pivot_root_lab_values_columns_male"} { + if got := rendered.BindVars[key]; got == nil { + t.Fatalf("pivot columns bind %q missing: %#v", key, rendered.BindVars) + } + } + if rendered.BindVars["__loom_physical_projection_1_name"] != "lab_values__female" || rendered.BindVars["__loom_physical_projection_2_name"] != "lab_values__male" { + t.Fatalf("flattened pivot projection names missing: %#v", rendered.BindVars) + } +} + +func TestChildPivotRetainsPayloadForDeferredEvaluation(t *testing.T) { + code := Selector{Steps: []SelectorStep{{Field: "code"}, {Field: "coding", Iterate: true}, {Field: "display"}}} + plan, err := BuildGenericPhysicalPlan(SemanticPlan{ + Version: 1, + Project: "p", + Root: SemanticNode{ + Alias: "root", ResourceType: "Patient", + Children: []SemanticNode{{ + Alias: "condition", ResourceType: "Condition", EdgeLabel: "subject_Patient", + Pivots: []SemanticPivot{{Name: "code_values", ColumnSelector: code, ValueSelector: code, Columns: []string{"active"}}}, + }}, + }, + }) + if err != nil { + t.Fatal(err) + } + var childSet *PhysicalSet + for _, operation := range plan.Operations { + if operation.Kind == PhysicalSetOp && operation.Set != nil && operation.Set.Variable == "child_set_1" { + childSet = operation.Set + break + } + } + if childSet == nil { + t.Fatalf("pivot child set was not lowered: %#v", plan.Operations) + } + if childSet.Projection != nil { + t.Fatalf("pivot child set must retain payload, got selector-only projection: %#v", childSet.Projection) + } + if childSet.Output == nil || !containsPhysicalSetOutputField(childSet.Output.Fields, PhysicalSetPayloadField) { + t.Fatalf("pivot child set must retain payload output: %#v", childSet.Output) + } + rendered, err := RenderPhysicalPlan(plan) + if err != nil { + t.Fatal(err) + } + if !strings.Contains(rendered.Query, "payload: child_set_1_node.payload") { + t.Fatalf("pivot child payload was omitted from rendered set:\n%s", rendered.Query) + } +} + +func containsPhysicalSetOutputField(fields []PhysicalSetOutputField, want PhysicalSetOutputField) bool { + for _, field := range fields { + if field == want { + return true + } + } + return false +} + +func TestRenderGenericPivotFlattensRepeatedItemSource(t *testing.T) { + codeText := Selector{Steps: []SelectorStep{{Field: "code"}, {Field: "text"}}} + valueString := Selector{Steps: []SelectorStep{{Field: "valueString"}}} + componentItems := Selector{Steps: []SelectorStep{{Field: "component", Iterate: true}}} + plan, err := BuildGenericPhysicalPlan(SemanticPlan{ + Version: 1, Project: "p", Root: SemanticNode{Alias: "root", ResourceType: "Observation", Pivots: []SemanticPivot{{ + Name: "component_values", ColumnSelector: codeText, ValueSelector: valueString, + ItemSource: componentItems, ItemResourceType: "ObservationComponent", Columns: []string{"Component"}, + }}}, + }) + if err != nil { + t.Fatal(err) + } + rendered, err := RenderPhysicalPlan(plan) + if err != nil { + t.Fatal(err) + } + if !strings.Contains(rendered.Query, "FOR __loom_physical_pivot_item_value IN FLATTEN(") { + t.Fatalf("repeated pivot item source was not flattened:\n%s", rendered.Query) } } diff --git a/internal/dataframe/compiler/ir/clone.go b/internal/dataframe/compiler/ir/clone.go index 7ad717f..47e2fe1 100644 --- a/internal/dataframe/compiler/ir/clone.go +++ b/internal/dataframe/compiler/ir/clone.go @@ -1,5 +1,35 @@ package ir +import "encoding/json" + +func cloneStrings(in []string) []string { + if in == nil { + return nil + } + return append([]string(nil), in...) +} + +func clonePhysicalBindValue(value any) any { + switch value := value.(type) { + case []any: + out := make([]any, len(value)) + for i, item := range value { + out[i] = clonePhysicalBindValue(item) + } + return out + case []string: + return append([]string(nil), value...) + case map[string]any: + out := make(map[string]any, len(value)) + for key, item := range value { + out[key] = clonePhysicalBindValue(item) + } + return out + default: + return value + } +} + func clonePhysicalPlan(plan PhysicalPlan) PhysicalPlan { copy := plan copy.BindVars = clonePhysicalBindVars(plan.BindVars) @@ -18,9 +48,6 @@ func ClonePhysicalOperation(operation PhysicalOperation) PhysicalOperation { func ClonePhysicalOperations(operations []PhysicalOperation) []PhysicalOperation { return clonePhysicalOperations(operations) } -func ClonePhysicalPredicate(predicate PhysicalPredicate) PhysicalPredicate { - return clonePhysicalPredicate(predicate) -} func ClonePhysicalPredicateExpression(predicate PhysicalPredicateExpression) PhysicalPredicateExpression { return clonePhysicalPredicateExpression(predicate) } @@ -81,14 +108,28 @@ func clonePhysicalOperation(operation PhysicalOperation) PhysicalOperation { } copy.Set = &setCopy } + if operation.Unnest != nil { + unnestCopy := *operation.Unnest + unnestCopy.Expression = clonePhysicalExpression(operation.Unnest.Expression) + copy.Unnest = &unnestCopy + } if operation.DerivedLet != nil { derivedCopy := *operation.DerivedLet derivedCopy.Inputs = make([]PhysicalValue, len(operation.DerivedLet.Inputs)) for index, input := range operation.DerivedLet.Inputs { derivedCopy.Inputs[index] = clonePhysicalValue(input) } + if operation.DerivedLet.Expression != nil { + expression := clonePhysicalExpression(*operation.DerivedLet.Expression) + derivedCopy.Expression = &expression + } copy.DerivedLet = &derivedCopy } + if operation.ExpressionLet != nil { + expressionCopy := *operation.ExpressionLet + expressionCopy.Expression = clonePhysicalExpression(operation.ExpressionLet.Expression) + copy.ExpressionLet = &expressionCopy + } if operation.Sort != nil { sortCopy := *operation.Sort sortCopy.Value = clonePhysicalValue(operation.Sort.Value) @@ -104,6 +145,10 @@ func clonePhysicalOperation(operation PhysicalOperation) PhysicalOperation { for index, projection := range operation.Return.Projections { projectionCopy := projection projectionCopy.Value = clonePhysicalValue(projection.Value) + if projection.Expression != nil { + expression := clonePhysicalExpression(*projection.Expression) + projectionCopy.Expression = &expression + } returnCopy.Projections[index] = projectionCopy } copy.Return = &returnCopy @@ -161,6 +206,8 @@ func clonePhysicalExpression(expression PhysicalExpression) PhysicalExpression { if expression.Pivot != nil { pivot := *expression.Pivot pivot.Source = clonePhysicalValue(expression.Pivot.Source) + pivot.ItemSource.Steps = append([]SelectorStep(nil), expression.Pivot.ItemSource.Steps...) + pivot.ValueFallbacks = append([]Selector(nil), expression.Pivot.ValueFallbacks...) pivot.ColumnsBindKey = expression.Pivot.ColumnsBindKey if pivot.PreparedKey != nil { prepared := *pivot.PreparedKey @@ -200,6 +247,38 @@ func clonePhysicalExpression(expression PhysicalExpression) PhysicalExpression { } copy.Slice = &slice } + if expression.Lookup != nil { + lookup := *expression.Lookup + lookup.Source = clonePhysicalExpression(expression.Lookup.Source) + lookup.ItemKey = clonePhysicalExpression(expression.Lookup.ItemKey) + lookup.ItemValue = clonePhysicalExpression(expression.Lookup.ItemValue) + copy.Lookup = &lookup + } + if expression.ObjectLookup != nil { + lookup := *expression.ObjectLookup + copy.ObjectLookup = &lookup + } + if expression.KeyedMap != nil { + keyed := *expression.KeyedMap + keyed.Source = clonePhysicalExpression(expression.KeyedMap.Source) + keyed.ItemKey = clonePhysicalExpression(expression.KeyedMap.ItemKey) + keyed.ItemValue = clonePhysicalExpression(expression.KeyedMap.ItemValue) + keyed.ValueFallbacks = make([]PhysicalExpression, len(expression.KeyedMap.ValueFallbacks)) + for index := range expression.KeyedMap.ValueFallbacks { + keyed.ValueFallbacks[index] = clonePhysicalExpression(expression.KeyedMap.ValueFallbacks[index]) + } + copy.KeyedMap = &keyed + } + if expression.ObjectKeys != nil { + keys := *expression.ObjectKeys + copy.ObjectKeys = &keys + } + if expression.KeySet != nil { + keySet := *expression.KeySet + keySet.Source = clonePhysicalExpression(expression.KeySet.Source) + keySet.ItemKey = clonePhysicalExpression(expression.KeySet.ItemKey) + copy.KeySet = &keySet + } if expression.Object != nil { object := *expression.Object object.Fields = make([]PhysicalExpressionProjection, len(expression.Object.Fields)) @@ -213,6 +292,20 @@ func clonePhysicalExpression(expression PhysicalExpression) PhysicalExpression { return copy } +// PhysicalExpressionFingerprint returns a deterministic structural identity +// for optimizer reuse decisions. It intentionally includes typed bind keys +// and cardinality/null contracts but never bind values. +func PhysicalExpressionFingerprint(expression PhysicalExpression) (string, error) { + if err := validatePhysicalExpressionObjectCycles(expression); err != nil { + return "", err + } + encoded, err := json.Marshal(expression) + if err != nil { + return "", err + } + return string(encoded), nil +} + func clonePhysicalSubplan(subplan PhysicalSubplan) PhysicalSubplan { copy := subplan copy.Captures = cloneStrings(subplan.Captures) diff --git a/internal/dataframe/compiler/ir/clone_values.go b/internal/dataframe/compiler/ir/clone_values.go deleted file mode 100644 index d19a7c6..0000000 --- a/internal/dataframe/compiler/ir/clone_values.go +++ /dev/null @@ -1,29 +0,0 @@ -package ir - -func cloneStrings(in []string) []string { - if in == nil { - return nil - } - return append([]string(nil), in...) -} - -func clonePhysicalBindValue(value any) any { - switch value := value.(type) { - case []any: - out := make([]any, len(value)) - for i, item := range value { - out[i] = clonePhysicalBindValue(item) - } - return out - case []string: - return append([]string(nil), value...) - case map[string]any: - out := make(map[string]any, len(value)) - for key, item := range value { - out[key] = clonePhysicalBindValue(item) - } - return out - default: - return value - } -} diff --git a/internal/dataframe/compiler/ir/diagnostics.go b/internal/dataframe/compiler/ir/diagnostics.go index 9ff2bc9..be867d7 100644 --- a/internal/dataframe/compiler/ir/diagnostics.go +++ b/internal/dataframe/compiler/ir/diagnostics.go @@ -1,14 +1,25 @@ package ir import ( + "crypto/sha256" + "encoding/hex" "encoding/json" + "fmt" "sort" ) +func valueString(value any) string { + return fmt.Sprint(value) +} + // CompilerPlanDiagnostics describes work the physical renderer will ask AQL to // perform. It deliberately reports compiler facts, not estimated database // cost: use it alongside Arango PROFILE to decide which rewrite is worthwhile. type CompilerPlanDiagnostics struct { + // Fingerprint is a deterministic structural identity for the validated + // physical plan. It is safe to expose in Explain because it is a digest, + // never the plan's bind values or authorization paths. + Fingerprint string TraversalSets int EndpointTraversalCount int NativeTraversalCount int @@ -21,6 +32,9 @@ type CompilerPlanDiagnostics struct { PotentialSharingOpportunitySets int RichSourceReuse []RichSourceReuse RichConsumerGroups []RichConsumerGroup + ExpressionBindingCount int + SharedKeyedMapCount int + ObjectLookupConsumerCount int // OptimizationPolicy is the explainable decision record for optional // physical rewrites. It reports both enabled rewrites and conservative // rejections so rendered AQL is never the only evidence of a decision. @@ -76,6 +90,7 @@ func (r RichSourceReuse) TotalConsumers() int { func physicalPlanDiagnostics(plan PhysicalPlan) CompilerPlanDiagnostics { diagnostics := CompilerPlanDiagnostics{ + Fingerprint: physicalPlanFingerprint(plan), SharedTraversalCount: plan.SharedTraversalCount, RequiredMatchReuseCount: plan.RequiredMatchReuseCount, OptimizationPolicy: clonePhysicalOptimizationReport(plan.OptimizationPolicy), @@ -90,6 +105,12 @@ func physicalPlanDiagnostics(plan PhysicalPlan) CompilerPlanDiagnostics { groups := map[string][]int{} potentialGroups := map[string][]int{} for i, operation := range plan.Operations { + if operation.Kind == PhysicalExpressionLetOp && operation.ExpressionLet != nil { + diagnostics.ExpressionBindingCount++ + if operation.ExpressionLet.Expression.Kind == PhysicalKeyedMapExpression { + diagnostics.SharedKeyedMapCount++ + } + } if operation.Kind == PhysicalTraversalOp && operation.Traversal != nil { traversal := operation.Traversal strategy := traversal.Strategy @@ -126,9 +147,9 @@ func physicalPlanDiagnostics(plan PhysicalPlan) CompilerPlanDiagnostics { set := operation.Set if set.SourceSetVariable == "" && len(set.Subplan.Operations) > 0 && set.Subplan.Operations[0].Traversal != nil { diagnostics.TraversalSets++ - key := physicalTraversalOpportunityKey(plan, *set) + key := physicalTraversalOpportunityKey(plan, *set, i) potentialGroups[key] = append(potentialGroups[key], i) - if decomposition, err := DecomposePhysicalTraversalPrefix(plan, *set); err == nil { + if decomposition, err := DecomposePhysicalTraversalPrefixAt(plan, *set, i); err == nil { groups[decomposition.PrefixKey] = append(groups[decomposition.PrefixKey], i) } } @@ -162,6 +183,14 @@ func physicalPlanDiagnostics(plan PhysicalPlan) CompilerPlanDiagnostics { diagnostics.RichSourceReuse = append(diagnostics.RichSourceReuse, *use) } } + for _, operation := range plan.Operations { + if operation.Kind != PhysicalReturnOp || operation.Return == nil { + continue + } + for _, projection := range operation.Return.Projections { + diagnostics.ObjectLookupConsumerCount += countObjectLookupConsumers(projection.Expression) + } + } sort.Slice(diagnostics.RichSourceReuse, func(i, j int) bool { return diagnostics.RichSourceReuse[i].SourceSet < diagnostics.RichSourceReuse[j].SourceSet }) @@ -192,6 +221,31 @@ func physicalPlanDiagnostics(plan PhysicalPlan) CompilerPlanDiagnostics { return diagnostics } +func countObjectLookupConsumers(expression *PhysicalExpression) int { + if expression == nil { + return 0 + } + count := 0 + if expression.Kind == PhysicalObjectLookupExpression { + count++ + } + if expression.Object != nil { + for index := range expression.Object.Fields { + count += countObjectLookupConsumers(&expression.Object.Fields[index].Expression) + } + } + return count +} + +func physicalPlanFingerprint(plan PhysicalPlan) string { + encoded, err := json.Marshal(plan) + if err != nil { + return "" + } + digest := sha256.Sum256(encoded) + return hex.EncodeToString(digest[:]) +} + func PhysicalPlanDiagnostics(plan PhysicalPlan) CompilerPlanDiagnostics { return physicalPlanDiagnostics(plan) } @@ -241,12 +295,19 @@ func collectRichConsumerGroups(expression *PhysicalExpression, groups map[string // physicalTraversalOpportunityKey intentionally ignores scoped filters and // semantic provenance. It identifies broader neighbor traversals that could // serve multiple typed children once the scope-safe rewrite exists. -func physicalTraversalOpportunityKey(plan PhysicalPlan, set PhysicalSet) string { +func physicalTraversalOpportunityKey(plan PhysicalPlan, set PhysicalSet, setIndex int) string { traversal := set.Subplan.Operations[0].Traversal if traversal == nil { return "" } - return traversal.SourceVariable + "|" + string(traversal.Direction) + "|" + valueString(plan.BindVars[traversal.EdgeCollectionBindKey]) + "|" + valueString(plan.BindVars[traversal.EdgeLabelBindKey]) + "|" + traversal.EdgeTargetTypeField + unnestScope, err := physicalUnnestScopeIdentityAt(plan, setIndex) + if err != nil { + // Diagnostics must remain total even for a malformed candidate. The + // malformed scope is deliberately isolated from valid candidates rather + // than silently grouping it with the root scope. + unnestScope = "invalid-unnest-scope" + } + return traversal.SourceVariable + "|" + string(traversal.Direction) + "|" + valueString(plan.BindVars[traversal.EdgeCollectionBindKey]) + "|" + valueString(plan.BindVars[traversal.EdgeLabelBindKey]) + "|" + traversal.EdgeTargetTypeField + "|unnest=" + unnestScope } func multipleTargetTypes(plan PhysicalPlan, indices []int) bool { diff --git a/internal/dataframe/compiler/ir/lookup_test.go b/internal/dataframe/compiler/ir/lookup_test.go new file mode 100644 index 0000000..e754fc4 --- /dev/null +++ b/internal/dataframe/compiler/ir/lookup_test.go @@ -0,0 +1,56 @@ +package ir + +import "testing" + +func lookupExpression() PhysicalExpression { + value := func(path ...string) PhysicalExpression { + return PhysicalExpression{Kind: PhysicalValueExpression, Cardinality: PhysicalScalarCardinality, NullBehavior: PhysicalPreserveNull, Value: &PhysicalValue{Variable: "dynamic_item", Path: path}} + } + return PhysicalExpression{ + Kind: PhysicalLookupExpression, + Cardinality: PhysicalScalarCardinality, + NullBehavior: PhysicalPreserveNull, + Lookup: &PhysicalLookup{ + Source: PhysicalExpression{Kind: PhysicalValueExpression, Cardinality: PhysicalArrayCardinality, NullBehavior: PhysicalPreserveNull, Value: &PhysicalValue{Variable: "root", Path: []string{"items"}}}, + ItemVariable: "dynamic_item", + ItemKey: value("url"), + ItemValue: value("value"), + MatchBindKey: "dynamic_key", + }, + } +} + +func TestPhysicalLookupValidatesScopeAndBind(t *testing.T) { + plan := PhysicalPlan{Version: 1, BindVars: map[string]any{"collection": "Patient", "dynamic_key": "x"}, Operations: []PhysicalOperation{ + {Kind: PhysicalRootScanOp, RootScan: &PhysicalRootScan{Variable: "root", CollectionBindKey: "collection"}}, + {Kind: PhysicalReturnOp, Return: &PhysicalReturn{Projections: []PhysicalProjection{{Name: "value", Expression: func() *PhysicalExpression { value := lookupExpression(); return &value }()}}}}, + }} + if err := plan.Validate(); err != nil { + t.Fatal(err) + } + bad := lookupExpression() + bad.Lookup.ItemKey.Cardinality = PhysicalArrayCardinality + plan.Operations[1].Return.Projections[0].Expression = &bad + if err := plan.Validate(); err == nil { + t.Fatal("array-valued lookup key was accepted") + } +} + +func TestPhysicalLookupCloneAndFingerprint(t *testing.T) { + original := lookupExpression() + clone := ClonePhysicalExpression(original) + if clone.Lookup == original.Lookup || clone.Lookup.Source.Value == original.Lookup.Source.Value { + t.Fatal("lookup clone shares mutable payload") + } + left, err := PhysicalExpressionFingerprint(original) + if err != nil { + t.Fatal(err) + } + right, err := PhysicalExpressionFingerprint(clone) + if err != nil { + t.Fatal(err) + } + if left != right { + t.Fatalf("clone fingerprint differs: %s != %s", left, right) + } +} diff --git a/internal/dataframe/compiler/ir/physical_operations.go b/internal/dataframe/compiler/ir/physical_operations.go new file mode 100644 index 0000000..1b4e37d --- /dev/null +++ b/internal/dataframe/compiler/ir/physical_operations.go @@ -0,0 +1,183 @@ +package ir + +import ( + "regexp" +) + +// PhysicalPlan is the renderer-independent AQL operation graph produced after +// semantic planning. Operations are ordered because AQL variables have lexical +// scope: an operation may reference only variables introduced before it. +type PhysicalSet struct { + Variable string + Subplan PhysicalSubplan + Unique bool + // Output describes a compact, identity-safe projection of each set item. + // A nil output preserves the full stored document for shared traversal or + // other consumers that have not proved a smaller contract. + Output *PhysicalSetOutput + // Projection describes selector values computed in the original child-set + // subquery. It replaces the payload-bearing second prepared array when all + // downstream consumers have a projection-safe selector contract. + Projection *PhysicalSetProjection + // SourceSetVariable is set for a typed subset over an already materialized + // shared traversal. Such a set does not begin with TRAVERSAL; ItemVariable + // is bound by the renderer while iterating SourceSetVariable. + SourceSetVariable string + ItemVariable string + // SortByKey makes the set's node order part of physical semantics. Optional + // relationship materialization must not rely on Arango traversal order. + SortByKey bool + Prepared *PhysicalPreparedSet +} + +// PhysicalUnnestJoinMode controls the row-preservation contract for a +// cardinality-changing operation. The renderer chooses the equivalent AQL +// shape; this IR never stores a query fragment. +type PhysicalUnnestJoinMode string + +const ( + PhysicalUnnestInner PhysicalUnnestJoinMode = "INNER" + PhysicalUnnestOuter PhysicalUnnestJoinMode = "OUTER" +) + +// PhysicalUnnest introduces OutputVariable for each item produced by the +// array-valued Expression. Ordinality, when present, is a stable zero-based +// item position. InputVariable identifies the parent lexical scope used by +// the source expression and is a cardinality/scope barrier for optimizers. +type PhysicalUnnest struct { + InputVariable string + OutputVariable string + Ordinality string + Expression PhysicalExpression + JoinMode PhysicalUnnestJoinMode +} + +// PhysicalSetProjection is a single-materialization selector projection. The +// fields are arrays because selector evaluation preserves repeated FHIR +// values; scalar consumers apply their normal FIRST/FLATTEN semantics when +// reading the projected field. +type PhysicalSetProjection struct { + Fields []PhysicalSetProjectionField +} + +type PhysicalSetProjectionField struct { + Name string + ResourceType string + Selector Selector + ExecutionMode PhysicalSelectorExecutionMode +} + +// PhysicalSetOutputField names the only stored properties that may survive a +// compact set projection. The graph identity fields preserve nested traversal +// and duplicate-edge semantics; payload is retained only when a downstream +// selector or rich consumer needs it. +type PhysicalSetOutputField string + +const ( + PhysicalSetGraphIDField PhysicalSetOutputField = "_id" + PhysicalSetKeyField PhysicalSetOutputField = "_key" + PhysicalSetIDField PhysicalSetOutputField = "id" + PhysicalSetResourceTypeField PhysicalSetOutputField = "resourceType" + PhysicalSetPayloadField PhysicalSetOutputField = "payload" +) + +type PhysicalSetOutput struct { + Fields []PhysicalSetOutputField +} + +type PhysicalSubplan struct { + Captures []string + Operations []PhysicalOperation + Return PhysicalExpression +} + +type PhysicalPredicate struct { + Operator string + Left PhysicalValue + // LeftExpression lets a comparison consume a typed selector extraction. + // Exactly one of Left and LeftExpression is present. This keeps user + // filters out of AQL-shaped strings while the scope predicates can retain + // their compact value-only form. + LeftExpression *PhysicalExpression + Right *PhysicalValue + Quantifier ArrayQuantifier + ValueKind FilterValueKind +} + +type PhysicalPredicateKind string + +const ( + PhysicalComparisonPredicate PhysicalPredicateKind = "COMPARISON" + PhysicalAllPredicate PhysicalPredicateKind = "ALL" + PhysicalAnyPredicate PhysicalPredicateKind = "ANY" + PhysicalNotPredicate PhysicalPredicateKind = "NOT" + PhysicalExistsPredicate PhysicalPredicateKind = "EXISTS" +) + +// PhysicalPredicateExpression is the typed predicate tree used by rich +// physical operations. Exists contains a bounded correlated subplan; it is +// not a string-shaped LENGTH(FOR ...) compatibility escape hatch. +type PhysicalPredicateExpression struct { + Kind PhysicalPredicateKind + Comparison *PhysicalPredicate + Children []PhysicalPredicateExpression + Exists *PhysicalSubplan +} + +type PhysicalFilter struct { + // Predicate is retained for the frozen navigation plan. New lowering must + // use Expression so compound and existence predicates stay typed. + Predicate PhysicalPredicate + Expression *PhysicalPredicateExpression +} + +// PhysicalDerivedLet names a derived value. Operator is a compiler-owned +// symbolic operation (for example UNIQUE or LENGTH), never raw AQL. +type PhysicalDerivedLet struct { + Variable string + Operator string + Inputs []PhysicalValue + Expression *PhysicalExpression +} + +// PhysicalExpressionLet binds a deterministic expression once in the +// current root-row scope. It is distinct from symbolic derived operators. +type PhysicalExpressionLet struct { + Variable string + Expression PhysicalExpression +} + +// PhysicalSort is the deliberately small ordering primitive currently needed +// by generic root-grain previews. Additional sort keys and directions should +// be added only with a corresponding semantic ordering contract. +type PhysicalSort struct { + Value PhysicalValue +} + +// PhysicalLimit references a positive integer bind value. Keeping the value +// in BindVars retains the same parameterized execution boundary as filters. +type PhysicalLimit struct { + BindKey string +} + +type PhysicalProjection struct { + Name string + // Hidden projections are returned by the backend for executor-side + // validation (for example dynamic runtime key metadata) but are omitted + // from public dataframe columns after post-query materialization. + Hidden bool + Value PhysicalValue + Expression *PhysicalExpression +} + +type PhysicalReturn struct { + Projections []PhysicalProjection +} + +var ( + physicalVariablePattern = regexp.MustCompile(`^[A-Za-z_][A-Za-z0-9_]*$`) + physicalBindKeyPattern = regexp.MustCompile(`^[A-Za-z][A-Za-z0-9_]*$`) + physicalPathPartPattern = regexp.MustCompile(`^[A-Za-z_][A-Za-z0-9_]*$`) +) + +// Validate enforces the frozen physical-plan invariants without rendering AQL. diff --git a/internal/dataframe/compiler/ir/physical_types.go b/internal/dataframe/compiler/ir/physical_types.go new file mode 100644 index 0000000..b9c5766 --- /dev/null +++ b/internal/dataframe/compiler/ir/physical_types.go @@ -0,0 +1,128 @@ +package ir + +// PhysicalPlan is the renderer-independent AQL operation graph produced after +// semantic planning. Operations are ordered because AQL variables have lexical +// scope: an operation may reference only variables introduced before it. +type PhysicalPlan struct { + Version int + Source PhysicalSource + BindVars map[string]any + Operations []PhysicalOperation + // DeferredExpressionLets are construction-time shared family bindings. + // Lowering appends them after all source sets exist and before RETURN; + // completed plans must have this list empty. + DeferredExpressionLets []PhysicalOperation + // AppliedRules records physical rewrites without exposing renderer + // implementation details to callers. + AppliedRules []string + SharedTraversalCount int + // OptimizationPolicy records every optional rewrite decision made by the + // physical optimizer, including conservative rejections. + OptimizationPolicy PhysicalOptimizationReport + // RequiredMatchReuseCount records duplicate required EXISTS predicates + // removed during physical lowering. It is deliberately separate from + // shared traversal count: required predicates remain pre-window + // semi-joins, while optional sets are post-window materializations. + RequiredMatchReuseCount int +} + +// PhysicalSource retains semantic provenance through physical optimization so +// explain output and compiler errors can point back to user intent. +type PhysicalSource struct { + RecipeID string + TemplateID string + SemanticNode string + SemanticField string + ResourceType string + Relationship string +} + +type PhysicalOperationKind string + +const ( + PhysicalRootScanOp PhysicalOperationKind = "ROOT_SCAN" + PhysicalTraversalOp PhysicalOperationKind = "TRAVERSAL" + PhysicalFilterOp PhysicalOperationKind = "FILTER" + PhysicalDerivedLetOp PhysicalOperationKind = "DERIVED_LET" + PhysicalExpressionLetOp PhysicalOperationKind = "EXPRESSION_LET" + // PhysicalSetOp materializes a correlated, array-valued subplan. It is the + // only operation that can introduce a set variable; selectors, aggregates, + // pivots, and slices consume that variable through typed expressions. + PhysicalSetOp PhysicalOperationKind = "SET" + // PhysicalUnnestOp is the cardinality-changing operation used for a + // correlated UNNEST. It introduces an item binding (and optionally an + // ordinality binding) for downstream operations. + PhysicalUnnestOp PhysicalOperationKind = "UNNEST" + // PhysicalSortOp and PhysicalLimitOp describe the root execution window. + // They are intentionally typed so preview ordering and bounds cannot be + // smuggled into an AQL string by a caller. + PhysicalSortOp PhysicalOperationKind = "SORT" + PhysicalLimitOp PhysicalOperationKind = "LIMIT" + PhysicalReturnOp PhysicalOperationKind = "RETURN" +) + +// PhysicalOperation is a tagged union. Exactly one payload matching Kind must +// be set. Source can be more specific than the plan-level provenance. +type PhysicalOperation struct { + Kind PhysicalOperationKind + Source PhysicalSource + RootScan *PhysicalRootScan + Traversal *PhysicalTraversal + Filter *PhysicalFilter + DerivedLet *PhysicalDerivedLet + ExpressionLet *PhysicalExpressionLet + Set *PhysicalSet + Unnest *PhysicalUnnest + Sort *PhysicalSort + Limit *PhysicalLimit + Return *PhysicalReturn +} + +type PhysicalRootScan struct { + Variable string + CollectionBindKey string +} + +type PhysicalTraversalDirection string + +const ( + PhysicalOutbound PhysicalTraversalDirection = "OUTBOUND" + PhysicalInbound PhysicalTraversalDirection = "INBOUND" + PhysicalAny PhysicalTraversalDirection = "ANY" +) + +// PhysicalTraversalStrategy selects the execution shape for a validated +// depth-one relationship. Native graph traversal is the conservative +// fallback. EndpointLookup is only legal when storage-route metadata proves +// the endpoint/discriminator fields and their compound index contract. +type PhysicalTraversalStrategy string + +const ( + PhysicalTraversalNative PhysicalTraversalStrategy = "NATIVE" + PhysicalTraversalEndpointLookup PhysicalTraversalStrategy = "ENDPOINT_LOOKUP" +) + +type PhysicalTraversal struct { + SourceVariable string + TargetVariable string + EdgeVariable string + Direction PhysicalTraversalDirection + EdgeCollectionBindKey string + EdgeLabelBindKey string + TargetTypeBindKey string + // EdgeTargetTypeField is a compiler-owned fhir_edge discriminator used + // alongside TargetTypeBindKey. For a parent-to-child INBOUND route it is + // from_type; for a proven forward OUTBOUND route it is to_type. The node + // resourceType check remains independently mandatory. + EdgeTargetTypeField string + // Strategy is deliberately typed rather than an AQL fragment. Endpoint + // fields are supplied by resolveStorageRoute and validated against the + // direction before the renderer can use them. + Strategy PhysicalTraversalStrategy + EndpointField string + EndpointJoinField string + EndpointIndexFields []string +} + +// PhysicalValue is either a variable/path reference or a bind variable. A +// renderer must never interpret Path segments as AQL source text. diff --git a/internal/dataframe/compiler/ir/physical_validation.go b/internal/dataframe/compiler/ir/physical_validation.go new file mode 100644 index 0000000..8a41951 --- /dev/null +++ b/internal/dataframe/compiler/ir/physical_validation.go @@ -0,0 +1,483 @@ +package ir + +import ( + "fmt" + + "strings" +) + +func (p PhysicalPlan) Validate() error { + if p.Version <= 0 { + return fmt.Errorf("physical plan version must be positive") + } + for key := range p.BindVars { + if !physicalBindKeyPattern.MatchString(key) { + return fmt.Errorf("unsafe bind key %q", key) + } + } + defined := map[string]bool{} + rootScans := 0 + returns := 0 + for i, operation := range p.Operations { + if returns > 0 { + return fmt.Errorf("operation %d appears after RETURN", i) + } + if err := operation.validatePayload(); err != nil { + return fmt.Errorf("operation %d (%s): %w", i, operation.Kind, err) + } + switch operation.Kind { + case PhysicalRootScanOp: + rootScans++ + if rootScans > 1 { + return fmt.Errorf("operation %d: physical plan has multiple root scans", i) + } + if err := requireBind(p.BindVars, operation.RootScan.CollectionBindKey); err != nil { + return fmt.Errorf("operation %d: %w", i, err) + } + if err := requireCollectionBind(p.BindVars, operation.RootScan.CollectionBindKey); err != nil { + return fmt.Errorf("operation %d: %w", i, err) + } + if err := definePhysicalVariable(defined, operation.RootScan.Variable); err != nil { + return fmt.Errorf("operation %d: %w", i, err) + } + case PhysicalTraversalOp: + traversal := operation.Traversal + if !defined[traversal.SourceVariable] { + return fmt.Errorf("operation %d: traversal source variable %q is out of scope", i, traversal.SourceVariable) + } + if traversal.Direction != PhysicalOutbound && traversal.Direction != PhysicalInbound && traversal.Direction != PhysicalAny { + return fmt.Errorf("operation %d: invalid traversal direction %q", i, traversal.Direction) + } + if traversal.EdgeTargetTypeField != "" && !physicalPathPartPattern.MatchString(traversal.EdgeTargetTypeField) { + return fmt.Errorf("operation %d: unsafe traversal edge type field %q", i, traversal.EdgeTargetTypeField) + } + if err := validatePhysicalTraversalStrategy(*traversal); err != nil { + return fmt.Errorf("operation %d: %w", i, err) + } + for _, key := range []string{traversal.EdgeCollectionBindKey, traversal.EdgeLabelBindKey, traversal.TargetTypeBindKey} { + if key != "" { + if err := requireBind(p.BindVars, key); err != nil { + return fmt.Errorf("operation %d: %w", i, err) + } + } + } + if traversal.EdgeCollectionBindKey == "" { + return fmt.Errorf("operation %d: traversal edge collection bind key is required", i) + } + if err := requireCollectionBind(p.BindVars, traversal.EdgeCollectionBindKey); err != nil { + return fmt.Errorf("operation %d: %w", i, err) + } + if err := definePhysicalVariable(defined, traversal.TargetVariable); err != nil { + return fmt.Errorf("operation %d: %w", i, err) + } + if traversal.EdgeVariable != "" { + if err := definePhysicalVariable(defined, traversal.EdgeVariable); err != nil { + return fmt.Errorf("operation %d: %w", i, err) + } + } + case PhysicalFilterOp: + if err := validatePhysicalFilter(*operation.Filter, defined, p.BindVars); err != nil { + return fmt.Errorf("operation %d: %w", i, err) + } + case PhysicalDerivedLetOp: + derived := operation.DerivedLet + if err := validatePhysicalDerivedLet(*derived, defined, p.BindVars); err != nil { + return fmt.Errorf("operation %d: %w", i, err) + } + if err := definePhysicalVariable(defined, derived.Variable); err != nil { + return fmt.Errorf("operation %d: %w", i, err) + } + case PhysicalExpressionLetOp: + if operation.ExpressionLet == nil || strings.TrimSpace(operation.ExpressionLet.Variable) == "" { + return fmt.Errorf("operation %d: expression LET payload and variable are required", i) + } + if err := validatePhysicalExpression(operation.ExpressionLet.Expression, defined, p.BindVars); err != nil { + return fmt.Errorf("operation %d expression LET: %w", i, err) + } + if err := definePhysicalVariable(defined, operation.ExpressionLet.Variable); err != nil { + return fmt.Errorf("operation %d: %w", i, err) + } + case PhysicalSetOp: + set := operation.Set + if err := validatePhysicalSet(*set, defined, p.BindVars); err != nil { + return fmt.Errorf("operation %d set %q: %w", i, set.Variable, err) + } + if err := definePhysicalVariable(defined, set.Variable); err != nil { + return fmt.Errorf("operation %d: %w", i, err) + } + if set.Prepared != nil { + if err := definePhysicalVariable(defined, set.Prepared.Variable); err != nil { + return fmt.Errorf("operation %d prepared set: %w", i, err) + } + } + case PhysicalUnnestOp: + if err := validatePhysicalUnnest(*operation.Unnest, defined, p.BindVars); err != nil { + return fmt.Errorf("operation %d unnest: %w", i, err) + } + if err := definePhysicalVariable(defined, operation.Unnest.OutputVariable); err != nil { + return fmt.Errorf("operation %d: %w", i, err) + } + if operation.Unnest.Ordinality != "" { + if err := definePhysicalVariable(defined, operation.Unnest.Ordinality); err != nil { + return fmt.Errorf("operation %d: %w", i, err) + } + } + case PhysicalSortOp: + if err := validatePhysicalValue(operation.Sort.Value, defined, p.BindVars); err != nil { + return fmt.Errorf("operation %d: %w", i, err) + } + case PhysicalLimitOp: + if err := requireBind(p.BindVars, operation.Limit.BindKey); err != nil { + return fmt.Errorf("operation %d: %w", i, err) + } + limit, ok := p.BindVars[operation.Limit.BindKey].(int) + if !ok || limit <= 0 { + return fmt.Errorf("operation %d: limit bind %q must be a positive int", i, operation.Limit.BindKey) + } + case PhysicalReturnOp: + returns++ + seenNames := map[string]bool{} + for _, projection := range operation.Return.Projections { + if strings.TrimSpace(projection.Name) == "" || seenNames[projection.Name] { + return fmt.Errorf("operation %d: return projection name %q is empty or duplicated", i, projection.Name) + } + seenNames[projection.Name] = true + if err := validatePhysicalProjection(projection, defined, p.BindVars); err != nil { + return fmt.Errorf("operation %d projection %q: %w", i, projection.Name, err) + } + } + } + } + if rootScans != 1 { + return fmt.Errorf("physical plan requires exactly one root scan") + } + if returns != 1 { + return fmt.Errorf("physical plan requires exactly one RETURN") + } + return nil +} + +func validatePhysicalTraversalStrategy(traversal PhysicalTraversal) error { + strategy := traversal.Strategy + if strategy == "" || strategy == PhysicalTraversalNative { + return nil + } + if strategy != PhysicalTraversalEndpointLookup { + return fmt.Errorf("unsupported traversal strategy %q", strategy) + } + if traversal.Direction != PhysicalInbound && traversal.Direction != PhysicalOutbound { + return fmt.Errorf("endpoint lookup requires INBOUND or OUTBOUND direction") + } + if !physicalPathPartPattern.MatchString(traversal.EndpointField) || !physicalPathPartPattern.MatchString(traversal.EndpointJoinField) { + return fmt.Errorf("endpoint lookup requires safe endpoint and join fields") + } + if len(traversal.EndpointIndexFields) == 0 { + return fmt.Errorf("endpoint lookup requires declared compound index fields") + } + for _, field := range traversal.EndpointIndexFields { + if !physicalPathPartPattern.MatchString(field) { + return fmt.Errorf("endpoint lookup has unsafe index field %q", field) + } + } + return nil +} + +func validatePhysicalSet(set PhysicalSet, parent map[string]bool, bindVars map[string]any) error { + if set.Projection != nil { + if len(set.Projection.Fields) == 0 { + return fmt.Errorf("set %q projection requires at least one field", set.Variable) + } + seenProjectionFields := map[string]bool{} + for _, field := range set.Projection.Fields { + if !physicalVariablePattern.MatchString(field.Name) || seenProjectionFields[field.Name] { + return fmt.Errorf("set %q projection field %q is unsafe or duplicated", set.Variable, field.Name) + } + seenProjectionFields[field.Name] = true + if !schemaDefinitionExists(field.ResourceType) { + return fmt.Errorf("set %q projection field %q has invalid resource type %q", set.Variable, field.Name, field.ResourceType) + } + if err := validatePhysicalSelector(field.ResourceType, field.Selector); err != nil { + return fmt.Errorf("set %q projection field %q selector: %w", set.Variable, field.Name, err) + } + } + } + if set.Output != nil { + if len(set.Output.Fields) == 0 { + return fmt.Errorf("set %q compact output requires at least one retained field", set.Variable) + } + seenOutputFields := map[PhysicalSetOutputField]bool{} + for _, field := range set.Output.Fields { + switch field { + case PhysicalSetGraphIDField, PhysicalSetKeyField, PhysicalSetIDField, PhysicalSetResourceTypeField, PhysicalSetPayloadField: + default: + return fmt.Errorf("set %q compact output field %q is unsupported", set.Variable, field) + } + if seenOutputFields[field] { + return fmt.Errorf("set %q compact output field %q is duplicated", set.Variable, field) + } + seenOutputFields[field] = true + } + if !seenOutputFields[PhysicalSetGraphIDField] || !seenOutputFields[PhysicalSetKeyField] { + return fmt.Errorf("set %q compact output must retain _id and _key", set.Variable) + } + } + if set.Prepared != nil { + prepared := set.Prepared + if !physicalVariablePattern.MatchString(prepared.Variable) || !physicalVariablePattern.MatchString(prepared.SourceSetVariable) { + return fmt.Errorf("prepared set variables must be safe") + } + if prepared.SourceSetVariable != set.Variable { + return fmt.Errorf("prepared set source %q must equal owning set %q", prepared.SourceSetVariable, set.Variable) + } + if len(prepared.Fields) == 0 { + return fmt.Errorf("prepared set %q requires at least one field", prepared.Variable) + } + seen := map[string]bool{} + for _, field := range prepared.Fields { + if !physicalVariablePattern.MatchString(field.Name) || seen[field.Name] { + return fmt.Errorf("prepared set field %q is unsafe or duplicated", field.Name) + } + seen[field.Name] = true + if !schemaDefinitionExists(field.ResourceType) { + return fmt.Errorf("prepared set field %q has invalid resource type %q", field.Name, field.ResourceType) + } + if err := validatePhysicalSelector(field.ResourceType, field.Selector); err != nil { + return fmt.Errorf("prepared set field %q selector: %w", field.Name, err) + } + } + } + if set.SourceSetVariable == "" { + return validatePhysicalSubplan(set.Subplan, parent, bindVars) + } + if !physicalVariablePattern.MatchString(set.ItemVariable) { + return fmt.Errorf("shared subset %q has unsafe item variable", set.ItemVariable) + } + if !parent[set.SourceSetVariable] { + return fmt.Errorf("shared subset source %q is out of scope", set.SourceSetVariable) + } + if len(set.Subplan.Captures) != 1 || set.Subplan.Captures[0] != set.SourceSetVariable { + return fmt.Errorf("shared subset %q must capture exactly its source set", set.Variable) + } + defined := map[string]bool{set.SourceSetVariable: true, set.ItemVariable: true} + for index, operation := range set.Subplan.Operations { + if operation.Kind != PhysicalFilterOp && operation.Kind != PhysicalDerivedLetOp { + return fmt.Errorf("shared subset operation %d has unsupported kind %q", index, operation.Kind) + } + if operation.Kind == PhysicalFilterOp { + if err := validatePhysicalFilter(*operation.Filter, defined, bindVars); err != nil { + return err + } + } else { + if err := validatePhysicalDerivedLet(*operation.DerivedLet, defined, bindVars); err != nil { + return err + } + if err := definePhysicalVariable(defined, operation.DerivedLet.Variable); err != nil { + return err + } + } + } + return validatePhysicalExpression(set.Subplan.Return, defined, bindVars) +} + +func validatePhysicalUnnest(unnest PhysicalUnnest, defined map[string]bool, bindVars map[string]any) error { + if !physicalVariablePattern.MatchString(unnest.InputVariable) { + return fmt.Errorf("unnest input variable %q is unsafe", unnest.InputVariable) + } + if !defined[unnest.InputVariable] { + return fmt.Errorf("unnest input variable %q is out of scope", unnest.InputVariable) + } + if !physicalVariablePattern.MatchString(unnest.OutputVariable) { + return fmt.Errorf("unnest output variable %q is unsafe", unnest.OutputVariable) + } + if unnest.OutputVariable == unnest.InputVariable { + return fmt.Errorf("unnest output variable %q shadows its input variable", unnest.OutputVariable) + } + if unnest.Ordinality != "" { + if !physicalVariablePattern.MatchString(unnest.Ordinality) { + return fmt.Errorf("unnest ordinality variable %q is unsafe", unnest.Ordinality) + } + if unnest.Ordinality == unnest.InputVariable || unnest.Ordinality == unnest.OutputVariable { + return fmt.Errorf("unnest ordinality variable %q shadows an existing unnest binding", unnest.Ordinality) + } + } + switch unnest.JoinMode { + case PhysicalUnnestInner, PhysicalUnnestOuter: + default: + return fmt.Errorf("unsupported unnest join mode %q", unnest.JoinMode) + } + if unnest.Expression.Cardinality != PhysicalArrayCardinality { + return fmt.Errorf("unnest source expression must be array-valued, got %q", unnest.Expression.Cardinality) + } + if err := validatePhysicalExpression(unnest.Expression, defined, bindVars); err != nil { + return fmt.Errorf("unnest source expression: %w", err) + } + return nil +} + +func (operation PhysicalOperation) validatePayload() error { + payloads := 0 + if operation.RootScan != nil { + payloads++ + } + if operation.Traversal != nil { + payloads++ + } + if operation.Filter != nil { + payloads++ + } + if operation.DerivedLet != nil { + payloads++ + } + if operation.ExpressionLet != nil { + payloads++ + } + if operation.Set != nil { + payloads++ + } + if operation.Unnest != nil { + payloads++ + } + if operation.Sort != nil { + payloads++ + } + if operation.Limit != nil { + payloads++ + } + if operation.Return != nil { + payloads++ + } + if payloads != 1 { + return fmt.Errorf("operation must contain exactly one payload") + } + valid := (operation.Kind == PhysicalRootScanOp && operation.RootScan != nil) || + (operation.Kind == PhysicalTraversalOp && operation.Traversal != nil) || + (operation.Kind == PhysicalFilterOp && operation.Filter != nil) || + (operation.Kind == PhysicalDerivedLetOp && operation.DerivedLet != nil) || + (operation.Kind == PhysicalExpressionLetOp && operation.ExpressionLet != nil) || + (operation.Kind == PhysicalSetOp && operation.Set != nil) || + (operation.Kind == PhysicalUnnestOp && operation.Unnest != nil) || + (operation.Kind == PhysicalSortOp && operation.Sort != nil) || + (operation.Kind == PhysicalLimitOp && operation.Limit != nil) || + (operation.Kind == PhysicalReturnOp && operation.Return != nil) + if !valid { + return fmt.Errorf("payload does not match operation kind") + } + return nil +} + +func definePhysicalVariable(defined map[string]bool, variable string) error { + if !physicalVariablePattern.MatchString(variable) { + return fmt.Errorf("unsafe variable name %q", variable) + } + if defined[variable] { + return fmt.Errorf("variable %q is already defined", variable) + } + defined[variable] = true + return nil +} + +func requireBind(bindVars map[string]any, key string) error { + if !physicalBindKeyPattern.MatchString(key) { + return fmt.Errorf("unsafe bind key %q", key) + } + if _, ok := bindVars[key]; !ok { + return fmt.Errorf("bind key %q is not defined", key) + } + return nil +} + +func requireCollectionBind(bindVars map[string]any, key string) error { + value, ok := bindVars[key] + if !ok { + return fmt.Errorf("bind key %q is not defined", key) + } + collection, ok := value.(string) + if !ok || strings.TrimSpace(collection) == "" { + return fmt.Errorf("collection bind key %q must have a non-empty string value", key) + } + return nil +} + +func validatePhysicalPredicate(predicate PhysicalPredicate, defined map[string]bool, bindVars map[string]any) error { + operator := strings.ToUpper(strings.TrimSpace(predicate.Operator)) + switch operator { + case "EQUALS", "NOT_EQUALS", "IN", "EXISTS", "MISSING", "CONTAINS_TEXT", "GT", "GTE", "LT", "LTE": + default: + return fmt.Errorf("unknown physical filter operator %q", predicate.Operator) + } + hasLeftValue := predicate.Left.Variable != "" || predicate.Left.BindKey != "" || len(predicate.Left.Path) != 0 + hasLeftExpression := predicate.LeftExpression != nil + if hasLeftValue == hasLeftExpression { + return fmt.Errorf("physical filter predicate requires exactly one left value or expression") + } + if hasLeftExpression { + if err := validatePhysicalExpression(*predicate.LeftExpression, defined, bindVars); err != nil { + return fmt.Errorf("physical filter predicate left expression: %w", err) + } + if predicate.LeftExpression.Cardinality != PhysicalArrayCardinality { + return fmt.Errorf("physical filter predicate left expression must be array-valued") + } + if !predicate.ValueKind.Valid() { + return fmt.Errorf("physical filter predicate value kind %q is invalid", predicate.ValueKind) + } + if predicate.Quantifier != "" && !predicate.Quantifier.Valid() { + return fmt.Errorf("physical filter predicate quantifier %q is invalid", predicate.Quantifier) + } + } else if err := validatePhysicalValue(predicate.Left, defined, bindVars); err != nil { + return err + } + requiresRight := operator != "EXISTS" && operator != "MISSING" + if requiresRight != (predicate.Right != nil) { + return fmt.Errorf("physical filter operator %s right value presence is invalid", operator) + } + if predicate.Right != nil { + if err := validatePhysicalValue(*predicate.Right, defined, bindVars); err != nil { + return err + } + } + return nil +} + +func validatePhysicalFilter(filter PhysicalFilter, defined map[string]bool, bindVars map[string]any) error { + legacy := strings.TrimSpace(filter.Predicate.Operator) != "" + rich := filter.Expression != nil + if legacy == rich { + return fmt.Errorf("filter requires exactly one legacy predicate or predicate expression") + } + if legacy { + return validatePhysicalPredicate(filter.Predicate, defined, bindVars) + } + return validatePhysicalPredicateExpression(*filter.Expression, defined, bindVars) +} + +func validatePhysicalDerivedLet(derived PhysicalDerivedLet, defined map[string]bool, bindVars map[string]any) error { + legacy := strings.TrimSpace(derived.Operator) != "" || len(derived.Inputs) != 0 + rich := derived.Expression != nil + if legacy == rich { + return fmt.Errorf("derived LET requires exactly one legacy operation or expression") + } + if rich { + return validatePhysicalExpression(*derived.Expression, defined, bindVars) + } + if strings.TrimSpace(derived.Operator) == "" { + return fmt.Errorf("derived LET operator is required") + } + for _, input := range derived.Inputs { + if err := validatePhysicalValue(input, defined, bindVars); err != nil { + return err + } + } + return nil +} + +func validatePhysicalProjection(projection PhysicalProjection, defined map[string]bool, bindVars map[string]any) error { + hasValue := projection.Value.Variable != "" || projection.Value.BindKey != "" || len(projection.Value.Path) != 0 + hasExpression := projection.Expression != nil + if hasValue == hasExpression { + return fmt.Errorf("projection requires exactly one value or expression") + } + if hasExpression { + return validatePhysicalExpression(*projection.Expression, defined, bindVars) + } + return validatePhysicalValue(projection.Value, defined, bindVars) +} diff --git a/internal/dataframe/compiler/ir/physical_validation_expr.go b/internal/dataframe/compiler/ir/physical_validation_expr.go new file mode 100644 index 0000000..4cc5c10 --- /dev/null +++ b/internal/dataframe/compiler/ir/physical_validation_expr.go @@ -0,0 +1,426 @@ +package ir + +import ( + "fmt" + + "strings" +) + +func validatePhysicalExpression(expression PhysicalExpression, defined map[string]bool, bindVars map[string]any) error { + if err := validatePhysicalExpressionObjectCycles(expression); err != nil { + return err + } + if !expression.Cardinality.valid() { + return fmt.Errorf("expression has invalid cardinality %q", expression.Cardinality) + } + if !expression.NullBehavior.valid() { + return fmt.Errorf("expression has invalid null behavior %q", expression.NullBehavior) + } + payloads := 0 + if expression.Value != nil { + payloads++ + } + if expression.Literal != nil { + payloads++ + } + if expression.Extract != nil { + payloads++ + } + if expression.Aggregate != nil { + payloads++ + } + if expression.Pivot != nil { + payloads++ + } + if expression.Slice != nil { + payloads++ + } + if expression.Lookup != nil { + payloads++ + } + if expression.ObjectLookup != nil { + payloads++ + } + if expression.KeyedMap != nil { + payloads++ + } + if expression.ObjectKeys != nil { + payloads++ + } + if expression.KeySet != nil { + payloads++ + } + if expression.Object != nil { + payloads++ + } + if expression.Call != nil { + payloads++ + } + if payloads != 1 { + return fmt.Errorf("expression must contain exactly one payload") + } + switch expression.Kind { + case PhysicalValueExpression: + if expression.Value == nil { + return fmt.Errorf("expression payload does not match kind") + } + return validatePhysicalValue(*expression.Value, defined, bindVars) + case PhysicalLiteralExpression: + if expression.Literal == nil { + return fmt.Errorf("expression payload does not match kind") + } + if err := requireBind(bindVars, expression.Literal.BindKey); err != nil { + return err + } + return nil + case PhysicalExtractExpression: + if expression.Extract == nil { + return fmt.Errorf("expression payload does not match kind") + } + return validatePhysicalExtract(*expression.Extract, defined, bindVars) + case PhysicalAggregateExpression: + if expression.Aggregate == nil { + return fmt.Errorf("expression payload does not match kind") + } + return validatePhysicalAggregate(*expression.Aggregate, defined, bindVars) + case PhysicalPivotExpression: + if expression.Pivot == nil { + return fmt.Errorf("expression payload does not match kind") + } + return validatePhysicalPivot(*expression.Pivot, defined, bindVars) + case PhysicalSliceExpression: + if expression.Slice == nil { + return fmt.Errorf("expression payload does not match kind") + } + return validatePhysicalSlice(*expression.Slice, defined, bindVars) + case PhysicalLookupExpression: + if expression.Lookup == nil { + return fmt.Errorf("expression payload does not match kind") + } + return validatePhysicalLookup(*expression.Lookup, defined, bindVars) + case PhysicalObjectLookupExpression: + if expression.ObjectLookup == nil { + return fmt.Errorf("expression payload does not match kind") + } + if !defined[expression.ObjectLookup.ObjectVariable] { + return fmt.Errorf("object lookup references undefined variable %q", expression.ObjectLookup.ObjectVariable) + } + return requireBind(bindVars, expression.ObjectLookup.KeyBindKey) + case PhysicalKeyedMapExpression: + if expression.KeyedMap == nil { + return fmt.Errorf("expression payload does not match kind") + } + return validatePhysicalKeyedMap(*expression.KeyedMap, defined, bindVars) + case PhysicalObjectKeysExpression: + if expression.ObjectKeys == nil { + return fmt.Errorf("expression payload does not match kind") + } + if !defined[expression.ObjectKeys.ObjectVariable] { + return fmt.Errorf("object keys references undefined variable %q", expression.ObjectKeys.ObjectVariable) + } + return nil + case PhysicalKeySetExpression: + if expression.KeySet == nil { + return fmt.Errorf("expression payload does not match kind") + } + return validatePhysicalKeySet(*expression.KeySet, defined, bindVars) + case PhysicalObjectExpression: + if expression.Object == nil { + return fmt.Errorf("expression payload does not match kind") + } + return validatePhysicalObject(*expression.Object, defined, bindVars) + case PhysicalCallExpression: + if expression.Call == nil { + return fmt.Errorf("expression payload does not match kind") + } + return validatePhysicalCall(*expression.Call, defined, bindVars) + default: + return fmt.Errorf("unknown expression kind %q", expression.Kind) + } +} + +func validatePhysicalLookup(lookup PhysicalLookup, defined map[string]bool, bindVars map[string]any) error { + if !physicalVariablePattern.MatchString(lookup.ItemVariable) { + return fmt.Errorf("lookup item variable %q is unsafe", lookup.ItemVariable) + } + if lookup.Source.Cardinality != PhysicalArrayCardinality { + return fmt.Errorf("lookup source expression must be array-valued, got %q", lookup.Source.Cardinality) + } + if err := validatePhysicalExpression(lookup.Source, defined, bindVars); err != nil { + return fmt.Errorf("lookup source: %w", err) + } + if lookup.ItemKey.Cardinality == PhysicalArrayCardinality { + return fmt.Errorf("lookup item key expression must be scalar") + } + itemDefined := make(map[string]bool, len(defined)+1) + for name, value := range defined { + itemDefined[name] = value + } + itemDefined[lookup.ItemVariable] = true + if err := validatePhysicalExpression(lookup.ItemKey, itemDefined, bindVars); err != nil { + return fmt.Errorf("lookup item key: %w", err) + } + if err := validatePhysicalExpression(lookup.ItemValue, itemDefined, bindVars); err != nil { + return fmt.Errorf("lookup item value: %w", err) + } + if strings.TrimSpace(lookup.MatchBindKey) == "" { + return fmt.Errorf("lookup match bind key is required") + } + if err := requireBind(bindVars, lookup.MatchBindKey); err != nil { + return fmt.Errorf("lookup match bind: %w", err) + } + return nil +} + +func validatePhysicalKeyedMap(keyed PhysicalKeyedMap, defined map[string]bool, bindVars map[string]any) error { + if keyed.Source.Cardinality != PhysicalArrayCardinality { + return fmt.Errorf("keyed map source expression must be array-valued, got %q", keyed.Source.Cardinality) + } + if !physicalVariablePattern.MatchString(keyed.ItemVariable) { + return fmt.Errorf("keyed map item variable %q is unsafe", keyed.ItemVariable) + } + if keyed.Reduction != PhysicalMapFirst && keyed.Reduction != PhysicalMapFirstSorted { + return fmt.Errorf("unsupported keyed map reduction %q", keyed.Reduction) + } + if err := validatePhysicalExpression(keyed.Source, defined, bindVars); err != nil { + return fmt.Errorf("keyed map source: %w", err) + } + itemDefined := make(map[string]bool, len(defined)+1) + for name, value := range defined { + itemDefined[name] = value + } + itemDefined[keyed.ItemVariable] = true + if err := validatePhysicalExpression(keyed.ItemKey, itemDefined, bindVars); err != nil { + return fmt.Errorf("keyed map item key: %w", err) + } + if err := validatePhysicalExpression(keyed.ItemValue, itemDefined, bindVars); err != nil { + return fmt.Errorf("keyed map item value: %w", err) + } + for index, fallback := range keyed.ValueFallbacks { + if err := validatePhysicalExpression(fallback, itemDefined, bindVars); err != nil { + return fmt.Errorf("keyed map fallback %d: %w", index, err) + } + } + return nil +} + +func validatePhysicalKeySet(keySet PhysicalKeySet, defined map[string]bool, bindVars map[string]any) error { + if !physicalVariablePattern.MatchString(keySet.ItemVariable) { + return fmt.Errorf("key set item variable %q is unsafe", keySet.ItemVariable) + } + if keySet.Source.Cardinality != PhysicalArrayCardinality { + return fmt.Errorf("key set source expression must be array-valued, got %q", keySet.Source.Cardinality) + } + if err := validatePhysicalExpression(keySet.Source, defined, bindVars); err != nil { + return fmt.Errorf("key set source: %w", err) + } + itemDefined := make(map[string]bool, len(defined)+1) + for name, value := range defined { + itemDefined[name] = value + } + itemDefined[keySet.ItemVariable] = true + if err := validatePhysicalExpression(keySet.ItemKey, itemDefined, bindVars); err != nil { + return fmt.Errorf("key set item key: %w", err) + } + return nil +} + +// validatePhysicalCall checks the small, backend-neutral operator vocabulary +// accepted by the AQL renderer. Keeping this list here prevents a recipe from +// smuggling arbitrary function or query text through the physical plan. +func validatePhysicalCall(call PhysicalCall, defined map[string]bool, bindVars map[string]any) error { + name := strings.ToLower(strings.TrimSpace(call.Name)) + if name == "" { + return fmt.Errorf("call name is required") + } + known := map[string]bool{ + "coalesce": true, "coalesce_string": true, "fallback": true, "first": true, "all": true, "distinct": true, + "concat": true, "join": true, "cast": true, "reference_id": true, + "path_segment": true, "basename": true, "last_segment": true, + "sanitize_name": true, "sanitize_graphql_name": true, "uuid3": true, "uuid5": true, + "if": true, "case": true, "not": true, "and": true, "or": true, + "eq": true, "neq": true, "gt": true, "gte": true, "lt": true, "lte": true, + "contains": true, + } + if !known[name] { + return fmt.Errorf("unsupported call %q", call.Name) + } + if name == "cast" { + if len(call.Args) != 1 || strings.TrimSpace(call.TargetKind) == "" { + return fmt.Errorf("cast requires one argument and a target kind") + } + switch strings.ToLower(strings.TrimSpace(call.TargetKind)) { + case "string", "integer", "decimal", "boolean", "date", "date_time", "code", "uuid": + default: + return fmt.Errorf("cast target kind %q is unsupported", call.TargetKind) + } + } else if call.TargetKind != "" { + return fmt.Errorf("target kind is only valid for cast") + } + for index, arg := range call.Args { + if err := validatePhysicalExpression(arg, defined, bindVars); err != nil { + return fmt.Errorf("call %q argument %d: %w", name, index, err) + } + } + return nil +} + +// validatePhysicalExpressionObjectCycles protects the recursive expression +// validator from a malformed in-memory plan containing a cycle of +// PhysicalObject pointers. JSON decoding cannot produce such a cycle, but +// plans are also assembled by compiler stages and tests, where pointers can +// be wired directly. The active/visited split permits shared (DAG) objects +// while rejecting only true recursion. +func validatePhysicalExpressionObjectCycles(expression PhysicalExpression) error { + active := map[*PhysicalObject]bool{} + visited := map[*PhysicalObject]bool{} + var visitExpression func(PhysicalExpression) error + var visitObject func(*PhysicalObject) error + var visitPredicate func(*PhysicalPredicateExpression) error + var visitSubplan func(PhysicalSubplan) error + visitExpression = func(current PhysicalExpression) error { + if current.Object != nil { + if err := visitObject(current.Object); err != nil { + return err + } + } + if current.Aggregate != nil && current.Aggregate.Value != nil { + if err := visitExpression(*current.Aggregate.Value); err != nil { + return err + } + } + if current.Call != nil { + for index := range current.Call.Args { + if err := visitExpression(current.Call.Args[index]); err != nil { + return err + } + } + } + if current.Aggregate != nil && current.Aggregate.Predicate != nil { + if err := visitPredicate(current.Aggregate.Predicate); err != nil { + return err + } + } + if current.Slice != nil { + if current.Slice.Predicate != nil { + if err := visitPredicate(current.Slice.Predicate); err != nil { + return err + } + } + if current.Slice.Sort != nil { + if err := visitExpression(*current.Slice.Sort); err != nil { + return err + } + } + for _, projection := range current.Slice.Projections { + if err := visitExpression(projection.Expression); err != nil { + return err + } + } + } + if current.Lookup != nil { + if err := visitExpression(current.Lookup.Source); err != nil { + return err + } + if err := visitExpression(current.Lookup.ItemKey); err != nil { + return err + } + if err := visitExpression(current.Lookup.ItemValue); err != nil { + return err + } + } + if current.KeyedMap != nil { + if err := visitExpression(current.KeyedMap.Source); err != nil { + return err + } + if err := visitExpression(current.KeyedMap.ItemKey); err != nil { + return err + } + if err := visitExpression(current.KeyedMap.ItemValue); err != nil { + return err + } + for _, fallback := range current.KeyedMap.ValueFallbacks { + if err := visitExpression(fallback); err != nil { + return err + } + } + } + return nil + } + visitPredicate = func(predicate *PhysicalPredicateExpression) error { + if predicate == nil { + return nil + } + if predicate.Comparison != nil && predicate.Comparison.LeftExpression != nil { + if err := visitExpression(*predicate.Comparison.LeftExpression); err != nil { + return err + } + } + for index := range predicate.Children { + if err := visitPredicate(&predicate.Children[index]); err != nil { + return err + } + } + if predicate.Exists != nil { + return visitSubplan(*predicate.Exists) + } + return nil + } + visitSubplan = func(subplan PhysicalSubplan) error { + for _, operation := range subplan.Operations { + switch operation.Kind { + case PhysicalFilterOp: + if operation.Filter != nil && operation.Filter.Expression != nil { + if err := visitPredicate(operation.Filter.Expression); err != nil { + return err + } + } + case PhysicalDerivedLetOp: + if operation.DerivedLet != nil && operation.DerivedLet.Expression != nil { + if err := visitExpression(*operation.DerivedLet.Expression); err != nil { + return err + } + } + case PhysicalSetOp: + if operation.Set != nil { + if err := visitSubplan(operation.Set.Subplan); err != nil { + return err + } + } + case PhysicalUnnestOp: + if operation.Unnest != nil { + if err := visitExpression(operation.Unnest.Expression); err != nil { + return err + } + } + } + } + return visitExpression(subplan.Return) + } + visitObject = func(object *PhysicalObject) error { + if active[object] { + return fmt.Errorf("physical object expression contains a recursive cycle") + } + if visited[object] { + return nil + } + active[object] = true + for _, field := range object.Fields { + if err := visitExpression(field.Expression); err != nil { + return err + } + } + delete(active, object) + visited[object] = true + return nil + } + return visitExpression(expression) +} + +func (cardinality PhysicalCardinality) valid() bool { + return cardinality == PhysicalScalarCardinality || cardinality == PhysicalArrayCardinality || cardinality == PhysicalObjectCardinality +} + +func (behavior PhysicalNullBehavior) valid() bool { + return behavior == PhysicalPreserveNull || behavior == PhysicalOmitNulls || behavior == PhysicalEmptyOnNull +} diff --git a/internal/dataframe/compiler/ir/physical_validation_refs.go b/internal/dataframe/compiler/ir/physical_validation_refs.go new file mode 100644 index 0000000..d40022e --- /dev/null +++ b/internal/dataframe/compiler/ir/physical_validation_refs.go @@ -0,0 +1,137 @@ +package ir + +import ( + "fmt" + "strings" + + "github.com/calypr/loom/internal/dataframe/spec" + + "github.com/calypr/loom/fhirschema" +) + +func validatePhysicalExtract(extract PhysicalExtract, defined map[string]bool, bindVars map[string]any) error { + if err := validatePhysicalValue(extract.Source, defined, bindVars); err != nil { + return err + } + if !schemaDefinitionExists(extract.ResourceType) { + return fmt.Errorf("extract resource type %q is not represented by the active generated FHIR schema", extract.ResourceType) + } + if err := validatePhysicalSelector(extract.ResourceType, extract.Selector); err != nil { + return fmt.Errorf("extract selector: %w", err) + } + if extract.ExecutionMode != "" && extract.ExecutionMode != PhysicalSelectorGeneric && extract.ExecutionMode != PhysicalSelectorDirectScalar && extract.ExecutionMode != PhysicalSelectorConditionalArray { + return fmt.Errorf("unknown selector execution mode %q", extract.ExecutionMode) + } + if extract.ExecutionMode == PhysicalSelectorDirectScalar && (len(extract.Fallbacks) != 0 || extract.Selector.Filter != nil || !selectorHasNoArrays(extract.Selector)) { + return fmt.Errorf("direct scalar selector mode requires one fallback-free non-repeated selector") + } + if extract.ExecutionMode == PhysicalSelectorConditionalArray && (len(extract.Fallbacks) != 0 || extract.Selector.Filter != nil || !selectorHasIteratedArray(extract.Selector)) { + return fmt.Errorf("conditional array selector mode requires one fallback-free repeated selector") + } + for index, fallback := range extract.Fallbacks { + if err := validatePhysicalSelector(extract.ResourceType, fallback); err != nil { + return fmt.Errorf("extract fallback %d: %w", index, err) + } + } + if extract.Prepared != nil { + if err := validatePhysicalPreparedReference(*extract.Prepared, defined); err != nil { + return err + } + if len(extract.Fallbacks) != 0 { + return fmt.Errorf("prepared extract cannot use fallback selectors") + } + } + return nil +} + +func validatePhysicalPreparedReference(reference PhysicalPreparedReference, defined map[string]bool) error { + if !physicalVariablePattern.MatchString(reference.SetVariable) || !defined[reference.SetVariable] { + return fmt.Errorf("prepared set variable %q is out of scope", reference.SetVariable) + } + if !physicalVariablePattern.MatchString(reference.Field) { + return fmt.Errorf("prepared field %q is unsafe", reference.Field) + } + return nil +} + +func validatePhysicalSelector(resourceType string, selector Selector) error { + if len(selector.Steps) == 0 { + return fmt.Errorf("selector is required") + } + if _, _, err := spec.SelectorCardinality(resourceType, selector); err != nil { + return err + } + return nil +} + +// schemaDefinitionExists accepts both top-level FHIR resources and generated +// backbone/choice definitions such as GroupMember. A definition may be a +// valid selector source without being a graph collection or traversal node; +// collection and route validation deliberately continue to use HasResource. +func schemaDefinitionExists(resourceType string) bool { + return fhirschema.DefinitionExists(resourceType) +} + +func validatePhysicalAggregate(aggregate PhysicalAggregate, defined map[string]bool, bindVars map[string]any) error { + if err := validatePhysicalValue(aggregate.Source, defined, bindVars); err != nil { + return err + } + switch aggregate.Operation { + case PhysicalCountAggregate, PhysicalCountDistinctAggregate, PhysicalExistsAggregate, PhysicalDistinctValuesAggregate, PhysicalMinAggregate, PhysicalMaxAggregate, PhysicalFirstAggregate: + default: + return fmt.Errorf("unknown aggregate operation %q", aggregate.Operation) + } + needsValue := aggregate.Operation != PhysicalCountAggregate && aggregate.Operation != PhysicalExistsAggregate + if needsValue != (aggregate.Value != nil) { + return fmt.Errorf("aggregate operation %q value presence is invalid", aggregate.Operation) + } + if aggregate.Value != nil { + if err := validatePhysicalExpression(*aggregate.Value, defined, bindVars); err != nil { + return fmt.Errorf("aggregate value: %w", err) + } + } + if aggregate.Predicate != nil { + if err := validatePhysicalPredicateExpression(*aggregate.Predicate, defined, bindVars); err != nil { + return fmt.Errorf("aggregate predicate: %w", err) + } + } + return nil +} + +func validatePhysicalPivot(pivot PhysicalPivotMap, defined map[string]bool, bindVars map[string]any) error { + if err := validatePhysicalValue(pivot.Source, defined, bindVars); err != nil { + return err + } + if strings.TrimSpace(pivot.ResourceType) == "" || !fhirschema.HasResource(pivot.ResourceType) { + return fmt.Errorf("pivot resource type %q is not represented by the active generated FHIR schema", pivot.ResourceType) + } + if err := validatePhysicalSelector(pivot.ResourceType, pivot.KeySelector); err != nil { + return fmt.Errorf("pivot key selector: %w", err) + } + if err := validatePhysicalSelector(pivot.ResourceType, pivot.ValueSelector); err != nil { + return fmt.Errorf("pivot value selector: %w", err) + } + if err := requireBind(bindVars, pivot.ColumnsBindKey); err != nil { + return err + } + columns, ok := bindVars[pivot.ColumnsBindKey].([]string) + if !ok || len(columns) == 0 { + return fmt.Errorf("pivot columns bind %q must be a non-empty []string", pivot.ColumnsBindKey) + } + for _, column := range columns { + if strings.TrimSpace(column) == "" { + return fmt.Errorf("pivot columns bind %q contains an empty column", pivot.ColumnsBindKey) + } + } + if pivot.PreparedKey != nil { + if err := validatePhysicalPreparedReference(*pivot.PreparedKey, defined); err != nil { + return fmt.Errorf("prepared pivot key: %w", err) + } + } + if pivot.PreparedValue != nil { + if err := validatePhysicalPreparedReference(*pivot.PreparedValue, defined); err != nil { + return fmt.Errorf("prepared pivot value: %w", err) + } + } + return nil +} diff --git a/internal/dataframe/compiler/ir/physical_validation_shapes.go b/internal/dataframe/compiler/ir/physical_validation_shapes.go new file mode 100644 index 0000000..3c50e46 --- /dev/null +++ b/internal/dataframe/compiler/ir/physical_validation_shapes.go @@ -0,0 +1,201 @@ +package ir + +import ( + "fmt" + + "strings" +) + +func validatePhysicalSlice(slice PhysicalSlice, defined map[string]bool, bindVars map[string]any) error { + if err := validatePhysicalValue(slice.Source, defined, bindVars); err != nil { + return err + } + if slice.Predicate != nil { + if err := validatePhysicalPredicateExpression(*slice.Predicate, defined, bindVars); err != nil { + return fmt.Errorf("slice predicate: %w", err) + } + } + if slice.Sort == nil { + return fmt.Errorf("slice requires a stable sort expression") + } + if err := validatePhysicalExpression(*slice.Sort, defined, bindVars); err != nil { + return fmt.Errorf("slice sort: %w", err) + } + if err := requireBind(bindVars, slice.LimitBindKey); err != nil { + return err + } + limit, ok := bindVars[slice.LimitBindKey].(int) + if !ok || limit <= 0 { + return fmt.Errorf("slice limit bind %q must be a positive int", slice.LimitBindKey) + } + return validatePhysicalExpressionProjections(slice.Projections, defined, bindVars, "slice") +} + +func validatePhysicalObject(object PhysicalObject, defined map[string]bool, bindVars map[string]any) error { + return validatePhysicalExpressionProjections(object.Fields, defined, bindVars, "object") +} + +func validatePhysicalExpressionProjections(projections []PhysicalExpressionProjection, defined map[string]bool, bindVars map[string]any, owner string) error { + if len(projections) == 0 { + return fmt.Errorf("%s requires at least one projection", owner) + } + seen := map[string]bool{} + for _, projection := range projections { + if strings.TrimSpace(projection.Name) == "" || seen[projection.Name] { + return fmt.Errorf("%s projection name %q is empty or duplicated", owner, projection.Name) + } + seen[projection.Name] = true + if err := validatePhysicalExpression(projection.Expression, defined, bindVars); err != nil { + return fmt.Errorf("%s projection %q: %w", owner, projection.Name, err) + } + } + return nil +} + +func validatePhysicalPredicateExpression(predicate PhysicalPredicateExpression, defined map[string]bool, bindVars map[string]any) error { + switch predicate.Kind { + case PhysicalComparisonPredicate: + if predicate.Comparison == nil || len(predicate.Children) != 0 || predicate.Exists != nil { + return fmt.Errorf("comparison predicate requires exactly one comparison") + } + return validatePhysicalPredicate(*predicate.Comparison, defined, bindVars) + case PhysicalAllPredicate, PhysicalAnyPredicate: + if predicate.Comparison != nil || predicate.Exists != nil || len(predicate.Children) == 0 { + return fmt.Errorf("%s predicate requires one or more child predicates", predicate.Kind) + } + for index, child := range predicate.Children { + if err := validatePhysicalPredicateExpression(child, defined, bindVars); err != nil { + return fmt.Errorf("predicate child %d: %w", index, err) + } + } + return nil + case PhysicalNotPredicate: + if predicate.Comparison != nil || predicate.Exists != nil || len(predicate.Children) != 1 { + return fmt.Errorf("NOT predicate requires exactly one child predicate") + } + return validatePhysicalPredicateExpression(predicate.Children[0], defined, bindVars) + case PhysicalExistsPredicate: + if predicate.Comparison != nil || len(predicate.Children) != 0 || predicate.Exists == nil { + return fmt.Errorf("EXISTS predicate requires exactly one subplan") + } + return validatePhysicalSubplan(*predicate.Exists, defined, bindVars) + default: + return fmt.Errorf("unknown predicate kind %q", predicate.Kind) + } +} + +func validatePhysicalSubplan(subplan PhysicalSubplan, parent map[string]bool, bindVars map[string]any) error { + if len(subplan.Captures) == 0 { + return fmt.Errorf("subplan requires at least one explicit capture") + } + defined := make(map[string]bool, len(subplan.Captures)) + for _, capture := range subplan.Captures { + if !parent[capture] { + return fmt.Errorf("subplan capture %q is out of scope", capture) + } + if err := definePhysicalVariable(defined, capture); err != nil { + return fmt.Errorf("subplan capture: %w", err) + } + } + if len(subplan.Operations) == 0 { + return fmt.Errorf("subplan requires at least one operation") + } + for index, operation := range subplan.Operations { + if operation.Kind == PhysicalRootScanOp || operation.Kind == PhysicalReturnOp || operation.Kind == PhysicalSortOp || operation.Kind == PhysicalLimitOp { + return fmt.Errorf("subplan operation %d cannot be %s", index, operation.Kind) + } + if err := operation.validatePayload(); err != nil { + return fmt.Errorf("subplan operation %d (%s): %w", index, operation.Kind, err) + } + switch operation.Kind { + case PhysicalTraversalOp: + traversal := operation.Traversal + if !defined[traversal.SourceVariable] { + return fmt.Errorf("subplan operation %d: traversal source variable %q is out of scope", index, traversal.SourceVariable) + } + if traversal.Direction != PhysicalOutbound && traversal.Direction != PhysicalInbound && traversal.Direction != PhysicalAny { + return fmt.Errorf("subplan operation %d: invalid traversal direction %q", index, traversal.Direction) + } + if traversal.EdgeTargetTypeField != "" && !physicalPathPartPattern.MatchString(traversal.EdgeTargetTypeField) { + return fmt.Errorf("subplan operation %d: unsafe traversal edge type field %q", index, traversal.EdgeTargetTypeField) + } + if err := validatePhysicalTraversalStrategy(*traversal); err != nil { + return fmt.Errorf("subplan operation %d: %w", index, err) + } + if err := requireCollectionBind(bindVars, traversal.EdgeCollectionBindKey); err != nil { + return fmt.Errorf("subplan operation %d: %w", index, err) + } + for _, key := range []string{traversal.EdgeLabelBindKey, traversal.TargetTypeBindKey} { + if key != "" { + if err := requireBind(bindVars, key); err != nil { + return fmt.Errorf("subplan operation %d: %w", index, err) + } + } + } + if err := definePhysicalVariable(defined, traversal.TargetVariable); err != nil { + return fmt.Errorf("subplan operation %d: %w", index, err) + } + if traversal.EdgeVariable != "" { + if err := definePhysicalVariable(defined, traversal.EdgeVariable); err != nil { + return fmt.Errorf("subplan operation %d: %w", index, err) + } + } + case PhysicalFilterOp: + if err := validatePhysicalFilter(*operation.Filter, defined, bindVars); err != nil { + return fmt.Errorf("subplan operation %d: %w", index, err) + } + case PhysicalDerivedLetOp: + if err := validatePhysicalDerivedLet(*operation.DerivedLet, defined, bindVars); err != nil { + return fmt.Errorf("subplan operation %d: %w", index, err) + } + if err := definePhysicalVariable(defined, operation.DerivedLet.Variable); err != nil { + return fmt.Errorf("subplan operation %d: %w", index, err) + } + case PhysicalSetOp: + if err := validatePhysicalSet(*operation.Set, defined, bindVars); err != nil { + return fmt.Errorf("subplan operation %d set %q: %w", index, operation.Set.Variable, err) + } + if err := definePhysicalVariable(defined, operation.Set.Variable); err != nil { + return fmt.Errorf("subplan operation %d: %w", index, err) + } + case PhysicalUnnestOp: + if err := validatePhysicalUnnest(*operation.Unnest, defined, bindVars); err != nil { + return fmt.Errorf("subplan operation %d unnest: %w", index, err) + } + if err := definePhysicalVariable(defined, operation.Unnest.OutputVariable); err != nil { + return fmt.Errorf("subplan operation %d: %w", index, err) + } + if operation.Unnest.Ordinality != "" { + if err := definePhysicalVariable(defined, operation.Unnest.Ordinality); err != nil { + return fmt.Errorf("subplan operation %d: %w", index, err) + } + } + default: + return fmt.Errorf("subplan operation %d has unsupported kind %q", index, operation.Kind) + } + } + return validatePhysicalExpression(subplan.Return, defined, bindVars) +} + +func validatePhysicalValue(value PhysicalValue, defined map[string]bool, bindVars map[string]any) error { + hasVariable := value.Variable != "" + hasBind := value.BindKey != "" + if hasVariable == hasBind { + return fmt.Errorf("physical value must reference exactly one variable or bind key") + } + if hasBind { + if len(value.Path) != 0 { + return fmt.Errorf("bind value %q cannot have a path", value.BindKey) + } + return requireBind(bindVars, value.BindKey) + } + if !defined[value.Variable] { + return fmt.Errorf("variable %q is out of scope", value.Variable) + } + for _, part := range value.Path { + if !physicalPathPartPattern.MatchString(part) { + return fmt.Errorf("unsafe path segment %q", part) + } + } + return nil +} diff --git a/internal/dataframe/compiler/ir/physical_values.go b/internal/dataframe/compiler/ir/physical_values.go new file mode 100644 index 0000000..1d1a450 --- /dev/null +++ b/internal/dataframe/compiler/ir/physical_values.go @@ -0,0 +1,256 @@ +package ir + +// PhysicalPlan is the renderer-independent AQL operation graph produced after +// semantic planning. Operations are ordered because AQL variables have lexical +// scope: an operation may reference only variables introduced before it. +type PhysicalValue struct { + Variable string + Path []string + BindKey string +} + +// PhysicalCardinality and PhysicalNullBehavior are deliberately explicit on +// every rich expression. A renderer must not infer array/scalar/null behavior +// from where an expression appears in an AQL template. +type PhysicalCardinality string + +const ( + PhysicalScalarCardinality PhysicalCardinality = "SCALAR" + PhysicalArrayCardinality PhysicalCardinality = "ARRAY" + PhysicalObjectCardinality PhysicalCardinality = "OBJECT" +) + +type PhysicalNullBehavior string + +const ( + PhysicalPreserveNull PhysicalNullBehavior = "PRESERVE_NULL" + PhysicalOmitNulls PhysicalNullBehavior = "OMIT_NULLS" + PhysicalEmptyOnNull PhysicalNullBehavior = "EMPTY_ON_NULL" +) + +type PhysicalExpressionKind string + +const ( + PhysicalValueExpression PhysicalExpressionKind = "VALUE" + // PhysicalLiteralExpression is a typed bind-backed constant. Literals are + // kept out of generated AQL so recipe authors cannot inject query source. + PhysicalLiteralExpression PhysicalExpressionKind = "LITERAL" + PhysicalExtractExpression PhysicalExpressionKind = "EXTRACT" + PhysicalAggregateExpression PhysicalExpressionKind = "AGGREGATE" + PhysicalPivotExpression PhysicalExpressionKind = "PIVOT_MAP" + PhysicalSliceExpression PhysicalExpressionKind = "SLICE" + // PhysicalLookupExpression performs one bounded key lookup over a + // collection-valued expression. It is backend-neutral and is useful for + // any frontend that freezes a finite set of key/value columns before + // execution; it is not a recipe-specific dynamic-map operation. + PhysicalLookupExpression PhysicalExpressionKind = "LOOKUP" + PhysicalObjectLookupExpression PhysicalExpressionKind = "OBJECT_LOOKUP" + PhysicalKeyedMapExpression PhysicalExpressionKind = "KEYED_MAP" + PhysicalObjectKeysExpression PhysicalExpressionKind = "OBJECT_KEYS" + // PhysicalKeySetExpression returns the sorted distinct keys observed in a + // bounded source. It is executor metadata, not a public dataframe column. + PhysicalKeySetExpression PhysicalExpressionKind = "KEY_SET" + PhysicalObjectExpression PhysicalExpressionKind = "OBJECT" + // PhysicalCallExpression represents a recipe-neutral expression function. + // Name is validated against the compiler-owned operator registry and Args + // remain typed expressions; neither contains AQL source text. + PhysicalCallExpression PhysicalExpressionKind = "CALL" +) + +// PhysicalSelectorExecutionMode records a schema-proven selector lowering. +// Generic is the safe fallback for predicates, fallbacks, choice paths, and +// unknown cardinality. +type PhysicalSelectorExecutionMode string + +const ( + PhysicalSelectorGeneric PhysicalSelectorExecutionMode = "GENERIC" + PhysicalSelectorDirectScalar PhysicalSelectorExecutionMode = "DIRECT_SCALAR" + PhysicalSelectorConditionalArray PhysicalSelectorExecutionMode = "CONDITIONAL_ARRAY" +) + +// PhysicalExpression is a closed, renderer-independent value tree. It carries +// no AQL source text: selector paths have already been parsed and all literals +// are represented by bind values. +type PhysicalExpression struct { + Kind PhysicalExpressionKind + Cardinality PhysicalCardinality + NullBehavior PhysicalNullBehavior + Value *PhysicalValue + Literal *PhysicalLiteral + Extract *PhysicalExtract + Aggregate *PhysicalAggregate + Pivot *PhysicalPivotMap + Slice *PhysicalSlice + Lookup *PhysicalLookup + ObjectLookup *PhysicalObjectLookup + KeyedMap *PhysicalKeyedMap + ObjectKeys *PhysicalObjectKeys + KeySet *PhysicalKeySet + Object *PhysicalObject + Call *PhysicalCall +} + +// PhysicalLiteral references a value in the plan bind map. BindKey is +// deliberately required even for primitive constants so all recipe data uses +// the same parameterized execution boundary. +type PhysicalLiteral struct { + BindKey string +} + +// PhysicalCall is a backend-neutral expression operator. TargetKind is used +// only by cast and names a logical scalar type (for example "string" or +// "integer"), never a backend type or query fragment. +type PhysicalCall struct { + Name string + Args []PhysicalExpression + TargetKind string +} + +// PhysicalExtract obtains one FHIR selector from a variable or prior set +// element. ResourceType keeps schema validation available after semantic +// lowering; fallbacks preserve the existing FIRST_NON_NULL behavior. +type PhysicalExtract struct { + Source PhysicalValue + ResourceType string + Selector Selector + Fallbacks []Selector + // Distinct preserves the explicit DISTINCT projection mode after semantic + // lowering. It is meaningful only for an array-valued expression. + Distinct bool + ExecutionMode PhysicalSelectorExecutionMode + // Prepared points at a selector value projected by a prepared child set. + // Source remains the owning set for scope validation and diagnostics. + Prepared *PhysicalPreparedReference +} + +// PhysicalPreparedReference identifies one selector column in a prepared set. +type PhysicalPreparedReference struct { + SetVariable string + Field string +} + +// PhysicalPreparedSet describes selector projections shared by rich +// consumers over one materialized relationship set. Preparation stays +// attached to the correlated set so root row grain cannot change. +type PhysicalPreparedSet struct { + Variable string + SourceSetVariable string + Fields []PhysicalPreparedField +} + +type PhysicalPreparedField struct { + Name string + ResourceType string + Selector Selector +} + +type PhysicalAggregateOperation string + +const ( + PhysicalCountAggregate PhysicalAggregateOperation = "COUNT" + PhysicalCountDistinctAggregate PhysicalAggregateOperation = "COUNT_DISTINCT" + PhysicalExistsAggregate PhysicalAggregateOperation = "EXISTS" + PhysicalDistinctValuesAggregate PhysicalAggregateOperation = "DISTINCT_VALUES" + PhysicalMinAggregate PhysicalAggregateOperation = "MIN" + PhysicalMaxAggregate PhysicalAggregateOperation = "MAX" + PhysicalFirstAggregate PhysicalAggregateOperation = "FIRST" +) + +type PhysicalAggregate struct { + Source PhysicalValue + Operation PhysicalAggregateOperation + Value *PhysicalExpression + Predicate *PhysicalPredicateExpression +} + +type PhysicalPivotMap struct { + Source PhysicalValue + ResourceType string + // ItemSource and ItemResourceType describe a repeated backbone item that + // owns the key/value pair. Keeping this scope in the physical operation is + // what prevents key and value selectors from being flattened independently + // (for example Observation.component[].code paired with the same + // component's value[x]). + ItemSource Selector + ItemResourceType string + KeySelector Selector + ValueSelector Selector + ValueFallbacks []Selector + ColumnsBindKey string + FlattenSingleColumn bool + PreparedKey *PhysicalPreparedReference + PreparedValue *PhysicalPreparedReference +} + +// PhysicalLookup is a typed, bounded lookup over one array-valued source. +// ItemVariable is introduced only while evaluating ItemKey and ItemValue; +// neither expression contains backend query text. MatchBindKey names the +// scalar bind containing the frozen key for this output projection. +// +// Source is intentionally an expression rather than a PhysicalValue because +// dynamic sources commonly select repeated values from a root payload. The +// optimizer can fingerprint identical sources and the renderer can keep the +// lookup shape identical across frontends. +type PhysicalLookup struct { + Source PhysicalExpression + ItemVariable string + ItemKey PhysicalExpression + ItemValue PhysicalExpression + MatchBindKey string +} + +type PhysicalObjectLookup struct { + ObjectVariable string + KeyBindKey string +} + +type PhysicalMapReduction string + +const ( + PhysicalMapFirst PhysicalMapReduction = "FIRST" + PhysicalMapFirstSorted PhysicalMapReduction = "FIRST_SORTED" +) + +type PhysicalKeyedMap struct { + Source PhysicalExpression + ItemVariable string + ItemKey PhysicalExpression + ItemValue PhysicalExpression + ValueFallbacks []PhysicalExpression + Reduction PhysicalMapReduction + FlattenSource bool +} + +type PhysicalObjectKeys struct { + ObjectVariable string +} + +type PhysicalKeySet struct { + Source PhysicalExpression + ItemVariable string + ItemKey PhysicalExpression +} + +// PhysicalSlice is a bounded, stable, nested projection over a prior set. +// Limit remains a bind variable so callers cannot synthesize query literals. +type PhysicalSlice struct { + Source PhysicalValue + Predicate *PhysicalPredicateExpression + Sort *PhysicalExpression + LimitBindKey string + Projections []PhysicalExpressionProjection +} + +type PhysicalExpressionProjection struct { + Name string + Expression PhysicalExpression +} + +type PhysicalObject struct { + Fields []PhysicalExpressionProjection +} + +// PhysicalSet is a correlated, array-valued subplan. Captures are declared +// explicitly so validation can prevent accidental references to parent or +// future variables. The set variable becomes visible only after its subplan +// has validated successfully. diff --git a/internal/dataframe/compiler/ir/plan.go b/internal/dataframe/compiler/ir/plan.go deleted file mode 100644 index 7f39a4f..0000000 --- a/internal/dataframe/compiler/ir/plan.go +++ /dev/null @@ -1,1319 +0,0 @@ -package ir - -import ( - "fmt" - - "github.com/calypr/loom/internal/dataframe/spec" - "regexp" - "strings" - - "github.com/calypr/loom/fhirschema" -) - -// PhysicalPlan is the renderer-independent AQL operation graph produced after -// semantic planning. Operations are ordered because AQL variables have lexical -// scope: an operation may reference only variables introduced before it. -type PhysicalPlan struct { - Version int - Source PhysicalSource - BindVars map[string]any - Operations []PhysicalOperation - // AppliedRules records physical rewrites without exposing renderer - // implementation details to callers. - AppliedRules []string - SharedTraversalCount int - // OptimizationPolicy records every optional rewrite decision made by the - // physical optimizer, including conservative rejections. - OptimizationPolicy PhysicalOptimizationReport - // RequiredMatchReuseCount records duplicate required EXISTS predicates - // removed during physical lowering. It is deliberately separate from - // shared traversal count: required predicates remain pre-window - // semi-joins, while optional sets are post-window materializations. - RequiredMatchReuseCount int -} - -// PhysicalSource retains semantic provenance through physical optimization so -// explain output and compiler errors can point back to user intent. -type PhysicalSource struct { - RecipeID string - TemplateID string - SemanticNode string - SemanticField string - ResourceType string - Relationship string -} - -type PhysicalOperationKind string - -const ( - PhysicalRootScanOp PhysicalOperationKind = "ROOT_SCAN" - PhysicalTraversalOp PhysicalOperationKind = "TRAVERSAL" - PhysicalFilterOp PhysicalOperationKind = "FILTER" - PhysicalDerivedLetOp PhysicalOperationKind = "DERIVED_LET" - // PhysicalSetOp materializes a correlated, array-valued subplan. It is the - // only operation that can introduce a set variable; selectors, aggregates, - // pivots, and slices consume that variable through typed expressions. - PhysicalSetOp PhysicalOperationKind = "SET" - // PhysicalSortOp and PhysicalLimitOp describe the root execution window. - // They are intentionally typed so preview ordering and bounds cannot be - // smuggled into an AQL string by a caller. - PhysicalSortOp PhysicalOperationKind = "SORT" - PhysicalLimitOp PhysicalOperationKind = "LIMIT" - PhysicalReturnOp PhysicalOperationKind = "RETURN" -) - -// PhysicalOperation is a tagged union. Exactly one payload matching Kind must -// be set. Source can be more specific than the plan-level provenance. -type PhysicalOperation struct { - Kind PhysicalOperationKind - Source PhysicalSource - RootScan *PhysicalRootScan - Traversal *PhysicalTraversal - Filter *PhysicalFilter - DerivedLet *PhysicalDerivedLet - Set *PhysicalSet - Sort *PhysicalSort - Limit *PhysicalLimit - Return *PhysicalReturn -} - -type PhysicalRootScan struct { - Variable string - CollectionBindKey string -} - -type PhysicalTraversalDirection string - -const ( - PhysicalOutbound PhysicalTraversalDirection = "OUTBOUND" - PhysicalInbound PhysicalTraversalDirection = "INBOUND" - PhysicalAny PhysicalTraversalDirection = "ANY" -) - -// PhysicalTraversalStrategy selects the execution shape for a validated -// depth-one relationship. Native graph traversal is the conservative -// fallback. EndpointLookup is only legal when storage-route metadata proves -// the endpoint/discriminator fields and their compound index contract. -type PhysicalTraversalStrategy string - -const ( - PhysicalTraversalNative PhysicalTraversalStrategy = "NATIVE" - PhysicalTraversalEndpointLookup PhysicalTraversalStrategy = "ENDPOINT_LOOKUP" -) - -type PhysicalTraversal struct { - SourceVariable string - TargetVariable string - EdgeVariable string - Direction PhysicalTraversalDirection - EdgeCollectionBindKey string - EdgeLabelBindKey string - TargetTypeBindKey string - // EdgeTargetTypeField is a compiler-owned fhir_edge discriminator used - // alongside TargetTypeBindKey. For a parent-to-child INBOUND route it is - // from_type; for a proven forward OUTBOUND route it is to_type. The node - // resourceType check remains independently mandatory. - EdgeTargetTypeField string - // Strategy is deliberately typed rather than an AQL fragment. Endpoint - // fields are supplied by resolveStorageRoute and validated against the - // direction before the renderer can use them. - Strategy PhysicalTraversalStrategy - EndpointField string - EndpointJoinField string - EndpointIndexFields []string -} - -// PhysicalValue is either a variable/path reference or a bind variable. A -// renderer must never interpret Path segments as AQL source text. -type PhysicalValue struct { - Variable string - Path []string - BindKey string -} - -// PhysicalCardinality and PhysicalNullBehavior are deliberately explicit on -// every rich expression. A renderer must not infer array/scalar/null behavior -// from where an expression appears in an AQL template. -type PhysicalCardinality string - -const ( - PhysicalScalarCardinality PhysicalCardinality = "SCALAR" - PhysicalArrayCardinality PhysicalCardinality = "ARRAY" - PhysicalObjectCardinality PhysicalCardinality = "OBJECT" -) - -type PhysicalNullBehavior string - -const ( - PhysicalPreserveNull PhysicalNullBehavior = "PRESERVE_NULL" - PhysicalOmitNulls PhysicalNullBehavior = "OMIT_NULLS" - PhysicalEmptyOnNull PhysicalNullBehavior = "EMPTY_ON_NULL" -) - -type PhysicalExpressionKind string - -const ( - PhysicalValueExpression PhysicalExpressionKind = "VALUE" - PhysicalExtractExpression PhysicalExpressionKind = "EXTRACT" - PhysicalAggregateExpression PhysicalExpressionKind = "AGGREGATE" - PhysicalPivotExpression PhysicalExpressionKind = "PIVOT_MAP" - PhysicalSliceExpression PhysicalExpressionKind = "SLICE" - PhysicalObjectExpression PhysicalExpressionKind = "OBJECT" -) - -// PhysicalSelectorExecutionMode records a schema-proven selector lowering. -// Generic is the safe fallback for predicates, fallbacks, choice paths, and -// unknown cardinality. -type PhysicalSelectorExecutionMode string - -const ( - PhysicalSelectorGeneric PhysicalSelectorExecutionMode = "GENERIC" - PhysicalSelectorDirectScalar PhysicalSelectorExecutionMode = "DIRECT_SCALAR" - PhysicalSelectorConditionalArray PhysicalSelectorExecutionMode = "CONDITIONAL_ARRAY" -) - -// PhysicalExpression is a closed, renderer-independent value tree. It carries -// no AQL source text: selector paths have already been parsed and all literals -// are represented by bind values. -type PhysicalExpression struct { - Kind PhysicalExpressionKind - Cardinality PhysicalCardinality - NullBehavior PhysicalNullBehavior - Value *PhysicalValue - Extract *PhysicalExtract - Aggregate *PhysicalAggregate - Pivot *PhysicalPivotMap - Slice *PhysicalSlice - Object *PhysicalObject -} - -// PhysicalExtract obtains one FHIR selector from a variable or prior set -// element. ResourceType keeps schema validation available after semantic -// lowering; fallbacks preserve the existing FIRST_NON_NULL behavior. -type PhysicalExtract struct { - Source PhysicalValue - ResourceType string - Selector Selector - Fallbacks []Selector - // Distinct preserves the explicit DISTINCT projection mode after semantic - // lowering. It is meaningful only for an array-valued expression. - Distinct bool - ExecutionMode PhysicalSelectorExecutionMode - // Prepared points at a selector value projected by a prepared child set. - // Source remains the owning set for scope validation and diagnostics. - Prepared *PhysicalPreparedReference -} - -// PhysicalPreparedReference identifies one selector column in a prepared set. -type PhysicalPreparedReference struct { - SetVariable string - Field string -} - -// PhysicalPreparedSet describes selector projections shared by rich -// consumers over one materialized relationship set. Preparation stays -// attached to the correlated set so root row grain cannot change. -type PhysicalPreparedSet struct { - Variable string - SourceSetVariable string - Fields []PhysicalPreparedField -} - -type PhysicalPreparedField struct { - Name string - ResourceType string - Selector Selector -} - -type PhysicalAggregateOperation string - -const ( - PhysicalCountAggregate PhysicalAggregateOperation = "COUNT" - PhysicalCountDistinctAggregate PhysicalAggregateOperation = "COUNT_DISTINCT" - PhysicalExistsAggregate PhysicalAggregateOperation = "EXISTS" - PhysicalDistinctValuesAggregate PhysicalAggregateOperation = "DISTINCT_VALUES" - PhysicalMinAggregate PhysicalAggregateOperation = "MIN" - PhysicalMaxAggregate PhysicalAggregateOperation = "MAX" - PhysicalFirstAggregate PhysicalAggregateOperation = "FIRST" -) - -type PhysicalAggregate struct { - Source PhysicalValue - Operation PhysicalAggregateOperation - Value *PhysicalExpression - Predicate *PhysicalPredicateExpression -} - -type PhysicalPivotMap struct { - Source PhysicalValue - ResourceType string - KeySelector Selector - ValueSelector Selector - ColumnsBindKey string - PreparedKey *PhysicalPreparedReference - PreparedValue *PhysicalPreparedReference -} - -// PhysicalSlice is a bounded, stable, nested projection over a prior set. -// Limit remains a bind variable so callers cannot synthesize query literals. -type PhysicalSlice struct { - Source PhysicalValue - Predicate *PhysicalPredicateExpression - Sort *PhysicalExpression - LimitBindKey string - Projections []PhysicalExpressionProjection -} - -type PhysicalExpressionProjection struct { - Name string - Expression PhysicalExpression -} - -type PhysicalObject struct { - Fields []PhysicalExpressionProjection -} - -// PhysicalSet is a correlated, array-valued subplan. Captures are declared -// explicitly so validation can prevent accidental references to parent or -// future variables. The set variable becomes visible only after its subplan -// has validated successfully. -type PhysicalSet struct { - Variable string - Subplan PhysicalSubplan - Unique bool - // Output describes a compact, identity-safe projection of each set item. - // A nil output preserves the full stored document for shared traversal or - // other consumers that have not proved a smaller contract. - Output *PhysicalSetOutput - // Projection describes selector values computed in the original child-set - // subquery. It replaces the payload-bearing second prepared array when all - // downstream consumers have a projection-safe selector contract. - Projection *PhysicalSetProjection - // SourceSetVariable is set for a typed subset over an already materialized - // shared traversal. Such a set does not begin with TRAVERSAL; ItemVariable - // is bound by the renderer while iterating SourceSetVariable. - SourceSetVariable string - ItemVariable string - // SortByKey makes the set's node order part of physical semantics. Optional - // relationship materialization must not rely on Arango traversal order. - SortByKey bool - Prepared *PhysicalPreparedSet -} - -// PhysicalSetProjection is a single-materialization selector projection. The -// fields are arrays because selector evaluation preserves repeated FHIR -// values; scalar consumers apply their normal FIRST/FLATTEN semantics when -// reading the projected field. -type PhysicalSetProjection struct { - Fields []PhysicalSetProjectionField -} - -type PhysicalSetProjectionField struct { - Name string - ResourceType string - Selector Selector - ExecutionMode PhysicalSelectorExecutionMode -} - -// PhysicalSetOutputField names the only stored properties that may survive a -// compact set projection. The graph identity fields preserve nested traversal -// and duplicate-edge semantics; payload is retained only when a downstream -// selector or rich consumer needs it. -type PhysicalSetOutputField string - -const ( - PhysicalSetGraphIDField PhysicalSetOutputField = "_id" - PhysicalSetKeyField PhysicalSetOutputField = "_key" - PhysicalSetIDField PhysicalSetOutputField = "id" - PhysicalSetResourceTypeField PhysicalSetOutputField = "resourceType" - PhysicalSetPayloadField PhysicalSetOutputField = "payload" -) - -type PhysicalSetOutput struct { - Fields []PhysicalSetOutputField -} - -type PhysicalSubplan struct { - Captures []string - Operations []PhysicalOperation - Return PhysicalExpression -} - -type PhysicalPredicate struct { - Operator string - Left PhysicalValue - // LeftExpression lets a comparison consume a typed selector extraction. - // Exactly one of Left and LeftExpression is present. This keeps user - // filters out of AQL-shaped strings while the scope predicates can retain - // their compact value-only form. - LeftExpression *PhysicalExpression - Right *PhysicalValue - Quantifier ArrayQuantifier - ValueKind FilterValueKind -} - -type PhysicalPredicateKind string - -const ( - PhysicalComparisonPredicate PhysicalPredicateKind = "COMPARISON" - PhysicalAllPredicate PhysicalPredicateKind = "ALL" - PhysicalAnyPredicate PhysicalPredicateKind = "ANY" - PhysicalNotPredicate PhysicalPredicateKind = "NOT" - PhysicalExistsPredicate PhysicalPredicateKind = "EXISTS" -) - -// PhysicalPredicateExpression is the typed predicate tree used by rich -// physical operations. Exists contains a bounded correlated subplan; it is -// not a string-shaped LENGTH(FOR ...) compatibility escape hatch. -type PhysicalPredicateExpression struct { - Kind PhysicalPredicateKind - Comparison *PhysicalPredicate - Children []PhysicalPredicateExpression - Exists *PhysicalSubplan -} - -type PhysicalFilter struct { - // Predicate is retained for the frozen navigation plan. New lowering must - // use Expression so compound and existence predicates stay typed. - Predicate PhysicalPredicate - Expression *PhysicalPredicateExpression -} - -// PhysicalDerivedLet names a derived value. Operator is a compiler-owned -// symbolic operation (for example UNIQUE or LENGTH), never raw AQL. -type PhysicalDerivedLet struct { - Variable string - Operator string - Inputs []PhysicalValue - Expression *PhysicalExpression -} - -// PhysicalSort is the deliberately small ordering primitive currently needed -// by generic root-grain previews. Additional sort keys and directions should -// be added only with a corresponding semantic ordering contract. -type PhysicalSort struct { - Value PhysicalValue -} - -// PhysicalLimit references a positive integer bind value. Keeping the value -// in BindVars retains the same parameterized execution boundary as filters. -type PhysicalLimit struct { - BindKey string -} - -type PhysicalProjection struct { - Name string - Value PhysicalValue - Expression *PhysicalExpression -} - -type PhysicalReturn struct { - Projections []PhysicalProjection -} - -var ( - physicalVariablePattern = regexp.MustCompile(`^[A-Za-z_][A-Za-z0-9_]*$`) - physicalBindKeyPattern = regexp.MustCompile(`^[A-Za-z][A-Za-z0-9_]*$`) - physicalPathPartPattern = regexp.MustCompile(`^[A-Za-z_][A-Za-z0-9_]*$`) -) - -// Validate enforces the frozen physical-plan invariants without rendering AQL. -func (p PhysicalPlan) Validate() error { - if p.Version <= 0 { - return fmt.Errorf("physical plan version must be positive") - } - for key := range p.BindVars { - if !physicalBindKeyPattern.MatchString(key) { - return fmt.Errorf("unsafe bind key %q", key) - } - } - defined := map[string]bool{} - rootScans := 0 - returns := 0 - for i, operation := range p.Operations { - if returns > 0 { - return fmt.Errorf("operation %d appears after RETURN", i) - } - if err := operation.validatePayload(); err != nil { - return fmt.Errorf("operation %d (%s): %w", i, operation.Kind, err) - } - switch operation.Kind { - case PhysicalRootScanOp: - rootScans++ - if rootScans > 1 { - return fmt.Errorf("operation %d: physical plan has multiple root scans", i) - } - if err := requireBind(p.BindVars, operation.RootScan.CollectionBindKey); err != nil { - return fmt.Errorf("operation %d: %w", i, err) - } - if err := requireCollectionBind(p.BindVars, operation.RootScan.CollectionBindKey); err != nil { - return fmt.Errorf("operation %d: %w", i, err) - } - if err := definePhysicalVariable(defined, operation.RootScan.Variable); err != nil { - return fmt.Errorf("operation %d: %w", i, err) - } - case PhysicalTraversalOp: - traversal := operation.Traversal - if !defined[traversal.SourceVariable] { - return fmt.Errorf("operation %d: traversal source variable %q is out of scope", i, traversal.SourceVariable) - } - if traversal.Direction != PhysicalOutbound && traversal.Direction != PhysicalInbound && traversal.Direction != PhysicalAny { - return fmt.Errorf("operation %d: invalid traversal direction %q", i, traversal.Direction) - } - if traversal.EdgeTargetTypeField != "" && !physicalPathPartPattern.MatchString(traversal.EdgeTargetTypeField) { - return fmt.Errorf("operation %d: unsafe traversal edge type field %q", i, traversal.EdgeTargetTypeField) - } - if err := validatePhysicalTraversalStrategy(*traversal); err != nil { - return fmt.Errorf("operation %d: %w", i, err) - } - for _, key := range []string{traversal.EdgeCollectionBindKey, traversal.EdgeLabelBindKey, traversal.TargetTypeBindKey} { - if key != "" { - if err := requireBind(p.BindVars, key); err != nil { - return fmt.Errorf("operation %d: %w", i, err) - } - } - } - if traversal.EdgeCollectionBindKey == "" { - return fmt.Errorf("operation %d: traversal edge collection bind key is required", i) - } - if err := requireCollectionBind(p.BindVars, traversal.EdgeCollectionBindKey); err != nil { - return fmt.Errorf("operation %d: %w", i, err) - } - if err := definePhysicalVariable(defined, traversal.TargetVariable); err != nil { - return fmt.Errorf("operation %d: %w", i, err) - } - if traversal.EdgeVariable != "" { - if err := definePhysicalVariable(defined, traversal.EdgeVariable); err != nil { - return fmt.Errorf("operation %d: %w", i, err) - } - } - case PhysicalFilterOp: - if err := validatePhysicalFilter(*operation.Filter, defined, p.BindVars); err != nil { - return fmt.Errorf("operation %d: %w", i, err) - } - case PhysicalDerivedLetOp: - derived := operation.DerivedLet - if err := validatePhysicalDerivedLet(*derived, defined, p.BindVars); err != nil { - return fmt.Errorf("operation %d: %w", i, err) - } - if err := definePhysicalVariable(defined, derived.Variable); err != nil { - return fmt.Errorf("operation %d: %w", i, err) - } - case PhysicalSetOp: - set := operation.Set - if err := validatePhysicalSet(*set, defined, p.BindVars); err != nil { - return fmt.Errorf("operation %d set %q: %w", i, set.Variable, err) - } - if err := definePhysicalVariable(defined, set.Variable); err != nil { - return fmt.Errorf("operation %d: %w", i, err) - } - if set.Prepared != nil { - if err := definePhysicalVariable(defined, set.Prepared.Variable); err != nil { - return fmt.Errorf("operation %d prepared set: %w", i, err) - } - } - case PhysicalSortOp: - if err := validatePhysicalValue(operation.Sort.Value, defined, p.BindVars); err != nil { - return fmt.Errorf("operation %d: %w", i, err) - } - case PhysicalLimitOp: - if err := requireBind(p.BindVars, operation.Limit.BindKey); err != nil { - return fmt.Errorf("operation %d: %w", i, err) - } - limit, ok := p.BindVars[operation.Limit.BindKey].(int) - if !ok || limit <= 0 { - return fmt.Errorf("operation %d: limit bind %q must be a positive int", i, operation.Limit.BindKey) - } - case PhysicalReturnOp: - returns++ - seenNames := map[string]bool{} - for _, projection := range operation.Return.Projections { - if strings.TrimSpace(projection.Name) == "" || seenNames[projection.Name] { - return fmt.Errorf("operation %d: return projection name %q is empty or duplicated", i, projection.Name) - } - seenNames[projection.Name] = true - if err := validatePhysicalProjection(projection, defined, p.BindVars); err != nil { - return fmt.Errorf("operation %d projection %q: %w", i, projection.Name, err) - } - } - } - } - if rootScans != 1 { - return fmt.Errorf("physical plan requires exactly one root scan") - } - if returns != 1 { - return fmt.Errorf("physical plan requires exactly one RETURN") - } - return nil -} - -func validatePhysicalTraversalStrategy(traversal PhysicalTraversal) error { - strategy := traversal.Strategy - if strategy == "" || strategy == PhysicalTraversalNative { - return nil - } - if strategy != PhysicalTraversalEndpointLookup { - return fmt.Errorf("unsupported traversal strategy %q", strategy) - } - if traversal.Direction != PhysicalInbound && traversal.Direction != PhysicalOutbound { - return fmt.Errorf("endpoint lookup requires INBOUND or OUTBOUND direction") - } - if !physicalPathPartPattern.MatchString(traversal.EndpointField) || !physicalPathPartPattern.MatchString(traversal.EndpointJoinField) { - return fmt.Errorf("endpoint lookup requires safe endpoint and join fields") - } - if len(traversal.EndpointIndexFields) == 0 { - return fmt.Errorf("endpoint lookup requires declared compound index fields") - } - for _, field := range traversal.EndpointIndexFields { - if !physicalPathPartPattern.MatchString(field) { - return fmt.Errorf("endpoint lookup has unsafe index field %q", field) - } - } - return nil -} - -func validatePhysicalSet(set PhysicalSet, parent map[string]bool, bindVars map[string]any) error { - if set.Projection != nil { - if len(set.Projection.Fields) == 0 { - return fmt.Errorf("set %q projection requires at least one field", set.Variable) - } - seenProjectionFields := map[string]bool{} - for _, field := range set.Projection.Fields { - if !physicalVariablePattern.MatchString(field.Name) || seenProjectionFields[field.Name] { - return fmt.Errorf("set %q projection field %q is unsafe or duplicated", set.Variable, field.Name) - } - seenProjectionFields[field.Name] = true - if strings.TrimSpace(field.ResourceType) == "" || !fhirschema.HasResource(field.ResourceType) { - return fmt.Errorf("set %q projection field %q has invalid resource type %q", set.Variable, field.Name, field.ResourceType) - } - if err := validatePhysicalSelector(field.ResourceType, field.Selector); err != nil { - return fmt.Errorf("set %q projection field %q selector: %w", set.Variable, field.Name, err) - } - } - } - if set.Output != nil { - if len(set.Output.Fields) == 0 { - return fmt.Errorf("set %q compact output requires at least one retained field", set.Variable) - } - seenOutputFields := map[PhysicalSetOutputField]bool{} - for _, field := range set.Output.Fields { - switch field { - case PhysicalSetGraphIDField, PhysicalSetKeyField, PhysicalSetIDField, PhysicalSetResourceTypeField, PhysicalSetPayloadField: - default: - return fmt.Errorf("set %q compact output field %q is unsupported", set.Variable, field) - } - if seenOutputFields[field] { - return fmt.Errorf("set %q compact output field %q is duplicated", set.Variable, field) - } - seenOutputFields[field] = true - } - if !seenOutputFields[PhysicalSetGraphIDField] || !seenOutputFields[PhysicalSetKeyField] { - return fmt.Errorf("set %q compact output must retain _id and _key", set.Variable) - } - } - if set.Prepared != nil { - prepared := set.Prepared - if !physicalVariablePattern.MatchString(prepared.Variable) || !physicalVariablePattern.MatchString(prepared.SourceSetVariable) { - return fmt.Errorf("prepared set variables must be safe") - } - if prepared.SourceSetVariable != set.Variable { - return fmt.Errorf("prepared set source %q must equal owning set %q", prepared.SourceSetVariable, set.Variable) - } - if len(prepared.Fields) == 0 { - return fmt.Errorf("prepared set %q requires at least one field", prepared.Variable) - } - seen := map[string]bool{} - for _, field := range prepared.Fields { - if !physicalVariablePattern.MatchString(field.Name) || seen[field.Name] { - return fmt.Errorf("prepared set field %q is unsafe or duplicated", field.Name) - } - seen[field.Name] = true - if strings.TrimSpace(field.ResourceType) == "" || !fhirschema.HasResource(field.ResourceType) { - return fmt.Errorf("prepared set field %q has invalid resource type %q", field.Name, field.ResourceType) - } - if err := validatePhysicalSelector(field.ResourceType, field.Selector); err != nil { - return fmt.Errorf("prepared set field %q selector: %w", field.Name, err) - } - } - } - if set.SourceSetVariable == "" { - return validatePhysicalSubplan(set.Subplan, parent, bindVars) - } - if !physicalVariablePattern.MatchString(set.ItemVariable) { - return fmt.Errorf("shared subset %q has unsafe item variable", set.ItemVariable) - } - if !parent[set.SourceSetVariable] { - return fmt.Errorf("shared subset source %q is out of scope", set.SourceSetVariable) - } - if len(set.Subplan.Captures) != 1 || set.Subplan.Captures[0] != set.SourceSetVariable { - return fmt.Errorf("shared subset %q must capture exactly its source set", set.Variable) - } - defined := map[string]bool{set.SourceSetVariable: true, set.ItemVariable: true} - for index, operation := range set.Subplan.Operations { - if operation.Kind != PhysicalFilterOp && operation.Kind != PhysicalDerivedLetOp { - return fmt.Errorf("shared subset operation %d has unsupported kind %q", index, operation.Kind) - } - if operation.Kind == PhysicalFilterOp { - if err := validatePhysicalFilter(*operation.Filter, defined, bindVars); err != nil { - return err - } - } else { - if err := validatePhysicalDerivedLet(*operation.DerivedLet, defined, bindVars); err != nil { - return err - } - if err := definePhysicalVariable(defined, operation.DerivedLet.Variable); err != nil { - return err - } - } - } - return validatePhysicalExpression(set.Subplan.Return, defined, bindVars) -} - -func (operation PhysicalOperation) validatePayload() error { - payloads := 0 - if operation.RootScan != nil { - payloads++ - } - if operation.Traversal != nil { - payloads++ - } - if operation.Filter != nil { - payloads++ - } - if operation.DerivedLet != nil { - payloads++ - } - if operation.Set != nil { - payloads++ - } - if operation.Sort != nil { - payloads++ - } - if operation.Limit != nil { - payloads++ - } - if operation.Return != nil { - payloads++ - } - if payloads != 1 { - return fmt.Errorf("operation must contain exactly one payload") - } - valid := (operation.Kind == PhysicalRootScanOp && operation.RootScan != nil) || - (operation.Kind == PhysicalTraversalOp && operation.Traversal != nil) || - (operation.Kind == PhysicalFilterOp && operation.Filter != nil) || - (operation.Kind == PhysicalDerivedLetOp && operation.DerivedLet != nil) || - (operation.Kind == PhysicalSetOp && operation.Set != nil) || - (operation.Kind == PhysicalSortOp && operation.Sort != nil) || - (operation.Kind == PhysicalLimitOp && operation.Limit != nil) || - (operation.Kind == PhysicalReturnOp && operation.Return != nil) - if !valid { - return fmt.Errorf("payload does not match operation kind") - } - return nil -} - -func definePhysicalVariable(defined map[string]bool, variable string) error { - if !physicalVariablePattern.MatchString(variable) { - return fmt.Errorf("unsafe variable name %q", variable) - } - if defined[variable] { - return fmt.Errorf("variable %q is already defined", variable) - } - defined[variable] = true - return nil -} - -func requireBind(bindVars map[string]any, key string) error { - if !physicalBindKeyPattern.MatchString(key) { - return fmt.Errorf("unsafe bind key %q", key) - } - if _, ok := bindVars[key]; !ok { - return fmt.Errorf("bind key %q is not defined", key) - } - return nil -} - -func requireCollectionBind(bindVars map[string]any, key string) error { - value, ok := bindVars[key] - if !ok { - return fmt.Errorf("bind key %q is not defined", key) - } - collection, ok := value.(string) - if !ok || strings.TrimSpace(collection) == "" { - return fmt.Errorf("collection bind key %q must have a non-empty string value", key) - } - return nil -} - -func validatePhysicalPredicate(predicate PhysicalPredicate, defined map[string]bool, bindVars map[string]any) error { - operator := strings.ToUpper(strings.TrimSpace(predicate.Operator)) - switch operator { - case "EQUALS", "NOT_EQUALS", "IN", "EXISTS", "MISSING", "CONTAINS_TEXT", "GT", "GTE", "LT", "LTE": - default: - return fmt.Errorf("unknown physical filter operator %q", predicate.Operator) - } - hasLeftValue := predicate.Left.Variable != "" || predicate.Left.BindKey != "" || len(predicate.Left.Path) != 0 - hasLeftExpression := predicate.LeftExpression != nil - if hasLeftValue == hasLeftExpression { - return fmt.Errorf("physical filter predicate requires exactly one left value or expression") - } - if hasLeftExpression { - if err := validatePhysicalExpression(*predicate.LeftExpression, defined, bindVars); err != nil { - return fmt.Errorf("physical filter predicate left expression: %w", err) - } - if predicate.LeftExpression.Cardinality != PhysicalArrayCardinality { - return fmt.Errorf("physical filter predicate left expression must be array-valued") - } - if !predicate.ValueKind.Valid() { - return fmt.Errorf("physical filter predicate value kind %q is invalid", predicate.ValueKind) - } - if predicate.Quantifier != "" && !predicate.Quantifier.Valid() { - return fmt.Errorf("physical filter predicate quantifier %q is invalid", predicate.Quantifier) - } - } else if err := validatePhysicalValue(predicate.Left, defined, bindVars); err != nil { - return err - } - requiresRight := operator != "EXISTS" && operator != "MISSING" - if requiresRight != (predicate.Right != nil) { - return fmt.Errorf("physical filter operator %s right value presence is invalid", operator) - } - if predicate.Right != nil { - if err := validatePhysicalValue(*predicate.Right, defined, bindVars); err != nil { - return err - } - } - return nil -} - -func validatePhysicalFilter(filter PhysicalFilter, defined map[string]bool, bindVars map[string]any) error { - legacy := strings.TrimSpace(filter.Predicate.Operator) != "" - rich := filter.Expression != nil - if legacy == rich { - return fmt.Errorf("filter requires exactly one legacy predicate or predicate expression") - } - if legacy { - return validatePhysicalPredicate(filter.Predicate, defined, bindVars) - } - return validatePhysicalPredicateExpression(*filter.Expression, defined, bindVars) -} - -func validatePhysicalDerivedLet(derived PhysicalDerivedLet, defined map[string]bool, bindVars map[string]any) error { - legacy := strings.TrimSpace(derived.Operator) != "" || len(derived.Inputs) != 0 - rich := derived.Expression != nil - if legacy == rich { - return fmt.Errorf("derived LET requires exactly one legacy operation or expression") - } - if rich { - return validatePhysicalExpression(*derived.Expression, defined, bindVars) - } - if strings.TrimSpace(derived.Operator) == "" { - return fmt.Errorf("derived LET operator is required") - } - for _, input := range derived.Inputs { - if err := validatePhysicalValue(input, defined, bindVars); err != nil { - return err - } - } - return nil -} - -func validatePhysicalProjection(projection PhysicalProjection, defined map[string]bool, bindVars map[string]any) error { - hasValue := projection.Value.Variable != "" || projection.Value.BindKey != "" || len(projection.Value.Path) != 0 - hasExpression := projection.Expression != nil - if hasValue == hasExpression { - return fmt.Errorf("projection requires exactly one value or expression") - } - if hasExpression { - return validatePhysicalExpression(*projection.Expression, defined, bindVars) - } - return validatePhysicalValue(projection.Value, defined, bindVars) -} - -func validatePhysicalExpression(expression PhysicalExpression, defined map[string]bool, bindVars map[string]any) error { - if err := validatePhysicalExpressionObjectCycles(expression); err != nil { - return err - } - if !expression.Cardinality.valid() { - return fmt.Errorf("expression has invalid cardinality %q", expression.Cardinality) - } - if !expression.NullBehavior.valid() { - return fmt.Errorf("expression has invalid null behavior %q", expression.NullBehavior) - } - payloads := 0 - if expression.Value != nil { - payloads++ - } - if expression.Extract != nil { - payloads++ - } - if expression.Aggregate != nil { - payloads++ - } - if expression.Pivot != nil { - payloads++ - } - if expression.Slice != nil { - payloads++ - } - if expression.Object != nil { - payloads++ - } - if payloads != 1 { - return fmt.Errorf("expression must contain exactly one payload") - } - switch expression.Kind { - case PhysicalValueExpression: - if expression.Value == nil { - return fmt.Errorf("expression payload does not match kind") - } - return validatePhysicalValue(*expression.Value, defined, bindVars) - case PhysicalExtractExpression: - if expression.Extract == nil { - return fmt.Errorf("expression payload does not match kind") - } - return validatePhysicalExtract(*expression.Extract, defined, bindVars) - case PhysicalAggregateExpression: - if expression.Aggregate == nil { - return fmt.Errorf("expression payload does not match kind") - } - return validatePhysicalAggregate(*expression.Aggregate, defined, bindVars) - case PhysicalPivotExpression: - if expression.Pivot == nil { - return fmt.Errorf("expression payload does not match kind") - } - return validatePhysicalPivot(*expression.Pivot, defined, bindVars) - case PhysicalSliceExpression: - if expression.Slice == nil { - return fmt.Errorf("expression payload does not match kind") - } - return validatePhysicalSlice(*expression.Slice, defined, bindVars) - case PhysicalObjectExpression: - if expression.Object == nil { - return fmt.Errorf("expression payload does not match kind") - } - return validatePhysicalObject(*expression.Object, defined, bindVars) - default: - return fmt.Errorf("unknown expression kind %q", expression.Kind) - } -} - -// validatePhysicalExpressionObjectCycles protects the recursive expression -// validator from a malformed in-memory plan containing a cycle of -// PhysicalObject pointers. JSON decoding cannot produce such a cycle, but -// plans are also assembled by compiler stages and tests, where pointers can -// be wired directly. The active/visited split permits shared (DAG) objects -// while rejecting only true recursion. -func validatePhysicalExpressionObjectCycles(expression PhysicalExpression) error { - active := map[*PhysicalObject]bool{} - visited := map[*PhysicalObject]bool{} - var visitExpression func(PhysicalExpression) error - var visitObject func(*PhysicalObject) error - var visitPredicate func(*PhysicalPredicateExpression) error - var visitSubplan func(PhysicalSubplan) error - visitExpression = func(current PhysicalExpression) error { - if current.Object != nil { - if err := visitObject(current.Object); err != nil { - return err - } - } - if current.Aggregate != nil && current.Aggregate.Value != nil { - if err := visitExpression(*current.Aggregate.Value); err != nil { - return err - } - } - if current.Aggregate != nil && current.Aggregate.Predicate != nil { - if err := visitPredicate(current.Aggregate.Predicate); err != nil { - return err - } - } - if current.Slice != nil { - if current.Slice.Predicate != nil { - if err := visitPredicate(current.Slice.Predicate); err != nil { - return err - } - } - if current.Slice.Sort != nil { - if err := visitExpression(*current.Slice.Sort); err != nil { - return err - } - } - for _, projection := range current.Slice.Projections { - if err := visitExpression(projection.Expression); err != nil { - return err - } - } - } - return nil - } - visitPredicate = func(predicate *PhysicalPredicateExpression) error { - if predicate == nil { - return nil - } - if predicate.Comparison != nil && predicate.Comparison.LeftExpression != nil { - if err := visitExpression(*predicate.Comparison.LeftExpression); err != nil { - return err - } - } - for index := range predicate.Children { - if err := visitPredicate(&predicate.Children[index]); err != nil { - return err - } - } - if predicate.Exists != nil { - return visitSubplan(*predicate.Exists) - } - return nil - } - visitSubplan = func(subplan PhysicalSubplan) error { - for _, operation := range subplan.Operations { - switch operation.Kind { - case PhysicalFilterOp: - if operation.Filter != nil && operation.Filter.Expression != nil { - if err := visitPredicate(operation.Filter.Expression); err != nil { - return err - } - } - case PhysicalDerivedLetOp: - if operation.DerivedLet != nil && operation.DerivedLet.Expression != nil { - if err := visitExpression(*operation.DerivedLet.Expression); err != nil { - return err - } - } - case PhysicalSetOp: - if operation.Set != nil { - if err := visitSubplan(operation.Set.Subplan); err != nil { - return err - } - } - } - } - return visitExpression(subplan.Return) - } - visitObject = func(object *PhysicalObject) error { - if active[object] { - return fmt.Errorf("physical object expression contains a recursive cycle") - } - if visited[object] { - return nil - } - active[object] = true - for _, field := range object.Fields { - if err := visitExpression(field.Expression); err != nil { - return err - } - } - delete(active, object) - visited[object] = true - return nil - } - return visitExpression(expression) -} - -func (cardinality PhysicalCardinality) valid() bool { - return cardinality == PhysicalScalarCardinality || cardinality == PhysicalArrayCardinality || cardinality == PhysicalObjectCardinality -} - -func (behavior PhysicalNullBehavior) valid() bool { - return behavior == PhysicalPreserveNull || behavior == PhysicalOmitNulls || behavior == PhysicalEmptyOnNull -} - -func validatePhysicalExtract(extract PhysicalExtract, defined map[string]bool, bindVars map[string]any) error { - if err := validatePhysicalValue(extract.Source, defined, bindVars); err != nil { - return err - } - if strings.TrimSpace(extract.ResourceType) == "" || !fhirschema.HasResource(extract.ResourceType) { - return fmt.Errorf("extract resource type %q is not represented by the active generated FHIR schema", extract.ResourceType) - } - if err := validatePhysicalSelector(extract.ResourceType, extract.Selector); err != nil { - return fmt.Errorf("extract selector: %w", err) - } - if extract.ExecutionMode != "" && extract.ExecutionMode != PhysicalSelectorGeneric && extract.ExecutionMode != PhysicalSelectorDirectScalar && extract.ExecutionMode != PhysicalSelectorConditionalArray { - return fmt.Errorf("unknown selector execution mode %q", extract.ExecutionMode) - } - if extract.ExecutionMode == PhysicalSelectorDirectScalar && (len(extract.Fallbacks) != 0 || extract.Selector.Filter != nil || !selectorHasNoArrays(extract.Selector)) { - return fmt.Errorf("direct scalar selector mode requires one fallback-free non-repeated selector") - } - if extract.ExecutionMode == PhysicalSelectorConditionalArray && (len(extract.Fallbacks) != 0 || extract.Selector.Filter != nil || !selectorHasIteratedArray(extract.Selector)) { - return fmt.Errorf("conditional array selector mode requires one fallback-free repeated selector") - } - for index, fallback := range extract.Fallbacks { - if err := validatePhysicalSelector(extract.ResourceType, fallback); err != nil { - return fmt.Errorf("extract fallback %d: %w", index, err) - } - } - if extract.Prepared != nil { - if err := validatePhysicalPreparedReference(*extract.Prepared, defined); err != nil { - return err - } - if len(extract.Fallbacks) != 0 { - return fmt.Errorf("prepared extract cannot use fallback selectors") - } - } - return nil -} - -func validatePhysicalPreparedReference(reference PhysicalPreparedReference, defined map[string]bool) error { - if !physicalVariablePattern.MatchString(reference.SetVariable) || !defined[reference.SetVariable] { - return fmt.Errorf("prepared set variable %q is out of scope", reference.SetVariable) - } - if !physicalVariablePattern.MatchString(reference.Field) { - return fmt.Errorf("prepared field %q is unsafe", reference.Field) - } - return nil -} - -func validatePhysicalSelector(resourceType string, selector Selector) error { - if len(selector.Steps) == 0 { - return fmt.Errorf("selector is required") - } - if _, _, err := spec.SelectorCardinality(resourceType, selector); err != nil { - return err - } - return nil -} - -func validatePhysicalAggregate(aggregate PhysicalAggregate, defined map[string]bool, bindVars map[string]any) error { - if err := validatePhysicalValue(aggregate.Source, defined, bindVars); err != nil { - return err - } - switch aggregate.Operation { - case PhysicalCountAggregate, PhysicalCountDistinctAggregate, PhysicalExistsAggregate, PhysicalDistinctValuesAggregate, PhysicalMinAggregate, PhysicalMaxAggregate, PhysicalFirstAggregate: - default: - return fmt.Errorf("unknown aggregate operation %q", aggregate.Operation) - } - needsValue := aggregate.Operation != PhysicalCountAggregate && aggregate.Operation != PhysicalExistsAggregate - if needsValue != (aggregate.Value != nil) { - return fmt.Errorf("aggregate operation %q value presence is invalid", aggregate.Operation) - } - if aggregate.Value != nil { - if err := validatePhysicalExpression(*aggregate.Value, defined, bindVars); err != nil { - return fmt.Errorf("aggregate value: %w", err) - } - } - if aggregate.Predicate != nil { - if err := validatePhysicalPredicateExpression(*aggregate.Predicate, defined, bindVars); err != nil { - return fmt.Errorf("aggregate predicate: %w", err) - } - } - return nil -} - -func validatePhysicalPivot(pivot PhysicalPivotMap, defined map[string]bool, bindVars map[string]any) error { - if err := validatePhysicalValue(pivot.Source, defined, bindVars); err != nil { - return err - } - if strings.TrimSpace(pivot.ResourceType) == "" || !fhirschema.HasResource(pivot.ResourceType) { - return fmt.Errorf("pivot resource type %q is not represented by the active generated FHIR schema", pivot.ResourceType) - } - if err := validatePhysicalSelector(pivot.ResourceType, pivot.KeySelector); err != nil { - return fmt.Errorf("pivot key selector: %w", err) - } - if err := validatePhysicalSelector(pivot.ResourceType, pivot.ValueSelector); err != nil { - return fmt.Errorf("pivot value selector: %w", err) - } - if err := requireBind(bindVars, pivot.ColumnsBindKey); err != nil { - return err - } - columns, ok := bindVars[pivot.ColumnsBindKey].([]string) - if !ok || len(columns) == 0 { - return fmt.Errorf("pivot columns bind %q must be a non-empty []string", pivot.ColumnsBindKey) - } - for _, column := range columns { - if strings.TrimSpace(column) == "" { - return fmt.Errorf("pivot columns bind %q contains an empty column", pivot.ColumnsBindKey) - } - } - if pivot.PreparedKey != nil { - if err := validatePhysicalPreparedReference(*pivot.PreparedKey, defined); err != nil { - return fmt.Errorf("prepared pivot key: %w", err) - } - } - if pivot.PreparedValue != nil { - if err := validatePhysicalPreparedReference(*pivot.PreparedValue, defined); err != nil { - return fmt.Errorf("prepared pivot value: %w", err) - } - } - return nil -} - -func validatePhysicalSlice(slice PhysicalSlice, defined map[string]bool, bindVars map[string]any) error { - if err := validatePhysicalValue(slice.Source, defined, bindVars); err != nil { - return err - } - if slice.Predicate != nil { - if err := validatePhysicalPredicateExpression(*slice.Predicate, defined, bindVars); err != nil { - return fmt.Errorf("slice predicate: %w", err) - } - } - if slice.Sort == nil { - return fmt.Errorf("slice requires a stable sort expression") - } - if err := validatePhysicalExpression(*slice.Sort, defined, bindVars); err != nil { - return fmt.Errorf("slice sort: %w", err) - } - if err := requireBind(bindVars, slice.LimitBindKey); err != nil { - return err - } - limit, ok := bindVars[slice.LimitBindKey].(int) - if !ok || limit <= 0 { - return fmt.Errorf("slice limit bind %q must be a positive int", slice.LimitBindKey) - } - return validatePhysicalExpressionProjections(slice.Projections, defined, bindVars, "slice") -} - -func validatePhysicalObject(object PhysicalObject, defined map[string]bool, bindVars map[string]any) error { - return validatePhysicalExpressionProjections(object.Fields, defined, bindVars, "object") -} - -func validatePhysicalExpressionProjections(projections []PhysicalExpressionProjection, defined map[string]bool, bindVars map[string]any, owner string) error { - if len(projections) == 0 { - return fmt.Errorf("%s requires at least one projection", owner) - } - seen := map[string]bool{} - for _, projection := range projections { - if strings.TrimSpace(projection.Name) == "" || seen[projection.Name] { - return fmt.Errorf("%s projection name %q is empty or duplicated", owner, projection.Name) - } - seen[projection.Name] = true - if err := validatePhysicalExpression(projection.Expression, defined, bindVars); err != nil { - return fmt.Errorf("%s projection %q: %w", owner, projection.Name, err) - } - } - return nil -} - -func validatePhysicalPredicateExpression(predicate PhysicalPredicateExpression, defined map[string]bool, bindVars map[string]any) error { - switch predicate.Kind { - case PhysicalComparisonPredicate: - if predicate.Comparison == nil || len(predicate.Children) != 0 || predicate.Exists != nil { - return fmt.Errorf("comparison predicate requires exactly one comparison") - } - return validatePhysicalPredicate(*predicate.Comparison, defined, bindVars) - case PhysicalAllPredicate, PhysicalAnyPredicate: - if predicate.Comparison != nil || predicate.Exists != nil || len(predicate.Children) == 0 { - return fmt.Errorf("%s predicate requires one or more child predicates", predicate.Kind) - } - for index, child := range predicate.Children { - if err := validatePhysicalPredicateExpression(child, defined, bindVars); err != nil { - return fmt.Errorf("predicate child %d: %w", index, err) - } - } - return nil - case PhysicalNotPredicate: - if predicate.Comparison != nil || predicate.Exists != nil || len(predicate.Children) != 1 { - return fmt.Errorf("NOT predicate requires exactly one child predicate") - } - return validatePhysicalPredicateExpression(predicate.Children[0], defined, bindVars) - case PhysicalExistsPredicate: - if predicate.Comparison != nil || len(predicate.Children) != 0 || predicate.Exists == nil { - return fmt.Errorf("EXISTS predicate requires exactly one subplan") - } - return validatePhysicalSubplan(*predicate.Exists, defined, bindVars) - default: - return fmt.Errorf("unknown predicate kind %q", predicate.Kind) - } -} - -func validatePhysicalSubplan(subplan PhysicalSubplan, parent map[string]bool, bindVars map[string]any) error { - if len(subplan.Captures) == 0 { - return fmt.Errorf("subplan requires at least one explicit capture") - } - defined := make(map[string]bool, len(subplan.Captures)) - for _, capture := range subplan.Captures { - if !parent[capture] { - return fmt.Errorf("subplan capture %q is out of scope", capture) - } - if err := definePhysicalVariable(defined, capture); err != nil { - return fmt.Errorf("subplan capture: %w", err) - } - } - if len(subplan.Operations) == 0 { - return fmt.Errorf("subplan requires at least one operation") - } - for index, operation := range subplan.Operations { - if operation.Kind == PhysicalRootScanOp || operation.Kind == PhysicalReturnOp || operation.Kind == PhysicalSortOp || operation.Kind == PhysicalLimitOp { - return fmt.Errorf("subplan operation %d cannot be %s", index, operation.Kind) - } - if err := operation.validatePayload(); err != nil { - return fmt.Errorf("subplan operation %d (%s): %w", index, operation.Kind, err) - } - switch operation.Kind { - case PhysicalTraversalOp: - traversal := operation.Traversal - if !defined[traversal.SourceVariable] { - return fmt.Errorf("subplan operation %d: traversal source variable %q is out of scope", index, traversal.SourceVariable) - } - if traversal.Direction != PhysicalOutbound && traversal.Direction != PhysicalInbound && traversal.Direction != PhysicalAny { - return fmt.Errorf("subplan operation %d: invalid traversal direction %q", index, traversal.Direction) - } - if traversal.EdgeTargetTypeField != "" && !physicalPathPartPattern.MatchString(traversal.EdgeTargetTypeField) { - return fmt.Errorf("subplan operation %d: unsafe traversal edge type field %q", index, traversal.EdgeTargetTypeField) - } - if err := validatePhysicalTraversalStrategy(*traversal); err != nil { - return fmt.Errorf("subplan operation %d: %w", index, err) - } - if err := requireCollectionBind(bindVars, traversal.EdgeCollectionBindKey); err != nil { - return fmt.Errorf("subplan operation %d: %w", index, err) - } - for _, key := range []string{traversal.EdgeLabelBindKey, traversal.TargetTypeBindKey} { - if key != "" { - if err := requireBind(bindVars, key); err != nil { - return fmt.Errorf("subplan operation %d: %w", index, err) - } - } - } - if err := definePhysicalVariable(defined, traversal.TargetVariable); err != nil { - return fmt.Errorf("subplan operation %d: %w", index, err) - } - if traversal.EdgeVariable != "" { - if err := definePhysicalVariable(defined, traversal.EdgeVariable); err != nil { - return fmt.Errorf("subplan operation %d: %w", index, err) - } - } - case PhysicalFilterOp: - if err := validatePhysicalFilter(*operation.Filter, defined, bindVars); err != nil { - return fmt.Errorf("subplan operation %d: %w", index, err) - } - case PhysicalDerivedLetOp: - if err := validatePhysicalDerivedLet(*operation.DerivedLet, defined, bindVars); err != nil { - return fmt.Errorf("subplan operation %d: %w", index, err) - } - if err := definePhysicalVariable(defined, operation.DerivedLet.Variable); err != nil { - return fmt.Errorf("subplan operation %d: %w", index, err) - } - case PhysicalSetOp: - if err := validatePhysicalSet(*operation.Set, defined, bindVars); err != nil { - return fmt.Errorf("subplan operation %d set %q: %w", index, operation.Set.Variable, err) - } - if err := definePhysicalVariable(defined, operation.Set.Variable); err != nil { - return fmt.Errorf("subplan operation %d: %w", index, err) - } - default: - return fmt.Errorf("subplan operation %d has unsupported kind %q", index, operation.Kind) - } - } - return validatePhysicalExpression(subplan.Return, defined, bindVars) -} - -func validatePhysicalValue(value PhysicalValue, defined map[string]bool, bindVars map[string]any) error { - hasVariable := value.Variable != "" - hasBind := value.BindKey != "" - if hasVariable == hasBind { - return fmt.Errorf("physical value must reference exactly one variable or bind key") - } - if hasBind { - if len(value.Path) != 0 { - return fmt.Errorf("bind value %q cannot have a path", value.BindKey) - } - return requireBind(bindVars, value.BindKey) - } - if !defined[value.Variable] { - return fmt.Errorf("variable %q is out of scope", value.Variable) - } - for _, part := range value.Path { - if !physicalPathPartPattern.MatchString(part) { - return fmt.Errorf("unsafe path segment %q", part) - } - } - return nil -} diff --git a/internal/dataframe/compiler/ir/policy.go b/internal/dataframe/compiler/ir/policy.go index e2f711b..3867270 100644 --- a/internal/dataframe/compiler/ir/policy.go +++ b/internal/dataframe/compiler/ir/policy.go @@ -39,6 +39,7 @@ const ( PhysicalOptimizationRuleRichConsumerFusion PhysicalOptimizationRule = "rich_consumer_fusion" PhysicalOptimizationRuleCompactProjection PhysicalOptimizationRule = "compact_set_projection" PhysicalOptimizationRuleEndpointTraversal PhysicalOptimizationRule = "endpoint_traversal" + PhysicalOptimizationRuleKeyedMapSharing PhysicalOptimizationRule = "keyed_map_sharing" ) var allPhysicalOptimizationRules = []PhysicalOptimizationRule{ @@ -48,6 +49,7 @@ var allPhysicalOptimizationRules = []PhysicalOptimizationRule{ PhysicalOptimizationRuleRichConsumerFusion, PhysicalOptimizationRuleCompactProjection, PhysicalOptimizationRuleEndpointTraversal, + PhysicalOptimizationRuleKeyedMapSharing, } // RuleEnabled resolves one rule without mutating the caller's policy. A @@ -62,7 +64,7 @@ func (policy PhysicalOptimizationPolicy) RuleEnabled(rule PhysicalOptimizationRu } } switch rule { - case PhysicalOptimizationRuleTraversalSharing, PhysicalOptimizationRuleCompactProjection, PhysicalOptimizationRuleEndpointTraversal: + case PhysicalOptimizationRuleTraversalSharing, PhysicalOptimizationRuleCompactProjection, PhysicalOptimizationRuleEndpointTraversal, PhysicalOptimizationRuleKeyedMapSharing: return true default: return false @@ -151,6 +153,7 @@ func DefaultPhysicalOptimizationPolicy() PhysicalOptimizationPolicy { {name: "LOOM_PHYSICAL_RULE_RICH_FUSION", rule: PhysicalOptimizationRuleRichConsumerFusion}, {name: "LOOM_PHYSICAL_RULE_COMPACT_PROJECTION", rule: PhysicalOptimizationRuleCompactProjection}, {name: "LOOM_PHYSICAL_RULE_ENDPOINT_TRAVERSAL", rule: PhysicalOptimizationRuleEndpointTraversal}, + {name: "LOOM_PHYSICAL_RULE_KEYED_MAP_SHARING", rule: PhysicalOptimizationRuleKeyedMapSharing}, } { switch strings.ToLower(strings.TrimSpace(os.Getenv(setting.name))) { case "on", "1", "true", "enabled": diff --git a/internal/dataframe/compiler/ir/prefix.go b/internal/dataframe/compiler/ir/prefix.go index c2575ca..3ec9e79 100644 --- a/internal/dataframe/compiler/ir/prefix.go +++ b/internal/dataframe/compiler/ir/prefix.go @@ -25,8 +25,14 @@ type PhysicalTraversalPrefix struct { AuthUnrestrictedBindKey string ScopeAllowedBindKey string ScopeOperationCount int - NodeVariable string - EdgeVariable string + // UnnestScopeIdentity identifies the active cardinality-changing scope at + // the point where this set is materialized. It is intentionally derived + // from canonical PhysicalUnnest operations rather than renderer variable + // names. Prefixes on opposite sides of an unnest barrier must never share, + // even when they read the same root variable. + UnnestScopeIdentity string + NodeVariable string + EdgeVariable string } const ( @@ -64,6 +70,7 @@ const ( PhysicalPrefixUnsupportedDirection PhysicalTraversalPrefixRejectionReason = "UNSUPPORTED_DIRECTION" PhysicalPrefixInvalidRoute PhysicalTraversalPrefixRejectionReason = "INVALID_ROUTE" PhysicalPrefixInvalidScope PhysicalTraversalPrefixRejectionReason = "INVALID_SCOPE" + PhysicalPrefixUnnestBarrier PhysicalTraversalPrefixRejectionReason = "UNNEST_SCOPE_BARRIER" PhysicalPrefixInvalidTarget PhysicalTraversalPrefixRejectionReason = "INVALID_TARGET_SUBSET" ) @@ -91,6 +98,18 @@ func rejectPhysicalTraversalPrefix(reason PhysicalTraversalPrefixRejectionReason // rejects shared subsets, required EXISTS paths, non-proven directions, and // sets whose tenant scope is not the exact generic edge/node scope block. func DecomposePhysicalTraversalPrefix(plan PhysicalPlan, set PhysicalSet) (PhysicalTraversalPrefixDecomposition, error) { + return DecomposePhysicalTraversalPrefixAt(plan, set, physicalSetOperationIndex(plan, set.Variable)) +} + +// DecomposePhysicalTraversalPrefixAt is the position-aware form used by the +// optimizer and diagnostics. The operation index is required because an +// unnest changes row cardinality for all operations that follow it while the +// source variable may remain the same root binding. +func DecomposePhysicalTraversalPrefixAt(plan PhysicalPlan, set PhysicalSet, setIndex int) (PhysicalTraversalPrefixDecomposition, error) { + return decomposePhysicalTraversalPrefix(plan, set, setIndex) +} + +func decomposePhysicalTraversalPrefix(plan PhysicalPlan, set PhysicalSet, setIndex int) (PhysicalTraversalPrefixDecomposition, error) { if set.SourceSetVariable != "" { return PhysicalTraversalPrefixDecomposition{}, rejectPhysicalTraversalPrefix(PhysicalPrefixSharedSubset, "set %q reads %q", set.Variable, set.SourceSetVariable) } @@ -135,6 +154,10 @@ func DecomposePhysicalTraversalPrefix(plan PhysicalPlan, set PhysicalSet) (Physi if _, ok := plan.BindVars[traversal.TargetTypeBindKey].(string); !ok { return PhysicalTraversalPrefixDecomposition{}, rejectPhysicalTraversalPrefix(PhysicalPrefixInvalidTarget, "target type bind %q is not a string", traversal.TargetTypeBindKey) } + unnestScope, err := physicalUnnestScopeIdentityAt(plan, setIndex) + if err != nil { + return PhysicalTraversalPrefixDecomposition{}, rejectPhysicalTraversalPrefix(PhysicalPrefixUnnestBarrier, "%v", err) + } prefix := PhysicalTraversalPrefix{ SourceVariable: traversal.SourceVariable, @@ -148,6 +171,7 @@ func DecomposePhysicalTraversalPrefix(plan PhysicalPlan, set PhysicalSet) (Physi AuthUnrestrictedBindKey: physicalScopeAuthPathsUnrestrictedBind, ScopeAllowedBindKey: physicalScopeAllowedBind, ScopeOperationCount: len(scope), + UnnestScopeIdentity: unnestScope, NodeVariable: physicalTraversalPrefixNodeVariable, EdgeVariable: physicalTraversalPrefixEdgeVariable, } @@ -167,6 +191,62 @@ func DecomposePhysicalTraversalPrefix(plan PhysicalPlan, set PhysicalSet) (Physi }, nil } +func physicalSetOperationIndex(plan PhysicalPlan, variable string) int { + for index, operation := range plan.Operations { + if operation.Kind == PhysicalSetOp && operation.Set != nil && operation.Set.Variable == variable { + return index + } + } + return -1 +} + +// physicalUnnestScopeIdentityAt returns a deterministic identity for every +// active top-level unnest before operationIndex. A set after an unnest still +// reads the original root binding, so source-variable equality alone is not a +// sufficient sharing key. The expression and join mode are part of the key so +// a rewrite cannot cross a different cardinality/null-preservation contract. +func physicalUnnestScopeIdentityAt(plan PhysicalPlan, operationIndex int) (string, error) { + if operationIndex < 0 { + return "", nil + } + if operationIndex > len(plan.Operations) { + return "", fmt.Errorf("operation index %d is outside plan", operationIndex) + } + type unnestScope struct { + InputVariable string + OutputVariable string + Ordinality string + Expression PhysicalExpression + JoinMode PhysicalUnnestJoinMode + } + active := make([]unnestScope, 0) + for index := 0; index < operationIndex; index++ { + operation := plan.Operations[index] + if operation.Kind != PhysicalUnnestOp { + continue + } + if operation.Unnest == nil { + return "", fmt.Errorf("unnest operation %d has no payload", index) + } + unnest := operation.Unnest + active = append(active, unnestScope{ + InputVariable: unnest.InputVariable, + OutputVariable: unnest.OutputVariable, + Ordinality: unnest.Ordinality, + Expression: unnest.Expression, + JoinMode: unnest.JoinMode, + }) + } + if len(active) == 0 { + return "", nil + } + encoded, err := json.Marshal(active) + if err != nil { + return "", fmt.Errorf("encode active unnest scope: %w", err) + } + return string(encoded), nil +} + func physicalTraversalPrefixKey(plan PhysicalPlan, prefix PhysicalTraversalPrefix) (string, error) { bind := func(key string) (string, error) { value, found := plan.BindVars[key] @@ -210,8 +290,9 @@ func physicalTraversalPrefixKey(plan PhysicalPlan, prefix PhysicalTraversalPrefi key := struct { Source, Direction, Collection, Label, TargetField string Project, Generation, Paths, Unrestricted, Allowed string + UnnestScopeIdentity string ScopeOperationCount int - }{prefix.SourceVariable, string(prefix.Direction), collection, label, prefix.EdgeTargetTypeField, project, generation, paths, unrestricted, allowed, prefix.ScopeOperationCount} + }{prefix.SourceVariable, string(prefix.Direction), collection, label, prefix.EdgeTargetTypeField, project, generation, paths, unrestricted, allowed, prefix.UnnestScopeIdentity, prefix.ScopeOperationCount} encoded, err := json.Marshal(key) if err != nil { return "", fmt.Errorf("encode physical traversal prefix key: %w", err) diff --git a/internal/dataframe/compiler/ir/unnest_test.go b/internal/dataframe/compiler/ir/unnest_test.go new file mode 100644 index 0000000..30012cf --- /dev/null +++ b/internal/dataframe/compiler/ir/unnest_test.go @@ -0,0 +1,105 @@ +package ir + +import "testing" + +func unnestValueExpression(variable string) PhysicalExpression { + return PhysicalExpression{ + Kind: PhysicalValueExpression, + Cardinality: PhysicalArrayCardinality, + NullBehavior: PhysicalEmptyOnNull, + Value: &PhysicalValue{Variable: variable, Path: []string{"payload"}}, + } +} + +func unnestPlan(mode PhysicalUnnestJoinMode) PhysicalPlan { + return PhysicalPlan{ + Version: 1, + BindVars: map[string]any{ + "collection": "Patient", + }, + Operations: []PhysicalOperation{ + {Kind: PhysicalRootScanOp, RootScan: &PhysicalRootScan{Variable: "root", CollectionBindKey: "collection"}}, + {Kind: PhysicalUnnestOp, Unnest: &PhysicalUnnest{ + InputVariable: "root", OutputVariable: "item", Ordinality: "item_index", + Expression: unnestValueExpression("root"), JoinMode: mode, + }}, + {Kind: PhysicalReturnOp, Return: &PhysicalReturn{Projections: []PhysicalProjection{{ + Name: "item", Expression: &PhysicalExpression{ + Kind: PhysicalValueExpression, Cardinality: PhysicalScalarCardinality, + NullBehavior: PhysicalPreserveNull, Value: &PhysicalValue{Variable: "item"}, + }, + }}}}, + }, + } +} + +func TestPhysicalUnnestValidatesInnerAndOuterPlans(t *testing.T) { + for _, mode := range []PhysicalUnnestJoinMode{PhysicalUnnestInner, PhysicalUnnestOuter} { + plan := unnestPlan(mode) + if err := plan.Validate(); err != nil { + t.Fatalf("mode %s: %v", mode, err) + } + } +} + +func TestPhysicalUnnestRejectsInvalidScopeAndCardinality(t *testing.T) { + tests := []struct { + name string + edit func(*PhysicalPlan) + want string + }{ + {"source out of scope", func(plan *PhysicalPlan) { plan.Operations[1].Unnest.InputVariable = "future" }, "out of scope"}, + {"scalar source", func(plan *PhysicalPlan) { + plan.Operations[1].Unnest.Expression.Cardinality = PhysicalScalarCardinality + }, "array-valued"}, + {"shadowed output", func(plan *PhysicalPlan) { plan.Operations[1].Unnest.OutputVariable = "root" }, "shadows"}, + {"unsafe ordinality", func(plan *PhysicalPlan) { plan.Operations[1].Unnest.Ordinality = "item.index" }, "unsafe"}, + {"unknown mode", func(plan *PhysicalPlan) { plan.Operations[1].Unnest.JoinMode = "CROSS" }, "unsupported"}, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + plan := unnestPlan(PhysicalUnnestInner) + test.edit(&plan) + if err := plan.Validate(); err == nil || !contains(err.Error(), test.want) { + t.Fatalf("Validate() = %v, want error containing %q", err, test.want) + } + }) + } +} + +func TestPhysicalUnnestCanBeNestedInSubplan(t *testing.T) { + plan := unnestPlan(PhysicalUnnestInner) + plan.Operations[1] = PhysicalOperation{Kind: PhysicalSetOp, Set: &PhysicalSet{ + Variable: "items", + Subplan: PhysicalSubplan{ + Captures: []string{"root"}, + Operations: []PhysicalOperation{{Kind: PhysicalUnnestOp, Unnest: &PhysicalUnnest{ + InputVariable: "root", OutputVariable: "item", Expression: unnestValueExpression("root"), JoinMode: PhysicalUnnestInner, + }}}, + Return: PhysicalExpression{Kind: PhysicalValueExpression, Cardinality: PhysicalObjectCardinality, + NullBehavior: PhysicalPreserveNull, Value: &PhysicalValue{Variable: "item"}}, + }, + }} + plan.Operations[2].Return.Projections[0].Expression.Value.Variable = "items" + if err := plan.Validate(); err != nil { + t.Fatal(err) + } +} + +func TestClonePhysicalUnnestClonesExpression(t *testing.T) { + plan := unnestPlan(PhysicalUnnestOuter) + copy := ClonePhysicalPlan(plan) + copy.Operations[1].Unnest.Expression.Value.Path[0] = "changed" + if got := plan.Operations[1].Unnest.Expression.Value.Path[0]; got != "payload" { + t.Fatalf("clone mutated original unnest expression path: %q", got) + } +} + +func contains(value, want string) bool { + for i := 0; i+len(want) <= len(value); i++ { + if value[i:i+len(want)] == want { + return true + } + } + return false +} diff --git a/internal/dataframe/compiler/ir/value.go b/internal/dataframe/compiler/ir/value.go deleted file mode 100644 index b477937..0000000 --- a/internal/dataframe/compiler/ir/value.go +++ /dev/null @@ -1,7 +0,0 @@ -package ir - -import "fmt" - -func valueString(value any) string { - return fmt.Sprint(value) -} diff --git a/internal/dataframe/compiler/ir_helpers.go b/internal/dataframe/compiler/ir_helpers.go index 4d0f33b..f61ba9b 100644 --- a/internal/dataframe/compiler/ir_helpers.go +++ b/internal/dataframe/compiler/ir_helpers.go @@ -2,9 +2,6 @@ package compiler import "github.com/calypr/loom/internal/dataframe/compiler/ir" -func datasetGenerationBindValue(generation string) any { - return ir.DatasetGenerationBindValue(generation) -} func normalizeDatasetGeneration(generation string) string { return ir.NormalizeDatasetGeneration(generation) } @@ -14,15 +11,3 @@ func physicalScopeWindowEnd(operations []PhysicalOperation, start int) int { func physicalPlanDiagnostics(plan PhysicalPlan) CompilerPlanDiagnostics { return ir.PhysicalPlanDiagnostics(plan) } -func newPhysicalOptimizationReport(policy PhysicalOptimizationPolicy) PhysicalOptimizationReport { - return ir.NewPhysicalOptimizationReport(policy) -} -func clonePhysicalOptimizationReport(report PhysicalOptimizationReport) PhysicalOptimizationReport { - return ir.ClonePhysicalOptimizationReport(report) -} -func estimateTraversalSharingWork(prefix PhysicalTraversalPrefixDecomposition, candidateSets int) (int, int, int) { - return ir.EstimateTraversalSharingWork(prefix, candidateSets) -} -func estimatePreparedSelectorWork(selectorUseCount int) (int, int, int) { - return ir.EstimatePreparedSelectorWork(selectorUseCount) -} diff --git a/internal/dataframe/compiler/lower/aliases.go b/internal/dataframe/compiler/lower/aliases.go index 830af80..a10de57 100644 --- a/internal/dataframe/compiler/lower/aliases.go +++ b/internal/dataframe/compiler/lower/aliases.go @@ -14,8 +14,6 @@ type ( SemanticAggregate = semantic.SemanticAggregate SemanticSlice = semantic.SemanticSlice SelectionSemanticSpec = semantic.SelectionSemanticSpec - Builder = spec.Builder - FieldSelect = spec.FieldSelect TypedFilter = spec.TypedFilter Selector = spec.Selector FilterValue = spec.FilterValue @@ -37,6 +35,7 @@ type ( PhysicalPreparedReference = ir.PhysicalPreparedReference PhysicalAggregate = ir.PhysicalAggregate PhysicalPivotMap = ir.PhysicalPivotMap + PhysicalKeySet = ir.PhysicalKeySet PhysicalSlice = ir.PhysicalSlice PhysicalExpressionProjection = ir.PhysicalExpressionProjection PhysicalObject = ir.PhysicalObject @@ -50,6 +49,10 @@ type ( PhysicalPredicateExpression = ir.PhysicalPredicateExpression PhysicalFilter = ir.PhysicalFilter PhysicalDerivedLet = ir.PhysicalDerivedLet + PhysicalExpressionLet = ir.PhysicalExpressionLet + PhysicalObjectLookup = ir.PhysicalObjectLookup + PhysicalKeyedMap = ir.PhysicalKeyedMap + PhysicalObjectKeys = ir.PhysicalObjectKeys PhysicalProjection = ir.PhysicalProjection PhysicalReturn = ir.PhysicalReturn PhysicalPredicateKind = ir.PhysicalPredicateKind @@ -63,6 +66,7 @@ const ( PhysicalTraversalOp = ir.PhysicalTraversalOp PhysicalFilterOp = ir.PhysicalFilterOp PhysicalDerivedLetOp = ir.PhysicalDerivedLetOp + PhysicalExpressionLetOp = ir.PhysicalExpressionLetOp PhysicalSetOp = ir.PhysicalSetOp PhysicalScalarCardinality = ir.PhysicalScalarCardinality PhysicalArrayCardinality = ir.PhysicalArrayCardinality @@ -76,6 +80,10 @@ const ( PhysicalPivotExpression = ir.PhysicalPivotExpression PhysicalSliceExpression = ir.PhysicalSliceExpression PhysicalObjectExpression = ir.PhysicalObjectExpression + PhysicalKeySetExpression = ir.PhysicalKeySetExpression + PhysicalObjectLookupExpression = ir.PhysicalObjectLookupExpression + PhysicalKeyedMapExpression = ir.PhysicalKeyedMapExpression + PhysicalObjectKeysExpression = ir.PhysicalObjectKeysExpression PhysicalSelectorGeneric = ir.PhysicalSelectorGeneric PhysicalSelectorDirectScalar = ir.PhysicalSelectorDirectScalar PhysicalSelectorConditionalArray = ir.PhysicalSelectorConditionalArray diff --git a/internal/dataframe/compiler/lower/auth_scope.go b/internal/dataframe/compiler/lower/auth_scope.go index 6d67582..33b1861 100644 --- a/internal/dataframe/compiler/lower/auth_scope.go +++ b/internal/dataframe/compiler/lower/auth_scope.go @@ -2,9 +2,7 @@ package lower import "github.com/calypr/loom/internal/authscope" -// effectiveAuthScopeUnrestricted preserves the legacy direct-Builder contract -// while making resolved request scopes authoritative. In particular, a -// resolver-provided restricted empty path set returns false. +// effectiveAuthScopeUnrestricted makes resolved request scopes authoritative. func effectiveAuthScopeUnrestricted(paths []string, mode authscope.ReadScopeMode) bool { switch mode { case authscope.ReadScopeUnrestricted: @@ -19,10 +17,6 @@ func effectiveAuthScopeUnrestricted(paths []string, mode authscope.ReadScopeMode } } -func builderAuthScopeUnrestricted(builder Builder) bool { - return effectiveAuthScopeUnrestricted(builder.AuthResourcePaths, builder.AuthScopeMode) -} - func semanticAuthScopeUnrestricted(plan SemanticPlan) bool { return effectiveAuthScopeUnrestricted(plan.AuthResourcePaths, plan.AuthScopeMode) } diff --git a/internal/dataframe/compiler/lower/child_sets.go b/internal/dataframe/compiler/lower/child_sets.go new file mode 100644 index 0000000..20842c0 --- /dev/null +++ b/internal/dataframe/compiler/lower/child_sets.go @@ -0,0 +1,450 @@ +package lower + +import ( + "fmt" + "sort" + "strings" +) + +func buildOptionalChildPhysicalSet(physical *PhysicalPlan, setIndex int, parent SemanticNode, parentVariable string, child SemanticNode, projectionPrefix string, policy PhysicalOptimizationPolicy) (PhysicalSet, []PhysicalProjection, error) { + prefix := fmt.Sprintf("child_set_%d", setIndex) + targetVariable := fmt.Sprintf("%s_node", prefix) + edgeVariable := fmt.Sprintf("%s_edge", prefix) + traversal, err := BuildPhysicalTraversal(TraversalLoweringRequest{ + FromType: parent.ResourceType, EdgeLabel: child.EdgeLabel, ToType: child.ResourceType, + SourceVariable: parentVariable, TargetVariable: targetVariable, EdgeVariable: edgeVariable, + BindPrefix: prefix, Policy: policy, + }) + if err != nil { + return PhysicalSet{}, nil, err + } + for key, value := range traversal.BindVars { + physical.BindVars[key] = value + } + source := PhysicalSource{SemanticNode: child.Alias, ResourceType: child.ResourceType, Relationship: child.EdgeLabel} + subplan := PhysicalSubplan{Captures: []string{parentVariable}, Operations: []PhysicalOperation{{ + Kind: PhysicalTraversalOp, Source: source, + Traversal: &traversal.Traversal, + }}} + subplan.Operations = appendProjectScope(subplan.Operations, []string{edgeVariable, targetVariable}, child.EdgeLabel, child) + subplan.Operations = appendDatasetGenerationScope(subplan.Operations, []string{edgeVariable, targetVariable}, child.EdgeLabel, child) + subplan.Operations = appendAuthScope(subplan.Operations, []PhysicalValue{{Variable: edgeVariable, Path: []string{"auth_resource_path"}}, {Variable: targetVariable, Path: []string{"auth_resource_path"}}}, prefix+"_scope_allowed", child) + for index, filter := range child.Filters { + if err := ValidateTypedFilterForResource(child.ResourceType, filter); err != nil { + return PhysicalSet{}, nil, fmt.Errorf("child filter %q: %w", filter.FieldRef, err) + } + selector, err := ParseSelector(filter.Selector) + if err != nil { + return PhysicalSet{}, nil, fmt.Errorf("child filter %q selector: %w", filter.FieldRef, err) + } + predicate := PhysicalPredicate{Operator: string(filter.Operator), Quantifier: filter.Quantifier, ValueKind: filter.FieldKind, LeftExpression: &PhysicalExpression{Kind: PhysicalExtractExpression, Cardinality: PhysicalArrayCardinality, NullBehavior: PhysicalEmptyOnNull, Extract: &PhysicalExtract{Source: PhysicalValue{Variable: targetVariable, Path: []string{"payload"}}, ResourceType: child.ResourceType, Selector: selector, ExecutionMode: selectorExecutionMode(child.ResourceType, selector)}}} + if filter.Operator != FilterExists && filter.Operator != FilterMissing { + key := fmt.Sprintf("%s_filter_%d_value", prefix, index+1) + if filter.Operator == FilterIn { + values := make([]any, 0, len(filter.Values)) + for _, value := range filter.Values { + literal, err := filterLiteral(value) + if err != nil { + return PhysicalSet{}, nil, err + } + values = append(values, literal) + } + physical.BindVars[key] = values + } else { + if len(filter.Values) == 0 { + return PhysicalSet{}, nil, fmt.Errorf("child filter %q has no value", filter.FieldRef) + } + literal, err := filterLiteral(filter.Values[0]) + if err != nil { + return PhysicalSet{}, nil, err + } + physical.BindVars[key] = literal + } + predicate.Right = &PhysicalValue{BindKey: key} + } + subplan.Operations = append(subplan.Operations, PhysicalOperation{Kind: PhysicalFilterOp, Source: PhysicalSource{SemanticNode: child.Alias, ResourceType: child.ResourceType, SemanticField: filter.FieldRef}, Filter: &PhysicalFilter{Expression: &PhysicalPredicateExpression{Kind: PhysicalComparisonPredicate, Comparison: &predicate}}}) + } + subplan.Return = PhysicalExpression{Kind: PhysicalValueExpression, Cardinality: PhysicalObjectCardinality, NullBehavior: PhysicalPreserveNull, Value: &PhysicalValue{Variable: targetVariable}} + var output *PhysicalSetOutput + if policy.RuleEnabled(PhysicalOptimizationRuleCompactProjection) { + output = compactPhysicalSetOutput(child) + } + set := PhysicalSet{Variable: prefix, Subplan: subplan, Unique: true, SortByKey: true, Output: output} + projections := make([]PhysicalProjection, 0, len(child.Fields)) + for index, field := range child.Fields { + selection, err := ResolveSemanticField(child.ResourceType, child.Alias, index, field) + if err != nil { + return PhysicalSet{}, nil, err + } + cardinality, distinct := PhysicalScalarCardinality, false + nullBehavior := PhysicalPreserveNull + switch selection.Projection { + case ProjectionArray: + cardinality, nullBehavior = PhysicalArrayCardinality, PhysicalEmptyOnNull + case ProjectionDistinctArray: + cardinality, distinct, nullBehavior = PhysicalArrayCardinality, true, PhysicalEmptyOnNull + case ProjectionScalar, ProjectionFirst: + default: + return PhysicalSet{}, nil, fmt.Errorf("child field %q has unsupported projection %q", field.Name, selection.Projection) + } + projections = append(projections, PhysicalProjection{Name: projectionPrefix + "__" + field.Name, Expression: &PhysicalExpression{Kind: PhysicalExtractExpression, Cardinality: cardinality, NullBehavior: nullBehavior, Extract: &PhysicalExtract{Source: PhysicalValue{Variable: set.Variable}, ResourceType: child.ResourceType, Selector: selection.Selector, Fallbacks: append([]Selector(nil), selection.Fallbacks...), Distinct: distinct, ExecutionMode: selectorExecutionMode(child.ResourceType, selection.Selector, selection.Fallbacks...)}}}) + } + for _, aggregate := range child.Aggregates { + expression, err := physicalAggregateExpression(physical, child.ResourceType, PhysicalValue{Variable: set.Variable}, aggregate, true) + if err != nil { + return PhysicalSet{}, nil, err + } + projections = append(projections, PhysicalProjection{Name: projectionPrefix + "__" + aggregate.Name, Expression: &expression}) + } + for _, pivot := range child.Pivots { + pivotProjections, err := physicalPivotProjections(physical, child.ResourceType, PhysicalValue{Variable: set.Variable}, pivot, true, projectionPrefix+"__") + if err != nil { + return PhysicalSet{}, nil, err + } + projections = append(projections, pivotProjections...) + } + for _, slice := range child.Slices { + expression, err := physicalSliceExpression(physical, child.ResourceType, PhysicalValue{Variable: set.Variable}, slice, true) + if err != nil { + return PhysicalSet{}, nil, err + } + projections = append(projections, PhysicalProjection{Name: projectionPrefix + "__" + slice.Name, Expression: &expression}) + } + // A traversal-time projection is the production form of selector reuse. It + // computes selector arrays in the original child subquery and removes the + // payload before materialization. The older prepared-set pass is retained + // only for explicit experiments and is never layered on top of this shape. + if policy.RuleEnabled(PhysicalOptimizationRuleCompactProjection) { + // Dynamic maps and deferred pivot families both read the original + // resource payload at the outer return boundary. A selector-only + // projection would discard that payload and leave their lookup/pivot + // evaluation iterating over `child_set_N.payload` (null). Keep the + // compact identity/payload contract for those sets; ordinary selector + // consumers still use the cheaper single-materialization projection. + // + // A pivot's public columns are OBJECT_LOOKUP projections over its + // deferred map, so projectPhysicalChildSet cannot discover its key/value + // selectors from the return projection list. Do not drop payload until a + // correlated pivot-specific projection contract exists. + if len(child.DynamicMaps) == 0 && len(child.Pivots) == 0 { + projectPhysicalChildSet(&set, child.ResourceType, projections) + } else { + set.Output = compactPhysicalSetOutput(child) + } + } + if set.Projection == nil { + prepareRichChildSet(&set, child.ResourceType, projections, policy) + } + return set, projections, nil +} + +// compactPhysicalSetOutput retains graph identity and only the payload needed +// by downstream selectors/rich consumers. Scope predicates run before this +// projection, so project, generation, and authorization metadata do not need +// to remain in the post-window set. Nested traversal still receives _id, and +// UNIQUE/SORT retain both identity keys to preserve duplicate and ordering +// semantics. +func compactPhysicalSetOutput(node SemanticNode) *PhysicalSetOutput { + fields := []PhysicalSetOutputField{ + PhysicalSetGraphIDField, + PhysicalSetKeyField, + PhysicalSetIDField, + PhysicalSetResourceTypeField, + } + needsPayload := len(node.Fields) > 0 || len(node.Pivots) > 0 || len(node.Slices) > 0 || len(node.DynamicMaps) > 0 + if !needsPayload { + for _, aggregate := range node.Aggregates { + if aggregate.Selector != nil || aggregate.Predicate != nil { + needsPayload = true + break + } + } + } + if needsPayload { + fields = append(fields, PhysicalSetPayloadField) + } + return &PhysicalSetOutput{Fields: fields} +} + +// prepareRichChildSet adds a generic selector cache when at least two rich +// consumers read the same child relationship set. The cache is deliberately +// selector-based (not FHIR-resource-specific): any generated resource type +// with repeated aggregate, pivot, or slice selectors can use it. +func prepareRichChildSet(set *PhysicalSet, resourceType string, projections []PhysicalProjection, policy PhysicalOptimizationPolicy) { + if !policy.RuleEnabled(PhysicalOptimizationRulePreparedSelectors) { + return + } + counts := map[string]int{} + selectors := map[string]Selector{} + add := func(selector Selector) { + key := physicalSelectorIdentity(selector) + counts[key]++ + selectors[key] = selector + } + var collect func(*PhysicalExpression) + collect = func(expression *PhysicalExpression) { + if expression == nil { + return + } + switch expression.Kind { + case PhysicalAggregateExpression: + if expression.Aggregate != nil { + if expression.Aggregate.Value != nil { + collect(expression.Aggregate.Value) + } + if expression.Aggregate.Predicate != nil && expression.Aggregate.Predicate.Comparison != nil { + collect(expression.Aggregate.Predicate.Comparison.LeftExpression) + } + } + case PhysicalPivotExpression: + if expression.Pivot != nil { + add(expression.Pivot.KeySelector) + add(expression.Pivot.ValueSelector) + } + case PhysicalSliceExpression: + if expression.Slice != nil { + if expression.Slice.Predicate != nil && expression.Slice.Predicate.Comparison != nil { + collect(expression.Slice.Predicate.Comparison.LeftExpression) + } + for index := range expression.Slice.Projections { + collect(&expression.Slice.Projections[index].Expression) + } + } + case PhysicalExtractExpression: + if expression.Extract != nil { + add(expression.Extract.Selector) + } + case PhysicalObjectExpression: + if expression.Object != nil { + for index := range expression.Object.Fields { + collect(&expression.Object.Fields[index].Expression) + } + } + } + } + for index := range projections { + collect(projections[index].Expression) + } + eligible := map[string]string{} + fields := make([]PhysicalPreparedField, 0) + // Map iteration order must not influence the physical plan. Stable field + // allocation keeps generated AQL and bind-variable names deterministic, + // which is important for explain fixtures and cache keys. + keys := make([]string, 0, len(counts)) + for key := range counts { + keys = append(keys, key) + } + sort.Strings(keys) + for _, key := range keys { + count := counts[key] + _, _, savings := estimatePreparedSelectorWork(count) + if savings <= 0 { + continue + } + field := fmt.Sprintf("__loom_prepared_%d", len(fields)) + eligible[key] = field + fields = append(fields, PhysicalPreparedField{Name: field, ResourceType: resourceType, Selector: selectors[key]}) + } + if len(fields) == 0 { + return + } + set.Prepared = &PhysicalPreparedSet{Variable: set.Variable + "_prepared", SourceSetVariable: set.Variable, Fields: fields} + var annotate func(*PhysicalExpression) + annotate = func(expression *PhysicalExpression) { + if expression == nil { + return + } + annotateExtract := func(extract *PhysicalExtract) { + if extract == nil || extract.Source.Variable != set.Variable { + return + } + // Fallback selectors are an ordered fallback chain. A prepared + // primary value alone cannot preserve that contract, so leave these + // extracts on the original renderer path until the prepared schema + // can represent fallback alternatives explicitly. + if len(extract.Fallbacks) == 0 { + if field, ok := eligible[physicalSelectorIdentity(extract.Selector)]; ok { + extract.Prepared = &PhysicalPreparedReference{SetVariable: set.Prepared.Variable, Field: field} + } + } + } + switch expression.Kind { + case PhysicalAggregateExpression: + if expression.Aggregate != nil { + if expression.Aggregate.Value != nil { + annotate(expression.Aggregate.Value) + } + if expression.Aggregate.Predicate != nil && expression.Aggregate.Predicate.Comparison != nil { + annotate(expression.Aggregate.Predicate.Comparison.LeftExpression) + } + } + case PhysicalPivotExpression: + if expression.Pivot != nil { + if field, ok := eligible[physicalSelectorIdentity(expression.Pivot.KeySelector)]; ok { + expression.Pivot.PreparedKey = &PhysicalPreparedReference{SetVariable: set.Prepared.Variable, Field: field} + } + if field, ok := eligible[physicalSelectorIdentity(expression.Pivot.ValueSelector)]; ok { + expression.Pivot.PreparedValue = &PhysicalPreparedReference{SetVariable: set.Prepared.Variable, Field: field} + } + } + case PhysicalSliceExpression: + if expression.Slice != nil { + if expression.Slice.Predicate != nil && expression.Slice.Predicate.Comparison != nil { + annotate(expression.Slice.Predicate.Comparison.LeftExpression) + } + for index := range expression.Slice.Projections { + annotate(&expression.Slice.Projections[index].Expression) + } + } + case PhysicalExtractExpression: + annotateExtract(expression.Extract) + case PhysicalObjectExpression: + if expression.Object != nil { + for index := range expression.Object.Fields { + annotate(&expression.Object.Fields[index].Expression) + } + } + } + } + for index := range projections { + annotate(projections[index].Expression) + } +} + +// projectPhysicalChildSet attaches selector values to the original set item +// rather than creating a second prepared array. A fallback selector is an +// ordered multi-source contract that cannot be represented by one projected +// field, so the entire set conservatively retains its existing payload output. +func projectPhysicalChildSet(set *PhysicalSet, resourceType string, projections []PhysicalProjection) { + if set == nil || len(projections) == 0 { + return + } + selectors := map[string]Selector{} + fallback := false + var collect func(*PhysicalExpression) + collect = func(expression *PhysicalExpression) { + if expression == nil { + return + } + switch expression.Kind { + case PhysicalExtractExpression: + if expression.Extract == nil || expression.Extract.Source.Variable != set.Variable { + return + } + if len(expression.Extract.Fallbacks) != 0 { + fallback = true + return + } + selectors[physicalSelectorIdentity(expression.Extract.Selector)] = expression.Extract.Selector + case PhysicalAggregateExpression: + if expression.Aggregate != nil { + collect(expression.Aggregate.Value) + if expression.Aggregate.Predicate != nil && expression.Aggregate.Predicate.Comparison != nil { + collect(expression.Aggregate.Predicate.Comparison.LeftExpression) + } + } + case PhysicalPivotExpression: + if expression.Pivot != nil && expression.Pivot.Source.Variable == set.Variable { + selectors[physicalSelectorIdentity(expression.Pivot.KeySelector)] = expression.Pivot.KeySelector + selectors[physicalSelectorIdentity(expression.Pivot.ValueSelector)] = expression.Pivot.ValueSelector + } + case PhysicalSliceExpression: + if expression.Slice != nil { + if expression.Slice.Predicate != nil && expression.Slice.Predicate.Comparison != nil { + collect(expression.Slice.Predicate.Comparison.LeftExpression) + } + for index := range expression.Slice.Projections { + collect(&expression.Slice.Projections[index].Expression) + } + } + case PhysicalObjectExpression: + if expression.Object != nil { + for index := range expression.Object.Fields { + collect(&expression.Object.Fields[index].Expression) + } + } + } + } + for index := range projections { + collect(projections[index].Expression) + } + if fallback || len(selectors) == 0 { + return + } + keys := make([]string, 0, len(selectors)) + for key := range selectors { + keys = append(keys, key) + } + sort.Strings(keys) + fields := make([]PhysicalSetProjectionField, 0, len(keys)) + fieldBySelector := make(map[string]string, len(keys)) + for index, key := range keys { + name := fmt.Sprintf("__loom_projection_%d", index) + fields = append(fields, PhysicalSetProjectionField{Name: name, ResourceType: resourceType, Selector: selectors[key], ExecutionMode: selectorExecutionMode(resourceType, selectors[key])}) + fieldBySelector[key] = name + } + var rewrite func(*PhysicalExpression) + rewrite = func(expression *PhysicalExpression) { + if expression == nil { + return + } + switch expression.Kind { + case PhysicalExtractExpression: + if expression.Extract != nil && expression.Extract.Source.Variable == set.Variable && len(expression.Extract.Fallbacks) == 0 { + if field := fieldBySelector[physicalSelectorIdentity(expression.Extract.Selector)]; field != "" { + expression.Extract.Prepared = &PhysicalPreparedReference{SetVariable: set.Variable, Field: field} + } + } + case PhysicalAggregateExpression: + if expression.Aggregate != nil { + rewrite(expression.Aggregate.Value) + if expression.Aggregate.Predicate != nil && expression.Aggregate.Predicate.Comparison != nil { + rewrite(expression.Aggregate.Predicate.Comparison.LeftExpression) + } + } + case PhysicalPivotExpression: + if expression.Pivot != nil && expression.Pivot.Source.Variable == set.Variable { + expression.Pivot.PreparedKey = &PhysicalPreparedReference{SetVariable: set.Variable, Field: fieldBySelector[physicalSelectorIdentity(expression.Pivot.KeySelector)]} + expression.Pivot.PreparedValue = &PhysicalPreparedReference{SetVariable: set.Variable, Field: fieldBySelector[physicalSelectorIdentity(expression.Pivot.ValueSelector)]} + } + case PhysicalSliceExpression: + if expression.Slice != nil { + if expression.Slice.Predicate != nil && expression.Slice.Predicate.Comparison != nil { + rewrite(expression.Slice.Predicate.Comparison.LeftExpression) + } + for index := range expression.Slice.Projections { + rewrite(&expression.Slice.Projections[index].Expression) + } + } + case PhysicalObjectExpression: + if expression.Object != nil { + for index := range expression.Object.Fields { + rewrite(&expression.Object.Fields[index].Expression) + } + } + } + } + for index := range projections { + rewrite(projections[index].Expression) + } + set.Projection = &PhysicalSetProjection{Fields: fields} + set.Output = nil +} + +func physicalSelectorIdentity(selector Selector) string { + var b strings.Builder + for _, step := range selector.Steps { + b.WriteString(step.Field) + if step.Iterate { + b.WriteString("[]") + } + if step.Index != nil { + fmt.Fprintf(&b, "[%d]", *step.Index) + } + b.WriteByte('.') + } + if selector.Filter != nil { + b.WriteString("?" + selector.Filter.Field + "=" + selector.Filter.Needle) + } + return b.String() +} diff --git a/internal/dataframe/compiler/lower/generic_plan.go b/internal/dataframe/compiler/lower/generic_plan.go deleted file mode 100644 index b3ff76b..0000000 --- a/internal/dataframe/compiler/lower/generic_plan.go +++ /dev/null @@ -1,882 +0,0 @@ -package lower - -import ( - "fmt" - "sort" - "strings" - - "github.com/calypr/loom/fhirschema" -) - -// BuildGenericPhysicalPlan lowers generic navigation plus root and optional -// child selections into the typed physical IR. -func BuildGenericPhysicalPlan(semantic SemanticPlan) (PhysicalPlan, error) { - return BuildGenericPhysicalPlanWithPolicy(semantic, DefaultPhysicalOptimizationPolicy()) -} - -// BuildGenericPhysicalPlanWithPolicy threads an explicit optimizer policy -// through physical construction so prepared-selector ablations happen before -// references to prepared variables are attached to projections. -func BuildGenericPhysicalPlanWithPolicy(semantic SemanticPlan, policy PhysicalOptimizationPolicy) (PhysicalPlan, error) { - if strings.TrimSpace(semantic.Project) == "" { - return PhysicalPlan{}, fmt.Errorf("semantic plan project is required") - } - if err := ValidateSemanticGraph(semantic); err != nil { - return PhysicalPlan{}, err - } - if !fhirschema.ResourceExists(semantic.Root.ResourceType) { - return PhysicalPlan{}, fmt.Errorf("root resource type %q is not represented by the generated FHIR schema", semantic.Root.ResourceType) - } - if err := validateGenericPhysicalNode(semantic.Root, true); err != nil { - return PhysicalPlan{}, err - } - - physical := PhysicalPlan{ - Version: 1, - Source: PhysicalSource{ - SemanticNode: semantic.Root.Alias, - ResourceType: semantic.Root.ResourceType, - }, - BindVars: map[string]any{ - "root_collection": semantic.Root.ResourceType, - "project": semantic.Project, - datasetGenerationBindKey: datasetGenerationBindValue(semantic.DatasetGeneration), - "auth_resource_paths": append([]string(nil), semantic.AuthResourcePaths...), - "auth_resource_paths_unrestricted": semanticAuthScopeUnrestricted(semantic), - "scope_allowed": true, - }, - Operations: []PhysicalOperation{ - { - Kind: PhysicalRootScanOp, - Source: PhysicalSource{SemanticNode: semantic.Root.Alias, ResourceType: semantic.Root.ResourceType}, - RootScan: &PhysicalRootScan{Variable: "root", CollectionBindKey: "root_collection"}, - }, - }, - } - physical.Operations = appendProjectScope(physical.Operations, []string{"root"}, "", semantic.Root) - physical.Operations = appendDatasetGenerationScope(physical.Operations, []string{"root"}, "", semantic.Root) - physical.Operations = appendAuthScope(physical.Operations, []PhysicalValue{{Variable: "root", Path: []string{"auth_resource_path"}}}, "root_scope_allowed", semantic.Root) - if err := appendRootPhysicalFilters(&physical, semantic.Root); err != nil { - return PhysicalPlan{}, err - } - if err := appendRequiredTraversalMatchFilters(&physical, semantic.Root); err != nil { - return PhysicalPlan{}, err - } - - childSetIndex := 0 - returnProjections := []PhysicalProjection{} - var walk func(parent SemanticNode, parentVariable, projectionPrefix string) error - walk = func(parent SemanticNode, parentVariable, projectionPrefix string) error { - for _, child := range parent.Children { - if child.MatchMode.Required() { - // Required routes are represented by the root semi-join emitted - // above for membership, but they may still need a materialized - // child set for selected fields or nested shaping. The second - // physical set is post-window output work; it cannot change root - // membership because the semi-join remains before SORT/LIMIT. - if physicalNodeNeedsMaterializedSet(child) { - childSetIndex++ - childProjectionPrefix := child.Alias - if projectionPrefix != "" { - childProjectionPrefix = projectionPrefix + "__" + child.Alias - } - set, projections, err := buildOptionalChildPhysicalSet(&physical, childSetIndex, parent, parentVariable, child, childProjectionPrefix, policy) - if err != nil { - return err - } - physical.Operations = append(physical.Operations, PhysicalOperation{Kind: PhysicalSetOp, Source: PhysicalSource{SemanticNode: child.Alias, ResourceType: child.ResourceType, Relationship: child.EdgeLabel}, Set: &set}) - returnProjections = append(returnProjections, projections...) - if err := walk(child, set.Variable, childProjectionPrefix); err != nil { - return err - } - } - continue - } - if !physicalNodeNeedsMaterializedSet(child) { - route, err := resolveStorageRoute(parent.ResourceType, child.EdgeLabel, child.ResourceType) - if err != nil { - return err - } - traversalIndex := 1 - for _, operation := range physical.Operations { - if operation.Kind == PhysicalTraversalOp { - traversalIndex++ - } - } - nodeVariable := fmt.Sprintf("node_%d", traversalIndex) - edgeVariable := fmt.Sprintf("edge_%d", traversalIndex) - labelBind := fmt.Sprintf("traversal_%d_label", traversalIndex) - typeBind := fmt.Sprintf("traversal_%d_target_type", traversalIndex) - edgeCollectionBind := fmt.Sprintf("traversal_%d_edge_collection", traversalIndex) - physical.BindVars[labelBind] = child.EdgeLabel - physical.BindVars[typeBind] = child.ResourceType - physical.BindVars[edgeCollectionBind] = "fhir_edge" - strategy, endpointField, endpointJoinField, endpointIndexFields := physicalTraversalStrategyForRoute(policy, parentVariable, route) - physical.Operations = append(physical.Operations, PhysicalOperation{Kind: PhysicalTraversalOp, - Source: PhysicalSource{SemanticNode: child.Alias, ResourceType: child.ResourceType, Relationship: child.EdgeLabel}, - Traversal: &PhysicalTraversal{SourceVariable: parentVariable, TargetVariable: nodeVariable, EdgeVariable: edgeVariable, Direction: route.Direction, - EdgeCollectionBindKey: edgeCollectionBind, EdgeLabelBindKey: labelBind, TargetTypeBindKey: typeBind, EdgeTargetTypeField: route.targetEdgeTypeField(), Strategy: strategy, EndpointField: endpointField, EndpointJoinField: endpointJoinField, EndpointIndexFields: endpointIndexFields}}) - physical.Operations = appendProjectScope(physical.Operations, []string{edgeVariable, nodeVariable}, child.EdgeLabel, child) - physical.Operations = appendDatasetGenerationScope(physical.Operations, []string{edgeVariable, nodeVariable}, child.EdgeLabel, child) - physical.Operations = appendAuthScope(physical.Operations, []PhysicalValue{{Variable: edgeVariable, Path: []string{"auth_resource_path"}}, {Variable: nodeVariable, Path: []string{"auth_resource_path"}}}, fmt.Sprintf("traversal_%d_scope_allowed", traversalIndex), child) - if err := walk(child, nodeVariable, projectionPrefix); err != nil { - return err - } - continue - } - // Optional children are correlated sets. Keeping them in a LET - // subquery preserves the parent row grain while allowing typed child - // filters and projections to be applied before materialization. - childSetIndex++ - childProjectionPrefix := child.Alias - if projectionPrefix != "" { - childProjectionPrefix = projectionPrefix + "__" + child.Alias - } - set, projections, err := buildOptionalChildPhysicalSet(&physical, childSetIndex, parent, parentVariable, child, childProjectionPrefix, policy) - if err != nil { - return err - } - physical.Operations = append(physical.Operations, PhysicalOperation{Kind: PhysicalSetOp, Source: PhysicalSource{SemanticNode: child.Alias, ResourceType: child.ResourceType, Relationship: child.EdgeLabel}, Set: &set}) - returnProjections = append(returnProjections, projections...) - if err := walk(child, set.Variable, childProjectionPrefix); err != nil { - return err - } - } - return nil - } - if err := walk(semantic.Root, "root", ""); err != nil { - return PhysicalPlan{}, err - } - projections, err := rootPhysicalProjections(&physical, semantic.Root) - if err != nil { - return PhysicalPlan{}, err - } - projections = append(projections, returnProjections...) - physical.Operations = append(physical.Operations, PhysicalOperation{ - Kind: PhysicalReturnOp, - Source: PhysicalSource{SemanticNode: semantic.Root.Alias, ResourceType: semantic.Root.ResourceType, SemanticField: "_key"}, - Return: &PhysicalReturn{Projections: projections}, - }) - if err := physical.Validate(); err != nil { - return PhysicalPlan{}, fmt.Errorf("validate generic physical plan: %w", err) - } - if err := ValidateGenericPhysicalPlanScope(physical); err != nil { - return PhysicalPlan{}, fmt.Errorf("verify generic physical plan scope: %w", err) - } - return physical, nil -} - -// physicalNodeNeedsMaterializedSet reports whether a node or any optional -// descendant has shaped output. Materializing an otherwise unselected parent -// is necessary to give nested sets a stable correlated source variable. -func physicalNodeNeedsMaterializedSet(node SemanticNode) bool { - if len(node.Fields) != 0 || len(node.Filters) != 0 || len(node.Pivots) != 0 || len(node.Aggregates) != 0 || len(node.Slices) != 0 { - return true - } - for _, child := range node.Children { - if child.MatchMode.Required() || physicalNodeNeedsMaterializedSet(child) { - return true - } - } - return false -} - -func appendProjectScope(operations []PhysicalOperation, variables []string, relationship string, node SemanticNode) []PhysicalOperation { - right := PhysicalValue{BindKey: "project"} - for _, variable := range variables { - operations = append(operations, PhysicalOperation{ - Kind: PhysicalFilterOp, - Source: PhysicalSource{SemanticNode: node.Alias, ResourceType: node.ResourceType, Relationship: relationship, SemanticField: "project"}, - Filter: &PhysicalFilter{Predicate: PhysicalPredicate{ - Operator: "EQUALS", - Left: PhysicalValue{Variable: variable, Path: []string{"project"}}, - Right: &right, - }}, - }) - } - return operations -} - -// appendDatasetGenerationScope applies the same exact generation bind to -// every physical document participating in a scan/traversal. With a nil bind -// value this renders `dataset_generation == null`, deliberately isolating -// legacy documents from later generation-qualified loads. -func appendDatasetGenerationScope(operations []PhysicalOperation, variables []string, relationship string, node SemanticNode) []PhysicalOperation { - right := PhysicalValue{BindKey: datasetGenerationBindKey} - for _, variable := range variables { - operations = append(operations, PhysicalOperation{ - Kind: PhysicalFilterOp, - Source: PhysicalSource{SemanticNode: node.Alias, ResourceType: node.ResourceType, Relationship: relationship, SemanticField: datasetGenerationField}, - Filter: &PhysicalFilter{Predicate: PhysicalPredicate{ - Operator: "EQUALS", - Left: PhysicalValue{Variable: variable, Path: []string{datasetGenerationField}}, - Right: &right, - }}, - }) - } - return operations -} - -func appendAuthScope(operations []PhysicalOperation, scopedValues []PhysicalValue, resultVariable string, node SemanticNode) []PhysicalOperation { - inputs := append([]PhysicalValue(nil), scopedValues...) - inputs = append(inputs, PhysicalValue{BindKey: "auth_resource_paths"}, PhysicalValue{BindKey: "auth_resource_paths_unrestricted"}) - operations = append(operations, PhysicalOperation{ - Kind: PhysicalDerivedLetOp, - Source: PhysicalSource{SemanticNode: node.Alias, ResourceType: node.ResourceType, Relationship: node.EdgeLabel, SemanticField: "auth_resource_path"}, - DerivedLet: &PhysicalDerivedLet{Variable: resultVariable, Operator: "AUTH_RESOURCE_PATH_ALLOWED", Inputs: inputs}, - }) - right := PhysicalValue{BindKey: "scope_allowed"} - return append(operations, PhysicalOperation{ - Kind: PhysicalFilterOp, - Source: PhysicalSource{SemanticNode: node.Alias, ResourceType: node.ResourceType, Relationship: node.EdgeLabel, SemanticField: "auth_resource_path"}, - Filter: &PhysicalFilter{Predicate: PhysicalPredicate{Operator: "EQUALS", Left: PhysicalValue{Variable: resultVariable}, Right: &right}}, - }) -} - -func validateGenericPhysicalNode(node SemanticNode, root bool) error { - for _, child := range node.Children { - if err := validateGenericPhysicalNode(child, false); err != nil { - return err - } - } - return nil -} - -func rootPhysicalProjections(physical *PhysicalPlan, root SemanticNode) ([]PhysicalProjection, error) { - projections := []PhysicalProjection{{Name: "_key", Value: PhysicalValue{Variable: "root", Path: []string{"_key"}}}} - for index, field := range root.Fields { - selection, err := ResolveSemanticField(root.ResourceType, root.Alias, index, field) - if err != nil { - return nil, err - } - cardinality, distinct := PhysicalScalarCardinality, false - switch selection.Projection { - case ProjectionArray: - cardinality = PhysicalArrayCardinality - case ProjectionDistinctArray: - cardinality, distinct = PhysicalArrayCardinality, true - case ProjectionScalar, ProjectionFirst: - default: - return nil, fmt.Errorf("root field %q has unsupported projection %q", field.Name, selection.Projection) - } - expression := PhysicalExpression{ - Kind: PhysicalExtractExpression, Cardinality: cardinality, NullBehavior: PhysicalPreserveNull, - Extract: &PhysicalExtract{Source: PhysicalValue{Variable: "root", Path: []string{"payload"}}, ResourceType: root.ResourceType, Selector: field.Selector, Fallbacks: append([]Selector(nil), field.Fallbacks...), Distinct: distinct, ExecutionMode: selectorExecutionMode(root.ResourceType, field.Selector, field.Fallbacks...)}, - } - if cardinality == PhysicalArrayCardinality { - expression.NullBehavior = PhysicalEmptyOnNull - } - projections = append(projections, PhysicalProjection{Name: field.Name, Expression: &expression}) - } - for _, aggregate := range root.Aggregates { - expression, err := physicalAggregateExpression(physical, root.ResourceType, PhysicalValue{Variable: "root"}, aggregate, false) - if err != nil { - return nil, err - } - projections = append(projections, PhysicalProjection{Name: aggregate.Name, Expression: &expression}) - } - for _, pivot := range root.Pivots { - expression, err := physicalPivotExpression(physical, root.ResourceType, PhysicalValue{Variable: "root"}, pivot, false) - if err != nil { - return nil, err - } - projections = append(projections, PhysicalProjection{Name: pivot.Name, Expression: &expression}) - } - for _, slice := range root.Slices { - expression, err := physicalSliceExpression(physical, root.ResourceType, PhysicalValue{Variable: "root"}, slice, false) - if err != nil { - return nil, err - } - projections = append(projections, PhysicalProjection{Name: slice.Name, Expression: &expression}) - } - return projections, nil -} - -func physicalPivotExpression(physical *PhysicalPlan, resourceType string, source PhysicalValue, pivot SemanticPivot, sourceIsSet bool) (PhysicalExpression, error) { - if len(pivot.Columns) == 0 { - return PhysicalExpression{}, fmt.Errorf("pivot %q requires bounded columns", pivot.Name) - } - // Pivot sources are resource documents (root or correlated child set), not - // payload values. The renderer applies selectors against each item's - // payload, preserving the same semantics for singleton and set sources. - key := "pivot_" + sanitizeColumnName(source.Variable) + "_" + sanitizeColumnName(pivot.Name) + "_columns" - physical.BindVars[key] = append([]string(nil), pivot.Columns...) - return PhysicalExpression{ - Kind: PhysicalPivotExpression, Cardinality: PhysicalObjectCardinality, NullBehavior: PhysicalPreserveNull, - Pivot: &PhysicalPivotMap{Source: source, ResourceType: resourceType, KeySelector: pivot.ColumnSelector, ValueSelector: pivot.ValueSelector, ColumnsBindKey: key}, - }, nil -} - -// physicalAggregateExpression lowers one semantic aggregate into the typed -// aggregate IR. A root source is a singleton document; child sources are -// correlated PhysicalSet variables. Selector extraction remains typed so the -// renderer can choose the correct payload iteration without embedding user -// paths in AQL. -func physicalAggregateExpression(physical *PhysicalPlan, resourceType string, source PhysicalValue, aggregate SemanticAggregate, sourceIsSet bool) (PhysicalExpression, error) { - op := PhysicalAggregateOperation(strings.ToUpper(strings.TrimSpace(aggregate.Operation))) - switch op { - case PhysicalCountAggregate, PhysicalCountDistinctAggregate, PhysicalExistsAggregate, PhysicalDistinctValuesAggregate, PhysicalMinAggregate, PhysicalMaxAggregate, PhysicalFirstAggregate: - default: - return PhysicalExpression{}, fmt.Errorf("aggregate %q uses unsupported operation %q", aggregate.Name, aggregate.Operation) - } - aggregatePhysical := PhysicalAggregate{Source: source, Operation: op} - if aggregate.Selector != nil { - valueSource := source - if !sourceIsSet && source.Variable != "" && len(source.Path) == 0 { - valueSource = PhysicalValue{Variable: source.Variable, Path: []string{"payload"}} - } - value := PhysicalExpression{Kind: PhysicalExtractExpression, Cardinality: PhysicalArrayCardinality, NullBehavior: PhysicalEmptyOnNull, - Extract: &PhysicalExtract{Source: valueSource, ResourceType: resourceType, Selector: *aggregate.Selector, ExecutionMode: selectorExecutionMode(resourceType, *aggregate.Selector)}} - aggregatePhysical.Value = &value - } - if aggregate.Predicate != nil { - leftSource := source - if !sourceIsSet && source.Variable != "" && len(source.Path) == 0 { - leftSource = PhysicalValue{Variable: source.Variable, Path: []string{"payload"}} - } - left := PhysicalExpression{Kind: PhysicalExtractExpression, Cardinality: PhysicalArrayCardinality, NullBehavior: PhysicalEmptyOnNull, - Extract: &PhysicalExtract{Source: leftSource, ResourceType: resourceType, Selector: *aggregate.Predicate, ExecutionMode: selectorExecutionMode(resourceType, *aggregate.Predicate)}} - comparison := &PhysicalPredicate{Operator: "EXISTS", LeftExpression: &left} - if aggregate.PredicateEquals != "" { - comparison.Operator = "EQUALS" - comparison.ValueKind = FilterString - key := "aggregate_" + sanitizeColumnName(source.Variable) + "_" + sanitizeColumnName(aggregate.Name) + "_predicate_equals" - physical.BindVars[key] = aggregate.PredicateEquals - comparison.Right = &PhysicalValue{BindKey: key} - } - aggregatePhysical.Predicate = &PhysicalPredicateExpression{Kind: PhysicalComparisonPredicate, Comparison: comparison} - } - cardinality := PhysicalScalarCardinality - nullBehavior := PhysicalEmptyOnNull - if op == PhysicalDistinctValuesAggregate { - cardinality = PhysicalArrayCardinality - } - return PhysicalExpression{Kind: PhysicalAggregateExpression, Cardinality: cardinality, NullBehavior: nullBehavior, Aggregate: &aggregatePhysical}, nil -} - -// physicalSliceExpression lowers a representative slice into a typed, -// bounded projection over either the singleton root document or a correlated -// child set. The source set is already materialized in stable _key order; -// the explicit sort expression below makes that ordering part of the slice -// contract and gives the renderer a deterministic tie-break. -func physicalSliceExpression(physical *PhysicalPlan, resourceType string, source PhysicalValue, slice SemanticSlice, sourceIsSet bool) (PhysicalExpression, error) { - _ = sourceIsSet - if slice.Limit <= 0 { - return PhysicalExpression{}, fmt.Errorf("slice %q requires positive limit", slice.Name) - } - limitKey := "slice_" + sanitizeColumnName(source.Variable) + "_" + sanitizeColumnName(slice.Name) + "_limit" - physical.BindVars[limitKey] = slice.Limit - // Expressions are anchored to the declared source variable for validation; - // the renderer rebinds them to its per-item loop variable. - leftSource := source - physicalSlice := PhysicalSlice{ - Source: source, - LimitBindKey: limitKey, - Sort: &PhysicalExpression{Kind: PhysicalValueExpression, Cardinality: PhysicalScalarCardinality, NullBehavior: PhysicalPreserveNull, Value: &PhysicalValue{Variable: source.Variable, Path: []string{"_key"}}}, - Projections: make([]PhysicalExpressionProjection, 0, len(slice.Fields)), - } - if slice.Predicate != nil { - left := PhysicalExpression{Kind: PhysicalExtractExpression, Cardinality: PhysicalArrayCardinality, NullBehavior: PhysicalEmptyOnNull, - Extract: &PhysicalExtract{Source: leftSource, ResourceType: resourceType, Selector: *slice.Predicate, ExecutionMode: selectorExecutionMode(resourceType, *slice.Predicate)}} - comparison := &PhysicalPredicate{Operator: "EXISTS", LeftExpression: &left} - if slice.PredicateEquals != "" { - key := "slice_" + sanitizeColumnName(source.Variable) + "_" + sanitizeColumnName(slice.Name) + "_predicate_equals" - physical.BindVars[key] = slice.PredicateEquals - comparison.Operator = "EQUALS" - comparison.ValueKind = FilterString - comparison.Right = &PhysicalValue{BindKey: key} - } - physicalSlice.Predicate = &PhysicalPredicateExpression{Kind: PhysicalComparisonPredicate, Comparison: comparison} - } - for index, field := range slice.Fields { - selection := field - if selection.Name == "" { - return PhysicalExpression{}, fmt.Errorf("slice %q field %d requires name", slice.Name, index) - } - physicalSlice.Projections = append(physicalSlice.Projections, PhysicalExpressionProjection{ - Name: selection.Name, - Expression: PhysicalExpression{Kind: PhysicalExtractExpression, Cardinality: PhysicalScalarCardinality, NullBehavior: PhysicalPreserveNull, - Extract: &PhysicalExtract{Source: leftSource, ResourceType: resourceType, Selector: selection.Selector, Fallbacks: append([]Selector(nil), selection.Fallbacks...), ExecutionMode: selectorExecutionMode(resourceType, selection.Selector, selection.Fallbacks...)}}, - }) - } - return PhysicalExpression{Kind: PhysicalSliceExpression, Cardinality: PhysicalArrayCardinality, NullBehavior: PhysicalEmptyOnNull, Slice: &physicalSlice}, nil -} - -func appendRootPhysicalFilters(physical *PhysicalPlan, root SemanticNode) error { - for index, filter := range root.Filters { - if err := ValidateTypedFilterForResource(root.ResourceType, filter); err != nil { - return fmt.Errorf("root filter %q: %w", filter.FieldRef, err) - } - selector, err := ParseSelector(filter.Selector) - if err != nil { - return fmt.Errorf("root filter %q selector: %w", filter.FieldRef, err) - } - predicate := PhysicalPredicate{ - Operator: string(filter.Operator), Quantifier: filter.Quantifier, ValueKind: filter.FieldKind, - LeftExpression: &PhysicalExpression{Kind: PhysicalExtractExpression, Cardinality: PhysicalArrayCardinality, NullBehavior: PhysicalEmptyOnNull, - Extract: &PhysicalExtract{Source: PhysicalValue{Variable: "root", Path: []string{"payload"}}, ResourceType: root.ResourceType, Selector: selector, ExecutionMode: selectorExecutionMode(root.ResourceType, selector)}}, - } - if filter.Operator != FilterExists && filter.Operator != FilterMissing { - key := fmt.Sprintf("root_filter_%d_value", index+1) - if filter.Operator == FilterIn { - values := make([]any, 0, len(filter.Values)) - for _, value := range filter.Values { - literal, err := filterLiteral(value) - if err != nil { - return err - } - values = append(values, literal) - } - physical.BindVars[key] = values - } else { - literal, err := filterLiteral(filter.Values[0]) - if err != nil { - return err - } - physical.BindVars[key] = literal - } - predicate.Right = &PhysicalValue{BindKey: key} - } - physical.Operations = append(physical.Operations, PhysicalOperation{Kind: PhysicalFilterOp, - Source: PhysicalSource{SemanticNode: root.Alias, ResourceType: root.ResourceType, SemanticField: filter.FieldRef}, - Filter: &PhysicalFilter{Expression: &PhysicalPredicateExpression{Kind: PhysicalComparisonPredicate, Comparison: &predicate}}}) - } - return nil -} - -// buildOptionalChildPhysicalSet lowers one root-relative optional traversal. -// The set retains resource documents (rather than pre-shaped maps), allowing -// the renderer to apply each child selector against the document payload at -// the outer return while preserving root row grain. -func buildOptionalChildPhysicalSet(physical *PhysicalPlan, setIndex int, parent SemanticNode, parentVariable string, child SemanticNode, projectionPrefix string, policy PhysicalOptimizationPolicy) (PhysicalSet, []PhysicalProjection, error) { - route, err := resolveStorageRoute(parent.ResourceType, child.EdgeLabel, child.ResourceType) - if err != nil { - return PhysicalSet{}, nil, err - } - prefix := fmt.Sprintf("child_set_%d", setIndex) - labelBind := prefix + "_label" - typeBind := prefix + "_target_type" - edgeCollectionBind := prefix + "_edge_collection" - physical.BindVars[labelBind] = child.EdgeLabel - physical.BindVars[typeBind] = child.ResourceType - physical.BindVars[edgeCollectionBind] = "fhir_edge" - targetVariable := fmt.Sprintf("%s_node", prefix) - edgeVariable := fmt.Sprintf("%s_edge", prefix) - strategy, endpointField, endpointJoinField, endpointIndexFields := physicalTraversalStrategyForRoute(policy, parentVariable, route) - source := PhysicalSource{SemanticNode: child.Alias, ResourceType: child.ResourceType, Relationship: child.EdgeLabel} - subplan := PhysicalSubplan{Captures: []string{parentVariable}, Operations: []PhysicalOperation{{ - Kind: PhysicalTraversalOp, Source: source, - Traversal: &PhysicalTraversal{SourceVariable: parentVariable, TargetVariable: targetVariable, EdgeVariable: edgeVariable, Direction: route.Direction, EdgeCollectionBindKey: edgeCollectionBind, EdgeLabelBindKey: labelBind, TargetTypeBindKey: typeBind, EdgeTargetTypeField: route.targetEdgeTypeField(), Strategy: strategy, EndpointField: endpointField, EndpointJoinField: endpointJoinField, EndpointIndexFields: endpointIndexFields}, - }}} - subplan.Operations = appendProjectScope(subplan.Operations, []string{edgeVariable, targetVariable}, child.EdgeLabel, child) - subplan.Operations = appendDatasetGenerationScope(subplan.Operations, []string{edgeVariable, targetVariable}, child.EdgeLabel, child) - subplan.Operations = appendAuthScope(subplan.Operations, []PhysicalValue{{Variable: edgeVariable, Path: []string{"auth_resource_path"}}, {Variable: targetVariable, Path: []string{"auth_resource_path"}}}, prefix+"_scope_allowed", child) - for index, filter := range child.Filters { - if err := ValidateTypedFilterForResource(child.ResourceType, filter); err != nil { - return PhysicalSet{}, nil, fmt.Errorf("child filter %q: %w", filter.FieldRef, err) - } - selector, err := ParseSelector(filter.Selector) - if err != nil { - return PhysicalSet{}, nil, fmt.Errorf("child filter %q selector: %w", filter.FieldRef, err) - } - predicate := PhysicalPredicate{Operator: string(filter.Operator), Quantifier: filter.Quantifier, ValueKind: filter.FieldKind, LeftExpression: &PhysicalExpression{Kind: PhysicalExtractExpression, Cardinality: PhysicalArrayCardinality, NullBehavior: PhysicalEmptyOnNull, Extract: &PhysicalExtract{Source: PhysicalValue{Variable: targetVariable, Path: []string{"payload"}}, ResourceType: child.ResourceType, Selector: selector, ExecutionMode: selectorExecutionMode(child.ResourceType, selector)}}} - if filter.Operator != FilterExists && filter.Operator != FilterMissing { - key := fmt.Sprintf("%s_filter_%d_value", prefix, index+1) - if filter.Operator == FilterIn { - values := make([]any, 0, len(filter.Values)) - for _, value := range filter.Values { - literal, err := filterLiteral(value) - if err != nil { - return PhysicalSet{}, nil, err - } - values = append(values, literal) - } - physical.BindVars[key] = values - } else { - if len(filter.Values) == 0 { - return PhysicalSet{}, nil, fmt.Errorf("child filter %q has no value", filter.FieldRef) - } - literal, err := filterLiteral(filter.Values[0]) - if err != nil { - return PhysicalSet{}, nil, err - } - physical.BindVars[key] = literal - } - predicate.Right = &PhysicalValue{BindKey: key} - } - subplan.Operations = append(subplan.Operations, PhysicalOperation{Kind: PhysicalFilterOp, Source: PhysicalSource{SemanticNode: child.Alias, ResourceType: child.ResourceType, SemanticField: filter.FieldRef}, Filter: &PhysicalFilter{Expression: &PhysicalPredicateExpression{Kind: PhysicalComparisonPredicate, Comparison: &predicate}}}) - } - subplan.Return = PhysicalExpression{Kind: PhysicalValueExpression, Cardinality: PhysicalObjectCardinality, NullBehavior: PhysicalPreserveNull, Value: &PhysicalValue{Variable: targetVariable}} - var output *PhysicalSetOutput - if policy.RuleEnabled(PhysicalOptimizationRuleCompactProjection) { - output = compactPhysicalSetOutput(child) - } - set := PhysicalSet{Variable: prefix, Subplan: subplan, Unique: true, SortByKey: true, Output: output} - projections := make([]PhysicalProjection, 0, len(child.Fields)) - for index, field := range child.Fields { - selection, err := ResolveSemanticField(child.ResourceType, child.Alias, index, field) - if err != nil { - return PhysicalSet{}, nil, err - } - projections = append(projections, PhysicalProjection{Name: projectionPrefix + "__" + field.Name, Expression: &PhysicalExpression{Kind: PhysicalExtractExpression, Cardinality: PhysicalArrayCardinality, NullBehavior: PhysicalEmptyOnNull, Extract: &PhysicalExtract{Source: PhysicalValue{Variable: set.Variable}, ResourceType: child.ResourceType, Selector: selection.Selector, Fallbacks: append([]Selector(nil), selection.Fallbacks...), Distinct: selection.Projection == ProjectionDistinctArray, ExecutionMode: selectorExecutionMode(child.ResourceType, selection.Selector, selection.Fallbacks...)}}}) - } - for _, aggregate := range child.Aggregates { - expression, err := physicalAggregateExpression(physical, child.ResourceType, PhysicalValue{Variable: set.Variable}, aggregate, true) - if err != nil { - return PhysicalSet{}, nil, err - } - projections = append(projections, PhysicalProjection{Name: projectionPrefix + "__" + aggregate.Name, Expression: &expression}) - } - for _, pivot := range child.Pivots { - expression, err := physicalPivotExpression(physical, child.ResourceType, PhysicalValue{Variable: set.Variable}, pivot, true) - if err != nil { - return PhysicalSet{}, nil, err - } - projections = append(projections, PhysicalProjection{Name: projectionPrefix + "__" + pivot.Name, Expression: &expression}) - } - for _, slice := range child.Slices { - expression, err := physicalSliceExpression(physical, child.ResourceType, PhysicalValue{Variable: set.Variable}, slice, true) - if err != nil { - return PhysicalSet{}, nil, err - } - projections = append(projections, PhysicalProjection{Name: projectionPrefix + "__" + slice.Name, Expression: &expression}) - } - // A traversal-time projection is the production form of selector reuse. It - // computes selector arrays in the original child subquery and removes the - // payload before materialization. The older prepared-set pass is retained - // only for explicit experiments and is never layered on top of this shape. - if policy.RuleEnabled(PhysicalOptimizationRuleCompactProjection) { - projectPhysicalChildSet(&set, child.ResourceType, projections) - } - if set.Projection == nil { - prepareRichChildSet(&set, child.ResourceType, projections, policy) - } - return set, projections, nil -} - -func physicalTraversalStrategyForRoute(policy PhysicalOptimizationPolicy, parentVariable string, route storageRoute) (PhysicalTraversalStrategy, string, string, []string) { - // Endpoint equality is eligible for every validated depth-one route, - // including a root sibling group. When sibling sets are shared, the - // optimizer preserves this typed endpoint contract on the broad set and - // replaces the target-type predicate with a bind-backed list. If route - // metadata cannot provide the complete endpoint/index contract, native - // traversal remains the safe fallback. - if !policy.RuleEnabled(PhysicalOptimizationRuleEndpointTraversal) { - return PhysicalTraversalNative, "", "", nil - } - if parentField, joinField, fields, ok := route.endpointLookupFields(); ok && len(fields) > 0 { - return PhysicalTraversalEndpointLookup, parentField, joinField, append([]string(nil), fields...) - } - return PhysicalTraversalNative, "", "", nil -} - -// compactPhysicalSetOutput retains graph identity and only the payload needed -// by downstream selectors/rich consumers. Scope predicates run before this -// projection, so project, generation, and authorization metadata do not need -// to remain in the post-window set. Nested traversal still receives _id, and -// UNIQUE/SORT retain both identity keys to preserve duplicate and ordering -// semantics. -func compactPhysicalSetOutput(node SemanticNode) *PhysicalSetOutput { - fields := []PhysicalSetOutputField{ - PhysicalSetGraphIDField, - PhysicalSetKeyField, - PhysicalSetIDField, - PhysicalSetResourceTypeField, - } - needsPayload := len(node.Fields) > 0 || len(node.Pivots) > 0 || len(node.Slices) > 0 - if !needsPayload { - for _, aggregate := range node.Aggregates { - if aggregate.Selector != nil || aggregate.Predicate != nil { - needsPayload = true - break - } - } - } - if needsPayload { - fields = append(fields, PhysicalSetPayloadField) - } - return &PhysicalSetOutput{Fields: fields} -} - -// prepareRichChildSet adds a generic selector cache when at least two rich -// consumers read the same child relationship set. The cache is deliberately -// selector-based (not FHIR-resource-specific): any generated resource type -// with repeated aggregate, pivot, or slice selectors can use it. -func prepareRichChildSet(set *PhysicalSet, resourceType string, projections []PhysicalProjection, policy PhysicalOptimizationPolicy) { - if !policy.RuleEnabled(PhysicalOptimizationRulePreparedSelectors) { - return - } - counts := map[string]int{} - selectors := map[string]Selector{} - add := func(selector Selector) { - key := physicalSelectorIdentity(selector) - counts[key]++ - selectors[key] = selector - } - var collect func(*PhysicalExpression) - collect = func(expression *PhysicalExpression) { - if expression == nil { - return - } - switch expression.Kind { - case PhysicalAggregateExpression: - if expression.Aggregate != nil { - if expression.Aggregate.Value != nil { - collect(expression.Aggregate.Value) - } - if expression.Aggregate.Predicate != nil && expression.Aggregate.Predicate.Comparison != nil { - collect(expression.Aggregate.Predicate.Comparison.LeftExpression) - } - } - case PhysicalPivotExpression: - if expression.Pivot != nil { - add(expression.Pivot.KeySelector) - add(expression.Pivot.ValueSelector) - } - case PhysicalSliceExpression: - if expression.Slice != nil { - if expression.Slice.Predicate != nil && expression.Slice.Predicate.Comparison != nil { - collect(expression.Slice.Predicate.Comparison.LeftExpression) - } - for index := range expression.Slice.Projections { - collect(&expression.Slice.Projections[index].Expression) - } - } - case PhysicalExtractExpression: - if expression.Extract != nil { - add(expression.Extract.Selector) - } - case PhysicalObjectExpression: - if expression.Object != nil { - for index := range expression.Object.Fields { - collect(&expression.Object.Fields[index].Expression) - } - } - } - } - for index := range projections { - collect(projections[index].Expression) - } - eligible := map[string]string{} - fields := make([]PhysicalPreparedField, 0) - // Map iteration order must not influence the physical plan. Stable field - // allocation keeps generated AQL and bind-variable names deterministic, - // which is important for explain fixtures and cache keys. - keys := make([]string, 0, len(counts)) - for key := range counts { - keys = append(keys, key) - } - sort.Strings(keys) - for _, key := range keys { - count := counts[key] - _, _, savings := estimatePreparedSelectorWork(count) - if savings <= 0 { - continue - } - field := fmt.Sprintf("__loom_prepared_%d", len(fields)) - eligible[key] = field - fields = append(fields, PhysicalPreparedField{Name: field, ResourceType: resourceType, Selector: selectors[key]}) - } - if len(fields) == 0 { - return - } - set.Prepared = &PhysicalPreparedSet{Variable: set.Variable + "_prepared", SourceSetVariable: set.Variable, Fields: fields} - var annotate func(*PhysicalExpression) - annotate = func(expression *PhysicalExpression) { - if expression == nil { - return - } - annotateExtract := func(extract *PhysicalExtract) { - if extract == nil || extract.Source.Variable != set.Variable { - return - } - // Fallback selectors are an ordered fallback chain. A prepared - // primary value alone cannot preserve that contract, so leave these - // extracts on the original renderer path until the prepared schema - // can represent fallback alternatives explicitly. - if len(extract.Fallbacks) == 0 { - if field, ok := eligible[physicalSelectorIdentity(extract.Selector)]; ok { - extract.Prepared = &PhysicalPreparedReference{SetVariable: set.Prepared.Variable, Field: field} - } - } - } - switch expression.Kind { - case PhysicalAggregateExpression: - if expression.Aggregate != nil { - if expression.Aggregate.Value != nil { - annotate(expression.Aggregate.Value) - } - if expression.Aggregate.Predicate != nil && expression.Aggregate.Predicate.Comparison != nil { - annotate(expression.Aggregate.Predicate.Comparison.LeftExpression) - } - } - case PhysicalPivotExpression: - if expression.Pivot != nil { - if field, ok := eligible[physicalSelectorIdentity(expression.Pivot.KeySelector)]; ok { - expression.Pivot.PreparedKey = &PhysicalPreparedReference{SetVariable: set.Prepared.Variable, Field: field} - } - if field, ok := eligible[physicalSelectorIdentity(expression.Pivot.ValueSelector)]; ok { - expression.Pivot.PreparedValue = &PhysicalPreparedReference{SetVariable: set.Prepared.Variable, Field: field} - } - } - case PhysicalSliceExpression: - if expression.Slice != nil { - if expression.Slice.Predicate != nil && expression.Slice.Predicate.Comparison != nil { - annotate(expression.Slice.Predicate.Comparison.LeftExpression) - } - for index := range expression.Slice.Projections { - annotate(&expression.Slice.Projections[index].Expression) - } - } - case PhysicalExtractExpression: - annotateExtract(expression.Extract) - case PhysicalObjectExpression: - if expression.Object != nil { - for index := range expression.Object.Fields { - annotate(&expression.Object.Fields[index].Expression) - } - } - } - } - for index := range projections { - annotate(projections[index].Expression) - } -} - -// projectPhysicalChildSet attaches selector values to the original set item -// rather than creating a second prepared array. A fallback selector is an -// ordered multi-source contract that cannot be represented by one projected -// field, so the entire set conservatively retains its existing payload output. -func projectPhysicalChildSet(set *PhysicalSet, resourceType string, projections []PhysicalProjection) { - if set == nil || len(projections) == 0 { - return - } - selectors := map[string]Selector{} - fallback := false - var collect func(*PhysicalExpression) - collect = func(expression *PhysicalExpression) { - if expression == nil { - return - } - switch expression.Kind { - case PhysicalExtractExpression: - if expression.Extract == nil || expression.Extract.Source.Variable != set.Variable { - return - } - if len(expression.Extract.Fallbacks) != 0 { - fallback = true - return - } - selectors[physicalSelectorIdentity(expression.Extract.Selector)] = expression.Extract.Selector - case PhysicalAggregateExpression: - if expression.Aggregate != nil { - collect(expression.Aggregate.Value) - if expression.Aggregate.Predicate != nil && expression.Aggregate.Predicate.Comparison != nil { - collect(expression.Aggregate.Predicate.Comparison.LeftExpression) - } - } - case PhysicalPivotExpression: - if expression.Pivot != nil && expression.Pivot.Source.Variable == set.Variable { - selectors[physicalSelectorIdentity(expression.Pivot.KeySelector)] = expression.Pivot.KeySelector - selectors[physicalSelectorIdentity(expression.Pivot.ValueSelector)] = expression.Pivot.ValueSelector - } - case PhysicalSliceExpression: - if expression.Slice != nil { - if expression.Slice.Predicate != nil && expression.Slice.Predicate.Comparison != nil { - collect(expression.Slice.Predicate.Comparison.LeftExpression) - } - for index := range expression.Slice.Projections { - collect(&expression.Slice.Projections[index].Expression) - } - } - case PhysicalObjectExpression: - if expression.Object != nil { - for index := range expression.Object.Fields { - collect(&expression.Object.Fields[index].Expression) - } - } - } - } - for index := range projections { - collect(projections[index].Expression) - } - if fallback || len(selectors) == 0 { - return - } - keys := make([]string, 0, len(selectors)) - for key := range selectors { - keys = append(keys, key) - } - sort.Strings(keys) - fields := make([]PhysicalSetProjectionField, 0, len(keys)) - fieldBySelector := make(map[string]string, len(keys)) - for index, key := range keys { - name := fmt.Sprintf("__loom_projection_%d", index) - fields = append(fields, PhysicalSetProjectionField{Name: name, ResourceType: resourceType, Selector: selectors[key], ExecutionMode: selectorExecutionMode(resourceType, selectors[key])}) - fieldBySelector[key] = name - } - var rewrite func(*PhysicalExpression) - rewrite = func(expression *PhysicalExpression) { - if expression == nil { - return - } - switch expression.Kind { - case PhysicalExtractExpression: - if expression.Extract != nil && expression.Extract.Source.Variable == set.Variable && len(expression.Extract.Fallbacks) == 0 { - if field := fieldBySelector[physicalSelectorIdentity(expression.Extract.Selector)]; field != "" { - expression.Extract.Prepared = &PhysicalPreparedReference{SetVariable: set.Variable, Field: field} - } - } - case PhysicalAggregateExpression: - if expression.Aggregate != nil { - rewrite(expression.Aggregate.Value) - if expression.Aggregate.Predicate != nil && expression.Aggregate.Predicate.Comparison != nil { - rewrite(expression.Aggregate.Predicate.Comparison.LeftExpression) - } - } - case PhysicalPivotExpression: - if expression.Pivot != nil && expression.Pivot.Source.Variable == set.Variable { - expression.Pivot.PreparedKey = &PhysicalPreparedReference{SetVariable: set.Variable, Field: fieldBySelector[physicalSelectorIdentity(expression.Pivot.KeySelector)]} - expression.Pivot.PreparedValue = &PhysicalPreparedReference{SetVariable: set.Variable, Field: fieldBySelector[physicalSelectorIdentity(expression.Pivot.ValueSelector)]} - } - case PhysicalSliceExpression: - if expression.Slice != nil { - if expression.Slice.Predicate != nil && expression.Slice.Predicate.Comparison != nil { - rewrite(expression.Slice.Predicate.Comparison.LeftExpression) - } - for index := range expression.Slice.Projections { - rewrite(&expression.Slice.Projections[index].Expression) - } - } - case PhysicalObjectExpression: - if expression.Object != nil { - for index := range expression.Object.Fields { - rewrite(&expression.Object.Fields[index].Expression) - } - } - } - } - for index := range projections { - rewrite(projections[index].Expression) - } - set.Projection = &PhysicalSetProjection{Fields: fields} - set.Output = nil -} - -func physicalSelectorIdentity(selector Selector) string { - var b strings.Builder - for _, step := range selector.Steps { - b.WriteString(step.Field) - if step.Iterate { - b.WriteString("[]") - } - if step.Index != nil { - fmt.Fprintf(&b, "[%d]", *step.Index) - } - b.WriteByte('.') - } - if selector.Filter != nil { - b.WriteString("?" + selector.Filter.Field + "=" + selector.Filter.Needle) - } - return b.String() -} diff --git a/internal/dataframe/compiler/lower/helpers.go b/internal/dataframe/compiler/lower/helpers.go index ecda0c3..f76d1ab 100644 --- a/internal/dataframe/compiler/lower/helpers.go +++ b/internal/dataframe/compiler/lower/helpers.go @@ -47,22 +47,67 @@ func selectorHasIteratedArray(sel Selector) bool { return false } +// selectorModeContext is the complete physical context needed to classify a +// selector. Source/cardinality/null behavior are intentionally carried here +// even though the current schema proof only needs resource type and selector: +// keeping them at this boundary prevents individual frontends from making a +// mode decision with incomplete physical context and leaves one place for +// future cardinality-sensitive proofs. +type selectorModeContext struct { + ResourceType string + Selector Selector + Fallbacks []Selector + Source PhysicalValue + Cardinality PhysicalCardinality + NullBehavior PhysicalNullBehavior + Metadata fhirschema.TerminalScalarMetadata + MetadataOK bool +} + func selectorExecutionMode(resourceType string, selector Selector, fallbacks ...Selector) PhysicalSelectorExecutionMode { + return selectorExecutionModeWithContext(selectorModeContext{ + ResourceType: resourceType, + Selector: selector, + Fallbacks: fallbacks, + Cardinality: PhysicalScalarCardinality, + NullBehavior: PhysicalPreserveNull, + }) +} + +// selectorExecutionModeForExpression is the shared selector classifier used +// by expression frontends. It resolves schema metadata once and passes the +// complete physical expression context through the same proof used by +// generic field lowering. +func selectorExecutionModeForExpression(resourceType string, selector Selector, fallbacks []Selector, source PhysicalValue, cardinality PhysicalCardinality, nullBehavior PhysicalNullBehavior) PhysicalSelectorExecutionMode { + return selectorExecutionModeWithContext(selectorModeContext{ + ResourceType: resourceType, + Selector: selector, + Fallbacks: fallbacks, + Source: source, + Cardinality: cardinality, + NullBehavior: nullBehavior, + }) +} + +func selectorExecutionModeWithContext(input selectorModeContext) PhysicalSelectorExecutionMode { switch strings.ToLower(strings.TrimSpace(os.Getenv("LOOM_PHYSICAL_RULE_TYPED_SELECTORS"))) { case "off", "0", "false", "disabled": return PhysicalSelectorGeneric } - if len(fallbacks) != 0 || selector.Filter != nil { + if len(input.Fallbacks) != 0 || input.Selector.Filter != nil { return PhysicalSelectorGeneric } - metadata, ok := fhirschema.ResolveTerminalScalarMetadata(resourceType, selector.CanonicalPath()) + metadata, ok := input.Metadata, input.MetadataOK + if !ok { + metadata, ok = fhirschema.ResolveTerminalScalarMetadata(input.ResourceType, input.Selector.CanonicalPath()) + } if !ok { return PhysicalSelectorGeneric } - if selectorHasNoArrays(selector) && !metadata.Repeated { + if selectorHasNoArrays(input.Selector) && !metadata.Repeated { return PhysicalSelectorDirectScalar } - if selectorHasIteratedArray(selector) && metadata.Repeated { + if selectorHasIteratedArray(input.Selector) && metadata.Repeated { return PhysicalSelectorConditionalArray } return PhysicalSelectorGeneric diff --git a/internal/dataframe/compiler/lower/plan_orchestration.go b/internal/dataframe/compiler/lower/plan_orchestration.go new file mode 100644 index 0000000..9f2a8d3 --- /dev/null +++ b/internal/dataframe/compiler/lower/plan_orchestration.go @@ -0,0 +1,243 @@ +package lower + +import ( + "fmt" + "strings" + + "github.com/calypr/loom/fhirschema" +) + +// BuildGenericPhysicalPlan lowers generic navigation plus root and optional +// child selections into the typed physical IR. +func BuildGenericPhysicalPlan(semantic SemanticPlan) (PhysicalPlan, error) { + return BuildGenericPhysicalPlanWithPolicy(semantic, DefaultPhysicalOptimizationPolicy()) +} + +// BuildGenericPhysicalPlanWithPolicy threads an explicit optimizer policy +// through physical construction so prepared-selector ablations happen before +// references to prepared variables are attached to projections. +func BuildGenericPhysicalPlanWithPolicy(semantic SemanticPlan, policy PhysicalOptimizationPolicy) (PhysicalPlan, error) { + if strings.TrimSpace(semantic.Project) == "" { + return PhysicalPlan{}, fmt.Errorf("semantic plan project is required") + } + if err := ValidateSemanticGraph(semantic); err != nil { + return PhysicalPlan{}, err + } + if !fhirschema.ResourceExists(semantic.Root.ResourceType) { + return PhysicalPlan{}, fmt.Errorf("root resource type %q is not represented by the generated FHIR schema", semantic.Root.ResourceType) + } + if err := validateGenericPhysicalNode(semantic.Root, true); err != nil { + return PhysicalPlan{}, err + } + + physical := PhysicalPlan{ + Version: 1, + Source: PhysicalSource{ + SemanticNode: semantic.Root.Alias, + ResourceType: semantic.Root.ResourceType, + }, + BindVars: map[string]any{ + "root_collection": semantic.Root.ResourceType, + "project": semantic.Project, + datasetGenerationBindKey: datasetGenerationBindValue(semantic.DatasetGeneration), + "auth_resource_paths": append([]string(nil), semantic.AuthResourcePaths...), + "auth_resource_paths_unrestricted": semanticAuthScopeUnrestricted(semantic), + "scope_allowed": true, + }, + Operations: []PhysicalOperation{ + { + Kind: PhysicalRootScanOp, + Source: PhysicalSource{SemanticNode: semantic.Root.Alias, ResourceType: semantic.Root.ResourceType}, + RootScan: &PhysicalRootScan{Variable: "root", CollectionBindKey: "root_collection"}, + }, + }, + } + physical.Operations = appendProjectScope(physical.Operations, []string{"root"}, "", semantic.Root) + physical.Operations = appendDatasetGenerationScope(physical.Operations, []string{"root"}, "", semantic.Root) + physical.Operations = appendAuthScope(physical.Operations, []PhysicalValue{{Variable: "root", Path: []string{"auth_resource_path"}}}, "root_scope_allowed", semantic.Root) + if err := appendRootPhysicalFilters(&physical, semantic.Root); err != nil { + return PhysicalPlan{}, err + } + if err := appendRequiredTraversalMatchFilters(&physical, semantic.Root); err != nil { + return PhysicalPlan{}, err + } + + childSetIndex := 0 + returnProjections := []PhysicalProjection{} + var walk func(parent SemanticNode, parentVariable, projectionPrefix string) error + walk = func(parent SemanticNode, parentVariable, projectionPrefix string) error { + for _, child := range parent.Children { + if child.MatchMode.Required() { + // Required routes are represented by the root semi-join emitted + // above for membership, but they may still need a materialized + // child set for selected fields or nested shaping. The second + // physical set is post-window output work; it cannot change root + // membership because the semi-join remains before SORT/LIMIT. + if physicalNodeNeedsMaterializedSet(child) { + childSetIndex++ + childProjectionPrefix := child.Alias + if projectionPrefix != "" { + childProjectionPrefix = projectionPrefix + "__" + child.Alias + } + set, projections, err := buildOptionalChildPhysicalSet(&physical, childSetIndex, parent, parentVariable, child, childProjectionPrefix, policy) + if err != nil { + return err + } + physical.Operations = append(physical.Operations, PhysicalOperation{Kind: PhysicalSetOp, Source: PhysicalSource{SemanticNode: child.Alias, ResourceType: child.ResourceType, Relationship: child.EdgeLabel}, Set: &set}) + returnProjections = append(returnProjections, projections...) + if err := walk(child, set.Variable, childProjectionPrefix); err != nil { + return err + } + } + continue + } + if !physicalNodeNeedsMaterializedSet(child) { + traversalIndex := 1 + for _, operation := range physical.Operations { + if operation.Kind == PhysicalTraversalOp { + traversalIndex++ + } + } + nodeVariable := fmt.Sprintf("node_%d", traversalIndex) + edgeVariable := fmt.Sprintf("edge_%d", traversalIndex) + traversal, err := BuildPhysicalTraversal(TraversalLoweringRequest{ + FromType: parent.ResourceType, EdgeLabel: child.EdgeLabel, ToType: child.ResourceType, + SourceVariable: parentVariable, TargetVariable: nodeVariable, EdgeVariable: edgeVariable, + BindPrefix: fmt.Sprintf("traversal_%d", traversalIndex), Policy: policy, + }) + if err != nil { + return err + } + for key, value := range traversal.BindVars { + physical.BindVars[key] = value + } + physical.Operations = append(physical.Operations, PhysicalOperation{Kind: PhysicalTraversalOp, + Source: PhysicalSource{SemanticNode: child.Alias, ResourceType: child.ResourceType, Relationship: child.EdgeLabel}, + Traversal: &traversal.Traversal}) + physical.Operations = appendProjectScope(physical.Operations, []string{edgeVariable, nodeVariable}, child.EdgeLabel, child) + physical.Operations = appendDatasetGenerationScope(physical.Operations, []string{edgeVariable, nodeVariable}, child.EdgeLabel, child) + physical.Operations = appendAuthScope(physical.Operations, []PhysicalValue{{Variable: edgeVariable, Path: []string{"auth_resource_path"}}, {Variable: nodeVariable, Path: []string{"auth_resource_path"}}}, fmt.Sprintf("traversal_%d_scope_allowed", traversalIndex), child) + if err := walk(child, nodeVariable, projectionPrefix); err != nil { + return err + } + continue + } + // Optional children are correlated sets. Keeping them in a LET + // subquery preserves the parent row grain while allowing typed child + // filters and projections to be applied before materialization. + childSetIndex++ + childProjectionPrefix := child.Alias + if projectionPrefix != "" { + childProjectionPrefix = projectionPrefix + "__" + child.Alias + } + set, projections, err := buildOptionalChildPhysicalSet(&physical, childSetIndex, parent, parentVariable, child, childProjectionPrefix, policy) + if err != nil { + return err + } + physical.Operations = append(physical.Operations, PhysicalOperation{Kind: PhysicalSetOp, Source: PhysicalSource{SemanticNode: child.Alias, ResourceType: child.ResourceType, Relationship: child.EdgeLabel}, Set: &set}) + returnProjections = append(returnProjections, projections...) + if err := walk(child, set.Variable, childProjectionPrefix); err != nil { + return err + } + } + return nil + } + if err := walk(semantic.Root, "root", ""); err != nil { + return PhysicalPlan{}, err + } + projections, err := rootPhysicalProjections(&physical, semantic.Root) + if err != nil { + return PhysicalPlan{}, err + } + projections = append(projections, returnProjections...) + physical.Operations = append(physical.Operations, physical.DeferredExpressionLets...) + physical.DeferredExpressionLets = nil + physical.Operations = append(physical.Operations, PhysicalOperation{ + Kind: PhysicalReturnOp, + Source: PhysicalSource{SemanticNode: semantic.Root.Alias, ResourceType: semantic.Root.ResourceType, SemanticField: "_key"}, + Return: &PhysicalReturn{Projections: projections}, + }) + if err := physical.Validate(); err != nil { + return PhysicalPlan{}, fmt.Errorf("validate generic physical plan: %w", err) + } + if err := ValidateGenericPhysicalPlanScope(physical); err != nil { + return PhysicalPlan{}, fmt.Errorf("verify generic physical plan scope: %w", err) + } + return physical, nil +} + +// physicalNodeNeedsMaterializedSet reports whether a node or any optional +// descendant has shaped output. Materializing an otherwise unselected parent +// is necessary to give nested sets a stable correlated source variable. +func physicalNodeNeedsMaterializedSet(node SemanticNode) bool { + if len(node.Fields) != 0 || len(node.Filters) != 0 || len(node.Pivots) != 0 || len(node.Aggregates) != 0 || len(node.Slices) != 0 || len(node.DynamicMaps) != 0 { + return true + } + for _, child := range node.Children { + if child.MatchMode.Required() || physicalNodeNeedsMaterializedSet(child) { + return true + } + } + return false +} + +func appendProjectScope(operations []PhysicalOperation, variables []string, relationship string, node SemanticNode) []PhysicalOperation { + right := PhysicalValue{BindKey: "project"} + for _, variable := range variables { + operations = append(operations, PhysicalOperation{ + Kind: PhysicalFilterOp, + Source: PhysicalSource{SemanticNode: node.Alias, ResourceType: node.ResourceType, Relationship: relationship, SemanticField: "project"}, + Filter: &PhysicalFilter{Predicate: PhysicalPredicate{ + Operator: "EQUALS", + Left: PhysicalValue{Variable: variable, Path: []string{"project"}}, + Right: &right, + }}, + }) + } + return operations +} + +// appendDatasetGenerationScope applies the same exact generation bind to +// every physical document participating in a scan/traversal. With a nil bind +// value this renders `dataset_generation == null`, deliberately isolating +// legacy documents from later generation-qualified loads. +func appendDatasetGenerationScope(operations []PhysicalOperation, variables []string, relationship string, node SemanticNode) []PhysicalOperation { + right := PhysicalValue{BindKey: datasetGenerationBindKey} + for _, variable := range variables { + operations = append(operations, PhysicalOperation{ + Kind: PhysicalFilterOp, + Source: PhysicalSource{SemanticNode: node.Alias, ResourceType: node.ResourceType, Relationship: relationship, SemanticField: datasetGenerationField}, + Filter: &PhysicalFilter{Predicate: PhysicalPredicate{ + Operator: "EQUALS", + Left: PhysicalValue{Variable: variable, Path: []string{datasetGenerationField}}, + Right: &right, + }}, + }) + } + return operations +} + +func appendAuthScope(operations []PhysicalOperation, scopedValues []PhysicalValue, resultVariable string, node SemanticNode) []PhysicalOperation { + inputs := append([]PhysicalValue(nil), scopedValues...) + inputs = append(inputs, PhysicalValue{BindKey: "auth_resource_paths"}, PhysicalValue{BindKey: "auth_resource_paths_unrestricted"}) + operations = append(operations, PhysicalOperation{ + Kind: PhysicalDerivedLetOp, + Source: PhysicalSource{SemanticNode: node.Alias, ResourceType: node.ResourceType, Relationship: node.EdgeLabel, SemanticField: "auth_resource_path"}, + DerivedLet: &PhysicalDerivedLet{Variable: resultVariable, Operator: "AUTH_RESOURCE_PATH_ALLOWED", Inputs: inputs}, + }) + right := PhysicalValue{BindKey: "scope_allowed"} + return append(operations, PhysicalOperation{ + Kind: PhysicalFilterOp, + Source: PhysicalSource{SemanticNode: node.Alias, ResourceType: node.ResourceType, Relationship: node.EdgeLabel, SemanticField: "auth_resource_path"}, + Filter: &PhysicalFilter{Predicate: PhysicalPredicate{Operator: "EQUALS", Left: PhysicalValue{Variable: resultVariable}, Right: &right}}, + }) +} + +func validateGenericPhysicalNode(node SemanticNode, root bool) error { + for _, child := range node.Children { + if err := validateGenericPhysicalNode(child, false); err != nil { + return err + } + } + return nil +} diff --git a/internal/dataframe/compiler/lower/recipe_compile.go b/internal/dataframe/compiler/lower/recipe_compile.go new file mode 100644 index 0000000..5f922c7 --- /dev/null +++ b/internal/dataframe/compiler/lower/recipe_compile.go @@ -0,0 +1,175 @@ +package lower + +// This file contains the canonical recipe lowering boundary. Persisted +// recipes are a frontend: after resolution each output is lowered to the same +// ir.PhysicalPlan used by the GraphQL dataframe compiler. + +import ( + "fmt" + "strings" + + "github.com/calypr/loom/internal/dataframe/compiler/ir" + "github.com/calypr/loom/internal/dataframe/recipe" + "github.com/calypr/loom/internal/dataframe/semantic" + "github.com/calypr/loom/internal/dataframe/spec" +) + +// CompiledRecipe is orchestration metadata around one canonical physical plan +// per output. It deliberately contains no recipe-specific traversal, +// projection, or renderer structures. Output order is the persisted recipe +// order and therefore part of the stable materialization contract. +type CompiledRecipe struct { + Version int + RecipeDigest string + ResolvedSchemaDigest string + ScopeDigest string + SourceGeneration string + Outputs []CompiledRecipeOutput +} + +// CompiledRecipeOutput is the canonical compiler result for one output. +// Columns is metadata for the stream/materialization layer; all query +// semantics live in Plan. +type CompiledRecipeOutput struct { + Name string + RootResourceType string + RowGrain semantic.RowGrain + Columns []string + // OutputSchema is the compiler-owned ordered projection schema. It is + // captured from the finalized physical RETURN projections rather than + // reconstructed by a transport adapter from the semantic recipe tree. + // Internal projections remain present here so execution and diagnostics can + // distinguish them from the public dataframe contract. + OutputSchema []CompiledOutputColumn + // RowIdentity describes the stable semantic identity used by publication + // targets. It is metadata only; the physical plan remains authoritative for + // the returned values. + RowIdentity *spec.RowIdentity + // DynamicColumns is post-query validation metadata. The physical plan + // contains the bounded projections; observed-key/type checks remain above + // the backend execution boundary. + DynamicColumns []DynamicColumnMetadata + Plan ir.PhysicalPlan + // OptimizedPlan is populated by the request orchestrator after all outputs + // have been lowered. Keeping it separate from Plan lets preview windows be + // rendered repeatedly without re-running the optimizer or lowering stage. + OptimizedPlan *ir.PhysicalPlan +} + +// CompiledOutputColumn describes one finalized physical projection. Kind and +// Cardinality are logical, backend-neutral values; Internal projections are +// never exposed by dataframe transports. +type CompiledOutputColumn struct { + Name string + Kind string + Cardinality string + Nullable bool + Internal bool + Identity bool +} + +type DynamicColumnMetadata struct { + Name string + DynamicName string + SourceKey string + ValueType string +} + +func cloneRowIdentity(identity *spec.RowIdentity) *spec.RowIdentity { + if identity == nil { + return nil + } + copy := *identity + copy.Fields = append([]string(nil), identity.Fields...) + return © +} + +// CompileResolvedRecipePlan lowers every resolved recipe output into the +// canonical physical IR. The optimizer and renderer are intentionally not +// called here: callers can apply an explicit PhysicalOptimizationPolicy and +// execution window at the same boundary used by generic dataframe requests. +// +// Dynamic maps are lowered into bounded named projections below. Their +// observed-key/type checks remain metadata for post-query validation; no +// runtime map-shaped AQL is emitted. +func CompileResolvedRecipePlan(resolved semantic.ResolvedRecipePlan, policy PhysicalOptimizationPolicy) (CompiledRecipe, error) { + semanticPlan := resolved.SemanticPlan + if semanticPlan.Version <= 0 || strings.TrimSpace(semanticPlan.RecipeDigest) == "" { + return CompiledRecipe{}, fmt.Errorf("resolved recipe plan is missing semantic provenance") + } + if strings.TrimSpace(semanticPlan.Bindings.Project) == "" { + return CompiledRecipe{}, fmt.Errorf("resolved recipe bindings project is required") + } + result := CompiledRecipe{ + Version: 1, + RecipeDigest: semanticPlan.RecipeDigest, + ResolvedSchemaDigest: resolved.ResolvedSchemaDigest, + ScopeDigest: resolved.ScopeDigest, + SourceGeneration: resolved.SourceGeneration, + Outputs: make([]CompiledRecipeOutput, 0, len(semanticPlan.Outputs)), + } + selected := map[string]bool{} + for _, name := range semanticPlan.Bindings.OutputNames { + selected[name] = true + } + for _, output := range semanticPlan.Outputs { + if len(selected) > 0 && !selected[output.Name] { + continue + } + compiled, err := compileRecipeOutput(output, semanticPlan.Bindings, resolved.ResolvedColumns, policy) + if err != nil { + return CompiledRecipe{}, fmt.Errorf("output %q: %w", output.Name, err) + } + result.Outputs = append(result.Outputs, compiled) + } + return result, nil +} + +func compileRecipeOutput(output semantic.OutputPlan, bindings recipe.RuntimeBindings, resolvedColumns map[string][]semantic.ResolvedColumn, policy PhysicalOptimizationPolicy) (CompiledRecipeOutput, error) { + root := cloneRecipeNodeForPhysical(output.Root) + identity, ok := spec.DefaultRowIdentity(spec.RowGrain(output.RowGrain)) + if !ok { + return CompiledRecipeOutput{}, fmt.Errorf("row grain %q has no canonical identity", output.RowGrain) + } + semanticInput := SemanticPlan{ + Version: 1, + Project: bindings.Project, + DatasetGeneration: bindings.DatasetGeneration, + AuthResourcePaths: append([]string(nil), bindings.AuthResourcePaths...), + AuthScopeMode: bindings.AuthScopeMode, + Root: root, + RowIdentity: &identity, + } + physical, err := BuildGenericPhysicalPlanWithPolicy(semanticInput, policy) + if err != nil { + return CompiledRecipeOutput{}, err + } + + // Recipe expressions are richer than selector-only GraphQL fields. The + // generic plan supplies the complete scoped traversal/set structure; patch + // only the expression payloads using the already checked recipe AST. + if err := patchRecipeExpressions(&physical, output); err != nil { + return CompiledRecipeOutput{}, err + } + if err := appendRecipeIdentity(&physical, output); err != nil { + return CompiledRecipeOutput{}, err + } + if unnest := recipeUnnest(output); unnest != nil { + if err := appendRecipeUnnest(&physical, *unnest, output.RootResourceType); err != nil { + return CompiledRecipeOutput{}, err + } + } + dynamicMetadata, err := appendRecipeDynamicColumns(&physical, output, resolvedColumns) + if err != nil { + return CompiledRecipeOutput{}, err + } + if err := physical.Validate(); err != nil { + return CompiledRecipeOutput{}, fmt.Errorf("validate canonical physical plan: %w", err) + } + outputSchema := recipeOutputSchema(physical, output) + return CompiledRecipeOutput{ + Name: output.Name, RootResourceType: output.RootResourceType, + RowGrain: output.RowGrain, Columns: physicalOutputColumns(outputSchema), OutputSchema: outputSchema, + RowIdentity: cloneRowIdentity(&identity), DynamicColumns: dynamicMetadata, Plan: physical, + }, nil +} diff --git a/internal/dataframe/compiler/lower/recipe_dynamic_columns.go b/internal/dataframe/compiler/lower/recipe_dynamic_columns.go new file mode 100644 index 0000000..dfbb416 --- /dev/null +++ b/internal/dataframe/compiler/lower/recipe_dynamic_columns.go @@ -0,0 +1,234 @@ +package lower + +// This file contains the canonical recipe lowering boundary. Persisted +// recipes are a frontend: after resolution each output is lowered to the same +// ir.PhysicalPlan used by the GraphQL dataframe compiler. + +import ( + "fmt" + "strings" + + "github.com/calypr/loom/internal/dataframe/compiler/ir" + "github.com/calypr/loom/internal/dataframe/expression" + "github.com/calypr/loom/internal/dataframe/semantic" +) + +// CompiledRecipe is orchestration metadata around one canonical physical plan +// per output. It deliberately contains no recipe-specific traversal, +// projection, or renderer structures. Output order is the persisted recipe +// order and therefore part of the stable materialization contract. +func appendRecipeDynamicColumns(plan *ir.PhysicalPlan, output semantic.OutputPlan, resolvedColumns map[string][]semantic.ResolvedColumn) ([]DynamicColumnMetadata, error) { + dynamics := flattenRecipeDynamicMaps(output.Root) + if len(dynamics) == 0 { + return nil, nil + } + returnOp := -1 + for index := range plan.Operations { + if plan.Operations[index].Kind == ir.PhysicalReturnOp && plan.Operations[index].Return != nil { + returnOp = index + break + } + } + if returnOp < 0 { + return nil, fmt.Errorf("canonical plan has no RETURN operation for dynamic columns") + } + seen := map[string]bool{} + for _, projection := range plan.Operations[returnOp].Return.Projections { + seen[projection.Name] = true + } + metadata := make([]DynamicColumnMetadata, 0) + runtimeKeyFields := make([]ir.PhysicalExpressionProjection, 0, len(dynamics)) + setVariables := recipeSetVariables(*plan) + for _, dynamic := range dynamics { + columns := resolvedColumns[recipeDynamicMapKey(output.Name, dynamic)] + if dynamic.MaxColumns > 0 && len(columns) > dynamic.MaxColumns { + return nil, fmt.Errorf("dynamic map %q resolved %d columns, exceeding max %d", dynamic.Name, len(columns), dynamic.MaxColumns) + } + if len(columns) == 0 { + continue + } + resourceType := output.RootResourceType + if dynamic.ResourceType != "" { + resourceType = dynamic.ResourceType + } + source, err := lowerRecipeExpressionScoped(dynamic.Source.Expression, plan.BindVars, resourceType, recipeExpressionContexts(output)) + if err != nil { + return nil, fmt.Errorf("dynamic map %q source: %w", dynamic.Name, err) + } + if source.Cardinality != ir.PhysicalArrayCardinality { + return nil, fmt.Errorf("dynamic map %q source must remain array-valued", dynamic.Name) + } + key := ir.PhysicalExpression{Kind: ir.PhysicalValueExpression, Cardinality: ir.PhysicalScalarCardinality, NullBehavior: ir.PhysicalPreserveNull, Value: &ir.PhysicalValue{Variable: "dynamic_item"}} + if dynamic.Key != nil { + key, err = lowerDynamicItemExpression(*dynamic.Key, plan.BindVars, resourceType) + if err != nil { + return nil, fmt.Errorf("dynamic map %q key: %w", dynamic.Name, err) + } + } + value := ir.PhysicalExpression{Kind: ir.PhysicalValueExpression, Cardinality: ir.PhysicalScalarCardinality, NullBehavior: ir.PhysicalPreserveNull, Value: &ir.PhysicalValue{Variable: "dynamic_item"}} + if dynamic.Value != nil { + value, err = lowerDynamicItemExpression(*dynamic.Value, plan.BindVars, resourceType) + if err != nil { + return nil, fmt.Errorf("dynamic map %q value: %w", dynamic.Name, err) + } + } + if dynamic.ScopeAlias != "" { + if variable, ok := setVariables[dynamic.ScopeAlias]; ok { + rewriteRecipeExpressionVariable(&source, dynamic.ScopeAlias, variable) + if source.Extract != nil { + source.Extract.Source.Path = []string{"payload"} + } + } + } + runtimeName := dynamic.Name + if dynamic.ScopeAlias != "" && dynamic.ScopeAlias != "root" { + runtimeName = dynamic.ScopeAlias + "__" + dynamic.Name + } + familyVariable := dynamicFamilyVariable(*plan, runtimeName) + keyedMap := ir.PhysicalExpression{Kind: ir.PhysicalKeyedMapExpression, Cardinality: ir.PhysicalObjectCardinality, NullBehavior: ir.PhysicalPreserveNull, KeyedMap: &ir.PhysicalKeyedMap{Source: ir.ClonePhysicalExpression(source), ItemVariable: "dynamic_item", ItemKey: ir.ClonePhysicalExpression(key), ItemValue: ir.ClonePhysicalExpression(value), Reduction: ir.PhysicalMapFirstSorted, FlattenSource: true}} + insertPhysicalExpressionLet(plan, returnOp, familyVariable, keyedMap) + returnOp++ + runtimeKey := ir.PhysicalExpression{Kind: ir.PhysicalObjectKeysExpression, Cardinality: ir.PhysicalArrayCardinality, NullBehavior: ir.PhysicalEmptyOnNull, ObjectKeys: &ir.PhysicalObjectKeys{ObjectVariable: familyVariable}} + runtimeKeyFields = append(runtimeKeyFields, ir.PhysicalExpressionProjection{Name: runtimeName, Expression: runtimeKey}) + projectionPrefix := "" + if dynamic.ScopeAlias != "" && dynamic.ScopeAlias != "root" { + projectionPrefix = dynamic.ScopeAlias + "__" + } + for index, column := range columns { + outputName := projectionPrefix + column.Column.Name + if column.Column.Name == "" || seen[outputName] { + if seen[outputName] { + return nil, fmt.Errorf("dynamic column %q collides with another output column", outputName) + } + return nil, fmt.Errorf("dynamic map %q contains an empty resolved column name", dynamic.Name) + } + matchBindKey := nextDynamicBindKey(plan.BindVars, projectionPrefix+dynamic.Name, index) + plan.BindVars[matchBindKey] = column.Column.SourceKey + lookup := ir.PhysicalExpression{Kind: ir.PhysicalObjectLookupExpression, Cardinality: value.Cardinality, NullBehavior: value.NullBehavior, ObjectLookup: &ir.PhysicalObjectLookup{ObjectVariable: familyVariable, KeyBindKey: matchBindKey}} + plan.Operations[returnOp].Return.Projections = append(plan.Operations[returnOp].Return.Projections, ir.PhysicalProjection{Name: outputName, Expression: &lookup}) + seen[outputName] = true + metadata = append(metadata, DynamicColumnMetadata{Name: outputName, DynamicName: runtimeName, SourceKey: column.Column.SourceKey, ValueType: column.Column.ValueType}) + } + } + if len(runtimeKeyFields) > 0 { + runtime := ir.PhysicalExpression{Kind: ir.PhysicalObjectExpression, Cardinality: ir.PhysicalObjectCardinality, NullBehavior: ir.PhysicalPreserveNull, Object: &ir.PhysicalObject{Fields: runtimeKeyFields}} + plan.Operations[returnOp].Return.Projections = append(plan.Operations[returnOp].Return.Projections, ir.PhysicalProjection{Name: "__loom_dynamic_runtime_keys", Hidden: true, Expression: &runtime}) + } + return metadata, nil +} + +func flattenRecipeDynamicMaps(root semantic.SemanticNode) []semantic.SemanticDynamicMap { + result := make([]semantic.SemanticDynamicMap, 0) + var walk func(semantic.SemanticNode) + walk = func(node semantic.SemanticNode) { + result = append(result, node.DynamicMaps...) + for _, child := range node.Children { + walk(child) + } + } + walk(root) + return result +} + +func recipeDynamicMapKey(output string, dynamic semantic.SemanticDynamicMap) string { + if dynamic.ScopeAlias == "" || dynamic.ScopeAlias == "root" { + return output + ":" + dynamic.Name + } + return output + ":" + dynamic.ScopeAlias + ":" + dynamic.Name +} + +func nextDynamicBindKey(bindVars map[string]any, name string, index int) string { + base := "recipe_dynamic_" + strings.TrimSpace(name) + "_key_" + fmt.Sprintf("%d", index) + base = strings.Map(func(r rune) rune { + if (r >= 'a' && r <= 'z') || (r >= 'A' && r <= 'Z') || (r >= '0' && r <= '9') || r == '_' { + return r + } + return '_' + }, base) + key := base + for suffix := 1; ; suffix++ { + if _, exists := bindVars[key]; !exists { + return key + } + key = fmt.Sprintf("%s_%d", base, suffix) + } +} + +func lowerDynamicItemExpression(input semantic.SemanticExpression, bindVars map[string]any, resourceType string) (ir.PhysicalExpression, error) { + if input.Expression.Selector != nil && strings.EqualFold(strings.TrimSpace(input.Expression.Selector.Context), "item") { + selector, err := ParseSelector(input.Expression.Selector.Path) + if err != nil { + return ir.PhysicalExpression{}, err + } + path := make([]string, 0, len(selector.Steps)) + for _, step := range selector.Steps { + if step.Iterate || step.Index != nil { + return ir.PhysicalExpression{}, fmt.Errorf("dynamic item selector %q must be a scalar path", input.Expression.Selector.Path) + } + path = append(path, step.Field) + } + cardinality := ir.PhysicalScalarCardinality + if input.Type.Cardinality == expression.Many { + cardinality = ir.PhysicalArrayCardinality + } + behavior := ir.PhysicalPreserveNull + if input.Expression.NullBehavior == expression.NullEmpty { + behavior = ir.PhysicalEmptyOnNull + } + return ir.PhysicalExpression{Kind: ir.PhysicalValueExpression, Cardinality: cardinality, NullBehavior: behavior, Value: &ir.PhysicalValue{Variable: "dynamic_item", Path: path}}, nil + } + return lowerDynamicItemAST(input.Expression, bindVars, resourceType) +} + +// lowerDynamicItemAST lowers a dynamic value/key expression after semantic +// checking has already proven every selector. It intentionally handles item +// selectors as raw payload paths: the item resource type is a nested FHIR +// datatype (Extension, Identifier, ObservationComponent, ...), not the parent +// resource type carried by the dynamic map. Re-validating it against the +// parent schema would reject valid generic choice fields. +func lowerDynamicItemAST(input expression.Expression, bindVars map[string]any, resourceType string) (ir.PhysicalExpression, error) { + if input.Selector != nil && strings.EqualFold(strings.TrimSpace(input.Selector.Context), "item") { + selector, err := ParseSelector(input.Selector.Path) + if err != nil { + return ir.PhysicalExpression{}, err + } + path := make([]string, 0, len(selector.Steps)) + for _, step := range selector.Steps { + if step.Iterate || step.Index != nil { + return ir.PhysicalExpression{}, fmt.Errorf("dynamic item selector %q must be a scalar path", input.Selector.Path) + } + path = append(path, step.Field) + } + cardinality := ir.PhysicalScalarCardinality + if input.Type.Cardinality == expression.Many { + cardinality = ir.PhysicalArrayCardinality + } + behavior := ir.PhysicalPreserveNull + if input.NullBehavior == expression.NullEmpty { + behavior = ir.PhysicalEmptyOnNull + } + return ir.PhysicalExpression{Kind: ir.PhysicalValueExpression, Cardinality: cardinality, NullBehavior: behavior, Value: &ir.PhysicalValue{Variable: "dynamic_item", Path: path}}, nil + } + if input.Call == nil { + return lowerRecipeExpressionScoped(input, bindVars, resourceType, map[string]string{"root": resourceType}) + } + call := &ir.PhysicalCall{Name: strings.ToLower(strings.TrimSpace(input.Call.Name))} + if input.Call.Target != nil { + call.TargetKind = string(input.Call.Target.Kind) + } + for index, argument := range input.Call.Args { + if call.Name == "cast" && input.Call.Target != nil && index == 1 { + continue + } + lowered, err := lowerDynamicItemAST(argument, bindVars, resourceType) + if err != nil { + return ir.PhysicalExpression{}, fmt.Errorf("call %q argument %d: %w", input.Call.Name, index, err) + } + call.Args = append(call.Args, lowered) + } + cardinality := ir.PhysicalScalarCardinality + if input.Type.Cardinality == expression.Many { + cardinality = ir.PhysicalArrayCardinality + } + return ir.PhysicalExpression{Kind: ir.PhysicalCallExpression, Cardinality: cardinality, NullBehavior: ir.PhysicalPreserveNull, Call: call}, nil +} diff --git a/internal/dataframe/compiler/lower/recipe_expression.go b/internal/dataframe/compiler/lower/recipe_expression.go new file mode 100644 index 0000000..d47bab1 --- /dev/null +++ b/internal/dataframe/compiler/lower/recipe_expression.go @@ -0,0 +1,127 @@ +package lower + +// This file is the narrow bridge from the persisted, backend-neutral +// expression AST to the physical expression IR. It deliberately performs no +// output-name or resource-specific dispatch: selectors and calls remain data +// all the way to the renderer. + +import ( + "fmt" + "strings" + + "github.com/calypr/loom/fhirschema" + "github.com/calypr/loom/internal/dataframe/compiler/ir" + "github.com/calypr/loom/internal/dataframe/expression" + "github.com/calypr/loom/internal/dataframe/spec" +) + +// LowerRecipeExpression lowers one checked recipe expression into the typed +// physical tree. bindVars receives literal values using deterministic, +// collision-safe keys. resourceType is used to retain schema provenance for +// selector validation; it is not used to select implementation behavior. +func LowerRecipeExpression(input expression.Expression, bindVars map[string]any, resourceType string) (ir.PhysicalExpression, error) { + if bindVars == nil { + return ir.PhysicalExpression{}, fmt.Errorf("bind vars map is required") + } + return lowerRecipeExpression(input, bindVars, resourceType) +} + +func lowerRecipeExpression(input expression.Expression, bindVars map[string]any, resourceType string) (ir.PhysicalExpression, error) { + cardinality := ir.PhysicalScalarCardinality + behavior := ir.PhysicalPreserveNull + if input.Type.Valid() { + if input.Type.Cardinality == expression.Many { + cardinality = ir.PhysicalArrayCardinality + } + } + switch input.NullBehavior { + case expression.NullEmpty: + behavior = ir.PhysicalEmptyOnNull + case expression.NullError: + // Physical execution has no error-valued AQL expression. Preserve null + // here and let the semantic checker reject error-sensitive recipes until + // an explicit error channel is added to the execution contract. + behavior = ir.PhysicalPreserveNull + } + result := ir.PhysicalExpression{Cardinality: cardinality, NullBehavior: behavior} + switch input.Kind { + case expression.SelectorNode: + if input.Selector == nil { + return ir.PhysicalExpression{}, fmt.Errorf("selector node is missing selector") + } + selectorPath := strings.TrimSpace(input.Selector.Path) + if err := input.Selector.Validate(); err != nil { + return ir.PhysicalExpression{}, err + } + selector, err := spec.ParseSelector(selectorPath) + if err != nil { + return ir.PhysicalExpression{}, fmt.Errorf("parse selector %q: %w", selectorPath, err) + } + // Semantic recipe plans normally carry a checked expression type, but + // the narrow public lowering entrypoint also accepts the persisted AST. + // Recover repetition from generated schema metadata in that case so a + // repeated FHIR selector cannot silently become a scalar physical value. + if !input.Type.Valid() { + if metadata, ok := fhirschema.ResolveTerminalScalarMetadata(resourceType, selector.CanonicalPath()); ok && metadata.Repeated { + cardinality = ir.PhysicalArrayCardinality + } + } + result.Cardinality = cardinality + variable := strings.TrimSpace(input.Selector.Context) + if variable == "" { + variable = "root" + } + result.Kind = ir.PhysicalExtractExpression + result.Extract = &ir.PhysicalExtract{ + Source: ir.PhysicalValue{Variable: variable, Path: []string{"payload"}}, + ResourceType: resourceType, + Selector: selector, + ExecutionMode: selectorExecutionModeForExpression(resourceType, selector, nil, ir.PhysicalValue{Variable: variable, Path: []string{"payload"}}, cardinality, behavior), + } + return result, nil + case expression.LiteralNode: + if input.Literal == nil { + return ir.PhysicalExpression{}, fmt.Errorf("literal node is missing literal") + } + key := nextLiteralBindKey(bindVars) + bindVars[key] = input.Literal.Value + result.Kind = ir.PhysicalLiteralExpression + result.Literal = &ir.PhysicalLiteral{BindKey: key} + return result, nil + case expression.CallNode: + if input.Call == nil { + return ir.PhysicalExpression{}, fmt.Errorf("call node is missing call") + } + call := &ir.PhysicalCall{Name: strings.ToLower(strings.TrimSpace(input.Call.Name))} + if input.Call.Target != nil { + call.TargetKind = string(input.Call.Target.Kind) + } + for index, arg := range input.Call.Args { + // Recipe wire syntax represents cast as (value, target literal), + // while the typed AST stores target in Call.Target. The target is + // metadata, not a runtime physical argument. + if call.Name == "cast" && input.Call.Target != nil && index == 1 { + continue + } + lowered, err := lowerRecipeExpression(arg, bindVars, resourceType) + if err != nil { + return ir.PhysicalExpression{}, fmt.Errorf("call %q argument %d: %w", input.Call.Name, index, err) + } + call.Args = append(call.Args, lowered) + } + result.Kind = ir.PhysicalCallExpression + result.Call = call + return result, nil + default: + return ir.PhysicalExpression{}, fmt.Errorf("unsupported expression node %q", input.Kind) + } +} + +func nextLiteralBindKey(bindVars map[string]any) string { + for index := 0; ; index++ { + key := fmt.Sprintf("recipe_literal_%d", index) + if _, exists := bindVars[key]; !exists { + return key + } + } +} diff --git a/internal/dataframe/compiler/lower/recipe_expression_lowering.go b/internal/dataframe/compiler/lower/recipe_expression_lowering.go new file mode 100644 index 0000000..662943b --- /dev/null +++ b/internal/dataframe/compiler/lower/recipe_expression_lowering.go @@ -0,0 +1,286 @@ +package lower + +// This file contains the canonical recipe lowering boundary. Persisted +// recipes are a frontend: after resolution each output is lowered to the same +// ir.PhysicalPlan used by the GraphQL dataframe compiler. + +import ( + "fmt" + "strings" + + "github.com/calypr/loom/fhirschema" + "github.com/calypr/loom/internal/dataframe/compiler/ir" + "github.com/calypr/loom/internal/dataframe/expression" + "github.com/calypr/loom/internal/dataframe/semantic" +) + +// CompiledRecipe is orchestration metadata around one canonical physical plan +// per output. It deliberately contains no recipe-specific traversal, +// projection, or renderer structures. Output order is the persisted recipe +// order and therefore part of the stable materialization contract. +func patchRecipeExpressions(plan *ir.PhysicalPlan, output semantic.OutputPlan) error { + if plan == nil { + return fmt.Errorf("physical plan is nil") + } + fields := map[string]semantic.SemanticExpression{} + for _, field := range output.Fields { + fields[field.Name] = field.Expr + } + for _, operation := range plan.Operations { + if operation.Kind != ir.PhysicalReturnOp || operation.Return == nil { + continue + } + for index := range operation.Return.Projections { + projection := &operation.Return.Projections[index] + field, ok := fields[projection.Name] + if !ok { + continue + } + // Selector expressions are already lowered by the generic semantic + // node, including AUTO/FIRST/ALL/DISTINCT, fallbacks, and prepared + // selector metadata. Replacing that physical extract with the raw + // recipe expression would drop the legacy AUTO cardinality contract + // (for example, repeated selectors become FIRST). Only richer + // expression ASTs need a post-lowering patch. + if field.Expression.Selector != nil { + continue + } + physical, err := lowerRecipeExpressionScoped(field.Expression, plan.BindVars, output.RootResourceType, recipeExpressionContexts(output)) + if err != nil { + return fmt.Errorf("field expression: %w", err) + } + projection.Expression = &physical + projection.Value = ir.PhysicalValue{} + } + } + setVariables := recipeSetVariables(*plan) + + // Child fields are returned as flattened projections named alias__field by + // the generic lowerer. Match those names deterministically and retain the + // child resource type for selector provenance. + var walk func(semantic.SemanticNode) error + walk = func(node semantic.SemanticNode) error { + for _, child := range node.Children { + for _, field := range child.Fields { + name := child.Alias + "__" + field.Name + for index := range plan.Operations { + op := &plan.Operations[index] + if op.Kind != ir.PhysicalReturnOp || op.Return == nil { + continue + } + for projectionIndex := range op.Return.Projections { + projection := &op.Return.Projections[projectionIndex] + if projection.Name != name { + continue + } + if field.Expr != nil && field.Expr.Selector != nil { + continue + } + physical, err := lowerRecipeExpressionScoped(*field.Expr, plan.BindVars, output.RootResourceType, recipeExpressionContexts(output)) + if err != nil { + return fmt.Errorf("child field %q: %w", name, err) + } + setVariable, ok := setVariables[child.Alias] + if !ok { + return fmt.Errorf("child field %q has no canonical set variable for alias %q", name, child.Alias) + } + rewriteRecipeExpressionVariable(&physical, child.Alias, setVariable) + projection.Expression = &physical + projection.Value = ir.PhysicalValue{} + } + } + } + if err := walk(child); err != nil { + return err + } + } + return nil + } + return walk(output.Root) +} + +func recipeSetVariables(plan ir.PhysicalPlan) map[string]string { + variables := map[string]string{"root": "root"} + for _, operation := range plan.Operations { + if operation.Kind == ir.PhysicalSetOp && operation.Set != nil && operation.Source.SemanticNode != "" { + variables[operation.Source.SemanticNode] = operation.Set.Variable + } + } + return variables +} + +func insertPhysicalExpressionLet(plan *ir.PhysicalPlan, returnIndex int, variable string, expression ir.PhysicalExpression) { + operation := ir.PhysicalOperation{Kind: ir.PhysicalExpressionLetOp, Source: ir.PhysicalSource{SemanticField: "shared_family"}, ExpressionLet: &ir.PhysicalExpressionLet{Variable: variable, Expression: expression}} + plan.Operations = append(plan.Operations, ir.PhysicalOperation{}) + copy(plan.Operations[returnIndex+1:], plan.Operations[returnIndex:]) + plan.Operations[returnIndex] = operation +} + +func dynamicFamilyVariable(plan ir.PhysicalPlan, name string) string { + base := "__loom_family_" + sanitizeColumnName(name) + if base == "__loom_family_" { + base = "__loom_family_dynamic" + } + used := map[string]bool{} + for _, operation := range plan.Operations { + if operation.ExpressionLet != nil { + used[operation.ExpressionLet.Variable] = true + } + if operation.Set != nil { + used[operation.Set.Variable] = true + } + } + if !used[base] { + return base + } + for index := 1; ; index++ { + candidate := fmt.Sprintf("%s_%d", base, index) + if !used[candidate] { + return candidate + } + } +} + +func rewriteRecipeExpressionVariable(value *ir.PhysicalExpression, from, to string) { + if value == nil { + return + } + if value.Extract != nil && value.Extract.Source.Variable == from { + value.Extract.Source.Variable = to + } + if value.Call != nil { + for index := range value.Call.Args { + rewriteRecipeExpressionVariable(&value.Call.Args[index], from, to) + } + } +} + +func recipeExpressionContexts(output semantic.OutputPlan) map[string]string { + contexts := map[string]string{"root": output.RootResourceType} + var walk func(semantic.SemanticNode) + walk = func(node semantic.SemanticNode) { + for _, child := range node.Children { + contexts[child.Alias] = child.ResourceType + walk(child) + } + } + walk(output.Root) + if unnest := recipeUnnest(output); unnest != nil && unnest.Source.Expression.Selector != nil { + selector := unnest.Source.Expression.Selector + path := strings.TrimSuffix(strings.TrimPrefix(selector.Path, "."), "[]") + if semantics, ok := fhirschema.ResolveFieldSemantics(output.RootResourceType, path+"[]"); ok && semantics.Reference != "" { + contexts[unnest.As] = semantics.Reference + } + } + return contexts +} + +// lowerRecipeExpressionScoped is the recipe-expression counterpart to the +// generic lowerer's selector classifier. A recipe expression may combine +// selectors from several lexical bindings (root plus an expanded item), so a +// single resourceType argument is insufficient. Each selector is lowered in +// its binding's generated schema type and item selectors are rooted at the +// item value rather than at a FHIR document payload. +func lowerRecipeExpressionScoped(input expression.Expression, bindVars map[string]any, rootResourceType string, contexts map[string]string) (ir.PhysicalExpression, error) { + if input.Selector != nil { + variable := strings.TrimSpace(input.Selector.Context) + if variable == "" { + variable = "root" + } + resourceType := rootResourceType + if resolved, ok := contexts[variable]; ok { + resourceType = resolved + } + physical, err := LowerRecipeExpression(input, bindVars, resourceType) + if err != nil { + return ir.PhysicalExpression{}, err + } + if variable != "root" { + physical.Extract.Source.Variable = variable + physical.Extract.Source.Path = nil + } + return physical, nil + } + if input.Literal != nil { + return LowerRecipeExpression(input, bindVars, rootResourceType) + } + if input.Call == nil { + return ir.PhysicalExpression{}, fmt.Errorf("recipe expression has no selector, literal, or call") + } + cardinality := ir.PhysicalScalarCardinality + if input.Type.Cardinality == expression.Many { + cardinality = ir.PhysicalArrayCardinality + } + behavior := ir.PhysicalPreserveNull + if input.NullBehavior == expression.NullEmpty { + behavior = ir.PhysicalEmptyOnNull + } + call := &ir.PhysicalCall{Name: strings.ToLower(strings.TrimSpace(input.Call.Name))} + if input.Call.Target != nil { + call.TargetKind = string(input.Call.Target.Kind) + } + for index, argument := range input.Call.Args { + if call.Name == "cast" && input.Call.Target != nil && index == 1 { + continue + } + lowered, err := lowerRecipeExpressionScoped(argument, bindVars, rootResourceType, contexts) + if err != nil { + return ir.PhysicalExpression{}, fmt.Errorf("call %q argument %d: %w", input.Call.Name, index, err) + } + call.Args = append(call.Args, lowered) + } + return ir.PhysicalExpression{Kind: ir.PhysicalCallExpression, Cardinality: cardinality, NullBehavior: behavior, Call: call}, nil +} + +func appendRecipeIdentity(plan *ir.PhysicalPlan, output semantic.OutputPlan) error { + if output.Identity == nil { + return nil + } + physical, err := lowerRecipeExpressionScoped(output.Identity.Expression, plan.BindVars, output.RootResourceType, recipeExpressionContexts(output)) + if err != nil { + return fmt.Errorf("identity: %w", err) + } + for index := range plan.Operations { + operation := &plan.Operations[index] + if operation.Kind != ir.PhysicalReturnOp || operation.Return == nil { + continue + } + operation.Return.Projections = append(operation.Return.Projections, ir.PhysicalProjection{Name: "__loom_row_id", Expression: &physical}) + return nil + } + return fmt.Errorf("canonical plan has no RETURN operation for identity") +} + +func recipeUnnest(output semantic.OutputPlan) *semantic.SemanticUnnest { + if output.Unnest != nil { + copy := *output.Unnest + return © + } + return nil +} + +func appendRecipeUnnest(plan *ir.PhysicalPlan, unnest semantic.SemanticUnnest, resourceType string) error { + if err := unnest.Validate(); err != nil { + return fmt.Errorf("unnest: %w", err) + } + expression, err := lowerRecipeExpressionScoped(unnest.Source.Expression, plan.BindVars, resourceType, map[string]string{"root": resourceType}) + if err != nil { + return fmt.Errorf("unnest source: %w", err) + } + joinMode := ir.PhysicalUnnestJoinMode(unnest.JoinMode) + operation := ir.PhysicalOperation{Kind: ir.PhysicalUnnestOp, Source: ir.PhysicalSource{ResourceType: resourceType, SemanticField: "expand"}, Unnest: &ir.PhysicalUnnest{InputVariable: "root", OutputVariable: unnest.As, Ordinality: unnest.Ordinality, Expression: expression, JoinMode: joinMode}} + // Place the cardinality barrier immediately after root qualification and + // before any child set materialization. This ensures child operations can + // never accidentally consume an item binding from a later scope. + insert := len(plan.Operations) + for index, current := range plan.Operations { + if current.Kind == ir.PhysicalTraversalOp || current.Kind == ir.PhysicalSetOp || current.Kind == ir.PhysicalReturnOp { + insert = index + break + } + } + plan.Operations = append(plan.Operations, ir.PhysicalOperation{}) + copy(plan.Operations[insert+1:], plan.Operations[insert:]) + plan.Operations[insert] = operation + return nil +} diff --git a/internal/dataframe/compiler/lower/recipe_expression_test.go b/internal/dataframe/compiler/lower/recipe_expression_test.go new file mode 100644 index 0000000..a7ea2d7 --- /dev/null +++ b/internal/dataframe/compiler/lower/recipe_expression_test.go @@ -0,0 +1,71 @@ +package lower + +import ( + "testing" + + "github.com/calypr/loom/fhirschema" + "github.com/calypr/loom/internal/dataframe/compiler/ir" + "github.com/calypr/loom/internal/dataframe/expression" +) + +func TestLowerRecipeExpressionUsesTypedPhysicalCallsAndBinds(t *testing.T) { + stringType := expression.Type{Kind: expression.KindString, Cardinality: expression.RequiredOne} + input := expression.Function("concat", + expression.Constant(stringType, "left"), + expression.Constant(stringType, "right"), + ) + binds := map[string]any{} + physical, err := LowerRecipeExpression(input, binds, "Patient") + if err != nil { + t.Fatal(err) + } + if physical.Kind != ir.PhysicalCallExpression || physical.Call == nil || physical.Call.Name != "concat" { + t.Fatalf("unexpected physical call: %#v", physical) + } + if len(physical.Call.Args) != 2 || len(binds) != 2 { + t.Fatalf("literal arguments were not lowered to bind-backed nodes: physical=%#v binds=%#v", physical, binds) + } + for _, arg := range physical.Call.Args { + if arg.Kind != ir.PhysicalLiteralExpression || arg.Literal == nil { + t.Fatalf("literal argument was not preserved as physical literal: %#v", arg) + } + if _, ok := binds[arg.Literal.BindKey]; !ok { + t.Fatalf("literal bind %q is missing: %#v", arg.Literal.BindKey, binds) + } + } +} + +func TestLowerRecipeExpressionSpecializesRepeatedSelector(t *testing.T) { + selector := expression.Select(expression.SelectorRef{Context: "root", Path: "component[].valueInteger"}) + physical, err := LowerRecipeExpression(selector, map[string]any{}, "Observation") + if err != nil { + t.Fatal(err) + } + if physical.Kind != ir.PhysicalExtractExpression || physical.Extract == nil { + t.Fatalf("selector was not lowered to physical extract: %#v", physical) + } + if physical.Cardinality != ir.PhysicalArrayCardinality { + t.Fatalf("schema repetition was not preserved in recipe cardinality: %q", physical.Cardinality) + } + if physical.Extract.ExecutionMode != ir.PhysicalSelectorConditionalArray { + t.Fatalf("schema-proven repeated recipe selector did not specialize: %q", physical.Extract.ExecutionMode) + } +} + +func TestSelectorModeClassifierKeepsPredicateAndFallbackGeneric(t *testing.T) { + selector, err := ParseSelector("identifier[].value") + if err != nil { + t.Fatal(err) + } + selector.Filter = &fhirschema.ContainsFilter{Field: "system", Needle: "case_id"} + if got := selectorExecutionModeForExpression("Patient", selector, nil, ir.PhysicalValue{Variable: "root", Path: []string{"payload"}}, ir.PhysicalArrayCardinality, ir.PhysicalEmptyOnNull); got != ir.PhysicalSelectorGeneric { + t.Fatalf("predicate selector unexpectedly specialized: %q", got) + } + plain, err := ParseSelector("gender") + if err != nil { + t.Fatal(err) + } + if got := selectorExecutionModeForExpression("Patient", plain, []Selector{selector}, ir.PhysicalValue{Variable: "root", Path: []string{"payload"}}, ir.PhysicalScalarCardinality, ir.PhysicalPreserveNull); got != ir.PhysicalSelectorGeneric { + t.Fatalf("fallback selector unexpectedly specialized: %q", got) + } +} diff --git a/internal/dataframe/compiler/lower/recipe_output_test.go b/internal/dataframe/compiler/lower/recipe_output_test.go new file mode 100644 index 0000000..e838139 --- /dev/null +++ b/internal/dataframe/compiler/lower/recipe_output_test.go @@ -0,0 +1,480 @@ +package lower + +import ( + "strconv" + "strings" + "testing" + + "github.com/calypr/loom/internal/authscope" + "github.com/calypr/loom/internal/dataframe/compiler/ir" + "github.com/calypr/loom/internal/dataframe/compiler/render/aql" + "github.com/calypr/loom/internal/dataframe/recipe" + "github.com/calypr/loom/internal/dataframe/semantic" +) + +func TestCompileResolvedRecipePlanProducesCanonicalPhysicalPlans(t *testing.T) { + bundle := resolvedDefaultBundleForLowerTest(t) + plan, err := semantic.BuildRecipePlan(bundle, recipe.RuntimeBindings{Project: "project", DatasetGeneration: "generation"}) + if err != nil { + t.Fatal(err) + } + resolved, err := semantic.ResolveRecipePlan(plan, "scope", "generation") + if err != nil { + t.Fatal(err) + } + compiled, err := CompileResolvedRecipePlan(resolved, DefaultPhysicalOptimizationPolicy()) + if err != nil { + t.Fatal(err) + } + if len(compiled.Outputs) != len(plan.Outputs) { + t.Fatalf("compiled output count = %d, want %d", len(compiled.Outputs), len(plan.Outputs)) + } + for _, output := range compiled.Outputs { + if len(output.Plan.Operations) == 0 { + t.Fatalf("output %q has no canonical operations", output.Name) + } + if err := output.Plan.Validate(); err != nil { + t.Fatalf("output %q canonical plan invalid: %v", output.Name, err) + } + if output.Plan.Operations[0].Kind != PhysicalRootScanOp { + t.Fatalf("output %q first operation = %s, want ROOT_SCAN", output.Name, output.Plan.Operations[0].Kind) + } + } +} + +func TestCompiledRecipeOutputSchemaMatchesFinalReturnProjectionOrder(t *testing.T) { + bundle := resolvedDefaultBundleForLowerTest(t) + plan, err := semantic.BuildRecipePlan(bundle, recipe.RuntimeBindings{Project: "project", DatasetGeneration: "generation"}) + if err != nil { + t.Fatal(err) + } + resolved, err := semantic.ResolveRecipePlan(plan, "scope", "generation") + if err != nil { + t.Fatal(err) + } + compiled, err := CompileResolvedRecipePlan(resolved, DefaultPhysicalOptimizationPolicy()) + if err != nil { + t.Fatal(err) + } + for _, output := range compiled.Outputs { + var projections []ir.PhysicalProjection + for _, operation := range output.Plan.Operations { + if operation.Kind == ir.PhysicalReturnOp && operation.Return != nil { + projections = operation.Return.Projections + break + } + } + if len(projections) == 0 { + t.Fatalf("output %q has no return projections", output.Name) + } + if len(output.OutputSchema) != len(projections) { + t.Fatalf("output %q schema length=%d projections=%d", output.Name, len(output.OutputSchema), len(projections)) + } + for index, projection := range projections { + column := output.OutputSchema[index] + if column.Name != projection.Name { + t.Fatalf("output %q schema[%d]=%q projection=%q", output.Name, index, column.Name, projection.Name) + } + wantInternal := projection.Hidden || projection.Name == "_key" || strings.HasPrefix(projection.Name, "__loom_") + if column.Internal != wantInternal { + t.Fatalf("output %q column %q internal=%v want %v", output.Name, column.Name, column.Internal, wantInternal) + } + } + } +} + +func TestCompileResolvedRecipePlanUsesCanonicalUnnest(t *testing.T) { + bundle := resolvedDefaultBundleForLowerTest(t) + plan, err := semantic.BuildRecipePlan(bundle, recipe.RuntimeBindings{Project: "project", DatasetGeneration: "generation"}) + if err != nil { + t.Fatal(err) + } + group := plan.Outputs[len(plan.Outputs)-1] + plan.Outputs = []semantic.OutputPlan{group} + resolved, err := semantic.ResolveRecipePlan(plan, "scope", "generation") + if err != nil { + t.Fatal(err) + } + compiled, err := CompileResolvedRecipePlan(resolved, DefaultPhysicalOptimizationPolicy()) + if err != nil { + t.Fatal(err) + } + var found bool + for _, output := range compiled.Outputs { + if output.Name != "GroupMember" { + continue + } + for _, operation := range output.Plan.Operations { + if operation.Kind != ir.PhysicalUnnestOp || operation.Unnest == nil { + continue + } + found = true + if operation.Unnest.InputVariable != "root" || operation.Unnest.OutputVariable != "member" { + t.Fatalf("unexpected unnest bindings: %#v", operation.Unnest) + } + } + } + if !found { + t.Fatal("GroupMember output did not lower to canonical UNNEST") + } +} + +func resolvedDefaultBundleForLowerTest(t *testing.T) recipe.Bundle { + t.Helper() + bundle, err := recipe.DefaultACEDBundle() + if err != nil { + t.Fatal(err) + } + var walk func([]recipe.DynamicColumn, []recipe.Pivot, []recipe.Traversal) + walk = func(dynamicColumns []recipe.DynamicColumn, pivots []recipe.Pivot, traversals []recipe.Traversal) { + for i := range dynamicColumns { + if len(dynamicColumns[i].Columns) == 0 { + dynamicColumns[i].Columns = []string{"test"} + } + } + for i := range pivots { + pivots[i].Discovery = nil + if len(pivots[i].Columns) == 0 { + pivots[i].Columns = []string{"test"} + } + if pivots[i].ColumnExpr.Select == "" { + pivots[i].ColumnExpr.Select = "code.coding[].display" + } + if pivots[i].ValueExpr.Select == "" { + pivots[i].ValueExpr.Select = "valueString" + } + } + for i := range traversals { + walk(traversals[i].DynamicColumns, traversals[i].Pivots, traversals[i].Traversals) + } + } + for i := range bundle.Outputs { + walk(bundle.Outputs[i].DynamicColumns, bundle.Outputs[i].Pivots, bundle.Outputs[i].Traversals) + } + return bundle +} + +func TestCompileResolvedRecipePlanLowersBoundedDynamicMap(t *testing.T) { + bundle := recipe.Bundle{RecipeSchemaVersion: 1, Name: "dynamic", TranslationVersion: "test", Outputs: []recipe.Output{{Name: "Patient", RootResourceType: "Patient", RowGrain: "patient", Fields: []recipe.Field{{Name: "id", Expr: recipe.Expression{Select: "root.id"}}}, DynamicColumns: []recipe.DynamicColumn{{Name: "dynamic", Source: recipe.Expression{Select: "root.identifier[].value"}, Columns: []string{"x"}}}}}} + var err error + plan, err := semantic.BuildRecipePlan(bundle, recipe.RuntimeBindings{Project: "project", DatasetGeneration: "generation"}) + if err != nil { + t.Fatal(err) + } + resolved, err := semantic.ResolveRecipePlan(plan, "scope", "generation") + if err != nil { + t.Fatal(err) + } + compiled, err := CompileResolvedRecipePlan(resolved, DefaultPhysicalOptimizationPolicy()) + if err != nil { + t.Fatal(err) + } + if len(compiled.Outputs) != 1 || len(compiled.Outputs[0].DynamicColumns) != 1 { + t.Fatalf("dynamic metadata = %#v", compiled.Outputs) + } + found := false + foundRuntimeKeys := false + for _, operation := range compiled.Outputs[0].Plan.Operations { + if operation.Kind != ir.PhysicalReturnOp || operation.Return == nil { + continue + } + for _, projection := range operation.Return.Projections { + if projection.Name == "__loom_dynamic_runtime_keys" && projection.Hidden && projection.Expression != nil && projection.Expression.Kind == ir.PhysicalObjectExpression { + foundRuntimeKeys = true + } + if projection.Name == "dynamic_x" && projection.Expression != nil && projection.Expression.Kind == ir.PhysicalObjectLookupExpression { + found = true + } + } + } + if !found || !foundRuntimeKeys { + t.Fatal("dynamic projection was not lowered to canonical LOOKUP") + } +} + +func TestCompileResolvedRecipePlanLowersDynamicItemLookup(t *testing.T) { + bundle := recipe.Bundle{RecipeSchemaVersion: 1, Name: "dynamic-item", TranslationVersion: "test", Outputs: []recipe.Output{{Name: "Patient", RootResourceType: "Patient", RowGrain: "patient", Fields: []recipe.Field{{Name: "id", Expr: recipe.Expression{Select: "root.id"}}}, DynamicColumns: []recipe.DynamicColumn{{Name: "extension", Source: recipe.Expression{Select: "root.extension[]"}, Key: &recipe.Expression{Select: "item.url"}, Value: &recipe.Expression{Select: "item.url"}, Columns: []string{"http://example.org/code"}}}}}} + plan, err := semantic.BuildRecipePlan(bundle, recipe.RuntimeBindings{Project: "project", DatasetGeneration: "generation"}) + if err != nil { + t.Fatal(err) + } + resolved, err := semantic.ResolveRecipePlan(plan, "scope", "generation") + if err != nil { + t.Fatal(err) + } + compiled, err := CompileResolvedRecipePlan(resolved, DefaultPhysicalOptimizationPolicy()) + if err != nil { + t.Fatal(err) + } + if len(compiled.Outputs) != 1 || len(compiled.Outputs[0].DynamicColumns) != 1 { + t.Fatalf("dynamic metadata = %#v", compiled.Outputs) + } + for _, operation := range compiled.Outputs[0].Plan.Operations { + if operation.Kind != ir.PhysicalReturnOp || operation.Return == nil { + continue + } + for _, projection := range operation.Return.Projections { + if projection.Name != "extension_http___example_org_code" || projection.Expression == nil || projection.Expression.ObjectLookup == nil { + continue + } + for _, candidate := range compiled.Outputs[0].Plan.Operations { + if candidate.Kind == ir.PhysicalExpressionLetOp && candidate.ExpressionLet != nil && candidate.ExpressionLet.Expression.KeyedMap != nil { + if candidate.ExpressionLet.Expression.KeyedMap.ItemKey.Value == nil || candidate.ExpressionLet.Expression.KeyedMap.ItemKey.Value.Path[0] != "url" { + t.Fatalf("item key was not lowered to dynamic item path: %#v", candidate.ExpressionLet.Expression.KeyedMap.ItemKey) + } + return + } + } + } + } + t.Fatal("dynamic item lookup projection not found") +} + +func TestCompileResolvedRecipePlanRendersDynamicLookupThroughCanonicalRenderer(t *testing.T) { + bundle := recipe.Bundle{RecipeSchemaVersion: 1, Name: "dynamic", TranslationVersion: "test", Outputs: []recipe.Output{{Name: "Patient", RootResourceType: "Patient", RowGrain: "patient", Fields: []recipe.Field{{Name: "id", Expr: recipe.Expression{Select: "root.id"}}}, DynamicColumns: []recipe.DynamicColumn{{Name: "dynamic", Source: recipe.Expression{Select: "root.identifier[].value"}, Columns: []string{"x"}}}}}} + plan, err := semantic.BuildRecipePlan(bundle, recipe.RuntimeBindings{Project: "project", DatasetGeneration: "generation"}) + if err != nil { + t.Fatal(err) + } + resolved, err := semantic.ResolveRecipePlan(plan, "scope", "generation") + if err != nil { + t.Fatal(err) + } + compiled, err := CompileResolvedRecipePlan(resolved, DefaultPhysicalOptimizationPolicy()) + if err != nil { + t.Fatal(err) + } + rendered, err := aql.RenderPhysicalPlan(compiled.Outputs[0].Plan) + if err != nil { + t.Fatal(err) + } + if !strings.Contains(rendered.Query, "__loom_family") || !strings.Contains(rendered.Query, "COLLECT __loom_keyed_group_key") { + t.Fatalf("canonical renderer omitted lookup shape: %s", rendered.Query) + } +} + +func TestWideDynamicFamilyUsesOneSourceComputation(t *testing.T) { + columns := make([]string, 150) + for index := range columns { + columns[index] = "identifier-" + strconv.Itoa(index) + } + bundle := recipe.Bundle{RecipeSchemaVersion: 1, Name: "wide-dynamic", TranslationVersion: "test", Outputs: []recipe.Output{{ + Name: "Patient", RootResourceType: "Patient", RowGrain: "patient", + Fields: []recipe.Field{{Name: "id", Expr: recipe.Expression{Select: "root.id"}}}, + DynamicColumns: []recipe.DynamicColumn{{Name: "identifiers", Source: recipe.Expression{Select: "root.identifier[]"}, Key: &recipe.Expression{Select: "item.value"}, Value: &recipe.Expression{Select: "item.value"}, Columns: columns}}, + }}} + plan, err := semantic.BuildRecipePlan(bundle, recipe.RuntimeBindings{Project: "project", DatasetGeneration: "generation"}) + if err != nil { + t.Fatal(err) + } + resolved, err := semantic.ResolveRecipePlan(plan, "scope", "generation") + if err != nil { + t.Fatal(err) + } + compiled, err := CompileResolvedRecipePlan(resolved, DefaultPhysicalOptimizationPolicy()) + if err != nil { + t.Fatal(err) + } + rendered, err := aql.RenderPhysicalPlan(compiled.Outputs[0].Plan) + if err != nil { + t.Fatal(err) + } + if got := strings.Count(rendered.Query, "root.payload.identifier"); got != 2 { + t.Fatalf("wide family source rendered %d times, want one guarded shared computation\n%s", got, rendered.Query) + } + if got := strings.Count(rendered.Query, "__loom_family_identifiers["); got != len(columns) { + t.Fatalf("wide family lookups = %d, want %d", got, len(columns)) + } + t.Logf("wide family columns=%d aql_bytes=%d bind_vars=%d operations=%d", len(columns), len(rendered.Query), len(rendered.BindVars), len(compiled.Outputs[0].Plan.Operations)) +} + +func TestCompileResolvedRecipePlanLowersTraversalDynamicMap(t *testing.T) { + bundle := recipe.Bundle{RecipeSchemaVersion: 1, Name: "nested-dynamic", TranslationVersion: "test", Outputs: []recipe.Output{{Name: "ResearchSubject", RootResourceType: "ResearchSubject", RowGrain: "study_enrollment", Traversals: []recipe.Traversal{{Name: "subject_Patient", ToResourceType: "Patient", Alias: "patient", DynamicColumns: []recipe.DynamicColumn{{Name: "identifiers", Source: recipe.Expression{Select: "patient.identifier[]"}, Key: &recipe.Expression{Select: "item.value"}, Columns: []string{"identifier"}}}}}}}} + plan, err := semantic.BuildRecipePlan(bundle, recipe.RuntimeBindings{Project: "project", DatasetGeneration: "generation"}) + if err != nil { + t.Fatal(err) + } + resolved, err := semantic.ResolveRecipePlan(plan, "scope", "generation") + if err != nil { + t.Fatal(err) + } + compiled, err := CompileResolvedRecipePlan(resolved, DefaultPhysicalOptimizationPolicy()) + if err != nil { + t.Fatal(err) + } + if len(compiled.Outputs[0].DynamicColumns) != 1 || compiled.Outputs[0].DynamicColumns[0].Name != "patient__identifiers_identifier" { + t.Fatalf("unexpected traversal dynamic metadata: %#v", compiled.Outputs[0].DynamicColumns) + } + if value, ok := compiled.Outputs[0].Plan.BindVars["recipe_dynamic_patient__identifiers_key_0"].(string); !ok || value != "identifier" { + t.Fatalf("missing traversal dynamic key bind vars: %#v", compiled.Outputs[0].Plan.BindVars) + } + rendered, err := aql.RenderPhysicalPlan(compiled.Outputs[0].Plan) + if err != nil { + t.Fatal(err) + } + foundProjectionName := false + for _, value := range rendered.BindVars { + if value == "patient__identifiers_identifier" { + foundProjectionName = true + break + } + } + if !foundProjectionName || !strings.Contains(rendered.Query, "FOR __item IN child_set_1") { + t.Fatalf("rendered traversal dynamic projection missing: query=%s binds=%#v", rendered.Query, rendered.BindVars) + } +} + +func TestCompileResolvedRecipePlanCorrelatesRepeatedPivotItems(t *testing.T) { + bundle := recipe.Bundle{RecipeSchemaVersion: 1, Name: "component-pivot", TranslationVersion: "test", Outputs: []recipe.Output{{ + Name: "Observation", RootResourceType: "Observation", RowGrain: "resource", + Pivots: []recipe.Pivot{{Name: "components", ColumnExpr: recipe.Expression{Select: "root.component[].code.coding[].display"}, ValueExpr: recipe.Expression{Select: "root.component[].valueString"}, ItemSource: recipe.Expression{Select: "root.component[]"}, ItemResourceType: "ObservationComponent", Columns: []string{"hemoglobin"}}}, + }}} + plan, err := semantic.BuildRecipePlan(bundle, recipe.RuntimeBindings{Project: "project", DatasetGeneration: "generation"}) + if err != nil { + t.Fatal(err) + } + resolved, err := semantic.ResolveRecipePlan(plan, "scope", "generation") + if err != nil { + t.Fatal(err) + } + compiled, err := CompileResolvedRecipePlan(resolved, DefaultPhysicalOptimizationPolicy()) + if err != nil { + t.Fatal(err) + } + rendered, err := aql.RenderPhysicalPlan(compiled.Outputs[0].Plan) + if err != nil { + t.Fatal(err) + } + if strings.Count(rendered.Query, "pivot_item_value") < 1 || !strings.Contains(rendered.Query, "__root.code") { + t.Fatalf("component pivot was not lowered as one correlated item scope: %s", rendered.Query) + } +} + +func TestCompileResolvedRecipePlanCarriesRichShapingIntoCanonicalIR(t *testing.T) { + bundle := recipe.Bundle{ + RecipeSchemaVersion: 1, + Name: "rich", + TranslationVersion: "test", + Outputs: []recipe.Output{{ + Name: "conditions", + RootResourceType: "Condition", + RowGrain: "diagnosis", + Fields: []recipe.Field{{Name: "id", Expr: recipe.Expression{Select: "id"}}}, + Pivots: []recipe.Pivot{{ + Name: "diagnosis", + ColumnExpr: recipe.Expression{Select: "code.coding[].display"}, + ValueExpr: recipe.Expression{Select: "code.text"}, + Columns: []string{"melanoma", "glioma"}, + }}, + Aggregates: []recipe.Aggregate{{Name: "condition_count", Operation: recipe.AggregateCount}}, + Slices: []recipe.RepresentativeSlice{{ + Name: "representatives", Limit: 2, + Fields: []recipe.Field{{Name: "status", Expr: recipe.Expression{Select: "verificationStatus"}}}, + }}, + }}, + } + plan, err := semantic.BuildRecipePlan(bundle, recipe.RuntimeBindings{Project: "project", DatasetGeneration: "generation"}) + if err != nil { + t.Fatal(err) + } + resolved, err := semantic.ResolveRecipePlan(plan, "scope", "generation") + if err != nil { + t.Fatal(err) + } + compiled, err := CompileResolvedRecipePlan(resolved, DefaultPhysicalOptimizationPolicy()) + if err != nil { + t.Fatal(err) + } + if len(compiled.Outputs) != 1 { + t.Fatalf("compiled outputs = %d, want 1", len(compiled.Outputs)) + } + var kinds map[string]ir.PhysicalExpressionKind + for _, operation := range compiled.Outputs[0].Plan.Operations { + if operation.Kind != ir.PhysicalReturnOp || operation.Return == nil { + continue + } + kinds = make(map[string]ir.PhysicalExpressionKind, len(operation.Return.Projections)) + for _, projection := range operation.Return.Projections { + if projection.Expression != nil { + kinds[projection.Name] = projection.Expression.Kind + } + } + } + for name, want := range map[string]ir.PhysicalExpressionKind{ + "diagnosis__glioma": ir.PhysicalObjectLookupExpression, + "diagnosis__melanoma": ir.PhysicalObjectLookupExpression, + "condition_count": ir.PhysicalAggregateExpression, + "representatives": ir.PhysicalSliceExpression, + } { + if got := kinds[name]; got != want { + t.Fatalf("projection %q kind = %q, want %q (all=%#v)", name, got, want, kinds) + } + } +} + +func TestCompileResolvedRecipePlanPreservesRestrictedEmptyAuthScope(t *testing.T) { + bundle := recipe.Bundle{RecipeSchemaVersion: 1, Name: "scope", TranslationVersion: "test", Outputs: []recipe.Output{{ + Name: "patients", RootResourceType: "Patient", RowGrain: "patient", + Fields: []recipe.Field{{Name: "id", Expr: recipe.Expression{Select: "id"}}}, + }}} + for _, test := range []struct { + name string + mode authscope.ReadScopeMode + wantBypass bool + }{ + {name: "restricted-empty", mode: authscope.ReadScopeRestricted, wantBypass: false}, + {name: "legacy-unrestricted", mode: authscope.ReadScopeUnrestricted, wantBypass: true}, + } { + t.Run(test.name, func(t *testing.T) { + bindings := recipe.RuntimeBindings{Project: "project", DatasetGeneration: "generation", AuthScopeMode: test.mode} + plan, err := semantic.BuildRecipePlan(bundle, bindings) + if err != nil { + t.Fatal(err) + } + resolved, err := semantic.ResolveRecipePlan(plan, "scope", "generation") + if err != nil { + t.Fatal(err) + } + compiled, err := CompileResolvedRecipePlan(resolved, DefaultPhysicalOptimizationPolicy()) + if err != nil { + t.Fatal(err) + } + got, ok := compiled.Outputs[0].Plan.BindVars["auth_resource_paths_unrestricted"].(bool) + if !ok || got != test.wantBypass { + t.Fatalf("auth bypass bind = %#v, want %t", compiled.Outputs[0].Plan.BindVars["auth_resource_paths_unrestricted"], test.wantBypass) + } + }) + } +} + +func TestCompileResolvedRecipePlanLowersRequiredTraversalFilter(t *testing.T) { + bundle := recipe.Bundle{RecipeSchemaVersion: 1, Name: "required", TranslationVersion: "test", Outputs: []recipe.Output{{ + Name: "patients", RootResourceType: "Patient", RowGrain: "patient", + Fields: []recipe.Field{{Name: "id", Expr: recipe.Expression{Select: "id"}}}, + Traversals: []recipe.Traversal{{ + Name: "subject_Patient", Alias: "condition", ToResourceType: "Condition", MatchMode: recipe.MatchRequired, + Filters: []recipe.Filter{{Select: "id", Operator: recipe.FilterExists}}, + }}, + }}} + plan, err := semantic.BuildRecipePlan(bundle, recipe.RuntimeBindings{Project: "project", DatasetGeneration: "generation"}) + if err != nil { + t.Fatal(err) + } + resolved, err := semantic.ResolveRecipePlan(plan, "scope", "generation") + if err != nil { + t.Fatal(err) + } + compiled, err := CompileResolvedRecipePlan(resolved, DefaultPhysicalOptimizationPolicy()) + if err != nil { + t.Fatal(err) + } + found := false + for _, operation := range compiled.Outputs[0].Plan.Operations { + if operation.Kind == ir.PhysicalFilterOp && operation.Filter != nil && operation.Source.SemanticNode == "condition" { + found = true + } + } + if !found { + t.Fatal("required traversal filter did not reach physical plan") + } +} diff --git a/internal/dataframe/compiler/lower/recipe_schema.go b/internal/dataframe/compiler/lower/recipe_schema.go new file mode 100644 index 0000000..8a38be8 --- /dev/null +++ b/internal/dataframe/compiler/lower/recipe_schema.go @@ -0,0 +1,144 @@ +package lower + +// This file contains the canonical recipe lowering boundary. Persisted +// recipes are a frontend: after resolution each output is lowered to the same +// ir.PhysicalPlan used by the GraphQL dataframe compiler. + +import ( + "strings" + + "github.com/calypr/loom/internal/dataframe/compiler/ir" + "github.com/calypr/loom/internal/dataframe/expression" + "github.com/calypr/loom/internal/dataframe/semantic" +) + +// CompiledRecipe is orchestration metadata around one canonical physical plan +// per output. It deliberately contains no recipe-specific traversal, +// projection, or renderer structures. Output order is the persisted recipe +// order and therefore part of the stable materialization contract. +func cloneRecipeNodeForPhysical(node semantic.SemanticNode) semantic.SemanticNode { + copy := node + copy.Fields = append([]semantic.SemanticField(nil), node.Fields...) + copy.Children = make([]semantic.SemanticNode, len(node.Children)) + for index := range node.Children { + copy.Children[index] = cloneRecipeNodeForPhysical(node.Children[index]) + } + // BuildGenericPhysicalPlan currently consumes selector metadata for its + // structural projections. Recipe expressions are patched after the + // generic plan is built, so provide a schema-valid selector seed for call + // expressions that contain no selector (for example a literal-only call). + for index := range copy.Fields { + // The generic lowerer uses SemanticField.Selector only to construct a + // valid traversal/set skeleton. Recipe expressions may refer to an + // expanded item (for example member.entity.reference), which is not a + // selector relative to the root resource. The checked recipe expression + // replaces this seed immediately after the skeleton is built. + if copy.Fields[index].Expr == nil || copy.Fields[index].Expr.Selector == nil { + if selector, err := ParseSelector("id"); err == nil { + copy.Fields[index].Selector = selector + } + } + } + return copy +} + +// recipeOutputSchema is the single schema capture point for recipe outputs. +// Names and order come from the physical RETURN operation after all recipe +// identity and bounded dynamic projections have been appended. Semantic +// metadata is used only to enrich those already-finalized projections with +// logical type/cardinality information. +func recipeOutputSchema(plan ir.PhysicalPlan, output semantic.OutputPlan) []CompiledOutputColumn { + logical := make(map[string]CompiledOutputColumn) + addLogical := func(name, kind, cardinality string, nullable bool) { + if strings.TrimSpace(name) == "" { + return + } + if _, exists := logical[name]; exists { + return + } + logical[name] = CompiledOutputColumn{Name: name, Kind: kind, Cardinality: cardinality, Nullable: nullable} + } + addType := func(name string, typ expression.Type) { + kind := string(typ.Kind) + if kind == "" { + kind = string(expression.KindString) + } + cardinality := string(typ.Cardinality) + if cardinality == "" { + cardinality = string(expression.RequiredOne) + } + addLogical(name, kind, cardinality, typ.Cardinality.Optional()) + } + for _, field := range output.Fields { + addType(field.Name, field.Expr.Type) + } + var addNode func(semantic.SemanticNode, string) + addNode = func(node semantic.SemanticNode, prefix string) { + for _, field := range node.Fields { + name := prefix + field.Name + if field.Expr != nil { + addType(name, field.ExprType) + } else { + addLogical(name, string(expression.KindString), string(expression.RequiredOne), true) + } + } + for _, aggregate := range node.Aggregates { + addLogical(prefix+aggregate.Name, string(expression.KindInteger), string(expression.RequiredOne), true) + } + for _, pivot := range node.Pivots { + for _, column := range pivot.Columns { + addLogical(prefix+pivot.Name+"__"+sanitizeColumnName(column), string(expression.KindObject), string(expression.RequiredOne), true) + } + } + for _, slice := range node.Slices { + addLogical(prefix+slice.Name, string(expression.KindObject), string(expression.RequiredOne), true) + } + for _, child := range node.Children { + childPrefix := child.Alias + if prefix != "" { + childPrefix = strings.TrimSuffix(prefix, "__") + "__" + child.Alias + } + addNode(child, childPrefix+"__") + } + } + addNode(output.Root, "") + + for _, operation := range plan.Operations { + if operation.Kind != ir.PhysicalReturnOp || operation.Return == nil { + continue + } + result := make([]CompiledOutputColumn, 0, len(operation.Return.Projections)) + for _, projection := range operation.Return.Projections { + column, ok := logical[projection.Name] + if !ok { + column = CompiledOutputColumn{Name: projection.Name, Kind: string(expression.KindString), Cardinality: string(expression.RequiredOne), Nullable: true} + } + if projection.Expression != nil { + if column.Cardinality == string(expression.RequiredOne) && projection.Expression.Cardinality == ir.PhysicalArrayCardinality { + column.Cardinality = string(expression.Many) + column.Nullable = true + } + if projection.Expression.Kind == ir.PhysicalPivotExpression || projection.Expression.Kind == ir.PhysicalObjectExpression { + column.Kind = string(expression.KindObject) + } + } + column.Name = projection.Name + column.Internal = projection.Hidden || projection.Name == "_key" || strings.HasPrefix(projection.Name, "__loom_") + column.Identity = projection.Name == "_key" || projection.Name == "__loom_row_id" + result = append(result, column) + } + return result + } + return nil +} + +func physicalOutputColumns(schema []CompiledOutputColumn) []string { + columns := make([]string, 0, len(schema)) + for _, column := range schema { + if column.Name == "__loom_dynamic_runtime_keys" { + continue + } + columns = append(columns, column.Name) + } + return columns +} diff --git a/internal/dataframe/compiler/lower/root_projection.go b/internal/dataframe/compiler/lower/root_projection.go new file mode 100644 index 0000000..8ea345a --- /dev/null +++ b/internal/dataframe/compiler/lower/root_projection.go @@ -0,0 +1,235 @@ +package lower + +import ( + "fmt" + "strings" +) + +func rootPhysicalProjections(physical *PhysicalPlan, root SemanticNode) ([]PhysicalProjection, error) { + projections := []PhysicalProjection{{Name: "_key", Value: PhysicalValue{Variable: "root", Path: []string{"_key"}}}} + for index, field := range root.Fields { + selection, err := ResolveSemanticField(root.ResourceType, root.Alias, index, field) + if err != nil { + return nil, err + } + cardinality, distinct := PhysicalScalarCardinality, false + switch selection.Projection { + case ProjectionArray: + cardinality = PhysicalArrayCardinality + case ProjectionDistinctArray: + cardinality, distinct = PhysicalArrayCardinality, true + case ProjectionScalar, ProjectionFirst: + default: + return nil, fmt.Errorf("root field %q has unsupported projection %q", field.Name, selection.Projection) + } + expression := PhysicalExpression{ + Kind: PhysicalExtractExpression, Cardinality: cardinality, NullBehavior: PhysicalPreserveNull, + Extract: &PhysicalExtract{Source: PhysicalValue{Variable: "root", Path: []string{"payload"}}, ResourceType: root.ResourceType, Selector: field.Selector, Fallbacks: append([]Selector(nil), field.Fallbacks...), Distinct: distinct, ExecutionMode: selectorExecutionMode(root.ResourceType, field.Selector, field.Fallbacks...)}, + } + if cardinality == PhysicalArrayCardinality { + expression.NullBehavior = PhysicalEmptyOnNull + } + projections = append(projections, PhysicalProjection{Name: field.Name, Expression: &expression}) + } + for _, aggregate := range root.Aggregates { + expression, err := physicalAggregateExpression(physical, root.ResourceType, PhysicalValue{Variable: "root"}, aggregate, false) + if err != nil { + return nil, err + } + projections = append(projections, PhysicalProjection{Name: aggregate.Name, Expression: &expression}) + } + for _, pivot := range root.Pivots { + pivotProjections, err := physicalPivotProjections(physical, root.ResourceType, PhysicalValue{Variable: "root"}, pivot, false, "") + if err != nil { + return nil, err + } + projections = append(projections, pivotProjections...) + } + for _, slice := range root.Slices { + expression, err := physicalSliceExpression(physical, root.ResourceType, PhysicalValue{Variable: "root"}, slice, false) + if err != nil { + return nil, err + } + projections = append(projections, PhysicalProjection{Name: slice.Name, Expression: &expression}) + } + return projections, nil +} + +func physicalPivotProjections(physical *PhysicalPlan, resourceType string, source PhysicalValue, pivot SemanticPivot, sourceIsSet bool, prefix string) ([]PhysicalProjection, error) { + if len(pivot.Columns) == 0 { + return nil, fmt.Errorf("pivot %q requires bounded columns", pivot.Name) + } + projections := make([]PhysicalProjection, 0, len(pivot.Columns)) + columnsBindKey := "pivot_" + sanitizeColumnName(source.Variable) + "_" + sanitizeColumnName(pivot.Name) + "_columns" + physical.BindVars[columnsBindKey] = append([]string(nil), pivot.Columns...) + familyVariable := "__loom_pivot_" + sanitizeColumnName(source.Variable) + "_" + sanitizeColumnName(pivot.Name) + for index := 1; deferredExpressionVariableExists(*physical, familyVariable); index++ { + familyVariable = fmt.Sprintf("__loom_pivot_%s_%s_%d", sanitizeColumnName(source.Variable), sanitizeColumnName(pivot.Name), index) + } + sharedExpression := PhysicalExpression{Kind: PhysicalPivotExpression, Cardinality: PhysicalObjectCardinality, NullBehavior: PhysicalPreserveNull, + Pivot: &PhysicalPivotMap{Source: source, ResourceType: resourceType, ItemSource: pivot.ItemSource, ItemResourceType: pivot.ItemResourceType, KeySelector: pivot.ColumnSelector, ValueSelector: pivot.ValueSelector, ValueFallbacks: append([]Selector(nil), pivot.ValueFallbacks...), ColumnsBindKey: columnsBindKey, FlattenSingleColumn: false}} + physical.DeferredExpressionLets = append(physical.DeferredExpressionLets, PhysicalOperation{Kind: PhysicalExpressionLetOp, Source: PhysicalSource{ResourceType: resourceType, SemanticField: "pivot_family"}, ExpressionLet: &PhysicalExpressionLet{Variable: familyVariable, Expression: sharedExpression}}) + for _, column := range pivot.Columns { + columnBindKey := columnsBindKey + "_" + sanitizeColumnName(column) + physical.BindVars[columnBindKey] = column + expression := PhysicalExpression{Kind: PhysicalObjectLookupExpression, Cardinality: PhysicalScalarCardinality, NullBehavior: PhysicalPreserveNull, ObjectLookup: &PhysicalObjectLookup{ObjectVariable: familyVariable, KeyBindKey: columnBindKey}} + projections = append(projections, PhysicalProjection{Name: prefix + pivot.Name + "__" + sanitizeColumnName(column), Expression: &expression}) + } + return projections, nil +} + +func deferredExpressionVariableExists(physical PhysicalPlan, variable string) bool { + for _, operation := range physical.DeferredExpressionLets { + if operation.ExpressionLet != nil && operation.ExpressionLet.Variable == variable { + return true + } + } + for _, operation := range physical.Operations { + if operation.ExpressionLet != nil && operation.ExpressionLet.Variable == variable { + return true + } + } + return false +} + +// physicalAggregateExpression lowers one semantic aggregate into the typed +// aggregate IR. A root source is a singleton document; child sources are +// correlated PhysicalSet variables. Selector extraction remains typed so the +// renderer can choose the correct payload iteration without embedding user +// paths in AQL. +func physicalAggregateExpression(physical *PhysicalPlan, resourceType string, source PhysicalValue, aggregate SemanticAggregate, sourceIsSet bool) (PhysicalExpression, error) { + op := PhysicalAggregateOperation(strings.ToUpper(strings.TrimSpace(aggregate.Operation))) + switch op { + case PhysicalCountAggregate, PhysicalCountDistinctAggregate, PhysicalExistsAggregate, PhysicalDistinctValuesAggregate, PhysicalMinAggregate, PhysicalMaxAggregate, PhysicalFirstAggregate: + default: + return PhysicalExpression{}, fmt.Errorf("aggregate %q uses unsupported operation %q", aggregate.Name, aggregate.Operation) + } + aggregatePhysical := PhysicalAggregate{Source: source, Operation: op} + if aggregate.Selector != nil { + valueSource := source + if !sourceIsSet && source.Variable != "" && len(source.Path) == 0 { + valueSource = PhysicalValue{Variable: source.Variable, Path: []string{"payload"}} + } + value := PhysicalExpression{Kind: PhysicalExtractExpression, Cardinality: PhysicalArrayCardinality, NullBehavior: PhysicalEmptyOnNull, + Extract: &PhysicalExtract{Source: valueSource, ResourceType: resourceType, Selector: *aggregate.Selector, ExecutionMode: selectorExecutionMode(resourceType, *aggregate.Selector)}} + aggregatePhysical.Value = &value + } + if aggregate.Predicate != nil { + leftSource := source + if !sourceIsSet && source.Variable != "" && len(source.Path) == 0 { + leftSource = PhysicalValue{Variable: source.Variable, Path: []string{"payload"}} + } + left := PhysicalExpression{Kind: PhysicalExtractExpression, Cardinality: PhysicalArrayCardinality, NullBehavior: PhysicalEmptyOnNull, + Extract: &PhysicalExtract{Source: leftSource, ResourceType: resourceType, Selector: *aggregate.Predicate, ExecutionMode: selectorExecutionMode(resourceType, *aggregate.Predicate)}} + comparison := &PhysicalPredicate{Operator: "EXISTS", LeftExpression: &left} + if aggregate.PredicateEquals != "" { + comparison.Operator = "EQUALS" + comparison.ValueKind = FilterString + key := "aggregate_" + sanitizeColumnName(source.Variable) + "_" + sanitizeColumnName(aggregate.Name) + "_predicate_equals" + physical.BindVars[key] = aggregate.PredicateEquals + comparison.Right = &PhysicalValue{BindKey: key} + } + aggregatePhysical.Predicate = &PhysicalPredicateExpression{Kind: PhysicalComparisonPredicate, Comparison: comparison} + } + cardinality := PhysicalScalarCardinality + nullBehavior := PhysicalEmptyOnNull + if op == PhysicalDistinctValuesAggregate { + cardinality = PhysicalArrayCardinality + } + return PhysicalExpression{Kind: PhysicalAggregateExpression, Cardinality: cardinality, NullBehavior: nullBehavior, Aggregate: &aggregatePhysical}, nil +} + +// physicalSliceExpression lowers a representative slice into a typed, +// bounded projection over either the singleton root document or a correlated +// child set. The source set is already materialized in stable _key order; +// the explicit sort expression below makes that ordering part of the slice +// contract and gives the renderer a deterministic tie-break. +func physicalSliceExpression(physical *PhysicalPlan, resourceType string, source PhysicalValue, slice SemanticSlice, sourceIsSet bool) (PhysicalExpression, error) { + _ = sourceIsSet + if slice.Limit <= 0 { + return PhysicalExpression{}, fmt.Errorf("slice %q requires positive limit", slice.Name) + } + limitKey := "slice_" + sanitizeColumnName(source.Variable) + "_" + sanitizeColumnName(slice.Name) + "_limit" + physical.BindVars[limitKey] = slice.Limit + // Expressions are anchored to the declared source variable for validation; + // the renderer rebinds them to its per-item loop variable. + leftSource := source + physicalSlice := PhysicalSlice{ + Source: source, + LimitBindKey: limitKey, + Sort: &PhysicalExpression{Kind: PhysicalValueExpression, Cardinality: PhysicalScalarCardinality, NullBehavior: PhysicalPreserveNull, Value: &PhysicalValue{Variable: source.Variable, Path: []string{"_key"}}}, + Projections: make([]PhysicalExpressionProjection, 0, len(slice.Fields)), + } + if slice.Predicate != nil { + left := PhysicalExpression{Kind: PhysicalExtractExpression, Cardinality: PhysicalArrayCardinality, NullBehavior: PhysicalEmptyOnNull, + Extract: &PhysicalExtract{Source: leftSource, ResourceType: resourceType, Selector: *slice.Predicate, ExecutionMode: selectorExecutionMode(resourceType, *slice.Predicate)}} + comparison := &PhysicalPredicate{Operator: "EXISTS", LeftExpression: &left} + if slice.PredicateEquals != "" { + key := "slice_" + sanitizeColumnName(source.Variable) + "_" + sanitizeColumnName(slice.Name) + "_predicate_equals" + physical.BindVars[key] = slice.PredicateEquals + comparison.Operator = "EQUALS" + comparison.ValueKind = FilterString + comparison.Right = &PhysicalValue{BindKey: key} + } + physicalSlice.Predicate = &PhysicalPredicateExpression{Kind: PhysicalComparisonPredicate, Comparison: comparison} + } + for index, field := range slice.Fields { + selection := field + if selection.Name == "" { + return PhysicalExpression{}, fmt.Errorf("slice %q field %d requires name", slice.Name, index) + } + physicalSlice.Projections = append(physicalSlice.Projections, PhysicalExpressionProjection{ + Name: selection.Name, + Expression: PhysicalExpression{Kind: PhysicalExtractExpression, Cardinality: PhysicalScalarCardinality, NullBehavior: PhysicalPreserveNull, + Extract: &PhysicalExtract{Source: leftSource, ResourceType: resourceType, Selector: selection.Selector, Fallbacks: append([]Selector(nil), selection.Fallbacks...), ExecutionMode: selectorExecutionMode(resourceType, selection.Selector, selection.Fallbacks...)}}, + }) + } + return PhysicalExpression{Kind: PhysicalSliceExpression, Cardinality: PhysicalArrayCardinality, NullBehavior: PhysicalEmptyOnNull, Slice: &physicalSlice}, nil +} + +func appendRootPhysicalFilters(physical *PhysicalPlan, root SemanticNode) error { + for index, filter := range root.Filters { + if err := ValidateTypedFilterForResource(root.ResourceType, filter); err != nil { + return fmt.Errorf("root filter %q: %w", filter.FieldRef, err) + } + selector, err := ParseSelector(filter.Selector) + if err != nil { + return fmt.Errorf("root filter %q selector: %w", filter.FieldRef, err) + } + predicate := PhysicalPredicate{ + Operator: string(filter.Operator), Quantifier: filter.Quantifier, ValueKind: filter.FieldKind, + LeftExpression: &PhysicalExpression{Kind: PhysicalExtractExpression, Cardinality: PhysicalArrayCardinality, NullBehavior: PhysicalEmptyOnNull, + Extract: &PhysicalExtract{Source: PhysicalValue{Variable: "root", Path: []string{"payload"}}, ResourceType: root.ResourceType, Selector: selector, ExecutionMode: selectorExecutionMode(root.ResourceType, selector)}}, + } + if filter.Operator != FilterExists && filter.Operator != FilterMissing { + key := fmt.Sprintf("root_filter_%d_value", index+1) + if filter.Operator == FilterIn { + values := make([]any, 0, len(filter.Values)) + for _, value := range filter.Values { + literal, err := filterLiteral(value) + if err != nil { + return err + } + values = append(values, literal) + } + physical.BindVars[key] = values + } else { + literal, err := filterLiteral(filter.Values[0]) + if err != nil { + return err + } + physical.BindVars[key] = literal + } + predicate.Right = &PhysicalValue{BindKey: key} + } + physical.Operations = append(physical.Operations, PhysicalOperation{Kind: PhysicalFilterOp, + Source: PhysicalSource{SemanticNode: root.Alias, ResourceType: root.ResourceType, SemanticField: filter.FieldRef}, + Filter: &PhysicalFilter{Expression: &PhysicalPredicateExpression{Kind: PhysicalComparisonPredicate, Comparison: &predicate}}}) + } + return nil +} + +// buildOptionalChildPhysicalSet lowers one root-relative optional traversal. +// The set retains resource documents (rather than pre-shaped maps), allowing +// the renderer to apply each child selector against the document payload at +// the outer return while preserving root row grain. diff --git a/internal/dataframe/compiler/lower/storage_route.go b/internal/dataframe/compiler/lower/storage_route.go index 45621f9..f86866f 100644 --- a/internal/dataframe/compiler/lower/storage_route.go +++ b/internal/dataframe/compiler/lower/storage_route.go @@ -33,10 +33,6 @@ func ResolveStorageRoute(fromType, label, toType string) (StorageRoute, error) { return resolveStorageRoute(fromType, label, toType) } -func (route storageRoute) namedSetDirection() string { - return string(route.Direction) -} - // targetEdgeTypeField returns the fhir_edge type discriminator for the node // reached by this route. It is deliberately direction-aware: forward FHIR // references store their target in to_type, while the generated builder route @@ -74,13 +70,11 @@ func (route storageRoute) endpointLookupFields() (parentField, joinField string, // normal FHIR references as child _from -> parent _to, so a parent -> child // dataframe route is an INBOUND fhir_edge traversal. // -// It also accepts one explicit forward route: ResearchSubject --study--> -// ResearchStudy. The checked-in graph schema proves the logical FHIR target -// (an exact ResearchStudy/* hint, OUTBOUND direction, and has_one -// multiplicity), and the generated edge-extractor regression proves that it -// is materialized as ResearchSubject _from -> ResearchStudy _to in fhir_edge. -// Other generated forward metadata remains deliberately non-executable until -// it has the same storage proof. +// It also accepts generated forward routes when the schema gives an exact +// target hint and an outbound logical reference. EdgeFromGrip (the single +// edge adapter used by ingest) materializes every such reference as source +// _from -> referenced target _to in fhir_edge, so the generated target hint is +// the storage proof; no resource-specific allowlist is needed. func resolveStorageRoute(fromType, edgeLabel, toType string) (storageRoute, error) { if !fhirschema.HasResource(fromType) { return storageRoute{}, unsupportedStorageRouteError(fromType, edgeLabel, toType, "source resource type is not represented by the active generated FHIR schema") @@ -113,18 +107,15 @@ func resolveStorageRoute(fromType, edgeLabel, toType string) (storageRoute, erro return storageRoute{Relationship: relationship, Direction: PhysicalInbound}, nil } - if isProvenResearchSubjectStudyOutboundRoute(spec, relationship) { + if isProvenExactOutboundRoute(spec, relationship) { return storageRoute{Relationship: relationship, Direction: PhysicalOutbound}, nil } - return storageRoute{}, unsupportedStorageRouteError(fromType, edgeLabel, toType, fmt.Sprintf("is not a proven stored route: generated reverse/builder routes require RegexMatch %q; generated forward routes require an explicit fhir_edge contract (got %s)", fromType+"/*", formatRegexMatchHints(spec.RegexMatch))) + return storageRoute{}, unsupportedStorageRouteError(fromType, edgeLabel, toType, fmt.Sprintf("is not a proven stored route: reverse routes require RegexMatch %q; outbound references require an exact target hint (got %s)", fromType+"/*", formatRegexMatchHints(spec.RegexMatch))) } -func isProvenResearchSubjectStudyOutboundRoute(spec fhirschema.TraversalSpec, relationship fhirschema.CompilerTraversal) bool { - if spec.FromType != "ResearchSubject" || spec.EdgeLabel != "study" || spec.ToType != "ResearchStudy" { - return false - } - if relationship.Direction != fhirschema.TraversalOutbound || relationship.Multiplicity != fhirschema.TraversalOne { +func isProvenExactOutboundRoute(spec fhirschema.TraversalSpec, relationship fhirschema.CompilerTraversal) bool { + if relationship.Direction != fhirschema.TraversalOutbound { return false } return hasExactStorageTargetHint(spec.RegexMatch, spec.ToType) diff --git a/internal/dataframe/compiler/lower/traversal.go b/internal/dataframe/compiler/lower/traversal.go new file mode 100644 index 0000000..23aedcf --- /dev/null +++ b/internal/dataframe/compiler/lower/traversal.go @@ -0,0 +1,136 @@ +package lower + +import ( + "fmt" + "strings" +) + +// TraversalLoweringRequest is the backend-neutral input to the shared +// schema-derived traversal constructor. The caller supplies semantic resource +// identities and compiler-owned variable names; route direction, edge type +// discriminator, and endpoint fields are resolved from fhirschema through +// resolveStorageRoute. +// +// BindPrefix is part of the physical naming contract. It must be unique in +// the containing plan and stable for a given semantic traversal. It is used +// only to derive bind keys; it is never rendered as AQL source. +type TraversalLoweringRequest struct { + FromType string + EdgeLabel string + ToType string + SourceVariable string + TargetVariable string + EdgeVariable string + BindPrefix string + Policy PhysicalOptimizationPolicy +} + +// TraversalLoweringResult contains the canonical physical traversal and the +// bind values required by its renderer. The route is retained as provenance +// for callers that need to inspect the schema-derived contract; callers must +// not mutate it or substitute a direction after construction. +type TraversalLoweringResult struct { + Route StorageRoute + Traversal PhysicalTraversal + BindVars map[string]any +} + +// BuildPhysicalTraversal resolves one FHIR relationship and constructs the +// canonical physical traversal used by every compiler frontend. It is the +// only lower-layer entry point that should assemble traversal direction, +// target discriminator, endpoint fields, and traversal bind keys. +func BuildPhysicalTraversal(request TraversalLoweringRequest) (TraversalLoweringResult, error) { + if strings.TrimSpace(request.FromType) == "" { + return TraversalLoweringResult{}, fmt.Errorf("traversal source resource type is required") + } + if strings.TrimSpace(request.EdgeLabel) == "" { + return TraversalLoweringResult{}, fmt.Errorf("traversal edge label is required") + } + if strings.TrimSpace(request.ToType) == "" { + return TraversalLoweringResult{}, fmt.Errorf("traversal target resource type is required") + } + if strings.TrimSpace(request.SourceVariable) == "" { + return TraversalLoweringResult{}, fmt.Errorf("traversal source variable is required") + } + if strings.TrimSpace(request.TargetVariable) == "" { + return TraversalLoweringResult{}, fmt.Errorf("traversal target variable is required") + } + if strings.TrimSpace(request.EdgeVariable) == "" { + return TraversalLoweringResult{}, fmt.Errorf("traversal edge variable is required") + } + prefix := strings.TrimSpace(request.BindPrefix) + if prefix == "" { + return TraversalLoweringResult{}, fmt.Errorf("traversal bind prefix is required") + } + identifiers := []struct { + name string + value string + }{ + {name: "source variable", value: request.SourceVariable}, + {name: "target variable", value: request.TargetVariable}, + {name: "edge variable", value: request.EdgeVariable}, + {name: "bind prefix", value: prefix}, + } + for _, identifier := range identifiers { + if !isCompilerIdentifier(identifier.value) { + return TraversalLoweringResult{}, fmt.Errorf("traversal %s %q is not a safe compiler identifier", identifier.name, identifier.value) + } + } + + route, err := resolveStorageRoute(request.FromType, request.EdgeLabel, request.ToType) + if err != nil { + return TraversalLoweringResult{}, err + } + strategy, endpointField, endpointJoinField, endpointIndexFields := physicalTraversalStrategyForRoute(request.Policy, route) + labelBind := prefix + "_label" + typeBind := prefix + "_target_type" + edgeCollectionBind := prefix + "_edge_collection" + return TraversalLoweringResult{ + Route: route, + Traversal: PhysicalTraversal{ + SourceVariable: request.SourceVariable, + TargetVariable: request.TargetVariable, + EdgeVariable: request.EdgeVariable, + Direction: route.Direction, + EdgeCollectionBindKey: edgeCollectionBind, + EdgeLabelBindKey: labelBind, + TargetTypeBindKey: typeBind, + EdgeTargetTypeField: route.targetEdgeTypeField(), + Strategy: strategy, + EndpointField: endpointField, + EndpointJoinField: endpointJoinField, + EndpointIndexFields: endpointIndexFields, + }, + BindVars: map[string]any{ + labelBind: request.EdgeLabel, + typeBind: request.ToType, + edgeCollectionBind: "fhir_edge", + }, + }, nil +} + +func isCompilerIdentifier(value string) bool { + if value == "" { + return false + } + for index, char := range value { + if (char >= 'a' && char <= 'z') || (char >= 'A' && char <= 'Z') || char == '_' || (index > 0 && char >= '0' && char <= '9') { + continue + } + return false + } + return true +} + +// physicalTraversalStrategyForRoute selects an execution strategy only after +// route metadata has been validated. Endpoint lookup is a typed physical +// choice; it does not permit callers to provide endpoint fields or AQL. +func physicalTraversalStrategyForRoute(policy PhysicalOptimizationPolicy, route storageRoute) (PhysicalTraversalStrategy, string, string, []string) { + if !policy.RuleEnabled(PhysicalOptimizationRuleEndpointTraversal) { + return PhysicalTraversalNative, "", "", nil + } + if parentField, joinField, fields, ok := route.endpointLookupFields(); ok && len(fields) > 0 { + return PhysicalTraversalEndpointLookup, parentField, joinField, append([]string(nil), fields...) + } + return PhysicalTraversalNative, "", "", nil +} diff --git a/internal/dataframe/compiler/lower/traversal_test.go b/internal/dataframe/compiler/lower/traversal_test.go new file mode 100644 index 0000000..4e27bdd --- /dev/null +++ b/internal/dataframe/compiler/lower/traversal_test.go @@ -0,0 +1,139 @@ +package lower + +import "testing" + +func TestBuildPhysicalTraversalUsesSchemaDerivedInboundRoute(t *testing.T) { + policy := DefaultPhysicalOptimizationPolicy() + got, err := BuildPhysicalTraversal(TraversalLoweringRequest{ + FromType: "Patient", + EdgeLabel: "subject_Patient", + ToType: "Specimen", + SourceVariable: "root", + TargetVariable: "child_node", + EdgeVariable: "child_edge", + BindPrefix: "child_set_1", + Policy: policy, + }) + if err != nil { + t.Fatalf("BuildPhysicalTraversal() error = %v", err) + } + if got.Route.Direction != PhysicalInbound { + t.Fatalf("route direction = %q, want %q", got.Route.Direction, PhysicalInbound) + } + if got.Traversal.Direction != PhysicalInbound || got.Traversal.EdgeTargetTypeField != "from_type" { + t.Fatalf("traversal route contract = %#v", got.Traversal) + } + if got.Traversal.Strategy != PhysicalTraversalEndpointLookup { + t.Fatalf("traversal strategy = %q, want endpoint lookup", got.Traversal.Strategy) + } + if got.Traversal.EndpointField != "_to" || got.Traversal.EndpointJoinField != "_from" { + t.Fatalf("endpoint contract = %#v", got.Traversal) + } + wantBinds := map[string]any{ + "child_set_1_label": "subject_Patient", + "child_set_1_target_type": "Specimen", + "child_set_1_edge_collection": "fhir_edge", + } + for key, want := range wantBinds { + if got.BindVars[key] != want { + t.Fatalf("bind %q = %#v, want %#v", key, got.BindVars[key], want) + } + } +} + +func TestBuildPhysicalTraversalUsesSchemaDerivedOutboundRoute(t *testing.T) { + got, err := BuildPhysicalTraversal(TraversalLoweringRequest{ + FromType: "ResearchSubject", + EdgeLabel: "study", + ToType: "ResearchStudy", + SourceVariable: "root", + TargetVariable: "study_node", + EdgeVariable: "study_edge", + BindPrefix: "study_1", + Policy: DefaultPhysicalOptimizationPolicy(), + }) + if err != nil { + t.Fatalf("BuildPhysicalTraversal() error = %v", err) + } + if got.Route.Direction != PhysicalOutbound || got.Traversal.Direction != PhysicalOutbound { + t.Fatalf("outbound route contract = route %#v traversal %#v", got.Route, got.Traversal) + } + if got.Traversal.EdgeTargetTypeField != "to_type" { + t.Fatalf("target discriminator = %q, want to_type", got.Traversal.EdgeTargetTypeField) + } + if got.Traversal.EndpointField != "_from" || got.Traversal.EndpointJoinField != "_to" { + t.Fatalf("outbound endpoint contract = %#v", got.Traversal) + } +} + +func TestBuildPhysicalTraversalUsesGeneratedOutboundReferenceForAnyFHIRType(t *testing.T) { + got, err := BuildPhysicalTraversal(TraversalLoweringRequest{ + FromType: "ResearchSubject", EdgeLabel: "subject_Patient", ToType: "Patient", + SourceVariable: "root", TargetVariable: "patient_node", EdgeVariable: "patient_edge", + BindPrefix: "subject_1", Policy: DefaultPhysicalOptimizationPolicy(), + }) + if err != nil { + t.Fatalf("BuildPhysicalTraversal() error = %v", err) + } + if got.Route.Direction != PhysicalOutbound || got.Traversal.Direction != PhysicalOutbound { + t.Fatalf("generated outbound route contract = route %#v traversal %#v", got.Route, got.Traversal) + } + if got.Traversal.EndpointField != "_from" || got.Traversal.EndpointJoinField != "_to" { + t.Fatalf("generated outbound endpoint contract = %#v", got.Traversal) + } +} + +func TestBuildPhysicalTraversalPolicyCanForceNativeRoute(t *testing.T) { + policy := DefaultPhysicalOptimizationPolicy().WithRule(PhysicalOptimizationRuleEndpointTraversal, false) + got, err := BuildPhysicalTraversal(TraversalLoweringRequest{ + FromType: "Patient", + EdgeLabel: "subject_Patient", + ToType: "Specimen", + SourceVariable: "root", + TargetVariable: "node", + EdgeVariable: "edge", + BindPrefix: "traversal_1", + Policy: policy, + }) + if err != nil { + t.Fatalf("BuildPhysicalTraversal() error = %v", err) + } + if got.Traversal.Strategy != PhysicalTraversalNative { + t.Fatalf("strategy = %q, want native", got.Traversal.Strategy) + } + if got.Traversal.EndpointField != "" || got.Traversal.EndpointJoinField != "" || len(got.Traversal.EndpointIndexFields) != 0 { + t.Fatalf("native traversal retained endpoint contract: %#v", got.Traversal) + } +} + +func TestBuildPhysicalTraversalRejectsUnknownSchemaRoute(t *testing.T) { + _, err := BuildPhysicalTraversal(TraversalLoweringRequest{ + FromType: "Patient", + EdgeLabel: "not_generated", + ToType: "Specimen", + SourceVariable: "root", + TargetVariable: "node", + EdgeVariable: "edge", + BindPrefix: "traversal_1", + Policy: DefaultPhysicalOptimizationPolicy(), + }) + if err == nil { + t.Fatal("BuildPhysicalTraversal() succeeded for an unknown schema route") + } +} + +func TestBuildPhysicalTraversalRequiresCompilerNames(t *testing.T) { + cases := []TraversalLoweringRequest{ + {EdgeLabel: "subject_Patient", ToType: "Specimen", SourceVariable: "root", TargetVariable: "node", EdgeVariable: "edge", BindPrefix: "x"}, + {FromType: "Patient", ToType: "Specimen", SourceVariable: "root", TargetVariable: "node", EdgeVariable: "edge", BindPrefix: "x"}, + {FromType: "Patient", EdgeLabel: "subject_Patient", ToType: "Specimen", TargetVariable: "node", EdgeVariable: "edge", BindPrefix: "x"}, + {FromType: "Patient", EdgeLabel: "subject_Patient", ToType: "Specimen", SourceVariable: "root", EdgeVariable: "edge", BindPrefix: "x"}, + {FromType: "Patient", EdgeLabel: "subject_Patient", ToType: "Specimen", SourceVariable: "root", TargetVariable: "node", EdgeVariable: "edge"}, + {FromType: "Patient", EdgeLabel: "subject_Patient", ToType: "Specimen", SourceVariable: "root", TargetVariable: "node", EdgeVariable: "edge", BindPrefix: "unsafe-prefix"}, + } + for index, request := range cases { + if _, err := BuildPhysicalTraversal(request); err == nil { + t.Errorf("case %d: BuildPhysicalTraversal() accepted incomplete request", index) + } + } +} diff --git a/internal/dataframe/compiler/lower_aliases.go b/internal/dataframe/compiler/lower_aliases.go index dea757e..632c7e6 100644 --- a/internal/dataframe/compiler/lower_aliases.go +++ b/internal/dataframe/compiler/lower_aliases.go @@ -3,6 +3,10 @@ package compiler import "github.com/calypr/loom/internal/dataframe/compiler/lower" type StorageRoute = lower.StorageRoute +type CompiledRecipe = lower.CompiledRecipe +type CompiledRecipeOutput = lower.CompiledRecipeOutput +type DynamicColumnMetadata = lower.DynamicColumnMetadata +type CompiledOutputColumn = lower.CompiledOutputColumn var ( BuildPhysicalPlan = lower.BuildPhysicalPlan @@ -11,4 +15,5 @@ var ( BuildGenericPhysicalPlanWithPolicy = lower.BuildGenericPhysicalPlanWithPolicy ResolveStorageRoute = lower.ResolveStorageRoute ErrUnsupportedStorageRoute = lower.ErrUnsupportedStorageRoute + CompileResolvedRecipePlan = lower.CompileResolvedRecipePlan ) diff --git a/internal/dataframe/compiler/lowering_helpers.go b/internal/dataframe/compiler/lowering_helpers.go deleted file mode 100644 index 38d0bf9..0000000 --- a/internal/dataframe/compiler/lowering_helpers.go +++ /dev/null @@ -1,16 +0,0 @@ -package compiler - -// genericPhysicalPlanUnavailableReason is semantic-shape validation that -// remains in the compiler facade until the lowerer is extracted. -func genericPhysicalPlanUnavailableReason(node SemanticNode) string { - return genericPhysicalNodeUnavailableReason(node, true) -} - -func genericPhysicalNodeUnavailableReason(node SemanticNode, root bool) string { - for _, child := range node.Children { - if reason := genericPhysicalNodeUnavailableReason(child, false); reason != "" { - return reason - } - } - return "" -} diff --git a/internal/dataframe/compiler/optimization_helpers.go b/internal/dataframe/compiler/optimization_helpers.go deleted file mode 100644 index c72bce8..0000000 --- a/internal/dataframe/compiler/optimization_helpers.go +++ /dev/null @@ -1,10 +0,0 @@ -package compiler - -func appendUniqueRule(rules []string, rule string) []string { - for _, existing := range rules { - if existing == rule { - return rules - } - } - return append(rules, rule) -} diff --git a/internal/dataframe/compiler/optimize/aliases.go b/internal/dataframe/compiler/optimize/aliases.go index 5ad8de6..0937abe 100644 --- a/internal/dataframe/compiler/optimize/aliases.go +++ b/internal/dataframe/compiler/optimize/aliases.go @@ -40,6 +40,7 @@ const ( PhysicalTraversalOp = ir.PhysicalTraversalOp PhysicalOptimizationRuleTraversalSharing = ir.PhysicalOptimizationRuleTraversalSharing PhysicalOptimizationRuleEndpointTraversal = ir.PhysicalOptimizationRuleEndpointTraversal + PhysicalOptimizationRuleKeyedMapSharing = ir.PhysicalOptimizationRuleKeyedMapSharing PhysicalTraversalEndpointLookup = ir.PhysicalTraversalEndpointLookup PhysicalValueExpression = ir.PhysicalValueExpression PhysicalObjectCardinality = ir.PhysicalObjectCardinality @@ -48,8 +49,9 @@ const ( ) var ( - DefaultPhysicalOptimizationPolicy = ir.DefaultPhysicalOptimizationPolicy - DecomposePhysicalTraversalPrefix = ir.DecomposePhysicalTraversalPrefix + DefaultPhysicalOptimizationPolicy = ir.DefaultPhysicalOptimizationPolicy + DecomposePhysicalTraversalPrefix = ir.DecomposePhysicalTraversalPrefix + DecomposePhysicalTraversalPrefixAt = ir.DecomposePhysicalTraversalPrefixAt ) func clonePhysicalPlan(plan PhysicalPlan) PhysicalPlan { return ir.ClonePhysicalPlan(plan) } diff --git a/internal/dataframe/compiler/optimize/keyed_map.go b/internal/dataframe/compiler/optimize/keyed_map.go new file mode 100644 index 0000000..62cacff --- /dev/null +++ b/internal/dataframe/compiler/optimize/keyed_map.go @@ -0,0 +1,134 @@ +package optimize + +import ( + "crypto/sha256" + "encoding/hex" + "fmt" + "sort" + + "github.com/calypr/loom/internal/dataframe/compiler/ir" +) + +type keyedMapCandidate struct { + Signature string + Uses []*ir.PhysicalExpression + Lookup ir.PhysicalLookup +} + +type keyedMapBinding struct { + Variable string + Lookup ir.PhysicalLookup +} + +// sharePhysicalLookupFamilies converts repeated source-bearing lookup +// expressions in one RETURN scope into one keyed map and constant-time object +// lookups. The pass is deliberately lexical: candidates are collected per +// RETURN operation, so an optional child scope or an unnest can never leak a +// binding into a different row grain. +func sharePhysicalLookupFamilies(plan *PhysicalPlan, policy PhysicalOptimizationPolicy) { + for returnIndex := range plan.Operations { + operation := &plan.Operations[returnIndex] + if operation.Kind != ir.PhysicalReturnOp || operation.Return == nil { + continue + } + candidates := map[string]*keyedMapCandidate{} + for projectionIndex := range operation.Return.Projections { + collectLookupCandidates(operation.Return.Projections[projectionIndex].Expression, candidates) + } + keys := make([]string, 0, len(candidates)) + for key, candidate := range candidates { + if len(candidate.Uses) >= 2 { + keys = append(keys, key) + } + } + sort.Strings(keys) + if len(keys) == 0 { + continue + } + bindings := make(map[string]keyedMapBinding, len(keys)) + lets := make([]ir.PhysicalOperation, 0, len(keys)) + for _, key := range keys { + candidate := candidates[key] + baseline := len(candidate.Uses) * 3 + optimized := len(candidate.Uses) + 1 + decision := ir.PhysicalOptimizationDecision{Rule: string(PhysicalOptimizationRuleKeyedMapSharing), CandidateSets: len(candidate.Uses), EstimatedBaselineWork: baseline, EstimatedOptimizedWork: optimized, EstimatedSavings: baseline - optimized} + if !policy.RuleEnabled(PhysicalOptimizationRuleKeyedMapSharing) { + decision.Reason = "keyed map sharing disabled by policy" + plan.OptimizationPolicy.AddDecision(decision) + continue + } + if decision.EstimatedSavings < policy.MinimumSavings { + decision.Reason = fmt.Sprintf("estimated savings %d is below policy minimum %d", decision.EstimatedSavings, policy.MinimumSavings) + plan.OptimizationPolicy.AddDecision(decision) + continue + } + digest := sha256.Sum256([]byte(key)) + variable := "__loom_shared_lookup_" + hex.EncodeToString(digest[:])[:12] + bindings[key] = keyedMapBinding{Variable: variable, Lookup: candidate.Lookup} + lets = append(lets, ir.PhysicalOperation{Kind: ir.PhysicalExpressionLetOp, Source: ir.PhysicalSource{SemanticField: "shared_lookup_family"}, ExpressionLet: &ir.PhysicalExpressionLet{Variable: variable, Expression: ir.PhysicalExpression{Kind: ir.PhysicalKeyedMapExpression, Cardinality: ir.PhysicalObjectCardinality, NullBehavior: ir.PhysicalPreserveNull, KeyedMap: &ir.PhysicalKeyedMap{Source: ir.ClonePhysicalExpression(candidate.Lookup.Source), ItemVariable: candidate.Lookup.ItemVariable, ItemKey: ir.ClonePhysicalExpression(candidate.Lookup.ItemKey), ItemValue: ir.ClonePhysicalExpression(candidate.Lookup.ItemValue), Reduction: ir.PhysicalMapFirst}}}}) + decision.Enabled = true + decision.Reason = "repeated lookup source and selectors share one lexical keyed map" + plan.OptimizationPolicy.AddDecision(decision) + } + if len(bindings) == 0 { + continue + } + for projectionIndex := range operation.Return.Projections { + replaceLookupCandidates(operation.Return.Projections[projectionIndex].Expression, bindings) + } + operations := make([]ir.PhysicalOperation, 0, len(plan.Operations)+len(lets)) + operations = append(operations, plan.Operations[:returnIndex]...) + operations = append(operations, lets...) + operations = append(operations, plan.Operations[returnIndex:]...) + plan.Operations = operations + returnIndex += len(lets) + } +} + +func collectLookupCandidates(expression *ir.PhysicalExpression, candidates map[string]*keyedMapCandidate) { + if expression == nil { + return + } + if expression.Kind == ir.PhysicalLookupExpression && expression.Lookup != nil && expression.Lookup.ItemVariable != "" && expression.Lookup.MatchBindKey != "" { + keyed := ir.PhysicalKeyedMap{Source: ir.ClonePhysicalExpression(expression.Lookup.Source), ItemVariable: expression.Lookup.ItemVariable, ItemKey: ir.ClonePhysicalExpression(expression.Lookup.ItemKey), ItemValue: ir.ClonePhysicalExpression(expression.Lookup.ItemValue), Reduction: ir.PhysicalMapFirst} + candidateExpression := ir.PhysicalExpression{Kind: ir.PhysicalKeyedMapExpression, Cardinality: ir.PhysicalObjectCardinality, NullBehavior: ir.PhysicalPreserveNull, KeyedMap: &keyed} + signature, err := ir.PhysicalExpressionFingerprint(candidateExpression) + if err == nil { + candidate := candidates[signature] + if candidate == nil { + candidate = &keyedMapCandidate{Signature: signature, Lookup: *expression.Lookup} + candidates[signature] = candidate + } + candidate.Uses = append(candidate.Uses, expression) + } + } + if expression.Object != nil { + for index := range expression.Object.Fields { + collectLookupCandidates(&expression.Object.Fields[index].Expression, candidates) + } + } +} + +func replaceLookupCandidates(expression *ir.PhysicalExpression, bindings map[string]keyedMapBinding) { + if expression == nil { + return + } + if expression.Kind == ir.PhysicalLookupExpression && expression.Lookup != nil { + keyed := ir.PhysicalKeyedMap{Source: ir.ClonePhysicalExpression(expression.Lookup.Source), ItemVariable: expression.Lookup.ItemVariable, ItemKey: ir.ClonePhysicalExpression(expression.Lookup.ItemKey), ItemValue: ir.ClonePhysicalExpression(expression.Lookup.ItemValue), Reduction: ir.PhysicalMapFirst} + candidateExpression := ir.PhysicalExpression{Kind: ir.PhysicalKeyedMapExpression, Cardinality: ir.PhysicalObjectCardinality, NullBehavior: ir.PhysicalPreserveNull, KeyedMap: &keyed} + if signature, err := ir.PhysicalExpressionFingerprint(candidateExpression); err == nil { + if binding, ok := bindings[signature]; ok { + matchBindKey := expression.Lookup.MatchBindKey + expression.Kind = ir.PhysicalObjectLookupExpression + expression.Lookup = nil + expression.KeyedMap = nil + expression.ObjectLookup = &ir.PhysicalObjectLookup{ObjectVariable: binding.Variable, KeyBindKey: matchBindKey} + } + } + } + if expression.Object != nil { + for index := range expression.Object.Fields { + replaceLookupCandidates(&expression.Object.Fields[index].Expression, bindings) + } + } +} diff --git a/internal/dataframe/compiler/optimize/optimize.go b/internal/dataframe/compiler/optimize/optimize.go index 9c8c000..b18c93f 100644 --- a/internal/dataframe/compiler/optimize/optimize.go +++ b/internal/dataframe/compiler/optimize/optimize.go @@ -28,7 +28,7 @@ func OptimizePhysicalPlanWithPolicy(plan PhysicalPlan, policy PhysicalOptimizati if op.Kind != PhysicalSetOp || op.Set == nil || op.Set.SourceSetVariable != "" { continue } - decomposition, err := DecomposePhysicalTraversalPrefix(out, *op.Set) + decomposition, err := DecomposePhysicalTraversalPrefixAt(out, *op.Set, i) if err != nil { // Ineligibility is an expected optimizer outcome. The original plan // remains the execution oracle. @@ -91,6 +91,9 @@ func OptimizePhysicalPlanWithPolicy(plan PhysicalPlan, policy PhysicalOptimizati decision.Reason = "estimated prefix work reduction exceeds policy minimum" out.OptimizationPolicy.AddDecision(decision) } + if policy.RuleEnabled(PhysicalOptimizationRuleKeyedMapSharing) { + sharePhysicalLookupFamilies(&out, policy) + } if err := out.Validate(); err != nil { return PhysicalPlan{}, fmt.Errorf("validate optimized physical plan: %w", err) } @@ -279,6 +282,17 @@ func rewritePhysicalOperationVariables(op PhysicalOperation, fromTarget, toTarge } op.DerivedLet = &d } + if op.Unnest != nil { + u := *op.Unnest + if u.InputVariable == fromTarget { + u.InputVariable = toTarget + } + if u.InputVariable == fromEdge { + u.InputVariable = toEdge + } + u.Expression = rewritePhysicalExpressionVariables(u.Expression, fromTarget, toTarget, fromEdge, toEdge) + op.Unnest = &u + } return op } @@ -334,6 +348,14 @@ func rewritePhysicalExpressionVariables(expression PhysicalExpression, fromTarge } expression.Object = &object } + if expression.Call != nil { + call := *expression.Call + call.Args = append([]PhysicalExpression(nil), call.Args...) + for index := range call.Args { + call.Args[index] = rewritePhysicalExpressionVariables(call.Args[index], fromTarget, toTarget, fromEdge, toEdge) + } + expression.Call = &call + } return expression } diff --git a/internal/dataframe/compiler/optimize/optimize_test.go b/internal/dataframe/compiler/optimize/optimize_test.go new file mode 100644 index 0000000..f755814 --- /dev/null +++ b/internal/dataframe/compiler/optimize/optimize_test.go @@ -0,0 +1,122 @@ +package optimize + +import ( + "testing" + + "github.com/calypr/loom/internal/dataframe/compiler/ir" +) + +func TestOptimizePhysicalPlanDoesNotShareAcrossUnnestBarrier(t *testing.T) { + plan := optimizerBarrierPlan() + first := plan.Operations[1].Set + second := plan.Operations[3].Set + firstPrefix, err := ir.DecomposePhysicalTraversalPrefixAt(plan, *first, 1) + if err != nil { + t.Fatalf("first prefix: %v", err) + } + secondPrefix, err := ir.DecomposePhysicalTraversalPrefixAt(plan, *second, 3) + if err != nil { + t.Fatalf("second prefix: %v", err) + } + if firstPrefix.PrefixKey == secondPrefix.PrefixKey { + t.Fatalf("prefixes across unnest barrier unexpectedly matched: %s", firstPrefix.PrefixKey) + } + if firstPrefix.Prefix.UnnestScopeIdentity != "" { + t.Fatalf("root-side prefix unexpectedly carried unnest scope: %q", firstPrefix.Prefix.UnnestScopeIdentity) + } + if secondPrefix.Prefix.UnnestScopeIdentity == "" { + t.Fatal("post-unnest prefix omitted active unnest scope identity") + } + + optimized, err := OptimizePhysicalPlan(plan) + if err != nil { + t.Fatalf("OptimizePhysicalPlan() error = %v", err) + } + if optimized.SharedTraversalCount != 0 { + t.Fatalf("sharing crossed unnest barrier: count=%d", optimized.SharedTraversalCount) + } +} + +func TestRewritePhysicalOperationVariablesRewritesUnnestInputAndExpression(t *testing.T) { + op := ir.PhysicalOperation{ + Kind: ir.PhysicalUnnestOp, + Unnest: &ir.PhysicalUnnest{ + InputVariable: "target", + OutputVariable: "item", + Expression: ir.PhysicalExpression{ + Kind: ir.PhysicalValueExpression, + Cardinality: ir.PhysicalArrayCardinality, + NullBehavior: ir.PhysicalEmptyOnNull, + Value: &ir.PhysicalValue{Variable: "target", Path: []string{"payload"}}, + }, + JoinMode: ir.PhysicalUnnestInner, + }, + } + rewritten := rewritePhysicalOperationVariables(op, "target", "shared_item", "edge", "shared_edge") + if got := rewritten.Unnest.InputVariable; got != "shared_item" { + t.Fatalf("unnest input variable = %q, want shared_item", got) + } + if got := rewritten.Unnest.Expression.Value.Variable; got != "shared_item" { + t.Fatalf("unnest expression source = %q, want shared_item", got) + } +} + +func optimizerBarrierPlan() ir.PhysicalPlan { + binds := map[string]any{ + "root_collection": "Patient", + "edge_collection": "fhir_edge", + "condition_label": "subject_Patient", + "condition_type": "Condition", + "specimen_label": "subject_Patient", + "specimen_type": "Specimen", + "project": "project-1", + "dataset_generation": nil, + "auth_resource_paths": []string{}, + "auth_resource_paths_unrestricted": true, + "scope_allowed": true, + } + return ir.PhysicalPlan{ + Version: 1, + BindVars: binds, + Operations: []ir.PhysicalOperation{ + {Kind: ir.PhysicalRootScanOp, RootScan: &ir.PhysicalRootScan{Variable: "root", CollectionBindKey: "root_collection"}}, + {Kind: ir.PhysicalSetOp, Set: optimizerBarrierSet("condition_set", "condition_label", "condition_type")}, + {Kind: ir.PhysicalUnnestOp, Unnest: &ir.PhysicalUnnest{ + InputVariable: "root", + OutputVariable: "expanded", + Expression: ir.PhysicalExpression{ + Kind: ir.PhysicalValueExpression, + Cardinality: ir.PhysicalArrayCardinality, + NullBehavior: ir.PhysicalEmptyOnNull, + Value: &ir.PhysicalValue{Variable: "root", Path: []string{"payload"}}, + }, + JoinMode: ir.PhysicalUnnestInner, + }}, + {Kind: ir.PhysicalSetOp, Set: optimizerBarrierSet("specimen_set", "specimen_label", "specimen_type")}, + {Kind: ir.PhysicalReturnOp, Return: &ir.PhysicalReturn{Projections: []ir.PhysicalProjection{{Name: "id", Value: ir.PhysicalValue{Variable: "root", Path: []string{"_key"}}}}}}, + }, + } +} + +func optimizerBarrierSet(variable, labelBind, typeBind string) *ir.PhysicalSet { + traversal := ir.PhysicalTraversal{ + SourceVariable: "root", + TargetVariable: variable + "_node", + EdgeVariable: variable + "_edge", + Direction: ir.PhysicalInbound, + EdgeCollectionBindKey: "edge_collection", + EdgeLabelBindKey: labelBind, + TargetTypeBindKey: typeBind, + EdgeTargetTypeField: "from_type", + } + scope := []ir.PhysicalOperation{ + {Kind: ir.PhysicalTraversalOp, Traversal: &traversal}, + {Kind: ir.PhysicalFilterOp, Filter: &ir.PhysicalFilter{Predicate: ir.PhysicalPredicate{Operator: "EQUALS", Left: ir.PhysicalValue{Variable: traversal.EdgeVariable, Path: []string{"project"}}, Right: &ir.PhysicalValue{BindKey: "project"}}}}, + {Kind: ir.PhysicalFilterOp, Filter: &ir.PhysicalFilter{Predicate: ir.PhysicalPredicate{Operator: "EQUALS", Left: ir.PhysicalValue{Variable: traversal.TargetVariable, Path: []string{"project"}}, Right: &ir.PhysicalValue{BindKey: "project"}}}}, + {Kind: ir.PhysicalFilterOp, Filter: &ir.PhysicalFilter{Predicate: ir.PhysicalPredicate{Operator: "EQUALS", Left: ir.PhysicalValue{Variable: traversal.EdgeVariable, Path: []string{"dataset_generation"}}, Right: &ir.PhysicalValue{BindKey: "dataset_generation"}}}}, + {Kind: ir.PhysicalFilterOp, Filter: &ir.PhysicalFilter{Predicate: ir.PhysicalPredicate{Operator: "EQUALS", Left: ir.PhysicalValue{Variable: traversal.TargetVariable, Path: []string{"dataset_generation"}}, Right: &ir.PhysicalValue{BindKey: "dataset_generation"}}}}, + {Kind: ir.PhysicalDerivedLetOp, DerivedLet: &ir.PhysicalDerivedLet{Variable: variable + "_scope", Operator: "AUTH_RESOURCE_PATH_ALLOWED", Inputs: []ir.PhysicalValue{{Variable: traversal.EdgeVariable, Path: []string{"auth_resource_path"}}, {Variable: traversal.TargetVariable, Path: []string{"auth_resource_path"}}, {BindKey: "auth_resource_paths"}, {BindKey: "auth_resource_paths_unrestricted"}}}}, + {Kind: ir.PhysicalFilterOp, Filter: &ir.PhysicalFilter{Predicate: ir.PhysicalPredicate{Operator: "EQUALS", Left: ir.PhysicalValue{Variable: variable + "_scope"}, Right: &ir.PhysicalValue{BindKey: "scope_allowed"}}}}, + } + return &ir.PhysicalSet{Variable: variable, Subplan: ir.PhysicalSubplan{Captures: []string{"root"}, Operations: scope, Return: ir.PhysicalExpression{Kind: ir.PhysicalValueExpression, Cardinality: ir.PhysicalObjectCardinality, NullBehavior: ir.PhysicalPreserveNull, Value: &ir.PhysicalValue{Variable: traversal.TargetVariable}}}} +} diff --git a/internal/dataframe/compiler/optimize_aliases.go b/internal/dataframe/compiler/optimize_aliases.go index 5e95030..1a9500e 100644 --- a/internal/dataframe/compiler/optimize_aliases.go +++ b/internal/dataframe/compiler/optimize_aliases.go @@ -1,8 +1,6 @@ package compiler import ( - "fmt" - "github.com/calypr/loom/internal/dataframe/compiler/optimize" ) @@ -10,7 +8,3 @@ var ( OptimizePhysicalPlan = optimize.OptimizePhysicalPlan OptimizePhysicalPlanWithPolicy = optimize.OptimizePhysicalPlanWithPolicy ) - -func valueString(value any) string { - return fmt.Sprint(value) -} diff --git a/internal/dataframe/compiler/optimizer.go b/internal/dataframe/compiler/optimizer.go index bc217c4..5312d3c 100644 --- a/internal/dataframe/compiler/optimizer.go +++ b/internal/dataframe/compiler/optimizer.go @@ -10,12 +10,3 @@ const ( // post-projection filters. OptimizerRuleRelationshipSemiJoin = "root_relationship_semi_join" ) - -func containsOptimizerRule(rules []string, want string) bool { - for _, rule := range rules { - if rule == want { - return true - } - } - return false -} diff --git a/internal/dataframe/compiler/output.go b/internal/dataframe/compiler/output.go index fa47991..f0126bc 100644 --- a/internal/dataframe/compiler/output.go +++ b/internal/dataframe/compiler/output.go @@ -2,61 +2,29 @@ package compiler import "fmt" -const genericPhysicalExecutionLimitBind = "limit" - -// compilePhysicalExecution renders a validated semantic physical plan. Its -// caller has already established that the plan represents the complete -// request, so it never needs compatibility named sets or string lowering. -func compilePhysicalExecution(physical PhysicalPlan, semantic SemanticPlan, limit int) (CompiledQuery, error) { - var err error - physical, err = withGenericPhysicalExecutionWindow(physical, limit) - if err != nil { - return CompiledQuery{}, err - } - rendered, err := RenderPhysicalPlan(physical) - if err != nil { - return CompiledQuery{}, fmt.Errorf("render generic physical execution plan: %w", err) +// publicOutputColumns returns the ordered transport schema owned by the +// compiler. Physical identity and executor-validation projections are kept in +// OutputSchema but never leak into dataframe JSON/CSV/publication columns. +func publicOutputColumns(schema []CompiledOutputColumn) []string { + columns := make([]string, 0, len(schema)) + for _, column := range schema { + if column.Internal { + continue + } + columns = append(columns, column.Name) } - - columns, pivotFields := physicalProjectionMetadata(physical) - return CompiledQuery{ - Project: semantic.Project, - DatasetGeneration: normalizeDatasetGeneration(semantic.DatasetGeneration), - RootResourceType: semantic.Root.ResourceType, - AuthResourcePaths: cloneStrings(semantic.AuthResourcePaths), - PlanMode: "physical", - PlanProfile: "generic_fhir_graph", - TraversalCount: physicalTraversalCount(physical), - OptimizationRules: physicalOptimizationRules(semantic.Root), - RowIdentity: cloneRowIdentity(semantic.RowIdentity), - Query: rendered.Query, - BindVars: rendered.BindVars, - Columns: columns, - PivotFields: pivotFields, - Limit: limit, - PlanDiagnostics: physicalPlanDiagnostics(physical), - }, nil + return columns } -func physicalOptimizationRules(node SemanticNode) []string { - rules := make([]string, 0, 2) - if len(node.Filters) != 0 { - rules = append(rules, OptimizerRuleFilterPushdown) - } - for _, child := range node.Children { - if child.MatchMode.Required() { - rules = appendUniqueRule(rules, OptimizerRuleRelationshipSemiJoin) - } - for _, rule := range physicalOptimizationRules(child) { - rules = appendUniqueRule(rules, rule) - } - } - if len(rules) == 0 { - return nil - } - return rules +// PublicOutputColumns returns a copy of the compiler-owned transport schema. +// It is used by recipe execution adapters that need to carry the schema with +// streamed rows without re-walking semantic recipe nodes. +func PublicOutputColumns(schema []CompiledOutputColumn) []string { + return publicOutputColumns(schema) } +const genericPhysicalExecutionLimitBind = "limit" + func physicalProjectionMetadata(plan PhysicalPlan) ([]string, []string) { for _, operation := range plan.Operations { if operation.Kind != PhysicalReturnOp || operation.Return == nil { @@ -65,6 +33,9 @@ func physicalProjectionMetadata(plan PhysicalPlan) ([]string, []string) { columns := make([]string, 0, len(operation.Return.Projections)) var pivots []string for _, projection := range operation.Return.Projections { + if projection.Hidden { + continue + } columns = append(columns, projection.Name) if projection.Expression != nil && projection.Expression.Kind == PhysicalPivotExpression { pivots = append(pivots, projection.Name) @@ -100,6 +71,13 @@ func withGenericPhysicalExecutionWindow(plan PhysicalPlan, limit int) (PhysicalP // to the first traversal or terminal return. BuildGenericPhysicalPlan has // already proven that the whole prefix scopes the root correctly. insertAt := physicalScopeWindowEnd(plan.Operations, 1) + // Shared expression LETs are deliberately emitted immediately before + // RETURN for root-only plans. Put the root execution window before them so + // AQL can discard rows before evaluating the family maps, while traversal + // plans still insert the window before their first child SET. + for insertAt > 1 && insertAt <= len(plan.Operations) && plan.Operations[insertAt-1].Kind == PhysicalExpressionLetOp { + insertAt-- + } if insertAt <= 1 || insertAt >= len(plan.Operations) { return PhysicalPlan{}, fmt.Errorf("generic physical execution plan requires a scoped root followed by RETURN or traversal") } diff --git a/internal/dataframe/compiler/physical_aliases.go b/internal/dataframe/compiler/physical_aliases.go index f120bde..7e92902 100644 --- a/internal/dataframe/compiler/physical_aliases.go +++ b/internal/dataframe/compiler/physical_aliases.go @@ -17,6 +17,8 @@ type ( PhysicalExpressionKind = ir.PhysicalExpressionKind PhysicalSelectorExecutionMode = ir.PhysicalSelectorExecutionMode PhysicalExpression = ir.PhysicalExpression + PhysicalLiteral = ir.PhysicalLiteral + PhysicalCall = ir.PhysicalCall PhysicalExtract = ir.PhysicalExtract PhysicalPreparedReference = ir.PhysicalPreparedReference PhysicalPreparedSet = ir.PhysicalPreparedSet @@ -24,6 +26,8 @@ type ( PhysicalAggregateOperation = ir.PhysicalAggregateOperation PhysicalAggregate = ir.PhysicalAggregate PhysicalPivotMap = ir.PhysicalPivotMap + PhysicalLookup = ir.PhysicalLookup + PhysicalKeySet = ir.PhysicalKeySet PhysicalSlice = ir.PhysicalSlice PhysicalExpressionProjection = ir.PhysicalExpressionProjection PhysicalObject = ir.PhysicalObject @@ -38,6 +42,10 @@ type ( PhysicalPredicateExpression = ir.PhysicalPredicateExpression PhysicalFilter = ir.PhysicalFilter PhysicalDerivedLet = ir.PhysicalDerivedLet + PhysicalExpressionLet = ir.PhysicalExpressionLet + PhysicalObjectLookup = ir.PhysicalObjectLookup + PhysicalKeyedMap = ir.PhysicalKeyedMap + PhysicalObjectKeys = ir.PhysicalObjectKeys PhysicalSort = ir.PhysicalSort PhysicalLimit = ir.PhysicalLimit PhysicalProjection = ir.PhysicalProjection @@ -63,6 +71,7 @@ const ( PhysicalTraversalOp = ir.PhysicalTraversalOp PhysicalFilterOp = ir.PhysicalFilterOp PhysicalDerivedLetOp = ir.PhysicalDerivedLetOp + PhysicalExpressionLetOp = ir.PhysicalExpressionLetOp PhysicalSetOp = ir.PhysicalSetOp PhysicalSortOp = ir.PhysicalSortOp PhysicalLimitOp = ir.PhysicalLimitOp @@ -79,11 +88,18 @@ const ( PhysicalOmitNulls = ir.PhysicalOmitNulls PhysicalEmptyOnNull = ir.PhysicalEmptyOnNull PhysicalValueExpression = ir.PhysicalValueExpression + PhysicalLiteralExpression = ir.PhysicalLiteralExpression PhysicalExtractExpression = ir.PhysicalExtractExpression PhysicalAggregateExpression = ir.PhysicalAggregateExpression PhysicalPivotExpression = ir.PhysicalPivotExpression PhysicalSliceExpression = ir.PhysicalSliceExpression + PhysicalLookupExpression = ir.PhysicalLookupExpression PhysicalObjectExpression = ir.PhysicalObjectExpression + PhysicalCallExpression = ir.PhysicalCallExpression + PhysicalKeySetExpression = ir.PhysicalKeySetExpression + PhysicalObjectLookupExpression = ir.PhysicalObjectLookupExpression + PhysicalKeyedMapExpression = ir.PhysicalKeyedMapExpression + PhysicalObjectKeysExpression = ir.PhysicalObjectKeysExpression PhysicalSelectorGeneric = ir.PhysicalSelectorGeneric PhysicalSelectorDirectScalar = ir.PhysicalSelectorDirectScalar PhysicalSelectorConditionalArray = ir.PhysicalSelectorConditionalArray @@ -118,6 +134,7 @@ const ( PhysicalOptimizationRuleRichConsumerFusion = ir.PhysicalOptimizationRuleRichConsumerFusion PhysicalOptimizationRuleCompactProjection = ir.PhysicalOptimizationRuleCompactProjection PhysicalOptimizationRuleEndpointTraversal = ir.PhysicalOptimizationRuleEndpointTraversal + PhysicalOptimizationRuleKeyedMapSharing = ir.PhysicalOptimizationRuleKeyedMapSharing ) var ( diff --git a/internal/dataframe/compiler/physical_clone_aliases.go b/internal/dataframe/compiler/physical_clone_aliases.go deleted file mode 100644 index d9a6cd7..0000000 --- a/internal/dataframe/compiler/physical_clone_aliases.go +++ /dev/null @@ -1,23 +0,0 @@ -package compiler - -import "github.com/calypr/loom/internal/dataframe/compiler/ir" - -func clonePhysicalPlan(plan PhysicalPlan) PhysicalPlan { return ir.ClonePhysicalPlan(plan) } -func clonePhysicalOperation(operation PhysicalOperation) PhysicalOperation { - return ir.ClonePhysicalOperation(operation) -} -func clonePhysicalOperations(operations []PhysicalOperation) []PhysicalOperation { - return ir.ClonePhysicalOperations(operations) -} -func clonePhysicalPredicate(predicate PhysicalPredicate) PhysicalPredicate { - return ir.ClonePhysicalPredicate(predicate) -} -func clonePhysicalPredicateExpression(predicate PhysicalPredicateExpression) PhysicalPredicateExpression { - return ir.ClonePhysicalPredicateExpression(predicate) -} -func clonePhysicalExpression(expression PhysicalExpression) PhysicalExpression { - return ir.ClonePhysicalExpression(expression) -} -func clonePhysicalSubplan(subplan PhysicalSubplan) PhysicalSubplan { - return ir.ClonePhysicalSubplan(subplan) -} diff --git a/internal/dataframe/compiler/physical_execution_test.go b/internal/dataframe/compiler/physical_execution_test.go deleted file mode 100644 index 8ed8166..0000000 --- a/internal/dataframe/compiler/physical_execution_test.go +++ /dev/null @@ -1,107 +0,0 @@ -package compiler - -import ( - "strings" - "testing" -) - -func TestCompileRequestUsesPhysicalExecutionForNavigationOnlyGenericPlan(t *testing.T) { - builder := Builder{ - Project: "P1", - DatasetGeneration: "generation-a", - AuthResourcePaths: []string{"/programs/p1"}, - RootResourceType: "Specimen", - Traversals: []TraversalStep{{ - Label: "subject_Specimen", - ToResourceType: "DocumentReference", - Alias: "file", - }}, - } - physical, err := CompileRequest(builder, 7) - if err != nil { - t.Fatalf("CompileRequest() error = %v", err) - } - if got := physical.BindVars["@root_collection"]; got != "Specimen" { - t.Fatalf("physical root collection bind = %#v", got) - } - if got := physical.BindVars["@traversal_1_edge_collection"]; got != "fhir_edge" { - t.Fatalf("physical edge collection bind = %#v", got) - } - for _, want := range []string{ - "FOR root IN @@root_collection", - "SORT root._key", - "LIMIT @limit", - "FOR edge_1 IN @@traversal_1_edge_collection", - "LET node_1 = DOCUMENT(edge_1._from)", - "RETURN { [@__loom_physical_projection_0_name]: root._key }", - } { - if !strings.Contains(physical.Query, want) { - t.Fatalf("physical execution query missing %q:\n%s", want, physical.Query) - } - } - if got, want := strings.Index(physical.Query, "LIMIT @limit"), strings.Index(physical.Query, "LET __loom_physical_set_1"); got < 0 || want < 0 || got > want { - t.Fatalf("root limit must occur before optional navigation materialization:\n%s", physical.Query) - } - if strings.Contains(physical.Query, "FOR root IN Specimen") || strings.Contains(physical.Query, "generic_file_set") { - t.Fatalf("physical request unexpectedly used a compatibility string renderer:\n%s", physical.Query) - } - -} - -func TestCompileRequestUsesPhysicalExecutionForRootSelection(t *testing.T) { - compiled, err := CompileRequest(Builder{ - Project: "P1", - RootResourceType: "Specimen", - Fields: []FieldSelect{{Name: "id", Select: "id"}}, - }, 3) - if err != nil { - t.Fatalf("CompileRequest() error = %v", err) - } - if !strings.Contains(compiled.Query, "FOR root IN @@root_collection") || !strings.Contains(compiled.Query, "root.payload.id") { - t.Fatalf("selection request did not use the physical renderer:\n%s", compiled.Query) - } - if !strings.Contains(compiled.Query, "@@root_collection") { - t.Fatalf("selection request was not routed through the physical renderer:\n%s", compiled.Query) - } -} - -func TestGenericPhysicalExecutionWindowIsTypedAndOptional(t *testing.T) { - semantic, err := BuildSemanticPlan(Builder{Project: "P1", RootResourceType: "Patient"}) - if err != nil { - t.Fatal(err) - } - plan, err := BuildGenericPhysicalPlan(semantic) - if err != nil { - t.Fatal(err) - } - plan, err = withGenericPhysicalExecutionWindow(plan, 2) - if err != nil { - t.Fatal(err) - } - if len(plan.Operations) < 8 || plan.Operations[5].Kind != PhysicalSortOp || plan.Operations[6].Kind != PhysicalLimitOp { - t.Fatalf("typed execution window = %#v", plan.Operations) - } - if got := plan.BindVars[genericPhysicalExecutionLimitBind]; got != 2 { - t.Fatalf("typed limit bind = %#v", got) - } - if err := plan.Validate(); err != nil { - t.Fatalf("windowed physical plan did not validate: %v", err) - } - - withoutLimit, err := withGenericPhysicalExecutionWindow(buildPhysicalPlanForTest(t, semantic), 0) - if err != nil { - t.Fatal(err) - } - if len(withoutLimit.Operations) < 7 || withoutLimit.Operations[5].Kind != PhysicalSortOp || withoutLimit.Operations[6].Kind == PhysicalLimitOp { - t.Fatalf("unbounded physical execution window = %#v", withoutLimit.Operations) - } -} - -func buildPhysicalPlanForTest(t *testing.T, semantic SemanticPlan) PhysicalPlan { - t.Helper() - plan, err := BuildGenericPhysicalPlan(semantic) - if err != nil { - t.Fatal(err) - } - return plan -} diff --git a/internal/dataframe/compiler/physical_optimize_test.go b/internal/dataframe/compiler/physical_optimize_test.go index c3023f8..00f6db6 100644 --- a/internal/dataframe/compiler/physical_optimize_test.go +++ b/internal/dataframe/compiler/physical_optimize_test.go @@ -1,6 +1,8 @@ package compiler import ( + "fmt" + "slices" "strings" "testing" ) @@ -14,7 +16,7 @@ func TestOptimizePhysicalPlanSharesEquivalentTypedPrefixes(t *testing.T) { if optimized.SharedTraversalCount != 1 { t.Fatalf("shared traversal count = %d, want 1", optimized.SharedTraversalCount) } - if !containsOptimizerRule(optimized.AppliedRules, OptimizerRuleTraversalSharing) { + if !slices.Contains(optimized.AppliedRules, OptimizerRuleTraversalSharing) { t.Fatalf("missing sharing rule: %#v", optimized.AppliedRules) } if len(optimized.Operations) != len(plan.Operations)+1 { @@ -39,10 +41,48 @@ func TestOptimizePhysicalPlanSharesEquivalentTypedPrefixes(t *testing.T) { } } +func TestOptimizePhysicalPlanSharesRepeatedLookups(t *testing.T) { + plan, err := BuildGenericPhysicalPlan(SemanticPlan{Version: 1, Project: "p", Root: SemanticNode{Alias: "root", ResourceType: "Patient"}}) + if err != nil { + t.Fatal(err) + } + plan.BindVars["lookup_a"] = "a" + plan.BindVars["lookup_b"] = "b" + source := PhysicalExpression{Kind: PhysicalValueExpression, Cardinality: PhysicalArrayCardinality, NullBehavior: PhysicalPreserveNull, Value: &PhysicalValue{Variable: "root", Path: []string{"payload", "identifier"}}} + key := PhysicalExpression{Kind: PhysicalValueExpression, Cardinality: PhysicalScalarCardinality, NullBehavior: PhysicalPreserveNull, Value: &PhysicalValue{Variable: "lookup_item", Path: []string{"value"}}} + value := key + returnOp := &plan.Operations[len(plan.Operations)-1] + for _, item := range []struct{ name, bind string }{{"a", "lookup_a"}, {"b", "lookup_b"}} { + expression := PhysicalExpression{Kind: PhysicalLookupExpression, Cardinality: PhysicalScalarCardinality, NullBehavior: PhysicalPreserveNull, Lookup: &PhysicalLookup{Source: source, ItemVariable: "lookup_item", ItemKey: key, ItemValue: value, MatchBindKey: item.bind}} + returnOp.Return.Projections = append(returnOp.Return.Projections, PhysicalProjection{Name: item.name, Expression: &expression}) + } + optimized, err := OptimizePhysicalPlan(plan) + if err != nil { + t.Fatal(err) + } + var lets, lookups int + for _, operation := range optimized.Operations { + if operation.Kind == PhysicalExpressionLetOp { + lets++ + } + if operation.Kind != PhysicalReturnOp || operation.Return == nil { + continue + } + for _, projection := range operation.Return.Projections { + if projection.Expression != nil && projection.Expression.Kind == PhysicalObjectLookupExpression { + lookups++ + } + } + } + if lets != 1 || lookups != 2 { + t.Fatalf("shared lookup plan has %d lets and %d object lookups, want 1/2", lets, lookups) + } +} + func TestOptimizePhysicalPlanKeepsConsumerProjectionOffBroadSharedSet(t *testing.T) { plan := physicalScopedSiblingPlan(t) for _, set := range physicalSets(plan) { - resourceType := valueString(plan.BindVars[set.Subplan.Operations[0].Traversal.TargetTypeBindKey]) + resourceType := fmt.Sprint(plan.BindVars[set.Subplan.Operations[0].Traversal.TargetTypeBindKey]) set.Projection = &PhysicalSetProjection{Fields: []PhysicalSetProjectionField{{Name: "__loom_projection_0", ResourceType: resourceType, Selector: mustPhysicalSelector(t, "id")}}} set.Output = nil } @@ -126,15 +166,6 @@ func TestOptimizePhysicalPlanCostPolicyMinimumSavingsRejectsCandidate(t *testing } } -func TestEstimatePreparedSelectorWorkRequiresRepeatedConsumer(t *testing.T) { - if baseline, optimized, savings := estimatePreparedSelectorWork(1); baseline != 0 || optimized != 0 || savings != 0 { - t.Fatalf("single selector estimate = %d/%d/%d, want no preparation", baseline, optimized, savings) - } - if baseline, optimized, savings := estimatePreparedSelectorWork(2); baseline <= optimized || savings <= 0 { - t.Fatalf("repeated selector estimate = %d/%d/%d, want positive savings", baseline, optimized, savings) - } -} - func TestDefaultPhysicalOptimizationPolicyDeveloperSwitch(t *testing.T) { t.Setenv("LOOM_PHYSICAL_COST_POLICY", "off") policy := DefaultPhysicalOptimizationPolicy() @@ -382,7 +413,7 @@ func physicalScopedSiblingPlanWithFilters(t *testing.T) PhysicalPlan { traversal := set.Subplan.Operations[0].Traversal predicate := PhysicalPredicate{Operator: string(FilterExists), ValueKind: FilterString, LeftExpression: &PhysicalExpression{Kind: PhysicalExtractExpression, Cardinality: PhysicalArrayCardinality, NullBehavior: PhysicalEmptyOnNull, - Extract: &PhysicalExtract{Source: PhysicalValue{Variable: traversal.TargetVariable, Path: []string{"payload"}}, ResourceType: valueString(plan.BindVars[traversal.TargetTypeBindKey]), Selector: mustPhysicalSelector(t, "id")}}} + Extract: &PhysicalExtract{Source: PhysicalValue{Variable: traversal.TargetVariable, Path: []string{"payload"}}, ResourceType: fmt.Sprint(plan.BindVars[traversal.TargetTypeBindKey]), Selector: mustPhysicalSelector(t, "id")}}} set.Subplan.Operations = append(set.Subplan.Operations, PhysicalOperation{Kind: PhysicalFilterOp, Filter: &PhysicalFilter{Expression: &PhysicalPredicateExpression{Kind: PhysicalComparisonPredicate, Comparison: &predicate}}}) } if err := plan.Validate(); err != nil { diff --git a/internal/dataframe/compiler/physical_render_test.go b/internal/dataframe/compiler/physical_render_test.go index 270cfc3..703acdf 100644 --- a/internal/dataframe/compiler/physical_render_test.go +++ b/internal/dataframe/compiler/physical_render_test.go @@ -152,17 +152,6 @@ func TestRenderPhysicalPlanIsDeterministicAndCopiesBindVars(t *testing.T) { } } -func TestPruneUnusedRuntimeBindVars(t *testing.T) { - bindVars := map[string]any{"used": 1, "unused": 2} - got := pruneUnusedRuntimeBindVars(bindVars, "FOR doc IN c FILTER @used == 1 RETURN doc") - if _, ok := got["used"]; !ok { - t.Fatalf("used bind was pruned: %#v", got) - } - if _, ok := got["unused"]; ok { - t.Fatalf("unused bind was retained: %#v", got) - } -} - func TestRenderPhysicalPlanNestedObjectExpression(t *testing.T) { plan, err := BuildGenericPhysicalPlan(SemanticPlan{ Version: 1, Project: "project-1", AuthResourcePaths: []string{"/programs/p1"}, diff --git a/internal/dataframe/compiler/physical_scope_compat.go b/internal/dataframe/compiler/physical_scope_compat.go deleted file mode 100644 index 5187526..0000000 --- a/internal/dataframe/compiler/physical_scope_compat.go +++ /dev/null @@ -1,23 +0,0 @@ -package compiler - -// These small test-facing predicates preserve the historical same-package -// test vocabulary while scope validation now lives in compiler/ir. -func physicalPathEquals(left, right []string) bool { - if len(left) != len(right) { - return false - } - for i := range left { - if left[i] != right[i] { - return false - } - } - return true -} - -func isProjectScopePredicate(predicate PhysicalPredicate, variable string) bool { - return predicate.Operator == "EQUALS" && predicate.Left.Variable == variable && physicalPathEquals(predicate.Left.Path, []string{"project"}) && predicate.Right != nil && predicate.Right.BindKey == "project" -} - -func isScopeAllowedPredicate(predicate PhysicalPredicate, variable string) bool { - return predicate.Operator == "EQUALS" && predicate.Left.Variable == variable && predicate.Right != nil && predicate.Right.BindKey == "scope_allowed" -} diff --git a/internal/dataframe/compiler/physical_scope_test.go b/internal/dataframe/compiler/physical_scope_test.go index 246f4a3..5d7c000 100644 --- a/internal/dataframe/compiler/physical_scope_test.go +++ b/internal/dataframe/compiler/physical_scope_test.go @@ -5,6 +5,26 @@ import ( "testing" ) +func physicalPathEquals(left, right []string) bool { + if len(left) != len(right) { + return false + } + for i := range left { + if left[i] != right[i] { + return false + } + } + return true +} + +func isProjectScopePredicate(predicate PhysicalPredicate, variable string) bool { + return predicate.Operator == "EQUALS" && predicate.Left.Variable == variable && physicalPathEquals(predicate.Left.Path, []string{"project"}) && predicate.Right != nil && predicate.Right.BindKey == "project" +} + +func isScopeAllowedPredicate(predicate PhysicalPredicate, variable string) bool { + return predicate.Operator == "EQUALS" && predicate.Left.Variable == variable && predicate.Right != nil && predicate.Right.BindKey == "scope_allowed" +} + func TestValidateGenericPhysicalPlanScope(t *testing.T) { plan := genericScopePhysicalPlan(t) if err := ValidateGenericPhysicalPlanScope(plan); err != nil { diff --git a/internal/dataframe/compiler/recipe_execution.go b/internal/dataframe/compiler/recipe_execution.go new file mode 100644 index 0000000..5d905dc --- /dev/null +++ b/internal/dataframe/compiler/recipe_execution.go @@ -0,0 +1,118 @@ +package compiler + +import ( + "fmt" + + "github.com/calypr/loom/internal/dataframe/recipe" + "github.com/calypr/loom/internal/dataframe/semantic" +) + +// CompileResolvedRecipePlanWithPolicy is the common execution boundary for a +// resolved recipe bundle. Lowering produces canonical physical plans; this +// function applies the generic optimizer, inserts the typed execution window, +// and renders through RenderPhysicalPlan for each output. +func CompileResolvedRecipePlanWithPolicy(resolved semantic.ResolvedRecipePlan, limit int, policy PhysicalOptimizationPolicy) ([]CompiledQuery, error) { + compiled, err := CompileResolvedRecipePlan(resolved, policy) + if err != nil { + return nil, err + } + queries := make([]CompiledQuery, 0, len(compiled.Outputs)) + for _, output := range compiled.Outputs { + query, err := CompileRecipeOutputWithPolicy(output, resolved.SemanticPlan.Bindings, limit, policy) + if err != nil { + return nil, fmt.Errorf("output %q: %w", output.Name, err) + } + queries = append(queries, query) + } + return queries, nil +} + +// CompileRecipeOutputWithPolicy applies the common optimizer, execution +// window, and canonical renderer to one already-lowered recipe output. +func CompileRecipeOutputWithPolicy(output CompiledRecipeOutput, bindings recipe.RuntimeBindings, limit int, policy PhysicalOptimizationPolicy) (CompiledQuery, error) { + var physical PhysicalPlan + if output.OptimizedPlan != nil { + physical = clonePhysicalPlan(*output.OptimizedPlan) + } else { + var err error + physical, err = OptimizePhysicalPlanWithPolicy(output.Plan, policy) + if err != nil { + return CompiledQuery{}, fmt.Errorf("optimize canonical recipe plan: %w", err) + } + } + physical, err := withGenericPhysicalExecutionWindow(physical, limit) + if err != nil { + return CompiledQuery{}, fmt.Errorf("apply canonical recipe execution window: %w", err) + } + if bindings.IncludeAuthResourcePath { + if err := appendAuthResourcePathProjection(&physical); err != nil { + return CompiledQuery{}, err + } + } + rendered, err := RenderPhysicalPlan(physical) + if err != nil { + return CompiledQuery{}, fmt.Errorf("render canonical recipe physical plan: %w", err) + } + columns, pivotFields := physicalProjectionMetadata(physical) + if len(output.Columns) != 0 { + columns = append([]string(nil), output.Columns...) + } + outputSchema := append([]CompiledOutputColumn(nil), output.OutputSchema...) + publicColumns := publicOutputColumns(outputSchema) + if len(publicColumns) == 0 { + for _, column := range columns { + if column == "_key" || column == "__loom_row_id" || column == "__loom_dynamic_runtime_keys" { + continue + } + publicColumns = append(publicColumns, column) + } + } + return CompiledQuery{ + Project: bindings.Project, + DatasetGeneration: normalizeDatasetGeneration(bindings.DatasetGeneration), + RootResourceType: output.RootResourceType, + AuthResourcePaths: cloneStrings(bindings.AuthResourcePaths), + PlanMode: "physical", + PlanProfile: "generic_fhir_graph_recipe", + TraversalCount: physicalTraversalCount(physical), + RowIdentity: cloneRowIdentity(output.RowIdentity), + OptimizationRules: recipeOptimizationRules(physical), + Query: rendered.Query, + BindVars: rendered.BindVars, + Columns: columns, + OutputSchema: outputSchema, + PublicColumns: publicColumns, + PivotFields: pivotFields, + Limit: limit, + PlanDiagnostics: physicalPlanDiagnostics(physical), + }, nil +} + +func appendAuthResourcePathProjection(physical *PhysicalPlan) error { + for index := range physical.Operations { + operation := &physical.Operations[index] + if operation.Kind != PhysicalReturnOp || operation.Return == nil { + continue + } + for _, projection := range operation.Return.Projections { + if projection.Name == "auth_resource_path" { + return nil + } + } + operation.Return.Projections = append(operation.Return.Projections, PhysicalProjection{ + Name: "auth_resource_path", Hidden: true, + Value: PhysicalValue{Variable: "root", Path: []string{"auth_resource_path"}}, + }) + return nil + } + return fmt.Errorf("canonical plan has no RETURN operation for auth_resource_path projection") +} + +func recipeOptimizationRules(plan PhysicalPlan) []string { + for _, operation := range plan.Operations { + if operation.Kind == PhysicalFilterOp { + return []string{OptimizerRuleFilterPushdown} + } + } + return nil +} diff --git a/internal/dataframe/compiler/render/aql/aliases.go b/internal/dataframe/compiler/render/aql/aliases.go index 138c4ae..39c11b6 100644 --- a/internal/dataframe/compiler/render/aql/aliases.go +++ b/internal/dataframe/compiler/render/aql/aliases.go @@ -20,6 +20,8 @@ type ( PhysicalExpressionKind = ir.PhysicalExpressionKind PhysicalSelectorExecutionMode = ir.PhysicalSelectorExecutionMode PhysicalExpression = ir.PhysicalExpression + PhysicalLiteral = ir.PhysicalLiteral + PhysicalCall = ir.PhysicalCall PhysicalExtract = ir.PhysicalExtract PhysicalPreparedReference = ir.PhysicalPreparedReference PhysicalPreparedSet = ir.PhysicalPreparedSet @@ -28,9 +30,13 @@ type ( PhysicalAggregate = ir.PhysicalAggregate PhysicalPivotMap = ir.PhysicalPivotMap PhysicalSlice = ir.PhysicalSlice + PhysicalLookup = ir.PhysicalLookup + PhysicalKeySet = ir.PhysicalKeySet PhysicalExpressionProjection = ir.PhysicalExpressionProjection PhysicalObject = ir.PhysicalObject PhysicalSet = ir.PhysicalSet + PhysicalUnnest = ir.PhysicalUnnest + PhysicalUnnestJoinMode = ir.PhysicalUnnestJoinMode PhysicalSetProjection = ir.PhysicalSetProjection PhysicalSetProjectionField = ir.PhysicalSetProjectionField PhysicalSetOutputField = ir.PhysicalSetOutputField @@ -41,6 +47,10 @@ type ( PhysicalPredicateExpression = ir.PhysicalPredicateExpression PhysicalFilter = ir.PhysicalFilter PhysicalDerivedLet = ir.PhysicalDerivedLet + PhysicalExpressionLet = ir.PhysicalExpressionLet + PhysicalObjectLookup = ir.PhysicalObjectLookup + PhysicalKeyedMap = ir.PhysicalKeyedMap + PhysicalObjectKeys = ir.PhysicalObjectKeys PhysicalSort = ir.PhysicalSort PhysicalLimit = ir.PhysicalLimit PhysicalProjection = ir.PhysicalProjection @@ -69,7 +79,9 @@ const ( PhysicalTraversalOp = ir.PhysicalTraversalOp PhysicalFilterOp = ir.PhysicalFilterOp PhysicalDerivedLetOp = ir.PhysicalDerivedLetOp + PhysicalExpressionLetOp = ir.PhysicalExpressionLetOp PhysicalSetOp = ir.PhysicalSetOp + PhysicalUnnestOp = ir.PhysicalUnnestOp PhysicalSortOp = ir.PhysicalSortOp PhysicalLimitOp = ir.PhysicalLimitOp PhysicalReturnOp = ir.PhysicalReturnOp @@ -84,11 +96,18 @@ const ( PhysicalOmitNulls = ir.PhysicalOmitNulls PhysicalEmptyOnNull = ir.PhysicalEmptyOnNull PhysicalValueExpression = ir.PhysicalValueExpression + PhysicalLiteralExpression = ir.PhysicalLiteralExpression PhysicalExtractExpression = ir.PhysicalExtractExpression PhysicalAggregateExpression = ir.PhysicalAggregateExpression PhysicalPivotExpression = ir.PhysicalPivotExpression PhysicalSliceExpression = ir.PhysicalSliceExpression + PhysicalLookupExpression = ir.PhysicalLookupExpression + PhysicalObjectLookupExpression = ir.PhysicalObjectLookupExpression + PhysicalKeyedMapExpression = ir.PhysicalKeyedMapExpression + PhysicalObjectKeysExpression = ir.PhysicalObjectKeysExpression + PhysicalKeySetExpression = ir.PhysicalKeySetExpression PhysicalObjectExpression = ir.PhysicalObjectExpression + PhysicalCallExpression = ir.PhysicalCallExpression PhysicalSelectorGeneric = ir.PhysicalSelectorGeneric PhysicalSelectorDirectScalar = ir.PhysicalSelectorDirectScalar PhysicalSelectorConditionalArray = ir.PhysicalSelectorConditionalArray @@ -99,11 +118,15 @@ const ( PhysicalMinAggregate = ir.PhysicalMinAggregate PhysicalMaxAggregate = ir.PhysicalMaxAggregate PhysicalFirstAggregate = ir.PhysicalFirstAggregate + PhysicalMapFirst = ir.PhysicalMapFirst + PhysicalMapFirstSorted = ir.PhysicalMapFirstSorted PhysicalSetGraphIDField = ir.PhysicalSetGraphIDField PhysicalSetKeyField = ir.PhysicalSetKeyField PhysicalSetIDField = ir.PhysicalSetIDField PhysicalSetResourceTypeField = ir.PhysicalSetResourceTypeField PhysicalSetPayloadField = ir.PhysicalSetPayloadField + PhysicalUnnestInner = ir.PhysicalUnnestInner + PhysicalUnnestOuter = ir.PhysicalUnnestOuter PhysicalComparisonPredicate = ir.PhysicalComparisonPredicate PhysicalAllPredicate = ir.PhysicalAllPredicate PhysicalAnyPredicate = ir.PhysicalAnyPredicate diff --git a/internal/dataframe/compiler/render/aql/call_test.go b/internal/dataframe/compiler/render/aql/call_test.go new file mode 100644 index 0000000..a20bdf3 --- /dev/null +++ b/internal/dataframe/compiler/render/aql/call_test.go @@ -0,0 +1,181 @@ +package aql + +import ( + "strings" + "testing" +) + +func scalarLiteral(key string) PhysicalExpression { + return PhysicalExpression{ + Kind: PhysicalLiteralExpression, + Cardinality: PhysicalScalarCardinality, + NullBehavior: PhysicalPreserveNull, + Literal: &PhysicalLiteral{BindKey: key}, + } +} + +func callExpression(name string, args ...PhysicalExpression) PhysicalExpression { + return PhysicalExpression{ + Kind: PhysicalCallExpression, + Cardinality: PhysicalScalarCardinality, + NullBehavior: PhysicalPreserveNull, + Call: &PhysicalCall{Name: name, Args: args}, + } +} + +func TestRenderGenericPhysicalCallsParameterizeLiterals(t *testing.T) { + renderer := &physicalPlanRenderer{ + bindVars: map[string]any{"left": "A", "right": "B", "delimiter": ","}, + collectionKeys: map[string]struct{}{}, + setVariables: map[string]string{}, + reservedVars: map[string]struct{}{}, + } + concat := callExpression("concat", scalarLiteral("left"), scalarLiteral("right")) + got, err := renderer.renderExpression(concat) + if err != nil { + t.Fatal(err) + } + if got != "CONCAT(@left, @right)" { + t.Fatalf("concat = %q", got) + } + join := callExpression("join", callExpression("all", scalarLiteral("left")), scalarLiteral("delimiter")) + got, err = renderer.renderExpression(join) + if err != nil { + t.Fatal(err) + } + if got != "CONCAT_SEPARATOR(@delimiter, [@left])" { + t.Fatalf("join = %q", got) + } + if strings.Contains(got, `"A"`) || strings.Contains(got, `"B"`) { + t.Fatalf("literal values leaked into AQL: %q", got) + } +} + +func TestRenderSetSelectorUsesCollectionVariable(t *testing.T) { + renderer := &physicalPlanRenderer{ + bindVars: map[string]any{}, + collectionKeys: map[string]struct{}{}, + setVariables: map[string]string{"child_set_1": "child_set_1"}, + reservedVars: map[string]struct{}{}, + } + expression := PhysicalExpression{ + Kind: PhysicalExtractExpression, + Cardinality: PhysicalArrayCardinality, + Extract: &PhysicalExtract{ + Source: PhysicalValue{Variable: "child_set_1", Path: []string{"payload"}}, + Selector: Selector{Steps: []SelectorStep{{Field: "identifier", Iterate: true}, {Field: "value"}}}, + ExecutionMode: PhysicalSelectorGeneric, + }, + } + got, err := renderer.renderExpression(expression) + if err != nil { + t.Fatal(err) + } + if strings.Contains(got, "IN child_set_1.payload") || !strings.Contains(got, "IN child_set_1") { + t.Fatalf("set selector iterated the collection payload instead of each item: %q", got) + } +} + +func TestRenderGenericCoalesceStringPreservesNulls(t *testing.T) { + renderer := &physicalPlanRenderer{ + bindVars: map[string]any{"number": 7, "text": "seven"}, + collectionKeys: map[string]struct{}{}, + setVariables: map[string]string{}, + reservedVars: map[string]struct{}{}, + } + got, err := renderer.renderExpression(callExpression("coalesce_string", scalarLiteral("number"), scalarLiteral("text"))) + if err != nil { + t.Fatal(err) + } + if !strings.Contains(got, "TO_STRING(@number)") || !strings.Contains(got, "TO_STRING(@text)") || !strings.Contains(got, "== null") { + t.Fatalf("coalesce_string = %q", got) + } +} + +func TestRenderGenericPhysicalCaseUsesBoundNull(t *testing.T) { + renderer := &physicalPlanRenderer{ + bindVars: map[string]any{"condition": true, "value": "yes"}, + collectionKeys: map[string]struct{}{}, + setVariables: map[string]string{}, + reservedVars: map[string]struct{}{}, + } + call := callExpression("case", scalarLiteral("condition"), scalarLiteral("value")) + got, err := renderer.renderExpression(call) + if err != nil { + t.Fatal(err) + } + if !strings.Contains(got, "@condition") || !strings.Contains(got, "@value") { + t.Fatalf("case did not preserve bind-backed args: %q", got) + } + if !strings.Contains(got, "@__loom_physical_call_null") { + t.Fatalf("case without ELSE did not bind null: %q", got) + } + if renderer.bindVars["__loom_physical_call_null"] != nil { + t.Fatalf("case null bind is not nil: %#v", renderer.bindVars) + } +} + +func TestRenderGenericPhysicalUUIDUsesExactPostQueryMarker(t *testing.T) { + renderer := &physicalPlanRenderer{ + bindVars: map[string]any{"namespace": "ns", "name": "name"}, + collectionKeys: map[string]struct{}{}, + setVariables: map[string]string{}, + reservedVars: map[string]struct{}{}, + } + uuid := callExpression("uuid5", scalarLiteral("namespace"), scalarLiteral("name")) + got, err := renderer.renderExpression(uuid) + if err != nil || !strings.Contains(got, "__loom_exact_uuid_operation") || !strings.Contains(got, "uuid5") { + t.Fatalf("uuid5 exact marker was not rendered: query=%q err=%v", got, err) + } + uuid3 := callExpression("uuid3", scalarLiteral("namespace"), scalarLiteral("name")) + got, err = renderer.renderExpression(uuid3) + if err != nil || !strings.Contains(got, "__loom_exact_uuid_operation") || !strings.Contains(got, "uuid3") { + t.Fatalf("uuid3 exact marker was not rendered: query=%q err=%v", got, err) + } + got, err = renderer.renderExpression(callExpression("sanitize_name", scalarLiteral("name"))) + if err != nil { + t.Fatal(err) + } + if !strings.Contains(got, "REGEX_REPLACE(TO_STRING(@name)") { + t.Fatalf("sanitize_name was not lowered: %q", got) + } +} + +func TestRenderNestedUUIDUsesRecursivePostQueryCallMarker(t *testing.T) { + renderer := &physicalPlanRenderer{ + bindVars: map[string]any{"namespace": "ns", "name": "name", "suffix": "-suffix"}, + collectionKeys: map[string]struct{}{}, + setVariables: map[string]string{}, + reservedVars: map[string]struct{}{}, + } + nested := callExpression("concat", callExpression("uuid5", scalarLiteral("namespace"), scalarLiteral("name")), scalarLiteral("suffix")) + got, err := renderer.renderExpression(nested) + if err != nil || !strings.Contains(got, "__loom_postquery_call") || !strings.Contains(got, "concat") { + t.Fatalf("nested UUID marker was not rendered: query=%q err=%v", got, err) + } +} + +func TestPhysicalPlanValidatesLiteralAndCallPayloads(t *testing.T) { + valid := callExpression("concat", scalarLiteral("left"), scalarLiteral("right")) + plan := PhysicalPlan{ + Version: 1, + BindVars: map[string]any{"root_collection": "Patient", "left": "A", "right": "B"}, + Operations: []PhysicalOperation{ + {Kind: PhysicalRootScanOp, RootScan: &PhysicalRootScan{Variable: "root", CollectionBindKey: "root_collection"}}, + {Kind: PhysicalReturnOp, Return: &PhysicalReturn{Projections: []PhysicalProjection{{Name: "value", Expression: &valid}}}}, + }, + } + if err := plan.Validate(); err != nil { + t.Fatalf("valid literal/call plan rejected: %v", err) + } + missing := scalarLiteral("missing") + plan.Operations[1].Return.Projections[0].Expression = &missing + if err := plan.Validate(); err == nil || !strings.Contains(err.Error(), "bind key \"missing\"") { + t.Fatalf("missing literal bind was accepted: %v", err) + } + unsupported := callExpression("not_a_recipe_operator", scalarLiteral("left")) + plan.Operations[1].Return.Projections[0].Expression = &unsupported + if err := plan.Validate(); err == nil || !strings.Contains(err.Error(), "unsupported call") { + t.Fatalf("unsupported call was accepted: %v", err) + } +} diff --git a/internal/dataframe/compiler/render/aql/calls.go b/internal/dataframe/compiler/render/aql/calls.go new file mode 100644 index 0000000..461891c --- /dev/null +++ b/internal/dataframe/compiler/render/aql/calls.go @@ -0,0 +1,211 @@ +package aql + +import ( + "fmt" + "strings" +) + +func (r *physicalPlanRenderer) renderCall(expression PhysicalExpression) (string, error) { + call := expression.Call + if call == nil { + return "", fmt.Errorf("CALL expression is missing payload") + } + name := strings.ToLower(strings.TrimSpace(call.Name)) + nestedUUID := name != "uuid3" && name != "uuid5" && containsExactUUIDCall(call.Args) + args := make([]string, len(call.Args)) + for index, arg := range call.Args { + value, err := r.renderExpression(arg) + if err != nil { + return "", fmt.Errorf("call %q argument %d: %w", name, index, err) + } + args[index] = value + } + if nestedUUID { + return fmt.Sprintf("{\"__loom_postquery_call\": %q, \"__loom_postquery_target\": %q, \"__loom_postquery_args\": [%s]}", name, call.TargetKind, strings.Join(args, ", ")), nil + } + require := func(count int) error { + if len(args) != count { + return fmt.Errorf("%s requires %d argument(s), got %d", name, count, len(args)) + } + return nil + } + joinArgs := func(separator string) string { return strings.Join(args, separator) } + scalarNull := func() string { + key := r.newInternalBindKey("call_null") + r.bindVars[key] = nil + return "@" + key + } + switch name { + case "coalesce_string": + if len(args) == 0 { + return "", fmt.Errorf("coalesce_string requires at least one argument") + } + converted := make([]string, len(args)) + for index, arg := range args { + converted[index] = "(" + arg + " == null ? null : TO_STRING(" + arg + "))" + } + return "FIRST(FOR __loom_call_value IN [" + strings.Join(converted, ", ") + "] FILTER __loom_call_value != null RETURN __loom_call_value)", nil + case "coalesce", "fallback": + if len(args) == 0 { + return "", fmt.Errorf("%s requires at least one argument", name) + } + return "FIRST(FOR __loom_call_value IN [" + joinArgs(", ") + "] FILTER __loom_call_value != null RETURN __loom_call_value)", nil + case "first": + if err := require(1); err != nil { + return "", err + } + return "FIRST(FLATTEN(" + args[0] + "))", nil + case "all": + if err := require(1); err != nil { + return "", err + } + if expression.Cardinality != PhysicalArrayCardinality { + return "[" + args[0] + "]", nil + } + return args[0], nil + case "distinct": + if err := require(1); err != nil { + return "", err + } + return "SORTED_UNIQUE(FLATTEN(" + args[0] + "))", nil + case "concat": + if len(args) == 0 { + return "", fmt.Errorf("concat requires at least one argument") + } + return "CONCAT(" + joinArgs(", ") + ")", nil + case "join": + if err := require(2); err != nil { + return "", err + } + // AQL has no JOIN function. Recipe expressions pass the value + // collection first and the delimiter second; CONCAT_SEPARATOR uses the + // inverse argument order and accepts an array as its value operand. + return "CONCAT_SEPARATOR(" + args[1] + ", " + args[0] + ")", nil + case "cast": + if err := require(1); err != nil { + return "", err + } + switch strings.ToLower(strings.TrimSpace(call.TargetKind)) { + case "string", "code", "uuid": + return "TO_STRING(" + args[0] + ")", nil + case "integer", "decimal": + return "TO_NUMBER(" + args[0] + ")", nil + case "boolean": + return "TO_BOOL(" + args[0] + ")", nil + case "date", "date_time": + return "DATE_ISO8601(" + args[0] + ")", nil + default: + return "", fmt.Errorf("cast target kind %q is unsupported", call.TargetKind) + } + case "reference_id", "path_segment", "basename", "last_segment": + if err := require(1); err != nil { + return "", err + } + trimPattern := r.newInternalBindKey("call_path_trim_pattern") + segmentPattern := r.newInternalBindKey("call_path_segment_pattern") + emptyReplacement := r.newInternalBindKey("call_path_empty_replacement") + r.bindVars[trimPattern] = `/+$` + r.bindVars[segmentPattern] = `[/#]` + r.bindVars[emptyReplacement] = "" + trimmed := "REGEX_REPLACE(TO_STRING(" + args[0] + "), @" + trimPattern + ", @" + emptyReplacement + ")" + return "(" + args[0] + " == null ? null : LAST(REGEX_SPLIT(" + trimmed + ", @" + segmentPattern + ")))", nil + case "sanitize_name", "sanitize_graphql_name": + if err := require(1); err != nil { + return "", err + } + pattern := r.newInternalBindKey("call_name_pattern") + replacement := r.newInternalBindKey("call_name_replacement") + empty := r.newInternalBindKey("call_name_empty") + underscore := r.newInternalBindKey("call_name_underscore") + startsDigit := r.newInternalBindKey("call_name_starts_digit") + dunder := r.newInternalBindKey("call_name_dunder") + r.bindVars[pattern] = `[^A-Za-z0-9_]` + r.bindVars[replacement] = "_" + r.bindVars[empty] = "" + r.bindVars[underscore] = "_" + r.bindVars[startsDigit] = `^[0-9]` + r.bindVars[dunder] = `^__` + clean := "REGEX_REPLACE(TO_STRING(" + args[0] + "), @" + pattern + ", @" + replacement + ")" + return "(" + clean + " == @" + empty + " ? @" + underscore + " : (REGEX_TEST(" + clean + ", @" + startsDigit + ") ? CONCAT(@" + underscore + ", " + clean + ") : (REGEX_TEST(" + clean + ", @" + dunder + ") ? CONCAT(@" + underscore + ", SUBSTRING(" + clean + ", 2)) : " + clean + ")))", nil + case "uuid3", "uuid5": + if len(args) < 2 { + return "", fmt.Errorf("%s requires a namespace and at least one name", name) + } + // AQL cannot reproduce namespace-byte hashing and RFC bit handling + // portably. Return a typed marker for the compiler-owned post-query + // stage instead of emitting a textual hash approximation. + return fmt.Sprintf("{\"__loom_exact_uuid_operation\": %q, \"__loom_exact_uuid_args\": [%s]}", name, strings.Join(args, ", ")), nil + case "if": + if err := require(3); err != nil { + return "", err + } + return "(" + args[0] + " ? " + args[1] + " : " + args[2] + ")", nil + case "case": + if len(args) < 2 { + return "", fmt.Errorf("case requires at least one condition/result pair") + } + withElse := len(args)%2 == 1 + end := len(args) + result := scalarNull() + if withElse { + result = args[end-1] + end-- + } + if end%2 != 0 { + return "", fmt.Errorf("case requires condition/result pairs and optional else") + } + for index := end - 2; index >= 0; index -= 2 { + result = "(" + args[index] + " ? " + args[index+1] + " : " + result + ")" + } + return result, nil + case "not": + if err := require(1); err != nil { + return "", err + } + return "NOT (" + args[0] + ")", nil + case "and", "or": + if len(args) < 2 { + return "", fmt.Errorf("%s requires at least two arguments", name) + } + operator := " AND " + if name == "or" { + operator = " OR " + } + return "(" + joinArgs(operator) + ")", nil + case "eq", "neq", "gt", "gte", "lt", "lte": + if err := require(2); err != nil { + return "", err + } + operator := map[string]string{"eq": "==", "neq": "!=", "gt": ">", "gte": ">=", "lt": "<", "lte": "<="}[name] + return "(" + args[0] + " " + operator + " " + args[1] + ")", nil + case "contains": + if err := require(2); err != nil { + return "", err + } + return "CONTAINS(TO_STRING(" + args[0] + "), TO_STRING(" + args[1] + "))", nil + default: + return "", fmt.Errorf("unsupported physical call %q", call.Name) + } +} + +func containsExactUUIDCall(expressions []PhysicalExpression) bool { + for _, expression := range expressions { + if expression.Call == nil { + continue + } + name := strings.ToLower(strings.TrimSpace(expression.Call.Name)) + if name == "uuid3" || name == "uuid5" || containsExactUUIDCall(expression.Call.Args) { + return true + } + } + return false +} + +// renderObject renders a recursively typed object expression. Sorting a copy +// gives equivalent physical plans stable AQL and bind-key allocation without +// changing the semantic field order held by the plan. +// +// The compact dynamic-key literal is used when every field preserves nulls. +// If any field requests OMIT_NULLS, fields are represented as a temporary +// stream and merged so null-valued fields can be removed without evaluating +// their expression twice. diff --git a/internal/dataframe/compiler/render/aql/collections.go b/internal/dataframe/compiler/render/aql/collections.go new file mode 100644 index 0000000..3ce117a --- /dev/null +++ b/internal/dataframe/compiler/render/aql/collections.go @@ -0,0 +1,402 @@ +package aql + +import ( + "fmt" + "sort" + "strconv" + "strings" +) + +func (r *physicalPlanRenderer) renderObject(expression PhysicalExpression) (string, error) { + object := expression.Object + if object == nil { + return "", fmt.Errorf("OBJECT expression is missing payload") + } + fields := append([]PhysicalExpressionProjection(nil), object.Fields...) + sort.SliceStable(fields, func(left, right int) bool { + return fields[left].Name < fields[right].Name + }) + + type renderedField struct { + nameKey string + value string + omit bool + } + rendered := make([]renderedField, 0, len(fields)) + for index, field := range fields { + value, err := r.renderExpression(field.Expression) + if err != nil { + return "", fmt.Errorf("object field %q: %w", field.Name, err) + } + nameKey := r.newInternalBindKey("object_field_" + strconv.Itoa(index) + "_name") + r.bindVars[nameKey] = field.Name + rendered = append(rendered, renderedField{ + nameKey: nameKey, + value: value, + omit: field.Expression.NullBehavior == PhysicalOmitNulls, + }) + } + + hasOmittedField := false + for _, field := range rendered { + if field.omit { + hasOmittedField = true + break + } + } + if !hasOmittedField { + parts := make([]string, 0, len(rendered)) + for _, field := range rendered { + parts = append(parts, fmt.Sprintf("[@%s]: %s", field.nameKey, field.value)) + } + return "{ " + strings.Join(parts, ", ") + " }", nil + } + + items := make([]string, 0, len(rendered)) + for _, field := range rendered { + items = append(items, fmt.Sprintf("{ __loom_object_name: @%s, __loom_object_value: %s, __loom_object_omit: %t }", field.nameKey, field.value, field.omit)) + } + return fmt.Sprintf(`MERGE( + FOR __loom_object_field IN [%s] + FILTER __loom_object_field.__loom_object_omit == false OR __loom_object_field.__loom_object_value != null + RETURN { [__loom_object_field.__loom_object_name]: __loom_object_field.__loom_object_value } +)`, strings.Join(items, ", ")), nil +} + +// renderSlice emits a correlated, bounded array projection. Sort and the +// _key tie-break are rendered inside the subquery so representative values +// are deterministic even when traversal order changes. +func (r *physicalPlanRenderer) renderSlice(expression PhysicalExpression) (string, error) { + slice := expression.Slice + if slice == nil { + return "", fmt.Errorf("SLICE expression is missing payload") + } + source, err := r.renderValue(slice.Source) + if err != nil { + return "", err + } + items := source + preparedVariable := slicePreparedVariable(slice) + if preparedVariable != "" { + items = preparedVariable + } + setSource := slice.Source.Variable != "" && r.setVariables[slice.Source.Variable] != "" + if !setSource { + items = "[" + source + "]" + } + item := r.newInternalVariable("slice_item") + lines := []string{"(FOR " + item + " IN " + items} + if slice.Predicate != nil { + if slice.Predicate.Kind != PhysicalComparisonPredicate || slice.Predicate.Comparison == nil { + return "", fmt.Errorf("slice predicate must be a comparison") + } + comparison := *slice.Predicate.Comparison + if comparison.LeftExpression != nil && comparison.LeftExpression.Extract != nil { + left := *comparison.LeftExpression + extract := *left.Extract + extract.Source = PhysicalValue{Variable: item, Path: []string{"payload"}} + left.Extract = &extract + comparison.LeftExpression = &left + } else { + comparison.Left = PhysicalValue{Variable: item} + } + previousPreparedItem := r.preparedItem + r.preparedItem = item + predicate, err := r.renderPredicate(comparison) + r.preparedItem = previousPreparedItem + if err != nil { + return "", err + } + lines = append(lines, " FILTER "+predicate) + } + if slice.Sort == nil { + return "", fmt.Errorf("slice requires sort expression") + } + sortExpression := *slice.Sort + if sortExpression.Kind == PhysicalValueExpression && sortExpression.Value != nil { + value := *sortExpression.Value + value.Variable = item + value.BindKey = "" + sortExpression.Value = &value + } + sortValue, err := r.renderExpression(sortExpression) + if err != nil { + return "", err + } + lines = append(lines, " SORT "+sortValue+" ASC, "+item+"._key ASC") + lines = append(lines, " LIMIT @"+slice.LimitBindKey) + fields := make([]string, 0, len(slice.Projections)) + for index, projection := range slice.Projections { + projectionExpression := projection.Expression + if projectionExpression.Kind == PhysicalExtractExpression && projectionExpression.Extract != nil { + extract := *projectionExpression.Extract + extract.Source = PhysicalValue{Variable: item, Path: []string{"payload"}} + projectionExpression.Extract = &extract + } + previousPreparedItem := r.preparedItem + r.preparedItem = item + value, err := r.renderExpression(projectionExpression) + r.preparedItem = previousPreparedItem + if err != nil { + return "", fmt.Errorf("slice projection %d (%s): %w", index, projection.Name, err) + } + nameKey := r.newInternalBindKey("slice_projection_" + strconv.Itoa(index) + "_name") + r.bindVars[nameKey] = projection.Name + fields = append(fields, "["+"@"+nameKey+"]: "+value) + } + lines = append(lines, " RETURN { "+strings.Join(fields, ", ")+" }") + return strings.Join(lines, "\n") + "\n)", nil +} + +func slicePreparedVariable(slice *PhysicalSlice) string { + if slice == nil { + return "" + } + if slice.Predicate != nil && slice.Predicate.Comparison != nil && slice.Predicate.Comparison.LeftExpression != nil && slice.Predicate.Comparison.LeftExpression.Extract != nil && slice.Predicate.Comparison.LeftExpression.Extract.Prepared != nil { + return slice.Predicate.Comparison.LeftExpression.Extract.Prepared.SetVariable + } + for _, projection := range slice.Projections { + if projection.Expression.Extract != nil && projection.Expression.Extract.Prepared != nil { + return projection.Expression.Extract.Prepared.SetVariable + } + } + return "" +} + +// renderPivot emits a bounded sparse object keyed by the requested catalog +// columns. Values from all matching resources are combined per key and reduced +// deterministically to the first sorted value while keeping selectors and +// column values typed. +func (r *physicalPlanRenderer) renderPivot(expression PhysicalExpression) (string, error) { + pivot := expression.Pivot + if pivot == nil { + return "", fmt.Errorf("PIVOT expression is missing payload") + } + if _, collection := r.collectionKeys[pivot.ColumnsBindKey]; collection { + return "", fmt.Errorf("pivot columns bind %q cannot be a collection bind", pivot.ColumnsBindKey) + } + columns, ok := r.bindVars[pivot.ColumnsBindKey].([]string) + if !ok || len(columns) == 0 { + return "", fmt.Errorf("pivot columns bind %q is not a non-empty []string", pivot.ColumnsBindKey) + } + items, err := r.renderValue(pivot.Source) + if err != nil { + return "", err + } + if pivot.Source.Variable == "" || r.setVariables[pivot.Source.Variable] == "" { + items = "[" + items + "]" + } + if pivot.PreparedKey != nil { + items = pivot.PreparedKey.SetVariable + } + item := r.newInternalVariable("pivot_item") + itemExpression := item + itemLoop := fmt.Sprintf("FOR %s IN %s", item, items) + if pivot.ItemResourceType != "" { + if pivot.ItemSource.CanonicalPath() == "" { + return "", fmt.Errorf("pivot item source is required when item resource type is set") + } + itemValues, sourceErr := r.renderSelectorArrayFromSource(item+".payload", pivot.ItemSource, false) + if sourceErr != nil { + return "", fmt.Errorf("pivot item source: %w", sourceErr) + } + itemExpression = r.newInternalVariable("pivot_item_value") + // A selector ending in an iterated array is represented by the + // selector renderer as one subquery row containing that array. Pivot + // item sources need the repeated elements themselves so key/value + // selectors run against each component/backbone item, not against the + // wrapper array. Flatten exactly that one selector-result layer; + // deeper nesting remains owned by the selector's own iteration steps. + itemLoop += fmt.Sprintf("\n FOR %s IN FLATTEN(%s)", itemExpression, itemValues) + } + previousPreparedItem := r.preparedItem + r.preparedItem = itemExpression + keyExpr, err := r.renderPivotSelector(item, itemExpression, pivot.PreparedKey, pivot.KeySelector, pivot.ItemResourceType != "") + if err != nil { + r.preparedItem = previousPreparedItem + return "", err + } + valueSelectors := append([]Selector{pivot.ValueSelector}, pivot.ValueFallbacks...) + valueExpressions := make([]string, 0, len(valueSelectors)) + for _, selector := range valueSelectors { + value, valueErr := r.renderPivotSelector(item, itemExpression, nil, selector, pivot.ItemResourceType != "") + if valueErr != nil { + r.preparedItem = previousPreparedItem + return "", valueErr + } + valueExpressions = append(valueExpressions, value) + } + if pivot.PreparedValue != nil { + valueExpressions = []string{itemExpression + "." + pivot.PreparedValue.Field} + } + r.preparedItem = previousPreparedItem + if len(valueExpressions) == 0 { + return "", fmt.Errorf("pivot value selector is required") + } + valueExpr := valueExpressions[0] + if len(valueExpressions) > 1 { + valueExpr = "FIRST(FOR __pivot_candidate IN [" + strings.Join(valueExpressions, ", ") + "] FILTER LENGTH(__pivot_candidate) > 0 RETURN __pivot_candidate)" + } + pairs := fmt.Sprintf(`FOR __pair IN ( + %s + LET __pivot_keys = UNIQUE(%s) + LET __pivot_values = %s + FILTER LENGTH(__pivot_values) > 0 + FOR __pivot_key IN __pivot_keys + FILTER POSITION(@%s, __pivot_key) + RETURN { key: __pivot_key, values: __pivot_values } + )`, itemLoop, keyExpr, valueExpr, pivot.ColumnsBindKey) + if pivot.FlattenSingleColumn { + return fmt.Sprintf(`FIRST( + %s + COLLECT __pivot_key = __pair.key INTO __pivot_group + LET __pivot_flat_values = SORTED_UNIQUE(FLATTEN(__pivot_group[*].__pair.values)) + FILTER LENGTH(__pivot_flat_values) > 0 + RETURN FIRST(__pivot_flat_values) +)`, pairs), nil + } + return fmt.Sprintf(`MERGE( + %s + COLLECT __pivot_key = __pair.key INTO __pivot_group + LET __pivot_flat_values = SORTED_UNIQUE(FLATTEN(__pivot_group[*].__pair.values)) + FILTER LENGTH(__pivot_flat_values) > 0 + RETURN { [__pivot_key]: FIRST(__pivot_flat_values) } +)`, pairs), nil +} + +// renderPivotSelector evaluates a selector against either the resource +// document or a correlated repeated item. Keeping the item scope explicit +// prevents keys and values from different repeated elements being paired. +func (r *physicalPlanRenderer) renderPivotSelector(resourceItem, item string, prepared *PhysicalPreparedReference, selector Selector, itemScoped bool) (string, error) { + if prepared != nil { + return item + "." + prepared.Field, nil + } + source := resourceItem + ".payload" + if itemScoped { + source = item + } + return r.renderSelectorArrayFromSource(source, selector, false) +} + +// renderAggregate emits reductions over either a correlated PhysicalSet or a +// singleton root document. The source is kept typed in the IR; this method is +// the only place that decides the AQL collection expression (`set` versus +// `[root]`). +func (r *physicalPlanRenderer) renderAggregate(expression PhysicalExpression) (string, error) { + aggregate := expression.Aggregate + if aggregate == nil { + return "", fmt.Errorf("AGGREGATE expression is missing payload") + } + source, err := r.renderValue(aggregate.Source) + if err != nil { + return "", err + } + items := source + if preparedVariable := aggregatePreparedVariable(aggregate); preparedVariable != "" { + items = preparedVariable + } + if aggregate.Source.Variable == "" || r.setVariables[aggregate.Source.Variable] == "" { + items = "[" + source + "]" + } + perItem := aggregate.Predicate != nil + if perItem { + if aggregate.Predicate.Kind != PhysicalComparisonPredicate || aggregate.Predicate.Comparison == nil { + return "", fmt.Errorf("aggregate predicate must be a comparison") + } + item := r.newInternalVariable("aggregate_item") + comparison := *aggregate.Predicate.Comparison + if comparison.LeftExpression == nil || comparison.LeftExpression.Extract == nil { + return "", fmt.Errorf("aggregate predicate must extract a selector") + } + left := *comparison.LeftExpression + extract := *left.Extract + if extract.Prepared == nil { + extract.Source = PhysicalValue{Variable: item, Path: []string{"payload"}} + } + left.Extract = &extract + comparison.LeftExpression = &left + previousPreparedItem := r.preparedItem + r.preparedItem = item + predicate, err := r.renderPredicate(comparison) + r.preparedItem = previousPreparedItem + if err != nil { + return "", err + } + items = "(FOR " + item + " IN " + items + " FILTER " + predicate + " RETURN " + item + ")" + } + switch aggregate.Operation { + case PhysicalCountAggregate: + return "LENGTH(" + items + ")", nil + case PhysicalExistsAggregate: + if aggregate.Value == nil { + return "LENGTH(" + items + ") > 0", nil + } + values, err := r.renderAggregateValue(*aggregate.Value, items, perItem) + if err != nil { + return "", err + } + return "LENGTH(FOR __value IN FLATTEN(" + values + ") FILTER __value != null LIMIT 1 RETURN 1) > 0", nil + case PhysicalCountDistinctAggregate, PhysicalDistinctValuesAggregate, PhysicalMinAggregate, PhysicalMaxAggregate, PhysicalFirstAggregate: + if aggregate.Value == nil { + return "", fmt.Errorf("aggregate operation %q requires a value expression", aggregate.Operation) + } + values, err := r.renderAggregateValue(*aggregate.Value, items, perItem) + if err != nil { + return "", err + } + flattened := "FLATTEN(" + values + ")" + switch aggregate.Operation { + case PhysicalCountDistinctAggregate: + return "LENGTH(SORTED_UNIQUE(" + flattened + "))", nil + case PhysicalDistinctValuesAggregate: + return "SORTED_UNIQUE(" + flattened + ")", nil + case PhysicalMinAggregate: + return "MIN(" + flattened + ")", nil + case PhysicalMaxAggregate: + return "MAX(" + flattened + ")", nil + case PhysicalFirstAggregate: + return "FIRST(" + flattened + ")", nil + } + } + return "", fmt.Errorf("unsupported aggregate operation %q", aggregate.Operation) +} + +func aggregatePreparedVariable(aggregate *PhysicalAggregate) string { + if aggregate == nil { + return "" + } + if aggregate.Value != nil && aggregate.Value.Extract != nil && aggregate.Value.Extract.Prepared != nil { + return aggregate.Value.Extract.Prepared.SetVariable + } + if aggregate.Predicate != nil && aggregate.Predicate.Comparison != nil && aggregate.Predicate.Comparison.LeftExpression != nil && aggregate.Predicate.Comparison.LeftExpression.Extract != nil && aggregate.Predicate.Comparison.LeftExpression.Extract.Prepared != nil { + return aggregate.Predicate.Comparison.LeftExpression.Extract.Prepared.SetVariable + } + return "" +} + +func (r *physicalPlanRenderer) renderAggregateValue(expression PhysicalExpression, items string, perItem bool) (string, error) { + if !perItem { + if expression.Extract != nil && expression.Extract.Prepared != nil { + return "(FOR __loom_prepared_value IN " + expression.Extract.Prepared.SetVariable + " RETURN __loom_prepared_value." + expression.Extract.Prepared.Field + ")", nil + } + return r.renderExpression(expression) + } + if expression.Kind != PhysicalExtractExpression || expression.Extract == nil { + return "", fmt.Errorf("aggregate predicates require an extract value expression") + } + item := r.newInternalVariable("aggregate_value_item") + clone := expression + extract := *expression.Extract + if extract.Prepared == nil { + extract.Source = PhysicalValue{Variable: item, Path: []string{"payload"}} + } + clone.Extract = &extract + previousPreparedItem := r.preparedItem + r.preparedItem = item + value, err := r.renderExtract(clone) + r.preparedItem = previousPreparedItem + if err != nil { + return "", err + } + return "(FOR " + item + " IN " + items + " RETURN " + value + ")", nil +} diff --git a/internal/dataframe/compiler/render/aql/filter_literal.go b/internal/dataframe/compiler/render/aql/filter_literal.go deleted file mode 100644 index fcf5b6b..0000000 --- a/internal/dataframe/compiler/render/aql/filter_literal.go +++ /dev/null @@ -1,31 +0,0 @@ -package aql - -import "fmt" - -// filterLiteral converts the typed filter value into the scalar representation -// bound to AQL. Validation has already established the active member and its -// kind; retaining this conversion beside the physical planner keeps the old -// string renderer out of the execution path. -func filterLiteral(value FilterValue) (any, error) { - if err := value.Validate(); err != nil { - return nil, err - } - switch value.Kind { - case FilterString: - return *value.String, nil - case FilterCode: - return value.Code.Code, nil - case FilterBoolean: - return *value.Boolean, nil - case FilterInteger: - return *value.Integer, nil - case FilterDecimal: - return *value.Decimal, nil - case FilterDate: - return *value.Date, nil - case FilterDateTime: - return *value.DateTime, nil - default: - return nil, fmt.Errorf("unsupported filter literal kind %q", value.Kind) - } -} diff --git a/internal/dataframe/compiler/render/aql/lookup_test.go b/internal/dataframe/compiler/render/aql/lookup_test.go new file mode 100644 index 0000000..0b03630 --- /dev/null +++ b/internal/dataframe/compiler/render/aql/lookup_test.go @@ -0,0 +1,53 @@ +package aql_test + +import ( + "strings" + "testing" + + "github.com/calypr/loom/internal/dataframe/compiler" + "github.com/calypr/loom/internal/dataframe/compiler/ir" + aql "github.com/calypr/loom/internal/dataframe/compiler/render/aql" +) + +func TestRenderPhysicalLookupExpression(t *testing.T) { + value := func(path ...string) ir.PhysicalExpression { + return ir.PhysicalExpression{Kind: ir.PhysicalValueExpression, Cardinality: ir.PhysicalScalarCardinality, NullBehavior: ir.PhysicalPreserveNull, Value: &ir.PhysicalValue{Variable: "dynamic_item", Path: path}} + } + lookup := ir.PhysicalExpression{ + Kind: ir.PhysicalLookupExpression, + Cardinality: ir.PhysicalScalarCardinality, + NullBehavior: ir.PhysicalPreserveNull, + Lookup: &ir.PhysicalLookup{ + Source: ir.PhysicalExpression{Kind: ir.PhysicalValueExpression, Cardinality: ir.PhysicalArrayCardinality, NullBehavior: ir.PhysicalPreserveNull, Value: &ir.PhysicalValue{Variable: "root", Path: []string{"items"}}}, + ItemVariable: "dynamic_item", + ItemKey: value("url"), + ItemValue: value("value"), + MatchBindKey: "dynamic_key", + }, + } + plan, err := compiler.BuildGenericPhysicalPlan(compiler.SemanticPlan{ + Version: 1, + Project: "project-1", + AuthResourcePaths: []string{"/programs/p1"}, + Root: compiler.SemanticNode{Alias: "root", ResourceType: "Patient"}, + }) + if err != nil { + t.Fatal(err) + } + plan.BindVars["dynamic_key"] = "x" + for index := range plan.Operations { + if plan.Operations[index].Kind == ir.PhysicalReturnOp { + plan.Operations[index].Return.Projections = []ir.PhysicalProjection{{Name: "lookup", Expression: &lookup}} + } + } + rendered, err := aql.RenderPhysicalPlan(plan) + if err != nil { + t.Fatal(err) + } + if !strings.Contains(rendered.Query, "FIRST(FOR dynamic_item IN") || !strings.Contains(rendered.Query, "dynamic_item.url == @dynamic_key") || !strings.Contains(rendered.Query, "RETURN dynamic_item.value") { + t.Fatalf("unexpected lookup AQL: %s", rendered.Query) + } + if rendered.BindVars["dynamic_key"] != "x" { + t.Fatalf("lookup bind changed: %#v", rendered.BindVars) + } +} diff --git a/internal/dataframe/compiler/render/aql/navigation_layout.go b/internal/dataframe/compiler/render/aql/navigation_layout.go new file mode 100644 index 0000000..f3dd2d2 --- /dev/null +++ b/internal/dataframe/compiler/render/aql/navigation_layout.go @@ -0,0 +1,264 @@ +package aql + +import ( + "fmt" + "strings" +) + +func buildNavigationRenderLayout(plan PhysicalPlan) (physicalNavigationRenderLayout, error) { + if len(plan.Operations) < 6 { + return physicalNavigationRenderLayout{}, fmt.Errorf("generic navigation renderer requires ROOT_SCAN, scope operations, and RETURN") + } + if plan.Operations[0].Kind != PhysicalRootScanOp { + return physicalNavigationRenderLayout{}, fmt.Errorf("generic navigation renderer requires ROOT_SCAN as the first operation") + } + last := len(plan.Operations) - 1 + if plan.Operations[last].Kind != PhysicalReturnOp { + return physicalNavigationRenderLayout{}, fmt.Errorf("generic navigation renderer requires RETURN as the final operation") + } + + layout := physicalNavigationRenderLayout{ + root: *plan.Operations[0].RootScan, + rootScope: append([]PhysicalOperation(nil), plan.Operations[1:5]...), + returnOp: *plan.Operations[last].Return, + } + rootScopeVariable, err := validateGenericNavigationScopeBlock(layout.rootScope, layout.root.Variable, "", layout.root.Variable) + if err != nil { + return physicalNavigationRenderLayout{}, fmt.Errorf("root navigation scope: %w", err) + } + + index := 5 + for index < last && plan.Operations[index].Kind == PhysicalFilterOp && plan.Operations[index].Filter.Expression != nil { + layout.rootPredicates = append(layout.rootPredicates, plan.Operations[index]) + index++ + } + // UNNEST is a cardinality boundary. It is kept after root predicates and + // before the execution window so a row-grain-aware compiler can put a + // database-side LIMIT after expansion. This remains a canonical physical + // operation; the slice only captures the validated renderer layout. + for index < last && plan.Operations[index].Kind == PhysicalUnnestOp { + if plan.Operations[index].Unnest == nil { + return physicalNavigationRenderLayout{}, fmt.Errorf("unnest at operation %d is missing payload", index) + } + layout.unnests = append(layout.unnests, *plan.Operations[index].Unnest) + index++ + } + if index < last && plan.Operations[index].Kind == PhysicalSortOp { + if err := validateGenericNavigationRootSort(plan.Operations[index], layout.root.Variable); err != nil { + return physicalNavigationRenderLayout{}, fmt.Errorf("root execution window at operation %d: %w", index, err) + } + layout.rootWindow = append(layout.rootWindow, plan.Operations[index]) + index++ + if index < last && plan.Operations[index].Kind == PhysicalLimitOp { + if err := validateGenericNavigationRootLimit(plan.Operations[index]); err != nil { + return physicalNavigationRenderLayout{}, fmt.Errorf("root execution window at operation %d: %w", index, err) + } + layout.rootWindow = append(layout.rootWindow, plan.Operations[index]) + index++ + } + } else if index < last && plan.Operations[index].Kind == PhysicalLimitOp { + return physicalNavigationRenderLayout{}, fmt.Errorf("root execution window at operation %d: LIMIT requires deterministic root SORT", index) + } + for index < last { + operation := plan.Operations[index] + if operation.Kind == PhysicalExpressionLetOp { + layout.expressionLets = append(layout.expressionLets, operation) + index++ + continue + } + if operation.Kind == PhysicalSetOp { + layout.sets = append(layout.sets, *operation.Set) + index++ + continue + } + if operation.Kind == PhysicalUnnestOp { + return physicalNavigationRenderLayout{}, fmt.Errorf("unnest at operation %d must appear before the root execution window and traversal/set operations", index) + } + if operation.Kind != PhysicalTraversalOp { + return physicalNavigationRenderLayout{}, fmt.Errorf("generic navigation renderer expected TRAVERSAL at operation %d, got %s", index, operation.Kind) + } + const traversalScopeLength = 6 // edge + target project/generation, then auth LET/filter + if index+traversalScopeLength >= last { + return physicalNavigationRenderLayout{}, fmt.Errorf("traversal at operation %d is missing its project/auth scope block", index) + } + traversal := *operation.Traversal + if err := validateGenericNavigationTraversal(plan, traversal); err != nil { + return physicalNavigationRenderLayout{}, fmt.Errorf("traversal at operation %d: %w", index, err) + } + scope := append([]PhysicalOperation(nil), plan.Operations[index+1:index+1+traversalScopeLength]...) + if _, err := validateGenericNavigationScopeBlock(scope, traversal.TargetVariable, traversal.EdgeVariable, traversal.TargetVariable); err != nil { + return physicalNavigationRenderLayout{}, fmt.Errorf("traversal at operation %d scope: %w", index, err) + } + layout.traversals = append(layout.traversals, physicalNavigationTraversal{traversal: traversal, scope: scope}) + index += 1 + traversalScopeLength + } + unnestVariables := map[string]struct{}{} + for _, unnest := range layout.unnests { + unnestVariables[unnest.OutputVariable] = struct{}{} + if unnest.Ordinality != "" { + unnestVariables[unnest.Ordinality] = struct{}{} + } + } + if err := validateNavigationReturnScope(layout.returnOp, layout.root.Variable, rootScopeVariable, unnestVariables); err != nil { + return physicalNavigationRenderLayout{}, err + } + return layout, nil +} + +func validateGenericNavigationTraversal(plan PhysicalPlan, traversal PhysicalTraversal) error { + if traversal.Direction != PhysicalInbound && traversal.Direction != PhysicalOutbound { + return fmt.Errorf("generic navigation traversal direction must be INBOUND or OUTBOUND, got %q", traversal.Direction) + } + wantEdgeTypeField := "from_type" + if traversal.Direction == PhysicalOutbound { + wantEdgeTypeField = "to_type" + } + if traversal.EdgeTargetTypeField != wantEdgeTypeField { + return fmt.Errorf("generic navigation traversal %s must constrain edge.%s, got %q", traversal.Direction, wantEdgeTypeField, traversal.EdgeTargetTypeField) + } + if traversal.EdgeVariable == "" || traversal.EdgeLabelBindKey == "" || traversal.TargetTypeBindKey == "" { + return fmt.Errorf("generic navigation traversal requires edge variable, edge label bind, and target type bind") + } + collection, ok := plan.BindVars[traversal.EdgeCollectionBindKey].(string) + if !ok || collection != "fhir_edge" { + return fmt.Errorf("generic navigation traversal must use fhir_edge through its collection bind") + } + strategy := traversal.Strategy + if strategy == "" { + strategy = PhysicalTraversalNative + } + if strategy != PhysicalTraversalNative && strategy != PhysicalTraversalEndpointLookup { + return fmt.Errorf("unsupported generic navigation traversal strategy %q", strategy) + } + if strategy == PhysicalTraversalEndpointLookup { + wantEndpoint, wantJoin := "_to", "_from" + wantIndexType := "from_type" + if traversal.Direction == PhysicalOutbound { + wantEndpoint, wantJoin, wantIndexType = "_from", "_to", "to_type" + } + if traversal.EndpointField != wantEndpoint || traversal.EndpointJoinField != wantJoin { + return fmt.Errorf("endpoint lookup %s requires %s -> %s, got %s -> %s", traversal.Direction, wantEndpoint, wantJoin, traversal.EndpointField, traversal.EndpointJoinField) + } + wantIndex := []string{wantEndpoint, "project", "dataset_generation", "label", wantIndexType} + if len(traversal.EndpointIndexFields) != len(wantIndex) { + return fmt.Errorf("endpoint lookup requires compound index fields %#v", wantIndex) + } + for index := range wantIndex { + if traversal.EndpointIndexFields[index] != wantIndex[index] { + return fmt.Errorf("endpoint lookup index field %d = %q, want %q", index, traversal.EndpointIndexFields[index], wantIndex[index]) + } + } + } + return nil +} + +func validateGenericNavigationRootSort(operation PhysicalOperation, rootVariable string) error { + if operation.Sort == nil || !sameRenderPhysicalValue(operation.Sort.Value, PhysicalValue{Variable: rootVariable, Path: []string{"_key"}}) { + return fmt.Errorf("SORT must order the root variable %s._key", rootVariable) + } + return nil +} + +func validateGenericNavigationRootLimit(operation PhysicalOperation) error { + if operation.Limit == nil || operation.Limit.BindKey != genericPhysicalExecutionLimitBind { + return fmt.Errorf("LIMIT must use @%s", genericPhysicalExecutionLimitBind) + } + return nil +} + +// validateGenericNavigationScopeBlock accepts the exact generation-safe scope +// operations emitted by appendProjectScope, appendDatasetGenerationScope, and +// appendAuthScope. The standalone +// scope verifier is intentionally more flexible; rendering is stricter so it +// can relocate the whole block safely into a LET subquery. +func validateGenericNavigationScopeBlock(operations []PhysicalOperation, resourceVariable, edgeVariable, targetVariable string) (string, error) { + expectedProjectVariables := []string{resourceVariable} + expectedGenerationVariables := []string{resourceVariable} + if edgeVariable != "" { + expectedProjectVariables = []string{edgeVariable, targetVariable} + expectedGenerationVariables = []string{edgeVariable, targetVariable} + } + expectedLength := len(expectedProjectVariables) + len(expectedGenerationVariables) + 2 + if len(operations) != expectedLength || operations[0].Kind != PhysicalFilterOp { + return "", fmt.Errorf("requires project filters for every graph document, dataset_generation filters, LET AUTH_RESOURCE_PATH_ALLOWED, FILTER scope_allowed in order") + } + for index, variable := range expectedProjectVariables { + operation := operations[index] + if operation.Kind != PhysicalFilterOp || !matchesPhysicalEquality(operation.Filter.Predicate, PhysicalValue{Variable: variable, Path: []string{"project"}}, PhysicalValue{BindKey: "project"}) { + return "", fmt.Errorf("project scope must be %s.project == @project", variable) + } + } + for index, variable := range expectedGenerationVariables { + operation := operations[len(expectedProjectVariables)+index] + if operation.Kind != PhysicalFilterOp || !matchesPhysicalEquality(operation.Filter.Predicate, PhysicalValue{Variable: variable, Path: []string{datasetGenerationField}}, PhysicalValue{BindKey: datasetGenerationBindKey}) { + return "", fmt.Errorf("dataset generation scope must be %s.%s == @%s", variable, datasetGenerationField, datasetGenerationBindKey) + } + } + authLetIndex := len(expectedProjectVariables) + len(expectedGenerationVariables) + authFilterIndex := authLetIndex + 1 + + if operations[authLetIndex].Kind != PhysicalDerivedLetOp || operations[authLetIndex].DerivedLet == nil { + return "", fmt.Errorf("scope block requires AUTH_RESOURCE_PATH_ALLOWED LET after dataset generation scope") + } + derived := operations[authLetIndex].DerivedLet + if strings.ToUpper(strings.TrimSpace(derived.Operator)) != "AUTH_RESOURCE_PATH_ALLOWED" { + return "", fmt.Errorf("scope LET must use AUTH_RESOURCE_PATH_ALLOWED") + } + expectedInputs := []PhysicalValue{{Variable: resourceVariable, Path: []string{"auth_resource_path"}}} + if edgeVariable != "" { + expectedInputs = []PhysicalValue{ + {Variable: edgeVariable, Path: []string{"auth_resource_path"}}, + {Variable: targetVariable, Path: []string{"auth_resource_path"}}, + } + } + expectedInputs = append(expectedInputs, PhysicalValue{BindKey: "auth_resource_paths"}, PhysicalValue{BindKey: "auth_resource_paths_unrestricted"}) + if len(derived.Inputs) != len(expectedInputs) { + return "", fmt.Errorf("AUTH_RESOURCE_PATH_ALLOWED requires the exact generic auth scope inputs") + } + for index := range expectedInputs { + if !sameRenderPhysicalValue(derived.Inputs[index], expectedInputs[index]) { + return "", fmt.Errorf("AUTH_RESOURCE_PATH_ALLOWED input %d is not the required generic scope value", index) + } + } + if operations[authFilterIndex].Kind != PhysicalFilterOp || !matchesPhysicalEquality(operations[authFilterIndex].Filter.Predicate, PhysicalValue{Variable: derived.Variable}, PhysicalValue{BindKey: "scope_allowed"}) { + return "", fmt.Errorf("auth scope must be %s == @scope_allowed", derived.Variable) + } + return derived.Variable, nil +} + +func matchesPhysicalEquality(predicate PhysicalPredicate, left, right PhysicalValue) bool { + return strings.ToUpper(strings.TrimSpace(predicate.Operator)) == "EQUALS" && + predicate.Right != nil && + sameRenderPhysicalValue(predicate.Left, left) && + sameRenderPhysicalValue(*predicate.Right, right) +} + +func sameRenderPhysicalValue(left, right PhysicalValue) bool { + if left.Variable != right.Variable || left.BindKey != right.BindKey || len(left.Path) != len(right.Path) { + return false + } + for index := range left.Path { + if left.Path[index] != right.Path[index] { + return false + } + } + return true +} + +func validateNavigationReturnScope(returnOp PhysicalReturn, rootVariable, rootScopeVariable string, unnestVariables map[string]struct{}) error { + for _, projection := range returnOp.Projections { + if projection.Expression != nil { + continue + } + if projection.Value.BindKey != "" { + continue + } + if projection.Value.Variable != rootVariable && projection.Value.Variable != rootScopeVariable { + if _, ok := unnestVariables[projection.Value.Variable]; ok { + continue + } + return fmt.Errorf("RETURN projection %q references %q, but traversal variables are local to LET subqueries", projection.Name, projection.Value.Variable) + } + } + return nil +} diff --git a/internal/dataframe/compiler/render/aql/navigation_render.go b/internal/dataframe/compiler/render/aql/navigation_render.go new file mode 100644 index 0000000..d1d278c --- /dev/null +++ b/internal/dataframe/compiler/render/aql/navigation_render.go @@ -0,0 +1,385 @@ +package aql + +import ( + "fmt" + "strings" +) + +func (r *physicalPlanRenderer) renderTraversalSet(block physicalNavigationTraversal, rootVariable string, traversalIndex int) ([]string, error) { + traversal := block.traversal + setVariable := r.newInternalVariable(fmt.Sprintf("set_%d", traversalIndex)) + parentVariable := traversal.SourceVariable + traversalIndent := " " + lines := []string{fmt.Sprintf(" LET %s = (", setVariable)} + if traversal.SourceVariable != rootVariable { + parentSet, ok := r.setVariables[traversal.SourceVariable] + if !ok { + return nil, fmt.Errorf("source variable %q has no previously rendered parent set", traversal.SourceVariable) + } + parentVariable = r.newInternalVariable(fmt.Sprintf("parent_%d", traversalIndex)) + lines = append(lines, fmt.Sprintf(" FOR %s IN %s", parentVariable, parentSet)) + traversalIndent = " " + } + strategy := traversal.Strategy + if strategy == "" { + strategy = PhysicalTraversalNative + } + if strategy == PhysicalTraversalEndpointLookup { + lines = append(lines, + fmt.Sprintf("%sFOR %s IN @@%s", traversalIndent, traversal.EdgeVariable, traversal.EdgeCollectionBindKey), + fmt.Sprintf("%s FILTER %s.%s == %s._id", traversalIndent, traversal.EdgeVariable, traversal.EndpointField, parentVariable), + fmt.Sprintf("%s FILTER %s.label == @%s", traversalIndent, traversal.EdgeVariable, traversal.EdgeLabelBindKey), + fmt.Sprintf("%s FILTER %s.%s == @%s", traversalIndent, traversal.EdgeVariable, traversal.EdgeTargetTypeField, traversal.TargetTypeBindKey), + fmt.Sprintf("%s LET %s = DOCUMENT(%s.%s)", traversalIndent, traversal.TargetVariable, traversal.EdgeVariable, traversal.EndpointJoinField), + fmt.Sprintf("%s FILTER %s != null", traversalIndent, traversal.TargetVariable), + fmt.Sprintf("%s FILTER %s.resourceType == @%s", traversalIndent, traversal.TargetVariable, traversal.TargetTypeBindKey), + ) + } else { + lines = append(lines, + fmt.Sprintf("%sFOR %s, %s IN 1..1 %s %s @@%s", traversalIndent, traversal.TargetVariable, traversal.EdgeVariable, traversal.Direction, parentVariable, traversal.EdgeCollectionBindKey), + fmt.Sprintf("%s FILTER %s.label == @%s", traversalIndent, traversal.EdgeVariable, traversal.EdgeLabelBindKey), + fmt.Sprintf("%s FILTER %s.%s == @%s", traversalIndent, traversal.EdgeVariable, traversal.EdgeTargetTypeField, traversal.TargetTypeBindKey), + fmt.Sprintf("%s FILTER %s.resourceType == @%s", traversalIndent, traversal.TargetVariable, traversal.TargetTypeBindKey), + ) + } + for scopeIndex, operation := range block.scope { + line, err := r.renderScopeOperation(operation, traversalIndent+" ") + if err != nil { + return nil, fmt.Errorf("render traversal scope operation %d: %w", scopeIndex, err) + } + lines = append(lines, line...) + } + lines = append(lines, traversalIndent+" RETURN "+traversal.TargetVariable, " )") + r.setVariables[traversal.TargetVariable] = setVariable + return lines, nil +} + +// renderUnnest lowers the canonical cardinality-changing operation into +// correlated AQL loops. The physical IR deliberately does not contain AQL; +// this is the sole renderer implementation for both recipe-originated and +// generic plans. +// +// A source is evaluated once per parent row and normalized to [] on null. An +// INNER unnest emits no rows for an empty source. OUTER uses a nullable index +// sentinel so it emits exactly one parent row with a null item. Indexed loops +// are used whenever ordinality or OUTER semantics are requested, preserving +// duplicate values and a stable zero-based position. +func (r *physicalPlanRenderer) renderUnnest(unnest PhysicalUnnest, indent string, ordinal, depth int) ([]string, error) { + source, err := r.renderExpression(unnest.Expression) + if err != nil { + return nil, fmt.Errorf("unnest source: %w", err) + } + if unnest.InputVariable == "" || unnest.OutputVariable == "" { + return nil, fmt.Errorf("unnest requires input and output variables") + } + baseIndent := indent + if depth > 0 { + baseIndent = strings.Repeat(" ", depth+1) + } + sourceVariable := r.newInternalVariable(fmt.Sprintf("unnest_source_%d", ordinal)) + // Selector extraction preserves one array layer per repeated path. UNNEST + // consumes the resulting collection, so flatten exactly one layer here; + // otherwise a source such as root.member[] becomes [member[]] and emits one + // row per parent rather than one row per member. + lines := []string{fmt.Sprintf("%sLET %s = (%s == null ? [] : FLATTEN(%s))", baseIndent, sourceVariable, source, source)} + indexed := unnest.JoinMode == PhysicalUnnestOuter || unnest.Ordinality != "" + if !indexed { + lines = append(lines, fmt.Sprintf("%sFOR %s IN %s", baseIndent, unnest.OutputVariable, sourceVariable)) + return lines, nil + } + indexVariable := r.newInternalVariable(fmt.Sprintf("unnest_index_%d", ordinal)) + indices := fmt.Sprintf("LENGTH(%s) == 0 ? %s : RANGE(0, LENGTH(%s) - 1)", sourceVariable, "[]", sourceVariable) + if unnest.JoinMode == PhysicalUnnestOuter { + indices = fmt.Sprintf("LENGTH(%s) == 0 ? [null] : RANGE(0, LENGTH(%s) - 1)", sourceVariable, sourceVariable) + } + lines = append(lines, fmt.Sprintf("%sFOR %s IN (%s)", baseIndent, indexVariable, indices)) + item := fmt.Sprintf("%s == null ? null : %s[%s]", indexVariable, sourceVariable, indexVariable) + lines = append(lines, fmt.Sprintf("%sLET %s = %s", baseIndent, unnest.OutputVariable, item)) + if unnest.Ordinality != "" { + lines = append(lines, fmt.Sprintf("%sLET %s = %s", baseIndent, unnest.Ordinality, indexVariable)) + } + return lines, nil +} + +func (r *physicalPlanRenderer) renderSet(set PhysicalSet, index int) ([]string, error) { + if set.SourceSetVariable != "" { + return r.renderSharedSubset(set) + } + if len(set.Subplan.Operations) == 0 { + return nil, fmt.Errorf("set %q has no subplan operations", set.Variable) + } + first := set.Subplan.Operations[0] + if first.Kind != PhysicalTraversalOp || first.Traversal == nil { + return nil, fmt.Errorf("set %q must begin with TRAVERSAL", set.Variable) + } + t := first.Traversal + parentVariable := t.SourceVariable + indent := " " + lines := []string{fmt.Sprintf(" LET %s = (", set.Variable)} + if parentSet, ok := r.setVariables[t.SourceVariable]; ok { + parentVariable = r.newInternalVariable(fmt.Sprintf("parent_set_%d", index)) + lines = append(lines, fmt.Sprintf(" FOR %s IN %s", parentVariable, parentSet)) + indent = " " + } + strategy := t.Strategy + if strategy == "" { + strategy = PhysicalTraversalNative + } + if strategy == PhysicalTraversalEndpointLookup { + // The endpoint equality is the first edge predicate so Arango can use + // the route's compound endpoint index. The node is materialized only + // after edge scope/type predicates have narrowed the candidate set. + lines = append(lines, + fmt.Sprintf("%sFOR %s IN @@%s", indent, t.EdgeVariable, t.EdgeCollectionBindKey), + fmt.Sprintf("%s FILTER %s.%s == %s._id", indent, t.EdgeVariable, t.EndpointField, parentVariable), + fmt.Sprintf("%s FILTER %s.label == @%s", indent, t.EdgeVariable, t.EdgeLabelBindKey), + ) + if t.TargetTypeBindKey != "" && t.EdgeTargetTypeField != "" { + if _, ok := r.bindVars[t.TargetTypeBindKey].([]string); ok { + lines = append(lines, fmt.Sprintf("%s FILTER POSITION(@%s, %s.%s)", indent, t.TargetTypeBindKey, t.EdgeVariable, t.EdgeTargetTypeField)) + } else { + lines = append(lines, fmt.Sprintf("%s FILTER %s.%s == @%s", indent, t.EdgeVariable, t.EdgeTargetTypeField, t.TargetTypeBindKey)) + } + } + lines = append(lines, + fmt.Sprintf("%s LET %s = DOCUMENT(%s.%s)", indent, t.TargetVariable, t.EdgeVariable, t.EndpointJoinField), + fmt.Sprintf("%s FILTER %s != null", indent, t.TargetVariable), + ) + if t.TargetTypeBindKey != "" { + if _, ok := r.bindVars[t.TargetTypeBindKey].([]string); ok { + lines = append(lines, fmt.Sprintf("%s FILTER POSITION(@%s, %s.resourceType)", indent, t.TargetTypeBindKey, t.TargetVariable)) + } else { + lines = append(lines, fmt.Sprintf("%s FILTER %s.resourceType == @%s", indent, t.TargetVariable, t.TargetTypeBindKey)) + } + } + } else { + lines = append(lines, fmt.Sprintf("%sFOR %s, %s IN 1..1 %s %s @@%s", indent, t.TargetVariable, t.EdgeVariable, t.Direction, parentVariable, t.EdgeCollectionBindKey), fmt.Sprintf("%s FILTER %s.label == @%s", indent, t.EdgeVariable, t.EdgeLabelBindKey)) + if t.EdgeTargetTypeField != "" { + lines = append(lines, r.renderTraversalTypeFilters(t, indent)...) + } + } + for opIndex, operation := range set.Subplan.Operations[1:] { + if operation.Kind == PhysicalUnnestOp { + if operation.Unnest == nil { + return nil, fmt.Errorf("set operation %d: unnest payload is missing", opIndex+1) + } + rendered, err := r.renderUnnest(*operation.Unnest, indent+" ", opIndex+1, 0) + if err != nil { + return nil, fmt.Errorf("set operation %d: %w", opIndex+1, err) + } + lines = append(lines, rendered...) + continue + } + rendered, err := r.renderScopeOperation(operation, indent+" ") + if err != nil { + return nil, fmt.Errorf("set operation %d: %w", opIndex+1, err) + } + lines = append(lines, rendered...) + } + value, err := r.renderExpression(set.Subplan.Return) + if err != nil { + return nil, err + } + if set.Projection != nil { + value, err = r.renderPhysicalSetProjection(t.TargetVariable, *set.Projection) + } else { + value, err = renderPhysicalSetOutput(value, set.Output) + } + if err != nil { + return nil, err + } + if set.SortByKey { + lines = append(lines, indent+" SORT "+t.TargetVariable+"._key") + } + lines = append(lines, indent+" RETURN "+value, " )") + if set.Unique { + lines[0] = fmt.Sprintf(" LET %s = UNIQUE((", set.Variable) + lines[len(lines)-1] = " ))" + } + r.setVariables[set.Variable] = set.Variable + if set.Prepared != nil { + prepared, err := r.renderPreparedSet(*set.Prepared) + if err != nil { + return nil, err + } + lines = append(lines, prepared...) + r.setVariables[set.Prepared.Variable] = set.Prepared.Variable + } + _ = index + return lines, nil +} + +func renderPhysicalSetOutput(value string, output *PhysicalSetOutput) (string, error) { + if output == nil { + return value, nil + } + fields := make([]string, 0, len(output.Fields)) + for _, field := range output.Fields { + name := string(field) + switch field { + case PhysicalSetGraphIDField, PhysicalSetKeyField, PhysicalSetIDField, PhysicalSetResourceTypeField, PhysicalSetPayloadField: + fields = append(fields, fmt.Sprintf("%s: %s.%s", name, value, name)) + default: + return "", fmt.Errorf("unsupported compact set output field %q", field) + } + } + return "{ " + strings.Join(fields, ", ") + " }", nil +} + +func (r *physicalPlanRenderer) renderPhysicalSetProjection(item string, projection PhysicalSetProjection) (string, error) { + if len(projection.Fields) == 0 { + return "", fmt.Errorf("set projection requires at least one field") + } + fields := []string{ + "_id: " + item + "._id", + "_key: " + item + "._key", + "id: " + item + ".id", + "resourceType: " + item + ".resourceType", + } + for _, field := range projection.Fields { + values, err := r.renderSelectorByMode(item+".payload", field.Selector, field.ExecutionMode) + if err != nil { + return "", fmt.Errorf("projected field %q: %w", field.Name, err) + } + fields = append(fields, field.Name+": "+values) + } + return "{ " + strings.Join(fields, ", ") + " }", nil +} + +func (r *physicalPlanRenderer) renderPreparedSet(prepared PhysicalPreparedSet) ([]string, error) { + if r.setVariables[prepared.SourceSetVariable] == "" { + return nil, fmt.Errorf("prepared source set %q has not been rendered", prepared.SourceSetVariable) + } + item := r.newInternalVariable("prepared_item") + lines := []string{fmt.Sprintf(" LET %s = (", prepared.Variable), fmt.Sprintf(" FOR %s IN %s", item, prepared.SourceSetVariable)} + // Rich consumers may combine a prepared selector with a direct payload + // fallback (slice identity, an unprepared pivot value, or a nested object + // field). Preserve the node-facing fields those consumers already read while + // adding prepared projections; the optimizer can remove this retention only + // after a separate compact-set contract proves it safe. + fields := []string{ + fmt.Sprintf("_key: %s._key", item), + fmt.Sprintf("id: %s.id", item), + fmt.Sprintf("resourceType: %s.resourceType", item), + fmt.Sprintf("payload: %s.payload", item), + fmt.Sprintf("__loom_prepared_node: %s", item), + } + for _, field := range prepared.Fields { + values, err := r.renderSelectorArrayFromSource(item+".payload", field.Selector, false) + if err != nil { + return nil, fmt.Errorf("prepared field %q: %w", field.Name, err) + } + fields = append(fields, field.Name+": "+values) + } + lines = append(lines, " RETURN { "+strings.Join(fields, ", ")+" }", " )") + return lines, nil +} + +func (r *physicalPlanRenderer) renderTraversalTypeFilters(t *PhysicalTraversal, indent string) []string { + if t.TargetTypeBindKey == "" || t.EdgeTargetTypeField == "" { + return nil + } + if _, ok := r.bindVars[t.TargetTypeBindKey].([]string); ok { + return []string{ + fmt.Sprintf("%s FILTER POSITION(@%s, %s.%s)", indent, t.TargetTypeBindKey, t.EdgeVariable, t.EdgeTargetTypeField), + fmt.Sprintf("%s FILTER POSITION(@%s, %s.resourceType)", indent, t.TargetTypeBindKey, t.TargetVariable), + } + } + return []string{fmt.Sprintf("%s FILTER %s.%s == @%s", indent, t.EdgeVariable, t.EdgeTargetTypeField, t.TargetTypeBindKey), fmt.Sprintf("%s FILTER %s.resourceType == @%s", indent, t.TargetVariable, t.TargetTypeBindKey)} +} + +func (r *physicalPlanRenderer) renderSharedSubset(set PhysicalSet) ([]string, error) { + if r.setVariables[set.SourceSetVariable] == "" { + return nil, fmt.Errorf("shared subset source %q has not been rendered", set.SourceSetVariable) + } + lines := []string{fmt.Sprintf(" LET %s = (", set.Variable), fmt.Sprintf(" FOR %s IN %s", set.ItemVariable, set.SourceSetVariable)} + for index, operation := range set.Subplan.Operations { + if operation.Kind == PhysicalUnnestOp { + if operation.Unnest == nil { + return nil, fmt.Errorf("shared subset operation %d: unnest payload is missing", index) + } + rendered, err := r.renderUnnest(*operation.Unnest, " ", index, 0) + if err != nil { + return nil, fmt.Errorf("shared subset operation %d: %w", index, err) + } + lines = append(lines, rendered...) + continue + } + rendered, err := r.renderScopeOperation(operation, " ") + if err != nil { + return nil, fmt.Errorf("shared subset operation %d: %w", index, err) + } + lines = append(lines, rendered...) + } + value, err := r.renderExpression(set.Subplan.Return) + if err != nil { + return nil, err + } + if set.Projection != nil { + value, err = r.renderPhysicalSetProjection(set.ItemVariable, *set.Projection) + } else { + value, err = renderPhysicalSetOutput(value, set.Output) + } + if err != nil { + return nil, err + } + if set.SortByKey { + lines = append(lines, " SORT "+set.ItemVariable+"._key") + } + lines = append(lines, " RETURN "+value, " )") + if set.Unique { + lines[0] = fmt.Sprintf(" LET %s = UNIQUE((", set.Variable) + lines[len(lines)-1] = " ))" + } + r.setVariables[set.Variable] = set.Variable + if set.Prepared != nil { + prepared, err := r.renderPreparedSet(*set.Prepared) + if err != nil { + return nil, err + } + lines = append(lines, prepared...) + r.setVariables[set.Prepared.Variable] = set.Prepared.Variable + } + return lines, nil +} + +func physicalPlanVariableNames(plan PhysicalPlan) map[string]struct{} { + variables := map[string]struct{}{} + for _, operation := range plan.Operations { + switch operation.Kind { + case PhysicalRootScanOp: + variables[operation.RootScan.Variable] = struct{}{} + case PhysicalTraversalOp: + variables[operation.Traversal.SourceVariable] = struct{}{} + variables[operation.Traversal.TargetVariable] = struct{}{} + if operation.Traversal.EdgeVariable != "" { + variables[operation.Traversal.EdgeVariable] = struct{}{} + } + case PhysicalDerivedLetOp: + variables[operation.DerivedLet.Variable] = struct{}{} + case PhysicalExpressionLetOp: + variables[operation.ExpressionLet.Variable] = struct{}{} + case PhysicalSetOp: + variables[operation.Set.Variable] = struct{}{} + case PhysicalUnnestOp: + variables[operation.Unnest.InputVariable] = struct{}{} + variables[operation.Unnest.OutputVariable] = struct{}{} + if operation.Unnest.Ordinality != "" { + variables[operation.Unnest.Ordinality] = struct{}{} + } + } + } + return variables +} + +func (r *physicalPlanRenderer) newInternalVariable(suffix string) string { + base := "__loom_physical_" + suffix + variable := base + for counter := 1; ; counter++ { + if _, exists := r.reservedVars[variable]; !exists { + r.reservedVars[variable] = struct{}{} + return variable + } + variable = fmt.Sprintf("%s_%d", base, counter) + } +} diff --git a/internal/dataframe/compiler/render/aql/predicates.go b/internal/dataframe/compiler/render/aql/predicates.go new file mode 100644 index 0000000..7fa2215 --- /dev/null +++ b/internal/dataframe/compiler/render/aql/predicates.go @@ -0,0 +1,148 @@ +package aql + +import ( + "fmt" + "strings" +) + +func (r *physicalPlanRenderer) renderPredicate(predicate PhysicalPredicate) (string, error) { + if predicate.LeftExpression != nil { + return r.renderSelectorPredicate(predicate) + } + if strings.ToUpper(strings.TrimSpace(predicate.Operator)) != "EQUALS" { + return "", fmt.Errorf("unsupported physical filter operator %q", predicate.Operator) + } + if predicate.Right == nil { + return "", fmt.Errorf("EQUALS filter requires a right value") + } + left, err := r.renderValue(predicate.Left) + if err != nil { + return "", err + } + right, err := r.renderValue(*predicate.Right) + if err != nil { + return "", err + } + return left + " == " + right, nil +} + +func (r *physicalPlanRenderer) renderSelectorPredicate(predicate PhysicalPredicate) (string, error) { + values, err := r.renderExpression(*predicate.LeftExpression) + if err != nil { + return "", err + } + operator := strings.ToUpper(strings.TrimSpace(predicate.Operator)) + if operator == "EXISTS" { + return "LENGTH(" + values + ") > 0", nil + } + if operator == "MISSING" { + return "LENGTH(" + values + ") == 0", nil + } + if predicate.Right == nil { + return "", fmt.Errorf("physical filter operator %q requires a right value", predicate.Operator) + } + right, err := r.renderValue(*predicate.Right) + if err != nil { + return "", err + } + valueVar := r.newInternalVariable("filter_value") + match := "" + switch operator { + case "EQUALS": + match = valueVar + " == " + right + case "NOT_EQUALS": + match = valueVar + " != " + right + case "IN": + match = "POSITION(" + right + ", " + valueVar + ")" + case "CONTAINS_TEXT": + match = "CONTAINS(TO_STRING(" + valueVar + "), " + right + ")" + case "GT", "GTE", "LT", "LTE": + left, comparisonRight := valueVar, right + if predicate.ValueKind == FilterDate || predicate.ValueKind == FilterDateTime { + left, comparisonRight = "DATE_TIMESTAMP("+valueVar+")", "DATE_TIMESTAMP("+right+")" + } + operatorText := map[string]string{"GT": ">", "GTE": ">=", "LT": "<", "LTE": "<="}[operator] + match = left + " " + operatorText + " " + comparisonRight + default: + return "", fmt.Errorf("unsupported physical selector filter operator %q", predicate.Operator) + } + matching := "LENGTH(FOR " + valueVar + " IN " + values + " FILTER " + match + " LIMIT 1 RETURN 1)" + quantifier := predicate.Quantifier + if quantifier == "" { + quantifier = QuantifierAny + } + switch quantifier { + case QuantifierAny: + return matching + " > 0", nil + case QuantifierNone: + return matching + " == 0", nil + case QuantifierAll: + return "LENGTH(" + values + ") > 0 AND LENGTH(FOR " + valueVar + " IN " + values + " FILTER NOT (" + match + ") LIMIT 1 RETURN 1) == 0", nil + default: + return "", fmt.Errorf("unsupported physical selector filter quantifier %q", quantifier) + } +} + +func (r *physicalPlanRenderer) renderPredicateExpression(predicate PhysicalPredicateExpression, indent string) (string, error) { + switch predicate.Kind { + case PhysicalComparisonPredicate: + return r.renderPredicate(*predicate.Comparison) + case PhysicalAllPredicate, PhysicalAnyPredicate: + parts := make([]string, 0, len(predicate.Children)) + for _, child := range predicate.Children { + part, err := r.renderPredicateExpression(child, indent) + if err != nil { + return "", err + } + parts = append(parts, "("+part+")") + } + join := " AND " + if predicate.Kind == PhysicalAnyPredicate { + join = " OR " + } + return strings.Join(parts, join), nil + case PhysicalNotPredicate: + child, err := r.renderPredicateExpression(predicate.Children[0], indent) + if err != nil { + return "", err + } + return "NOT (" + child + ")", nil + case PhysicalExistsPredicate: + return r.renderExistsSubplan(*predicate.Exists, indent) + default: + return "", fmt.Errorf("unsupported physical predicate kind %q", predicate.Kind) + } +} + +// renderExistsSubplan serializes a validated correlated subplan. EXISTS is +// always bounded: relationship matching is a semi-join, never a row-expanding +// traversal, so the renderer appends LIMIT 1 immediately before RETURN. +func (r *physicalPlanRenderer) renderExistsSubplan(subplan PhysicalSubplan, indent string) (string, error) { + lines := make([]string, 0, len(subplan.Operations)*3+2) + for index, operation := range subplan.Operations { + switch operation.Kind { + case PhysicalTraversalOp: + traversal := operation.Traversal + lines = append(lines, + fmt.Sprintf("%sFOR %s, %s IN 1..1 %s %s @@%s", indent+" ", traversal.TargetVariable, traversal.EdgeVariable, traversal.Direction, traversal.SourceVariable, traversal.EdgeCollectionBindKey), + fmt.Sprintf("%s FILTER %s.label == @%s", indent+" ", traversal.EdgeVariable, traversal.EdgeLabelBindKey), + fmt.Sprintf("%s FILTER %s.%s == @%s", indent+" ", traversal.EdgeVariable, traversal.EdgeTargetTypeField, traversal.TargetTypeBindKey), + fmt.Sprintf("%s FILTER %s.resourceType == @%s", indent+" ", traversal.TargetVariable, traversal.TargetTypeBindKey), + ) + case PhysicalFilterOp, PhysicalDerivedLetOp: + rendered, err := r.renderScopeOperation(operation, indent+" ") + if err != nil { + return "", fmt.Errorf("subplan operation %d (%s): %w", index, operation.Kind, err) + } + lines = append(lines, rendered...) + default: + return "", fmt.Errorf("subplan operation %d has unsupported render kind %q", index, operation.Kind) + } + } + value, err := r.renderExpression(subplan.Return) + if err != nil { + return "", err + } + lines = append(lines, indent+" LIMIT 1", indent+" RETURN "+value) + return "LENGTH((\n" + strings.Join(lines, "\n") + "\n" + indent + " )) > 0", nil +} diff --git a/internal/dataframe/compiler/render/aql/render.go b/internal/dataframe/compiler/render/aql/render.go index 75a3df9..1aef99c 100644 --- a/internal/dataframe/compiler/render/aql/render.go +++ b/internal/dataframe/compiler/render/aql/render.go @@ -2,8 +2,6 @@ package aql import ( "fmt" - "sort" - "strconv" "strings" ) @@ -68,6 +66,13 @@ func RenderPhysicalPlan(plan PhysicalPlan) (RenderedPhysicalPlan, error) { } lines = append(lines, line...) } + for index, unnest := range layout.unnests { + line, err := renderer.renderUnnest(unnest, " ", index, 0) + if err != nil { + return RenderedPhysicalPlan{}, fmt.Errorf("render unnest %d: %w", index+1, err) + } + lines = append(lines, line...) + } for index, operation := range layout.rootWindow { line, err := renderer.renderRootWindowOperation(operation, " ") if err != nil { @@ -89,6 +94,13 @@ func RenderPhysicalPlan(plan PhysicalPlan) (RenderedPhysicalPlan, error) { } lines = append(lines, line...) } + for index, operation := range layout.expressionLets { + line, err := renderer.renderExpressionLet(operation, " ") + if err != nil { + return RenderedPhysicalPlan{}, fmt.Errorf("render expression LET %d: %w", index, err) + } + lines = append(lines, line...) + } returnExpression, err := renderer.renderReturn(layout.returnOp) if err != nil { return RenderedPhysicalPlan{}, fmt.Errorf("render RETURN: %w", err) @@ -116,10 +128,6 @@ func pruneUnusedRuntimeBindVars(bindVars map[string]any, query string) map[strin return pruned } -func PruneUnusedRuntimeBindVars(bindVars map[string]any, query string) map[string]any { - return pruneUnusedRuntimeBindVars(bindVars, query) -} - func (r *physicalPlanRenderer) renderRootWindowOperation(operation PhysicalOperation, indent string) ([]string, error) { switch operation.Kind { case PhysicalSortOp: @@ -146,6 +154,17 @@ type physicalPlanRenderer struct { preparedItem string } +func (r *physicalPlanRenderer) renderExpressionLet(operation PhysicalOperation, indent string) ([]string, error) { + if operation.ExpressionLet == nil { + return nil, fmt.Errorf("expression LET is missing payload") + } + expression, err := r.renderExpression(operation.ExpressionLet.Expression) + if err != nil { + return nil, err + } + return []string{fmt.Sprintf("%sLET %s = %s", indent, operation.ExpressionLet.Variable, expression)}, nil +} + func (r *physicalPlanRenderer) renderScopeOperation(operation PhysicalOperation, indent string) ([]string, error) { switch operation.Kind { case PhysicalFilterOp: @@ -166,6 +185,8 @@ func (r *physicalPlanRenderer) renderScopeOperation(operation PhysicalOperation, return nil, err } return []string{fmt.Sprintf("%sLET %s = %s", indent, operation.DerivedLet.Variable, expression)}, nil + case PhysicalExpressionLetOp: + return r.renderExpressionLet(operation, indent) default: return nil, fmt.Errorf("navigation scope cannot contain physical operation %q", operation.Kind) } @@ -180,7 +201,9 @@ type physicalNavigationRenderLayout struct { rootPredicates []PhysicalOperation rootWindow []PhysicalOperation traversals []physicalNavigationTraversal + unnests []PhysicalUnnest sets []PhysicalSet + expressionLets []PhysicalOperation returnOp PhysicalReturn } @@ -188,1479 +211,3 @@ type physicalNavigationTraversal struct { traversal PhysicalTraversal scope []PhysicalOperation } - -func buildNavigationRenderLayout(plan PhysicalPlan) (physicalNavigationRenderLayout, error) { - if len(plan.Operations) < 6 { - return physicalNavigationRenderLayout{}, fmt.Errorf("generic navigation renderer requires ROOT_SCAN, scope operations, and RETURN") - } - if plan.Operations[0].Kind != PhysicalRootScanOp { - return physicalNavigationRenderLayout{}, fmt.Errorf("generic navigation renderer requires ROOT_SCAN as the first operation") - } - last := len(plan.Operations) - 1 - if plan.Operations[last].Kind != PhysicalReturnOp { - return physicalNavigationRenderLayout{}, fmt.Errorf("generic navigation renderer requires RETURN as the final operation") - } - - layout := physicalNavigationRenderLayout{ - root: *plan.Operations[0].RootScan, - rootScope: append([]PhysicalOperation(nil), plan.Operations[1:5]...), - returnOp: *plan.Operations[last].Return, - } - rootScopeVariable, err := validateGenericNavigationScopeBlock(layout.rootScope, layout.root.Variable, "", layout.root.Variable) - if err != nil { - return physicalNavigationRenderLayout{}, fmt.Errorf("root navigation scope: %w", err) - } - - index := 5 - for index < last && plan.Operations[index].Kind == PhysicalFilterOp && plan.Operations[index].Filter.Expression != nil { - layout.rootPredicates = append(layout.rootPredicates, plan.Operations[index]) - index++ - } - if index < last && plan.Operations[index].Kind == PhysicalSortOp { - if err := validateGenericNavigationRootSort(plan.Operations[index], layout.root.Variable); err != nil { - return physicalNavigationRenderLayout{}, fmt.Errorf("root execution window at operation %d: %w", index, err) - } - layout.rootWindow = append(layout.rootWindow, plan.Operations[index]) - index++ - if index < last && plan.Operations[index].Kind == PhysicalLimitOp { - if err := validateGenericNavigationRootLimit(plan.Operations[index]); err != nil { - return physicalNavigationRenderLayout{}, fmt.Errorf("root execution window at operation %d: %w", index, err) - } - layout.rootWindow = append(layout.rootWindow, plan.Operations[index]) - index++ - } - } else if index < last && plan.Operations[index].Kind == PhysicalLimitOp { - return physicalNavigationRenderLayout{}, fmt.Errorf("root execution window at operation %d: LIMIT requires deterministic root SORT", index) - } - for index < last { - operation := plan.Operations[index] - if operation.Kind == PhysicalSetOp { - layout.sets = append(layout.sets, *operation.Set) - index++ - continue - } - if operation.Kind != PhysicalTraversalOp { - return physicalNavigationRenderLayout{}, fmt.Errorf("generic navigation renderer expected TRAVERSAL at operation %d, got %s", index, operation.Kind) - } - const traversalScopeLength = 6 // edge + target project/generation, then auth LET/filter - if index+traversalScopeLength >= last { - return physicalNavigationRenderLayout{}, fmt.Errorf("traversal at operation %d is missing its project/auth scope block", index) - } - traversal := *operation.Traversal - if err := validateGenericNavigationTraversal(plan, traversal); err != nil { - return physicalNavigationRenderLayout{}, fmt.Errorf("traversal at operation %d: %w", index, err) - } - scope := append([]PhysicalOperation(nil), plan.Operations[index+1:index+1+traversalScopeLength]...) - if _, err := validateGenericNavigationScopeBlock(scope, traversal.TargetVariable, traversal.EdgeVariable, traversal.TargetVariable); err != nil { - return physicalNavigationRenderLayout{}, fmt.Errorf("traversal at operation %d scope: %w", index, err) - } - layout.traversals = append(layout.traversals, physicalNavigationTraversal{traversal: traversal, scope: scope}) - index += 1 + traversalScopeLength - } - if err := validateNavigationReturnScope(layout.returnOp, layout.root.Variable, rootScopeVariable); err != nil { - return physicalNavigationRenderLayout{}, err - } - return layout, nil -} - -func validateGenericNavigationTraversal(plan PhysicalPlan, traversal PhysicalTraversal) error { - if traversal.Direction != PhysicalInbound && traversal.Direction != PhysicalOutbound { - return fmt.Errorf("generic navigation traversal direction must be INBOUND or OUTBOUND, got %q", traversal.Direction) - } - wantEdgeTypeField := "from_type" - if traversal.Direction == PhysicalOutbound { - wantEdgeTypeField = "to_type" - } - if traversal.EdgeTargetTypeField != wantEdgeTypeField { - return fmt.Errorf("generic navigation traversal %s must constrain edge.%s, got %q", traversal.Direction, wantEdgeTypeField, traversal.EdgeTargetTypeField) - } - if traversal.EdgeVariable == "" || traversal.EdgeLabelBindKey == "" || traversal.TargetTypeBindKey == "" { - return fmt.Errorf("generic navigation traversal requires edge variable, edge label bind, and target type bind") - } - collection, ok := plan.BindVars[traversal.EdgeCollectionBindKey].(string) - if !ok || collection != "fhir_edge" { - return fmt.Errorf("generic navigation traversal must use fhir_edge through its collection bind") - } - strategy := traversal.Strategy - if strategy == "" { - strategy = PhysicalTraversalNative - } - if strategy != PhysicalTraversalNative && strategy != PhysicalTraversalEndpointLookup { - return fmt.Errorf("unsupported generic navigation traversal strategy %q", strategy) - } - if strategy == PhysicalTraversalEndpointLookup { - wantEndpoint, wantJoin := "_to", "_from" - wantIndexType := "from_type" - if traversal.Direction == PhysicalOutbound { - wantEndpoint, wantJoin, wantIndexType = "_from", "_to", "to_type" - } - if traversal.EndpointField != wantEndpoint || traversal.EndpointJoinField != wantJoin { - return fmt.Errorf("endpoint lookup %s requires %s -> %s, got %s -> %s", traversal.Direction, wantEndpoint, wantJoin, traversal.EndpointField, traversal.EndpointJoinField) - } - wantIndex := []string{wantEndpoint, "project", "dataset_generation", "label", wantIndexType} - if len(traversal.EndpointIndexFields) != len(wantIndex) { - return fmt.Errorf("endpoint lookup requires compound index fields %#v", wantIndex) - } - for index := range wantIndex { - if traversal.EndpointIndexFields[index] != wantIndex[index] { - return fmt.Errorf("endpoint lookup index field %d = %q, want %q", index, traversal.EndpointIndexFields[index], wantIndex[index]) - } - } - } - return nil -} - -func validateGenericNavigationRootSort(operation PhysicalOperation, rootVariable string) error { - if operation.Sort == nil || !sameRenderPhysicalValue(operation.Sort.Value, PhysicalValue{Variable: rootVariable, Path: []string{"_key"}}) { - return fmt.Errorf("SORT must order the root variable %s._key", rootVariable) - } - return nil -} - -func validateGenericNavigationRootLimit(operation PhysicalOperation) error { - if operation.Limit == nil || operation.Limit.BindKey != genericPhysicalExecutionLimitBind { - return fmt.Errorf("LIMIT must use @%s", genericPhysicalExecutionLimitBind) - } - return nil -} - -// validateGenericNavigationScopeBlock accepts the exact generation-safe scope -// operations emitted by appendProjectScope, appendDatasetGenerationScope, and -// appendAuthScope. The standalone -// scope verifier is intentionally more flexible; rendering is stricter so it -// can relocate the whole block safely into a LET subquery. -func validateGenericNavigationScopeBlock(operations []PhysicalOperation, resourceVariable, edgeVariable, targetVariable string) (string, error) { - expectedProjectVariables := []string{resourceVariable} - expectedGenerationVariables := []string{resourceVariable} - if edgeVariable != "" { - expectedProjectVariables = []string{edgeVariable, targetVariable} - expectedGenerationVariables = []string{edgeVariable, targetVariable} - } - expectedLength := len(expectedProjectVariables) + len(expectedGenerationVariables) + 2 - if len(operations) != expectedLength || operations[0].Kind != PhysicalFilterOp { - return "", fmt.Errorf("requires project filters for every graph document, dataset_generation filters, LET AUTH_RESOURCE_PATH_ALLOWED, FILTER scope_allowed in order") - } - for index, variable := range expectedProjectVariables { - operation := operations[index] - if operation.Kind != PhysicalFilterOp || !matchesPhysicalEquality(operation.Filter.Predicate, PhysicalValue{Variable: variable, Path: []string{"project"}}, PhysicalValue{BindKey: "project"}) { - return "", fmt.Errorf("project scope must be %s.project == @project", variable) - } - } - for index, variable := range expectedGenerationVariables { - operation := operations[len(expectedProjectVariables)+index] - if operation.Kind != PhysicalFilterOp || !matchesPhysicalEquality(operation.Filter.Predicate, PhysicalValue{Variable: variable, Path: []string{datasetGenerationField}}, PhysicalValue{BindKey: datasetGenerationBindKey}) { - return "", fmt.Errorf("dataset generation scope must be %s.%s == @%s", variable, datasetGenerationField, datasetGenerationBindKey) - } - } - authLetIndex := len(expectedProjectVariables) + len(expectedGenerationVariables) - authFilterIndex := authLetIndex + 1 - - if operations[authLetIndex].Kind != PhysicalDerivedLetOp || operations[authLetIndex].DerivedLet == nil { - return "", fmt.Errorf("scope block requires AUTH_RESOURCE_PATH_ALLOWED LET after dataset generation scope") - } - derived := operations[authLetIndex].DerivedLet - if strings.ToUpper(strings.TrimSpace(derived.Operator)) != "AUTH_RESOURCE_PATH_ALLOWED" { - return "", fmt.Errorf("scope LET must use AUTH_RESOURCE_PATH_ALLOWED") - } - expectedInputs := []PhysicalValue{{Variable: resourceVariable, Path: []string{"auth_resource_path"}}} - if edgeVariable != "" { - expectedInputs = []PhysicalValue{ - {Variable: edgeVariable, Path: []string{"auth_resource_path"}}, - {Variable: targetVariable, Path: []string{"auth_resource_path"}}, - } - } - expectedInputs = append(expectedInputs, PhysicalValue{BindKey: "auth_resource_paths"}, PhysicalValue{BindKey: "auth_resource_paths_unrestricted"}) - if len(derived.Inputs) != len(expectedInputs) { - return "", fmt.Errorf("AUTH_RESOURCE_PATH_ALLOWED requires the exact generic auth scope inputs") - } - for index := range expectedInputs { - if !sameRenderPhysicalValue(derived.Inputs[index], expectedInputs[index]) { - return "", fmt.Errorf("AUTH_RESOURCE_PATH_ALLOWED input %d is not the required generic scope value", index) - } - } - if operations[authFilterIndex].Kind != PhysicalFilterOp || !matchesPhysicalEquality(operations[authFilterIndex].Filter.Predicate, PhysicalValue{Variable: derived.Variable}, PhysicalValue{BindKey: "scope_allowed"}) { - return "", fmt.Errorf("auth scope must be %s == @scope_allowed", derived.Variable) - } - return derived.Variable, nil -} - -func matchesPhysicalEquality(predicate PhysicalPredicate, left, right PhysicalValue) bool { - return strings.ToUpper(strings.TrimSpace(predicate.Operator)) == "EQUALS" && - predicate.Right != nil && - sameRenderPhysicalValue(predicate.Left, left) && - sameRenderPhysicalValue(*predicate.Right, right) -} - -func sameRenderPhysicalValue(left, right PhysicalValue) bool { - if left.Variable != right.Variable || left.BindKey != right.BindKey || len(left.Path) != len(right.Path) { - return false - } - for index := range left.Path { - if left.Path[index] != right.Path[index] { - return false - } - } - return true -} - -func validateNavigationReturnScope(returnOp PhysicalReturn, rootVariable, rootScopeVariable string) error { - for _, projection := range returnOp.Projections { - if projection.Expression != nil { - continue - } - if projection.Value.BindKey != "" { - continue - } - if projection.Value.Variable != rootVariable && projection.Value.Variable != rootScopeVariable { - return fmt.Errorf("RETURN projection %q references %q, but traversal variables are local to LET subqueries", projection.Name, projection.Value.Variable) - } - } - return nil -} - -func (r *physicalPlanRenderer) renderTraversalSet(block physicalNavigationTraversal, rootVariable string, traversalIndex int) ([]string, error) { - traversal := block.traversal - setVariable := r.newInternalVariable(fmt.Sprintf("set_%d", traversalIndex)) - parentVariable := traversal.SourceVariable - traversalIndent := " " - lines := []string{fmt.Sprintf(" LET %s = (", setVariable)} - if traversal.SourceVariable != rootVariable { - parentSet, ok := r.setVariables[traversal.SourceVariable] - if !ok { - return nil, fmt.Errorf("source variable %q has no previously rendered parent set", traversal.SourceVariable) - } - parentVariable = r.newInternalVariable(fmt.Sprintf("parent_%d", traversalIndex)) - lines = append(lines, fmt.Sprintf(" FOR %s IN %s", parentVariable, parentSet)) - traversalIndent = " " - } - strategy := traversal.Strategy - if strategy == "" { - strategy = PhysicalTraversalNative - } - if strategy == PhysicalTraversalEndpointLookup { - lines = append(lines, - fmt.Sprintf("%sFOR %s IN @@%s", traversalIndent, traversal.EdgeVariable, traversal.EdgeCollectionBindKey), - fmt.Sprintf("%s FILTER %s.%s == %s._id", traversalIndent, traversal.EdgeVariable, traversal.EndpointField, parentVariable), - fmt.Sprintf("%s FILTER %s.label == @%s", traversalIndent, traversal.EdgeVariable, traversal.EdgeLabelBindKey), - fmt.Sprintf("%s FILTER %s.%s == @%s", traversalIndent, traversal.EdgeVariable, traversal.EdgeTargetTypeField, traversal.TargetTypeBindKey), - fmt.Sprintf("%s LET %s = DOCUMENT(%s.%s)", traversalIndent, traversal.TargetVariable, traversal.EdgeVariable, traversal.EndpointJoinField), - fmt.Sprintf("%s FILTER %s != null", traversalIndent, traversal.TargetVariable), - fmt.Sprintf("%s FILTER %s.resourceType == @%s", traversalIndent, traversal.TargetVariable, traversal.TargetTypeBindKey), - ) - } else { - lines = append(lines, - fmt.Sprintf("%sFOR %s, %s IN 1..1 %s %s @@%s", traversalIndent, traversal.TargetVariable, traversal.EdgeVariable, traversal.Direction, parentVariable, traversal.EdgeCollectionBindKey), - fmt.Sprintf("%s FILTER %s.label == @%s", traversalIndent, traversal.EdgeVariable, traversal.EdgeLabelBindKey), - fmt.Sprintf("%s FILTER %s.%s == @%s", traversalIndent, traversal.EdgeVariable, traversal.EdgeTargetTypeField, traversal.TargetTypeBindKey), - fmt.Sprintf("%s FILTER %s.resourceType == @%s", traversalIndent, traversal.TargetVariable, traversal.TargetTypeBindKey), - ) - } - for scopeIndex, operation := range block.scope { - line, err := r.renderScopeOperation(operation, traversalIndent+" ") - if err != nil { - return nil, fmt.Errorf("render traversal scope operation %d: %w", scopeIndex, err) - } - lines = append(lines, line...) - } - lines = append(lines, traversalIndent+" RETURN "+traversal.TargetVariable, " )") - r.setVariables[traversal.TargetVariable] = setVariable - return lines, nil -} - -func (r *physicalPlanRenderer) renderSet(set PhysicalSet, index int) ([]string, error) { - if set.SourceSetVariable != "" { - return r.renderSharedSubset(set) - } - if len(set.Subplan.Operations) == 0 { - return nil, fmt.Errorf("set %q has no subplan operations", set.Variable) - } - first := set.Subplan.Operations[0] - if first.Kind != PhysicalTraversalOp || first.Traversal == nil { - return nil, fmt.Errorf("set %q must begin with TRAVERSAL", set.Variable) - } - t := first.Traversal - parentVariable := t.SourceVariable - indent := " " - lines := []string{fmt.Sprintf(" LET %s = (", set.Variable)} - if parentSet, ok := r.setVariables[t.SourceVariable]; ok { - parentVariable = r.newInternalVariable(fmt.Sprintf("parent_set_%d", index)) - lines = append(lines, fmt.Sprintf(" FOR %s IN %s", parentVariable, parentSet)) - indent = " " - } - strategy := t.Strategy - if strategy == "" { - strategy = PhysicalTraversalNative - } - if strategy == PhysicalTraversalEndpointLookup { - // The endpoint equality is the first edge predicate so Arango can use - // the route's compound endpoint index. The node is materialized only - // after edge scope/type predicates have narrowed the candidate set. - lines = append(lines, - fmt.Sprintf("%sFOR %s IN @@%s", indent, t.EdgeVariable, t.EdgeCollectionBindKey), - fmt.Sprintf("%s FILTER %s.%s == %s._id", indent, t.EdgeVariable, t.EndpointField, parentVariable), - fmt.Sprintf("%s FILTER %s.label == @%s", indent, t.EdgeVariable, t.EdgeLabelBindKey), - ) - if t.TargetTypeBindKey != "" && t.EdgeTargetTypeField != "" { - if _, ok := r.bindVars[t.TargetTypeBindKey].([]string); ok { - lines = append(lines, fmt.Sprintf("%s FILTER POSITION(@%s, %s.%s)", indent, t.TargetTypeBindKey, t.EdgeVariable, t.EdgeTargetTypeField)) - } else { - lines = append(lines, fmt.Sprintf("%s FILTER %s.%s == @%s", indent, t.EdgeVariable, t.EdgeTargetTypeField, t.TargetTypeBindKey)) - } - } - lines = append(lines, - fmt.Sprintf("%s LET %s = DOCUMENT(%s.%s)", indent, t.TargetVariable, t.EdgeVariable, t.EndpointJoinField), - fmt.Sprintf("%s FILTER %s != null", indent, t.TargetVariable), - ) - if t.TargetTypeBindKey != "" { - if _, ok := r.bindVars[t.TargetTypeBindKey].([]string); ok { - lines = append(lines, fmt.Sprintf("%s FILTER POSITION(@%s, %s.resourceType)", indent, t.TargetTypeBindKey, t.TargetVariable)) - } else { - lines = append(lines, fmt.Sprintf("%s FILTER %s.resourceType == @%s", indent, t.TargetVariable, t.TargetTypeBindKey)) - } - } - } else { - lines = append(lines, fmt.Sprintf("%sFOR %s, %s IN 1..1 %s %s @@%s", indent, t.TargetVariable, t.EdgeVariable, t.Direction, parentVariable, t.EdgeCollectionBindKey), fmt.Sprintf("%s FILTER %s.label == @%s", indent, t.EdgeVariable, t.EdgeLabelBindKey)) - if t.EdgeTargetTypeField != "" { - lines = append(lines, r.renderTraversalTypeFilters(t, indent)...) - } - } - for opIndex, operation := range set.Subplan.Operations[1:] { - rendered, err := r.renderScopeOperation(operation, indent+" ") - if err != nil { - return nil, fmt.Errorf("set operation %d: %w", opIndex+1, err) - } - lines = append(lines, rendered...) - } - value, err := r.renderExpression(set.Subplan.Return) - if err != nil { - return nil, err - } - if set.Projection != nil { - value, err = r.renderPhysicalSetProjection(t.TargetVariable, *set.Projection) - } else { - value, err = renderPhysicalSetOutput(value, set.Output) - } - if err != nil { - return nil, err - } - if set.SortByKey { - lines = append(lines, indent+" SORT "+t.TargetVariable+"._key") - } - lines = append(lines, indent+" RETURN "+value, " )") - if set.Unique { - lines[0] = fmt.Sprintf(" LET %s = UNIQUE((", set.Variable) - lines[len(lines)-1] = " ))" - } - r.setVariables[set.Variable] = set.Variable - if set.Prepared != nil { - prepared, err := r.renderPreparedSet(*set.Prepared) - if err != nil { - return nil, err - } - lines = append(lines, prepared...) - r.setVariables[set.Prepared.Variable] = set.Prepared.Variable - } - _ = index - return lines, nil -} - -func renderPhysicalSetOutput(value string, output *PhysicalSetOutput) (string, error) { - if output == nil { - return value, nil - } - fields := make([]string, 0, len(output.Fields)) - for _, field := range output.Fields { - name := string(field) - switch field { - case PhysicalSetGraphIDField, PhysicalSetKeyField, PhysicalSetIDField, PhysicalSetResourceTypeField, PhysicalSetPayloadField: - fields = append(fields, fmt.Sprintf("%s: %s.%s", name, value, name)) - default: - return "", fmt.Errorf("unsupported compact set output field %q", field) - } - } - return "{ " + strings.Join(fields, ", ") + " }", nil -} - -func (r *physicalPlanRenderer) renderPhysicalSetProjection(item string, projection PhysicalSetProjection) (string, error) { - if len(projection.Fields) == 0 { - return "", fmt.Errorf("set projection requires at least one field") - } - fields := []string{ - "_id: " + item + "._id", - "_key: " + item + "._key", - "id: " + item + ".id", - "resourceType: " + item + ".resourceType", - } - for _, field := range projection.Fields { - values, err := r.renderSelectorByMode(item+".payload", field.Selector, field.ExecutionMode) - if err != nil { - return "", fmt.Errorf("projected field %q: %w", field.Name, err) - } - fields = append(fields, field.Name+": "+values) - } - return "{ " + strings.Join(fields, ", ") + " }", nil -} - -func (r *physicalPlanRenderer) renderPreparedSet(prepared PhysicalPreparedSet) ([]string, error) { - if r.setVariables[prepared.SourceSetVariable] == "" { - return nil, fmt.Errorf("prepared source set %q has not been rendered", prepared.SourceSetVariable) - } - item := r.newInternalVariable("prepared_item") - lines := []string{fmt.Sprintf(" LET %s = (", prepared.Variable), fmt.Sprintf(" FOR %s IN %s", item, prepared.SourceSetVariable)} - // Rich consumers may combine a prepared selector with a direct payload - // fallback (slice identity, an unprepared pivot value, or a nested object - // field). Preserve the node-facing fields those consumers already read while - // adding prepared projections; the optimizer can remove this retention only - // after a separate compact-set contract proves it safe. - fields := []string{ - fmt.Sprintf("_key: %s._key", item), - fmt.Sprintf("id: %s.id", item), - fmt.Sprintf("resourceType: %s.resourceType", item), - fmt.Sprintf("payload: %s.payload", item), - fmt.Sprintf("__loom_prepared_node: %s", item), - } - for _, field := range prepared.Fields { - values, err := r.renderSelectorArrayFromSource(item+".payload", field.Selector, false) - if err != nil { - return nil, fmt.Errorf("prepared field %q: %w", field.Name, err) - } - fields = append(fields, field.Name+": "+values) - } - lines = append(lines, " RETURN { "+strings.Join(fields, ", ")+" }", " )") - return lines, nil -} - -func (r *physicalPlanRenderer) renderTraversalTypeFilters(t *PhysicalTraversal, indent string) []string { - if t.TargetTypeBindKey == "" || t.EdgeTargetTypeField == "" { - return nil - } - if _, ok := r.bindVars[t.TargetTypeBindKey].([]string); ok { - return []string{ - fmt.Sprintf("%s FILTER POSITION(@%s, %s.%s)", indent, t.TargetTypeBindKey, t.EdgeVariable, t.EdgeTargetTypeField), - fmt.Sprintf("%s FILTER POSITION(@%s, %s.resourceType)", indent, t.TargetTypeBindKey, t.TargetVariable), - } - } - return []string{fmt.Sprintf("%s FILTER %s.%s == @%s", indent, t.EdgeVariable, t.EdgeTargetTypeField, t.TargetTypeBindKey), fmt.Sprintf("%s FILTER %s.resourceType == @%s", indent, t.TargetVariable, t.TargetTypeBindKey)} -} - -func (r *physicalPlanRenderer) renderSharedSubset(set PhysicalSet) ([]string, error) { - if r.setVariables[set.SourceSetVariable] == "" { - return nil, fmt.Errorf("shared subset source %q has not been rendered", set.SourceSetVariable) - } - lines := []string{fmt.Sprintf(" LET %s = (", set.Variable), fmt.Sprintf(" FOR %s IN %s", set.ItemVariable, set.SourceSetVariable)} - for index, operation := range set.Subplan.Operations { - rendered, err := r.renderScopeOperation(operation, " ") - if err != nil { - return nil, fmt.Errorf("shared subset operation %d: %w", index, err) - } - lines = append(lines, rendered...) - } - value, err := r.renderExpression(set.Subplan.Return) - if err != nil { - return nil, err - } - if set.Projection != nil { - value, err = r.renderPhysicalSetProjection(set.ItemVariable, *set.Projection) - } else { - value, err = renderPhysicalSetOutput(value, set.Output) - } - if err != nil { - return nil, err - } - if set.SortByKey { - lines = append(lines, " SORT "+set.ItemVariable+"._key") - } - lines = append(lines, " RETURN "+value, " )") - if set.Unique { - lines[0] = fmt.Sprintf(" LET %s = UNIQUE((", set.Variable) - lines[len(lines)-1] = " ))" - } - r.setVariables[set.Variable] = set.Variable - if set.Prepared != nil { - prepared, err := r.renderPreparedSet(*set.Prepared) - if err != nil { - return nil, err - } - lines = append(lines, prepared...) - r.setVariables[set.Prepared.Variable] = set.Prepared.Variable - } - return lines, nil -} - -func physicalPlanVariableNames(plan PhysicalPlan) map[string]struct{} { - variables := map[string]struct{}{} - for _, operation := range plan.Operations { - switch operation.Kind { - case PhysicalRootScanOp: - variables[operation.RootScan.Variable] = struct{}{} - case PhysicalTraversalOp: - variables[operation.Traversal.SourceVariable] = struct{}{} - variables[operation.Traversal.TargetVariable] = struct{}{} - if operation.Traversal.EdgeVariable != "" { - variables[operation.Traversal.EdgeVariable] = struct{}{} - } - case PhysicalDerivedLetOp: - variables[operation.DerivedLet.Variable] = struct{}{} - case PhysicalSetOp: - variables[operation.Set.Variable] = struct{}{} - } - } - return variables -} - -func (r *physicalPlanRenderer) newInternalVariable(suffix string) string { - base := "__loom_physical_" + suffix - variable := base - for counter := 1; ; counter++ { - if _, exists := r.reservedVars[variable]; !exists { - r.reservedVars[variable] = struct{}{} - return variable - } - variable = fmt.Sprintf("%s_%d", base, counter) - } -} - -func (r *physicalPlanRenderer) renderPredicate(predicate PhysicalPredicate) (string, error) { - if predicate.LeftExpression != nil { - return r.renderSelectorPredicate(predicate) - } - if strings.ToUpper(strings.TrimSpace(predicate.Operator)) != "EQUALS" { - return "", fmt.Errorf("unsupported physical filter operator %q", predicate.Operator) - } - if predicate.Right == nil { - return "", fmt.Errorf("EQUALS filter requires a right value") - } - left, err := r.renderValue(predicate.Left) - if err != nil { - return "", err - } - right, err := r.renderValue(*predicate.Right) - if err != nil { - return "", err - } - return left + " == " + right, nil -} - -func (r *physicalPlanRenderer) renderSelectorPredicate(predicate PhysicalPredicate) (string, error) { - values, err := r.renderExpression(*predicate.LeftExpression) - if err != nil { - return "", err - } - operator := strings.ToUpper(strings.TrimSpace(predicate.Operator)) - if operator == "EXISTS" { - return "LENGTH(" + values + ") > 0", nil - } - if operator == "MISSING" { - return "LENGTH(" + values + ") == 0", nil - } - if predicate.Right == nil { - return "", fmt.Errorf("physical filter operator %q requires a right value", predicate.Operator) - } - right, err := r.renderValue(*predicate.Right) - if err != nil { - return "", err - } - valueVar := r.newInternalVariable("filter_value") - match := "" - switch operator { - case "EQUALS": - match = valueVar + " == " + right - case "NOT_EQUALS": - match = valueVar + " != " + right - case "IN": - match = "POSITION(" + right + ", " + valueVar + ")" - case "CONTAINS_TEXT": - match = "CONTAINS(TO_STRING(" + valueVar + "), " + right + ")" - case "GT", "GTE", "LT", "LTE": - left, comparisonRight := valueVar, right - if predicate.ValueKind == FilterDate || predicate.ValueKind == FilterDateTime { - left, comparisonRight = "DATE_TIMESTAMP("+valueVar+")", "DATE_TIMESTAMP("+right+")" - } - operatorText := map[string]string{"GT": ">", "GTE": ">=", "LT": "<", "LTE": "<="}[operator] - match = left + " " + operatorText + " " + comparisonRight - default: - return "", fmt.Errorf("unsupported physical selector filter operator %q", predicate.Operator) - } - matching := "LENGTH(FOR " + valueVar + " IN " + values + " FILTER " + match + " LIMIT 1 RETURN 1)" - quantifier := predicate.Quantifier - if quantifier == "" { - quantifier = QuantifierAny - } - switch quantifier { - case QuantifierAny: - return matching + " > 0", nil - case QuantifierNone: - return matching + " == 0", nil - case QuantifierAll: - return "LENGTH(" + values + ") > 0 AND LENGTH(FOR " + valueVar + " IN " + values + " FILTER NOT (" + match + ") LIMIT 1 RETURN 1) == 0", nil - default: - return "", fmt.Errorf("unsupported physical selector filter quantifier %q", quantifier) - } -} - -func (r *physicalPlanRenderer) renderPredicateExpression(predicate PhysicalPredicateExpression, indent string) (string, error) { - switch predicate.Kind { - case PhysicalComparisonPredicate: - return r.renderPredicate(*predicate.Comparison) - case PhysicalAllPredicate, PhysicalAnyPredicate: - parts := make([]string, 0, len(predicate.Children)) - for _, child := range predicate.Children { - part, err := r.renderPredicateExpression(child, indent) - if err != nil { - return "", err - } - parts = append(parts, "("+part+")") - } - join := " AND " - if predicate.Kind == PhysicalAnyPredicate { - join = " OR " - } - return strings.Join(parts, join), nil - case PhysicalNotPredicate: - child, err := r.renderPredicateExpression(predicate.Children[0], indent) - if err != nil { - return "", err - } - return "NOT (" + child + ")", nil - case PhysicalExistsPredicate: - return r.renderExistsSubplan(*predicate.Exists, indent) - default: - return "", fmt.Errorf("unsupported physical predicate kind %q", predicate.Kind) - } -} - -// renderExistsSubplan serializes a validated correlated subplan. EXISTS is -// always bounded: relationship matching is a semi-join, never a row-expanding -// traversal, so the renderer appends LIMIT 1 immediately before RETURN. -func (r *physicalPlanRenderer) renderExistsSubplan(subplan PhysicalSubplan, indent string) (string, error) { - lines := make([]string, 0, len(subplan.Operations)*3+2) - for index, operation := range subplan.Operations { - switch operation.Kind { - case PhysicalTraversalOp: - traversal := operation.Traversal - lines = append(lines, - fmt.Sprintf("%sFOR %s, %s IN 1..1 %s %s @@%s", indent+" ", traversal.TargetVariable, traversal.EdgeVariable, traversal.Direction, traversal.SourceVariable, traversal.EdgeCollectionBindKey), - fmt.Sprintf("%s FILTER %s.label == @%s", indent+" ", traversal.EdgeVariable, traversal.EdgeLabelBindKey), - fmt.Sprintf("%s FILTER %s.%s == @%s", indent+" ", traversal.EdgeVariable, traversal.EdgeTargetTypeField, traversal.TargetTypeBindKey), - fmt.Sprintf("%s FILTER %s.resourceType == @%s", indent+" ", traversal.TargetVariable, traversal.TargetTypeBindKey), - ) - case PhysicalFilterOp, PhysicalDerivedLetOp: - rendered, err := r.renderScopeOperation(operation, indent+" ") - if err != nil { - return "", fmt.Errorf("subplan operation %d (%s): %w", index, operation.Kind, err) - } - lines = append(lines, rendered...) - default: - return "", fmt.Errorf("subplan operation %d has unsupported render kind %q", index, operation.Kind) - } - } - value, err := r.renderExpression(subplan.Return) - if err != nil { - return "", err - } - lines = append(lines, indent+" LIMIT 1", indent+" RETURN "+value) - return "LENGTH((\n" + strings.Join(lines, "\n") + "\n" + indent + " )) > 0", nil -} - -func (r *physicalPlanRenderer) renderExpression(expression PhysicalExpression) (string, error) { - switch expression.Kind { - case PhysicalValueExpression: - return r.renderValue(*expression.Value) - case PhysicalExtractExpression: - return r.renderExtract(expression) - case PhysicalAggregateExpression: - return r.renderAggregate(expression) - case PhysicalPivotExpression: - return r.renderPivot(expression) - case PhysicalSliceExpression: - return r.renderSlice(expression) - case PhysicalObjectExpression: - return r.renderObject(expression) - default: - return "", fmt.Errorf("physical renderer does not yet support expression kind %q", expression.Kind) - } -} - -// renderObject renders a recursively typed object expression. Sorting a copy -// gives equivalent physical plans stable AQL and bind-key allocation without -// changing the semantic field order held by the plan. -// -// The compact dynamic-key literal is used when every field preserves nulls. -// If any field requests OMIT_NULLS, fields are represented as a temporary -// stream and merged so null-valued fields can be removed without evaluating -// their expression twice. -func (r *physicalPlanRenderer) renderObject(expression PhysicalExpression) (string, error) { - object := expression.Object - if object == nil { - return "", fmt.Errorf("OBJECT expression is missing payload") - } - fields := append([]PhysicalExpressionProjection(nil), object.Fields...) - sort.SliceStable(fields, func(left, right int) bool { - return fields[left].Name < fields[right].Name - }) - - type renderedField struct { - nameKey string - value string - omit bool - } - rendered := make([]renderedField, 0, len(fields)) - for index, field := range fields { - value, err := r.renderExpression(field.Expression) - if err != nil { - return "", fmt.Errorf("object field %q: %w", field.Name, err) - } - nameKey := r.newInternalBindKey("object_field_" + strconv.Itoa(index) + "_name") - r.bindVars[nameKey] = field.Name - rendered = append(rendered, renderedField{ - nameKey: nameKey, - value: value, - omit: field.Expression.NullBehavior == PhysicalOmitNulls, - }) - } - - hasOmittedField := false - for _, field := range rendered { - if field.omit { - hasOmittedField = true - break - } - } - if !hasOmittedField { - parts := make([]string, 0, len(rendered)) - for _, field := range rendered { - parts = append(parts, fmt.Sprintf("[@%s]: %s", field.nameKey, field.value)) - } - return "{ " + strings.Join(parts, ", ") + " }", nil - } - - items := make([]string, 0, len(rendered)) - for _, field := range rendered { - items = append(items, fmt.Sprintf("{ __loom_object_name: @%s, __loom_object_value: %s, __loom_object_omit: %t }", field.nameKey, field.value, field.omit)) - } - return fmt.Sprintf(`MERGE( - FOR __loom_object_field IN [%s] - FILTER __loom_object_field.__loom_object_omit == false OR __loom_object_field.__loom_object_value != null - RETURN { [__loom_object_field.__loom_object_name]: __loom_object_field.__loom_object_value } -)`, strings.Join(items, ", ")), nil -} - -// renderSlice emits a correlated, bounded array projection. Sort and the -// _key tie-break are rendered inside the subquery so representative values -// are deterministic even when traversal order changes. -func (r *physicalPlanRenderer) renderSlice(expression PhysicalExpression) (string, error) { - slice := expression.Slice - if slice == nil { - return "", fmt.Errorf("SLICE expression is missing payload") - } - source, err := r.renderValue(slice.Source) - if err != nil { - return "", err - } - items := source - preparedVariable := slicePreparedVariable(slice) - if preparedVariable != "" { - items = preparedVariable - } - setSource := slice.Source.Variable != "" && r.setVariables[slice.Source.Variable] != "" - if !setSource { - items = "[" + source + "]" - } - item := r.newInternalVariable("slice_item") - lines := []string{"(FOR " + item + " IN " + items} - if slice.Predicate != nil { - if slice.Predicate.Kind != PhysicalComparisonPredicate || slice.Predicate.Comparison == nil { - return "", fmt.Errorf("slice predicate must be a comparison") - } - comparison := *slice.Predicate.Comparison - if comparison.LeftExpression != nil && comparison.LeftExpression.Extract != nil { - left := *comparison.LeftExpression - extract := *left.Extract - extract.Source = PhysicalValue{Variable: item, Path: []string{"payload"}} - left.Extract = &extract - comparison.LeftExpression = &left - } else { - comparison.Left = PhysicalValue{Variable: item} - } - previousPreparedItem := r.preparedItem - r.preparedItem = item - predicate, err := r.renderPredicate(comparison) - r.preparedItem = previousPreparedItem - if err != nil { - return "", err - } - lines = append(lines, " FILTER "+predicate) - } - if slice.Sort == nil { - return "", fmt.Errorf("slice requires sort expression") - } - sortExpression := *slice.Sort - if sortExpression.Kind == PhysicalValueExpression && sortExpression.Value != nil { - value := *sortExpression.Value - value.Variable = item - value.BindKey = "" - sortExpression.Value = &value - } - sortValue, err := r.renderExpression(sortExpression) - if err != nil { - return "", err - } - lines = append(lines, " SORT "+sortValue+" ASC, "+item+"._key ASC") - lines = append(lines, " LIMIT @"+slice.LimitBindKey) - fields := make([]string, 0, len(slice.Projections)) - for index, projection := range slice.Projections { - projectionExpression := projection.Expression - if projectionExpression.Kind == PhysicalExtractExpression && projectionExpression.Extract != nil { - extract := *projectionExpression.Extract - extract.Source = PhysicalValue{Variable: item, Path: []string{"payload"}} - projectionExpression.Extract = &extract - } - previousPreparedItem := r.preparedItem - r.preparedItem = item - value, err := r.renderExpression(projectionExpression) - r.preparedItem = previousPreparedItem - if err != nil { - return "", fmt.Errorf("slice projection %d (%s): %w", index, projection.Name, err) - } - nameKey := r.newInternalBindKey("slice_projection_" + strconv.Itoa(index) + "_name") - r.bindVars[nameKey] = projection.Name - fields = append(fields, "["+"@"+nameKey+"]: "+value) - } - lines = append(lines, " RETURN { "+strings.Join(fields, ", ")+" }") - return strings.Join(lines, "\n") + "\n)", nil -} - -func slicePreparedVariable(slice *PhysicalSlice) string { - if slice == nil { - return "" - } - if slice.Predicate != nil && slice.Predicate.Comparison != nil && slice.Predicate.Comparison.LeftExpression != nil && slice.Predicate.Comparison.LeftExpression.Extract != nil && slice.Predicate.Comparison.LeftExpression.Extract.Prepared != nil { - return slice.Predicate.Comparison.LeftExpression.Extract.Prepared.SetVariable - } - for _, projection := range slice.Projections { - if projection.Expression.Extract != nil && projection.Expression.Extract.Prepared != nil { - return projection.Expression.Extract.Prepared.SetVariable - } - } - return "" -} - -// renderPivot emits a bounded sparse object keyed by the requested catalog -// columns. Values from all matching resources are combined per key and reduced -// deterministically to the first sorted value while keeping selectors and -// column values typed. -func (r *physicalPlanRenderer) renderPivot(expression PhysicalExpression) (string, error) { - pivot := expression.Pivot - if pivot == nil { - return "", fmt.Errorf("PIVOT expression is missing payload") - } - if _, collection := r.collectionKeys[pivot.ColumnsBindKey]; collection { - return "", fmt.Errorf("pivot columns bind %q cannot be a collection bind", pivot.ColumnsBindKey) - } - columns, ok := r.bindVars[pivot.ColumnsBindKey].([]string) - if !ok || len(columns) == 0 { - return "", fmt.Errorf("pivot columns bind %q is not a non-empty []string", pivot.ColumnsBindKey) - } - items, err := r.renderValue(pivot.Source) - if err != nil { - return "", err - } - if pivot.Source.Variable == "" || r.setVariables[pivot.Source.Variable] == "" { - items = "[" + items + "]" - } - if pivot.PreparedKey != nil { - items = pivot.PreparedKey.SetVariable - } - item := r.newInternalVariable("pivot_item") - previousPreparedItem := r.preparedItem - r.preparedItem = item - keyExpr, err := r.renderPreparedOrSelector(item, pivot.PreparedKey, pivot.KeySelector) - if err != nil { - r.preparedItem = previousPreparedItem - return "", err - } - valueExpr, err := r.renderPreparedOrSelector(item, pivot.PreparedValue, pivot.ValueSelector) - r.preparedItem = previousPreparedItem - if err != nil { - return "", err - } - return fmt.Sprintf(`MERGE( - FOR __pair IN ( - FOR %s IN %s - LET __pivot_keys = UNIQUE(%s) - LET __pivot_values = %s - FILTER LENGTH(__pivot_values) > 0 - FOR __pivot_key IN __pivot_keys - FILTER POSITION(@%s, __pivot_key) - RETURN { key: __pivot_key, values: __pivot_values } - ) - COLLECT __pivot_key = __pair.key INTO __pivot_group - LET __pivot_flat_values = SORTED_UNIQUE(FLATTEN(__pivot_group[*].__pair.values)) - FILTER LENGTH(__pivot_flat_values) > 0 - RETURN { [__pivot_key]: FIRST(__pivot_flat_values) } -)`, item, items, keyExpr, valueExpr, pivot.ColumnsBindKey), nil -} - -// renderAggregate emits reductions over either a correlated PhysicalSet or a -// singleton root document. The source is kept typed in the IR; this method is -// the only place that decides the AQL collection expression (`set` versus -// `[root]`). -func (r *physicalPlanRenderer) renderAggregate(expression PhysicalExpression) (string, error) { - aggregate := expression.Aggregate - if aggregate == nil { - return "", fmt.Errorf("AGGREGATE expression is missing payload") - } - source, err := r.renderValue(aggregate.Source) - if err != nil { - return "", err - } - items := source - if preparedVariable := aggregatePreparedVariable(aggregate); preparedVariable != "" { - items = preparedVariable - } - if aggregate.Source.Variable == "" || r.setVariables[aggregate.Source.Variable] == "" { - items = "[" + source + "]" - } - perItem := aggregate.Predicate != nil - if perItem { - if aggregate.Predicate.Kind != PhysicalComparisonPredicate || aggregate.Predicate.Comparison == nil { - return "", fmt.Errorf("aggregate predicate must be a comparison") - } - item := r.newInternalVariable("aggregate_item") - comparison := *aggregate.Predicate.Comparison - if comparison.LeftExpression == nil || comparison.LeftExpression.Extract == nil { - return "", fmt.Errorf("aggregate predicate must extract a selector") - } - left := *comparison.LeftExpression - extract := *left.Extract - if extract.Prepared == nil { - extract.Source = PhysicalValue{Variable: item, Path: []string{"payload"}} - } - left.Extract = &extract - comparison.LeftExpression = &left - previousPreparedItem := r.preparedItem - r.preparedItem = item - predicate, err := r.renderPredicate(comparison) - r.preparedItem = previousPreparedItem - if err != nil { - return "", err - } - items = "(FOR " + item + " IN " + items + " FILTER " + predicate + " RETURN " + item + ")" - } - switch aggregate.Operation { - case PhysicalCountAggregate: - return "LENGTH(" + items + ")", nil - case PhysicalExistsAggregate: - if aggregate.Value == nil { - return "LENGTH(" + items + ") > 0", nil - } - values, err := r.renderAggregateValue(*aggregate.Value, items, perItem) - if err != nil { - return "", err - } - return "LENGTH(FOR __value IN FLATTEN(" + values + ") FILTER __value != null LIMIT 1 RETURN 1) > 0", nil - case PhysicalCountDistinctAggregate, PhysicalDistinctValuesAggregate, PhysicalMinAggregate, PhysicalMaxAggregate, PhysicalFirstAggregate: - if aggregate.Value == nil { - return "", fmt.Errorf("aggregate operation %q requires a value expression", aggregate.Operation) - } - values, err := r.renderAggregateValue(*aggregate.Value, items, perItem) - if err != nil { - return "", err - } - flattened := "FLATTEN(" + values + ")" - switch aggregate.Operation { - case PhysicalCountDistinctAggregate: - return "LENGTH(SORTED_UNIQUE(" + flattened + "))", nil - case PhysicalDistinctValuesAggregate: - return "SORTED_UNIQUE(" + flattened + ")", nil - case PhysicalMinAggregate: - return "MIN(" + flattened + ")", nil - case PhysicalMaxAggregate: - return "MAX(" + flattened + ")", nil - case PhysicalFirstAggregate: - return "FIRST(" + flattened + ")", nil - } - } - return "", fmt.Errorf("unsupported aggregate operation %q", aggregate.Operation) -} - -func aggregatePreparedVariable(aggregate *PhysicalAggregate) string { - if aggregate == nil { - return "" - } - if aggregate.Value != nil && aggregate.Value.Extract != nil && aggregate.Value.Extract.Prepared != nil { - return aggregate.Value.Extract.Prepared.SetVariable - } - if aggregate.Predicate != nil && aggregate.Predicate.Comparison != nil && aggregate.Predicate.Comparison.LeftExpression != nil && aggregate.Predicate.Comparison.LeftExpression.Extract != nil && aggregate.Predicate.Comparison.LeftExpression.Extract.Prepared != nil { - return aggregate.Predicate.Comparison.LeftExpression.Extract.Prepared.SetVariable - } - return "" -} - -func (r *physicalPlanRenderer) renderAggregateValue(expression PhysicalExpression, items string, perItem bool) (string, error) { - if !perItem { - if expression.Extract != nil && expression.Extract.Prepared != nil { - return "(FOR __loom_prepared_value IN " + expression.Extract.Prepared.SetVariable + " RETURN __loom_prepared_value." + expression.Extract.Prepared.Field + ")", nil - } - return r.renderExpression(expression) - } - if expression.Kind != PhysicalExtractExpression || expression.Extract == nil { - return "", fmt.Errorf("aggregate predicates require an extract value expression") - } - item := r.newInternalVariable("aggregate_value_item") - clone := expression - extract := *expression.Extract - if extract.Prepared == nil { - extract.Source = PhysicalValue{Variable: item, Path: []string{"payload"}} - } - clone.Extract = &extract - previousPreparedItem := r.preparedItem - r.preparedItem = item - value, err := r.renderExtract(clone) - r.preparedItem = previousPreparedItem - if err != nil { - return "", err - } - return "(FOR " + item + " IN " + items + " RETURN " + value + ")", nil -} - -func (r *physicalPlanRenderer) renderPreparedOrSelector(item string, reference *PhysicalPreparedReference, selector Selector) (string, error) { - if reference != nil { - return item + "." + reference.Field, nil - } - return r.renderSelectorArrayFromSource(item+".payload", selector, false) -} - -func (r *physicalPlanRenderer) renderExtract(expression PhysicalExpression) (string, error) { - extract := expression.Extract - if extract == nil { - return "", fmt.Errorf("EXTRACT expression is missing payload") - } - if extract.Prepared != nil { - value := "" - if r.preparedItem != "" { - value = r.preparedItem + "." + extract.Prepared.Field - } else { - value = "(FOR __loom_prepared_value IN " + extract.Prepared.SetVariable + " RETURN __loom_prepared_value." + extract.Prepared.Field + ")" - } - if expression.Cardinality == PhysicalArrayCardinality { - if extract.Distinct { - return "SORTED_UNIQUE(FLATTEN(" + value + "))", nil - } - return value, nil - } - return "FIRST(FLATTEN(" + value + "))", nil - } - source, err := r.renderValue(extract.Source) - if err != nil { - return "", err - } - if len(extract.Fallbacks) == 0 && extract.Selector.Filter == nil { - switch extract.ExecutionMode { - case PhysicalSelectorDirectScalar: - if expression.Cardinality != PhysicalArrayCardinality { - return compileDirectExpr(source, extract.Selector.Steps), nil - } - case PhysicalSelectorConditionalArray: - values, err := r.renderConditionalSelectorArray(source, extract.Selector) - if err != nil { - return "", err - } - if expression.Cardinality == PhysicalArrayCardinality { - if extract.Distinct { - return "SORTED_UNIQUE(" + values + ")", nil - } - return values, nil - } - return "FIRST(" + values + ")", nil - } - } - arrays := make([]string, 0, 1+len(extract.Fallbacks)) - setSource := extract.Source.Variable != "" && r.setVariables[extract.Source.Variable] != "" - for _, selector := range append([]Selector{extract.Selector}, extract.Fallbacks...) { - array, err := r.renderSelectorArrayFromSource(source, selector, setSource) - if err != nil { - return "", err - } - arrays = append(arrays, array) - } - values := arrays[0] - if len(arrays) > 1 { - values = "FLATTEN([" + strings.Join(arrays, ", ") + "])" - } - if expression.Cardinality == PhysicalArrayCardinality { - if extract.Distinct { - return "SORTED_UNIQUE(" + values + ")", nil - } - return values, nil - } - if !setSource && len(arrays) == 1 && extract.Selector.Filter == nil && selectorHasNoArrays(extract.Selector) { - return compileDirectExpr(source, extract.Selector.Steps), nil - } - return "FIRST(" + values + ")", nil -} - -func (r *physicalPlanRenderer) renderSelectorByMode(source string, selector Selector, mode PhysicalSelectorExecutionMode) (string, error) { - if mode == PhysicalSelectorDirectScalar && selectorHasNoArrays(selector) && selector.Filter == nil { - return "(FOR __loom_value IN [" + compileDirectExpr(source, selector.Steps) + "] FILTER __loom_value != null RETURN __loom_value)", nil - } - if mode == PhysicalSelectorConditionalArray && selectorHasIteratedArray(selector) && selector.Filter == nil { - return r.renderConditionalSelectorArray(source, selector) - } - return r.renderSelectorArrayFromSource(source, selector, false) -} - -func (r *physicalPlanRenderer) renderConditionalSelectorArray(source string, selector Selector) (string, error) { - if len(selector.Steps) == 0 { - return "", fmt.Errorf("selector is required") - } - prefix, last := selector.Steps[:len(selector.Steps)-1], selector.Steps[len(selector.Steps)-1] - lines := make([]string, 0, len(prefix)+3) - current := source - for index, step := range prefix { - next := fmt.Sprintf("__loom_selector_%d", index) - switch { - case step.Iterate: - lines = append(lines, fmt.Sprintf("FOR %s IN (%s.%s ? %s.%s : [])", next, current, step.Field, current, step.Field)) - case step.Index != nil: - lines = append(lines, fmt.Sprintf("LET %s = ((%s.%s ? %s.%s : [])[%d])", next, current, step.Field, current, step.Field, *step.Index), "FILTER "+next+" != null") - default: - lines = append(lines, fmt.Sprintf("LET %s = %s.%s", next, current, step.Field), "FILTER "+next+" != null") - } - current = next - } - lines = append(lines, "LET __value = "+extractFinalExpr(current, last), "FILTER __value != null", "RETURN __value") - return "(\n " + strings.Join(lines, "\n ") + "\n )", nil -} - -func (r *physicalPlanRenderer) renderSelectorArrayFromSource(source string, selector Selector, setSource bool) (string, error) { - if len(selector.Steps) == 0 { - return "", fmt.Errorf("selector is required") - } - prefix, last := selector.Steps[:len(selector.Steps)-1], selector.Steps[len(selector.Steps)-1] - lines, current := []string{"FOR __root IN [" + source + "]"}, "__root" - if setSource { - lines, current = []string{"FOR __item IN " + source, " FOR __root IN [__item.payload]"}, "__root" - } - for index, step := range prefix { - next := fmt.Sprintf("__s%d", index) - switch { - case step.Iterate: - lines = append(lines, fmt.Sprintf(" FOR %s IN (%s.%s ? %s.%s : [])", next, current, step.Field, current, step.Field)) - case step.Index != nil: - lines = append(lines, fmt.Sprintf(" LET %s = ((%s.%s ? %s.%s : [])[%d])", next, current, step.Field, current, step.Field, *step.Index), " FILTER "+next+" != null") - default: - lines = append(lines, fmt.Sprintf(" LET %s = %s.%s", next, current, step.Field), " FILTER "+next+" != null") - } - current = next - } - if selector.Filter != nil { - key := r.newInternalBindKey("selector_contains") - r.bindVars[key] = selector.Filter.Needle - lines = append(lines, fmt.Sprintf(" FILTER CONTAINS(%s.%s ? %s.%s : \"\", @%s)", current, selector.Filter.Field, current, selector.Filter.Field, key)) - } - lines = append(lines, " LET __value = "+extractFinalExpr(current, last), " FILTER __value != null", " RETURN __value") - return "(\n " + strings.Join(lines, "\n ") + "\n )", nil -} - -func (r *physicalPlanRenderer) renderDerivedLet(derived PhysicalDerivedLet) (string, error) { - if strings.ToUpper(strings.TrimSpace(derived.Operator)) != "AUTH_RESOURCE_PATH_ALLOWED" { - return "", fmt.Errorf("unsupported physical derived LET operator %q", derived.Operator) - } - if len(derived.Inputs) < 3 { - return "", fmt.Errorf("AUTH_RESOURCE_PATH_ALLOWED requires one or more scope values plus paths and unrestricted inputs") - } - - paths := derived.Inputs[len(derived.Inputs)-2] - unrestricted := derived.Inputs[len(derived.Inputs)-1] - if paths.BindKey == "" || paths.Variable != "" || unrestricted.BindKey == "" || unrestricted.Variable != "" { - return "", fmt.Errorf("AUTH_RESOURCE_PATH_ALLOWED requires paths and unrestricted bind inputs") - } - pathsExpression, err := r.renderValue(paths) - if err != nil { - return "", err - } - unrestrictedExpression, err := r.renderValue(unrestricted) - if err != nil { - return "", err - } - - scopeChecks := make([]string, 0, len(derived.Inputs)-2) - for _, input := range derived.Inputs[:len(derived.Inputs)-2] { - if input.Variable == "" || input.BindKey != "" { - return "", fmt.Errorf("AUTH_RESOURCE_PATH_ALLOWED scope inputs must be variable paths") - } - scopeValue, err := r.renderValue(input) - if err != nil { - return "", err - } - scopeChecks = append(scopeChecks, scopeValue+" IN "+pathsExpression) - } - - scopeExpression := strings.Join(scopeChecks, " AND ") - if len(scopeChecks) > 1 { - scopeExpression = "(" + scopeExpression + ")" - } - return unrestrictedExpression + " == true OR " + scopeExpression, nil -} - -func (r *physicalPlanRenderer) renderReturn(returnOp PhysicalReturn) (string, error) { - if len(returnOp.Projections) == 0 { - return "{}", nil - } - projections := make([]string, 0, len(returnOp.Projections)) - for index, projection := range returnOp.Projections { - nameBindKey := r.newInternalBindKey(fmt.Sprintf("projection_%d_name", index)) - r.bindVars[nameBindKey] = projection.Name - var value string - var err error - if projection.Expression != nil { - value, err = r.renderExpression(*projection.Expression) - } else { - value, err = r.renderValue(projection.Value) - } - if err != nil { - return "", err - } - projections = append(projections, fmt.Sprintf("[@%s]: %s", nameBindKey, value)) - } - return "{ " + strings.Join(projections, ", ") + " }", nil -} - -func (r *physicalPlanRenderer) renderValue(value PhysicalValue) (string, error) { - if value.BindKey != "" { - if _, collectionBinding := r.collectionKeys[value.BindKey]; collectionBinding { - return "", fmt.Errorf("bind key %q cannot be used as both a collection and scalar bind", value.BindKey) - } - return "@" + value.BindKey, nil - } - if value.Variable == "" { - return "", fmt.Errorf("physical value has no variable or bind key") - } - if len(value.Path) == 0 { - return value.Variable, nil - } - return value.Variable + "." + strings.Join(value.Path, "."), nil -} - -func (r *physicalPlanRenderer) newInternalBindKey(suffix string) string { - base := "__loom_physical_" + suffix - key := base - for counter := 1; ; counter++ { - if _, exists := r.bindVars[key]; !exists { - return key - } - key = fmt.Sprintf("%s_%d", base, counter) - } -} - -func collectionBindKeys(plan PhysicalPlan) (map[string]struct{}, error) { - keys := map[string]struct{}{} - var collectOperations func([]PhysicalOperation, string) error - collectOperations = func(operations []PhysicalOperation, owner string) error { - for index, operation := range operations { - switch operation.Kind { - case PhysicalRootScanOp: - keys[operation.RootScan.CollectionBindKey] = struct{}{} - case PhysicalTraversalOp: - if operation.Traversal.EdgeCollectionBindKey == "" { - return fmt.Errorf("%s operation %d (TRAVERSAL): edge collection bind key is required", owner, index) - } - keys[operation.Traversal.EdgeCollectionBindKey] = struct{}{} - case PhysicalFilterOp: - if operation.Filter.Expression != nil { - if err := collectPredicateCollections(*operation.Filter.Expression, collectOperations, owner); err != nil { - return err - } - } - case PhysicalSetOp: - if err := collectOperations(operation.Set.Subplan.Operations, owner+" SET"); err != nil { - return err - } - } - } - return nil - } - if err := collectOperations(plan.Operations, "render"); err != nil { - return nil, err - } - for key := range keys { - value, ok := plan.BindVars[key] - if !ok { - return nil, fmt.Errorf("collection bind key %q is not defined", key) - } - collection, ok := value.(string) - if !ok || strings.TrimSpace(collection) == "" { - return nil, fmt.Errorf("collection bind key %q must have a non-empty string value", key) - } - } - return keys, nil -} - -func collectPredicateCollections(predicate PhysicalPredicateExpression, collectOperations func([]PhysicalOperation, string) error, owner string) error { - if predicate.Exists != nil { - return collectOperations(predicate.Exists.Operations, owner+" EXISTS") - } - for _, child := range predicate.Children { - if err := collectPredicateCollections(child, collectOperations, owner); err != nil { - return err - } - } - return nil -} - -func validateRenderablePhysicalPlan(plan PhysicalPlan, collectionKeys map[string]struct{}) error { - for index, operation := range plan.Operations { - if err := validateRenderableOperation(operation, collectionKeys); err != nil { - return fmt.Errorf("render operation %d (%s): %w", index, operation.Kind, err) - } - } - return nil -} - -func validateRenderableOperation(operation PhysicalOperation, collectionKeys map[string]struct{}) error { - valueIsCollection := func(value PhysicalValue) error { - if _, isCollection := collectionKeys[value.BindKey]; isCollection { - return fmt.Errorf("bind key %q cannot be used as both a collection and scalar bind", value.BindKey) - } - return nil - } - checkValue := func(value PhysicalValue) error { - if err := valueIsCollection(value); err != nil { - return err - } - return nil - } - - switch operation.Kind { - case PhysicalRootScanOp: - return nil - case PhysicalTraversalOp: - traversal := operation.Traversal - if traversal.EdgeVariable == "" { - return fmt.Errorf("TRAVERSAL requires an edge variable for edge-label and project scope checks") - } - if traversal.EdgeLabelBindKey == "" || traversal.TargetTypeBindKey == "" { - return fmt.Errorf("TRAVERSAL requires edge label and target resource type bind keys") - } - return nil - case PhysicalSetOp: - for index, suboperation := range operation.Set.Subplan.Operations { - if err := validateRenderableOperation(suboperation, collectionKeys); err != nil { - return fmt.Errorf("SET subplan operation %d: %w", index, err) - } - } - return nil - case PhysicalFilterOp: - if operation.Filter.Expression != nil { - return validateRenderablePredicateExpression(*operation.Filter.Expression, collectionKeys) - } - if strings.ToUpper(strings.TrimSpace(operation.Filter.Predicate.Operator)) != "EQUALS" { - return fmt.Errorf("unsupported physical filter operator %q", operation.Filter.Predicate.Operator) - } - if operation.Filter.Predicate.Right == nil { - return fmt.Errorf("EQUALS filter requires a right value") - } - if err := checkValue(operation.Filter.Predicate.Left); err != nil { - return err - } - return checkValue(*operation.Filter.Predicate.Right) - case PhysicalDerivedLetOp: - if strings.ToUpper(strings.TrimSpace(operation.DerivedLet.Operator)) != "AUTH_RESOURCE_PATH_ALLOWED" { - return fmt.Errorf("unsupported physical derived LET operator %q", operation.DerivedLet.Operator) - } - for _, input := range operation.DerivedLet.Inputs { - if err := checkValue(input); err != nil { - return err - } - } - return nil - case PhysicalSortOp: - return checkValue(operation.Sort.Value) - case PhysicalLimitOp: - if _, isCollection := collectionKeys[operation.Limit.BindKey]; isCollection { - return fmt.Errorf("bind key %q cannot be used as both a collection and scalar bind", operation.Limit.BindKey) - } - return nil - case PhysicalReturnOp: - for _, projection := range operation.Return.Projections { - if projection.Expression != nil { - if projection.Expression.Kind != PhysicalValueExpression && projection.Expression.Kind != PhysicalExtractExpression && projection.Expression.Kind != PhysicalAggregateExpression && projection.Expression.Kind != PhysicalPivotExpression && projection.Expression.Kind != PhysicalSliceExpression && projection.Expression.Kind != PhysicalObjectExpression { - return fmt.Errorf("unsupported physical return expression kind %q", projection.Expression.Kind) - } - continue - } - if err := checkValue(projection.Value); err != nil { - return err - } - } - return nil - default: - return fmt.Errorf("unsupported physical operation %q", operation.Kind) - } -} - -func validateRenderablePredicateExpression(predicate PhysicalPredicateExpression, collectionKeys map[string]struct{}) error { - switch predicate.Kind { - case PhysicalExistsPredicate: - if predicate.Exists == nil { - return fmt.Errorf("EXISTS predicate requires a subplan") - } - for index, operation := range predicate.Exists.Operations { - if err := validateRenderableOperation(operation, collectionKeys); err != nil { - return fmt.Errorf("EXISTS subplan operation %d (%s): %w", index, operation.Kind, err) - } - } - if predicate.Exists.Return.Kind != PhysicalValueExpression || predicate.Exists.Return.Value == nil { - return fmt.Errorf("EXISTS subplan return must be a physical value expression") - } - return nil - case PhysicalComparisonPredicate: - if predicate.Comparison == nil { - return fmt.Errorf("comparison predicate requires a comparison") - } - return nil - case PhysicalAllPredicate, PhysicalAnyPredicate, PhysicalNotPredicate: - for _, child := range predicate.Children { - if err := validateRenderablePredicateExpression(child, collectionKeys); err != nil { - return err - } - } - return nil - default: - return fmt.Errorf("unsupported physical predicate kind %q", predicate.Kind) - } -} - -func runtimePhysicalBindVars(bindVars map[string]any, collectionKeys map[string]struct{}) map[string]any { - out := make(map[string]any, len(bindVars)) - for key, value := range bindVars { - if _, collectionBinding := collectionKeys[key]; collectionBinding { - out["@"+key] = clonePhysicalBindValue(value) - continue - } - out[key] = clonePhysicalBindValue(value) - } - return out -} - -func clonePhysicalBindValue(value any) any { - switch typed := value.(type) { - case []string: - return append([]string(nil), typed...) - case []any: - out := make([]any, len(typed)) - for index, item := range typed { - out[index] = clonePhysicalBindValue(item) - } - return out - case map[string]any: - out := make(map[string]any, len(typed)) - for key, item := range typed { - out[key] = clonePhysicalBindValue(item) - } - return out - default: - return value - } -} diff --git a/internal/dataframe/compiler/render/aql/selector.go b/internal/dataframe/compiler/render/aql/selector.go index 6e67bcc..20b6755 100644 --- a/internal/dataframe/compiler/render/aql/selector.go +++ b/internal/dataframe/compiler/render/aql/selector.go @@ -1,19 +1,6 @@ package aql -import ( - "fmt" -) - -func selectorStepText(step SelectorStep) string { - switch { - case step.Iterate: - return step.Field + "[]" - case step.Index != nil: - return fmt.Sprintf("%s[%d]", step.Field, *step.Index) - default: - return step.Field - } -} +import "fmt" // selectorHasNoArrays and the helpers below are shared by the physical // renderer and typed-filter compiler. They deliberately render only validated diff --git a/internal/dataframe/compiler/render/aql/selectors.go b/internal/dataframe/compiler/render/aql/selectors.go new file mode 100644 index 0000000..e74861f --- /dev/null +++ b/internal/dataframe/compiler/render/aql/selectors.go @@ -0,0 +1,229 @@ +package aql + +import ( + "fmt" + "strings" +) + +func (r *physicalPlanRenderer) renderExtract(expression PhysicalExpression) (string, error) { + extract := expression.Extract + if extract == nil { + return "", fmt.Errorf("EXTRACT expression is missing payload") + } + if extract.Prepared != nil { + value := "" + if r.preparedItem != "" { + value = r.preparedItem + "." + extract.Prepared.Field + } else { + value = "(FOR __loom_prepared_value IN " + extract.Prepared.SetVariable + " RETURN __loom_prepared_value." + extract.Prepared.Field + ")" + } + if expression.Cardinality == PhysicalArrayCardinality { + if extract.Distinct { + return "SORTED_UNIQUE(FLATTEN(" + value + "))", nil + } + return value, nil + } + return "FIRST(FLATTEN(" + value + "))", nil + } + source, err := r.renderValue(extract.Source) + if err != nil { + return "", err + } + setSource := extract.Source.Variable != "" && r.setVariables[extract.Source.Variable] != "" + // A set-valued source is a collection of resource documents. Its + // PhysicalValue may carry the document payload path from semantic lowering, + // but that path cannot be applied to the collection itself (`set.payload` + // is null in AQL). The selector renderer already projects each set item + // through `item.payload`, so pass the collection variable as the loop source. + if setSource { + source = extract.Source.Variable + } + if len(extract.Fallbacks) == 0 && extract.Selector.Filter == nil { + switch extract.ExecutionMode { + case PhysicalSelectorDirectScalar: + if !setSource && expression.Cardinality != PhysicalArrayCardinality { + return compileDirectExpr(source, extract.Selector.Steps), nil + } + case PhysicalSelectorConditionalArray: + if setSource { + break + } + values, err := r.renderConditionalSelectorArray(source, extract.Selector) + if err != nil { + return "", err + } + if expression.Cardinality == PhysicalArrayCardinality { + if extract.Distinct { + return "SORTED_UNIQUE(" + values + ")", nil + } + return values, nil + } + return "FIRST(" + values + ")", nil + } + } + arrays := make([]string, 0, 1+len(extract.Fallbacks)) + for _, selector := range append([]Selector{extract.Selector}, extract.Fallbacks...) { + array, err := r.renderSelectorArrayFromSource(source, selector, setSource) + if err != nil { + return "", err + } + arrays = append(arrays, array) + } + values := arrays[0] + if len(arrays) > 1 { + values = "FLATTEN([" + strings.Join(arrays, ", ") + "])" + } + if expression.Cardinality == PhysicalArrayCardinality { + if extract.Distinct { + return "SORTED_UNIQUE(" + values + ")", nil + } + return values, nil + } + if !setSource && len(arrays) == 1 && extract.Selector.Filter == nil && selectorHasNoArrays(extract.Selector) { + return compileDirectExpr(source, extract.Selector.Steps), nil + } + return "FIRST(" + values + ")", nil +} + +func (r *physicalPlanRenderer) renderSelectorByMode(source string, selector Selector, mode PhysicalSelectorExecutionMode) (string, error) { + if mode == PhysicalSelectorDirectScalar && selectorHasNoArrays(selector) && selector.Filter == nil { + return "(FOR __loom_value IN [" + compileDirectExpr(source, selector.Steps) + "] FILTER __loom_value != null RETURN __loom_value)", nil + } + if mode == PhysicalSelectorConditionalArray && selectorHasIteratedArray(selector) && selector.Filter == nil { + return r.renderConditionalSelectorArray(source, selector) + } + return r.renderSelectorArrayFromSource(source, selector, false) +} + +func (r *physicalPlanRenderer) renderConditionalSelectorArray(source string, selector Selector) (string, error) { + if len(selector.Steps) == 0 { + return "", fmt.Errorf("selector is required") + } + prefix, last := selector.Steps[:len(selector.Steps)-1], selector.Steps[len(selector.Steps)-1] + lines := make([]string, 0, len(prefix)+3) + current := source + for index, step := range prefix { + next := fmt.Sprintf("__loom_selector_%d", index) + switch { + case step.Iterate: + lines = append(lines, fmt.Sprintf("FOR %s IN (%s.%s ? %s.%s : [])", next, current, step.Field, current, step.Field)) + case step.Index != nil: + lines = append(lines, fmt.Sprintf("LET %s = ((%s.%s ? %s.%s : [])[%d])", next, current, step.Field, current, step.Field, *step.Index), "FILTER "+next+" != null") + default: + lines = append(lines, fmt.Sprintf("LET %s = %s.%s", next, current, step.Field), "FILTER "+next+" != null") + } + current = next + } + lines = append(lines, "LET __value = "+extractFinalExpr(current, last), "FILTER __value != null", "RETURN __value") + return "(\n " + strings.Join(lines, "\n ") + "\n )", nil +} + +func (r *physicalPlanRenderer) renderSelectorArrayFromSource(source string, selector Selector, setSource bool) (string, error) { + if len(selector.Steps) == 0 { + return "", fmt.Errorf("selector is required") + } + prefix, last := selector.Steps[:len(selector.Steps)-1], selector.Steps[len(selector.Steps)-1] + lines, current := []string{"FOR __root IN [" + source + "]"}, "__root" + if setSource { + lines, current = []string{"FOR __item IN " + source, " FOR __root IN [__item.payload]"}, "__root" + } + for index, step := range prefix { + next := fmt.Sprintf("__s%d", index) + switch { + case step.Iterate: + lines = append(lines, fmt.Sprintf(" FOR %s IN (%s.%s ? %s.%s : [])", next, current, step.Field, current, step.Field)) + case step.Index != nil: + lines = append(lines, fmt.Sprintf(" LET %s = ((%s.%s ? %s.%s : [])[%d])", next, current, step.Field, current, step.Field, *step.Index), " FILTER "+next+" != null") + default: + lines = append(lines, fmt.Sprintf(" LET %s = %s.%s", next, current, step.Field), " FILTER "+next+" != null") + } + current = next + } + if selector.Filter != nil { + key := r.newInternalBindKey("selector_contains") + r.bindVars[key] = selector.Filter.Needle + lines = append(lines, fmt.Sprintf(" FILTER CONTAINS(%s.%s ? %s.%s : \"\", @%s)", current, selector.Filter.Field, current, selector.Filter.Field, key)) + } + lines = append(lines, " LET __value = "+extractFinalExpr(current, last), " FILTER __value != null", " RETURN __value") + return "(\n " + strings.Join(lines, "\n ") + "\n )", nil +} + +func (r *physicalPlanRenderer) renderDerivedLet(derived PhysicalDerivedLet) (string, error) { + if strings.ToUpper(strings.TrimSpace(derived.Operator)) != "AUTH_RESOURCE_PATH_ALLOWED" { + return "", fmt.Errorf("unsupported physical derived LET operator %q", derived.Operator) + } + if len(derived.Inputs) < 3 { + return "", fmt.Errorf("AUTH_RESOURCE_PATH_ALLOWED requires one or more scope values plus paths and unrestricted inputs") + } + + paths := derived.Inputs[len(derived.Inputs)-2] + unrestricted := derived.Inputs[len(derived.Inputs)-1] + if paths.BindKey == "" || paths.Variable != "" || unrestricted.BindKey == "" || unrestricted.Variable != "" { + return "", fmt.Errorf("AUTH_RESOURCE_PATH_ALLOWED requires paths and unrestricted bind inputs") + } + pathsExpression, err := r.renderValue(paths) + if err != nil { + return "", err + } + unrestrictedExpression, err := r.renderValue(unrestricted) + if err != nil { + return "", err + } + + scopeChecks := make([]string, 0, len(derived.Inputs)-2) + for _, input := range derived.Inputs[:len(derived.Inputs)-2] { + if input.Variable == "" || input.BindKey != "" { + return "", fmt.Errorf("AUTH_RESOURCE_PATH_ALLOWED scope inputs must be variable paths") + } + scopeValue, err := r.renderValue(input) + if err != nil { + return "", err + } + scopeChecks = append(scopeChecks, scopeValue+" IN "+pathsExpression) + } + + scopeExpression := strings.Join(scopeChecks, " AND ") + if len(scopeChecks) > 1 { + scopeExpression = "(" + scopeExpression + ")" + } + return unrestrictedExpression + " == true OR " + scopeExpression, nil +} + +func (r *physicalPlanRenderer) renderReturn(returnOp PhysicalReturn) (string, error) { + if len(returnOp.Projections) == 0 { + return "{}", nil + } + projections := make([]string, 0, len(returnOp.Projections)) + for index, projection := range returnOp.Projections { + nameBindKey := r.newInternalBindKey(fmt.Sprintf("projection_%d_name", index)) + r.bindVars[nameBindKey] = projection.Name + var value string + var err error + if projection.Expression != nil { + value, err = r.renderExpression(*projection.Expression) + } else { + value, err = r.renderValue(projection.Value) + } + if err != nil { + return "", err + } + projections = append(projections, fmt.Sprintf("[@%s]: %s", nameBindKey, value)) + } + return "{ " + strings.Join(projections, ", ") + " }", nil +} + +func (r *physicalPlanRenderer) renderValue(value PhysicalValue) (string, error) { + if value.BindKey != "" { + if _, collectionBinding := r.collectionKeys[value.BindKey]; collectionBinding { + return "", fmt.Errorf("bind key %q cannot be used as both a collection and scalar bind", value.BindKey) + } + return "@" + value.BindKey, nil + } + if value.Variable == "" { + return "", fmt.Errorf("physical value has no variable or bind key") + } + if len(value.Path) == 0 { + return value.Variable, nil + } + return value.Variable + "." + strings.Join(value.Path, "."), nil +} diff --git a/internal/dataframe/compiler/render/aql/unnest_test.go b/internal/dataframe/compiler/render/aql/unnest_test.go new file mode 100644 index 0000000..4b34c4c --- /dev/null +++ b/internal/dataframe/compiler/render/aql/unnest_test.go @@ -0,0 +1,96 @@ +package aql_test + +import ( + "strings" + "testing" + + "github.com/calypr/loom/internal/dataframe/compiler" + "github.com/calypr/loom/internal/dataframe/compiler/ir" + aql "github.com/calypr/loom/internal/dataframe/compiler/render/aql" +) + +func TestRenderPhysicalPlanInnerUnnestUsesCanonicalCorrelatedLoop(t *testing.T) { + plan := genericUnnestPlan(t, ir.PhysicalUnnestInner, "") + rendered, err := aql.RenderPhysicalPlan(plan) + if err != nil { + t.Fatalf("RenderPhysicalPlan() error = %v", err) + } + for _, want := range []string{ + "LET __loom_physical_unnest_source_0 = (root.payload.identifier == null ? [] : FLATTEN(root.payload.identifier))", + "FOR item IN __loom_physical_unnest_source_0", + "RETURN { [@__loom_physical_projection_0_name]: item }", + } { + if !strings.Contains(rendered.Query, want) { + t.Fatalf("inner unnest query missing %q:\n%s", want, rendered.Query) + } + } + if strings.Contains(rendered.Query, "[null] : RANGE") { + t.Fatalf("INNER unnest emitted an OUTER sentinel:\n%s", rendered.Query) + } +} + +func TestRenderPhysicalPlanOuterUnnestPreservesEmptyParentAndOrdinality(t *testing.T) { + plan := genericUnnestPlan(t, ir.PhysicalUnnestOuter, "item_index") + rendered, err := aql.RenderPhysicalPlan(plan) + if err != nil { + t.Fatalf("RenderPhysicalPlan() error = %v", err) + } + for _, want := range []string{ + "FOR __loom_physical_unnest_index_0 IN (LENGTH(__loom_physical_unnest_source_0) == 0 ? [null] : RANGE(0, LENGTH(__loom_physical_unnest_source_0) - 1))", + "LET item = __loom_physical_unnest_index_0 == null ? null : __loom_physical_unnest_source_0[__loom_physical_unnest_index_0]", + "LET item_index = __loom_physical_unnest_index_0", + } { + if !strings.Contains(rendered.Query, want) { + t.Fatalf("outer unnest query missing %q:\n%s", want, rendered.Query) + } + } +} + +func TestRenderPhysicalPlanRejectsUnnestAfterRootWindow(t *testing.T) { + plan := genericUnnestPlan(t, ir.PhysicalUnnestInner, "") + last := len(plan.Operations) - 1 + window := ir.PhysicalOperation{ + Kind: ir.PhysicalSortOp, + Sort: &ir.PhysicalSort{Value: ir.PhysicalValue{Variable: "root", Path: []string{"_key"}}}, + } + unnest := plan.Operations[last-1] + plan.Operations = append(append(append([]ir.PhysicalOperation{}, plan.Operations[:last-1]...), window, unnest), plan.Operations[last]) + if _, err := aql.RenderPhysicalPlan(plan); err == nil || !strings.Contains(err.Error(), "unnest") { + t.Fatalf("RenderPhysicalPlan() error = %v, want unnest ordering failure", err) + } +} + +func genericUnnestPlan(t *testing.T, mode ir.PhysicalUnnestJoinMode, ordinality string) compiler.PhysicalPlan { + t.Helper() + plan, err := compiler.BuildGenericPhysicalPlan(compiler.SemanticPlan{ + Version: 1, + Project: "project-1", + AuthResourcePaths: []string{"/programs/p1"}, + Root: compiler.SemanticNode{Alias: "root", ResourceType: "Patient"}, + }) + if err != nil { + t.Fatal(err) + } + last := len(plan.Operations) - 1 + plan.Operations[last].Return.Projections = []ir.PhysicalProjection{{ + Name: "item", + Value: ir.PhysicalValue{Variable: "item"}, + }} + unnest := ir.PhysicalOperation{ + Kind: ir.PhysicalUnnestOp, + Unnest: &ir.PhysicalUnnest{ + InputVariable: "root", + OutputVariable: "item", + Ordinality: ordinality, + Expression: ir.PhysicalExpression{ + Kind: ir.PhysicalValueExpression, + Cardinality: ir.PhysicalArrayCardinality, + NullBehavior: ir.PhysicalEmptyOnNull, + Value: &ir.PhysicalValue{Variable: "root", Path: []string{"payload", "identifier"}}, + }, + JoinMode: mode, + }, + } + plan.Operations = append(append(append([]ir.PhysicalOperation{}, plan.Operations[:last]...), unnest), plan.Operations[last]) + return plan +} diff --git a/internal/dataframe/compiler/render/aql/validation.go b/internal/dataframe/compiler/render/aql/validation.go new file mode 100644 index 0000000..b138e6c --- /dev/null +++ b/internal/dataframe/compiler/render/aql/validation.go @@ -0,0 +1,241 @@ +package aql + +import ( + "fmt" + "strings" +) + +func (r *physicalPlanRenderer) newInternalBindKey(suffix string) string { + base := "__loom_physical_" + suffix + key := base + for counter := 1; ; counter++ { + if _, exists := r.bindVars[key]; !exists { + return key + } + key = fmt.Sprintf("%s_%d", base, counter) + } +} + +func collectionBindKeys(plan PhysicalPlan) (map[string]struct{}, error) { + keys := map[string]struct{}{} + var collectOperations func([]PhysicalOperation, string) error + collectOperations = func(operations []PhysicalOperation, owner string) error { + for index, operation := range operations { + switch operation.Kind { + case PhysicalRootScanOp: + keys[operation.RootScan.CollectionBindKey] = struct{}{} + case PhysicalTraversalOp: + if operation.Traversal.EdgeCollectionBindKey == "" { + return fmt.Errorf("%s operation %d (TRAVERSAL): edge collection bind key is required", owner, index) + } + keys[operation.Traversal.EdgeCollectionBindKey] = struct{}{} + case PhysicalFilterOp: + if operation.Filter.Expression != nil { + if err := collectPredicateCollections(*operation.Filter.Expression, collectOperations, owner); err != nil { + return err + } + } + case PhysicalSetOp: + if err := collectOperations(operation.Set.Subplan.Operations, owner+" SET"); err != nil { + return err + } + } + } + return nil + } + if err := collectOperations(plan.Operations, "render"); err != nil { + return nil, err + } + for key := range keys { + value, ok := plan.BindVars[key] + if !ok { + return nil, fmt.Errorf("collection bind key %q is not defined", key) + } + collection, ok := value.(string) + if !ok || strings.TrimSpace(collection) == "" { + return nil, fmt.Errorf("collection bind key %q must have a non-empty string value", key) + } + } + return keys, nil +} + +func collectPredicateCollections(predicate PhysicalPredicateExpression, collectOperations func([]PhysicalOperation, string) error, owner string) error { + if predicate.Exists != nil { + return collectOperations(predicate.Exists.Operations, owner+" EXISTS") + } + for _, child := range predicate.Children { + if err := collectPredicateCollections(child, collectOperations, owner); err != nil { + return err + } + } + return nil +} + +func validateRenderablePhysicalPlan(plan PhysicalPlan, collectionKeys map[string]struct{}) error { + for index, operation := range plan.Operations { + if err := validateRenderableOperation(operation, collectionKeys); err != nil { + return fmt.Errorf("render operation %d (%s): %w", index, operation.Kind, err) + } + } + return nil +} + +func validateRenderableOperation(operation PhysicalOperation, collectionKeys map[string]struct{}) error { + valueIsCollection := func(value PhysicalValue) error { + if _, isCollection := collectionKeys[value.BindKey]; isCollection { + return fmt.Errorf("bind key %q cannot be used as both a collection and scalar bind", value.BindKey) + } + return nil + } + checkValue := func(value PhysicalValue) error { + if err := valueIsCollection(value); err != nil { + return err + } + return nil + } + + switch operation.Kind { + case PhysicalRootScanOp: + return nil + case PhysicalTraversalOp: + traversal := operation.Traversal + if traversal.EdgeVariable == "" { + return fmt.Errorf("TRAVERSAL requires an edge variable for edge-label and project scope checks") + } + if traversal.EdgeLabelBindKey == "" || traversal.TargetTypeBindKey == "" { + return fmt.Errorf("TRAVERSAL requires edge label and target resource type bind keys") + } + return nil + case PhysicalSetOp: + for index, suboperation := range operation.Set.Subplan.Operations { + if err := validateRenderableOperation(suboperation, collectionKeys); err != nil { + return fmt.Errorf("SET subplan operation %d: %w", index, err) + } + } + return nil + case PhysicalUnnestOp: + if operation.Unnest == nil { + return fmt.Errorf("UNNEST requires a payload") + } + if operation.Unnest.Expression.Cardinality != PhysicalArrayCardinality { + return fmt.Errorf("UNNEST source expression must be array-valued") + } + return nil + case PhysicalFilterOp: + if operation.Filter.Expression != nil { + return validateRenderablePredicateExpression(*operation.Filter.Expression, collectionKeys) + } + if strings.ToUpper(strings.TrimSpace(operation.Filter.Predicate.Operator)) != "EQUALS" { + return fmt.Errorf("unsupported physical filter operator %q", operation.Filter.Predicate.Operator) + } + if operation.Filter.Predicate.Right == nil { + return fmt.Errorf("EQUALS filter requires a right value") + } + if err := checkValue(operation.Filter.Predicate.Left); err != nil { + return err + } + return checkValue(*operation.Filter.Predicate.Right) + case PhysicalDerivedLetOp: + if strings.ToUpper(strings.TrimSpace(operation.DerivedLet.Operator)) != "AUTH_RESOURCE_PATH_ALLOWED" { + return fmt.Errorf("unsupported physical derived LET operator %q", operation.DerivedLet.Operator) + } + for _, input := range operation.DerivedLet.Inputs { + if err := checkValue(input); err != nil { + return err + } + } + return nil + case PhysicalExpressionLetOp: + if operation.ExpressionLet == nil { + return fmt.Errorf("expression LET requires a payload") + } + return nil + case PhysicalSortOp: + return checkValue(operation.Sort.Value) + case PhysicalLimitOp: + if _, isCollection := collectionKeys[operation.Limit.BindKey]; isCollection { + return fmt.Errorf("bind key %q cannot be used as both a collection and scalar bind", operation.Limit.BindKey) + } + return nil + case PhysicalReturnOp: + for _, projection := range operation.Return.Projections { + if projection.Expression != nil { + if projection.Expression.Kind != PhysicalValueExpression && projection.Expression.Kind != PhysicalExtractExpression && projection.Expression.Kind != PhysicalAggregateExpression && projection.Expression.Kind != PhysicalPivotExpression && projection.Expression.Kind != PhysicalSliceExpression && projection.Expression.Kind != PhysicalLookupExpression && projection.Expression.Kind != PhysicalObjectLookupExpression && projection.Expression.Kind != PhysicalKeyedMapExpression && projection.Expression.Kind != PhysicalObjectKeysExpression && projection.Expression.Kind != PhysicalKeySetExpression && projection.Expression.Kind != PhysicalObjectExpression && projection.Expression.Kind != PhysicalCallExpression { + return fmt.Errorf("unsupported physical return expression kind %q", projection.Expression.Kind) + } + continue + } + if err := checkValue(projection.Value); err != nil { + return err + } + } + return nil + default: + return fmt.Errorf("unsupported physical operation %q", operation.Kind) + } +} + +func validateRenderablePredicateExpression(predicate PhysicalPredicateExpression, collectionKeys map[string]struct{}) error { + switch predicate.Kind { + case PhysicalExistsPredicate: + if predicate.Exists == nil { + return fmt.Errorf("EXISTS predicate requires a subplan") + } + for index, operation := range predicate.Exists.Operations { + if err := validateRenderableOperation(operation, collectionKeys); err != nil { + return fmt.Errorf("EXISTS subplan operation %d (%s): %w", index, operation.Kind, err) + } + } + if predicate.Exists.Return.Kind != PhysicalValueExpression || predicate.Exists.Return.Value == nil { + return fmt.Errorf("EXISTS subplan return must be a physical value expression") + } + return nil + case PhysicalComparisonPredicate: + if predicate.Comparison == nil { + return fmt.Errorf("comparison predicate requires a comparison") + } + return nil + case PhysicalAllPredicate, PhysicalAnyPredicate, PhysicalNotPredicate: + for _, child := range predicate.Children { + if err := validateRenderablePredicateExpression(child, collectionKeys); err != nil { + return err + } + } + return nil + default: + return fmt.Errorf("unsupported physical predicate kind %q", predicate.Kind) + } +} + +func runtimePhysicalBindVars(bindVars map[string]any, collectionKeys map[string]struct{}) map[string]any { + out := make(map[string]any, len(bindVars)) + for key, value := range bindVars { + if _, collectionBinding := collectionKeys[key]; collectionBinding { + out["@"+key] = clonePhysicalBindValue(value) + continue + } + out[key] = clonePhysicalBindValue(value) + } + return out +} + +func clonePhysicalBindValue(value any) any { + switch typed := value.(type) { + case []string: + return append([]string(nil), typed...) + case []any: + out := make([]any, len(typed)) + for index, item := range typed { + out[index] = clonePhysicalBindValue(item) + } + return out + case map[string]any: + out := make(map[string]any, len(typed)) + for key, item := range typed { + out[key] = clonePhysicalBindValue(item) + } + return out + default: + return value + } +} diff --git a/internal/dataframe/compiler/render/aql/values.go b/internal/dataframe/compiler/render/aql/values.go new file mode 100644 index 0000000..bbb8bf9 --- /dev/null +++ b/internal/dataframe/compiler/render/aql/values.go @@ -0,0 +1,176 @@ +package aql + +import ( + "fmt" + "strings" +) + +func (r *physicalPlanRenderer) renderExpression(expression PhysicalExpression) (string, error) { + switch expression.Kind { + case PhysicalValueExpression: + return r.renderValue(*expression.Value) + case PhysicalLiteralExpression: + if expression.Literal == nil { + return "", fmt.Errorf("LITERAL expression is missing payload") + } + return r.renderLiteral(*expression.Literal) + case PhysicalExtractExpression: + return r.renderExtract(expression) + case PhysicalAggregateExpression: + return r.renderAggregate(expression) + case PhysicalPivotExpression: + return r.renderPivot(expression) + case PhysicalSliceExpression: + return r.renderSlice(expression) + case PhysicalLookupExpression: + return r.renderLookup(expression) + case PhysicalObjectLookupExpression: + if expression.ObjectLookup == nil { + return "", fmt.Errorf("OBJECT_LOOKUP expression is missing payload") + } + if _, ok := r.bindVars[expression.ObjectLookup.KeyBindKey]; !ok { + return "", fmt.Errorf("object lookup bind %q is not defined", expression.ObjectLookup.KeyBindKey) + } + return fmt.Sprintf("%s[@%s]", expression.ObjectLookup.ObjectVariable, expression.ObjectLookup.KeyBindKey), nil + case PhysicalKeyedMapExpression: + return r.renderKeyedMap(expression) + case PhysicalObjectKeysExpression: + if expression.ObjectKeys == nil { + return "", fmt.Errorf("OBJECT_KEYS expression is missing payload") + } + return fmt.Sprintf("SORTED_UNIQUE(ATTRIBUTES(%s, true))", expression.ObjectKeys.ObjectVariable), nil + case PhysicalKeySetExpression: + return r.renderKeySet(expression) + case PhysicalObjectExpression: + return r.renderObject(expression) + case PhysicalCallExpression: + return r.renderCall(expression) + default: + return "", fmt.Errorf("physical renderer does not yet support expression kind %q", expression.Kind) + } +} + +func (r *physicalPlanRenderer) renderKeyedMap(expression PhysicalExpression) (string, error) { + keyed := expression.KeyedMap + if keyed == nil { + return "", fmt.Errorf("KEYED_MAP expression is missing payload") + } + source, err := r.renderExpression(keyed.Source) + if err != nil { + return "", fmt.Errorf("keyed map source: %w", err) + } + item := keyed.ItemVariable + previous := r.preparedItem + r.preparedItem = item + key, err := r.renderExpression(keyed.ItemKey) + if err != nil { + r.preparedItem = previous + return "", fmt.Errorf("keyed map key: %w", err) + } + valueExpressions := make([]string, 0, 1+len(keyed.ValueFallbacks)) + value, err := r.renderExpression(keyed.ItemValue) + if err != nil { + r.preparedItem = previous + return "", fmt.Errorf("keyed map value: %w", err) + } + valueExpressions = append(valueExpressions, value) + for _, fallback := range keyed.ValueFallbacks { + fallbackValue, fallbackErr := r.renderExpression(fallback) + if fallbackErr != nil { + r.preparedItem = previous + return "", fmt.Errorf("keyed map fallback: %w", fallbackErr) + } + valueExpressions = append(valueExpressions, fallbackValue) + } + r.preparedItem = previous + valueExpression := valueExpressions[0] + if len(valueExpressions) > 1 { + valueExpression = "FIRST(FOR __loom_keyed_candidate IN [" + strings.Join(valueExpressions, ", ") + "] FILTER __loom_keyed_candidate != null RETURN __loom_keyed_candidate)" + } + // Selector extraction represents an iterated field as a one-element + // subquery whose result can itself be an array. Flatten exactly that outer + // wrapper so a keyed family iterates the source items, not one array value. + sourceLoop := source + if keyed.FlattenSource { + sourceLoop = "FLATTEN([" + source + "])" + } + values := "FIRST(__loom_keyed_group[*].__loom_keyed_value)" + if keyed.Reduction == PhysicalMapFirstSorted { + values = "FIRST(SORTED_UNIQUE(__loom_keyed_group[*].__loom_keyed_value))" + } + return fmt.Sprintf(`MERGE( + FOR %s IN %s + LET __loom_keyed_key = %s + LET __loom_keyed_value = %s + FILTER __loom_keyed_key != null + FILTER __loom_keyed_value != null + COLLECT __loom_keyed_group_key = __loom_keyed_key INTO __loom_keyed_group + LET __loom_keyed_values = %s + RETURN { [__loom_keyed_group_key]: __loom_keyed_values } +)`, item, sourceLoop, key, valueExpression, values), nil +} + +// renderLookup emits the sole canonical AQL lowering for a bounded dynamic +// key/value projection. AQL's FOR-over-null behavior supplies the empty +// source semantics; FIRST preserves the historical scalar-column contract +// when a key is absent or appears more than once. +func (r *physicalPlanRenderer) renderLookup(expression PhysicalExpression) (string, error) { + lookup := expression.Lookup + if lookup == nil { + return "", fmt.Errorf("LOOKUP expression is missing payload") + } + source, err := r.renderExpression(lookup.Source) + if err != nil { + return "", fmt.Errorf("lookup source: %w", err) + } + key, err := r.renderExpression(lookup.ItemKey) + if err != nil { + return "", fmt.Errorf("lookup item key: %w", err) + } + value, err := r.renderExpression(lookup.ItemValue) + if err != nil { + return "", fmt.Errorf("lookup item value: %w", err) + } + if lookup.MatchBindKey == "" { + return "", fmt.Errorf("lookup match bind key is required") + } + if _, ok := r.bindVars[lookup.MatchBindKey]; !ok { + return "", fmt.Errorf("lookup match bind key %q is not defined", lookup.MatchBindKey) + } + // AQL FOR over a null array produces no rows, which is the canonical + // missing-key/null projection behavior. Keep the source expression single + // evaluated per lookup instead of duplicating it in a null ternary. + return fmt.Sprintf("FIRST(FOR %s IN %s FILTER %s == @%s RETURN %s)", lookup.ItemVariable, source, key, lookup.MatchBindKey, value), nil +} + +func (r *physicalPlanRenderer) renderKeySet(expression PhysicalExpression) (string, error) { + keySet := expression.KeySet + if keySet == nil { + return "", fmt.Errorf("KEY_SET expression is missing payload") + } + source, err := r.renderExpression(keySet.Source) + if err != nil { + return "", fmt.Errorf("key set source: %w", err) + } + previousPreparedItem := r.preparedItem + r.preparedItem = keySet.ItemVariable + key, err := r.renderExpression(keySet.ItemKey) + r.preparedItem = previousPreparedItem + if err != nil { + return "", fmt.Errorf("key set item key: %w", err) + } + return fmt.Sprintf("SORTED_UNIQUE(FLATTEN(FOR %s IN %s RETURN %s))", keySet.ItemVariable, source, key), nil +} + +func (r *physicalPlanRenderer) renderLiteral(literal PhysicalLiteral) (string, error) { + if literal.BindKey == "" { + return "", fmt.Errorf("literal bind key is required") + } + if _, collection := r.collectionKeys[literal.BindKey]; collection { + return "", fmt.Errorf("literal bind key %q cannot be a collection bind", literal.BindKey) + } + if _, ok := r.bindVars[literal.BindKey]; !ok { + return "", fmt.Errorf("literal bind key %q is not defined", literal.BindKey) + } + return "@" + literal.BindKey, nil +} diff --git a/internal/dataframe/compiler/render_test_compat.go b/internal/dataframe/compiler/render_test_compat.go deleted file mode 100644 index 082362e..0000000 --- a/internal/dataframe/compiler/render_test_compat.go +++ /dev/null @@ -1,7 +0,0 @@ -package compiler - -import aql "github.com/calypr/loom/internal/dataframe/compiler/render/aql" - -func pruneUnusedRuntimeBindVars(bindVars map[string]any, query string) map[string]any { - return aql.PruneUnusedRuntimeBindVars(bindVars, query) -} diff --git a/internal/dataframe/compiler/selector_helpers.go b/internal/dataframe/compiler/selector_helpers.go deleted file mode 100644 index 4a68398..0000000 --- a/internal/dataframe/compiler/selector_helpers.go +++ /dev/null @@ -1,57 +0,0 @@ -package compiler - -import ( - "fmt" - "strings" - - "github.com/calypr/loom/fhirschema" -) - -func selectorStepText(step SelectorStep) string { - switch { - case step.Iterate: - return step.Field + "[]" - case step.Index != nil: - return fmt.Sprintf("%s[%d]", step.Field, *step.Index) - default: - return step.Field - } -} - -func sanitizeColumnName(in string) string { - var b strings.Builder - for _, r := range in { - switch { - case r >= 'a' && r <= 'z', r >= 'A' && r <= 'Z', r >= '0' && r <= '9': - b.WriteRune(r) - default: - b.WriteRune('_') - } - } - return b.String() -} - -func selectorSpecFromSelector(sel Selector) fhirschema.FieldSelectorSpec { - sourcePath := "" - valuePath := "" - if len(sel.Steps) > 0 { - last := len(sel.Steps) - 1 - valuePath = selectorStepText(sel.Steps[last]) - if last > 0 { - parts := make([]string, 0, last) - for _, step := range sel.Steps[:last] { - parts = append(parts, selectorStepText(step)) - } - sourcePath = strings.Join(parts, ".") - } - } - var where *fhirschema.FieldPredicateSpec - if sel.Filter != nil { - where = &fhirschema.FieldPredicateSpec{ - Path: sel.Filter.Field, - Op: fhirschema.PredicateContains, - Value: sel.Filter.Needle, - } - } - return fhirschema.FieldSelectorSpec{SourcePath: sourcePath, Where: where, ValuePath: valuePath} -} diff --git a/internal/dataframe/compiler/selector_identity_compat.go b/internal/dataframe/compiler/selector_identity_compat.go deleted file mode 100644 index dca043a..0000000 --- a/internal/dataframe/compiler/selector_identity_compat.go +++ /dev/null @@ -1,24 +0,0 @@ -package compiler - -import ( - "fmt" - "strings" -) - -func physicalSelectorIdentity(selector Selector) string { - var b strings.Builder - for _, step := range selector.Steps { - b.WriteString(step.Field) - if step.Iterate { - b.WriteString("[]") - } - if step.Index != nil { - fmt.Fprintf(&b, "[%d]", *step.Index) - } - b.WriteByte('.') - } - if selector.Filter != nil { - b.WriteString("?" + selector.Filter.Field + "=" + selector.Filter.Needle) - } - return b.String() -} diff --git a/internal/dataframe/compiler/selector_mode.go b/internal/dataframe/compiler/selector_mode.go deleted file mode 100644 index a926a83..0000000 --- a/internal/dataframe/compiler/selector_mode.go +++ /dev/null @@ -1,50 +0,0 @@ -package compiler - -import ( - "os" - "strings" - - "github.com/calypr/loom/fhirschema" -) - -func selectorHasNoArrays(sel Selector) bool { - for _, step := range sel.Steps { - if step.Iterate || step.Index != nil { - return false - } - } - return true -} - -func selectorHasIteratedArray(sel Selector) bool { - for _, step := range sel.Steps { - if step.Iterate { - return true - } - } - return false -} - -// selectorExecutionMode is a physical lowering choice. Keeping it in the -// compiler facade during migration prevents semantic planning from depending -// on physical IR types; it will move with the lowerer in the next extraction. -func selectorExecutionMode(resourceType string, selector Selector, fallbacks ...Selector) PhysicalSelectorExecutionMode { - switch strings.ToLower(strings.TrimSpace(os.Getenv("LOOM_PHYSICAL_RULE_TYPED_SELECTORS"))) { - case "off", "0", "false", "disabled": - return PhysicalSelectorGeneric - } - if len(fallbacks) != 0 || selector.Filter != nil { - return PhysicalSelectorGeneric - } - metadata, ok := fhirschema.ResolveTerminalScalarMetadata(resourceType, selector.CanonicalPath()) - if !ok { - return PhysicalSelectorGeneric - } - if selectorHasNoArrays(selector) && !metadata.Repeated { - return PhysicalSelectorDirectScalar - } - if selectorHasIteratedArray(selector) && metadata.Repeated { - return PhysicalSelectorConditionalArray - } - return PhysicalSelectorGeneric -} diff --git a/internal/dataframe/compiler/selector_mode_test.go b/internal/dataframe/compiler/selector_mode_test.go deleted file mode 100644 index 80de27e..0000000 --- a/internal/dataframe/compiler/selector_mode_test.go +++ /dev/null @@ -1,96 +0,0 @@ -package compiler - -import ( - "strings" - "testing" -) - -func TestTypedSelectorModesUseSchemaProvenShapes(t *testing.T) { - semantic, err := BuildSemanticPlan(Builder{Project: "p", RootResourceType: "Patient", Fields: []FieldSelect{{Name: "gender", Select: "gender"}}}) - if err != nil { - t.Fatal(err) - } - plan, err := BuildGenericPhysicalPlanWithPolicy(semantic, DefaultPhysicalOptimizationPolicy()) - if err != nil { - t.Fatal(err) - } - rendered, err := RenderPhysicalPlan(withSelectorModeTestWindow(t, plan)) - if err != nil { - t.Fatal(err) - } - if !strings.Contains(rendered.Query, "root.payload.gender") { - t.Fatalf("direct scalar selector did not lower to a direct expression:\n%s", rendered.Query) - } - semantic, err = BuildSemanticPlan(Builder{Project: "p", RootResourceType: "Condition", Fields: []FieldSelect{{Name: "diagnosis", Select: "code.coding[].display", ValueMode: "ALL"}}}) - if err != nil { - t.Fatal(err) - } - plan, err = BuildGenericPhysicalPlanWithPolicy(semantic, DefaultPhysicalOptimizationPolicy()) - if err != nil { - t.Fatal(err) - } - rendered, err = RenderPhysicalPlan(withSelectorModeTestWindow(t, plan)) - if err != nil { - t.Fatal(err) - } - if strings.Contains(rendered.Query, "FOR __root IN [root.payload]") { - t.Fatalf("conditional selector retained singleton root enumeration:\n%s", rendered.Query) - } - if !strings.Contains(rendered.Query, "FOR __loom_selector_1 IN (__loom_selector_0.coding ? __loom_selector_0.coding : [])") { - t.Fatalf("conditional repeated selector did not use guarded array iteration:\n%s", rendered.Query) - } -} - -func TestTypedSelectorModeFallsBackForPredicatesAndFallbacks(t *testing.T) { - semantic, err := BuildSemanticPlan(Builder{ - Project: "p", RootResourceType: "Patient", - Fields: []FieldSelect{{Name: "case_id", Select: `identifier[].value where system contains "case_id"`, ValueMode: "FIRST"}}, - }) - if err != nil { - t.Fatal(err) - } - plan, err := BuildGenericPhysicalPlanWithPolicy(semantic, DefaultPhysicalOptimizationPolicy()) - if err != nil { - t.Fatal(err) - } - for _, operation := range plan.Operations { - if operation.Return == nil { - continue - } - for _, projection := range operation.Return.Projections { - if projection.Expression == nil || projection.Expression.Extract == nil { - continue - } - if projection.Expression.Extract.ExecutionMode != PhysicalSelectorGeneric { - t.Fatalf("predicate selector unexpectedly specialized: %#v", projection.Expression.Extract.ExecutionMode) - } - } - } -} - -func TestTypedConditionalSelectorPreservesScalarCardinality(t *testing.T) { - semantic, err := BuildSemanticPlan(Builder{Project: "p", RootResourceType: "Patient", Fields: []FieldSelect{{Name: "identifier", Select: "identifier[].value", ValueMode: "FIRST"}}}) - if err != nil { - t.Fatal(err) - } - plan, err := BuildGenericPhysicalPlanWithPolicy(semantic, DefaultPhysicalOptimizationPolicy()) - if err != nil { - t.Fatal(err) - } - rendered, err := RenderPhysicalPlan(withSelectorModeTestWindow(t, plan)) - if err != nil { - t.Fatal(err) - } - if !strings.Contains(rendered.Query, "FIRST((") || !strings.Contains(rendered.Query, "FOR __loom_selector_0 IN (root.payload.identifier ? root.payload.identifier : [])") { - t.Fatalf("repeated scalar selector lost FIRST/cardinality boundary:\n%s", rendered.Query) - } -} - -func withSelectorModeTestWindow(t *testing.T, plan PhysicalPlan) PhysicalPlan { - t.Helper() - windowed, err := withGenericPhysicalExecutionWindow(plan, 25) - if err != nil { - t.Fatal(err) - } - return windowed -} diff --git a/internal/dataframe/compiler/semantic_aliases.go b/internal/dataframe/compiler/semantic_aliases.go index 069cecb..f308c1d 100644 --- a/internal/dataframe/compiler/semantic_aliases.go +++ b/internal/dataframe/compiler/semantic_aliases.go @@ -3,6 +3,17 @@ package compiler import "github.com/calypr/loom/internal/dataframe/semantic" type ( + RecipePlan = semantic.RecipePlan + OutputPlan = semantic.OutputPlan + SemanticExpression = semantic.SemanticExpression + SemanticProjection = semantic.SemanticProjection + SemanticDynamicMap = semantic.SemanticDynamicMap + RecipePlanExplanation = semantic.RecipePlanExplanation + OutputPlanExplanation = semantic.OutputPlanExplanation + ExpressionExplanation = semantic.ExpressionExplanation + ExpansionExplanation = semantic.ExpansionExplanation + ResolvedColumn = semantic.ResolvedColumn + ResolvedRecipePlan = semantic.ResolvedRecipePlan SemanticPlan = semantic.SemanticPlan SemanticNode = semantic.SemanticNode SemanticField = semantic.SemanticField @@ -17,7 +28,8 @@ type ( const MaxSemanticTraversalDepth = semantic.MaxSemanticTraversalDepth var ( - BuildSemanticPlan = semantic.BuildSemanticPlan + BuildRecipePlan = semantic.BuildRecipePlan + ResolveRecipePlan = semantic.ResolveRecipePlan ValidateSemanticGraph = semantic.ValidateSemanticGraph NormalizeSelectionPlan = semantic.NormalizeSelectionPlan ResolveSemanticField = semantic.ResolveSemanticField diff --git a/internal/dataframe/compiler/semantic_plan_test.go b/internal/dataframe/compiler/semantic_plan_test.go deleted file mode 100644 index 5d5cce6..0000000 --- a/internal/dataframe/compiler/semantic_plan_test.go +++ /dev/null @@ -1,97 +0,0 @@ -package compiler - -import "testing" - -func TestBuildSemanticPlanPreservesNestedFHIRSelections(t *testing.T) { - plan, err := BuildSemanticPlan(Builder{ - Project: "P1", - AuthResourcePaths: []string{"pathA"}, - RootResourceType: "Patient", - Fields: []FieldSelect{{ - Name: "case_id", - FieldRef: "Patient.identifier_value", - Select: `identifier[].value where system contains "case_id"`, - FallbackSelects: []string{"id"}, - ValueMode: "FIRST", - }}, - Traversals: []TraversalStep{{ - Label: "subject_Patient", - ToResourceType: "Observation", - Alias: "observation", - Pivots: []PivotSelect{{ - Name: "values", - ColumnSelect: "code.coding[].display", - ValueSelect: "valueQuantity.value", - Columns: []string{"Hemoglobin"}, - }}, - Aggregates: []AggregateSelect{{Name: "count", Operation: "COUNT"}}, - Traversals: []TraversalStep{{ - Label: "subject_Observation", - ToResourceType: "DocumentReference", - Alias: "document_reference", - }}, - }}, - }) - if err != nil { - t.Fatalf("BuildSemanticPlan() error = %v", err) - } - if plan.Version != 1 || plan.Root.ResourceType != "Patient" { - t.Fatalf("unexpected plan root: %#v", plan) - } - if plan.RowIdentity == nil || plan.RowIdentity.Grain != RowGrainPatient { - t.Fatalf("expected inferred Patient row identity, got %#v", plan.RowIdentity) - } - if len(plan.Root.Fields) != 1 || plan.Root.Fields[0].Selector.CanonicalPath() != "identifier[].value" { - t.Fatalf("root field not parsed: %#v", plan.Root.Fields) - } - if len(plan.Root.Children) != 1 || plan.Root.Children[0].Alias != "observation" { - t.Fatalf("child not preserved: %#v", plan.Root.Children) - } - if len(plan.Root.Children[0].Pivots) != 1 || plan.Root.Children[0].Pivots[0].ColumnSelector.CanonicalPath() != "code.coding[].display" { - t.Fatalf("pivot not parsed: %#v", plan.Root.Children[0].Pivots) - } - got := plan.Explain() - if len(got.Nodes) != 3 || got.Nodes[1].ParentAlias != "root" || got.Nodes[2].ParentAlias != "observation" { - t.Fatalf("unexpected explanation: %#v", got) - } -} - -func TestBuildSemanticPlanRejectsBadSelector(t *testing.T) { - _, err := BuildSemanticPlan(Builder{ - Project: "P1", - RootResourceType: "Patient", - Fields: []FieldSelect{{Name: "bad", Select: "identifier[nope]"}}, - }) - if err == nil { - t.Fatal("expected bad selector error") - } -} - -func TestBuildSemanticPlanUsesGenericIdentityForGeneratedNonProductRoot(t *testing.T) { - plan, err := BuildSemanticPlan(Builder{Project: "P1", RootResourceType: "Organization"}) - if err != nil { - t.Fatal(err) - } - if plan.RowIdentity == nil || plan.RowIdentity.Grain != RowGrainResource { - t.Fatalf("generic root identity = %#v", plan.RowIdentity) - } -} - -func TestBuildSemanticPlanRejectsCrossGrainRootBeforeLowering(t *testing.T) { - _, err := BuildSemanticPlan(Builder{ - Project: "P1", RootResourceType: "Patient", RowGrain: RowGrainSpecimen, - }) - if err == nil { - t.Fatal("cross-grain semantic plan unexpectedly succeeded") - } -} - -func TestCompileRequestCarriesStableRowIdentity(t *testing.T) { - compiled, err := CompileRequest(Builder{Project: "P1", RootResourceType: "Specimen"}, 1) - if err != nil { - t.Fatal(err) - } - if compiled.RowIdentity == nil || compiled.RowIdentity.Grain != RowGrainSpecimen { - t.Fatalf("compiled identity = %#v", compiled.RowIdentity) - } -} diff --git a/internal/dataframe/compiler/spec_aliases.go b/internal/dataframe/compiler/spec_aliases.go index fa6529d..e93976b 100644 --- a/internal/dataframe/compiler/spec_aliases.go +++ b/internal/dataframe/compiler/spec_aliases.go @@ -6,26 +6,20 @@ package compiler import "github.com/calypr/loom/internal/dataframe/spec" type ( - Builder = spec.Builder - TraversalStep = spec.TraversalStep - RepresentativeSlice = spec.RepresentativeSlice - FieldSelect = spec.FieldSelect - PivotSelect = spec.PivotSelect - AggregateSelect = spec.AggregateSelect - RowGrain = spec.RowGrain - ProjectionMode = spec.ProjectionMode - Cardinality = spec.Cardinality - RowIdentity = spec.RowIdentity - TraversalMatchMode = spec.TraversalMatchMode - FilterOperator = spec.FilterOperator - FilterValueKind = spec.FilterValueKind - ArrayQuantifier = spec.ArrayQuantifier - CodeValue = spec.CodeValue - FilterValue = spec.FilterValue - TypedFilter = spec.TypedFilter - Selector = spec.Selector - SelectorStep = spec.SelectorStep - ContainsFilter = spec.ContainsFilter + RowGrain = spec.RowGrain + ProjectionMode = spec.ProjectionMode + Cardinality = spec.Cardinality + RowIdentity = spec.RowIdentity + TraversalMatchMode = spec.TraversalMatchMode + FilterOperator = spec.FilterOperator + FilterValueKind = spec.FilterValueKind + ArrayQuantifier = spec.ArrayQuantifier + CodeValue = spec.CodeValue + FilterValue = spec.FilterValue + TypedFilter = spec.TypedFilter + Selector = spec.Selector + SelectorStep = spec.SelectorStep + ContainsFilter = spec.ContainsFilter ) const ( diff --git a/internal/dataframe/compiler_arango_integration_test.go b/internal/dataframe/compiler_arango_integration_test.go deleted file mode 100644 index 538da05..0000000 --- a/internal/dataframe/compiler_arango_integration_test.go +++ /dev/null @@ -1,353 +0,0 @@ -package dataframe - -import ( - "context" - "fmt" - "os" - "reflect" - "sort" - "strings" - "testing" - - arangostore "github.com/calypr/loom/internal/store/arango" -) - -// TestGenericSpecimenPlanExplainsAndRunsAgainstArango is deliberately opt-in: -// it reads the locally loaded development FHIR database and never mutates it. -// Run with LOOM_COMPILER_ARANGO_INTEGRATION=1 after provisioning the documented -// local Arango/META fixture database. -func TestGenericSpecimenPlanExplainsAndRunsAgainstArango(t *testing.T) { - if os.Getenv("LOOM_COMPILER_ARANGO_INTEGRATION") == "" { - t.Skip("set LOOM_COMPILER_ARANGO_INTEGRATION=1 to run compiler/Arango integration") - } - url := os.Getenv("LOOM_ARANGO_URL") - if url == "" { - url = "http://127.0.0.1:8529" - } - database := os.Getenv("LOOM_ARANGO_DATABASE") - if database == "" { - database = "fhir_proto" - } - project := os.Getenv("LOOM_ARANGO_PROJECT") - if project == "" { - project = "ARANGODB_PROTO" - } - - compiled, err := CompileRequest(Builder{ - Project: project, - RootResourceType: "Specimen", - Filters: []TypedFilter{{ - FieldRef: "Specimen.type_display", - Selector: "type.coding[].display", - FieldKind: FilterString, - Repeated: true, - Quantifier: QuantifierAny, - Operator: FilterExists, - }}, - Fields: []FieldSelect{ - {Name: "specimen_type", Select: "type.coding[].display"}, - }, - Traversals: []TraversalStep{{ - Label: "subject_Specimen", - ToResourceType: "DocumentReference", - Alias: "file", - Filters: []TypedFilter{{ - FieldRef: "DocumentReference.file_name", - Selector: "content[].attachment.title", - FieldKind: FilterString, - Repeated: true, - Quantifier: QuantifierAny, - Operator: FilterExists, - }}, - Aggregates: []AggregateSelect{{Name: "file_count", Operation: "COUNT"}}, - }}, - }, 5) - if err != nil { - t.Fatalf("CompileRequest() error = %v", err) - } - if compiled.PlanProfile != "generic_fhir_graph" { - t.Fatalf("plan profile = %q, want generic_fhir_graph", compiled.PlanProfile) - } - t.Logf("compiled generic AQL:\n%s", compiled.Query) - - opts := arangostore.ConnectionOptions{URL: url, Database: database} - explain, err := ExplainCompiledQuery(context.Background(), opts, compiled) - if err != nil { - t.Fatalf("ExplainCompiledQuery() error = %v", err) - } - if explain.Plan == nil { - t.Fatalf("explain returned no selected plan: %#v", explain) - } - if uses := arangostore.ExtractPlanIndexes(explain); len(uses) == 0 { - t.Fatalf("generic compiler plan used no reported indexes: %#v", explain.Plan) - } else { - t.Logf("EXPLAIN indexes: %#v", uses) - } - assessment := arangostore.AssessExplainResult(explain) - t.Logf("EXPLAIN assessment: plans=%#v fullCollectionScans=%#v optimizerRules=%#v warnings=%#v", assessment.Plans, assessment.FullCollectionScans, assessment.AppliedOptimizerRules, assessment.Warnings) - if !hasExplainIndex(assessment, "fhir_edge", []string{"_to"}) { - t.Fatalf("generic inbound traversal did not use fhir_edge's _to edge index: assessment=%#v", assessment) - } - - rows := 0 - err = ExecuteQueryRows(context.Background(), ExecuteQueryOptions{ - ConnectionOptions: opts, - BatchSize: 5, - }, compiled.Query, compiled.BindVars, func(map[string]any) error { - rows++ - return nil - }) - if err != nil { - t.Fatalf("ExecuteQueryRows() error = %v", err) - } - if rows == 0 { - t.Fatalf("generic Specimen plan returned no rows for project %q", project) - } -} - -// TestGenericRootPreviewUsesScopedSortIndexAgainstArango protects the index -// family that makes a generic, root-grain preview viable. The loader now -// provisions this index for every resource collection; Patient is used here -// because the checked-in META fixture already contains it. -func TestGenericRootPreviewUsesScopedSortIndexAgainstArango(t *testing.T) { - if os.Getenv("LOOM_COMPILER_ARANGO_INTEGRATION") == "" { - t.Skip("set LOOM_COMPILER_ARANGO_INTEGRATION=1 to run compiler/Arango integration") - } - url, database, project := compilerArangoTarget() - compiled, err := CompileRequest(Builder{ - Project: project, - RootResourceType: "Patient", - }, 5) - if err != nil { - t.Fatalf("CompileRequest() error = %v", err) - } - if !strings.Contains(compiled.Query, "FOR root IN @@root_collection") { - t.Fatalf("root preview did not use the physical execution renderer:\n%s", compiled.Query) - } - explain, err := ExplainCompiledQuery(context.Background(), arangostore.ConnectionOptions{URL: url, Database: database}, compiled) - if err != nil { - t.Fatalf("ExplainCompiledQuery() error = %v", err) - } - assessment := arangostore.AssessExplainResult(explain) - if !hasScopedRootPreviewIndex(assessment, "Patient") { - t.Fatalf("root preview did not use the project/_key index; assessment=%#v", assessment) - } -} - -// TestRenderedGenericPhysicalNavigationExplainsAgainstArango verifies the -// typed navigation renderer against the real Arango parser without replacing -// the legacy dataframe renderer yet. It protects collection-bind handling, -// scoped inbound traversal syntax, and root-row-preserving LET subqueries. -// The navigation set is intentionally not projected yet, so Arango may prune -// it; this test verifies parseability and scoped-root index selection rather -// than requiring an unused traversal to execute. -func TestRenderedGenericPhysicalNavigationExplainsAgainstArango(t *testing.T) { - if os.Getenv("LOOM_COMPILER_ARANGO_INTEGRATION") == "" { - t.Skip("set LOOM_COMPILER_ARANGO_INTEGRATION=1 to run compiler/Arango integration") - } - url, database, project := compilerArangoTarget() - physical, err := BuildGenericPhysicalPlan(SemanticPlan{ - Version: 1, - Project: project, - Root: SemanticNode{ - Alias: "root", ResourceType: "Patient", - Children: []SemanticNode{{ - Alias: "specimen", ResourceType: "Specimen", EdgeLabel: "subject_Patient", - }}, - }, - }) - if err != nil { - t.Fatalf("BuildGenericPhysicalPlan() error = %v", err) - } - rendered, err := RenderPhysicalPlan(physical) - if err != nil { - t.Fatalf("RenderPhysicalPlan() error = %v", err) - } - explain, err := ExplainCompiledQuery(context.Background(), arangostore.ConnectionOptions{URL: url, Database: database}, CompiledQuery{ - Query: rendered.Query, - BindVars: rendered.BindVars, - }) - if err != nil { - t.Fatalf("Explain rendered physical navigation: %v\nAQL:\n%s", err, rendered.Query) - } - assessment := arangostore.AssessExplainResult(explain) - if len(assessment.FullCollectionScans) != 0 { - t.Fatalf("rendered physical navigation unexpectedly full-scanned a collection: assessment=%#v", assessment) - } - if !hasScopedRootPreviewIndex(assessment, "Patient") { - t.Fatalf("rendered physical navigation did not use a scoped root index; assessment=%#v", assessment) - } -} - -// BenchmarkGenericCompilerAgainstArango measures two different costs against -// the locally loaded META fixture: AQL compilation in-process and execution -// of the already-compiled AQL over one reusable Arango client. The latter is -// intentionally an end-to-end database benchmark, not a Go allocation proxy. -// It is opt-in so normal unit test runs never require a local database. -func BenchmarkGenericCompilerAgainstArango(b *testing.B) { - if os.Getenv("LOOM_COMPILER_ARANGO_INTEGRATION") == "" { - b.Skip("set LOOM_COMPILER_ARANGO_INTEGRATION=1 to benchmark against local Arango/META") - } - url, database, project := compilerArangoTarget() - builder := genericMetaSpecimenBuilder(project) - - b.Run("compile_specimen_file", func(b *testing.B) { - b.ReportAllocs() - b.ResetTimer() - for i := 0; i < b.N; i++ { - if _, err := CompileRequest(builder, 25); err != nil { - b.Fatal(err) - } - } - }) - - compiled, err := CompileRequest(builder, 25) - if err != nil { - b.Fatal(err) - } - client, err := arangostore.Open(context.Background(), url, database) - if err != nil { - b.Fatal(err) - } - defer client.Close(context.Background()) - b.Run("execute_specimen_file", func(b *testing.B) { - b.ReportAllocs() - rows := 0 - b.ResetTimer() - for i := 0; i < b.N; i++ { - rows = 0 - err := client.QueryRows(context.Background(), compiled.Query, 25, compiled.BindVars, func(map[string]any) error { - rows++ - return nil - }) - if err != nil { - b.Fatal(err) - } - } - b.ReportMetric(float64(rows), "rows/op") - }) -} - -func genericMetaSpecimenBuilder(project string) Builder { - return Builder{ - Project: project, - RootResourceType: "Specimen", - Filters: []TypedFilter{{ - FieldRef: "Specimen.type_display", Selector: "type.coding[].display", FieldKind: FilterString, - Repeated: true, Quantifier: QuantifierAny, Operator: FilterExists, - }}, - Fields: []FieldSelect{{Name: "specimen_type", Select: "type.coding[].display"}}, - Traversals: []TraversalStep{{ - Label: "subject_Specimen", ToResourceType: "DocumentReference", Alias: "file", - Filters: []TypedFilter{{ - FieldRef: "DocumentReference.file_name", Selector: "content[].attachment.title", FieldKind: FilterString, - Repeated: true, Quantifier: QuantifierAny, Operator: FilterExists, - }}, - Aggregates: []AggregateSelect{{Name: "file_count", Operation: "COUNT"}}, - }}, - } -} - -func compilerArangoTarget() (url, database, project string) { - url = os.Getenv("LOOM_ARANGO_URL") - if url == "" { - url = "http://127.0.0.1:8529" - } - database = os.Getenv("LOOM_ARANGO_DATABASE") - if database == "" { - database = "fhir_proto" - } - project = os.Getenv("LOOM_ARANGO_PROJECT") - if project == "" { - project = "ARANGODB_PROTO" - } - return url, database, project -} - -func executeCompiledRows(ctx context.Context, opts arangostore.ConnectionOptions, compiled CompiledQuery) ([]map[string]any, error) { - rows := make([]map[string]any, 0) - err := ExecuteQueryRows(ctx, ExecuteQueryOptions{ConnectionOptions: opts, BatchSize: 25}, compiled.Query, compiled.BindVars, func(row map[string]any) error { - clone := make(map[string]any, len(row)) - for key, value := range row { - clone[key] = value - } - rows = append(rows, clone) - return nil - }) - return rows, err -} - -func sortRowsByKey(rows []map[string]any) { - sort.Slice(rows, func(i, j int) bool { - return fmt.Sprint(rows[i]["_key"]) < fmt.Sprint(rows[j]["_key"]) - }) -} - -func firstRowDifference(genericRows, specializedRows []map[string]any) string { - if len(genericRows) != len(specializedRows) { - return fmt.Sprintf("row count generic=%d specialized=%d", len(genericRows), len(specializedRows)) - } - for index := range genericRows { - generic := genericRows[index] - specialized := specializedRows[index] - if key := fmt.Sprint(generic["_key"]); key != fmt.Sprint(specialized["_key"]) { - return fmt.Sprintf("row %d key generic=%q specialized=%q", index, key, fmt.Sprint(specialized["_key"])) - } - keys := make([]string, 0, len(generic)+len(specialized)) - seen := map[string]bool{} - for key := range generic { - seen[key] = true - keys = append(keys, key) - } - for key := range specialized { - if !seen[key] { - keys = append(keys, key) - } - } - sort.Strings(keys) - for _, key := range keys { - if reflect.DeepEqual(generic[key], specialized[key]) { - continue - } - return fmt.Sprintf("row %q field %q generic=%s specialized=%s", fmt.Sprint(generic["_key"]), key, summarizeParityValue(generic[key]), summarizeParityValue(specialized[key])) - } - } - return "" -} - -func summarizeParityValue(value any) string { - rv := reflect.ValueOf(value) - if rv.IsValid() && (rv.Kind() == reflect.Array || rv.Kind() == reflect.Slice) { - limit := rv.Len() - if limit > 3 { - limit = 3 - } - prefix := make([]any, 0, limit) - for index := 0; index < limit; index++ { - prefix = append(prefix, rv.Index(index).Interface()) - } - return fmt.Sprintf("%T(len=%d, prefix=%#v)", value, rv.Len(), prefix) - } - return fmt.Sprintf("%#v", value) -} - -func hasExplainIndex(assessment arangostore.ExplainAssessment, collection string, fields []string) bool { - for _, index := range assessment.Indexes { - if index.Collection == collection && reflect.DeepEqual(index.Fields, fields) { - return true - } - } - return false -} - -func hasScopedRootPreviewIndex(assessment arangostore.ExplainAssessment, collection string) bool { - for _, index := range assessment.Indexes { - if index.Collection != collection || len(index.Fields) < 2 { - continue - } - if index.Fields[0] == "project" && index.Fields[len(index.Fields)-1] == "_key" { - return true - } - } - return false -} diff --git a/internal/dataframe/dataset_generation_test.go b/internal/dataframe/dataset_generation_test.go deleted file mode 100644 index 6ff6367..0000000 --- a/internal/dataframe/dataset_generation_test.go +++ /dev/null @@ -1,214 +0,0 @@ -package dataframe - -import ( - "context" - "strings" - "testing" - - "github.com/calypr/loom/internal/catalog" -) - -func TestDatasetGenerationCompilesRootTraversalAndRequiredMatch(t *testing.T) { - compiled, err := CompileRequest(Builder{ - Project: "P1", - DatasetGeneration: " generation-a ", - RootResourceType: "Patient", - Traversals: []TraversalStep{{ - Label: "subject_Patient", - ToResourceType: "Condition", - Alias: "diagnosis", - MatchMode: TraversalMatchRequired, - }}, - }, 5) - if err != nil { - t.Fatal(err) - } - if got := compiled.DatasetGeneration; got != "generation-a" { - t.Fatalf("compiled DatasetGeneration = %q, want normalized exact generation", got) - } - if got := compiled.BindVars[datasetGenerationBindKey]; got != "generation-a" { - t.Fatalf("compiled generation bind = %#v, want generation-a", got) - } - for _, want := range []string{ - "root.dataset_generation == @dataset_generation", - "required_0_edge_0.dataset_generation == @dataset_generation", - "required_0_node_0.dataset_generation == @dataset_generation", - } { - if !strings.Contains(compiled.Query, want) { - t.Fatalf("compiled query is missing generation predicate %q:\n%s", want, compiled.Query) - } - } - - semantic, err := BuildSemanticPlan(Builder{Project: "P1", DatasetGeneration: " generation-a ", RootResourceType: "Patient"}) - if err != nil { - t.Fatal(err) - } - if got := semantic.DatasetGeneration; got != "generation-a" { - t.Fatalf("semantic DatasetGeneration = %q, want normalized exact generation", got) - } -} - -func TestDatasetGenerationAbsentCompilesLegacyNullNamespace(t *testing.T) { - compiled, err := CompileRequest(Builder{ - Project: "P1", - RootResourceType: "Specimen", - Traversals: []TraversalStep{{ - Label: "subject_Specimen", - ToResourceType: "DocumentReference", - Alias: "file", - }}, - }, 5) - if err != nil { - t.Fatal(err) - } - if got, present := compiled.BindVars[datasetGenerationBindKey]; !present || got != nil { - t.Fatalf("legacy compiled generation bind = %#v (present=%t), want explicit nil", got, present) - } - for _, want := range []string{ - "root.dataset_generation == @dataset_generation", - "edge_1.dataset_generation == @dataset_generation", - "node_1.dataset_generation == @dataset_generation", - } { - if !strings.Contains(compiled.Query, want) { - t.Fatalf("legacy query is missing null-generation predicate %q:\n%s", want, compiled.Query) - } - } -} - -func TestDatasetGenerationPhysicalPlanAndRendererFilterEveryGraphDocument(t *testing.T) { - semantic := SemanticPlan{ - Version: 1, - Project: "P1", - DatasetGeneration: "generation-a", - Root: SemanticNode{ - Alias: "root", ResourceType: "Patient", - Children: []SemanticNode{{ - Alias: "specimen", ResourceType: "Specimen", EdgeLabel: "subject_Patient", - }}, - }, - } - plan, err := BuildGenericPhysicalPlan(semantic) - if err != nil { - t.Fatal(err) - } - if got := plan.BindVars[datasetGenerationBindKey]; got != "generation-a" { - t.Fatalf("physical generation bind = %#v, want generation-a", got) - } - for _, variable := range []string{"root", "edge_1", "node_1"} { - if !hasDatasetGenerationFilter(plan, variable) { - t.Fatalf("physical plan is missing exact generation filter for %q: %#v", variable, plan.Operations) - } - } - if err := ValidateGenericPhysicalPlanScope(plan); err != nil { - t.Fatalf("generation-safe physical plan failed scope validation: %v", err) - } - - rendered, err := RenderPhysicalPlan(plan) - if err != nil { - t.Fatal(err) - } - if got := rendered.BindVars[datasetGenerationBindKey]; got != "generation-a" { - t.Fatalf("rendered generation bind = %#v, want generation-a", got) - } - for _, want := range []string{ - "FILTER root.dataset_generation == @dataset_generation", - "FILTER edge_1.dataset_generation == @dataset_generation", - "FILTER node_1.dataset_generation == @dataset_generation", - } { - if !strings.Contains(rendered.Query, want) { - t.Fatalf("rendered physical query missing %q:\n%s", want, rendered.Query) - } - } - - legacy := semantic - legacy.DatasetGeneration = "" - legacyPlan, err := BuildGenericPhysicalPlan(legacy) - if err != nil { - t.Fatal(err) - } - if got, present := legacyPlan.BindVars[datasetGenerationBindKey]; !present || got != nil { - t.Fatalf("legacy physical generation bind = %#v (present=%t), want explicit nil", got, present) - } -} - -func TestDatasetGenerationPhysicalScopeRejectsMissingTargetPredicate(t *testing.T) { - plan, err := BuildGenericPhysicalPlan(SemanticPlan{ - Version: 1, - Project: "P1", - Root: SemanticNode{ - Alias: "root", ResourceType: "Patient", - Children: []SemanticNode{{Alias: "specimen", ResourceType: "Specimen", EdgeLabel: "subject_Patient"}}, - }, - }) - if err != nil { - t.Fatal(err) - } - for index, operation := range plan.Operations { - if operation.Kind == PhysicalFilterOp && isDatasetGenerationScopePredicate(operation.Filter.Predicate, "node_1") { - plan.Operations = append(plan.Operations[:index], plan.Operations[index+1:]...) - break - } - } - if err := ValidateGenericPhysicalPlanScope(plan); err == nil || !strings.Contains(err.Error(), "node_1.dataset_generation") { - t.Fatalf("missing target generation predicate error = %v", err) - } -} - -func TestServicePropagatesDatasetGenerationToEveryCatalogRead(t *testing.T) { - fieldOptions := make([]catalog.PopulatedFieldOptions, 0) - referenceOptions := make([]catalog.PopulatedReferenceOptions, 0) - service := NewService(ServiceConfig{ - DiscoverFields: func(_ context.Context, options catalog.PopulatedFieldOptions) ([]catalog.PopulatedField, error) { - fieldOptions = append(fieldOptions, options) - return []catalog.PopulatedField{}, nil - }, - DiscoverReferences: func(_ context.Context, options catalog.PopulatedReferenceOptions) ([]catalog.PopulatedReference, error) { - referenceOptions = append(referenceOptions, options) - return []catalog.PopulatedReference{{FromType: "Patient", Label: "subject_Patient", ToType: "Condition"}}, nil - }, - ExecuteRows: func(_ context.Context, _ ExecuteQueryOptions, query string, bindVars map[string]any, _ func(map[string]any) error) error { - if got := bindVars[datasetGenerationBindKey]; got != "generation-a" { - t.Fatalf("execution generation bind = %#v, want generation-a", got) - } - if !strings.Contains(query, "root.dataset_generation == @dataset_generation") { - t.Fatalf("execution query lost root generation predicate:\n%s", query) - } - return nil - }, - }) - _, err := service.Run(context.Background(), RunRequest{Builder: Builder{ - Project: "P1", - DatasetGeneration: " generation-a ", - RootResourceType: "Patient", - Traversals: []TraversalStep{{ - Label: "subject_Patient", - ToResourceType: "Condition", - Alias: "diagnosis", - }}, - }, Limit: 1}) - if err != nil { - t.Fatal(err) - } - if len(fieldOptions) == 0 || len(referenceOptions) == 0 { - t.Fatalf("catalog calls fields=%d references=%d, want both", len(fieldOptions), len(referenceOptions)) - } - for index, options := range fieldOptions { - if got := catalog.DatasetGenerationBindValue(options.DatasetGeneration); got != "generation-a" { - t.Fatalf("field catalog call %d generation bind = %#v, want generation-a", index, got) - } - } - for index, options := range referenceOptions { - if got := catalog.DatasetGenerationBindValue(options.DatasetGeneration); got != "generation-a" { - t.Fatalf("reference catalog call %d generation bind = %#v, want generation-a", index, got) - } - } -} - -func hasDatasetGenerationFilter(plan PhysicalPlan, variable string) bool { - for _, operation := range plan.Operations { - if operation.Kind == PhysicalFilterOp && isDatasetGenerationScopePredicate(operation.Filter.Predicate, variable) { - return true - } - } - return false -} diff --git a/internal/dataframe/doc.go b/internal/dataframe/doc.go index 677d878..74b4ec9 100644 --- a/internal/dataframe/doc.go +++ b/internal/dataframe/doc.go @@ -1,7 +1,7 @@ -// Package dataframe is Loom's stable dataframe facade. +// Package dataframe is Loom's public dataframe facade. // // Request contracts live in spec, schema-backed planning in semantic, physical -// planning in compiler, and catalog-aware execution in runtime. This package -// intentionally contains aliases and small compatibility entrypoints only; -// new implementation code belongs in the owning subpackage. +// planning in compiler, and catalog-aware execution in runtime. The facade +// keeps the public service and compiler contracts discoverable while the +// implementation remains in those owning subpackages. package dataframe diff --git a/internal/dataframe/expression/expression_checking.go b/internal/dataframe/expression/expression_checking.go new file mode 100644 index 0000000..ddd9cff --- /dev/null +++ b/internal/dataframe/expression/expression_checking.go @@ -0,0 +1,408 @@ +// Package expression contains the typed, backend-neutral expression AST used +// by dataframe recipes. It intentionally has no knowledge of AQL, SQL, or +// output names: a physical compiler consumes the checked tree later. +package expression + +import ( + "fmt" + "reflect" + "strings" +) + +func (e Expression) Check(ctx TypeContext) (CheckedExpression, error) { + if ctx.MaxDepth <= 0 { + ctx.MaxDepth = 64 + } + if ctx.MaxNodes <= 0 { + ctx.MaxNodes = 4096 + } + state := checkState{ctx: ctx} + t, err := state.check(e, "expr", 0) + if err != nil { + return CheckedExpression{}, err + } + if e.Type.Valid() && e.Type != t { + return CheckedExpression{}, fmt.Errorf("expr: declared type %s conflicts with inferred type %s", e.Type, t) + } + return CheckedExpression{Expression: e, Type: t}, nil +} + +func (e Expression) Validate(ctx TypeContext) error { + _, err := e.Check(ctx) + return err +} + +type checkState struct { + ctx TypeContext + nodes int +} + +func (s *checkState) check(e Expression, path string, depth int) (Type, error) { + s.nodes++ + if s.nodes > s.ctx.MaxNodes { + return Type{}, fmt.Errorf("%s: expression node limit %d exceeded", path, s.ctx.MaxNodes) + } + if depth > s.ctx.MaxDepth { + return Type{}, fmt.Errorf("%s: expression depth limit %d exceeded", path, s.ctx.MaxDepth) + } + if !e.NullBehavior.Valid() { + return Type{}, fmt.Errorf("%s: invalid null behavior %q", path, e.NullBehavior) + } + switch e.Kind { + case SelectorNode: + if e.Selector == nil || e.Literal != nil || e.Call != nil { + return Type{}, fmt.Errorf("%s: selector node requires only selector", path) + } + if err := e.Selector.Validate(); err != nil { + return Type{}, fmt.Errorf("%s: %w", path, err) + } + t, ok := s.ctx.Selectors[e.Selector.String()] + if !ok { + t, ok = s.ctx.Selectors[e.Selector.Path] + } + if !ok && s.ctx.Resolve != nil { + var err error + t, err = s.ctx.Resolve(*e.Selector) + if err != nil { + return Type{}, fmt.Errorf("%s: resolve selector %q: %w", path, e.Selector.String(), err) + } + ok = true + } + if !ok { + return Type{}, fmt.Errorf("%s: selector %q has no type binding", path, e.Selector.String()) + } + if !t.Valid() { + return Type{}, fmt.Errorf("%s: selector %q has invalid type %s", path, e.Selector.String(), t) + } + return t, nil + case LiteralNode: + if e.Literal == nil || e.Selector != nil || e.Call != nil { + return Type{}, fmt.Errorf("%s: literal node requires only literal", path) + } + if err := validateLiteral(*e.Literal); err != nil { + return Type{}, fmt.Errorf("%s: %w", path, err) + } + return e.Literal.Type, nil + case CallNode: + if e.Call == nil || e.Selector != nil || e.Literal != nil { + return Type{}, fmt.Errorf("%s: call node requires only call", path) + } + return s.checkCall(*e.Call, path, depth) + default: + return Type{}, fmt.Errorf("%s: unknown expression node kind %q", path, e.Kind) + } +} + +func validateLiteral(l Literal) error { + if !l.Type.Valid() { + return fmt.Errorf("literal type %s is invalid", l.Type) + } + if l.Type.Kind == KindNull { + if l.Value != nil { + return fmt.Errorf("null literal must have nil value") + } + return nil + } + if l.Value == nil { + return fmt.Errorf("%s literal requires a value", l.Type.Kind) + } + if l.Type.Cardinality == Many { + valueSlice := reflect.ValueOf(l.Value) + if valueSlice.Kind() != reflect.Slice && valueSlice.Kind() != reflect.Array { + return fmt.Errorf("many literal requires a slice, got %T", l.Value) + } + for index := 0; index < valueSlice.Len(); index++ { + value := valueSlice.Index(index).Interface() + if err := validateLiteral(Literal{Type: Type{Kind: l.Type.Kind, Cardinality: RequiredOne}, Value: value}); err != nil { + return err + } + } + return nil + } + if l.Type.Cardinality == Many { + value := reflect.ValueOf(l.Value) + if value.Kind() != reflect.Array && value.Kind() != reflect.Slice { + return fmt.Errorf("many literal value %T must be an array or slice", l.Value) + } + for index := 0; index < value.Len(); index++ { + if !validScalarLiteral(l.Type.Kind, value.Index(index).Interface()) { + return fmt.Errorf("many literal element %d is incompatible with %s", index, l.Type.Kind) + } + } + return nil + } + if !validScalarLiteral(l.Type.Kind, l.Value) { + return fmt.Errorf("literal value %T is incompatible with %s", l.Value, l.Type.Kind) + } + return nil +} + +func validScalarLiteral(kind ValueKind, value any) bool { + valid := false + switch kind { + case KindBoolean: + _, valid = value.(bool) + case KindInteger: + switch value.(type) { + case int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64: + valid = true + } + case KindDecimal: + switch value.(type) { + case float32, float64: + valid = true + } + case KindString, KindDate, KindDateTime, KindCode, KindUUID: + _, valid = value.(string) + } + return valid +} + +func (s *checkState) checkCall(c Call, path string, depth int) (Type, error) { + name := strings.ToLower(strings.TrimSpace(c.Name)) + if name == "" { + return Type{}, fmt.Errorf("%s: function name is required", path) + } + args := make([]Type, len(c.Args)) + for i, arg := range c.Args { + t, err := s.check(arg, fmt.Sprintf("%s.args[%d]", path, i), depth+1) + if err != nil { + return Type{}, err + } + args[i] = t + } + if c.Target != nil && !c.Target.Valid() { + return Type{}, fmt.Errorf("%s: cast target type %s is invalid", path, c.Target) + } + result, err := inferCall(name, args, c.Target) + if err != nil { + return Type{}, fmt.Errorf("%s: %w", path, err) + } + return result, nil +} + +func inferCall(name string, args []Type, target *Type) (Type, error) { + if name == "coalesce_string" { + if len(args) == 0 { + return Type{}, fmt.Errorf("coalesce_string requires at least one argument") + } + for _, arg := range args { + if arg.Cardinality == Many || (arg.Kind != KindString && arg.Kind != KindCode && arg.Kind != KindUUID && arg.Kind != KindInteger && arg.Kind != KindDecimal && arg.Kind != KindBoolean && arg.Kind != KindDate && arg.Kind != KindDateTime) { + return Type{}, fmt.Errorf("coalesce_string accepts scalar primitive arguments, got %s", arg) + } + } + return Type{Kind: KindString, Cardinality: OptionalOne}, nil + } + if name == "coalesce" || name == "fallback" { + if len(args) == 0 { + return Type{}, fmt.Errorf("%s requires at least one argument", name) + } + var result Type + for i, arg := range args { + if arg.Kind == KindNull { + continue + } + if i == 0 || result.Kind == "" { + result = arg + continue + } + if !sameShape(result, arg) { + return Type{}, fmt.Errorf("%s arguments must have compatible types, got %s and %s", name, result, arg) + } + result.Cardinality = mergeCardinality(result.Cardinality, arg.Cardinality) + } + if result.Kind == "" { + return Type{Kind: KindNull, Cardinality: OptionalOne}, nil + } + return result, nil + } + if name == "first" { + if len(args) != 1 { + return Type{}, fmt.Errorf("first requires one argument") + } + if args[0].Kind == KindNull { + return Type{Kind: KindNull, Cardinality: OptionalOne}, nil + } + args[0].Cardinality = OptionalOne + return args[0], nil + } + if name == "all" || name == "distinct" { + if len(args) != 1 { + return Type{}, fmt.Errorf("%s requires one argument", name) + } + if args[0].Kind == KindNull { + return Type{Kind: KindNull, Cardinality: Many}, nil + } + args[0].Cardinality = Many + return args[0], nil + } + if name == "concat" { + if len(args) < 1 { + return Type{}, fmt.Errorf("concat requires at least one argument") + } + optional := false + for _, arg := range args { + if arg.Kind != KindString || arg.Cardinality == Many { + return Type{}, fmt.Errorf("concat accepts only scalar strings, got %s", arg) + } + optional = optional || arg.Cardinality == OptionalOne + } + return Type{Kind: KindString, Cardinality: optionalCardinality(optional)}, nil + } + if name == "join" { + if len(args) != 2 { + return Type{}, fmt.Errorf("join requires values and delimiter arguments") + } + if args[0].Kind != KindString || args[0].Cardinality != Many || args[1].Kind != KindString || args[1].Cardinality == Many { + return Type{}, fmt.Errorf("join requires repeated string values and a scalar string delimiter") + } + return Type{Kind: KindString, Cardinality: OptionalOne}, nil + } + if name == "cast" { + if len(args) != 1 || target == nil { + return Type{}, fmt.Errorf("cast requires one argument and a target type") + } + if target.Kind == KindNull { + return Type{}, fmt.Errorf("cast target cannot be null") + } + if args[0].Cardinality == Many || target.Cardinality == Many { + return Type{}, fmt.Errorf("cast does not accept repeated values") + } + result := *target + if args[0].Cardinality == OptionalOne { + result.Cardinality = OptionalOne + } + return result, nil + } + if name == "reference_id" || name == "path_segment" || name == "sanitize_name" { + if len(args) != 1 || args[0].Kind != KindString || args[0].Cardinality == Many { + return Type{}, fmt.Errorf("%s requires one scalar string argument", name) + } + return Type{Kind: KindString, Cardinality: OptionalOne}, nil + } + if name == "uuid3" || name == "uuid5" { + if len(args) < 2 || !allScalarKind(args, KindString) { + return Type{}, fmt.Errorf("%s requires a namespace and at least one name scalar string", name) + } + optional := false + for _, arg := range args { + optional = optional || arg.Cardinality == OptionalOne + } + return Type{Kind: KindUUID, Cardinality: optionalCardinality(optional)}, nil + } + if name == "if" { + if len(args) != 3 || args[0].Kind != KindBoolean || args[0].Cardinality == Many { + return Type{}, fmt.Errorf("if requires scalar boolean condition and two branches") + } + return mergeBranchTypes(args[1], args[2]) + } + if name == "case" { + if len(args) < 2 { + return Type{}, fmt.Errorf("case requires at least one condition/result pair") + } + withElse := len(args)%2 == 1 + lastPairEnd := len(args) + if withElse { + lastPairEnd-- + } + if lastPairEnd%2 != 0 { + return Type{}, fmt.Errorf("case requires condition/result pairs and an optional else result") + } + var result Type + for i := 0; i < lastPairEnd; i += 2 { + if args[i].Kind != KindBoolean || args[i].Cardinality == Many { + return Type{}, fmt.Errorf("case conditions must be scalar booleans") + } + if result.Kind == "" { + result = args[i+1] + continue + } + var err error + result, err = mergeBranchTypes(result, args[i+1]) + if err != nil { + return Type{}, err + } + } + if !withElse { + return mergeBranchTypes(result, NullType()) + } + return mergeBranchTypes(result, args[len(args)-1]) + } + if name == "not" { + if len(args) != 1 || args[0].Kind != KindBoolean || args[0].Cardinality == Many { + return Type{}, fmt.Errorf("not requires one scalar boolean") + } + return Type{Kind: KindBoolean, Cardinality: OptionalOne}, nil + } + if name == "and" || name == "or" { + if len(args) < 2 { + return Type{}, fmt.Errorf("%s requires at least two arguments", name) + } + for _, arg := range args { + if arg.Kind != KindBoolean || arg.Cardinality == Many { + return Type{}, fmt.Errorf("%s accepts only scalar booleans", name) + } + } + return Type{Kind: KindBoolean, Cardinality: OptionalOne}, nil + } + if name == "eq" || name == "neq" || name == "gt" || name == "gte" || name == "lt" || name == "lte" || name == "contains" { + if len(args) != 2 || args[0].Cardinality == Many || args[1].Cardinality == Many || !sameShape(args[0], args[1]) { + return Type{}, fmt.Errorf("%s requires two compatible scalar arguments", name) + } + if name == "contains" && args[0].Kind != KindString { + return Type{}, fmt.Errorf("contains requires strings") + } + return Type{Kind: KindBoolean, Cardinality: OptionalOne}, nil + } + return Type{}, fmt.Errorf("unsupported function %q", name) +} + +func allScalarKind(args []Type, kind ValueKind) bool { + for _, arg := range args { + if arg.Kind != kind || arg.Cardinality == Many { + return false + } + } + return true +} + +func sameShape(a, b Type) bool { + return a.Kind == b.Kind && a.Cardinality != Many && b.Cardinality != Many +} + +func mergeCardinality(a, b Cardinality) Cardinality { + if a == Many || b == Many { + return Many + } + if a == OptionalOne || b == OptionalOne { + return OptionalOne + } + return RequiredOne +} + +func optionalCardinality(optional bool) Cardinality { + if optional { + return OptionalOne + } + return RequiredOne +} + +func NullType() Type { return Type{Kind: KindNull, Cardinality: OptionalOne} } + +func mergeBranchTypes(a, b Type) (Type, error) { + if a.Kind == KindNull { + b.Cardinality = OptionalOne + return b, nil + } + if b.Kind == KindNull { + a.Cardinality = OptionalOne + return a, nil + } + if a.Kind != b.Kind || a.Cardinality == Many || b.Cardinality == Many || (a.Cardinality == Many) != (b.Cardinality == Many) { + return Type{}, fmt.Errorf("branches must have compatible scalar types, got %s and %s", a, b) + } + a.Cardinality = mergeCardinality(a.Cardinality, b.Cardinality) + return a, nil +} + +// Constructors keep callers from manufacturing malformed tagged nodes. diff --git a/internal/dataframe/expression/expression_test.go b/internal/dataframe/expression/expression_test.go new file mode 100644 index 0000000..aa35b8e --- /dev/null +++ b/internal/dataframe/expression/expression_test.go @@ -0,0 +1,105 @@ +package expression + +import ( + "strings" + "testing" +) + +func TestCheckSelectorUsesBoundSchemaType(t *testing.T) { + checked, err := Select(SelectorRef{Context: "root", Path: "id"}).Check(TypeContext{ + Selectors: map[string]Type{"root.id": {Kind: KindString, Cardinality: RequiredOne}}, + }) + if err != nil { + t.Fatalf("check selector: %v", err) + } + if checked.Type != (Type{Kind: KindString, Cardinality: RequiredOne}) { + t.Fatalf("type = %s", checked.Type) + } +} + +func TestCheckOperationTypes(t *testing.T) { + stringOne := Constant(Type{Kind: KindString, Cardinality: RequiredOne}, "x") + ctx := TypeContext{Selectors: map[string]Type{"root.alt": {Kind: KindString, Cardinality: OptionalOne}}} + tests := []struct { + name string + expr Expression + want Type + }{ + {"coalesce_string", Function("coalesce_string", Constant(Type{Kind: KindInteger, Cardinality: OptionalOne}, int64(3)), stringOne), Type{Kind: KindString, Cardinality: OptionalOne}}, + {"concat", Function("concat", stringOne, stringOne), Type{Kind: KindString, Cardinality: RequiredOne}}, + {"first", Function("first", Function("all", stringOne)), Type{Kind: KindString, Cardinality: OptionalOne}}, + {"uuid5", Function("uuid5", stringOne, stringOne), Type{Kind: KindUUID, Cardinality: RequiredOne}}, + {"case", Function("case", Constant(Type{Kind: KindBoolean, Cardinality: RequiredOne}, true), stringOne), Type{Kind: KindString, Cardinality: OptionalOne}}, + {"eq", Function("eq", stringOne, stringOne), Type{Kind: KindBoolean, Cardinality: OptionalOne}}, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + checked, err := test.expr.Check(ctx) + if err != nil { + t.Fatalf("check: %v", err) + } + if checked.Type != test.want { + t.Fatalf("type = %s, want %s", checked.Type, test.want) + } + }) + } +} + +func TestCheckRejectsInvalidArityAndTypes(t *testing.T) { + stringOne := Constant(Type{Kind: KindString, Cardinality: RequiredOne}, "x") + tests := []Expression{ + Function("concat"), + Function("join", stringOne, stringOne), + Function("uuid5", stringOne), + Function("unknown", stringOne), + Function("if", stringOne, stringOne, stringOne), + } + for i, expr := range tests { + if err := expr.Validate(TypeContext{}); err == nil { + t.Errorf("case %d unexpectedly passed", i) + } + } +} + +func TestCheckRejectsMalformedSelectorAndLiteral(t *testing.T) { + if err := Select(SelectorRef{Path: "root.id\" FOR x IN y"}).Validate(TypeContext{}); err == nil || !strings.Contains(err.Error(), "logical selector") { + t.Fatalf("malformed selector error = %v", err) + } + bad := Constant(Type{Kind: KindInteger, Cardinality: RequiredOne}, "not an integer") + if err := bad.Validate(TypeContext{}); err == nil || !strings.Contains(err.Error(), "incompatible") { + t.Fatalf("bad literal error = %v", err) + } +} + +func TestCheckUsesResolverAndLimitsTree(t *testing.T) { + checked, err := Select(SelectorRef{Context: "member", Path: "entity.reference"}).Check(TypeContext{ + Resolve: func(ref SelectorRef) (Type, error) { + if ref.String() != "member.entity.reference" { + t.Fatalf("resolved ref = %q", ref.String()) + } + return Type{Kind: KindString, Cardinality: OptionalOne}, nil + }, + }) + if err != nil || checked.Type.Kind != KindString { + t.Fatalf("resolver result = %#v, err=%v", checked, err) + } + + deep := Constant(Type{Kind: KindString, Cardinality: RequiredOne}, "x") + for i := 0; i < 4; i++ { + deep = Function("concat", deep) + } + if err := deep.Validate(TypeContext{MaxDepth: 2}); err == nil || !strings.Contains(err.Error(), "depth limit") { + t.Fatalf("depth error = %v", err) + } +} + +func TestManyLiteralValidatesElements(t *testing.T) { + good := Constant(Type{Kind: KindString, Cardinality: Many}, []string{"a", "b"}) + if err := good.Validate(TypeContext{}); err != nil { + t.Fatalf("many literal: %v", err) + } + bad := Constant(Type{Kind: KindString, Cardinality: Many}, []int{1}) + if err := bad.Validate(TypeContext{}); err == nil { + t.Fatal("incompatible many literal unexpectedly passed") + } +} diff --git a/internal/dataframe/expression/expression_types.go b/internal/dataframe/expression/expression_types.go new file mode 100644 index 0000000..574977f --- /dev/null +++ b/internal/dataframe/expression/expression_types.go @@ -0,0 +1,190 @@ +// Package expression contains the typed, backend-neutral expression AST used +// by dataframe recipes. It intentionally has no knowledge of AQL, SQL, or +// output names: a physical compiler consumes the checked tree later. +package expression + +import ( + "fmt" + "regexp" + "strings" +) + +// ValueKind is the logical type of an expression value. Cardinality is kept +// separately in Type so a repeated string and a scalar string remain visibly +// different to a compiler. +type ValueKind string + +const ( + KindNull ValueKind = "null" + KindBoolean ValueKind = "boolean" + KindInteger ValueKind = "integer" + KindDecimal ValueKind = "decimal" + KindString ValueKind = "string" + KindDate ValueKind = "date" + KindDateTime ValueKind = "date_time" + KindCode ValueKind = "code" + KindUUID ValueKind = "uuid" + // KindObject represents a structured value whose fields are resolved by a + // lexical recipe scope. It is intentionally backend-neutral; physical + // lowerers decide whether the object is a document, object expression, or + // relationship item. + KindObject ValueKind = "object" +) + +// Cardinality describes the number of values produced for one row context. +type Cardinality string + +const ( + RequiredOne Cardinality = "required_one" + OptionalOne Cardinality = "optional_one" + Many Cardinality = "many" +) + +func (c Cardinality) Valid() bool { + return c == RequiredOne || c == OptionalOne || c == Many +} + +func (c Cardinality) AllowsMany() bool { return c == Many } +func (c Cardinality) Optional() bool { return c == OptionalOne || c == Many } + +// Type is a logical expression type. Element is reserved for object-like +// types in later recipe versions; scalar arrays are represented by the same +// Kind with Cardinality=Many so functions can reason about them uniformly. +type Type struct { + Kind ValueKind `json:"kind"` + Cardinality Cardinality `json:"cardinality"` + Element *Type `json:"element,omitempty"` +} + +func (t Type) Valid() bool { + if t.Kind == "" || t.Cardinality == "" || !t.Cardinality.Valid() { + return false + } + switch t.Kind { + case KindNull, KindBoolean, KindInteger, KindDecimal, KindString, + KindDate, KindDateTime, KindCode, KindUUID, KindObject: + return t.Element == nil + default: + return false + } +} + +func (t Type) String() string { + if t.Kind == "" { + return "" + } + return string(t.Kind) + "/" + string(t.Cardinality) +} + +func scalar(kind ValueKind) Type { return Type{Kind: kind, Cardinality: RequiredOne} } + +// NullBehavior makes null handling part of semantic meaning rather than an +// incidental renderer choice. +type NullBehavior string + +const ( + NullPropagate NullBehavior = "propagate" + NullEmpty NullBehavior = "empty" + NullError NullBehavior = "error" +) + +func (n NullBehavior) Valid() bool { + return n == "" || n == NullPropagate || n == NullEmpty || n == NullError +} + +// NodeKind identifies a node in the expression tree. +type NodeKind string + +const ( + SelectorNode NodeKind = "selector" + LiteralNode NodeKind = "literal" + CallNode NodeKind = "call" +) + +// SelectorRef identifies a value in a named row context. The path is a +// logical selector, not a query-language fragment. +type SelectorRef struct { + Context string `json:"context,omitempty"` + Path string `json:"path"` +} + +func (s SelectorRef) String() string { + if strings.TrimSpace(s.Context) == "" { + return strings.TrimSpace(s.Path) + } + return strings.TrimSpace(s.Context) + "." + strings.TrimPrefix(strings.TrimSpace(s.Path), ".") +} + +var selectorNamePattern = regexp.MustCompile(`^[A-Za-z_][A-Za-z0-9_.\[\]-]*$`) + +func (s SelectorRef) Validate() error { + if strings.TrimSpace(s.Path) == "" { + return fmt.Errorf("selector path is required") + } + if strings.TrimSpace(s.Context) != "" && !selectorNamePattern.MatchString(s.Context) { + return fmt.Errorf("selector context %q is not a logical name", s.Context) + } + path := strings.TrimSpace(s.Path) + if !selectorNamePattern.MatchString(path) || strings.Contains(path, "..") || strings.Contains(path, "[][") { + return fmt.Errorf("selector path %q is not a logical selector", s.Path) + } + return nil +} + +// Literal is a typed constant. Value is intentionally opaque to this package +// after type validation, which keeps physical encodings out of the AST. +type Literal struct { + Type Type `json:"type"` + Value any `json:"value,omitempty"` +} + +// Call is a named generic function. Target is only used by cast and is kept +// as a type rather than accepting a backend or language type name. +type Call struct { + Name string `json:"name"` + Args []Expression `json:"args,omitempty"` + Target *Type `json:"target,omitempty"` +} + +// Expression is a lowering-neutral AST node. Type is an optional annotation; +// Check recomputes it and rejects a conflicting annotation. +type Expression struct { + Kind NodeKind `json:"kind"` + Type Type `json:"type,omitempty"` + NullBehavior NullBehavior `json:"nullBehavior,omitempty"` + Selector *SelectorRef `json:"selector,omitempty"` + Literal *Literal `json:"literal,omitempty"` + Call *Call `json:"call,omitempty"` +} + +// SelectorResolver supplies schema-aware types without coupling this package +// to a particular generated FHIR schema or storage backend. +type SelectorResolver func(SelectorRef) (Type, error) + +// TypeContext is the environment used to type-check selectors. Selectors map +// keys may be either SelectorRef.String() or the path alone. +type TypeContext struct { + Selectors map[string]Type + Resolve SelectorResolver + MaxDepth int + MaxNodes int +} + +// CheckedExpression is an expression plus its inferred semantic result type. +type CheckedExpression struct { + Expression Expression + Type Type +} + +// Check validates an expression tree and returns its inferred type. +func Select(ref SelectorRef) Expression { + return Expression{Kind: SelectorNode, Selector: &ref, NullBehavior: NullPropagate} +} + +func Constant(t Type, value any) Expression { + return Expression{Kind: LiteralNode, Literal: &Literal{Type: t, Value: value}, NullBehavior: NullPropagate} +} + +func Function(name string, args ...Expression) Expression { + return Expression{Kind: CallNode, Call: &Call{Name: name, Args: args}, NullBehavior: NullPropagate} +} diff --git a/internal/dataframe/expression/recipe_adapter.go b/internal/dataframe/expression/recipe_adapter.go new file mode 100644 index 0000000..c816c33 --- /dev/null +++ b/internal/dataframe/expression/recipe_adapter.go @@ -0,0 +1,170 @@ +package expression + +import ( + "encoding/json" + "fmt" + "strconv" + "strings" + + "github.com/calypr/loom/internal/dataframe/recipe" +) + +// FromRecipe converts the persisted wire expression into the typed AST. Type +// checking remains a separate operation because selector types come from the +// active generated schema/catalog. +func FromRecipe(input recipe.Expression) (Expression, error) { + if input.Select != "" { + parts := strings.SplitN(input.Select, ".", 2) + ref := SelectorRef{Path: input.Select} + if len(parts) == 2 && isContextName(parts[0]) { + ref.Context, ref.Path = parts[0], parts[1] + } + return Select(ref), nil + } + if input.Literal != nil { + literal, err := recipeLiteral(input.Literal) + if err != nil { + return Expression{}, err + } + return Constant(literal.Type, literal.Value), nil + } + if input.Call == "" { + return Expression{}, fmt.Errorf("recipe expression is empty") + } + call := Call{Name: input.Call} + for _, arg := range input.Args { + converted, err := FromRecipe(arg) + if err != nil { + return Expression{}, err + } + call.Args = append(call.Args, converted) + } + if strings.EqualFold(input.Call, "cast") && len(input.Args) == 2 && input.Args[1].Literal != nil { + var targetName string + if err := json.Unmarshal(input.Args[1].Literal, &targetName); err == nil { + target := typeForName(targetName) + if target.Kind != "" { + call.Target = &target + } + } + } + return Expression{Kind: CallNode, Call: &call, NullBehavior: NullPropagate}, nil +} + +// FromRecipeInContexts is the scope-aware variant used by semantic recipe +// lowering. The wire format keeps selectors as strings; this helper turns a +// leading lexical alias into SelectorRef.Context while rejecting aliases that +// are not visible at the current recipe node. An unqualified selector is +// resolved by the caller's root scope. +func FromRecipeInContexts(input recipe.Expression, contexts map[string]struct{}) (Expression, error) { + expr, err := FromRecipe(input) + if err != nil { + return Expression{}, err + } + var rewrite func(*Expression) error + rewrite = func(node *Expression) error { + if node == nil { + return nil + } + if node.Selector != nil { + ref := node.Selector + if ref.Context == "" { + parts := strings.SplitN(ref.Path, ".", 2) + if len(parts) == 2 { + if _, ok := contexts[parts[0]]; ok { + ref.Context, ref.Path = parts[0], parts[1] + } + } + } else if len(contexts) > 0 { + if _, ok := contexts[ref.Context]; !ok { + return fmt.Errorf("selector context %q is not in scope", ref.Context) + } + } + return ref.Validate() + } + if node.Call != nil { + for i := range node.Call.Args { + if err := rewrite(&node.Call.Args[i]); err != nil { + return err + } + } + } + return nil + } + if err := rewrite(&expr); err != nil { + return Expression{}, err + } + return expr, nil +} + +func isContextName(value string) bool { return value == "root" || value == "item" } + +func recipeLiteral(raw json.RawMessage) (Literal, error) { + dec := json.NewDecoder(strings.NewReader(string(raw))) + dec.UseNumber() + var value any + if err := dec.Decode(&value); err != nil { + return Literal{}, err + } + switch v := value.(type) { + case nil: + return Literal{Type: Type{Kind: KindNull, Cardinality: OptionalOne}}, nil + case bool: + return Literal{Type: scalar(KindBoolean), Value: v}, nil + case string: + return Literal{Type: scalar(KindString), Value: v}, nil + case json.Number: + parsed, err := strconv.ParseFloat(v.String(), 64) + if err != nil { + return Literal{}, err + } + return Literal{Type: scalar(KindDecimal), Value: parsed}, nil + case []any: + kind := KindString + if len(v) > 0 { + switch v[0].(type) { + case bool: + kind = KindBoolean + case json.Number: + kind = KindDecimal + } + } + if kind == KindDecimal { + converted := make([]any, len(v)) + for i, item := range v { + number, err := strconv.ParseFloat(item.(json.Number).String(), 64) + if err != nil { + return Literal{}, err + } + converted[i] = number + } + v = converted + } + return Literal{Type: Type{Kind: kind, Cardinality: Many}, Value: v}, nil + default: + return Literal{}, fmt.Errorf("unsupported recipe literal %T", value) + } +} + +func typeForName(name string) Type { + switch strings.ToLower(strings.TrimSpace(name)) { + case "string": + return scalar(KindString) + case "integer", "int": + return scalar(KindInteger) + case "decimal", "float": + return scalar(KindDecimal) + case "boolean", "bool": + return scalar(KindBoolean) + case "date": + return scalar(KindDate) + case "date_time", "datetime": + return scalar(KindDateTime) + case "code": + return scalar(KindCode) + case "uuid": + return scalar(KindUUID) + default: + return Type{} + } +} diff --git a/internal/dataframe/expression/recipe_adapter_test.go b/internal/dataframe/expression/recipe_adapter_test.go new file mode 100644 index 0000000..9a3c87f --- /dev/null +++ b/internal/dataframe/expression/recipe_adapter_test.go @@ -0,0 +1,26 @@ +package expression + +import ( + "testing" + + "github.com/calypr/loom/internal/dataframe/recipe" +) + +func TestFromRecipeBuildsTypedAST(t *testing.T) { + input := recipe.Expression{Call: "reference_id", Args: []recipe.Expression{{Select: "root.subject.reference"}}} + expr, err := FromRecipe(input) + if err != nil { + t.Fatal(err) + } + if expr.Kind != CallNode || expr.Call == nil || expr.Call.Name != "reference_id" || expr.Call.Args[0].Selector.Context != "root" { + t.Fatalf("unexpected expression: %#v", expr) + } + ctx := TypeContext{Selectors: map[string]Type{"root.subject.reference": Type{Kind: KindString, Cardinality: OptionalOne}}} + checked, err := expr.Check(ctx) + if err != nil { + t.Fatal(err) + } + if checked.Type.Kind != KindString { + t.Fatalf("type = %s", checked.Type) + } +} diff --git a/internal/dataframe/materialization/arango/bundle_registry.go b/internal/dataframe/materialization/arango/bundle_registry.go new file mode 100644 index 0000000..243cf60 --- /dev/null +++ b/internal/dataframe/materialization/arango/bundle_registry.go @@ -0,0 +1,138 @@ +package arango + +import ( + "context" + "encoding/json" + "time" + + "github.com/calypr/loom/internal/dataframe/materialization" +) + +const ( + BundleExecutionsCollection = "loom_dataframe_bundle_executions" + BundlePointersCollection = "loom_dataframe_bundle_pointers" +) + +// AQLExecutor is implemented by Loom's concrete Arango client. Keeping it +// optional preserves the small fake Client used by existing registry tests. +type AQLExecutor interface { + ExecuteAQL(context.Context, string, map[string]interface{}) error +} + +func (r *Registry) SaveExecution(ctx context.Context, execution materialization.BundleExecution) error { + data, err := json.Marshal(execution) + if err != nil { + return err + } + var doc map[string]any + if err := json.Unmarshal(data, &doc); err != nil { + return err + } + doc["_key"] = execution.ID + data, err = json.Marshal(doc) + if err != nil { + return err + } + return r.client.InsertBatchRaw(ctx, BundleExecutionsCollection, []json.RawMessage{data}, true, "document") +} + +func (r *Registry) GetExecution(ctx context.Context, id string) (materialization.BundleExecution, error) { + return r.loadExecution(ctx, `FOR doc IN @@collection FILTER doc._key == @key RETURN doc`, map[string]interface{}{"@collection": BundleExecutionsCollection, "key": id}) +} + +func (r *Registry) FindExecutionByKey(ctx context.Context, key string) (materialization.BundleExecution, error) { + return r.loadExecution(ctx, `FOR doc IN @@collection FILTER doc.key == @key SORT doc.createdAt DESC LIMIT 1 RETURN doc`, map[string]interface{}{"@collection": BundleExecutionsCollection, "key": key}) +} + +func (r *Registry) ListExecutions(ctx context.Context, state materialization.BundleState, before time.Time) ([]materialization.BundleExecution, error) { + out := []materialization.BundleExecution{} + err := r.client.QueryRows(ctx, `FOR doc IN @@collection FILTER doc.state == @state AND doc.updatedAt < @before SORT doc.updatedAt ASC RETURN doc`, r.batchSize, map[string]interface{}{"@collection": BundleExecutionsCollection, "state": state, "before": before}, func(row map[string]any) error { + data, err := json.Marshal(row) + if err != nil { + return err + } + var execution materialization.BundleExecution + if err := json.Unmarshal(data, &execution); err != nil { + return err + } + out = append(out, execution) + return nil + }) + return out, err +} + +func (r *Registry) loadExecution(ctx context.Context, query string, vars map[string]interface{}) (materialization.BundleExecution, error) { + var found *materialization.BundleExecution + err := r.client.QueryRows(ctx, query, r.batchSize, vars, func(row map[string]any) error { + data, err := json.Marshal(row) + if err != nil { + return err + } + var execution materialization.BundleExecution + if err := json.Unmarshal(data, &execution); err != nil { + return err + } + found = &execution + return nil + }) + if err != nil { + return materialization.BundleExecution{}, err + } + if found == nil { + return materialization.BundleExecution{}, materialization.ErrBundleNotFound + } + return *found, nil +} + +func (r *Registry) GetPointer(ctx context.Context, name string) (materialization.BundlePointer, error) { + var found *materialization.BundlePointer + err := r.client.QueryRows(ctx, `FOR doc IN @@collection FILTER doc._key == @key RETURN doc`, r.batchSize, map[string]interface{}{"@collection": BundlePointersCollection, "key": name}, func(row map[string]any) error { + data, err := json.Marshal(row) + if err != nil { + return err + } + var pointer materialization.BundlePointer + if err := json.Unmarshal(data, &pointer); err != nil { + return err + } + found = &pointer + return nil + }) + if err != nil { + return materialization.BundlePointer{}, err + } + if found == nil { + return materialization.BundlePointer{}, materialization.ErrBundleNotFound + } + return *found, nil +} + +// CompareAndSwapPointer performs the visibility update in Arango. The query +// returns a small object so the generic QueryRows client can inspect whether +// the expected version won the race. +func (r *Registry) CompareAndSwapPointer(ctx context.Context, name, expected, next string) error { + var result struct { + Updated bool `json:"updated"` + } + updated := false + err := r.client.QueryRows(ctx, `LET existing = FIRST(FOR doc IN @@collection FILTER doc._key == @key RETURN doc) +LET changed = existing == null ? (INSERT {_key: @key, name: @key, executionId: @next, updatedAt: @updatedAt} IN @@collection RETURN true) : (existing.executionId == @expected ? (UPDATE existing WITH {executionId: @next, updatedAt: @updatedAt} IN @@collection RETURN true) : [false]) +RETURN {updated: FIRST(changed)}`, r.batchSize, map[string]interface{}{"@collection": BundlePointersCollection, "key": name, "expected": expected, "next": next, "updatedAt": time.Now().UTC()}, func(row map[string]any) error { + data, err := json.Marshal(row) + if err != nil { + return err + } + return json.Unmarshal(data, &result) + }) + if err != nil { + return err + } + updated = result.Updated + if !updated { + return materialization.ErrBundlePointerConflict + } + return nil +} + +var _ materialization.BundleCatalog = (*Registry)(nil) +var _ materialization.StaleBundleCatalog = (*Registry)(nil) diff --git a/internal/dataframe/materialization/arango/registry.go b/internal/dataframe/materialization/arango/registry.go index dafaacf..049f423 100644 --- a/internal/dataframe/materialization/arango/registry.go +++ b/internal/dataframe/materialization/arango/registry.go @@ -32,6 +32,12 @@ func BootstrapSpec() arangostore.BootstrapSpec { return arangostore.BootstrapSpec{Collections: []arangostore.CollectionSpec{{ Name: Collection, Indexes: [][]string{{"project", "state"}, {"project", "datasetGeneration"}, {"state"}}, + }, { + Name: BundleExecutionsCollection, + Indexes: [][]string{{"key"}, {"state"}, {"project", "datasetGeneration", "name", "state"}}, + }, { + Name: BundlePointersCollection, + Indexes: [][]string{{"executionId"}}, }}} } diff --git a/internal/dataframe/materialization/bundle.go b/internal/dataframe/materialization/bundle.go new file mode 100644 index 0000000..e8d1a8c --- /dev/null +++ b/internal/dataframe/materialization/bundle.go @@ -0,0 +1,266 @@ +package materialization + +import ( + "context" + "crypto/sha256" + "encoding/hex" + "encoding/json" + "fmt" + "strings" + "time" + + "github.com/calypr/loom/internal/authscope" + "github.com/calypr/loom/internal/store/clickhouse" +) + +// BundleOutput is a fully resolved output stream ready for publication. The +// producer is responsible for consuming one resolved physical plan per +// output; this package only owns the all-or-nothing publication boundary. +type BundleOutput struct { + Name string + Columns []Column + Rows []map[string]any +} + +// StreamOutput is the bounded-memory input to PublishStreamBundle. The +// callback must call visit once for each logical row and stop when it returns +// an error. +type StreamOutput struct { + Name string + Columns []clickhouse.Column + Stream func(context.Context, func(map[string]any) error) error +} + +// StreamPublishConfig controls row and byte buffering during publication. +type StreamPublishConfig struct { + BatchRows int + BatchBytes int +} + +type AtomicBundleStore interface { + BeginBundle(context.Context) (AtomicBundleTx, error) +} + +type AtomicBundleTx interface { + CreateOutput(context.Context, string, []clickhouse.Column) error + InsertRows(context.Context, string, []clickhouse.Column, []map[string]any) error + Commit(context.Context) error + Rollback(context.Context) error +} + +// BundleState is the durable lifecycle of a multi-output publication. The +// physical ClickHouse tables are never reader-visible until the logical +// pointer is advanced to READY. +type BundleState string + +const ( + BundlePending BundleState = "PENDING" + BundlePreflight BundleState = "PREFLIGHT" + BundleLoading BundleState = "LOADING" + BundleValidating BundleState = "VALIDATING" + BundleReady BundleState = "READY" + BundleFailed BundleState = "FAILED" +) + +type BundleIdentity struct { + Name, Project, DatasetGeneration string + RecipeDigest, SchemaDigest string + ScopeDigest, EngineVersion string + AuthResourcePaths []string `json:"authResourcePaths,omitempty"` +} + +// PointerName is the visibility namespace for a published logical dataset. +// Project and generation are part of the key so two tenants can publish the +// same recipe/output name without racing a shared pointer. +func (i BundleIdentity) PointerName() string { + return strings.Join([]string{i.Project, i.DatasetGeneration, i.Name}, "\x00") +} + +func (i BundleIdentity) Key() string { + b, _ := json.Marshal(i) + sum := sha256.Sum256(b) + return hex.EncodeToString(sum[:]) +} + +type BundleOutputRecord struct { + Name, Alias, PhysicalTable string + Columns []Column + RowCount, ByteCount int64 + State BundleState +} + +type BundleExecution struct { + ID string `json:"id"` + Key string `json:"key"` + BundleIdentity + State BundleState `json:"state"` + Outputs []BundleOutputRecord `json:"outputs,omitempty"` + CreatedAt time.Time `json:"createdAt"` + UpdatedAt time.Time `json:"updatedAt"` + ReadyAt *time.Time `json:"readyAt,omitempty"` + Error string `json:"error,omitempty"` +} + +type BundlePointer struct { + Name string `json:"name"` + ExecutionID string `json:"executionId"` + UpdatedAt time.Time `json:"updatedAt"` +} + +// StaleBundleCatalog is optional so existing lightweight registries remain +// source-compatible. Production catalogs implement it for startup recovery. +type StaleBundleCatalog interface { + ListExecutions(context.Context, BundleState, time.Time) ([]BundleExecution, error) +} + +// BundleCatalog is the durable metadata/pointer boundary. Implementations +// should make CompareAndSwapPointer atomic in their backing store. +type BundleCatalog interface { + SaveExecution(context.Context, BundleExecution) error + GetExecution(context.Context, string) (BundleExecution, error) + FindExecutionByKey(context.Context, string) (BundleExecution, error) + GetPointer(context.Context, string) (BundlePointer, error) + CompareAndSwapPointer(context.Context, string, string, string) error +} + +// ResolvePublishedOutput resolves the current READY output for one logical +// dataset. The alias currently defaults to the output name; publication code +// may set BundleOutputRecord.Alias when an Explorer-facing alias differs. +func ResolvePublishedOutput(ctx context.Context, catalog BundleCatalog, project, generation, alias string) (Materialization, error) { + if catalog == nil { + return Materialization{}, fmt.Errorf("bundle catalog is required") + } + listed, ok := catalog.(StaleBundleCatalog) + if !ok { + return Materialization{}, fmt.Errorf("bundle catalog does not support dataset resolution") + } + executions, err := listed.ListExecutions(ctx, BundleReady, time.Now().UTC().Add(time.Second)) + if err != nil { + return Materialization{}, err + } + var newest *BundleExecution + for index := range executions { + execution := executions[index] + if execution.Project != project || execution.DatasetGeneration != generation || execution.State != BundleReady { + continue + } + pointer, pointerErr := catalog.GetPointer(ctx, execution.PointerName()) + if pointerErr != nil || pointer.ExecutionID != execution.ID { + continue + } + for _, output := range execution.Outputs { + outputAlias := output.Alias + if outputAlias == "" { + outputAlias = output.Name + } + if outputAlias == alias && (newest == nil || execution.UpdatedAt.After(newest.UpdatedAt)) { + copy := execution + newest = © + break + } + } + } + if newest != nil { + for _, output := range newest.Outputs { + outputAlias := output.Alias + if outputAlias == "" { + outputAlias = output.Name + } + if outputAlias == alias { + return publishedMaterialization(*newest, output, outputAlias), nil + } + } + } + return Materialization{}, fmt.Errorf("published dataset %q was not found for project %q and generation %q", alias, project, generation) +} + +// ListPublishedOutputs lists READY outputs for one project and generation when +// the catalog supports execution listing. Production catalogs do; lightweight +// test catalogs may only support direct resolution. +func ListPublishedOutputs(ctx context.Context, catalog BundleCatalog, project, generation string) ([]Materialization, error) { + listed, ok := catalog.(StaleBundleCatalog) + if !ok { + return nil, fmt.Errorf("bundle catalog does not support dataset listing") + } + executions, err := listed.ListExecutions(ctx, BundleReady, time.Now().UTC().Add(time.Second)) + if err != nil { + return nil, err + } + result := make([]Materialization, 0) + for _, execution := range executions { + if execution.Project != project || execution.DatasetGeneration != generation || execution.State != BundleReady { + continue + } + pointer, pointerErr := catalog.GetPointer(ctx, execution.PointerName()) + if pointerErr != nil || pointer.ExecutionID != execution.ID { + continue + } + for _, output := range execution.Outputs { + alias := output.Alias + if alias == "" { + alias = output.Name + } + result = append(result, publishedMaterialization(execution, output, alias)) + } + } + return result, nil +} + +func publishedMaterialization(execution BundleExecution, output BundleOutputRecord, alias string) Materialization { + return Materialization{ID: execution.ID + ":" + output.Name, Name: alias, Revision: execution.ID, Project: execution.Project, DatasetGeneration: execution.DatasetGeneration, State: StateReady, AuthScopeMode: authScopeMode(execution.AuthResourcePaths), AuthResourcePaths: append([]string(nil), execution.AuthResourcePaths...), Columns: output.Columns, PhysicalTable: output.PhysicalTable, RowCount: output.RowCount, CreatedAt: execution.CreatedAt, ReadyAt: execution.ReadyAt} +} + +func authScopeMode(paths []string) authscope.ReadScopeMode { + if len(paths) == 0 { + return authscope.ReadScopeUnrestricted + } + return authscope.ReadScopeRestricted +} + +var ErrBundleNotFound = fmt.Errorf("bundle execution not found") +var ErrBundlePointerConflict = fmt.Errorf("bundle pointer compare-and-swap conflict") + +// PublishBundle stages every output in one backend transaction and commits +// only after all outputs have been created and loaded. A failed output rolls +// back the entire bundle, so no partial READY set can be observed. +func PublishBundle(ctx context.Context, store AtomicBundleStore, outputs []BundleOutput) error { + if store == nil { + return fmt.Errorf("atomic bundle store is required") + } + if len(outputs) == 0 { + return fmt.Errorf("bundle must contain at least one output") + } + tx, err := store.BeginBundle(ctx) + if err != nil { + return err + } + rollback := func(cause error) error { + if rollbackErr := tx.Rollback(context.Background()); rollbackErr != nil { + return fmt.Errorf("%w (bundle rollback failed: %v)", cause, rollbackErr) + } + return cause + } + seen := map[string]struct{}{} + for _, output := range outputs { + if strings.TrimSpace(output.Name) == "" { + return rollback(fmt.Errorf("bundle output name is required")) + } + if _, ok := seen[output.Name]; ok { + return rollback(fmt.Errorf("bundle output %q is duplicated", output.Name)) + } + seen[output.Name] = struct{}{} + columns := toClickHouseColumns(output.Columns) + if err := tx.CreateOutput(ctx, output.Name, columns); err != nil { + return rollback(fmt.Errorf("output %q create: %w", output.Name, err)) + } + if len(output.Rows) > 0 { + if err := tx.InsertRows(ctx, output.Name, columns, output.Rows); err != nil { + return rollback(fmt.Errorf("output %q insert: %w", output.Name, err)) + } + } + } + if err := tx.Commit(ctx); err != nil { + return rollback(fmt.Errorf("bundle commit: %w", err)) + } + return nil +} diff --git a/internal/dataframe/materialization/bundle_test.go b/internal/dataframe/materialization/bundle_test.go new file mode 100644 index 0000000..1185f56 --- /dev/null +++ b/internal/dataframe/materialization/bundle_test.go @@ -0,0 +1,38 @@ +package materialization + +import ( + "context" + "testing" + + "github.com/calypr/loom/internal/store/clickhouse" +) + +type bundleTx struct { + committed, rolledBack bool + failInsert bool +} + +func (t *bundleTx) CreateOutput(context.Context, string, []clickhouse.Column) error { return nil } +func (t *bundleTx) InsertRows(context.Context, string, []clickhouse.Column, []map[string]any) error { + if t.failInsert { + return context.Canceled + } + return nil +} +func (t *bundleTx) Commit(context.Context) error { t.committed = true; return nil } +func (t *bundleTx) Rollback(context.Context) error { t.rolledBack = true; return nil } + +type bundleStore struct{ tx *bundleTx } + +func (s *bundleStore) BeginBundle(context.Context) (AtomicBundleTx, error) { + s.tx = &bundleTx{failInsert: true} + return s.tx, nil +} + +func TestPublishBundleRollsBackAllOutputsOnFailure(t *testing.T) { + store := &bundleStore{} + err := PublishBundle(context.Background(), store, []BundleOutput{{Name: "a", Rows: []map[string]any{{"x": 1}}}, {Name: "b"}}) + if err == nil || !store.tx.rolledBack || store.tx.committed { + t.Fatalf("bundle was not rolled back: err=%v tx=%#v", err, store.tx) + } +} diff --git a/internal/dataframe/materialization/clickhouse_bundle.go b/internal/dataframe/materialization/clickhouse_bundle.go new file mode 100644 index 0000000..e9deced --- /dev/null +++ b/internal/dataframe/materialization/clickhouse_bundle.go @@ -0,0 +1,323 @@ +package materialization + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "regexp" + "strings" + "sync" + "time" + + "github.com/calypr/loom/internal/store/clickhouse" + "github.com/google/uuid" +) + +var ( + ErrBundleInFlight = errors.New("identical bundle execution is already in flight") + ErrBundleAlreadyReady = errors.New("identical bundle execution is already ready") +) + +// IdentityBundleStore is implemented by the production adapter. The older +// AtomicBundleStore interface remains available for fixtures and callers that +// do not yet have recipe provenance. +type IdentityBundleStore interface { + AtomicBundleStore + BeginBundleFor(context.Context, BundleIdentity) (AtomicBundleTx, error) +} + +// ClickHouseBundleStore publishes staged tables by advancing a durable logical +// pointer in BundleCatalog. ClickHouse itself has no cross-table transaction; +// the pointer is therefore the visibility boundary. +type ClickHouseBundleStore struct { + ClickHouse BundleClickHouseStore + Catalog BundleCatalog + Prefix string + mu sync.Mutex +} + +type BundleClickHouseStore interface { + CreateTable(context.Context, string, []clickhouse.Column) error + InsertRows(context.Context, string, []clickhouse.Column, []map[string]any) error + DropTable(context.Context, string) error +} + +func NewClickHouseBundleStore(client BundleClickHouseStore, catalog BundleCatalog) (*ClickHouseBundleStore, error) { + if client == nil || catalog == nil { + return nil, fmt.Errorf("ClickHouse client and bundle catalog are required") + } + return &ClickHouseBundleStore{ClickHouse: client, Catalog: catalog, Prefix: "loom_bundle"}, nil +} + +func (s *ClickHouseBundleStore) BeginBundle(ctx context.Context) (AtomicBundleTx, error) { + identity := BundleIdentity{Name: "anonymous-" + uuid.NewString(), EngineVersion: "loom"} + return s.BeginBundleFor(ctx, identity) +} + +func (s *ClickHouseBundleStore) BeginBundleFor(ctx context.Context, identity BundleIdentity) (AtomicBundleTx, error) { + if strings.TrimSpace(identity.Name) == "" { + return nil, fmt.Errorf("bundle name is required") + } + if identity.EngineVersion == "" { + identity.EngineVersion = "loom" + } + key := identity.Key() + if existing, err := s.Catalog.FindExecutionByKey(ctx, key); err == nil { + switch existing.State { + case BundleReady: + return &clickHouseBundleTx{store: s, execution: existing, idempotent: true}, nil + case BundlePending, BundlePreflight, BundleLoading, BundleValidating: + return nil, fmt.Errorf("%w: %s", ErrBundleInFlight, existing.ID) + } + } else if !errors.Is(err, ErrBundleNotFound) { + return nil, err + } + id := uuid.NewString() + now := time.Now().UTC() + execution := BundleExecution{ID: id, Key: key, BundleIdentity: identity, State: BundlePending, CreatedAt: now, UpdatedAt: now} + if err := s.Catalog.SaveExecution(ctx, execution); err != nil { + return nil, err + } + execution.State = BundleLoading + execution.UpdatedAt = time.Now().UTC() + if err := s.Catalog.SaveExecution(ctx, execution); err != nil { + return nil, err + } + return &clickHouseBundleTx{store: s, execution: execution, expectedPointer: s.pointer(ctx, identity.PointerName())}, nil +} + +func (s *ClickHouseBundleStore) pointer(ctx context.Context, name string) string { + p, err := s.Catalog.GetPointer(ctx, name) + if err != nil { + return "" + } + return p.ExecutionID +} + +type clickHouseBundleTx struct { + store *ClickHouseBundleStore + execution BundleExecution + expectedPointer string + idempotent bool + closed bool +} + +func (t *clickHouseBundleTx) CreateOutput(ctx context.Context, name string, columns []clickhouse.Column) error { + if t.idempotent { + return nil + } + if t.closed { + return fmt.Errorf("bundle transaction is closed") + } + if !validBundleOutput(name) { + return fmt.Errorf("invalid bundle output name %q", name) + } + for _, output := range t.execution.Outputs { + if output.Name == name { + return fmt.Errorf("bundle output %q is duplicated", name) + } + } + if len(columns) == 0 { + return fmt.Errorf("bundle output %q has no columns", name) + } + columns = withRowIdentityColumn(columns) + for _, c := range columns { + if c.Name == "__loom_row_id" { + continue + } + if err := validateSchemaColumn(Column{Name: c.Name, ClickHouse: c.Type}); err != nil { + return err + } + } + table := t.tableName(name) + if err := t.store.ClickHouse.CreateTable(ctx, table, columns); err != nil { + return err + } + converted := make([]Column, len(columns)) + for i, c := range columns { + converted[i] = Column{Name: c.Name, ClickHouse: c.Type} + } + t.execution.Outputs = append(t.execution.Outputs, BundleOutputRecord{Name: name, PhysicalTable: table, Columns: converted, State: BundleLoading}) + return t.save(ctx) +} + +func (t *clickHouseBundleTx) InsertRows(ctx context.Context, name string, columns []clickhouse.Column, rows []map[string]any) error { + if t.idempotent || len(rows) == 0 { + return nil + } + if t.closed { + return fmt.Errorf("bundle transaction is closed") + } + idx := t.outputIndex(name) + if idx < 0 { + return fmt.Errorf("bundle output %q was not created", name) + } + record := &t.execution.Outputs[idx] + effectiveColumns := columns + if len(columns)+1 == len(record.Columns) && record.Columns[0].Name == "__loom_row_id" { + effectiveColumns = make([]clickhouse.Column, 0, len(columns)+1) + effectiveColumns = append(effectiveColumns, clickhouse.Column{Name: "__loom_row_id", Type: record.Columns[0].ClickHouse}) + effectiveColumns = append(effectiveColumns, columns...) + rows = addGeneratedRowIdentities(rows, record.RowCount) + } + if len(effectiveColumns) != len(record.Columns) { + return fmt.Errorf("output %q schema changed after preflight", name) + } + for i, c := range effectiveColumns { + if record.Columns[i].Name != c.Name || record.Columns[i].ClickHouse != c.Type { + return fmt.Errorf("output %q schema changed after preflight", name) + } + } + if err := t.store.ClickHouse.InsertRows(ctx, record.PhysicalTable, effectiveColumns, rows); err != nil { + return err + } + record.RowCount += int64(len(rows)) + for _, row := range rows { + encoded, _ := json.Marshal(row) + record.ByteCount += int64(len(encoded)) + } + return t.save(ctx) +} + +func withRowIdentityColumn(columns []clickhouse.Column) []clickhouse.Column { + for _, column := range columns { + if column.Name == "__loom_row_id" { + return append([]clickhouse.Column(nil), columns...) + } + } + result := make([]clickhouse.Column, 0, len(columns)+1) + result = append(result, clickhouse.Column{Name: "__loom_row_id", Type: "String"}) + return append(result, columns...) +} + +func addGeneratedRowIdentities(rows []map[string]any, start int64) []map[string]any { + result := make([]map[string]any, len(rows)) + for index, row := range rows { + copy := cloneBundleRow(row) + if _, ok := copy["__loom_row_id"]; !ok { + copy["__loom_row_id"] = fmt.Sprintf("%d", start+int64(index)) + } + result[index] = copy + } + return result +} + +func cloneBundleRow(row map[string]any) map[string]any { + copy := make(map[string]any, len(row)+1) + for key, value := range row { + copy[key] = value + } + return copy +} + +func (t *clickHouseBundleTx) Commit(ctx context.Context) error { + if t.idempotent { + t.closed = true + return nil + } + if t.closed { + return fmt.Errorf("bundle transaction is closed") + } + if len(t.execution.Outputs) == 0 { + return t.fail(ctx, fmt.Errorf("bundle has no outputs")) + } + t.execution.State = BundleValidating + if err := t.save(ctx); err != nil { + return err + } + for i := range t.execution.Outputs { + if t.execution.Outputs[i].RowCount < 0 { + return t.fail(ctx, fmt.Errorf("output %q has invalid row count", t.execution.Outputs[i].Name)) + } + t.execution.Outputs[i].State = BundleReady + } + if err := t.save(ctx); err != nil { + return err + } + now := time.Now().UTC() + t.execution.State, t.execution.ReadyAt, t.execution.UpdatedAt = BundleReady, &now, now + if err := t.save(ctx); err != nil { + return err + } + if err := t.store.Catalog.CompareAndSwapPointer(ctx, t.execution.PointerName(), t.expectedPointer, t.execution.ID); err != nil { + return t.fail(ctx, fmt.Errorf("publish bundle pointer: %w", err)) + } + t.closed = true + return nil +} + +func (t *clickHouseBundleTx) Rollback(ctx context.Context) error { + if t.idempotent || t.closed { + return nil + } + var first error + for _, output := range t.execution.Outputs { + if err := t.store.ClickHouse.DropTable(context.Background(), output.PhysicalTable); err != nil && first == nil { + first = err + } + } + if err := t.fail(ctx, fmt.Errorf("bundle rolled back")); err != nil && first == nil { + first = err + } + t.closed = true + return first +} + +func (t *clickHouseBundleTx) outputIndex(name string) int { + for i := range t.execution.Outputs { + if t.execution.Outputs[i].Name == name { + return i + } + } + return -1 +} + +func (t *clickHouseBundleTx) tableName(output string) string { + return t.store.Prefix + "_" + strings.ReplaceAll(t.execution.ID, "-", "") + "_" + output +} + +func (t *clickHouseBundleTx) save(ctx context.Context) error { + t.execution.UpdatedAt = time.Now().UTC() + return t.store.Catalog.SaveExecution(ctx, t.execution) +} + +func (t *clickHouseBundleTx) fail(ctx context.Context, err error) error { + t.execution.State, t.execution.Error = BundleFailed, err.Error() + _ = t.save(ctx) + return err +} + +var bundleOutputRE = regexp.MustCompile(`^[A-Za-z_][A-Za-z0-9_]*$`) + +func validBundleOutput(value string) bool { return bundleOutputRE.MatchString(value) } + +// Reconcile marks abandoned executions failed and removes their staging +// tables. It is safe to call repeatedly during server startup; READY pointers +// are never touched. +func (s *ClickHouseBundleStore) Reconcile(ctx context.Context, olderThan time.Time) error { + catalog, ok := s.Catalog.(StaleBundleCatalog) + if !ok { + return fmt.Errorf("bundle catalog does not support stale execution listing") + } + for _, state := range []BundleState{BundlePending, BundlePreflight, BundleLoading, BundleValidating} { + executions, err := catalog.ListExecutions(ctx, state, olderThan) + if err != nil { + return err + } + for _, execution := range executions { + for _, output := range execution.Outputs { + if err := s.ClickHouse.DropTable(context.Background(), output.PhysicalTable); err != nil { + return err + } + } + execution.State = BundleFailed + execution.Error = "stale execution reconciled" + execution.UpdatedAt = time.Now().UTC() + if err := s.Catalog.SaveExecution(ctx, execution); err != nil { + return err + } + } + } + return nil +} diff --git a/internal/dataframe/materialization/clickhouse_bundle_test.go b/internal/dataframe/materialization/clickhouse_bundle_test.go new file mode 100644 index 0000000..7e97301 --- /dev/null +++ b/internal/dataframe/materialization/clickhouse_bundle_test.go @@ -0,0 +1,218 @@ +package materialization + +import ( + "context" + "errors" + "strings" + "testing" + "time" + + "github.com/calypr/loom/internal/store/clickhouse" +) + +type bundleCatalogFixture struct { + executions map[string]BundleExecution + pointers map[string]BundlePointer +} + +func newBundleCatalogFixture() *bundleCatalogFixture { + return &bundleCatalogFixture{executions: map[string]BundleExecution{}, pointers: map[string]BundlePointer{}} +} +func (c *bundleCatalogFixture) SaveExecution(_ context.Context, e BundleExecution) error { + c.executions[e.ID] = e + return nil +} +func (c *bundleCatalogFixture) GetExecution(_ context.Context, id string) (BundleExecution, error) { + e, ok := c.executions[id] + if !ok { + return BundleExecution{}, ErrBundleNotFound + } + return e, nil +} +func (c *bundleCatalogFixture) FindExecutionByKey(_ context.Context, key string) (BundleExecution, error) { + for _, e := range c.executions { + if e.Key == key { + return e, nil + } + } + return BundleExecution{}, ErrBundleNotFound +} +func (c *bundleCatalogFixture) GetPointer(_ context.Context, name string) (BundlePointer, error) { + p, ok := c.pointers[name] + if !ok { + return BundlePointer{}, ErrBundleNotFound + } + return p, nil +} +func (c *bundleCatalogFixture) CompareAndSwapPointer(_ context.Context, name, expected, next string) error { + p, ok := c.pointers[name] + if ok && p.ExecutionID != expected { + return ErrBundlePointerConflict + } + c.pointers[name] = BundlePointer{Name: name, ExecutionID: next} + return nil +} +func (c *bundleCatalogFixture) ListExecutions(_ context.Context, state BundleState, before time.Time) ([]BundleExecution, error) { + out := []BundleExecution{} + for _, e := range c.executions { + if e.State == state && e.UpdatedAt.Before(before) { + out = append(out, e) + } + } + return out, nil +} + +type bundleClickHouseFixture struct { + tables map[string][]map[string]any + failInsert bool + insertCalls int + maxRows int +} + +func newBundleClickHouseFixture() *bundleClickHouseFixture { + return &bundleClickHouseFixture{tables: map[string][]map[string]any{}} +} +func (c *bundleClickHouseFixture) CreateTable(_ context.Context, name string, _ []clickhouse.Column) error { + c.tables[name] = nil + return nil +} +func (c *bundleClickHouseFixture) InsertRows(_ context.Context, name string, _ []clickhouse.Column, rows []map[string]any) error { + c.insertCalls++ + if len(rows) > c.maxRows { + c.maxRows = len(rows) + } + if c.failInsert { + return errors.New("insert failed") + } + c.tables[name] = append(c.tables[name], rows...) + return nil +} + +func (c *bundleClickHouseFixture) DropTable(_ context.Context, name string) error { + delete(c.tables, name) + return nil +} +func (c *bundleClickHouseFixture) QueryRows(_ context.Context, query string, _ []string) ([]map[string]any, error) { + for table, rows := range c.tables { + if strings.Contains(query, "`"+table+"`") { + return rows, nil + } + } + return nil, errors.New("table not found") +} + +func TestClickHouseBundleStoreReconcilesStaleExecution(t *testing.T) { + catalog := newBundleCatalogFixture() + client := newBundleClickHouseFixture() + store, _ := NewClickHouseBundleStore(client, catalog) + tx, err := store.BeginBundleFor(context.Background(), BundleIdentity{Name: "stale"}) + if err != nil { + t.Fatal(err) + } + if err := tx.CreateOutput(context.Background(), "one", []clickhouse.Column{{Name: "id", Type: "String"}}); err != nil { + t.Fatal(err) + } + id := "" + for key, execution := range catalog.executions { + id = key + execution.UpdatedAt = time.Now().Add(-time.Hour) + catalog.executions[key] = execution + } + if err := store.Reconcile(context.Background(), time.Now().Add(-time.Minute)); err != nil { + t.Fatal(err) + } + if catalog.executions[id].State != BundleFailed { + t.Fatalf("stale execution not failed: %#v", catalog.executions[id]) + } + if len(client.tables) != 0 { + t.Fatalf("staging tables survived reconciliation: %#v", client.tables) + } +} + +func TestPublishedOutputResolutionIsProjectAndGenerationScoped(t *testing.T) { + catalog := newBundleCatalogFixture() + identities := []BundleIdentity{ + {Name: "observation", Project: "project-a", DatasetGeneration: "generation-1"}, + {Name: "observation", Project: "project-b", DatasetGeneration: "generation-1"}, + } + for index, identity := range identities { + execution := BundleExecution{ + ID: "execution-" + string(rune('a'+index)), BundleIdentity: identity, + State: BundleReady, CreatedAt: time.Now().UTC(), UpdatedAt: time.Now().UTC(), + Outputs: []BundleOutputRecord{{Name: "observation", PhysicalTable: "loom_table_" + identity.Project, Columns: []Column{{Name: "id", ClickHouse: "String"}}, State: BundleReady}}, + } + catalog.executions[execution.ID] = execution + catalog.pointers[identity.PointerName()] = BundlePointer{Name: identity.PointerName(), ExecutionID: execution.ID} + } + first, err := ResolvePublishedOutput(context.Background(), catalog, "project-a", "generation-1", "observation") + if err != nil { + t.Fatal(err) + } + second, err := ResolvePublishedOutput(context.Background(), catalog, "project-b", "generation-1", "observation") + if err != nil { + t.Fatal(err) + } + if first.PhysicalTable == second.PhysicalTable || first.Project == second.Project { + t.Fatalf("project-scoped outputs collided: first=%#v second=%#v", first, second) + } +} + +func TestResolveFederatedDatasetUsesAuthorizedProjectSet(t *testing.T) { + catalog := newBundleCatalogFixture() + for index, project := range []string{"project-a", "project-b", "project-c"} { + execution := BundleExecution{ + ID: "federated-execution-" + string(rune('a'+index)), + BundleIdentity: BundleIdentity{Name: "observation", Project: project, DatasetGeneration: "generation-1"}, + State: BundleReady, CreatedAt: time.Now().UTC(), UpdatedAt: time.Now().UTC(), + Outputs: []BundleOutputRecord{{Name: "observation", Alias: "observation", PhysicalTable: "loom_" + project, Columns: []Column{{Name: "auth_resource_path", ClickHouse: "Nullable(String)"}, {Name: "id", ClickHouse: "String"}}, State: BundleReady}}, + } + catalog.executions[execution.ID] = execution + catalog.pointers[execution.PointerName()] = BundlePointer{Name: execution.PointerName(), ExecutionID: execution.ID} + } + dataset, err := ResolveFederatedDataset(context.Background(), catalog, []string{"project-b", "project-a", "project-b"}, "observation") + if err != nil { + t.Fatal(err) + } + if len(dataset.Sources) != 2 || dataset.Sources[0].Project != "project-a" || dataset.Sources[1].Project != "project-b" { + t.Fatalf("federated sources = %#v", dataset.Sources) + } + if len(dataset.Columns) != 2 || dataset.Columns[0].Name != "auth_resource_path" { + t.Fatalf("federated columns = %#v", dataset.Columns) + } + if dataset.Revision == "" { + t.Fatal("federated revision is empty") + } +} + +func TestResolveFederatedDatasetRejectsSchemaConflict(t *testing.T) { + catalog := newBundleCatalogFixture() + identities := []BundleIdentity{ + {Name: "observation", Project: "project-a", DatasetGeneration: "generation-1"}, + {Name: "observation", Project: "project-b", DatasetGeneration: "generation-1"}, + } + for index, identity := range identities { + tableType := "String" + if index == 1 { + tableType = "Int64" + } + execution := BundleExecution{ID: "conflict-" + string(rune('a'+index)), BundleIdentity: identity, State: BundleReady, CreatedAt: time.Now().UTC(), UpdatedAt: time.Now().UTC(), Outputs: []BundleOutputRecord{{Name: "observation", PhysicalTable: "loom_conflict_" + identity.Project, Columns: []Column{{Name: "id", ClickHouse: tableType}}, State: BundleReady}}} + catalog.executions[execution.ID] = execution + catalog.pointers[execution.PointerName()] = BundlePointer{Name: execution.PointerName(), ExecutionID: execution.ID} + } + if _, err := ResolveFederatedDataset(context.Background(), catalog, []string{"project-a", "project-b"}, "observation"); err == nil { + t.Fatal("schema conflict unexpectedly resolved") + } +} + +func TestClickHouseBundleStoreRejectsDuplicateInFlightExecution(t *testing.T) { + catalog := newBundleCatalogFixture() + client := newBundleClickHouseFixture() + store, _ := NewClickHouseBundleStore(client, catalog) + identity := BundleIdentity{Name: "in-flight"} + if _, err := store.BeginBundleFor(context.Background(), identity); err != nil { + t.Fatal(err) + } + if _, err := store.BeginBundleFor(context.Background(), identity); !errors.Is(err, ErrBundleInFlight) { + t.Fatalf("duplicate execution error = %v", err) + } +} diff --git a/internal/dataframe/materialization/federation.go b/internal/dataframe/materialization/federation.go new file mode 100644 index 0000000..f8dc7c5 --- /dev/null +++ b/internal/dataframe/materialization/federation.go @@ -0,0 +1,611 @@ +package materialization + +import ( + "context" + "crypto/sha256" + "encoding/hex" + "fmt" + "sort" + "strings" + "time" +) + +// FederatedDataset is the logical reader view of one alias across all +// authorized project publications. Project remains source metadata; it is +// intentionally not a public dataframe row column. +type FederatedDataset struct { + Name string + Revision string + Sources []Materialization + Columns []Column + RowCount int64 +} + +// FederatedPageRequest is the projectless reader request. The caller supplies +// the already-resolved effective scope; this type never accepts a project +// selector from the browser. +type FederatedPageRequest struct { + Columns []string + Filters []Filter + Sort *Sort + First int + After string + AuthResourcePaths []string + AuthUnrestricted bool + AuthPathsByProject map[string][]string + UnrestrictedByProject map[string]bool +} + +// FederatedPage is the projectless row response. Source metadata is retained +// only for internal cursor/revision handling. +type FederatedPage struct { + Dataset FederatedDataset + Columns []string + Rows []map[string]any + TotalCount int64 + HasNext bool + NextCursor string +} + +// FederatedAggregateRequest describes an aggregate over the same authorized +// union used by row reads. +type FederatedAggregateRequest struct { + GroupBy []string + Filters []Filter + Operation string + Column string + AuthResourcePaths []string + AuthUnrestricted bool + AuthPathsByProject map[string][]string + UnrestrictedByProject map[string]bool +} + +type FederatedAggregateResult struct { + Dataset FederatedDataset + Columns []string + Rows []map[string]any +} + +// ResolveFederatedDataset resolves the current READY output for alias from +// each requested authorized project and reconciles one stable public schema. +// The project list is an authorization result, never a browser input. +func ResolveFederatedDataset(ctx context.Context, catalog BundleCatalog, projects []string, alias string) (FederatedDataset, error) { + if catalog == nil { + return FederatedDataset{}, fmt.Errorf("bundle catalog is required") + } + listed, ok := catalog.(StaleBundleCatalog) + if !ok { + return FederatedDataset{}, fmt.Errorf("bundle catalog does not support dataset resolution") + } + alias = strings.TrimSpace(alias) + if alias == "" { + return FederatedDataset{}, fmt.Errorf("dataType is required") + } + uniqueProjects := normalizedProjects(projects) + if len(uniqueProjects) == 0 { + return FederatedDataset{}, fmt.Errorf("principal has no authorized projects") + } + executions, err := listed.ListExecutions(ctx, BundleReady, nowPlusSecond()) + if err != nil { + return FederatedDataset{}, err + } + allowed := make(map[string]struct{}, len(uniqueProjects)) + for _, project := range uniqueProjects { + allowed[project] = struct{}{} + } + latest := make(map[string]BundleExecution, len(uniqueProjects)) + for _, execution := range executions { + if execution.State != BundleReady { + continue + } + if _, ok := allowed[execution.Project]; !ok { + continue + } + pointer, pointerErr := catalog.GetPointer(ctx, execution.PointerName()) + if pointerErr != nil || pointer.ExecutionID != execution.ID { + continue + } + if !hasOutputAlias(execution, alias) { + continue + } + current, ok := latest[execution.Project] + if !ok || execution.UpdatedAt.After(current.UpdatedAt) { + latest[execution.Project] = execution + } + } + if len(latest) == 0 { + return FederatedDataset{}, fmt.Errorf("published dataset %q was not found", alias) + } + sources := make([]Materialization, 0, len(latest)) + for _, project := range uniqueProjects { + execution, ok := latest[project] + if !ok { + continue + } + for _, output := range execution.Outputs { + outputAlias := output.Alias + if outputAlias == "" { + outputAlias = output.Name + } + if outputAlias == alias { + sources = append(sources, publishedMaterialization(execution, output, alias)) + break + } + } + } + if len(sources) == 0 { + return FederatedDataset{}, fmt.Errorf("published dataset %q was not found", alias) + } + columns, err := reconcileFederatedColumns(sources) + if err != nil { + return FederatedDataset{}, err + } + rowCount := int64(0) + parts := make([]string, 0, len(sources)) + for _, source := range sources { + rowCount += source.RowCount + parts = append(parts, source.ID, source.DatasetGeneration, source.PhysicalTable) + } + sort.Strings(parts) + digest := sha256.Sum256([]byte(strings.Join(parts, "\x00"))) + return FederatedDataset{Name: alias, Revision: hex.EncodeToString(digest[:]), Sources: sources, Columns: columns, RowCount: rowCount}, nil +} + +func normalizedProjects(projects []string) []string { + seen := make(map[string]struct{}, len(projects)) + result := make([]string, 0, len(projects)) + for _, project := range projects { + project = strings.TrimSpace(project) + if project == "" { + continue + } + if _, ok := seen[project]; ok { + continue + } + seen[project] = struct{}{} + result = append(result, project) + } + sort.Strings(result) + return result +} + +func hasOutputAlias(execution BundleExecution, alias string) bool { + for _, output := range execution.Outputs { + outputAlias := output.Alias + if outputAlias == "" { + outputAlias = output.Name + } + if outputAlias == alias { + return true + } + } + return false +} + +func reconcileFederatedColumns(sources []Materialization) ([]Column, error) { + if len(sources) == 0 { + return nil, fmt.Errorf("federation has no sources") + } + base := make([]Column, 0, len(sources[0].Columns)) + for _, column := range sources[0].Columns { + if column.Name == "__loom_row_id" { + continue + } + base = append(base, column) + } + for _, source := range sources[1:] { + candidate := make([]Column, 0, len(source.Columns)) + for _, column := range source.Columns { + if column.Name != "__loom_row_id" { + candidate = append(candidate, column) + } + } + if len(candidate) != len(base) { + return nil, fmt.Errorf("incompatible schemas for federated dataset %q", source.Name) + } + for index := range base { + if base[index] != candidate[index] { + return nil, fmt.Errorf("incompatible schema column %q in federated dataset %q", base[index].Name, source.Name) + } + } + } + return base, nil +} + +func nowPlusSecond() time.Time { + return time.Now().UTC().Add(time.Second) +} + +func (r *Reader) ResolveFederatedDataset(ctx context.Context, projects []string, alias string) (FederatedDataset, error) { + if r == nil || r.Catalog == nil { + return FederatedDataset{}, fmt.Errorf("bundle catalog dependency is required") + } + return ResolveFederatedDataset(ctx, r.Catalog, projects, alias) +} + +func (r *Reader) FederatedDatasets(ctx context.Context, projects []string) ([]FederatedDataset, error) { + if r == nil || r.Catalog == nil { + return nil, fmt.Errorf("bundle catalog dependency is required") + } + listed, ok := r.Catalog.(StaleBundleCatalog) + if !ok { + return nil, fmt.Errorf("bundle catalog does not support dataset listing") + } + uniqueProjects := normalizedProjects(projects) + if len(uniqueProjects) == 0 { + return nil, fmt.Errorf("principal has no authorized projects") + } + allowed := make(map[string]struct{}, len(uniqueProjects)) + for _, project := range uniqueProjects { + allowed[project] = struct{}{} + } + executions, err := listed.ListExecutions(ctx, BundleReady, nowPlusSecond()) + if err != nil { + return nil, err + } + aliases := make(map[string]struct{}) + for _, execution := range executions { + if execution.State != BundleReady { + continue + } + if _, ok := allowed[execution.Project]; !ok { + continue + } + pointer, pointerErr := r.Catalog.GetPointer(ctx, execution.PointerName()) + if pointerErr != nil || pointer.ExecutionID != execution.ID { + continue + } + for _, output := range execution.Outputs { + alias := output.Alias + if alias == "" { + alias = output.Name + } + aliases[alias] = struct{}{} + } + } + names := make([]string, 0, len(aliases)) + for alias := range aliases { + names = append(names, alias) + } + sort.Strings(names) + result := make([]FederatedDataset, 0, len(names)) + for _, alias := range names { + dataset, err := ResolveFederatedDataset(ctx, r.Catalog, uniqueProjects, alias) + if err != nil { + continue + } + result = append(result, dataset) + } + return result, nil +} + +// PublishedProjects returns project identities that have a current READY +// publication. It is used only as a candidate set when the authenticator does +// not embed project claims; row/source authorization still happens per source. +func (r *Reader) PublishedProjects(ctx context.Context) ([]string, error) { + if r == nil || r.Catalog == nil { + return nil, fmt.Errorf("bundle catalog dependency is required") + } + listed, ok := r.Catalog.(StaleBundleCatalog) + if !ok { + return nil, fmt.Errorf("bundle catalog does not support project discovery") + } + executions, err := listed.ListExecutions(ctx, BundleReady, nowPlusSecond()) + if err != nil { + return nil, err + } + projects := make(map[string]struct{}) + for _, execution := range executions { + if execution.State != BundleReady || execution.Project == "" { + continue + } + pointer, pointerErr := r.Catalog.GetPointer(ctx, execution.PointerName()) + if pointerErr == nil && pointer.ExecutionID == execution.ID { + projects[execution.Project] = struct{}{} + } + } + result := make([]string, 0, len(projects)) + for project := range projects { + result = append(result, project) + } + sort.Strings(result) + return result, nil +} + +func (r *Reader) PageFederated(ctx context.Context, projects []string, alias string, req FederatedPageRequest) (FederatedPage, error) { + if r == nil || r.ClickHouse == nil || r.Catalog == nil { + return FederatedPage{}, fmt.Errorf("ClickHouse and bundle catalog dependencies are required") + } + dataset, err := r.ResolveFederatedDataset(ctx, projects, alias) + if err != nil { + return FederatedPage{}, err + } + allowed := make(map[string]struct{}, len(dataset.Columns)) + for _, column := range dataset.Columns { + allowed[column.Name] = struct{}{} + } + columns := append([]string(nil), req.Columns...) + if len(columns) == 0 { + for _, column := range dataset.Columns { + columns = append(columns, column.Name) + } + } + if err := validateReaderColumns(columns, allowed); err != nil { + return FederatedPage{}, err + } + if req.Sort != nil { + if err := validateReaderColumns([]string{req.Sort.Column}, allowed); err != nil { + return FederatedPage{}, fmt.Errorf("sort: %w", err) + } + } + first := req.First + if first <= 0 { + first = 100 + } + if r.MaxPage > 0 && first > r.MaxPage { + first = r.MaxPage + } + cursor, err := decodeCursor(req.After) + if err != nil { + return FederatedPage{}, err + } + whereBySource := make([]string, 0, len(dataset.Sources)) + whereArgs := make([]any, 0) + for _, source := range dataset.Sources { + baseFilters := make([]Filter, 0, len(req.Filters)+1) + baseFilters = append(baseFilters, req.Filters...) + unrestricted := req.AuthUnrestricted + paths := req.AuthResourcePaths + if req.UnrestrictedByProject != nil { + unrestricted = req.UnrestrictedByProject[source.Project] + } + if req.AuthPathsByProject != nil { + paths = req.AuthPathsByProject[source.Project] + } + if !unrestricted { + baseFilters = append(baseFilters, Filter{Column: "auth_resource_path", Op: "IN", Value: paths}) + } + where, args, err := buildWhere(baseFilters, allowed) + if err != nil { + return FederatedPage{}, err + } + selects := make([]string, 0, len(columns)+2) + for _, column := range columns { + selects = append(selects, fmt.Sprintf("`%s`", column)) + } + selects = append(selects, + "toString(`__loom_row_id`) AS `__loom_row_id`", + "concat(?, ':', toString(`__loom_row_id`)) AS `__loom_global_row_id`", + ) + branch := fmt.Sprintf("SELECT %s FROM `%s`", strings.Join(selects, ", "), source.PhysicalTable) + if len(where) > 0 { + branch += " WHERE " + strings.Join(where, " AND ") + } + whereBySource = append(whereBySource, branch) + whereArgs = append(whereArgs, source.ID) + whereArgs = append(whereArgs, args...) + } + union := strings.Join(whereBySource, " UNION ALL ") + queryColumns := append([]string(nil), columns...) + queryColumns = append(queryColumns, "__loom_row_id", "__loom_global_row_id") + query := fmt.Sprintf("SELECT %s FROM (%s) AS __loom_federated", quotedColumns(queryColumns), union) + queryArgs := append([]any(nil), whereArgs...) + if cursor != nil { + cursorWhere, cursorArgs, err := federatedCursorPredicate(cursor, req.Sort) + if err != nil { + return FederatedPage{}, err + } + query += " WHERE " + cursorWhere + queryArgs = append(queryArgs, cursorArgs...) + } + if req.Sort != nil { + direction := "ASC" + if req.Sort.Desc { + direction = "DESC" + } + query += fmt.Sprintf(" ORDER BY `%s` %s, `__loom_global_row_id` ASC", req.Sort.Column, direction) + } else { + query += " ORDER BY `__loom_global_row_id` ASC" + } + query += fmt.Sprintf(" LIMIT %d", first+1) + rows, err := r.ClickHouse.QueryRowsArgs(ctx, query, queryColumns, queryArgs...) + if err != nil { + return FederatedPage{}, err + } + count, err := r.federatedCount(ctx, dataset, allowed, req) + if err != nil { + return FederatedPage{}, err + } + hasNext := len(rows) > first + if hasNext { + rows = rows[:first] + } + next := "" + if hasNext && len(rows) > 0 { + last := rows[len(rows)-1] + rowID := fmt.Sprint(last["__loom_global_row_id"]) + var sortValue any + if req.Sort != nil { + sortValue = last[req.Sort.Column] + if sortValue == nil { + return FederatedPage{}, fmt.Errorf("cannot create keyset cursor from NULL sort value in %q", req.Sort.Column) + } + } + next = encodeCursor(rowID, sortValue) + } + for _, row := range rows { + delete(row, "__loom_row_id") + delete(row, "__loom_global_row_id") + } + return FederatedPage{Dataset: dataset, Columns: columns, Rows: rows, TotalCount: count, HasNext: hasNext, NextCursor: next}, nil +} + +func (r *Reader) federatedCount(ctx context.Context, dataset FederatedDataset, allowed map[string]struct{}, req FederatedPageRequest) (int64, error) { + branches := make([]string, 0, len(dataset.Sources)) + args := make([]any, 0) + for _, source := range dataset.Sources { + filters := make([]Filter, 0, len(req.Filters)+1) + filters = append(filters, req.Filters...) + unrestricted := req.AuthUnrestricted + paths := req.AuthResourcePaths + if req.UnrestrictedByProject != nil { + unrestricted = req.UnrestrictedByProject[source.Project] + } + if req.AuthPathsByProject != nil { + paths = req.AuthPathsByProject[source.Project] + } + if !unrestricted { + filters = append(filters, Filter{Column: "auth_resource_path", Op: "IN", Value: paths}) + } + where, branchArgs, err := buildWhere(filters, allowed) + if err != nil { + return 0, err + } + branch := fmt.Sprintf("SELECT count() AS `__loom_total` FROM `%s`", source.PhysicalTable) + if len(where) > 0 { + branch += " WHERE " + strings.Join(where, " AND ") + } + branches = append(branches, branch) + args = append(args, branchArgs...) + } + rows, err := r.ClickHouse.QueryRowsArgs(ctx, strings.Join(branches, " UNION ALL "), []string{"__loom_total"}, args...) + if err != nil { + return 0, err + } + var total int64 + for _, row := range rows { + value, err := numericCount(row["__loom_total"]) + if err != nil { + return 0, err + } + total += value + } + return total, nil +} + +func (r *Reader) AggregateFederated(ctx context.Context, projects []string, alias string, req FederatedAggregateRequest) (FederatedAggregateResult, error) { + if r == nil || r.ClickHouse == nil || r.Catalog == nil { + return FederatedAggregateResult{}, fmt.Errorf("ClickHouse and bundle catalog dependencies are required") + } + dataset, err := r.ResolveFederatedDataset(ctx, projects, alias) + if err != nil { + return FederatedAggregateResult{}, err + } + allowed := make(map[string]struct{}, len(dataset.Columns)) + for _, column := range dataset.Columns { + allowed[column.Name] = struct{}{} + } + for _, column := range req.GroupBy { + if _, ok := allowed[column]; !ok { + return FederatedAggregateResult{}, fmt.Errorf("group column %q is not in federated dataset schema", column) + } + } + operation := strings.ToUpper(req.Operation) + if operation != "COUNT" && operation != "COUNT_DISTINCT" && operation != "SUM" && operation != "AVG" && operation != "MIN" && operation != "MAX" { + return FederatedAggregateResult{}, fmt.Errorf("unsupported dataframe aggregate operation %q", req.Operation) + } + if operation != "COUNT" { + if _, ok := allowed[req.Column]; !ok { + return FederatedAggregateResult{}, fmt.Errorf("aggregate column %q is not in federated dataset schema", req.Column) + } + } + columns := append([]string(nil), req.GroupBy...) + metricName := strings.ToLower(operation) + if operation == "COUNT" { + metricName = "count" + } + columns = append(columns, metricName) + branches := make([]string, 0, len(dataset.Sources)) + args := make([]any, 0) + for _, source := range dataset.Sources { + filters := append([]Filter(nil), req.Filters...) + unrestricted := req.AuthUnrestricted + paths := req.AuthResourcePaths + if req.UnrestrictedByProject != nil { + unrestricted = req.UnrestrictedByProject[source.Project] + } + if req.AuthPathsByProject != nil { + paths = req.AuthPathsByProject[source.Project] + } + if !unrestricted { + filters = append(filters, Filter{Column: "auth_resource_path", Op: "IN", Value: paths}) + } + where, branchArgs, err := buildWhere(filters, allowed) + if err != nil { + return FederatedAggregateResult{}, err + } + selects := make([]string, 0, len(req.GroupBy)+1) + for _, group := range req.GroupBy { + selects = append(selects, fmt.Sprintf("`%s`", group)) + } + metric := "count()" + switch operation { + case "COUNT_DISTINCT": + metric = fmt.Sprintf("uniqExact(`%s`)", req.Column) + case "SUM": + metric = fmt.Sprintf("sum(`%s`)", req.Column) + case "AVG": + metric = fmt.Sprintf("avg(`%s`)", req.Column) + case "MIN": + metric = fmt.Sprintf("min(`%s`)", req.Column) + case "MAX": + metric = fmt.Sprintf("max(`%s`)", req.Column) + } + selects = append(selects, metric+" AS `"+metricName+"`") + branch := fmt.Sprintf("SELECT %s FROM `%s`", strings.Join(selects, ", "), source.PhysicalTable) + if len(where) > 0 { + branch += " WHERE " + strings.Join(where, " AND ") + } + if len(req.GroupBy) > 0 { + branch += " GROUP BY " + quotedColumns(req.GroupBy) + } + branches = append(branches, branch) + args = append(args, branchArgs...) + } + query := fmt.Sprintf("SELECT %s FROM (%s) AS __loom_aggregate", quotedColumns(columns), strings.Join(branches, " UNION ALL ")) + if len(req.GroupBy) > 0 { + query += " ORDER BY " + quotedColumns(req.GroupBy) + } + rows, err := r.ClickHouse.QueryRowsArgs(ctx, query, columns, args...) + if err != nil { + return FederatedAggregateResult{}, err + } + return FederatedAggregateResult{Dataset: dataset, Columns: columns, Rows: rows}, nil +} + +func validateReaderColumns(columns []string, allowed map[string]struct{}) error { + for _, column := range columns { + if column == "__loom_row_id" || column == "__loom_global_row_id" { + return fmt.Errorf("column %q is internal to dataframe pagination", column) + } + if _, ok := allowed[column]; !ok { + return fmt.Errorf("column %q is not in federated dataset schema", column) + } + } + return nil +} + +func quotedColumns(columns []string) string { + quoted := make([]string, len(columns)) + for index, column := range columns { + quoted[index] = fmt.Sprintf("`%s`", column) + } + return strings.Join(quoted, ", ") +} + +func federatedCursorPredicate(cursor *pageCursor, sort *Sort) (string, []any, error) { + if cursor == nil || cursor.RowID == "" { + return "", nil, fmt.Errorf("invalid federated cursor") + } + if sort == nil { + return "`__loom_global_row_id` > ?", []any{cursor.RowID}, nil + } + if cursor.SortValue == nil { + return "", nil, fmt.Errorf("federated keyset cursor is missing sort value") + } + op := ">" + if sort.Desc { + op = "<" + } + return fmt.Sprintf("(`%s` %s ? OR (`%s` = ? AND `__loom_global_row_id` > ?))", sort.Column, op, sort.Column), []any{cursor.SortValue, cursor.SortValue, cursor.RowID}, nil +} diff --git a/internal/dataframe/materialization/integration_test.go b/internal/dataframe/materialization/integration_test.go deleted file mode 100644 index a04f6dc..0000000 --- a/internal/dataframe/materialization/integration_test.go +++ /dev/null @@ -1,81 +0,0 @@ -package materialization - -import ( - "context" - "os" - "testing" - "time" - - "github.com/google/uuid" - - clickhousestore "github.com/calypr/loom/internal/store/clickhouse" -) - -// This opt-in test exercises the actual materialization reader against -// ClickHouse, including keyset pagination and aggregate filters. -func TestClickHouseReaderPaginationAndFilteredAggregate(t *testing.T) { - url := os.Getenv("LOOM_CLICKHOUSE_URL") - if url == "" { - t.Skip("LOOM_CLICKHOUSE_URL is not set") - } - database := os.Getenv("LOOM_CLICKHOUSE_DATABASE") - if database == "" { - database = "loom_test" - } - client, err := clickhousestore.New(clickhousestore.Options{URL: url, Database: database, Timeout: 10 * time.Second}) - if err != nil { - t.Fatal(err) - } - defer client.Close() - ctx := context.Background() - if err := client.EnsureDatabase(ctx); err != nil { - t.Fatal(err) - } - table := "loom_reader_it_" + uuid.NewString()[:12] - defer client.DropTable(ctx, table) - columns := []clickhousestore.Column{ - {Name: "__loom_row_id", Type: "UInt64"}, - {Name: "status", Type: "String"}, - {Name: "score", Type: "Float64"}, - } - if err := client.CreateTable(ctx, table, columns); err != nil { - t.Fatal(err) - } - if err := client.InsertRows(ctx, table, columns, []map[string]any{ - {"__loom_row_id": uint64(1), "status": "active", "score": 1.0}, - {"__loom_row_id": uint64(2), "status": "inactive", "score": 2.0}, - {"__loom_row_id": uint64(3), "status": "active", "score": 3.0}, - }); err != nil { - t.Fatal(err) - } - registry := NewMemoryRegistry() - if err := registry.Save(ctx, Materialization{ID: "it", State: StateReady, PhysicalTable: table, Columns: []Column{ - {Name: "__loom_row_id", ClickHouse: "UInt64"}, - {Name: "status", ClickHouse: "String"}, - {Name: "score", ClickHouse: "Float64"}, - }}); err != nil { - t.Fatal(err) - } - reader := &Reader{ClickHouse: client, Registry: registry, MaxPage: 10} - first, err := reader.Page(ctx, PageRequest{MaterializationID: "it", Columns: []string{"status", "score"}, Sort: &Sort{Column: "score"}, First: 1}) - if err != nil { - t.Fatal(err) - } - if len(first.Rows) != 1 || !first.HasNext || first.NextCursor == "" { - t.Fatalf("first page = %#v", first) - } - second, err := reader.Page(ctx, PageRequest{MaterializationID: "it", Columns: []string{"status", "score"}, Sort: &Sort{Column: "score"}, First: 2, After: first.NextCursor}) - if err != nil { - t.Fatal(err) - } - if len(second.Rows) != 2 || second.Rows[0]["score"] != float64(2) { - t.Fatalf("second page = %#v", second) - } - aggregate, err := reader.Aggregate(ctx, AggregateRequest{MaterializationID: "it", GroupBy: []string{"status"}, Filters: []Filter{{Column: "status", Op: "EQ", Value: "active"}}, Operation: "COUNT"}) - if err != nil { - t.Fatal(err) - } - if len(aggregate.Rows) != 1 || aggregate.Rows[0]["count"] != float64(2) { - t.Fatalf("filtered aggregate = %#v", aggregate.Rows) - } -} diff --git a/internal/dataframe/materialization/read.go b/internal/dataframe/materialization/read.go index c372935..5665b25 100644 --- a/internal/dataframe/materialization/read.go +++ b/internal/dataframe/materialization/read.go @@ -5,7 +5,6 @@ import ( "encoding/base64" "encoding/json" "fmt" - "strconv" "strings" "github.com/calypr/loom/internal/store/clickhouse" @@ -13,7 +12,7 @@ import ( type Reader struct { ClickHouse *clickhouse.Client - Registry Registry + Catalog BundleCatalog MaxPage int } @@ -41,6 +40,7 @@ type Page struct { Materialization Materialization Columns []string Rows []map[string]any + TotalCount int64 HasNext bool NextCursor string } @@ -59,14 +59,31 @@ type AggregateResult struct { Rows []map[string]any } -func (r *Reader) Aggregate(ctx context.Context, req AggregateRequest) (AggregateResult, error) { - if r.ClickHouse == nil || r.Registry == nil { - return AggregateResult{}, fmt.Errorf("ClickHouse and registry dependencies are required") +func (r *Reader) AggregateDataset(ctx context.Context, project, generation, alias string, req AggregateRequest) (AggregateResult, error) { + if r.ClickHouse == nil || r.Catalog == nil { + return AggregateResult{}, fmt.Errorf("ClickHouse and bundle catalog dependencies are required") } - m, err := r.Registry.Get(ctx, req.MaterializationID) + m, err := ResolvePublishedOutput(ctx, r.Catalog, project, generation, alias) if err != nil { return AggregateResult{}, err } + req.MaterializationID = m.ID + return r.aggregateResolved(ctx, req, m) +} + +func (r *Reader) AggregatePublishedID(ctx context.Context, id string, req AggregateRequest) (AggregateResult, error) { + if r.ClickHouse == nil || r.Catalog == nil { + return AggregateResult{}, fmt.Errorf("ClickHouse and bundle catalog dependencies are required") + } + m, err := r.publishedByID(ctx, id) + if err != nil { + return AggregateResult{}, err + } + req.MaterializationID = m.ID + return r.aggregateResolved(ctx, req, m) +} + +func (r *Reader) aggregateResolved(ctx context.Context, req AggregateRequest, m Materialization) (AggregateResult, error) { if m.State != StateReady { return AggregateResult{}, fmt.Errorf("materialization %q is not ready", m.ID) } @@ -79,12 +96,12 @@ func (r *Reader) Aggregate(ctx context.Context, req AggregateRequest) (Aggregate return AggregateResult{}, fmt.Errorf("group column %q is not in materialization schema", column) } } - where, err := buildWhere(req.Filters, allowed) + where, args, err := buildWhere(req.Filters, allowed) if err != nil { return AggregateResult{}, err } operation := strings.ToUpper(req.Operation) - if operation != "COUNT" && operation != "COUNT_DISTINCT" && operation != "SUM" && operation != "MIN" && operation != "MAX" { + if operation != "COUNT" && operation != "COUNT_DISTINCT" && operation != "SUM" && operation != "AVG" && operation != "MIN" && operation != "MAX" { return AggregateResult{}, fmt.Errorf("unsupported dataframe aggregate operation %q", req.Operation) } if operation != "COUNT" { @@ -107,6 +124,10 @@ func (r *Reader) Aggregate(ctx context.Context, req AggregateRequest) (Aggregate metric = fmt.Sprintf("sum(`%s`)", req.Column) metricName = "sum" } + if operation == "AVG" { + metric = fmt.Sprintf("avg(`%s`)", req.Column) + metricName = "avg" + } if operation == "MIN" { metric = fmt.Sprintf("min(`%s`)", req.Column) metricName = "min" @@ -124,21 +145,87 @@ func (r *Reader) Aggregate(ctx context.Context, req AggregateRequest) (Aggregate if len(req.GroupBy) > 0 { query += " GROUP BY " + strings.Join(selects[:len(req.GroupBy)], ", ") + " ORDER BY " + strings.Join(selects[:len(req.GroupBy)], ", ") } - rows, err := r.ClickHouse.QueryRows(ctx, query, columns) + rows, err := r.ClickHouse.QueryRowsArgs(ctx, query, columns, args...) if err != nil { return AggregateResult{}, err } return AggregateResult{Materialization: m, Columns: columns, Rows: rows}, nil } -func (r *Reader) Page(ctx context.Context, req PageRequest) (Page, error) { - if r.ClickHouse == nil || r.Registry == nil { - return Page{}, fmt.Errorf("ClickHouse and registry dependencies are required") +func (r *Reader) PageDataset(ctx context.Context, project, generation, alias string, req PageRequest) (Page, error) { + if r.ClickHouse == nil || r.Catalog == nil { + return Page{}, fmt.Errorf("ClickHouse and bundle catalog dependencies are required") } - m, err := r.Registry.Get(ctx, req.MaterializationID) + m, err := ResolvePublishedOutput(ctx, r.Catalog, project, generation, alias) if err != nil { return Page{}, err } + req.MaterializationID = m.ID + return r.pageResolved(ctx, req, m) +} + +func (r *Reader) PagePublishedID(ctx context.Context, id string, req PageRequest) (Page, error) { + if r.ClickHouse == nil || r.Catalog == nil { + return Page{}, fmt.Errorf("ClickHouse and bundle catalog dependencies are required") + } + m, err := r.publishedByID(ctx, id) + if err != nil { + return Page{}, err + } + req.MaterializationID = m.ID + return r.pageResolved(ctx, req, m) +} + +func (r *Reader) Dataset(ctx context.Context, project, generation, alias string) (Materialization, error) { + if r.Catalog == nil { + return Materialization{}, fmt.Errorf("bundle catalog dependency is required") + } + return ResolvePublishedOutput(ctx, r.Catalog, project, generation, alias) +} + +func (r *Reader) Datasets(ctx context.Context, project, generation string) ([]Materialization, error) { + if r.Catalog == nil { + return nil, fmt.Errorf("bundle catalog dependency is required") + } + return ListPublishedOutputs(ctx, r.Catalog, project, generation) +} + +func (r *Reader) DatasetByPublishedID(ctx context.Context, id string) (Materialization, error) { + if r.Catalog == nil { + return Materialization{}, fmt.Errorf("bundle catalog dependency is required") + } + return r.publishedByID(ctx, id) +} + +func (r *Reader) publishedByID(ctx context.Context, id string) (Materialization, error) { + parts := strings.SplitN(id, ":", 2) + if len(parts) != 2 || parts[0] == "" || parts[1] == "" { + return Materialization{}, fmt.Errorf("invalid published dataset id %q", id) + } + execution, err := r.Catalog.GetExecution(ctx, parts[0]) + if err != nil { + return Materialization{}, err + } + if execution.State != BundleReady { + return Materialization{}, fmt.Errorf("published dataset %q is not ready", id) + } + pointer, err := r.Catalog.GetPointer(ctx, execution.PointerName()) + if err != nil || pointer.ExecutionID != execution.ID { + return Materialization{}, fmt.Errorf("published dataset %q is not current", id) + } + for _, output := range execution.Outputs { + if output.Name == parts[1] { + alias := output.Alias + if alias == "" { + alias = output.Name + } + return publishedMaterialization(execution, output, alias), nil + } + } + return Materialization{}, fmt.Errorf("published dataset id %q was not found", id) +} + +func (r *Reader) pageResolved(ctx context.Context, req PageRequest, m Materialization) (Page, error) { if m.State != StateReady { return Page{}, fmt.Errorf("materialization %q is not ready", m.ID) } @@ -183,7 +270,22 @@ func (r *Reader) Page(ctx context.Context, req PageRequest) (Page, error) { if err != nil { return Page{}, err } - where, err := buildWhere(req.Filters, allowed) + where, whereArgs, err := buildWhere(req.Filters, allowed) + if err != nil { + return Page{}, err + } + countQuery := fmt.Sprintf("SELECT count() AS `__loom_total` FROM `%s`", m.PhysicalTable) + if len(where) > 0 { + countQuery += " WHERE " + strings.Join(where, " AND ") + } + countRows, err := r.ClickHouse.QueryRowsArgs(ctx, countQuery, []string{"__loom_total"}, whereArgs...) + if err != nil { + return Page{}, err + } + if len(countRows) == 0 { + return Page{}, fmt.Errorf("ClickHouse count query returned no rows") + } + totalCount, err := numericCount(countRows[0]["__loom_total"]) if err != nil { return Page{}, err } @@ -203,10 +305,11 @@ func (r *Reader) Page(ctx context.Context, req PageRequest) (Page, error) { query += " WHERE " + strings.Join(where, " AND ") } if cursor != nil { - cursorWhere, err := cursorPredicate(cursor, req.Sort) + cursorWhere, cursorArgs, err := cursorPredicate(cursor, req.Sort) if err != nil { return Page{}, err } + whereArgs = append(whereArgs, cursorArgs...) if strings.Contains(query, " WHERE ") { query += " AND " + cursorWhere } else { @@ -223,7 +326,7 @@ func (r *Reader) Page(ctx context.Context, req PageRequest) (Page, error) { query += " ORDER BY toUInt64(`__loom_row_id`) ASC" } query += fmt.Sprintf(" LIMIT %d", first+1) - rows, err := r.ClickHouse.QueryRows(ctx, query, queryColumns) + rows, err := r.ClickHouse.QueryRowsArgs(ctx, query, queryColumns, whereArgs...) if err != nil { return Page{}, err } @@ -253,29 +356,91 @@ func (r *Reader) Page(ctx context.Context, req PageRequest) (Page, error) { delete(row, req.Sort.Column) } } - return Page{Materialization: m, Columns: columns, Rows: rows, HasNext: hasNext, NextCursor: next}, nil + return Page{Materialization: m, Columns: columns, Rows: rows, TotalCount: totalCount, HasNext: hasNext, NextCursor: next}, nil } -func buildWhere(filters []Filter, allowed map[string]struct{}) ([]string, error) { +func numericCount(value any) (int64, error) { + switch typed := value.(type) { + case int: + return int64(typed), nil + case int64: + return typed, nil + case float64: + return int64(typed), nil + case json.Number: + return typed.Int64() + default: + return 0, fmt.Errorf("ClickHouse count returned unsupported value %T", value) + } +} + +func buildWhere(filters []Filter, allowed map[string]struct{}) ([]string, []any, error) { where := make([]string, 0, len(filters)) + args := make([]any, 0, len(filters)) for _, filter := range filters { if _, ok := allowed[filter.Column]; !ok { - return nil, fmt.Errorf("filter column %q is not in materialization schema", filter.Column) - } - literal, err := jsonLiteral(filter.Value) - if err != nil { - return nil, err + return nil, nil, fmt.Errorf("filter column %q is not in materialization schema", filter.Column) } switch strings.ToUpper(filter.Op) { case "EQ": - where = append(where, fmt.Sprintf("`%s` = %s", filter.Column, literal)) + where = append(where, fmt.Sprintf("`%s` = ?", filter.Column)) + args = append(args, filter.Value) + case "NEQ": + where = append(where, fmt.Sprintf("`%s` != ?", filter.Column)) + args = append(args, filter.Value) + case "IN", "NOT_IN": + if emptyFilterCollection(filter.Value) { + if strings.EqualFold(filter.Op, "IN") { + where = append(where, "0") + } else { + where = append(where, "1") + } + continue + } + op := "IN" + if strings.EqualFold(filter.Op, "NOT_IN") { + op = "NOT IN" + } + where = append(where, fmt.Sprintf("`%s` %s ?", filter.Column, op)) + args = append(args, filter.Value) + case "LT", "LTE", "GT", "GTE": + op := map[string]string{"LT": "<", "LTE": "<=", "GT": ">", "GTE": ">="}[strings.ToUpper(filter.Op)] + where = append(where, fmt.Sprintf("`%s` %s ?", filter.Column, op)) + args = append(args, filter.Value) case "CONTAINS": - where = append(where, fmt.Sprintf("positionCaseInsensitive(toString(`%s`), %s) > 0", filter.Column, literal)) + where = append(where, fmt.Sprintf("positionCaseInsensitive(toString(`%s`), ?) > 0", filter.Column)) + args = append(args, filter.Value) + case "STARTS_WITH": + where = append(where, fmt.Sprintf("startsWith(toString(`%s`), ?)", filter.Column)) + args = append(args, filter.Value) + case "EXISTS": + where = append(where, fmt.Sprintf("isNotNull(`%s`)", filter.Column)) + case "IS_NULL": + where = append(where, fmt.Sprintf("isNull(`%s`)", filter.Column)) + case "ARRAY_CONTAINS": + where = append(where, fmt.Sprintf("has(`%s`, ?)", filter.Column)) + args = append(args, filter.Value) + case "ARRAY_OVERLAPS": + where = append(where, fmt.Sprintf("hasAny(`%s`, ?)", filter.Column)) + args = append(args, filter.Value) default: - return nil, fmt.Errorf("unsupported dataframe filter operation %q", filter.Op) + return nil, nil, fmt.Errorf("unsupported dataframe filter operation %q", filter.Op) } } - return where, nil + return where, args, nil +} + +func emptyFilterCollection(value any) bool { + switch typed := value.(type) { + case []string: + return len(typed) == 0 + case []any: + return len(typed) == 0 + case nil: + return true + default: + return false + } } type pageCursor struct { @@ -303,27 +468,19 @@ func decodeCursor(cursor string) (*pageCursor, error) { return &value, nil } -func cursorPredicate(cursor *pageCursor, sort *Sort) (string, error) { - rowID, err := jsonLiteral(cursor.RowID) - if err != nil { - return "", err - } - row := fmt.Sprintf("toUInt64(`__loom_row_id`) > toUInt64(%s)", rowID) +func cursorPredicate(cursor *pageCursor, sort *Sort) (string, []any, error) { + row := "toUInt64(`__loom_row_id`) > toUInt64(?)" if sort == nil { - return row, nil + return row, []any{cursor.RowID}, nil } if cursor.SortValue == nil { - return "", fmt.Errorf("keyset cursor is missing sort value") - } - literal, err := jsonLiteral(cursor.SortValue) - if err != nil { - return "", err + return "", nil, fmt.Errorf("keyset cursor is missing sort value") } operator := ">" if sort.Desc { operator = "<" } - return fmt.Sprintf("(`%s` %s %s OR (`%s` = %s AND %s))", sort.Column, operator, literal, sort.Column, literal, row), nil + return fmt.Sprintf("(`%s` %s ? OR (`%s` = ? AND %s))", sort.Column, operator, sort.Column, row), []any{cursor.SortValue, cursor.SortValue, cursor.RowID}, nil } func contains(values []string, needle string) bool { @@ -334,28 +491,3 @@ func contains(values []string, needle string) bool { } return false } - -func jsonLiteral(value any) (string, error) { - data, err := json.Marshal(value) - if err != nil { - return "", err - } - return string(data), nil -} -func encodeOffset(value int) string { - return base64.RawURLEncoding.EncodeToString([]byte(strconv.Itoa(value))) -} -func decodeOffset(cursor string) (int, error) { - if cursor == "" { - return 0, nil - } - data, err := base64.RawURLEncoding.DecodeString(cursor) - if err != nil { - return 0, fmt.Errorf("invalid dataframe cursor: %w", err) - } - value, err := strconv.Atoi(string(data)) - if err != nil || value < 0 { - return 0, fmt.Errorf("invalid dataframe cursor") - } - return value, nil -} diff --git a/internal/dataframe/materialization/read_test.go b/internal/dataframe/materialization/read_test.go index e1f9168..00d9d45 100644 --- a/internal/dataframe/materialization/read_test.go +++ b/internal/dataframe/materialization/read_test.go @@ -11,21 +11,41 @@ func TestKeysetCursorRoundTrip(t *testing.T) { if decoded.RowID != "42" || decoded.SortValue != "alice" { t.Fatalf("decoded cursor = %#v", decoded) } - predicate, err := cursorPredicate(decoded, &Sort{Column: "name"}) + predicate, args, err := cursorPredicate(decoded, &Sort{Column: "name"}) if err != nil { t.Fatal(err) } - if predicate == "" || predicate == "toUInt64(`__loom_row_id`) > toUInt64(\"42\")" { + if predicate == "" || len(args) != 3 || predicate == "toUInt64(`__loom_row_id`) > toUInt64(\"42\")" { t.Fatalf("unexpected sort predicate %q", predicate) } } func TestBuildWhereSupportsAggregateFilters(t *testing.T) { - where, err := buildWhere([]Filter{{Column: "status", Op: "EQ", Value: "active"}}, map[string]struct{}{"status": {}}) + where, args, err := buildWhere([]Filter{{Column: "status", Op: "EQ", Value: "active"}}, map[string]struct{}{"status": {}}) if err != nil { t.Fatal(err) } - if len(where) != 1 || where[0] == "" { + if len(where) != 1 || len(args) != 1 || where[0] != "`status` = ?" { t.Fatalf("where = %#v", where) } } + +func TestBuildWhereSupportsGenericReaderOperators(t *testing.T) { + where, args, err := buildWhere([]Filter{ + {Column: "status", Op: "IN", Value: []string{"active", "pending"}}, + {Column: "score", Op: "GTE", Value: 10}, + {Column: "tags", Op: "ARRAY_CONTAINS", Value: "priority"}, + {Column: "deleted_at", Op: "IS_NULL"}, + }, map[string]struct{}{"status": {}, "score": {}, "tags": {}, "deleted_at": {}}) + if err != nil { + t.Fatal(err) + } + if len(where) != 4 || len(args) != 3 { + t.Fatalf("where = %#v", where) + } + for _, expression := range where { + if expression == "" { + t.Fatal("empty filter expression") + } + } +} diff --git a/internal/dataframe/materialization/schema.go b/internal/dataframe/materialization/schema.go index 7ce44bc..407a6e2 100644 --- a/internal/dataframe/materialization/schema.go +++ b/internal/dataframe/materialization/schema.go @@ -9,7 +9,7 @@ import ( var schemaIdentifierRE = regexp.MustCompile(`^[A-Za-z_][A-Za-z0-9_]*$`) -var supportedSchemaScalarRE = regexp.MustCompile(`^(String|Bool|Int8|Int16|Int32|Int64|Int128|Int256|UInt8|UInt16|UInt32|UInt64|UInt128|UInt256|Float32|Float64|Date|Date32|DateTime|DateTime64(\([^)]*\))?)$`) +var supportedSchemaScalarRE = regexp.MustCompile(`^(String|UUID|Bool|Int8|Int16|Int32|Int64|Int128|Int256|UInt8|UInt16|UInt32|UInt64|UInt128|UInt256|Float32|Float64|Date|Date32|DateTime|DateTime64(\([^)]*\))?)$`) func validateSchemaColumn(column Column) error { if column.Name == "" || !schemaIdentifierRE.MatchString(column.Name) || column.Name == "__loom_row_id" { @@ -74,6 +74,10 @@ func validateScalar(name, typ string, value any) error { if _, ok := value.(string); !ok { return fmt.Errorf("column %q expects String but received %T", name, value) } + case typ == "UUID": + if _, ok := value.(string); !ok { + return fmt.Errorf("column %q expects UUID text but received %T", name, value) + } case typ == "Bool": if _, ok := value.(bool); !ok { return fmt.Errorf("column %q expects Bool but received %T", name, value) diff --git a/internal/dataframe/materialization/service.go b/internal/dataframe/materialization/service.go index d92d4d9..223807c 100644 --- a/internal/dataframe/materialization/service.go +++ b/internal/dataframe/materialization/service.go @@ -2,8 +2,6 @@ package materialization import ( "context" - "crypto/sha256" - "encoding/hex" "encoding/json" "fmt" "sort" @@ -42,13 +40,24 @@ type Request struct { Schema []SchemaColumn } +const authResourcePathColumn = "auth_resource_path" + +func withReservedAuthColumn(columns []Column) ([]Column, error) { + for _, column := range columns { + if column.Name == authResourcePathColumn { + return nil, fmt.Errorf("schema column %q is reserved by Loom", authResourcePathColumn) + } + } + return append([]Column{{Name: authResourcePathColumn, ClickHouse: "Nullable(String)"}}, columns...), nil +} + // Preflight validates an explicit output schema before any ClickHouse table // is created. An empty schema preserves the original inference behavior. func (s *Service) Preflight(req Request) ([]Column, error) { if strings.TrimSpace(req.Name) == "" { return nil, fmt.Errorf("materialization name is required") } - if req.Run.Builder.Project == "" || req.Run.Builder.RootResourceType == "" { + if req.Run.Bindings.Project == "" || len(req.Run.Recipe.Outputs) == 0 || req.Run.Recipe.Outputs[0].RootResourceType == "" { return nil, fmt.Errorf("materialization dataframe project and root resource type are required") } if len(req.Schema) == 0 { @@ -66,7 +75,7 @@ func (s *Service) Preflight(req Request) ([]Column, error) { seen[column.Name] = struct{}{} result = append(result, column) } - return result, nil + return withReservedAuthColumn(result) } func (s *Service) Materialize(ctx context.Context, req Request) (Materialization, error) { @@ -83,10 +92,10 @@ func (s *Service) Materialize(ctx context.Context, req Request) (Materialization } id := uuid.NewString() m := Materialization{ - ID: id, Name: req.Name, Project: req.Run.Builder.Project, - DatasetGeneration: req.Run.Builder.DatasetGeneration, - State: StatePending, AuthScopeMode: req.Run.Builder.AuthScopeMode, - AuthResourcePaths: append([]string(nil), req.Run.Builder.AuthResourcePaths...), + ID: id, Name: req.Name, Project: req.Run.Bindings.Project, + DatasetGeneration: req.Run.Bindings.DatasetGeneration, + State: StatePending, AuthScopeMode: req.Run.Bindings.AuthScopeMode, + AuthResourcePaths: append([]string(nil), req.Run.Bindings.AuthResourcePaths...), PhysicalTable: "loom_df_" + strings.ReplaceAll(id, "-", ""), CreatedAt: time.Now().UTC(), } if err := s.Registry.Save(ctx, m); err != nil { @@ -140,6 +149,13 @@ func (s *Service) Materialize(ctx context.Context, req Request) (Materialization streamResult, err := s.Dataframes.Stream(ctx, req.Run, func(row map[string]any) error { rowCount++ row = cloneMap(row) + if _, ok := row[authResourcePathColumn]; !ok { + if len(req.Run.Bindings.AuthResourcePaths) == 1 { + row[authResourcePathColumn] = req.Run.Bindings.AuthResourcePaths[0] + } else { + row[authResourcePathColumn] = "" + } + } row["__loom_row_id"] = rowCount newColumns := make([]Column, 0) for name, value := range row { @@ -267,38 +283,3 @@ type MemoryRegistry struct { mu sync.RWMutex byID map[string]Materialization } - -func NewMemoryRegistry() *MemoryRegistry { return &MemoryRegistry{byID: map[string]Materialization{}} } -func (r *MemoryRegistry) Save(_ context.Context, m Materialization) error { - r.mu.Lock() - defer r.mu.Unlock() - r.byID[m.ID] = m - return nil -} -func (r *MemoryRegistry) Get(_ context.Context, id string) (Materialization, error) { - r.mu.RLock() - defer r.mu.RUnlock() - m, ok := r.byID[id] - if !ok { - return Materialization{}, fmt.Errorf("materialization %q not found", id) - } - return m, nil -} -func (r *MemoryRegistry) ListReady(_ context.Context, project string) ([]Materialization, error) { - r.mu.RLock() - defer r.mu.RUnlock() - out := []Materialization{} - for _, m := range r.byID { - if m.Project == project && m.State == StateReady { - out = append(out, m) - } - } - sort.Slice(out, func(i, j int) bool { return out[i].CreatedAt.Before(out[j].CreatedAt) }) - return out, nil -} - -func RecipeHash(value any) string { - data, _ := json.Marshal(value) - sum := sha256.Sum256(data) - return hex.EncodeToString(sum[:]) -} diff --git a/internal/dataframe/materialization/service_test.go b/internal/dataframe/materialization/service_test.go deleted file mode 100644 index c517cff..0000000 --- a/internal/dataframe/materialization/service_test.go +++ /dev/null @@ -1,203 +0,0 @@ -package materialization - -import ( - "context" - "errors" - "testing" - - dataframeruntime "github.com/calypr/loom/internal/dataframe/runtime" - "github.com/calypr/loom/internal/store/clickhouse" -) - -func TestInferColumnMapsPublishedDataframeValues(t *testing.T) { - cases := []struct { - name string - value any - want string - }{ - {"string", "x", "Nullable(String)"}, - {"integer", int64(1), "Nullable(Int64)"}, - {"float", 1.5, "Nullable(Float64)"}, - {"boolean", true, "Nullable(Bool)"}, - {"strings", []string{"a", "b"}, "Array(String)"}, - {"null", nil, "Nullable(String)"}, - } - for _, tc := range cases { - got, err := InferColumn(tc.name, tc.value) - if err != nil { - t.Fatalf("%s: %v", tc.name, err) - } - if got.ClickHouse != tc.want { - t.Fatalf("%s: type = %q, want %q", tc.name, got.ClickHouse, tc.want) - } - } -} - -func TestMemoryRegistryOnlyListsReadyRows(t *testing.T) { - registry := NewMemoryRegistry() - if err := registry.Save(nil, Materialization{ID: "pending", Project: "P", State: StatePending}); err != nil { - t.Fatal(err) - } - if err := registry.Save(nil, Materialization{ID: "ready", Project: "P", State: StateReady}); err != nil { - t.Fatal(err) - } - rows, err := registry.ListReady(nil, "P") - if err != nil { - t.Fatal(err) - } - if len(rows) != 1 || rows[0].ID != "ready" { - t.Fatalf("ready rows = %#v", rows) - } -} - -func TestPreflightValidatesExplicitSchema(t *testing.T) { - service := &Service{} - request := Request{ - Name: "patients", - Run: dataframeruntime.RunRequest{Builder: dataframeruntime.Builder{Project: "P", RootResourceType: "Patient"}}, - Schema: []SchemaColumn{{Name: "patient_id", ClickHouse: "Nullable(String)"}, {Name: "score", ClickHouse: "Nullable(Float64)"}}, - } - columns, err := service.Preflight(request) - if err != nil { - t.Fatal(err) - } - if len(columns) != 2 || columns[1].ClickHouse != "Nullable(Float64)" { - t.Fatalf("preflight schema = %#v", columns) - } - request.Schema[1].ClickHouse = "Map(String,String)" - if _, err := service.Preflight(request); err == nil { - t.Fatal("expected unsupported schema type error") - } -} - -func TestValidateValueAgainstSchema(t *testing.T) { - if err := ValidateValue(Column{Name: "tags", ClickHouse: "Array(String)"}, []string{"a"}); err != nil { - t.Fatal(err) - } - if err := ValidateValue(Column{Name: "score", ClickHouse: "Nullable(Float64)"}, "not-a-number"); err == nil { - t.Fatal("expected type validation error") - } -} - -type fixtureStreamer struct { - rows []map[string]any -} - -func (f fixtureStreamer) Stream(_ context.Context, _ dataframeruntime.RunRequest, visit func(map[string]any) error) (dataframeruntime.StreamResult, error) { - for _, row := range f.rows { - if err := visit(row); err != nil { - return dataframeruntime.StreamResult{}, err - } - } - return dataframeruntime.StreamResult{RowCount: len(f.rows)}, nil -} - -type fixtureClickHouse struct { - created []string - dropped []string - inserted []map[string]any - failInsert bool -} - -func (f *fixtureClickHouse) CreateTable(_ context.Context, table string, _ []clickhouse.Column) error { - f.created = append(f.created, table) - return nil -} - -func (f *fixtureClickHouse) AddColumn(_ context.Context, _ string, _ clickhouse.Column) error { - return nil -} - -func (f *fixtureClickHouse) InsertRows(_ context.Context, _ string, _ []clickhouse.Column, rows []map[string]any) error { - if f.failInsert { - return errors.New("fixture insert failed") - } - f.inserted = append(f.inserted, rows...) - return nil -} - -func (f *fixtureClickHouse) DropTable(_ context.Context, table string) error { - f.dropped = append(f.dropped, table) - return nil -} - -func fixtureRequest() Request { - return Request{ - Name: "fixture", - Run: dataframeruntime.RunRequest{Builder: dataframeruntime.Builder{Project: "P", RootResourceType: "Patient"}}, - Schema: []SchemaColumn{ - {Name: "patient_id", ClickHouse: "Nullable(String)"}, - {Name: "score", ClickHouse: "Nullable(Float64)"}, - {Name: "active", ClickHouse: "Nullable(Bool)"}, - {Name: "tags", ClickHouse: "Array(String)"}, - {Name: "note", ClickHouse: "Nullable(String)"}, - }, - } -} - -func TestMaterializeRepresentativeFixture(t *testing.T) { - store := &fixtureClickHouse{} - service := &Service{Dataframes: fixtureStreamer{rows: []map[string]any{ - {"patient_id": "p1", "score": 1.5, "active": true, "tags": []string{"a", "b"}, "note": nil}, - {"patient_id": "p2", "score": 2.5, "active": false, "tags": []string{}, "note": "ok"}, - }}, ClickHouse: store, Registry: NewMemoryRegistry(), BatchSize: 1} - result, err := service.Materialize(context.Background(), fixtureRequest()) - if err != nil { - t.Fatal(err) - } - if result.State != StateReady || result.RowCount != 2 || len(store.inserted) != 2 { - t.Fatalf("materialization = %#v, inserted = %#v", result, store.inserted) - } - if len(store.dropped) != 0 { - t.Fatalf("successful materialization dropped tables: %#v", store.dropped) - } -} - -func TestMaterializeExplicitSchemaAllowsEmptyResult(t *testing.T) { - store := &fixtureClickHouse{} - service := &Service{Dataframes: fixtureStreamer{}, ClickHouse: store, Registry: NewMemoryRegistry()} - result, err := service.Materialize(context.Background(), fixtureRequest()) - if err != nil { - t.Fatal(err) - } - if result.State != StateReady || result.RowCount != 0 || len(store.created) != 1 { - t.Fatalf("empty materialization = %#v, created = %#v", result, store.created) - } -} - -func TestMaterializeFailureDropsCreatedTable(t *testing.T) { - store := &fixtureClickHouse{failInsert: true} - registry := NewMemoryRegistry() - service := &Service{Dataframes: fixtureStreamer{rows: []map[string]any{{ - "patient_id": "p1", "score": 1.5, "active": true, "tags": []string{"a"}, "note": nil, - }}}, ClickHouse: store, Registry: registry} - _, err := service.Materialize(context.Background(), fixtureRequest()) - if err == nil { - t.Fatal("expected insert failure") - } - if len(store.created) != 1 || len(store.dropped) != 1 || store.created[0] != store.dropped[0] { - t.Fatalf("cleanup created=%#v dropped=%#v", store.created, store.dropped) - } - registry.mu.RLock() - defer registry.mu.RUnlock() - for _, materialization := range registry.byID { - if materialization.State != StateFailed { - t.Fatalf("failed materialization state = %s", materialization.State) - } - return - } - t.Fatal("materialization was not registered") -} - -func TestMaterializeRejectsFixtureTypeMismatchAndCleansUp(t *testing.T) { - store := &fixtureClickHouse{} - service := &Service{Dataframes: fixtureStreamer{rows: []map[string]any{{ - "patient_id": "p1", "score": "wrong", "active": true, "tags": []string{"a"}, "note": nil, - }}}, ClickHouse: store, Registry: NewMemoryRegistry()} - if _, err := service.Materialize(context.Background(), fixtureRequest()); err == nil { - t.Fatal("expected schema type mismatch") - } - if len(store.dropped) != 1 { - t.Fatalf("type mismatch cleanup = %#v", store.dropped) - } -} diff --git a/internal/dataframe/materialization/types.go b/internal/dataframe/materialization/types.go index df399d7..095d5b1 100644 --- a/internal/dataframe/materialization/types.go +++ b/internal/dataframe/materialization/types.go @@ -29,6 +29,7 @@ type SchemaColumn = Column type Materialization struct { ID string `json:"id"` Name string `json:"name"` + Revision string `json:"revision,omitempty"` Project string `json:"project"` DatasetGeneration string `json:"datasetGeneration"` State State `json:"state"` diff --git a/internal/dataframe/physical_required_match_arango_integration_test.go b/internal/dataframe/physical_required_match_arango_integration_test.go deleted file mode 100644 index 1bd570f..0000000 --- a/internal/dataframe/physical_required_match_arango_integration_test.go +++ /dev/null @@ -1,48 +0,0 @@ -package dataframe - -import ( - "context" - "os" - "strings" - "testing" - - arangostore "github.com/calypr/loom/internal/store/arango" -) - -// TestPhysicalRequiredTraversalMatchesExplainAgainstArango keeps the P4 -// execution contract tied to the real parser and optimizer. It is opt-in like -// the other compiler integration tests because it targets a developer-loaded -// META database. It covers both the normal parent-to-child INBOUND route and -// the explicitly proven ResearchSubject -> ResearchStudy OUTBOUND route. -func TestPhysicalRequiredTraversalMatchesExplainAgainstArango(t *testing.T) { - if os.Getenv("LOOM_COMPILER_ARANGO_INTEGRATION") == "" { - t.Skip("set LOOM_COMPILER_ARANGO_INTEGRATION=1 to run compiler/Arango integration") - } - url, database, project := compilerArangoTarget() - for _, test := range []struct { - name string - root string - label string - target string - direction string - }{ - {name: "inbound", root: "Patient", label: "subject_Patient", target: "Specimen", direction: "INBOUND"}, - {name: "proven outbound", root: "ResearchSubject", label: "study", target: "ResearchStudy", direction: "OUTBOUND"}, - } { - t.Run(test.name, func(t *testing.T) { - compiled, err := CompileRequest(Builder{ - Project: project, RootResourceType: test.root, - Traversals: []TraversalStep{{Label: test.label, ToResourceType: test.target, Alias: "required", MatchMode: TraversalMatchRequired}}, - }, 5) - if err != nil { - t.Fatal(err) - } - if !strings.Contains(compiled.Query, "FOR required_0_node_0, required_0_edge_0 IN 1..1 "+test.direction+" root @@required_0_0_edge_collection") { - t.Fatalf("required traversal did not use physical renderer:\n%s", compiled.Query) - } - if _, err := ExplainCompiledQuery(context.Background(), arangostore.ConnectionOptions{URL: url, Database: database}, compiled); err != nil { - t.Fatalf("ExplainCompiledQuery() error = %v\nAQL:\n%s", err, compiled.Query) - } - }) - } -} diff --git a/internal/dataframe/publication/clickhouse/target.go b/internal/dataframe/publication/clickhouse/target.go new file mode 100644 index 0000000..5d8d07f --- /dev/null +++ b/internal/dataframe/publication/clickhouse/target.go @@ -0,0 +1,120 @@ +// Package clickhouse adapts Loom's existing atomic ClickHouse bundle store to +// the backend-neutral publication runner. +package clickhouse + +import ( + "context" + "fmt" + "strings" + + "github.com/calypr/loom/internal/dataframe/materialization" + "github.com/calypr/loom/internal/dataframe/publication" + store "github.com/calypr/loom/internal/store/clickhouse" +) + +type Target struct { + Store materialization.IdentityBundleStore +} + +func New(store materialization.IdentityBundleStore) (*Target, error) { + if store == nil { + return nil, fmt.Errorf("ClickHouse bundle store is required") + } + return &Target{Store: store}, nil +} + +func (t *Target) Begin(ctx context.Context, identity publication.PublicationIdentity, schemas []publication.OutputSchema) (publication.Transaction, error) { + bundleIdentity := materialization.BundleIdentity{Name: identity.Name, Project: identity.Project, DatasetGeneration: identity.DatasetGeneration, RecipeDigest: identity.RecipeDigest, SchemaDigest: identity.SchemaDigest, ScopeDigest: identity.ScopeDigest, EngineVersion: identity.EngineVersion, AuthResourcePaths: append([]string(nil), identity.AuthResourcePaths...)} + tx, err := t.Store.BeginBundleFor(ctx, bundleIdentity) + if err != nil { + return nil, err + } + result := &transaction{tx: tx, columns: make(map[string][]store.Column, len(schemas))} + for _, schema := range schemas { + columns, err := toColumns(schema.Columns) + if err != nil { + _ = tx.Rollback(context.Background()) + return nil, fmt.Errorf("output %q schema: %w", schema.Name, err) + } + if err := tx.CreateOutput(ctx, schema.Name, columns); err != nil { + _ = tx.Rollback(context.Background()) + return nil, fmt.Errorf("output %q create: %w", schema.Name, err) + } + result.columns[schema.Name] = columns + } + return result, nil +} + +type transaction struct { + tx materialization.AtomicBundleTx + columns map[string][]store.Column + closed bool +} + +func (t *transaction) WriteBatch(ctx context.Context, output string, rows []map[string]any) error { + if t.closed { + return fmt.Errorf("ClickHouse publication transaction is closed") + } + columns, ok := t.columns[output] + if !ok { + return fmt.Errorf("output %q was not declared", output) + } + return t.tx.InsertRows(ctx, output, columns, rows) +} + +func (t *transaction) Validate(context.Context) error { + if t.closed { + return fmt.Errorf("ClickHouse publication transaction is closed") + } + return nil +} + +func (t *transaction) Commit(ctx context.Context) ([]publication.PublishedOutput, error) { + if t.closed { + return nil, fmt.Errorf("ClickHouse publication transaction is closed") + } + if err := t.tx.Commit(ctx); err != nil { + return nil, err + } + t.closed = true + return nil, nil +} + +func (t *transaction) Rollback(ctx context.Context) error { + if t.closed { + return nil + } + t.closed = true + return t.tx.Rollback(ctx) +} + +func toColumns(columns []publication.LogicalColumn) ([]store.Column, error) { + result := make([]store.Column, 0, len(columns)) + for _, column := range columns { + kind := strings.ToLower(strings.TrimSpace(column.Kind)) + columnType := "String" + switch kind { + case "boolean": + columnType = "Bool" + case "integer": + columnType = "Int64" + case "decimal": + columnType = "Float64" + case "date": + columnType = "Date" + case "date-time": + columnType = "DateTime64(3)" + case "uuid": + columnType = "UUID" + case "code": + columnType = "String" + case "object": + return nil, fmt.Errorf("object-valued column %q is not supported", column.Name) + } + if column.Repeated { + columnType = "Array(" + columnType + ")" + } + result = append(result, store.Column{Name: column.Name, Type: columnType}) + } + return result, nil +} diff --git a/internal/dataframe/publication/runner.go b/internal/dataframe/publication/runner.go new file mode 100644 index 0000000..427145d --- /dev/null +++ b/internal/dataframe/publication/runner.go @@ -0,0 +1,258 @@ +package publication + +import ( + "context" + "encoding/json" + "fmt" + "math" + "reflect" + "strings" +) + +// Publish consumes each stream once and keeps at most one bounded batch in +// memory. A target is not committed until every stream has passed validation. +func Publish(ctx context.Context, target Target, identity PublicationIdentity, outputs []OutputStream, limits Limits) (Result, error) { + if target == nil { + return Result{}, fmt.Errorf("publication target is required") + } + if len(outputs) == 0 { + return Result{}, fmt.Errorf("publication requires at least one output") + } + normalizedOutputs, err := injectAuthResourcePath(identity, outputs) + if err != nil { + return Result{}, err + } + schemas, err := validateOutputs(normalizedOutputs) + if err != nil { + return Result{}, err + } + limits = limits.normalized() + tx, err := target.Begin(ctx, identity, schemas) + if err != nil { + return Result{}, err + } + fail := func(cause error) (Result, error) { + if rollbackErr := tx.Rollback(context.Background()); rollbackErr != nil { + return Result{}, fmt.Errorf("%w (publication rollback failed: %v)", cause, rollbackErr) + } + return Result{}, cause + } + stats := make(map[string]PublishedOutput, len(normalizedOutputs)) + for _, output := range normalizedOutputs { + stat := PublishedOutput{Name: output.Name} + batch := make([]map[string]any, 0, limits.BatchRows) + batchBytes := 0 + flush := func() error { + if len(batch) == 0 { + return nil + } + if err := tx.WriteBatch(ctx, output.Name, batch); err != nil { + return err + } + batch = batch[:0] + batchBytes = 0 + return nil + } + err := output.Stream(ctx, func(row map[string]any) error { + if err := ctx.Err(); err != nil { + return err + } + if err := validateRow(output.Columns, row); err != nil { + return err + } + encoded, err := json.Marshal(row) + if err != nil { + return fmt.Errorf("output %q encode row: %w", output.Name, err) + } + if len(encoded) > limits.BatchBytes { + return fmt.Errorf("output %q row exceeds batch byte limit %d", output.Name, limits.BatchBytes) + } + batch = append(batch, row) + batchBytes += len(encoded) + stat.RowCount++ + stat.ByteCount += int64(len(encoded)) + if len(batch) >= limits.BatchRows || batchBytes >= limits.BatchBytes { + return flush() + } + return nil + }) + if err != nil { + return fail(fmt.Errorf("output %q stream: %w", output.Name, err)) + } + if err := flush(); err != nil { + return fail(fmt.Errorf("output %q final batch: %w", output.Name, err)) + } + stats[output.Name] = stat + } + if err := tx.Validate(ctx); err != nil { + return fail(fmt.Errorf("publication validation: %w", err)) + } + published, err := tx.Commit(ctx) + if err != nil { + return fail(fmt.Errorf("publication commit: %w", err)) + } + if len(published) == 0 { + published = make([]PublishedOutput, 0, len(outputs)) + for _, output := range normalizedOutputs { + published = append(published, stats[output.Name]) + } + } else { + for i := range published { + if stat, ok := stats[published[i].Name]; ok { + published[i].RowCount = stat.RowCount + published[i].ByteCount = stat.ByteCount + } + } + } + return Result{Outputs: published}, nil +} + +func injectAuthResourcePath(identity PublicationIdentity, outputs []OutputStream) ([]OutputStream, error) { + result := make([]OutputStream, 0, len(outputs)) + for _, output := range outputs { + for _, column := range output.Columns { + if column.Name == "auth_resource_path" { + return nil, fmt.Errorf("output %q column %q is reserved by Loom", output.Name, column.Name) + } + } + copyOutput := output + copyOutput.Columns = append([]LogicalColumn{{Name: "auth_resource_path", Kind: "string", Nullable: true}}, output.Columns...) + originalStream := output.Stream + copyOutput.Stream = func(ctx context.Context, visit func(map[string]any) error) error { + return originalStream(ctx, func(row map[string]any) error { + if row == nil { + return visit(nil) + } + if _, ok := row["auth_resource_path"]; !ok { + row = cloneRow(row) + if len(identity.AuthResourcePaths) == 1 { + row["auth_resource_path"] = identity.AuthResourcePaths[0] + } else { + row["auth_resource_path"] = "" + } + } + return visit(row) + }) + } + result = append(result, copyOutput) + } + return result, nil +} + +func cloneRow(row map[string]any) map[string]any { + copy := make(map[string]any, len(row)+1) + for key, value := range row { + copy[key] = value + } + return copy +} + +func validateOutputs(outputs []OutputStream) ([]OutputSchema, error) { + seen := map[string]struct{}{} + schemas := make([]OutputSchema, 0, len(outputs)) + for _, output := range outputs { + name := strings.TrimSpace(output.Name) + if name == "" || output.Stream == nil { + return nil, fmt.Errorf("output name and stream are required") + } + if _, ok := seen[name]; ok { + return nil, fmt.Errorf("output %q is duplicated", name) + } + seen[name] = struct{}{} + if len(output.Columns) == 0 { + return nil, fmt.Errorf("output %q has no columns", name) + } + columns := append([]LogicalColumn(nil), output.Columns...) + columnSeen := map[string]struct{}{} + for _, column := range columns { + if strings.TrimSpace(column.Name) == "" || strings.TrimSpace(column.Kind) == "" { + return nil, fmt.Errorf("output %q has an invalid column", name) + } + if _, ok := columnSeen[column.Name]; ok { + return nil, fmt.Errorf("output %q column %q is duplicated", name, column.Name) + } + columnSeen[column.Name] = struct{}{} + } + schemas = append(schemas, OutputSchema{Name: name, Columns: columns}) + } + return schemas, nil +} + +func validateRow(columns []LogicalColumn, row map[string]any) error { + if row == nil { + return fmt.Errorf("row is nil") + } + known := make(map[string]LogicalColumn, len(columns)) + for _, column := range columns { + known[column.Name] = column + value, ok := row[column.Name] + if !ok || value == nil { + if !column.Nullable { + return fmt.Errorf("required column %q is missing", column.Name) + } + continue + } + if err := validateValue(column, value); err != nil { + return err + } + } + for name := range row { + if _, ok := known[name]; !ok { + return fmt.Errorf("row contains undeclared column %q", name) + } + } + return nil +} + +func validateValue(column LogicalColumn, value any) error { + if column.Repeated { + v := reflect.ValueOf(value) + if v.Kind() != reflect.Array && v.Kind() != reflect.Slice { + return fmt.Errorf("column %q must be repeated", column.Name) + } + for i := 0; i < v.Len(); i++ { + if err := validateScalar(column, v.Index(i).Interface()); err != nil { + return err + } + } + return nil + } + return validateScalar(column, value) +} + +func validateScalar(column LogicalColumn, value any) error { + if value == nil { + return nil + } + kind := strings.ToLower(strings.TrimSpace(column.Kind)) + valid := false + switch kind { + case "string", "code", "uuid", "date", "date-time", "datetime": + _, valid = value.(string) + case "integer": + switch value.(type) { + case int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64, float64, json.Number: + valid = true + } + case "decimal": + switch value.(type) { + case int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64, float32, float64, json.Number: + valid = true + } + case "boolean": + _, valid = value.(bool) + case "object": + return fmt.Errorf("object-valued column %q is not supported by the flat publication contract", column.Name) + default: + return fmt.Errorf("column %q has unsupported logical kind %q", column.Name, column.Kind) + } + if !valid { + return fmt.Errorf("column %q has value of incompatible type %T", column.Name, value) + } + if kind == "integer" { + if f, ok := value.(float64); ok && (math.IsNaN(f) || math.IsInf(f, 0) || math.Trunc(f) != f) { + return fmt.Errorf("column %q has non-integral value", column.Name) + } + } + return nil +} diff --git a/internal/dataframe/publication/runner_test.go b/internal/dataframe/publication/runner_test.go new file mode 100644 index 0000000..5947091 --- /dev/null +++ b/internal/dataframe/publication/runner_test.go @@ -0,0 +1,96 @@ +package publication + +import ( + "context" + "errors" + "testing" +) + +type fakeTx struct { + batches [][]map[string]any + validated bool + committed bool + rolledBack bool +} + +func (t *fakeTx) WriteBatch(_ context.Context, _ string, rows []map[string]any) error { + t.batches = append(t.batches, rows) + return nil +} +func (t *fakeTx) Validate(context.Context) error { t.validated = true; return nil } +func (t *fakeTx) Commit(context.Context) ([]PublishedOutput, error) { + t.committed = true + return []PublishedOutput{{Name: "patients", PhysicalName: "staged_patients"}}, nil +} +func (t *fakeTx) Rollback(context.Context) error { t.rolledBack = true; return nil } + +type fakeTarget struct { + tx *fakeTx + schemas []OutputSchema +} + +func (t *fakeTarget) Begin(_ context.Context, _ PublicationIdentity, schemas []OutputSchema) (Transaction, error) { + t.schemas = schemas + t.tx = &fakeTx{} + return t.tx, nil +} + +func TestPublishValidatesAndBoundsBatches(t *testing.T) { + target := &fakeTarget{} + result, err := Publish(context.Background(), target, PublicationIdentity{Name: "r", AuthResourcePaths: []string{"/programs/p1"}}, []OutputStream{{ + Name: "patients", + Columns: []LogicalColumn{{Name: "__loom_row_id", Kind: "string", IsIdentity: true}, {Name: "id", Kind: "string"}}, + Stream: func(_ context.Context, visit func(map[string]any) error) error { + for _, row := range []map[string]any{{"__loom_row_id": "a", "id": "p1"}, {"__loom_row_id": "b", "id": "p2"}, {"__loom_row_id": "c", "id": "p3"}} { + if err := visit(row); err != nil { + return err + } + } + return nil + }, + }}, Limits{BatchRows: 2, BatchBytes: 1024}) + if err != nil { + t.Fatal(err) + } + if !target.tx.validated || !target.tx.committed || target.tx.rolledBack || len(target.tx.batches) != 2 { + t.Fatalf("unexpected transaction lifecycle: %#v", target.tx) + } + if result.Outputs[0].RowCount != 3 || result.Outputs[0].PhysicalName != "staged_patients" { + t.Fatalf("unexpected result: %#v", result) + } + if len(target.schemas) != 1 || target.schemas[0].Columns[0].Name != "auth_resource_path" { + t.Fatalf("reserved auth column missing: %#v", target.schemas) + } + if got := target.tx.batches[0][0]["auth_resource_path"]; got != "/programs/p1" { + t.Fatalf("auth resource path = %#v", got) + } +} + +func TestPublishRollsBackOnSchemaViolation(t *testing.T) { + target := &fakeTarget{} + _, err := Publish(context.Background(), target, PublicationIdentity{Name: "r"}, []OutputStream{{ + Name: "patients", + Columns: []LogicalColumn{{Name: "__loom_row_id", Kind: "string"}, {Name: "id", Kind: "string"}}, + Stream: func(_ context.Context, visit func(map[string]any) error) error { + return visit(map[string]any{"__loom_row_id": "a", "unknown": "x"}) + }, + }}, Limits{}) + if err == nil || target.tx == nil || !target.tx.rolledBack || target.tx.committed { + t.Fatalf("expected rollback on row violation: err=%v tx=%#v", err, target.tx) + } +} + +func TestPublishHonorsCancellation(t *testing.T) { + target := &fakeTarget{} + ctx, cancel := context.WithCancel(context.Background()) + cancel() + _, err := Publish(ctx, target, PublicationIdentity{Name: "r"}, []OutputStream{{ + Name: "patients", Columns: []LogicalColumn{{Name: "id", Kind: "string"}}, + Stream: func(_ context.Context, visit func(map[string]any) error) error { + return visit(map[string]any{"id": "p1"}) + }, + }}, Limits{}) + if !errors.Is(err, context.Canceled) || target.tx == nil || !target.tx.rolledBack { + t.Fatalf("expected cancellation rollback: err=%v tx=%#v", err, target.tx) + } +} diff --git a/internal/dataframe/publication/types.go b/internal/dataframe/publication/types.go new file mode 100644 index 0000000..d6559c3 --- /dev/null +++ b/internal/dataframe/publication/types.go @@ -0,0 +1,81 @@ +// Package publication owns the backend-neutral publication contract for +// resolved dataframe streams. Targets stage rows and decide how a publication +// becomes visible; this package owns validation, bounded batching, and the +// lifecycle around that target. +package publication + +import "context" + +// LogicalColumn is the backend-independent schema emitted by the compiler. +// Kind is one of string, code, uuid, date, date-time, integer, decimal, +// boolean, or object. Object values are rejected by the generic MVP runner +// unless a target explicitly opts into a serialization policy. +type LogicalColumn struct { + Name string + Kind string + Repeated bool + Nullable bool + IsIdentity bool +} + +type OutputSchema struct { + Name string + Columns []LogicalColumn +} + +// OutputStream is consumed exactly once. The callback must invoke visit for +// each row and must stop when visit returns an error. +type OutputStream struct { + Name string + Columns []LogicalColumn + Stream func(context.Context, func(map[string]any) error) error +} + +type PublicationIdentity struct { + Name string + Project string + DatasetGeneration string + RecipeDigest string + SchemaDigest string + ScopeDigest string + EngineVersion string + TargetConfigDigest string + AuthResourcePaths []string +} + +type PublishedOutput struct { + Name string + PhysicalName string + RowCount int64 + ByteCount int64 +} + +type Target interface { + Begin(context.Context, PublicationIdentity, []OutputSchema) (Transaction, error) +} + +type Transaction interface { + WriteBatch(context.Context, string, []map[string]any) error + Validate(context.Context) error + Commit(context.Context) ([]PublishedOutput, error) + Rollback(context.Context) error +} + +type Limits struct { + BatchRows int + BatchBytes int +} + +func (l Limits) normalized() Limits { + if l.BatchRows <= 0 { + l.BatchRows = 1000 + } + if l.BatchBytes <= 0 { + l.BatchBytes = 4 << 20 + } + return l +} + +type Result struct { + Outputs []PublishedOutput +} diff --git a/internal/dataframe/recipe/canonical.go b/internal/dataframe/recipe/canonical.go new file mode 100644 index 0000000..66d819a --- /dev/null +++ b/internal/dataframe/recipe/canonical.go @@ -0,0 +1,64 @@ +// Package recipe defines the persistence-neutral recipe document used by the +// dataframe compiler. A recipe describes semantic row shaping only; it never +// carries database collection, table, AQL, or SQL details. +package recipe + +import ( + "crypto/sha256" + "encoding/hex" + "encoding/json" +) + +func (b Bundle) CanonicalJSON() ([]byte, error) { + if err := b.Validate(); err != nil { + return nil, err + } + return json.Marshal(b) +} + +// Digest returns the SHA-256 digest of CanonicalJSON encoded as lowercase hex. +func (b Bundle) Digest() (string, error) { + canonical, err := b.CanonicalJSON() + if err != nil { + return "", err + } + sum := sha256.Sum256(canonical) + return hex.EncodeToString(sum[:]), nil +} + +// Explain returns the validated semantic shape and canonical digest. +func (b Bundle) Explain() (Explanation, error) { + digest, err := b.Digest() + if err != nil { + return Explanation{}, err + } + exp := Explanation{RecipeSchemaVersion: b.RecipeSchemaVersion, Name: b.Name, TranslationVersion: b.TranslationVersion, Digest: digest, Outputs: make([]OutputExplanation, len(b.Outputs))} + for i, out := range b.Outputs { + e := OutputExplanation{Name: out.Name, RootResourceType: out.RootResourceType, RowGrain: out.RowGrain, Expanded: out.Expand != nil} + for _, f := range out.Fields { + e.FieldNames = append(e.FieldNames, f.Name) + } + for _, t := range out.Traversals { + e.TraversalNames = append(e.TraversalNames, t.Name) + } + for _, d := range out.DynamicColumns { + e.DynamicColumns = append(e.DynamicColumns, d.Name) + } + for _, projection := range out.CatalogProjections { + e.CatalogProjections = append(e.CatalogProjections, projection.Name) + } + exp.Outputs[i] = e + } + return exp, nil +} + +type ValidationError struct { + Code string + Path string + Message string +} + +func (e *ValidationError) Error() string { return e.Code + " at " + e.Path + ": " + e.Message } +func validationError(code, path, message string) error { + return &ValidationError{Code: code, Path: path, Message: message} +} diff --git a/internal/dataframe/recipe/control/explain.go b/internal/dataframe/recipe/control/explain.go new file mode 100644 index 0000000..d92e647 --- /dev/null +++ b/internal/dataframe/recipe/control/explain.go @@ -0,0 +1,107 @@ +package control + +import ( + "context" + + "github.com/calypr/loom/internal/dataframe/compiler" + "github.com/calypr/loom/internal/dataframe/recipe" + "github.com/calypr/loom/internal/store/arango" +) + +// PhysicalExplanation is the output-scoped physical half of recipe Explain. +// It contains compiler structure and an optional live Arango assessment; it +// never contains AQL text, bind values, auth paths, credentials, or connection +// information. +type PhysicalExplanation struct { + Outputs []PhysicalOutputExplanation +} + +type PhysicalOutputExplanation struct { + Name string + PlanFingerprint string + Columns []string + Diagnostics compiler.CompilerPlanDiagnostics + Live *ExplainAssessment +} + +// ExplainAssessment is a transport-neutral, sanitized copy of Arango's +// ExplainAssessment. Keeping this DTO here lets GraphQL and HTTP adapters use +// one assessment shape without parsing Explain responses themselves. +type ExplainAssessment struct { + Plans []ExplainPlanEstimate + FullCollectionScans []ExplainCollectionScan + Indexes []ExplainIndexSummary + Warnings []ExplainWarning + AppliedOptimizerRules []string +} + +type ExplainPlanEstimate struct { + Plan int + EstimatedCost float64 + EstimatedNrItems float64 +} + +type ExplainCollectionScan struct { + Plan int + NodeID int64 + Collection string +} + +type ExplainIndexSummary struct { + Collection string + ID string + Name string + Type string + Fields []string + Uses []ExplainIndexLocation +} + +type ExplainIndexLocation struct { + Plan int + NodeID int64 +} + +type ExplainWarning struct { + Code int + Message string +} + +// ExplainPhysicalFunc allows the production recipe engine to provide its +// canonical compiled outputs without coupling this control-plane package to +// engine. The live flag requests a configured Arango Explain; when false +// the callback must return compiler-only diagnostics and fingerprints. +type ExplainPhysicalFunc func(context.Context, string, recipe.RuntimeBindings, bool) (PhysicalExplanation, error) + +// AssessmentFromArango converts the common store assessment to the sanitized +// control-plane DTO. It copies every slice so callers cannot mutate shared +// assessment state. +func AssessmentFromArango(input arango.ExplainAssessment) ExplainAssessment { + out := ExplainAssessment{ + Plans: make([]ExplainPlanEstimate, 0, len(input.Plans)), + FullCollectionScans: make([]ExplainCollectionScan, 0, len(input.FullCollectionScans)), + Indexes: make([]ExplainIndexSummary, 0, len(input.Indexes)), + Warnings: make([]ExplainWarning, 0, len(input.Warnings)), + AppliedOptimizerRules: append([]string(nil), input.AppliedOptimizerRules...), + } + for _, plan := range input.Plans { + out.Plans = append(out.Plans, ExplainPlanEstimate{Plan: plan.Plan, EstimatedCost: plan.EstimatedCost, EstimatedNrItems: plan.EstimatedNrItems}) + } + for _, scan := range input.FullCollectionScans { + out.FullCollectionScans = append(out.FullCollectionScans, ExplainCollectionScan{Plan: scan.Plan, NodeID: scan.NodeID, Collection: scan.Collection}) + } + for _, index := range input.Indexes { + copyIndex := ExplainIndexSummary{Collection: index.Collection, ID: index.ID, Name: index.Name, Type: index.Type, Fields: append([]string(nil), index.Fields...), Uses: make([]ExplainIndexLocation, 0, len(index.Uses))} + for _, use := range index.Uses { + copyIndex.Uses = append(copyIndex.Uses, ExplainIndexLocation{Plan: use.Plan, NodeID: use.NodeID}) + } + out.Indexes = append(out.Indexes, copyIndex) + } + for _, warning := range input.Warnings { + out.Warnings = append(out.Warnings, ExplainWarning{Code: warning.Code, Message: warning.Message}) + } + return out +} + +// ClonePhysicalExplanation returns an ownership-safe copy suitable for a +// transport adapter. Compiler diagnostics are structural and contain no bind +// values; clone their nested slices before returning them to callers. diff --git a/internal/dataframe/recipe/control/service.go b/internal/dataframe/recipe/control/service.go new file mode 100644 index 0000000..3bc3296 --- /dev/null +++ b/internal/dataframe/recipe/control/service.go @@ -0,0 +1,62 @@ +// Package control exposes the backend-neutral control-plane operations +// needed by GraphQL or HTTP adapters. It deliberately contains no transport +// types and no translation-specific output names. +package control + +import ( + "context" + + "github.com/calypr/loom/internal/dataframe/recipe" + "github.com/calypr/loom/internal/dataframe/recipe/exec" + "github.com/calypr/loom/internal/dataframe/semantic" +) + +type Registry interface { + Get(string) (exec.Entry, bool) +} + +// ContextRegistry is the durable registry shape. Durable adapters keep +// storage errors distinct from a missing recipe; the control-plane adapter +// exposes the small transport-neutral Registry interface used by this +// package's existing in-memory tests. +type ContextRegistry interface { + LoadRecipe(context.Context, string) (exec.Entry, error) +} + +type DurableRegistry struct{ Store ContextRegistry } + +type Service struct { + Registry Registry + ScopeDigest func(recipe.RuntimeBindings) string + // ExplainPhysicalFn is injected by the production canonical compiler + // boundary. Keeping it as a callback prevents this control package from + // owning recipe execution or a second physical renderer. + ExplainPhysicalFn ExplainPhysicalFunc +} + +type Validation struct { + Entry exec.Entry + Plan semantic.RecipePlan +} + +type Preview struct { + Plan semantic.ResolvedRecipePlan + Rows map[string][]map[string]any + // Outputs preserves recipe order and carries the compiler-owned public + // schema alongside rows. Rows is retained for non-transport callers during + // the migration, but adapters must use Outputs when present. + Outputs []OutputRows +} + +type OutputRows struct { + Name string + Columns []string + Rows []map[string]any +} + +type ExecuteFunc func(context.Context, semantic.ResolvedRecipePlan, int) (map[string][]map[string]any, error) + +// ExplainPhysical returns compiler diagnostics and, when requested by the +// caller, a live Arango assessment for every resolved output. Semantic Explain +// remains available through Explain and is intentionally not replaced by this +// backend-facing view. diff --git a/internal/dataframe/recipe/default.go b/internal/dataframe/recipe/default.go new file mode 100644 index 0000000..f7c8b47 --- /dev/null +++ b/internal/dataframe/recipe/default.go @@ -0,0 +1,13 @@ +package recipe + +import ( + _ "embed" +) + +// DefaultACEDJSON is the checked-in recipe data used as the initial translation +// bundle. It is data, not a Go implementation of any output behavior. +// +//go:embed default_aced.json +var DefaultACEDJSON []byte + +func DefaultACEDBundle() (Bundle, error) { return Parse(DefaultACEDJSON) } diff --git a/internal/dataframe/recipe/default_aced.json b/internal/dataframe/recipe/default_aced.json new file mode 100644 index 0000000..bd9d4ef --- /dev/null +++ b/internal/dataframe/recipe/default_aced.json @@ -0,0 +1,138 @@ +{ + "recipeSchemaVersion": 1, + "name": "aced-meta-default", + "translationVersion": "gen3-util-development-d461f11c7f0ccb078128349ccffd377e4014b62a", + "outputs": [ + { + "name": "DocumentReference", + "rootResourceType": "DocumentReference", + "rowGrain": "file", + "collisionPolicy": "overwrite", + "fields": [ + {"name": "id", "expr": {"select": "root.id"}}, + {"name": "identifier", "expr": {"call": "first", "args": [{"select": "root.identifier[].value"}]}}, + {"name": "status", "expr": {"select": "root.status"}}, + {"name": "subject", "expr": {"call": "reference_id", "args": [{"select": "root.subject.reference"}]}}, + {"name": "based_on", "expr": {"call": "all", "args": [{"select": "root.basedOn[].reference"}]}} + ], + "catalogProjections": [{"name": "populated_fields", "includePaths": ["*"], "kinds": ["scalar"], "naming": "PATH", "valueMode": "FIRST", "maxColumns": 256}], + "dynamicColumns": [ + {"name": "identifier_by_system", "source": {"select": "root.identifier[]"}, "key": {"select": "item.system"}, "value": {"select": "item.value"}, "maxColumns": 64}, + {"name": "extension_by_url", "source": {"select": "root.extension[]"}, "key": {"select": "item.url"}, "value": {"call": "coalesce_string", "args": [{"select": "item.valueString"}, {"select": "item.valueCode"}, {"select": "item.valueInteger"}, {"select": "item.valueDecimal"}, {"select": "item.valueBoolean"}, {"select": "item.valueDate"}, {"select": "item.valueDateTime"}]}, "maxColumns": 128} + ], + "traversals": [ + { + "name": "subject_Specimen", + "toResourceType": "Specimen", + "alias": "specimen", + "dynamicColumns": [{"name": "identifier_by_system", "source": {"select": "specimen.identifier[]"}, "key": {"select": "item.system"}, "value": {"select": "item.value"}, "maxColumns": 64}, {"name": "extension_by_url", "source": {"select": "specimen.extension[]"}, "key": {"select": "item.url"}, "value": {"call": "coalesce_string", "args": [{"select": "item.valueString"}, {"select": "item.valueCode"}, {"select": "item.valueInteger"}, {"select": "item.valueDecimal"}, {"select": "item.valueBoolean"}, {"select": "item.valueDate"}, {"select": "item.valueDateTime"}]}, "maxColumns": 128}], + "catalogProjections": [{"name": "populated_fields", "includePaths": ["*"], "kinds": ["scalar"], "naming": "PATH", "valueMode": "FIRST", "maxColumns": 256}], + "traversals": [{ + "name": "subject_Patient", + "toResourceType": "Patient", + "alias": "patient", + "fields": [{"name": "patient_id", "expr": {"select": "patient.id"}}], + "dynamicColumns": [{"name": "identifier_by_system", "source": {"select": "patient.identifier[]"}, "key": {"select": "item.system"}, "value": {"select": "item.value"}, "maxColumns": 64}, {"name": "extension_by_url", "source": {"select": "patient.extension[]"}, "key": {"select": "item.url"}, "value": {"call": "coalesce_string", "args": [{"select": "item.valueString"}, {"select": "item.valueCode"}, {"select": "item.valueInteger"}, {"select": "item.valueDecimal"}, {"select": "item.valueBoolean"}, {"select": "item.valueDate"}, {"select": "item.valueDateTime"}]}, "maxColumns": 128}], + "catalogProjections": [{"name": "populated_fields", "includePaths": ["*"], "kinds": ["scalar"], "naming": "PATH", "valueMode": "FIRST", "maxColumns": 256}] + }] + }, + { + "name": "subject_ResearchStudy", + "toResourceType": "ResearchStudy", + "alias": "researchstudy", + "dynamicColumns": [{"name": "identifier_by_system", "source": {"select": "researchstudy.identifier[]"}, "key": {"select": "item.system"}, "value": {"select": "item.value"}, "maxColumns": 64}, {"name": "extension_by_url", "source": {"select": "researchstudy.extension[]"}, "key": {"select": "item.url"}, "value": {"call": "coalesce_string", "args": [{"select": "item.valueString"}, {"select": "item.valueCode"}, {"select": "item.valueInteger"}, {"select": "item.valueDecimal"}, {"select": "item.valueBoolean"}, {"select": "item.valueDate"}, {"select": "item.valueDateTime"}]}, "maxColumns": 128}], + "catalogProjections": [{"name": "populated_fields", "includePaths": ["*"], "kinds": ["scalar"], "naming": "PATH", "valueMode": "FIRST", "maxColumns": 256}] + }, + { + "name": "focus_DocumentReference", + "toResourceType": "Observation", + "alias": "observation", + "pivots": [{"name": "observation_values", "discovery": {"path": "code", "maxColumns": 256}}, {"name": "observation_component_values", "discovery": {"path": "component[].code", "maxColumns": 256}}], + "catalogProjections": [{"name": "populated_fields", "includePaths": ["*"], "kinds": ["scalar"], "naming": "PATH", "valueMode": "FIRST", "maxColumns": 256}], + "fields": [ + {"name": "observation_code", "expr": {"call": "coalesce", "args": [{"select": "observation.code.text"}, {"call": "first", "args": [{"select": "observation.code.coding[].display"}]}]}} + ] + } + ] + }, + { + "name": "ResearchSubject", + "rootResourceType": "ResearchSubject", + "rowGrain": "study_enrollment", + "collisionPolicy": "overwrite", + "fields": [ + {"name": "id", "expr": {"select": "root.id"}}, + {"name": "identifier", "expr": {"call": "first", "args": [{"select": "root.identifier[].value"}]}}, + {"name": "status", "expr": {"select": "root.status"}} + ], + "catalogProjections": [{"name": "populated_fields", "includePaths": ["*"], "kinds": ["scalar"], "naming": "PATH", "valueMode": "FIRST", "maxColumns": 256}], + "dynamicColumns": [ + {"name": "identifier_by_system", "source": {"select": "root.identifier[]"}, "key": {"select": "item.system"}, "value": {"select": "item.value"}, "maxColumns": 64}, + {"name": "extension_by_url", "source": {"select": "root.extension[]"}, "key": {"select": "item.url"}, "value": {"call": "coalesce_string", "args": [{"select": "item.valueString"}, {"select": "item.valueCode"}, {"select": "item.valueInteger"}, {"select": "item.valueDecimal"}, {"select": "item.valueBoolean"}, {"select": "item.valueDate"}, {"select": "item.valueDateTime"}]}, "maxColumns": 128} + ], + "traversals": [ + {"name": "subject_Patient", "toResourceType": "Patient", "alias": "patient", "fields": [ + {"name": "patient_id", "expr": {"select": "patient.id"}}, + {"name": "patient_identifier", "expr": {"call": "first", "args": [{"select": "patient.identifier[].value"}]}} + ], "dynamicColumns": [{"name": "identifier_by_system", "source": {"select": "patient.identifier[]"}, "key": {"select": "item.system"}, "value": {"select": "item.value"}, "maxColumns": 64}, {"name": "extension_by_url", "source": {"select": "patient.extension[]"}, "key": {"select": "item.url"}, "value": {"call": "coalesce_string", "args": [{"select": "item.valueString"}, {"select": "item.valueCode"}, {"select": "item.valueInteger"}, {"select": "item.valueDecimal"}, {"select": "item.valueBoolean"}, {"select": "item.valueDate"}, {"select": "item.valueDateTime"}]}, "maxColumns": 128}], "catalogProjections": [{"name": "populated_fields", "includePaths": ["*"], "kinds": ["scalar"], "naming": "PATH", "valueMode": "FIRST", "maxColumns": 256}], "traversals": [ + {"name": "subject_Patient", "toResourceType": "Observation", "alias": "observation", "fields": [{"name": "observation_id", "expr": {"select": "observation.id"}}], "pivots": [{"name": "observation_values", "discovery": {"path": "code", "maxColumns": 256}}, {"name": "observation_component_values", "discovery": {"path": "component[].code", "maxColumns": 256}}], "catalogProjections": [{"name": "populated_fields", "includePaths": ["*"], "kinds": ["scalar"], "naming": "PATH", "valueMode": "FIRST", "maxColumns": 256}]}, + {"name": "subject_Patient", "toResourceType": "Condition", "alias": "condition", "fields": [{"name": "condition_id", "expr": {"select": "condition.id"}}], "dynamicColumns": [{"name": "identifier_by_system", "source": {"select": "condition.identifier[]"}, "key": {"select": "item.system"}, "value": {"select": "item.value"}, "maxColumns": 64}], "catalogProjections": [{"name": "populated_fields", "includePaths": ["*"], "kinds": ["scalar"], "naming": "PATH", "valueMode": "FIRST", "maxColumns": 256}]} + ]} + ] + }, + { + "name": "MedicationAdministration", + "rootResourceType": "MedicationAdministration", + "rowGrain": "resource", + "collisionPolicy": "overwrite", + "fields": [ + {"name": "id", "expr": {"select": "root.id"}}, + {"name": "status", "expr": {"select": "root.status"}}, + {"name": "total_dosage", "expr": {"select": "root.dosage.dose.value"}} + ], + "catalogProjections": [{"name": "populated_fields", "includePaths": ["*"], "kinds": ["scalar"], "naming": "PATH", "valueMode": "FIRST", "maxColumns": 256}], + "dynamicColumns": [{"name": "identifier_by_system", "source": {"select": "root.identifier[]"}, "key": {"select": "item.system"}, "value": {"select": "item.value"}, "maxColumns": 64}, {"name": "extension_by_url", "source": {"select": "root.extension[]"}, "key": {"select": "item.url"}, "value": {"call": "coalesce_string", "args": [{"select": "item.valueString"}, {"select": "item.valueCode"}, {"select": "item.valueInteger"}, {"select": "item.valueDecimal"}, {"select": "item.valueBoolean"}, {"select": "item.valueDate"}, {"select": "item.valueDateTime"}]}, "maxColumns": 128}], + "traversals": [ + {"name": "subject_Patient", "toResourceType": "Patient", "alias": "patient", "fields": [ + {"name": "patient_id", "expr": {"select": "patient.id"}}, + {"name": "patient_identifier", "expr": {"call": "first", "args": [{"select": "patient.identifier[].value"}]}} + ], "dynamicColumns": [{"name": "identifier_by_system", "source": {"select": "patient.identifier[]"}, "key": {"select": "item.system"}, "value": {"select": "item.value"}, "maxColumns": 64}, {"name": "extension_by_url", "source": {"select": "patient.extension[]"}, "key": {"select": "item.url"}, "value": {"call": "coalesce_string", "args": [{"select": "item.valueString"}, {"select": "item.valueCode"}, {"select": "item.valueInteger"}, {"select": "item.valueDecimal"}, {"select": "item.valueBoolean"}, {"select": "item.valueDate"}, {"select": "item.valueDateTime"}]}, "maxColumns": 128}], "catalogProjections": [{"name": "populated_fields", "includePaths": ["*"], "kinds": ["scalar"], "naming": "PATH", "valueMode": "FIRST", "maxColumns": 256}]} + ] + }, + { + "name": "Specimen", + "rootResourceType": "Specimen", + "rowGrain": "specimen", + "collisionPolicy": "overwrite", + "fields": [ + {"name": "id", "expr": {"select": "root.id"}}, + {"name": "identifier", "expr": {"call": "first", "args": [{"select": "root.identifier[].value"}]}}, + {"name": "parent", "expr": {"call": "join", "args": [{"select": "root.parent[].reference"}, {"literal": ","}]}} + ], + "catalogProjections": [{"name": "populated_fields", "includePaths": ["*"], "kinds": ["scalar"], "naming": "PATH", "valueMode": "FIRST", "maxColumns": 256}], + "dynamicColumns": [{"name": "identifier_by_system", "source": {"select": "root.identifier[]"}, "key": {"select": "item.system"}, "value": {"select": "item.value"}, "maxColumns": 64}, {"name": "extension_by_url", "source": {"select": "root.extension[]"}, "key": {"select": "item.url"}, "value": {"call": "coalesce_string", "args": [{"select": "item.valueString"}, {"select": "item.valueCode"}, {"select": "item.valueInteger"}, {"select": "item.valueDecimal"}, {"select": "item.valueBoolean"}, {"select": "item.valueDate"}, {"select": "item.valueDateTime"}]}, "maxColumns": 128}], + "traversals": [ + {"name": "subject_Patient", "toResourceType": "Patient", "alias": "patient", "fields": [ + {"name": "patient_id", "expr": {"select": "patient.id"}}, + {"name": "patient_identifier", "expr": {"call": "first", "args": [{"select": "patient.identifier[].value"}]}} + ], "dynamicColumns": [{"name": "identifier_by_system", "source": {"select": "patient.identifier[]"}, "key": {"select": "item.system"}, "value": {"select": "item.value"}, "maxColumns": 64}, {"name": "extension_by_url", "source": {"select": "patient.extension[]"}, "key": {"select": "item.url"}, "value": {"call": "coalesce_string", "args": [{"select": "item.valueString"}, {"select": "item.valueCode"}, {"select": "item.valueInteger"}, {"select": "item.valueDecimal"}, {"select": "item.valueBoolean"}, {"select": "item.valueDate"}, {"select": "item.valueDateTime"}]}, "maxColumns": 128}], "catalogProjections": [{"name": "populated_fields", "includePaths": ["*"], "kinds": ["scalar"], "naming": "PATH", "valueMode": "FIRST", "maxColumns": 256}]}, + {"name": "focus_Specimen", "toResourceType": "Observation", "alias": "observation", "fields": [ + {"name": "observation_code", "expr": {"call": "coalesce", "args": [{"select": "observation.code.text"}, {"call": "first", "args": [{"select": "observation.code.coding[].display"}]}]}} + ], "pivots": [{"name": "observation_values", "discovery": {"path": "code", "maxColumns": 256}}, {"name": "observation_component_values", "discovery": {"path": "component[].code", "maxColumns": 256}}], "catalogProjections": [{"name": "populated_fields", "includePaths": ["*"], "kinds": ["scalar"], "naming": "PATH", "valueMode": "FIRST", "maxColumns": 256}]} + ] + }, + { + "name": "GroupMember", + "rootResourceType": "Group", + "rowGrain": "expanded", + "collisionPolicy": "overwrite", + "expand": {"from": {"select": "root.member[]"}, "as": "member"}, + "identity": {"name": "id", "expr": {"call": "uuid5", "args": [{"literal": "aced-idp.org"}, {"select": "root.id"}, {"call": "concat", "args": [{"literal": ","}, {"call": "reference_id", "args": [{"select": "member.entity.reference"}]}]}]}}, + "fields": [ + {"name": "group_id", "expr": {"select": "root.id"}}, + {"name": "member_id", "expr": {"call": "reference_id", "args": [{"select": "member.entity.reference"}]}} + ], + "dynamicColumns": [{"name": "identifier_by_system", "source": {"select": "root.identifier[]"}, "key": {"select": "item.system"}, "value": {"select": "item.value"}, "maxColumns": 64}, {"name": "extension_by_url", "source": {"select": "root.extension[]"}, "key": {"select": "item.url"}, "value": {"call": "coalesce_string", "args": [{"select": "item.valueString"}, {"select": "item.valueCode"}, {"select": "item.valueInteger"}, {"select": "item.valueDecimal"}, {"select": "item.valueBoolean"}, {"select": "item.valueDate"}, {"select": "item.valueDateTime"}]}, "maxColumns": 128}] + } + ] +} diff --git a/internal/dataframe/recipe/document_types.go b/internal/dataframe/recipe/document_types.go new file mode 100644 index 0000000..894b32c --- /dev/null +++ b/internal/dataframe/recipe/document_types.go @@ -0,0 +1,377 @@ +// Package recipe defines the persistence-neutral recipe document used by the +// dataframe compiler. A recipe describes semantic row shaping only; it never +// carries database collection, table, AQL, or SQL details. +package recipe + +import ( + "encoding/json" + "strings" + + "github.com/calypr/loom/internal/authscope" +) + +// CurrentSchemaVersion is the first stable recipe document schema. +const CurrentSchemaVersion = 1 + +const ( + maxExpressionDepth = 64 + maxExpressionNodes = 4096 + maxLiteralArray = 256 +) + +// Bundle is an immutable, versioned collection of dataframe outputs. +type Bundle struct { + RecipeSchemaVersion int `json:"recipeSchemaVersion"` + Name string `json:"name"` + TranslationVersion string `json:"translationVersion"` + Fragments *FragmentLibrary `json:"fragments,omitempty"` + Outputs []Output `json:"outputs"` +} + +// Output describes one row-shaped result. Names are semantic names, not +// storage identifiers. +type Output struct { + Name string `json:"name"` + RootResourceType string `json:"rootResourceType"` + RowGrain string `json:"rowGrain"` + Fields []Field `json:"fields,omitempty"` + Filters []Filter `json:"filters,omitempty"` + Pivots []Pivot `json:"pivots,omitempty"` + Aggregates []Aggregate `json:"aggregates,omitempty"` + Slices []RepresentativeSlice `json:"slices,omitempty"` + Traversals []Traversal `json:"traversals,omitempty"` + Expand *Expansion `json:"expand,omitempty"` + Identity *Identity `json:"identity,omitempty"` + DynamicColumns []DynamicColumn `json:"dynamicColumns,omitempty"` + CatalogProjections []CatalogProjection `json:"catalogProjections,omitempty"` + CollisionPolicy string `json:"collisionPolicy,omitempty"` +} + +// Field projects one named semantic value into an output row. +type Field struct { + Name string `json:"name"` + FieldRef string `json:"fieldRef,omitempty"` + Expr Expression `json:"expr"` + Fallbacks []Expression `json:"fallbacks,omitempty"` + ValueMode ValueMode `json:"valueMode,omitempty"` +} + +// ValueMode controls how a checked selector contributes values to one row. +// The empty value is the backwards-compatible AUTO default and is omitted from +// persisted JSON so existing recipe bytes and digests remain stable. +type ValueMode string + +const ( + ValueModeAuto ValueMode = "AUTO" + ValueModeFirst ValueMode = "FIRST" + ValueModeAll ValueMode = "ALL" + ValueModeDistinct ValueMode = "DISTINCT" +) + +func (m ValueMode) Valid() bool { + return m == "" || m == ValueModeAuto || m == ValueModeFirst || m == ValueModeAll || m == ValueModeDistinct +} + +func (m ValueMode) Normalized() ValueMode { + if m == "" { + return ValueModeAuto + } + return m +} + +// Filter is a typed, storage-independent predicate. Select is the executable +// selector; FieldRef is optional frontend/catalog provenance. +type Filter struct { + Select string `json:"select"` + FieldRef string `json:"fieldRef,omitempty"` + Operator FilterOperator `json:"operator"` + Quantifier ArrayQuantifier `json:"quantifier,omitempty"` + Values []FilterValue `json:"values,omitempty"` +} + +type FilterOperator string + +const ( + FilterEquals FilterOperator = "EQUALS" + FilterNotEquals FilterOperator = "NOT_EQUALS" + FilterIn FilterOperator = "IN" + FilterExists FilterOperator = "EXISTS" + FilterMissing FilterOperator = "MISSING" + FilterContains FilterOperator = "CONTAINS_TEXT" + FilterGreaterThan FilterOperator = "GT" + FilterGreaterEq FilterOperator = "GTE" + FilterLessThan FilterOperator = "LT" + FilterLessEq FilterOperator = "LTE" +) + +func (op FilterOperator) Valid() bool { + switch op { + case FilterEquals, FilterNotEquals, FilterIn, FilterExists, FilterMissing, + FilterContains, FilterGreaterThan, FilterGreaterEq, FilterLessThan, FilterLessEq: + return true + default: + return false + } +} + +type ArrayQuantifier string + +const ( + QuantifierAny ArrayQuantifier = "ANY" + QuantifierAll ArrayQuantifier = "ALL" + QuantifierNone ArrayQuantifier = "NONE" +) + +func (q ArrayQuantifier) Valid() bool { + return q == QuantifierAny || q == QuantifierAll || q == QuantifierNone +} + +type FilterValueKind string + +const ( + FilterString FilterValueKind = "STRING" + FilterCode FilterValueKind = "CODE" + FilterBoolean FilterValueKind = "BOOLEAN" + FilterInteger FilterValueKind = "INTEGER" + FilterDecimal FilterValueKind = "DECIMAL" + FilterDate FilterValueKind = "DATE" + FilterDateTime FilterValueKind = "DATE_TIME" +) + +func (kind FilterValueKind) Valid() bool { + switch kind { + case FilterString, FilterCode, FilterBoolean, FilterInteger, FilterDecimal, FilterDate, FilterDateTime: + return true + default: + return false + } +} + +type CodeValue struct { + System string `json:"system,omitempty"` + Code string `json:"code"` + Display string `json:"display,omitempty"` +} + +// FilterValue is a strict tagged union. Pointer scalar members preserve false, +// zero, and empty string values during JSON decoding. +type FilterValue struct { + Kind FilterValueKind `json:"kind"` + String *string `json:"string,omitempty"` + Code *CodeValue `json:"code,omitempty"` + Boolean *bool `json:"boolean,omitempty"` + Integer *int64 `json:"integer,omitempty"` + Decimal *float64 `json:"decimal,omitempty"` + Date *string `json:"date,omitempty"` + DateTime *string `json:"dateTime,omitempty"` +} + +// Pivot describes a bounded, schema-validated column/value mapping. +type Pivot struct { + Name string `json:"name"` + FieldRef string `json:"fieldRef,omitempty"` + ColumnExpr Expression `json:"columnExpr"` + ValueExpr Expression `json:"valueExpr"` + ValueFallbacks []Expression `json:"valueFallbacks,omitempty"` + ItemSource Expression `json:"itemSource,omitempty"` + ItemResourceType string `json:"itemResourceType,omitempty"` + Columns []string `json:"columns"` + Discovery *PivotDiscovery `json:"discovery,omitempty"` +} + +// MarshalJSON permits catalog-backed pivots to omit selectors in their stored +// declaration. The schema resolver fills those selectors from the scoped +// catalog metadata before semantic compilation; ordinary static pivots still +// serialize both required expressions and therefore retain the strict AST +// contract. +func (p Pivot) MarshalJSON() ([]byte, error) { + type pivotJSON struct { + Name string `json:"name"` + FieldRef string `json:"fieldRef,omitempty"` + ColumnExpr *Expression `json:"columnExpr,omitempty"` + ValueExpr *Expression `json:"valueExpr,omitempty"` + ValueFallbacks []Expression `json:"valueFallbacks,omitempty"` + ItemSource *Expression `json:"itemSource,omitempty"` + ItemResourceType string `json:"itemResourceType,omitempty"` + Columns []string `json:"columns"` + Discovery *PivotDiscovery `json:"discovery,omitempty"` + } + wire := pivotJSON{Name: p.Name, FieldRef: p.FieldRef, ValueFallbacks: p.ValueFallbacks, ItemResourceType: p.ItemResourceType, Columns: p.Columns, Discovery: p.Discovery} + if p.Discovery == nil || !p.ColumnExpr.zero() { + value := p.ColumnExpr + wire.ColumnExpr = &value + } + if p.Discovery == nil || !p.ValueExpr.zero() { + value := p.ValueExpr + wire.ValueExpr = &value + } + if !p.ItemSource.zero() { + value := p.ItemSource + wire.ItemSource = &value + } + return json.Marshal(wire) +} + +func (e Expression) zero() bool { + return e.Select == "" && e.Call == "" && e.Literal == nil && len(e.Args) == 0 +} + +// PivotDiscovery requests a bounded column set from the scoped field catalog. +// The schema resolver replaces this declaration with concrete Columns before +// semantic typing and physical lowering. +type PivotDiscovery struct { + Family string `json:"family,omitempty"` + Path string `json:"path,omitempty"` + MaxColumns int `json:"maxColumns"` +} + +type AggregateOperation string + +const ( + AggregateCount AggregateOperation = "COUNT" + AggregateCountDistinct AggregateOperation = "COUNT_DISTINCT" + AggregateExists AggregateOperation = "EXISTS" + AggregateDistinctValues AggregateOperation = "DISTINCT_VALUES" + AggregateMin AggregateOperation = "MIN" + AggregateMax AggregateOperation = "MAX" +) + +func (op AggregateOperation) Valid() bool { + switch op { + case AggregateCount, AggregateCountDistinct, AggregateExists, AggregateDistinctValues, AggregateMin, AggregateMax: + return true + default: + return false + } +} + +type Aggregate struct { + Name string `json:"name"` + Operation AggregateOperation `json:"operation"` + FieldRef string `json:"fieldRef,omitempty"` + Expr *Expression `json:"expr,omitempty"` + Where *Filter `json:"where,omitempty"` + ValueMode ValueMode `json:"valueMode,omitempty"` +} + +type RepresentativeSlice struct { + Name string `json:"name"` + Where *Filter `json:"where,omitempty"` + Limit int `json:"limit"` + Fields []Field `json:"fields"` +} + +// TraversalMatchMode is deliberately closed. Empty remains the legacy +// optional default and is normalized only at semantic lowering time. +type TraversalMatchMode string + +const ( + MatchOptional TraversalMatchMode = "OPTIONAL" + MatchRequired TraversalMatchMode = "REQUIRED" +) + +func (m TraversalMatchMode) Valid() bool { + return m == "" || m == MatchOptional || m == MatchRequired || strings.EqualFold(string(m), string(MatchOptional)) || strings.EqualFold(string(m), string(MatchRequired)) +} + +func (m TraversalMatchMode) Normalized() TraversalMatchMode { + switch strings.ToUpper(string(m)) { + case string(MatchRequired): + return MatchRequired + default: + return MatchOptional + } +} + +// Traversal describes a relationship traversal without naming a physical +// graph collection or edge table. +type Traversal struct { + Name string `json:"name"` + ToResourceType string `json:"toResourceType"` + Alias string `json:"alias,omitempty"` + From *Expression `json:"from,omitempty"` + MatchMode TraversalMatchMode `json:"matchMode,omitempty"` + Fields []Field `json:"fields,omitempty"` + Filters []Filter `json:"filters,omitempty"` + Pivots []Pivot `json:"pivots,omitempty"` + Aggregates []Aggregate `json:"aggregates,omitempty"` + Slices []RepresentativeSlice `json:"slices,omitempty"` + Traversals []Traversal `json:"traversals,omitempty"` + DynamicColumns []DynamicColumn `json:"dynamicColumns,omitempty"` + CatalogProjections []CatalogProjection `json:"catalogProjections,omitempty"` +} + +// Expansion turns a repeated expression into one row per element. +type Expansion struct { + From Expression `json:"from"` + As string `json:"as"` +} + +// Identity derives a deterministic row identity. +type Identity struct { + Name string `json:"name"` + Expr Expression `json:"expr"` +} + +// DynamicColumn discovers a bounded set of key/value columns. The compiler +// freezes discovered keys before materialization. +type DynamicColumn struct { + Name string `json:"name"` + Source Expression `json:"source"` + Key *Expression `json:"key,omitempty"` + Value *Expression `json:"value,omitempty"` + Columns []string `json:"columns,omitempty"` + MaxColumns int `json:"maxColumns,omitempty"` +} + +// CatalogProjection describes a bounded family of populated FHIR paths. It +// is storage-neutral; the scoped schema resolver turns it into concrete typed +// recipe fields before semantic compilation. +type CatalogProjection struct { + Name string `json:"name"` + IncludePaths []string `json:"includePaths,omitempty"` + ExcludePaths []string `json:"excludePaths,omitempty"` + Kinds []string `json:"kinds,omitempty"` + Naming ColumnNaming `json:"naming,omitempty"` + ValueMode ValueMode `json:"valueMode,omitempty"` + MaxColumns int `json:"maxColumns"` +} + +type ColumnNaming string + +const ( + ColumnNamingPath ColumnNaming = "PATH" + ColumnNamingPathSuffix ColumnNaming = "PATH_SUFFIX" +) + +// RuntimeBindings are request-scoped and deliberately not part of a stored +// recipe digest. +type RuntimeBindings struct { + Project string + DatasetGeneration string + AuthResourcePaths []string + AuthScopeMode authscope.ReadScopeMode + PreviewLimit int + // IncludeAuthResourcePath is set only for ClickHouse publication streams. + // It keeps the reserved row-level authorization field out of ordinary + // dataframe previews while ensuring published rows carry their source path. + IncludeAuthResourcePath bool + // OutputNames limits execution to named outputs for preview requests. An + // empty list preserves the bundle's all-output behavior and is not part of + // recipe or schema identity. + OutputNames []string +} + +// Clone returns request-scoped bindings with independent authorization paths. +// Runtime bindings are never serialized into a recipe or included in its +// digest. +func (b RuntimeBindings) Clone() RuntimeBindings { + b.AuthResourcePaths = append([]string(nil), b.AuthResourcePaths...) + b.OutputNames = append([]string(nil), b.OutputNames...) + return b +} + +// ExpandFragments resolves the optional declarative fragment library and +// returns a standalone bundle suitable for semantic compilation. The stored +// library is omitted from the returned document because the expanded recipe +// is the immutable compiler input and its digest is what runtime plans carry. diff --git a/internal/dataframe/recipe/document_validation.go b/internal/dataframe/recipe/document_validation.go new file mode 100644 index 0000000..f3701b9 --- /dev/null +++ b/internal/dataframe/recipe/document_validation.go @@ -0,0 +1,247 @@ +// Package recipe defines the persistence-neutral recipe document used by the +// dataframe compiler. A recipe describes semantic row shaping only; it never +// carries database collection, table, AQL, or SQL details. +package recipe + +import ( + "bytes" + "encoding/json" + "fmt" + "strconv" + "strings" +) + +func (b Bundle) Validate() error { + if b.RecipeSchemaVersion != CurrentSchemaVersion { + return validationError("unsupported_schema_version", "$.recipeSchemaVersion", fmt.Sprintf("must be %d", CurrentSchemaVersion)) + } + if strings.TrimSpace(b.Name) == "" { + return validationError("required", "$.name", "name is required") + } + if strings.TrimSpace(b.TranslationVersion) == "" { + return validationError("required", "$.translationVersion", "translationVersion is required") + } + if len(b.Outputs) == 0 { + return validationError("required", "$.outputs", "at least one output is required") + } + if b.Fragments != nil { + if err := b.Fragments.Validate(); err != nil { + return validationError("invalid_fragments", "$.fragments", err.Error()) + } + } + seen := map[string]bool{} + for i, output := range b.Outputs { + path := fmt.Sprintf("$.outputs[%d]", i) + if err := validateRecipeName(output.Name, path+".name"); err != nil { + return err + } + if seen[output.Name] { + return validationError("duplicate_name", path+".name", "duplicate output name") + } + seen[output.Name] = true + if strings.TrimSpace(output.RootResourceType) == "" { + return validationError("required", path+".rootResourceType", "rootResourceType is required") + } + if strings.TrimSpace(output.RowGrain) == "" { + return validationError("required", path+".rowGrain", "rowGrain is required") + } + if output.CollisionPolicy != "" && output.CollisionPolicy != "error" && output.CollisionPolicy != "overwrite" && output.CollisionPolicy != "coalesce" { + return validationError("invalid_collision_policy", path+".collisionPolicy", "must be error, overwrite, or coalesce") + } + budget := 0 + if err := validateNodeShape(output.Fields, output.Filters, output.Pivots, output.Aggregates, output.Slices, path, &budget); err != nil { + return err + } + if err := validateTraversals(output.Traversals, path+".traversals", 0); err != nil { + return err + } + if output.Expand != nil { + if err := validateRecipeName(output.Expand.As, path+".expand.as"); err != nil { + return err + } + if err := validateExpressionBudget(output.Expand.From, path+".expand.from", &budget); err != nil { + return err + } + } + if output.Identity != nil { + if err := validateRecipeName(output.Identity.Name, path+".identity.name"); err != nil { + return err + } + if err := validateExpressionBudget(output.Identity.Expr, path+".identity.expr", &budget); err != nil { + return err + } + } + if err := validateDynamicColumns(output.DynamicColumns, path+".dynamicColumns", &budget); err != nil { + return err + } + for index, projection := range output.CatalogProjections { + if err := projection.validateAt(fmt.Sprintf("%s.catalogProjections[%d]", path, index)); err != nil { + return err + } + } + } + return nil +} + +func validateTraversals(items []Traversal, path string, depth int) error { + if depth > maxExpressionDepth { + return validationError("max_depth", path, "traversal depth exceeds limit") + } + seen := map[string]bool{} + for i, t := range items { + p := fmt.Sprintf("%s[%d]", path, i) + if err := validateRecipeName(t.Name, p+".name"); err != nil { + return err + } + // The relationship label is not an output namespace: two routes may + // legitimately use the same edge label while targeting different FHIR + // resources (for example Patient -> Condition and Patient -> Specimen). + // Alias is the lexical identity when supplied; otherwise the edge name + // remains the backwards-compatible uniqueness key. + traversalKey := t.Alias + if traversalKey == "" { + traversalKey = t.Name + } + if seen[traversalKey] { + return validationError("duplicate_name", p+".alias", "duplicate traversal alias") + } + seen[traversalKey] = true + if strings.TrimSpace(t.ToResourceType) == "" { + return validationError("required", p+".toResourceType", "toResourceType is required") + } + if !t.MatchMode.Valid() { + return validationError("invalid_match_mode", p+".matchMode", "must be OPTIONAL or REQUIRED") + } + budget := 0 + if t.From != nil { + if err := validateExpressionBudget(*t.From, p+".from", &budget); err != nil { + return err + } + } + if err := validateNodeShape(t.Fields, t.Filters, t.Pivots, t.Aggregates, t.Slices, p, &budget); err != nil { + return err + } + if err := validateDynamicColumns(t.DynamicColumns, p+".dynamicColumns", &budget); err != nil { + return err + } + for index, projection := range t.CatalogProjections { + if err := projection.validateAt(fmt.Sprintf("%s.catalogProjections[%d]", p, index)); err != nil { + return err + } + } + if err := validateTraversals(t.Traversals, p+".traversals", depth+1); err != nil { + return err + } + } + return nil +} + +func validateExpression(e Expression, path string) error { + nodes := 0 + var walk func(Expression, string, int) error + walk = func(node Expression, p string, depth int) error { + nodes++ + if nodes > maxExpressionNodes { + return validationError("max_nodes", p, "expression node count exceeds limit") + } + if depth > maxExpressionDepth { + return validationError("max_depth", p, "expression depth exceeds limit") + } + operators := 0 + if node.Select != "" { + operators++ + } + if node.Call != "" { + operators++ + } + if node.Literal != nil { + operators++ + } + if operators != 1 { + return validationError("invalid_expression", p, "expression must contain exactly one operator") + } + if node.Literal != nil { + if err := validateLiteral(node.Literal, p+".literal"); err != nil { + return err + } + return nil + } + if node.Select != "" { + if strings.TrimSpace(node.Select) == "" { + return validationError("required", p+".select", "select is required") + } + return nil + } + arity, ok := callArities[node.Call] + if strings.HasPrefix(node.Call, "fragment:") { + if strings.TrimSpace(strings.TrimPrefix(node.Call, "fragment:")) == "" { + return validationError("required", p+".call", "fragment name is required") + } + for i, arg := range node.Args { + if err := walk(arg, fmt.Sprintf("%s.args[%d]", p, i), depth+1); err != nil { + return err + } + } + return nil + } + if !ok { + return validationError("unsupported_operation", p+".call", "unsupported call "+strconv.Quote(node.Call)) + } + if len(node.Args) < arity.min || (arity.max >= 0 && len(node.Args) > arity.max) { + return validationError("invalid_arity", p+".args", fmt.Sprintf("call %q expects %d..%s arguments", node.Call, arity.min, maxString(arity.max))) + } + for i, arg := range node.Args { + if err := walk(arg, fmt.Sprintf("%s.args[%d]", p, i), depth+1); err != nil { + return err + } + } + return nil + } + return walk(e, path, 0) +} + +type arity struct{ min, max int } + +var callArities = map[string]arity{ + "coalesce": {1, -1}, "coalesce_string": {1, -1}, "first": {1, 1}, "all": {1, 1}, "distinct": {1, 1}, + "concat": {1, -1}, "join": {2, 2}, "cast": {2, 2}, "reference_id": {1, 1}, + "path_segment": {1, 1}, "basename": {1, 1}, "last_segment": {1, 1}, + "sanitize_name": {1, 1}, "sanitize_graphql_name": {1, 1}, "uuid3": {3, 3}, "uuid5": {3, 3}, + "if": {3, 3}, "case": {2, -1}, +} + +func maxString(max int) string { + if max < 0 { + return "many" + } + return strconv.Itoa(max) +} + +func validateLiteral(raw json.RawMessage, path string) error { + dec := json.NewDecoder(bytes.NewReader(raw)) + dec.UseNumber() + var value any + if err := dec.Decode(&value); err != nil { + return validationError("invalid_literal", path, err.Error()) + } + switch v := value.(type) { + case nil, string, bool, json.Number: + return nil + case []any: + if len(v) > maxLiteralArray { + return validationError("literal_limit", path, "literal array is too large") + } + for i, item := range v { + switch item.(type) { + case nil, string, bool, json.Number: + default: + return validationError("invalid_literal", fmt.Sprintf("%s[%d]", path, i), "literal arrays may contain only scalar values") + } + } + return nil + default: + return validationError("invalid_literal", path, "literal must be a scalar or bounded scalar array") + } +} + +// CanonicalJSON returns stable compact JSON for a validated document. diff --git a/internal/dataframe/recipe/dynamic_validation.go b/internal/dataframe/recipe/dynamic_validation.go new file mode 100644 index 0000000..b5f7905 --- /dev/null +++ b/internal/dataframe/recipe/dynamic_validation.go @@ -0,0 +1,47 @@ +package recipe + +import ( + "fmt" + "strings" +) + +func validateDynamicColumns(items []DynamicColumn, path string, budget *int) error { + seen := map[string]bool{} + for index, dynamic := range items { + dp := fmt.Sprintf("%s[%d]", path, index) + if err := validateRecipeName(dynamic.Name, dp+".name"); err != nil { + return err + } + if seen[dynamic.Name] { + return validationError("duplicate_name", dp+".name", "duplicate dynamic column name") + } + seen[dynamic.Name] = true + if dynamic.MaxColumns < 0 { + return validationError("invalid_limit", dp+".maxColumns", "must not be negative") + } + seenColumns := map[string]bool{} + for columnIndex, column := range dynamic.Columns { + if strings.TrimSpace(column) == "" { + return validationError("required", fmt.Sprintf("%s.columns[%d]", dp, columnIndex), "column name is required") + } + if seenColumns[column] { + return validationError("duplicate_name", fmt.Sprintf("%s.columns[%d]", dp, columnIndex), "duplicate dynamic column") + } + seenColumns[column] = true + } + if err := validateExpressionBudget(dynamic.Source, dp+".source", budget); err != nil { + return err + } + if dynamic.Key != nil { + if err := validateExpressionBudget(*dynamic.Key, dp+".key", budget); err != nil { + return err + } + } + if dynamic.Value != nil { + if err := validateExpressionBudget(*dynamic.Value, dp+".value", budget); err != nil { + return err + } + } + } + return nil +} diff --git a/internal/dataframe/recipe/engine/control.go b/internal/dataframe/recipe/engine/control.go new file mode 100644 index 0000000..f632837 --- /dev/null +++ b/internal/dataframe/recipe/engine/control.go @@ -0,0 +1,167 @@ +package engine + +import ( + "context" + "fmt" + + "github.com/calypr/loom/internal/dataframe/compiler" + "github.com/calypr/loom/internal/dataframe/recipe" + "github.com/calypr/loom/internal/dataframe/recipe/control" + "github.com/calypr/loom/internal/dataframe/runtime" + "github.com/calypr/loom/internal/dataframe/semantic" + arangostore "github.com/calypr/loom/internal/store/arango" +) + +// Control adapts the production recipe engine to the backend-neutral +// recipe control contract consumed by GraphQL and other transport adapters. +// It resolves a recipe exactly once per operation and exposes only sanitized +// physical diagnostics through ExplainPhysical; raw IR and AQL stay private. +type Control struct { + Engine *Engine + ExplainConnection *arangostore.ConnectionOptions +} + +func (c Control) Validate(ctx context.Context, name string, bindings recipe.RuntimeBindings) (control.Validation, error) { + if c.Engine == nil { + return control.Validation{}, fmt.Errorf("recipe engine is required") + } + entry, err := c.Engine.registry.LoadRecipe(ctx, name) + if err != nil { + return control.Validation{}, err + } + bundle := entry.Bundle + if c.Engine.resolveBundle != nil { + bundle, err = c.Engine.resolveBundle(ctx, bundle, bindings) + if err != nil { + return control.Validation{}, fmt.Errorf("resolve recipe schema: %w", err) + } + } + plan, err := semantic.BuildRecipePlan(bundle, bindings) + if err != nil { + return control.Validation{}, err + } + if digest, digestErr := entry.Bundle.Digest(); digestErr == nil { + plan.RecipeDigest = digest + } + return control.Validation{Entry: entry, Plan: plan}, nil +} + +func (c Control) Explain(ctx context.Context, name string, bindings recipe.RuntimeBindings) (semantic.RecipePlanExplanation, error) { + validated, err := c.Validate(ctx, name, bindings) + if err != nil { + return semantic.RecipePlanExplanation{}, err + } + return validated.Plan.Explain(), nil +} + +// ExplainPhysical compiles the same resolved canonical outputs consumed by +// Preview and Materialize. The compiler-only path is always available; live +// Arango assessment is opt-in and requires ExplainConnection. No AQL or bind +// values cross the recipe control boundary. +func (c Control) ExplainPhysical(ctx context.Context, name string, bindings recipe.RuntimeBindings, live bool) (control.PhysicalExplanation, error) { + if c.Engine == nil { + return control.PhysicalExplanation{}, fmt.Errorf("recipe engine is required") + } + resolved, err := c.Engine.Resolve(ctx, name, bindings) + if err != nil { + return control.PhysicalExplanation{}, err + } + limit := bindings.PreviewLimit + if limit <= 0 { + limit = 25 + } + result := control.PhysicalExplanation{Outputs: make([]control.PhysicalOutputExplanation, 0, len(resolved.Compiled.Outputs))} + for _, output := range resolved.Compiled.Outputs { + compiled, err := compiler.CompileRecipeOutputWithPolicy(output, resolved.Semantic.SemanticPlan.Bindings, limit, compiler.DefaultPhysicalOptimizationPolicy()) + if err != nil { + return control.PhysicalExplanation{}, fmt.Errorf("output %q: %w", output.Name, err) + } + item := control.PhysicalOutputExplanation{ + Name: output.Name, PlanFingerprint: runtime.CompiledQueryFingerprint(compiled), + Columns: append([]string(nil), compiled.PublicColumns...), Diagnostics: compiled.PlanDiagnostics, + } + if live { + if c.ExplainConnection == nil { + return control.PhysicalExplanation{}, fmt.Errorf("live physical recipe Explain requires an Arango connection") + } + assessment, err := runtime.ExplainCompiledQueryAssessment(ctx, *c.ExplainConnection, compiled) + if err != nil { + return control.PhysicalExplanation{}, fmt.Errorf("output %q live Explain: %w", output.Name, err) + } + converted := control.AssessmentFromArango(assessment) + item.Live = &converted + } + result.Outputs = append(result.Outputs, item) + } + return result, nil +} + +func (c Control) Resolve(ctx context.Context, name string, bindings recipe.RuntimeBindings) (semantic.ResolvedRecipePlan, error) { + if c.Engine == nil { + return semantic.ResolvedRecipePlan{}, fmt.Errorf("recipe engine is required") + } + resolved, err := c.Engine.Resolve(ctx, name, bindings) + if err != nil { + return semantic.ResolvedRecipePlan{}, err + } + return resolved.Semantic, nil +} + +func (c Control) Preview(ctx context.Context, name string, bindings recipe.RuntimeBindings, execute control.ExecuteFunc) (control.Preview, error) { + if c.Engine == nil { + return control.Preview{}, fmt.Errorf("recipe engine is required") + } + full, err := c.Engine.Resolve(ctx, name, bindings) + if err != nil { + return control.Preview{}, err + } + // The callback remains in the transport-neutral interface for source + // compatibility, but the production engine never delegates preview work + // to it. Doing so would reintroduce a caller-selected renderer and bypass + // the canonical physical optimizer. All rows come from Engine.Preview. + _ = execute + resolved := full.Semantic + rows, err := c.Engine.Preview(ctx, full, bindings.PreviewLimit) + if err != nil { + return control.Preview{}, err + } + return control.Preview{Plan: resolved, Rows: rows, Outputs: outputRows(full, rows)}, nil +} + +// Run executes the complete resolved recipe without a preview limit. It is an +// explicit, bounded-by-the-caller's-memory transport operation; production +// bulk workflows should use Materialize instead. +func (c Control) Run(ctx context.Context, name string, bindings recipe.RuntimeBindings) (control.Preview, error) { + if c.Engine == nil { + return control.Preview{}, fmt.Errorf("recipe engine is required") + } + full, err := c.Engine.Resolve(ctx, name, bindings) + if err != nil { + return control.Preview{}, err + } + rows, err := c.Engine.Run(ctx, full) + if err != nil { + return control.Preview{}, err + } + return control.Preview{Plan: full.Semantic, Rows: rows, Outputs: outputRows(full, rows)}, nil +} + +func outputRows(resolved Resolved, rows map[string][]map[string]any) []control.OutputRows { + outputs := make([]control.OutputRows, 0, len(resolved.Compiled.Outputs)) + for _, output := range resolved.Compiled.Outputs { + outputRows := rows[output.Name] + outputs = append(outputs, control.OutputRows{ + Name: output.Name, Columns: compiler.PublicOutputColumns(output.OutputSchema), + Rows: outputRows, + }) + } + return outputs +} + +var _ interface { + Validate(context.Context, string, recipe.RuntimeBindings) (control.Validation, error) + Explain(context.Context, string, recipe.RuntimeBindings) (semantic.RecipePlanExplanation, error) + ExplainPhysical(context.Context, string, recipe.RuntimeBindings, bool) (control.PhysicalExplanation, error) + Resolve(context.Context, string, recipe.RuntimeBindings) (semantic.ResolvedRecipePlan, error) + Preview(context.Context, string, recipe.RuntimeBindings, control.ExecuteFunc) (control.Preview, error) +} = Control{} diff --git a/internal/dataframe/recipe/engine/engine.go b/internal/dataframe/recipe/engine/engine.go new file mode 100644 index 0000000..cc10099 --- /dev/null +++ b/internal/dataframe/recipe/engine/engine.go @@ -0,0 +1,352 @@ +// Package engine is Loom's production recipe execution seam. It owns +// recipe resolution, scoped discovery, canonical compiler orchestration, and +// streaming row execution; transport adapters do not interpret recipes or +// construct AQL. +package engine + +import ( + "context" + "crypto/sha256" + "encoding/hex" + "encoding/json" + "fmt" + "sort" + "strings" + + "github.com/calypr/loom/internal/dataframe/compiler" + "github.com/calypr/loom/internal/dataframe/recipe" + "github.com/calypr/loom/internal/dataframe/recipe/exec" + "github.com/calypr/loom/internal/dataframe/semantic" +) + +type Registry interface { + LoadRecipe(context.Context, string) (exec.Entry, error) +} + +// LocalRegistry adapts the process-local registry for tests and local runs. +// Production deployments should provide the durable Arango adapter. +type LocalRegistry struct{ Registry *exec.Registry } + +type QueryRows func(context.Context, string, int, map[string]any, func(map[string]any) error) error + +type Config struct { + Registry Registry + ResolveBundle func(context.Context, recipe.Bundle, recipe.RuntimeBindings) (recipe.Bundle, error) + QueryRows QueryRows + ScopeDigest func(recipe.RuntimeBindings) string + BatchSize int +} + +type Engine struct { + registry Registry + resolveBundle func(context.Context, recipe.Bundle, recipe.RuntimeBindings) (recipe.Bundle, error) + queryRows QueryRows + scopeDigest func(recipe.RuntimeBindings) string + batchSize int +} + +// Resolved contains semantic discovery provenance and canonical physical plans +// per output. It deliberately contains no recipe-specific physical model. +type Resolved struct { + Semantic semantic.ResolvedRecipePlan + Compiled compiler.CompiledRecipe + StoredRecipeDigest string + ResolvedSchemaDigest string +} + +type OutputStream struct { + Name string + Columns []string + RowIdentity *compiler.RowIdentity + Query string + BindVars map[string]any + DynamicChecks map[string]map[string]DynamicColumnCheck + stream QueryRows + batchSize int +} + +type DynamicColumnCheck struct { + ColumnName string + ValueType string +} + +type StreamResult struct { + Output string + Columns []string + RowCount int +} + +func New(cfg Config) (*Engine, error) { + if cfg.Registry == nil { + return nil, fmt.Errorf("recipe registry is required") + } + if cfg.QueryRows == nil { + return nil, fmt.Errorf("recipe query executor is required") + } + batch := cfg.BatchSize + if batch <= 0 { + batch = 1000 + } + return &Engine{registry: cfg.Registry, resolveBundle: cfg.ResolveBundle, queryRows: cfg.QueryRows, scopeDigest: cfg.ScopeDigest, batchSize: batch}, nil +} + +func (e *Engine) Resolve(ctx context.Context, name string, bindings recipe.RuntimeBindings) (Resolved, error) { + if strings.TrimSpace(bindings.Project) == "" { + return Resolved{}, fmt.Errorf("recipe project is required") + } + entry, err := e.registry.LoadRecipe(ctx, name) + if err != nil { + return Resolved{}, err + } + bundle := entry.Bundle + storedRecipeDigest, err := bundle.Digest() + if err != nil { + return Resolved{}, fmt.Errorf("digest stored recipe: %w", err) + } + if e.resolveBundle != nil { + bundle, err = e.resolveBundle(ctx, bundle, bindings) + if err != nil { + return Resolved{}, fmt.Errorf("resolve recipe schema: %w", err) + } + } + semanticPlan, err := semantic.BuildRecipePlan(bundle, bindings) + if err != nil { + return Resolved{}, err + } + // The public recipe identity is the registered document digest. The + // resolved schema digest below captures catalog-derived fields and scope so + // materializations cannot collide when one recipe resolves differently. + semanticPlan.RecipeDigest = storedRecipeDigest + scope := "" + if e.scopeDigest != nil { + scope = e.scopeDigest(bindings) + } + resolved, err := semantic.ResolveRecipePlan(semanticPlan, scope, bindings.DatasetGeneration) + if err != nil { + return Resolved{}, err + } + compiled, err := compiler.CompileResolvedRecipePlan(resolved, compiler.DefaultPhysicalOptimizationPolicy()) + if err != nil { + return Resolved{}, err + } + // Lowering and optimization are request-scoped work. Cache the optimized + // physical plan on the resolved result so preview/materialization streams + // only clone, window, and render it; they must not rebuild the recipe. + policy := compiler.DefaultPhysicalOptimizationPolicy() + for index := range compiled.Outputs { + optimized, optimizeErr := compiler.OptimizePhysicalPlanWithPolicy(compiled.Outputs[index].Plan, policy) + if optimizeErr != nil { + return Resolved{}, fmt.Errorf("optimize output %q: %w", compiled.Outputs[index].Name, optimizeErr) + } + compiled.Outputs[index].OptimizedPlan = &optimized + } + resolvedSchemaDigest, err := resolvedBundleSchemaDigest(storedRecipeDigest, bundle, bindings) + if err != nil { + return Resolved{}, err + } + resolved.ResolvedSchemaDigest = resolvedSchemaDigest + return Resolved{Semantic: resolved, Compiled: compiled, StoredRecipeDigest: storedRecipeDigest, ResolvedSchemaDigest: resolvedSchemaDigest}, nil +} + +func resolvedBundleSchemaDigest(storedDigest string, bundle recipe.Bundle, bindings recipe.RuntimeBindings) (string, error) { + resolvedDigest, err := bundle.Digest() + if err != nil { + return "", err + } + paths := append([]string(nil), bindings.AuthResourcePaths...) + sort.Strings(paths) + payload, err := json.Marshal(struct { + Stored, Resolved, Project, Generation, ScopeMode string + Paths []string + }{storedDigest, resolvedDigest, bindings.Project, bindings.DatasetGeneration, string(bindings.AuthScopeMode), paths}) + if err != nil { + return "", err + } + sum := sha256.Sum256(payload) + return hex.EncodeToString(sum[:]), nil +} + +// Materialize resolves a recipe once and hands the complete resolved plan to +// the publisher. Resolve and compile therefore share one discovery snapshot. +func (e *Engine) Materialize(ctx context.Context, name string, bindings recipe.RuntimeBindings, publish func(context.Context, Resolved) error) (Resolved, error) { + resolved, err := e.Resolve(ctx, name, bindings) + if err != nil { + return Resolved{}, err + } + if publish != nil { + if err := publish(ctx, resolved); err != nil { + return Resolved{}, err + } + } + return resolved, nil +} + +func (e *Engine) Streams(ctx context.Context, resolved Resolved) ([]OutputStream, error) { + return e.streamsWithLimit(ctx, resolved, 0) +} + +// Run consumes every row from every selected output. Unlike Preview, this +// deliberately compiles without an execution LIMIT and is intended for +// callers that explicitly requested the complete dataframe in memory. +func (e *Engine) Run(ctx context.Context, resolved Resolved) (map[string][]map[string]any, error) { + streams, err := e.Streams(ctx, resolved) + if err != nil { + return nil, err + } + result := make(map[string][]map[string]any, len(streams)) + for _, stream := range streams { + rows := make([]map[string]any, 0) + _, err := stream.Stream(ctx, func(row map[string]any) error { + rows = append(rows, row) + return nil + }) + if err != nil { + return nil, fmt.Errorf("output %q: %w", stream.Name, err) + } + result[stream.Name] = rows + } + return result, nil +} + +func (e *Engine) streamsWithLimit(_ context.Context, resolved Resolved, limit int) ([]OutputStream, error) { + streams := make([]OutputStream, 0, len(resolved.Compiled.Outputs)) + selected := selectedOutputNames(resolved.Semantic.SemanticPlan.Bindings.OutputNames, resolved.Compiled.Outputs) + for _, name := range resolved.Semantic.SemanticPlan.Bindings.OutputNames { + if !selected[name] { + return nil, fmt.Errorf("requested recipe output %q was not found", name) + } + } + for _, output := range resolved.Compiled.Outputs { + if selected != nil && !selected[output.Name] { + continue + } + query, err := compiler.CompileRecipeOutputWithPolicy(output, resolved.Semantic.SemanticPlan.Bindings, limit, compiler.DefaultPhysicalOptimizationPolicy()) + if err != nil { + return nil, fmt.Errorf("output %q: %w", output.Name, err) + } + streams = append(streams, OutputStream{ + Name: output.Name, Columns: append([]string(nil), query.PublicColumns...), RowIdentity: cloneRowIdentity(query.RowIdentity), Query: query.Query, + BindVars: query.BindVars, DynamicChecks: dynamicChecks(output.DynamicColumns), stream: e.queryRows, batchSize: e.batchSize, + }) + } + return streams, nil +} + +func selectedOutputNames(names []string, outputs []compiler.CompiledRecipeOutput) map[string]bool { + if len(names) == 0 { + return nil + } + known := make(map[string]bool, len(outputs)) + for _, output := range outputs { + known[output.Name] = false + } + for _, name := range names { + if _, ok := known[name]; ok { + known[name] = true + } + } + return known +} + +func (e *Engine) Preview(ctx context.Context, resolved Resolved, limit int) (map[string][]map[string]any, error) { + if limit <= 0 { + limit = 25 + } + streams, err := e.streamsWithLimit(ctx, resolved, limit) + if err != nil { + return nil, err + } + result := make(map[string][]map[string]any, len(streams)) + for _, stream := range streams { + rows := make([]map[string]any, 0, limit) + count := 0 + err := stream.stream(ctx, stream.Query, stream.batchSize, stream.BindVars, func(row map[string]any) error { + if count >= limit { + return errPreviewLimit + } + resolvedRow, err := materializePostQueryRowWithChecks(row, stream.DynamicChecks) + if err != nil { + return err + } + rows = append(rows, resolvedRow) + count++ + return nil + }) + if err != nil && err != errPreviewLimit { + return nil, fmt.Errorf("output %q: %w", stream.Name, err) + } + result[stream.Name] = rows + } + return result, nil +} + +var errPreviewLimit = fmt.Errorf("preview limit reached") + +func (s OutputStream) Stream(ctx context.Context, visit func(map[string]any) error) (StreamResult, error) { + if visit == nil { + return StreamResult{}, fmt.Errorf("row visitor is required") + } + count := 0 + err := s.stream(ctx, s.Query, s.batchSize, s.BindVars, func(row map[string]any) error { + resolved, err := materializePostQueryRowWithChecks(row, s.DynamicChecks) + if err != nil { + return err + } + if err := ensureStableRowIdentity(resolved, s.RowIdentity, s.BindVars); err != nil { + return err + } + count++ + return visit(resolved) + }) + return StreamResult{Output: s.Name, Columns: append([]string(nil), s.Columns...), RowCount: count}, err +} + +func ensureStableRowIdentity(row map[string]any, identity *compiler.RowIdentity, bindVars map[string]any) error { + if row == nil { + return fmt.Errorf("row is nil") + } + if value, ok := row["__loom_row_id"]; ok && value != nil && fmt.Sprint(value) != "" { + return nil + } + if identity == nil || len(identity.Fields) == 0 { + return fmt.Errorf("compiled output is missing a stable row identity") + } + parts := make([]any, 0, len(identity.Fields)) + for _, field := range identity.Fields { + value, ok := row[field] + if !ok && bindVars != nil { + value, ok = bindVars[field] + } + if !ok || value == nil { + return fmt.Errorf("stable row identity field %q is missing", field) + } + parts = append(parts, value) + } + encoded, err := json.Marshal(parts) + if err != nil { + return fmt.Errorf("encode stable row identity: %w", err) + } + digest := sha256.Sum256(encoded) + row["__loom_row_id"] = hex.EncodeToString(digest[:]) + return nil +} + +func cloneRowIdentity(identity *compiler.RowIdentity) *compiler.RowIdentity { + if identity == nil { + return nil + } + copy := *identity + copy.Fields = append([]string(nil), identity.Fields...) + return © +} + +func dynamicChecks(metadata []compiler.DynamicColumnMetadata) map[string]map[string]DynamicColumnCheck { + checks := make(map[string]map[string]DynamicColumnCheck) + for _, column := range metadata { + if checks[column.DynamicName] == nil { + checks[column.DynamicName] = map[string]DynamicColumnCheck{} + } + checks[column.DynamicName][column.SourceKey] = DynamicColumnCheck{ColumnName: column.Name, ValueType: column.ValueType} + } + return checks +} diff --git a/internal/dataframe/recipe/engine/identity_test.go b/internal/dataframe/recipe/engine/identity_test.go new file mode 100644 index 0000000..e63d4e2 --- /dev/null +++ b/internal/dataframe/recipe/engine/identity_test.go @@ -0,0 +1,40 @@ +package engine + +import ( + "testing" + + "github.com/calypr/loom/internal/dataframe/compiler" +) + +func TestEnsureStableRowIdentityIsDeterministicAndScoped(t *testing.T) { + identity := &compiler.RowIdentity{Grain: compiler.RowGrainPatient, Fields: []string{"project", "_key"}} + first := map[string]any{"_key": "p1"} + second := map[string]any{"_key": "p1"} + binds := map[string]any{"project": "P1"} + if err := ensureStableRowIdentity(first, identity, binds); err != nil { + t.Fatal(err) + } + if err := ensureStableRowIdentity(second, identity, binds); err != nil { + t.Fatal(err) + } + if first["__loom_row_id"] != second["__loom_row_id"] { + t.Fatalf("identity is not deterministic: %v != %v", first["__loom_row_id"], second["__loom_row_id"]) + } + third := map[string]any{"_key": "p1"} + if err := ensureStableRowIdentity(third, identity, map[string]any{"project": "P2"}); err != nil { + t.Fatal(err) + } + if first["__loom_row_id"] == third["__loom_row_id"] { + t.Fatal("different project scopes produced the same identity") + } +} + +func TestEnsureStableRowIdentityPreservesCompiledIdentity(t *testing.T) { + row := map[string]any{"__loom_row_id": "recipe-id"} + if err := ensureStableRowIdentity(row, nil, nil); err != nil { + t.Fatal(err) + } + if row["__loom_row_id"] != "recipe-id" { + t.Fatalf("compiled identity changed: %#v", row) + } +} diff --git a/internal/dataframe/recipe/engine/postquery.go b/internal/dataframe/recipe/engine/postquery.go new file mode 100644 index 0000000..5d03db1 --- /dev/null +++ b/internal/dataframe/recipe/engine/postquery.go @@ -0,0 +1,325 @@ +package engine + +import ( + "fmt" + "math" + "reflect" + "strconv" + "strings" + + "github.com/calypr/loom/internal/dataframe/uuidcompat" +) + +const ( + exactUUIDOperationKey = "__loom_exact_uuid_operation" + exactUUIDArgsKey = "__loom_exact_uuid_args" + postQueryCallKey = "__loom_postquery_call" + postQueryArgsKey = "__loom_postquery_args" + postQueryTargetKey = "__loom_postquery_target" +) + +func materializePostQueryRowWithChecks(row map[string]any, checks map[string]map[string]DynamicColumnCheck) (map[string]any, error) { + value, err := materializePostQueryValue(row) + if err != nil { + return nil, err + } + result, ok := value.(map[string]any) + if !ok { + return nil, fmt.Errorf("post-query row is not an object") + } + if err := validateDynamicDrift(result, checks); err != nil { + return nil, err + } + delete(result, "__loom_dynamic_runtime_keys") + return result, nil +} + +func validateDynamicDrift(row map[string]any, checks map[string]map[string]DynamicColumnCheck) error { + if len(checks) == 0 { + return nil + } + observed, ok := row["__loom_dynamic_runtime_keys"].(map[string]any) + if !ok { + return fmt.Errorf("dynamic runtime key metadata is missing") + } + for dynamicName, values := range observed { + allowed := checks[dynamicName] + items, err := dynamicRuntimeKeys(values) + if err != nil { + return fmt.Errorf("dynamic map %q runtime key metadata is malformed", dynamicName) + } + for _, value := range items { + key := fmt.Sprint(value) + column, ok := allowed[key] + if !ok { + return fmt.Errorf("dynamic map %q emitted unexpected key %q", dynamicName, key) + } + if actual, exists := row[column.ColumnName]; exists && !dynamicValueMatches(actual, column.ValueType) { + return fmt.Errorf("dynamic map %q column %q has incompatible value type %q", dynamicName, column.ColumnName, column.ValueType) + } + } + } + return nil +} + +func dynamicRuntimeKeys(value any) ([]any, error) { + switch typed := value.(type) { + case []any: + return typed, nil + case []string: + result := make([]any, len(typed)) + for index, item := range typed { + result[index] = item + } + return result, nil + default: + return nil, fmt.Errorf("expected an array") + } +} + +func dynamicValueMatches(value any, logicalType string) bool { + if value == nil || logicalType == "" || logicalType == "unknown" { + return true + } + switch logicalType { + case "string", "code", "uuid", "date", "datetime": + _, ok := value.(string) + return ok + case "boolean": + _, ok := value.(bool) + return ok + case "integer": + switch typed := value.(type) { + case int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64: + return true + case float64: + return math.Trunc(typed) == typed + case float32: + return math.Trunc(float64(typed)) == float64(typed) + case string: + _, err := strconv.ParseInt(typed, 10, 64) + return err == nil + default: + return false + } + case "decimal": + switch typed := value.(type) { + case float32, float64, int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64: + return true + case string: + _, err := strconv.ParseFloat(typed, 64) + return err == nil + default: + return false + } + default: + return true + } +} + +func materializePostQueryValue(value any) (any, error) { + switch typed := value.(type) { + case map[string]any: + if operation, ok := typed[exactUUIDOperationKey].(string); ok { + args, ok := typed[exactUUIDArgsKey].([]any) + if !ok { + return nil, fmt.Errorf("exact UUID marker arguments are malformed") + } + resolvedArgs := make([]any, len(args)) + for index, arg := range args { + resolved, err := materializePostQueryValue(arg) + if err != nil { + return nil, err + } + resolvedArgs[index] = resolved + } + return uuidcompat.Compute(operation, resolvedArgs) + } + if operation, ok := typed[postQueryCallKey].(string); ok { + args, ok := typed[postQueryArgsKey].([]any) + if !ok { + return nil, fmt.Errorf("post-query call arguments are malformed") + } + resolvedArgs := make([]any, len(args)) + for index, arg := range args { + resolved, err := materializePostQueryValue(arg) + if err != nil { + return nil, err + } + resolvedArgs[index] = resolved + } + target, _ := typed[postQueryTargetKey].(string) + return evaluatePostQueryCall(operation, target, resolvedArgs) + } + result := make(map[string]any, len(typed)) + for key, item := range typed { + resolved, err := materializePostQueryValue(item) + if err != nil { + return nil, err + } + result[key] = resolved + } + return result, nil + case []any: + result := make([]any, len(typed)) + for index, item := range typed { + resolved, err := materializePostQueryValue(item) + if err != nil { + return nil, err + } + result[index] = resolved + } + return result, nil + default: + return value, nil + } +} + +func evaluatePostQueryCall(operation, target string, args []any) (any, error) { + if operation == "uuid3" || operation == "uuid5" { + return uuidcompat.Compute(operation, args) + } + isNull := func(value any) bool { return value == nil } + switch operation { + case "coalesce", "fallback": + for _, value := range args { + if !isNull(value) { + return value, nil + } + } + return nil, nil + case "first": + if len(args) != 1 { + return nil, fmt.Errorf("first requires one argument") + } + if values, ok := args[0].([]any); ok { + if len(values) == 0 { + return nil, nil + } + return values[0], nil + } + return args[0], nil + case "all": + return args, nil + case "concat": + var result string + for _, value := range args { + if value != nil { + result += fmt.Sprint(value) + } + } + return result, nil + case "join": + if len(args) != 2 { + return nil, fmt.Errorf("join requires two arguments") + } + values, ok := args[0].([]any) + if !ok { + return nil, fmt.Errorf("join requires an array") + } + separator := fmt.Sprint(args[1]) + parts := make([]string, len(values)) + for index, value := range values { + parts[index] = fmt.Sprint(value) + } + return strings.Join(parts, separator), nil + case "cast": + if len(args) != 1 { + return nil, fmt.Errorf("cast requires one argument") + } + return castPostQueryValue(args[0], target) + case "if": + if len(args) != 3 { + return nil, fmt.Errorf("if requires three arguments") + } + if truthyPostQuery(args[0]) { + return args[1], nil + } + return args[2], nil + case "case": + for index := 0; index+1 < len(args); index += 2 { + if truthyPostQuery(args[index]) { + return args[index+1], nil + } + } + if len(args)%2 == 1 { + return args[len(args)-1], nil + } + return nil, nil + case "not": + if len(args) != 1 { + return nil, fmt.Errorf("not requires one argument") + } + return !truthyPostQuery(args[0]), nil + case "and", "or": + if len(args) < 2 { + return nil, fmt.Errorf("%s requires two arguments", operation) + } + result := operation == "and" + for _, value := range args { + if operation == "and" { + result = result && truthyPostQuery(value) + } else { + result = result || truthyPostQuery(value) + } + } + return result, nil + case "eq", "neq", "gt", "gte", "lt", "lte": + if len(args) != 2 { + return nil, fmt.Errorf("%s requires two arguments", operation) + } + left, right := fmt.Sprint(args[0]), fmt.Sprint(args[1]) + switch operation { + case "eq": + return reflect.DeepEqual(args[0], args[1]) || left == right, nil + case "neq": + return !(reflect.DeepEqual(args[0], args[1]) || left == right), nil + case "gt": + return left > right, nil + case "gte": + return left >= right, nil + case "lt": + return left < right, nil + default: + return left <= right, nil + } + case "contains": + if len(args) != 2 { + return nil, fmt.Errorf("contains requires two arguments") + } + return strings.Contains(fmt.Sprint(args[0]), fmt.Sprint(args[1])), nil + default: + return nil, fmt.Errorf("unsupported nested post-query call %q", operation) + } +} + +func truthyPostQuery(value any) bool { + switch typed := value.(type) { + case nil: + return false + case bool: + return typed + case string: + return typed != "" + default: + return true + } +} + +func castPostQueryValue(value any, target string) (any, error) { + if value == nil { + return nil, nil + } + switch target { + case "string", "code", "uuid", "date", "date_time": + return fmt.Sprint(value), nil + case "integer": + return fmt.Sprint(value), nil + case "decimal": + return fmt.Sprint(value), nil + case "boolean": + return truthyPostQuery(value), nil + default: + return nil, fmt.Errorf("unsupported nested cast target %q", target) + } +} diff --git a/internal/dataframe/recipe/engine/postquery_test.go b/internal/dataframe/recipe/engine/postquery_test.go new file mode 100644 index 0000000..e72b40e --- /dev/null +++ b/internal/dataframe/recipe/engine/postquery_test.go @@ -0,0 +1,45 @@ +package engine + +import "testing" + +func TestMaterializePostQueryRejectsDynamicKeyDrift(t *testing.T) { + checks := map[string]map[string]DynamicColumnCheck{ + "code": {"a": {ColumnName: "code_a", ValueType: "string"}}, + } + row := map[string]any{ + "code_a": "value", + "__loom_dynamic_runtime_keys": map[string]any{"code": []any{"unexpected"}}, + } + if _, err := materializePostQueryRowWithChecks(row, checks); err == nil { + t.Fatal("unexpected dynamic key was accepted") + } +} + +func TestMaterializePostQueryRejectsDynamicTypeDrift(t *testing.T) { + checks := map[string]map[string]DynamicColumnCheck{ + "code": {"a": {ColumnName: "code_a", ValueType: "string"}}, + } + row := map[string]any{ + "code_a": 42, + "__loom_dynamic_runtime_keys": map[string]any{"code": []any{"a"}}, + } + if _, err := materializePostQueryRowWithChecks(row, checks); err == nil { + t.Fatal("dynamic type drift was accepted") + } +} + +func TestMaterializePostQueryStripsDynamicMetadata(t *testing.T) { + checks := map[string]map[string]DynamicColumnCheck{ + "code": {"a": {ColumnName: "code_a", ValueType: "string"}}, + } + row, err := materializePostQueryRowWithChecks(map[string]any{ + "code_a": "value", + "__loom_dynamic_runtime_keys": map[string]any{"code": []any{"a"}}, + }, checks) + if err != nil { + t.Fatal(err) + } + if _, ok := row["__loom_dynamic_runtime_keys"]; ok { + t.Fatal("dynamic metadata leaked into logical row") + } +} diff --git a/internal/dataframe/recipe/exec/arango/registry.go b/internal/dataframe/recipe/exec/arango/registry.go new file mode 100644 index 0000000..45a9b17 --- /dev/null +++ b/internal/dataframe/recipe/exec/arango/registry.go @@ -0,0 +1,77 @@ +// Package arango provides the durable exec.Store adapter used by Loom +// deployments that already persist dataset metadata in ArangoDB. +package arango + +import ( + "context" + "encoding/json" + "fmt" + + "github.com/calypr/loom/internal/dataframe/recipe/exec" + arangostore "github.com/calypr/loom/internal/store/arango" +) + +const Collection = "loom_dataframe_recipes" + +type Client interface { + QueryRows(context.Context, string, int, map[string]interface{}, arangostore.RowVisitor) error + InsertBatchRaw(context.Context, string, []json.RawMessage, bool, string) error +} + +type Registry struct { + client Client + batchSize int +} + +func New(client Client) (*Registry, error) { + if client == nil { + return nil, fmt.Errorf("Arango recipe registry client is required") + } + return &Registry{client: client, batchSize: 32}, nil +} + +func BootstrapSpec() arangostore.BootstrapSpec { + return arangostore.BootstrapSpec{Collections: []arangostore.CollectionSpec{{Name: Collection, Indexes: [][]string{{"name"}, {"digest"}}}}} +} + +func (r *Registry) SaveRecipe(ctx context.Context, entry exec.Entry) error { + data, err := json.Marshal(entry) + if err != nil { + return err + } + var doc map[string]any + if err := json.Unmarshal(data, &doc); err != nil { + return err + } + doc["name"] = entry.Bundle.Name + doc["digest"] = entry.Digest + doc["_key"] = entry.Digest + data, err = json.Marshal(doc) + if err != nil { + return err + } + return r.client.InsertBatchRaw(ctx, Collection, []json.RawMessage{data}, true, "document") +} + +func (r *Registry) LoadRecipe(ctx context.Context, name string) (exec.Entry, error) { + var found *exec.Entry + err := r.client.QueryRows(ctx, `FOR doc IN @@collection FILTER doc.name == @name RETURN doc`, r.batchSize, map[string]interface{}{"@collection": Collection, "name": name}, func(row map[string]any) error { + data, err := json.Marshal(row) + if err != nil { + return err + } + var entry exec.Entry + if err := json.Unmarshal(data, &entry); err != nil { + return err + } + found = &entry + return nil + }) + if err != nil { + return exec.Entry{}, err + } + if found == nil { + return exec.Entry{}, fmt.Errorf("%w: %s", exec.ErrRecipeNotFound, name) + } + return *found, nil +} diff --git a/internal/dataframe/recipe/exec/persistent.go b/internal/dataframe/recipe/exec/persistent.go new file mode 100644 index 0000000..c6d0269 --- /dev/null +++ b/internal/dataframe/recipe/exec/persistent.go @@ -0,0 +1,75 @@ +package exec + +import ( + "context" + "encoding/json" + "errors" + "fmt" + + "github.com/calypr/loom/internal/dataframe/recipe" +) + +// Store is the persistence boundary for recipe registration. Implementations +// may use Arango, a file-backed registry, or another durable store; the +// registry itself owns validation, canonicalization, and idempotency rules. +type Store interface { + SaveRecipe(context.Context, Entry) error + LoadRecipe(context.Context, string) (Entry, error) +} + +var ErrRecipeNotFound = errors.New("recipe not found") + +// PersistentRegistry provides the same immutable/idempotent contract as the +// process-local Registry while making persistence explicit for production +// wiring. It never evaluates recipe data. +type PersistentRegistry struct { + Store Store +} + +func (r PersistentRegistry) Register(ctx context.Context, bundle recipe.Bundle) (Entry, error) { + entry, err := canonicalEntry(bundle) + if err != nil { + return Entry{}, err + } + if r.Store == nil { + return Entry{}, fmt.Errorf("persistent recipe store is required") + } + if existing, err := r.Store.LoadRecipe(ctx, bundle.Name); err == nil { + if existing.Digest != entry.Digest { + return Entry{}, fmt.Errorf("recipe %q is already registered with a different digest", bundle.Name) + } + return existing, nil + } else if !errors.Is(err, ErrRecipeNotFound) { + return Entry{}, fmt.Errorf("load recipe %q: %w", bundle.Name, err) + } + if err := r.Store.SaveRecipe(ctx, entry); err != nil { + return Entry{}, err + } + return entry, nil +} + +func canonicalEntry(bundle recipe.Bundle) (Entry, error) { + if bundle.Fragments != nil { + expanded, err := bundle.ExpandFragments() + if err != nil { + return Entry{}, err + } + bundle = expanded + } + if err := bundle.Validate(); err != nil { + return Entry{}, err + } + canonical, err := bundle.CanonicalJSON() + if err != nil { + return Entry{}, err + } + var immutable recipe.Bundle + if err := json.Unmarshal(canonical, &immutable); err != nil { + return Entry{}, fmt.Errorf("clone recipe: %w", err) + } + digest, err := immutable.Digest() + if err != nil { + return Entry{}, err + } + return Entry{Bundle: immutable, Digest: digest}, nil +} diff --git a/internal/dataframe/recipe/exec/persistent_test.go b/internal/dataframe/recipe/exec/persistent_test.go new file mode 100644 index 0000000..9c0c654 --- /dev/null +++ b/internal/dataframe/recipe/exec/persistent_test.go @@ -0,0 +1,43 @@ +package exec + +import ( + "context" + "testing" + + "github.com/calypr/loom/internal/dataframe/recipe" +) + +type memoryRecipeStore struct{ entries map[string]Entry } + +func (s *memoryRecipeStore) SaveRecipe(_ context.Context, entry Entry) error { + if s.entries == nil { + s.entries = map[string]Entry{} + } + s.entries[entry.Bundle.Name] = entry + return nil +} +func (s *memoryRecipeStore) LoadRecipe(_ context.Context, name string) (Entry, error) { + entry, ok := s.entries[name] + if !ok { + return Entry{}, ErrRecipeNotFound + } + return entry, nil +} + +func TestPersistentRegistryIsIdempotentAndDigestLocked(t *testing.T) { + store := &memoryRecipeStore{} + registry := PersistentRegistry{Store: store} + bundle := recipe.Bundle{RecipeSchemaVersion: 1, Name: "r", TranslationVersion: "v", Outputs: []recipe.Output{{Name: "Patient", RootResourceType: "Patient", RowGrain: "patient"}}} + first, err := registry.Register(context.Background(), bundle) + if err != nil { + t.Fatal(err) + } + second, err := registry.Register(context.Background(), bundle) + if err != nil || first.Digest != second.Digest { + t.Fatalf("not idempotent: %#v %#v %v", first, second, err) + } + bundle.TranslationVersion = "changed" + if _, err := registry.Register(context.Background(), bundle); err == nil { + t.Fatal("expected digest conflict") + } +} diff --git a/internal/dataframe/recipe/exec/runner.go b/internal/dataframe/recipe/exec/runner.go new file mode 100644 index 0000000..df4f4da --- /dev/null +++ b/internal/dataframe/recipe/exec/runner.go @@ -0,0 +1,44 @@ +// Package exec owns generic recipe registration and execution. A caller +// supplies the root-resource and relationship resolvers; the package never +// names a storage backend or a translation-specific output. +package exec + +import ( + "context" + "sync" + + "github.com/calypr/loom/internal/dataframe/recipe" + "github.com/calypr/loom/internal/dataframe/recipe/reference" +) + +type Registry struct { + mu sync.RWMutex + entries map[string]Entry +} + +type Entry struct { + Bundle recipe.Bundle + Digest string +} + +func NewRegistry() *Registry { return &Registry{entries: map[string]Entry{}} } + +// Register stores an immutable bundle. Re-registering the same name is only +// allowed when the canonical digest is identical, making retries idempotent. + +type Roots func(context.Context, recipe.Output, recipe.RuntimeBindings) ([]map[string]any, error) + +type Runner struct { + Registry *Registry + Roots Roots + Related reference.Related +} + +type Result struct { + RecipeName string + Digest string + Outputs map[string][]map[string]any +} + +// Run evaluates every output before returning. An error discards the complete +// result, so consumers never publish a partially translated bundle. diff --git a/internal/dataframe/recipe/fragments.go b/internal/dataframe/recipe/fragments.go new file mode 100644 index 0000000..506df62 --- /dev/null +++ b/internal/dataframe/recipe/fragments.go @@ -0,0 +1,247 @@ +package recipe + +import ( + "crypto/sha256" + "encoding/hex" + "encoding/json" + "fmt" + "sort" + "strings" +) + +// Fragment is a versioned, expression-only recipe macro. Parameters are +// substituted hygienically in selector placeholders `$name` or `${name}`. +// Fragments contain no storage or output-specific behavior. +type Fragment struct { + Name string `json:"name"` + Version string `json:"version"` + Params []string `json:"params,omitempty"` + Expr Expression `json:"expr"` +} + +type FragmentLibrary struct { + Fragments map[string]Fragment `json:"fragments"` +} + +func (l FragmentLibrary) Validate() error { + if len(l.Fragments) == 0 { + return fmt.Errorf("fragment library must not be empty") + } + for name, fragment := range l.Fragments { + if strings.TrimSpace(name) == "" || name != fragment.Name { + return fmt.Errorf("fragment name %q is inconsistent", name) + } + if strings.TrimSpace(fragment.Version) == "" { + return fmt.Errorf("fragment %q version is required", name) + } + seen := map[string]struct{}{} + for _, param := range fragment.Params { + if strings.TrimSpace(param) == "" { + return fmt.Errorf("fragment %q has an empty parameter", name) + } + if _, ok := seen[param]; ok { + return fmt.Errorf("fragment %q repeats parameter %q", name, param) + } + seen[param] = struct{}{} + } + if err := validateExpression(fragment.Expr, "$fragments."+name+".expr"); err != nil { + return err + } + } + return nil +} + +func (l FragmentLibrary) Digest() (string, error) { + if err := l.Validate(); err != nil { + return "", err + } + data, err := json.Marshal(l) + if err != nil { + return "", err + } + sum := sha256.Sum256(data) + return hex.EncodeToString(sum[:]), nil +} + +// ExpandExpression resolves fragment calls and returns a normal expression +// tree that contains only built-in recipe operators. +func (l FragmentLibrary) ExpandExpression(input Expression) (Expression, error) { + if err := l.Validate(); err != nil { + return Expression{}, err + } + return l.expand(input, nil, 0) +} + +// ExpandBundle returns a validated bundle with every fragment invocation +// replaced by built-in recipe expressions. The returned canonical document is +// therefore safe to digest and persist as the resolved translation source. +func (l FragmentLibrary) ExpandBundle(bundle Bundle) (Bundle, error) { + if err := bundle.Validate(); err != nil { + return Bundle{}, err + } + copy := bundle + copy.Outputs = make([]Output, len(bundle.Outputs)) + for i, output := range bundle.Outputs { + resolved, err := l.expandOutput(output) + if err != nil { + return Bundle{}, fmt.Errorf("output %q: %w", output.Name, err) + } + copy.Outputs[i] = resolved + } + if err := copy.Validate(); err != nil { + return Bundle{}, err + } + return copy, nil +} + +func (l FragmentLibrary) expandOutput(output Output) (Output, error) { + for i, field := range output.Fields { + expr, err := l.ExpandExpression(field.Expr) + if err != nil { + return Output{}, fmt.Errorf("field %q: %w", field.Name, err) + } + output.Fields[i].Expr = expr + } + for i, traversal := range output.Traversals { + resolved, err := l.expandTraversal(traversal) + if err != nil { + return Output{}, err + } + output.Traversals[i] = resolved + } + if output.Expand != nil { + expr, err := l.ExpandExpression(output.Expand.From) + if err != nil { + return Output{}, err + } + output.Expand.From = expr + } + if output.Identity != nil { + expr, err := l.ExpandExpression(output.Identity.Expr) + if err != nil { + return Output{}, err + } + output.Identity.Expr = expr + } + for i, dynamic := range output.DynamicColumns { + expr, err := l.ExpandExpression(dynamic.Source) + if err != nil { + return Output{}, err + } + output.DynamicColumns[i].Source = expr + if dynamic.Key != nil { + value, err := l.ExpandExpression(*dynamic.Key) + if err != nil { + return Output{}, err + } + output.DynamicColumns[i].Key = &value + } + if dynamic.Value != nil { + value, err := l.ExpandExpression(*dynamic.Value) + if err != nil { + return Output{}, err + } + output.DynamicColumns[i].Value = &value + } + } + return output, nil +} + +func (l FragmentLibrary) expandTraversal(traversal Traversal) (Traversal, error) { + for i, field := range traversal.Fields { + expr, err := l.ExpandExpression(field.Expr) + if err != nil { + return Traversal{}, err + } + traversal.Fields[i].Expr = expr + } + for i, child := range traversal.Traversals { + resolved, err := l.expandTraversal(child) + if err != nil { + return Traversal{}, err + } + traversal.Traversals[i] = resolved + } + if traversal.From != nil { + expr, err := l.ExpandExpression(*traversal.From) + if err != nil { + return Traversal{}, err + } + traversal.From = &expr + } + return traversal, nil +} + +func (l FragmentLibrary) expand(input Expression, stack []string, depth int) (Expression, error) { + if depth > maxExpressionDepth { + return Expression{}, fmt.Errorf("fragment expansion depth exceeds limit") + } + if input.Call == "" { + if input.Select == "" { + return input, nil + } + return input, nil + } + args := make([]Expression, len(input.Args)) + for i, arg := range input.Args { + expanded, err := l.expand(arg, stack, depth+1) + if err != nil { + return Expression{}, err + } + args[i] = expanded + } + if !strings.HasPrefix(input.Call, "fragment:") { + input.Args = args + return input, nil + } + name := strings.TrimPrefix(input.Call, "fragment:") + fragment, ok := l.Fragments[name] + if !ok { + return Expression{}, fmt.Errorf("unknown fragment %q", name) + } + for _, parent := range stack { + if parent == name { + return Expression{}, fmt.Errorf("fragment cycle includes %q", name) + } + } + if len(args) != len(fragment.Params) { + return Expression{}, fmt.Errorf("fragment %q expects %d arguments, got %d", name, len(fragment.Params), len(args)) + } + bindings := make(map[string]Expression, len(args)) + for i, param := range fragment.Params { + bindings[param] = args[i] + } + body := substitute(fragment.Expr, bindings) + return l.expand(body, append(stack, name), depth+1) +} + +func substitute(input Expression, bindings map[string]Expression) Expression { + if input.Select != "" { + key := strings.TrimSpace(input.Select) + for name, value := range bindings { + placeholder := "$" + name + if key == placeholder || key == "${"+name+"}" { + return value + } + if strings.HasPrefix(key, placeholder+".") && value.Select != "" { + value.Select += strings.TrimPrefix(key, placeholder) + return value + } + } + return input + } + for i := range input.Args { + input.Args[i] = substitute(input.Args[i], bindings) + } + return input +} + +// SortedNames makes explanation and registration output deterministic. +func (l FragmentLibrary) SortedNames() []string { + names := make([]string, 0, len(l.Fragments)) + for name := range l.Fragments { + names = append(names, name) + } + sort.Strings(names) + return names +} diff --git a/internal/dataframe/recipe/fragments_test.go b/internal/dataframe/recipe/fragments_test.go new file mode 100644 index 0000000..a160a11 --- /dev/null +++ b/internal/dataframe/recipe/fragments_test.go @@ -0,0 +1,38 @@ +package recipe + +import "testing" + +func TestFragmentExpansionIsHygienicAndDigestable(t *testing.T) { + lib := FragmentLibrary{Fragments: map[string]Fragment{"reference_id": {Name: "reference_id", Version: "1", Params: []string{"value"}, Expr: Expression{Call: "path_segment", Args: []Expression{{Select: "$value"}}}}}} + input := Expression{Call: "fragment:reference_id", Args: []Expression{{Select: "root.subject.reference"}}} + expanded, err := lib.ExpandExpression(input) + if err != nil { + t.Fatal(err) + } + if expanded.Call != "path_segment" || expanded.Args[0].Select != "root.subject.reference" { + t.Fatalf("unexpected expansion: %#v", expanded) + } + if _, err := lib.Digest(); err != nil { + t.Fatal(err) + } +} + +func TestFragmentCycleRejected(t *testing.T) { + lib := FragmentLibrary{Fragments: map[string]Fragment{"a": {Name: "a", Version: "1", Expr: Expression{Call: "fragment:b"}}, "b": {Name: "b", Version: "1", Expr: Expression{Call: "fragment:a"}}}} + if _, err := lib.ExpandExpression(Expression{Call: "fragment:a"}); err == nil { + t.Fatal("expected fragment cycle") + } +} + +func TestBundleExpandFragmentsProducesStandaloneDigestInput(t *testing.T) { + bundle := Bundle{RecipeSchemaVersion: 1, Name: "fragments", TranslationVersion: "v", Fragments: &FragmentLibrary{Fragments: map[string]Fragment{ + "id": {Name: "id", Version: "1", Params: []string{"ctx"}, Expr: Expression{Select: "$ctx.id"}}, + }}, Outputs: []Output{{Name: "Patient", RootResourceType: "Patient", RowGrain: "patient", Fields: []Field{{Name: "id", Expr: Expression{Call: "fragment:id", Args: []Expression{{Select: "root"}}}}}}}} + expanded, err := bundle.ExpandFragments() + if err != nil { + t.Fatal(err) + } + if expanded.Fragments != nil || expanded.Outputs[0].Fields[0].Expr.Select != "root.id" { + t.Fatalf("bundle was not standalone: %#v", expanded) + } +} diff --git a/internal/dataframe/recipe/json_encoding.go b/internal/dataframe/recipe/json_encoding.go new file mode 100644 index 0000000..224f1e4 --- /dev/null +++ b/internal/dataframe/recipe/json_encoding.go @@ -0,0 +1,167 @@ +// Package recipe defines the persistence-neutral recipe document used by the +// dataframe compiler. A recipe describes semantic row shaping only; it never +// carries database collection, table, AQL, or SQL details. +package recipe + +import ( + "bytes" + "encoding/json" + "fmt" + "strings" +) + +func (b Bundle) ExpandFragments() (Bundle, error) { + if b.Fragments == nil { + return b, nil + } + expanded, err := b.Fragments.ExpandBundle(b) + if err != nil { + return Bundle{}, err + } + expanded.Fragments = nil + return expanded, nil +} + +// Explanation is a storage-independent summary of a validated recipe. +type Explanation struct { + RecipeSchemaVersion int `json:"recipeSchemaVersion"` + Name string `json:"name"` + TranslationVersion string `json:"translationVersion"` + Digest string `json:"digest"` + Outputs []OutputExplanation `json:"outputs"` +} + +// OutputExplanation intentionally omits physical implementation details. +type OutputExplanation struct { + Name string `json:"name"` + RootResourceType string `json:"rootResourceType"` + RowGrain string `json:"rowGrain"` + FieldNames []string `json:"fieldNames,omitempty"` + TraversalNames []string `json:"traversalNames,omitempty"` + Expanded bool `json:"expanded,omitempty"` + DynamicColumns []string `json:"dynamicColumns,omitempty"` + CatalogProjections []string `json:"catalogProjections,omitempty"` +} + +// Expression is the generic recipe expression input AST. Exactly one of +// Select, Literal, or Call is set; Call nodes may contain Args. +type Expression struct { + Select string + Literal json.RawMessage + Call string + Args []Expression +} + +func (e Expression) MarshalJSON() ([]byte, error) { + switch { + case e.Select != "" && e.Call == "" && e.Literal == nil: + return json.Marshal(struct { + Select string `json:"select"` + }{e.Select}) + case e.Call != "" && e.Select == "" && e.Literal == nil: + if len(e.Args) == 0 { + return json.Marshal(struct { + Call string `json:"call"` + }{e.Call}) + } + return json.Marshal(struct { + Call string `json:"call"` + Args []Expression `json:"args"` + }{e.Call, e.Args}) + case e.Literal != nil && e.Select == "" && e.Call == "": + if !json.Valid(e.Literal) { + return nil, fmt.Errorf("literal is not valid JSON") + } + return append([]byte(`{"literal":`), append(e.Literal, '}')...), nil + default: + return nil, fmt.Errorf("expression must contain exactly one of select, literal, or call") + } +} + +func (e *Expression) UnmarshalJSON(data []byte) error { + var object map[string]json.RawMessage + dec := json.NewDecoder(bytes.NewReader(data)) + dec.DisallowUnknownFields() + if err := dec.Decode(&object); err != nil { + return fmt.Errorf("expression must be an object: %w", err) + } + var extra any + if err := dec.Decode(&extra); err == nil { + return fmt.Errorf("expression contains trailing JSON") + } + if len(object) == 0 { + return fmt.Errorf("expression must contain an operator") + } + for key := range object { + if key != "select" && key != "literal" && key != "call" && key != "args" { + return fmt.Errorf("unknown expression field %q", key) + } + } + _, hasSelect := object["select"] + _, hasLiteral := object["literal"] + _, hasCall := object["call"] + _, hasArgs := object["args"] + operatorCount := 0 + if hasSelect { + operatorCount++ + } + if hasLiteral { + operatorCount++ + } + if hasCall { + operatorCount++ + } + if operatorCount != 1 { + return fmt.Errorf("expression must contain exactly one operator") + } + if hasArgs && !hasCall { + return fmt.Errorf("args is only valid with call") + } + *e = Expression{} + if hasSelect { + if err := json.Unmarshal(object["select"], &e.Select); err != nil || strings.TrimSpace(e.Select) == "" { + return fmt.Errorf("select must be a non-empty string") + } + return nil + } + if hasLiteral { + if !json.Valid(object["literal"]) { + return fmt.Errorf("literal is not valid JSON") + } + e.Literal = append(json.RawMessage(nil), object["literal"]...) + return nil + } + if err := json.Unmarshal(object["call"], &e.Call); err != nil || strings.TrimSpace(e.Call) == "" { + return fmt.Errorf("call must be a non-empty string") + } + if hasArgs { + if err := json.Unmarshal(object["args"], &e.Args); err != nil { + return fmt.Errorf("args must be an array: %w", err) + } + } + return nil +} + +// Parse strictly decodes and validates a recipe document. +func Parse(data []byte) (Bundle, error) { + if err := detectDuplicateKeys(data); err != nil { + return Bundle{}, err + } + var b Bundle + dec := json.NewDecoder(bytes.NewReader(data)) + dec.DisallowUnknownFields() + if err := dec.Decode(&b); err != nil { + return Bundle{}, validationError("parse_error", "$", err.Error()) + } + var trailing any + if err := dec.Decode(&trailing); err == nil { + return Bundle{}, validationError("parse_error", "$", "multiple JSON values") + } + if err := b.Validate(); err != nil { + return Bundle{}, err + } + return b, nil +} + +// Validate checks semantic document invariants and resource-independent +// expression safety. diff --git a/internal/dataframe/recipe/plan/schema.go b/internal/dataframe/recipe/plan/schema.go new file mode 100644 index 0000000..cb26ca4 --- /dev/null +++ b/internal/dataframe/recipe/plan/schema.go @@ -0,0 +1,120 @@ +// Package plan contains immutable, backend-neutral plans produced after +// recipe validation and scoped schema discovery. +package plan + +import ( + "crypto/sha256" + "encoding/hex" + "encoding/json" + "fmt" + "regexp" + "sort" + "strings" +) + +type DynamicSpec struct { + Name string + AllowedKeys []string + MaxColumns int + Collision string +} + +type Candidate struct { + Key string + ValueType string +} + +type Column struct { + Name string `json:"name"` + ValueType string `json:"valueType"` + SourceKey string `json:"sourceKey,omitempty"` +} + +type FrozenSchema struct { + Columns []Column `json:"columns"` + Digest string `json:"digest"` +} + +// Freeze converts discovery candidates into a deterministic bounded schema. +// It does not perform discovery itself; callers provide candidates obtained +// through the same scoped physical plan used for execution. +func Freeze(spec DynamicSpec, candidates []Candidate) (FrozenSchema, error) { + if strings.TrimSpace(spec.Name) == "" { + return FrozenSchema{}, fmt.Errorf("dynamic column name is required") + } + if spec.MaxColumns < 0 { + return FrozenSchema{}, fmt.Errorf("dynamic column max must not be negative") + } + policy := spec.Collision + if policy == "" { + policy = "error" + } + if policy != "error" && policy != "overwrite" && policy != "coalesce" { + return FrozenSchema{}, fmt.Errorf("unsupported dynamic collision policy %q", policy) + } + allowed := map[string]struct{}{} + for _, key := range spec.AllowedKeys { + allowed[key] = struct{}{} + } + columns := map[string]Column{} + for _, candidate := range candidates { + if len(allowed) > 0 { + if _, ok := allowed[candidate.Key]; !ok { + continue + } + } + key := sanitize(candidate.Key) + if key == "" { + continue + } + name := spec.Name + "_" + key + column := Column{Name: name, ValueType: candidate.ValueType, SourceKey: candidate.Key} + if column.ValueType == "" { + column.ValueType = "unknown" + } + if existing, ok := columns[name]; ok { + if policy == "error" { + return FrozenSchema{}, fmt.Errorf("dynamic key collision at %q", name) + } + if existing.ValueType != column.ValueType && policy != "coalesce" { + return FrozenSchema{}, fmt.Errorf("dynamic column %q has incompatible types %q and %q", name, existing.ValueType, column.ValueType) + } + if policy == "coalesce" && existing.ValueType == "unknown" { + columns[name] = column + } + continue + } + if spec.MaxColumns > 0 && len(columns) >= spec.MaxColumns { + return FrozenSchema{}, fmt.Errorf("dynamic column limit %d exceeded", spec.MaxColumns) + } + columns[name] = column + } + result := FrozenSchema{Columns: make([]Column, 0, len(columns))} + for _, column := range columns { + result.Columns = append(result.Columns, column) + } + sort.Slice(result.Columns, func(i, j int) bool { return result.Columns[i].Name < result.Columns[j].Name }) + canonical, err := json.Marshal(result.Columns) + if err != nil { + return FrozenSchema{}, err + } + sum := sha256.Sum256(canonical) + result.Digest = hex.EncodeToString(sum[:]) + return result, nil +} + +var invalidColumn = regexp.MustCompile(`[^A-Za-z0-9_]`) + +func sanitize(value string) string { + value = invalidColumn.ReplaceAllString(value, "_") + if value == "" { + return "_" + } + if value[0] >= '0' && value[0] <= '9' { + value = "_" + value + } + if strings.HasPrefix(value, "__") { + value = "_" + strings.TrimPrefix(value, "__") + } + return value +} diff --git a/internal/dataframe/recipe/plan/schema_test.go b/internal/dataframe/recipe/plan/schema_test.go new file mode 100644 index 0000000..52fab6a --- /dev/null +++ b/internal/dataframe/recipe/plan/schema_test.go @@ -0,0 +1,26 @@ +package plan + +import "testing" + +func TestFreezeIsBoundedDeterministicAndTyped(t *testing.T) { + first, err := Freeze(DynamicSpec{Name: "code", MaxColumns: 4}, []Candidate{{Key: "10-a", ValueType: "string"}, {Key: "b", ValueType: "integer"}}) + if err != nil { + t.Fatal(err) + } + second, err := Freeze(DynamicSpec{Name: "code", MaxColumns: 4}, []Candidate{{Key: "b", ValueType: "integer"}, {Key: "10-a", ValueType: "string"}}) + if err != nil { + t.Fatal(err) + } + if first.Digest != second.Digest || first.Columns[0].Name != "code__10_a" { + t.Fatalf("unstable schema: %#v %#v", first, second) + } +} + +func TestFreezeRejectsSanitizedCollisionAndLimit(t *testing.T) { + if _, err := Freeze(DynamicSpec{Name: "x"}, []Candidate{{Key: "a-b", ValueType: "string"}, {Key: "a_b", ValueType: "string"}}); err == nil { + t.Fatal("expected collision") + } + if _, err := Freeze(DynamicSpec{Name: "x", MaxColumns: 1}, []Candidate{{Key: "a", ValueType: "string"}, {Key: "b", ValueType: "string"}}); err == nil { + t.Fatal("expected limit") + } +} diff --git a/internal/dataframe/recipe/reference/doc.go b/internal/dataframe/recipe/reference/doc.go new file mode 100644 index 0000000..c4e4b3c --- /dev/null +++ b/internal/dataframe/recipe/reference/doc.go @@ -0,0 +1,5 @@ +// Package reference is a storage-neutral reference interpreter for +// differential and conformance tests only. Production server, runtime, and +// materialization paths must consume checked semantic plans and the physical +// compiler; they must not import this map-based evaluator. +package reference diff --git a/internal/dataframe/recipe/reference/eval.go b/internal/dataframe/recipe/reference/eval.go new file mode 100644 index 0000000..a901d45 --- /dev/null +++ b/internal/dataframe/recipe/reference/eval.go @@ -0,0 +1,25 @@ +// Package reference evaluates the storage-neutral recipe language over a +// caller-provided row and relationship resolver. It is deliberately generic: +// the evaluator knows only logical contexts and expressions, never FHIR types, +// graph collections, or ClickHouse tables. +package reference + +import ( + "regexp" + + "github.com/calypr/loom/internal/dataframe/recipe" +) + +// Related resolves one logical traversal. Implementations may use Loom's +// generated relationship catalog, a fixture graph, or another backend. +type Related func(parent map[string]any, traversal recipe.Traversal) ([]map[string]any, error) + +// EvaluateOutput evaluates one output for a root resource. One root can yield +// multiple rows when the recipe declares an expansion. + +// DiscoverColumns evaluates dynamic column sources without emitting rows. +// Callers use the result to freeze a schema before materialization. + +type context map[string]any + +var invalidName = regexp.MustCompile(`[^A-Za-z0-9_]`) diff --git a/internal/dataframe/recipe/schema/discovery.go b/internal/dataframe/recipe/schema/discovery.go new file mode 100644 index 0000000..35c3bb4 --- /dev/null +++ b/internal/dataframe/recipe/schema/discovery.go @@ -0,0 +1,345 @@ +// Package schema resolves catalog-backed recipe declarations into a +// concrete, typed recipe before semantic compilation. It deliberately knows +// nothing about AQL, Arango collections, or output-specific behavior. +package schema + +import ( + "context" + "encoding/json" + "fmt" + "path" + "sort" + "strings" + + "github.com/calypr/loom/fhirschema" + "github.com/calypr/loom/internal/dataframe/recipe" +) + +func resolveProjectionSets(ctx context.Context, scope Scope, discovery Discovery, resourceType, alias string, sets []recipe.CatalogProjection) ([]recipe.Field, error) { + fields := make([]recipe.Field, 0) + seen := map[string]struct{}{} + for _, set := range sets { + candidates, err := discovery.Fields(ctx, scope, resourceType) + if err != nil { + return nil, err + } + selected := filterCandidates(candidates, set) + // Catalog projections are optional shape discovery. A resource type can + // be absent from a valid dataset (or have no populated fields matching + // the requested kind); retain the static recipe fields and resolve this + // projection set to zero columns rather than rejecting the whole bundle. + if len(selected) == 0 { + continue + } + if len(selected) > set.MaxColumns { + return nil, fmt.Errorf("projection set %q matched %d fields, max is %d", set.Name, len(selected), set.MaxColumns) + } + sort.SliceStable(selected, func(i, j int) bool { return selected[i].Path < selected[j].Path }) + for _, candidate := range selected { + name := projectionName(set, candidate.Path) + if _, exists := seen[name]; exists { + return nil, fmt.Errorf("projection set %q produced duplicate column %q", set.Name, name) + } + seen[name] = struct{}{} + selectPath := strings.TrimPrefix(candidate.Path, ".") + if alias != "" && alias != "root" { + selectPath = alias + "." + selectPath + } else { + selectPath = "root." + selectPath + } + fields = append(fields, recipe.Field{Name: name, FieldRef: candidate.Path, ValueMode: set.ValueMode, Expr: recipe.Expression{Select: selectPath}}) + } + } + return fields, nil +} + +func resolvePivots(ctx context.Context, scope Scope, discovery Discovery, resourceType, alias string, pivots []recipe.Pivot) error { + for index := range pivots { + pivot := &pivots[index] + if pivot.Discovery == nil { + continue + } + candidates, err := discovery.Fields(ctx, scope, resourceType) + if err != nil { + return err + } + columns := map[string]struct{}{} + var columnSelect, valueSelect string + var pivotSpec fhirschema.PivotSpec + var hasPivotSpec bool + for _, candidate := range candidates { + if !candidate.PivotCandidate { + continue + } + if pivot.Discovery.Family != "" && !strings.EqualFold(candidate.PivotFamily, pivot.Discovery.Family) { + continue + } + if pivot.Discovery.Path != "" && candidate.Path != pivot.Discovery.Path { + continue + } + if !hasPivotSpec { + observedValuePath := "" + if candidate.PivotFamily == "observation_code_value" { + observedValuePath = candidate.PivotValueSelect + } + if spec, ok := fhirschema.DefaultPivotSpec(resourceType, candidate.Path, observedValuePath); ok { + pivotSpec, hasPivotSpec = spec, true + // Persisted catalog metadata is an immutable observation of + // the loaded shape. Prefer it when present, while retaining the + // generated schema default for older catalog documents. + if candidate.PivotColumnSelect != "" { + pivotSpec.ColumnSelector = fhirschema.FieldSelectorSpecFromPath(candidate.PivotColumnSelect) + } + if candidate.PivotValueSelect != "" { + pivotSpec.ValueSelector = fhirschema.FieldSelectorSpecFromPath(candidate.PivotValueSelect) + } + if candidate.PivotItemSource != "" { + pivotSpec.ItemSourcePath = candidate.PivotItemSource + } + if candidate.PivotItemResourceType != "" { + pivotSpec.ItemResourceType = candidate.PivotItemResourceType + } + if len(candidate.PivotValueSelectors) > 0 { + pivotSpec.ValueSelectors = make([]fhirschema.FieldSelectorSpec, 0, len(candidate.PivotValueSelectors)) + for _, selector := range candidate.PivotValueSelectors { + pivotSpec.ValueSelectors = append(pivotSpec.ValueSelectors, fhirschema.FieldSelectorSpecFromPath(selector)) + } + pivotSpec.ValueSelector = pivotSpec.ValueSelectors[0] + } + if pivotSpec.ItemSourcePath != "" { + pivotSpec.ColumnSelector = fhirschema.FieldSelectorSpecFromPath(relativePivotPath(pivotSpec.ItemSourcePath, fhirschema.SelectorExpression(pivotSpec.ColumnSelector))) + pivotSpec.ValueSelector = fhirschema.FieldSelectorSpecFromPath(relativePivotPath(pivotSpec.ItemSourcePath, fhirschema.SelectorExpression(pivotSpec.ValueSelector))) + for index, selector := range pivotSpec.ValueSelectors { + pivotSpec.ValueSelectors[index] = fhirschema.FieldSelectorSpecFromPath(relativePivotPath(pivotSpec.ItemSourcePath, fhirschema.SelectorExpression(selector))) + } + } + } + } + if columnSelect == "" { + columnSelect = candidate.PivotColumnSelect + } + if valueSelect == "" { + valueSelect = candidate.PivotValueSelect + } + for _, column := range candidate.PivotColumns { + columns[column] = struct{}{} + } + if len(candidate.PivotColumns) == 0 { + for _, column := range candidate.DistinctValues { + columns[column] = struct{}{} + } + } + } + pivot.Columns = sortedLimited(columns, pivot.Discovery.MaxColumns) + if len(pivot.Columns) == 0 { + return fmt.Errorf("pivot %q discovery matched no columns", pivot.Name) + } + if pivot.ColumnExpr.Select == "" && pivot.ColumnExpr.Call == "" { + if hasPivotSpec { + pivot.ColumnExpr = recipe.Expression{Select: qualifyPivotSelector(alias, pivotSpec.ItemSourcePath, fhirschema.SelectorExpression(pivotSpec.ColumnSelector))} + pivot.ValueExpr = recipe.Expression{Select: qualifyPivotSelector(alias, pivotSpec.ItemSourcePath, fhirschema.SelectorExpression(pivotSpec.ValueSelector))} + if pivotSpec.ItemSourcePath != "" { + pivot.ItemResourceType = pivotSpec.ItemResourceType + pivot.ItemSource = recipe.Expression{Select: qualifyDiscoveredSelector(alias, pivotSpec.ItemSourcePath)} + } + for _, fallback := range pivotSpec.ValueSelectors[1:] { + pivot.ValueFallbacks = append(pivot.ValueFallbacks, recipe.Expression{Select: qualifyPivotSelector(alias, pivotSpec.ItemSourcePath, fhirschema.SelectorExpression(fallback))}) + } + } else { + if columnSelect == "" { + return fmt.Errorf("pivot %q discovery did not provide a column selector", pivot.Name) + } + pivot.ColumnExpr = recipe.Expression{Select: qualifyDiscoveredSelector(alias, columnSelect)} + } + } + if pivot.ValueExpr.Select == "" && pivot.ValueExpr.Call == "" { + if valueSelect == "" { + return fmt.Errorf("pivot %q discovery did not provide a value selector", pivot.Name) + } + pivot.ValueExpr = recipe.Expression{Select: qualifyDiscoveredSelector(alias, valueSelect)} + } + pivot.Discovery = nil + } + return nil +} + +func qualifyPivotSelector(alias, itemSource, selector string) string { + path := strings.TrimSpace(selector) + if itemSource != "" { + path = strings.TrimSuffix(itemSource, ".") + "." + path + } + return qualifyDiscoveredSelector(alias, path) +} + +func relativePivotPath(itemSource, selector string) string { + itemSource = fhirschema.CanonicalizePath(itemSource) + selector = fhirschema.CanonicalizePath(selector) + prefix := itemSource + "." + if strings.HasPrefix(selector, prefix) { + return strings.TrimPrefix(selector, prefix) + } + return selector +} + +func resolveDynamicColumns(ctx context.Context, scope Scope, discovery Discovery, resourceType string, dynamics []recipe.DynamicColumn) error { + for index := range dynamics { + dynamic := &dynamics[index] + if len(dynamic.Columns) > 0 { + continue + } + if dynamic.Key == nil { + return fmt.Errorf("dynamic column %q has no static columns or key selector", dynamic.Name) + } + keySelect := strings.TrimPrefix(strings.TrimSpace(dynamic.Key.Select), "item.") + source := strings.TrimPrefix(strings.TrimSpace(dynamic.Source.Select), "root.") + if dot := strings.IndexByte(source, '.'); dot >= 0 { + source = source[dot+1:] + } + keyPath := source + "." + keySelect + candidates, err := discovery.Fields(ctx, scope, resourceType) + if err != nil { + return err + } + values := map[string]struct{}{} + for _, candidate := range candidates { + if candidate.Path != keyPath { + continue + } + if candidate.DistinctTruncated { + return fmt.Errorf("dynamic column %q key discovery at %q was truncated", dynamic.Name, keyPath) + } + for _, value := range candidate.DistinctValues { + values[value] = struct{}{} + } + } + if dynamic.MaxColumns <= 0 { + return fmt.Errorf("dynamic column %q requires maxColumns when discovered", dynamic.Name) + } + dynamic.Columns = sortedLimited(values, dynamic.MaxColumns) + // A keyed family is optional: a valid FHIR dataset may have no values + // for an extension/identifier family at all. Keep the declaration with + // an empty frozen column set so resolution remains schema-stable and the + // compiler emits no columns for that family instead of rejecting the + // entire dataframe. + } + return nil +} + +func qualifyDiscoveredSelector(alias, selector string) string { + selector = strings.TrimPrefix(strings.TrimSpace(selector), ".") + if selector == "" || alias == "" || alias == "root" { + if alias == "root" && selector != "" { + return "root." + selector + } + return selector + } + return alias + "." + selector +} + +func filterCandidates(candidates []FieldCandidate, set recipe.CatalogProjection) []FieldCandidate { + kinds := map[string]struct{}{} + for _, kind := range set.Kinds { + kinds[kind] = struct{}{} + } + selected := make([]FieldCandidate, 0) + for _, candidate := range candidates { + if len(kinds) > 0 { + if _, ok := kinds[candidate.Kind]; !ok { + continue + } + } + if !matchesAny(candidate.Path, set.IncludePaths) || matchesAny(candidate.Path, set.ExcludePaths) { + continue + } + selected = append(selected, candidate) + } + return selected +} + +func matchesAny(value string, patterns []string) bool { + for _, pattern := range patterns { + if ok, err := path.Match(pattern, value); err == nil && ok { + return true + } + if strings.HasSuffix(pattern, "*") && strings.HasPrefix(value, strings.TrimSuffix(pattern, "*")) { + return true + } + if value == pattern { + return true + } + } + return false +} + +func projectionName(set recipe.CatalogProjection, fieldPath string) string { + name := fieldPath + if set.Naming == recipe.ColumnNamingPathSuffix { + name = name[strings.LastIndex(name, ".")+1:] + } + name = strings.NewReplacer("[]", "", ".", "_", "-", "_", "/", "_").Replace(name) + var b strings.Builder + for _, r := range name { + if (r >= 'a' && r <= 'z') || (r >= 'A' && r <= 'Z') || (r >= '0' && r <= '9') || r == '_' { + b.WriteRune(r) + } else { + b.WriteByte('_') + } + } + return strings.Trim(b.String(), "_") +} + +func sortedLimited(values map[string]struct{}, limit int) []string { + result := make([]string, 0, len(values)) + for value := range values { + result = append(result, value) + } + sort.Strings(result) + if limit > 0 && len(result) > limit { + result = result[:limit] + } + return result +} + +func hasCatalogDeclarations(bundle recipe.Bundle) bool { + for _, output := range bundle.Outputs { + if len(output.CatalogProjections) > 0 || hasCatalogPivots(output.Pivots) || hasCatalogDynamic(output.DynamicColumns) { + return true + } + for _, traversal := range output.Traversals { + if traversalHasCatalog(traversal) { + return true + } + } + } + return false +} + +func traversalHasCatalog(traversal recipe.Traversal) bool { + return len(traversal.CatalogProjections) > 0 || hasCatalogPivots(traversal.Pivots) || hasCatalogDynamic(traversal.DynamicColumns) +} + +func hasCatalogPivots(pivots []recipe.Pivot) bool { + for _, pivot := range pivots { + if pivot.Discovery != nil { + return true + } + } + return false +} + +func hasCatalogDynamic(dynamics []recipe.DynamicColumn) bool { + for _, dynamic := range dynamics { + if len(dynamic.Columns) == 0 { + return true + } + } + return false +} + +func cloneBundle(bundle recipe.Bundle) (recipe.Bundle, error) { + data, err := json.Marshal(bundle) + if err != nil { + return recipe.Bundle{}, err + } + return recipe.Parse(data) +} diff --git a/internal/dataframe/recipe/schema/resolve.go b/internal/dataframe/recipe/schema/resolve.go new file mode 100644 index 0000000..aac1a3a --- /dev/null +++ b/internal/dataframe/recipe/schema/resolve.go @@ -0,0 +1,229 @@ +// Package schema resolves catalog-backed recipe declarations into a +// concrete, typed recipe before semantic compilation. It deliberately knows +// nothing about AQL, Arango collections, or output-specific behavior. +package schema + +import ( + "context" + "crypto/sha256" + "encoding/hex" + "encoding/json" + "fmt" + "sort" + "strings" + + "github.com/calypr/loom/internal/dataframe/recipe" +) + +type Scope struct { + Project string + DatasetGeneration string + AuthResourcePaths []string + AuthScopeMode string +} + +type FieldCandidate struct { + ResourceType string + Path string + Kind string + DistinctValues []string + DistinctTruncated bool + PivotCandidate bool + PivotFamily string + PivotColumns []string + PivotColumnSelect string + PivotValueSelect string + PivotItemSource string + PivotItemResourceType string + PivotValueSelectors []string +} + +// Discovery is the only backend-facing seam needed by recipe resolution. The +// implementation in server adapts Loom's existing field catalog reader. +type Discovery interface { + Fields(context.Context, Scope, string) ([]FieldCandidate, error) +} + +type Resolved struct { + Bundle recipe.Bundle + Scope Scope + StoredRecipeDigest string + ResolvedSchemaDigest string + ScopeDigest string + SourceSnapshot SourceSnapshot +} + +// SourceSnapshot records the exact logical dataset namespace used during +// discovery. An empty generation is the legacy-null namespace, not an +// unscoped request. +type SourceSnapshot struct { + Project string + DatasetGeneration string + LegacyNull bool + AuthScopeMode string + AuthResourcePaths []string +} + +// memoizedDiscovery keeps one immutable catalog snapshot per resource type for +// the lifetime of a resolution. A recipe can have several projection sets, +// pivots, and keyed maps on the same node; issuing the catalog query once per +// declaration needlessly adds latency and can produce an internally +// inconsistent schema if the catalog changes between reads. +type memoizedDiscovery struct { + delegate Discovery + byType map[string][]FieldCandidate +} + +func (d *memoizedDiscovery) Fields(ctx context.Context, scope Scope, resourceType string) ([]FieldCandidate, error) { + if fields, ok := d.byType[resourceType]; ok { + return cloneCandidates(fields), nil + } + fields, err := d.delegate.Fields(ctx, scope, resourceType) + if err != nil { + return nil, err + } + d.byType[resourceType] = cloneCandidates(fields) + return cloneCandidates(fields), nil +} + +func Resolve(ctx context.Context, bundle recipe.Bundle, scope Scope, discovery Discovery) (Resolved, error) { + if err := bundle.Validate(); err != nil { + return Resolved{}, err + } + storedDigest, err := bundle.Digest() + if err != nil { + return Resolved{}, fmt.Errorf("digest stored recipe: %w", err) + } + base := Resolved{Scope: scope, StoredRecipeDigest: storedDigest, SourceSnapshot: sourceSnapshot(scope)} + if discovery == nil { + if hasCatalogDeclarations(bundle) { + return Resolved{}, fmt.Errorf("catalog-backed recipe requires schema discovery") + } + base.Bundle = bundle + base.ScopeDigest, base.ResolvedSchemaDigest, err = resolutionDigests(storedDigest, bundle, scope) + if err != nil { + return Resolved{}, err + } + return base, nil + } + resolved, err := cloneBundle(bundle) + if err != nil { + return Resolved{}, err + } + memo := &memoizedDiscovery{delegate: discovery, byType: make(map[string][]FieldCandidate)} + for outputIndex := range resolved.Outputs { + if err := resolveOutput(ctx, &resolved.Outputs[outputIndex], scope, memo); err != nil { + return Resolved{}, fmt.Errorf("output %q: %w", resolved.Outputs[outputIndex].Name, err) + } + } + if err := resolved.Validate(); err != nil { + return Resolved{}, fmt.Errorf("resolved recipe: %w", err) + } + base.Bundle = resolved + base.ScopeDigest, base.ResolvedSchemaDigest, err = resolutionDigests(storedDigest, resolved, scope) + if err != nil { + return Resolved{}, err + } + return base, nil +} + +func sourceSnapshot(scope Scope) SourceSnapshot { + paths := append([]string(nil), scope.AuthResourcePaths...) + sort.Strings(paths) + return SourceSnapshot{Project: scope.Project, DatasetGeneration: scope.DatasetGeneration, LegacyNull: strings.TrimSpace(scope.DatasetGeneration) == "", AuthScopeMode: scope.AuthScopeMode, AuthResourcePaths: paths} +} + +func resolutionDigests(storedDigest string, bundle recipe.Bundle, scope Scope) (string, string, error) { + snapshot := sourceSnapshot(scope) + scopeBytes, err := json.Marshal(snapshot) + if err != nil { + return "", "", err + } + scopeSum := sha256.Sum256(scopeBytes) + scopeDigest := hex.EncodeToString(scopeSum[:]) + bundleDigest, err := bundle.Digest() + if err != nil { + return "", "", err + } + payload, err := json.Marshal(struct { + Stored, Resolved, Scope string + }{storedDigest, bundleDigest, scopeDigest}) + if err != nil { + return "", "", err + } + resolvedSum := sha256.Sum256(payload) + return scopeDigest, hex.EncodeToString(resolvedSum[:]), nil +} + +func cloneCandidates(in []FieldCandidate) []FieldCandidate { + if len(in) == 0 { + return []FieldCandidate{} + } + out := make([]FieldCandidate, len(in)) + for i := range in { + out[i] = in[i] + out[i].DistinctValues = append([]string(nil), in[i].DistinctValues...) + out[i].PivotColumns = append([]string(nil), in[i].PivotColumns...) + } + return out +} + +func resolveOutput(ctx context.Context, output *recipe.Output, scope Scope, discovery Discovery) error { + fields, err := resolveProjectionSets(ctx, scope, discovery, output.RootResourceType, "root", output.CatalogProjections) + if err != nil { + return fmt.Errorf("catalog projections: %w", err) + } + output.Fields = appendUniqueFields(output.Fields, fields) + if err := resolvePivots(ctx, scope, discovery, output.RootResourceType, "root", output.Pivots); err != nil { + return fmt.Errorf("pivots: %w", err) + } + if err := resolveDynamicColumns(ctx, scope, discovery, output.RootResourceType, output.DynamicColumns); err != nil { + return fmt.Errorf("dynamic columns: %w", err) + } + for index := range output.Traversals { + if err := resolveTraversal(ctx, &output.Traversals[index], scope, discovery); err != nil { + return err + } + } + return nil +} + +func resolveTraversal(ctx context.Context, traversal *recipe.Traversal, scope Scope, discovery Discovery) error { + alias := traversal.Alias + if alias == "" { + alias = traversal.Name + } + fields, err := resolveProjectionSets(ctx, scope, discovery, traversal.ToResourceType, alias, traversal.CatalogProjections) + if err != nil { + return fmt.Errorf("traversal %q catalog projections: %w", traversal.Name, err) + } + traversal.Fields = appendUniqueFields(traversal.Fields, fields) + if err := resolvePivots(ctx, scope, discovery, traversal.ToResourceType, alias, traversal.Pivots); err != nil { + return fmt.Errorf("traversal %q pivots: %w", traversal.Name, err) + } + if err := resolveDynamicColumns(ctx, scope, discovery, traversal.ToResourceType, traversal.DynamicColumns); err != nil { + return fmt.Errorf("traversal %q dynamic columns: %w", traversal.Name, err) + } + for index := range traversal.Traversals { + if err := resolveTraversal(ctx, &traversal.Traversals[index], scope, discovery); err != nil { + return err + } + } + return nil +} + +func appendUniqueFields(existing, discovered []recipe.Field) []recipe.Field { + seen := make(map[string]struct{}, len(existing)+len(discovered)) + for _, field := range existing { + seen[field.Name] = struct{}{} + } + result := append([]recipe.Field(nil), existing...) + for _, field := range discovered { + if _, ok := seen[field.Name]; ok { + continue + } + seen[field.Name] = struct{}{} + result = append(result, field) + } + return result +} diff --git a/internal/dataframe/recipe/schema/resolve_test.go b/internal/dataframe/recipe/schema/resolve_test.go new file mode 100644 index 0000000..f1089a5 --- /dev/null +++ b/internal/dataframe/recipe/schema/resolve_test.go @@ -0,0 +1,196 @@ +package schema + +import ( + "context" + "strings" + "testing" + + "github.com/calypr/loom/internal/dataframe/compiler" + "github.com/calypr/loom/internal/dataframe/recipe" + "github.com/calypr/loom/internal/dataframe/semantic" +) + +type fakeDiscovery struct { + fields []FieldCandidate + byType map[string][]FieldCandidate + calls map[string]int +} + +func (f fakeDiscovery) Fields(_ context.Context, _ Scope, resourceType string) ([]FieldCandidate, error) { + if f.calls != nil { + f.calls[resourceType]++ + } + if f.byType != nil { + return append([]FieldCandidate(nil), f.byType[resourceType]...), nil + } + return append([]FieldCandidate(nil), f.fields...), nil +} + +type countingDiscovery struct { + delegate fakeDiscovery + calls map[string]int +} + +func (d *countingDiscovery) Fields(ctx context.Context, scope Scope, resourceType string) ([]FieldCandidate, error) { + d.calls[resourceType]++ + return d.delegate.Fields(ctx, scope, resourceType) +} + +func TestResolveCatalogProjectionBeforeSemanticTyping(t *testing.T) { + bundle := recipe.Bundle{RecipeSchemaVersion: 1, Name: "catalog", TranslationVersion: "1", Outputs: []recipe.Output{{ + Name: "Patient", RootResourceType: "Patient", RowGrain: "patient", + CatalogProjections: []recipe.CatalogProjection{{Name: "common", IncludePaths: []string{"identifier[].value", "gender"}, Kinds: []string{"scalar"}, MaxColumns: 4, ValueMode: recipe.ValueModeFirst}}, + }}} + resolved, err := Resolve(context.Background(), bundle, Scope{Project: "p", DatasetGeneration: "g"}, fakeDiscovery{fields: []FieldCandidate{ + {ResourceType: "Patient", Path: "gender", Kind: "scalar"}, + {ResourceType: "Patient", Path: "identifier[].value", Kind: "scalar"}, + }}) + if err != nil { + t.Fatal(err) + } + if len(resolved.Bundle.Outputs[0].Fields) != 2 || resolved.Bundle.Outputs[0].Fields[0].Name != "gender" { + t.Fatalf("resolved fields = %#v", resolved.Bundle.Outputs[0].Fields) + } + if _, err := semantic.BuildRecipePlan(resolved.Bundle, recipe.RuntimeBindings{Project: "p", DatasetGeneration: "g"}); err != nil { + t.Fatalf("resolved fields did not type-check: %v", err) + } +} + +func TestResolveDiscoveryFreezesPivotAndKeyedColumns(t *testing.T) { + bundle := recipe.Bundle{RecipeSchemaVersion: 1, Name: "catalog", TranslationVersion: "1", Outputs: []recipe.Output{{ + Name: "Patient", RootResourceType: "Patient", RowGrain: "patient", + Pivots: []recipe.Pivot{{Name: "observations", ColumnExpr: recipe.Expression{Select: "root.gender"}, ValueExpr: recipe.Expression{Select: "root.gender"}, Discovery: &recipe.PivotDiscovery{Family: "observation_code_value", MaxColumns: 4}}}, + DynamicColumns: []recipe.DynamicColumn{{Name: "identifiers", Source: recipe.Expression{Select: "root.identifier[]"}, Key: &recipe.Expression{Select: "item.system"}, Value: &recipe.Expression{Select: "item.value"}, MaxColumns: 4}}, + }}} + resolved, err := Resolve(context.Background(), bundle, Scope{Project: "p", DatasetGeneration: "g"}, fakeDiscovery{fields: []FieldCandidate{ + {ResourceType: "Patient", Path: "code", Kind: "codeable_concept", PivotCandidate: true, PivotFamily: "observation_code_value", PivotColumns: []string{"a", "b"}}, + {ResourceType: "Patient", Path: "identifier[].system", Kind: "scalar", DistinctValues: []string{"urn:z", "urn:a"}}, + }}) + if err != nil { + t.Fatal(err) + } + output := resolved.Bundle.Outputs[0] + if len(output.Pivots) != 1 || len(output.Pivots[0].Columns) != 2 || output.Pivots[0].Discovery != nil { + t.Fatalf("pivot was not frozen: %#v", output.Pivots) + } + if len(output.DynamicColumns) != 1 || len(output.DynamicColumns[0].Columns) != 2 || output.DynamicColumns[0].Columns[0] != "urn:a" { + t.Fatalf("dynamic columns were not frozen: %#v", output.DynamicColumns) + } +} + +func TestResolveFillsDiscoveredPivotSelectorsInTraversalScope(t *testing.T) { + bundle := recipe.Bundle{RecipeSchemaVersion: 1, Name: "pivot-selectors", TranslationVersion: "1", Outputs: []recipe.Output{{ + Name: "Patient", RootResourceType: "Patient", RowGrain: "patient", + Traversals: []recipe.Traversal{{Name: "focus_Observation", ToResourceType: "Observation", Alias: "observation", Pivots: []recipe.Pivot{{Name: "values", Discovery: &recipe.PivotDiscovery{Family: "observation_code_value", MaxColumns: 4}}}}}, + }}} + resolved, err := Resolve(context.Background(), bundle, Scope{Project: "p"}, fakeDiscovery{byType: map[string][]FieldCandidate{ + "Observation": {{ResourceType: "Observation", Path: "code", Kind: "codeable_concept", PivotCandidate: true, PivotFamily: "observation_code_value", PivotColumns: []string{"A"}, PivotColumnSelect: "code.coding[].display", PivotValueSelect: "valueString"}}, + }}) + if err != nil { + t.Fatal(err) + } + pivot := resolved.Bundle.Outputs[0].Traversals[0].Pivots[0] + if pivot.Discovery != nil || pivot.ColumnExpr.Select != "observation.code.coding[].display" || pivot.ValueExpr.Select != "observation.valueString" { + t.Fatalf("resolved traversal pivot selectors = %#v", pivot) + } +} + +func TestResolveSharesOneCatalogSnapshotPerResourceType(t *testing.T) { + bundle := recipe.Bundle{RecipeSchemaVersion: 1, Name: "memo", TranslationVersion: "v", Outputs: []recipe.Output{{ + Name: "Patient", RootResourceType: "Patient", RowGrain: "patient", + CatalogProjections: []recipe.CatalogProjection{{Name: "a", IncludePaths: []string{"gender"}, Kinds: []string{"scalar"}, MaxColumns: 4}, {Name: "b", IncludePaths: []string{"id"}, Kinds: []string{"scalar"}, MaxColumns: 4}}, + }}} + d := &countingDiscovery{delegate: fakeDiscovery{byType: map[string][]FieldCandidate{"Patient": {{ResourceType: "Patient", Path: "gender", Kind: "scalar"}, {ResourceType: "Patient", Path: "id", Kind: "scalar"}}}, calls: map[string]int{}}, calls: map[string]int{}} + if _, err := Resolve(context.Background(), bundle, Scope{Project: "p"}, d); err != nil { + t.Fatal(err) + } + if d.calls["Patient"] != 1 { + t.Fatalf("catalog calls for Patient = %d, want one snapshot", d.calls["Patient"]) + } +} + +func TestResolveRequiresDiscoveryForCatalogDeclarations(t *testing.T) { + bundle := recipe.Bundle{RecipeSchemaVersion: 1, Name: "catalog", TranslationVersion: "1", Outputs: []recipe.Output{{ + Name: "Patient", RootResourceType: "Patient", RowGrain: "patient", + CatalogProjections: []recipe.CatalogProjection{{Name: "fields", IncludePaths: []string{"*"}, MaxColumns: 4}}, + }}} + if _, err := Resolve(context.Background(), bundle, Scope{}, nil); err == nil { + t.Fatal("expected missing discovery error") + } +} + +func TestResolveRejectsTruncatedDynamicKeyDiscovery(t *testing.T) { + bundle := recipe.Bundle{RecipeSchemaVersion: 1, Name: "truncated", TranslationVersion: "v", Outputs: []recipe.Output{{ + Name: "Patient", RootResourceType: "Patient", RowGrain: "patient", + DynamicColumns: []recipe.DynamicColumn{{Name: "identifiers", Source: recipe.Expression{Select: "root.identifier[]"}, Key: &recipe.Expression{Select: "item.system"}, Value: &recipe.Expression{Select: "item.value"}, MaxColumns: 8}}, + }}} + _, err := Resolve(context.Background(), bundle, Scope{Project: "p"}, fakeDiscovery{fields: []FieldCandidate{{Path: "identifier[].system", Kind: "scalar", DistinctValues: []string{"a"}, DistinctTruncated: true}}}) + if err == nil || !strings.Contains(err.Error(), "truncated") { + t.Fatalf("expected truncated discovery rejection, got %v", err) + } +} + +func TestResolveRecordsStoredAndScopedSchemaDigests(t *testing.T) { + bundle := recipe.Bundle{RecipeSchemaVersion: 1, Name: "digest", TranslationVersion: "v", Outputs: []recipe.Output{{Name: "Patient", RootResourceType: "Patient", RowGrain: "patient", Fields: []recipe.Field{{Name: "id", Expr: recipe.Expression{Select: "root.id"}}}}}} + first, err := Resolve(context.Background(), bundle, Scope{Project: "p", DatasetGeneration: "g", AuthScopeMode: "restricted", AuthResourcePaths: []string{"/b", "/a"}}, nil) + if err != nil { + t.Fatal(err) + } + second, err := Resolve(context.Background(), bundle, Scope{Project: "p", DatasetGeneration: "g", AuthScopeMode: "restricted", AuthResourcePaths: []string{"/a", "/b"}}, nil) + if err != nil { + t.Fatal(err) + } + if first.StoredRecipeDigest == "" || first.ResolvedSchemaDigest == "" || first.ScopeDigest == "" || first.SourceSnapshot.LegacyNull { + t.Fatalf("incomplete immutable snapshot: %#v", first) + } + if first.StoredRecipeDigest != second.StoredRecipeDigest || first.ResolvedSchemaDigest != second.ResolvedSchemaDigest || first.ScopeDigest != second.ScopeDigest { + t.Fatalf("scope ordering changed resolution identity: first=%#v second=%#v", first, second) + } +} + +func TestDefaultRecipeCatalogDeclarationsRemainSemanticallyCompilable(t *testing.T) { + bundle, err := recipe.DefaultACEDBundle() + if err != nil { + t.Fatal(err) + } + resourceTypes := []string{"DocumentReference", "Specimen", "Patient", "ResearchStudy", "Observation", "ResearchSubject", "Condition", "MedicationAdministration", "Group"} + byType := make(map[string][]FieldCandidate, len(resourceTypes)) + for _, resourceType := range resourceTypes { + byType[resourceType] = []FieldCandidate{ + {ResourceType: resourceType, Path: "id", Kind: "scalar"}, + {ResourceType: resourceType, Path: "identifier[].system", Kind: "scalar", DistinctValues: []string{"system"}}, + {ResourceType: resourceType, Path: "identifier[].value", Kind: "scalar"}, + {ResourceType: resourceType, Path: "extension[].url", Kind: "scalar", DistinctValues: []string{"url"}}, + } + if resourceType == "Observation" { + byType[resourceType] = append(byType[resourceType], + FieldCandidate{ResourceType: resourceType, Path: "component[].code.text", Kind: "scalar", DistinctValues: []string{"component"}}, + FieldCandidate{ResourceType: resourceType, Path: "component[].valueString", Kind: "scalar"}, + FieldCandidate{ResourceType: resourceType, Path: "component[].valueInteger", Kind: "scalar"}, + FieldCandidate{ResourceType: resourceType, Path: "component[].valueBoolean", Kind: "scalar"}, + FieldCandidate{ResourceType: resourceType, Path: "component[].valueDateTime", Kind: "scalar"}, + FieldCandidate{ResourceType: resourceType, Path: "component[].valueQuantity.value", Kind: "scalar"}, + FieldCandidate{ResourceType: resourceType, Path: "code", Kind: "codeable_concept", PivotCandidate: true, PivotFamily: "observation_code_value", PivotColumns: []string{"code"}, PivotColumnSelect: "code.coding[].display", PivotValueSelect: "valueString"}, + FieldCandidate{ResourceType: resourceType, Path: "component[].code", Kind: "codeable_concept", PivotCandidate: true, PivotFamily: "codeable_concept", PivotColumns: []string{"component"}, PivotColumnSelect: "component[].code.coding[].display", PivotValueSelect: "component[].code.coding[].display"}, + ) + } + } + resolved, err := Resolve(context.Background(), bundle, Scope{Project: "p", DatasetGeneration: "g"}, fakeDiscovery{byType: byType}) + if err != nil { + t.Fatal(err) + } + if _, err := semantic.BuildRecipePlan(resolved.Bundle, recipe.RuntimeBindings{Project: "p", DatasetGeneration: "g"}); err != nil { + t.Fatalf("resolved default recipe did not type-check: %v", err) + } + plan, err := semantic.BuildRecipePlan(resolved.Bundle, recipe.RuntimeBindings{Project: "p", DatasetGeneration: "g"}) + if err != nil { + t.Fatal(err) + } + physical, err := semantic.ResolveRecipePlan(plan, "scope", "g") + if err != nil { + t.Fatal(err) + } + if _, err := compiler.CompileResolvedRecipePlan(physical, compiler.DefaultPhysicalOptimizationPolicy()); err != nil { + t.Fatalf("resolved default recipe did not compile physically: %v", err) + } +} diff --git a/internal/dataframe/recipe/strict_json.go b/internal/dataframe/recipe/strict_json.go new file mode 100644 index 0000000..35c74fb --- /dev/null +++ b/internal/dataframe/recipe/strict_json.go @@ -0,0 +1,90 @@ +package recipe + +import ( + "bytes" + "encoding/json" + "fmt" + "strings" +) + +// detectDuplicateKeys walks the JSON token stream before typed decoding. The +// standard library's struct decoder accepts duplicate object keys, which +// would make recipe digests dependent on decoder behavior. +func detectDuplicateKeys(data []byte) error { + dec := json.NewDecoder(bytes.NewReader(data)) + dec.UseNumber() + var walk func(string) error + walk = func(path string) error { + tok, err := dec.Token() + if err != nil { + return validationError("parse_error", path, err.Error()) + } + if delim, ok := tok.(json.Delim); ok { + switch delim { + case '{': + seen := map[string]bool{} + for dec.More() { + keyToken, err := dec.Token() + if err != nil { + return validationError("parse_error", path, err.Error()) + } + key, ok := keyToken.(string) + if !ok { + return validationError("parse_error", path, "object key is not a string") + } + keyPath := path + "." + key + if seen[key] { + return validationError("duplicate_field", keyPath, "duplicate JSON object key") + } + seen[key] = true + if forbiddenStorageKey(key) { + return validationError("forbidden_storage_binding", keyPath, "storage implementation fields are not allowed in recipes") + } + if err := walk(keyPath); err != nil { + return err + } + } + end, err := dec.Token() + if err != nil { + return validationError("parse_error", path, err.Error()) + } + if end != json.Delim('}') { + return validationError("parse_error", path, "invalid object") + } + case '[': + index := 0 + for dec.More() { + if err := walk(fmt.Sprintf("%s[%d]", path, index)); err != nil { + return err + } + index++ + } + end, err := dec.Token() + if err != nil { + return validationError("parse_error", path, err.Error()) + } + if end != json.Delim(']') { + return validationError("parse_error", path, "invalid array") + } + } + } + return nil + } + if err := walk("$"); err != nil { + return err + } + var trailing any + if err := dec.Decode(&trailing); err == nil { + return validationError("parse_error", "$", "multiple JSON values") + } + return nil +} + +func forbiddenStorageKey(key string) bool { + switch strings.ToLower(key) { + case "aql", "sql", "collection", "table", "database", "arangocollection", "clickhousetable": + return true + default: + return false + } +} diff --git a/internal/dataframe/recipe/types_test.go b/internal/dataframe/recipe/types_test.go new file mode 100644 index 0000000..0041e5b --- /dev/null +++ b/internal/dataframe/recipe/types_test.go @@ -0,0 +1,130 @@ +package recipe + +import ( + "bytes" + "encoding/json" + "strings" + "testing" +) + +const validDocument = `{ + "recipeSchemaVersion": 1, + "name": "example", + "translationVersion": "1", + "outputs": [{ + "name": "members", + "rootResourceType": "Group", + "rowGrain": "group_member", + "expand": {"from": {"select": "member[]"}, "as": "member"}, + "identity": {"name": "id", "expr": {"call": "uuid5", "args": [ + {"literal": "example"}, + {"literal": "group"}, + {"select": "member.id"} + ]}}, + "fields": [ + {"name": "group_id", "expr": {"select": "root.id"}}, + {"name": "member_id", "expr": {"call": "reference_id", "args": [{"select": "member.reference"}]}} + ] + }] +}` + +func TestParseCanonicalDigestIgnoresFormatting(t *testing.T) { + b, err := Parse([]byte(validDocument)) + if err != nil { + t.Fatal(err) + } + canonical, err := b.CanonicalJSON() + if err != nil { + t.Fatal(err) + } + if !json.Valid(canonical) || bytes.ContainsAny(canonical, " \n\t") { + t.Fatalf("canonical JSON is not compact: %s", canonical) + } + other := strings.ReplaceAll(strings.ReplaceAll(validDocument, " ", ""), "\n", "") + b2, err := Parse([]byte(other)) + if err != nil { + t.Fatal(err) + } + d1, err := b.Digest() + if err != nil { + t.Fatal(err) + } + d2, err := b2.Digest() + if err != nil { + t.Fatal(err) + } + if d1 != d2 { + t.Fatalf("formatting changed digest: %s != %s", d1, d2) + } +} + +func TestParseRejectsDuplicateUnknownAndStorageFields(t *testing.T) { + tests := []struct { + name string + json string + code string + }{ + {"duplicate", `{"recipeSchemaVersion":1,"recipeSchemaVersion":1,"name":"x","translationVersion":"1","outputs":[]}`, "duplicate_field"}, + {"unknown", `{"recipeSchemaVersion":1,"name":"x","translationVersion":"1","outputs":[],"unexpected":true}`, "parse_error"}, + {"storage", `{"recipeSchemaVersion":1,"name":"x","translationVersion":"1","outputs":[],"sql":"select 1"}`, "forbidden_storage_binding"}, + } + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + _, err := Parse([]byte(tc.json)) + if err == nil || !strings.HasPrefix(err.Error(), tc.code+" ") { + t.Fatalf("expected %s, got %v", tc.code, err) + } + }) + } +} + +func TestValidationRejectsVersionNamesAndArity(t *testing.T) { + bad := []string{ + `{"recipeSchemaVersion":2,"name":"x","translationVersion":"1","outputs":[]}`, + `{"recipeSchemaVersion":1,"name":"x","translationVersion":"1","outputs":[{"name":"x","rootResourceType":"R","rowGrain":"r","fields":[{"name":"a","expr":{"call":"join","args":[{"literal":"one"}]}}]}]}`, + `{"recipeSchemaVersion":1,"name":"x","translationVersion":"1","outputs":[{"name":"x","rootResourceType":"R","rowGrain":"r","fields":[{"name":"a","expr":{"call":"does_not_exist"}}]}]}`, + } + for _, input := range bad { + if _, err := Parse([]byte(input)); err == nil { + t.Fatalf("expected validation failure for %s", input) + } + } +} + +func TestExplainContainsNoPhysicalDetails(t *testing.T) { + b, err := Parse([]byte(validDocument)) + if err != nil { + t.Fatal(err) + } + explanation, err := b.Explain() + if err != nil { + t.Fatal(err) + } + if explanation.Digest == "" || len(explanation.Outputs) != 1 || !explanation.Outputs[0].Expanded { + t.Fatalf("unexpected explanation: %#v", explanation) + } + raw, _ := json.Marshal(explanation) + if bytes.Contains(raw, []byte("collection")) || bytes.Contains(raw, []byte("table")) || bytes.Contains(raw, []byte("aql")) { + t.Fatalf("explanation contains physical details: %s", raw) + } +} + +func TestRuntimeBindingsAreNotDigestContent(t *testing.T) { + var b RuntimeBindings + if b.Project != "" || b.DatasetGeneration != "" { + t.Fatal("zero runtime bindings should be empty") + } +} + +func TestDefaultACEDBundleIsRecipeData(t *testing.T) { + bundle, err := DefaultACEDBundle() + if err != nil { + t.Fatal(err) + } + if len(bundle.Outputs) != 5 || bundle.Name != "aced-meta-default" { + t.Fatalf("unexpected default bundle: %#v", bundle) + } + if _, err := bundle.Digest(); err != nil { + t.Fatal(err) + } +} diff --git a/internal/dataframe/recipe/validation_rich.go b/internal/dataframe/recipe/validation_rich.go new file mode 100644 index 0000000..e748c38 --- /dev/null +++ b/internal/dataframe/recipe/validation_rich.go @@ -0,0 +1,371 @@ +package recipe + +import ( + "fmt" + "regexp" + "strings" + + "github.com/calypr/loom/fhirstructs" +) + +const ( + maxRepresentativeSliceLimit = 1000 + maxPivotColumns = 256 + maxCatalogProjectionColumns = 512 +) + +var recipeNamePattern = regexp.MustCompile(`^[A-Za-z_][A-Za-z0-9_]*$`) + +func validateRecipeName(value, path string) error { + if strings.TrimSpace(value) == "" { + return validationError("required", path, "name is required") + } + if !recipeNamePattern.MatchString(value) { + return validationError("invalid_name", path, "must contain only letters, digits, and underscores and may not start with a digit") + } + return nil +} + +func (v FilterValue) Validate() error { + if !v.Kind.Valid() { + return fmt.Errorf("unknown value kind %q", v.Kind) + } + populated := 0 + for _, present := range []bool{v.String != nil, v.Code != nil, v.Boolean != nil, v.Integer != nil, v.Decimal != nil, v.Date != nil, v.DateTime != nil} { + if present { + populated++ + } + } + if populated != 1 { + return fmt.Errorf("exactly one typed value member is required, got %d", populated) + } + switch v.Kind { + case FilterString: + if v.String == nil { + return fmt.Errorf("STRING requires string") + } + case FilterCode: + if v.Code == nil || strings.TrimSpace(v.Code.Code) == "" { + return fmt.Errorf("CODE requires a non-empty code") + } + case FilterBoolean: + if v.Boolean == nil { + return fmt.Errorf("BOOLEAN requires boolean") + } + case FilterInteger: + if v.Integer == nil { + return fmt.Errorf("INTEGER requires integer") + } + case FilterDecimal: + if v.Decimal == nil { + return fmt.Errorf("DECIMAL requires decimal") + } + case FilterDate: + if v.Date == nil { + return fmt.Errorf("DATE requires date") + } + if err := fhirstructs.ValidateFhirDate(*v.Date); err != nil { + return fmt.Errorf("DATE must use a valid FHIR date: %w", err) + } + case FilterDateTime: + if v.DateTime == nil { + return fmt.Errorf("DATE_TIME requires dateTime") + } + if err := fhirstructs.ValidateFhirDateTime(*v.DateTime); err != nil { + return fmt.Errorf("DATE_TIME must use a valid FHIR date-time: %w", err) + } + } + return nil +} + +func (f Filter) Validate() error { return f.validateAt("$") } + +func (f Filter) validateAt(path string) error { + if strings.TrimSpace(f.Select) == "" { + return validationError("required", path+".select", "select is required") + } + if !f.Operator.Valid() { + return validationError("invalid_filter_operator", path+".operator", fmt.Sprintf("unsupported operator %q", f.Operator)) + } + if f.Quantifier != "" && !f.Quantifier.Valid() { + return validationError("invalid_filter_quantifier", path+".quantifier", fmt.Sprintf("unsupported quantifier %q", f.Quantifier)) + } + if f.Operator == FilterExists || f.Operator == FilterMissing { + if len(f.Values) != 0 { + return validationError("invalid_filter_values", path+".values", "EXISTS and MISSING do not accept values") + } + } else if f.Operator == FilterIn { + if len(f.Values) == 0 { + return validationError("invalid_filter_values", path+".values", "IN requires at least one value") + } + } else if len(f.Values) != 1 { + return validationError("invalid_filter_values", path+".values", "operator requires exactly one value") + } + var kind FilterValueKind + for i, value := range f.Values { + if err := value.Validate(); err != nil { + return validationError("invalid_filter_value", fmt.Sprintf("%s.values[%d]", path, i), err.Error()) + } + if i == 0 { + kind = value.Kind + } else if value.Kind != kind { + return validationError("invalid_filter_value", fmt.Sprintf("%s.values[%d].kind", path, i), "all values must use the same kind") + } + if !filterOperatorSupportsKind(f.Operator, value.Kind) { + return validationError("invalid_filter_value", fmt.Sprintf("%s.values[%d].kind", path, i), fmt.Sprintf("operator %s is incompatible with %s", f.Operator, value.Kind)) + } + } + return nil +} + +func filterOperatorSupportsKind(op FilterOperator, kind FilterValueKind) bool { + switch op { + case FilterExists, FilterMissing, FilterEquals, FilterNotEquals, FilterIn: + return kind.Valid() + case FilterContains: + return kind == FilterString + case FilterGreaterThan, FilterGreaterEq, FilterLessThan, FilterLessEq: + return kind == FilterInteger || kind == FilterDecimal || kind == FilterDate || kind == FilterDateTime + default: + return false + } +} + +func (p Pivot) validateAt(path string, budget *int) error { + if err := validateRecipeName(p.Name, path+".name"); err != nil { + return err + } + if p.Discovery != nil && len(p.Columns) != 0 { + return validationError("ambiguous_columns", path, "pivot may use static columns or discovery, not both") + } + if p.Discovery == nil && (len(p.Columns) == 0 || len(p.Columns) > maxPivotColumns) { + return validationError("invalid_columns", path+".columns", fmt.Sprintf("must contain 1..%d columns", maxPivotColumns)) + } + if p.Discovery != nil { + if strings.TrimSpace(p.Discovery.Family) == "" && strings.TrimSpace(p.Discovery.Path) == "" { + return validationError("required", path+".discovery", "family or path is required") + } + if p.Discovery.MaxColumns <= 0 || p.Discovery.MaxColumns > maxPivotColumns { + return validationError("invalid_max_columns", path+".discovery.maxColumns", fmt.Sprintf("must be 1..%d", maxPivotColumns)) + } + } + seen := map[string]bool{} + for i, column := range p.Columns { + if strings.TrimSpace(column) == "" { + return validationError("required", fmt.Sprintf("%s.columns[%d]", path, i), "pivot column is required") + } + if seen[column] { + return validationError("duplicate_name", fmt.Sprintf("%s.columns[%d]", path, i), "duplicate pivot column") + } + seen[column] = true + } + // A catalog-backed pivot may omit selectors: the scoped resolver fills them + // from the catalog's validated pivot metadata before semantic compilation. + if p.Discovery != nil && p.ColumnExpr.Select == "" && p.ValueExpr.Select == "" && p.ColumnExpr.Call == "" && p.ValueExpr.Call == "" { + return nil + } + if err := validateExpressionBudget(p.ColumnExpr, path+".columnExpr", budget); err != nil { + return err + } + if err := validateExpressionBudget(p.ValueExpr, path+".valueExpr", budget); err != nil { + return err + } + if !p.ItemSource.zero() { + if err := validateExpressionBudget(p.ItemSource, path+".itemSource", budget); err != nil { + return err + } + } + for index, fallback := range p.ValueFallbacks { + if err := validateExpressionBudget(fallback, fmt.Sprintf("%s.valueFallbacks[%d]", path, index), budget); err != nil { + return err + } + } + return nil +} + +func (p CatalogProjection) validateAt(path string) error { + if err := validateRecipeName(p.Name, path+".name"); err != nil { + return err + } + if len(p.IncludePaths) == 0 { + return validationError("required", path+".includePaths", "at least one path pattern is required") + } + if p.MaxColumns <= 0 || p.MaxColumns > maxCatalogProjectionColumns { + return validationError("invalid_max_columns", path+".maxColumns", fmt.Sprintf("must be 1..%d", maxCatalogProjectionColumns)) + } + if p.Naming != "" && p.Naming != ColumnNamingPath && p.Naming != ColumnNamingPathSuffix { + return validationError("invalid_naming", path+".naming", "must be PATH or PATH_SUFFIX") + } + if !p.ValueMode.Valid() { + return validationError("invalid_value_mode", path+".valueMode", fmt.Sprintf("unsupported value mode %q", p.ValueMode)) + } + seen := map[string]struct{}{} + for i, kind := range p.Kinds { + kind = strings.TrimSpace(kind) + if kind == "" { + return validationError("required", fmt.Sprintf("%s.kinds[%d]", path, i), "kind is required") + } + if _, ok := seen[kind]; ok { + return validationError("duplicate_name", fmt.Sprintf("%s.kinds[%d]", path, i), "duplicate kind") + } + seen[kind] = struct{}{} + } + return nil +} + +// Validate checks a standalone catalog projection before it is attached to an +// output or traversal. Bundle validation uses the path-aware variant so +// callers still receive the precise location of malformed declarations. +func (p CatalogProjection) Validate() error { + return p.validateAt("catalogProjection") +} + +func (a Aggregate) validateAt(path string, budget *int) error { + if err := validateRecipeName(a.Name, path+".name"); err != nil { + return err + } + if !a.Operation.Valid() { + return validationError("invalid_aggregate_operation", path+".operation", fmt.Sprintf("unsupported operation %q", a.Operation)) + } + if !a.ValueMode.Valid() { + return validationError("invalid_value_mode", path+".valueMode", fmt.Sprintf("unsupported value mode %q", a.ValueMode)) + } + requiresExpr := a.Operation == AggregateCountDistinct || a.Operation == AggregateDistinctValues || a.Operation == AggregateMin || a.Operation == AggregateMax + if requiresExpr && a.Expr == nil { + return validationError("required", path+".expr", "operation requires expr") + } + if !requiresExpr && a.Expr != nil { + return validationError("invalid_expr", path+".expr", "COUNT and EXISTS do not accept expr") + } + if a.Expr != nil { + if err := validateExpressionBudget(*a.Expr, path+".expr", budget); err != nil { + return err + } + } + if a.Where != nil { + if err := a.Where.validateAt(path + ".where"); err != nil { + return err + } + } + return nil +} + +func (s RepresentativeSlice) validateAt(path string, budget *int) error { + if err := validateRecipeName(s.Name, path+".name"); err != nil { + return err + } + if s.Limit <= 0 || s.Limit > maxRepresentativeSliceLimit { + return validationError("invalid_limit", path+".limit", fmt.Sprintf("must be between 1 and %d", maxRepresentativeSliceLimit)) + } + if len(s.Fields) == 0 { + return validationError("required", path+".fields", "at least one field is required") + } + if s.Where != nil { + if err := s.Where.validateAt(path + ".where"); err != nil { + return err + } + } + return validateFieldsBudget(s.Fields, path+".fields", budget) +} + +func validateExpressionBudget(e Expression, path string, budget *int) error { + if err := validateExpression(e, path); err != nil { + return err + } + if budget == nil { + return nil + } + *budget += expressionNodeCount(e) + if *budget > maxExpressionNodes { + return validationError("max_nodes", path, "expression node count exceeds limit") + } + return nil +} + +func expressionNodeCount(e Expression) int { + count := 1 + for _, arg := range e.Args { + count += expressionNodeCount(arg) + } + return count +} + +func validateFieldsBudget(fields []Field, path string, budget *int) error { + seen := map[string]bool{} + for i, f := range fields { + p := fmt.Sprintf("%s[%d]", path, i) + if err := validateRecipeName(f.Name, p+".name"); err != nil { + return err + } + if seen[f.Name] { + return validationError("duplicate_name", p+".name", "duplicate field name") + } + seen[f.Name] = true + if err := validateExpressionBudget(f.Expr, p+".expr", budget); err != nil { + return err + } + for j, fallback := range f.Fallbacks { + fp := fmt.Sprintf("%s.fallbacks[%d]", p, j) + if fallback.Select == "" || fallback.Call != "" || fallback.Literal != nil { + return validationError("invalid_fallback", fp, "fallback must be a selector expression") + } + if err := validateExpressionBudget(fallback, fp, budget); err != nil { + return err + } + } + if !f.ValueMode.Valid() { + return validationError("invalid_value_mode", p+".valueMode", fmt.Sprintf("unsupported value mode %q", f.ValueMode)) + } + } + return nil +} + +func validateNodeShape(fields []Field, filters []Filter, pivots []Pivot, aggregates []Aggregate, slices []RepresentativeSlice, path string, budget *int) error { + if err := validateFieldsBudget(fields, path+".fields", budget); err != nil { + return err + } + names := make(map[string]bool, len(fields)+len(pivots)+len(aggregates)+len(slices)) + for _, f := range fields { + names[f.Name] = true + } + check := func(name, p string) error { + if names[name] { + return validationError("duplicate_name", p+".name", "name conflicts with another node output") + } + names[name] = true + return nil + } + for i, pivot := range pivots { + p := fmt.Sprintf("%s.pivots[%d]", path, i) + if err := check(pivot.Name, p); err != nil { + return err + } + if err := pivot.validateAt(p, budget); err != nil { + return err + } + } + for i, aggregate := range aggregates { + p := fmt.Sprintf("%s.aggregates[%d]", path, i) + if err := check(aggregate.Name, p); err != nil { + return err + } + if err := aggregate.validateAt(p, budget); err != nil { + return err + } + } + for i, slice := range slices { + p := fmt.Sprintf("%s.slices[%d]", path, i) + if err := check(slice.Name, p); err != nil { + return err + } + if err := slice.validateAt(p, budget); err != nil { + return err + } + } + for i, filter := range filters { + if err := filter.validateAt(fmt.Sprintf("%s.filters[%d]", path, i)); err != nil { + return err + } + } + return nil +} diff --git a/internal/dataframe/relationship_match_arango_integration_test.go b/internal/dataframe/relationship_match_arango_integration_test.go deleted file mode 100644 index f6acb5d..0000000 --- a/internal/dataframe/relationship_match_arango_integration_test.go +++ /dev/null @@ -1,51 +0,0 @@ -package dataframe - -import ( - "context" - "os" - "testing" - - arangostore "github.com/calypr/loom/internal/store/arango" -) - -// TestRequiredTraversalMatchExplainsAndRunsAgainstArango verifies that the -// root-correlated bounded subquery is accepted by the real Arango parser and -// executes against the locally loaded META fixture. It remains opt-in because -// it reads a developer's local database. -func TestRequiredTraversalMatchExplainsAndRunsAgainstArango(t *testing.T) { - if os.Getenv("LOOM_COMPILER_ARANGO_INTEGRATION") == "" { - t.Skip("set LOOM_COMPILER_ARANGO_INTEGRATION=1 to run compiler/Arango integration") - } - url, database, project := compilerArangoTarget() - compiled, err := CompileRequest(Builder{ - Project: project, - RootResourceType: "Specimen", - Traversals: []TraversalStep{{ - Label: "subject_Specimen", - ToResourceType: "DocumentReference", - Alias: "file", - MatchMode: TraversalMatchRequired, - Filters: []TypedFilter{{ - FieldRef: "DocumentReference.file_name", - Selector: "content[].attachment.title", - FieldKind: FilterString, - Repeated: true, - Quantifier: QuantifierAny, - Operator: FilterExists, - }}, - }}, - }, 5) - if err != nil { - t.Fatalf("CompileRequest() error = %v", err) - } - if _, err := ExplainCompiledQuery(context.Background(), arangostore.ConnectionOptions{URL: url, Database: database}, compiled); err != nil { - t.Fatalf("ExplainCompiledQuery() error = %v\nAQL:\n%s", err, compiled.Query) - } - rows, err := executeCompiledRows(context.Background(), arangostore.ConnectionOptions{URL: url, Database: database}, compiled) - if err != nil { - t.Fatalf("execute required relationship match: %v\nAQL:\n%s", err, compiled.Query) - } - if len(rows) == 0 { - t.Fatalf("required relationship match returned no rows for project %q", project) - } -} diff --git a/internal/dataframe/runtime/active_generation_test.go b/internal/dataframe/runtime/active_generation_test.go index d9ca712..2cae6d8 100644 --- a/internal/dataframe/runtime/active_generation_test.go +++ b/internal/dataframe/runtime/active_generation_test.go @@ -7,7 +7,7 @@ import ( "testing" "github.com/calypr/loom/internal/authscope" - "github.com/calypr/loom/internal/catalog" + "github.com/calypr/loom/internal/dataframe/recipe" "github.com/calypr/loom/internal/dataset" ) @@ -25,20 +25,9 @@ func (r *dataframeActiveManifestResolver) ResolveActiveManifest(_ context.Contex return r.manifest.Clone(), nil } -type dataframeActiveResourceAccess struct{} - -func (dataframeActiveResourceAccess) GetAllowedResources(context.Context, string, string, string) ([]string, error) { - return []string{"/programs/example/projects/allowed"}, nil -} - func dataframeReadyManifest(t *testing.T, project, generation string) dataset.Manifest { t.Helper() - schema, err := dataset.NewSchemaIdentitySnapshot( - "urn:loom:dataframe-active-test", - "", - strings.Repeat("a", 64), - []string{"Patient", "Specimen"}, - ) + schema, err := dataset.NewSchemaIdentitySnapshot("urn:loom:dataframe-active-test", "", strings.Repeat("a", 64), []string{"Patient"}) if err != nil { t.Fatalf("NewSchemaIdentitySnapshot() error = %v", err) } @@ -50,11 +39,7 @@ func dataframeReadyManifest(t *testing.T, project, generation string) dataset.Ma if err != nil { t.Fatalf("NewManifest() error = %v", err) } - for _, state := range []dataset.ManifestState{ - dataset.ManifestStateLoading, - dataset.ManifestStateAnalyzing, - dataset.ManifestStateReady, - } { + for _, state := range []dataset.ManifestState{dataset.ManifestStateLoading, dataset.ManifestStateAnalyzing, dataset.ManifestStateReady} { manifest, err = manifest.Transition(state) if err != nil { t.Fatalf("Transition(%s) error = %v", state, err) @@ -63,42 +48,17 @@ func dataframeReadyManifest(t *testing.T, project, generation string) dataset.Ma return manifest } -func TestServiceActiveManifestPinsScopeCatalogAndCompilerGeneration(t *testing.T) { - const project = "P1" - const generation = "generation-a" - active := &dataframeActiveManifestResolver{manifest: dataframeReadyManifest(t, project, generation)} - var existingCalls []catalog.AuthResourcePathOptions - var fieldCalls []catalog.PopulatedFieldOptions - var referenceCalls []catalog.PopulatedReferenceOptions +func patientRecipe() recipe.Bundle { + return recipe.Bundle{RecipeSchemaVersion: recipe.CurrentSchemaVersion, Name: "test", TranslationVersion: "test", Outputs: []recipe.Output{{Name: "patients", RootResourceType: "Patient", RowGrain: "patient", Fields: []recipe.Field{{Name: "gender", Expr: recipe.Expression{Select: "gender"}}}}}} +} - scopeResolver := authscope.NewScopeResolver(authscope.ScopeResolverConfig{ - ResourceAccess: dataframeActiveResourceAccess{}, - ListExistingAuthResourcePaths: func(_ context.Context, options catalog.AuthResourcePathOptions) ([]string, error) { - existingCalls = append(existingCalls, options) - return []string{"example-allowed"}, nil - }, - }) +func TestServiceActiveManifestPinsCompilerGeneration(t *testing.T) { + const project, generation = "P1", "generation-a" + active := &dataframeActiveManifestResolver{manifest: dataframeReadyManifest(t, project, generation)} + // The runtime service accepts a pre-resolved unrestricted scope in this + // recipe test; active-generation selection is the behavior under test. svc := NewService(ServiceConfig{ ActiveManifestResolver: active, - ScopeResolver: scopeResolver, - DiscoverFields: func(_ context.Context, options catalog.PopulatedFieldOptions) ([]catalog.PopulatedField, error) { - fieldCalls = append(fieldCalls, options) - if options.PivotOnly { - return []catalog.PopulatedField{}, nil - } - switch options.ResourceType { - case "Patient": - return []catalog.PopulatedField{{Project: project, ResourceType: "Patient", Path: "gender", Kind: "scalar"}}, nil - case "Specimen": - return []catalog.PopulatedField{{Project: project, ResourceType: "Specimen", Path: "id", Kind: "scalar"}}, nil - default: - return []catalog.PopulatedField{}, nil - } - }, - DiscoverReferences: func(_ context.Context, options catalog.PopulatedReferenceOptions) ([]catalog.PopulatedReference, error) { - referenceCalls = append(referenceCalls, options) - return []catalog.PopulatedReference{{FromType: "Patient", Label: "subject_Patient", ToType: "Specimen", EdgeCount: 1}}, nil - }, ExecuteRows: func(_ context.Context, _ ExecuteQueryOptions, query string, binds map[string]any, visit func(map[string]any) error) error { if got := binds[datasetGenerationBindKey]; got != generation { t.Fatalf("compiled dataset generation bind = %#v, want %q", got, generation) @@ -109,113 +69,19 @@ func TestServiceActiveManifestPinsScopeCatalogAndCompilerGeneration(t *testing.T return visit(map[string]any{"_key": "patient-1", "gender": "female"}) }, }) - - ctx := authscope.ContextWithPrincipal(context.Background(), &authscope.Principal{ - AuthorizationHeader: "Bearer header.payload.signature", - }) - result, err := svc.Run(ctx, RunRequest{Builder: Builder{ - Project: project, - RootResourceType: "Patient", - Fields: []FieldSelect{{Name: "gender", Select: "gender"}}, - Traversals: []TraversalStep{{ - Label: "subject_Patient", - ToResourceType: "Specimen", - Alias: "specimen", - Fields: []FieldSelect{{Name: "id", Select: "id"}}, - }}, - }, Limit: 1}) + result, err := svc.Run(context.Background(), RunRequest{Recipe: patientRecipe(), Bindings: recipe.RuntimeBindings{Project: project, AuthScopeMode: authscope.ReadScopeUnrestricted}, Limit: 1}) if err != nil { t.Fatalf("Run() error = %v", err) } - if result.RowCount != 1 { - t.Fatalf("row count = %d, want 1", result.RowCount) - } - if len(active.projects) != 1 || active.projects[0] != project { - t.Fatalf("active resolver projects = %#v, want %q", active.projects, project) - } - if len(existingCalls) != 1 || existingCalls[0].Project != project || existingCalls[0].DatasetGeneration != generation { - t.Fatalf("scope existing-path calls = %#v, want %s/%s", existingCalls, project, generation) - } - for index, options := range fieldCalls { - if options.Project != project || options.DatasetGeneration != generation { - t.Fatalf("field catalog call %d = %+v, want %s/%s", index, options, project, generation) - } - } - for index, options := range referenceCalls { - if options.Project != project || options.DatasetGeneration != generation { - t.Fatalf("reference catalog call %d = %+v, want %s/%s", index, options, project, generation) - } - } -} - -func TestServiceActiveManifestKeepsRestrictedEmptyScopeWithinGeneration(t *testing.T) { - const project = "P1" - const generation = "generation-a" - scopeResolver := authscope.NewScopeResolver(authscope.ScopeResolverConfig{ - ResourceAccess: dataframeActiveResourceAccess{}, - ListExistingAuthResourcePaths: func(_ context.Context, options catalog.AuthResourcePathOptions) ([]string, error) { - if options.DatasetGeneration != generation { - t.Fatalf("scope generation = %q, want %q", options.DatasetGeneration, generation) - } - return []string{"example-unrelated"}, nil - }, - }) - assertRestricted := func(options catalog.PopulatedFieldOptions) { - if options.DatasetGeneration != generation { - t.Fatalf("field generation = %q, want %q", options.DatasetGeneration, generation) - } - if options.AuthResourcePathsUnrestricted == nil || *options.AuthResourcePathsUnrestricted || len(options.AuthResourcePaths) != 0 { - t.Fatalf("field restricted-empty options = %+v", options) - } - } - svc := NewService(ServiceConfig{ - ActiveManifestResolver: &dataframeActiveManifestResolver{manifest: dataframeReadyManifest(t, project, generation)}, - ScopeResolver: scopeResolver, - DiscoverFields: func(_ context.Context, options catalog.PopulatedFieldOptions) ([]catalog.PopulatedField, error) { - assertRestricted(options) - return []catalog.PopulatedField{}, nil - }, - DiscoverReferences: func(_ context.Context, options catalog.PopulatedReferenceOptions) ([]catalog.PopulatedReference, error) { - if options.DatasetGeneration != generation || options.AuthResourcePathsUnrestricted == nil || *options.AuthResourcePathsUnrestricted || len(options.AuthResourcePaths) != 0 { - t.Fatalf("reference restricted-empty options = %+v", options) - } - return []catalog.PopulatedReference{}, nil - }, - ExecuteRows: func(_ context.Context, _ ExecuteQueryOptions, _ string, binds map[string]any, _ func(map[string]any) error) error { - if got := binds[datasetGenerationBindKey]; got != generation { - t.Fatalf("compiled generation bind = %#v, want %q", got, generation) - } - if got, ok := binds["auth_resource_paths_unrestricted"].(bool); !ok || got { - t.Fatalf("compiled unrestricted bind = %#v, want false", binds["auth_resource_paths_unrestricted"]) - } - return nil - }, - }) - ctx := authscope.ContextWithPrincipal(context.Background(), &authscope.Principal{AuthorizationHeader: "Bearer header.payload.signature"}) - if _, err := svc.Run(ctx, RunRequest{Builder: Builder{Project: project, RootResourceType: "Patient"}}); err != nil { - t.Fatalf("Run() error = %v", err) + if result.RowCount != 1 || len(active.projects) != 1 || active.projects[0] != project { + t.Fatalf("result=%#v active projects=%#v", result, active.projects) } } -func TestServiceRejectsBuilderGenerationThatConflictsWithActiveManifest(t *testing.T) { - catalogCalled := false - svc := NewService(ServiceConfig{ - ActiveManifestResolver: &dataframeActiveManifestResolver{manifest: dataframeReadyManifest(t, "P1", "generation-a")}, - DiscoverFields: func(context.Context, catalog.PopulatedFieldOptions) ([]catalog.PopulatedField, error) { - catalogCalled = true - return nil, nil - }, - }) - - _, err := svc.Run(context.Background(), RunRequest{Builder: Builder{ - Project: "P1", - DatasetGeneration: "generation-b", - RootResourceType: "Patient", - }}) +func TestServiceRejectsRecipeGenerationThatConflictsWithActiveManifest(t *testing.T) { + svc := NewService(ServiceConfig{ActiveManifestResolver: &dataframeActiveManifestResolver{manifest: dataframeReadyManifest(t, "P1", "generation-a")}}) + _, err := svc.Run(context.Background(), RunRequest{Recipe: patientRecipe(), Bindings: recipe.RuntimeBindings{Project: "P1", DatasetGeneration: "generation-b"}}) if !errors.Is(err, ErrActiveGenerationConflict) { t.Fatalf("Run() error = %v, want ErrActiveGenerationConflict", err) } - if catalogCalled { - t.Fatal("catalog work occurred after active-generation conflict") - } } diff --git a/internal/dataframe/runtime/auth_scope_test.go b/internal/dataframe/runtime/auth_scope_test.go index 971f0aa..a7bb8fd 100644 --- a/internal/dataframe/runtime/auth_scope_test.go +++ b/internal/dataframe/runtime/auth_scope_test.go @@ -7,6 +7,8 @@ import ( "github.com/calypr/loom/internal/authscope" "github.com/calypr/loom/internal/catalog" + "github.com/calypr/loom/internal/dataframe/compiler" + "github.com/calypr/loom/internal/dataframe/recipe" ) type restrictedEmptyResourceAccess struct{} @@ -25,18 +27,8 @@ func restrictedEmptyScopeResolver() *authscope.ScopeResolver { } func TestServiceRestrictedEmptyScopeStaysRestrictedInCatalogAndAQL(t *testing.T) { - catalogCalls := 0 svc := NewService(ServiceConfig{ ScopeResolver: restrictedEmptyScopeResolver(), - DiscoverReferences: func(_ context.Context, opts catalog.PopulatedReferenceOptions) ([]catalog.PopulatedReference, error) { - assertRestrictedEmptyReferenceOptions(t, opts) - return []catalog.PopulatedReference{}, nil - }, - DiscoverFields: func(_ context.Context, opts catalog.PopulatedFieldOptions) ([]catalog.PopulatedField, error) { - catalogCalls++ - assertRestrictedEmptyFieldOptions(t, opts) - return []catalog.PopulatedField{}, nil - }, ExecuteRows: func(_ context.Context, _ ExecuteQueryOptions, query string, bindVars map[string]any, _ func(map[string]any) error) error { if got, ok := bindVars["auth_resource_paths_unrestricted"].(bool); !ok || got { t.Fatalf("dataframe unrestricted bind = %#v, want false", bindVars["auth_resource_paths_unrestricted"]) @@ -52,27 +44,21 @@ func TestServiceRestrictedEmptyScopeStaysRestrictedInCatalogAndAQL(t *testing.T) ctx := authscope.ContextWithPrincipal(context.Background(), &authscope.Principal{ AuthorizationHeader: "Bearer header.payload.signature", }) - result, err := svc.Run(ctx, RunRequest{Builder: Builder{ - Project: "P1", - RootResourceType: "Patient", - }}) + result, err := svc.Run(ctx, RunRequest{Recipe: recipe.Bundle{RecipeSchemaVersion: recipe.CurrentSchemaVersion, Name: "scope", TranslationVersion: "test", Outputs: []recipe.Output{{Name: "patients", RootResourceType: "Patient", RowGrain: "patient"}}}, Bindings: recipe.RuntimeBindings{Project: "P1"}}) if err != nil { t.Fatalf("Run() error = %v", err) } if result.RowCount != 0 { t.Fatalf("result row count = %d, want no rows from the scoped executor", result.RowCount) } - if catalogCalls == 0 { - t.Fatal("expected scope-aware catalog validation calls") - } } func TestGenericPhysicalPlanRestrictedEmptyScopeBindsFalse(t *testing.T) { - plan, err := BuildGenericPhysicalPlan(SemanticPlan{ + plan, err := compiler.BuildGenericPhysicalPlan(compiler.SemanticPlan{ Version: 1, Project: "P1", AuthScopeMode: authscope.ReadScopeRestricted, - Root: SemanticNode{Alias: "root", ResourceType: "Patient"}, + Root: compiler.SemanticNode{Alias: "root", ResourceType: "Patient"}, }) if err != nil { t.Fatal(err) @@ -80,7 +66,7 @@ func TestGenericPhysicalPlanRestrictedEmptyScopeBindsFalse(t *testing.T) { if got, ok := plan.BindVars["auth_resource_paths_unrestricted"].(bool); !ok || got { t.Fatalf("physical unrestricted bind = %#v, want false", plan.BindVars["auth_resource_paths_unrestricted"]) } - rendered, err := RenderPhysicalPlan(plan) + rendered, err := compiler.RenderPhysicalPlan(plan) if err != nil { t.Fatal(err) } @@ -88,23 +74,3 @@ func TestGenericPhysicalPlanRestrictedEmptyScopeBindsFalse(t *testing.T) { t.Fatalf("rendered physical unrestricted bind = %#v, want false", rendered.BindVars["auth_resource_paths_unrestricted"]) } } - -func assertRestrictedEmptyFieldOptions(t *testing.T, opts catalog.PopulatedFieldOptions) { - t.Helper() - if opts.AuthResourcePathsUnrestricted == nil || *opts.AuthResourcePathsUnrestricted { - t.Fatalf("field catalog scope mode = %#v, want explicit false", opts.AuthResourcePathsUnrestricted) - } - if len(opts.AuthResourcePaths) != 0 { - t.Fatalf("field catalog paths = %#v, want empty", opts.AuthResourcePaths) - } -} - -func assertRestrictedEmptyReferenceOptions(t *testing.T, opts catalog.PopulatedReferenceOptions) { - t.Helper() - if opts.AuthResourcePathsUnrestricted == nil || *opts.AuthResourcePathsUnrestricted { - t.Fatalf("reference catalog scope mode = %#v, want explicit false", opts.AuthResourcePathsUnrestricted) - } - if len(opts.AuthResourcePaths) != 0 { - t.Fatalf("reference catalog paths = %#v, want empty", opts.AuthResourcePaths) - } -} diff --git a/internal/dataframe/runtime/catalog_validation.go b/internal/dataframe/runtime/catalog_validation.go deleted file mode 100644 index 9df8d9e..0000000 --- a/internal/dataframe/runtime/catalog_validation.go +++ /dev/null @@ -1,316 +0,0 @@ -package runtime - -import ( - "context" - "fmt" - "strings" - - "github.com/calypr/loom/fhirschema" - "github.com/calypr/loom/internal/catalog" -) - -func (s *Service) validateBuilder(ctx context.Context, builder Builder) error { - seenAliases := map[string]struct{}{} - authResourcePathsUnrestricted := builderAuthScopeUnrestricted(builder) - rootFields, err := s.discoverFields(ctx, catalog.PopulatedFieldOptions{ - ConnectionOptions: s.connOpts, - Project: builder.Project, - DatasetGeneration: builder.DatasetGeneration, - AuthResourcePathsUnrestricted: catalog.ExplicitAuthResourcePathsUnrestricted(authResourcePathsUnrestricted), - AuthResourcePaths: builder.AuthResourcePaths, - ResourceType: builder.RootResourceType, - }) - if err != nil { - return err - } - rootPivots, err := s.discoverFields(ctx, catalog.PopulatedFieldOptions{ - ConnectionOptions: s.connOpts, - Project: builder.Project, - DatasetGeneration: builder.DatasetGeneration, - AuthResourcePathsUnrestricted: catalog.ExplicitAuthResourcePathsUnrestricted(authResourcePathsUnrestricted), - AuthResourcePaths: builder.AuthResourcePaths, - ResourceType: builder.RootResourceType, - PivotOnly: true, - }) - if err != nil { - return err - } - if err := validateNodeSelections(builder.Fields, builder.Filters, builder.Pivots, builder.Aggregates, builder.Slices, rootFields, rootPivots); err != nil { - return err - } - for _, step := range builder.Traversals { - if err := s.validateTraversal(ctx, builder.Project, builder.DatasetGeneration, builder.AuthResourcePaths, authResourcePathsUnrestricted, builder.RootResourceType, step, seenAliases); err != nil { - return err - } - } - return nil -} - -func (s *Service) validateTraversal(ctx context.Context, project, datasetGeneration string, authResourcePaths []string, authResourcePathsUnrestricted bool, sourceType string, step TraversalStep, seenAliases map[string]struct{}) error { - if err := step.MatchMode.Validate(); err != nil { - return fmt.Errorf("traversal %s -> %s (%s): %w", sourceType, step.ToResourceType, step.Label, err) - } - if step.Alias == "" { - return fmt.Errorf("traversal alias is required") - } - if _, ok := seenAliases[step.Alias]; ok { - return fmt.Errorf("traversal alias %q is duplicated", step.Alias) - } - seenAliases[step.Alias] = struct{}{} - - refs, err := s.discoverReferences(ctx, catalog.PopulatedReferenceOptions{ - ConnectionOptions: s.connOpts, - Project: project, - DatasetGeneration: datasetGeneration, - AuthResourcePathsUnrestricted: catalog.ExplicitAuthResourcePathsUnrestricted(authResourcePathsUnrestricted), - AuthResourcePaths: authResourcePaths, - NodeType: sourceType, - Mode: catalog.TraversalModeBuilder, - }) - if err != nil { - return err - } - found := false - for _, ref := range refs { - if ref.Label == step.Label && ref.ToType == step.ToResourceType { - found = true - break - } - } - if !found { - return fmt.Errorf("traversal %s -> %s (%s) is not populated", sourceType, step.ToResourceType, step.Label) - } - - fields, err := s.discoverFields(ctx, catalog.PopulatedFieldOptions{ - ConnectionOptions: s.connOpts, - Project: project, - DatasetGeneration: datasetGeneration, - AuthResourcePathsUnrestricted: catalog.ExplicitAuthResourcePathsUnrestricted(authResourcePathsUnrestricted), - AuthResourcePaths: authResourcePaths, - ResourceType: step.ToResourceType, - }) - if err != nil { - return err - } - pivotFields, err := s.discoverFields(ctx, catalog.PopulatedFieldOptions{ - ConnectionOptions: s.connOpts, - Project: project, - DatasetGeneration: datasetGeneration, - AuthResourcePathsUnrestricted: catalog.ExplicitAuthResourcePathsUnrestricted(authResourcePathsUnrestricted), - AuthResourcePaths: authResourcePaths, - ResourceType: step.ToResourceType, - PivotOnly: true, - }) - if err != nil { - return err - } - if err := validateNodeSelections(step.Fields, step.Filters, step.Pivots, step.Aggregates, step.Slices, fields, pivotFields); err != nil { - return fmt.Errorf("alias %s: %w", step.Alias, err) - } - for _, child := range step.Traversals { - if err := s.validateTraversal(ctx, project, datasetGeneration, authResourcePaths, authResourcePathsUnrestricted, step.ToResourceType, child, seenAliases); err != nil { - return err - } - } - return nil -} - -func validateNodeSelections(fields []FieldSelect, filters []TypedFilter, pivots []PivotSelect, aggregates []AggregateSelect, slices []RepresentativeSlice, discovered []catalog.PopulatedField, pivotable []catalog.PopulatedField) error { - seenFields := map[string]struct{}{} - for _, field := range fields { - if field.Name == "" || field.Select == "" { - return fmt.Errorf("field selections require name and select") - } - if _, ok := seenFields[field.Name]; ok { - return fmt.Errorf("field name %q is duplicated", field.Name) - } - seenFields[field.Name] = struct{}{} - if _, err := ParseSelector(field.Select); err != nil { - return fmt.Errorf("invalid selector for field %q: %w", field.Name, err) - } - for _, fallback := range field.FallbackSelects { - if _, err := ParseSelector(fallback); err != nil { - return fmt.Errorf("invalid fallback selector for field %q: %w", field.Name, err) - } - } - } - - for _, filter := range filters { - if err := ValidateTypedFilterForResource(resourceTypeFromDiscovered(discovered), filter); err != nil { - return fmt.Errorf("invalid filter %q: %w", filter.FieldRef, err) - } - sel, err := ParseSelector(filter.Selector) - if err != nil { - return fmt.Errorf("invalid filter selector for %q: %w", filter.FieldRef, err) - } - if findFieldByPath(discovered, sel.CanonicalPath()) == nil { - return fmt.Errorf("filter selector %q is not present in populated fields", filter.Selector) - } - } - - seenPivots := map[string]struct{}{} - for _, pivot := range pivots { - if pivot.Name == "" || pivot.ColumnSelect == "" || pivot.ValueSelect == "" { - return fmt.Errorf("pivot selections require name, column selector, and value selector") - } - if _, ok := seenPivots[pivot.Name]; ok { - return fmt.Errorf("pivot name %q is duplicated", pivot.Name) - } - seenPivots[pivot.Name] = struct{}{} - - columnSel, err := ParseSelector(pivot.ColumnSelect) - if err != nil { - return fmt.Errorf("invalid column selector for pivot %q: %w", pivot.Name, err) - } - valueSel, err := ParseSelector(pivot.ValueSelect) - if err != nil { - return fmt.Errorf("invalid value selector for pivot %q: %w", pivot.Name, err) - } - - pivotSpec, err := fhirschema.ValidatePivotSelectors(resourceTypeFromDiscovered(discovered), selectorSpecFromSelector(columnSel), selectorSpecFromSelector(valueSel)) - if err != nil { - return fmt.Errorf("pivot %q: %w", pivot.Name, err) - } - match := findFieldByPath(pivotable, pivotSpec.CatalogRootPath) - if match == nil || !match.PivotCandidate { - return fmt.Errorf("pivot selector %q is not pivotable", pivot.ColumnSelect) - } - if len(pivot.Columns) == 0 && len(match.PivotColumns) == 0 { - return fmt.Errorf("pivot %q has no available pivot columns", pivot.Name) - } - pivot.PivotFamily = pivotSpec.Family - } - - seenAggregates := map[string]struct{}{} - for _, agg := range aggregates { - if strings.TrimSpace(agg.Name) == "" { - return fmt.Errorf("aggregate selections require name") - } - if _, ok := seenAggregates[agg.Name]; ok { - return fmt.Errorf("aggregate name %q is duplicated", agg.Name) - } - seenAggregates[agg.Name] = struct{}{} - switch strings.ToUpper(strings.TrimSpace(agg.Operation)) { - case "COUNT", "COUNT_DISTINCT", "EXISTS", "DISTINCT_VALUES", "MIN", "MAX": - default: - return fmt.Errorf("aggregate %q uses unsupported operation %q", agg.Name, agg.Operation) - } - if strings.TrimSpace(agg.Select) != "" { - sel, err := ParseSelector(agg.Select) - if err != nil { - return fmt.Errorf("invalid aggregate selector for %q: %w", agg.Name, err) - } - if findFieldByPath(discovered, sel.CanonicalPath()) == nil { - return fmt.Errorf("aggregate selector %q is not present in populated fields", agg.Select) - } - } - if aggregateOperationRequiresSelector(strings.ToUpper(strings.TrimSpace(agg.Operation))) && strings.TrimSpace(agg.Select) == "" { - return fmt.Errorf("aggregate %q operation %s requires a selector", agg.Name, agg.Operation) - } - if strings.TrimSpace(agg.PredicatePath) != "" { - sel, err := ParseSelector(agg.PredicatePath) - if err != nil { - return fmt.Errorf("invalid aggregate predicate selector for %q: %w", agg.Name, err) - } - if findFieldByPath(discovered, sel.CanonicalPath()) == nil { - return fmt.Errorf("aggregate predicate selector %q is not present in populated fields", agg.PredicatePath) - } - } - } - - seenSlices := map[string]struct{}{} - for _, slice := range slices { - if strings.TrimSpace(slice.Name) == "" { - return fmt.Errorf("representative slices require name") - } - if _, ok := seenSlices[slice.Name]; ok { - return fmt.Errorf("representative slice name %q is duplicated", slice.Name) - } - seenSlices[slice.Name] = struct{}{} - if slice.Limit <= 0 { - return fmt.Errorf("representative slice %q requires positive limit", slice.Name) - } - if strings.TrimSpace(slice.PredicatePath) != "" { - sel, err := ParseSelector(slice.PredicatePath) - if err != nil { - return fmt.Errorf("invalid representative slice predicate for %q: %w", slice.Name, err) - } - if findFieldByPath(discovered, sel.CanonicalPath()) == nil { - return fmt.Errorf("representative slice predicate %q is not present in populated fields", slice.PredicatePath) - } - } - for _, field := range slice.Fields { - if strings.TrimSpace(field.Name) == "" || strings.TrimSpace(field.Select) == "" { - return fmt.Errorf("representative slice %q requires fields with name and select", slice.Name) - } - sel, err := ParseSelector(field.Select) - if err != nil { - return fmt.Errorf("invalid representative slice field for %q: %w", slice.Name, err) - } - if findFieldByPath(discovered, sel.CanonicalPath()) == nil { - return fmt.Errorf("representative slice selector %q is not present in populated fields", field.Select) - } - for _, fallback := range field.FallbackSelects { - fallbackSel, err := ParseSelector(fallback) - if err != nil { - return fmt.Errorf("invalid representative slice fallback selector for %q: %w", slice.Name, err) - } - if findFieldByPath(discovered, fallbackSel.CanonicalPath()) == nil { - return fmt.Errorf("representative slice fallback selector %q is not present in populated fields", fallback) - } - } - } - } - - for _, field := range fields { - sel, _ := ParseSelector(field.Select) - if findFieldByPath(discovered, sel.CanonicalPath()) == nil { - return fmt.Errorf("selector %q is not present in populated fields", field.Select) - } - for _, fallback := range field.FallbackSelects { - fallbackSel, _ := ParseSelector(fallback) - if findFieldByPath(discovered, fallbackSel.CanonicalPath()) == nil { - return fmt.Errorf("fallback selector %q is not present in populated fields", fallback) - } - } - } - return nil -} - -func selectorSpecFromSelector(sel Selector) fhirschema.FieldSelectorSpec { - sourcePath := "" - valuePath := "" - if len(sel.Steps) > 0 { - last := len(sel.Steps) - 1 - valuePath = selectorStepText(sel.Steps[last]) - if last > 0 { - parts := make([]string, 0, last) - for _, step := range sel.Steps[:last] { - parts = append(parts, selectorStepText(step)) - } - sourcePath = strings.Join(parts, ".") - } - } - var where *fhirschema.FieldPredicateSpec - if sel.Filter != nil { - where = &fhirschema.FieldPredicateSpec{ - Path: sel.Filter.Field, - Op: fhirschema.PredicateContains, - Value: sel.Filter.Needle, - } - } - return fhirschema.FieldSelectorSpec{ - SourcePath: sourcePath, - Where: where, - ValuePath: valuePath, - } -} - -func findFieldByPath(fields []catalog.PopulatedField, path string) *catalog.PopulatedField { - for i := range fields { - if fields[i].Path == path { - return &fields[i] - } - } - return nil -} diff --git a/internal/dataframe/runtime/compatibility.go b/internal/dataframe/runtime/compatibility.go deleted file mode 100644 index 224ca8c..0000000 --- a/internal/dataframe/runtime/compatibility.go +++ /dev/null @@ -1,275 +0,0 @@ -// Package runtime owns catalog-aware dataframe preparation and execution. -// -// Deprecated compatibility aliases in compatibility.go keep existing direct -// runtime importers source-compatible. New callers should import request and -// compiler contracts from their canonical spec/compiler owners instead. -package runtime - -import ( - "github.com/calypr/loom/internal/dataframe/compiler" - dataframeerrors "github.com/calypr/loom/internal/dataframe/errors" -) - -type ( - Builder = compiler.Builder - TraversalStep = compiler.TraversalStep - RepresentativeSlice = compiler.RepresentativeSlice - FieldSelect = compiler.FieldSelect - PivotSelect = compiler.PivotSelect - AggregateSelect = compiler.AggregateSelect - CompiledQuery = compiler.CompiledQuery - SemanticPlan = compiler.SemanticPlan - SemanticNode = compiler.SemanticNode - SemanticField = compiler.SemanticField - SemanticPivot = compiler.SemanticPivot - SemanticAggregate = compiler.SemanticAggregate - SemanticSlice = compiler.SemanticSlice - SemanticPlanExplanation = compiler.SemanticPlanExplanation - SemanticNodeExplanation = compiler.SemanticNodeExplanation - SelectionSemanticSpec = compiler.SelectionSemanticSpec - RowGrain = compiler.RowGrain - ProjectionMode = compiler.ProjectionMode - Cardinality = compiler.Cardinality - RowIdentity = compiler.RowIdentity - TraversalMatchMode = compiler.TraversalMatchMode - FilterOperator = compiler.FilterOperator - FilterValueKind = compiler.FilterValueKind - ArrayQuantifier = compiler.ArrayQuantifier - CodeValue = compiler.CodeValue - FilterValue = compiler.FilterValue - TypedFilter = compiler.TypedFilter - Selector = compiler.Selector - SelectorStep = compiler.SelectorStep - ContainsFilter = compiler.ContainsFilter - PhysicalOptimizationPolicy = compiler.PhysicalOptimizationPolicy - PhysicalOptimizationRule = compiler.PhysicalOptimizationRule - PhysicalOptimizationDecision = compiler.PhysicalOptimizationDecision - PhysicalOptimizationReport = compiler.PhysicalOptimizationReport - PhysicalOptimizationRuleState = compiler.PhysicalOptimizationRuleState - CompilerPlanDiagnostics = compiler.CompilerPlanDiagnostics - PhysicalTraversalDecision = compiler.PhysicalTraversalDecision - RichSourceReuse = compiler.RichSourceReuse - RichConsumerGroup = compiler.RichConsumerGroup - RenderedPhysicalPlan = compiler.RenderedPhysicalPlan - PhysicalTraversalPrefix = compiler.PhysicalTraversalPrefix - PhysicalTraversalSubset = compiler.PhysicalTraversalSubset - PhysicalTraversalPrefixDecomposition = compiler.PhysicalTraversalPrefixDecomposition - PhysicalTraversalPrefixRejectionReason = compiler.PhysicalTraversalPrefixRejectionReason - PhysicalTraversalPrefixError = compiler.PhysicalTraversalPrefixError - PhysicalPlan = compiler.PhysicalPlan - PhysicalSource = compiler.PhysicalSource - PhysicalOperationKind = compiler.PhysicalOperationKind - PhysicalOperation = compiler.PhysicalOperation - PhysicalRootScan = compiler.PhysicalRootScan - PhysicalTraversalDirection = compiler.PhysicalTraversalDirection - PhysicalTraversalStrategy = compiler.PhysicalTraversalStrategy - PhysicalTraversal = compiler.PhysicalTraversal - PhysicalValue = compiler.PhysicalValue - PhysicalCardinality = compiler.PhysicalCardinality - PhysicalNullBehavior = compiler.PhysicalNullBehavior - PhysicalExpressionKind = compiler.PhysicalExpressionKind - PhysicalSelectorExecutionMode = compiler.PhysicalSelectorExecutionMode - PhysicalExpression = compiler.PhysicalExpression - PhysicalExtract = compiler.PhysicalExtract - PhysicalPreparedReference = compiler.PhysicalPreparedReference - PhysicalPreparedSet = compiler.PhysicalPreparedSet - PhysicalPreparedField = compiler.PhysicalPreparedField - PhysicalAggregateOperation = compiler.PhysicalAggregateOperation - PhysicalAggregate = compiler.PhysicalAggregate - PhysicalPivotMap = compiler.PhysicalPivotMap - PhysicalSlice = compiler.PhysicalSlice - PhysicalExpressionProjection = compiler.PhysicalExpressionProjection - PhysicalObject = compiler.PhysicalObject - PhysicalSet = compiler.PhysicalSet - PhysicalSetProjection = compiler.PhysicalSetProjection - PhysicalSetProjectionField = compiler.PhysicalSetProjectionField - PhysicalSetOutputField = compiler.PhysicalSetOutputField - PhysicalSetOutput = compiler.PhysicalSetOutput - PhysicalSubplan = compiler.PhysicalSubplan - PhysicalPredicate = compiler.PhysicalPredicate - PhysicalPredicateKind = compiler.PhysicalPredicateKind - PhysicalPredicateExpression = compiler.PhysicalPredicateExpression - PhysicalFilter = compiler.PhysicalFilter - PhysicalDerivedLet = compiler.PhysicalDerivedLet - PhysicalSort = compiler.PhysicalSort - PhysicalLimit = compiler.PhysicalLimit - PhysicalProjection = compiler.PhysicalProjection - PhysicalReturn = compiler.PhysicalReturn - StorageRoute = compiler.StorageRoute - ErrorCode = dataframeerrors.ErrorCode - UserError = dataframeerrors.UserError - Error = dataframeerrors.Error - ErrorOption = dataframeerrors.ErrorOption -) - -const ( - MaxSemanticTraversalDepth = compiler.MaxSemanticTraversalDepth - RowGrainResource = compiler.RowGrainResource - RowGrainPatient = compiler.RowGrainPatient - RowGrainSpecimen = compiler.RowGrainSpecimen - RowGrainFile = compiler.RowGrainFile - RowGrainDiagnosis = compiler.RowGrainDiagnosis - RowGrainObservation = compiler.RowGrainObservation - RowGrainStudyEnrollment = compiler.RowGrainStudyEnrollment - ProjectionScalar = compiler.ProjectionScalar - ProjectionFirst = compiler.ProjectionFirst - ProjectionArray = compiler.ProjectionArray - ProjectionDistinctArray = compiler.ProjectionDistinctArray - ProjectionAggregate = compiler.ProjectionAggregate - ProjectionPivot = compiler.ProjectionPivot - ProjectionExplode = compiler.ProjectionExplode - CardinalityRequiredOne = compiler.CardinalityRequiredOne - CardinalityOptionalOne = compiler.CardinalityOptionalOne - CardinalityMany = compiler.CardinalityMany - CardinalityUnknownObservedMany = compiler.CardinalityUnknownObservedMany - TraversalMatchOptional = compiler.TraversalMatchOptional - TraversalMatchRequired = compiler.TraversalMatchRequired - FilterEquals = compiler.FilterEquals - FilterNotEquals = compiler.FilterNotEquals - FilterIn = compiler.FilterIn - FilterExists = compiler.FilterExists - FilterMissing = compiler.FilterMissing - FilterContains = compiler.FilterContains - FilterGreaterThan = compiler.FilterGreaterThan - FilterGreaterEq = compiler.FilterGreaterEq - FilterLessThan = compiler.FilterLessThan - FilterLessEq = compiler.FilterLessEq - FilterString = compiler.FilterString - FilterCode = compiler.FilterCode - FilterBoolean = compiler.FilterBoolean - FilterInteger = compiler.FilterInteger - FilterDecimal = compiler.FilterDecimal - FilterDate = compiler.FilterDate - FilterDateTime = compiler.FilterDateTime - QuantifierAny = compiler.QuantifierAny - QuantifierAll = compiler.QuantifierAll - QuantifierNone = compiler.QuantifierNone - PhysicalTraversalNative = compiler.PhysicalTraversalNative - PhysicalTraversalEndpointLookup = compiler.PhysicalTraversalEndpointLookup - PhysicalOutbound = compiler.PhysicalOutbound - PhysicalInbound = compiler.PhysicalInbound - PhysicalAny = compiler.PhysicalAny - PhysicalRootScanOp = compiler.PhysicalRootScanOp - PhysicalTraversalOp = compiler.PhysicalTraversalOp - PhysicalFilterOp = compiler.PhysicalFilterOp - PhysicalDerivedLetOp = compiler.PhysicalDerivedLetOp - PhysicalSetOp = compiler.PhysicalSetOp - PhysicalSortOp = compiler.PhysicalSortOp - PhysicalLimitOp = compiler.PhysicalLimitOp - PhysicalReturnOp = compiler.PhysicalReturnOp - PhysicalScalarCardinality = compiler.PhysicalScalarCardinality - PhysicalArrayCardinality = compiler.PhysicalArrayCardinality - PhysicalObjectCardinality = compiler.PhysicalObjectCardinality - PhysicalPreserveNull = compiler.PhysicalPreserveNull - PhysicalOmitNulls = compiler.PhysicalOmitNulls - PhysicalEmptyOnNull = compiler.PhysicalEmptyOnNull - PhysicalValueExpression = compiler.PhysicalValueExpression - PhysicalExtractExpression = compiler.PhysicalExtractExpression - PhysicalAggregateExpression = compiler.PhysicalAggregateExpression - PhysicalPivotExpression = compiler.PhysicalPivotExpression - PhysicalSliceExpression = compiler.PhysicalSliceExpression - PhysicalObjectExpression = compiler.PhysicalObjectExpression - PhysicalSelectorGeneric = compiler.PhysicalSelectorGeneric - PhysicalSelectorDirectScalar = compiler.PhysicalSelectorDirectScalar - PhysicalSelectorConditionalArray = compiler.PhysicalSelectorConditionalArray - PhysicalCountAggregate = compiler.PhysicalCountAggregate - PhysicalCountDistinctAggregate = compiler.PhysicalCountDistinctAggregate - PhysicalExistsAggregate = compiler.PhysicalExistsAggregate - PhysicalDistinctValuesAggregate = compiler.PhysicalDistinctValuesAggregate - PhysicalMinAggregate = compiler.PhysicalMinAggregate - PhysicalMaxAggregate = compiler.PhysicalMaxAggregate - PhysicalFirstAggregate = compiler.PhysicalFirstAggregate - PhysicalSetGraphIDField = compiler.PhysicalSetGraphIDField - PhysicalSetKeyField = compiler.PhysicalSetKeyField - PhysicalSetIDField = compiler.PhysicalSetIDField - PhysicalSetResourceTypeField = compiler.PhysicalSetResourceTypeField - PhysicalSetPayloadField = compiler.PhysicalSetPayloadField - PhysicalComparisonPredicate = compiler.PhysicalComparisonPredicate - PhysicalAllPredicate = compiler.PhysicalAllPredicate - PhysicalAnyPredicate = compiler.PhysicalAnyPredicate - PhysicalNotPredicate = compiler.PhysicalNotPredicate - PhysicalExistsPredicate = compiler.PhysicalExistsPredicate - PhysicalPrefixNotOptionalSet = compiler.PhysicalPrefixNotOptionalSet - PhysicalPrefixSharedSubset = compiler.PhysicalPrefixSharedSubset - PhysicalPrefixInvalidCapture = compiler.PhysicalPrefixInvalidCapture - PhysicalPrefixMissingTraversal = compiler.PhysicalPrefixMissingTraversal - PhysicalPrefixUnsupportedDirection = compiler.PhysicalPrefixUnsupportedDirection - PhysicalPrefixInvalidRoute = compiler.PhysicalPrefixInvalidRoute - PhysicalPrefixInvalidScope = compiler.PhysicalPrefixInvalidScope - PhysicalPrefixInvalidTarget = compiler.PhysicalPrefixInvalidTarget - PhysicalOptimizationRuleTraversalSharing = compiler.PhysicalOptimizationRuleTraversalSharing - PhysicalOptimizationRulePreparedSelectors = compiler.PhysicalOptimizationRulePreparedSelectors - PhysicalOptimizationRuleNestedSharing = compiler.PhysicalOptimizationRuleNestedSharing - PhysicalOptimizationRuleRichConsumerFusion = compiler.PhysicalOptimizationRuleRichConsumerFusion - PhysicalOptimizationRuleCompactProjection = compiler.PhysicalOptimizationRuleCompactProjection - PhysicalOptimizationRuleEndpointTraversal = compiler.PhysicalOptimizationRuleEndpointTraversal - OptimizerRuleFilterPushdown = compiler.OptimizerRuleFilterPushdown - OptimizerRuleTraversalSharing = compiler.OptimizerRuleTraversalSharing - OptimizerRuleRelationshipSemiJoin = compiler.OptimizerRuleRelationshipSemiJoin - CodeProjectRequired = dataframeerrors.CodeProjectRequired - CodeRootResourceTypeRequired = dataframeerrors.CodeRootResourceTypeRequired - CodeUnauthorizedProject = dataframeerrors.CodeUnauthorizedProject - CodeUnknownField = dataframeerrors.CodeUnknownField - CodeFieldNotPopulated = dataframeerrors.CodeFieldNotPopulated - CodeInvalidTraversal = dataframeerrors.CodeInvalidTraversal - CodeUnsafeTraversalRoute = dataframeerrors.CodeUnsafeTraversalRoute - CodeInvalidFilter = dataframeerrors.CodeInvalidFilter - CodeUnboundedPivot = dataframeerrors.CodeUnboundedPivot - CodeInvalidPivotColumn = dataframeerrors.CodeInvalidPivotColumn - CodeInvalidSlice = dataframeerrors.CodeInvalidSlice - CodePlanTooExpensive = dataframeerrors.CodePlanTooExpensive - CodeInvalidCursor = dataframeerrors.CodeInvalidCursor - CodeStaleCursor = dataframeerrors.CodeStaleCursor - CodeDatasetGenerationChanged = dataframeerrors.CodeDatasetGenerationChanged - CodeUnsupportedExportFormat = dataframeerrors.CodeUnsupportedExportFormat - CodeClientCanceled = dataframeerrors.CodeClientCanceled - CodeBackendUnavailable = dataframeerrors.CodeBackendUnavailable - CodeInternalError = dataframeerrors.CodeInternalError -) - -var ( - DefaultPhysicalOptimizationPolicy = compiler.DefaultPhysicalOptimizationPolicy - CompileRequest = compiler.CompileRequest - CompileRequestWithPolicy = compiler.CompileRequestWithPolicy - BuildSemanticPlan = compiler.BuildSemanticPlan - ValidateSemanticGraph = compiler.ValidateSemanticGraph - BuildPhysicalPlan = compiler.BuildPhysicalPlan - BuildPhysicalPlanWithPolicy = compiler.BuildPhysicalPlanWithPolicy - BuildGenericPhysicalPlan = compiler.BuildGenericPhysicalPlan - BuildGenericPhysicalPlanWithPolicy = compiler.BuildGenericPhysicalPlanWithPolicy - OptimizePhysicalPlan = compiler.OptimizePhysicalPlan - OptimizePhysicalPlanWithPolicy = compiler.OptimizePhysicalPlanWithPolicy - RenderPhysicalPlan = compiler.RenderPhysicalPlan - ParseSelector = compiler.ParseSelector - ValidateTypedFilterForResource = compiler.ValidateTypedFilterForResource - NormalizeSelectionPlan = compiler.NormalizeSelectionPlan - ResolveSemanticField = compiler.ResolveSemanticField - InferRowGrain = compiler.InferRowGrain - RootResourceForGrain = compiler.RootResourceForGrain - ValidateRootGrain = compiler.ValidateRootGrain - DefaultRowIdentity = compiler.DefaultRowIdentity - ValidateProjection = compiler.ValidateProjection - OperatorSupportsKind = compiler.OperatorSupportsKind - ValidateGenericPhysicalPlanScope = compiler.ValidateGenericPhysicalPlanScope - DecomposePhysicalTraversalPrefix = compiler.DecomposePhysicalTraversalPrefix - ResolveStorageRoute = compiler.ResolveStorageRoute - AsUserError = dataframeerrors.AsUserError - Normalize = dataframeerrors.Normalize - PublicMessage = dataframeerrors.PublicMessage - NewError = dataframeerrors.NewError - Wrap = dataframeerrors.Wrap - WithFieldPath = dataframeerrors.WithFieldPath - WithDetails = dataframeerrors.WithDetails - WithRetryable = dataframeerrors.WithRetryable - WithCause = dataframeerrors.WithCause - IsUserCorrectable = dataframeerrors.IsUserCorrectable - IsRetryableCode = dataframeerrors.IsRetryableCode - IsOperatorFailure = dataframeerrors.IsOperatorFailure - Errorf = dataframeerrors.Errorf -) - -var ( - AllErrorCodes = dataframeerrors.AllErrorCodes - ErrBackendUnavailable = dataframeerrors.ErrBackendUnavailable - ErrClientCanceled = dataframeerrors.ErrClientCanceled -) diff --git a/internal/dataframe/runtime/execution_service.go b/internal/dataframe/runtime/execution_service.go index 1ab629f..81490b5 100644 --- a/internal/dataframe/runtime/execution_service.go +++ b/internal/dataframe/runtime/execution_service.go @@ -24,6 +24,22 @@ func (s *Service) runQuery(ctx context.Context, compiled CompiledQuery) (*Result }, nil } +// RunCompiled executes an already canonical-compiled query. It is the shared +// execution seam for non-GraphQL frontends (for example an ephemeral recipe +// produced by GraphQL); compilation and scope resolution remain the caller's +// responsibility. The query still uses the same cursor, row flattening, and +// diagnostics path as the ordinary recipe-backed Run. +func (s *Service) RunCompiled(ctx context.Context, compiled CompiledQuery) (*Result, error) { + started := time.Now() + result, err := s.runQuery(ctx, compiled) + if err != nil { + return nil, err + } + result.Diagnostics.Plan = compiled.PlanDiagnostics + result.Diagnostics.Total = time.Since(started) + return result, nil +} + // Stream compiles the same catalog- and authorization-validated request used // by Run, but delivers flattened rows as Arango yields them instead of // retaining the complete dataframe in Loom memory. Each invocation receives a diff --git a/internal/dataframe/runtime/explain.go b/internal/dataframe/runtime/explain.go index b7ad112..875b32c 100644 --- a/internal/dataframe/runtime/explain.go +++ b/internal/dataframe/runtime/explain.go @@ -2,6 +2,10 @@ package runtime import ( "context" + "crypto/sha256" + "encoding/hex" + "sort" + "strings" arangostore "github.com/calypr/loom/internal/store/arango" ) @@ -20,3 +24,47 @@ func ExplainCompiledQuery(ctx context.Context, opts arangostore.ConnectionOption BindVars: compiled.BindVars, }) } + +// ExplainCompiledQueryAssessment is the shared live Explain entrypoint for +// every compiler frontend. It deliberately delegates parsing and assessment to +// the Arango store package so recipe queries cannot grow a second Explain +// parser or cost model. +func ExplainCompiledQueryAssessment(ctx context.Context, opts arangostore.ConnectionOptions, compiled CompiledQuery) (arangostore.ExplainAssessment, error) { + result, err := ExplainCompiledQuery(ctx, opts, compiled) + if err != nil { + return arangostore.ExplainAssessment{}, err + } + return arangostore.AssessExplainResult(result), nil +} + +// CompiledQueryFingerprint returns a deterministic, value-safe identifier for +// a compiled plan. Bind values are intentionally excluded; rendered query +// shape, bind-key names, and output metadata participate instead. +func CompiledQueryFingerprint(compiled CompiledQuery) string { + keys := make([]string, 0, len(compiled.BindVars)) + for key := range compiled.BindVars { + keys = append(keys, key) + } + sort.Strings(keys) + var builder strings.Builder + builder.WriteString(compiled.Query) + builder.WriteByte('\x00') + for _, key := range keys { + builder.WriteString(key) + builder.WriteByte('\x00') + } + for _, column := range compiled.Columns { + builder.WriteString(column) + builder.WriteByte('\x00') + } + for _, column := range compiled.PublicColumns { + builder.WriteString(column) + builder.WriteByte('\x00') + } + for _, field := range compiled.PivotFields { + builder.WriteString(field) + builder.WriteByte('\x00') + } + sum := sha256.Sum256([]byte(builder.String())) + return hex.EncodeToString(sum[:]) +} diff --git a/internal/dataframe/runtime/explain_test.go b/internal/dataframe/runtime/explain_test.go index e78f082..9ce6e9e 100644 --- a/internal/dataframe/runtime/explain_test.go +++ b/internal/dataframe/runtime/explain_test.go @@ -18,3 +18,16 @@ func TestExplainCompiledQueryRequiresConnection(t *testing.T) { t.Fatal("expected connection error") } } + +func TestCompiledQueryFingerprintExcludesBindValues(t *testing.T) { + left := CompiledQuery{Query: "FOR root IN Patient FILTER root.id == @id RETURN root", BindVars: map[string]any{"id": "one"}, Columns: []string{"id"}} + right := left + right.BindVars = map[string]any{"id": "two"} + if got, want := CompiledQueryFingerprint(left), CompiledQueryFingerprint(right); got != want { + t.Fatalf("fingerprint includes bind value: left=%q right=%q", got, want) + } + right.BindVars["other"] = "value" + if CompiledQueryFingerprint(left) == CompiledQueryFingerprint(right) { + t.Fatal("fingerprint did not include bind-key shape") + } +} diff --git a/internal/dataframe/runtime/helpers.go b/internal/dataframe/runtime/helpers.go index 00633f0..fa4b6da 100644 --- a/internal/dataframe/runtime/helpers.go +++ b/internal/dataframe/runtime/helpers.go @@ -1,8 +1,9 @@ package runtime import ( - "fmt" "strings" + + "github.com/calypr/loom/internal/dataframe/compiler" ) func cloneStrings(in []string) []string { @@ -16,26 +17,6 @@ func normalizeDatasetGeneration(generation string) string { return strings.TrimSpace(generation) } -func selectorStepText(step SelectorStep) string { - switch { - case step.Iterate: - return step.Field + "[]" - case step.Index != nil: - return fmt.Sprintf("%s[%d]", step.Field, *step.Index) - default: - return step.Field - } -} - -func aggregateOperationRequiresSelector(operation string) bool { - switch strings.ToUpper(strings.TrimSpace(operation)) { - case "COUNT_DISTINCT", "EXISTS", "DISTINCT_VALUES", "MIN", "MAX": - return true - default: - return false - } -} - func sanitizeColumnName(in string) string { var b strings.Builder for _, r := range in { @@ -54,7 +35,7 @@ const ( datasetGenerationField = "dataset_generation" ) -func cloneRowIdentity(in *RowIdentity) *RowIdentity { +func cloneRowIdentity(in *compiler.RowIdentity) *compiler.RowIdentity { if in == nil { return nil } @@ -62,17 +43,3 @@ func cloneRowIdentity(in *RowIdentity) *RowIdentity { out.Fields = cloneStrings(in.Fields) return &out } - -func isDatasetGenerationScopePredicate(predicate PhysicalPredicate, variable string) bool { - return predicate.Operator == "EQUALS" && - predicate.Left.Variable == variable && - len(predicate.Left.Path) == 1 && predicate.Left.Path[0] == "dataset_generation" && - predicate.Right != nil && predicate.Right.BindKey == "dataset_generation" && - predicate.Right.Variable == "" && len(predicate.Right.Path) == 0 -} - -type storageRoute = StorageRoute - -func resolveStorageRoute(fromType, label, toType string) (storageRoute, error) { - return ResolveStorageRoute(fromType, label, toType) -} diff --git a/internal/dataframe/runtime/pivot_materialization.go b/internal/dataframe/runtime/pivot_materialization.go deleted file mode 100644 index 4a7e8eb..0000000 --- a/internal/dataframe/runtime/pivot_materialization.go +++ /dev/null @@ -1,105 +0,0 @@ -package runtime - -import ( - "context" - "fmt" - - "github.com/calypr/loom/fhirschema" - "github.com/calypr/loom/internal/catalog" -) - -func (s *Service) expandPivotColumns(ctx context.Context, builder Builder) (Builder, error) { - pivots, err := s.discoverFields(ctx, catalog.PopulatedFieldOptions{ - ConnectionOptions: s.connOpts, - Project: builder.Project, - DatasetGeneration: builder.DatasetGeneration, - AuthResourcePathsUnrestricted: catalog.ExplicitAuthResourcePathsUnrestricted(builderAuthScopeUnrestricted(builder)), - AuthResourcePaths: builder.AuthResourcePaths, - ResourceType: builder.RootResourceType, - PivotOnly: true, - }) - if err != nil { - return Builder{}, err - } - resolved, err := fillPivotColumns(builder.Pivots, pivots) - if err != nil { - return Builder{}, err - } - builder.Pivots = resolved - for i := range builder.Traversals { - if err := s.expandTraversalPivotColumns(ctx, builder.Project, builder.DatasetGeneration, builder.AuthResourcePaths, builderAuthScopeUnrestricted(builder), &builder.Traversals[i]); err != nil { - return Builder{}, err - } - } - return builder, nil -} - -func (s *Service) expandTraversalPivotColumns(ctx context.Context, project, datasetGeneration string, authResourcePaths []string, authResourcePathsUnrestricted bool, step *TraversalStep) error { - pivots, err := s.discoverFields(ctx, catalog.PopulatedFieldOptions{ - ConnectionOptions: s.connOpts, - Project: project, - DatasetGeneration: datasetGeneration, - AuthResourcePathsUnrestricted: catalog.ExplicitAuthResourcePathsUnrestricted(authResourcePathsUnrestricted), - AuthResourcePaths: authResourcePaths, - ResourceType: step.ToResourceType, - PivotOnly: true, - }) - if err != nil { - return err - } - resolved, err := fillPivotColumns(step.Pivots, pivots) - if err != nil { - return err - } - step.Pivots = resolved - for i := range step.Traversals { - if err := s.expandTraversalPivotColumns(ctx, project, datasetGeneration, authResourcePaths, authResourcePathsUnrestricted, &step.Traversals[i]); err != nil { - return err - } - } - return nil -} - -// fillPivotColumns resolves an omitted user column list to the bounded, -// scope-aware catalog values computed at ingest time. Rendering an empty list -// would materialize every observed pivot key and is not a safe fallback. -func fillPivotColumns(in []PivotSelect, discovered []catalog.PopulatedField) ([]PivotSelect, error) { - if len(in) == 0 { - return []PivotSelect{}, nil - } - out := make([]PivotSelect, 0, len(in)) - resourceType := resourceTypeFromDiscovered(discovered) - for _, pivot := range in { - columnSel, err := ParseSelector(pivot.ColumnSelect) - if err != nil { - return nil, fmt.Errorf("pivot %q column selector: %w", pivot.Name, err) - } - valueSel, err := ParseSelector(pivot.ValueSelect) - if err != nil { - return nil, fmt.Errorf("pivot %q value selector: %w", pivot.Name, err) - } - spec, err := fhirschema.ValidatePivotSelectors(resourceType, selectorSpecFromSelector(columnSel), selectorSpecFromSelector(valueSel)) - if err != nil { - return nil, fmt.Errorf("pivot %q: %w", pivot.Name, err) - } - if pivot.PivotFamily == "" { - pivot.PivotFamily = spec.Family - } - if len(pivot.Columns) == 0 { - match := findFieldByPath(discovered, spec.CatalogRootPath) - if match == nil || len(match.PivotColumns) == 0 { - return nil, fmt.Errorf("pivot %q has no bounded catalog columns", pivot.Name) - } - pivot.Columns = cloneStrings(match.PivotColumns) - } - out = append(out, pivot) - } - return out, nil -} - -func resourceTypeFromDiscovered(fields []catalog.PopulatedField) string { - if len(fields) == 0 { - return "" - } - return fields[0].ResourceType -} diff --git a/internal/dataframe/runtime/pivots_test.go b/internal/dataframe/runtime/pivots_test.go deleted file mode 100644 index 78b44bf..0000000 --- a/internal/dataframe/runtime/pivots_test.go +++ /dev/null @@ -1,46 +0,0 @@ -package runtime - -import ( - "reflect" - "strings" - "testing" - - "github.com/calypr/loom/internal/catalog" -) - -func TestFillPivotColumnsUsesBoundedCatalogValues(t *testing.T) { - resolved, err := fillPivotColumns([]PivotSelect{{ - Name: "lab_values", ColumnSelect: "code.coding[].display", ValueSelect: "valueQuantity.value", - }}, []catalog.PopulatedField{{ - ResourceType: "Observation", Path: "code", PivotCandidate: true, - PivotColumns: []string{"Hemoglobin", "Platelets"}, - }}) - if err != nil { - t.Fatal(err) - } - if len(resolved) != 1 || !reflect.DeepEqual(resolved[0].Columns, []string{"Hemoglobin", "Platelets"}) { - t.Fatalf("resolved pivots = %#v", resolved) - } - if resolved[0].PivotFamily == "" { - t.Fatalf("expected generated pivot family, got %#v", resolved[0]) - } -} - -func TestFillPivotColumnsRejectsUnboundedDiscovery(t *testing.T) { - _, err := fillPivotColumns([]PivotSelect{{ - Name: "lab_values", ColumnSelect: "code.coding[].display", ValueSelect: "valueQuantity.value", - }}, []catalog.PopulatedField{{ResourceType: "Observation", Path: "code", PivotCandidate: true}}) - if err == nil || !strings.Contains(err.Error(), "bounded catalog columns") { - t.Fatalf("error = %v, want bounded catalog column error", err) - } -} - -func TestBuildSemanticPlanRejectsUnresolvedPivotDiscovery(t *testing.T) { - _, err := BuildSemanticPlan(Builder{ - Project: "P1", RootResourceType: "Observation", - Pivots: []PivotSelect{{Name: "lab_values", ColumnSelect: "code.coding[].display", ValueSelect: "valueQuantity.value"}}, - }) - if err == nil || !strings.Contains(err.Error(), "bounded columns") { - t.Fatalf("error = %v, want bounded pivot error", err) - } -} diff --git a/internal/dataframe/runtime/prepare_generation.go b/internal/dataframe/runtime/prepare_generation.go deleted file mode 100644 index 94e8005..0000000 --- a/internal/dataframe/runtime/prepare_generation.go +++ /dev/null @@ -1,37 +0,0 @@ -package runtime - -import ( - "context" - "errors" - "fmt" - - "github.com/calypr/loom/internal/dataset" -) - -var ( - // ErrActiveGenerationConflict reports a caller-supplied generation that is - // not the one selected by the configured active-manifest resolver. Failing - // closed here prevents a catalog/compiled-query split across generations. - ErrActiveGenerationConflict = errors.New("requested dataset generation conflicts with active generation") -) - -// resolveActiveBuilder pins one execution request to the configured active -// READY manifest before any catalog or compiler work starts. Without a -// resolver the historical direct-Builder contract is unchanged: the builder's -// generation (or legacy null namespace) is used as supplied. -func (s *Service) resolveActiveBuilder(ctx context.Context, builder Builder) (Builder, error) { - if s == nil || s.activeManifestResolver == nil { - return builder, nil - } - manifest, err := dataset.ResolveReadyActiveManifest(ctx, s.activeManifestResolver, builder.Project) - if err != nil { - return Builder{}, fmt.Errorf("resolve active dataset generation: %w", err) - } - requested := normalizeDatasetGeneration(builder.DatasetGeneration) - active := manifest.Dataset.Generation - if requested != "" && requested != active { - return Builder{}, fmt.Errorf("%w: project %q requested %q but active is %q", ErrActiveGenerationConflict, builder.Project, requested, active) - } - builder.DatasetGeneration = active - return builder, nil -} diff --git a/internal/dataframe/runtime/request_validation.go b/internal/dataframe/runtime/request_validation.go index 719c65c..7694029 100644 --- a/internal/dataframe/runtime/request_validation.go +++ b/internal/dataframe/runtime/request_validation.go @@ -2,22 +2,17 @@ package runtime import ( "context" - "crypto/sha256" - "encoding/hex" - "encoding/json" "time" + + "github.com/calypr/loom/internal/dataframe/recipe" ) -// ValidateRequest describes a non-executing validation/compile request from a -// client. It deliberately uses the same Builder shape accepted by RunRequest -// so validation and execution cannot drift apart. type ValidateRequest struct { - Builder Builder - Limit int + Recipe recipe.Bundle + Bindings recipe.RuntimeBindings + Limit int } -// ValidationWarning is a stable, machine-readable advisory attached to a -// successful validation result. Warnings never make a request invalid. type ValidationWarning struct { Code string Message string @@ -25,11 +20,10 @@ type ValidationWarning struct { Details map[string]any } -// ValidationResult is the normalized contract consumed by preview/export -// frontends. No database rows are read by Validate. type ValidationResult struct { Valid bool - Builder Builder + Recipe recipe.Bundle + Bindings recipe.RuntimeBindings Project string DatasetGeneration string RootResourceType string @@ -45,86 +39,34 @@ type ValidationResult struct { Diagnostics QueryDiagnostics } -// Validate prepares, validates, and compiles a dataframe request without -// executing it against Arango. The exact normalized builder is returned so a -// caller can persist it as a reusable recipe or pass it to Run unchanged. func (s *Service) Validate(ctx context.Context, req ValidateRequest) (ValidationResult, error) { started := now() - builder, compiled, diagnostics, err := s.prepareAndCompile(ctx, req.Builder, req.Limit) + compiled, diagnostics, err := s.prepareAndCompile(ctx, RunRequest{Recipe: req.Recipe, Bindings: req.Bindings, Limit: req.Limit}) if err != nil { return ValidationResult{}, err } - fingerprint, err := requestFingerprint(builder, compiled.Limit) - if err != nil { - return ValidationResult{}, err + limit := compiled.Limit + warnings := make([]ValidationWarning, 0, 1) + if len(req.Recipe.Outputs) == 1 { + output := req.Recipe.Outputs[0] + if len(output.Fields) == 0 && len(output.Pivots) == 0 && len(output.Aggregates) == 0 && len(output.Slices) == 0 && len(output.Traversals) == 0 { + warnings = append(warnings, ValidationWarning{Code: "NO_SELECTED_COLUMNS", Message: "No explicit fields, pivots, aggregates, slices, or traversals were selected; only the row identity will be returned."}) + } + } + if limit > 1000 { + warnings = append(warnings, ValidationWarning{Code: "PREVIEW_LIMIT_CAPPED", Message: "Preview limits above 1000 rows may be capped by the frontend.", Details: map[string]any{"limit": limit, "recommended_max": 1000}}) } diagnostics.Total = now().Sub(started) return ValidationResult{ - Valid: true, - Builder: builder, - Project: compiled.Project, - DatasetGeneration: compiled.DatasetGeneration, - RootResourceType: compiled.RootResourceType, - Limit: compiled.Limit, - Columns: cloneStrings(compiled.Columns), - PivotFields: cloneStrings(compiled.PivotFields), + Valid: true, Recipe: req.Recipe, Bindings: req.Bindings, + Project: compiled.Project, DatasetGeneration: compiled.DatasetGeneration, + RootResourceType: compiled.RootResourceType, Limit: limit, + Columns: cloneStrings(compiled.Columns), PivotFields: cloneStrings(compiled.PivotFields), RowIdentity: cloneRowIdentity(compiled.RowIdentity), - RequestFingerprint: fingerprint, - Warnings: validationWarnings(builder, compiled), - Plan: compiled.PlanDiagnostics, - PreviewAllowed: true, - ExportAllowed: true, - Diagnostics: diagnostics, + RequestFingerprint: compiled.PlanDiagnostics.Fingerprint, + Warnings: warnings, Plan: compiled.PlanDiagnostics, + PreviewAllowed: true, ExportAllowed: true, Diagnostics: diagnostics, }, nil } var now = time.Now - -func requestFingerprint(builder Builder, limit int) (string, error) { - payload := struct { - Builder Builder - Limit int - }{Builder: builder, Limit: limit} - b, err := json.Marshal(payload) - if err != nil { - return "", err - } - sum := sha256.Sum256(b) - return hex.EncodeToString(sum[:]), nil -} - -func validationWarnings(builder Builder, compiled CompiledQuery) []ValidationWarning { - warnings := make([]ValidationWarning, 0, 2) - if len(builder.Fields) == 0 && len(builder.Pivots) == 0 && len(builder.Aggregates) == 0 && len(builder.Slices) == 0 && len(builder.Traversals) == 0 { - warnings = append(warnings, ValidationWarning{ - Code: "NO_SELECTED_COLUMNS", - Message: "No explicit fields, pivots, aggregates, slices, or traversals were selected; only the row identity will be returned.", - }) - } - if compiled.Limit > 1000 { - warnings = append(warnings, ValidationWarning{ - Code: "PREVIEW_LIMIT_CAPPED", - Message: "Preview limits above 1000 rows may be capped by the frontend.", - Details: map[string]any{"limit": compiled.Limit, "recommended_max": 1000}, - }) - } - return warnings -} - -func cloneValidationWarnings(in []ValidationWarning) []ValidationWarning { - if len(in) == 0 { - return nil - } - out := make([]ValidationWarning, len(in)) - for i, warning := range in { - out[i] = warning - out[i].Path = append([]string(nil), warning.Path...) - if warning.Details != nil { - out[i].Details = make(map[string]any, len(warning.Details)) - for key, value := range warning.Details { - out[i].Details[key] = value - } - } - } - return out -} diff --git a/internal/dataframe/runtime/scope.go b/internal/dataframe/runtime/scope.go deleted file mode 100644 index 83f3467..0000000 --- a/internal/dataframe/runtime/scope.go +++ /dev/null @@ -1,20 +0,0 @@ -package runtime - -import ( - "github.com/calypr/loom/internal/authscope" -) - -// builderAuthScopeUnrestricted preserves the historical runtime preparation -// contract while the compiler-owned semantic scope helper lives in compiler. -func builderAuthScopeUnrestricted(builder Builder) bool { - switch builder.AuthScopeMode { - case authscope.ReadScopeUnrestricted: - return true - case authscope.ReadScopeRestricted: - return false - case "": - return len(builder.AuthResourcePaths) == 0 - default: - return false - } -} diff --git a/internal/dataframe/runtime/service.go b/internal/dataframe/runtime/service.go index 08fe54e..33dd5ec 100644 --- a/internal/dataframe/runtime/service.go +++ b/internal/dataframe/runtime/service.go @@ -2,60 +2,52 @@ package runtime import ( "context" + "errors" "fmt" "time" "github.com/calypr/loom/internal/authscope" "github.com/calypr/loom/internal/catalog" + "github.com/calypr/loom/internal/dataframe/compiler" + "github.com/calypr/loom/internal/dataframe/recipe" + "github.com/calypr/loom/internal/dataframe/semantic" "github.com/calypr/loom/internal/dataset" arangostore "github.com/calypr/loom/internal/store/arango" ) const defaultRowLimit = 25 +var ErrActiveGenerationConflict = errors.New("requested dataset generation conflicts with active generation") + type ServiceConfig struct { - ConnectionOptions arangostore.ConnectionOptions - DiscoverReferences func(context.Context, catalog.PopulatedReferenceOptions) ([]catalog.PopulatedReference, error) - DiscoverFields func(context.Context, catalog.PopulatedFieldOptions) ([]catalog.PopulatedField, error) - ExecuteRows func(context.Context, ExecuteQueryOptions, string, map[string]any, func(map[string]any) error) error - ScopeResolver *authscope.ScopeResolver - // ActiveManifestResolver is optional. When configured, every Run/Stream - // selects one READY active generation before resolving scope, catalog facts, - // lowering, or AQL. A Builder's explicit generation must agree with it. + ConnectionOptions arangostore.ConnectionOptions + // Catalog callbacks are retained for callers that share a deployment config + // with GraphQL discovery. Recipe execution itself does not invoke them. + DiscoverReferences func(context.Context, catalog.PopulatedReferenceOptions) ([]catalog.PopulatedReference, error) + DiscoverFields func(context.Context, catalog.PopulatedFieldOptions) ([]catalog.PopulatedField, error) + ExecuteRows func(context.Context, ExecuteQueryOptions, string, map[string]any, func(map[string]any) error) error + ScopeResolver *authscope.ScopeResolver ActiveManifestResolver dataset.ActiveManifestResolver } type Service struct { connOpts arangostore.ConnectionOptions - discoverReferences func(context.Context, catalog.PopulatedReferenceOptions) ([]catalog.PopulatedReference, error) - discoverFields func(context.Context, catalog.PopulatedFieldOptions) ([]catalog.PopulatedField, error) executeRows func(context.Context, ExecuteQueryOptions, string, map[string]any, func(map[string]any) error) error scopeResolver *authscope.ScopeResolver activeManifestResolver dataset.ActiveManifestResolver } func NewService(cfg ServiceConfig) *Service { - svc := &Service{ + executeRows := cfg.ExecuteRows + if executeRows == nil { + executeRows = ExecuteQueryRows + } + return &Service{ connOpts: cfg.ConnectionOptions, + executeRows: executeRows, scopeResolver: cfg.ScopeResolver, activeManifestResolver: cfg.ActiveManifestResolver, } - if cfg.DiscoverReferences != nil { - svc.discoverReferences = cfg.DiscoverReferences - } else { - svc.discoverReferences = catalog.DiscoverPopulatedReferences - } - if cfg.DiscoverFields != nil { - svc.discoverFields = cfg.DiscoverFields - } else { - svc.discoverFields = catalog.DiscoverPopulatedFields - } - if cfg.ExecuteRows != nil { - svc.executeRows = cfg.ExecuteRows - } else { - svc.executeRows = ExecuteQueryRows - } - return svc } func (s *Service) Run(ctx context.Context, req RunRequest) (*Result, error) { @@ -77,78 +69,74 @@ func (s *Service) Run(ctx context.Context, req RunRequest) (*Result, error) { } func (s *Service) compileRunRequestWithDiagnostics(ctx context.Context, req RunRequest) (CompiledQuery, QueryDiagnostics, error) { - _, compiled, diagnostics, err := s.prepareAndCompile(ctx, req.Builder, req.Limit) + compiled, diagnostics, err := s.prepareAndCompile(ctx, req) return compiled, diagnostics, err } -// prepareAndCompile is the single preparation/compilation boundary shared by -// execution and execution-independent validation. Keeping it here prevents a -// frontend validation path from silently accepting shapes that Run rejects. -func (s *Service) prepareAndCompile(ctx context.Context, builder Builder, requestedLimit int) (Builder, CompiledQuery, QueryDiagnostics, error) { +// prepareAndCompile is the only runtime compiler boundary. It accepts the +// canonical recipe wire format, resolves request-scoped authorization and +// generation bindings, and invokes the shared semantic/physical compiler. +func (s *Service) prepareAndCompile(ctx context.Context, req RunRequest) (CompiledQuery, QueryDiagnostics, error) { prepareStarted := time.Now() - spec, err := s.prepareSpec(ctx, builder) + bindings, err := s.prepareBindings(ctx, req.Bindings) if err != nil { - return Builder{}, CompiledQuery{}, QueryDiagnostics{}, err + return CompiledQuery{}, QueryDiagnostics{}, err } diagnostics := QueryDiagnostics{RequestPreparation: time.Since(prepareStarted)} - limit := requestedLimit + limit := req.Limit if limit <= 0 { limit = defaultRowLimit } - // Keep the validated logical request through the physical compiler boundary. + bindings.PreviewLimit = limit compileStarted := time.Now() - compiled, err := CompileRequest(spec, limit) + plan, err := semantic.BuildRecipePlan(req.Recipe, bindings) + if err != nil { + return CompiledQuery{}, QueryDiagnostics{}, err + } + resolved, err := semantic.ResolveRecipePlan(plan, "runtime", bindings.DatasetGeneration) + if err != nil { + return CompiledQuery{}, QueryDiagnostics{}, err + } + queries, err := compiler.CompileResolvedRecipePlanWithPolicy(resolved, limit, compiler.DefaultPhysicalOptimizationPolicy()) if err != nil { - return Builder{}, CompiledQuery{}, QueryDiagnostics{}, err + return CompiledQuery{}, QueryDiagnostics{}, err + } + if len(queries) != 1 { + return CompiledQuery{}, QueryDiagnostics{}, fmt.Errorf("runtime recipe produced %d outputs, want 1", len(queries)) } diagnostics.Compilation = time.Since(compileStarted) - diagnostics.Plan = compiled.PlanDiagnostics - return spec, compiled, diagnostics, nil + diagnostics.Plan = queries[0].PlanDiagnostics + return queries[0], diagnostics, nil } -func (s *Service) prepareSpec(ctx context.Context, builder Builder) (Builder, error) { - if builder.Project == "" { - return Builder{}, fmt.Errorf("project is required") - } - if builder.RootResourceType == "" { - return Builder{}, fmt.Errorf("rootResourceType is required") +func (s *Service) prepareBindings(ctx context.Context, bindings recipe.RuntimeBindings) (recipe.RuntimeBindings, error) { + if bindings.Project == "" { + return recipe.RuntimeBindings{}, fmt.Errorf("project is required") } - principal, _ := authscope.PrincipalFromContext(ctx) - if err := authorizeProject(principal, builder.Project, s.scopeResolver != nil); err != nil { - return Builder{}, err - } - resolvedBuilder, err := s.resolveActiveBuilder(ctx, builder) - if err != nil { - return Builder{}, err + if err := authorizeProject(principal, bindings.Project, s.scopeResolver != nil); err != nil { + return recipe.RuntimeBindings{}, err } - builder = resolvedBuilder - var scope authscope.ReadScope - // A dataframebuilder service can hand an already-resolved scope to a - // dataframe service configured without its own resolver. Preserve that - // explicit mode rather than reinterpreting a restricted empty list through - // the legacy no-paths-means-unrestricted rule. - if s.scopeResolver == nil && builder.AuthScopeMode != "" { - scope = authscope.ReadScope{ - AuthResourcePaths: cloneStrings(builder.AuthResourcePaths), - Mode: builder.AuthScopeMode, - } - } else { - var err error - scope, err = s.resolveReadScopeForGeneration(ctx, principal, builder.Project, builder.DatasetGeneration, builder.AuthResourcePaths) + if s.activeManifestResolver != nil { + manifest, err := dataset.ResolveReadyActiveManifest(ctx, s.activeManifestResolver, bindings.Project) if err != nil { - return Builder{}, err + return recipe.RuntimeBindings{}, fmt.Errorf("resolve active dataset generation: %w", err) } + active := manifest.Dataset.Generation + requested := normalizeDatasetGeneration(bindings.DatasetGeneration) + if requested != "" && requested != active { + return recipe.RuntimeBindings{}, fmt.Errorf("%w: project %q requested %q but active is %q", ErrActiveGenerationConflict, bindings.Project, requested, active) + } + bindings.DatasetGeneration = active } - - builder.AuthResourcePaths = cloneStrings(scope.AuthResourcePaths) - builder.AuthScopeMode = scope.Mode - if err := s.validateBuilder(ctx, builder); err != nil { - return Builder{}, err + if s.scopeResolver == nil && bindings.AuthScopeMode != "" { + return bindings, nil } - expanded, err := s.expandPivotColumns(ctx, builder) + scope, err := s.resolveReadScopeForGeneration(ctx, principal, bindings.Project, bindings.DatasetGeneration, bindings.AuthResourcePaths) if err != nil { - return Builder{}, err + return recipe.RuntimeBindings{}, err } - return expanded, nil + bindings.AuthResourcePaths = cloneStrings(scope.AuthResourcePaths) + bindings.AuthScopeMode = scope.Mode + return bindings, nil } diff --git a/internal/dataframe/runtime/types.go b/internal/dataframe/runtime/types.go index f880035..9eb95e4 100644 --- a/internal/dataframe/runtime/types.go +++ b/internal/dataframe/runtime/types.go @@ -4,14 +4,19 @@ import ( "time" "github.com/calypr/loom/internal/dataframe/compiler" - "github.com/calypr/loom/internal/dataframe/spec" + "github.com/calypr/loom/internal/dataframe/recipe" ) type RunRequest struct { - Builder spec.Builder - Limit int + Recipe recipe.Bundle + Bindings recipe.RuntimeBindings + Limit int } +type CompiledQuery = compiler.CompiledQuery +type RowIdentity = compiler.RowIdentity +type CompilerPlanDiagnostics = compiler.CompilerPlanDiagnostics + type Result struct { Columns []string Rows []map[string]any diff --git a/internal/dataframe/runtime/validation_service_test.go b/internal/dataframe/runtime/validation_service_test.go index 0ae240d..63af4b5 100644 --- a/internal/dataframe/runtime/validation_service_test.go +++ b/internal/dataframe/runtime/validation_service_test.go @@ -5,85 +5,40 @@ import ( "strings" "testing" - "github.com/calypr/loom/internal/catalog" + "github.com/calypr/loom/internal/dataframe/recipe" ) -func TestRequestFingerprintIsDeterministicAndSensitive(t *testing.T) { - builder := Builder{ - Project: "P1", - RootResourceType: "Patient", - Fields: []FieldSelect{{Name: "gender", Select: "gender"}}, - } - first, err := requestFingerprint(builder, 25) +func TestValidationRecipeDigestIsDeterministicAndSensitive(t *testing.T) { + bundle := recipe.Bundle{RecipeSchemaVersion: recipe.CurrentSchemaVersion, Name: "patients", TranslationVersion: "test", Outputs: []recipe.Output{{Name: "patients", RootResourceType: "Patient", RowGrain: "patient"}}} + first, err := bundle.Digest() if err != nil { - t.Fatalf("requestFingerprint() error = %v", err) + t.Fatal(err) } - second, err := requestFingerprint(builder, 25) + second, err := bundle.Digest() if err != nil { - t.Fatalf("requestFingerprint() second error = %v", err) + t.Fatal(err) } if first == "" || first != second || len(first) != 64 { - t.Fatalf("fingerprint is not deterministic sha256: %q / %q", first, second) + t.Fatalf("recipe digest is not deterministic sha256: %q / %q", first, second) } - - changed := builder - changed.Project = "P2" - third, err := requestFingerprint(changed, 25) + bundle.Outputs[0].Name = "changed" + third, err := bundle.Digest() if err != nil { - t.Fatalf("requestFingerprint() changed error = %v", err) + t.Fatal(err) } if first == third { - t.Fatalf("fingerprint did not change when request changed: %q", first) - } - changedLimit, err := requestFingerprint(builder, 26) - if err != nil { - t.Fatalf("requestFingerprint() limit error = %v", err) - } - if first == changedLimit { - t.Fatalf("fingerprint did not include limit") - } -} - -func TestValidationWarningsExposeStableCodes(t *testing.T) { - warnings := validationWarnings(Builder{}, CompiledQuery{Limit: 1001}) - if len(warnings) != 2 { - t.Fatalf("validationWarnings() len = %d, want 2", len(warnings)) - } - seen := map[string]bool{} - for _, warning := range warnings { - seen[warning.Code] = true - if strings.TrimSpace(warning.Message) == "" { - t.Errorf("warning %q has empty message", warning.Code) - } - } - if !seen["NO_SELECTED_COLUMNS"] || !seen["PREVIEW_LIMIT_CAPPED"] { - t.Fatalf("warning codes = %#v", seen) - } - cloned := cloneValidationWarnings(warnings) - cloned[1].Details["limit"] = 1 - if warnings[1].Details["limit"] == 1 { - t.Fatal("cloneValidationWarnings aliased Details") + t.Fatal("recipe digest did not change when recipe changed") } } func TestValidateCompilesWithoutExecutingRows(t *testing.T) { executed := false - service := NewService(ServiceConfig{ - DiscoverFields: func(context.Context, catalog.PopulatedFieldOptions) ([]catalog.PopulatedField, error) { - return []catalog.PopulatedField{}, nil - }, - DiscoverReferences: func(context.Context, catalog.PopulatedReferenceOptions) ([]catalog.PopulatedReference, error) { - return []catalog.PopulatedReference{}, nil - }, - ExecuteRows: func(context.Context, ExecuteQueryOptions, string, map[string]any, func(map[string]any) error) error { - executed = true - return nil - }, - }) - result, err := service.Validate(context.Background(), ValidateRequest{ - Builder: Builder{Project: "P1", RootResourceType: "Patient"}, - Limit: 25, - }) + service := NewService(ServiceConfig{ExecuteRows: func(context.Context, ExecuteQueryOptions, string, map[string]any, func(map[string]any) error) error { + executed = true + return nil + }}) + recipeInput := recipe.Bundle{RecipeSchemaVersion: recipe.CurrentSchemaVersion, Name: "patients", TranslationVersion: "test", Outputs: []recipe.Output{{Name: "patients", RootResourceType: "Patient", RowGrain: "patient"}}} + result, err := service.Validate(context.Background(), ValidateRequest{Recipe: recipeInput, Bindings: recipe.RuntimeBindings{Project: "P1"}, Limit: 25}) if err != nil { t.Fatalf("Validate() error = %v", err) } @@ -99,4 +54,8 @@ func TestValidateCompilesWithoutExecutingRows(t *testing.T) { if result.Diagnostics.Compilation <= 0 || result.Diagnostics.Total <= 0 { t.Fatalf("validation diagnostics missing compilation/total: %#v", result.Diagnostics) } + warnings := result.Warnings + if len(warnings) != 1 || strings.TrimSpace(warnings[0].Message) == "" { + t.Fatalf("unexpected validation warnings: %#v", warnings) + } } diff --git a/internal/dataframe/semantic/recipe_filter.go b/internal/dataframe/semantic/recipe_filter.go new file mode 100644 index 0000000..e46224e --- /dev/null +++ b/internal/dataframe/semantic/recipe_filter.go @@ -0,0 +1,161 @@ +package semantic + +import ( + "fmt" + "strings" + + "github.com/calypr/loom/fhirschema" + "github.com/calypr/loom/internal/dataframe/recipe" + "github.com/calypr/loom/internal/dataframe/spec" +) + +// LowerRecipeFilters converts the closed recipe filter wire type into the +// canonical typed-filter representation consumed by the semantic and physical +// compilers. Selectors are relative to resourceType unless they are prefixed +// with root. +func LowerRecipeFilters(resourceType string, filters []recipe.Filter) ([]TypedFilter, error) { + return lowerRecipeFiltersForAlias(resourceType, "root", filters) +} + +// LowerRecipeFiltersForAlias is the traversal-local variant of +// LowerRecipeFilters. The alias prefix is accepted for GraphQL/catalog +// round-trips, but is removed before schema resolution; the canonical typed +// filter always carries a resource-relative selector. +func LowerRecipeFiltersForAlias(resourceType, alias string, filters []recipe.Filter) ([]TypedFilter, error) { + return lowerRecipeFiltersForAlias(resourceType, alias, filters) +} + +func lowerRecipeFiltersForAlias(resourceType, alias string, filters []recipe.Filter) ([]TypedFilter, error) { + if !fhirschema.HasResource(resourceType) { + return nil, fmt.Errorf("filter resource type %q is not represented by the active generated FHIR schema", resourceType) + } + out := make([]TypedFilter, 0, len(filters)) + for index, input := range filters { + filter, err := lowerRecipeFilterForAlias(resourceType, alias, input) + if err != nil { + return nil, fmt.Errorf("filters[%d]: %w", index, err) + } + out = append(out, filter) + } + return out, nil +} + +func lowerRecipeFilterForAlias(resourceType, alias string, input recipe.Filter) (TypedFilter, error) { + selectorText, err := normalizeRecipeFilterSelector(input.Select, alias) + if err != nil { + return TypedFilter{}, err + } + selector, err := spec.ParseSelector(selectorText) + if err != nil { + return TypedFilter{}, fmt.Errorf("filter selector %q: %w", input.Select, err) + } + canonical := selector.CanonicalPath() + metadata, ok := fhirschema.ResolveTerminalScalarMetadata(resourceType, canonical) + if !ok { + return TypedFilter{}, fmt.Errorf("filter selector %q is not represented by generated resource type %q", canonical, resourceType) + } + if metadata.Primitive == fhirschema.PrimitiveUnknown { + return TypedFilter{}, fmt.Errorf("filter selector %q does not resolve to a supported primitive", canonical) + } + fieldKind, err := recipeFilterKind(metadata.Primitive, canonical) + if err != nil { + return TypedFilter{}, err + } + repeated, _, err := spec.SelectorCardinality(resourceType, selector) + if err != nil { + return TypedFilter{}, fmt.Errorf("filter selector %q: %w", canonical, err) + } + // Terminal metadata includes repetition inherited from a repeated parent; + // the cardinality helper also proves that every repeated path was explicitly + // iterated. Keep the two facts in agreement rather than trusting the wire. + if repeated != metadata.Repeated { + return TypedFilter{}, fmt.Errorf("filter selector %q has inconsistent generated cardinality", canonical) + } + fieldRef := strings.TrimSpace(input.FieldRef) + if fieldRef == "" { + fieldRef = resourceType + "." + canonical + } + out := TypedFilter{ + FieldRef: fieldRef, + Selector: canonical, + FieldKind: fieldKind, + Repeated: metadata.Repeated, + Operator: spec.FilterOperator(input.Operator), + Quantifier: spec.ArrayQuantifier(input.Quantifier), + } + out.Values, err = recipeFilterValues(input.Values) + if err != nil { + return TypedFilter{}, err + } + if err := ValidateTypedFilterForResource(resourceType, out); err != nil { + return TypedFilter{}, fmt.Errorf("filter %q: %w", fieldRef, err) + } + return out, nil +} + +func normalizeRecipeFilterSelector(raw, alias string) (string, error) { + selector, err := spec.ParseSelector(raw) + if err != nil { + return "", err + } + alias = strings.TrimSpace(alias) + if alias == "" { + alias = "root" + } + if len(selector.Steps) > 0 && !selector.Steps[0].Iterate && selector.Steps[0].Index == nil && selector.Steps[0].Field == alias { + selector.Steps = selector.Steps[1:] + } + if len(selector.Steps) == 0 { + return "", fmt.Errorf("filter selector %q has no resource-relative path", raw) + } + return selector.CanonicalPath(), nil +} + +func recipeFilterKind(primitive fhirschema.PrimitiveKind, canonical string) (spec.FilterValueKind, error) { + switch primitive { + case fhirschema.PrimitiveBoolean: + return spec.FilterBoolean, nil + case fhirschema.PrimitiveInteger: + return spec.FilterInteger, nil + case fhirschema.PrimitiveDecimal: + return spec.FilterDecimal, nil + case fhirschema.PrimitiveDate: + return spec.FilterDate, nil + case fhirschema.PrimitiveDateTime: + return spec.FilterDateTime, nil + case fhirschema.PrimitiveString: + // Generated metadata does not carry terminology identity separately; + // code is safely recognized only at a terminal `.code` path. The shared + // validator rejects system/display values until paired Coding lowering + // exists in canonical physical IR. + if canonical == "code" || strings.HasSuffix(canonical, ".code") { + return spec.FilterCode, nil + } + return spec.FilterString, nil + default: + return "", fmt.Errorf("filter selector %q has unsupported primitive %q", canonical, primitive) + } +} + +func recipeFilterValues(values []recipe.FilterValue) ([]spec.FilterValue, error) { + out := make([]spec.FilterValue, 0, len(values)) + for index, input := range values { + value := spec.FilterValue{ + Kind: spec.FilterValueKind(input.Kind), + String: input.String, + Boolean: input.Boolean, + Integer: input.Integer, + Decimal: input.Decimal, + Date: input.Date, + DateTime: input.DateTime, + } + if input.Code != nil { + value.Code = &spec.CodeValue{System: input.Code.System, Code: input.Code.Code, Display: input.Code.Display} + } + if err := value.Validate(); err != nil { + return nil, fmt.Errorf("filter value %d: %w", index, err) + } + out = append(out, value) + } + return out, nil +} diff --git a/internal/dataframe/semantic/recipe_plan_build.go b/internal/dataframe/semantic/recipe_plan_build.go new file mode 100644 index 0000000..8a1a870 --- /dev/null +++ b/internal/dataframe/semantic/recipe_plan_build.go @@ -0,0 +1,176 @@ +package semantic + +// This file is the single semantic boundary for persisted recipes and the +// existing GraphQL dataframe request. It deliberately stops before physical +// lowering: no collection, AQL, SQL, or backend implementation detail belongs +// in these types. + +import ( + "fmt" + "strings" + + "github.com/calypr/loom/fhirschema" + "github.com/calypr/loom/internal/dataframe/expression" + "github.com/calypr/loom/internal/dataframe/recipe" + "github.com/calypr/loom/internal/dataframe/spec" +) + +func BuildRecipePlan(bundle recipe.Bundle, bindings recipe.RuntimeBindings) (RecipePlan, error) { + if bundle.Fragments != nil { + expanded, err := bundle.ExpandFragments() + if err != nil { + return RecipePlan{}, err + } + bundle = expanded + } + if err := bundle.Validate(); err != nil { + return RecipePlan{}, err + } + digest, err := bundle.Digest() + if err != nil { + return RecipePlan{}, err + } + plan := RecipePlan{Version: 1, RecipeDigest: digest, TranslationVersion: bundle.TranslationVersion, Bindings: cloneBindings(bindings), Outputs: make([]OutputPlan, 0, len(bundle.Outputs))} + for index, output := range bundle.Outputs { + compiled, err := buildRecipeOutput(output, bindings) + if err != nil { + return RecipePlan{}, fmt.Errorf("outputs[%d] %s: %w", index, output.Name, err) + } + plan.Outputs = append(plan.Outputs, compiled) + } + return plan, nil +} + +type scopeBinding struct { + ResourceType string + Prefix string + // ExpandedItem means this alias denotes one element of a repeated + // collection. The prefix retains the array path for schema resolution, + // but that collection's cardinality must not leak into expressions scoped + // to the item. + ExpandedItem bool +} + +type scopeFrame struct { + aliases map[string]scopeBinding +} + +func newRootScope(resourceType string) scopeFrame { + return scopeFrame{aliases: map[string]scopeBinding{"root": {ResourceType: resourceType}}} +} + +func (s scopeFrame) child(alias string, binding scopeBinding) (scopeFrame, error) { + alias = strings.TrimSpace(alias) + if alias == "" { + return scopeFrame{}, fmt.Errorf("alias is required") + } + if _, exists := s.aliases[alias]; exists { + return scopeFrame{}, fmt.Errorf("alias %q shadows an existing lexical binding", alias) + } + child := scopeFrame{aliases: make(map[string]scopeBinding, len(s.aliases)+1)} + for name, value := range s.aliases { + child.aliases[name] = value + } + child.aliases[alias] = binding + return child, nil +} + +func (s scopeFrame) expression(input recipe.Expression, path string) (SemanticExpression, error) { + expr, err := expression.FromRecipeInContexts(input, keys(s.aliases)) + if err != nil { + return SemanticExpression{}, fmt.Errorf("%s: %w", path, err) + } + checked, err := expr.Check(expression.TypeContext{Resolve: s.resolve}) + if err != nil { + return SemanticExpression{}, fmt.Errorf("%s: %w", path, err) + } + context := "" + if expr.Selector != nil { + context = expr.Selector.Context + } + return SemanticExpression{Expression: checked.Expression, Type: checked.Type, SourcePath: path, Context: context}, nil +} + +func (s scopeFrame) resolve(ref expression.SelectorRef) (expression.Type, error) { + alias := ref.Context + if alias == "" { + alias = "root" + } + binding, ok := s.aliases[alias] + if !ok { + return expression.Type{}, fmt.Errorf("selector context %q is not in scope", alias) + } + path := strings.TrimPrefix(strings.TrimSpace(ref.Path), ".") + if path == "" { + return expression.Type{}, fmt.Errorf("selector path is empty") + } + fullPath := path + if binding.Prefix != "" { + fullPath = binding.Prefix + "." + path + } + selector, err := spec.ParseSelector(fullPath) + if err != nil { + return expression.Type{}, err + } + canonicalPath := selector.CanonicalPath() + // An unqualified dotted selector may be either a nested root field or an + // alias-qualified selector. If the generated schema cannot resolve it as a + // root path, report the lexical alias error before the lower-level schema + // diagnostic so recipe authors get an actionable scope failure. + if ref.Context == "" { + if parts := strings.SplitN(path, ".", 2); len(parts) == 2 { + if _, visible := s.aliases[parts[0]]; !visible { + if _, resolved := fhirschema.ResolveTerminalScalarMetadata(binding.ResourceType, canonicalPath); !resolved { + return expression.Type{}, fmt.Errorf("selector context %q is not in scope", parts[0]) + } + } + } + } + repeated, _, err := spec.SelectorCardinality(binding.ResourceType, selector) + if err != nil { + if ref.Context == "" && strings.Contains(path, ".") { + return expression.Type{}, fmt.Errorf("selector context or path %q is undefined: %w", path, err) + } + return expression.Type{}, err + } + metadata, ok := fhirschema.ResolveTerminalScalarMetadata(binding.ResourceType, fullPath) + if !ok { + return expression.Type{}, fmt.Errorf("selector path %q is not in the active FHIR schema", fullPath) + } + kind := primitiveKind(metadata.Primitive) + if kind == expression.KindObject { + // A repeated object is a valid expansion source even though it has no + // terminal scalar metadata. + repeated = repeated || metadata.Repeated + } + if binding.ExpandedItem { + // The expansion itself is the row-shaping operation. Any [] markers in + // the item-relative selector still represent a many-valued expression, + // while the repeated prefix used to reach the item does not. + repeated = strings.Contains(path, "[]") + } + cardinality := expression.OptionalOne + if repeated { + cardinality = expression.Many + } + return expression.Type{Kind: kind, Cardinality: cardinality}, nil +} + +func primitiveKind(kind fhirschema.PrimitiveKind) expression.ValueKind { + switch kind { + case fhirschema.PrimitiveBoolean: + return expression.KindBoolean + case fhirschema.PrimitiveInteger: + return expression.KindInteger + case fhirschema.PrimitiveDecimal: + return expression.KindDecimal + case fhirschema.PrimitiveDate: + return expression.KindDate + case fhirschema.PrimitiveDateTime: + return expression.KindDateTime + case fhirschema.PrimitiveString: + return expression.KindString + default: + return expression.KindObject + } +} diff --git a/internal/dataframe/semantic/recipe_plan_output.go b/internal/dataframe/semantic/recipe_plan_output.go new file mode 100644 index 0000000..da7615f --- /dev/null +++ b/internal/dataframe/semantic/recipe_plan_output.go @@ -0,0 +1,271 @@ +package semantic + +// This file is the single semantic boundary for persisted recipes and the +// existing GraphQL dataframe request. It deliberately stops before physical +// lowering: no collection, AQL, SQL, or backend implementation detail belongs +// in these types. + +import ( + "fmt" + "strings" + + "github.com/calypr/loom/fhirschema" + "github.com/calypr/loom/internal/dataframe/expression" + "github.com/calypr/loom/internal/dataframe/recipe" +) + +func buildRecipeOutput(output recipe.Output, bindings recipe.RuntimeBindings) (OutputPlan, error) { + if !fhirschema.HasResource(output.RootResourceType) { + return OutputPlan{}, fmt.Errorf("root resource type %q is not represented by the active generated FHIR schema", output.RootResourceType) + } + grain := RowGrain(output.RowGrain) + if err := ValidateRootGrain(output.RootResourceType, grain); err != nil { + // Persisted recipes may introduce a product-specific grain when they + // also declare the row-shaping operation and an explicit identity. The + // GraphQL request contract remains strict and continues to use + // ValidateRootGrain above. + if output.Expand == nil || output.Identity == nil || !validCustomGrain(string(grain)) { + return OutputPlan{}, err + } + } + scope := newRootScope(output.RootResourceType) + if output.Expand != nil { + // The source is checked in the parent lexical scope first, then its + // selector path becomes the prefix for the expansion item alias. + from, err := scope.expression(output.Expand.From, "expand.from") + if err != nil { + return OutputPlan{}, err + } + if from.Expression.Selector == nil || from.Type.Cardinality != expression.Many { + return OutputPlan{}, fmt.Errorf("expand.from must be a repeated selector") + } + ref := from.Expression.Selector + binding, err := scopeBindingForSelector(scope, *ref) + if err != nil { + return OutputPlan{}, err + } + prefix := binding.Prefix + if prefix != "" { + prefix += "." + } + // The expansion alias denotes one item, not the repeated collection. + // An explicit index keeps schema cardinality scalar while retaining the + // canonical array path for generated metadata. + prefix += strings.TrimSuffix(strings.TrimPrefix(ref.Path, "."), "[]") + "[0]" + scope, err = scope.child(output.Expand.As, scopeBinding{ResourceType: binding.ResourceType, Prefix: prefix, ExpandedItem: true}) + if err != nil { + return OutputPlan{}, err + } + unnest := &SemanticUnnest{Source: from, As: output.Expand.As, JoinMode: UnnestInner} + if err := unnest.Validate(); err != nil { + return OutputPlan{}, fmt.Errorf("expand: %w", err) + } + plan := OutputPlan{Name: output.Name, RootResourceType: output.RootResourceType, RowGrain: grain, Collision: output.CollisionPolicy, Unnest: unnest} + return finishRecipeOutput(plan, output, scope, bindings) + } + plan := OutputPlan{Name: output.Name, RootResourceType: output.RootResourceType, RowGrain: grain, Collision: output.CollisionPolicy} + return finishRecipeOutput(plan, output, scope, bindings) +} + +func validCustomGrain(value string) bool { + if strings.TrimSpace(value) == "" { + return false + } + for index, r := range value { + if (r >= 'a' && r <= 'z') || (r >= 'A' && r <= 'Z') || r == '_' || (index > 0 && r >= '0' && r <= '9') { + continue + } + return false + } + return true +} + +func finishRecipeOutput(plan OutputPlan, output recipe.Output, scope scopeFrame, bindings recipe.RuntimeBindings) (OutputPlan, error) { + if plan.Collision == "" { + plan.Collision = "error" + } + plan.Fields = make([]SemanticProjection, 0, len(output.Fields)) + plan.CatalogProjections = make([]string, 0, len(output.CatalogProjections)) + for _, projection := range output.CatalogProjections { + plan.CatalogProjections = append(plan.CatalogProjections, projection.Name) + } + plan.DeclaredOrder = make([]string, 0, len(output.Fields)) + plan.Root = SemanticNode{Alias: "root", ResourceType: output.RootResourceType, Fields: make([]SemanticField, 0, len(output.Fields))} + rootFilters, err := LowerRecipeFilters(output.RootResourceType, output.Filters) + if err != nil { + return OutputPlan{}, fmt.Errorf("root filters: %w", err) + } + plan.Root.Filters = rootFilters + plan.Root.Pivots, plan.Root.Aggregates, plan.Root.Slices, err = lowerRecipeRichShaping(output.RootResourceType, "root", scope, output.Pivots, output.Aggregates, output.Slices) + if err != nil { + return OutputPlan{}, fmt.Errorf("root rich shaping: %w", err) + } + for index, field := range output.Fields { + normalized, err := normalizeRecipeProjection(field, scope, fmt.Sprintf("fields[%d]", index)) + if err != nil { + return OutputPlan{}, fmt.Errorf("field %q: %w", field.Name, err) + } + plan.Fields = append(plan.Fields, normalized.projection) + plan.DeclaredOrder = append(plan.DeclaredOrder, field.Name) + plan.Root.Fields = append(plan.Root.Fields, normalized.field) + } + for index, traversal := range output.Traversals { + child, err := buildRecipeTraversal(traversal, scope, fmt.Sprintf("traversals[%d]", index)) + if err != nil { + return OutputPlan{}, err + } + plan.Root.Children = append(plan.Root.Children, child) + } + if output.Identity != nil { + x, err := scope.expression(output.Identity.Expr, "identity.expr") + if err != nil { + return OutputPlan{}, err + } + if x.Type.Cardinality == expression.Many || x.Type.Kind == expression.KindObject || x.Type.Kind == expression.KindNull { + return OutputPlan{}, fmt.Errorf("identity expression must resolve to one scalar value") + } + plan.Identity = &x + } + dynamicMaps, err := buildRecipeDynamicMaps(output.DynamicColumns, scope, "dynamicColumns", "", output.RootResourceType) + if err != nil { + return OutputPlan{}, err + } + plan.DynamicMaps = append(plan.DynamicMaps, dynamicMaps...) + plan.Root.DynamicMaps = append(plan.Root.DynamicMaps, dynamicMaps...) + _ = bindings // retained in RecipePlan; output compilation is scope-only + return plan, nil +} + +func buildRecipeTraversal(input recipe.Traversal, parent scopeFrame, path string) (SemanticNode, error) { + if !fhirschema.HasResource(input.ToResourceType) { + return SemanticNode{}, fmt.Errorf("%s: target resource type %q is not represented by the active generated FHIR schema", path, input.ToResourceType) + } + alias := input.Alias + if strings.TrimSpace(alias) == "" { + alias = input.Name + } + input = qualifyTraversalLocals(input, alias, parent.aliases) + scope, err := parent.child(alias, scopeBinding{ResourceType: input.ToResourceType}) + if err != nil { + return SemanticNode{}, fmt.Errorf("%s: %w", path, err) + } + matchMode, err := NormalizeRecipeMatchMode(input.MatchMode) + if err != nil { + return SemanticNode{}, fmt.Errorf("%s.matchMode: %w", path, err) + } + node := SemanticNode{Alias: alias, ResourceType: input.ToResourceType, EdgeLabel: input.Name, MatchMode: matchMode} + dynamicMaps, err := buildRecipeDynamicMaps(input.DynamicColumns, scope, path+".dynamicColumns", alias, input.ToResourceType) + if err != nil { + return SemanticNode{}, err + } + node.DynamicMaps = dynamicMaps + node.Filters, err = LowerRecipeFiltersForAlias(input.ToResourceType, alias, input.Filters) + if err != nil { + return SemanticNode{}, fmt.Errorf("%s.filters: %w", path, err) + } + node.Pivots, node.Aggregates, node.Slices, err = lowerRecipeRichShaping(input.ToResourceType, alias, scope, input.Pivots, input.Aggregates, input.Slices) + if err != nil { + return SemanticNode{}, fmt.Errorf("%s rich shaping: %w", path, err) + } + if input.From != nil { + x, err := parent.expression(*input.From, path+".from") + if err != nil { + return SemanticNode{}, err + } + node.From = &x + } + for index, field := range input.Fields { + normalized, err := normalizeRecipeProjection(field, scope, fmt.Sprintf("%s.fields[%d]", path, index)) + if err != nil { + return SemanticNode{}, err + } + node.Fields = append(node.Fields, normalized.field) + } + for index, child := range input.Traversals { + nested, err := buildRecipeTraversal(child, scope, fmt.Sprintf("%s.traversals[%d]", path, index)) + if err != nil { + return SemanticNode{}, err + } + node.Children = append(node.Children, nested) + } + return node, nil +} + +func buildRecipeDynamicMaps(items []recipe.DynamicColumn, scope scopeFrame, path, scopeAlias, resourceType string) ([]SemanticDynamicMap, error) { + result := make([]SemanticDynamicMap, 0, len(items)) + for index, dynamic := range items { + columns := []string(nil) + if dynamic.Columns != nil { + // Preserve non-nil empty slices: the resolver uses that distinction + // to mark an optional family as resolved with zero discovered keys. + columns = append([]string{}, dynamic.Columns...) + } + item := SemanticDynamicMap{Name: dynamic.Name, ScopeAlias: scopeAlias, ResourceType: resourceType, Columns: columns, MaxColumns: dynamic.MaxColumns} + var err error + item.Source, err = scope.expression(dynamic.Source, fmt.Sprintf("%s[%d].source", path, index)) + if err != nil { + return nil, err + } + if item.Source.Type.Cardinality != expression.Many { + return nil, fmt.Errorf("%s[%d].source must be repeated", path, index) + } + dynamicScope := scope + if selector := item.Source.Expression.Selector; selector != nil && strings.Contains(selector.Path, "[]") { + binding, scopeErr := scopeBindingForSelector(scope, *selector) + if scopeErr != nil { + return nil, scopeErr + } + selectorPath := strings.TrimPrefix(strings.TrimSpace(selector.Path), ".") + prefix := strings.TrimPrefix(binding.Prefix+"."+selectorPath, ".") + dynamicScope, err = scope.child("item", scopeBinding{ResourceType: binding.ResourceType, Prefix: prefix, ExpandedItem: true}) + if err != nil { + return nil, fmt.Errorf("%s[%d] item scope: %w", path, index, err) + } + } + if dynamic.Key != nil { + x, err := dynamicScope.expression(*dynamic.Key, fmt.Sprintf("%s[%d].key", path, index)) + if err != nil { + return nil, err + } + if x.Type.Cardinality == expression.Many || (x.Type.Kind != expression.KindString && x.Type.Kind != expression.KindCode) { + return nil, fmt.Errorf("%s[%d].key must be a scalar string or code", path, index) + } + item.Key = &x + } + if dynamic.Value != nil { + x, err := dynamicScope.expression(*dynamic.Value, fmt.Sprintf("%s[%d].value", path, index)) + if err != nil { + return nil, err + } + item.Value = &x + } + result = append(result, item) + } + return result, nil +} + +func scopeBindingForSelector(scope scopeFrame, ref expression.SelectorRef) (scopeBinding, error) { + alias := ref.Context + if alias == "" { + alias = "root" + } + binding, ok := scope.aliases[alias] + if !ok { + return scopeBinding{}, fmt.Errorf("selector context %q is not in scope", alias) + } + return binding, nil +} + +func cloneBindings(in recipe.RuntimeBindings) recipe.RuntimeBindings { + in.AuthResourcePaths = append([]string(nil), in.AuthResourcePaths...) + in.OutputNames = append([]string(nil), in.OutputNames...) + return in +} + +func keys(values map[string]scopeBinding) map[string]struct{} { + result := make(map[string]struct{}, len(values)) + for key := range values { + result[key] = struct{}{} + } + return result +} diff --git a/internal/dataframe/semantic/recipe_plan_test.go b/internal/dataframe/semantic/recipe_plan_test.go new file mode 100644 index 0000000..daec6af --- /dev/null +++ b/internal/dataframe/semantic/recipe_plan_test.go @@ -0,0 +1,147 @@ +package semantic + +import ( + "context" + "strings" + "testing" + + "github.com/calypr/loom/internal/dataframe/expression" + "github.com/calypr/loom/internal/dataframe/recipe" + "github.com/calypr/loom/internal/dataframe/recipe/schema" +) + +func TestDefaultRecipeBuildsTypedSemanticPlan(t *testing.T) { + bundle := resolvedDefaultACEDBundle(t) + plan, err := BuildRecipePlan(bundle, recipe.RuntimeBindings{Project: "project"}) + if err != nil { + t.Fatal(err) + } + if len(plan.Outputs) != len(bundle.Outputs) || plan.RecipeDigest == "" { + t.Fatalf("unexpected plan: %#v", plan) + } + group := plan.Outputs[len(plan.Outputs)-1] + if group.Unnest == nil || group.Unnest.As != "member" || group.Unnest.JoinMode != UnnestInner || group.Identity == nil { + t.Fatalf("group expansion/identity missing: %#v", group) + } + if got := group.Fields[1].Expr.Type.Kind; got != "string" { + t.Fatalf("member_id type = %s", got) + } + if group.Fields[1].Expr.Expression.Call == nil || group.Fields[1].Expr.Expression.Call.Args[0].Selector == nil || group.Fields[1].Expr.Expression.Call.Args[0].Selector.Context != "member" { + t.Fatalf("member context = %#v", group.Fields[1].Expr.Expression) + } +} + +func resolvedDefaultACEDBundle(t *testing.T) recipe.Bundle { + t.Helper() + bundle, err := recipe.DefaultACEDBundle() + if err != nil { + t.Fatal(err) + } + resourceTypes := []string{"DocumentReference", "Specimen", "Patient", "ResearchStudy", "Observation", "ResearchSubject", "Condition", "MedicationAdministration", "Group"} + byType := make(map[string][]schema.FieldCandidate, len(resourceTypes)) + for _, resourceType := range resourceTypes { + byType[resourceType] = []schema.FieldCandidate{ + {ResourceType: resourceType, Path: "id", Kind: "scalar"}, + {ResourceType: resourceType, Path: "identifier[].system", Kind: "scalar", DistinctValues: []string{"system"}}, + {ResourceType: resourceType, Path: "identifier[].value", Kind: "scalar"}, + {ResourceType: resourceType, Path: "extension[].url", Kind: "scalar", DistinctValues: []string{"url"}}, + } + if resourceType == "Observation" { + byType[resourceType] = append(byType[resourceType], + schema.FieldCandidate{ResourceType: resourceType, Path: "component[].code.text", Kind: "scalar", DistinctValues: []string{"component"}}, + schema.FieldCandidate{ResourceType: resourceType, Path: "component[].valueString", Kind: "scalar"}, + schema.FieldCandidate{ResourceType: resourceType, Path: "component[].valueInteger", Kind: "scalar"}, + schema.FieldCandidate{ResourceType: resourceType, Path: "component[].valueBoolean", Kind: "scalar"}, + schema.FieldCandidate{ResourceType: resourceType, Path: "component[].valueDateTime", Kind: "scalar"}, + schema.FieldCandidate{ResourceType: resourceType, Path: "component[].valueQuantity.value", Kind: "scalar"}, + schema.FieldCandidate{ResourceType: resourceType, Path: "code", Kind: "codeable_concept", PivotCandidate: true, PivotFamily: "observation_code_value", PivotColumns: []string{"code"}, PivotColumnSelect: "code.coding[].display", PivotValueSelect: "valueString"}, + schema.FieldCandidate{ResourceType: resourceType, Path: "component[].code", Kind: "codeable_concept", PivotCandidate: true, PivotFamily: "codeable_concept", PivotColumns: []string{"component"}, PivotColumnSelect: "component[].code.coding[].display", PivotValueSelect: "component[].code.coding[].display"}, + ) + } + } + resolved, err := schema.Resolve(context.Background(), bundle, schema.Scope{Project: "test", DatasetGeneration: "generation"}, testDefaultDiscovery{fields: byType}) + if err != nil { + t.Fatal(err) + } + return resolved.Bundle +} + +type testDefaultDiscovery struct { + fields map[string][]schema.FieldCandidate +} + +func (d testDefaultDiscovery) Fields(_ context.Context, _ schema.Scope, resourceType string) ([]schema.FieldCandidate, error) { + return append([]schema.FieldCandidate(nil), d.fields[resourceType]...), nil +} + +func TestSemanticUnnestRejectsNonRepeatedAndUnsafeBindings(t *testing.T) { + base := SemanticUnnest{ + Source: SemanticExpression{Type: expression.Type{Kind: expression.KindString, Cardinality: expression.OptionalOne}}, + As: "item", JoinMode: UnnestInner, + } + if err := base.Validate(); err == nil || !strings.Contains(err.Error(), "repeated") { + t.Fatalf("expected repeated-source validation error, got %v", err) + } + base.Source.Type.Cardinality = expression.Many + base.As = "item.value" + if err := base.Validate(); err == nil || !strings.Contains(err.Error(), "safe logical name") { + t.Fatalf("expected safe-binding validation error, got %v", err) + } +} + +func TestSemanticUnnestOuterModeAndOrdinalityAreExplicit(t *testing.T) { + unnest := SemanticUnnest{ + Source: SemanticExpression{Type: expression.Type{Kind: expression.KindObject, Cardinality: expression.Many}}, + As: "item", Ordinality: "item_index", JoinMode: UnnestOuter, + } + if err := unnest.Validate(); err != nil { + t.Fatal(err) + } + if unnest.JoinMode != UnnestOuter || unnest.Ordinality != "item_index" { + t.Fatalf("unnest mode/ordinality changed: %#v", unnest) + } +} + +func TestSemanticUnnestRejectsBindingCollision(t *testing.T) { + unnest := SemanticUnnest{ + Source: SemanticExpression{Type: expression.Type{Kind: expression.KindObject, Cardinality: expression.Many}}, + As: "item", Ordinality: "item", JoinMode: UnnestInner, + } + if err := unnest.Validate(); err == nil || !strings.Contains(err.Error(), "differ") { + t.Fatalf("expected ordinality collision error, got %v", err) + } +} + +func TestDynamicItemExpressionsUseItemScope(t *testing.T) { + bundle := recipe.Bundle{RecipeSchemaVersion: 1, Name: "dynamic-item", TranslationVersion: "1", Outputs: []recipe.Output{{Name: "x", RootResourceType: "Patient", RowGrain: "patient", DynamicColumns: []recipe.DynamicColumn{{Name: "extension", Source: recipe.Expression{Select: "root.extension[]"}, Key: &recipe.Expression{Select: "item.url"}, Value: &recipe.Expression{Select: "item.url"}, Columns: []string{"http://example.org/code"}}}}}} + if _, err := BuildRecipePlan(bundle, recipe.RuntimeBindings{Project: "p"}); err != nil { + t.Fatalf("dynamic item expressions were not scoped: %v", err) + } +} + +func TestRecipeRejectsUndefinedAndShadowedAliases(t *testing.T) { + base := `{"recipeSchemaVersion":1,"name":"x","translationVersion":"1","outputs":[{"name":"x","rootResourceType":"Patient","rowGrain":"patient","fields":[{"name":"id","expr":{"select":"missing.id"}}]}]}` + bundle, err := recipe.Parse([]byte(base)) + if err != nil { + t.Fatal(err) + } + if _, err := BuildRecipePlan(bundle, recipe.RuntimeBindings{Project: "p"}); err == nil || !strings.Contains(err.Error(), "context") { + t.Fatalf("expected undefined context error, got %v", err) + } + + shadowed := `{"recipeSchemaVersion":1,"name":"x","translationVersion":"1","outputs":[{"name":"x","rootResourceType":"Patient","rowGrain":"patient","fields":[{"name":"id","expr":{"select":"root.id"}}],"traversals":[{"name":"subject","alias":"root","toResourceType":"Patient"}]}]}` + bundle, err = recipe.Parse([]byte(shadowed)) + if err != nil { + t.Fatal(err) + } + if _, err := BuildRecipePlan(bundle, recipe.RuntimeBindings{Project: "p"}); err == nil || !strings.Contains(err.Error(), "shadows") { + t.Fatalf("expected shadowing error, got %v", err) + } +} + +func TestRecipeRejectsRepeatedIdentity(t *testing.T) { + bundle := recipe.Bundle{RecipeSchemaVersion: 1, Name: "x", TranslationVersion: "1", Outputs: []recipe.Output{{Name: "x", RootResourceType: "Patient", RowGrain: "expanded", Expand: &recipe.Expansion{From: recipe.Expression{Select: "identifier[]"}, As: "item"}, Identity: &recipe.Identity{Name: "id", Expr: recipe.Expression{Select: "root.identifier[].value"}}}}} + if _, err := BuildRecipePlan(bundle, recipe.RuntimeBindings{Project: "p"}); err == nil || !strings.Contains(err.Error(), "scalar") { + t.Fatalf("expected scalar identity error, got %v", err) + } +} diff --git a/internal/dataframe/semantic/recipe_plan_types.go b/internal/dataframe/semantic/recipe_plan_types.go new file mode 100644 index 0000000..4918829 --- /dev/null +++ b/internal/dataframe/semantic/recipe_plan_types.go @@ -0,0 +1,203 @@ +package semantic + +// This file is the single semantic boundary for persisted recipes and the +// existing GraphQL dataframe request. It deliberately stops before physical +// lowering: no collection, AQL, SQL, or backend implementation detail belongs +// in these types. + +import ( + "fmt" + "regexp" + "strings" + + "github.com/calypr/loom/internal/dataframe/expression" + "github.com/calypr/loom/internal/dataframe/recipe" +) + +var semanticBindingNamePattern = regexp.MustCompile(`^[A-Za-z_][A-Za-z0-9_]*$`) + +// RecipePlan is an immutable, checked semantic representation of a recipe +// bundle. Runtime bindings are request-scoped and are intentionally excluded +// from the persisted recipe digest. +type RecipePlan struct { + Version int + RecipeDigest string + TranslationVersion string + Bindings recipe.RuntimeBindings + Outputs []OutputPlan +} + +type OutputPlan struct { + Name string + Root SemanticNode + RootResourceType string + RowGrain RowGrain + Identity *SemanticExpression + // Unnest is the canonical semantic row-producing operation for an output. + // Persisted recipe expand syntax is lowered into this operation; consumers + // must not infer cardinality-changing behavior from a transport-facing + // explanation field; the typed UNNEST operation is authoritative. + Unnest *SemanticUnnest + Fields []SemanticProjection + DynamicMaps []SemanticDynamicMap + CatalogProjections []string + Collision string + DeclaredOrder []string +} + +// SemanticExpression keeps the checked typed AST together with the logical +// source location and lexical context that produced it. SourcePath is a +// recipe JSON path for diagnostics, not a physical query fragment. +type SemanticExpression struct { + Expression expression.Expression + Type expression.Type + SourcePath string + Context string +} + +type SemanticProjection struct { + Name string + FieldRef string + ValueMode string + Expr SemanticExpression +} + +// UnnestJoinMode makes null/empty collection behavior explicit at the +// semantic boundary. The renderer must not infer this from the AQL context. +type UnnestJoinMode string + +const ( + UnnestInner UnnestJoinMode = "INNER" + UnnestOuter UnnestJoinMode = "OUTER" +) + +// SemanticUnnest is a backend-neutral row-producing operation. It is the +// semantic representation of recipe expand syntax and is available to any +// frontend that needs one output row per element of a repeated value. +// +// Source is evaluated once per parent row. As introduces the item binding; +// Ordinality, when non-empty, introduces a deterministic zero-based position +// binding. The operation changes row cardinality and therefore is not a +// projection or ordinary expression. +type SemanticUnnest struct { + Source SemanticExpression + As string + Ordinality string + JoinMode UnnestJoinMode +} + +// Validate checks the semantic invariants that are independent of a backend +// renderer. Lexical scope validation belongs to the caller because only the +// surrounding output scope knows which bindings are visible. +func (u SemanticUnnest) Validate() error { + if u.Source.Type.Cardinality != expression.Many { + return fmt.Errorf("unnest source must be a repeated expression, got %s", u.Source.Type) + } + if strings.TrimSpace(u.As) == "" { + return fmt.Errorf("unnest item binding is required") + } + if !semanticBindingNamePattern.MatchString(u.As) { + return fmt.Errorf("unnest item binding %q is not a safe logical name", u.As) + } + if strings.TrimSpace(u.Ordinality) != "" { + if !semanticBindingNamePattern.MatchString(u.Ordinality) { + return fmt.Errorf("unnest ordinality binding %q is not a safe logical name", u.Ordinality) + } + if u.Ordinality == u.As { + return fmt.Errorf("unnest ordinality binding must differ from item binding %q", u.As) + } + } + switch u.JoinMode { + case UnnestInner, UnnestOuter: + return nil + case "": + return fmt.Errorf("unnest join mode is required") + default: + return fmt.Errorf("unsupported unnest join mode %q", u.JoinMode) + } +} + +type SemanticDynamicMap struct { + Name string + ScopeAlias string + ResourceType string + Source SemanticExpression + Key *SemanticExpression + Value *SemanticExpression + Columns []string + MaxColumns int +} + +// RecipePlanExplanation is stable diagnostic output and contains only logical +// types and source paths. It never exposes a backend query or storage name. +type RecipePlanExplanation struct { + Version int + RecipeDigest string + TranslationVersion string + Outputs []OutputPlanExplanation +} + +type OutputPlanExplanation struct { + Name string + Root string + RowGrain RowGrain + Fields []ExpressionExplanation + Identity *ExpressionExplanation + Unnest *UnnestExplanation + Expansion *ExpansionExplanation + DynamicMap []string + CatalogProjections []string +} + +type ExpressionExplanation struct { + SourcePath string + Context string + Type expression.Type + Kind expression.NodeKind +} + +type ExpansionExplanation struct { + SourcePath string + As string +} + +type UnnestExplanation struct { + SourcePath string + As string + Ordinality string + JoinMode UnnestJoinMode +} + +// Explain returns a backend-neutral summary suitable for API diagnostics. +func (p RecipePlan) Explain() RecipePlanExplanation { + out := RecipePlanExplanation{Version: p.Version, RecipeDigest: p.RecipeDigest, TranslationVersion: p.TranslationVersion, Outputs: make([]OutputPlanExplanation, 0, len(p.Outputs))} + for _, output := range p.Outputs { + e := OutputPlanExplanation{Name: output.Name, Root: output.RootResourceType, RowGrain: output.RowGrain, DynamicMap: make([]string, 0, len(output.DynamicMaps)), CatalogProjections: append([]string(nil), output.CatalogProjections...)} + for _, field := range output.Fields { + e.Fields = append(e.Fields, explainExpression(field.Expr)) + } + if output.Identity != nil { + x := explainExpression(*output.Identity) + e.Identity = &x + } + if output.Unnest != nil { + e.Unnest = &UnnestExplanation{SourcePath: output.Unnest.Source.SourcePath, As: output.Unnest.As, Ordinality: output.Unnest.Ordinality, JoinMode: output.Unnest.JoinMode} + } + if output.Unnest != nil { + e.Expansion = &ExpansionExplanation{SourcePath: output.Unnest.Source.SourcePath, As: output.Unnest.As} + } + for _, dynamic := range output.DynamicMaps { + e.DynamicMap = append(e.DynamicMap, dynamic.Name) + } + out.Outputs = append(out.Outputs, e) + } + return out +} + +func explainExpression(e SemanticExpression) ExpressionExplanation { + return ExpressionExplanation{SourcePath: e.SourcePath, Context: e.Context, Type: e.Type, Kind: e.Expression.Kind} +} + +// BuildRecipePlan lowers and type-checks every expression in a stored bundle. +// The recipe remains data; no output name or resource-specific branch is used +// here. Type resolution is lexical and schema-backed. diff --git a/internal/dataframe/semantic/recipe_projection.go b/internal/dataframe/semantic/recipe_projection.go new file mode 100644 index 0000000..c5ce08f --- /dev/null +++ b/internal/dataframe/semantic/recipe_projection.go @@ -0,0 +1,182 @@ +package semantic + +// This file owns the recipe-to-semantic projection boundary. Recipe fields +// are deliberately normalized here, before physical lowering, so selector +// fallbacks continue to use the same typed extraction contract as recipes +// fields. In particular, this code must not choose a renderer or construct +// backend expressions. + +import ( + "fmt" + "strings" + + "github.com/calypr/loom/internal/dataframe/expression" + "github.com/calypr/loom/internal/dataframe/recipe" + "github.com/calypr/loom/internal/dataframe/spec" +) + +// buildRecipeProjection checks one recipe field in the supplied lexical scope +// and converts it to the semantic projection consumed by generic lowering. +// The function is intentionally kept separate from BuildRecipePlan so the +// root and traversal builders can use exactly the same fallback/value-mode +// rules, including slice fields. +// +// Schema-version-one fallbacks are selector-only. Selector-only fields retain +// SemanticField.Selector/Fallbacks, which lets the physical lowerer preserve +// typed selector specialization and prepared fallback reuse. A non-selector +// field is still checked through the generic expression AST; its explicit +// value mode is represented by an AST call rather than renderer branching. + +type normalizedRecipeProjection struct { + projection SemanticProjection + field SemanticField +} + +func normalizeRecipeProjection(field recipe.Field, scope scopeFrame, path string) (normalizedRecipeProjection, error) { + if strings.TrimSpace(field.Name) == "" { + return normalizedRecipeProjection{}, fmt.Errorf("%s.name is required", path) + } + primary, err := scope.expression(field.Expr, path+".expr") + if err != nil { + return normalizedRecipeProjection{}, err + } + + fallbacks := make([]SemanticExpression, 0, len(field.Fallbacks)) + for index, input := range field.Fallbacks { + fallback, err := scope.expression(input, fmt.Sprintf("%s.fallbacks[%d]", path, index)) + if err != nil { + return normalizedRecipeProjection{}, err + } + if fallback.Expression.Selector == nil { + return normalizedRecipeProjection{}, fmt.Errorf("%s.fallbacks[%d] must be a selector expression", path, index) + } + fallbacks = append(fallbacks, fallback) + } + + // A selector primary plus selector fallbacks is the optimized physical + // representation. Keep the checked expression as the projection source, + // while copying selectors into SemanticField below for generic lowering. + if primary.Expression.Selector != nil { + selector, err := recipeSelector(primary.Expression) + if err != nil { + return normalizedRecipeProjection{}, fmt.Errorf("%s: %w", path, err) + } + fallbackSelectors := make([]Selector, 0, len(fallbacks)) + for index, fallback := range fallbacks { + parsed, err := recipeSelector(fallback.Expression) + if err != nil { + return normalizedRecipeProjection{}, fmt.Errorf("%s.fallbacks[%d]: %w", path, index, err) + } + fallbackSelectors = append(fallbackSelectors, parsed) + } + fieldSemantic := SemanticField{ + Name: field.Name, Selector: selector, + Fallbacks: fallbackSelectors, ValueMode: string(field.ValueMode), + Expr: &primary.Expression, ExprType: primary.Type, SourcePath: primary.SourcePath, + } + if err := validateRecipeProjectionTypes(primary.Type, fallbacks); err != nil { + return normalizedRecipeProjection{}, fmt.Errorf("%s: %w", path, err) + } + if _, err := ResolveSemanticField(resourceTypeForScope(scope, primary.Expression), selectorContext(primary.Expression), 0, fieldSemantic); err != nil { + // ResolveSemanticField needs the resource type owning the selector. A + // malformed/unknown context is already rejected by scope.expression; + // retain its useful diagnostic here rather than silently weakening the + // cardinality contract. + return normalizedRecipeProjection{}, fmt.Errorf("%s: %w", path, err) + } + return normalizedRecipeProjection{projection: SemanticProjection{ + Name: field.Name, ValueMode: string(field.ValueMode), Expr: primary, + }, field: fieldSemantic}, nil + } + + if len(fallbacks) != 0 { + return normalizedRecipeProjection{}, fmt.Errorf("%s.fallbacks require a selector primary expression", path) + } + projected, err := applyRecipeValueMode(scope, primary, field.ValueMode) + if err != nil { + return normalizedRecipeProjection{}, fmt.Errorf("%s: %w", path, err) + } + return normalizedRecipeProjection{projection: SemanticProjection{Name: field.Name, ValueMode: string(field.ValueMode), Expr: projected}, field: SemanticField{ + Name: field.Name, ValueMode: string(field.ValueMode), Expr: &projected.Expression, ExprType: projected.Type, SourcePath: projected.SourcePath, + }}, nil +} + +// recipeProjectionField returns the canonical selector-bearing SemanticField +// for a projection. For non-selector expressions it returns a field whose +// Expr is the checked expression; physical expression lowering owns that +// case, while selector lowering remains schema-backed. +func recipeProjectionField(field recipe.Field, scope scopeFrame, path string) (SemanticField, error) { + result, err := normalizeRecipeProjection(field, scope, path) + if err != nil { + return SemanticField{}, err + } + return result.field, nil +} + +// recipeSelector converts a checked expression selector to the schema selector +// used by the existing semantic and physical extraction contracts. +func recipeSelector(input expression.Expression) (Selector, error) { + if input.Selector == nil { + return Selector{}, fmt.Errorf("expression is not a selector") + } + selector, err := spec.ParseSelector(input.Selector.Path) + if err != nil { + return Selector{}, err + } + return selector, nil +} + +func selectorContext(input expression.Expression) string { + if input.Selector == nil || strings.TrimSpace(input.Selector.Context) == "" { + return "root" + } + return input.Selector.Context +} + +func resourceTypeForScope(scope scopeFrame, input expression.Expression) string { + alias := selectorContext(input) + if binding, ok := scope.aliases[alias]; ok { + return binding.ResourceType + } + return "" +} + +func validateRecipeProjectionTypes(primary expression.Type, fallbacks []SemanticExpression) error { + for index, fallback := range fallbacks { + if fallback.Type.Kind == expression.KindNull || primary.Kind == expression.KindNull { + continue + } + if primary.Kind != fallback.Type.Kind { + return fmt.Errorf("fallback %d type %s is incompatible with primary type %s", index, fallback.Type, primary) + } + } + return nil +} + +// applyRecipeValueMode applies an explicit value mode to a non-selector +// expression using the typed expression AST. Selector expressions intentionally +// bypass this function so their optimized PhysicalExtract representation keeps +// fallback and selector specialization metadata. +func applyRecipeValueMode(scope scopeFrame, input SemanticExpression, mode recipe.ValueMode) (SemanticExpression, error) { + normalized := mode.Normalized() + if normalized == recipe.ValueModeAuto && input.Type.Cardinality != expression.Many { + return input, nil + } + name := "" + switch normalized { + case recipe.ValueModeAuto, recipe.ValueModeFirst: + name = "first" + case recipe.ValueModeAll: + name = "all" + case recipe.ValueModeDistinct: + name = "distinct" + default: + return SemanticExpression{}, fmt.Errorf("unsupported value mode %q", mode) + } + wrapper := expression.Function(name, input.Expression) + checked, err := wrapper.Check(expression.TypeContext{Resolve: scope.resolve}) + if err != nil { + return SemanticExpression{}, err + } + return SemanticExpression{Expression: checked.Expression, Type: checked.Type, SourcePath: input.SourcePath, Context: input.Context}, nil +} diff --git a/internal/dataframe/semantic/recipe_rich_shaping.go b/internal/dataframe/semantic/recipe_rich_shaping.go new file mode 100644 index 0000000..6a78c8b --- /dev/null +++ b/internal/dataframe/semantic/recipe_rich_shaping.go @@ -0,0 +1,401 @@ +package semantic + +// This file owns the recipe rich-shaping boundary. Pivots, aggregates, and +// representative slices are converted into canonical semantic operations. Nothing here chooses a traversal strategy or emits +// a backend expression; those decisions remain in the common compiler. + +import ( + "fmt" + "regexp" + "strings" + + "github.com/calypr/loom/fhirschema" + "github.com/calypr/loom/internal/dataframe/recipe" + "github.com/calypr/loom/internal/dataframe/spec" +) + +var recipeRichNamePattern = regexp.MustCompile(`^[A-Za-z_][A-Za-z0-9_]*$`) + +// lowerRecipePivots converts bounded recipe pivots into canonical semantic +// pivots. Both expressions must be selectors owned by the containing node; +// schema metadata, rather than a client-supplied family, proves that the +// selector pair is supported. +func lowerRecipePivots(resourceType, alias string, scope scopeFrame, pivots []recipe.Pivot) ([]SemanticPivot, error) { + if !fhirschema.HasResource(resourceType) { + return nil, fmt.Errorf("pivot resource type %q is not represented by the active generated FHIR schema", resourceType) + } + out := make([]SemanticPivot, 0, len(pivots)) + seen := make(map[string]struct{}, len(pivots)) + for index, input := range pivots { + path := fmt.Sprintf("pivots[%d]", index) + if err := validateRecipeRichName(input.Name, path+".name"); err != nil { + return nil, err + } + if _, exists := seen[input.Name]; exists { + return nil, fmt.Errorf("%s.name %q is duplicated", path, input.Name) + } + seen[input.Name] = struct{}{} + if len(input.Columns) == 0 { + return nil, fmt.Errorf("%s.columns must contain at least one bounded column", path) + } + columns := make([]string, len(input.Columns)) + columnNames := make(map[string]struct{}, len(input.Columns)) + for columnIndex, column := range input.Columns { + columnPath := fmt.Sprintf("%s.columns[%d]", path, columnIndex) + if strings.TrimSpace(column) == "" { + return nil, fmt.Errorf("%s must not be empty", columnPath) + } + if _, exists := columnNames[column]; exists { + return nil, fmt.Errorf("%s is duplicated", columnPath) + } + columnNames[column] = struct{}{} + columns[columnIndex] = column + } + + column, err := recipeNodeSelector(resourceType, alias, scope, input.ColumnExpr, path+".columnExpr") + if err != nil { + return nil, err + } + value, err := recipeNodeSelector(resourceType, alias, scope, input.ValueExpr, path+".valueExpr") + if err != nil { + return nil, err + } + validationResourceType := resourceType + validationColumn := column + validationValue := value + if input.ItemResourceType != "" { + validationResourceType = input.ItemResourceType + validationColumn, err = relativePivotSelector(column, input.ItemSource.Select) + if err != nil { + return nil, fmt.Errorf("%s column selector: %w", path, err) + } + validationValue, err = relativePivotSelector(value, input.ItemSource.Select) + if err != nil { + return nil, fmt.Errorf("%s value selector: %w", path, err) + } + } + pivotSpec, err := fhirschema.ValidatePivotSelectors(validationResourceType, selectorSpecFromSelector(validationColumn), selectorSpecFromSelector(validationValue)) + if err != nil { + return nil, fmt.Errorf("%s: %w", path, err) + } + if input.ItemResourceType != "" { + pivotSpec.ItemResourceType = input.ItemResourceType + pivotSpec.ItemSourcePath = selectorPath(input.ItemSource.Select) + } + itemResourceType := pivotSpec.ItemResourceType + if pivotSpec.ItemSourcePath == "" { + // A resource-level pivot has no nested item scope; leave the item + // type empty so the physical renderer uses its singleton-document + // lowering rather than requiring an item-source selector. + itemResourceType = "" + } else if itemResourceType == "" { + itemResourceType = resourceType + } + itemSource := Selector{} + if pivotSpec.ItemSourcePath != "" { + itemSource, err = spec.ParseSelector(pivotSpec.ItemSourcePath) + if err != nil { + return nil, fmt.Errorf("%s item source: %w", path, err) + } + if err := validateSemanticSelector(resourceType, itemSource); err != nil { + return nil, fmt.Errorf("%s item source: %w", path, err) + } + } + if pivotSpec.ItemSourcePath != "" { + // The schema owns the relative selector conversion. Reparse the + // normalized selectors so component pivots are evaluated against one + // generated backbone item rather than independently against the + // containing Observation. + column, err = spec.ParseSelector(fhirschema.SelectorExpression(pivotSpec.ColumnSelector)) + if err != nil { + return nil, fmt.Errorf("%s column selector: %w", path, err) + } + value, err = spec.ParseSelector(fhirschema.SelectorExpression(pivotSpec.ValueSelector)) + if err != nil { + return nil, fmt.Errorf("%s value selector: %w", path, err) + } + } + fallbacks := make([]Selector, 0, len(pivotSpec.ValueSelectors)) + if len(input.ValueFallbacks) > 0 { + for index, fallbackInput := range input.ValueFallbacks { + fallback, fallbackErr := recipeNodeSelector(resourceType, alias, scope, fallbackInput, fmt.Sprintf("%s.valueFallbacks[%d]", path, index)) + if fallbackErr != nil { + return nil, fallbackErr + } + if input.ItemResourceType != "" { + fallback, fallbackErr = relativePivotSelector(fallback, input.ItemSource.Select) + if fallbackErr != nil { + return nil, fmt.Errorf("%s value selector fallback %d: %w", path, index, fallbackErr) + } + } + fallbacks = append(fallbacks, fallback) + } + } else { + for index, alternative := range pivotSpec.ValueSelectors { + if index == 0 { + continue + } + fallback, parseErr := spec.ParseSelector(fhirschema.SelectorExpression(alternative)) + if parseErr != nil { + return nil, fmt.Errorf("%s value selector fallback %d: %w", path, index, parseErr) + } + fallbacks = append(fallbacks, fallback) + } + } + out = append(out, SemanticPivot{ + Name: input.Name, + FieldRef: input.FieldRef, + ColumnSelector: column, + ValueSelector: value, + ValueFallbacks: fallbacks, + ItemSource: itemSource, + ItemResourceType: itemResourceType, + Columns: columns, + Family: pivotSpec.Family, + }) + } + return out, nil +} + +func relativePivotSelector(selector Selector, source string) (Selector, error) { + if source == "" { + return selector, nil + } + selectorPath := selector.CanonicalPath() + prefix := selectorPathFromExpression(source) + if prefix == "" { + return selector, nil + } + if selectorPath == prefix { + return Selector{}, fmt.Errorf("pivot selector cannot be the item source itself") + } + if !strings.HasPrefix(selectorPath, prefix+".") { + return Selector{}, fmt.Errorf("selector %q is outside item source %q", selectorPath, prefix) + } + return spec.ParseSelector(strings.TrimPrefix(selectorPath, prefix+".")) +} + +func selectorPathFromExpression(input string) string { + input = strings.TrimSpace(input) + if index := strings.Index(input, "."); index >= 0 { + return strings.TrimPrefix(input[index+1:], ".") + } + return "" +} + +func selectorPath(input string) string { + return selectorPathFromExpression(input) +} + +// lowerRecipeAggregates converts recipe aggregate declarations into the +// canonical aggregate representation. Version-one aggregate expressions are +// selector-only because SemanticAggregate intentionally stores selector +// metadata and the existing physical aggregate lowerer consumes that shape. +func lowerRecipeAggregates(resourceType, alias string, scope scopeFrame, aggregates []recipe.Aggregate) ([]SemanticAggregate, error) { + if !fhirschema.HasResource(resourceType) { + return nil, fmt.Errorf("aggregate resource type %q is not represented by the active generated FHIR schema", resourceType) + } + out := make([]SemanticAggregate, 0, len(aggregates)) + seen := make(map[string]struct{}, len(aggregates)) + for index, input := range aggregates { + path := fmt.Sprintf("aggregates[%d]", index) + if err := validateRecipeRichName(input.Name, path+".name"); err != nil { + return nil, err + } + if _, exists := seen[input.Name]; exists { + return nil, fmt.Errorf("%s.name %q is duplicated", path, input.Name) + } + seen[input.Name] = struct{}{} + operation := strings.ToUpper(strings.TrimSpace(string(input.Operation))) + if !recipe.AggregateOperation(operation).Valid() { + return nil, fmt.Errorf("%s.operation %q is unsupported", path, input.Operation) + } + if !input.ValueMode.Valid() { + return nil, fmt.Errorf("%s.valueMode %q is unsupported", path, input.ValueMode) + } + semanticAggregate := SemanticAggregate{ + Name: input.Name, + Operation: operation, + FieldRef: input.FieldRef, + ValueMode: string(input.ValueMode), + } + requiresSelector := operation == string(recipe.AggregateCountDistinct) || + operation == string(recipe.AggregateDistinctValues) || + operation == string(recipe.AggregateMin) || + operation == string(recipe.AggregateMax) + if input.Expr != nil { + if !requiresSelector { + return nil, fmt.Errorf("%s.expr is not accepted for operation %s", path, operation) + } + selector, err := recipeNodeSelector(resourceType, alias, scope, *input.Expr, path+".expr") + if err != nil { + return nil, err + } + semanticAggregate.Selector = &selector + } else if requiresSelector { + return nil, fmt.Errorf("%s.expr is required for operation %s", path, operation) + } + if input.Where != nil { + predicate, equals, err := lowerRecipePredicate(resourceType, alias, scope, input.Where, path+".where") + if err != nil { + return nil, err + } + semanticAggregate.Predicate = predicate + semanticAggregate.PredicateEquals = equals + } + out = append(out, semanticAggregate) + } + return out, nil +} + +// lowerRecipeSlices converts representative slices into canonical bounded +// slices. Slice fields use the ordinary recipe projection checker, but the +// canonical SemanticSlice representation intentionally remains selector-based +// so the existing physical slice lowerer can retain typed extraction, +// fallback, and value-mode behavior. +func lowerRecipeSlices(resourceType, alias string, scope scopeFrame, slices []recipe.RepresentativeSlice) ([]SemanticSlice, error) { + if !fhirschema.HasResource(resourceType) { + return nil, fmt.Errorf("slice resource type %q is not represented by the active generated FHIR schema", resourceType) + } + out := make([]SemanticSlice, 0, len(slices)) + seen := make(map[string]struct{}, len(slices)) + for index, input := range slices { + path := fmt.Sprintf("slices[%d]", index) + if err := validateRecipeRichName(input.Name, path+".name"); err != nil { + return nil, err + } + if _, exists := seen[input.Name]; exists { + return nil, fmt.Errorf("%s.name %q is duplicated", path, input.Name) + } + seen[input.Name] = struct{}{} + if input.Limit <= 0 { + return nil, fmt.Errorf("%s.limit must be positive", path) + } + if len(input.Fields) == 0 { + return nil, fmt.Errorf("%s.fields must contain at least one field", path) + } + semanticSlice := SemanticSlice{Name: input.Name, Limit: input.Limit, Fields: make([]SemanticField, 0, len(input.Fields))} + seenFields := make(map[string]struct{}, len(input.Fields)) + for fieldIndex, field := range input.Fields { + fieldPath := fmt.Sprintf("%s.fields[%d]", path, fieldIndex) + semanticField, err := recipeProjectionField(field, scope, fieldPath) + if err != nil { + return nil, err + } + if _, exists := seenFields[field.Name]; exists { + return nil, fmt.Errorf("%s.name %q is duplicated", fieldPath, field.Name) + } + seenFields[field.Name] = struct{}{} + if semanticField.Expr == nil || semanticField.Expr.Selector == nil { + return nil, fmt.Errorf("%s.expr must be a selector for canonical slice lowering", fieldPath) + } + if err := ensureSelectorOwnedByNode(semanticField.Expr.Selector.Context, alias, fieldPath+".expr"); err != nil { + return nil, err + } + semanticSlice.Fields = append(semanticSlice.Fields, semanticField) + } + if input.Where != nil { + predicate, equals, err := lowerRecipePredicate(resourceType, alias, scope, input.Where, path+".where") + if err != nil { + return nil, err + } + semanticSlice.Predicate = predicate + semanticSlice.PredicateEquals = equals + } + out = append(out, semanticSlice) + } + return out, nil +} + +// lowerRecipeRichShaping is the coordinated entrypoint used by the recipe +// node builder. Keeping the three conversions together makes it easy for the +// caller to attach them to exactly one semantic node while preserving the +// independent validation and tests for each operation family. +func lowerRecipeRichShaping(resourceType, alias string, scope scopeFrame, pivots []recipe.Pivot, aggregates []recipe.Aggregate, slices []recipe.RepresentativeSlice) ([]SemanticPivot, []SemanticAggregate, []SemanticSlice, error) { + pivotPlan, err := lowerRecipePivots(resourceType, alias, scope, pivots) + if err != nil { + return nil, nil, nil, err + } + aggregatePlan, err := lowerRecipeAggregates(resourceType, alias, scope, aggregates) + if err != nil { + return nil, nil, nil, err + } + slicePlan, err := lowerRecipeSlices(resourceType, alias, scope, slices) + if err != nil { + return nil, nil, nil, err + } + return pivotPlan, aggregatePlan, slicePlan, nil +} + +func recipeNodeSelector(resourceType, alias string, scope scopeFrame, input recipe.Expression, path string) (Selector, error) { + checked, err := scope.expression(input, path) + if err != nil { + return Selector{}, err + } + if checked.Expression.Selector == nil { + return Selector{}, fmt.Errorf("%s must be a selector expression", path) + } + if err := ensureSelectorOwnedByNode(checked.Expression.Selector.Context, alias, path); err != nil { + return Selector{}, err + } + selector, err := spec.ParseSelector(checked.Expression.Selector.Path) + if err != nil { + return Selector{}, fmt.Errorf("%s: %w", path, err) + } + if err := validateSemanticSelector(resourceType, selector); err != nil { + return Selector{}, fmt.Errorf("%s: %w", path, err) + } + return selector, nil +} + +// lowerRecipePredicate maps the only predicate shape currently representable +// by SemanticAggregate and SemanticSlice: selector existence or a string +// equality. Richer boolean predicates remain ordinary node filters until the +// canonical physical predicate IR grows an equivalent typed representation. +func lowerRecipePredicate(resourceType, alias string, scope scopeFrame, input *recipe.Filter, path string) (*Selector, string, error) { + if input == nil { + return nil, "", nil + } + filters, err := LowerRecipeFiltersForAlias(resourceType, alias, []recipe.Filter{*input}) + if err != nil { + return nil, "", fmt.Errorf("%s: %w", path, err) + } + if len(filters) != 1 { + return nil, "", fmt.Errorf("%s must contain exactly one predicate", path) + } + filter := filters[0] + selector, err := spec.ParseSelector(filter.Selector) + if err != nil { + return nil, "", fmt.Errorf("%s selector: %w", path, err) + } + if err := validateSemanticSelector(resourceType, selector); err != nil { + return nil, "", fmt.Errorf("%s selector: %w", path, err) + } + switch filter.Operator { + case spec.FilterExists: + return &selector, "", nil + case spec.FilterEquals: + if len(filter.Values) != 1 || filter.Values[0].String == nil || filter.Values[0].Kind != spec.FilterString { + return nil, "", fmt.Errorf("%s equality predicate must use one STRING value", path) + } + return &selector, *filter.Values[0].String, nil + default: + return nil, "", fmt.Errorf("%s operator %s is not representable by canonical aggregate/slice predicates", path, filter.Operator) + } +} + +func ensureSelectorOwnedByNode(context, alias, path string) error { + context = strings.TrimSpace(context) + alias = strings.TrimSpace(alias) + if context != "" && context != alias { + return fmt.Errorf("%s selector context %q must match containing node %q", path, context, alias) + } + return nil +} + +func validateRecipeRichName(value, path string) error { + if !recipeRichNamePattern.MatchString(strings.TrimSpace(value)) { + return fmt.Errorf("%s %q is not a safe semantic name", path, value) + } + return nil +} diff --git a/internal/dataframe/semantic/recipe_rich_shaping_test.go b/internal/dataframe/semantic/recipe_rich_shaping_test.go new file mode 100644 index 0000000..eba28ef --- /dev/null +++ b/internal/dataframe/semantic/recipe_rich_shaping_test.go @@ -0,0 +1,159 @@ +package semantic + +import ( + "strings" + "testing" + + "github.com/calypr/loom/internal/dataframe/recipe" +) + +func TestLowerRecipePivotsUsesGeneratedFamilyAndDeclaredColumns(t *testing.T) { + scope := newRootScope("Condition") + pivots, err := lowerRecipePivots("Condition", "root", scope, []recipe.Pivot{{ + Name: "diagnosis", + ColumnExpr: recipe.Expression{Select: "code.coding[].display"}, + ValueExpr: recipe.Expression{Select: "code.text"}, + Columns: []string{"melanoma", "glioma"}, + }}) + if err != nil { + t.Fatal(err) + } + if len(pivots) != 1 { + t.Fatalf("pivots = %#v", pivots) + } + got := pivots[0] + if got.Family == "" || got.ColumnSelector.CanonicalPath() != "code.coding[].display" || got.ValueSelector.CanonicalPath() != "code.text" { + t.Fatalf("unexpected pivot = %#v", got) + } + if got.Columns[0] != "melanoma" || got.Columns[1] != "glioma" { + t.Fatalf("declared column order changed: %#v", got.Columns) + } +} + +func TestLowerRecipePivotsRejectsUnsupportedPairAndDuplicateColumn(t *testing.T) { + scope := newRootScope("Patient") + _, err := lowerRecipePivots("Patient", "root", scope, []recipe.Pivot{{ + Name: "bad", + ColumnExpr: recipe.Expression{Select: "gender"}, + ValueExpr: recipe.Expression{Select: "id"}, + Columns: []string{"x", "x"}, + }}) + if err == nil || !strings.Contains(err.Error(), "duplicated") { + t.Fatalf("expected duplicate-column error, got %v", err) + } + _, err = lowerRecipePivots("Patient", "root", scope, []recipe.Pivot{{ + Name: "bad", + ColumnExpr: recipe.Expression{Select: "gender"}, + ValueExpr: recipe.Expression{Select: "id"}, + Columns: []string{"x"}, + }}) + if err == nil || !strings.Contains(err.Error(), "unsupported pivot selector pair") { + t.Fatalf("expected generated pivot-family error, got %v", err) + } +} + +func TestLowerRecipeAggregatesMapsSelectorWhereAndOperation(t *testing.T) { + scope := newRootScope("Patient") + want := "female" + aggregates, err := lowerRecipeAggregates("Patient", "root", scope, []recipe.Aggregate{{ + Name: "female_count", + Operation: recipe.AggregateCountDistinct, + Expr: recipeExpr("gender"), + Where: &recipe.Filter{ + Select: "gender", + Operator: recipe.FilterEquals, + Values: []recipe.FilterValue{{Kind: recipe.FilterString, String: &want}}, + }, + }}) + if err != nil { + t.Fatal(err) + } + if len(aggregates) != 1 || aggregates[0].Selector == nil || aggregates[0].Predicate == nil { + t.Fatalf("aggregate selectors missing: %#v", aggregates) + } + if aggregates[0].Operation != string(recipe.AggregateCountDistinct) || aggregates[0].PredicateEquals != want { + t.Fatalf("aggregate = %#v", aggregates[0]) + } +} + +func TestLowerRecipeAggregatesRejectsUnrepresentableWhereAndCountExpr(t *testing.T) { + scope := newRootScope("Patient") + _, err := lowerRecipeAggregates("Patient", "root", scope, []recipe.Aggregate{{ + Name: "count", Operation: recipe.AggregateCount, Expr: recipeExpr("id"), + }}) + if err == nil || !strings.Contains(err.Error(), "not accepted") { + t.Fatalf("expected count expression rejection, got %v", err) + } + _, err = lowerRecipeAggregates("Patient", "root", scope, []recipe.Aggregate{{ + Name: "count", Operation: recipe.AggregateCount, Where: &recipe.Filter{ + Select: "gender", Operator: recipe.FilterNotEquals, + Values: []recipe.FilterValue{{Kind: recipe.FilterString, String: stringPtr("female")}}, + }, + }}) + if err == nil || !strings.Contains(err.Error(), "not representable") { + t.Fatalf("expected predicate-shape rejection, got %v", err) + } +} + +func TestLowerRecipeSlicesUsesProjectionFallbacksAndPredicate(t *testing.T) { + scope := newRootScope("Patient") + want := "female" + slices, err := lowerRecipeSlices("Patient", "root", scope, []recipe.RepresentativeSlice{{ + Name: "representatives", Limit: 3, + Where: &recipe.Filter{Select: "gender", Operator: recipe.FilterEquals, Values: []recipe.FilterValue{{Kind: recipe.FilterString, String: &want}}}, + Fields: []recipe.Field{{ + Name: "identifier", Expr: *recipeExpr("id"), + Fallbacks: []recipe.Expression{*recipeExpr("gender")}, ValueMode: recipe.ValueModeFirst, + }}, + }}) + if err != nil { + t.Fatal(err) + } + if len(slices) != 1 || len(slices[0].Fields) != 1 || slices[0].Predicate == nil || slices[0].PredicateEquals != want { + t.Fatalf("slice = %#v", slices) + } + field := slices[0].Fields[0] + if len(field.Fallbacks) != 1 || field.ValueMode != string(recipe.ValueModeFirst) { + t.Fatalf("slice field = %#v", field) + } +} + +func TestLowerRecipeSlicesRejectsExpressionFieldsAndForeignAlias(t *testing.T) { + scope := newRootScope("Patient") + _, err := lowerRecipeSlices("Patient", "root", scope, []recipe.RepresentativeSlice{{ + Name: "representatives", Limit: 1, + Fields: []recipe.Field{{Name: "x", Expr: recipe.Expression{Call: "first", Args: []recipe.Expression{*recipeExpr("id")}}}}, + }}) + if err == nil || !strings.Contains(err.Error(), "must be a selector") { + t.Fatalf("expected selector-only slice error, got %v", err) + } + _, err = lowerRecipeSlices("Patient", "root", scope, []recipe.RepresentativeSlice{{ + Name: "representatives", Limit: 1, + Fields: []recipe.Field{{Name: "x", Expr: recipe.Expression{Select: "other.id"}}}, + }}) + if err == nil || !strings.Contains(err.Error(), "context") { + t.Fatalf("expected foreign-context error, got %v", err) + } +} + +func TestLowerRecipeRichShapingCoordinatesFamilies(t *testing.T) { + scope := newRootScope("Condition") + pivots, aggregates, slices, err := lowerRecipeRichShaping("Condition", "root", scope, + []recipe.Pivot{{Name: "diagnosis", ColumnExpr: recipe.Expression{Select: "code.coding[].display"}, ValueExpr: recipe.Expression{Select: "code.text"}, Columns: []string{"x"}}}, + []recipe.Aggregate{{Name: "count", Operation: recipe.AggregateCount}}, + []recipe.RepresentativeSlice{{Name: "rows", Limit: 1, Fields: []recipe.Field{{Name: "id", Expr: *recipeExpr("id")}}}}, + ) + if err != nil { + t.Fatal(err) + } + if len(pivots) != 1 || len(aggregates) != 1 || len(slices) != 1 { + t.Fatalf("rich shaping = %#v %#v %#v", pivots, aggregates, slices) + } +} + +func recipeExpr(selectPath string) *recipe.Expression { + expr := recipe.Expression{Select: selectPath} + return &expr +} + +func stringPtr(value string) *string { return &value } diff --git a/internal/dataframe/semantic/recipe_traversal.go b/internal/dataframe/semantic/recipe_traversal.go new file mode 100644 index 0000000..1f060f2 --- /dev/null +++ b/internal/dataframe/semantic/recipe_traversal.go @@ -0,0 +1,131 @@ +package semantic + +import ( + "fmt" + "strings" + + "github.com/calypr/loom/internal/dataframe/recipe" +) + +// qualifyTraversalLocals makes the traversal node's local selectors explicit +// before expression checking. Recipe selectors are intentionally concise +// ("status", "type[].coding[].display") while the semantic AST needs a +// lexical alias to distinguish them from parent/root references. Existing +// aliases remain untouched so a recipe can deliberately read a visible parent +// binding. +func qualifyTraversalLocals(input recipe.Traversal, alias string, visible map[string]scopeBinding) recipe.Traversal { + copy := input + copy.Fields = append([]recipe.Field(nil), input.Fields...) + for index := range copy.Fields { + copy.Fields[index].Expr = qualifyRecipeExpression(copy.Fields[index].Expr, alias, visible) + copy.Fields[index].Fallbacks = append([]recipe.Expression(nil), input.Fields[index].Fallbacks...) + for fallbackIndex := range copy.Fields[index].Fallbacks { + copy.Fields[index].Fallbacks[fallbackIndex] = qualifyRecipeExpression(copy.Fields[index].Fallbacks[fallbackIndex], alias, visible) + } + } + copy.Filters = append([]recipe.Filter(nil), input.Filters...) + for index := range copy.Filters { + copy.Filters[index].Select = qualifyRecipeSelector(copy.Filters[index].Select, alias, visible) + } + copy.Pivots = append([]recipe.Pivot(nil), input.Pivots...) + for index := range copy.Pivots { + copy.Pivots[index].ColumnExpr = qualifyRecipeExpression(copy.Pivots[index].ColumnExpr, alias, visible) + copy.Pivots[index].ValueExpr = qualifyRecipeExpression(copy.Pivots[index].ValueExpr, alias, visible) + } + copy.Aggregates = append([]recipe.Aggregate(nil), input.Aggregates...) + for index := range copy.Aggregates { + if input.Aggregates[index].Expr != nil { + expr := qualifyRecipeExpression(*input.Aggregates[index].Expr, alias, visible) + copy.Aggregates[index].Expr = &expr + } + if input.Aggregates[index].Where != nil { + where := *input.Aggregates[index].Where + where.Select = qualifyRecipeSelector(where.Select, alias, visible) + copy.Aggregates[index].Where = &where + } + } + copy.Slices = append([]recipe.RepresentativeSlice(nil), input.Slices...) + for index := range copy.Slices { + if input.Slices[index].Where != nil { + where := *input.Slices[index].Where + where.Select = qualifyRecipeSelector(where.Select, alias, visible) + copy.Slices[index].Where = &where + } + copy.Slices[index].Fields = append([]recipe.Field(nil), input.Slices[index].Fields...) + for fieldIndex := range copy.Slices[index].Fields { + copy.Slices[index].Fields[fieldIndex].Expr = qualifyRecipeExpression(copy.Slices[index].Fields[fieldIndex].Expr, alias, visible) + copy.Slices[index].Fields[fieldIndex].Fallbacks = append([]recipe.Expression(nil), input.Slices[index].Fields[fieldIndex].Fallbacks...) + for fallbackIndex := range copy.Slices[index].Fields[fieldIndex].Fallbacks { + copy.Slices[index].Fields[fieldIndex].Fallbacks[fallbackIndex] = qualifyRecipeExpression(copy.Slices[index].Fields[fieldIndex].Fallbacks[fallbackIndex], alias, visible) + } + } + } + copy.DynamicColumns = append([]recipe.DynamicColumn(nil), input.DynamicColumns...) + for index := range copy.DynamicColumns { + copy.DynamicColumns[index].Source = qualifyRecipeExpression(copy.DynamicColumns[index].Source, alias, visible) + if copy.DynamicColumns[index].Key != nil { + key := qualifyRecipeExpression(*copy.DynamicColumns[index].Key, alias, visible) + copy.DynamicColumns[index].Key = &key + } + if copy.DynamicColumns[index].Value != nil { + value := qualifyRecipeExpression(*copy.DynamicColumns[index].Value, alias, visible) + copy.DynamicColumns[index].Value = &value + } + } + return copy +} + +func qualifyRecipeExpression(input recipe.Expression, alias string, visible map[string]scopeBinding) recipe.Expression { + copy := input + if strings.TrimSpace(copy.Select) != "" { + copy.Select = qualifyRecipeSelector(copy.Select, alias, visible) + } + copy.Args = append([]recipe.Expression(nil), input.Args...) + for index := range copy.Args { + copy.Args[index] = qualifyRecipeExpression(copy.Args[index], alias, visible) + } + return copy +} + +func qualifyRecipeSelector(input, alias string, visible map[string]scopeBinding) string { + trimmed := strings.TrimSpace(input) + if trimmed == "" { + return trimmed + } + path := trimmed + suffix := "" + if before, after, found := strings.Cut(trimmed, " where "); found { + path, suffix = before, " where "+after + } + first := strings.SplitN(strings.TrimPrefix(strings.TrimSpace(path), "."), ".", 2)[0] + if first == "root" || first == "item" || first == alias { + return trimmed + } + if _, ok := visible[first]; ok { + return trimmed + } + return alias + "." + strings.TrimPrefix(strings.TrimSpace(path), ".") + suffix +} + +// NormalizeRecipeMatchMode applies the persisted recipe default and returns +// the canonical semantic relationship mode. Empty and case-insensitive values +// remain OPTIONAL for version-1 recipe compatibility; unknown values fail +// before traversal lowering. +func NormalizeRecipeMatchMode(mode recipe.TraversalMatchMode) (TraversalMatchMode, error) { + if !mode.Valid() { + return "", fmt.Errorf("unsupported traversal match mode %q", mode) + } + switch mode.Normalized() { + case recipe.MatchRequired: + return TraversalMatchRequired, nil + case recipe.MatchOptional: + return TraversalMatchOptional, nil + default: + return "", fmt.Errorf("unsupported traversal match mode %q", mode) + } +} + +// LowerRecipeTraversalFilters performs the closed semantic portion of one +// traversal. The caller attaches the returned filters to the child +// SemanticNode, preserving the optional/required boundary for the common +// physical lowering path. diff --git a/internal/dataframe/semantic/resolved_plan.go b/internal/dataframe/semantic/resolved_plan.go new file mode 100644 index 0000000..721ccfd --- /dev/null +++ b/internal/dataframe/semantic/resolved_plan.go @@ -0,0 +1,145 @@ +package semantic + +import ( + "crypto/sha256" + "encoding/hex" + "encoding/json" + "fmt" + "sort" + "strings" + + planpkg "github.com/calypr/loom/internal/dataframe/recipe/plan" +) + +// ResolvedColumn is a discovered output column whose name and logical value +// type have been frozen before execution. It is intentionally independent of +// ClickHouse or any other storage type. +type ResolvedColumn struct { + Output string + DynamicName string + Column planpkg.Column +} + +// ResolvedRecipePlan is the only recipe representation accepted by a +// production execution/materialization adapter. Stored recipe data and +// request-scoped discovery are never mutated in place. +type ResolvedRecipePlan struct { + SemanticPlan RecipePlan + ResolvedColumns map[string][]ResolvedColumn + ResolvedSchemaDigest string + ScopeDigest string + SourceGeneration string +} + +// ResolveRecipePlan freezes all dynamic schemas and records the scope and +// source generation used to obtain them. A plan with no dynamic maps still +// receives a deterministic schema digest. Dynamic schemas must already have +// been resolved by the catalog-backed recipe resolver before this boundary. +func ResolveRecipePlan(plan RecipePlan, scopeDigest, sourceGeneration string) (ResolvedRecipePlan, error) { + if strings.TrimSpace(sourceGeneration) == "" { + sourceGeneration = plan.Bindings.DatasetGeneration + } + // An empty generation is the intentional legacy-null dataset namespace. It + // remains a precise scope when the catalog resolver used the same empty + // generation during discovery; physical lowering renders the nil bind. + if strings.TrimSpace(scopeDigest) == "" { + for _, output := range plan.Outputs { + if outputHasDynamicMaps(output.Root) || len(output.DynamicMaps) > 0 { + return ResolvedRecipePlan{}, fmt.Errorf("scoped authorization digest is required for dynamic discovery") + } + } + scopeDigest = "unscoped" + } + resolved := ResolvedRecipePlan{ + SemanticPlan: plan, + ResolvedColumns: make(map[string][]ResolvedColumn), + ScopeDigest: scopeDigest, + SourceGeneration: sourceGeneration, + } + for _, output := range plan.Outputs { + var walk func(SemanticNode) error + walk = func(node SemanticNode) error { + for _, dynamic := range node.DynamicMaps { + if dynamic.Columns == nil { + return fmt.Errorf("output %q dynamic map %q is unresolved; resolve it through the catalog-backed recipe resolver first", output.Name, dynamic.Name) + } + if len(dynamic.Columns) == 0 { + // An empty catalog result is a valid optional family. The + // resolver has already frozen it to zero columns, so there is + // no schema or physical lookup to attach to this node. + continue + } + candidates := make([]planpkg.Candidate, 0, len(dynamic.Columns)) + for _, column := range dynamic.Columns { + candidates = append(candidates, planpkg.Candidate{Key: column, ValueType: "unknown"}) + } + schema, err := planpkg.Freeze(planpkg.DynamicSpec{ + Name: dynamic.Name, AllowedKeys: dynamic.Columns, MaxColumns: dynamic.MaxColumns, Collision: output.Collision, + }, candidates) + if err != nil { + return fmt.Errorf("output %q dynamic map %q: %w", output.Name, dynamic.Name, err) + } + key := dynamicMapKey(output.Name, dynamic) + columns := make([]ResolvedColumn, 0, len(schema.Columns)) + for _, column := range schema.Columns { + columns = append(columns, ResolvedColumn{Output: output.Name, DynamicName: dynamic.Name, Column: column}) + } + resolved.ResolvedColumns[key] = columns + } + for _, child := range node.Children { + if err := walk(child); err != nil { + return err + } + } + return nil + } + if err := walk(output.Root); err != nil { + return ResolvedRecipePlan{}, err + } + } + // Marshal map keys deterministically by normalizing through sorted keys. + keys := make([]string, 0, len(resolved.ResolvedColumns)) + for key := range resolved.ResolvedColumns { + keys = append(keys, key) + } + sort.Strings(keys) + ordered := make([]struct { + Key string `json:"key"` + Columns []ResolvedColumn `json:"columns"` + }, 0, len(keys)) + for _, key := range keys { + ordered = append(ordered, struct { + Key string `json:"key"` + Columns []ResolvedColumn `json:"columns"` + }{key, resolved.ResolvedColumns[key]}) + } + canonical, err := json.Marshal(struct { + RecipeDigest, ScopeDigest, Generation string + Columns any `json:"columns"` + }{plan.RecipeDigest, scopeDigest, sourceGeneration, ordered}) + if err != nil { + return ResolvedRecipePlan{}, fmt.Errorf("resolved schema digest: %w", err) + } + sum := sha256.Sum256(canonical) + resolved.ResolvedSchemaDigest = hex.EncodeToString(sum[:]) + return resolved, nil +} + +func dynamicMapKey(output string, dynamic SemanticDynamicMap) string { + if strings.TrimSpace(dynamic.ScopeAlias) == "" || dynamic.ScopeAlias == "root" { + return output + ":" + dynamic.Name + } + return output + ":" + dynamic.ScopeAlias + ":" + dynamic.Name +} + +func outputHasDynamicMaps(node SemanticNode) bool { + if len(node.DynamicMaps) > 0 { + return true + } + for _, child := range node.Children { + if outputHasDynamicMaps(child) { + return true + } + } + return false +} diff --git a/internal/dataframe/semantic/resolved_plan_test.go b/internal/dataframe/semantic/resolved_plan_test.go new file mode 100644 index 0000000..ceb1ce6 --- /dev/null +++ b/internal/dataframe/semantic/resolved_plan_test.go @@ -0,0 +1,46 @@ +package semantic + +import ( + "testing" + + "github.com/calypr/loom/internal/dataframe/recipe" +) + +func TestResolveRecipePlanFreezesScopedDynamicColumns(t *testing.T) { + bundle := recipe.Bundle{ + RecipeSchemaVersion: 1, + Name: "dynamic", + TranslationVersion: "test", + Outputs: []recipe.Output{{ + Name: "Patient", RootResourceType: "Patient", RowGrain: "patient", + Fields: []recipe.Field{{Name: "id", Expr: recipe.Expression{Select: "id"}}}, + DynamicColumns: []recipe.DynamicColumn{{Name: "code", Source: recipe.Expression{Select: "identifier[].value"}, MaxColumns: 4, Columns: []string{"b", "a"}}}, + }}, + } + plan, err := BuildRecipePlan(bundle, recipe.RuntimeBindings{Project: "p", DatasetGeneration: "g"}) + if err != nil { + t.Fatal(err) + } + resolved, err := ResolveRecipePlan(plan, "scope-1", "g") + if err != nil { + t.Fatal(err) + } + columns := resolved.ResolvedColumns["Patient:code"] + if len(columns) != 2 || columns[0].Column.Name != "code_a" || columns[1].Column.Name != "code_b" { + t.Fatalf("unexpected frozen columns: %#v", columns) + } + if resolved.ResolvedSchemaDigest == "" || resolved.ScopeDigest != "scope-1" || resolved.SourceGeneration != "g" { + t.Fatalf("missing resolved provenance: %#v", resolved) + } +} + +func TestResolveRecipePlanRequiresDiscoveryForUnfrozenDynamicMap(t *testing.T) { + bundle := recipe.Bundle{RecipeSchemaVersion: 1, Name: "dynamic", TranslationVersion: "test", Outputs: []recipe.Output{{Name: "Patient", RootResourceType: "Patient", RowGrain: "patient", DynamicColumns: []recipe.DynamicColumn{{Name: "code", Source: recipe.Expression{Select: "identifier[].value"}}}}}} + plan, err := BuildRecipePlan(bundle, recipe.RuntimeBindings{Project: "p"}) + if err != nil { + t.Fatal(err) + } + if _, err := ResolveRecipePlan(plan, "scope", "g"); err == nil { + t.Fatal("expected missing scoped discovery error") + } +} diff --git a/internal/dataframe/semantic/selection_semantics.go b/internal/dataframe/semantic/selection_semantics.go index 277e897..073091b 100644 --- a/internal/dataframe/semantic/selection_semantics.go +++ b/internal/dataframe/semantic/selection_semantics.go @@ -126,15 +126,6 @@ func projectionForValueMode(valueMode string, cardinality Cardinality) (Projecti } } -func isKnownValueMode(valueMode string) bool { - switch strings.ToUpper(strings.TrimSpace(valueMode)) { - case "", "AUTO", "FIRST", "ALL", "DISTINCT": - return true - default: - return false - } -} - func sortedUniqueStrings(values []string) []string { seen := make(map[string]struct{}, len(values)) out := make([]string, 0, len(values)) diff --git a/internal/dataframe/semantic/semantic_plan.go b/internal/dataframe/semantic/semantic_plan.go index ab42aa8..72eff10 100644 --- a/internal/dataframe/semantic/semantic_plan.go +++ b/internal/dataframe/semantic/semantic_plan.go @@ -1,66 +1,61 @@ package semantic -import ( - "fmt" - "strings" +// SemanticPlan is the backend-independent graph consumed by the physical +// compiler. Recipe bundles are the only production input format; the recipe +// builder populates this graph after schema, scope, and expression validation. - "github.com/calypr/loom/fhirschema" +import ( "github.com/calypr/loom/internal/authscope" + "github.com/calypr/loom/internal/dataframe/expression" "github.com/calypr/loom/internal/dataframe/spec" ) -// SemanticPlan is the validated, backend-independent meaning of a dataframe -// request. It deliberately contains no AQL variable names, named-set choices, -// or optimizer decisions. Physical lowering is free to choose those details -// while preserving this plan's row and selection semantics. -// -// It is currently constructed from Builder so the existing GraphQL contract -// can remain stable while the compiler moves away from string-oriented lowering. type SemanticPlan struct { Version int Project string DatasetGeneration string AuthResourcePaths []string - // AuthScopeMode is copied from Builder so physical plans retain the - // restricted-empty distinction after semantic lowering. - AuthScopeMode authscope.ReadScopeMode - RowIdentity *RowIdentity - Root SemanticNode + AuthScopeMode authscope.ReadScopeMode + RowIdentity *RowIdentity + Root SemanticNode } -// SemanticNode represents one FHIR resource reached at a stable alias. The -// root alias is always "root". Child nodes are relationship traversals from -// this node; a child selection is represented as an array, aggregate, pivot, -// or representative slice unless a later row-grain plan explicitly explodes -// it into rows. type SemanticNode struct { Alias string ResourceType string EdgeLabel string MatchMode TraversalMatchMode + From *SemanticExpression Fields []SemanticField Filters []TypedFilter Pivots []SemanticPivot Aggregates []SemanticAggregate Slices []SemanticSlice Children []SemanticNode + DynamicMaps []SemanticDynamicMap } type SemanticField struct { - Name string - FieldRef string - Selector Selector - Fallbacks []Selector - ValueMode string + Name string + FieldRef string + Selector Selector + Fallbacks []Selector + ValueMode string + Expr *expression.Expression + ExprType expression.Type + SourcePath string } type SemanticPivot struct { - Name string - FieldRef string - ColumnSelector Selector - ValueSelector Selector - Columns []string - Family string + Name string + FieldRef string + ColumnSelector Selector + ValueSelector Selector + ValueFallbacks []Selector + ItemSource Selector + ItemResourceType string + Columns []string + Family string } type SemanticAggregate struct { @@ -83,333 +78,13 @@ type SemanticSlice struct { Fields []SemanticField } -// BuildSemanticPlan parses the selector-bearing portions of Builder exactly -// once and preserves the user's requested traversal shape. Catalog and -// authorization validation remain the responsibility of Service.validateBuilder -// because they require a request context and an observed dataset. -func BuildSemanticPlan(builder Builder) (SemanticPlan, error) { - if strings.TrimSpace(builder.Project) == "" { - return SemanticPlan{}, fmt.Errorf("project is required") - } - if strings.TrimSpace(builder.RootResourceType) == "" { - return SemanticPlan{}, fmt.Errorf("rootResourceType is required") - } - if !fhirschema.HasResource(builder.RootResourceType) { - return SemanticPlan{}, fmt.Errorf("root resource type %q is not represented by the active generated FHIR schema", builder.RootResourceType) - } - - root, err := semanticNodeFromBuilder("root", builder.RootResourceType, "", "", builder.Fields, builder.Filters, builder.Pivots, builder.Aggregates, builder.Slices, builder.Traversals) - if err != nil { - return SemanticPlan{}, err - } - plan := SemanticPlan{ - Version: 1, - Project: builder.Project, - DatasetGeneration: strings.TrimSpace(builder.DatasetGeneration), - AuthResourcePaths: cloneStrings(builder.AuthResourcePaths), - AuthScopeMode: builder.AuthScopeMode, - Root: root, - } - grain := builder.RowGrain - if grain == "" { - var known bool - grain, known = InferRowGrain(builder.RootResourceType) - if !known { - return SemanticPlan{}, fmt.Errorf("no row grain is available for root resource type %q", builder.RootResourceType) - } - } - if err := ValidateRootGrain(builder.RootResourceType, grain); err != nil { - return SemanticPlan{}, err - } - identity, ok := DefaultRowIdentity(grain) - if !ok { - return SemanticPlan{}, fmt.Errorf("invalid row grain %q", grain) - } - plan.RowIdentity = &identity - if err := ValidateSemanticGraph(plan); err != nil { - return SemanticPlan{}, err - } - if _, err := NormalizeSelectionPlan(plan); err != nil { - return SemanticPlan{}, err - } - return plan, nil -} - -func semanticNodeFromBuilder(alias, resourceType, edgeLabel string, matchMode TraversalMatchMode, fields []FieldSelect, filters []TypedFilter, pivots []PivotSelect, aggregates []AggregateSelect, slices []RepresentativeSlice, traversals []TraversalStep) (SemanticNode, error) { - node := SemanticNode{ - Alias: alias, - ResourceType: resourceType, - EdgeLabel: edgeLabel, - MatchMode: matchMode, - Fields: make([]SemanticField, 0, len(fields)), - Filters: append([]TypedFilter(nil), filters...), - Pivots: make([]SemanticPivot, 0, len(pivots)), - Aggregates: make([]SemanticAggregate, 0, len(aggregates)), - Slices: make([]SemanticSlice, 0, len(slices)), - Children: make([]SemanticNode, 0, len(traversals)), - } - for _, filter := range node.Filters { - if err := ValidateTypedFilterForResource(resourceType, filter); err != nil { - return SemanticNode{}, fmt.Errorf("filter %q: %w", filter.FieldRef, err) - } - } - - seenFields := map[string]struct{}{} - for _, field := range fields { - if strings.TrimSpace(field.Name) == "" { - return SemanticNode{}, fmt.Errorf("field selections require name and select") - } - if _, exists := seenFields[field.Name]; exists { - return SemanticNode{}, fmt.Errorf("field name %q is duplicated", field.Name) - } - seenFields[field.Name] = struct{}{} - selector, err := ParseSelector(field.Select) - if err != nil { - return SemanticNode{}, fmt.Errorf("field %q: %w", field.Name, err) - } - fallbacks := make([]Selector, 0, len(field.FallbackSelects)) - for _, fallback := range field.FallbackSelects { - parsed, err := ParseSelector(fallback) - if err != nil { - return SemanticNode{}, fmt.Errorf("field %q fallback: %w", field.Name, err) - } - fallbacks = append(fallbacks, parsed) - } - node.Fields = append(node.Fields, SemanticField{ - Name: field.Name, - FieldRef: field.FieldRef, - Selector: selector, - Fallbacks: fallbacks, - ValueMode: field.ValueMode, - }) - } - - seenPivots := map[string]struct{}{} - for _, pivot := range pivots { - if strings.TrimSpace(pivot.Name) == "" { - return SemanticNode{}, fmt.Errorf("pivot selections require name") - } - if _, exists := seenPivots[pivot.Name]; exists { - return SemanticNode{}, fmt.Errorf("pivot name %q is duplicated", pivot.Name) - } - seenPivots[pivot.Name] = struct{}{} - if len(pivot.Columns) == 0 { - return SemanticNode{}, fmt.Errorf("pivot %q requires bounded columns resolved from the field catalog before compilation", pivot.Name) - } - column, err := ParseSelector(pivot.ColumnSelect) - if err != nil { - return SemanticNode{}, fmt.Errorf("pivot %q column selector: %w", pivot.Name, err) - } - value, err := ParseSelector(pivot.ValueSelect) - if err != nil { - return SemanticNode{}, fmt.Errorf("pivot %q value selector: %w", pivot.Name, err) - } - if err := validateSemanticSelector(resourceType, column); err != nil { - return SemanticNode{}, fmt.Errorf("pivot %q column selector: %w", pivot.Name, err) - } - if err := validateSemanticSelector(resourceType, value); err != nil { - return SemanticNode{}, fmt.Errorf("pivot %q value selector: %w", pivot.Name, err) - } - pivotSpec, err := fhirschema.ValidatePivotSelectors(resourceType, selectorSpecFromSelector(column), selectorSpecFromSelector(value)) - if err != nil { - return SemanticNode{}, fmt.Errorf("pivot %q: %w", pivot.Name, err) - } - if pivot.PivotFamily != "" && pivot.PivotFamily != pivotSpec.Family { - return SemanticNode{}, fmt.Errorf("pivot %q family %q conflicts with generated family %q", pivot.Name, pivot.PivotFamily, pivotSpec.Family) - } - node.Pivots = append(node.Pivots, SemanticPivot{ - Name: pivot.Name, - FieldRef: pivot.FieldRef, - ColumnSelector: column, - ValueSelector: value, - Columns: cloneStrings(pivot.Columns), - Family: pivotSpec.Family, - }) - } - - seenAggregates := map[string]struct{}{} - for _, aggregate := range aggregates { - if strings.TrimSpace(aggregate.Name) == "" { - return SemanticNode{}, fmt.Errorf("aggregate selections require name") - } - if _, exists := seenAggregates[aggregate.Name]; exists { - return SemanticNode{}, fmt.Errorf("aggregate name %q is duplicated", aggregate.Name) - } - seenAggregates[aggregate.Name] = struct{}{} - operation := strings.ToUpper(strings.TrimSpace(aggregate.Operation)) - if !isKnownAggregateOperation(operation) { - return SemanticNode{}, fmt.Errorf("aggregate %q uses unsupported operation %q", aggregate.Name, aggregate.Operation) - } - if !isKnownValueMode(aggregate.ValueMode) { - return SemanticNode{}, fmt.Errorf("aggregate %q uses unsupported value mode %q", aggregate.Name, aggregate.ValueMode) - } - semanticAggregate := SemanticAggregate{ - Name: aggregate.Name, - Operation: operation, - FieldRef: aggregate.FieldRef, - PredicateField: aggregate.PredicateFieldRef, - PredicateEquals: aggregate.PredicateEquals, - ValueMode: aggregate.ValueMode, - } - if strings.TrimSpace(aggregate.Select) != "" { - selector, err := ParseSelector(aggregate.Select) - if err != nil { - return SemanticNode{}, fmt.Errorf("aggregate %q selector: %w", aggregate.Name, err) - } - if err := validateSemanticSelector(resourceType, selector); err != nil { - return SemanticNode{}, fmt.Errorf("aggregate %q selector: %w", aggregate.Name, err) - } - semanticAggregate.Selector = &selector - } - if strings.TrimSpace(aggregate.PredicatePath) != "" { - predicate, err := ParseSelector(aggregate.PredicatePath) - if err != nil { - return SemanticNode{}, fmt.Errorf("aggregate %q predicate: %w", aggregate.Name, err) - } - if err := validateSemanticSelector(resourceType, predicate); err != nil { - return SemanticNode{}, fmt.Errorf("aggregate %q predicate: %w", aggregate.Name, err) - } - semanticAggregate.Predicate = &predicate - } - if aggregateOperationRequiresSelector(operation) && semanticAggregate.Selector == nil { - return SemanticNode{}, fmt.Errorf("aggregate %q operation %s requires a selector", aggregate.Name, operation) - } - node.Aggregates = append(node.Aggregates, semanticAggregate) - } - - seenSlices := map[string]struct{}{} - for _, slice := range slices { - if strings.TrimSpace(slice.Name) == "" { - return SemanticNode{}, fmt.Errorf("representative slices require name") - } - if _, exists := seenSlices[slice.Name]; exists { - return SemanticNode{}, fmt.Errorf("representative slice name %q is duplicated", slice.Name) - } - seenSlices[slice.Name] = struct{}{} - if slice.Limit <= 0 { - return SemanticNode{}, fmt.Errorf("representative slice %q requires positive limit", slice.Name) - } - semanticSlice := SemanticSlice{ - Name: slice.Name, - Limit: slice.Limit, - PredicateField: slice.PredicateFieldRef, - PredicateEquals: slice.PredicateEquals, - Fields: make([]SemanticField, 0, len(slice.Fields)), - } - if strings.TrimSpace(slice.PredicatePath) != "" { - predicate, err := ParseSelector(slice.PredicatePath) - if err != nil { - return SemanticNode{}, fmt.Errorf("representative slice %q predicate: %w", slice.Name, err) - } - if err := validateSemanticSelector(resourceType, predicate); err != nil { - return SemanticNode{}, fmt.Errorf("representative slice %q predicate: %w", slice.Name, err) - } - semanticSlice.Predicate = &predicate - } - seenSliceFields := map[string]struct{}{} - for _, field := range slice.Fields { - if strings.TrimSpace(field.Name) == "" { - return SemanticNode{}, fmt.Errorf("representative slice %q requires field name", slice.Name) - } - if _, exists := seenSliceFields[field.Name]; exists { - return SemanticNode{}, fmt.Errorf("representative slice %q field name %q is duplicated", slice.Name, field.Name) - } - seenSliceFields[field.Name] = struct{}{} - selector, err := ParseSelector(field.Select) - if err != nil { - return SemanticNode{}, fmt.Errorf("representative slice %q field %q: %w", slice.Name, field.Name, err) - } - if err := validateSemanticSelector(resourceType, selector); err != nil { - return SemanticNode{}, fmt.Errorf("representative slice %q field %q: %w", slice.Name, field.Name, err) - } - fallbacks := make([]Selector, 0, len(field.FallbackSelects)) - for _, fallback := range field.FallbackSelects { - parsed, err := ParseSelector(fallback) - if err != nil { - return SemanticNode{}, fmt.Errorf("representative slice %q field %q fallback: %w", slice.Name, field.Name, err) - } - if err := validateSemanticSelector(resourceType, parsed); err != nil { - return SemanticNode{}, fmt.Errorf("representative slice %q field %q fallback: %w", slice.Name, field.Name, err) - } - fallbacks = append(fallbacks, parsed) - } - if !isKnownValueMode(field.ValueMode) { - return SemanticNode{}, fmt.Errorf("representative slice %q field %q uses unsupported value mode %q", slice.Name, field.Name, field.ValueMode) - } - semanticSlice.Fields = append(semanticSlice.Fields, SemanticField{ - Name: field.Name, - FieldRef: field.FieldRef, - Selector: selector, - Fallbacks: fallbacks, - ValueMode: field.ValueMode, - }) - } - node.Slices = append(node.Slices, semanticSlice) - } - - for _, traversal := range traversals { - child, err := semanticNodeFromBuilder(traversal.Alias, traversal.ToResourceType, traversal.Label, traversal.MatchMode, traversal.Fields, traversal.Filters, traversal.Pivots, traversal.Aggregates, traversal.Slices, traversal.Traversals) - if err != nil { - return SemanticNode{}, err - } - node.Children = append(node.Children, child) - } - return node, nil -} - func validateSemanticSelector(resourceType string, selector Selector) error { _, _, err := spec.SelectorCardinality(resourceType, selector) return err } -func aggregateOperationRequiresSelector(operation string) bool { - switch operation { - case "COUNT_DISTINCT", "DISTINCT_VALUES", "MIN", "MAX": - return true - default: - return false - } -} - -func isKnownAggregateOperation(operation string) bool { - switch strings.ToUpper(strings.TrimSpace(operation)) { - case "COUNT", "COUNT_DISTINCT", "EXISTS", "DISTINCT_VALUES", "MIN", "MAX": - return true - default: - return false - } -} - -// Explain returns a compact stable summary suitable for diagnostics and tests. -// It intentionally does not expose physical AQL decisions. -func (p SemanticPlan) Explain() SemanticPlanExplanation { - explanation := SemanticPlanExplanation{ - Version: p.Version, - RootResourceType: p.Root.ResourceType, - DatasetGeneration: p.DatasetGeneration, - RowIdentity: cloneRowIdentity(p.RowIdentity), - Nodes: make([]SemanticNodeExplanation, 0), - } - var walk func(SemanticNode, string) - walk = func(node SemanticNode, parentAlias string) { - explanation.Nodes = append(explanation.Nodes, SemanticNodeExplanation{ - Alias: node.Alias, - ParentAlias: parentAlias, - ResourceType: node.ResourceType, - EdgeLabel: node.EdgeLabel, - MatchMode: node.MatchMode, - FieldCount: len(node.Fields), - PivotCount: len(node.Pivots), - AggregateCount: len(node.Aggregates), - SliceCount: len(node.Slices), - }) - for _, child := range node.Children { - walk(child, node.Alias) - } - } - walk(p.Root, "") - return explanation -} +// Explain returns a compact logical summary. It intentionally does not expose +// physical AQL decisions. type SemanticPlanExplanation struct { Version int @@ -419,22 +94,6 @@ type SemanticPlanExplanation struct { Nodes []SemanticNodeExplanation } -func cloneRowIdentity(identity *RowIdentity) *RowIdentity { - if identity == nil { - return nil - } - copy := *identity - copy.Fields = cloneStrings(identity.Fields) - return © -} - -func cloneStrings(in []string) []string { - if in == nil { - return nil - } - return append([]string(nil), in...) -} - type SemanticNodeExplanation struct { Alias string ParentAlias string diff --git a/internal/dataframe/semantic/spec_aliases.go b/internal/dataframe/semantic/spec_aliases.go index 6fdbc02..ce5174a 100644 --- a/internal/dataframe/semantic/spec_aliases.go +++ b/internal/dataframe/semantic/spec_aliases.go @@ -3,20 +3,14 @@ package semantic import "github.com/calypr/loom/internal/dataframe/spec" type ( - Builder = spec.Builder - TraversalStep = spec.TraversalStep - RepresentativeSlice = spec.RepresentativeSlice - FieldSelect = spec.FieldSelect - PivotSelect = spec.PivotSelect - AggregateSelect = spec.AggregateSelect - RowGrain = spec.RowGrain - ProjectionMode = spec.ProjectionMode - Cardinality = spec.Cardinality - RowIdentity = spec.RowIdentity - TraversalMatchMode = spec.TraversalMatchMode - TypedFilter = spec.TypedFilter - Selector = spec.Selector - SelectorStep = spec.SelectorStep + RowGrain = spec.RowGrain + ProjectionMode = spec.ProjectionMode + Cardinality = spec.Cardinality + RowIdentity = spec.RowIdentity + TraversalMatchMode = spec.TraversalMatchMode + TypedFilter = spec.TypedFilter + Selector = spec.Selector + SelectorStep = spec.SelectorStep ) const ( @@ -26,6 +20,8 @@ const ( ProjectionDistinctArray = spec.ProjectionDistinctArray CardinalityOptionalOne = spec.CardinalityOptionalOne CardinalityMany = spec.CardinalityMany + TraversalMatchOptional = spec.TraversalMatchOptional + TraversalMatchRequired = spec.TraversalMatchRequired ) var ( diff --git a/internal/dataframe/spec/builder_types.go b/internal/dataframe/spec/builder_types.go deleted file mode 100644 index 8702e50..0000000 --- a/internal/dataframe/spec/builder_types.go +++ /dev/null @@ -1,83 +0,0 @@ -package spec - -import "github.com/calypr/loom/internal/authscope" - -type Builder struct { - Project string - // DatasetGeneration is optional. An empty value deliberately targets only - // the legacy null generation namespace; it never means every generation. - DatasetGeneration string - AuthResourcePaths []string - // AuthScopeMode is set by a request-level authorization resolver. It is - // required to distinguish a restricted empty path set from an unrestricted - // one. The empty value preserves the legacy direct-Builder convention that - // no paths means unrestricted. - AuthScopeMode authscope.ReadScopeMode - RootResourceType string - RowGrain RowGrain - Fields []FieldSelect - Filters []TypedFilter - Pivots []PivotSelect - Aggregates []AggregateSelect - Slices []RepresentativeSlice - Traversals []TraversalStep -} - -type TraversalStep struct { - Label string - ToResourceType string - Alias string - Fields []FieldSelect - Filters []TypedFilter - Pivots []PivotSelect - Aggregates []AggregateSelect - Slices []RepresentativeSlice - // MatchMode defaults to OPTIONAL. REQUIRED retains only root rows with a - // matching relationship route; it is lowered as a semi-join rather than a - // post-projection filter. - MatchMode TraversalMatchMode - Traversals []TraversalStep -} - -// RepresentativeSlice is a bounded child projection requested by the -// semantic dataframe request. It is consumed directly by the physical plan; -// it is not a lowered named-set artifact. -type RepresentativeSlice struct { - Name string - SourceSet string - Predicate string - PredicateFieldRef string - PredicatePath string - PredicateEquals string - Limit int - Fields []FieldSelect -} - -type FieldSelect struct { - Name string - FieldRef string - Select string - FallbackFieldRefs []string - FallbackSelects []string - ValueMode string -} - -type PivotSelect struct { - Name string - FieldRef string - ColumnSelect string - ValueSelect string - Columns []string - PivotFamily string -} - -type AggregateSelect struct { - Name string - Operation string - FieldRef string - Select string - PredicateFieldRef string - PredicatePath string - PredicateEquals string - ValueMode string -} diff --git a/internal/dataframe/spec/grain.go b/internal/dataframe/spec/grain.go index 9c0a814..83ab5bd 100644 --- a/internal/dataframe/spec/grain.go +++ b/internal/dataframe/spec/grain.go @@ -21,12 +21,15 @@ const ( RowGrainDiagnosis RowGrain = "diagnosis" RowGrainObservation RowGrain = "observation" RowGrainStudyEnrollment RowGrain = "study_enrollment" + // RowGrainExpanded is a synthetic grain whose identity is supplied by a + // recipe expansion expression rather than by the root resource key. + RowGrainExpanded RowGrain = "expanded" ) func (g RowGrain) Validate() error { switch g { case RowGrainResource, RowGrainPatient, RowGrainSpecimen, RowGrainFile, RowGrainDiagnosis, - RowGrainObservation, RowGrainStudyEnrollment: + RowGrainObservation, RowGrainStudyEnrollment, RowGrainExpanded: return nil case "": return fmt.Errorf("row grain is required") @@ -68,6 +71,8 @@ func RootResourceForGrain(grain RowGrain) (string, bool) { switch grain { case RowGrainResource: return "", true + case RowGrainExpanded: + return "", true case RowGrainPatient: return "Patient", true case RowGrainSpecimen: diff --git a/internal/dataframe/spec/relationship_match.go b/internal/dataframe/spec/relationship_match.go index 1a94bef..35815be 100644 --- a/internal/dataframe/spec/relationship_match.go +++ b/internal/dataframe/spec/relationship_match.go @@ -10,7 +10,7 @@ import ( // OPTIONAL is the legacy behavior: a missing child yields an empty child // projection but does not remove the root row. REQUIRED is intentionally // opt-in and lowers to a root-scoped semi-join. The empty value is OPTIONAL so -// existing Builder callers retain their current behavior. +// callers retain the same schema-backed relationship behavior. type TraversalMatchMode string const ( diff --git a/internal/dataframe/spec/selectors.go b/internal/dataframe/spec/selectors.go index 5f742b7..d92d45d 100644 --- a/internal/dataframe/spec/selectors.go +++ b/internal/dataframe/spec/selectors.go @@ -1,10 +1,6 @@ package spec import ( - "encoding/json" - "fmt" - "strings" - "github.com/calypr/loom/fhirschema" ) @@ -15,50 +11,3 @@ type ContainsFilter = fhirschema.ContainsFilter func ParseSelector(input string) (Selector, error) { return fhirschema.ParseSelector(input) } - -func selectorStepText(step SelectorStep) string { - switch { - case step.Iterate: - return step.Field + "[]" - case step.Index != nil: - return fmt.Sprintf("%s[%d]", step.Field, *step.Index) - default: - return step.Field - } -} - -func sanitizeColumnName(in string) string { - var b strings.Builder - for _, r := range in { - switch { - case r >= 'a' && r <= 'z', r >= 'A' && r <= 'Z', r >= '0' && r <= '9': - b.WriteRune(r) - default: - b.WriteRune('_') - } - } - return b.String() -} - -func isSafeAQLFieldIdentifier(in string) bool { - if in == "" { - return false - } - for index, r := range in { - if index == 0 { - if (r < 'a' || r > 'z') && (r < 'A' || r > 'Z') && r != '_' { - return false - } - continue - } - if (r < 'a' || r > 'z') && (r < 'A' || r > 'Z') && (r < '0' || r > '9') && r != '_' { - return false - } - } - return true -} - -func quoteKey(key string) string { - data, _ := json.Marshal(key) - return string(data) -} diff --git a/internal/dataframe/template/availability.go b/internal/dataframe/template/availability.go index 2454df2..ff40ed6 100644 --- a/internal/dataframe/template/availability.go +++ b/internal/dataframe/template/availability.go @@ -1,7 +1,6 @@ package template import ( - "fmt" "strings" "github.com/calypr/loom/fhirschema" @@ -200,14 +199,3 @@ func contains(values []string, wanted string) bool { // ValidateStarter performs the local structural checks that can be proven // without a request context. Production validation remains the existing // dataframe input resolver and compiler. -func ValidateStarter(starter StarterRequest) error { - if strings.TrimSpace(starter.RootResourceType) == "" || !fhirschema.HasResource(starter.RootResourceType) { - return fmt.Errorf("starter root resource type %q is not a generated FHIR resource", starter.RootResourceType) - } - for _, traversal := range starter.Traversals { - if _, found, err := fhirschema.ResolveCompilerTraversal(traversal.FromType, traversal.EdgeLabel, traversal.ToType); err != nil || !found { - return fmt.Errorf("starter traversal %q is not present in generated FHIR metadata", traversal.ID) - } - } - return nil -} diff --git a/internal/dataframe/template/registry_test.go b/internal/dataframe/template/registry_test.go deleted file mode 100644 index c8b6b3a..0000000 --- a/internal/dataframe/template/registry_test.go +++ /dev/null @@ -1,76 +0,0 @@ -package template - -import "testing" - -func TestDefaultRegistryHasStableSixTemplateOrder(t *testing.T) { - registry := DefaultRegistry() - definitions := registry.Definitions() - want := []string{"patient-cohort", "specimen-inventory", "file-manifest", "diagnoses", "labs-observations", "study-enrollment"} - if len(definitions) != len(want) { - t.Fatalf("definition count = %d, want %d", len(definitions), len(want)) - } - for i, id := range want { - if definitions[i].ID != id || definitions[i].Version != 1 { - t.Fatalf("definition %d = %#v, want %s v1", i, definitions[i], id) - } - } -} - -func TestRegistryDefinitionsAreDefensiveCopies(t *testing.T) { - registry := DefaultRegistry() - definitions := registry.Definitions() - definitions[0].RootCandidates[0] = "Observation" - definitions[0].SuggestedColumns[0].FieldRefAlternatives[0] = "Observation.id" - got, ok := registry.Definition("patient-cohort") - if !ok { - t.Fatal("patient-cohort not found") - } - if got.RootCandidates[0] != "Patient" || got.SuggestedColumns[0].FieldRefAlternatives[0] != "Patient.identifier_value" { - t.Fatalf("registry was mutated through returned definition: %#v", got) - } -} - -func TestRegistryRejectsDuplicateAndUnknownDefinitions(t *testing.T) { - base := Definition{ID: "x", Version: 1, Label: "X", RootCandidates: []string{"Patient"}} - if _, err := NewRegistry([]Definition{base, base}); err == nil { - t.Fatal("expected duplicate id error") - } - unknown := base - unknown.ID = "unknown" - unknown.RootCandidates = []string{"NotFHIR"} - if _, err := NewRegistry([]Definition{unknown}); err == nil { - t.Fatal("expected unknown root error") - } -} - -func TestDefaultStartersContainOnlyDefaultSuggestions(t *testing.T) { - registry := DefaultRegistry() - definition, ok := registry.Definition("labs-observations") - if !ok { - t.Fatal("labs-observations not found") - } - snapshot := CapabilitySnapshot{ - Resources: []ResourceCapability{{ - ResourceType: "Observation", Present: true, - Fields: []FieldCapability{ - {ResourceType: "Observation", FieldRef: "Observation.id"}, - {ResourceType: "Observation", FieldRef: "Observation.code_coding_display"}, - {ResourceType: "Observation", FieldRef: "Observation.valuequantity_value"}, - {ResourceType: "Observation", FieldRef: "Observation.code", PivotCandidate: true, PivotColumns: []string{"A", "B"}}, - }, - }}, - } - availability := Resolve(definition, snapshot) - if availability.Status != StatusPartial { - t.Fatalf("status = %s, want PARTIAL because optional fields are absent", availability.Status) - } - if len(availability.Starter.Fields) != 3 { - t.Fatalf("starter fields = %#v, want 3 default fields", availability.Starter.Fields) - } - if len(availability.Starter.Pivots) != 1 || availability.Starter.Pivots[0].Columns[0] != "A" { - t.Fatalf("starter pivots = %#v", availability.Starter.Pivots) - } - if err := ValidateStarter(availability.Starter); err != nil { - t.Fatal(err) - } -} diff --git a/internal/dataframe/template/types.go b/internal/dataframe/template/types.go index a68073c..4e852d6 100644 --- a/internal/dataframe/template/types.go +++ b/internal/dataframe/template/types.go @@ -193,29 +193,6 @@ func (d Definition) clone() Definition { return d } -func (a Availability) clone() Availability { - a.CommonColumns = append([]SelectedColumn(nil), a.CommonColumns...) - a.AdvancedColumns = append([]SelectedColumn(nil), a.AdvancedColumns...) - a.Traversals = append([]SelectedTraversal(nil), a.Traversals...) - a.Pivots = append([]SelectedPivot(nil), a.Pivots...) - for i := range a.Pivots { - a.Pivots[i].Columns = cloneStrings(a.Pivots[i].Columns) - } - a.Missing = append([]MissingCapability(nil), a.Missing...) - a.Reasons = cloneStrings(a.Reasons) - a.Starter = StarterRequest{ - RootResourceType: a.Starter.RootResourceType, - RowGrain: a.Starter.RowGrain, - Fields: append([]SelectedColumn(nil), a.Starter.Fields...), - Traversals: append([]SelectedTraversal(nil), a.Starter.Traversals...), - Pivots: append([]SelectedPivot(nil), a.Starter.Pivots...), - } - for i := range a.Starter.Pivots { - a.Starter.Pivots[i].Columns = cloneStrings(a.Starter.Pivots[i].Columns) - } - return a -} - func cloneStrings(in []string) []string { if len(in) == 0 { return []string{} diff --git a/internal/dataframe/uuidcompat/uuid.go b/internal/dataframe/uuidcompat/uuid.go new file mode 100644 index 0000000..67b1bf4 --- /dev/null +++ b/internal/dataframe/uuidcompat/uuid.go @@ -0,0 +1,47 @@ +// Package uuidcompat contains the exact named-UUID contract shared by +// compiler-owned post-query execution and legacy differential tests. +package uuidcompat + +import ( + "fmt" + "strings" + + "github.com/google/uuid" +) + +// Compute applies the legacy uuid3/uuid5 contract. A nil argument propagates +// null so optional recipe expressions do not mint an ID from the text "". +func Compute(operation string, args []any) (any, error) { + if len(args) < 2 { + return nil, fmt.Errorf("%s requires a namespace and at least one name", operation) + } + for _, value := range args { + if value == nil { + return nil, nil + } + } + namespace, err := namespace(args[0]) + if err != nil { + return nil, err + } + var name strings.Builder + for _, value := range args[1:] { + name.WriteString(fmt.Sprint(value)) + } + switch strings.ToLower(operation) { + case "uuid3": + return uuid.NewMD5(namespace, []byte(name.String())).String(), nil + case "uuid5": + return uuid.NewSHA1(namespace, []byte(name.String())).String(), nil + default: + return nil, fmt.Errorf("unsupported named UUID operation %q", operation) + } +} + +func namespace(value any) (uuid.UUID, error) { + raw := fmt.Sprint(value) + if parsed, err := uuid.Parse(raw); err == nil { + return parsed, nil + } + return uuid.NewMD5(uuid.NameSpaceDNS, []byte(raw)), nil +} diff --git a/internal/dataset/active.go b/internal/dataset/active.go index 252653f..7b3d41b 100644 --- a/internal/dataset/active.go +++ b/internal/dataset/active.go @@ -48,31 +48,6 @@ type ActivationPlan struct { // READY manifest. The existing manifest is not mutated. If a replacement is // needed, Previous names the generation a persistence adapter must supersede // atomically with writing the new active reference. -func PlanActivation(current *Manifest, candidate Manifest) (ActivationPlan, error) { - active, err := ActiveGenerationFor(candidate) - if err != nil { - return ActivationPlan{}, err - } - plan := ActivationPlan{Active: active} - if current == nil { - return plan, nil - } - if err := current.Validate(); err != nil { - return ActivationPlan{}, err - } - if current.State != ManifestStateReady { - return ActivationPlan{}, fmt.Errorf("%w: current generation %s is %s", ErrGenerationNotReady, current.Dataset.Generation, current.State) - } - if current.Dataset.Project != candidate.Dataset.Project { - return ActivationPlan{}, fmt.Errorf("%w: current and candidate projects differ", ErrInvalidActiveGeneration) - } - if current.Dataset.Equal(candidate.Dataset) { - return plan, nil - } - previous := current.Dataset - plan.Previous = &previous - return plan, nil -} // Validate checks that an activation plan is internally coherent. It does not // verify that a storage transaction was executed. diff --git a/internal/dataset/active_test.go b/internal/dataset/active_test.go index 71d8a72..8ff540f 100644 --- a/internal/dataset/active_test.go +++ b/internal/dataset/active_test.go @@ -1,60 +1 @@ package dataset - -import ( - "encoding/json" - "errors" - "testing" -) - -func TestActiveGenerationResolutionAndActivationPlan(t *testing.T) { - previous := readyManifest(t, "project-a", "generation-old") - candidate := readyManifest(t, "project-a", "generation-new") - if _, err := ActiveGenerationFor(candidate); err != nil { - t.Fatalf("ActiveGenerationFor(candidate): %v", err) - } - plan, err := PlanActivation(&previous, candidate) - if err != nil { - t.Fatalf("PlanActivation: %v", err) - } - if !plan.Active.Dataset.Equal(candidate.Dataset) { - t.Fatalf("activation active = %#v, want %#v", plan.Active.Dataset, candidate.Dataset) - } - if plan.Previous == nil || !plan.Previous.Equal(previous.Dataset) { - t.Fatalf("activation previous = %#v, want %#v", plan.Previous, previous.Dataset) - } - if previous.State != ManifestStateReady { - t.Fatalf("PlanActivation mutated previous manifest to %s", previous.State) - } - - idempotent, err := PlanActivation(&candidate, candidate) - if err != nil { - t.Fatalf("PlanActivation(idempotent): %v", err) - } - if idempotent.Previous != nil { - t.Fatalf("idempotent plan Previous = %#v, want nil", idempotent.Previous) - } - - encoded, err := json.Marshal(plan) - if err != nil { - t.Fatalf("json.Marshal(ActivationPlan): %v", err) - } - var decoded ActivationPlan - if err := json.Unmarshal(encoded, &decoded); err != nil { - t.Fatalf("json.Unmarshal(ActivationPlan): %v", err) - } - if !decoded.Active.Dataset.Equal(plan.Active.Dataset) || decoded.Previous == nil || !decoded.Previous.Equal(*plan.Previous) { - t.Fatalf("activation plan did not round trip\ngot: %#v\nwant: %#v", decoded, plan) - } - - otherProject := readyManifest(t, "project-b", "generation-old") - if _, err := PlanActivation(&otherProject, candidate); !errors.Is(err, ErrInvalidActiveGeneration) { - t.Fatalf("PlanActivation(cross-project) error = %v, want ErrInvalidActiveGeneration", err) - } - loading := transitionManifest(t, fixtureManifest(t, "project-a", "generation-loading"), ManifestStateLoading) - if _, err := PlanActivation(&previous, loading); !errors.Is(err, ErrGenerationNotReady) { - t.Fatalf("PlanActivation(LOADING candidate) error = %v, want ErrGenerationNotReady", err) - } - if _, err := PlanActivation(&previous, failedManifest(t, "project-a", "generation-failed")); !errors.Is(err, ErrGenerationNotReady) { - t.Fatalf("PlanActivation(FAILED candidate) error = %v, want ErrGenerationNotReady", err) - } -} diff --git a/internal/dataset/arango/document_codec.go b/internal/dataset/arango/document_codec.go new file mode 100644 index 0000000..98afc7a --- /dev/null +++ b/internal/dataset/arango/document_codec.go @@ -0,0 +1,322 @@ +package arango + +import ( + "context" + "crypto/sha256" + "encoding/hex" + "encoding/json" + "fmt" + + "github.com/calypr/loom/internal/dataset" +) + +func (s *Store) validate() error { + if s == nil || s.client == nil { + return ErrNilQueryClient + } + if s.batchSize <= 0 { + return ErrInvalidCursorBatchSize + } + return nil +} + +func (s *Store) manifestRows(ctx context.Context, query string, bindVars map[string]any) ([]dataset.Manifest, error) { + rows := make([]dataset.Manifest, 0, 1) + var unexpected error + err := s.client.QueryRows(ctx, query, s.batchSize, bindVars, func(row map[string]any) error { + manifest, err := manifestFromValue(row) + if err != nil { + unexpected = err + return err + } + rows = append(rows, manifest) + if len(rows) > 1 { + unexpected = fmt.Errorf("%w: query returned multiple manifests", ErrUnexpectedStoreResult) + return unexpected + } + return nil + }) + if err != nil { + if unexpected != nil { + return nil, unexpected + } + return nil, err + } + return rows, nil +} + +func lifecycleBindVars(project string) map[string]any { + return map[string]any{ + "@lifecycle_collection": LifecycleCollection, + "project": project, + "manifest_record_type": manifestRecordType, + "active_record_type": activeRecordType, + } +} + +func validateProject(project string) error { + _, err := dataset.NewDatasetRef(project, "datasetstore-project-validation") + return err +} + +func manifestDocument(manifest dataset.Manifest) (map[string]any, error) { + data, err := json.Marshal(manifest) + if err != nil { + return nil, fmt.Errorf("encode dataset manifest document: %w", err) + } + var document map[string]any + if err := json.Unmarshal(data, &document); err != nil { + return nil, fmt.Errorf("decode dataset manifest document: %w", err) + } + document["_key"] = manifestDocumentKey(manifest.Dataset) + document["recordType"] = manifestRecordType + return document, nil +} + +func activePlaceholderDocument(project string) map[string]any { + return map[string]any{ + "_key": activeDocumentKey(project), + "recordType": activeRecordType, + "project": project, + } +} + +func schemaIdentityBindValue(identity dataset.SchemaIdentitySnapshot) (map[string]any, error) { + data, err := json.Marshal(identity) + if err != nil { + return nil, fmt.Errorf("encode schema identity bind value: %w", err) + } + var value map[string]any + if err := json.Unmarshal(data, &value); err != nil { + return nil, fmt.Errorf("decode schema identity bind value: %w", err) + } + return value, nil +} + +func manifestFromValue(value any) (dataset.Manifest, error) { + data, err := json.Marshal(value) + if err != nil { + return dataset.Manifest{}, fmt.Errorf("%w: encode manifest row: %v", ErrUnexpectedStoreResult, err) + } + var manifest dataset.Manifest + if err := json.Unmarshal(data, &manifest); err != nil { + return dataset.Manifest{}, fmt.Errorf("%w: decode manifest row: %v", ErrUnexpectedStoreResult, err) + } + return manifest, nil +} + +func activeResolutionFromRow(row map[string]any) (activeResolution, error) { + active, err := activeFromValue(row["active"]) + if err != nil { + return activeResolution{}, err + } + manifest, err := manifestFromValue(row["manifest"]) + if err != nil { + return activeResolution{}, err + } + return activeResolution{active: active, manifest: manifest}, nil +} + +func activeFromValue(value any) (dataset.ActiveGeneration, error) { + data, err := json.Marshal(value) + if err != nil { + return dataset.ActiveGeneration{}, fmt.Errorf("%w: encode active generation row: %v", ErrUnexpectedStoreResult, err) + } + var active dataset.ActiveGeneration + if err := json.Unmarshal(data, &active); err != nil { + return dataset.ActiveGeneration{}, fmt.Errorf("%w: decode active generation row: %v", ErrUnexpectedStoreResult, err) + } + return active, nil +} + +func datasetRefFromValue(value any) (dataset.DatasetRef, error) { + data, err := json.Marshal(value) + if err != nil { + return dataset.DatasetRef{}, fmt.Errorf("%w: encode dataset reference row: %v", ErrUnexpectedStoreResult, err) + } + var ref dataset.DatasetRef + if err := json.Unmarshal(data, &ref); err != nil { + return dataset.DatasetRef{}, fmt.Errorf("%w: decode dataset reference row: %v", ErrUnexpectedStoreResult, err) + } + return ref, nil +} + +func manifestIdentityEqual(left, right dataset.Manifest) bool { + return left.Dataset.Equal(right.Dataset) && + left.State == right.State && + left.SchemaIdentity.Equal(right.SchemaIdentity) && + left.AnalysisVersion == right.AnalysisVersion +} + +func manifestDocumentKey(ref dataset.DatasetRef) string { + return documentKey("manifest", ref.Project, ref.Generation) +} + +func activeDocumentKey(project string) string { + return documentKey("active", project) +} + +func documentKey(kind string, values ...string) string { + hash := sha256.New() + _, _ = hash.Write([]byte(documentKeyDomain)) + for _, value := range append([]string{kind}, values...) { + _, _ = hash.Write([]byte{0}) + _, _ = hash.Write([]byte(value)) + } + return kind + "_" + hex.EncodeToString(hash.Sum(nil)) +} + +// The collection is always bound through @@lifecycle_collection. Project, +// generation, states, schema metadata, and document payloads are scalar bind +// values; no user-controlled identifier is interpolated into AQL text. +const createManifestAQL = ` +LET existing = FIRST( + FOR manifest IN @@lifecycle_collection + FILTER manifest._key == @manifest_key + FILTER manifest.recordType == @manifest_record_type + RETURN manifest +) +FILTER existing == null +LET active = FIRST( + FOR pointer IN @@lifecycle_collection + FILTER pointer._key == @active_key + FILTER pointer.recordType == @active_record_type + FILTER pointer.project == @project + RETURN pointer +) +LET documents = active == null ? [@manifest, @active_placeholder] : [@manifest] +FOR document IN documents + INSERT document INTO @@lifecycle_collection + RETURN { + recordType: NEW.recordType, + manifest: NEW.recordType == @manifest_record_type ? { + dataset: NEW.dataset, + state: NEW.state, + schemaIdentity: NEW.schemaIdentity, + analysisVersion: NEW.analysisVersion + } : null + } +` + +const readManifestAQL = ` +FOR manifest IN @@lifecycle_collection + FILTER manifest._key == @manifest_key + FILTER manifest.recordType == @manifest_record_type + FILTER manifest.dataset.project == @project + FILTER manifest.dataset.generation == @generation + LIMIT 2 + RETURN { + dataset: manifest.dataset, + state: manifest.state, + schemaIdentity: manifest.schemaIdentity, + analysisVersion: manifest.analysisVersion + } +` + +const transitionManifestAQL = ` +FOR manifest IN @@lifecycle_collection + FILTER manifest._key == @manifest_key + FILTER manifest.recordType == @manifest_record_type + FILTER manifest.dataset.project == @project + FILTER manifest.dataset.generation == @generation + FILTER manifest.state == @expected_state + FILTER manifest.schemaIdentity == @schema_identity + FILTER manifest.analysisVersion == @analysis_version + UPDATE manifest WITH { state: @next_state } IN @@lifecycle_collection + RETURN { + dataset: NEW.dataset, + state: NEW.state, + schemaIdentity: NEW.schemaIdentity, + analysisVersion: NEW.analysisVersion + } +` + +const readActiveAQL = ` +FOR active IN @@lifecycle_collection + FILTER active._key == @active_key + FILTER active.recordType == @active_record_type + FILTER active.project == @project + FILTER active.manifestKey != null + FOR manifest IN @@lifecycle_collection + FILTER manifest._key == active.manifestKey + FILTER manifest.recordType == @manifest_record_type + FILTER manifest.dataset == active.dataset + FILTER manifest.state == @ready_state + LIMIT 2 + RETURN { + active: { dataset: active.dataset }, + manifest: { + dataset: manifest.dataset, + state: manifest.state, + schemaIdentity: manifest.schemaIdentity, + analysisVersion: manifest.analysisVersion + } + } +` + +// activateAQL has exactly one UPDATE statement. The candidate guard updates +// READY to READY only to put its revision into the AQL write set; it is a CAS +// guard, not a lifecycle transition. With ignoreRevs:false, a concurrent +// change to the candidate, prior active manifest, or active pointer aborts the +// whole single-server transaction instead of selecting stale state. +const activateAQL = ` +LET candidate = FIRST( + FOR manifest IN @@lifecycle_collection + FILTER manifest._key == @candidate_key + FILTER manifest.recordType == @manifest_record_type + FILTER manifest.dataset.project == @project + FILTER manifest.dataset.generation == @generation + FILTER manifest.state == @ready_state + FILTER manifest.schemaIdentity == @schema_identity + FILTER manifest.analysisVersion == @analysis_version + RETURN manifest +) +LET active = FIRST( + FOR pointer IN @@lifecycle_collection + FILTER pointer._key == @active_key + FILTER pointer.recordType == @active_record_type + FILTER pointer.project == @project + RETURN pointer +) +FILTER candidate != null +FILTER active != null +LET previous = FIRST( + FOR manifest IN @@lifecycle_collection + FILTER active.manifestKey != null + FILTER manifest._key == active.manifestKey + FILTER manifest.recordType == @manifest_record_type + FILTER manifest.dataset == active.dataset + FILTER manifest.state == @ready_state + RETURN manifest +) +FILTER ( + (active.dataset == null AND active.manifestKey == null) OR + ( + active.dataset != null AND + active.dataset.project == @project AND + active.manifestKey != null AND + previous != null + ) +) +LET updates = APPEND( + [{ document: candidate, patch: { state: @ready_state }, role: @candidate_guard_role }], + APPEND( + previous != null AND previous._key != candidate._key + ? [{ document: previous, patch: { state: @superseded_state }, role: @superseded_role }] + : [], + [{ + document: active, + patch: { dataset: candidate.dataset, manifestKey: candidate._key }, + role: @active_record_type + }] + ) +) +FOR update IN updates + UPDATE update.document WITH update.patch IN @@lifecycle_collection + OPTIONS { ignoreRevs: false, mergeObjects: false } + RETURN { + role: update.role, + dataset: NEW.dataset, + previous: update.role == @superseded_role ? OLD.dataset : null + } +` diff --git a/internal/dataset/arango/store.go b/internal/dataset/arango/manifest_lifecycle.go similarity index 61% rename from internal/dataset/arango/store.go rename to internal/dataset/arango/manifest_lifecycle.go index 0199c83..218f1e0 100644 --- a/internal/dataset/arango/store.go +++ b/internal/dataset/arango/manifest_lifecycle.go @@ -2,9 +2,6 @@ package arango import ( "context" - "crypto/sha256" - "encoding/hex" - "encoding/json" "errors" "fmt" @@ -391,314 +388,3 @@ func (s *Store) Activate(ctx context.Context, candidate dataset.Manifest) (datas } return result, nil } - -func (s *Store) validate() error { - if s == nil || s.client == nil { - return ErrNilQueryClient - } - if s.batchSize <= 0 { - return ErrInvalidCursorBatchSize - } - return nil -} - -func (s *Store) manifestRows(ctx context.Context, query string, bindVars map[string]any) ([]dataset.Manifest, error) { - rows := make([]dataset.Manifest, 0, 1) - var unexpected error - err := s.client.QueryRows(ctx, query, s.batchSize, bindVars, func(row map[string]any) error { - manifest, err := manifestFromValue(row) - if err != nil { - unexpected = err - return err - } - rows = append(rows, manifest) - if len(rows) > 1 { - unexpected = fmt.Errorf("%w: query returned multiple manifests", ErrUnexpectedStoreResult) - return unexpected - } - return nil - }) - if err != nil { - if unexpected != nil { - return nil, unexpected - } - return nil, err - } - return rows, nil -} - -func lifecycleBindVars(project string) map[string]any { - return map[string]any{ - "@lifecycle_collection": LifecycleCollection, - "project": project, - "manifest_record_type": manifestRecordType, - "active_record_type": activeRecordType, - } -} - -func validateProject(project string) error { - _, err := dataset.NewDatasetRef(project, "datasetstore-project-validation") - return err -} - -func manifestDocument(manifest dataset.Manifest) (map[string]any, error) { - data, err := json.Marshal(manifest) - if err != nil { - return nil, fmt.Errorf("encode dataset manifest document: %w", err) - } - var document map[string]any - if err := json.Unmarshal(data, &document); err != nil { - return nil, fmt.Errorf("decode dataset manifest document: %w", err) - } - document["_key"] = manifestDocumentKey(manifest.Dataset) - document["recordType"] = manifestRecordType - return document, nil -} - -func activePlaceholderDocument(project string) map[string]any { - return map[string]any{ - "_key": activeDocumentKey(project), - "recordType": activeRecordType, - "project": project, - } -} - -func schemaIdentityBindValue(identity dataset.SchemaIdentitySnapshot) (map[string]any, error) { - data, err := json.Marshal(identity) - if err != nil { - return nil, fmt.Errorf("encode schema identity bind value: %w", err) - } - var value map[string]any - if err := json.Unmarshal(data, &value); err != nil { - return nil, fmt.Errorf("decode schema identity bind value: %w", err) - } - return value, nil -} - -func manifestFromValue(value any) (dataset.Manifest, error) { - data, err := json.Marshal(value) - if err != nil { - return dataset.Manifest{}, fmt.Errorf("%w: encode manifest row: %v", ErrUnexpectedStoreResult, err) - } - var manifest dataset.Manifest - if err := json.Unmarshal(data, &manifest); err != nil { - return dataset.Manifest{}, fmt.Errorf("%w: decode manifest row: %v", ErrUnexpectedStoreResult, err) - } - return manifest, nil -} - -func activeResolutionFromRow(row map[string]any) (activeResolution, error) { - active, err := activeFromValue(row["active"]) - if err != nil { - return activeResolution{}, err - } - manifest, err := manifestFromValue(row["manifest"]) - if err != nil { - return activeResolution{}, err - } - return activeResolution{active: active, manifest: manifest}, nil -} - -func activeFromValue(value any) (dataset.ActiveGeneration, error) { - data, err := json.Marshal(value) - if err != nil { - return dataset.ActiveGeneration{}, fmt.Errorf("%w: encode active generation row: %v", ErrUnexpectedStoreResult, err) - } - var active dataset.ActiveGeneration - if err := json.Unmarshal(data, &active); err != nil { - return dataset.ActiveGeneration{}, fmt.Errorf("%w: decode active generation row: %v", ErrUnexpectedStoreResult, err) - } - return active, nil -} - -func datasetRefFromValue(value any) (dataset.DatasetRef, error) { - data, err := json.Marshal(value) - if err != nil { - return dataset.DatasetRef{}, fmt.Errorf("%w: encode dataset reference row: %v", ErrUnexpectedStoreResult, err) - } - var ref dataset.DatasetRef - if err := json.Unmarshal(data, &ref); err != nil { - return dataset.DatasetRef{}, fmt.Errorf("%w: decode dataset reference row: %v", ErrUnexpectedStoreResult, err) - } - return ref, nil -} - -func manifestIdentityEqual(left, right dataset.Manifest) bool { - return left.Dataset.Equal(right.Dataset) && - left.State == right.State && - left.SchemaIdentity.Equal(right.SchemaIdentity) && - left.AnalysisVersion == right.AnalysisVersion -} - -func manifestDocumentKey(ref dataset.DatasetRef) string { - return documentKey("manifest", ref.Project, ref.Generation) -} - -func activeDocumentKey(project string) string { - return documentKey("active", project) -} - -func documentKey(kind string, values ...string) string { - hash := sha256.New() - _, _ = hash.Write([]byte(documentKeyDomain)) - for _, value := range append([]string{kind}, values...) { - _, _ = hash.Write([]byte{0}) - _, _ = hash.Write([]byte(value)) - } - return kind + "_" + hex.EncodeToString(hash.Sum(nil)) -} - -// The collection is always bound through @@lifecycle_collection. Project, -// generation, states, schema metadata, and document payloads are scalar bind -// values; no user-controlled identifier is interpolated into AQL text. -const createManifestAQL = ` -LET existing = FIRST( - FOR manifest IN @@lifecycle_collection - FILTER manifest._key == @manifest_key - FILTER manifest.recordType == @manifest_record_type - RETURN manifest -) -FILTER existing == null -LET active = FIRST( - FOR pointer IN @@lifecycle_collection - FILTER pointer._key == @active_key - FILTER pointer.recordType == @active_record_type - FILTER pointer.project == @project - RETURN pointer -) -LET documents = active == null ? [@manifest, @active_placeholder] : [@manifest] -FOR document IN documents - INSERT document INTO @@lifecycle_collection - RETURN { - recordType: NEW.recordType, - manifest: NEW.recordType == @manifest_record_type ? { - dataset: NEW.dataset, - state: NEW.state, - schemaIdentity: NEW.schemaIdentity, - analysisVersion: NEW.analysisVersion - } : null - } -` - -const readManifestAQL = ` -FOR manifest IN @@lifecycle_collection - FILTER manifest._key == @manifest_key - FILTER manifest.recordType == @manifest_record_type - FILTER manifest.dataset.project == @project - FILTER manifest.dataset.generation == @generation - LIMIT 2 - RETURN { - dataset: manifest.dataset, - state: manifest.state, - schemaIdentity: manifest.schemaIdentity, - analysisVersion: manifest.analysisVersion - } -` - -const transitionManifestAQL = ` -FOR manifest IN @@lifecycle_collection - FILTER manifest._key == @manifest_key - FILTER manifest.recordType == @manifest_record_type - FILTER manifest.dataset.project == @project - FILTER manifest.dataset.generation == @generation - FILTER manifest.state == @expected_state - FILTER manifest.schemaIdentity == @schema_identity - FILTER manifest.analysisVersion == @analysis_version - UPDATE manifest WITH { state: @next_state } IN @@lifecycle_collection - RETURN { - dataset: NEW.dataset, - state: NEW.state, - schemaIdentity: NEW.schemaIdentity, - analysisVersion: NEW.analysisVersion - } -` - -const readActiveAQL = ` -FOR active IN @@lifecycle_collection - FILTER active._key == @active_key - FILTER active.recordType == @active_record_type - FILTER active.project == @project - FILTER active.manifestKey != null - FOR manifest IN @@lifecycle_collection - FILTER manifest._key == active.manifestKey - FILTER manifest.recordType == @manifest_record_type - FILTER manifest.dataset == active.dataset - FILTER manifest.state == @ready_state - LIMIT 2 - RETURN { - active: { dataset: active.dataset }, - manifest: { - dataset: manifest.dataset, - state: manifest.state, - schemaIdentity: manifest.schemaIdentity, - analysisVersion: manifest.analysisVersion - } - } -` - -// activateAQL has exactly one UPDATE statement. The candidate guard updates -// READY to READY only to put its revision into the AQL write set; it is a CAS -// guard, not a lifecycle transition. With ignoreRevs:false, a concurrent -// change to the candidate, prior active manifest, or active pointer aborts the -// whole single-server transaction instead of selecting stale state. -const activateAQL = ` -LET candidate = FIRST( - FOR manifest IN @@lifecycle_collection - FILTER manifest._key == @candidate_key - FILTER manifest.recordType == @manifest_record_type - FILTER manifest.dataset.project == @project - FILTER manifest.dataset.generation == @generation - FILTER manifest.state == @ready_state - FILTER manifest.schemaIdentity == @schema_identity - FILTER manifest.analysisVersion == @analysis_version - RETURN manifest -) -LET active = FIRST( - FOR pointer IN @@lifecycle_collection - FILTER pointer._key == @active_key - FILTER pointer.recordType == @active_record_type - FILTER pointer.project == @project - RETURN pointer -) -FILTER candidate != null -FILTER active != null -LET previous = FIRST( - FOR manifest IN @@lifecycle_collection - FILTER active.manifestKey != null - FILTER manifest._key == active.manifestKey - FILTER manifest.recordType == @manifest_record_type - FILTER manifest.dataset == active.dataset - FILTER manifest.state == @ready_state - RETURN manifest -) -FILTER ( - (active.dataset == null AND active.manifestKey == null) OR - ( - active.dataset != null AND - active.dataset.project == @project AND - active.manifestKey != null AND - previous != null - ) -) -LET updates = APPEND( - [{ document: candidate, patch: { state: @ready_state }, role: @candidate_guard_role }], - APPEND( - previous != null AND previous._key != candidate._key - ? [{ document: previous, patch: { state: @superseded_state }, role: @superseded_role }] - : [], - [{ - document: active, - patch: { dataset: candidate.dataset, manifestKey: candidate._key }, - role: @active_record_type - }] - ) -) -FOR update IN updates - UPDATE update.document WITH update.patch IN @@lifecycle_collection - OPTIONS { ignoreRevs: false, mergeObjects: false } - RETURN { - role: update.role, - dataset: NEW.dataset, - previous: update.role == @superseded_role ? OLD.dataset : null - } -` diff --git a/internal/dataset/manifest.go b/internal/dataset/manifest.go index a28cb7f..2a7b2ff 100644 --- a/internal/dataset/manifest.go +++ b/internal/dataset/manifest.go @@ -24,16 +24,6 @@ const ( type AnalysisVersion string // NewAnalysisVersion validates a non-empty opaque analysis-version value. -func NewAnalysisVersion(value string) (AnalysisVersion, error) { - version := AnalysisVersion(value) - if !version.IsSet() { - return "", fmt.Errorf("%w: value is required", ErrInvalidAnalysisVersion) - } - if err := version.Validate(); err != nil { - return "", err - } - return version, nil -} // IsSet reports whether a finalized analysis version is present. func (v AnalysisVersion) IsSet() bool { return v != "" } diff --git a/internal/dataset/manifest_test.go b/internal/dataset/manifest_test.go index 0700f5e..365a8ae 100644 --- a/internal/dataset/manifest_test.go +++ b/internal/dataset/manifest_test.go @@ -6,84 +6,6 @@ import ( "testing" ) -func TestManifestLifecycleTransitionsAndTerminalStates(t *testing.T) { - preflight := fixtureManifest(t, "project-a", "generation-a") - if got, want := preflight.State, ManifestStatePreflight; got != want { - t.Fatalf("initial state = %s, want %s", got, want) - } - if _, err := preflight.Transition(ManifestStateReady); !errors.Is(err, ErrInvalidTransition) { - t.Fatalf("PREFLIGHT -> READY error = %v, want ErrInvalidTransition", err) - } - - loading := transitionManifest(t, preflight, ManifestStateLoading) - if preflight.State != ManifestStatePreflight { - t.Fatalf("Transition mutated source state to %s", preflight.State) - } - if _, err := loading.Transition(ManifestStateReady); !errors.Is(err, ErrInvalidTransition) { - t.Fatalf("LOADING -> READY error = %v, want ErrInvalidTransition", err) - } - - analyzing := transitionManifest(t, loading, ManifestStateAnalyzing) - version, err := NewAnalysisVersion("analysis-2026-07-11") - if err != nil { - t.Fatalf("NewAnalysisVersion: %v", err) - } - withAnalysis, err := analyzing.WithAnalysisVersion(version) - if err != nil { - t.Fatalf("WithAnalysisVersion: %v", err) - } - if analyzing.AnalysisVersion.IsSet() { - t.Fatal("WithAnalysisVersion mutated source manifest") - } - if got, want := withAnalysis.AnalysisVersion, version; got != want { - t.Fatalf("AnalysisVersion = %q, want %q", got, want) - } - - ready := transitionManifest(t, withAnalysis, ManifestStateReady) - if !ready.IsReady() { - t.Fatal("READY manifest was not eligible for activation") - } - if _, err := ready.WithAnalysisVersion(version); !errors.Is(err, ErrInvalidTransition) { - t.Fatalf("READY WithAnalysisVersion error = %v, want ErrInvalidTransition", err) - } - if _, err := ready.Transition(ManifestStateFailed); !errors.Is(err, ErrInvalidTransition) { - t.Fatalf("READY -> FAILED error = %v, want ErrInvalidTransition", err) - } - - superseded := transitionManifest(t, ready, ManifestStateSuperseded) - if _, err := superseded.Transition(ManifestStateReady); !errors.Is(err, ErrInvalidTransition) { - t.Fatalf("SUPERSEDED -> READY error = %v, want ErrInvalidTransition", err) - } - if _, err := ActiveGenerationFor(superseded); !errors.Is(err, ErrGenerationNotReady) { - t.Fatalf("ActiveGenerationFor(SUPERSEDED) error = %v, want ErrGenerationNotReady", err) - } - - failed := failedManifest(t, "project-a", "generation-failed") - if _, err := failed.Transition(ManifestStateLoading); !errors.Is(err, ErrInvalidTransition) { - t.Fatalf("FAILED -> LOADING error = %v, want ErrInvalidTransition", err) - } - if _, err := ActiveGenerationFor(failed); !errors.Is(err, ErrGenerationNotReady) { - t.Fatalf("ActiveGenerationFor(FAILED) error = %v, want ErrGenerationNotReady", err) - } -} - -func TestManifestReadyAllowsAnalysisPlaceholderButNeverSynthesizesIt(t *testing.T) { - ready := readyManifest(t, "project-a", "generation-no-analysis") - if ready.AnalysisVersion.IsSet() { - t.Fatalf("ready AnalysisVersion = %q, want empty C1 placeholder", ready.AnalysisVersion) - } - if err := ready.AnalysisVersion.Validate(); err != nil { - t.Fatalf("empty analysis placeholder Validate: %v", err) - } - if _, err := NewAnalysisVersion(""); !errors.Is(err, ErrInvalidAnalysisVersion) { - t.Fatalf("NewAnalysisVersion(empty) error = %v, want ErrInvalidAnalysisVersion", err) - } - var decoded AnalysisVersion - if err := json.Unmarshal([]byte("null"), &decoded); !errors.Is(err, ErrInvalidAnalysisVersion) { - t.Fatalf("json.Unmarshal(null AnalysisVersion) error = %v, want ErrInvalidAnalysisVersion", err) - } -} - func TestManifestJSONValidationAndCopy(t *testing.T) { manifest := readyManifest(t, "project-a", "generation-a") encoded, err := json.Marshal(manifest) diff --git a/internal/graphstore/documents.go b/internal/graphstore/documents.go index fbcd722..2c4a86f 100644 --- a/internal/graphstore/documents.go +++ b/internal/graphstore/documents.go @@ -12,7 +12,7 @@ import ( "regexp" "strings" - "github.com/bmeg/grip/gripql" + "github.com/bmeg/jsonschemagraph/model" ) var validKeyPart = regexp.MustCompile(`[^A-Za-z0-9_\-:.@()+,=;$!*'%]`) @@ -36,10 +36,6 @@ type EdgeDocument struct { ToType string `json:"to_type"` } -func VertexFromFHIR(project, resourceType string, payload map[string]any) (VertexDocument, error) { - return VertexFromFHIRWithExtra(project, resourceType, payload, nil) -} - func VertexFromFHIRWithExtra(project, resourceType string, payload, extraArgs map[string]any) (VertexDocument, error) { id, ok := payload["id"].(string) if !ok || strings.TrimSpace(id) == "" { @@ -63,7 +59,7 @@ func VertexFromFHIRWithExtra(project, resourceType string, payload, extraArgs ma }, nil } -func EdgeFromGrip(project, sourceType string, edge *gripql.Edge) (EdgeDocument, error) { +func EdgeFromGrip(project, sourceType string, edge *model.Edge) (EdgeDocument, error) { if edge == nil { return EdgeDocument{}, fmt.Errorf("nil edge") } diff --git a/internal/httpapi/errors.go b/internal/httpapi/errors.go index 5474f81..b36f268 100644 --- a/internal/httpapi/errors.go +++ b/internal/httpapi/errors.go @@ -1,12 +1,5 @@ package httpapi -import ( - "errors" - "net/http" - - "github.com/calypr/loom/internal/dataframe" -) - // ErrorResponse is the transport-neutral JSON payload the HTTP adapter can // write before a streaming response has begun. type ErrorResponse struct { @@ -31,68 +24,8 @@ type MappedError struct { Cause error } -func (e MappedError) Error() string { - return e.Body.Error.Message -} - -func (e MappedError) Unwrap() error { return e.Cause } - // MapDataframeError maps a service error without inspecting its text. Unknown // errors are always redacted to INTERNAL_ERROR. -func MapDataframeError(err error, requestID string) MappedError { - userErr := dataframe.Normalize(err) - if userErr == nil { - return MappedError{} - } - code := userErr.Code() - return MappedError{ - Status: statusForDataframeCode(dataframe.ErrorCode(code)), - Body: ErrorResponse{Error: HTTPErrorBody{ - Code: code, - Message: dataframe.PublicMessage(userErr), - FieldPath: append([]string(nil), userErr.FieldPath()...), - Details: userErr.Details(), - Retryable: userErr.Retryable(), - RequestID: requestID, - }}, - Cause: err, - } -} - -func statusForDataframeCode(code dataframe.ErrorCode) int { - switch code { - case dataframe.CodeProjectRequired, - dataframe.CodeRootResourceTypeRequired, - dataframe.CodeUnknownField, - dataframe.CodeFieldNotPopulated, - dataframe.CodeInvalidTraversal, - dataframe.CodeUnsafeTraversalRoute, - dataframe.CodeInvalidFilter, - dataframe.CodeUnboundedPivot, - dataframe.CodeInvalidPivotColumn, - dataframe.CodeInvalidSlice, - dataframe.CodeInvalidCursor, - dataframe.CodeStaleCursor, - dataframe.CodeUnsupportedExportFormat: - return http.StatusBadRequest - case dataframe.CodeUnauthorizedProject: - return http.StatusForbidden - case dataframe.CodeClientCanceled: - return 499 // Client Closed Request, used by Fiber-compatible adapters. - case dataframe.CodeDatasetGenerationChanged: - return http.StatusConflict - case dataframe.CodePlanTooExpensive: - return http.StatusUnprocessableEntity - case dataframe.CodeBackendUnavailable: - return http.StatusServiceUnavailable - default: - return http.StatusInternalServerError - } -} // IsMappedError lets a coordinator preserve an already mapped error while // adding request logging. -func IsMappedError(err error) bool { - var mapped MappedError - return errors.As(err, &mapped) -} diff --git a/internal/httpapi/errors_test.go b/internal/httpapi/errors_test.go deleted file mode 100644 index 2c3b92f..0000000 --- a/internal/httpapi/errors_test.go +++ /dev/null @@ -1,61 +0,0 @@ -package httpapi - -import ( - "errors" - "net/http" - "testing" - - "github.com/calypr/loom/internal/dataframe" -) - -func TestMapDataframeErrorUsesSharedCodeAndStatus(t *testing.T) { - mapped := MapDataframeError(dataframe.NewError( - dataframe.CodeUnauthorizedProject, - "do not expose this message", - dataframe.WithDetails(map[string]any{"project": "secret", "collection": "Patient"}), - ), "request-789") - if mapped.Status != http.StatusForbidden { - t.Fatalf("status = %d", mapped.Status) - } - if mapped.Body.Error.Code != string(dataframe.CodeUnauthorizedProject) { - t.Fatalf("code = %q", mapped.Body.Error.Code) - } - if mapped.Body.Error.Message != "the requested project is not available" { - t.Fatalf("message = %q", mapped.Body.Error.Message) - } - if mapped.Body.Error.RequestID != "request-789" { - t.Fatalf("request ID = %q", mapped.Body.Error.RequestID) - } - if _, ok := mapped.Body.Error.Details["collection"]; ok { - t.Fatalf("collection leaked: %#v", mapped.Body.Error.Details) - } -} - -func TestMapDataframeErrorMapsOperationalConditions(t *testing.T) { - cases := []struct { - name string - err error - status int - code string - }{ - {name: "backend", err: dataframe.ErrBackendUnavailable, status: http.StatusServiceUnavailable, code: string(dataframe.CodeBackendUnavailable)}, - {name: "generation", err: dataframe.NewError(dataframe.CodeDatasetGenerationChanged, ""), status: http.StatusConflict, code: string(dataframe.CodeDatasetGenerationChanged)}, - {name: "internal", err: errors.New("driver AQL details"), status: http.StatusInternalServerError, code: string(dataframe.CodeInternalError)}, - } - for _, test := range cases { - t.Run(test.name, func(t *testing.T) { - mapped := MapDataframeError(test.err, "request") - if mapped.Status != test.status || mapped.Body.Error.Code != test.code { - t.Fatalf("mapped = %#v", mapped) - } - }) - } -} - -func TestMappedErrorPreservesCauseForLogging(t *testing.T) { - cause := errors.New("backend") - mapped := MapDataframeError(dataframe.Wrap(cause, dataframe.CodeBackendUnavailable, "private"), "request") - if !errors.Is(mapped, cause) { - t.Fatal("mapped error did not preserve cause") - } -} diff --git a/internal/httpapi/export.go b/internal/httpapi/export.go new file mode 100644 index 0000000..dcbd7e3 --- /dev/null +++ b/internal/httpapi/export.go @@ -0,0 +1,52 @@ +package httpapi + +import ( + "context" + "encoding/json" + "fmt" + "io" + + "github.com/calypr/loom/internal/authscope" + "github.com/calypr/loom/internal/dataset" + arangostore "github.com/calypr/loom/internal/store/arango" +) + +type RawExporter interface { + ExportRaw(context.Context, string, string, authscope.ReadScope, io.Writer) error +} + +type QueryRowsClient interface { + QueryRows(context.Context, string, int, map[string]interface{}, arangostore.RowVisitor) error +} + +type ArangoRawExporter struct { + Query QueryRowsClient + Manifests interface { + ReadManifest(context.Context, dataset.DatasetRef) (dataset.Manifest, error) + } +} + +func (e ArangoRawExporter) ExportRaw(ctx context.Context, project, generation string, scope authscope.ReadScope, out io.Writer) error { + ref, err := dataset.NewDatasetRef(project, generation) + if err != nil { + return err + } + manifest, err := e.Manifests.ReadManifest(ctx, ref) + if err != nil { + return err + } + if !manifest.IsReady() { + return fmt.Errorf("dataset generation %s/%s is not READY", project, generation) + } + encoder := json.NewEncoder(out) + for _, resourceType := range manifest.SchemaIdentity.GeneratedResourceTypes() { + collection := resourceType + query := "FOR doc IN @@collection FILTER doc.project == @project AND doc.dataset_generation == @generation AND (@auth_resource_paths_unrestricted == true OR doc.auth_resource_path IN @auth_resource_paths) SORT doc._key RETURN doc.payload" + if err := e.Query.QueryRows(ctx, query, 500, map[string]interface{}{"@collection": collection, "project": project, "generation": generation, "auth_resource_paths_unrestricted": scope.Unrestricted(), "auth_resource_paths": scope.AuthResourcePaths}, func(row map[string]any) error { + return encoder.Encode(row) + }); err != nil { + return fmt.Errorf("export %s/%s %s: %w", project, generation, resourceType, err) + } + } + return nil +} diff --git a/internal/httpapi/export_route.go b/internal/httpapi/export_route.go new file mode 100644 index 0000000..899ffa2 --- /dev/null +++ b/internal/httpapi/export_route.go @@ -0,0 +1,36 @@ +package httpapi + +import ( + "strings" + + "github.com/calypr/loom/internal/authscope" + "github.com/gofiber/fiber/v3" +) + +func (s *HTTPServer) exportGeneration(c fiber.Ctx) error { + if s.rawExporter == nil { + return &apiError{Status: fiber.StatusNotImplemented, Code: "export_not_configured", Message: "generation export is not configured"} + } + project := strings.TrimSpace(c.Params("project")) + generation := strings.TrimSpace(c.Params("generation")) + if project == "" || generation == "" { + return &apiError{Status: fiber.StatusBadRequest, Code: "missing_generation_identity", Message: "project and generation are required"} + } + principal, _ := c.Locals("principal").(*authscope.Principal) + scope := authscope.ReadScope{Mode: authscope.ReadScopeUnrestricted} + if s.scopeResolver != nil { + resolved, err := s.scopeResolver.ResolveReadScopeForGeneration(c.Context(), principal, project, generation, nil) + if err != nil { + return &apiError{Status: fiber.StatusForbidden, Code: "forbidden", Message: err.Error()} + } + if resolved.Mode == authscope.ReadScopeRestricted && len(resolved.AuthResourcePaths) == 0 { + return &apiError{Status: fiber.StatusForbidden, Code: "forbidden", Message: "caller has no read access to project"} + } + scope = resolved + } + c.Set(fiber.HeaderContentType, "application/x-ndjson") + if err := s.rawExporter.ExportRaw(c.Context(), project, generation, scope, c); err != nil { + return &apiError{Status: fiber.StatusBadRequest, Code: "export_failed", Message: err.Error()} + } + return nil +} diff --git a/internal/httpapi/generation_route.go b/internal/httpapi/generation_route.go new file mode 100644 index 0000000..277e72a --- /dev/null +++ b/internal/httpapi/generation_route.go @@ -0,0 +1,91 @@ +package httpapi + +import ( + "io" + "mime/multipart" + "os" + "path/filepath" + "strings" + + "github.com/calypr/loom/internal/authscope" + "github.com/gofiber/fiber/v3" +) + +func (s *HTTPServer) createGeneration(c fiber.Ctx) error { + if !c.IsMultipart() { + return &apiError{Status: fiber.StatusUnsupportedMediaType, Code: "unsupported_media_type", Message: "expected multipart/form-data"} + } + form, err := c.Req().MultipartForm() + if err != nil { + return &apiError{Status: fiber.StatusBadRequest, Code: "invalid_multipart_form", Message: err.Error()} + } + project := strings.TrimSpace(c.Req().FormValue("project")) + generation := strings.TrimSpace(c.Req().FormValue("generation")) + if project == "" || generation == "" { + return &apiError{Status: fiber.StatusBadRequest, Code: "missing_generation_identity", Message: "project and generation are required"} + } + files := form.File["file"] + if len(files) == 0 { + return &apiError{Status: fiber.StatusBadRequest, Code: "missing_file", Message: "at least one file is required"} + } + authResourcePath := strings.TrimSpace(c.Req().FormValue("auth_resource_path")) + principal, _ := c.Locals("principal").(*authscope.Principal) + if err := s.authz.AuthorizeWrite(c.Context(), principal, project, authResourcePath); err != nil { + return &apiError{Status: fiber.StatusForbidden, Code: "forbidden", Message: err.Error()} + } + stagedDir, err := stageGenerationFiles(files) + if err != nil { + return &apiError{Status: fiber.StatusInternalServerError, Code: "stage_failed", Message: err.Error()} + } + defer os.RemoveAll(stagedDir) + req := GenerationLoadRequest{Project: project, Generation: generation, AuthResourcePath: authResourcePath, StagedDir: stagedDir} + if principal != nil { + req.SubmittedBy = principal.Subject + } + result, err := s.service.RunGeneration(c.Context(), req) + if err != nil { + return &apiError{Status: fiber.StatusBadRequest, Code: "invalid_generation_request", Message: err.Error()} + } + return c.Status(fiber.StatusOK).JSON(result) +} + +func stageGenerationFiles(headers []*multipart.FileHeader) (string, error) { + dir, err := os.MkdirTemp("", "loom-generation-") + if err != nil { + return "", err + } + cleanup := func() { _ = os.RemoveAll(dir) } + seen := make(map[string]struct{}, len(headers)) + for _, header := range headers { + name := filepath.Base(header.Filename) + if name == "." || name == "" || name != header.Filename || filepath.Ext(name) != ".ndjson" { + cleanup() + return "", os.ErrInvalid + } + if _, ok := seen[name]; ok { + cleanup() + return "", os.ErrExist + } + seen[name] = struct{}{} + src, err := header.Open() + if err != nil { + cleanup() + return "", err + } + dst, err := os.Create(filepath.Join(dir, name)) + if err == nil { + _, err = io.Copy(dst, src) + } + _ = src.Close() + if dst != nil { + if closeErr := dst.Close(); err == nil { + err = closeErr + } + } + if err != nil { + cleanup() + return "", err + } + } + return dir, nil +} diff --git a/internal/httpapi/generation_route_test.go b/internal/httpapi/generation_route_test.go new file mode 100644 index 0000000..93298f2 --- /dev/null +++ b/internal/httpapi/generation_route_test.go @@ -0,0 +1,72 @@ +package httpapi + +import ( + "bytes" + "context" + "mime/multipart" + "net/http" + "net/http/httptest" + "testing" + + "github.com/calypr/loom/internal/ingest" +) + +type fakeGenerationRunner struct { + summary ingest.LoadSummary + req GenerationLoadRequest +} + +func (r *fakeGenerationRunner) RunGeneration(_ context.Context, req GenerationLoadRequest, _ ingest.EventSink) (ingest.LoadSummary, error) { + r.req = req + return r.summary, nil +} + +func TestCreateGenerationStagesCompleteBundle(t *testing.T) { + runner := &fakeGenerationRunner{summary: ingest.LoadSummary{Files: 2, VerticesInserted: 4}} + svc, err := NewService(ServiceConfig{Runner: fakeRunner{}, GenerationRunner: runner}) + if err != nil { + t.Fatal(err) + } + server, err := NewHTTPServer(HTTPConfig{Service: svc}) + if err != nil { + t.Fatal(err) + } + req := newGenerationMultipartRequest(t, "P1", "generation-1", map[string][]byte{ + "Patient.ndjson": []byte(`{"resourceType":"Patient","id":"1"}` + "\n"), + "Specimen.ndjson": []byte(`{"resourceType":"Specimen","id":"2"}` + "\n"), + }) + resp, err := server.App().Test(req) + if err != nil { + t.Fatal(err) + } + defer resp.Body.Close() + if resp.StatusCode != http.StatusOK { + t.Fatalf("status = %d, want %d", resp.StatusCode, http.StatusOK) + } + if runner.req.Project != "P1" || runner.req.Generation != "generation-1" { + t.Fatalf("request = %#v", runner.req) + } +} + +func newGenerationMultipartRequest(t *testing.T, project, generation string, files map[string][]byte) *http.Request { + t.Helper() + var body bytes.Buffer + writer := multipart.NewWriter(&body) + _ = writer.WriteField("project", project) + _ = writer.WriteField("generation", generation) + for name, content := range files { + part, err := writer.CreateFormFile("file", name) + if err != nil { + t.Fatal(err) + } + if _, err := part.Write(content); err != nil { + t.Fatal(err) + } + } + if err := writer.Close(); err != nil { + t.Fatal(err) + } + req := httptest.NewRequest(http.MethodPost, "/api/v1/datasets/P1/generations/generation-1", &body) + req.Header.Set("Content-Type", writer.FormDataContentType()) + return req +} diff --git a/internal/httpapi/http_test.go b/internal/httpapi/http_test.go index cd4a0f3..16b095e 100644 --- a/internal/httpapi/http_test.go +++ b/internal/httpapi/http_test.go @@ -65,6 +65,62 @@ func TestCreateImportAccepted(t *testing.T) { } } +func TestHealthzDoesNotRequireAuthentication(t *testing.T) { + svc, err := NewService(ServiceConfig{Runner: fakeRunner{}}) + if err != nil { + t.Fatal(err) + } + server, err := NewHTTPServer(HTTPConfig{Service: svc, Authenticator: authscope.BasicAuthenticator{Username: "u", Password: "p"}}) + if err != nil { + t.Fatal(err) + } + resp, err := server.App().Test(httptest.NewRequest(http.MethodGet, "/healthz", nil)) + if err != nil { + t.Fatal(err) + } + defer resp.Body.Close() + if resp.StatusCode != http.StatusOK { + t.Fatalf("health status = %d", resp.StatusCode) + } +} + +func TestNamedGraphQLBackendRoutes(t *testing.T) { + svc, err := NewService(ServiceConfig{Runner: fakeRunner{}}) + if err != nil { + t.Fatal(err) + } + handler := http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + w.WriteHeader(http.StatusNoContent) + }) + server, err := NewHTTPServer(HTTPConfig{ + Service: svc, + GraphQLHandler: handler, + ClickHouseGraphQLHandler: handler, + }) + if err != nil { + t.Fatal(err) + } + for _, path := range []string{"/graphql/graph", "/graphql/flat"} { + resp, err := server.App().Test(httptest.NewRequest(http.MethodPost, path, nil)) + if err != nil { + t.Fatalf("request %s: %v", path, err) + } + if resp.StatusCode != http.StatusNoContent { + resp.Body.Close() + t.Fatalf("request %s status = %d", path, resp.StatusCode) + } + resp.Body.Close() + } + resp, err := server.App().Test(httptest.NewRequest(http.MethodPost, "/graphql", nil)) + if err != nil { + t.Fatal(err) + } + resp.Body.Close() + if resp.StatusCode == http.StatusNoContent { + t.Fatalf("legacy /graphql unexpectedly served the GraphQL handler") + } +} + func TestCreateImportRejectsMissingProject(t *testing.T) { svc, err := NewService(ServiceConfig{ Runner: fakeRunner{summary: ingest.LoadSummary{Files: 1}}, diff --git a/internal/httpapi/middleware.go b/internal/httpapi/middleware.go index 46e6e3f..7924f76 100644 --- a/internal/httpapi/middleware.go +++ b/internal/httpapi/middleware.go @@ -44,6 +44,11 @@ func (s *HTTPServer) loggingMiddleware(c fiber.Ctx) error { } func (s *HTTPServer) authenticationMiddleware(c fiber.Ctx) error { + // Health probes must remain available before credentials are configured and + // are deliberately not project/data APIs. + if c.Path() == "/healthz" { + return c.Next() + } principal, err := s.authn.Authenticate(c.Context(), c.GetReqHeaders()) if err != nil { return &apiError{Status: fiber.StatusUnauthorized, Code: "unauthorized", Message: err.Error()} diff --git a/internal/httpapi/routes.go b/internal/httpapi/routes.go index 1e250d3..7450fdb 100644 --- a/internal/httpapi/routes.go +++ b/internal/httpapi/routes.go @@ -10,6 +10,14 @@ func (s *HTTPServer) register() { s.registerHealthRoutes() s.registerGraphQLRoutes() s.registerImportRoutes() + s.registerGenerationRoutes() +} + +func (s *HTTPServer) registerGenerationRoutes() { + s.app.Post("/api/v1/datasets/:project/generations/:generation", s.createGeneration) + if s.rawExporter != nil { + s.app.Get("/api/v1/datasets/:project/generations/:generation/export", s.exportGeneration) + } } func (s *HTTPServer) registerHealthRoutes() { @@ -20,13 +28,16 @@ func (s *HTTPServer) registerHealthRoutes() { func (s *HTTPServer) registerGraphQLRoutes() { if s.cfgGraphQLPlaygroundHandler != nil { - s.app.Get("/graphql", adaptor.HTTPHandlerWithContext(s.cfgGraphQLPlaygroundHandler)) + s.app.Get("/graphql/graph", adaptor.HTTPHandlerWithContext(s.cfgGraphQLPlaygroundHandler)) } if s.cfgApolloSandboxHandler != nil { s.app.Get("/apollo", adaptor.HTTPHandlerWithContext(s.cfgApolloSandboxHandler)) } if s.cfgGraphQLHandler != nil { - s.app.Post("/graphql", adaptor.HTTPHandlerWithContext(s.cfgGraphQLHandler)) + s.app.Post("/graphql/graph", adaptor.HTTPHandlerWithContext(s.cfgGraphQLHandler)) + } + if s.cfgClickHouseGraphQLHandler != nil { + s.app.Post("/graphql/flat", adaptor.HTTPHandlerWithContext(s.cfgClickHouseGraphQLHandler)) } } diff --git a/internal/httpapi/server.go b/internal/httpapi/server.go index b626d91..01f7ad3 100644 --- a/internal/httpapi/server.go +++ b/internal/httpapi/server.go @@ -14,7 +14,9 @@ type HTTPConfig struct { Service *Service Authenticator authscope.Authenticator Authorizer authscope.Authorizer + ScopeResolver *authscope.ScopeResolver GraphQLHandler http.Handler + ClickHouseGraphQLHandler http.Handler GraphQLPlaygroundHandler http.Handler ApolloSandboxHandler http.Handler Logger *slog.Logger @@ -25,6 +27,7 @@ type HTTPConfig struct { // a complete staged bundle loader instead: one uploaded resource file can // never safely become an immutable active dataset generation. DisableSingleResourceImports bool + RawExporter RawExporter } type HTTPServer struct { @@ -32,11 +35,14 @@ type HTTPServer struct { service *Service authn authscope.Authenticator authz authscope.Authorizer + scopeResolver *authscope.ScopeResolver logger *slog.Logger cfgGraphQLHandler http.Handler + cfgClickHouseGraphQLHandler http.Handler cfgGraphQLPlaygroundHandler http.Handler cfgApolloSandboxHandler http.Handler disableSingleResourceImports bool + rawExporter RawExporter } type apiError struct { @@ -81,11 +87,14 @@ func NewHTTPServer(cfg HTTPConfig) (*HTTPServer, error) { service: cfg.Service, authn: cfg.Authenticator, authz: cfg.Authorizer, + scopeResolver: cfg.ScopeResolver, logger: cfg.Logger, cfgGraphQLHandler: cfg.GraphQLHandler, + cfgClickHouseGraphQLHandler: cfg.ClickHouseGraphQLHandler, cfgGraphQLPlaygroundHandler: cfg.GraphQLPlaygroundHandler, cfgApolloSandboxHandler: cfg.ApolloSandboxHandler, disableSingleResourceImports: cfg.DisableSingleResourceImports, + rawExporter: cfg.RawExporter, } app := fiber.New(fiber.Config{ BodyLimit: cfg.BodyLimit, diff --git a/internal/httpapi/service.go b/internal/httpapi/service.go index 70038a2..5fc4cf0 100644 --- a/internal/httpapi/service.go +++ b/internal/httpapi/service.go @@ -5,6 +5,7 @@ import ( "errors" "log/slog" + "github.com/calypr/loom/internal/dataset" "github.com/calypr/loom/internal/ingest" ) @@ -28,10 +29,30 @@ type ImportResult struct { Summary *ingest.LoadSummary `json:"summary,omitempty"` } +type GenerationLoadRequest struct { + Project string + Generation string + AuthResourcePath string + StagedDir string + SubmittedBy string +} + +type GenerationLoadResult struct { + Project string `json:"project"` + Generation string `json:"generation"` + AuthResourcePath string `json:"auth_resource_path,omitempty"` + SubmittedBy string `json:"submitted_by,omitempty"` + Summary *ingest.LoadSummary `json:"summary,omitempty"` +} + type Runner interface { Run(ctx context.Context, req ImportRequest, sink ingest.EventSink) (ingest.LoadSummary, error) } +type GenerationRunner interface { + RunGeneration(ctx context.Context, req GenerationLoadRequest, sink ingest.EventSink) (ingest.LoadSummary, error) +} + type IngestRunner struct { BaseOptions ingest.LoadOptions } @@ -46,16 +67,33 @@ func (r IngestRunner) Run(ctx context.Context, req ImportRequest, sink ingest.Ev return ingest.LoadSingleResourceFile(ctx, opts, req.ResourceType, req.StagedFilePath) } +func (r IngestRunner) RunGeneration(ctx context.Context, req GenerationLoadRequest, sink ingest.EventSink) (ingest.LoadSummary, error) { + ref, err := dataset.NewDatasetRef(req.Project, req.Generation) + if err != nil { + return ingest.LoadSummary{}, err + } + opts := r.BaseOptions + opts.Project = req.Project + opts.AuthResourcePath = req.AuthResourcePath + opts.MetaDir = req.StagedDir + opts.Dataset = &ref + opts.Truncate = false + opts.EventSink = sink + return ingest.Load(ctx, opts) +} + type ServiceConfig struct { - Runner Runner - Logger *slog.Logger - OnSuccess func(project string) + Runner Runner + GenerationRunner GenerationRunner + Logger *slog.Logger + OnSuccess func(project string) } type Service struct { - runner Runner - logger *slog.Logger - onSuccess func(project string) + runner Runner + generationRunner GenerationRunner + logger *slog.Logger + onSuccess func(project string) } func NewService(cfg ServiceConfig) (*Service, error) { @@ -66,12 +104,32 @@ func NewService(cfg ServiceConfig) (*Service, error) { cfg.Logger = slog.Default() } return &Service{ - runner: cfg.Runner, - logger: cfg.Logger, - onSuccess: cfg.OnSuccess, + runner: cfg.Runner, + generationRunner: cfg.GenerationRunner, + logger: cfg.Logger, + onSuccess: cfg.OnSuccess, }, nil } +func (s *Service) RunGeneration(ctx context.Context, req GenerationLoadRequest) (*GenerationLoadResult, error) { + if s.generationRunner == nil { + return nil, errors.New("generation runner is not configured") + } + if req.Project == "" || req.Generation == "" || req.StagedDir == "" { + return nil, errors.New("project, generation, and staged directory are required") + } + summary, err := s.generationRunner.RunGeneration(ctx, req, nil) + if err != nil { + s.logger.Error("generation load failed", "project", req.Project, "generation", req.Generation, "error", err.Error()) + return nil, err + } + if s.onSuccess != nil { + s.onSuccess(req.Project) + } + s.logger.Info("generation load succeeded", "project", req.Project, "generation", req.Generation, "vertices", summary.VerticesInserted, "edges", summary.EdgesInserted) + return &GenerationLoadResult{Project: req.Project, Generation: req.Generation, AuthResourcePath: req.AuthResourcePath, SubmittedBy: req.SubmittedBy, Summary: &summary}, nil +} + func (s *Service) Run(ctx context.Context, req ImportRequest) (*ImportResult, error) { if req.Project == "" { return nil, errors.New("project is required") diff --git a/internal/ingest/generation_orchestration.go b/internal/ingest/generation_orchestration.go new file mode 100644 index 0000000..e9f5e8a --- /dev/null +++ b/internal/ingest/generation_orchestration.go @@ -0,0 +1,312 @@ +package ingest + +import ( + "context" + "errors" + "fmt" + "path/filepath" + "time" + + "github.com/calypr/loom/internal/catalog" + "github.com/calypr/loom/internal/dataset" + datasetarango "github.com/calypr/loom/internal/dataset/arango" + "github.com/calypr/loom/internal/graphschema" + + "github.com/bmeg/jsonschemagraph/graph" +) + +// Load selects the legacy loader only when no immutable Dataset reference was +// supplied. Dataset mode is a separate write contract: every physical graph +// document, catalog row, and lifecycle operation is bound to one generation. +func Load(ctx context.Context, opts LoadOptions) (LoadSummary, error) { + if opts.Dataset == nil { + return loadLegacy(ctx, opts) + } + return loadGeneration(ctx, opts) +} + +func loadGeneration(ctx context.Context, opts LoadOptions) (summary LoadSummary, err error) { + opts = normalizeLoadOptions(opts) + + start := time.Now() + files, err := DiscoverNDJSON(opts.MetaDir) + if err != nil { + return LoadSummary{}, err + } + summary = LoadSummary{ + Files: len(files), + Resources: map[string]int{}, + BatchCounts: map[string]int{}, + StageSeconds: map[string]float64{}, + } + + // Keep the existing ordered evidence path: discover staged files, parse the + // graph with its authoritative loader, capture identity from the same file, + // then preflight every staged payload before contacting Arango. + schema, err := graph.Load(opts.Schema) + if err != nil { + return summary, err + } + schemaIdentity, err := graphschema.Load(opts.Schema) + if err != nil { + return summary, err + } + summary.SchemaIdentity = &schemaIdentity + preflightSampleRows := opts.PreflightSampleRows + if preflightSampleRows <= 0 { + preflightSampleRows = defaultPreflightSampleRows + } + emitEvent(opts.EventSink, "go_preflight_start", map[string]any{ + "files": len(files), + "sampleRows": preflightSampleRows, + "schemaSha256": schemaIdentity.SchemaSHA256(), + "generatedRootCount": len(schemaIdentity.GeneratedResourceTypes()), + }) + preflightStart := time.Now() + preflight, err := PreflightFiles(files, schema, preflightSampleRows) + summary.Preflight = preflight + summary.StageSeconds["preflight"] = time.Since(preflightStart).Seconds() + emitEvent(opts.EventSink, "go_preflight_complete", map[string]any{ + "files": len(files), + "resources": len(preflight.Resources), + "issues": len(preflight.Issues), + "seconds": summary.StageSeconds["preflight"], + }) + if err != nil { + return summary, err + } + + plan, err := newGenerationLoadPlan(opts, files, schemaIdentity) + if err != nil { + return summary, err + } + // Load dispatch guarantees a plan, but retaining this check makes the + // generation-only helper safe if it is called directly in a future test. + if plan == nil { + return summary, fmt.Errorf("dataset generation plan is required") + } + datasetRef := plan.Dataset + summary.Dataset = &datasetRef + if err := ctx.Err(); err != nil { + return summary, err + } + + resourceTypes := make([]string, 0, len(preflight.Resources)) + for _, resource := range preflight.Resources { + resourceTypes = append(resourceTypes, resource.ResourceType) + } + + emitEvent(opts.EventSink, "go_backend_connect_start", map[string]any{ + "backend": "arango", + "url": opts.URL, + "database": opts.Database, + }) + connectStart := time.Now() + client, err := openBackend(ctx, opts.ConnectionOptions) + if err != nil { + return summary, err + } + defer func() { _ = client.Close(context.WithoutCancel(ctx)) }() + emitEvent(opts.EventSink, "go_backend_connect_complete", map[string]any{ + "backend": "arango", + "url": opts.URL, + "database": opts.Database, + "seconds": time.Since(connectStart).Seconds(), + }) + + bootstrapStart := time.Now() + // Metadata comes first and never truncates. This order means a failed + // graph bootstrap cannot delete lifecycle history from a prior generation. + emitEvent(opts.EventSink, "go_bootstrap_start", map[string]any{ + "database": opts.Database, + "resources": 0, + "truncate": false, + "collections": "dataset_lifecycle", + }) + if err = client.Bootstrap(ctx, lifecycleBootstrapSpecWithReporter(opts.EventSink)); err != nil { + return summary, err + } + emitEvent(opts.EventSink, "go_bootstrap_start", map[string]any{ + "database": opts.Database, + "resources": len(resourceTypes), + "truncate": false, + }) + if err = client.Bootstrap(ctx, bootstrapSpecWithReporter(resourceTypes, false, opts.EventSink)); err != nil { + return summary, err + } + summary.StageSeconds["bootstrap"] = time.Since(bootstrapStart).Seconds() + + lifecycleStore, err := datasetarango.New(client) + if err != nil { + return summary, err + } + manifest, err := lifecycleStore.CreateManifest(ctx, plan.Manifest) + if err != nil { + return summary, err + } + manifestReady := false + defer func() { + if err == nil || manifestReady { + return + } + // A canceled request must not strand a PRE-FLIGHT/LOADING/ANALYZING + // manifest. Once READY is persisted we deliberately leave it alone, + // because an activation error is an unknown outcome rather than proof + // that the generation failed. + _, cleanupErr := lifecycleStore.TransitionManifest( + context.WithoutCancel(ctx), + manifest, + dataset.ManifestStateFailed, + ) + if cleanupErr != nil { + err = errors.Join(err, fmt.Errorf("mark dataset generation %s/%s failed: %w", plan.Dataset.Project, plan.Dataset.Generation, cleanupErr)) + } + }() + loadingManifest, transitionErr := lifecycleStore.TransitionManifest(ctx, manifest, dataset.ManifestStateLoading) + if transitionErr != nil { + return summary, transitionErr + } + manifest = loadingManifest + + catalogs := make(map[generationCatalogKey]*catalog.Profiler) + relationshipCounts := make(map[catalog.RelationshipKey]int64) + for _, file := range files { + if err = ctx.Err(); err != nil { + return summary, err + } + fileStart := time.Now() + resourceType := ResourceTypeFromPath(file) + emitEvent(opts.EventSink, "go_load_file_start", map[string]any{"file": file, "resource": resourceType}) + + result, fileErr := loadGenerationFile( + ctx, + opts, + client, + schema, + file, + plan.Dataset.Generation, + start, + summary.VerticesInserted, + summary.EdgesInserted, + ) + if fileErr != nil { + return summary, fileErr + } + + summary.VerticesInserted += result.VerticesInserted + summary.EdgesInserted += result.EdgesInserted + summary.ValidationErrors += result.ValidationErrors + summary.GenerationErrors += result.GenerationErrors + summary.EdgeErrors += result.EdgeErrors + summary.Resources[result.ResourceType] += result.Rows + summary.BatchCounts["vertex_insert"] += result.VertexBatches + summary.BatchCounts["edge_insert"] += result.EdgeBatches + for name, seconds := range result.StageSeconds { + summary.StageSeconds[name] += seconds + } + catalog.MergeRelationshipCounts(relationshipCounts, result.RelationshipCounts) + + key := generationCatalogKey{ + project: opts.Project, + datasetGeneration: plan.Dataset.Generation, + authResourcePath: opts.AuthResourcePath, + resourceType: result.ResourceType, + } + merged := catalogs[key] + if merged == nil { + merged = catalog.NewProfilerForGeneration( + key.project, + key.datasetGeneration, + key.authResourcePath, + key.resourceType, + catalog.NewShapePlanCache(), + ) + catalogs[key] = merged + } + if err = merged.Merge(result.Catalog); err != nil { + return summary, fmt.Errorf("merge catalog for dataset generation %s/%s %s: %w", plan.Dataset.Project, plan.Dataset.Generation, result.ResourceType, err) + } + + emitEvent(opts.EventSink, "go_load_file_complete", map[string]any{ + "file": filepath.Base(file), + "resource": result.ResourceType, + "file_rows": result.Rows, + "file_vertices": result.VerticesBuilt, + "file_edges": result.EdgesBuilt, + "seconds": time.Since(fileStart).Seconds(), + }) + } + + if summary.ValidationErrors != 0 || summary.GenerationErrors != 0 || summary.EdgeErrors != 0 { + return summary, &GenerationLoadIncompleteError{ + Dataset: plan.Dataset, + ValidationErrors: summary.ValidationErrors, + GenerationErrors: summary.GenerationErrors, + EdgeErrors: summary.EdgeErrors, + } + } + if err = ctx.Err(); err != nil { + return summary, err + } + + // ANALYZING is intentionally limited to catalog finalization in this + // packet. No synthetic analysis version is attached; a later analysis owner + // can add one without changing the load/activation contract. + analyzingManifest, transitionErr := lifecycleStore.TransitionManifest(ctx, manifest, dataset.ManifestStateAnalyzing) + if transitionErr != nil { + return summary, transitionErr + } + manifest = analyzingManifest + for _, key := range sortedGenerationCatalogKeys(catalogs) { + if err = ctx.Err(); err != nil { + return summary, err + } + if err = catalog.WriteFieldCatalog( + ctx, + client, + catalog.FieldCatalogCollection, + catalogs[key].Documents(), + opts.BatchSize, + false, + opts.WriteAPI, + summary.StageSeconds, + ); err != nil { + return summary, err + } + } + if err = catalog.WriteRelationshipCatalog( + ctx, + client, + catalog.RelationshipCatalogDocuments(relationshipCounts), + opts.BatchSize, + false, + opts.WriteAPI, + summary.StageSeconds, + ); err != nil { + return summary, err + } + if err = ctx.Err(); err != nil { + return summary, err + } + readyManifest, transitionErr := lifecycleStore.TransitionManifest(ctx, manifest, dataset.ManifestStateReady) + if transitionErr != nil { + return summary, transitionErr + } + manifest = readyManifest + manifestReady = true + if _, activationErr := lifecycleStore.Activate(ctx, manifest); activationErr != nil { + return summary, &ActivationOutcomeError{Dataset: plan.Dataset, Err: activationErr} + } + + emitEvent(opts.EventSink, "go_load_complete", map[string]any{ + "files": summary.Files, + "vertices_inserted": summary.VerticesInserted, + "edges_inserted": summary.EdgesInserted, + "validation_errors": summary.ValidationErrors, + "generation_errors": summary.GenerationErrors, + "edge_errors": summary.EdgeErrors, + "dataset_generation": plan.Dataset.Generation, + "seconds": SecondsSince(start), + }) + return summary, nil +} diff --git a/internal/ingest/generation_load.go b/internal/ingest/generation_workers.go similarity index 53% rename from internal/ingest/generation_load.go rename to internal/ingest/generation_workers.go index c6142c5..37e7f3d 100644 --- a/internal/ingest/generation_load.go +++ b/internal/ingest/generation_workers.go @@ -3,7 +3,6 @@ package ingest import ( "context" "encoding/json" - "errors" "fmt" "path/filepath" "sort" @@ -13,310 +12,11 @@ import ( "time" "github.com/calypr/loom/internal/catalog" - "github.com/calypr/loom/internal/dataset" - datasetarango "github.com/calypr/loom/internal/dataset/arango" - "github.com/calypr/loom/internal/graphschema" arangostore "github.com/calypr/loom/internal/store/arango" "github.com/bmeg/jsonschemagraph/graph" ) -// Load selects the legacy loader only when no immutable Dataset reference was -// supplied. Dataset mode is a separate write contract: every physical graph -// document, catalog row, and lifecycle operation is bound to one generation. -func Load(ctx context.Context, opts LoadOptions) (LoadSummary, error) { - if opts.Dataset == nil { - return loadLegacy(ctx, opts) - } - return loadGeneration(ctx, opts) -} - -func loadGeneration(ctx context.Context, opts LoadOptions) (summary LoadSummary, err error) { - opts = normalizeLoadOptions(opts) - - start := time.Now() - files, err := DiscoverNDJSON(opts.MetaDir) - if err != nil { - return LoadSummary{}, err - } - summary = LoadSummary{ - Files: len(files), - Resources: map[string]int{}, - BatchCounts: map[string]int{}, - StageSeconds: map[string]float64{}, - } - - // Keep the existing ordered evidence path: discover staged files, parse the - // graph with its authoritative loader, capture identity from the same file, - // then preflight every staged payload before contacting Arango. - schema, err := graph.Load(opts.Schema) - if err != nil { - return summary, err - } - schemaIdentity, err := graphschema.Load(opts.Schema) - if err != nil { - return summary, err - } - summary.SchemaIdentity = &schemaIdentity - preflightSampleRows := opts.PreflightSampleRows - if preflightSampleRows <= 0 { - preflightSampleRows = defaultPreflightSampleRows - } - emitEvent(opts.EventSink, "go_preflight_start", map[string]any{ - "files": len(files), - "sampleRows": preflightSampleRows, - "schemaSha256": schemaIdentity.SchemaSHA256(), - "generatedRootCount": len(schemaIdentity.GeneratedResourceTypes()), - }) - preflightStart := time.Now() - preflight, err := PreflightFiles(files, schema, preflightSampleRows) - summary.Preflight = preflight - summary.StageSeconds["preflight"] = time.Since(preflightStart).Seconds() - emitEvent(opts.EventSink, "go_preflight_complete", map[string]any{ - "files": len(files), - "resources": len(preflight.Resources), - "issues": len(preflight.Issues), - "seconds": summary.StageSeconds["preflight"], - }) - if err != nil { - return summary, err - } - - plan, err := newGenerationLoadPlan(opts, files, schemaIdentity) - if err != nil { - return summary, err - } - // Load dispatch guarantees a plan, but retaining this check makes the - // generation-only helper safe if it is called directly in a future test. - if plan == nil { - return summary, fmt.Errorf("dataset generation plan is required") - } - datasetRef := plan.Dataset - summary.Dataset = &datasetRef - if err := ctx.Err(); err != nil { - return summary, err - } - - resourceTypes := make([]string, 0, len(preflight.Resources)) - for _, resource := range preflight.Resources { - resourceTypes = append(resourceTypes, resource.ResourceType) - } - - emitEvent(opts.EventSink, "go_backend_connect_start", map[string]any{ - "backend": "arango", - "url": opts.URL, - "database": opts.Database, - }) - connectStart := time.Now() - client, err := openBackend(ctx, opts.ConnectionOptions) - if err != nil { - return summary, err - } - defer func() { _ = client.Close(context.WithoutCancel(ctx)) }() - emitEvent(opts.EventSink, "go_backend_connect_complete", map[string]any{ - "backend": "arango", - "url": opts.URL, - "database": opts.Database, - "seconds": time.Since(connectStart).Seconds(), - }) - - bootstrapStart := time.Now() - // Metadata comes first and never truncates. This order means a failed - // graph bootstrap cannot delete lifecycle history from a prior generation. - emitEvent(opts.EventSink, "go_bootstrap_start", map[string]any{ - "database": opts.Database, - "resources": 0, - "truncate": false, - "collections": "dataset_lifecycle", - }) - if err = client.Bootstrap(ctx, lifecycleBootstrapSpecWithReporter(opts.EventSink)); err != nil { - return summary, err - } - emitEvent(opts.EventSink, "go_bootstrap_start", map[string]any{ - "database": opts.Database, - "resources": len(resourceTypes), - "truncate": false, - }) - if err = client.Bootstrap(ctx, bootstrapSpecWithReporter(resourceTypes, false, opts.EventSink)); err != nil { - return summary, err - } - summary.StageSeconds["bootstrap"] = time.Since(bootstrapStart).Seconds() - - lifecycleStore, err := datasetarango.New(client) - if err != nil { - return summary, err - } - manifest, err := lifecycleStore.CreateManifest(ctx, plan.Manifest) - if err != nil { - return summary, err - } - manifestReady := false - defer func() { - if err == nil || manifestReady { - return - } - // A canceled request must not strand a PRE-FLIGHT/LOADING/ANALYZING - // manifest. Once READY is persisted we deliberately leave it alone, - // because an activation error is an unknown outcome rather than proof - // that the generation failed. - _, cleanupErr := lifecycleStore.TransitionManifest( - context.WithoutCancel(ctx), - manifest, - dataset.ManifestStateFailed, - ) - if cleanupErr != nil { - err = errors.Join(err, fmt.Errorf("mark dataset generation %s/%s failed: %w", plan.Dataset.Project, plan.Dataset.Generation, cleanupErr)) - } - }() - loadingManifest, transitionErr := lifecycleStore.TransitionManifest(ctx, manifest, dataset.ManifestStateLoading) - if transitionErr != nil { - return summary, transitionErr - } - manifest = loadingManifest - - catalogs := make(map[generationCatalogKey]*catalog.Profiler) - relationshipCounts := make(map[catalog.RelationshipKey]int64) - for _, file := range files { - if err = ctx.Err(); err != nil { - return summary, err - } - fileStart := time.Now() - resourceType := ResourceTypeFromPath(file) - emitEvent(opts.EventSink, "go_load_file_start", map[string]any{"file": file, "resource": resourceType}) - - result, fileErr := loadGenerationFile( - ctx, - opts, - client, - schema, - file, - plan.Dataset.Generation, - start, - summary.VerticesInserted, - summary.EdgesInserted, - ) - if fileErr != nil { - return summary, fileErr - } - - summary.VerticesInserted += result.VerticesInserted - summary.EdgesInserted += result.EdgesInserted - summary.ValidationErrors += result.ValidationErrors - summary.GenerationErrors += result.GenerationErrors - summary.EdgeErrors += result.EdgeErrors - summary.Resources[result.ResourceType] += result.Rows - summary.BatchCounts["vertex_insert"] += result.VertexBatches - summary.BatchCounts["edge_insert"] += result.EdgeBatches - for name, seconds := range result.StageSeconds { - summary.StageSeconds[name] += seconds - } - catalog.MergeRelationshipCounts(relationshipCounts, result.RelationshipCounts) - - key := generationCatalogKey{ - project: opts.Project, - datasetGeneration: plan.Dataset.Generation, - authResourcePath: opts.AuthResourcePath, - resourceType: result.ResourceType, - } - merged := catalogs[key] - if merged == nil { - merged = catalog.NewProfilerForGeneration( - key.project, - key.datasetGeneration, - key.authResourcePath, - key.resourceType, - catalog.NewShapePlanCache(), - ) - catalogs[key] = merged - } - if err = merged.Merge(result.Catalog); err != nil { - return summary, fmt.Errorf("merge catalog for dataset generation %s/%s %s: %w", plan.Dataset.Project, plan.Dataset.Generation, result.ResourceType, err) - } - - emitEvent(opts.EventSink, "go_load_file_complete", map[string]any{ - "file": filepath.Base(file), - "resource": result.ResourceType, - "file_rows": result.Rows, - "file_vertices": result.VerticesBuilt, - "file_edges": result.EdgesBuilt, - "seconds": time.Since(fileStart).Seconds(), - }) - } - - if summary.ValidationErrors != 0 || summary.GenerationErrors != 0 || summary.EdgeErrors != 0 { - return summary, &GenerationLoadIncompleteError{ - Dataset: plan.Dataset, - ValidationErrors: summary.ValidationErrors, - GenerationErrors: summary.GenerationErrors, - EdgeErrors: summary.EdgeErrors, - } - } - if err = ctx.Err(); err != nil { - return summary, err - } - - // ANALYZING is intentionally limited to catalog finalization in this - // packet. No synthetic analysis version is attached; a later analysis owner - // can add one without changing the load/activation contract. - analyzingManifest, transitionErr := lifecycleStore.TransitionManifest(ctx, manifest, dataset.ManifestStateAnalyzing) - if transitionErr != nil { - return summary, transitionErr - } - manifest = analyzingManifest - for _, key := range sortedGenerationCatalogKeys(catalogs) { - if err = ctx.Err(); err != nil { - return summary, err - } - if err = catalog.WriteFieldCatalog( - ctx, - client, - catalog.FieldCatalogCollection, - catalogs[key].Documents(), - opts.BatchSize, - false, - opts.WriteAPI, - summary.StageSeconds, - ); err != nil { - return summary, err - } - } - if err = catalog.WriteRelationshipCatalog( - ctx, - client, - catalog.RelationshipCatalogDocuments(relationshipCounts), - opts.BatchSize, - false, - opts.WriteAPI, - summary.StageSeconds, - ); err != nil { - return summary, err - } - if err = ctx.Err(); err != nil { - return summary, err - } - readyManifest, transitionErr := lifecycleStore.TransitionManifest(ctx, manifest, dataset.ManifestStateReady) - if transitionErr != nil { - return summary, transitionErr - } - manifest = readyManifest - manifestReady = true - if _, activationErr := lifecycleStore.Activate(ctx, manifest); activationErr != nil { - return summary, &ActivationOutcomeError{Dataset: plan.Dataset, Err: activationErr} - } - - emitEvent(opts.EventSink, "go_load_complete", map[string]any{ - "files": summary.Files, - "vertices_inserted": summary.VerticesInserted, - "edges_inserted": summary.EdgesInserted, - "validation_errors": summary.ValidationErrors, - "generation_errors": summary.GenerationErrors, - "edge_errors": summary.EdgeErrors, - "dataset_generation": plan.Dataset.Generation, - "seconds": SecondsSince(start), - }) - return summary, nil -} - type generationCatalogKey struct { project string datasetGeneration string diff --git a/internal/ingest/load.go b/internal/ingest/legacy_load.go similarity index 61% rename from internal/ingest/load.go rename to internal/ingest/legacy_load.go index 4ac6339..defc548 100644 --- a/internal/ingest/load.go +++ b/internal/ingest/legacy_load.go @@ -3,10 +3,7 @@ package ingest import ( "context" "encoding/json" - "errors" "fmt" - "io" - "os" "path/filepath" "strings" "sync" @@ -14,196 +11,11 @@ import ( "time" "github.com/calypr/loom/internal/catalog" - "github.com/calypr/loom/internal/dataset" "github.com/calypr/loom/internal/graphschema" - arangostore "github.com/calypr/loom/internal/store/arango" - "github.com/bmeg/jsonschema/v6" "github.com/bmeg/jsonschemagraph/graph" - "github.com/bmeg/jsonschemagraph/util" ) -type LoadOptions struct { - arangostore.ConnectionOptions - Schema string - MetaDir string - Project string - AuthResourcePath string - BatchSize int - ProgressEvery int - WriterCount int - Truncate bool - FailFast bool - UseGeneric bool - WriteAPI string - EventSink EventSink - // Dataset selects immutable generation mode. A nil value preserves the - // original unversioned loader behavior. A non-nil value requires a complete - // directory import, writes generation-qualified graph identities, and only - // activates the generation after every graph file and catalog finalization - // succeeds. - Dataset *dataset.DatasetRef - // PreflightSampleRows bounds the number of payloads inspected from every - // staged file before Loom opens or mutates Arango. Zero uses the safe - // default; full row validation still happens in the loader. - PreflightSampleRows int -} - -type LoadSummary struct { - Files int `json:"files"` - VerticesInserted int `json:"vertices_inserted"` - EdgesInserted int `json:"edges_inserted"` - ValidationErrors int `json:"validation_errors"` - GenerationErrors int `json:"generation_errors"` - EdgeErrors int `json:"edge_errors"` - BatchCounts map[string]int `json:"batch_counts,omitempty"` - Resources map[string]int `json:"resources"` - StageSeconds map[string]float64 `json:"stage_seconds"` - Preflight PreflightReport `json:"preflight"` - // SchemaIdentity is the exact configured graph-schema evidence used for - // this load. It remains nil when Loom cannot load the configured schema, so - // an early failure never looks like a successful schema observation. - SchemaIdentity *graphschema.Identity `json:"schema_identity,omitempty"` - // Dataset is the immutable target when this was a generation load. It is - // present even on a failed generation load so callers can identify the - // inactive manifest that needs operational inspection. - Dataset *dataset.DatasetRef `json:"dataset,omitempty"` -} - -// normalizeLoadOptions applies the operational defaults shared by both loader -// modes. The immutable-generation loader deliberately has a different write -// lifecycle, but both modes must agree on batching, progress, concurrency, and -// the Arango write API before any input is examined. -func normalizeLoadOptions(opts LoadOptions) LoadOptions { - if opts.BatchSize <= 0 { - opts.BatchSize = 5000 - } - if opts.ProgressEvery <= 0 { - opts.ProgressEvery = 50000 - } - if opts.WriterCount <= 0 { - opts.WriterCount = 8 - } - if opts.WriteAPI == "" { - opts.WriteAPI = "import" - } - return opts -} - -var ( - // ErrGenerationLoadRequiresDirectory prevents a one-file or arbitrary-file - // load from being mistaken for a complete immutable dataset snapshot. - ErrGenerationLoadRequiresDirectory = errors.New("dataset generation load requires a directory") - // ErrGenerationLoadRequiresFiles prevents an empty staged directory from - // becoming an active but meaningless generation. - ErrGenerationLoadRequiresFiles = errors.New("dataset generation load requires at least one NDJSON file") - // ErrGenerationLoadTruncateForbidden prevents a new generation from - // deleting active or historical graph data. - ErrGenerationLoadTruncateForbidden = errors.New("dataset generation load cannot truncate collections") - // ErrGenerationDatasetProjectMismatch prevents graph documents and their - // lifecycle manifest from being scoped to different projects. - ErrGenerationDatasetProjectMismatch = errors.New("dataset generation project does not match load project") - // ErrGenerationSingleResourceUnsupported makes the legacy HTTP one-file - // path explicitly unavailable in immutable snapshot mode. - ErrGenerationSingleResourceUnsupported = errors.New("single-resource imports cannot create a dataset generation") -) - -// ActivationOutcomeError means the generation reached READY but Loom could -// not prove that the active-generation pointer was updated. READY is kept for -// an operator to reconcile; it must never be downgraded to FAILED because the -// activation request may have committed before its error reached the caller. -type ActivationOutcomeError struct { - Dataset dataset.DatasetRef - Err error -} - -func (e *ActivationOutcomeError) Error() string { - if e == nil { - return "dataset generation activation outcome is unknown" - } - return fmt.Sprintf("activate dataset generation %s/%s: %v", e.Dataset.Project, e.Dataset.Generation, e.Err) -} - -func (e *ActivationOutcomeError) Unwrap() error { - if e == nil { - return nil - } - return e.Err -} - -// GenerationLoadIncompleteError means row-level validation, generation, or -// edge construction failures were observed while FailFast was disabled. The -// graph may contain partial immutable documents, but the lifecycle manifest is -// deliberately left FAILED and cannot be selected for reads. -type GenerationLoadIncompleteError struct { - Dataset dataset.DatasetRef - ValidationErrors int - GenerationErrors int - EdgeErrors int -} - -func (e *GenerationLoadIncompleteError) Error() string { - if e == nil { - return "dataset generation load is incomplete" - } - return fmt.Sprintf( - "dataset generation %s/%s is incomplete: validation_errors=%d generation_errors=%d edge_errors=%d", - e.Dataset.Project, - e.Dataset.Generation, - e.ValidationErrors, - e.GenerationErrors, - e.EdgeErrors, - ) -} - -type generationLoadPlan struct { - Dataset dataset.DatasetRef - Manifest dataset.Manifest -} - -// newGenerationLoadPlan validates and snapshots all immutable information -// after input preflight and before a database connection is opened. Nil keeps -// the legacy loader path exactly unversioned. -func newGenerationLoadPlan(opts LoadOptions, files []string, identity graphschema.Identity) (*generationLoadPlan, error) { - if opts.Dataset == nil { - return nil, nil - } - - ref := *opts.Dataset - if err := ref.Validate(); err != nil { - return nil, err - } - if ref.Project != opts.Project { - return nil, fmt.Errorf("%w: dataset project %q, load project %q", ErrGenerationDatasetProjectMismatch, ref.Project, opts.Project) - } - if opts.Truncate { - return nil, ErrGenerationLoadTruncateForbidden - } - info, err := os.Stat(opts.MetaDir) - if err != nil { - return nil, fmt.Errorf("inspect dataset generation directory: %w", err) - } - if !info.IsDir() { - return nil, fmt.Errorf("%w: %q", ErrGenerationLoadRequiresDirectory, opts.MetaDir) - } - if len(files) == 0 { - return nil, fmt.Errorf("%w: %q", ErrGenerationLoadRequiresFiles, opts.MetaDir) - } - - schemaSnapshot, err := dataset.SnapshotSchemaIdentity(identity) - if err != nil { - return nil, fmt.Errorf("snapshot dataset generation schema identity: %w", err) - } - manifest, err := dataset.NewManifest(ref, schemaSnapshot) - if err != nil { - return nil, fmt.Errorf("create dataset generation manifest: %w", err) - } - return &generationLoadPlan{Dataset: ref, Manifest: manifest}, nil -} - -// loadLegacy is the original unversioned loader. Load dispatches to it only -// when Dataset is nil so existing import/API behavior and physical identities -// remain unchanged while generation mode evolves independently. func loadLegacy(ctx context.Context, opts LoadOptions) (LoadSummary, error) { opts = normalizeLoadOptions(opts) start := time.Now() @@ -632,60 +444,3 @@ func loadLegacy(ctx context.Context, opts LoadOptions) (LoadSummary, error) { }) return summary, nil } - -func graphObjectID(payload map[string]any, class *jsonschema.Schema) (string, error) { - return util.GetObjectID(payload, class) -} - -func graphExtraArgs(authResourcePath string) map[string]any { - if authResourcePath == "" { - return nil - } - return map[string]any{ - "auth_resource_path": authResourcePath, - } -} - -func LoadSingleResourceReader(ctx context.Context, opts LoadOptions, resourceType string, reader io.Reader, compressed bool) (LoadSummary, error) { - if opts.Dataset != nil { - return LoadSummary{}, ErrGenerationSingleResourceUnsupported - } - dir, err := os.MkdirTemp("", "arango-fhir-single-resource-*") - if err != nil { - return LoadSummary{}, err - } - defer os.RemoveAll(dir) - - name := resourceType + ".ndjson" - if compressed { - name += ".gz" - } - target := filepath.Join(dir, name) - f, err := os.Create(target) - if err != nil { - return LoadSummary{}, err - } - if _, err := io.Copy(f, reader); err != nil { - f.Close() - return LoadSummary{}, err - } - if err := f.Close(); err != nil { - return LoadSummary{}, err - } - - singleOpts := opts - singleOpts.MetaDir = dir - return Load(ctx, singleOpts) -} - -func LoadSingleResourceFile(ctx context.Context, opts LoadOptions, resourceType, path string) (LoadSummary, error) { - if opts.Dataset != nil { - return LoadSummary{}, ErrGenerationSingleResourceUnsupported - } - file, err := os.Open(path) - if err != nil { - return LoadSummary{}, err - } - defer file.Close() - return LoadSingleResourceReader(ctx, opts, resourceType, file, strings.HasSuffix(path, ".gz")) -} diff --git a/internal/ingest/load_options.go b/internal/ingest/load_options.go new file mode 100644 index 0000000..8818ed2 --- /dev/null +++ b/internal/ingest/load_options.go @@ -0,0 +1,193 @@ +package ingest + +import ( + "errors" + "fmt" + "os" + + "github.com/calypr/loom/internal/dataset" + "github.com/calypr/loom/internal/graphschema" + arangostore "github.com/calypr/loom/internal/store/arango" +) + +type LoadOptions struct { + arangostore.ConnectionOptions + Schema string + MetaDir string + Project string + AuthResourcePath string + BatchSize int + ProgressEvery int + WriterCount int + Truncate bool + FailFast bool + UseGeneric bool + WriteAPI string + EventSink EventSink + // Dataset selects immutable generation mode. A nil value preserves the + // original unversioned loader behavior. A non-nil value requires a complete + // directory import, writes generation-qualified graph identities, and only + // activates the generation after every graph file and catalog finalization + // succeeds. + Dataset *dataset.DatasetRef + // PreflightSampleRows bounds the number of payloads inspected from every + // staged file before Loom opens or mutates Arango. Zero uses the safe + // default; full row validation still happens in the loader. + PreflightSampleRows int +} + +type LoadSummary struct { + Files int `json:"files"` + VerticesInserted int `json:"vertices_inserted"` + EdgesInserted int `json:"edges_inserted"` + ValidationErrors int `json:"validation_errors"` + GenerationErrors int `json:"generation_errors"` + EdgeErrors int `json:"edge_errors"` + BatchCounts map[string]int `json:"batch_counts,omitempty"` + Resources map[string]int `json:"resources"` + StageSeconds map[string]float64 `json:"stage_seconds"` + Preflight PreflightReport `json:"preflight"` + // SchemaIdentity is the exact configured graph-schema evidence used for + // this load. It remains nil when Loom cannot load the configured schema, so + // an early failure never looks like a successful schema observation. + SchemaIdentity *graphschema.Identity `json:"schema_identity,omitempty"` + // Dataset is the immutable target when this was a generation load. It is + // present even on a failed generation load so callers can identify the + // inactive manifest that needs operational inspection. + Dataset *dataset.DatasetRef `json:"dataset,omitempty"` +} + +// normalizeLoadOptions applies the operational defaults shared by both loader +// modes. The immutable-generation loader deliberately has a different write +// lifecycle, but both modes must agree on batching, progress, concurrency, and +// the Arango write API before any input is examined. +func normalizeLoadOptions(opts LoadOptions) LoadOptions { + if opts.BatchSize <= 0 { + opts.BatchSize = 5000 + } + if opts.ProgressEvery <= 0 { + opts.ProgressEvery = 50000 + } + if opts.WriterCount <= 0 { + opts.WriterCount = 8 + } + if opts.WriteAPI == "" { + opts.WriteAPI = "import" + } + return opts +} + +var ( + // ErrGenerationLoadRequiresDirectory prevents a one-file or arbitrary-file + // load from being mistaken for a complete immutable dataset snapshot. + ErrGenerationLoadRequiresDirectory = errors.New("dataset generation load requires a directory") + // ErrGenerationLoadRequiresFiles prevents an empty staged directory from + // becoming an active but meaningless generation. + ErrGenerationLoadRequiresFiles = errors.New("dataset generation load requires at least one NDJSON file") + // ErrGenerationLoadTruncateForbidden prevents a new generation from + // deleting active or historical graph data. + ErrGenerationLoadTruncateForbidden = errors.New("dataset generation load cannot truncate collections") + // ErrGenerationDatasetProjectMismatch prevents graph documents and their + // lifecycle manifest from being scoped to different projects. + ErrGenerationDatasetProjectMismatch = errors.New("dataset generation project does not match load project") + // ErrGenerationSingleResourceUnsupported makes the legacy HTTP one-file + // path explicitly unavailable in immutable snapshot mode. + ErrGenerationSingleResourceUnsupported = errors.New("single-resource imports cannot create a dataset generation") +) + +// ActivationOutcomeError means the generation reached READY but Loom could +// not prove that the active-generation pointer was updated. READY is kept for +// an operator to reconcile; it must never be downgraded to FAILED because the +// activation request may have committed before its error reached the caller. +type ActivationOutcomeError struct { + Dataset dataset.DatasetRef + Err error +} + +func (e *ActivationOutcomeError) Error() string { + if e == nil { + return "dataset generation activation outcome is unknown" + } + return fmt.Sprintf("activate dataset generation %s/%s: %v", e.Dataset.Project, e.Dataset.Generation, e.Err) +} + +func (e *ActivationOutcomeError) Unwrap() error { + if e == nil { + return nil + } + return e.Err +} + +// GenerationLoadIncompleteError means row-level validation, generation, or +// edge construction failures were observed while FailFast was disabled. The +// graph may contain partial immutable documents, but the lifecycle manifest is +// deliberately left FAILED and cannot be selected for reads. +type GenerationLoadIncompleteError struct { + Dataset dataset.DatasetRef + ValidationErrors int + GenerationErrors int + EdgeErrors int +} + +func (e *GenerationLoadIncompleteError) Error() string { + if e == nil { + return "dataset generation load is incomplete" + } + return fmt.Sprintf( + "dataset generation %s/%s is incomplete: validation_errors=%d generation_errors=%d edge_errors=%d", + e.Dataset.Project, + e.Dataset.Generation, + e.ValidationErrors, + e.GenerationErrors, + e.EdgeErrors, + ) +} + +type generationLoadPlan struct { + Dataset dataset.DatasetRef + Manifest dataset.Manifest +} + +// newGenerationLoadPlan validates and snapshots all immutable information +// after input preflight and before a database connection is opened. Nil keeps +// the legacy loader path exactly unversioned. +func newGenerationLoadPlan(opts LoadOptions, files []string, identity graphschema.Identity) (*generationLoadPlan, error) { + if opts.Dataset == nil { + return nil, nil + } + + ref := *opts.Dataset + if err := ref.Validate(); err != nil { + return nil, err + } + if ref.Project != opts.Project { + return nil, fmt.Errorf("%w: dataset project %q, load project %q", ErrGenerationDatasetProjectMismatch, ref.Project, opts.Project) + } + if opts.Truncate { + return nil, ErrGenerationLoadTruncateForbidden + } + info, err := os.Stat(opts.MetaDir) + if err != nil { + return nil, fmt.Errorf("inspect dataset generation directory: %w", err) + } + if !info.IsDir() { + return nil, fmt.Errorf("%w: %q", ErrGenerationLoadRequiresDirectory, opts.MetaDir) + } + if len(files) == 0 { + return nil, fmt.Errorf("%w: %q", ErrGenerationLoadRequiresFiles, opts.MetaDir) + } + + schemaSnapshot, err := dataset.SnapshotSchemaIdentity(identity) + if err != nil { + return nil, fmt.Errorf("snapshot dataset generation schema identity: %w", err) + } + manifest, err := dataset.NewManifest(ref, schemaSnapshot) + if err != nil { + return nil, fmt.Errorf("create dataset generation manifest: %w", err) + } + return &generationLoadPlan{Dataset: ref, Manifest: manifest}, nil +} + +// loadLegacy is the original unversioned loader. Load dispatches to it only +// when Dataset is nil so existing import/API behavior and physical identities +// remain unchanged while generation mode evolves independently. diff --git a/internal/ingest/single_resource_load.go b/internal/ingest/single_resource_load.go new file mode 100644 index 0000000..489f2a0 --- /dev/null +++ b/internal/ingest/single_resource_load.go @@ -0,0 +1,69 @@ +package ingest + +import ( + "context" + "io" + "os" + "path/filepath" + "strings" + + "github.com/bmeg/jsonschema/v6" + "github.com/bmeg/jsonschemagraph/util" +) + +func graphObjectID(payload map[string]any, class *jsonschema.Schema) (string, error) { + return util.GetObjectID(payload, class) +} + +func graphExtraArgs(authResourcePath string) map[string]any { + if authResourcePath == "" { + return nil + } + return map[string]any{ + "auth_resource_path": authResourcePath, + } +} + +func LoadSingleResourceReader(ctx context.Context, opts LoadOptions, resourceType string, reader io.Reader, compressed bool) (LoadSummary, error) { + if opts.Dataset != nil { + return LoadSummary{}, ErrGenerationSingleResourceUnsupported + } + dir, err := os.MkdirTemp("", "arango-fhir-single-resource-*") + if err != nil { + return LoadSummary{}, err + } + defer os.RemoveAll(dir) + + name := resourceType + ".ndjson" + if compressed { + name += ".gz" + } + target := filepath.Join(dir, name) + f, err := os.Create(target) + if err != nil { + return LoadSummary{}, err + } + if _, err := io.Copy(f, reader); err != nil { + f.Close() + return LoadSummary{}, err + } + if err := f.Close(); err != nil { + return LoadSummary{}, err + } + + singleOpts := opts + singleOpts.MetaDir = dir + return Load(ctx, singleOpts) +} + +func LoadSingleResourceFile(ctx context.Context, opts LoadOptions, resourceType, path string) (LoadSummary, error) { + if opts.Dataset != nil { + return LoadSummary{}, ErrGenerationSingleResourceUnsupported + } + file, err := os.Open(path) + if err != nil { + return LoadSummary{}, err + } + defer file.Close() + return LoadSingleResourceReader(ctx, opts, resourceType, file, strings.HasSuffix(path, ".gz")) +} diff --git a/internal/server/config.go b/internal/server/config.go new file mode 100644 index 0000000..9868ca8 --- /dev/null +++ b/internal/server/config.go @@ -0,0 +1,128 @@ +package server + +import ( + "fmt" + "os" + "strings" + "time" + + "go.yaml.in/yaml/v3" +) + +type Config struct { + Server ServerConfig `yaml:"server"` + Auth AuthConfig `yaml:"auth"` +} + +type ServerConfig struct { + Listen string `yaml:"listen"` + Backend string `yaml:"backend"` + URL string `yaml:"url"` + Database string `yaml:"database"` + Schema string `yaml:"schema"` + Arango ArangoConfig `yaml:"arango"` + DatasetGenerations bool `yaml:"dataset_generations"` + ClickHouse ClickHouseConfig `yaml:"clickhouse"` + RecipeBatchRows int `yaml:"recipe_batch_rows"` + RecipeBatchBytes int `yaml:"recipe_batch_bytes"` + AllowUnauthenticated bool `yaml:"allow_unauthenticated"` +} + +type ArangoConfig struct { + URL string `yaml:"url"` + Database string `yaml:"database"` +} + +type ClickHouseConfig struct { + URL string `yaml:"url"` + Database string `yaml:"database"` +} + +type AuthConfig struct { + Mode string `yaml:"mode"` + Basic BasicAuthConfig `yaml:"basic"` + Calypr CalyprAuthConfig `yaml:"calypr"` + AllowUnauthenticated bool `yaml:"allow_unauthenticated"` +} + +type BasicAuthConfig struct { + Username string `yaml:"username"` + Password string `yaml:"password"` +} + +type CalyprAuthConfig struct { + RequestTimeout time.Duration `yaml:"request_timeout"` + CacheTTL time.Duration `yaml:"cache_ttl"` +} + +func DefaultConfig() Config { + return Config{ + Server: ServerConfig{ + Listen: ":8080", Backend: "arango", URL: "http://127.0.0.1:8529", Database: "fhir_proto", + Schema: "schemas/graph-fhir.json", ClickHouse: ClickHouseConfig{URL: "clickhouse://127.0.0.1:9000", Database: "loom"}, + RecipeBatchRows: 1000, RecipeBatchBytes: 4 << 20, + }, + Auth: AuthConfig{Mode: "basic", Calypr: CalyprAuthConfig{RequestTimeout: 5 * time.Second, CacheTTL: 30 * time.Second}}, + } +} + +func LoadConfig(path string) (Config, error) { + cfg := DefaultConfig() + if strings.TrimSpace(path) == "" { + return applyEnvironment(cfg), nil + } + data, err := os.ReadFile(path) + if err != nil { + return Config{}, fmt.Errorf("read server config %q: %w", path, err) + } + decoder := yaml.NewDecoder(strings.NewReader(string(data))) + decoder.KnownFields(true) + if err := decoder.Decode(&cfg); err != nil { + return Config{}, fmt.Errorf("decode server config %q: %w", path, err) + } + if cfg.Server.Arango.URL != "" { + cfg.Server.URL = cfg.Server.Arango.URL + } + if cfg.Server.Arango.Database != "" { + cfg.Server.Database = cfg.Server.Arango.Database + } + return applyEnvironment(cfg), nil +} + +func applyEnvironment(cfg Config) Config { + if cfg.Auth.Basic.Username == "" { + cfg.Auth.Basic.Username = os.Getenv("LOOM_AUTH_BASIC_USERNAME") + } + if cfg.Auth.Basic.Password == "" { + cfg.Auth.Basic.Password = os.Getenv("LOOM_AUTH_BASIC_PASSWORD") + } + return cfg +} + +func (c Config) Validate() error { + cfg := c + cfg.Auth.Mode = strings.ToLower(strings.TrimSpace(cfg.Auth.Mode)) + if cfg.Auth.Mode == "" { + cfg.Auth.Mode = "basic" + } + if cfg.Auth.Mode != "basic" && cfg.Auth.Mode != "calypr" { + return fmt.Errorf("auth.mode must be basic or calypr, got %q", cfg.Auth.Mode) + } + if cfg.Server.AllowUnauthenticated || cfg.Auth.AllowUnauthenticated { + return nil + } + if cfg.Auth.Mode == "basic" { + if cfg.Auth.Basic.Username == "" || cfg.Auth.Basic.Password == "" { + return fmt.Errorf("auth.basic.username and auth.basic.password are required in basic mode") + } + } + if cfg.Auth.Mode == "calypr" { + if cfg.Auth.Calypr.RequestTimeout <= 0 { + return fmt.Errorf("auth.calypr.request_timeout must be positive") + } + if cfg.Auth.Calypr.CacheTTL <= 0 { + return fmt.Errorf("auth.calypr.cache_ttl must be positive") + } + } + return nil +} diff --git a/internal/server/config_test.go b/internal/server/config_test.go new file mode 100644 index 0000000..3d1ca58 --- /dev/null +++ b/internal/server/config_test.go @@ -0,0 +1,62 @@ +package server + +import ( + "os" + "path/filepath" + "testing" + "time" +) + +func TestConfigDefaultsToBasicAndRequiresCredentials(t *testing.T) { + cfg := DefaultConfig() + if cfg.Auth.Mode != "basic" { + t.Fatalf("default auth mode = %q", cfg.Auth.Mode) + } + if err := cfg.Validate(); err == nil { + t.Fatal("default config without credentials unexpectedly validated") + } +} + +func TestLoadConfigStrictlyDecodesAndAppliesAuthSettings(t *testing.T) { + path := filepath.Join(t.TempDir(), "config.yaml") + if err := os.WriteFile(path, []byte("server:\n listen: :18080\nauth:\n mode: calypr\n calypr:\n request_timeout: 7s\n cache_ttl: 45s\n"), 0o600); err != nil { + t.Fatal(err) + } + cfg, err := LoadConfig(path) + if err != nil { + t.Fatalf("LoadConfig() error = %v", err) + } + if err := cfg.Validate(); err != nil { + t.Fatalf("Validate() error = %v", err) + } + if cfg.Server.Listen != ":18080" || cfg.Auth.Calypr.RequestTimeout != 7*time.Second || cfg.Auth.Calypr.CacheTTL != 45*time.Second { + t.Fatalf("config = %#v", cfg) + } +} + +func TestLoadConfigRejectsUnknownFields(t *testing.T) { + path := filepath.Join(t.TempDir(), "config.yaml") + if err := os.WriteFile(path, []byte("auth:\n mode: basic\n typo: true\n"), 0o600); err != nil { + t.Fatal(err) + } + if _, err := LoadConfig(path); err == nil { + t.Fatal("unknown config field unexpectedly accepted") + } +} + +func TestLoadConfigRejectsRemovedPublicationBackends(t *testing.T) { + for name, contents := range map[string]string{ + "publication target": "server:\n publication_target: elasticsearch\n", + "elasticsearch": "server:\n elasticsearch:\n url: http://elasticsearch:9200\n", + } { + t.Run(name, func(t *testing.T) { + path := filepath.Join(t.TempDir(), "config.yaml") + if err := os.WriteFile(path, []byte(contents), 0o600); err != nil { + t.Fatal(err) + } + if _, err := LoadConfig(path); err == nil { + t.Fatal("removed publication backend configuration unexpectedly accepted") + } + }) + } +} diff --git a/internal/server/recipe_authorization.go b/internal/server/recipe_authorization.go new file mode 100644 index 0000000..a014338 --- /dev/null +++ b/internal/server/recipe_authorization.go @@ -0,0 +1,55 @@ +package server + +import ( + "context" + "fmt" + + "github.com/calypr/loom/graphqlapi" + "github.com/calypr/loom/internal/authscope" + "github.com/calypr/loom/internal/dataframe/recipe" +) + +type recipeAuthorization struct { + resolver *authscope.ScopeResolver +} + +var _ graphqlapi.RecipeAuthorizer = recipeAuthorization{} + +func (a recipeAuthorization) AuthorizeRead(ctx context.Context, bindings recipe.RuntimeBindings) (recipe.RuntimeBindings, error) { + if bindings.Project == "" { + return recipe.RuntimeBindings{}, fmt.Errorf("recipe project is required") + } + if a.resolver == nil { + bindings.AuthScopeMode = authscope.ReadScopeUnrestricted + return bindings, nil + } + principal, _ := authscope.PrincipalFromContext(ctx) + scope, err := a.resolver.ResolveReadScopeForGeneration(ctx, principal, bindings.Project, bindings.DatasetGeneration, bindings.AuthResourcePaths) + if err != nil { + return recipe.RuntimeBindings{}, err + } + if scope.Mode == authscope.ReadScopeRestricted && len(scope.AuthResourcePaths) == 0 { + return recipe.RuntimeBindings{}, fmt.Errorf("caller has no read access to project %q", bindings.Project) + } + bindings.AuthResourcePaths = append([]string(nil), scope.AuthResourcePaths...) + bindings.AuthScopeMode = scope.Mode + return bindings, nil +} + +func (a recipeAuthorization) AuthorizeWrite(ctx context.Context, bindings recipe.RuntimeBindings) (recipe.RuntimeBindings, error) { + if bindings.Project == "" { + return recipe.RuntimeBindings{}, fmt.Errorf("recipe project is required") + } + if a.resolver == nil { + bindings.AuthScopeMode = authscope.ReadScopeUnrestricted + return bindings, nil + } + principal, _ := authscope.PrincipalFromContext(ctx) + scope, err := a.resolver.ResolveWriteScopeForGeneration(ctx, principal, bindings.Project, bindings.DatasetGeneration, bindings.AuthResourcePaths) + if err != nil { + return recipe.RuntimeBindings{}, err + } + bindings.AuthResourcePaths = append([]string(nil), scope.AuthResourcePaths...) + bindings.AuthScopeMode = scope.Mode + return bindings, nil +} diff --git a/internal/server/recipe_discovery.go b/internal/server/recipe_discovery.go new file mode 100644 index 0000000..717af27 --- /dev/null +++ b/internal/server/recipe_discovery.go @@ -0,0 +1,75 @@ +package server + +import ( + "context" + + "github.com/calypr/loom/internal/authscope" + "github.com/calypr/loom/internal/catalog" + "github.com/calypr/loom/internal/dataframe/recipe" + "github.com/calypr/loom/internal/dataframe/recipe/schema" + arangostore "github.com/calypr/loom/internal/store/arango" +) + +type recipeCatalogDiscovery struct { + connectionOptions arangostore.ConnectionOptions + read func(context.Context, catalog.PopulatedFieldOptions) ([]catalog.PopulatedField, error) +} + +func (d recipeCatalogDiscovery) Fields(ctx context.Context, scope schema.Scope, resourceType string) ([]schema.FieldCandidate, error) { + var unrestricted *bool + switch authscope.ReadScopeMode(scope.AuthScopeMode) { + case authscope.ReadScopeUnrestricted: + value := true + unrestricted = &value + case authscope.ReadScopeRestricted: + value := false + unrestricted = &value + } + reader := d.read + if reader == nil { + reader = catalog.DiscoverPopulatedFields + } + fields, err := reader(ctx, catalog.PopulatedFieldOptions{ + ConnectionOptions: d.connectionOptions, + Project: scope.Project, + DatasetGeneration: scope.DatasetGeneration, + AuthResourcePaths: append([]string(nil), scope.AuthResourcePaths...), + AuthResourcePathsUnrestricted: unrestricted, + ResourceType: resourceType, + }) + if err != nil { + return nil, err + } + result := make([]schema.FieldCandidate, 0, len(fields)) + for _, field := range fields { + result = append(result, schema.FieldCandidate{ + ResourceType: field.ResourceType, Path: field.Path, Kind: field.Kind, + DistinctValues: append([]string(nil), field.DistinctValues...), DistinctTruncated: field.DistinctTruncated, + PivotCandidate: field.PivotCandidate, PivotFamily: field.PivotFamily, + PivotColumns: append([]string(nil), field.PivotColumns...), + PivotColumnSelect: field.PivotColumnSelect, PivotValueSelect: field.PivotValueSelect, + PivotItemSource: field.PivotItemSource, PivotItemResourceType: field.PivotItemResourceType, + PivotValueSelectors: append([]string(nil), field.PivotValueSelectors...), + }) + } + return result, nil +} + +func recipeSchemaResolver(connectionOptions arangostore.ConnectionOptions, cache *catalog.Cache) func(context.Context, recipe.Bundle, recipe.RuntimeBindings) (recipe.Bundle, error) { + read := catalog.DiscoverPopulatedFields + if cache != nil { + read = cache.DiscoverFields(read) + } + discovery := recipeCatalogDiscovery{connectionOptions: connectionOptions, read: read} + return func(ctx context.Context, bundle recipe.Bundle, bindings recipe.RuntimeBindings) (recipe.Bundle, error) { + resolved, err := schema.Resolve(ctx, bundle, schema.Scope{ + Project: bindings.Project, DatasetGeneration: bindings.DatasetGeneration, + AuthResourcePaths: append([]string(nil), bindings.AuthResourcePaths...), + AuthScopeMode: string(bindings.AuthScopeMode), + }, discovery) + if err != nil { + return recipe.Bundle{}, err + } + return resolved.Bundle, nil + } +} diff --git a/internal/server/server.go b/internal/server/server.go index 55ba75e..9075484 100644 --- a/internal/server/server.go +++ b/internal/server/server.go @@ -2,20 +2,34 @@ package server import ( "context" + "crypto/sha256" + "encoding/hex" "flag" "fmt" "log/slog" + "net/http" "os" "os/signal" + "sort" + "strings" "syscall" + "time" "github.com/calypr/loom/graphqlapi" + clickhousegraphql "github.com/calypr/loom/graphqlapi/clickhouse" + materializationapi "github.com/calypr/loom/graphqlapi/materialization" queryapi "github.com/calypr/loom/graphqlapi/query" "github.com/calypr/loom/internal/authscope" "github.com/calypr/loom/internal/catalog" "github.com/calypr/loom/internal/dataframe" "github.com/calypr/loom/internal/dataframe/materialization" materializationarango "github.com/calypr/loom/internal/dataframe/materialization/arango" + publication "github.com/calypr/loom/internal/dataframe/publication" + publicationclickhouse "github.com/calypr/loom/internal/dataframe/publication/clickhouse" + "github.com/calypr/loom/internal/dataframe/recipe" + "github.com/calypr/loom/internal/dataframe/recipe/engine" + "github.com/calypr/loom/internal/dataframe/recipe/exec" + recipearango "github.com/calypr/loom/internal/dataframe/recipe/exec/arango" "github.com/calypr/loom/internal/dataset" datasetarango "github.com/calypr/loom/internal/dataset/arango" api "github.com/calypr/loom/internal/httpapi" @@ -27,12 +41,13 @@ import ( // Run starts the Loom HTTP server using the process command-line flags. func Run() { var ( - listen = flag.String("listen", ":8080", "HTTP listen address") - noAuth = flag.Bool("no-auth", false, "disable scoped authorization for local development") - backend = flag.String("backend", "arango", "storage backend") - url = flag.String("url", "http://127.0.0.1:8529", "ArangoDB URL") - database = flag.String("database", "fhir_proto", "ArangoDB database") - schema = flag.String("schema", "schemas/graph-fhir.json", "FHIR graph schema path for imports") + configPath = flag.String("config", "", "YAML server configuration file") + listen = flag.String("listen", ":8080", "HTTP listen address") + noAuth = flag.Bool("no-auth", false, "disable scoped authorization for local development") + backend = flag.String("backend", "arango", "storage backend") + url = flag.String("url", "http://127.0.0.1:8529", "ArangoDB URL") + database = flag.String("database", "fhir_proto", "ArangoDB database") + schema = flag.String("schema", "schemas/graph-fhir.json", "FHIR graph schema path for imports") // Dataset generations opt the server into resolving a project's READY // active manifest before dataframe discovery or execution. This mode // disables the legacy one-file import route because that route cannot @@ -40,9 +55,36 @@ func Run() { datasetGenerations = flag.Bool("dataset-generations", false, "resolve active immutable dataset generations for dataframe reads and disable legacy single-resource imports") clickhouseURL = flag.String("clickhouse-url", "clickhouse://127.0.0.1:9000", "ClickHouse native URL for published dataframe reads") clickhouseDatabase = flag.String("clickhouse-database", "loom", "ClickHouse database for published dataframe reads") + recipeBatchRows = flag.Int("recipe-batch-rows", 1000, "maximum recipe materialization rows per ClickHouse batch") + recipeBatchBytes = flag.Int("recipe-batch-bytes", 4<<20, "maximum recipe materialization bytes per ClickHouse batch") ) flag.Parse() + serverConfig, err := LoadConfig(*configPath) + if err != nil { + exitf("load server config: %v", err) + } + if *configPath != "" { + *listen = serverConfig.Server.Listen + *backend = serverConfig.Server.Backend + *url = serverConfig.Server.URL + *database = serverConfig.Server.Database + *schema = serverConfig.Server.Schema + *datasetGenerations = serverConfig.Server.DatasetGenerations + *clickhouseURL = serverConfig.Server.ClickHouse.URL + *clickhouseDatabase = serverConfig.Server.ClickHouse.Database + *recipeBatchRows = serverConfig.Server.RecipeBatchRows + *recipeBatchBytes = serverConfig.Server.RecipeBatchBytes + *noAuth = serverConfig.Server.AllowUnauthenticated || serverConfig.Auth.AllowUnauthenticated + } + if *noAuth { + serverConfig.Server.AllowUnauthenticated = true + serverConfig.Auth.AllowUnauthenticated = true + } + if err := serverConfig.Validate(); err != nil { + exitf("invalid server config: %v", err) + } + if *backend != "arango" { exitf("unsupported backend %q: only arango is wired in this server", *backend) } @@ -53,23 +95,38 @@ func Run() { Database: *database, } + lifecycleClient, err := arangostore.Open(context.Background(), connOpts.URL, connOpts.Database) + if err != nil { + exitf("open dataset lifecycle store: %v", err) + } + defer lifecycleClient.Close(context.Background()) + if err := lifecycleClient.Bootstrap(context.Background(), datasetarango.BootstrapSpec()); err != nil { + exitf("bootstrap dataset lifecycle store: %v", err) + } + if err := lifecycleClient.Bootstrap(context.Background(), recipearango.BootstrapSpec()); err != nil { + exitf("bootstrap recipe registry: %v", err) + } + recipeRegistry, err := recipearango.New(lifecycleClient) + if err != nil { + exitf("create recipe registry: %v", err) + } + defaultBundle, err := recipe.DefaultACEDBundle() + if err != nil { + exitf("load default dataframe recipe: %v", err) + } + if _, err := (exec.PersistentRegistry{Store: recipeRegistry}).Register(context.Background(), defaultBundle); err != nil { + exitf("register default dataframe recipe: %v", err) + } + lifecycleStore, err := datasetarango.New(lifecycleClient) + if err != nil { + exitf("create dataset lifecycle store: %v", err) + } // Keep this as an interface, not a typed *datasetarango.Store nil. Passing a // typed nil into queryapi.Config makes the interface non-nil and // incorrectly activates immutable-generation lookup for legacy META loads. var activeManifestResolver dataset.ActiveManifestResolver if *datasetGenerations { - lifecycleClient, err := arangostore.Open(context.Background(), connOpts.URL, connOpts.Database) - if err != nil { - exitf("open dataset lifecycle store: %v", err) - } - defer lifecycleClient.Close(context.Background()) - if err := lifecycleClient.Bootstrap(context.Background(), datasetarango.BootstrapSpec()); err != nil { - exitf("bootstrap dataset lifecycle store: %v", err) - } - activeManifestResolver, err = datasetarango.New(lifecycleClient) - if err != nil { - exitf("create dataset lifecycle store: %v", err) - } + activeManifestResolver = lifecycleStore } discoveryCache := catalog.NewCache() @@ -78,19 +135,29 @@ func Run() { var scopeResolver *authscope.ScopeResolver var authorizer authscope.Authorizer - if *noAuth { + var authenticator authscope.Authenticator + switch { + case *noAuth: + authenticator = authscope.StaticAuthenticator{Principal: authscope.Principal{Subject: "anonymous"}} authorizer = authscope.AllowAllAuthorizer{} - } else { + case strings.EqualFold(serverConfig.Auth.Mode, "basic"): + authenticator = authscope.BasicAuthenticator{Username: serverConfig.Auth.Basic.Username, Password: serverConfig.Auth.Basic.Password} + authorizer = authscope.AllowAllAuthorizer{} + case strings.EqualFold(serverConfig.Auth.Mode, "calypr"): + authenticator = authscope.CalyprAuthenticator{} + client := &http.Client{Timeout: serverConfig.Auth.Calypr.RequestTimeout} scopeResolver = authscope.NewScopeResolver(authscope.ScopeResolverConfig{ ConnectionOptions: connOpts, + ResourceAccess: authscope.NewFenceUserAccessClientWithTTL(client, serverConfig.Auth.Calypr.CacheTTL), + CacheTTL: serverConfig.Auth.Calypr.CacheTTL, }) authorizer = authscope.ScopeAuthorizer{Resolver: scopeResolver} + default: + exitf("unsupported auth mode %q", serverConfig.Auth.Mode) } dataframes := dataframe.NewService(dataframe.ServiceConfig{ ConnectionOptions: connOpts, - DiscoverReferences: discoverReferences, - DiscoverFields: discoverFields, ScopeResolver: scopeResolver, ActiveManifestResolver: activeManifestResolver, }) @@ -111,7 +178,43 @@ func Run() { exitf("create ClickHouse client: %v", err) } defer clickhouse.Close() - materializationReader := &materialization.Reader{ClickHouse: clickhouse, Registry: registry, MaxPage: 1000} + // The Arango-backed dataframe loader publishes into this database. Create + // it during server startup so a fresh ClickHouse instance does not require + // an operator to run a separate DDL/API step before materialization. + if err := clickhouse.EnsureDatabase(context.Background()); err != nil { + exitf("ensure ClickHouse database %q: %v", *clickhouseDatabase, err) + } + materializationReader := &materialization.Reader{ClickHouse: clickhouse, Catalog: registry, MaxPage: 1000} + recipeEngine, err := engine.New(engine.Config{ + Registry: recipeRegistry, + ResolveBundle: recipeSchemaResolver(connOpts, discoveryCache), + QueryRows: func(ctx context.Context, query string, batchSize int, bindVars map[string]any, visit func(map[string]any) error) error { + started := time.Now() + digest := sha256.Sum256([]byte(query)) + queryID := hex.EncodeToString(digest[:8]) + logger.Info("dataframe AQL start", "query_id", queryID, "query_bytes", len(query), "bind_vars", len(bindVars), "cursor_batch_size", batchSize) + err := lifecycleClient.QueryRows(ctx, query, batchSize, bindVars, visit) + fields := []any{"query_id", queryID, "query_bytes", len(query), "bind_vars", len(bindVars), "seconds", time.Since(started).Seconds()} + if err != nil { + logger.Error("dataframe AQL failed", append(fields, "error", err.Error())...) + return err + } + logger.Info("dataframe AQL complete", fields...) + return nil + }, + ScopeDigest: recipeScopeDigest, + }) + if err != nil { + exitf("create dataframe recipe engine: %v", err) + } + bundleStore, err := materialization.NewClickHouseBundleStore(clickhouse, registry) + if err != nil { + exitf("create dataframe bundle store: %v", err) + } + bundleTarget, err := publicationclickhouse.New(bundleStore) + if err != nil { + exitf("create dataframe publication target: %v", err) + } resolver := graphqlapi.NewResolver(graphqlapi.ResolverConfig{ DataframeQuery: queryapi.Config{ ConnectionOptions: connOpts, @@ -122,6 +225,50 @@ func Run() { ActiveManifestResolver: activeManifestResolver, }, MaterializationReader: materializationReader, + RecipeControl: engine.Control{Engine: recipeEngine, ExplainConnection: &connOpts}, + RecipeAuthorizer: recipeAuthorization{resolver: scopeResolver}, + RecipeExecutions: graphqlapi.NewAuthorizedRecipeExecutionReader(registry, scopeResolver), + RecipeMaterialize: func(ctx context.Context, name string, bindings recipe.RuntimeBindings) (graphqlapi.RecipeExecution, error) { + bindings.IncludeAuthResourcePath = true + var identity materialization.BundleIdentity + _, err := recipeEngine.Materialize(ctx, name, bindings, func(ctx context.Context, full engine.Resolved) error { + streams, err := recipeEngine.Streams(ctx, full) + if err != nil { + return err + } + identity = materialization.BundleIdentity{Name: name, Project: bindings.Project, DatasetGeneration: bindings.DatasetGeneration, RecipeDigest: full.StoredRecipeDigest, SchemaDigest: full.ResolvedSchemaDigest, ScopeDigest: full.Semantic.ScopeDigest, EngineVersion: "loom-recipe-v1", AuthResourcePaths: append([]string(nil), bindings.AuthResourcePaths...)} + streamInputs := make([]publication.OutputStream, 0, len(streams)) + for _, stream := range streams { + stream := stream + columns := recipeOutputLogicalColumns(full, stream.Name) + streamInputs = append(streamInputs, publication.OutputStream{ + Name: stream.Name, Columns: columns, + Stream: func(streamCtx context.Context, visit func(map[string]any) error) error { + _, err := stream.Stream(streamCtx, func(row map[string]any) error { + return visit(row) + }) + return err + }, + }) + } + publicationIdentity := publication.PublicationIdentity{Name: identity.Name, Project: identity.Project, DatasetGeneration: identity.DatasetGeneration, RecipeDigest: identity.RecipeDigest, SchemaDigest: identity.SchemaDigest, ScopeDigest: identity.ScopeDigest, EngineVersion: identity.EngineVersion, AuthResourcePaths: append([]string(nil), bindings.AuthResourcePaths...)} + _, err = publication.Publish(ctx, bundleTarget, publicationIdentity, streamInputs, publication.Limits{BatchRows: *recipeBatchRows, BatchBytes: *recipeBatchBytes}) + return err + }) + if err != nil { + return graphqlapi.RecipeExecution{}, err + } + published, err := registry.FindExecutionByKey(ctx, identity.Key()) + if err != nil { + return graphqlapi.RecipeExecution{}, fmt.Errorf("load published recipe execution: %w", err) + } + return graphqlapi.RecipeExecution{ID: published.ID, Name: name, RecipeDigest: identity.RecipeDigest, ResolvedSchemaDigest: identity.SchemaDigest, SourceGeneration: identity.DatasetGeneration, State: string(materialization.BundleReady)}, nil + }, + }) + clickhouseService := materializationapi.NewService(materializationapi.Config{ + Reader: materializationReader, + ScopeResolver: scopeResolver, + ActiveManifestResolver: activeManifestResolver, }) importService, err := api.NewService(api.ServiceConfig{ @@ -129,6 +276,10 @@ func Run() { ConnectionOptions: connOpts, Schema: *schema, }}, + GenerationRunner: api.IngestRunner{BaseOptions: ingest.LoadOptions{ + ConnectionOptions: connOpts, + Schema: *schema, + }}, Logger: logger, OnSuccess: func(project string) { discoveryCache.InvalidateProject(project) @@ -143,11 +294,15 @@ func Run() { server, err := api.NewHTTPServer(api.HTTPConfig{ Service: importService, + Authenticator: authenticator, Authorizer: authorizer, + ScopeResolver: scopeResolver, GraphQLHandler: graphqlapi.NewHandler(resolver), - GraphQLPlaygroundHandler: graphqlapi.NewPlaygroundHandler("/graphql"), - ApolloSandboxHandler: graphqlapi.NewApolloSandboxHandler("/graphql"), + ClickHouseGraphQLHandler: clickhousegraphql.NewHandler(clickhouseService), + GraphQLPlaygroundHandler: graphqlapi.NewPlaygroundHandler("/graphql/graph"), + ApolloSandboxHandler: graphqlapi.NewApolloSandboxHandler("/graphql/graph"), DisableSingleResourceImports: *datasetGenerations, + RawExporter: api.ArangoRawExporter{Query: lifecycleClient, Manifests: lifecycleStore}, Logger: logger, }) if err != nil { @@ -176,6 +331,52 @@ func Run() { } } +func recipeScopeDigest(bindings recipe.RuntimeBindings) string { + paths := append([]string(nil), bindings.AuthResourcePaths...) + sort.Strings(paths) + hash := sha256.Sum256([]byte(bindings.Project + "\x00" + bindings.DatasetGeneration + "\x00" + strings.Join(paths, "\x00"))) + return hex.EncodeToString(hash[:]) +} + +// recipeOutputLogicalColumns is the one conversion point from the finalized +// compiler schema to the backend-neutral publication schema. Publication must +// not reconstruct nested names from semantic recipe nodes because those names +// are finalized by physical lowering. +func recipeOutputLogicalColumns(plan engine.Resolved, outputName string) []publication.LogicalColumn { + for _, output := range plan.Compiled.Outputs { + if output.Name != outputName { + continue + } + columns := make([]publication.LogicalColumn, 0, len(output.OutputSchema)+1) + identityAdded := false + for _, column := range output.OutputSchema { + if column.Identity && column.Name == "__loom_row_id" { + columns = append(columns, publication.LogicalColumn{Name: column.Name, Kind: "string", IsIdentity: true}) + identityAdded = true + break + } + } + if !identityAdded { + columns = append(columns, publication.LogicalColumn{Name: "__loom_row_id", Kind: "string", IsIdentity: true}) + } + for _, column := range output.OutputSchema { + if column.Internal { + continue + } + kind := column.Kind + if kind == "date_time" { + kind = "date-time" + } + if kind == "" { + kind = "string" + } + columns = append(columns, publication.LogicalColumn{Name: column.Name, Kind: kind, Repeated: column.Cardinality == "many", Nullable: column.Nullable}) + } + return columns + } + return []publication.LogicalColumn{{Name: "__loom_row_id", Kind: "string", IsIdentity: true}} +} + func exitf(format string, args ...any) { _, _ = fmt.Fprintf(os.Stderr, format+"\n", args...) os.Exit(1) diff --git a/internal/store/arango/client.go b/internal/store/arango/client.go index 43dfbae..321f4d8 100644 --- a/internal/store/arango/client.go +++ b/internal/store/arango/client.go @@ -167,6 +167,27 @@ func (c *Client) QueryRows(ctx context.Context, query string, batchSize int, bin return nil } +// ExecuteAQL runs a write/query statement and drains its cursor. It is kept +// deliberately small so durable registries can perform compare-and-swap +// updates without exposing the Arango driver through their public APIs. +func (c *Client) ExecuteAQL(ctx context.Context, query string, bindVars map[string]interface{}) error { + cursor, err := c.db.Query(ctx, query, &driver.QueryOptions{BatchSize: 32, BindVars: bindVars}) + if err != nil { + return err + } + defer cursor.Close() + for cursor.HasMore() { + var discard any + if _, err := cursor.ReadDocument(ctx, &discard); err != nil { + if shared.IsNoMoreDocuments(err) { + return nil + } + return err + } + } + return nil +} + func (c *Client) Close(ctx context.Context) error { return nil } diff --git a/internal/store/arango/explain.go b/internal/store/arango/explain.go index 8b22b65..92026d5 100644 --- a/internal/store/arango/explain.go +++ b/internal/store/arango/explain.go @@ -204,42 +204,6 @@ func ParseExplainResult(data []byte) (ExplainResult, error) { // ExtractPlanIndexes returns deterministic, deduplicated index uses from the // single plan followed by any alternative plans. -func ExtractPlanIndexes(result ExplainResult) []ExplainIndexUse { - plans := make([]ExplainPlan, 0, 1+len(result.Plans)) - if result.Plan != nil { - plans = append(plans, *result.Plan) - } - plans = append(plans, result.Plans...) - - uses := make([]ExplainIndexUse, 0) - seen := map[string]bool{} - for planIndex, plan := range plans { - for _, node := range plan.Nodes { - for _, index := range node.Indexes { - collection := explainIndexCollection(node, index) - key := fmt.Sprintf("%d\x00%d\x00%s\x00%s\x00%s", planIndex, node.ID, collection, index.ID, index.Name) - if seen[key] { - continue - } - seen[key] = true - uses = append(uses, ExplainIndexUse{Plan: planIndex, NodeID: node.ID, NodeType: node.Type, Collection: collection, Index: index}) - } - } - } - sort.SliceStable(uses, func(i, j int) bool { - if uses[i].Plan != uses[j].Plan { - return uses[i].Plan < uses[j].Plan - } - if uses[i].NodeID != uses[j].NodeID { - return uses[i].NodeID < uses[j].NodeID - } - if uses[i].Collection != uses[j].Collection { - return uses[i].Collection < uses[j].Collection - } - return uses[i].Index.Name < uses[j].Index.Name - }) - return uses -} // explainIndexCollection resolves the collection that owns an index across // Arango's node shapes. Traversal nodes typically omit `collection` and put diff --git a/internal/store/arango/explain_test.go b/internal/store/arango/explain_test.go index a117fd9..453c78f 100644 --- a/internal/store/arango/explain_test.go +++ b/internal/store/arango/explain_test.go @@ -23,54 +23,6 @@ func TestExplainRequestJSON(t *testing.T) { } } -func TestParseExplainResultAndExtractIndexes(t *testing.T) { - payload := []byte(`{ - "plan": { - "nodes": [ - {"type":"IndexNode","id":7,"collection":"Patient","estimatedCost":2.5,"estimatedNrItems":1,"indexes":[ - {"id":"Patient/123","name":"idx_project","type":"persistent","fields":["project"],"unique":false,"sparse":false,"selectivityEstimate":0.2}, - {"id":"Patient/123","name":"idx_project","type":"persistent","fields":["project"]} - ]} - ], - "rules":["use-indexes"], - "collections":[{"name":"Patient","type":"read"}], - "estimatedCost":3.5, - "estimatedNrItems":1 - }, - "warnings":[{"code":1577,"message":"example"}], - "stats":{"plansCreated":1,"rulesExecuted":8,"rulesSkipped":2,"peakMemoryUsage":4096}, - "cacheable":true -}`) - result, err := ParseExplainResult(payload) - if err != nil { - t.Fatal(err) - } - if result.Plan == nil || result.Plan.EstimatedCost != 3.5 || len(result.Warnings) != 1 || result.Stats.PeakMemoryUsage != 4096 { - t.Fatalf("unexpected parsed explain result: %#v", result) - } - uses := ExtractPlanIndexes(result) - if len(uses) != 1 { - t.Fatalf("index uses = %#v", uses) - } - if uses[0].Plan != 0 || uses[0].NodeID != 7 || uses[0].Collection != "Patient" || uses[0].Index.Name != "idx_project" { - t.Fatalf("unexpected index use: %#v", uses[0]) - } - if uses[0].Index.SelectivityEstimate == nil || *uses[0].Index.SelectivityEstimate != 0.2 { - t.Fatalf("missing selectivity estimate: %#v", uses[0].Index) - } -} - -func TestExtractIndexesFromAlternativePlans(t *testing.T) { - result := ExplainResult{Plans: []ExplainPlan{ - {Nodes: []ExplainNode{{ID: 9, Type: "IndexNode", Collection: "Observation", Indexes: ExplainIndexes{{Name: "z"}}}}}, - {Nodes: []ExplainNode{{ID: 2, Type: "IndexNode", Indexes: ExplainIndexes{{Name: "a", Collection: "Specimen"}}}}}, - }} - uses := ExtractPlanIndexes(result) - if len(uses) != 2 || uses[0].Plan != 0 || uses[1].Plan != 1 || uses[1].Collection != "Specimen" { - t.Fatalf("unexpected alternative-plan indexes: %#v", uses) - } -} - func TestParseExplainResultRejectsErrorsAndInvalidBodies(t *testing.T) { tests := []struct { name string @@ -91,39 +43,3 @@ func TestParseExplainResultRejectsErrorsAndInvalidBodies(t *testing.T) { }) } } - -func TestParseExplainResultAcceptsObjectIndexes(t *testing.T) { - result, err := ParseExplainResult([]byte(`{ - "plan": {"nodes": [ - {"type":"TraversalNode","id":9,"collection":"fhir_edge","indexes":{"edge":{"id":"edge/1","name":"edge_index","type":"edge","fields":["_from"]}}} - ]} -}`)) - if err != nil { - t.Fatal(err) - } - uses := ExtractPlanIndexes(result) - if len(uses) != 1 || uses[0].Index.Name != "edge_index" { - t.Fatalf("unexpected object indexes: %#v", uses) - } -} - -func TestParseExplainResultAcceptsNestedTraversalIndexes(t *testing.T) { - result, err := ParseExplainResult([]byte(`{ - "plan": {"nodes": [ - {"type":"TraversalNode","id":9,"edgeCollections":["fhir_edge"],"indexes":{ - "base":[{"id":"fhir_edge/2","name":"edge","type":"edge","fields":["_to"]}], - "levels":{"1":[{"id":"fhir_edge/12","name":"project_label","type":"persistent","fields":["project","label"]}]} - }} - ]} -}`)) - if err != nil { - t.Fatal(err) - } - uses := ExtractPlanIndexes(result) - if len(uses) != 2 { - t.Fatalf("nested traversal index uses = %#v", uses) - } - if uses[0].Index.Name != "edge" || uses[1].Index.Name != "project_label" || uses[0].Collection != "fhir_edge" || uses[1].Collection != "fhir_edge" { - t.Fatalf("nested traversal indexes = %#v", uses) - } -} diff --git a/internal/store/clickhouse/client.go b/internal/store/clickhouse/client.go index 8e58713..3119db6 100644 --- a/internal/store/clickhouse/client.go +++ b/internal/store/clickhouse/client.go @@ -198,10 +198,10 @@ func (c *Client) InsertRows(ctx context.Context, table string, columns []Column, return nil } -// QueryRows executes a SELECT and decodes each row through ClickHouse's native -// driver. The tuple JSON projection preserves arrays and mixed scalar types -// while keeping the generic dataframe result shape map[string]any. -func (c *Client) QueryRows(ctx context.Context, query string, columns []string) ([]map[string]any, error) { +// QueryRowsArgs executes a SELECT with driver-bound positional arguments and +// decodes each row through ClickHouse's native driver. Query-controlled values +// must be supplied as args rather than interpolated into query text. +func (c *Client) QueryRowsArgs(ctx context.Context, query string, columns []string, args ...any) ([]map[string]any, error) { if len(columns) == 0 { return nil, fmt.Errorf("ClickHouse query columns are required") } @@ -214,7 +214,7 @@ func (c *Client) QueryRows(ctx context.Context, query string, columns []string) quoted[i] = fmt.Sprintf("`%s`", column) } wrapped := fmt.Sprintf("SELECT toJSONString(tuple(%s)) AS __loom_json FROM (%s) AS __loom_rows", strings.Join(quoted, ", "), base) - rows, err := c.conn.Query(ctx, wrapped) + rows, err := c.conn.Query(ctx, wrapped, args...) if err != nil { return nil, err } diff --git a/internal/store/clickhouse/integration_test.go b/internal/store/clickhouse/integration_test.go index d65397f..681d1de 100644 --- a/internal/store/clickhouse/integration_test.go +++ b/internal/store/clickhouse/integration_test.go @@ -48,7 +48,7 @@ func TestClickHouseNativeRoundTrip(t *testing.T) { }, []map[string]any{{"__loom_row_id": uint64(1), "name": "alice", "score": 2.5, "tags": []string{"a", "b"}}}); err != nil { t.Fatal(err) } - rows, err := client.QueryRows(ctx, "SELECT `name`, `score`, `tags` FROM `"+table+"`", []string{"name", "score", "tags"}) + rows, err := client.QueryRowsArgs(ctx, "SELECT `name`, `score`, `tags` FROM `"+table+"`", []string{"name", "score", "tags"}) if err != nil { t.Fatal(err) }