diff --git a/.github/workflows/verify.yml b/.github/workflows/verify.yml index d0af8bebd5..a6a078e3e1 100644 --- a/.github/workflows/verify.yml +++ b/.github/workflows/verify.yml @@ -16,6 +16,7 @@ jobs: name: Classify verification scope runs-on: ubuntu-24.04 outputs: + checks: ${{ steps.scope.outputs.checks }} duckdb: ${{ steps.scope.outputs.duckdb }} postgres: ${{ steps.scope.outputs.postgres }} performance: ${{ steps.scope.outputs.performance }} @@ -39,11 +40,12 @@ jobs: echo "performance=$3" echo "format_performance=$4" echo "rules_performance=$5" + echo "checks=$6" } >> "${GITHUB_OUTPUT}" } if [[ "${EVENT_NAME}" != "pull_request" ]]; then - write_scope true true true true true + write_scope true true true true true true exit 0 fi @@ -54,8 +56,15 @@ jobs: rules_performance=false broad=false changed=false + # The website builds and deploys in its own workflow, so a change that only touches it + # skips the Python checks and test suites. + website_only=true while IFS= read -r -d '' path; do changed=true + case "${path}" in + website/*|.github/workflows/website.yml) ;; + *) website_only=false ;; + esac case "${path}" in crates/*|Cargo.toml|Cargo.lock|pyproject.toml|uv.lock) broad=true @@ -94,7 +103,7 @@ jobs: format_performance=true rules_performance=true ;; - .github/*|docs/*|*.md|.gitignore|LICENSE) + .github/*|docs/*|website/*|*.md|.gitignore|LICENSE) ;; *) broad=true @@ -103,10 +112,15 @@ jobs: done < <(git diff --name-only -z "${BASE_SHA}" "${HEAD_SHA}") if [[ "${changed}" != "true" ]]; then - write_scope true true true true true + write_scope true true true true true true exit 0 fi + checks=true + if [[ "${website_only}" == "true" ]]; then + checks=false + fi + if [[ "${broad}" == "true" ]]; then duckdb=true postgres=true @@ -117,7 +131,8 @@ jobs: "${postgres}" \ "${performance}" \ "${format_performance}" \ - "${rules_performance}" + "${rules_performance}" \ + "${checks}" lockfile: name: Lockfile consistency @@ -137,7 +152,8 @@ jobs: checks: name: Static checks and unit + integration tests - needs: lockfile + needs: [changes, lockfile] + if: needs.changes.outputs.checks == 'true' runs-on: ubuntu-24.04 timeout-minutes: 30 steps: @@ -542,6 +558,7 @@ jobs: - name: Confirm all required verification jobs passed env: CHANGES_RESULT: ${{ needs.changes.result }} + CHECKS_SELECTED: ${{ needs.changes.outputs.checks }} LOCKFILE_RESULT: ${{ needs.lockfile.result }} CHECKS_RESULT: ${{ needs.checks.result }} E2E_SHARDS_RESULT: ${{ needs.e2e_shards.result }} @@ -553,10 +570,13 @@ jobs: RULES_PERFORMANCE_RESULT: ${{ needs.rules_performance.result }} POSTGRES_RESULT: ${{ needs.postgres.result }} run: | - if [[ "${CHANGES_RESULT}" != "success" || \ - "${LOCKFILE_RESULT}" != "success" || \ - "${CHECKS_RESULT}" != "success" ]]; then - echo "Required classification, lockfile, or static checks did not pass" >&2 + if [[ "${CHANGES_RESULT}" != "success" || "${LOCKFILE_RESULT}" != "success" ]]; then + echo "Required classification or lockfile checks did not pass" >&2 + exit 1 + fi + if [[ "${CHECKS_RESULT}" != "success" ]] && \ + ! [[ "${CHECKS_SELECTED}" == "false" && "${CHECKS_RESULT}" == "skipped" ]]; then + echo "Required static checks did not pass: ${CHECKS_RESULT}" >&2 exit 1 fi for result in \ diff --git a/.github/workflows/website.yml b/.github/workflows/website.yml new file mode 100644 index 0000000000..b10b2fb3fb --- /dev/null +++ b/.github/workflows/website.yml @@ -0,0 +1,67 @@ +name: Website + +on: + push: + branches: [main] + paths: + - "website/**" + - ".github/workflows/website.yml" + pull_request: + paths: + - "website/**" + - ".github/workflows/website.yml" + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: website-${{ github.ref }} + cancel-in-progress: true + +jobs: + build: + name: Build sqlbuild.com + runs-on: ubuntu-24.04 + defaults: + run: + working-directory: website + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-node@v4 + with: + node-version: 22 + cache: npm + cache-dependency-path: website/package-lock.json + + - name: Install dependencies + run: npm ci + + - name: Build + run: npm run build + + - name: Check links + run: python3 scripts/check_links.py dist + + - name: Upload site + if: github.event_name != 'pull_request' + uses: actions/upload-pages-artifact@v3 + with: + path: website/dist + + deploy: + name: Deploy to GitHub Pages + if: github.event_name != 'pull_request' && github.ref == 'refs/heads/main' + needs: build + runs-on: ubuntu-24.04 + permissions: + pages: write + id-token: write + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + steps: + - name: Deploy + id: deployment + uses: actions/deploy-pages@v4 diff --git a/CHANGELOG.md b/CHANGELOG.md index 6bd814ab16..26a9969463 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1730,11 +1730,11 @@ ### Features -* **cli:** adopt balanced color hierarchy for human output (CHI-45) ([eac4ddc](https://github.com/chio-labs/sqlbuild/commit/eac4ddc9842a670038a1f57c9821fb6ab92d8fe4)) +* **cli:** adopt balanced color hierarchy for human output ([eac4ddc](https://github.com/chio-labs/sqlbuild/commit/eac4ddc9842a670038a1f57c9821fb6ab92d8fe4)) * **cli:** clarify virtual state and expand previews ([3b94316](https://github.com/chio-labs/sqlbuild/commit/3b943167e06784f8cc174b92ef71ef489b2f67ee)) * **cli:** dim kind columns, accent change actions, fix spinner wrap and error-label padding ([b17b1ef](https://github.com/chio-labs/sqlbuild/commit/b17b1efc31f98fa77c0596707a282373dff95025)) * **cli:** refine hierarchy and add output previews ([7607c4f](https://github.com/chio-labs/sqlbuild/commit/7607c4f08636a00d525fdc406a7b65e4939d8804)) -* **cli:** structural output hierarchy across SQLBuild CLI (CHI-45) ([7c46d43](https://github.com/chio-labs/sqlbuild/commit/7c46d43174f1e61238e919a4201e37e699b63d80)) +* **cli:** structural output hierarchy across SQLBuild CLI ([7c46d43](https://github.com/chio-labs/sqlbuild/commit/7c46d43174f1e61238e919a4201e37e699b63d80)) * **cli:** structure source freshness output ([c0e42f4](https://github.com/chio-labs/sqlbuild/commit/c0e42f4ef110995d985b9b32e559ae154b657ae7)) * **cli:** structured build surface - phase lines, status cells, dim timings ([e321e11](https://github.com/chio-labs/sqlbuild/commit/e321e11232e414fc9d954c61c23cafd59da5a1d2)) * **cli:** structured clone completion and connection phase lines ([e5a154d](https://github.com/chio-labs/sqlbuild/commit/e5a154d711125854449c8e309eaee285bbb7e7a5)) diff --git a/README.md b/README.md index 42c8441733..17c0e9dd83 100644 --- a/README.md +++ b/README.md @@ -24,7 +24,7 @@ All state is persisted as append-only tables in the warehouse alongside your dat - **Cursor-based incremental processing.** Automatic gap detection and resume, with microbatch mode for large ranges. No external checkpoint to maintain. - **Ingestion and Python nodes.** Load external data with Python `@loader` functions, and run `@task`, `@asset`, and `@check` nodes as first-class members of the same DAG as your SQL models. -See the [documentation](https://docs.sqlbuild.com) for the full feature set, including providers, lifecycle hooks, Python macros, UDFs, custom materializations, data diffs, zero-copy cloning, and virtual environments. This repository also includes focused guides to [target execution limits](docs/execution-limits.md) and [model and raw-query diffs](docs/query-diffs.md). To coordinate dbt and SQLBuild projects, see the [dbt compatibility guide](https://docs.sqlbuild.com/concepts/dbt-compatibility/overview). +See the [documentation](https://docs.sqlbuild.com) for the full feature set, including providers, lifecycle hooks, Python macros, UDFs, custom materializations, data diffs, zero-copy cloning, and virtual environments. To coordinate dbt and SQLBuild projects, see the [dbt compatibility guide](https://docs.sqlbuild.com/concepts/dbt-compatibility/overview). ## Quick start @@ -198,7 +198,7 @@ Family selectors automatically include new built-in rules on upgrade. Exact-code their current membership and must be updated manually. In particular, selecting `SQBRSQL` enables `SQBRSQL040` (plain JOIN predicates) and `SQBRSQL041` (terminal CTE naming); existing projects may need to extract computed join keys into input CTEs and rename their last CTE to `final`. -See the [SQL rule conventions](docs/native-lint-rule-matrix.md#join-keys-and-final-cte-names) +See the [SQL rule conventions](https://sqlbuild.com/docs/concepts/rules/configuration-and-selection/#join-keys-and-final-cte-names) for their exact scope and relationship to `SQBRSQL035`. `sqb format` reports a file-specific `format-unsafe` fault whenever a SQL body cannot be safely @@ -308,12 +308,6 @@ digest, not SQL text. Executed SQL artifacts are stored separately under the sen Full documentation is available at [docs.sqlbuild.com](https://docs.sqlbuild.com). -Runtime operator and extension contracts: - -- [Execution observability and local troubleshooting](docs/execution-observability.md) -- [SQLite and PostgreSQL execution history](docs/execution-history.md) -- [Typed lifecycle and command-output sinks](docs/sinks.md) - ## Contributing We welcome contributions. Please see [CONTRIBUTING.md](CONTRIBUTING.md) for guidelines. diff --git a/docs/compiler-cache.md b/docs/compiler-cache.md deleted file mode 100644 index 7fc2ba4f2b..0000000000 --- a/docs/compiler-cache.md +++ /dev/null @@ -1,156 +0,0 @@ -# Compiler cache architecture - -The compiler cache exists to avoid proving unchanged facts again. It must never turn an uncertain -input into a cache hit, and a missing, stale, corrupt, oversized, or contended cache must behave like -an ordinary uncached compile. - -This document defines the target architecture. The current vertical slice implements versioned -model-analysis entries, dependency-aware compact analysis generations, shared SQL-test closure -indexes, bounded compact-generation retention, corruption fallback, and analysis hit/miss telemetry. -Project inventory, discovery/attachment reuse, and command snapshots remain later layers; their -sections below are design constraints rather than claims of current implementation. - -## Goals - -- Make unchanged fresh-process compiles proportional to project inventory and requested output, not - total SQL complexity. -- Make a small edit proportional to the changed resource and its semantic dependents. -- Reuse downstream analysis when an upstream implementation changes without changing its exported - column interface. -- Preserve diagnostics, bindings, types and nullability, lineage, tests, audits, hooks, macros, - ordering, artifacts, and exit codes exactly. -- Keep cache data project-local, bounded, inspectable, non-executable, and safe under concurrent - compiler processes. - -## Non-goals - -- The cache is not a second source of project truth. -- The cache does not make arbitrary Python execution deterministic. A stage with dynamic inputs that - are not represented in its identity must bypass persistent reuse. -- The cache does not store pickles, code objects, callables, imported modules, adapter instances, or - open connections. -- The cache does not replace warehouse execution state or change build selection semantics. - -## Layers - -### 1. Project inventory - -The inventory records every compiler-owned authored input as a normalized project-relative path, -resource role, content digest, size, and filesystem identity metadata. Configuration, ignore files, -adapter identity, compiler and SQL-analysis engine versions, selected target, CLI variables, and -compile modes are explicit identity inputs. - -Filesystem metadata may identify an unchanged candidate, but reusable semantic facts remain -content-addressed. Ambiguous or concurrently changing observations are misses. Added, removed, -renamed, and role-changing paths are first-class changes. - -### 2. Static discovery facts - -Parsing results for SQL, YAML, CSV metadata, and statically inspected Python declarations are keyed -by content digest, parser version, resource role, and parse options. These records contain only -validated JSON-compatible values. Runtime Python discovery still imports current project code and -is never reconstructed from executable cache data. - -### 3. Attached resource facts - -Attachment resolves configuration, declarations, macros, hooks, schemas, tests, and references. -Each record carries the exact identities it observed: - -- authored resource content; -- effective target, variables, and relevant configuration; -- visible enum and constant declarations; -- macro implementation and dependency closure; -- referenced schema or hook declarations; -- adapter rendering contract. - -If any observed identity is absent or unstable, that resource is attached normally. A global -configuration change is broad only when the affected value was observed broadly. - -### 4. Semantic analysis facts - -Model SQL analysis remains keyed by expanded SQL, analysis profile, referenced relation interfaces, -binding schema, and engine versions. The persistent representation should retain compact interned -facts so warm reads do not reconstruct duplicate lineage objects entry by entry. - -Each model publishes a deterministic interface signature containing the output columns, types, -nullability, wildcard status, and any other fact consumed by downstream analysis. A changed model -invalidates downstream semantic analysis only when this exported interface changes. Its own compiled -artifact still changes whenever its implementation changes. - -For supported, schema-bound queries, the native compiler derives immutable CTE output facts from -one parsed query tree. It retains every projection position, including unnamed expressions used by -CTE column lists and positional unions. Known input columns take precedence over same-named SELECT -aliases. Queries with incomplete facts, unsupported source shapes, or unresolved types retain the -existing analysis path. Changes to this inference algorithm invalidate prior analysis identities. - -Compiler-expanded SQL and its source-location spans are also reused within one invocation. SQL lint -may start after attachment while semantic analysis continues. Compiler-proven dynamic-output paths -are checked again with their completed proof. Native and custom rules can execute concurrently; -their combined findings pass through the same final exception and suppression policy. These -invocation-local objects and workers are not persisted in the compiler cache. - -### 5. Shared derived indexes - -Project-wide indexes are built once per compile and shared by all consumers. They include model -dependencies, test model closures, declaration visibility, selector indexes, and artifact identities. -No per-test, per-rule, or per-artifact path may rebuild an unchanged whole-project index. - -### 6. Command snapshots - -A command snapshot records validated, non-executable output facts after all lower layers complete. -It may provide a no-op fast path only when every input observed by that command is represented in -the snapshot identity. Commands involving untracked runtime values bypass this layer while still -using safe lower-layer facts. - -## Invalidation - -Invalidation follows recorded dependency edges rather than directory-wide guesses: - -1. Compare the current inventory with the last committed inventory. -2. Invalidate directly changed static facts. -3. Follow declaration, macro, configuration, schema, hook, and test dependency edges. -4. Reattach directly affected resources. -5. Reanalyze changed models and compare exported interface signatures. -6. Reanalyze downstream models only when a consumed interface changed. -7. Rebuild tests, audits, rules, and artifacts only when their recorded inputs changed. - -Removing an edge is itself an invalidating change. Cache readers must not infer validity from the -absence of a newly introduced dependency record. - -## Storage and publication - -- Records are schema-versioned and include algorithm and package fingerprints. -- Payloads have explicit size and collection-count limits before semantic acceptance. -- Digests cover keys and canonical serialized payloads. -- Writers publish in one transaction or by atomic replacement; readers observe one committed - generation. -- Concurrent lock or I/O failure is a cache miss, never a compile failure. -- Cache growth is bounded by generation and byte limits. Pruning removes old immutable records and - does not mutate current semantic state. -- `--no-cache`, target configuration, and the compile-cache environment override bypass every - persistent layer and do not publish new records. - -## Observability and guards - -The target machine-readable contract reports per-layer hits, misses, invalidations, bytes read and -written, and phase timings. The current vertical slice reports model-analysis batch hits, entry -hits, misses, bypasses, and phase timings. Counts describe semantic records, not low-level database -operations. - -Concurrent phase timings can overlap. Use the total compile time and fresh-process wall time for -end-to-end comparisons rather than summing individual phase durations. - -Fresh-process performance guards use representative semantic projects and cover: - -- cold compile with a clean target; -- unchanged warm compile; -- leaf-model implementation edit; -- upstream implementation edit with an unchanged interface; -- interface-changing upstream edit; -- test-only and macro edits; -- broad project-configuration edit; -- cache corruption and `--no-cache` fallback. - -Every path verifies exact resource counts, zero unexpected diagnostics, deterministic semantic -fingerprints, bounded peak RSS, and expected hit and invalidation counts. Wall-time ceilings are -calibrated only after the behavior and identity assertions are stable. diff --git a/docs/contract-adoption.md b/docs/contract-adoption.md deleted file mode 100644 index 002c2a2d1f..0000000000 --- a/docs/contract-adoption.md +++ /dev/null @@ -1,159 +0,0 @@ -# Contract adoption - -SQLBuild validates SQL and authored contracts offline during ordinary compilation. It does not -download warehouse schemas as part of `sqb compile`. - -When adopting an existing physical schema, use the explicit online `contract` command. The target -named by `--from` supplies only the database/schema namespace to inspect. SQLBuild uses the active -project connection for the inspection and never resolves a second set of origin credentials. - -```bash -# Compare code with selected physical relations. No files or relations are changed. -sqb contract diff --from prod --select tag:finance - -# Print the same adoption findings without changing source. -sqb contract generate --from prod --select tag:finance - -# Fill missing types and append missing physical columns in repository declarations. -sqb contract generate --from prod --select tag:finance --write - -# Explicitly replace conflicting physical names/types and remove declarations absent physically. -sqb contract generate --from prod --select tag:finance --write --overwrite - -# Sources are first-class selectable resources. -sqb contract diff --from prod --select source:raw_orders -``` - -`diff` returns exit code `0` when declarations agree and `1` when differences exist. Operational -and configuration failures remain command errors. JSON output is available with `--json` and -contains the evidence namespace, resource identity, finding kind, and declared/physical types. It -never includes connection credentials. - -## Safety and ownership - -- `--from` is read-only. Contract commands do not mutate warehouse relations, state, fingerprints, - or lifecycle records. -- Generation changes repository declarations only when `--write` is present. -- Additive generation preserves existing types on conflicts, existing columns absent physically, - comments, descriptions, audits, nullability, enums, tags, freshness, and loader configuration. -- `--overwrite` is the explicit repository replacement policy. It is not a warehouse write. -- Generation does not add `contract enforced`; activation remains an authored policy decision. -- A shared `SCHEMA()` is never changed from one model's evidence. SQLBuild reports an ownership - conflict instead. -- Files marked as generated are not hand-edited. -- Writes are atomic. SQLBuild reparses and recompiles the project, restoring exact prior contents - if validation fails. - -## SQL analysis and contracts - -`settings.sql_analysis` is the single public compiler analysis gate. It controls syntax analysis, -column binding, expression typing, output inference, and semantic validation. The corresponding -model override is `sql_analysis false`, and the CLI escape hatch is `--no-sql-analysis`. - -The legacy `settings.sql_validation`, model `sql_validation`, and `--no-sql-validation` spellings -remain compatibility aliases. Conflicting old and new configuration values are rejected. - -An enforced upstream contract is an authoritative compile-time interface. Missing or ambiguous -column references fail in projections, joins, filters, grouping, `HAVING`, `QUALIFY`, windows, and -ordering. Complete derived CTE/subquery/model shapes can also prove absence. Partial or opaque input -schemas remain open: SQLBuild does not turn missing metadata into a false negative assertion. - -Direct non-projection uses can be requested separately from value-producing column lineage: - -```bash -sqb lineage fact_orders --include-uses --format json -``` - -The `semantic_uses` payload groups direct resolved columns by contexts such as `join_on`, `where`, -`group_by`, `having`, `qualify`, `window_partition_by`, `window_order_by`, and `order_by`. These facts -are intentionally not emitted as `ColumnLineageEdge` values and are not propagated transitively. - -Compile-time binding does not replace runtime enforcement. Persisted tables and incrementals still -use staged contract/type validation before target mutation. Views receive static validation but do -not receive runtime cast reconstruction. `sqb plan` remains responsible for online physical drift -and execution decisions. - -## Proven, mismatched, and unknown output types - -Contract type validation distinguishes evidence from uncertainty: - -- An inferred type equivalent to the declared type passes. -- A proven incompatible type reports `K002`; it is an error when contract or type enforcement is - active. -- An output whose type cannot be proven reports `K003` when type enforcement is active. This remains - a warning because incomplete analysis is not proof that otherwise valid SQL violates its contract. - -`SQBRCONTRACT106` requires every column in an enforced contract to declare an adapter type. Use it -with `SQBRCONTRACT101` so every model has an enforced contract and no contract can remain a -name-only shape. Resolve unknown types at authoritative sources or explicit SQL output boundaries; -do not choose a type solely to silence the Rule. - -Projects that require every contract boundary to be statically explicit can select -`SQBRCONTRACT105`. Selected Rule findings are enforced. The Rule accepts a direct column only when -its exact declared type is proven, and requires an outer `CAST` around literals, calculations, -conditionals, functions, aggregates, windows, semi-structured expressions, and other derived -outputs. The cast target must use the declared contract type; an alias with different type syntax is -not treated as an explicit declaration of that contract. An exact outer cast establishes the Rule's -authored type boundary even when general expression inference remains incomplete. Every -set-operation branch must cast its outputs before common-type coercion. Wildcards are not an -explicit contract boundary unless they expose a compiler-proven dynamic family as described below. - -### Runtime-dynamic pivot families - -An enforced contract can remain closed when a native pivot discovers output names from data at -runtime. Declare fixed grouping columns normally and declare each generated family separately: - -```sql -MODEL ( - contract enforced, - columns (customer_id (type INTEGER)), - dynamic_columns ( - category_amounts ( - pivot_column category, - value_column amount, - aggregate MAX, - type "DECIMAL(12,2)" - ) - ), -); - -PIVOT __ref("stg_order_amounts") -ON category -USING MAX(amount) -GROUP BY customer_id -``` - -This is not an open wildcard contract. SQLBuild proves that the complete output boundary is either -one supported runtime-dynamic pivot or an exact wildcard passthrough from an upstream model with the -same families. It derives the fixed columns from the pivot input, matches every pivot aggregate to -one family, and proves the generated type from the authoritative input contract or an explicit cast. -An unproven boundary fails with `K011`; an incompatible family type fails with `K002`; an unknown -family type reports `K003`. `SQBRCONTRACT105` accepts the wildcard only after this compiler proof. - -Snowflake `PIVOT ... IN (ANY ...)` and dynamic subquery pivots are supported. DuckDB and MotherDuck -native dynamic `PIVOT ... ON ... USING ...` statements are supported. A static `IN` list continues -to use ordinary exact column declarations. Dynamically constructed SQL strings are not contract -proof, and adapters without a structurally analyzable native dynamic pivot reject -`dynamic_columns`. - -When more than one family is declared, each requires `name_pattern`, a regular expression matched -against the complete generated column name. Runtime validation requires every non-fixed physical -column to match exactly one family and have that family's declared type. Contract inspection applies -the same classification. Contract generation preserves the family declaration and does not freeze -the current runtime members into `columns`. - -When `SQBRSQL035` is also selected, the final top-level CTE owns these casts and the terminal -`SELECT` remains a plain read from that CTE: - -```sql -WITH final AS ( - SELECT - order_id, - CAST(quantity * unit_price AS DECIMAL(18, 2)) AS total_amount - FROM prepared_orders -) -SELECT - order_id, - total_amount -FROM final -``` diff --git a/docs/dense-compile-performance.md b/docs/dense-compile-performance.md deleted file mode 100644 index deaf541b62..0000000000 --- a/docs/dense-compile-performance.md +++ /dev/null @@ -1,85 +0,0 @@ -# Dense all-rules compile guard - -The dense compile guard covers cold compilation of 1,000, 3,000, 5,000 and -10,000 models with every built-in compiler rule and one custom project rule -enabled. It complements the existing semantic and incremental-cache benchmarks. - -The deterministic fixture uses neutral order data and compiles offline with the -Snowflake dialect. Each 1,000-model group has 240 sources, 50 seeds, 25 SQL -functions, 1,700 audits and 1,000 SQL tests. Queries contain dependency import -CTEs, joins, named unions, explicit contracts, wide projections and Python macro -expansion. Independent declaration scopes preserve per-model macro visibility -as the project grows. SQL tests mock immediate inputs to keep test expansion -bounded. A smaller integration fixture executes its generated tests in DuckDB. - -The guard checks: - -- Complete built-in rule selection, one selected custom rule, and no ignored - rules, scoped exceptions or relaxed thresholds. -- Resource counts, minimum authored SQL volume, and zero diagnostics. -- Zero analysis/rule cache hits and analysis bypasses for every model. -- A fingerprint of semantic JSON and every compiled artifact. -- Fresh-process wall time and peak RSS, with phase timings in test logs. - -| Models | Wall-time ceiling | Process RSS ceiling | -| ---: | ---: | ---: | -| 1,000 | 14s | 1.5GiB | -| 3,000 | 34s | 2.75GiB | -| 5,000 | 55s | 3.25GiB | -| 10,000 | 145s | 4GiB | - -These are regression ceilings for the implemented compiler, with headroom for -host variation. They are not a claim of sub-10-second compilation at every size. - -CI additionally runs each size in a 4GiB cgroup with swap disabled, covering -the test harness and child processes together. Compiler, lint and rule-engine -changes select these jobs. Local process RSS measurements alone do not prove -the cgroup gate passes. - -Run an individual size with: - -```sh -make test-e2e-dense-compile-performance SQLBUILD_BENCHMARK_MODELS=1000 -``` - -For requests containing at least 32MiB of unique SQL, the compiler compacts native -analysis results in batches of 64. Smaller requests retain a single batch. Both -use up to four workers; local resolution views avoid copying CTE definitions -that are already available through the source catalogue. -These limits change scheduling, not analysis or fallback semantics. -Projects with at least 10,000 model projections and 48MiB of prepared SQL use one -native analysis worker to leave memory headroom for the isolated custom-rule host -after analysis. Smaller SQL payloads retain parallel analysis. -The 10k cold ceiling includes headroom for this memory-bounded scheduling; warm -cache hits and small edit batches keep their existing scheduling. - -The compiler reuses borrowed syntax-tree facts when it can prove complete reference -binding and infer types without consulting mutable type annotations. Unsupported -clauses, incomplete schemas and unresolved expressions retain the normal validation -and analysis paths. - -For cold projects with 128–5,000 models and selected rules, artifact rendering can overlap rule -evaluation after graph and contract analysis. Rendering uses one background thread -and a disposable directory outside the project. The normal write phase publishes -the staged files only after its existing diagnostic gates pass. Unchanged files -retain their timestamps, stale files are removed using the normal policy, and -cached compilation keeps its normal artifact-cache path. Temporary-storage failure -falls back to ordinary artifact writing. -Larger projects retain sequential rendering to bound combined compiler and rule-host -memory; projects without selected rules avoid staging's extra file I/O. - -Detailed phase timings can overlap; physical-write time includes any staging work. -Use whole-process wall time for performance acceptance rather than summing phases. -Custom rules continue to run in their isolated host. Disabled rules caches avoid -unused fact fingerprints; enabled caches retain their normal identities and -invalidation behavior. - -The [varied warm-compile guard](warm-compile-performance.md) adds operator-shape -diversity and a shared dependency graph, measuring unchanged, leaf, upstream and -macro-edit compiles against uncached semantic/artifact oracles. - -This fixture protects dense query analysis, declaration expansion, rules and -artifact generation. It is not an exact reproduction of every application: -dynamic plugins, incremental materialization mixes and deeply nested query -tails still require application-specific acceptance measurements. Keep those -measurements private; publish only neutral fixtures and their own results. diff --git a/docs/execution-history.md b/docs/execution-history.md deleted file mode 100644 index 37258df52d..0000000000 --- a/docs/execution-history.md +++ /dev/null @@ -1,152 +0,0 @@ -# Execution history storage - -`LifecycleEventLogStorage` stores canonical immutable lifecycle facts. `RunStorage` serves an atomically -published, rebuildable run projection. Diagnostic and compute logs are never accepted by either -contract. The public types, filters, pages, records, errors, and helper functions are exported from -`sqlbuild.execution_history`. - -## Public contracts - -`LifecycleEventLogStorage` provides `append_event`, `append_events`, `get_events`, `get_schema_version`, -`upgrade_schema`, `close`, `dispose`, and context management. `RunStorage` provides `get_run`, -`get_runs`, `project`, `rebuild_from_events`, the schema/lifecycle methods, and context management. -The concrete SQLite and PostgreSQL classes additionally provide transactional -`append_and_project`, `reconcile`, and `check_health` operations. - -The deprecated `EventLogStorage` alias has been removed; use `LifecycleEventLogStorage`. Storage -tables use the lifecycle-specific names `lifecycle_event_log` in SQLite and -`sqlbuild_lifecycle_event_log` in PostgreSQL; the former `event_log` and `sqlbuild_event_log` names -are retired. - -Each newly stored event receives a positive, increasing `storage_order`, opaque stable `cursor`, -and UTC `received_at`. Pages ascend in storage order. `after_cursor` is exclusive, -`next_cursor` identifies the last returned record and is `None` for an empty page, and `has_more` -states whether another page exists. Limits default to 100 and cannot exceed 1000. Cursors are -backend- and storage-instance-owned; persist them verbatim and never parse them. They identify a -global position and remain valid if a later call changes filters. - -`EventFilter` supports invocation ID, run ID, event types, event family, producer, and inclusive -event-time bounds. Event types and family are mutually exclusive. `EventFamily` contains -invocation, run, resource-attempt, operation, and statement; select retry facts with -`EventFilter(event_types=("retry_scheduled",))`. `RunFilter` supports invocation, status, and -inclusive projection-creation bounds. A run cursor identifies the global -`(created_at, run_id)` order and is also filter-independent. - -Projection applies durable facts in `storage_order`, never `occurred_at` order. `created_at` is the -first run fact's `received_at`; start/end times retain event time. A run with no terminal is -`RunStatus.UNKNOWN` and `is_complete=False`. The last durable terminal wins if conflicting terminal -facts exist. Incremental projection and a full rebuild produce the same result, and one projection -call publishes all changes or none. - -## Local SQLite - -The CLI does not open local history implicitly. Construct and subscribe this backend only when an -application has a concrete durable-history consumer. - -```python -from pathlib import Path - -from sqlbuild.execution_history import EventFilter, RunFilter -from sqlbuild.sqlite_history import SQLiteExecutionHistory - -with SQLiteExecutionHistory(project_dir=Path(".")) as history: - assert history.check_health() - - cursor = None - while True: - page = history.get_events( - event_filter=EventFilter(run_id="RUN_ID"), - after_cursor=cursor, - ) - for stored in page.records: - print(stored.storage_order, stored.event) - if not page.has_more: - break - cursor = page.next_cursor - - for run in history.get_runs(run_filter=RunFilter()).records: - print(run.run_id, run.status, run.is_complete) -``` - -With no arguments, `SQLiteExecutionHistory()` uses the current directory. Passing `project_dir` -uses `/.sqlbuild/history.sqlite3`; passing `path` selects an exact file, and `path` may -be `":memory:"`. Do not pass both. Startup creates/migrates schema version 1 and reconciles the run -projection from all event facts. - -Schema v1 has `execution_history_metadata`, append-only `lifecycle_event_log`, and disposable -`run_projection` tables plus run, invocation, type, and created-time indexes. SQLite uses WAL mode, -foreign keys, a configurable 5000 ms default busy timeout, a process-local reentrant lock, and -transactions for append/projection publication. Multiple processes may use SQLite subject to normal -SQLite/WAL host-filesystem constraints and busy timeout; it is a host-local backend, not a deployed -coordination service. Startup refuses unknown future schema versions and never resets or downgrades -data. - -## Deployed PostgreSQL - -Install `sqlbuild[postgres]` only in deployed processes and construct the backend explicitly. The -CLI never selects PostgreSQL implicitly. Neither backend is selected by ordinary CLI commands. - -```python -import os -from datetime import UTC, datetime - -from sqlbuild.execution_history import EventFilter -from sqlbuild.observability import create_lifecycle_event, invocation_scope, run_scope -from sqlbuild.postgres_history import PostgresExecutionHistory - -occurred_at = datetime(2026, 9, 2, 10, 15, tzinfo=UTC) -with invocation_scope("deployment-20260902-1015"): - with run_scope("run-20260902-1015"): - run_started = create_lifecycle_event( - event_type="run_started", - event_id="run-20260902-1015-started", - occurred_at=occurred_at, - payload={"run_kind": "build", "selected_count": 12}, - ) - -with PostgresExecutionHistory(os.environ["SQLBUILD_HISTORY_DSN"]) as history: - first = history.append_and_project((run_started,))[0] - retry = history.append_event(run_started) - assert retry.storage_order == first.storage_order - - page = history.get_events( - event_filter=EventFilter(run_id="run-20260902-1015"), - after_cursor=None, - ) - checkpoint = page.next_cursor - -# Persist checkpoint securely. A later process resumes after that exact global position. -with PostgresExecutionHistory(os.environ["SQLBUILD_HISTORY_DSN"]) as history: - later = history.get_events( - event_filter=EventFilter(run_id="run-20260902-1015"), - after_cursor=checkpoint, - ) -``` - -The DSN must be resolved from a secret before construction. SQLBuild neither retains it for -diagnostics nor includes it in exceptions or `repr`. PostgreSQL schema v1 uses -`sqlbuild_storage_migrations`, authoritative `sqlbuild_lifecycle_event_log`, and disposable -`sqlbuild_run_projection`. Startup takes a transaction-scoped advisory migration lock, performs -forward-only migrations, and reconciles projection. All instances should run the same SQLBuild -version during rollout. - -Append and projection serialize under a history advisory lock. Only serialization failures and -deadlocks (`40001`, `40P01`) are retried internally, up to three retries by default. If connection -loss makes commit acknowledgement uncertain, discard the backend, reconnect, and retry the exact -same deterministic event ID and content. Equal content returns the existing stored fact; different -content raises `IntegrityConflictError`. This is storage idempotency, not exporter exactly-once -delivery. - -Grant deployed application roles only the required schema/table privileges and reserve migration, -backup, retention, and destructive privileges for operators. Back up `sqlbuild_lifecycle_event_log` and -`sqlbuild_storage_migrations`, preserving event IDs, canonical JSON, identity sequence values, and -storage namespace. `sqlbuild_run_projection` can be rebuilt with `reconcile()`. PostgreSQL performs -no automatic retention, partitioning, or archival. - -These are selected operational examples rather than an exhaustive API reference. The public -`sqlbuild.execution_history` facade also exports canonical content/ID helpers, page-limit -validation, projection helpers, schema and paging constants, storage protocols, models, filters, -statuses, and typed storage/cursor/schema errors. - -See [execution observability](execution-observability.md) for authority, local files, security, and -failure behavior. diff --git a/docs/execution-limits.md b/docs/execution-limits.md deleted file mode 100644 index 60c07e6ec5..0000000000 --- a/docs/execution-limits.md +++ /dev/null @@ -1,68 +0,0 @@ -# Target execution limits - -Named targets can set optional limits for `sqb build`. Limits are useful for development targets -where an unexpectedly broad selector or long-running command should stop instead of consuming an -unbounded amount of warehouse compute. - -```toml -[targets.dev] -connection = "warehouse" -database = "analytics" -schema = "dev_alice" - -[targets.dev.execution_limits] -max_models = 125 -max_duration = "45m" -remediation = """ -Review the expanded plan and confirm that the selected scope is intentional. -Automated tools must request approval before changing this policy. -""" -``` - -All fields are optional. A target without an `execution_limits` section retains unlimited existing -behavior, so production or orchestrated targets do not need placeholder values. - -## Model limit - -`max_models` is a positive integer. SQLBuild expands selectors and counts model entries that would -execute. When the count exceeds the target limit, the build exits before clone, load, or model -execution begins. The error reports the target, selected count, configured maximum, configuration -key, and confirms that no warehouse changes were made. - -`sqb plan` remains unrestricted and can be used to inspect a broad selection safely. - -## Duration limit - -`max_duration` is a positive fixed duration such as `30m`, `2h`, or `90s`. It covers planning and -warehouse-mutating build phases after target resolution. SQLBuild stops dispatching work at the -deadline and passes the remaining duration to each warehouse statement so in-flight work is -cancelled by the adapter. - -Safe in-flight cancellation is currently supported by the Snowflake adapter, whose maximum accepted -duration is seven days. SQLBuild rejects an unsupported duration or an adapter that cannot guarantee -active statement cancellation rather than presenting a limit that may leave warehouse work running. - -A duration failure may occur after earlier models have completed. Its error therefore distinguishes -the timeout from a pre-execution model-limit failure and states that warehouse changes may already -have occurred. - -## Custom remediation - -`remediation` is optional project-authored text. SQLBuild appends it to model-count, duration, and -unsupported-adapter errors after the factual error details. This lets each project define its own -approval or recovery process without SQLBuild recommending that users weaken local policy. - -## Local overrides - -The same section is accepted in `sqlbuild_local.toml` and follows normal field-by-field local target -precedence: - -```toml -[targets.dev.execution_limits] -max_models = 200 -max_duration = "90m" -``` - -This permits an intentional developer override while keeping the shared target policy visible in -`sqlbuild_project.toml`. Other target fields and any execution-limit fields omitted locally continue -to inherit their project values. diff --git a/docs/execution-observability.md b/docs/execution-observability.md deleted file mode 100644 index d01dea295c..0000000000 --- a/docs/execution-observability.md +++ /dev/null @@ -1,250 +0,0 @@ -# Execution observability - -SQLBuild deliberately separates execution facts from logs and output projections. Choose the -record that answers the question instead of treating every JSON or log file as interchangeable. - -## Records and authority - -| Record | Purpose | Authority and retention | -| --- | --- | --- | -| `LifecycleEvent` | Immutable facts about invocation, run, resource-attempt, microbatch, operation, retry, and statement lifecycles | Canonical execution evidence. An explicitly attached `LifecycleEventLogStorage` is authoritative once the fact is durable. SQLite and PostgreSQL retain facts until an operator removes the database or applies an external retention policy. | -| `DiagnosticLog` | Structured framework and user diagnostics | Explanatory only. It cannot establish that work started, completed, failed, skipped, or retried. Local compute-log retention applies. | -| `stdout.log` and `stderr.log` | Exact process output for one invocation | Troubleshooting data, not execution truth. Local compute-log retention applies. | -| `CommandOutputRecord` | Normalized, ANSI-free, line-oriented stdout/stderr chunks for an explicitly configured remote sink | Potentially sensitive, bounded, and potentially lossy troubleshooting data. It is not execution evidence and destination retention is project-owned. | -| `target/run/` | Full executed model, function, and test SQL, Python-check output, and scenario artifacts | Sensitive, command-written runtime artifacts. This shared tree is not invocation-isolated and may be replaced by later commands. It is not lifecycle truth. | -| `target/executions//statements.jsonl` | Best-effort run statement ledger with statement/resource identity, status, timing, query ID, and SQL SHA256 | A run-owned cost/query-ID input. It omits SQL text and does not establish lifecycle status. SQLBuild has no automatic retention for this tree. | -| `RunRecord` | Current run summary rebuilt from durable run facts | Disposable serving state. Rebuild it from the event log; never use it as the only lifecycle authority. | -| Integration-result JSONL | Bounded resource terminal plus integration enrichment | Canonical SQLBuild-owned integration side channel, but still a projection/consumer of lifecycle facts rather than event history. | -| Final `--json` or `--json-output` document | Aggregate command result | A separate end-of-command projection. It is not enabled by integration-result output and is not lifecycle authority. | -| `_sqlbuild_audit_results` | Append-only audit-result facts in the active warehouse | Best-effort observability projection with deterministic IDs and idempotent writes. It never changes audit gating or lifecycle truth. | - -Compute logs, command-output records, final output, full SQL artifacts, user messages, and -Python/check metadata can contain sensitive data. Lifecycle, integration-result, and sink-health records intentionally exclude -full SQL, parameter values, credentials, arbitrary user messages, and unbounded metadata. - -## Lifecycle envelope - -Schema version 1 has these fields: - -```json -{ - "event_id": "9fb6d899754d4bd79c703067a8d5046c", - "event_type": "statement_completed", - "schema_version": 1, - "producer": "sqlbuild", - "producer_version": "1.2.3", - "occurred_at": "2026-09-02T10:15:30.125000+00:00", - "invocation_id": "4cc757dd93fe466aad2a220f1e76625e", - "run_id": "run-20260902-101500", - "resource_id": "model:orders", - "resource_attempt_id": "97933c4aa29441cda133641345515e20", - "operation_id": "c978e47367494051bdde68ad03e56db4", - "statement_id": "5113ebc5afe247eb9c41dc0b27ef1a9a", - "payload": { - "adapter": "snowflake", - "duration_ms": 318.4, - "query_id": "01b6f1c2-0000-0000-0000-000000000000", - "sql_digest": "881b77b00a75e07936d7f20ea308a55e9ef98f2f1fd42c144a3a998d32094c1b" - } -} -``` - -`event_id` identifies immutable content. Re-appending canonically equal content with the same ID is -an idempotent no-op; different content under that ID is an integrity error. `producer` and -`producer_version` identify the emitting implementation, while `schema_version` identifies the -wire contract. Known schema-v1 events reject unknown envelope and payload fields. Schema version 2 -adds `invocation_sequence` and `external_context`, and catalogs bounded microbatch lifecycle facts. -Readers preserve unknown event names or newer schema versions as opaque envelopes rather than -inventing semantics. -History storage retains those opaque envelopes and exposes them to queries, but run projection, -native progress, integration-result output, and lifecycle-event sinks consume only known -`LifecycleEvent` values and ignore opaque facts. - -An invocation ID exists for every fact. A run groups executable graph work. A stable `resource_id` -identifies the logical model, source, check, or other resource, while a new -`resource_attempt_id` identifies each attempt. `operation_id` identifies one blocking non-SQL -operation and `statement_id` one SQL statement. Microbatch facts correlate through the containing -resource attempt and identify their interval through bounded payload fields. IDs are correlation -values, not timestamps. - -Publication is synchronous and preserves each producer's call order. Concurrent producers have no -global event-time ordering guarantee. Durable `storage_order` is the total order assigned by one -storage backend; `occurred_at` never reorders it. Starts and `statement_submitted` are nonterminal. -Completed, failed, and `resource_attempt_skipped` facts are terminal. `retry_scheduled` is a -first-class fact between attempts. A start without terminal evidence remains unknown or presumed -lost: no storage, projection, integration, or sink may fabricate success or failure. - -## Microbatch lifecycle - -Each active microbatch emits `microbatch_started` before staging begins. The payload records the -inclusive `cursor_start`, exclusive `cursor_end_exclusive`, complete planned range, one-based -`batch_index`, `batch_count`, configured and effective batch sizes, strategy, run type, and whether -the interval is ordinary or recovery work. `microbatch_completed` repeats that context with -duration and optional affected rows; `microbatch_failed` repeats it with bounded error identity. - -The start fact is sufficient to identify the active interval when a process is cancelled or lost -before it can publish a terminal. Concurrent intervals share the containing resource-attempt -identity and retain distinct bounds and indexes; their publication order is not a completion-order -guarantee. - -## Integration consumers - -`SQLBUILD_INTEGRATION_RESULT_PATH` (or SQLBuild's integration-owned hidden `--event-output` route) -enables schema-v1 `integration_result` JSONL. Each record contains the canonical resource-terminal -identity and bounded asset or check enrichment. The envelope fields are `schema_version`, -`record_kind`, `event_id`, `event_sequence`, `event_type`, `occurred_at`, all lifecycle correlation -IDs, resource kind/name, attempt number, duration, output kind, command, bounded failure or skip -codes, and one asset or check result. - -The writer claims each matching resource-attempt terminal once. Retries correlate through the same -resource ID and distinct attempt IDs; the latest exact terminal is claimed. Physical JSONL order is -result-callback order. `event_sequence` is the zero-based position among unique, known lifecycle -events observed by that invocation's in-memory terminal index. It excludes duplicate publications -and opaque facts, and it is neither durable `storage_order` nor a cross-process ordering key. Use it -to restore the terminal index's observed order among records from the same invocation. A result -callback without matching terminal evidence emits no record, and final aggregate JSON omits that -incomplete resource rather than inventing an outcome. Closing the writer emits nothing. - -Each confirmed audit also emits `audit_completed` during the run, as soon as its outcome is -confirmed (model audits when their model result completes). The event records the case name, reusable audit -definition name, optional authored description, attachment identity, evaluation mode, outcome, -severity and run scope, plus applicable measurement, threshold and bounded evidence details. `pass` -and `insufficient` are non-failing outcomes; `insufficient` remains distinct for consumers. - -An explicit writer path takes precedence over `SQLBUILD_INTEGRATION_RESULT_PATH`. Integration-result -files are opened in append mode, so an integration that reuses a path must truncate or rotate it -before starting a new invocation. The hidden `--event-output` transport option is available only on -`build` and `clone`; other producers use the environment-owned path. Final `--json-output` is a -different path: it overwrites its destination, and when supplied with `--json` it suppresses the -aggregate document on stdout. - -The Dagster adapter owns creation and consumption of this side channel. At a high level it streams -each validated envelope into Dagster asset materializations and checks, then uses the separate final -aggregate JSON only where end-of-command enrichment is still needed. The removed Dagster live v1 -JSONL format is neither authoritative nor supported; CHI-192 replaced it with integration-result -envelopes. - -## Audit-result projection - -Native warehouse adapters best-effort append confirmed audits to `_sqlbuild_audit_results`. Typed -columns cover result/run/audit identity (including definition name and authored description), -timestamps and target, evaluation mode and outcome, -violation or measured value, sample count/unit and minimum samples. Threshold policy and bounded -evidence use JSON columns with evidence count/truncation/error diagnostics. Rendered measurement, -evidence and executed SQL are retained in dedicated columns for inspection. - -Rows are immutable facts rather than lifecycle state. Deterministic result IDs make retries -idempotent. A projection write failure preserves the audit outcome and command exit code, reports -attempted/written/failed counts, and sets `projection_degraded`; lifecycle events and configured -sinks fail independently. - -## Local paths and troubleshooting - -For a command started on 2 September 2026 with invocation ID `INVOCATION_ID`, inspect: - -```text -logs/2026-09-02/INVOCATION_ID/ - metadata.json - stdout.log - stderr.log - diagnostics.jsonl - complete -target/run/ -target/executions//statements.jsonl -``` - -`metadata.json` initially records the invocation, command, resolved project directory, UTC start -date/time, optional selected target, and the run ID if one already exists at capture setup. On a -clean close SQLBuild atomically replaces it with final metadata containing completion time, exit -code, and stream byte counts, then creates the empty `complete` marker. The marker proves only that -capture finalized; it is not lifecycle completion evidence. Its absence means the capture is live -or abandoned, not that execution failed. - -Use the supported API to inventory and follow exact stream bytes: - -```python -from pathlib import Path - -from sqlbuild.compute_logs import ComputeLogStream, open_local_compute_log_storage - -with open_local_compute_log_storage(project_dir=Path(".")) as logs: - for capture in logs.inventory().captures: - print(capture.invocation_id, capture.is_complete, capture.path) - - cursor = 0 - while True: - chunk = logs.read( - invocation_id="INVOCATION_ID", - stream=ComputeLogStream.STDERR, - cursor=cursor, - ) - print(chunk.data.decode("utf-8", errors="replace"), end="") - cursor = chunk.next_cursor - if chunk.is_complete and not chunk.data: - break -``` - -Byte cursors are nonnegative exact offsets in one stream; they are not event cursors. Reads are -bounded to 1 MiB. The default prune policy retains the newest 20 complete captures and never removes -incomplete captures. CLI-managed capture calls `prune()` after successful finalization. Direct API -users must call `prune()` themselves; setting `retention_count=None` makes that call a no-op. - -The local files are host-local. The compute-log implementation rejects symlink roots and path -escapes but does not force file modes; permissions follow directory ownership and the process umask. -Set a restrictive umask and secure or encrypt the host volume. Operators must secure parent -directories, backups, `logs/`, and `target/`. An explicitly constructed SQLite history creates -`.sqlbuild/` with mode `0700` when new and applies mode `0600` to `history.sqlite3`; the CLI does not -construct execution-history storage implicitly. - -SQLBuild does not write a shared `target/sqlbuild.log`. Use the invocation-specific paths above so -concurrent and historical command diagnostics remain isolated. Project-creation commands (`sqb -init`, `sqb playground`, and `sqb dbt init`) create none of these runtime paths in the destination -before project creation. - -## Target layout - -`target/` contains disposable generated artifacts and caches: - -| Path | Contents | -| --- | --- | -| `target/cache/compiler/` | Regenerable compiler analysis, SQL-reference, and declaration-scope caches. | -| `target/compiled/` | Human-inspectable compiled models, functions, audits, and tests. | -| `target/run/` | Latest command's executable SQL and runtime artifacts; shared and replaceable. | -| `target/executions//` | Per-run statement ledgers and cost records. | -| `target/manifest.json` | Compiled project manifest when requested. | -| `target/sqlbuild_dag.json` | Dagster-facing project DAG artifact when requested. | - -All cache and execution paths under `target/` may be removed and regenerated. SQLBuild does not -read compatibility paths such as `target/compile-cache/`, `target/runs/`, or the removed dbt reuse -cache under `target/sqlbuild/`. - -## Failure behavior - -| Failure | Evidence and command effect | -| --- | --- | -| Explicit lifecycle storage unavailable | Direct storage API calls raise `ExecutionHistoryStorageError`; the CLI does not select a backend implicitly. | -| Lifecycle storage append fails | The dispatcher isolates the explicitly attached history subscriber and command work continues. The missing fact cannot be recovered from a projection. | -| Compute-log open or write fails | SQLBuild reports best-effort diagnostics, preserves console operation, and preserves the command result. The capture may be absent or incomplete. | -| Integration-result path creation, open, append, flush, or close fails | The error is not best effort and can fail the command or integration-output phase. The JSONL file may contain a valid prefix. | -| Final `--json-output` directory creation or write fails | The error can fail the command output phase. SQLBuild does not claim that the aggregate document was published. | -| Native audit-result projection fails | The confirmed audit outcome and exit code are preserved; projection accounting reports the failure and `projection_degraded` is set. | -| Exporter module discovery, declaration validation, provider construction, or provider setup fails | Startup fails before command execution because project extension configuration is invalid. | -| Export queue overflow or priority displacement | Eligible attempts are counted `dropped`; command correctness is unchanged. | -| Exporter raises or times out | The attempt is counted `failed`; that exporter is isolated and a timed-out exporter is blocked from later calls. Command correctness is unchanged. | -| Exporter hangs forever | Its daemon thread may remain alive and retain the shared provider session until process exit. Shutdown remains bounded and `flush_complete` is false. | -| Failure/health notification callback fails or blocks | It is isolated on a daemon worker. Failure notices may be dropped and periodic health is coalesced; command and exporter delivery continue. | -| Process interruption | Terminal facts and compute-log `complete` may be absent. Treat affected scopes as unknown, and never infer failure solely from missing evidence. | -| Exporter shutdown deadline expires | Queued and unattempted eligible pairs become `dropped`; timed-out attempts are `failed`; `flush_complete` is false. Command correctness is unchanged. | - -Observability degradation does not turn successful warehouse work into failure. Conversely, a clean -log or exporter return does not prove command success. Extension discovery/setup is different: an -invalid configured project extension prevents the command from starting. - -## Explicit exclusions - -This foundation does not provide a SQLBuild UI or API server, scheduler, sensors, remote agents, -launch/cancel control plane, durable exporter spool, object-storage compute logs, or core -Kafka/ClickHouse integration. Kafka, ClickHouse, and other exporter destinations are project-owned. - -See [execution history](execution-history.md) for storage APIs and -[typed sinks](sinks.md) for lifecycle-event and command-output delivery semantics. diff --git a/docs/model-layer-rules.md b/docs/model-layer-rules.md deleted file mode 100644 index b31c5ae74a..0000000000 --- a/docs/model-layer-rules.md +++ /dev/null @@ -1,39 +0,0 @@ -# Model layer rules - -SQLBuild model layers describe semantic responsibility, not the highest layer among a model's -dependencies. A model keeps its role when it imports a published interface. - -Model identities use exactly one authoritative layer: - -```text -____[__] -``` - -Supported layers and canonical folders are: - -| Layer | Folder | Logical schema | -| --- | --- | --- | -| `stg`, `stg_v` | `staging` | `staging` | -| `int_clean` | `intermediate/clean` | `intermediate` | -| `int_enriched`, `int_v` | `intermediate/enriched` | `intermediate` | -| `mart`, `mart_v` | `mart` | `mart` | - -`mart` and `mart_v` are published interfaces. Staging and intermediate layers are internal. The -internal dependency order is `stg`/`stg_v` → `int_clean` → `int_enriched`/`int_v`; dependencies on -published marts are outside that rank check. - -Schemas belong in central path defaults, not individual `MODEL()` declarations. View -materialization requires the matching `_v` layer, and `_v` appears only in the canonical layer -segment. - -Temporary internal dependency debt uses exact central exceptions: - -```toml -[[rules.graph_edge_exceptions]] -consumer = "orders__stg__daily_orders" -dependency = "orders__int_clean__customer_orders" -reason = "Remove after the shared normalization is moved upstream." -``` - -An exception matches one consumer/dependency edge. Wildcards are unsupported, and an entry that no -longer suppresses an internal inversion is reported as stale. diff --git a/docs/native-lint-rule-matrix.md b/docs/native-lint-rule-matrix.md deleted file mode 100644 index dc06de8fcd..0000000000 --- a/docs/native-lint-rule-matrix.md +++ /dev/null @@ -1,201 +0,0 @@ -# Native SQL Rules and formatting disposition - -SQLBuild owns SQL Rules semantics and canonical formatting while consuming Polyglot as an external -parser dependency. This ledger accounts for every rule in SQLFluff 4.3's 78-rule catalogue and the -prior Sqruff catalogue. A rule is not omitted merely because SQLBuild assigns it somewhere other -than generic SQL analysis. - -Disposition meanings: - -- **Core compiler**: default native SQL diagnostic, suppressible with a reason. -- **Optional rule**: selected with `[rules].select`; suppressible with a reason. -- **Compiler**: mandatory SQLBuild artifact invariant; ordinary parser/validator behavior for plain SQL. -- **Format**: one deterministic canonical representation, not a configurable warning. -- **project-aware Rules**: a deterministic check that requires SQLBuild resource or repository evidence. -- **Dialect/not applicable**: statement family is outside SQLBuild model queries or already rejected by - the applicable dialect parser. -- **Drop**: subjective convention whose cost or ambiguity exceeds its generic value. - -Rewrite classifications describe formatter-owned transformations: **always**, **conditional**, or **never**. Rules never mutate source. These classifications document shared analysis that `sqb format` may use for deterministic rewrites. - -## Aliasing - -| Upstream | SQLBuild disposition | Native code | Rewrite | Rationale | -| --- | --- | --- | --- | --- | -| AL01 table alias spelling | Format | — | format | Canonical printer owns explicit `AS` spelling. | -| AL02 column alias spelling | Format | — | format | Canonical printer owns explicit `AS` spelling. | -| AL03 calculated expression alias | Optional rule | `SQBRSQL022` | never | A meaningful output name requires intent. | -| AL04 duplicate table alias | Core compiler | `SQBRSQL009` | never | Ambiguous relation identity; compiler rejects when project evidence is complete. | -| AL05 unused table alias | Optional rule | `SQBRSQL023` | conditional | Remove only aliases without qualified/correlated use or a relation column list. | -| AL06 alias length | Drop | — | never | Arbitrary naming threshold. | -| AL07 forbid aliases | Drop | — | never | Counterproductive for nontrivial joins. | -| AL08 duplicate output alias | Core compiler | `SQBRSQL010` | never | Dangerous output identity; compiler owns managed outer schemas. | -| AL09 self alias | Optional rule | `SQBRSQL016` | always | Exact `name AS name` is redundant. | -| AL10 required derived alias | Compiler/dialect | — | never | Polyglot and the active dialect own syntax requirements. | - -## Ambiguity - -| Upstream | SQLBuild disposition | Native code | Rewrite | Rationale | -| --- | --- | --- | --- | --- | -| AM01 DISTINCT with GROUP BY | Core compiler | `SQBRSQL006` | always | SQLBuild reports only demonstrably redundant `DISTINCT`. | -| AM02 bare UNION | Optional rule | `SQBRSQL008` | conditional | Use `UNION DISTINCT` only in dialects that accept the explicit spelling. | -| AM03 mixed ORDER direction | Optional rule | `SQBRSQL026` | never | Diagnosis is safe; multi-expression authored edits remain conservative. | -| AM04 unknown output width | Optional rule | `SQBRSQL021` | never | Column enumeration needs schema evidence; canonical `__empty_fixture()` input CTEs in SQL tests are compiler-controlled. | -| AM05 implicit JOIN type | Optional rule | `SQBRSQL025` | always | Conditioned plain `JOIN` is spelled `INNER JOIN`. | -| AM06 mixed ordinal/name references | Optional rule | `SQBRSQL011` | never | Rewriting ordinals needs resolved projection names. | -| AM07 set branch arity | Core compiler | `SQBRSQL020` | never | Invalid branch shape; intended schema requires input. | -| AM08 implicit cartesian join | Core compiler | `SQBRSQL002`, `SQBRSQL003` | conditional | Explicit `CROSS JOIN` only where precedence and authored mapping are proven. | -| AM09 unordered LIMIT/OFFSET | Core compiler | `SQBRSQL004` | never | Ordering keys and tie-breakers require intent. | - -## Capitalization - -| Upstream | SQLBuild disposition | Rewrite | -| --- | --- | --- | -| CP01 keywords | Format | format | -| CP02 identifiers | Format when dialect semantics permit | format | -| CP03 functions | Format | format | -| CP04 literals | Format | format | -| CP05 data types | Format | format | - -SQLBuild has one canonical formatter and does not expose capitalization rule configuration. - -## Conventions - -| Upstream | SQLBuild disposition | Native code | Rewrite | Rationale | -| --- | --- | --- | --- | --- | -| CV01 inequality spelling | Format | — | format | Canonical operator spelling is presentation. | -| CV02 prefer COALESCE | Drop | — | never | Dialect-native alternatives can differ in typing and evaluation. | -| CV03 trailing comma | Format | — | format | Canonical list layout. | -| CV04 COUNT spelling | Optional rule/simplification | `SQBRSQL017` | always | `COUNT(1)` becomes explicit row-count `COUNT(*)`. | -| CV05 NULL comparison | Core compiler | `SQBRSQL001` | conditional | Right-side equality forms have deterministic `IS` repairs. | -| CV06 terminator | Format | — | format | Canonical statement/file boundary. | -| CV07 statement brackets | Format | — | format | Remove only with equivalence proof. | -| CV08 prefer LEFT JOIN | Drop | — | never | Direction is contextual, not correctness. | -| CV09 blocked words | project-aware Rules | — | never | Repository governance, not generic SQL. | -| CV10 literal quote style | Format | — | format | Normalize only with dialect-safe equivalence. | -| CV11 cast style | Format | — | format | Canonical printer owns spelling without changing types. | -| CV12 hidden join condition | Core compiler | `SQBRSQL002` | conditional | Comma joins become explicit only when precedence is clear. | - -## Jinja and layout - -| Upstream | SQLBuild disposition | Rewrite | -| --- | --- | --- | -| JJ01 Jinja padding | Not applicable | never | SQLBuild uses Python interpolation rather than Jinja. | -| LT01–LT15 | Format | format | Spacing, indentation, line width, operators, commas, CTE layout, clause breaks, and file newlines belong to the canonical formatter. | - -## References - -| Upstream | SQLBuild disposition | Native code | Rewrite | Rationale | -| --- | --- | --- | --- | --- | -| RF01 reference absent from scope | Core compiler | `SQBRSQL029` | never | Native SQL analysis reports proven unknown qualifiers; compiler owns schema-resolved failures. | -| RF02 qualify multi-relation columns | Optional rule | `SQBRSQL027` | never | Correct qualifier requires relation-resolution evidence. | -| RF03 consistent single-relation qualification | Optional rule | `SQBRSQL028` | never | Readability policy; do not rewrite several ranges speculatively. | -| RF04 keyword identifiers | Dialect/project-aware Rules | — | never | Dialect parser owns illegal forms; legal naming convention requires project context. | -| RF05 special characters | project-aware Rules | — | never | Project naming convention, not generic correctness. | -| RF06 unnecessary quotes | Format | — | format | Remove only when identifier semantics remain identical. | -| RF07 window alias references | Compiler/dialect | — | never | Scope resolution and active dialect determine legality. | - -## Structure - -| Upstream | SQLBuild disposition | Native code | Rewrite | Rationale | -| --- | --- | --- | --- | --- | -| ST01 ELSE NULL | Optional rule | `SQBRSQL012` | always | Omission has the same CASE fallback. | -| ST02 simple boolean CASE | Optional rule | `SQBRSQL030` | conditional | Exact TRUE/FALSE shape becomes NULL-safe `COALESCE`. | -| ST03 unused CTE | Core compiler | `SQBRSQL005` | conditional | Remove only authored, comment-safe, select-only, non-recursive dead CTEs. | -| ST04 nested ELSE CASE | Optional rule | `SQBRSQL031` | never | Flattening needs multi-range branch reconstruction. | -| ST05 prefer CTE over subquery | Drop | — | never | Subjective and can harm optimizer/readability choices. | -| ST06 column category order | Drop | — | never | Subjective and potentially schema-breaking. | -| ST07 prefer ON over USING | Drop | — | never | Forms have semantic and output-shape tradeoffs. | -| ST08 DISTINCT parentheses | Optional rule | `SQBRSQL013` | conditional | Rewrite only comment-free, balanced authored forms. | -| ST09 join operand order | Drop | — | never | Cosmetic. | -| ST10 constant predicate | Optional rule | `SQBRSQL014` | never | Removing scaffolding can change intended control flow. | -| ST11 unused join | Optional rule | `SQBRSQL032` | never | Join may intentionally filter or multiply rows. | -| ST12 repeated semicolon | Optional rule/format | `SQBRSQL015` | always | Empty statement terminator is removable. | - -## Dialect-specific bundles - -| Upstream | SQLBuild disposition | Rationale | -| --- | --- | --- | -| OR01 Oracle empty batch | Dialect/not applicable | SQLBuild model bodies are query expressions; active parser owns batch syntax. | -| PG01 excessive locks | Dialect/not applicable | Transaction/DDL operational behavior is outside model-query Rules. | -| PG02 NOT VALID foreign key | Dialect/not applicable | SQLBuild model bodies do not own PostgreSQL constraint DDL. | -| TQ01 `sp_` procedure prefix | project-aware Rules/not applicable | Procedure naming convention is not generic model SQL. | -| TQ02 procedure BEGIN/END | Dialect/not applicable | Procedure body syntax is owned by the T-SQL parser/compiler. | -| TQ03 empty batch | Format/dialect | Canonical file output removes empty statements where applicable. | -| TQ04 procedure alias spelling | Format/dialect | Canonical spelling only in supported T-SQL statement contexts. | - -## SQLBuild-owned additions - -| Native code | Disposition | Rewrite | Purpose | -| --- | --- | --- | --- | -| `SQBRSQL007` | Core compiler | never | Positional set operation with star is vulnerable to schema-order drift. | -| `SQBRSQL018` | Core compiler | never | `ROW_NUMBER` without deterministic window ordering is unstable. | -| `SQBRSQL019` | Core compiler | never | Literal NULL in `NOT IN` triggers three-valued anti-filter behavior. | -| `SQBRSQL024` | Optional rule | never | Right-side WHERE reference can null-reject a LEFT JOIN. | -| `SQBRSQL033` | Optional rule | never | Standalone comments attach immediately to a supported syntax node. | -| `SQBRSQL034` | Optional rule | never | Transformation logic remains in top-level CTEs. | -| `SQBRSQL035` | Optional rule | never | The terminal SELECT reads plainly from the final top-level CTE. | -| `SQBRSQL036` | Optional rule | never | CTEs remain at statement top level. | -| `SQBRSQL037` | Optional rule | never | Recursive CTEs require an explicit alternative design. | -| `SQBRSQL038` | Optional rule | never | Explicit cartesian products require a reasoned suppression. | -| `SQBRSQL039` | Core compiler | never | Non-lateral derived relations are named as top-level CTEs instead of being hidden inside `FROM` or `JOIN`. | -| `SQBRSQL040` | Optional rule | never | JOIN predicates compare plain columns and literals; computed keys belong in input CTEs. | -| `SQBRSQL041` | Optional rule | never | The last top-level CTE is named `final`; no other CTE at any nesting level uses that name. | - -## Join keys and final CTE names - -`SQBRSQL040` checks every `JOIN ... ON` in the authored SQL resources covered by SQL Rules, -including joins inside CTEs and subqueries. `USING (...)` is accepted. Allowed predicates are: - -- Plain column comparisons, including equality, inequality, range and null-safe comparisons. -- A plain column compared with a literal, including a signed numeric literal. -- Literal-only predicates such as `ON TRUE`, `ON FALSE`, and `ON 1 = 1`, including - `LEFT JOIN LATERAL FLATTEN(...) AS f ON TRUE`. Functions or casts applied to literals are - still computed expressions. Other rules, such as `SQBRSQL003`, independently govern - placeholder join conditions; this allowance is specific to `SQBRSQL040`. -- `BETWEEN` / `NOT BETWEEN` on a plain column with plain-column or literal bounds. -- `IN` / `NOT IN` on a plain column with a literal list. -- `IS NULL` / `IS NOT NULL` on a plain column. -- Parenthesized predicates and `AND` or `OR` combinations whose **every branch** is allowed. - -Functions, casts (including `::`), arithmetic, concatenation, CASE, subqueries and other computed -operands are findings. For example, replace `ON LOWER(o.category) = c.category_key` with a CTE -that projects `LOWER(category) AS category_key`, then `ON o.category_key = c.category_key`. -An `OR` with only plain predicates passes; an `OR` containing a computed branch fails. -Snowflake variant path access is a computed operand: `ON a.data:key = b.id` is flagged. -Project that value as a named input column before joining. - -`SQBRSQL041` checks naming, while `SQBRSQL035` owns the terminal SELECT's plain projection and -reference to the last top-level CTE. Select both (normally via `SQBRSQL`) for the complete -convention. The last top-level CTE must be named `final`, compared case-insensitively as in the -existing SQL rules, including quoted identifiers. Every other CTE, including a nested one, must -use a different name. Queries without CTEs are unaffected. SQL-test fixture CTEs and their -ceremonial `SELECT 1` remain exempt from this model naming convention. -Parentheses enclosing the entire query, including multiple wrapper pairs, do not create a nested -CTE scope. CTEs inside scalar subqueries or CTE bodies remain nested. `SQBRSQL035` and -`SQBRSQL036` use the same root-wrapper distinction. - -Prefer `[rules] select = ["SQBRSQL", "SQBRMODEL", "SQBRGRAPH"]` to exact-code lists. New rules -in those families apply automatically after an upgrade, so adopting this release can expose -computed join keys or noncanonical CTE names previously accepted by a family selection. - -The formatter preserves authored implicit aliases when comments require token-aligned attachment. -This avoids a silent decline when canonical generation inserts an optional `AS`, including for -`LATERAL FLATTEN(...) f`. Every safety decline is surfaced as a `format-unsafe` fault rather -than being treated as already formatted. - -Schema-dependent nullable `NOT IN`, inferred join cardinality, denominator safety, integer division, -timezone comparison, persisted-output rules, and incremental cursor/replay checks remain compiler or -project-aware Rules work when they need project schema/materialization facts. They are not weakened into syntax-only -generic guesses. - -## Current native totals - -- 41 SQLBuild-owned generic native rule codes (`SQBRSQL001`–`SQBRSQL041`) -- 13 core defaults -- 28 optional rules selectable by exact code or family under `[rules].select` -- 14 always-or-conditionally formatter-rewritable native analysis types -- `SQBRSQL000` suppression validation, with stale standalone directives formatter-rewritable -- five SQLBuild header diagnostics, with whitespace and leading-comment promotion handled when safe - -The 33 upstream presentation rules remain formatter-owned rather than inflating the Rules count. diff --git a/docs/query-diffs.md b/docs/query-diffs.md deleted file mode 100644 index 2bce1b210a..0000000000 --- a/docs/query-diffs.md +++ /dev/null @@ -1,164 +0,0 @@ -# Query and model data diffs - -`sqb diff` compares either selected models or two literal read-only SQL queries. Both input modes -use the same schema inspection, row-count reporting, key validation, deterministic sampling, -numeric tolerance, excluded-column, evidence, JSON, and exit-code behavior. - -## Compare models - -Model mode keeps the `FROM:TO` range and selectors: - -```bash -sqb diff prod:dev --full --select orders -``` - -By default, row identity comes from each model's `unique_key`. Repeated `--key` arguments override -that configuration for one invocation: - -```bash -sqb diff prod:dev --full --select order_lines \ - --key order_id \ - --key line_id -``` - -For a model without a stable row key, opt in to exact full-row multiset comparison with -`--unkeyed`. Duplicate rows retain their multiplicity; SQLBuild never guesses a key. - -```bash -sqb diff prod:dev --full --select daily_order_totals --unkeyed -``` - -## Compare raw queries - -Query mode omits `FROM:TO` because each query identifies its own input. Supply each side inline or -from a UTF-8 file, and choose keyed or unkeyed comparison explicitly: - -```bash -sqb diff \ - --left-query 'SELECT order_id, amount_cents FROM analytics.orders_before' \ - --right-query 'SELECT order_id, amount_cents FROM analytics.orders_after' \ - --left-label before \ - --right-label after \ - --key order_id -``` - -```bash -sqb diff \ - --left-query-file queries/orders_before.sql \ - --right-query-file queries/orders_after.sql \ - --key account_id \ - --key order_id -``` - -Raw inputs must contain exactly one read-only `SELECT`, `WITH`, or `VALUES` expression. SQLBuild -does not expand `ref()` or other SQLBuild templates in these inputs. The active target connection -executes both sides; use `--target NAME` to select another configured connection. - -Query mode performs a full comparison when no comparison mode flag is present. `--schema-only` is -also available. `--bounded` is model-specific; filter both raw queries directly when a bounded -query comparison is needed. - -Raw-query discovery loads only project configuration, target configuration, and the selected -adapter. It does not import unrelated project sinks, hooks, loaders, or providers. - -### Sampling, tolerances, and exclusions - -Keyed query comparisons support deterministic union-key sampling and the same bounded mismatch -examples as model comparisons: - -```bash -sqb diff \ - --left-query-file queries/orders_before.sql \ - --right-query-file queries/orders_after.sql \ - --key order_id \ - --sample-rows 50000 \ - --sample-seed 7 -``` - -Use repeated invocation overrides where needed: - -```bash -sqb diff \ - --left-query-file queries/orders_before.sql \ - --right-query-file queries/orders_after.sql \ - --key order_id \ - --exclude-column loaded_at \ - --tolerance amount_cents:absolute=1 \ - --tolerance conversion_rate:relative=0.001 -``` - -`--exhaustive` disables inherited or requested sampling. Unkeyed comparison is always exhaustive -and exact, so it does not accept sampling or numeric tolerances. - -### Bounded example values - -Comparison remains exact regardless of how examples are rendered. By default, long values are -limited to 160 characters and show context around the first differing character. Every shortened -value is marked as truncated and includes its original length. - -Control evidence independently from the number of collected examples: - -```bash -# Increase the displayed context without changing comparison scope. -sqb diff --left-query-file before.sql --right-query-file after.sql \ - --key order_id --max-value-length 500 - -# Retain keys and aggregate counts but suppress example values. -sqb diff --left-query-file before.sql --right-query-file after.sql \ - --key order_id --no-example-values - -# Explicitly opt in to complete values. --verbose alone remains bounded. -sqb diff --left-query-file before.sql --right-query-file after.sql \ - --key order_id --full-example-values -``` - -Ordinary keys are kept complete. Exceptionally long keys are shortened with an explicit length and -stable digest so an operator can still identify them without flooding terminal output. - -## Stable query results and cleanup - -SQLBuild materializes each raw query once in the selected target's default schema before comparing -it. Artifacts use a reserved run-owned name and are dropped immediately when the command finishes, -including after comparison errors. - -Each successfully created artifact publishes an immutable `query_diff_artifact` fingerprint. The -fingerprint is ownership evidence for crash recovery only: running and comparing the queries never -reads historical fingerprints, and fingerprints do not represent planned, running, complete, or -cleaned workflow states. - -If a process stops before immediate cleanup, the next query diff and `sqb janitor` can remove an -expired artifact only when its exact physical identity and reserved name match its ownership -fingerprint. A matching name without ownership evidence is reported for manual investigation and -is not deleted automatically. - -The recovery TTL defaults to 24 hours and accepts SQLBuild fixed-duration syntax: - -```toml -[diff] -query_artifact_ttl = "24h" -``` - -The TTL is a crash-recovery backstop, not the normal cleanup path. Historical ownership inspection -problems are reported but do not prevent the current queries from running. Failure to publish -ownership for a newly created artifact, or failure to remove a current-run artifact, is an execution -error. - -## Structured output and exit behavior - -Use `--json-output PATH` in either mode, or `--json` to emit one JSON document to stdout while all -lifecycle progress remains on stderr. Query results include `input_kind = "query"`, named left and -right inputs, `query_comparisons`, sparse nonzero `column_mismatches`, lifecycle timings, and -explicit example truncation metadata. Existing `models` output remains available for compatibility. - -Raw-query automation distinguishes complete findings from incomplete evidence and execution -failure: - -| Exit | Outcome | -|---:|---| -| `0` | Complete comparison with no differences | -| `1` | Complete comparison with findings | -| `2` | Safety, schema, or key preflight prevented complete value evidence | -| `3` | Setup, connection, materialization, comparison execution, ownership, or cleanup failed | - -Human output and JSON use the same outcome. Lifecycle messages include elapsed connection, -reconciliation, inspection, materialization, comparison, cleanup, and total durations. diff --git a/docs/rules-migration-ledger.md b/docs/rules-migration-ledger.md deleted file mode 100644 index 54cd765772..0000000000 --- a/docs/rules-migration-ledger.md +++ /dev/null @@ -1,70 +0,0 @@ -# Rules migration ledger - -SQLBuild assigns deterministic checks by the evidence they require: - -> SQL rules evaluate one plain SQL statement. Project-aware rules evaluate how SQLBuild resources are -> organised, configured, documented, tested, and connected. - -Compiler checks remain mandatory artifact-correctness invariants. The formatter owns one canonical -presentation. A check is not project-aware rules merely because it is strict or opinionated. - -## Former Kata rule disposition - -This ledger accounts for every rule in the replaced `SQBK` catalogue. Old identities are not aliases -for the new rules. - -| Former code | Final owner | Final code | Reason | -| --- | --- | --- | --- | -| `SQBKS000` | SQL Rules | `SQBRSQL033` | Comment attachment is statement-local; the old first-inner-CTE-line restriction was deleted. | -| `SQBKS001` | SQL Rules | `SQBRSQL034` | Top-level CTE shape needs only parsed SQL. | -| `SQBKS002` | SQL Rules | `SQBRSQL035` | Terminal-select shape needs only parsed SQL. | -| `SQBKS101` | project-aware rules | `SQBRMODEL101` | Import CTEs depend on resolved SQLBuild references and sources. | -| `SQBKS201` | project-aware rules | `SQBRMODEL102` | The star allowance depends on identifying dependency-import CTEs and configured exceptions. | -| `SQBKS202` | SQL Rules | `SQBRSQL007` | Positional set-operation stars were already covered by native lint. | -| `SQBKS301` | SQL Rules | `SQBRSQL036` | Nested CTEs are visible in one parsed statement. | -| `SQBKS302` | SQL Rules | `SQBRSQL037` | Recursive CTEs are visible in one parsed statement. | -| `SQBKS401` | project-aware rules | `SQBRMODEL103` | The check compares model naming with resolved materialisation. | -| `SQBKS501` | Deleted | — | Judging whether a CTE name is descriptive is subjective. | -| `SQBKL001` | project-aware rules | `SQBRGRAPH101` | Layer direction requires the compiled dependency graph. | -| `SQBKL101` | project-aware rules | `SQBRGRAPH102` | Managed dependencies require SQLBuild reference/source ownership. | -| `SQBKR001` | project-aware rules | `SQBRPROJECT101` | Model identity is a SQLBuild resource fact. | -| `SQBKR002` | project-aware rules | `SQBRPROJECT102` | Folder ownership is repository context. | -| `SQBKR201` | project-aware rules | `SQBRPROJECT103` | Source vocabulary is project configuration. | -| `SQBKR301` | project-aware rules | `SQBRPROJECT104` | Referenced model identity comes from compilation. | -| `SQBKR401` | project-aware rules | `SQBRCONTRACT101` | Contract enforcement is MODEL metadata. | -| `SQBKR500`–`SQBKR503` | project-aware rules | `SQBRPROJECT201`–`SQBRPROJECT204` | Domain and owner layout require repository paths and project-wide inventory. | -| `SQBKJ001` | SQL Rules | `SQBRSQL002` | Comma joins were already covered by native lint. | -| `SQBKJ002` | SQL Rules | `SQBRSQL038` | An explicit cross join is statement-local and suppressible with a reason. | -| `SQBKJ101` | SQL Rules | `SQBRSQL003` | Missing or unconditional join constraints were already covered by native lint. | -| `SQBKN001`–`SQBKN003` | project-aware rules | `SQBRCONTRACT102`–`SQBRCONTRACT104` | These checks compare names with declared contract types. | -| `SQBKH001`–`SQBKH002` | project-aware rules | `SQBRDECLARATION101`–`SQBRDECLARATION102` | Enum and constant use requires declarations and compiled substitutions. | -| `SQBKH101` | project-aware rules | `SQBRDECLARATION201` | Duplicate declarations require project-wide inventory. | -| `SQBKH201` | project-aware rules | `SQBRDECLARATION301` | Declaration placement requires repository ownership. | -| `SQBKH301`–`SQBKH305` | project-aware rules | `SQBRDECLARATION302`–`SQBRDECLARATION306` | Declaration-container checks require paths and project-wide inventory. | -| `SQBKT001`–`SQBKT004` | project-aware rules | `SQBRTEST101`–`SQBRTEST104` | Test roots, names, and ownership require compiler test resources. | -| `SQBKT101` | project-aware rules | `SQBRTEST105` | Scenario description rules require SCENARIO metadata. | -| `SQBKX001`–`SQBKX002` | project-aware rules | `SQBRTEST201`–`SQBRTEST202` | Audit and test minima require compiled model evidence. | -| `SQBKX201` | project-aware rules | `SQBRTEST301` | Custom-rule coverage requires the configured Rules catalogue and test inventory. | - -## Rules added after the migration - -These built-ins have no former `SQBK` counterpart. - -| Code | Owner | Reason | -| --- | --- | --- | -| `SQBRTEST203` | project-aware rules | Rejecting empty-input-only SQL tests requires compiled test fixtures and assertions; see [SQL test coverage rules](sql-test-coverage-rules.md). | - -## Rules families - -- `SQBRCONTRACT`: contracts and declared column semantics. -- `SQBRDECLARATION`: declarations and decision vocabulary. -- `SQBRGRAPH`: compiled dependency graph rules. -- `SQBRPROJECT`: repository naming and ownership layout. -- `SQBRMODEL`: SQLBuild-aware model structure. -- `SQBRTEST`: SQL tests, audits, and custom-rule evidence. - -Current built-ins use `SQBR`. Current custom rules use -`XSQBR`. Former `SQBL001`–`SQBL038` codes map directly to -`SQBRSQL001`–`SQBRSQL038`. Former `SQBP` families map to the complete family words above. Former -`XSQBL...` and `XSQBP...` custom codes must be reassigned to globally unique `XSQBR...` codes. -The former identifiers are migration evidence only and are not accepted as aliases. diff --git a/docs/sinks.md b/docs/sinks.md deleted file mode 100644 index 86bd83e747..0000000000 --- a/docs/sinks.md +++ /dev/null @@ -1,261 +0,0 @@ -# Typed sinks - -Project sinks consume one declared record type. Lifecycle-event and command-output contracts are -separate even when both sinks use the same provider instance and remote service. Delivery is -bounded, isolated, destination-neutral, and best effort. A sink cannot change an execution result -and is not durable history. - -The former `sqlbuild.event_exporters`, `sqlbuild.output_capture`, `event_exporters/`, and -`[event_exporters]` interfaces have been replaced. Move declarations to `sinks/`, import the typed -APIs below from `sqlbuild.sinks`, and move lifecycle filters to `[sinks.lifecycle]`. Legacy project -directories and configuration fail with an explicit migration error rather than silently disabling -publication. - -## Declare a lifecycle-event sink - -SQLBuild imports public `sinks/**/*.py` files in sorted path order. It ignores -`__init__.py` and any file below a path component beginning with `_`. Only functions defined in the -module and decorated with the public API are sinks. - -```python -# sinks/publish.py -from providers.destination_client import DestinationClient -from sqlbuild.sinks import ( - LifecycleEvent, - LifecycleEventKind, - lifecycle_event_sink, - lifecycle_event_to_json, -) - - -@lifecycle_event_sink( - name="publish_lifecycle", - event_kinds={LifecycleEventKind.RUN, LifecycleEventKind.RESOURCE, LifecycleEventKind.AUDIT}, - min_severity="info", -) -def publish_lifecycle( - event: LifecycleEvent, - destination_client: DestinationClient, -) -> None: - destination_client.publish( - route="sqlbuild.lifecycle.v1", - key=event.event_id, - payload=lifecycle_event_to_json(event).encode("utf-8"), - ) -``` - -The function must be synchronous, declare `event` first, use only named parameters, have no -defaults or variadics, and return `None` if annotated. `event` may be unannotated or exactly -`LifecycleEvent`. Every later parameter is injected by provider name and may be unannotated or -annotated with exactly that discovered provider class. Sink names are unique lower-snake-case -identifiers; the default name is the function name. - -## Declare a command-output sink - -Remote stdout/stderr publication is disabled unless a public function under `sinks/` is explicitly -decorated with `@command_output_sink`. Lifecycle-event sinks never receive command output. - -```python -# sinks/publish.py -from providers.destination_client import DestinationClient -from sqlbuild.sinks import ( - CommandOutputRecord, - command_output_sink, - command_output_to_json, -) - - -@command_output_sink(name="publish_command_output", streams={"stdout", "stderr"}) -def publish_command_output( - record: CommandOutputRecord, - destination_client: DestinationClient, -) -> None: - destination_client.publish( - route="sqlbuild.command_output.v1", - key=record.record_id, - payload=command_output_to_json(record).encode("utf-8"), - ) -``` - -The function must be synchronous, declare `record` first, and follow the same provider-parameter -rules as a lifecycle sink. `streams` defaults to both streams and may explicitly narrow delivery to -`stdout` or `stderr`. Loss summaries are delivered to every command-output sink because a shared -bounded queue can lose records from either stream. - -`CommandOutputRecord` is a canonical versioned envelope. Records are ANSI-free, line-oriented, -UTF-8-size-bounded chunks with invocation/run correlation, per-invocation sequence, stream, -chunk position, producer metadata, and opaque JSON-compatible integration context. A -`command_output_loss` record reports bounded-queue loss. This stream is troubleshooting data: it is -potentially sensitive, may be lossy, and is not execution evidence. Exact host-local bytes remain in -the invocation's `stdout.log` and `stderr.log` compute logs. - -A minimal destination-neutral provider shape is: - -```python -# providers/destination_client.py -from sqlbuild.providers import Provider - - -class DestinationClient(Provider): - provider_name = "destination_client" - - def setup(self, ctx: object | None) -> None: - del ctx - # Create the project-owned SDK client from secret-resolved settings here. - - def publish(self, *, route: str, key: str, payload: bytes) -> None: - del route, key, payload - # Route, key, serialization, retries, acknowledgements, and durability are project policy. - - def teardown(self) -> None: - # Close the project-owned SDK client here. - pass -``` - -The example uses released SQLBuild imports and exact discovery signatures. Replace only the method -bodies with a project-owned destination SDK. SQLBuild owns event validation/redaction and local -dispatch; the project owns destination client settings, credentials, topic/route, key, -serialization, retry, acknowledgement, and durability. - -## Runtime filters - -Declaration filters opt in; runtime configuration can only narrow them: - -```toml -[sinks.lifecycle] -event_kinds = ["run", "resource", "operation", "statement"] -min_severity = "info" - -[sinks.lifecycle.named.publish_lifecycle] -event_kinds = ["resource", "statement"] -min_severity = "warning" -``` - -Effective kinds are the intersection of declaration, global, and named sets. Effective minimum -severity is the strictest of all three. Omitting declaration options means all kinds at `debug`. -Unknown lifecycle sink names, keys, kinds, or severities fail before execution. -Python declarations should use `LifecycleEventKind`; TOML continues to use the corresponding string -values. The `audit` kind carries `audit_completed` as soon as each audit outcome is confirmed during -the run: standalone, source, and end audits publish per audit, and model audits publish when their -model result completes. Each executed (non-reused) audit publishes exactly once. Its payload -includes audit and attachment identity, evaluation mode, outcome and severity, run scope, optional -measurement/sample/threshold summary, bounded evidence metadata and rendered audit SQL diagnostics. - -| Lifecycle event | Export kind | Severity | Queue priority | -| --- | --- | --- | --- | -| `invocation_started` | `invocation` | `debug` | 0 | -| `run_started` | `run` | `debug` | 0 | -| `resource_attempt_started` | `resource` | `debug` | 0 | -| `microbatch_started` | `microbatch` | `debug` | 0 | -| `operation_started` | `operation` | `debug` | 0 | -| `statement_started`, `statement_submitted` | `statement` | `debug` | 0 | -| `retry_scheduled` | `retry` | `warning` | 1 | -| `audit_completed` | `audit` | `info` | 2 | -| `invocation_completed`, `run_completed`, `resource_attempt_completed`, `resource_attempt_skipped`, `microbatch_completed`, `operation_completed`, `statement_completed` | Corresponding kind | `info` | 2 | -| Any `*_failed` event | Corresponding kind | `error` | 3 | - -Diagnostics and command output never enter lifecycle-event queues. - -## Discovery and provider lifetime - -Startup first imports sink modules and collects declarations without framework provider discovery. -If there are no decorated public sinks, SQLBuild does not discover, construct, or set up providers -and does not create delivery threads. Sink modules are ordinary Python modules, so -their own imports and import-time side effects still run; keep declarations import-safe. Otherwise -normal project discovery finds provider classes, validates name/type -injection, constructs providers, and calls each required provider's `setup` at most once on first -use. Shared providers are torn down once in reverse setup order. - -Shutdown stops lifecycle-event acceptance, performs a bounded drain, and only then requests provider -teardown. The drain defaults to two seconds and is configurable for all lifecycle sinks: - -```toml -[sinks.lifecycle] -shutdown_timeout = "30s" -``` - -`shutdown_timeout` is a fixed duration string from `"0s"` to `"10m"` such as `"30s"` or `"2m"`; -values outside that range, calendar units, or non-strings fail before execution. `"0s"` drops anything still queued at exit. The setting is global only and is not -accepted under `[sinks.lifecycle.named.]`. It bounds how long the command waits at exit; it does -not change the per-invocation timeout below. A sink that exports audit results should keep each call -well below that one-second invocation bound and use a drain long enough to flush the tail of a run. Each lifecycle sink invocation runs on its own daemon isolation thread. A call has a one-second -default timeout; after timeout that sink is blocked from later invocations. Python cannot kill -the thread or reverse destination side effects. Provider teardown is deferred until all live -invocations return, so a forever-hung daemon retains its provider session until process exit. A -provider is never torn down while live sink code can access it. Command-output capture has its own -bounded queue, batching, and shutdown deadline, then uses the already-bound command-scoped provider -instances. - -## Queueing and accounting - -One command owns one daemon dispatcher and a queue of 1024 events by default. Dispatch always takes -the highest priority and is FIFO by canonical enqueue sequence among equal priorities. If full, a -higher-priority arrival displaces the oldest queued event at the lowest lower priority. An arrival -that is not higher priority is dropped. Filtering happens before queueing, and each queued event -carries its exact eligible exporter set, preventing duplicate attempts during fanout or displacement. - -For every event/exporter pair exactly one rule applies: - -```text -filtered += 1 - -or - -accepted += 1 -accepted ends in exactly one of delivered, failed, or dropped -``` - -Every final per-exporter and aggregate snapshot therefore satisfies: - -```text -accepted == delivered + failed + dropped -``` - -`delivered` means only that the project callable returned normally. It does not prove remote -acknowledgement, persistence, deduplication, or exactly-once delivery unless project code establishes -those properties. - -Health defaults to 30 seconds and emits only after counters or queue depth change. A single pending -periodic slot is overwritten by the latest snapshot while notification is blocked, so health is -coalesced rather than backlogged. Failure notices use a separate bounded queue and may be dropped. -The final summary has a retained write-once slot. Notification callbacks run on one daemon worker; -exceptions are swallowed, and shutdown does not wait for callbacks. - -When the final summary reports any `dropped` or `failed` lifecycle attempts, the command prints one -warning line to stderr after its own output, for example: - -```text -Warning: command completed successfully, but lifecycle event export was incomplete: 140 of 248 events dropped, 0 failed (sink 'orders_events'). Increase sinks.lifecycle.shutdown_timeout or check sink health. -``` - -The success clause appears only when the command itself succeeded, so the warning never makes a -successful run look failed; otherwise it starts `Warning: lifecycle event export was incomplete`. -Export loss never changes the exit code, and stdout (including `--json`) is unaffected. Nothing is -printed when every accepted attempt was delivered. With several sinks the counts are event -deliveries and only affected sinks are named. - -Nonfinal health always has `flush_complete=false`. Final `flush_complete=true` means the dispatcher -stopped, no exporter invocation remains live, the queue is empty, and all accepted pairs satisfy the -accounting invariant. A timed-out live invocation makes it false even though that attempt is -`failed` and later/queued attempts are `dropped`. - -## Security and failure boundaries - -Lifecycle payloads have a catalogued allowlist. They contain no full SQL, parameter values, -credentials, arbitrary user messages, raw process output, destination details, or provider settings. -Exporter failure and health diagnostics include only exporter name, event kind/severity, exception -type, counters, and queue dimensions. They bypass lifecycle publication and exporter enqueue to -prevent recursion. - -Do not enrich exported events from `target/run/`, compute logs, final JSON, environment variables, -or provider secrets unless the destination has an explicit sensitive-data policy. Restrict -destination credentials and permissions to only the routes and operations this exporter needs. - -Discovery, declaration, configuration, provider construction, and provider setup errors are project -configuration errors and prevent command execution. Queue overload, exporter exceptions, timeouts, -hung calls, notification failures, and incomplete shutdown affect only best-effort export. Consult -the [failure matrix](execution-observability.md#failure-behavior) for operator response. - -SQLBuild core intentionally provides no durable spool and no Kafka or ClickHouse implementation. -See [execution observability](execution-observability.md) and -[execution history](execution-history.md) for canonical authority and durable storage. diff --git a/docs/sql-test-coverage-rules.md b/docs/sql-test-coverage-rules.md deleted file mode 100644 index 68e3216aa9..0000000000 --- a/docs/sql-test-coverage-rules.md +++ /dev/null @@ -1,83 +0,0 @@ -# SQL test coverage rules - -`SQBRTEST202` (`minimum-tests`) requires each non-passthrough model to have at least -`min_tests_per_model` model-mode SQL unit tests. `SQBRTEST203` (`empty-input-only-test`) rejects -tests that cannot fail because they only prove that empty inputs produce no rows. Both rules are -enabled by default when the `SQBRTEST` family is selected. - -## Empty-input-only tests - -Do not write tests like this one: - -```sql -TEST (name "orders__empty_inputs_produce_no_rows"); - -WITH __source__raw_orders AS ( - SELECT NULL AS order_id, NULL AS amount - WHERE FALSE -), __assert__empty_inputs_produce_no_rows AS ( - SELECT 1 AS unexpected_row - FROM __ref("orders") -) -SELECT 1 -``` - -When a model's rows come only from its inputs, this test passes for almost any model logic, so it -adds no coverage. `SQBRTEST203` flags a model-mode test when all of the following hold: - -- Every mock (`__ref__`, `__source__`, `__seed__`, `__dbt_ref__`, `__table_fn__`) is a single - `SELECT` without a set operator that is limited by `LIMIT 0`, is `SELECT * FROM __EMPTY_FIXTURE()`, - or is filtered by `WHERE FALSE` or `WHERE 1 = 0` without calling any function (an aggregate such - as `ORDER BY COUNT(*)` can still return a row). A `UNION` whose last branch is filtered is not - empty. -- Every `__expected__` CTE is empty in the same way, or there is none. -- Every `__assert__` CTE is a bare row-existence check, `SELECT FROM - __ref("")` on a tested model, with no `WHERE`, `JOIN`, `GROUP BY`, `HAVING`, `QUALIFY`, - set operator, `EXISTS`, subquery, `LIMIT`, `OFFSET` or `FETCH`, and no function call in its - select list or `ORDER BY`. -- The test has no `__macro__` mocks and no model query overrides. - -Macro, UDF, and table-function tests are never flagged, and SQL that the rules parser cannot read -is never flagged. - -Replace a flagged test with one that mocks representative rows and asserts concrete transformed -output, or delete it. - -## Legitimate empty-input tests - -An empty-input test is legitimate when it asserts concrete output. For example, a global aggregate -must still return one zero-valued summary row when it has no input: - -```sql -TEST (name "order_summary__empty_inputs_return_zero_row"); - -WITH __source__raw_orders AS ( - SELECT CAST(NULL AS INTEGER) AS order_id, CAST(NULL AS INTEGER) AS amount - WHERE FALSE -), __expected__order_summary AS ( - SELECT 0 AS order_count, 0 AS total_amount -) -SELECT 1 -``` - -An `__assert__` that compares the model against a summary row with `EXCEPT` in both directions, -or one that filters for invalid rows, is also not flagged. - -## Reviewed exceptions - -List deliberate exceptions by test name in the project file, not in the test: - -```toml -[rules.rule_options.SQBRTEST203] -allowed_tests = ["customers__empty_inputs_produce_no_rows"] -``` - -`allowed_tests` must be a list of strings. For a parameterized test, an entry may name the whole -test or one expanded case. An entry that names no existing test is reported as stale. - -## Interaction with minimum tests - -Tests matched by `SQBRTEST203` do not count toward `min_tests_per_model`, whether or not -`SQBRTEST203` is selected, so filler cannot satisfy the minimum. Tests listed in `allowed_tests` are -reviewed exceptions and do count. A `SQBRTEST202` finding reports how many empty-input-only tests -it did not count. diff --git a/docs/warm-compile-performance.md b/docs/warm-compile-performance.md deleted file mode 100644 index d349b27506..0000000000 --- a/docs/warm-compile-performance.md +++ /dev/null @@ -1,48 +0,0 @@ -# Varied warm-compile guard - -`make test-e2e-varied-cache-performance` exercises a generated 1,000-model order -project in fresh CLI processes. The cache performance CI job runs this guard in a -4GiB cgroup with swap disabled, alongside the existing scaling benchmarks. - -The workload uses independently authored synthetic SQL. It retains the dense -fixture's sources, seeds, SQL functions, audits, scoped Python macros, SQL tests, -all built-in rules and one isolated custom project rule. It adds a shared dependency -graph and varies the actual expressions: CASE, aggregate windows, array indexing, -null handling, grouped queries, lateral joins and unions. - -## Structural protection - -The guard measures structure from compiled SQL and model dependencies. It requires: - -- At least 950 distinct operator-tree shapes. Identifier names and literal values - do not count toward this diversity measurement. -- A connected component containing at least 900 models. -- Dependency depth between 40 and 60 models, with 150–400 leaf models. - -These checks prevent a fixture change from silently replacing the workload with -mostly repeated queries or independent shallow chains. Structural diversity is -still a proxy for application workloads; private acceptance measurements remain -separate from this public synthetic fixture. - -## Cache and correctness protection - -The guard measures initial cache fill, an unchanged warm compile, a leaf SQL edit, -a shared upstream SQL edit and a Python macro edit. Each edit is followed by an -unchanged compile and an uncached oracle with both analysis and rule cache reuse -disabled. Project configuration is restored after each oracle. - -Every edited cached result must match its oracle's semantic JSON and every compiled -artifact. Unchanged runs must reuse analysis for every model. Edit runs must record -both reuse and invalidation, rather than treating a high cache-hit count alone as -evidence of correctness. A smaller integration fixture executes its generated SQL -tests in DuckDB with the standard rules enabled. - -Warm and edit budgets apply to complete fresh-process CLI wall time, including -discovery, macro expansion, lint, rules, artifacts and output. SQL-analysis timing -alone is insufficient: these other phases can dominate after analysis is cached. - -The initial ceilings are 24s for cache fill, 10s for unchanged warm compiles and -13s for edits, with a 2GiB process-RSS ceiling and the separate 4GiB combined CI -limit. These are regression ceilings with hosted-execution headroom, not promised -application timings. Golden fingerprints also protect the baseline and edited -outputs across compiler releases. diff --git a/pyproject.toml b/pyproject.toml index dbd4041b24..292464a3b5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -119,6 +119,8 @@ markers = [ [tool.ruff] target-version = "py312" line-length = 100 +# The website's example projects are sample SQLBuild projects, not package code. +extend-exclude = ["website"] [tool.ruff.lint] select = ["B", "E", "F", "I", "PLR0913", "PLR0915", "UP"] diff --git a/src/sqlbuild/runtime/compute_logs/README.md b/src/sqlbuild/runtime/compute_logs/README.md index d2587871ca..8534f6c998 100644 --- a/src/sqlbuild/runtime/compute_logs/README.md +++ b/src/sqlbuild/runtime/compute_logs/README.md @@ -15,5 +15,5 @@ The local implementation rejects symlink roots and path escapes but leaves modes ownership and the process umask. It is host-local and may contain sensitive output. It does not provide object storage, lifecycle authority, or a cross-host log service. -Selected public API examples and the operator walkthrough are documented in -[`docs/execution-observability.md`](../../../../docs/execution-observability.md). +Operator documentation is at +[docs.sqlbuild.com/concepts/observability](https://docs.sqlbuild.com/concepts/observability). diff --git a/src/sqlbuild/runtime/event_exporting/README.md b/src/sqlbuild/runtime/event_exporting/README.md index 76b6c2fc03..68ff9dfb11 100644 --- a/src/sqlbuild/runtime/event_exporting/README.md +++ b/src/sqlbuild/runtime/event_exporting/README.md @@ -74,5 +74,5 @@ declarations without discovering providers. If those modules contain only helper not import project providers or construct exporter queue, dispatcher, or notification threads. Normal command-owned project discovery remains responsible for providers needed by nodes and hooks. -The project declaration, configuration, delivery, security, and failure contract is documented in -[`docs/sinks.md`](../../../../docs/sinks.md). +The project declaration, configuration, delivery, security, and failure contract is documented at +[docs.sqlbuild.com/concepts/observability/sinks](https://docs.sqlbuild.com/concepts/observability/sinks). diff --git a/src/sqlbuild/runtime/execution_history/README.md b/src/sqlbuild/runtime/execution_history/README.md index fc86f33ad9..26ebad4133 100644 --- a/src/sqlbuild/runtime/execution_history/README.md +++ b/src/sqlbuild/runtime/execution_history/README.md @@ -77,6 +77,3 @@ failure internally. The caller must discard the failed backend, construct a new secret-resolved DSN, and retry the same deterministic `event_id`. Canonical-content comparison then returns the committed fact as an idempotent no-op or rejects conflicting reuse. Errors produced in this recovery path remain DSN- and credential-free. - -Selected public API examples and local/deployed operator walkthroughs are in -[`docs/execution-history.md`](../../../../docs/execution-history.md). diff --git a/src/sqlbuild/runtime/observability/README.md b/src/sqlbuild/runtime/observability/README.md index b0a22135e8..d2ed7e72e0 100644 --- a/src/sqlbuild/runtime/observability/README.md +++ b/src/sqlbuild/runtime/observability/README.md @@ -121,5 +121,5 @@ check metadata, full SQL, raw process output, arbitrary user output, and unbound never copied into the integration stream. They remain available through final aggregate output and compute logs after process exit. -The complete operator contract, path ownership, migration mapping, and failure matrix are in -[`docs/execution-observability.md`](../../../../docs/execution-observability.md). +Operator documentation is at +[docs.sqlbuild.com/concepts/observability](https://docs.sqlbuild.com/concepts/observability). diff --git a/tests/unit/src/sqlbuild/runtime/execution_history/conformance/README.md b/tests/unit/src/sqlbuild/runtime/execution_history/conformance/README.md index 16f8dbf7c1..49a23891a3 100644 --- a/tests/unit/src/sqlbuild/runtime/execution_history/conformance/README.md +++ b/tests/unit/src/sqlbuild/runtime/execution_history/conformance/README.md @@ -1,7 +1,7 @@ # Execution history backend conformance Every test in this directory receives storage factories through the parametrized `backend_case` -fixture. `BACKEND_CASES` in `conftest.py` is the single backend registry. CHI-177 and CHI-178 can run +fixture. `BACKEND_CASES` in `conftest.py` is the single backend registry. Additional backends can run the exact same test functions by adding one `BackendCase` containing SQLite or PostgreSQL factories and their deterministic failure-injection factories; test bodies must not be copied or subclassed. diff --git a/website/.gitignore b/website/.gitignore new file mode 100644 index 0000000000..ddce69b680 --- /dev/null +++ b/website/.gitignore @@ -0,0 +1,3 @@ +node_modules/ +dist/ +.astro/ diff --git a/website/.nvmrc b/website/.nvmrc new file mode 100644 index 0000000000..2bd5a0a98a --- /dev/null +++ b/website/.nvmrc @@ -0,0 +1 @@ +22 diff --git a/website/README.md b/website/README.md new file mode 100644 index 0000000000..b920a0a57f --- /dev/null +++ b/website/README.md @@ -0,0 +1,21 @@ +# sqlbuild.com + +The SQLBuild website: the homepage at `/` and the documentation at `/docs/`, built as one [Astro Starlight](https://starlight.astro.build) site and styled to match the SQLBuild UI. + +- `public/index.html` and `public/styles.css`: the homepage, plain static HTML and CSS. Its terminal examples are real `sqb` output from the projects in `examples/`, trimmed to the relevant lines. +- `src/content/docs/docs/`: the documentation pages (MDX), first ported from the Mintlify docs and now edited here. `src/navigation.mjs` holds the sidebar and redirects. +- `src/components/`: the page layout, header and page title, replacing Starlight's defaults. `src/components/mintlify/` has drop-in versions of the Mintlify components the pages use (`Card`, `CardGroup`, `Frame`, `Note`, `Warning`). +- `public/fonts/`: self-hosted fonts, each with its licence. + +Search uses Pagefind, and `llms.txt`, `llms-small.txt` and `llms-full.txt` are generated at build time. + +Requires Node 22.12 or newer. + +```bash +npm install +npm run dev # live preview +npm run build # static site in dist/, including the search index +npm run preview # serve dist/ +``` + +Search only works on a built site (`npm run build`, then `npm run preview`). diff --git a/website/astro.config.mjs b/website/astro.config.mjs new file mode 100644 index 0000000000..2e2fc826d2 --- /dev/null +++ b/website/astro.config.mjs @@ -0,0 +1,76 @@ +import { defineConfig } from 'astro/config'; +import starlight from '@astrojs/starlight'; +import starlightLlmsTxt from 'starlight-llms-txt'; +import { sqlbuildDark, sqlbuildLight } from './src/code-themes.mjs'; +import { redirects, sidebar } from './src/navigation.mjs'; + +export default defineConfig({ + site: 'https://sqlbuild.com', + redirects, + integrations: [ + starlight({ + title: 'SQLBuild', + description: 'Verify early, test properly, and refactor safely.', + logo: { + light: './src/assets/logo-light.png', + dark: './src/assets/logo-dark.png', + replacesTitle: true, + }, + favicon: '/favicon.png', + social: [ + { icon: 'github', label: 'GitHub', href: 'https://github.com/chio-labs/sqlbuild' }, + { icon: 'discord', label: 'Discord', href: 'https://discord.gg/tYU4nXdsT' }, + ], + components: { + PageFrame: './src/components/PageFrame.astro', + TwoColumnContent: './src/components/TwoColumnContent.astro', + Header: './src/components/Header.astro', + PageTitle: './src/components/PageTitle.astro', + SiteTitle: './src/components/SiteTitle.astro', + Pagination: './src/components/Pagination.astro', + ThemeSelect: './src/components/ThemeSelect.astro', + }, + customCss: ['./src/styles/fonts.css', './src/styles/sqlbuild.css'], + expressiveCode: { + themes: [sqlbuildDark, sqlbuildLight], + defaultProps: { + // Plain boxes for shell snippets, without the fake terminal title bar. + overridesByLang: { 'bash,sh,shell,zsh,console,powershell': { frame: 'none' } }, + }, + styleOverrides: { + borderRadius: '2px', + borderWidth: '1px', + codeFontFamily: 'var(--sl-font-mono)', + codeFontSize: '13px', + codeLineHeight: '1.65', + uiFontFamily: 'var(--sl-font)', + frames: { + shadowColor: 'transparent', + inlineButtonBorder: 'transparent', + inlineButtonBackgroundIdleOpacity: '0', + inlineButtonBackgroundHoverOrFocusOpacity: '0.08', + inlineButtonForeground: 'var(--sqb-muted)', + frameBoxShadowCssValue: 'none', + editorTabBarBackground: 'var(--sqb-code-bg)', + editorActiveTabBackground: 'var(--sqb-code-bg)', + editorActiveTabIndicatorTopColor: 'var(--sqb-primary)', + editorTabBarBorderBottomColor: 'var(--sqb-line)', + terminalTitlebarBackground: 'var(--sqb-code-bg)', + terminalTitlebarBorderBottomColor: 'var(--sqb-line)', + terminalTitlebarDotsOpacity: '0.35', + terminalBackground: 'var(--sqb-code-bg)', + }, + borderColor: 'var(--sqb-line)', + }, + }, + sidebar, + plugins: [ + starlightLlmsTxt({ + projectName: 'SQLBuild', + description: + 'SQLBuild is a free, open-source framework for building SQL and Python data pipelines, with compile-time checks, tests, plans and diffs.', + }), + ], + }), + ], +}); diff --git a/website/examples/README.md b/website/examples/README.md new file mode 100644 index 0000000000..519c2c1f88 --- /dev/null +++ b/website/examples/README.md @@ -0,0 +1,56 @@ +# Landing page examples + +The projects that produced the real `sqb` output on the landing page, so it can be regenerated when +the CLI changes. Build output, logs and DuckDB files are not kept; every project runs locally on +DuckDB. + +| Project | Landing page | +|---------|--------------| +| `waffle-shop/` | Steps 01 to 07. It is `sqb playground waffle-shop` plus the custom Rule in `rules/layers.py` and its test, the scoped enum and macros under `models/marts/_sqlbuild/`, and the renamed `daily_order_rollup` model. | +| `tidy-shop/` | Step 08, the janitor. | +| `hero-shop/` | The hero: a model moved and renamed. | + +## Step 01: compile errors + +Step 01 needs semantic compile checks for unknown columns and type mismatches, which are not +released yet; the output was produced with a development build. In a copy of `waffle-shop`: + +1. In `sources/raw.yml`, add `contract: enforced` to `raw__customers` and `raw__orders`, so the + columns and types that flow into the models are authoritative. +2. In `models/marts/fact_orders.sql`, change `o.quantity,` to `o.qty,` and add + `WHERE o.ordered_at > 5` as the last line. +3. Run `sqb compile`. + +## Step 08: janitor + +`tidy-shop` shows the state after the stale model was removed. To regenerate the output, restore it +first: + +```bash +cd tidy-shop +cat > models/weekly_revenue.sql <<'SQL' +MODEL ( + materialized table, +); + +SELECT DATE_TRUNC('week', revenue_date) AS revenue_week, SUM(total_revenue_cents) AS total_revenue_cents +FROM __ref("daily_revenue") +GROUP BY 1 +SQL +sqb build +rm models/weekly_revenue.sql +sqb janitor +``` + +## Hero: move and rename + +`hero-shop` shows the state after the move. The model started as `models/marts/revenue.sql`: + +```bash +cd hero-shop +mkdir -p models/marts +mv models/finance/daily_revenue.sql models/marts/revenue.sql +sqb build +mv models/marts/revenue.sql models/finance/daily_revenue.sql +sqb plan +``` diff --git a/website/examples/hero-shop/models/finance/daily_revenue.sql b/website/examples/hero-shop/models/finance/daily_revenue.sql new file mode 100644 index 0000000000..21cbbb30b2 --- /dev/null +++ b/website/examples/hero-shop/models/finance/daily_revenue.sql @@ -0,0 +1,9 @@ +MODEL ( + materialized incremental, + incremental_strategy delete_insert, + cursor revenue_date, + cursor_type timestamp, + cursor_grain day, +); + +SELECT CAST('2026-04-01' AS DATE) AS revenue_date, 2850 AS total_revenue_cents diff --git a/website/examples/hero-shop/sqlbuild_project.toml b/website/examples/hero-shop/sqlbuild_project.toml new file mode 100644 index 0000000000..e39dfebc63 --- /dev/null +++ b/website/examples/hero-shop/sqlbuild_project.toml @@ -0,0 +1,10 @@ +name = "waffle_shop" +adapter = "duckdb" +default_target = "prod" + +[connections.local] +database = "warehouse.duckdb" + +[targets.prod] +connection = "local" +schema = "prod" diff --git a/website/examples/tidy-shop/models/daily_revenue.sql b/website/examples/tidy-shop/models/daily_revenue.sql new file mode 100644 index 0000000000..6440f96b40 --- /dev/null +++ b/website/examples/tidy-shop/models/daily_revenue.sql @@ -0,0 +1,5 @@ +MODEL ( + materialized table, +); + +SELECT CAST('2026-04-01' AS DATE) AS revenue_date, 2850 AS total_revenue_cents diff --git a/website/examples/tidy-shop/sqlbuild_project.toml b/website/examples/tidy-shop/sqlbuild_project.toml new file mode 100644 index 0000000000..6fc3dbe9a4 --- /dev/null +++ b/website/examples/tidy-shop/sqlbuild_project.toml @@ -0,0 +1,15 @@ +name = "waffle_shop" +adapter = "duckdb" +default_target = "prod" + +[connections.local] +database = "warehouse.duckdb" + +[targets.prod] +connection = "local" +schema = "prod" + +[janitor] +enabled = true +retention_days = 0 +archive_retention_days = 14 diff --git a/website/examples/waffle-shop/README.md b/website/examples/waffle-shop/README.md new file mode 100644 index 0000000000..9c679a8a59 --- /dev/null +++ b/website/examples/waffle-shop/README.md @@ -0,0 +1,65 @@ +# SQLBuild Waffle Shop Playground + +This project is a local DuckDB playground for SQLBuild. It is self-contained and does not require warehouse credentials. + +## Try It + +Compile the project: + +```bash +sqb compile +``` + +Build the project locally with DuckDB: + +```bash +sqb build +``` + +After the build creates the local DuckDB objects, run SQL unit tests: + +```bash +sqb test +``` + +Run audits: + +```bash +sqb audit +``` + +Inspect lineage: + +```bash +sqb lineage fact_orders +``` + +## What This Shows + +- DuckDB-backed local execution +- SQL models across staging, intermediate, and mart layers +- Seeds, expression-backed sources, and Python source loaders +- SQL unit tests and multi-model tests +- Built-in and custom audits +- SQL functions +- Macros and custom materialization hooks +- Incremental and partition-oriented model patterns + +The local DuckDB database is created as needed when you run build-oriented commands. + +## Source Loaders + +Two raw tables are populated by Python loaders before the selected model DAG runs: + +- `raw__customers` uses a list-returning loader with `write_strategy: table`. +- `raw__orders` uses a generator loader with `write_strategy: delete_insert` and `cursor_column: ordered_at`. + +The payment source remains expression-backed, so the project demonstrates both managed source loaders and inline demo data. + +Try the loader controls: + +```bash +sqb build --no-load # run models against already-loaded source tables +sqb build --reload # reload sources while leaving models incremental +sqb load # run only source loaders +``` diff --git a/website/examples/waffle-shop/audits/generic/expression_is_true.sql b/website/examples/waffle-shop/audits/generic/expression_is_true.sql new file mode 100644 index 0000000000..6bdbdcaded --- /dev/null +++ b/website/examples/waffle-shop/audits/generic/expression_is_true.sql @@ -0,0 +1,5 @@ +AUDIT (); + +SELECT * +FROM __ref("@model") +WHERE NOT (@expression) diff --git a/website/examples/waffle-shop/functions/sql/table_fn__customer_orders.sql b/website/examples/waffle-shop/functions/sql/table_fn__customer_orders.sql new file mode 100644 index 0000000000..7f1bc18a47 --- /dev/null +++ b/website/examples/waffle-shop/functions/sql/table_fn__customer_orders.sql @@ -0,0 +1,21 @@ +FUNCTION ( + arguments (p_customer_id INTEGER), + returns table ( + order_id INTEGER, + ordered_at TIMESTAMP, + waffle_name VARCHAR, + line_total_cents INTEGER, + order_status VARCHAR, + is_completed_order BOOLEAN + ) +); + +SELECT + order_id, + ordered_at, + waffle_name, + line_total_cents, + order_status, + is_completed_order +FROM __ref("fact_orders") +WHERE customer_id = p_customer_id diff --git a/website/examples/waffle-shop/functions/sql/udf__is_completed_order.sql b/website/examples/waffle-shop/functions/sql/udf__is_completed_order.sql new file mode 100644 index 0000000000..98ef857e40 --- /dev/null +++ b/website/examples/waffle-shop/functions/sql/udf__is_completed_order.sql @@ -0,0 +1,6 @@ +FUNCTION ( + arguments (order_status STRING), + returns BOOLEAN, +); + +order_status = 'completed' diff --git a/website/examples/waffle-shop/hooks/python/.gitkeep b/website/examples/waffle-shop/hooks/python/.gitkeep new file mode 100644 index 0000000000..e69de29bb2 diff --git a/website/examples/waffle-shop/hooks/sql/.gitkeep b/website/examples/waffle-shop/hooks/sql/.gitkeep new file mode 100644 index 0000000000..e69de29bb2 diff --git a/website/examples/waffle-shop/loaders/waffle_sources.py b/website/examples/waffle-shop/loaders/waffle_sources.py new file mode 100644 index 0000000000..b9267ec2b1 --- /dev/null +++ b/website/examples/waffle-shop/loaders/waffle_sources.py @@ -0,0 +1,75 @@ +from collections.abc import Iterator +from datetime import datetime + +from sqlbuild.loaders import loader + +CUSTOMERS: list[dict[str, object]] = [ + { + "id": 1, + "first_name": "Leslie", + "last_name": "Knope", + "email": "leslie@pawnee.gov", + "created_at": datetime(2026, 1, 15, 9, 0, 0), + }, + { + "id": 2, + "first_name": "Ron", + "last_name": "Swanson", + "email": "ron@pawnee.gov", + "created_at": datetime(2026, 2, 1, 8, 0, 0), + }, + { + "id": 3, + "first_name": "Ann", + "last_name": "Perkins", + "email": "ann@pawnee.gov", + "created_at": datetime(2026, 2, 14, 10, 0, 0), + }, + { + "id": 4, + "first_name": "Ben", + "last_name": "Wyatt", + "email": "ben@pawnee.gov", + "created_at": datetime(2026, 3, 1, 7, 30, 0), + }, + { + "id": 5, + "first_name": "April", + "last_name": "Ludgate", + "email": "april@pawnee.gov", + "created_at": datetime(2026, 3, 15, 11, 0, 0), + }, +] + +ORDERS: list[tuple[int, int, int, int, datetime, str]] = [ + (1, 1, 1, 2, datetime(2026, 4, 1, 9, 15, 0), "completed"), + (2, 1, 4, 1, datetime(2026, 4, 1, 9, 15, 0), "completed"), + (3, 2, 6, 3, datetime(2026, 4, 1, 10, 30, 0), "completed"), + (4, 3, 2, 1, datetime(2026, 4, 2, 8, 45, 0), "completed"), + (5, 4, 1, 1, datetime(2026, 4, 2, 12, 0, 0), "completed"), + (6, 4, 3, 1, datetime(2026, 4, 2, 12, 0, 0), "completed"), + (7, 5, 5, 2, datetime(2026, 4, 3, 9, 0, 0), "cancelled"), + (8, 1, 2, 1, datetime(2026, 4, 3, 11, 0, 0), "completed"), + (9, 2, 6, 2, datetime(2026, 4, 4, 10, 0, 0), "preparing"), + (10, 3, 1, 4, datetime(2026, 4, 4, 14, 30, 0), "placed"), +] + + +@loader +def raw__customers(ctx: object) -> list[dict[str, object]]: + _ = ctx + return CUSTOMERS + + +@loader +def raw__orders(ctx: object) -> Iterator[dict[str, object]]: + _ = ctx + for order_id, customer_id, waffle_type_id, quantity, ordered_at, status in ORDERS: + yield { + "id": order_id, + "customer_id": customer_id, + "waffle_type_id": waffle_type_id, + "quantity": quantity, + "ordered_at": ordered_at, + "status": status, + } diff --git a/website/examples/waffle-shop/macros/currency.py b/website/examples/waffle-shop/macros/currency.py new file mode 100644 index 0000000000..f4ccb3cc93 --- /dev/null +++ b/website/examples/waffle-shop/macros/currency.py @@ -0,0 +1,6 @@ +"""Project-wide currency macros.""" + + +def line_total_cents(price_cents: str, quantity: str) -> str: + """Calculate line total cents from a unit price and quantity.""" + return f"{price_cents} * {quantity}" diff --git a/website/examples/waffle-shop/materializations/partition_tracked.py b/website/examples/waffle-shop/materializations/partition_tracked.py new file mode 100644 index 0000000000..6f7487fa0c --- /dev/null +++ b/website/examples/waffle-shop/materializations/partition_tracked.py @@ -0,0 +1,176 @@ +"""Partition-tracked custom materialization for the waffle shop example. + +Tracks which date partitions have been materialized in a state table. +Generates the expected date range, checks the tracking table for untracked +partitions, then builds each one: CTAS into staging, audit, delete-insert +into target, record in tracking table. The target is never dropped. +""" + +from __future__ import annotations + +from typing import Any + +from sqlbuild.adapter.contract.types import FrameworkType +from sqlbuild.executor.auditing.models import AuditExecutionResult +from sqlbuild.executor.custom.models import MaterializationContext, MaterializationResult + +_AUDIT_ERROR: str = "ERROR" + + +def materialize(ctx: MaterializationContext) -> MaterializationResult: + tracking_table: str = ctx.qualify_in_destination_schema(str(ctx.config["tracking_table"])) + partition_col: str = str(ctx.config["partition_column"]) + date_start: str = str(ctx.config["date_range_start"]) + date_end: str = str(ctx.config["date_range_end"]) + staging: str = ctx.qualify_in_destination_schema(f"{ctx.destination_name}__staging") + string_type: str = ctx.adapter.render_framework_type(FrameworkType.STRING) + timestamp_type: str = ctx.adapter.render_framework_type(FrameworkType.TIMESTAMP) + built_at_default: str = "" + if ctx.adapter.sql_analysis_dialect() != "databricks": + built_at_default = " DEFAULT CURRENT_TIMESTAMP" + + ctx.execute_sql( + f"CREATE TABLE IF NOT EXISTS {tracking_table} " + f"(partition_value {string_type}, run_id {string_type}, " + f"built_at {timestamp_type}{built_at_default})" + ) + + if ctx.is_full_refresh: + ctx.log("clearing partition tracking state") + ctx.execute_sql(f"DELETE FROM {tracking_table}") + + all_partitions: list[str] = _generate_date_range(date_start, date_end) + ctx.log("checking for stale partitions") + stale: list[str] = _find_stale_partitions(ctx, tracking_table, all_partitions) + + if not stale: + if ctx.on_progress is not None: + ctx.on_progress("no stale partitions") + return MaterializationResult(relation=ctx.destination, audit_results=()) + + if ctx.on_progress is not None: + ctx.on_progress(f"{len(stale)} partitions to build") + + all_audit_results: list[AuditExecutionResult] = [] + target_exists: bool = ctx.adapter.relation_exists( + connection=ctx.connection, + database=ctx.destination_database, + schema=ctx.destination_schema, + name=ctx.destination_name, + ) + + partition_value: str + for i, partition_value in enumerate(stale): + next_day: str = _next_date(partition_value) + partition_sql: str = ctx.sql.replace("@@@partition_start", f"'{partition_value}'").replace( + "@@@partition_end", f"'{next_day}'" + ) + + if ctx.on_progress is not None: + ctx.on_progress(f"partition {i + 1}/{len(stale)}: {partition_value}") + ctx.log(f"building partition {partition_value}") + + ctx.adapter.drop( + connection=ctx.connection, + destination=staging, + if_exists=True, + statement_recorder=ctx.statement_recorder, + ) + ctx.adapter.create_table_as( + connection=ctx.connection, + destination=staging, + sql=partition_sql, + statement_recorder=ctx.statement_recorder, + ) + + audit_results: tuple[AuditExecutionResult, ...] = ctx.run_audits(staging) + all_audit_results.extend(audit_results) + if any(r.outcome.value == _AUDIT_ERROR for r in audit_results): + return MaterializationResult( + relation=ctx.destination, + failed=True, + error=f"audit failed for partition {partition_value}", + cleanup_relations=(staging,), + audit_results=tuple(all_audit_results), + ) + + if not target_exists: + ctx.log("promoting first partition into target") + ctx.adapter.rename( + connection=ctx.connection, + origin=staging, + destination=ctx.destination, + statement_recorder=ctx.statement_recorder, + ) + target_exists = True + else: + ctx.log("promoting partition into target") + ctx.execute_sql( + f"DELETE FROM {ctx.destination} " + f"WHERE CAST({partition_col} AS DATE) >= CAST('{partition_value}' AS DATE) " + f"AND CAST({partition_col} AS DATE) < CAST('{next_day}' AS DATE)" + ) + ctx.execute_sql(f"INSERT INTO {ctx.destination} SELECT * FROM {staging}") + + ctx.adapter.merge( + connection=ctx.connection, + destination=tracking_table, + sql=( + f"SELECT '{partition_value}' AS partition_value, " + f"'{ctx.run_id}' AS run_id, CURRENT_TIMESTAMP AS built_at" + ), + unique_key="partition_value", + statement_recorder=ctx.statement_recorder, + ) + + ctx.adapter.drop( + connection=ctx.connection, + destination=staging, + if_exists=True, + statement_recorder=ctx.statement_recorder, + ) + return MaterializationResult( + relation=ctx.destination, + cleanup_relations=(staging,), + audit_results=tuple(all_audit_results), + ) + + +def _find_stale_partitions( + ctx: MaterializationContext, + tracking_table: str, + all_partitions: list[str], +) -> list[str]: + if not all_partitions: + return [] + + cursor: Any = ctx.execute_sql(f"SELECT partition_value FROM {tracking_table}") + tracked: set[str] = {str(row[0]) for row in cursor.fetchall()} + stale: list[str] = [p for p in all_partitions if p not in tracked] + latest_partition: str = all_partitions[-1] + if latest_partition not in stale: + stale.append(latest_partition) + return stale + + +def _generate_date_range(start: str, end: str) -> list[str]: + result: list[str] = [] + current: str = start + while current < end: + result.append(current) + current = _next_date(current) + return result + + +def _next_date(date_str: str) -> str: + parts: list[str] = date_str.split("-") + year: int = int(parts[0]) + month: int = int(parts[1]) + day: int = int(parts[2]) + 1 + if day > 28: + day = 1 + month += 1 + if month > 12: + month = 1 + year += 1 + return f"{year:04d}-{month:02d}-{day:02d}" diff --git a/website/examples/waffle-shop/models/intermediate/customer_status_snapshot.sql b/website/examples/waffle-shop/models/intermediate/customer_status_snapshot.sql new file mode 100644 index 0000000000..eec03020cb --- /dev/null +++ b/website/examples/waffle-shop/models/intermediate/customer_status_snapshot.sql @@ -0,0 +1,35 @@ +MODEL ( + materialized incremental, + incremental_strategy merge, + unique_key [customer_id], + cursor last_ordered_at, + cursor_type timestamp, + cursor_grain second, + row_diff_exclude_columns [latest_order_status], + row_diff_tolerances ( + by_column ( + total_revenue_cents ( + absolute 1, + ), + ), + ), + cursor_inputs ( + fact_orders ordered_at, + ), + on_schema_change sync_all_columns, + replay_on_change full, + tags [intermediate, acceptance], + description "Merge-based customer snapshot with timestamp cursor and explicit backfill policies.", + columns ( + customer_id (audits [not_null, unique]), + ), +); + +SELECT + customer_id, + MAX(ordered_at) AS last_ordered_at, + MAX(order_status) AS latest_order_status, + COUNT(*) AS total_orders, + SUM(line_total_cents) AS total_revenue_cents +FROM __ref("fact_orders") +GROUP BY customer_id diff --git a/website/examples/waffle-shop/models/intermediate/order_status_index.sql b/website/examples/waffle-shop/models/intermediate/order_status_index.sql new file mode 100644 index 0000000000..a837e5ce0e --- /dev/null +++ b/website/examples/waffle-shop/models/intermediate/order_status_index.sql @@ -0,0 +1,22 @@ +MODEL ( + materialized incremental, + incremental_strategy delete_insert, + unique_key [order_id], + cursor order_id, + cursor_type integer, + on_schema_change append_new_columns, + replay_on_change bounded-7d, + tags [intermediate, acceptance], + description "Integer-cursor incremental projection over fact orders.", + columns ( + order_id (audits [not_null, unique]), + ), +); + +SELECT + order_id, + customer_id, + order_status, + ordered_at, + line_total_cents +FROM __ref("fact_orders") diff --git a/website/examples/waffle-shop/models/marts/_sqlbuild/_enums/payment_status.sql b/website/examples/waffle-shop/models/marts/_sqlbuild/_enums/payment_status.sql new file mode 100644 index 0000000000..e68ae4e8cf --- /dev/null +++ b/website/examples/waffle-shop/models/marts/_sqlbuild/_enums/payment_status.sql @@ -0,0 +1,7 @@ +ENUM ( + name payment_status, + members ( + SUCCESS "success", + FAILED "failed", + ), +); diff --git a/website/examples/waffle-shop/models/marts/_sqlbuild/_macros/currency.py b/website/examples/waffle-shop/models/marts/_sqlbuild/_macros/currency.py new file mode 100644 index 0000000000..010f20d531 --- /dev/null +++ b/website/examples/waffle-shop/models/marts/_sqlbuild/_macros/currency.py @@ -0,0 +1,6 @@ +"""Currency formatting macros.""" + + +def cents_to_dollars(column: str) -> str: + """Convert a cents integer column to a dollars decimal with two decimal places.""" + return f"ROUND(({column}) / 100.0, 2)" diff --git a/website/examples/waffle-shop/models/marts/_sqlbuild/_macros/datetime.py b/website/examples/waffle-shop/models/marts/_sqlbuild/_macros/datetime.py new file mode 100644 index 0000000000..4a540b76a3 --- /dev/null +++ b/website/examples/waffle-shop/models/marts/_sqlbuild/_macros/datetime.py @@ -0,0 +1,7 @@ +"""Datetime portability macros.""" + + +def timestamp_trunc(ctx, grain: str, expr: str) -> str: + if ctx.adapter_name == "bigquery": + return f"TIMESTAMP_TRUNC({expr}, {grain.upper()})" + return f"DATE_TRUNC('{grain}', {expr})" diff --git a/website/examples/waffle-shop/models/marts/daily_order_partitioned.sql b/website/examples/waffle-shop/models/marts/daily_order_partitioned.sql new file mode 100644 index 0000000000..34a127a08d --- /dev/null +++ b/website/examples/waffle-shop/models/marts/daily_order_partitioned.sql @@ -0,0 +1,34 @@ +MODEL ( + materialized partition_tracked, + tags [marts], + placeholders ( + partition_start "'2026-04-01'", + partition_end "'2026-04-05'", + ), + config ( + tracking_table partition_state, + partition_column order_date, + date_range_start 2026-04-01, + date_range_end 2026-04-05, + ), + description "Partition-tracked daily order summary using custom materialization.", + columns ( + order_date (audits [not_null]), + ), + audits [ + expression_is_true ( + name "waffles_ordered_is_positive", + expression "waffles_ordered > 0", + ), + ], +); + +SELECT + CAST(o.ordered_at AS DATE) AS order_date, + COUNT(DISTINCT o.order_id) AS order_count, + SUM(o.quantity) AS waffles_ordered, + COUNT(DISTINCT o.customer_id) AS unique_customers +FROM __ref("stg_orders") o +WHERE CAST(o.ordered_at AS DATE) >= CAST(@@@partition_start AS DATE) + AND CAST(o.ordered_at AS DATE) < CAST(@@@partition_end AS DATE) +GROUP BY CAST(o.ordered_at AS DATE) diff --git a/website/examples/waffle-shop/models/marts/daily_order_rollup.sql b/website/examples/waffle-shop/models/marts/daily_order_rollup.sql new file mode 100644 index 0000000000..005256c854 --- /dev/null +++ b/website/examples/waffle-shop/models/marts/daily_order_rollup.sql @@ -0,0 +1,35 @@ +MODEL ( + materialized incremental, + incremental_strategy delete_insert, + cursor activity_day, + cursor_type timestamp, + cursor_grain day, + microbatch_strategy watermark, + cursor_watermark_mode all, + cursor_inputs ( + hourly_order_activity (column activity_hour, roles [filter, watermark]), + ), + incremental_mode microbatch, + batch_size 2d, + replay_on_change bounded-14d, + tags [marts, acceptance], + description "Downstream daily microbatch rollup with a wider batch size than its hourly upstream.", + columns ( + activity_day (audits [not_null (run_scope delta_and_final)]), + ), + audits [ + expression_is_true ( + name "daily_orders_placed_is_non_negative", + expression "orders_placed >= 0", + run_scope delta_and_final, + ), + ], +); + +SELECT + @timestamp_trunc('day', 'activity_hour') AS activity_day, + SUM(orders_placed) AS orders_placed, + SUM(waffles_ordered) AS waffles_ordered, + SUM(revenue_cents) AS revenue_cents +FROM __ref("hourly_order_activity") +GROUP BY @timestamp_trunc('day', 'activity_hour') diff --git a/website/examples/waffle-shop/models/marts/daily_revenue.sql b/website/examples/waffle-shop/models/marts/daily_revenue.sql new file mode 100644 index 0000000000..54937bbc52 --- /dev/null +++ b/website/examples/waffle-shop/models/marts/daily_revenue.sql @@ -0,0 +1,25 @@ +MODEL ( + materialized table, + tags [marts], + description "Daily revenue aggregation.", + audits [ + expression_is_true ( + name "revenue_is_non_negative", + expression "total_revenue_cents >= 0", + ), + ], + columns ( + revenue_date (nullable false), + ), +); + +SELECT + CAST(o.ordered_at AS DATE) AS revenue_date, + COUNT(DISTINCT o.order_id) AS order_count, + SUM(o.quantity) AS waffles_sold, + SUM(p.amount_cents) AS total_revenue_cents, + @cents_to_dollars('SUM(p.amount_cents)') AS total_revenue_dollars, + SUM(p.amount_cents) / COUNT(DISTINCT o.order_id) AS avg_order_value_cents +FROM __ref("stg_orders") o +INNER JOIN __ref("stg_payments") p ON o.order_id = p.order_id AND p.payment_status = @enum("payment_status").SUCCESS +GROUP BY CAST(o.ordered_at AS DATE) diff --git a/website/examples/waffle-shop/models/marts/dim_customers.sql b/website/examples/waffle-shop/models/marts/dim_customers.sql new file mode 100644 index 0000000000..3cfc74903c --- /dev/null +++ b/website/examples/waffle-shop/models/marts/dim_customers.sql @@ -0,0 +1,23 @@ +MODEL ( + materialized table, + unique_key [customer_id], + row_diff_exclude_columns [email], + tags [marts], + description "Customer dimension with lifetime metrics.", + columns ( + customer_id (nullable false, audits [not_null, unique]), + ), +); + +SELECT + c.customer_id, + c.first_name, + c.last_name, + c.email, + c.created_at AS customer_since, + COUNT(DISTINCT o.order_id) AS lifetime_orders, + COALESCE(SUM(p.amount_cents), 0) AS lifetime_spend_cents +FROM __ref("stg_customers") c +LEFT JOIN __ref("stg_orders") o ON c.customer_id = o.customer_id +LEFT JOIN __ref("stg_payments") p ON o.order_id = p.order_id AND p.payment_status = 'success' +GROUP BY c.customer_id, c.first_name, c.last_name, c.email, c.created_at diff --git a/website/examples/waffle-shop/models/marts/fact_orders.sql b/website/examples/waffle-shop/models/marts/fact_orders.sql new file mode 100644 index 0000000000..b303f2a072 --- /dev/null +++ b/website/examples/waffle-shop/models/marts/fact_orders.sql @@ -0,0 +1,26 @@ +MODEL ( + materialized table, + tags [marts], + description "Order fact table with waffle and payment details.", + columns ( + order_id (nullable false, audits [not_null]), + ), +); + +SELECT + o.order_id, + o.customer_id, + o.waffle_type_id, + w.waffle_name, + w.category AS waffle_category, + o.quantity, + @line_total_cents("w.price_cents", "o.quantity") AS line_total_cents, + o.ordered_at, + o.status AS order_status, + __udf("udf__is_completed_order")(o.status) AS is_completed_order, + p.payment_method, + p.payment_status, + p.amount_cents AS payment_amount_cents +FROM __ref("stg_orders") o +LEFT JOIN __seed("waffle_types") w ON o.waffle_type_id = w.waffle_type_id +LEFT JOIN __ref("stg_payments") p ON o.order_id = p.order_id diff --git a/website/examples/waffle-shop/models/marts/hourly_activity_with_daily_context.sql b/website/examples/waffle-shop/models/marts/hourly_activity_with_daily_context.sql new file mode 100644 index 0000000000..cc46037031 --- /dev/null +++ b/website/examples/waffle-shop/models/marts/hourly_activity_with_daily_context.sql @@ -0,0 +1,37 @@ +MODEL ( + materialized incremental, + incremental_strategy delete_insert, + cursor activity_hour, + cursor_type timestamp, + cursor_grain hour, + microbatch_strategy watermark, + cursor_watermark_mode all, + cursor_inputs ( + hourly_order_activity (column activity_hour, roles [filter]), + daily_order_rollup (column activity_day, roles [filter, watermark]), + ), + incremental_mode microbatch, + batch_size 6h, + tags [marts, acceptance], + description "Hourly downstream microbatch model that depends on a coarser-grain daily upstream.", + columns ( + activity_hour (audits [not_null (run_scope delta_and_final)]), + ), + audits [ + expression_is_true ( + name "day_orders_cover_hourly_orders", + expression "day_orders_placed >= orders_placed", + run_scope delta_and_final, + ), + ], +); + +SELECT + h.activity_hour, + h.orders_placed, + d.orders_placed AS day_orders_placed, + h.waffles_ordered, + h.revenue_cents +FROM __ref("hourly_order_activity") h +INNER JOIN __ref("daily_order_rollup") d + ON @timestamp_trunc('day', 'h.activity_hour') = d.activity_day diff --git a/website/examples/waffle-shop/models/marts/hourly_order_activity.sql b/website/examples/waffle-shop/models/marts/hourly_order_activity.sql new file mode 100644 index 0000000000..447d88966c --- /dev/null +++ b/website/examples/waffle-shop/models/marts/hourly_order_activity.sql @@ -0,0 +1,34 @@ +MODEL ( + materialized incremental, + incremental_strategy delete_insert, + cursor activity_hour, + cursor_type timestamp, + cursor_grain hour, + microbatch_strategy watermark, + cursor_watermark_mode all, + cursor_inputs ( + fact_orders (column ordered_at, roles [filter, watermark]), + ), + incremental_mode microbatch, + batch_size 1d, + tags [marts], + description "Hourly order activity aggregated via microbatch incremental.", + columns ( + activity_hour (audits [not_null (run_scope delta_and_final)]), + ), + audits [ + expression_is_true ( + name "orders_placed_is_non_negative", + expression "orders_placed >= 0", + run_scope delta_and_final, + ), + ], +); + +SELECT + @timestamp_trunc('hour', 'o.ordered_at') AS activity_hour, + COUNT(*) AS orders_placed, + SUM(o.quantity) AS waffles_ordered, + SUM(o.line_total_cents) AS revenue_cents +FROM __ref("fact_orders") o +GROUP BY @timestamp_trunc('hour', 'o.ordered_at') diff --git a/website/examples/waffle-shop/models/staging/stg_customers.sql b/website/examples/waffle-shop/models/staging/stg_customers.sql new file mode 100644 index 0000000000..fbe76dac59 --- /dev/null +++ b/website/examples/waffle-shop/models/staging/stg_customers.sql @@ -0,0 +1,17 @@ +MODEL ( + materialized view, + tags [staging], + description "Cleaned customer records.", + columns ( + customer_id (audits [not_null, unique]), + email (audits [not_null]), + ), +); + +SELECT + id AS customer_id, + first_name, + last_name, + email, + created_at +FROM __source("raw__customers") diff --git a/website/examples/waffle-shop/models/staging/stg_orders.sql b/website/examples/waffle-shop/models/staging/stg_orders.sql new file mode 100644 index 0000000000..39679a7f07 --- /dev/null +++ b/website/examples/waffle-shop/models/staging/stg_orders.sql @@ -0,0 +1,26 @@ +MODEL ( + materialized view, + tags [staging], + description "Cleaned order records.", + columns ( + order_id (nullable false, audits [not_null, unique]), + customer_id (nullable false, audits [not_null]), + waffle_type_id ( + audits [relationships (to __seed("waffle_types"), field waffle_type_id)], + ), + status ( + audits [ + accepted_values (values ["placed", "preparing", "ready", "completed", "cancelled"]), + ], + ), + ), +); + +SELECT + id AS order_id, + customer_id, + waffle_type_id, + quantity, + ordered_at, + status +FROM __source("raw__orders") diff --git a/website/examples/waffle-shop/models/staging/stg_payments.sql b/website/examples/waffle-shop/models/staging/stg_payments.sql new file mode 100644 index 0000000000..eb7465a3d6 --- /dev/null +++ b/website/examples/waffle-shop/models/staging/stg_payments.sql @@ -0,0 +1,20 @@ +MODEL ( + materialized view, + tags [staging], + description "Cleaned payment records.", + columns ( + payment_id (audits [not_null, unique]), + order_id ( + audits [not_null, relationships (to __source("raw__orders"), field id)], + ), + ), +); + +SELECT + id AS payment_id, + order_id, + amount_cents, + payment_method, + paid_at, + status AS payment_status +FROM __source("raw__payments") diff --git a/website/examples/waffle-shop/rules/layers.py b/website/examples/waffle-shop/rules/layers.py new file mode 100644 index 0000000000..99b3c2755b --- /dev/null +++ b/website/examples/waffle-shop/rules/layers.py @@ -0,0 +1,15 @@ +from sqlbuild.rules import Finding, Model, RuleContext, rule + + +@rule( + code="XSQBRARCH001", + message="Marts must read sources through staging", + remediation="Reference a staging model with __ref() instead.", +) +def marts_use_staging(*, model: Model, ctx: RuleContext) -> list[Finding]: + layer = ctx.project.tree.relative_parts(path=model.path, under="models")[0] + sql = ctx.sql.for_model(model).authored.source + if layer != "marts" or "__source(" not in sql: + return [] + line = sql[: sql.index("__source(")].count("\n") + 1 + return [ctx.finding(subject=model, line=line)] diff --git a/website/examples/waffle-shop/seeds/lookups.yml b/website/examples/waffle-shop/seeds/lookups.yml new file mode 100644 index 0000000000..59ae29e937 --- /dev/null +++ b/website/examples/waffle-shop/seeds/lookups.yml @@ -0,0 +1,12 @@ +seeds: + - name: waffle_types + description: Waffle menu items with pricing. + columns: + - name: waffle_type_id + type: INTEGER + - name: waffle_name + type: VARCHAR + - name: category + type: VARCHAR + - name: price_cents + type: INTEGER diff --git a/website/examples/waffle-shop/seeds/waffle_types.csv b/website/examples/waffle-shop/seeds/waffle_types.csv new file mode 100644 index 0000000000..4ea7deb841 --- /dev/null +++ b/website/examples/waffle-shop/seeds/waffle_types.csv @@ -0,0 +1,7 @@ +waffle_type_id,waffle_name,category,price_cents +1,Classic Belgian,sweet,850 +2,Liege,sweet,950 +3,Brussels,sweet,750 +4,Cheddar Herb,savory,1050 +5,Everything Bagel,savory,1100 +6,Chicken and Waffle,savory,1450 diff --git a/website/examples/waffle-shop/sources/raw.yml b/website/examples/waffle-shop/sources/raw.yml new file mode 100644 index 0000000000..5bdbae9c55 --- /dev/null +++ b/website/examples/waffle-shop/sources/raw.yml @@ -0,0 +1,47 @@ +sources: + - name: raw__customers + managed: true + write_strategy: table + columns: + - name: id + type: INTEGER + - name: first_name + type: VARCHAR + - name: last_name + type: VARCHAR + - name: email + type: VARCHAR + - name: created_at + type: TIMESTAMP + + - name: raw__orders + managed: true + write_strategy: delete_insert + cursor_column: ordered_at + columns: + - name: id + type: INTEGER + - name: customer_id + type: INTEGER + - name: waffle_type_id + type: INTEGER + - name: quantity + type: INTEGER + - name: ordered_at + type: TIMESTAMP + - name: status + type: VARCHAR + + - name: raw__payments + expression: | + SELECT 1 AS id, 1 AS order_id, 1700 AS amount_cents, 'credit_card' AS payment_method, CAST('2026-04-01 09:16:00' AS TIMESTAMP) AS paid_at, 'success' AS status + UNION ALL SELECT 2 AS id, 2 AS order_id, 1050 AS amount_cents, 'credit_card' AS payment_method, CAST('2026-04-01 09:16:00' AS TIMESTAMP) AS paid_at, 'success' AS status + UNION ALL SELECT 3 AS id, 3 AS order_id, 4350 AS amount_cents, 'cash' AS payment_method, CAST('2026-04-01 10:31:00' AS TIMESTAMP) AS paid_at, 'success' AS status + UNION ALL SELECT 4 AS id, 4 AS order_id, 950 AS amount_cents, 'credit_card' AS payment_method, CAST('2026-04-02 08:46:00' AS TIMESTAMP) AS paid_at, 'success' AS status + UNION ALL SELECT 5 AS id, 5 AS order_id, 850 AS amount_cents, 'debit_card' AS payment_method, CAST('2026-04-02 12:01:00' AS TIMESTAMP) AS paid_at, 'success' AS status + UNION ALL SELECT 6 AS id, 6 AS order_id, 750 AS amount_cents, 'debit_card' AS payment_method, CAST('2026-04-02 12:01:00' AS TIMESTAMP) AS paid_at, 'success' AS status + UNION ALL SELECT 7 AS id, 7 AS order_id, 2200 AS amount_cents, 'credit_card' AS payment_method, CAST(NULL AS TIMESTAMP) AS paid_at, 'failed' AS status + UNION ALL SELECT 8 AS id, 8 AS order_id, 950 AS amount_cents, 'cash' AS payment_method, CAST('2026-04-03 11:01:00' AS TIMESTAMP) AS paid_at, 'success' AS status + columns: + - name: amount_cents + type: INTEGER diff --git a/website/examples/waffle-shop/sqlbuild_project.toml b/website/examples/waffle-shop/sqlbuild_project.toml new file mode 100644 index 0000000000..74b7dbc768 --- /dev/null +++ b/website/examples/waffle-shop/sqlbuild_project.toml @@ -0,0 +1,26 @@ +name = "waffle_shop" +adapter = "duckdb" +default_target = "dev" + +[connections.developer] +database = "waffle_shop_control.duckdb" + +[settings] +default_audit_severity = "warn" + +[defaults] +materialized = "table" + +[targets.prod] +connection = "developer" +schema = "prod" + +[targets.dev] +connection = "developer" +schema = "dev" + +[path_defaults.staging] +materialized = "view" + +[rules] +select = ["XSQBRARCH"] diff --git a/website/examples/waffle-shop/tests/scenarios/daily_revenue_minimal.sql b/website/examples/waffle-shop/tests/scenarios/daily_revenue_minimal.sql new file mode 100644 index 0000000000..e9bdee45b9 --- /dev/null +++ b/website/examples/waffle-shop/tests/scenarios/daily_revenue_minimal.sql @@ -0,0 +1,59 @@ +SCENARIO ( + description "Daily revenue includes only successful payments", + tags ["revenue", "example"] +); + +WITH +__ref__stg_orders AS ( + SELECT + 1 AS order_id, + 10 AS customer_id, + 1 AS waffle_type_id, + 2 AS quantity, + CAST('2026-04-01 09:15:00' AS TIMESTAMP) AS ordered_at, + 'completed' AS status + UNION ALL + SELECT + 2 AS order_id, + 10 AS customer_id, + 4 AS waffle_type_id, + 1 AS quantity, + CAST('2026-04-01 10:00:00' AS TIMESTAMP) AS ordered_at, + 'completed' AS status +), + +__ref__stg_payments AS ( + SELECT + 1 AS payment_id, + 1 AS order_id, + 1700 AS amount_cents, + 'credit_card' AS payment_method, + CAST('2026-04-01 09:16:00' AS TIMESTAMP) AS paid_at, + 'success' AS payment_status + UNION ALL + SELECT + 2 AS payment_id, + 2 AS order_id, + 1050 AS amount_cents, + 'credit_card' AS payment_method, + CAST('2026-04-01 10:01:00' AS TIMESTAMP) AS paid_at, + 'failed' AS payment_status +), + +__expected__daily_revenue AS ( + SELECT + CAST('2026-04-01' AS DATE) AS revenue_date, + 1 AS order_count, + 2 AS waffles_sold, + 1700 AS total_revenue_cents, + 17.0 AS total_revenue_dollars, + 1700 AS avg_order_value_cents +), + +__assert__no_negative_revenue AS ( + SELECT * + FROM __ref("daily_revenue") + WHERE total_revenue_cents < 0 +) + +SELECT 1 diff --git a/website/examples/waffle-shop/tests/scenarios/daily_revenue_multi_order.sql b/website/examples/waffle-shop/tests/scenarios/daily_revenue_multi_order.sql new file mode 100644 index 0000000000..902fc13985 --- /dev/null +++ b/website/examples/waffle-shop/tests/scenarios/daily_revenue_multi_order.sql @@ -0,0 +1,59 @@ +SCENARIO ( + description "Daily revenue aggregates multiple successful orders", + tags ["revenue", "example"] +); + +WITH +__ref__stg_orders AS ( + SELECT + 11 AS order_id, + 20 AS customer_id, + 1 AS waffle_type_id, + 1 AS quantity, + CAST('2026-04-02 08:30:00' AS TIMESTAMP) AS ordered_at, + 'completed' AS status + UNION ALL + SELECT + 12 AS order_id, + 21 AS customer_id, + 3 AS waffle_type_id, + 3 AS quantity, + CAST('2026-04-02 12:15:00' AS TIMESTAMP) AS ordered_at, + 'completed' AS status +), + +__ref__stg_payments AS ( + SELECT + 11 AS payment_id, + 11 AS order_id, + 850 AS amount_cents, + 'cash' AS payment_method, + CAST('2026-04-02 08:31:00' AS TIMESTAMP) AS paid_at, + 'success' AS payment_status + UNION ALL + SELECT + 12 AS payment_id, + 12 AS order_id, + 2250 AS amount_cents, + 'debit_card' AS payment_method, + CAST('2026-04-02 12:16:00' AS TIMESTAMP) AS paid_at, + 'success' AS payment_status +), + +__expected__daily_revenue AS ( + SELECT + CAST('2026-04-02' AS DATE) AS revenue_date, + 2 AS order_count, + 4 AS waffles_sold, + 3100 AS total_revenue_cents, + 31.0 AS total_revenue_dollars, + 1550 AS avg_order_value_cents +), + +__assert__positive_average_order_value AS ( + SELECT * + FROM __ref("daily_revenue") + WHERE avg_order_value_cents <= 0 +) + +SELECT 1 diff --git a/website/examples/waffle-shop/tests/test_rule_layers.py b/website/examples/waffle-shop/tests/test_rule_layers.py new file mode 100644 index 0000000000..8e8e35a80c --- /dev/null +++ b/website/examples/waffle-shop/tests/test_rule_layers.py @@ -0,0 +1,17 @@ +from rules.layers import marts_use_staging + +from sqlbuild.rules.testing import RuleCase, evaluate_rule + + +def test_given_mart_reading_source_when_evaluating_then_reports_finding() -> None: + result = evaluate_rule( + rule=marts_use_staging, + test_case=RuleCase( + description="mart reads a raw source", + source='MODEL ();\nSELECT id FROM __source("raw__payments")\n', + path="models/marts/payments.sql", + expected_finding_count=1, + ), + ) + + assert result.finding_count == 1 diff --git a/website/examples/waffle-shop/tests/unit/test_daily_revenue_chain.sql b/website/examples/waffle-shop/tests/unit/test_daily_revenue_chain.sql new file mode 100644 index 0000000000..0b23df64d3 --- /dev/null +++ b/website/examples/waffle-shop/tests/unit/test_daily_revenue_chain.sql @@ -0,0 +1,86 @@ +TEST(); + +WITH +__source__raw__orders AS ( + SELECT + 1 AS id, + 100 AS customer_id, + 2 AS waffle_type_id, + 3 AS quantity, + CAST('2026-04-01 10:00:00' AS TIMESTAMP) AS ordered_at, + 'completed' AS status + UNION ALL + SELECT + 2 AS id, + 101 AS customer_id, + 1 AS waffle_type_id, + 1 AS quantity, + CAST('2026-04-01 11:00:00' AS TIMESTAMP) AS ordered_at, + 'completed' AS status +), +__source__raw__payments AS ( + SELECT + 10 AS id, + 1 AS order_id, + 2850 AS amount_cents, + 'card' AS payment_method, + CAST('2026-04-01 10:05:00' AS TIMESTAMP) AS paid_at, + 'success' AS status + UNION ALL + SELECT + 11 AS id, + 2 AS order_id, + 850 AS amount_cents, + 'cash' AS payment_method, + CAST('2026-04-01 11:10:00' AS TIMESTAMP) AS paid_at, + 'failed' AS status +), +__expected__stg_orders AS ( + SELECT + 1 AS order_id, + 100 AS customer_id, + 2 AS waffle_type_id, + 3 AS quantity, + CAST('2026-04-01 10:00:00' AS TIMESTAMP) AS ordered_at, + 'completed' AS status + UNION ALL + SELECT + 2 AS order_id, + 101 AS customer_id, + 1 AS waffle_type_id, + 1 AS quantity, + CAST('2026-04-01 11:00:00' AS TIMESTAMP) AS ordered_at, + 'completed' AS status +), +__expected__stg_payments AS ( + SELECT + 10 AS payment_id, + 1 AS order_id, + 2850 AS amount_cents, + 'card' AS payment_method, + CAST('2026-04-01 10:05:00' AS TIMESTAMP) AS paid_at, + 'success' AS payment_status + UNION ALL + SELECT + 11 AS payment_id, + 2 AS order_id, + 850 AS amount_cents, + 'cash' AS payment_method, + CAST('2026-04-01 11:10:00' AS TIMESTAMP) AS paid_at, + 'failed' AS payment_status +), +__expected__daily_revenue AS ( + SELECT + CAST('2026-04-01' AS DATE) AS revenue_date, + 1 AS order_count, + 3 AS waffles_sold, + 2850 AS total_revenue_cents, + 28.5 AS total_revenue_dollars, + 2850 AS avg_order_value_cents +), +__assert__daily_revenue_is_non_negative AS ( + SELECT * + FROM __ref("daily_revenue") + WHERE total_revenue_cents < 0 +) +SELECT 1 diff --git a/website/examples/waffle-shop/tests/unit/test_fact_orders.sql b/website/examples/waffle-shop/tests/unit/test_fact_orders.sql new file mode 100644 index 0000000000..864b91fa4e --- /dev/null +++ b/website/examples/waffle-shop/tests/unit/test_fact_orders.sql @@ -0,0 +1,50 @@ +TEST(); + +WITH +__ref__stg_orders AS ( + SELECT + 1 AS order_id, + 100 AS customer_id, + 2 AS waffle_type_id, + 3 AS quantity, + CAST('2026-04-01 10:00:00' AS TIMESTAMP) AS ordered_at, + 'completed' AS status +), +__ref__stg_payments AS ( + SELECT + 10 AS payment_id, + 1 AS order_id, + 2850 AS amount_cents, + 'card' AS payment_method, + CAST('2026-04-01 10:05:00' AS TIMESTAMP) AS paid_at, + 'success' AS payment_status +), +__seed__waffle_types AS ( + SELECT + 2 AS waffle_type_id, + 'Liege' AS waffle_name, + 'sweet' AS category, + 950 AS price_cents +), +__expected__fact_orders AS ( + SELECT + 1 AS order_id, + 100 AS customer_id, + 2 AS waffle_type_id, + 'Liege' AS waffle_name, + 'sweet' AS waffle_category, + 3 AS quantity, + 2850 AS line_total_cents, + CAST('2026-04-01 10:00:00' AS TIMESTAMP) AS ordered_at, + 'completed' AS order_status, + TRUE AS is_completed_order, + 'card' AS payment_method, + 'success' AS payment_status, + 2850 AS payment_amount_cents +), +__assert__line_totals_are_non_negative AS ( + SELECT * + FROM __ref("fact_orders") + WHERE line_total_cents < 0 +) +SELECT 1 diff --git a/website/examples/waffle-shop/tests/unit/test_line_total_cents_macro.sql b/website/examples/waffle-shop/tests/unit/test_line_total_cents_macro.sql new file mode 100644 index 0000000000..5ce379959b --- /dev/null +++ b/website/examples/waffle-shop/tests/unit/test_line_total_cents_macro.sql @@ -0,0 +1,14 @@ +TEST (mode macro, name "calculates_line_total_cents"); + +WITH +input_values AS ( + SELECT 950 AS price_cents, 3 AS quantity +), +__macro_actual__ AS ( + SELECT @line_total_cents("price_cents", "quantity") AS line_total_cents + FROM input_values +), +__macro_expected__ AS ( + SELECT 2850 AS line_total_cents +) +SELECT 1 diff --git a/website/examples/waffle-shop/tests/unit/test_stg_orders.sql b/website/examples/waffle-shop/tests/unit/test_stg_orders.sql new file mode 100644 index 0000000000..8e6dd20e97 --- /dev/null +++ b/website/examples/waffle-shop/tests/unit/test_stg_orders.sql @@ -0,0 +1,27 @@ +TEST(); + +WITH +__source__raw__orders AS ( + SELECT + 1 AS id, + 100 AS customer_id, + 2 AS waffle_type_id, + 3 AS quantity, + CAST('2026-04-01 10:00:00' AS TIMESTAMP) AS ordered_at, + 'completed' AS status +), +__expected__stg_orders AS ( + SELECT + 1 AS order_id, + 100 AS customer_id, + 2 AS waffle_type_id, + 3 AS quantity, + CAST('2026-04-01 10:00:00' AS TIMESTAMP) AS ordered_at, + 'completed' AS status +), +__assert__order_ids_are_not_null AS ( + SELECT * + FROM __ref("stg_orders") + WHERE order_id IS NULL +) +SELECT 1 diff --git a/website/package-lock.json b/website/package-lock.json new file mode 100644 index 0000000000..4722504fba --- /dev/null +++ b/website/package-lock.json @@ -0,0 +1,6062 @@ +{ + "name": "sqlbuild-docs-site", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "sqlbuild-docs-site", + "dependencies": { + "@astrojs/starlight": "^0.42.4", + "@lucide/astro": "^1.48.0", + "astro": "^7.3.5", + "sharp": "^0.35.4", + "starlight-llms-txt": "^0.12.0" + }, + "engines": { + "node": ">=22.12.0" + } + }, + "node_modules/@astrojs/compiler-binding": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/@astrojs/compiler-binding/-/compiler-binding-0.5.0.tgz", + "integrity": "sha512-PxFTx3WoxQnbgUvvwmF6LRQwhHO77a4pewOtJvzxlrYtQ90tLgomLKvks/akrd2iix2ngGlOhxQonKNd8YoBHg==", + "license": "MIT", + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "optionalDependencies": { + "@astrojs/compiler-binding-android-arm64": "0.5.0", + "@astrojs/compiler-binding-darwin-arm64": "0.5.0", + "@astrojs/compiler-binding-darwin-x64": "0.5.0", + "@astrojs/compiler-binding-linux-arm64-gnu": "0.5.0", + "@astrojs/compiler-binding-linux-arm64-musl": "0.5.0", + "@astrojs/compiler-binding-linux-x64-gnu": "0.5.0", + "@astrojs/compiler-binding-linux-x64-musl": "0.5.0", + "@astrojs/compiler-binding-wasm32-wasi": "0.5.0", + "@astrojs/compiler-binding-win32-arm64-msvc": "0.5.0", + "@astrojs/compiler-binding-win32-x64-msvc": "0.5.0" + } + }, + "node_modules/@astrojs/compiler-binding-android-arm64": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/@astrojs/compiler-binding-android-arm64/-/compiler-binding-android-arm64-0.5.0.tgz", + "integrity": "sha512-wVviC/1KaBqGVF0DbTZ8UtAE3c8TN84CCe7w4ju3SGzo/ByWKn8HCqSU/+1wdltGCCSa30pjVk3H8DQUgnfhRQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@astrojs/compiler-binding-darwin-arm64": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/@astrojs/compiler-binding-darwin-arm64/-/compiler-binding-darwin-arm64-0.5.0.tgz", + "integrity": "sha512-5NNIkaGeiztD9QQfhOy+nNrBCZVeHqVZPlfJVjTKq4yJiiSsxAhuBzmVlmK3BDhbSgUT8dai4JV5m6t4/BIQaw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@astrojs/compiler-binding-darwin-x64": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/@astrojs/compiler-binding-darwin-x64/-/compiler-binding-darwin-x64-0.5.0.tgz", + "integrity": "sha512-HU/waaccOQDXpvjoWA/9xzadNZIcWKblJoaw19lCxfVs6AHQYdZZOzZUQjY8HaLYGerTXjdADbra2XU+kXk9Og==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@astrojs/compiler-binding-linux-arm64-gnu": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/@astrojs/compiler-binding-linux-arm64-gnu/-/compiler-binding-linux-arm64-gnu-0.5.0.tgz", + "integrity": "sha512-a21petBJRi0mNBf+gkRZFoWdGH+VOo0zRQSzM4Nuez5RL5rFFkmbondMejkaRZzvE3LVYU2NVD5MGRgAs4V+nQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@astrojs/compiler-binding-linux-arm64-musl": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/@astrojs/compiler-binding-linux-arm64-musl/-/compiler-binding-linux-arm64-musl-0.5.0.tgz", + "integrity": "sha512-t5rmu23b/6e6v0bhPibCtwPdABXMRL+bUaJXdkJwTpkINp4zCgH6fNa5Z6+RgkgFSCpmg9uf1MtRIqx1gdmBaA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@astrojs/compiler-binding-linux-x64-gnu": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/@astrojs/compiler-binding-linux-x64-gnu/-/compiler-binding-linux-x64-gnu-0.5.0.tgz", + "integrity": "sha512-JWtouuxSiDb2OKKFsxOwfqlpJuZHo1ExirlBGfDgoJVG5zeJWwcd2tU2w4UaJA4suT/tAFSLk/vFhM5fLjWV4A==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@astrojs/compiler-binding-linux-x64-musl": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/@astrojs/compiler-binding-linux-x64-musl/-/compiler-binding-linux-x64-musl-0.5.0.tgz", + "integrity": "sha512-LlaSO5v3UC6IxuTKXDViWV/fFHYhLKSO9Of118lNBXv/sjIWwziU+jjENxPgxfq/4oUeOu+613y6H93LBPNTOA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@astrojs/compiler-binding-wasm32-wasi": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/@astrojs/compiler-binding-wasm32-wasi/-/compiler-binding-wasm32-wasi-0.5.0.tgz", + "integrity": "sha512-IP+Cp0m9yKg/AUT47077vHEaAOh/oT/iUZx1PDnV2GJzUxLSEotdQy5Hhv+ycvEadO/DbETSJfAWeV7kte4etQ==", + "cpu": [ + "wasm32" + ], + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "1.11.3", + "@emnapi/runtime": "1.11.3", + "@napi-rs/wasm-runtime": "^1.2.4" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@astrojs/compiler-binding-win32-arm64-msvc": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/@astrojs/compiler-binding-win32-arm64-msvc/-/compiler-binding-win32-arm64-msvc-0.5.0.tgz", + "integrity": "sha512-v6plwy5GQBGvfv4QgiGhy/qkkQvYKW7Zh2vBArOnsGEDNHyUS43CWYwIQdw4ZdVVmLsFyhwTiueDZEP6zejddQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@astrojs/compiler-binding-win32-x64-msvc": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/@astrojs/compiler-binding-win32-x64-msvc/-/compiler-binding-win32-x64-msvc-0.5.0.tgz", + "integrity": "sha512-SgM158cSCJ248UIXaCCW2WqSTjOoFlDv93ed/ATguJqYpZNNVMlOdcPlgNH7WYj/OYeD0rIQr7XyRAYhB7+G1A==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@astrojs/compiler-rs": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/@astrojs/compiler-rs/-/compiler-rs-0.5.0.tgz", + "integrity": "sha512-2xIypn5R33FRWCXXXv0sfbDj8xcOvc2VmIbteytxMjOzP9LiGfvqaAFJjhdAjzhs6+ZKzYzbSE79rrPObYW5mg==", + "license": "MIT", + "dependencies": { + "@astrojs/compiler-binding": "0.5.0" + }, + "engines": { + "node": ">=22.12.0" + } + }, + "node_modules/@astrojs/internal-helpers": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@astrojs/internal-helpers/-/internal-helpers-0.11.0.tgz", + "integrity": "sha512-3rzxJ+xbo0+8YyqOzLziIN32wmsHdCjEVz2sGOpRxJ+Ben/KiLph4ItxBy1abEL+E8fkRzqjg0rfXmaHJGw9JA==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.4", + "@types/mdast": "^4.0.4", + "js-yaml": "^4.3.0", + "picomatch": "^4.0.4", + "retext-smartypants": "^6.2.0", + "shiki": "^4.0.2", + "smol-toml": "^1.6.0", + "unified": "^11.0.5" + } + }, + "node_modules/@astrojs/markdown-satteri": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/@astrojs/markdown-satteri/-/markdown-satteri-0.4.2.tgz", + "integrity": "sha512-g4QehXnB/MzxQWnSf17NciFInMqEJy9948p8cc7zGg/r7995m0Ln5PMr3Xlnj24iey1LchaoZ3SzTOa3D4P/3w==", + "license": "MIT", + "dependencies": { + "@astrojs/internal-helpers": "0.11.0", + "@astrojs/prism": "4.0.2", + "github-slugger": "^2.0.0", + "satteri": "^0.10.3" + } + }, + "node_modules/@astrojs/mdx": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@astrojs/mdx/-/mdx-8.0.2.tgz", + "integrity": "sha512-WedIVP2Bu8fXSzxb3ZYZ8xDrWWjf2SRKCneh52Llhoa+O3lcpHPhoZVP3FerlivUaH/0LJz2yVaCNvMdg0u8Bw==", + "license": "MIT", + "dependencies": { + "@astrojs/internal-helpers": "0.11.0", + "@astrojs/markdown-satteri": "0.4.2", + "es-module-lexer": "^2.0.0" + }, + "engines": { + "node": ">=22.12.0" + }, + "peerDependencies": { + "@astrojs/markdown-remark": "^7.3.0", + "@astrojs/markdown-satteri": "^0.4.0", + "astro": "^7.2.10" + }, + "peerDependenciesMeta": { + "@astrojs/markdown-remark": { + "optional": true + } + } + }, + "node_modules/@astrojs/prism": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@astrojs/prism/-/prism-4.0.2.tgz", + "integrity": "sha512-KTivpmnz6lDsC6o9H4+DNm2SrE/GHzw8cNAvEJwAvUT+eoaEnn/4NtbDNfRRaxaJHdp15gf+tfHAWiXR4wB3BA==", + "license": "MIT", + "dependencies": { + "prismjs": "^1.30.0" + }, + "engines": { + "node": ">=22.12.0" + } + }, + "node_modules/@astrojs/sitemap": { + "version": "3.7.4", + "resolved": "https://registry.npmjs.org/@astrojs/sitemap/-/sitemap-3.7.4.tgz", + "integrity": "sha512-LbKNC24bdUWcQf/pThB6qLlSqHojxGjZDURIzFocY8rlWnAn2t74nnhnK6S5x0NHriHoAduLEpVjRykmeGiVvA==", + "license": "MIT", + "dependencies": { + "sitemap": "^9.0.0", + "zod": "^4.3.6" + } + }, + "node_modules/@astrojs/starlight": { + "version": "0.42.4", + "resolved": "https://registry.npmjs.org/@astrojs/starlight/-/starlight-0.42.4.tgz", + "integrity": "sha512-EsQsqGJ1uTBQcIMojvLqqg4Ku9idwi6PSNuppIOoYmvVP18xjoUkijMdKoDkhmmwKdhbB4ctLhGjPRJzxaq+Jw==", + "license": "MIT", + "peer": true, + "dependencies": { + "@astrojs/markdown-satteri": "^0.4.0", + "@astrojs/mdx": "^8.0.0", + "@astrojs/sitemap": "^3.7.3", + "@pagefind/default-ui": "^1.3.0", + "@types/hast": "^3.0.5", + "@types/js-yaml": "^4.0.9", + "@types/mdast": "^4.0.4", + "astro-expressive-code": "^0.44.0", + "bcp-47": "^2.1.0", + "hast-util-format": "^1.1.0", + "hast-util-select": "^6.0.4", + "hast-util-to-html": "^9.0.5", + "hast-util-to-string": "^3.0.1", + "hastscript": "^9.0.1", + "i18next": "^26.0.7", + "js-yaml": "^4.1.1", + "klona": "^2.0.6", + "magic-string": "^1.2.3", + "mdast-util-directive": "^3.1.0", + "mdast-util-to-markdown": "^2.1.2", + "mdast-util-to-string": "^4.0.0", + "pagefind": "^1.5.2", + "remark-directive": "^4.0.0", + "satteri": "^0.10.3", + "ultrahtml": "^1.6.0", + "unified": "^11.0.5", + "unist-util-visit": "^5.1.0", + "vfile": "^6.0.3" + }, + "peerDependencies": { + "@astrojs/markdown-remark": "^7.3.0", + "astro": "^7.2.10" + }, + "peerDependenciesMeta": { + "@astrojs/markdown-remark": { + "optional": true + } + } + }, + "node_modules/@astrojs/telemetry": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/@astrojs/telemetry/-/telemetry-3.3.3.tgz", + "integrity": "sha512-C1TLn5sPJr0x4vk56piHWKbnqlEB8BKyte5Y45V02U+D7BGO5eMqZDH5aPjnkXQWJggvmsTXxH03QMZ9NgWLzQ==", + "license": "MIT", + "dependencies": { + "ci-info": "^4.4.0", + "dset": "^3.1.4", + "is-docker": "^4.0.0", + "package-manager-detector": "^1.6.0" + }, + "engines": { + "node": "18.20.8 || ^20.3.0 || >=22.0.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz", + "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", + "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.9", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.9.tgz", + "integrity": "sha512-CjXrNHTnvqBVqHgdBysY3vk2T8tpJHb5/RMeHJBTyVa9xgugCB0CJTx/3oO8RV2QRQP391RWpB7D6hLjm8V9uA==", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.8" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.8.tgz", + "integrity": "sha512-Vj1jF3cPfxg7OAfoI7QnVKLoILlm2JF9pnVHrX8qx7AHMiYWT+NDAA7jChlNgRS4WTLc/fD1lXLmPixluj+3Gg==", + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@bruits/satteri-darwin-arm64": { + "version": "0.10.5", + "resolved": "https://registry.npmjs.org/@bruits/satteri-darwin-arm64/-/satteri-darwin-arm64-0.10.5.tgz", + "integrity": "sha512-27KTVl4TJkVahMy/ohyA7qd4938G5UNneFUz/PsScYfpIhj0IVAS23mpcJXdPF44sa6nva198lmV/cKIb2YPyA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@bruits/satteri-darwin-x64": { + "version": "0.10.5", + "resolved": "https://registry.npmjs.org/@bruits/satteri-darwin-x64/-/satteri-darwin-x64-0.10.5.tgz", + "integrity": "sha512-IjnLe3nKspq6qaeqGgjT7MT8VrTV74yWRlaag7ZdNsI8TDAYZ0iPxMCo+9KQZHUk5EyVB+reBI/PFWL5KuFw9Q==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@bruits/satteri-linux-arm64-gnu": { + "version": "0.10.5", + "resolved": "https://registry.npmjs.org/@bruits/satteri-linux-arm64-gnu/-/satteri-linux-arm64-gnu-0.10.5.tgz", + "integrity": "sha512-glkYXZCJywjP13v67eAyAMSJdF+ncvEbYvgi/wOtffL9tQ27lr/zsyzUfgs+ovjJ9d8JNQKiXeiArJcX8PJL9w==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@bruits/satteri-linux-arm64-musl": { + "version": "0.10.5", + "resolved": "https://registry.npmjs.org/@bruits/satteri-linux-arm64-musl/-/satteri-linux-arm64-musl-0.10.5.tgz", + "integrity": "sha512-yWdgG1g17Nh2QyGVlFUxGRa3FEFwiMcpZEyMNWkbM3deC94cmVc+/i9OuyFpdKuWo3GkgoCtYVOoxk1uCnCZIA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@bruits/satteri-linux-x64-gnu": { + "version": "0.10.5", + "resolved": "https://registry.npmjs.org/@bruits/satteri-linux-x64-gnu/-/satteri-linux-x64-gnu-0.10.5.tgz", + "integrity": "sha512-FVaLoPT1fBgGl0J+AYebyyXJYBachGl8Oyyrf1lye4RTqCB4S0Gwkj1uM9RJyThUOvx5VUmAT1CnNh1SFHA+kw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@bruits/satteri-linux-x64-musl": { + "version": "0.10.5", + "resolved": "https://registry.npmjs.org/@bruits/satteri-linux-x64-musl/-/satteri-linux-x64-musl-0.10.5.tgz", + "integrity": "sha512-EHpVAx2bqW3GINHTKkljtxVfQmVDGWIuwOYOP5YghTj+0PkBa2o8oKPRtQ9Kbsr1Fye8jtUcDjhwj2jMNugZKg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@bruits/satteri-wasm32-wasi": { + "version": "0.10.5", + "resolved": "https://registry.npmjs.org/@bruits/satteri-wasm32-wasi/-/satteri-wasm32-wasi-0.10.5.tgz", + "integrity": "sha512-ypz8c/Zmipxp4IoeDa228Gstv6TLzVmNs3yC6wKCoNSOjx1iwpgzu87Y3hTkXFdwChVGU85qeUDuOIarGUZQLw==", + "cpu": [ + "wasm32" + ], + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "1.11.1", + "@emnapi/runtime": "1.11.1", + "@napi-rs/wasm-runtime": "^1.2.3" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@bruits/satteri-wasm32-wasi/node_modules/@emnapi/core": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.11.1.tgz", + "integrity": "sha512-RSvbQmHzdKzNsLYa/wHrbc3KN4sYLKAdPZxqiM2HATqv/SBk2/ENSHpvXGaLOMcsAyz0poEGqkmmKYG3OWiJEQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/wasi-threads": "1.2.2", + "tslib": "^2.4.0" + } + }, + "node_modules/@bruits/satteri-wasm32-wasi/node_modules/@emnapi/runtime": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.1.tgz", + "integrity": "sha512-vgj7R3y3Wgx24IQaGPA/R6YFXLHVMOZ0uVEyIQPaWs+rd1AzfEMXlAC22FYwO1XkKR6NPsq7mUandH8oIRdZFw==", + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@bruits/satteri-wasm32-wasi/node_modules/@emnapi/wasi-threads": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.2.tgz", + "integrity": "sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA==", + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@bruits/satteri-win32-arm64-msvc": { + "version": "0.10.5", + "resolved": "https://registry.npmjs.org/@bruits/satteri-win32-arm64-msvc/-/satteri-win32-arm64-msvc-0.10.5.tgz", + "integrity": "sha512-siTV88nb0LRqNpkL2gXboqCwVdq95sLtzMHS1/3eONV2gLbB3NAK46wmSMvCO/yquBvI2lvaFIfd8P12ecsxBw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@bruits/satteri-win32-x64-msvc": { + "version": "0.10.5", + "resolved": "https://registry.npmjs.org/@bruits/satteri-win32-x64-msvc/-/satteri-win32-x64-msvc-0.10.5.tgz", + "integrity": "sha512-C3IfPvfvMXmlzBxaMPKFS1XiuV9pu2mC7YqkPk7PSvTgPZ8gbdASIpHpztDLvTTQjqZ0z1Ol8tK5X+V6XXC0wQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@capsizecss/unpack": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@capsizecss/unpack/-/unpack-4.0.1.tgz", + "integrity": "sha512-CuNiSqg7+e1cO/GjffyMOm5Tt2jUF9CWHHnvQ/UkqvtkGfHdgwEC0wpmq7fkN3gxwpRnrAN0WzO3vREKmNolMQ==", + "license": "MIT", + "dependencies": { + "fontkitten": "^1.0.3" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@clack/core": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/@clack/core/-/core-1.5.1.tgz", + "integrity": "sha512-iHTrHA8MtVuLl2TfZySmcKv1qO2PoyC9Z7pfSDozEuV5vtY3/wcOPKJXlqJ5Oq2Cx5DDGQGAMVx6HZfRRoVEbQ==", + "license": "MIT", + "dependencies": { + "fast-wrap-ansi": "^0.2.0", + "sisteransi": "^1.0.5" + }, + "engines": { + "node": ">= 20.12.0" + } + }, + "node_modules/@clack/prompts": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/@clack/prompts/-/prompts-1.8.1.tgz", + "integrity": "sha512-dlT1m5e/0yUL0kRNcQn7yGLVThkgbB0Ga/1AmfDDC/8ik6AIiSf2QLQO2zPYvefsHP0aFgxO93cVLCCfDp7kzQ==", + "license": "MIT", + "dependencies": { + "@clack/core": "1.5.1", + "fast-string-width": "^3.0.2", + "fast-wrap-ansi": "^0.2.0", + "sisteransi": "^1.0.5" + }, + "engines": { + "node": ">= 20.12.0" + } + }, + "node_modules/@ctrl/tinycolor": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@ctrl/tinycolor/-/tinycolor-4.2.1.tgz", + "integrity": "sha512-Vh5uy9Y1JBMId6J1f68TMXnZrTCvNdo9Fu2k0d1YnJngR+sM5kWTKl441c/3TFs+cG0NWOjRWgQ/iaWhbYx+7A==", + "license": "MIT", + "engines": { + "node": ">=14" + } + }, + "node_modules/@emnapi/wasi-threads": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.3.tgz", + "integrity": "sha512-ELEBe8PsLvvJ6QMr0zLt8ffvOHW/dc1m3CEzNMg7aJUv3bMaoDtw2TXyDAwkYBuroxxuHEwhRTLJSe5sya547g==", + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.2.tgz", + "integrity": "sha512-XExcO+dvLKvVtNTibSTBej1NCAbaGhWn9Ww1ZPx80qsahhPFe/8jgWP0IchNe0F3HwkU7n8ejhH8bjonqht8mQ==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.2.tgz", + "integrity": "sha512-kXXoiPVVGQcnIYGOeaovwOURpniDBpSq4A03qkQ+BMQqtGG6HYap3xne9C1O1yo4TR3qxlCX5IqqmX6fFo2Lqg==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.2.tgz", + "integrity": "sha512-5YfKeeI8qWfBZIX+u2xZC3Zlb3Os/gLS2sbEKM+I4ZOcsWmHS2WLysCcQZDAFRslDUU5Oiq44gf6PYN1vGwG5A==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.2.tgz", + "integrity": "sha512-O387ite7SzUyCcy3JQX4P4bLtEA7bLLkx+esve5JHnyYfNTxcVpXZo9jhdB0lTKN44gztELTdU7nS8Nr16Fs1Q==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.2.tgz", + "integrity": "sha512-n4KqkOQrraxHJcgjM1RvwbigfQKIKJVpM7xp+KsxiyUSrRdIXnt73VhrPAx0fV44hgfmIVKjxMN9J1t5jySVkw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.2.tgz", + "integrity": "sha512-uq6suIWYP37qzGddBKPw5QEQPi6HiLGsO7UmkpfyaYNQ3D+rN6w6WfwH+nuqcGXWvawGwxOEroO4YGnFh95azw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.2.tgz", + "integrity": "sha512-n+I0BTSRIoy+d6RPKnEVwql5UwBJolytvY4mAOIEJorKlqgPII8ix6slVVrfZ5Tnj7glIZvloylbB/EJPMWEXw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.2.tgz", + "integrity": "sha512-78XJTJkvPs0kz2w61301PJjXl4g7q3JqiYMZ/M/yVI73EHBrCRTgkhu9oqG7vPqq+a/yadEW8aD+agKlk5xrmg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.2.tgz", + "integrity": "sha512-XlDnu2q5yoqems+xay6wSAcg9DDD7K9RLKZEBOMZm3ckNpJBvOX20tSfby8KfrrhINDyv9V2YVZKY/SpoGJI8w==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.2.tgz", + "integrity": "sha512-pW4AC0P3it8c7do9MVM4p51FzHzdM/TZrerurgRcHJ2WTa1VQ1CIq18xncfpBJw4ojkiZZrKW2yIBWBP92j6Ug==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.2.tgz", + "integrity": "sha512-CYbnj78HsIeA+DhgUKgFCfvNsTHFhMMrinUrMZpDXJXKN8T3XViTZ/+wtHeVxEWY8ewSzTFN+nRmSwO2tZaLUQ==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.2.tgz", + "integrity": "sha512-buwkd8nsph4R+ajRvw0qM5Hja/TXQow3ptzWO2EbG/cqcIkHloRrdlBtQlshyYGTNFvfkfJ5tpPLVkY4DtsPfQ==", + "cpu": [ + "loong64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.2.tgz", + "integrity": "sha512-ZVykbDyk7519VwiNb9Lcj9m8XM6v5V9uKPvrEMkkEedVewf+0itkhahp4HDpgERXhwLRpWFypsGbG/J8s0QjJA==", + "cpu": [ + "mips64el" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.2.tgz", + "integrity": "sha512-CAXl+Dtd9UUuJd8pKKdwh6MLm3MUMiqMPmhZ3tTSXPqfyQ3vDl6R5hZdZ/kYojK4ofXtdfSv1tFq8XzWx3heNQ==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.2.tgz", + "integrity": "sha512-GeXCej4IQtU1B+QlDV8W/RRvbzI3O/Stss+/bCXv4lZls5WGRtu2a+3JkA3i4qIUlMXpcHebWpF8AkJhATowuA==", + "cpu": [ + "riscv64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.2.tgz", + "integrity": "sha512-3H1weTYZPxt/WOhByszQZybS9w5lKzUn1FDMsgEChbHWQwHYQQRfBxgCcZvPhjHfKyJjIievvMmEUawJrdY9Dg==", + "cpu": [ + "s390x" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.2.tgz", + "integrity": "sha512-4xTZr1FUmSoQW4XIWmit3tzQrUTZM+N3P0XV8xROKYF50XfI7xeO90+1bZvNwxIufQ9hDQVRJH5YhgPVF8A/HQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.2.tgz", + "integrity": "sha512-sSATRjPeDBg3pdgHoQfoYBob11Kk1FGa9lui5RIHZCoCkJa9QKlvl3/vKz2usCmYYjs7ymJR/2Nnsqe+Hjt5nw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.2.tgz", + "integrity": "sha512-lqnzCV+mM0gIADaKihiCg6ifgfU2L3h5E33rNQBN1Y4MaVGnzryzmvvf7UHxprpQdE8hpqLolJ9Rl+SkIRDpyw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.2.tgz", + "integrity": "sha512-AL2qJILH7lNjrDmCQDvdxMfAUIv8KMNZOvrwAQ8i8//ntL9FflhOyMJ8OZSMBb8/AWXe3/5v5S20y3zCoZWKoQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.2.tgz", + "integrity": "sha512-QtiuPytchRyC4rwUKhexJdQKvDuZ6hWloi3igqPQNUJCS1/v9EiO3UTOXR6A3FoMo4fnAKbWJdqaIwhOzh8qEw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.2.tgz", + "integrity": "sha512-WkhYDmpTjLvGlScA1rwjRUmhl4k8oXR3cIbtqWmELgU/dFeHHlEllxDvdWcNJV9rbzCexB5vz8gtNewWLgCT7Q==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.2.tgz", + "integrity": "sha512-GPMSkTOtMnv2U2F8gxe4Io6qmVs+YKyp832Etqqxr0hFngmXQ3rzwytelm3GIn7T4VviRUlf3sOgBOiTdvaf7g==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.2.tgz", + "integrity": "sha512-PIhhEkE9uPBleRBrQEJpUn7MBnibZzbGzYWPmY3x+YoVg/95zbjB4CxPPOQ8l5tYYM4mMaCthF8/1DIfBQQyWQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.2.tgz", + "integrity": "sha512-YmJbfTlvU7Sdn9BB+4PRES4oB6pxgS37MAONj+hBr/cpXS1aBPKXxNnDbu+QCWPj0o9dgyxeq79g6c5P8KeuYA==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.2.tgz", + "integrity": "sha512-5ebpxr3nWMzrL/rnUI755Jkuee0bHL/Gq0WTF9lvcpv73wAp5eu8MfBUgWK9bhWvZjj7yX8etf/8tI8Ney695g==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@expressive-code/core": { + "version": "0.44.2", + "resolved": "https://registry.npmjs.org/@expressive-code/core/-/core-0.44.2.tgz", + "integrity": "sha512-Yw5KonQCD1HoOO2P89y4wifqS9qrJcGVY0LkVMUsVZjhi++bhz4WwQ37inenKFNOEVo50P7cjoVsCdaVnUbq8w==", + "license": "MIT", + "dependencies": { + "@ctrl/tinycolor": "^4.0.4", + "hast-util-select": "^6.0.2", + "hast-util-to-html": "^9.0.1", + "hast-util-to-text": "^4.0.1", + "hastscript": "^9.0.0", + "postcss": "^8.4.38", + "postcss-nested": "^6.0.1", + "unist-util-visit": "^5.0.0", + "unist-util-visit-parents": "^6.0.1" + } + }, + "node_modules/@expressive-code/plugin-frames": { + "version": "0.44.2", + "resolved": "https://registry.npmjs.org/@expressive-code/plugin-frames/-/plugin-frames-0.44.2.tgz", + "integrity": "sha512-gVLm6TQMNIRWIaM7o+mCtxkGNjLZY5/2faSTLoXeo9wv/Pkj/0QAtZyXPVPrRsPARqiipATNFmfvQFLJudxPHQ==", + "license": "MIT", + "dependencies": { + "@expressive-code/core": "^0.44.2" + } + }, + "node_modules/@expressive-code/plugin-shiki": { + "version": "0.44.2", + "resolved": "https://registry.npmjs.org/@expressive-code/plugin-shiki/-/plugin-shiki-0.44.2.tgz", + "integrity": "sha512-ebWIvCDzXvjyD8rH6T6DfytYJw1GoArLR4nusIAE4VxXrCQf3d+owytiv010KU/dUg+SRLZskDn0N+HaFXeuSg==", + "license": "MIT", + "dependencies": { + "@expressive-code/core": "^0.44.2", + "shiki": "^4.0.2" + } + }, + "node_modules/@expressive-code/plugin-text-markers": { + "version": "0.44.2", + "resolved": "https://registry.npmjs.org/@expressive-code/plugin-text-markers/-/plugin-text-markers-0.44.2.tgz", + "integrity": "sha512-SNI7EgUSiADqbqUzzMbj1ZeC/r2pa5eg3KyFOGAd3a7EsIA4qzjXQLp+ODWQtHVDKFCdGs4J3ovSJUqvKIK96g==", + "license": "MIT", + "dependencies": { + "@expressive-code/core": "^0.44.2" + } + }, + "node_modules/@img/colour": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@img/colour/-/colour-1.1.0.tgz", + "integrity": "sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@img/sharp-darwin-arm64": { + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.35.4.tgz", + "integrity": "sha512-Uhfl4V4lhP2nbUVF9+hyH1+luj86f1gUFeo8ALYxFoULoU+G87D43BfeMP8XHsk9boxAnCY/bf2EHwhA7MuGsA==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-arm64": "1.3.3" + } + }, + "node_modules/@img/sharp-darwin-x64": { + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.35.4.tgz", + "integrity": "sha512-hWniXY3bG5qKpkKrAwPe4y+VTPmf086YQAnkxWh7uA1YrlRouWGa0M0Mxj3ZjnXFkv7/TD1bTy9lGUK26vRvWw==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-x64": "1.3.3" + } + }, + "node_modules/@img/sharp-freebsd-wasm32": { + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-freebsd-wasm32/-/sharp-freebsd-wasm32-0.35.4.tgz", + "integrity": "sha512-lIsKw/BU+kjB4eZjxrYrZmwOJYi3Ajrv66iAlBmUPyKc3HpnloevB1g3wxGD9P/5BbQ1brBGl65VRRrCvQDEqA==", + "license": "Apache-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "dependencies": { + "@img/sharp-wasm32": "0.35.4" + }, + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-darwin-arm64": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.3.3.tgz", + "integrity": "sha512-suTBPTDGrI9WodccaDdwZItTSaBYASlBk1NSfElSHrUfzu3szG6lvIF58+WiFvnfzuK8ZBFS5zE00PxqxnRiPg==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-darwin-x64": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.3.3.tgz", + "integrity": "sha512-FVJZ5mITMobmXIz/hPDTw0EintTW5H3WfrxwLqEqjiIihlu+hVRyGrFQ60xl0Lxn7Bt3zdpevPaQi0HEzqz9fw==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.3.3.tgz", + "integrity": "sha512-3rbU4vqXXc3hY/OiXdl52xZvT0F1yEngWfvqudtPJg/KkyiaQw2DRsFrNzpmLvfavbwOq3qXn36GP8obHRULQA==", + "cpu": [ + "arm" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm64": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.3.3.tgz", + "integrity": "sha512-0DaL0A6Xu6sQSQFwe4iVCrKWU2cCTItnRsYsCdxAMm9NF6twAA9BKnoqy4hqz4+azQ0JHuA26qiUKsf1XJ/v5A==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-ppc64": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.3.3.tgz", + "integrity": "sha512-cdn1OvUBwsXhbC0zSzJnNzf5MZ/mTrobawDvNXBTxe8VtqKAm0sRuEY2Evzovb/w9JMk4TvRxqt1mekSuJz64w==", + "cpu": [ + "ppc64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-riscv64": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.3.3.tgz", + "integrity": "sha512-HjPVx7yKz+0lqdhDlTw1tt90wamBoxhiXpvl1XZpJLiHH4RCJ5yDTqH+VlYPv2fwFs89JFw4c1IexYOcQUi4IQ==", + "cpu": [ + "riscv64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-s390x": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.3.3.tgz", + "integrity": "sha512-neWLh+3yCNThxnfy3c4BbVBeGgt9aftno+XbT56iK28RgeDs3UOFWviLWlUu0bArYVYJaFDK+RRohbicUNCm8Q==", + "cpu": [ + "s390x" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-x64": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.3.3.tgz", + "integrity": "sha512-4vKmvAst9nrowcqquKFAyZJUDolUaIp8uRiN0mWFguJ1IplC9/pitXtlnnlU4aa/eJw3J7i67V+pwUL+wZGdsA==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-arm64": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.3.3.tgz", + "integrity": "sha512-Y9kQaLMuNoB0bPYOOdcZMaseNrFpPodIWWMrx+CZyydf2xn68j9WYc6sWWRrDwNkzCQjKYfc68L7jKjGlHMibw==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-x64": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.3.3.tgz", + "integrity": "sha512-fj8Mv0HHfD1Rr+4I68+3agJynxDWtBFgicTbSOb9Bke6pIwzGcJ+RX/yHjmiEGFMCavY/dxvem7MyNaJF+wDiw==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-linux-arm": { + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.35.4.tgz", + "integrity": "sha512-7OAS8gI0EReKGVN2HssHlM6umJgxF5VI3xN0p9FA91p/YO+ou5hiNghLdZ5BEHztwaaK5+bLKRf8x/o2L2nk9A==", + "cpu": [ + "arm" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm": "1.3.3" + } + }, + "node_modules/@img/sharp-linux-arm64": { + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.35.4.tgz", + "integrity": "sha512-De4jpEnAU8Hd5oT0j1G3uL4ZvTuipVMn7YC6vPaJhy6/7EwEae0SVAoBrUMYQbkLGDm85taVWwuPc1a44LTzCQ==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm64": "1.3.3" + } + }, + "node_modules/@img/sharp-linux-ppc64": { + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.35.4.tgz", + "integrity": "sha512-2oYZJeIl4kCcMGk4ouZVjnkCtFrpQFlNEtJ6GbxzhHQchwH0NH/qEb9ykmOl29dqwMq+JhFdZn+1ak2FKhI9fQ==", + "cpu": [ + "ppc64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-ppc64": "1.3.3" + } + }, + "node_modules/@img/sharp-linux-riscv64": { + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.35.4.tgz", + "integrity": "sha512-cPbNChoRURAWdebDIHSenxRpgEdy7JkPydSnUxRm9VvKD7m0/xVaR/8Fzlu81pk5nHEvHH87UZUA7cTtwnbJSA==", + "cpu": [ + "riscv64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-riscv64": "1.3.3" + } + }, + "node_modules/@img/sharp-linux-s390x": { + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.35.4.tgz", + "integrity": "sha512-RY0JFY8Fd6RonCBtHz+DvadaPkXDSI1AUn6yWL9TipqkZ1vY8w8evqdgyDFnkm4/K1ve1TvZiaePP5oSd4+WVQ==", + "cpu": [ + "s390x" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-s390x": "1.3.3" + } + }, + "node_modules/@img/sharp-linux-x64": { + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.35.4.tgz", + "integrity": "sha512-9qvvEAuk8k89TfWUoX2htWjbAMX8p+NxCppjpcg5k6xMsjhBQPTsoIh36h9Qde4WRuGpJeYnOjdosDn/cnv+OA==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-x64": "1.3.3" + } + }, + "node_modules/@img/sharp-linuxmusl-arm64": { + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.35.4.tgz", + "integrity": "sha512-KB5jxpfWQTr0nc3xdHtWChdbifHrBGsd2SM62Eyxrl8afikm+f5qGBU75SJIZBT/S1MC8XyacdlXBMSWq6OURA==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-arm64": "1.3.3" + } + }, + "node_modules/@img/sharp-linuxmusl-x64": { + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.35.4.tgz", + "integrity": "sha512-f+eZJZIQNEEd26RPSW+76chwOf1XtA2Y/O+5ocVyLliHkeih3e+jhLVBdNTd2rS3IbNXK8+ug93Vf5ZXtF5Lxg==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-x64": "1.3.3" + } + }, + "node_modules/@img/sharp-wasm32": { + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.35.4.tgz", + "integrity": "sha512-zQnl4Kwp7Q6NHsENtU2T/00Zi+w3AQNwz3+UaTyVBy2FpXrzXzGjndpK61onhZjRtRpQXxCTeqw19bVyXOh7jA==", + "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", + "optional": true, + "dependencies": { + "@emnapi/runtime": "^1.11.3" + }, + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-webcontainers-wasm32": { + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-webcontainers-wasm32/-/sharp-webcontainers-wasm32-0.35.4.tgz", + "integrity": "sha512-ESfNkywmCfPNyaZjxooddJQiQ+l/nTpGEOGthxiLnIHXC/CmcBixnfwUleX9mCz9ovrUUvKMap/pm8RYbzfwaA==", + "cpu": [ + "wasm32" + ], + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "@img/sharp-wasm32": "0.35.4" + }, + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-arm64": { + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.35.4.tgz", + "integrity": "sha512-iNdlBX9gLVvqe2I3uIJSIKTq6wckP/DYxZtcqxm09x5Gi24DnFBmPAWZmr60ZyYMG0xlzo6goG3670ar+RXvRw==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-ia32": { + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.35.4.tgz", + "integrity": "sha512-kqRsbaa5CS6KHlpxnN7WhE6vAAugXyZButpRdvDWetlv6Qv4N9WTcrWzF7tXfB9T7MsoadqdI8hmwLq6UlLvtw==", + "cpu": [ + "ia32" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-x64": { + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.35.4.tgz", + "integrity": "sha512-XtmnYhBcrORsJ4XJngyzr/EWP0hRZLAZRFaApdKuviyqF78+ylxh2y06ZmtULAMOnObJ3ucpN0AcwSWnMowTRg==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.6.0.tgz", + "integrity": "sha512-T7jf+5zgsZHwNJ4lvQ7/aezbyk0nNX+zJVWpmHA7VYsEx7a7qr5Rg5IbtJFqkgze5Y2sruq1RUY8Q837Od7iFw==", + "license": "MIT" + }, + "node_modules/@lucide/astro": { + "version": "1.48.0", + "resolved": "https://registry.npmjs.org/@lucide/astro/-/astro-1.48.0.tgz", + "integrity": "sha512-BL+7lLGjjSTfdYEBd9pAB0ixhJ7M84OVzxHoY3oG3M92kX/q1UG2+znp+pqH3waxpIHF1l8Hd1QZxytqv5WH8A==", + "license": "ISC", + "peerDependencies": { + "astro": "^4 || ^5 || ^6 || ^7" + } + }, + "node_modules/@napi-rs/wasm-runtime": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.2.4.tgz", + "integrity": "sha512-AJxoUD2/15ESHbvpcyjU274nsAPLuOtPHCk0vKJM5pj//Fg/B1FXNWjPnXTT9PymCYYiHo4zPj0ZomXBKhoy7g==", + "license": "MIT", + "optional": true, + "dependencies": { + "@tybys/wasm-util": "^0.10.3" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=23.5.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + }, + "peerDependencies": { + "@emnapi/core": "^1.7.1 || ^2.0.0-alpha.4", + "@emnapi/runtime": "^1.7.1 || ^2.0.0-alpha.4" + } + }, + "node_modules/@oslojs/encoding": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@oslojs/encoding/-/encoding-1.1.0.tgz", + "integrity": "sha512-70wQhgYmndg4GCPxPPxPGevRKqTIJ2Nh4OkiMWmDAVYsTQ+Ta7Sq+rPevXyXGdzr30/qZBnyOalCszoMxlyldQ==", + "license": "MIT" + }, + "node_modules/@oxc-project/types": { + "version": "0.151.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.151.0.tgz", + "integrity": "sha512-J1yXrIlNDZVzE3ada310xeAw7nH8yCAyLPuUIsjKatFPmfn5bS1oW+cM+QsGOtVWd5nhSpbwZWx/rue+r5Z+PA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/oxc-project" + } + }, + "node_modules/@pagefind/darwin-arm64": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/@pagefind/darwin-arm64/-/darwin-arm64-1.5.2.tgz", + "integrity": "sha512-MXpI+7HsAdPkvJ0gk9xj9g541BCqBZOBbdwj9g6lB5LCj6kSV6nqDSjzcAJwvOsfu0fjwvC8hQU+ecfhp+MpiQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@pagefind/darwin-x64": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/@pagefind/darwin-x64/-/darwin-x64-1.5.2.tgz", + "integrity": "sha512-IojxFWMEJe0RQ7PQ3KXQsPIImNsbpPYpoZ+QUDrL8fAl/O27IX+LVLs74/UzEZy5uA2LD8Nz1AiwKr72vrkZQw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@pagefind/default-ui": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/@pagefind/default-ui/-/default-ui-1.5.2.tgz", + "integrity": "sha512-pm1LMnQg8N2B3n2TnjKlhaFihpz6zTiA4HiGQ6/slKO/+8K9CAU5kcjdSSPgpuk1PMuuN4hxLipUIifnrkl3Sg==", + "license": "MIT" + }, + "node_modules/@pagefind/freebsd-x64": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/@pagefind/freebsd-x64/-/freebsd-x64-1.5.2.tgz", + "integrity": "sha512-7EVzo9+0w+2cbe671BtMj10UlNo83I+HrLVLfRxO731svHRJKUfJ/mo05gU14pe9PCfpKNQT8FS3Xc/oDN6pOA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@pagefind/linux-arm64": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/@pagefind/linux-arm64/-/linux-arm64-1.5.2.tgz", + "integrity": "sha512-Ovt9+K35sqzn8H3ZMXGwls4TD/wMJuvRtShHIsmUQREmaxjrDEX7gHckRCrwYJ4XE1H1p6HkLz3wukrAnsfXQw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@pagefind/linux-x64": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/@pagefind/linux-x64/-/linux-x64-1.5.2.tgz", + "integrity": "sha512-V+tFqHKXhQKq/WqPBD67AFy7scn1/aZID00ws4fSDd+1daSi5UHR9VVlRrOUYKxn3VuFQYRD7lYXdZK1WED1YA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@pagefind/windows-arm64": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/@pagefind/windows-arm64/-/windows-arm64-1.5.2.tgz", + "integrity": "sha512-hN9Nh90fNW61nNRCW9ZyQrAj/mD0eRvmJ8NlTUzkbuW8kIzGJUi3cxjFkEcMZ5h/8FsKWD/VcouZl4yo1F7B6g==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@pagefind/windows-x64": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/@pagefind/windows-x64/-/windows-x64-1.5.2.tgz", + "integrity": "sha512-Fa2Iyw7kaDRzGMfNYNUXNW2zbL5FQVDgSOcbDHdzBrDEdpqOqg8TcZ68F22ol6NJ9IGzvUdmeyZypLW5dyhqsg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rolldown/binding-android-arm-eabi": { + "version": "1.2.11", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm-eabi/-/binding-android-arm-eabi-1.2.11.tgz", + "integrity": "sha512-A5kXfGKvKWWZE0TtPrfsvT+q4Y5d1QG8gGUzpYjGydM+fARM9MuX90PrXYXe0XbsDVgyxxNzHo6giCj90bsFNw==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-android-arm64": { + "version": "1.2.11", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.2.11.tgz", + "integrity": "sha512-z6cTycz+iJ4PVkuL4HHW4DfTfoeU/2nqYYuSOrTmH7yHK5Y0LCOnA03V4ZNxavyVaU1oOqUgIg2klN/s+USGOA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-arm64": { + "version": "1.2.11", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.2.11.tgz", + "integrity": "sha512-jShvqNtP6vDC6/A5JOAzbVV+DkgHqhl/ScVCJEbt+TUY6QYz7YnXcrg3sLtFBniro0f/Ld50ZwCWA6f7KYD1nQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-x64": { + "version": "1.2.11", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.2.11.tgz", + "integrity": "sha512-f2i2xiNWq1Z1l2++q2fuhZRdLAT3aqxD6vRNm1RAxpUoBcdqNB3C0s1Bt+K+PbEx2F5F4gQp6hqKkphCY/xF9w==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-freebsd-x64": { + "version": "1.2.11", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.2.11.tgz", + "integrity": "sha512-4Ir5FSOKIAMr4r0kExpt1s3bMgzJU3rA45AYOHtQpls0oNeqcYBKrWMlckrYH4KCfGLfkfn1tN1dmZPMVsdXow==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm-gnueabihf": { + "version": "1.2.11", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.2.11.tgz", + "integrity": "sha512-/gnRDM+39BROzAN/k1OZjDPnDMcZxB/0EUxKjONO5yVkNEvlsoMDrxGNKgZi/ttFriS2gwlDNzB65pvNbFOXIQ==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-gnu": { + "version": "1.2.11", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.2.11.tgz", + "integrity": "sha512-PFaK8HwvAHbaKbBcDNQihjMKYvFnA5hiENx/l5tphTDz1E0WFp32l0A7aq7lyUwGsRw/xSrNIy/gIK4thrSCrw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-musl": { + "version": "1.2.11", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.2.11.tgz", + "integrity": "sha512-AskzJUIKRLPxkruR1wLKewGbOw+EYfU/9lOrBFj4AFrEA8hPpKFnODWNu2WLaNs0QNkEb9QIJufmVZZIL/bJlg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-ppc64-gnu": { + "version": "1.2.11", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.2.11.tgz", + "integrity": "sha512-qlUGAheh2yh8afH7QBgx0PrRHN85hKnNd78x8MeMhXivuevgd8vgf6/CstOzmNKY/lLTHvNTrPy98cLnAugzJw==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-s390x-gnu": { + "version": "1.2.11", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.2.11.tgz", + "integrity": "sha512-secpEad+0vCbSfn8upFySkDskv+bGPk3THSDS9Y89yc4rb4kzqHp8Dmyd9BkQW4SnhNXBZCl/6CrO//hZahNJQ==", + "cpu": [ + "s390x" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-gnu": { + "version": "1.2.11", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.2.11.tgz", + "integrity": "sha512-mOVBT3dPpkWm8XBWPmU4bf+U6dYDLeMo/9ojUmis4N0L5uu10qra5vOyngZ7/PSdoE4G9KvRt4bloRxNjLas7A==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-musl": { + "version": "1.2.11", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.2.11.tgz", + "integrity": "sha512-Is78i9A8Ui4SqcxUwFJ9uMmjDn58IbVTjFWYdQestFEgeuEmHMLGNriXnVJKkwG2YiZjw8cP0zCTyDMdDGtOOg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-openharmony-arm64": { + "version": "1.2.11", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.2.11.tgz", + "integrity": "sha512-dUCXneZ87INUMyQ0D+C0HrEBNUPNXHaPmU5GTjyKTJEiussw9Kaj5Ln8UztPe4epV/ffvgNBEadksdYhmW6xJA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-arm64-msvc": { + "version": "1.2.11", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.2.11.tgz", + "integrity": "sha512-jByxb6qfd+bH1xUd0qnfFnb17i9sWBPY2tOavJ0l3tdr3OTu+Kvtm8cd/JV5nFt657b1VqGltxg9olOEfofXWw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-x64-msvc": { + "version": "1.2.11", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.2.11.tgz", + "integrity": "sha512-/PzKqzAJ03i19oy2ItPvyvaVjOjBCNnfaJs8yvUdGBKmiESgnrJSQ2awd81QzFbbnAmu7YO9ZnJrDCb9VSJPRA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz", + "integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==", + "license": "MIT" + }, + "node_modules/@shikijs/core": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/@shikijs/core/-/core-4.4.3.tgz", + "integrity": "sha512-QCR4q2ZO/ILJEuwiBMel4wdcTDb1JGwfjKTxPDF6x8ixOaluPrVqIn06C99AcRPhmYlBR56d/Fb+GN58GzExpg==", + "license": "MIT", + "dependencies": { + "@shikijs/primitive": "4.4.3", + "@shikijs/types": "4.4.3", + "@shikijs/vscode-textmate": "^10.0.2", + "@types/hast": "^3.0.5", + "hast-util-to-html": "^9.0.5" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/@shikijs/engine-javascript": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/@shikijs/engine-javascript/-/engine-javascript-4.4.3.tgz", + "integrity": "sha512-FbOjFJp9VLdo1Wevs10BBtVxiTWwNLqZh5Gkhjgda/ioL15YOgeSl9n+6XMa3qRlPQzfhFNe641SrynFHYG0nQ==", + "license": "MIT", + "dependencies": { + "@shikijs/types": "4.4.3", + "@shikijs/vscode-textmate": "^10.0.2", + "oniguruma-to-es": "^4.3.6" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/@shikijs/engine-oniguruma": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/@shikijs/engine-oniguruma/-/engine-oniguruma-4.4.3.tgz", + "integrity": "sha512-EcOQkxdxGQrc1Row/cC2c96/v1dbZqGnEVu1qTuT/MJmp6+cXCvQussowVmCv5Tqr3KuY3c7IbM6HTW3LJ1k9w==", + "license": "MIT", + "dependencies": { + "@shikijs/types": "4.4.3", + "@shikijs/vscode-textmate": "^10.0.2" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/@shikijs/langs": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/@shikijs/langs/-/langs-4.4.3.tgz", + "integrity": "sha512-ePic0yfAJGOF83D5wBHK/00EjK65oahBYxFk5epgq33WRv7X9UuxLEV8PtR0szC0z8dl7INIpIodB99JRFlR+A==", + "license": "MIT", + "dependencies": { + "@shikijs/types": "4.4.3" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/@shikijs/primitive": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/@shikijs/primitive/-/primitive-4.4.3.tgz", + "integrity": "sha512-m0wBeLDQDeIxRdUmrCPdQqfuUamDwRL5isCfYbguKD6NiaKpVbsv+3J81DyIKgNW5h4WAIIr8T4EkgQrBBxvaQ==", + "license": "MIT", + "dependencies": { + "@shikijs/types": "4.4.3", + "@shikijs/vscode-textmate": "^10.0.2", + "@types/hast": "^3.0.5" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/@shikijs/themes": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/@shikijs/themes/-/themes-4.4.3.tgz", + "integrity": "sha512-w8UHjeUnIR965KMWJHUPXOc2mNJUnK3vpVLYLvw5IYU2mnTTJ89E24OrJDBNiJDQ0qzb0tc4l7mrIXx5cFeIyw==", + "license": "MIT", + "dependencies": { + "@shikijs/types": "4.4.3" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/@shikijs/types": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/@shikijs/types/-/types-4.4.3.tgz", + "integrity": "sha512-UEJxmRR++MAGR6hugn0vgVS2W/6lWAts84FFSrnlH9sP0LNol7E5+NQ792pH8liWUhyMyjhTgSUH3k7iD7tc5g==", + "license": "MIT", + "dependencies": { + "@shikijs/vscode-textmate": "^10.0.2", + "@types/hast": "^3.0.5" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/@shikijs/vscode-textmate": { + "version": "10.0.2", + "resolved": "https://registry.npmjs.org/@shikijs/vscode-textmate/-/vscode-textmate-10.0.2.tgz", + "integrity": "sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg==", + "license": "MIT" + }, + "node_modules/@tybys/wasm-util": { + "version": "0.10.4", + "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.4.tgz", + "integrity": "sha512-W3c4gRigFS0T/Ma4qIYF3GDAc5AQdHb1yL5znJT1Zv1YaD9Kitx656wBjvr19qbiosmZT8lWDM5BEMynUqX65A==", + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@types/braces": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@types/braces/-/braces-3.0.5.tgz", + "integrity": "sha512-SQFof9H+LXeWNz8wDe7oN5zu7ket0qwMu5vZubW4GCJ8Kkeh6nBWUz87+KTz/G3Kqsrp0j/W253XJb3KMEeg3w==", + "license": "MIT" + }, + "node_modules/@types/debug": { + "version": "4.1.13", + "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.13.tgz", + "integrity": "sha512-KSVgmQmzMwPlmtljOomayoR89W4FynCAi3E8PPs7vmDVPe84hT+vGPKkJfThkmXs0x0jAaa9U8uW8bbfyS2fWw==", + "license": "MIT", + "dependencies": { + "@types/ms": "*" + } + }, + "node_modules/@types/estree": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "license": "MIT" + }, + "node_modules/@types/estree-jsx": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@types/estree-jsx/-/estree-jsx-1.0.5.tgz", + "integrity": "sha512-52CcUVNFyfb1A2ALocQw/Dd1BQFNmSdkuC3BkZ6iqhdMfQz7JWOFRuJFloOzjk+6WijU56m9oKXFAXc7o3Towg==", + "license": "MIT", + "dependencies": { + "@types/estree": "*" + } + }, + "node_modules/@types/hast": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.5.tgz", + "integrity": "sha512-rp/ezSWaD1m44dPKICGhiskI13nVr7qTloFwDa/IYkhhf5nzwP+zIQcIJh3WIFSBOy/H1PzB40jPjMDksN4F+g==", + "license": "MIT", + "dependencies": { + "@types/unist": "*" + } + }, + "node_modules/@types/js-yaml": { + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/@types/js-yaml/-/js-yaml-4.0.9.tgz", + "integrity": "sha512-k4MGaQl5TGo/iipqb2UDG2UwjXziSWkh0uysQelTlJpX1qGlpUZYm8PnO4DxG1qBomtJUdYJ6qR6xdIah10JLg==", + "license": "MIT" + }, + "node_modules/@types/mdast": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-4.0.4.tgz", + "integrity": "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==", + "license": "MIT", + "dependencies": { + "@types/unist": "*" + } + }, + "node_modules/@types/micromatch": { + "version": "4.0.10", + "resolved": "https://registry.npmjs.org/@types/micromatch/-/micromatch-4.0.10.tgz", + "integrity": "sha512-5jOhFDElqr4DKTrTEbnW8DZ4Hz5LRUEmyrGpCMrD/NphYv3nUnaF08xmSLx1rGGnyEs/kFnhiw6dCgcDqMr5PQ==", + "license": "MIT", + "dependencies": { + "@types/braces": "*" + } + }, + "node_modules/@types/ms": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz", + "integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==", + "license": "MIT" + }, + "node_modules/@types/nlcst": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@types/nlcst/-/nlcst-2.0.3.tgz", + "integrity": "sha512-vSYNSDe6Ix3q+6Z7ri9lyWqgGhJTmzRjZRqyq15N0Z/1/UnVsno9G/N40NBijoYx2seFDIl0+B2mgAb9mezUCA==", + "license": "MIT", + "dependencies": { + "@types/unist": "*" + } + }, + "node_modules/@types/node": { + "version": "24.13.6", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.13.6.tgz", + "integrity": "sha512-SGrw/h3KPFshy3OE6ZL53LMBG5vGQQ8/gIpiqz/kRZhPJ7HgwCEs8LBuNtWLa8dvGZVpSF7+Bf+c11HUrCb/yg==", + "license": "MIT", + "dependencies": { + "undici-types": "~7.18.0" + } + }, + "node_modules/@types/sax": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/@types/sax/-/sax-1.2.7.tgz", + "integrity": "sha512-rO73L89PJxeYM3s3pPPjiPgVVcymqU490g0YO5n5By0k2Erzj6tay/4lr1CHAAU4JyOWd1rpQ8bCf6cZfHU96A==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/unist": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz", + "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==", + "license": "MIT" + }, + "node_modules/@ungap/structured-clone": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.4.0.tgz", + "integrity": "sha512-1mEZtMKPM09vDmQt5y7YvmN2+DFTP7Tg0EWXdic8/C6VRnpb33e4ghisCIE3WZjsE2N8mf+QV1Zqh7ZFYLWInQ==", + "license": "ISC" + }, + "node_modules/am-i-vibing": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/am-i-vibing/-/am-i-vibing-0.4.0.tgz", + "integrity": "sha512-MxT4XZL7pzLHpuvhDKdMaQHMGGkJDLluKBLsbstn+8wv9sWcFT6h+0ve9qkml95amVTZtZV83gQe2hY+ojgHLg==", + "license": "MIT", + "dependencies": { + "process-ancestry": "^0.1.0" + }, + "bin": { + "am-i-vibing": "dist/cli.mjs" + } + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/anymatch/node_modules/picomatch": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/arg": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz", + "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==", + "license": "MIT" + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "license": "Python-2.0" + }, + "node_modules/aria-query": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.2.tgz", + "integrity": "sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==", + "license": "Apache-2.0", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/astro": { + "version": "7.3.5", + "resolved": "https://registry.npmjs.org/astro/-/astro-7.3.5.tgz", + "integrity": "sha512-1p/ernaq/RuESbM5NMjWzbO6SiebMGrGcYlGjMfBhzzgM+n/X7AUmVyw8BqW2QYCdivpsHwJBcD1fqmlTZjZHg==", + "license": "MIT", + "peer": true, + "dependencies": { + "@astrojs/compiler-rs": "^0.5.0", + "@astrojs/internal-helpers": "0.11.0", + "@astrojs/markdown-satteri": "0.4.2", + "@astrojs/telemetry": "3.3.3", + "@capsizecss/unpack": "^4.0.0", + "@clack/prompts": "^1.1.0", + "@oslojs/encoding": "^1.1.0", + "am-i-vibing": "^0.4.0", + "aria-query": "^5.3.2", + "axobject-query": "^4.1.0", + "ci-info": "^4.4.0", + "clsx": "^2.1.1", + "common-ancestor-path": "^2.0.0", + "cookie": "^2.0.1", + "devalue": "^5.8.1", + "diff": "^9.0.0", + "dset": "^3.1.4", + "es-module-lexer": "^2.0.0", + "esbuild": "^0.28.0", + "find-proc": "0.2.0", + "flattie": "^1.1.1", + "fontace": "~0.4.1", + "get-tsconfig": "5.0.0-beta.4", + "github-slugger": "^2.0.0", + "html-escaper": "3.0.3", + "http-cache-semantics": "^4.2.0", + "js-yaml": "^4.3.2", + "jsonc-parser": "^3.3.1", + "magic-string": "^1.0.0", + "magicast": "^0.5.2", + "mrmime": "^2.0.1", + "neotraverse": "^1.0.1", + "obug": "^3.0.0", + "p-limit": "^7.3.0", + "p-queue": "^9.1.0", + "package-manager-detector": "^1.6.0", + "piccolore": "^0.1.3", + "picomatch": "^4.0.4", + "shiki": "^4.0.2", + "smol-toml": "^1.8.0", + "svgo": "^4.1.0", + "tinyclip": "^1.0.1", + "tinyexec": "^1.0.4", + "tinyglobby": "^0.2.15", + "ultrahtml": "^1.6.0", + "unifont": "~0.7.5", + "unstorage": "^1.17.5", + "verkit": "^0.4.0", + "vite": "^8.0.13", + "vitefu": "^1.1.2", + "xxhash-wasm": "^1.1.0", + "yargs-parser": "^22.0.0", + "zod": "^4.5.4" + }, + "bin": { + "astro": "bin/astro.mjs" + }, + "engines": { + "node": ">=22.12.0", + "npm": ">=9.6.5", + "pnpm": ">=7.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/astrodotbuild" + }, + "optionalDependencies": { + "sharp": "^0.35.4" + }, + "peerDependencies": { + "@astrojs/markdown-remark": "^7.3.0" + }, + "peerDependenciesMeta": { + "@astrojs/markdown-remark": { + "optional": true + } + } + }, + "node_modules/astro-expressive-code": { + "version": "0.44.2", + "resolved": "https://registry.npmjs.org/astro-expressive-code/-/astro-expressive-code-0.44.2.tgz", + "integrity": "sha512-avop7nZcRwC7quvIxVguhwEZ9Tp6IoTzKUCLwP30Fb7+er+pWQgcysP01dcuzRIJo81kLGg30q/YAI2Yb1Sdzg==", + "license": "MIT", + "dependencies": { + "rehype-expressive-code": "^0.44.2", + "url-extras": "^0.1.0" + }, + "peerDependencies": { + "astro": "^4.0.0-beta || ^5.0.0-beta || ^3.3.0 || ^6.0.0-beta || ^7.0.0" + } + }, + "node_modules/axobject-query": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz", + "integrity": "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==", + "license": "Apache-2.0", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/bail": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/bail/-/bail-2.0.2.tgz", + "integrity": "sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/bcp-47": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/bcp-47/-/bcp-47-2.1.1.tgz", + "integrity": "sha512-KLw+H/gd2p4zly1X7Yh/qziuyae5/w/QFnvTng9eZL5fvszL7Whl3MBoWF8yxL7ksUjBfOD+OxkytiqbBpG+Fw==", + "license": "MIT", + "dependencies": { + "is-alphabetical": "^2.0.0", + "is-alphanumerical": "^2.0.0", + "is-decimal": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/bcp-47-match": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/bcp-47-match/-/bcp-47-match-2.0.3.tgz", + "integrity": "sha512-JtTezzbAibu8G0R9op9zb3vcWZd9JF6M0xOYGPn0fNCd7wOpRB1mU2mH9T8gaBGbAAyIIVgB2G7xG0GP98zMAQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/boolbase": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", + "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==", + "license": "ISC" + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/ccount": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/ccount/-/ccount-2.0.1.tgz", + "integrity": "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/character-entities": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/character-entities/-/character-entities-2.0.2.tgz", + "integrity": "sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/character-entities-html4": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/character-entities-html4/-/character-entities-html4-2.1.0.tgz", + "integrity": "sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/character-entities-legacy": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/character-entities-legacy/-/character-entities-legacy-3.0.0.tgz", + "integrity": "sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/character-reference-invalid": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/character-reference-invalid/-/character-reference-invalid-2.0.1.tgz", + "integrity": "sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/chokidar": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-5.0.0.tgz", + "integrity": "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw==", + "license": "MIT", + "dependencies": { + "readdirp": "^5.0.0" + }, + "engines": { + "node": ">= 20.19.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/ci-info": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-4.4.0.tgz", + "integrity": "sha512-77PSwercCZU2Fc4sX94eF8k8Pxte6JAwL4/ICZLFjJLqegs7kCuAsqqj/70NQF6TvDpgFjkubQB2FW2ZZddvQg==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/clsx": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/comma-separated-tokens": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-2.0.3.tgz", + "integrity": "sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/commander": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-11.1.0.tgz", + "integrity": "sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ==", + "license": "MIT", + "engines": { + "node": ">=16" + } + }, + "node_modules/common-ancestor-path": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/common-ancestor-path/-/common-ancestor-path-2.0.0.tgz", + "integrity": "sha512-dnN3ibLeoRf2HNC+OlCiNc5d2zxbLJXOtiZUudNFSXZrNSydxcCsSpRzXwfu7BBWCIfHPw+xTayeBvJCP/D8Ng==", + "license": "BlueOak-1.0.0", + "engines": { + "node": ">= 18" + } + }, + "node_modules/cookie": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-2.0.1.tgz", + "integrity": "sha512-yuToqVvRrj6pfDXREyQAAv8SkAEk/8GS3jQRTiUMm66TVtBYmqQeoEjL2Lmq8Rpo6271vH76InTChTitEAm65w==", + "license": "MIT", + "engines": { + "node": ">=22" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/cookie-es": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/cookie-es/-/cookie-es-1.2.3.tgz", + "integrity": "sha512-lXVyvUvrNXblMqzIRrxHb57UUVmqsSWlxqt3XIjCkUP0wDAf6uicO6KMbEgYrMNtEvWgWHwe42CKxPu9MYAnWw==", + "license": "MIT" + }, + "node_modules/crossws": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/crossws/-/crossws-0.3.5.tgz", + "integrity": "sha512-ojKiDvcmByhwa8YYqbQI/hg7MEU0NC03+pSdEq4ZUnZR9xXpwk7E43SMNGkn+JxJGPFtNvQ48+vV2p+P1ml5PA==", + "license": "MIT", + "dependencies": { + "uncrypto": "^0.1.3" + } + }, + "node_modules/css-select": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/css-select/-/css-select-6.0.0.tgz", + "integrity": "sha512-rZZVSLle8v0+EY8QAkDWrKhpgt6SA5OtHsgBnsj6ZaLb5dmDVOWUDtQitd9ydxxvEjhewNudS6eTVU7uOyzvXw==", + "license": "BSD-2-Clause", + "dependencies": { + "boolbase": "^1.0.0", + "css-what": "^7.0.0", + "domhandler": "^5.0.3", + "domutils": "^3.2.2", + "nth-check": "^2.1.1" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/css-selector-parser": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/css-selector-parser/-/css-selector-parser-3.3.0.tgz", + "integrity": "sha512-Y2asgMGFqJKF4fq4xHDSlFYIkeVfRsm69lQC1q9kbEsH5XtnINTMrweLkjYMeaUgiXBy/uvKeO/a1JHTNnmB2g==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/mdevils" + }, + { + "type": "patreon", + "url": "https://patreon.com/mdevils" + } + ], + "license": "MIT" + }, + "node_modules/css-tree": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-3.2.1.tgz", + "integrity": "sha512-X7sjQzceUhu1u7Y/ylrRZFU2FS6LRiFVp6rKLPg23y3x3c3DOKAwuXGDp+PAGjh6CSnCjYeAul8pcT8bAl+lSA==", + "license": "MIT", + "dependencies": { + "mdn-data": "2.27.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0" + } + }, + "node_modules/css-what": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/css-what/-/css-what-7.0.0.tgz", + "integrity": "sha512-wD5oz5xibMOPHzy13CyGmogB3phdvcDaB5t0W/Nr5Z2O/agcB8YwOz6e2Lsp10pNDzBoDO9nVa3RGs/2BttpHQ==", + "license": "BSD-2-Clause", + "engines": { + "node": ">= 6" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/cssesc": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", + "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", + "license": "MIT", + "bin": { + "cssesc": "bin/cssesc" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/csso": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/csso/-/csso-5.0.5.tgz", + "integrity": "sha512-0LrrStPOdJj+SPCCrGhzryycLjwcgUSHBtxNA8aIDxf0GLsRh1cKYhB00Gd1lDOS4yGH69+SNn13+TWbVHETFQ==", + "license": "MIT", + "dependencies": { + "css-tree": "~2.2.0" + }, + "engines": { + "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0", + "npm": ">=7.0.0" + } + }, + "node_modules/csso/node_modules/css-tree": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-2.2.1.tgz", + "integrity": "sha512-OA0mILzGc1kCOCSJerOeqDxDQ4HOh+G8NbOJFOTgOCzpw7fCBubk0fEyxp8AgOL/jvLgYA/uV0cMbe43ElF1JA==", + "license": "MIT", + "dependencies": { + "mdn-data": "2.0.28", + "source-map-js": "^1.0.1" + }, + "engines": { + "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0", + "npm": ">=7.0.0" + } + }, + "node_modules/csso/node_modules/mdn-data": { + "version": "2.0.28", + "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.28.tgz", + "integrity": "sha512-aylIc7Z9y4yzHYAJNuESG3hfhC+0Ibp/MAMiaOZgNv4pmEdFyfZhhhny4MNiAfWdBQ1RQ2mfDWmM1x8SvGyp8g==", + "license": "CC0-1.0" + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decode-named-character-reference": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/decode-named-character-reference/-/decode-named-character-reference-1.3.0.tgz", + "integrity": "sha512-GtpQYB283KrPp6nRw50q3U9/VfOutZOe103qlN7BPP6Ad27xYnOIWv4lPzo8HCAL+mMZofJ9KEy30fq6MfaK6Q==", + "license": "MIT", + "dependencies": { + "character-entities": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/defu": { + "version": "6.1.7", + "resolved": "https://registry.npmjs.org/defu/-/defu-6.1.7.tgz", + "integrity": "sha512-7z22QmUWiQ/2d0KkdYmANbRUVABpZ9SNYyH5vx6PZ+nE5bcC0l7uFvEfHlyld/HcGBFTL536ClDt3DEcSlEJAQ==", + "license": "MIT" + }, + "node_modules/dequal": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", + "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/destr": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/destr/-/destr-2.0.5.tgz", + "integrity": "sha512-ugFTXCtDZunbzasqBxrK93Ik/DRYsO6S/fedkWEMKqt04xZ4csmnmwGDBAb07QWNaGMAmnTIemsYZCksjATwsA==", + "license": "MIT" + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/devalue": { + "version": "5.9.4", + "resolved": "https://registry.npmjs.org/devalue/-/devalue-5.9.4.tgz", + "integrity": "sha512-sPAT4pztbu6586/hrhOnMKS17IJrvg12mXiSPSS3W5qDeN2RGgvZ0diZCm31dBbnevfVmujNO3IM2wrS4Y2Rhg==", + "license": "MIT" + }, + "node_modules/devlop": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/devlop/-/devlop-1.1.0.tgz", + "integrity": "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==", + "license": "MIT", + "dependencies": { + "dequal": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/diff": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-9.0.0.tgz", + "integrity": "sha512-svtcdpS8CgJyqAjEQIXdb3OjhFVVYjzGAPO8WGCmRbrml64SPw/jJD4GoE98aR7r25A0XcgrK3F02yw9R/vhQw==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/direction": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/direction/-/direction-2.0.1.tgz", + "integrity": "sha512-9S6m9Sukh1cZNknO1CWAr2QAWsbKLafQiyM5gZ7VgXHeuaoUwffKN4q6NC4A/Mf9iiPlOXQEKW/Mv/mh9/3YFA==", + "license": "MIT", + "bin": { + "direction": "cli.js" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/dom-serializer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", + "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", + "license": "MIT", + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.2", + "entities": "^4.2.0" + }, + "funding": { + "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" + } + }, + "node_modules/dom-serializer/node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/domelementtype": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", + "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "license": "BSD-2-Clause" + }, + "node_modules/domhandler": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", + "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", + "license": "BSD-2-Clause", + "dependencies": { + "domelementtype": "^2.3.0" + }, + "engines": { + "node": ">= 4" + }, + "funding": { + "url": "https://github.com/fb55/domhandler?sponsor=1" + } + }, + "node_modules/domutils": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.2.2.tgz", + "integrity": "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==", + "license": "BSD-2-Clause", + "dependencies": { + "dom-serializer": "^2.0.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3" + }, + "funding": { + "url": "https://github.com/fb55/domutils?sponsor=1" + } + }, + "node_modules/dset": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/dset/-/dset-3.1.4.tgz", + "integrity": "sha512-2QF/g9/zTaPDc3BjNcVTGoBbXBgYfMTTceLaYcFJ/W9kggFUkhxD/hMEeuLKbugyef9SqAx8cpgwlIP/jinUTA==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/entities": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", + "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/es-module-lexer": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.3.2.tgz", + "integrity": "sha512-poHGpORABojJJucnV9KbOavETW8lBVnphkW77ER5/BQ5Fz7oXSoCNek7IH3vR5nRjdsEz926ibFYX8KtLQmdyw==", + "license": "MIT" + }, + "node_modules/esbuild": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.2.tgz", + "integrity": "sha512-HKVLS8dvII+xoKW9kmqxbRKrnWEXfJJr/FZhhJmiqIB0e053QNYFqOBouTMO/k5sID4MvCiUCvv8b9M4h32wIA==", + "hasInstallScript": true, + "license": "MIT", + "peer": true, + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.28.2", + "@esbuild/android-arm": "0.28.2", + "@esbuild/android-arm64": "0.28.2", + "@esbuild/android-x64": "0.28.2", + "@esbuild/darwin-arm64": "0.28.2", + "@esbuild/darwin-x64": "0.28.2", + "@esbuild/freebsd-arm64": "0.28.2", + "@esbuild/freebsd-x64": "0.28.2", + "@esbuild/linux-arm": "0.28.2", + "@esbuild/linux-arm64": "0.28.2", + "@esbuild/linux-ia32": "0.28.2", + "@esbuild/linux-loong64": "0.28.2", + "@esbuild/linux-mips64el": "0.28.2", + "@esbuild/linux-ppc64": "0.28.2", + "@esbuild/linux-riscv64": "0.28.2", + "@esbuild/linux-s390x": "0.28.2", + "@esbuild/linux-x64": "0.28.2", + "@esbuild/netbsd-arm64": "0.28.2", + "@esbuild/netbsd-x64": "0.28.2", + "@esbuild/openbsd-arm64": "0.28.2", + "@esbuild/openbsd-x64": "0.28.2", + "@esbuild/openharmony-arm64": "0.28.2", + "@esbuild/sunos-x64": "0.28.2", + "@esbuild/win32-arm64": "0.28.2", + "@esbuild/win32-ia32": "0.28.2", + "@esbuild/win32-x64": "0.28.2" + } + }, + "node_modules/escape-string-regexp": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz", + "integrity": "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eventemitter3": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.4.tgz", + "integrity": "sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==", + "license": "MIT" + }, + "node_modules/expressive-code": { + "version": "0.44.2", + "resolved": "https://registry.npmjs.org/expressive-code/-/expressive-code-0.44.2.tgz", + "integrity": "sha512-MLNghkvhyFgVW4oCD5DxBuYnnTXIf0PygzGmfT6OZeOOYWh/D4sH1j931biElkA5RG/J9wY/iuSMGAPF5vzHOg==", + "license": "MIT", + "dependencies": { + "@expressive-code/core": "^0.44.2", + "@expressive-code/plugin-frames": "^0.44.2", + "@expressive-code/plugin-shiki": "^0.44.2", + "@expressive-code/plugin-text-markers": "^0.44.2" + } + }, + "node_modules/extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", + "license": "MIT" + }, + "node_modules/fast-string-truncated-width": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/fast-string-truncated-width/-/fast-string-truncated-width-3.0.3.tgz", + "integrity": "sha512-0jjjIEL6+0jag3l2XWWizO64/aZVtpiGE3t0Zgqxv0DPuxiMjvB3M24fCyhZUO4KomJQPj3LTSUnDP3GpdwC0g==", + "license": "MIT" + }, + "node_modules/fast-string-width": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/fast-string-width/-/fast-string-width-3.0.2.tgz", + "integrity": "sha512-gX8LrtNEI5hq8DVUfRQMbr5lpaS4nMIWV+7XEbXk2b8kiQIizgnlr12B4dA3ZEx3308ze0O4Q1R+cHts8kyUJg==", + "license": "MIT", + "dependencies": { + "fast-string-truncated-width": "^3.0.2" + } + }, + "node_modules/fast-wrap-ansi": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/fast-wrap-ansi/-/fast-wrap-ansi-0.2.2.tgz", + "integrity": "sha512-7F2Fl+TjRSenLqlU3UjSH0iyqopqoZIu7eZVpEirP2g1GtWa2G/ecEmBdgz31+Mxr+ELclgg6sokpSFIQiZ02Q==", + "license": "MIT", + "dependencies": { + "fast-string-width": "^3.0.2" + } + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/find-proc": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/find-proc/-/find-proc-0.2.0.tgz", + "integrity": "sha512-a6h2nTrgB3g5Wrn4au9Ux4kMk7IJtyF6oA8uGn+mk3weLZssUtc1cp6meHIzzuW2o1srrBAxzkmXLbqjRkgB6w==", + "license": "MIT", + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/flattie": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/flattie/-/flattie-1.1.1.tgz", + "integrity": "sha512-9UbaD6XdAL97+k/n+N7JwX46K/M6Zc6KcFYskrYL8wbBV/Uyk0CTAMY0VT+qiK5PM7AIc9aTWYtq65U7T+aCNQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/fontace": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/fontace/-/fontace-0.4.1.tgz", + "integrity": "sha512-lDMvbAzSnHmbYMTEld5qdtvNH2/pWpICOqpean9IgC7vUbUJc3k+k5Dokp85CegamqQpFbXf0rAVkbzpyTA8aw==", + "license": "MIT", + "dependencies": { + "fontkitten": "^1.0.2" + } + }, + "node_modules/fontkitten": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/fontkitten/-/fontkitten-1.0.3.tgz", + "integrity": "sha512-Wp1zXWPVUPBmfoa3Cqc9ctaKuzKAV6uLstRqlR56kSjplf5uAce+qeyYym7F+PHbGTk+tCEdkCW6RD7DX/gBZw==", + "license": "MIT", + "dependencies": { + "tiny-inflate": "^1.0.3" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/get-tsconfig": { + "version": "5.0.0-beta.4", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-5.0.0-beta.4.tgz", + "integrity": "sha512-7nF7C9fIPFEMHgEMEfgIlO9wDdZ8CyHw27rWciFZfHvHDReIiPhsYuzPRXsfvBCqFy1l8RRyyWV7QLM+ZhUJsQ==", + "license": "MIT", + "dependencies": { + "resolve-pkg-maps": "^1.0.0" + }, + "engines": { + "node": ">=20.20.0" + }, + "funding": { + "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" + } + }, + "node_modules/github-slugger": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/github-slugger/-/github-slugger-2.0.0.tgz", + "integrity": "sha512-IaOQ9puYtjrkq7Y0Ygl9KDZnrf/aiUJYUpVf89y8kyaxbRG7Y1SrX/jaumrv81vc61+kiMempujsM3Yw7w5qcw==", + "license": "ISC" + }, + "node_modules/h3": { + "version": "1.15.11", + "resolved": "https://registry.npmjs.org/h3/-/h3-1.15.11.tgz", + "integrity": "sha512-L3THSe2MPeBwgIZVSH5zLdBBU90TOxarvhK9d04IDY2AmVS8j2Jz2LIWtwsGOU3lu2I5jCN7FNvVfY2+XyF+mg==", + "license": "MIT", + "dependencies": { + "cookie-es": "^1.2.3", + "crossws": "^0.3.5", + "defu": "^6.1.6", + "destr": "^2.0.5", + "iron-webcrypto": "^1.2.1", + "node-mock-http": "^1.0.4", + "radix3": "^1.1.2", + "ufo": "^1.6.3", + "uncrypto": "^0.1.3" + } + }, + "node_modules/hast-util-embedded": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/hast-util-embedded/-/hast-util-embedded-3.0.0.tgz", + "integrity": "sha512-naH8sld4Pe2ep03qqULEtvYr7EjrLK2QHY8KJR6RJkTUjPGObe1vnx585uzem2hGra+s1q08DZZpfgDVYRbaXA==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "hast-util-is-element": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-format": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/hast-util-format/-/hast-util-format-1.1.0.tgz", + "integrity": "sha512-yY1UDz6bC9rDvCWHpx12aIBGRG7krurX0p0Fm6pT547LwDIZZiNr8a+IHDogorAdreULSEzP82Nlv5SZkHZcjA==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "hast-util-embedded": "^3.0.0", + "hast-util-minify-whitespace": "^1.0.0", + "hast-util-phrasing": "^3.0.0", + "hast-util-whitespace": "^3.0.0", + "html-whitespace-sensitive-tag-names": "^3.0.0", + "unist-util-visit-parents": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-from-html": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/hast-util-from-html/-/hast-util-from-html-2.0.3.tgz", + "integrity": "sha512-CUSRHXyKjzHov8yKsQjGOElXy/3EKpyX56ELnkHH34vDVw1N1XSQ1ZcAvTyAPtGqLTuKP/uxM+aLkSPqF/EtMw==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "devlop": "^1.1.0", + "hast-util-from-parse5": "^8.0.0", + "parse5": "^7.0.0", + "vfile": "^6.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-from-parse5": { + "version": "8.0.3", + "resolved": "https://registry.npmjs.org/hast-util-from-parse5/-/hast-util-from-parse5-8.0.3.tgz", + "integrity": "sha512-3kxEVkEKt0zvcZ3hCRYI8rqrgwtlIOFMWkbclACvjlDw8Li9S2hk/d51OI0nr/gIpdMHNepwgOKqZ/sy0Clpyg==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/unist": "^3.0.0", + "devlop": "^1.0.0", + "hastscript": "^9.0.0", + "property-information": "^7.0.0", + "vfile": "^6.0.0", + "vfile-location": "^5.0.0", + "web-namespaces": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-has-property": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/hast-util-has-property/-/hast-util-has-property-3.0.0.tgz", + "integrity": "sha512-MNilsvEKLFpV604hwfhVStK0usFY/QmM5zX16bo7EjnAEGofr5YyI37kzopBlZJkHD4t887i+q/C8/tr5Q94cA==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-is-body-ok-link": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/hast-util-is-body-ok-link/-/hast-util-is-body-ok-link-3.0.1.tgz", + "integrity": "sha512-0qpnzOBLztXHbHQenVB8uNuxTnm/QBFUOmdOSsEn7GnBtyY07+ENTWVFBAnXd/zEgd9/SUG3lRY7hSIBWRgGpQ==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-is-element": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/hast-util-is-element/-/hast-util-is-element-3.0.0.tgz", + "integrity": "sha512-Val9mnv2IWpLbNPqc/pUem+a7Ipj2aHacCwgNfTiK0vJKl0LF+4Ba4+v1oPHFpf3bLYmreq0/l3Gud9S5OH42g==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-minify-whitespace": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/hast-util-minify-whitespace/-/hast-util-minify-whitespace-1.0.1.tgz", + "integrity": "sha512-L96fPOVpnclQE0xzdWb/D12VT5FabA7SnZOUMtL1DbXmYiHJMXZvFkIZfiMmTCNJHUeO2K9UYNXoVyfz+QHuOw==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "hast-util-embedded": "^3.0.0", + "hast-util-is-element": "^3.0.0", + "hast-util-whitespace": "^3.0.0", + "unist-util-is": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-parse-selector": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/hast-util-parse-selector/-/hast-util-parse-selector-4.0.0.tgz", + "integrity": "sha512-wkQCkSYoOGCRKERFWcxMVMOcYE2K1AaNLU8DXS9arxnLOUEWbOXKXiJUNzEpqZ3JOKpnha3jkFrumEjVliDe7A==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-phrasing": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/hast-util-phrasing/-/hast-util-phrasing-3.0.1.tgz", + "integrity": "sha512-6h60VfI3uBQUxHqTyMymMZnEbNl1XmEGtOxxKYL7stY2o601COo62AWAYBQR9lZbYXYSBoxag8UpPRXK+9fqSQ==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "hast-util-embedded": "^3.0.0", + "hast-util-has-property": "^3.0.0", + "hast-util-is-body-ok-link": "^3.0.0", + "hast-util-is-element": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-select": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/hast-util-select/-/hast-util-select-6.0.4.tgz", + "integrity": "sha512-RqGS1ZgI0MwxLaKLDxjprynNzINEkRHY2i8ln4DDjgv9ZhcYVIHN9rlpiYsqtFwrgpYU361SyWDQcGNIBVu3lw==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/unist": "^3.0.0", + "bcp-47-match": "^2.0.0", + "comma-separated-tokens": "^2.0.0", + "css-selector-parser": "^3.0.0", + "devlop": "^1.0.0", + "direction": "^2.0.0", + "hast-util-has-property": "^3.0.0", + "hast-util-to-string": "^3.0.0", + "hast-util-whitespace": "^3.0.0", + "nth-check": "^2.0.0", + "property-information": "^7.0.0", + "space-separated-tokens": "^2.0.0", + "unist-util-visit": "^5.0.0", + "zwitch": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-to-html": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/hast-util-to-html/-/hast-util-to-html-9.0.5.tgz", + "integrity": "sha512-OguPdidb+fbHQSU4Q4ZiLKnzWo8Wwsf5bZfbvu7//a9oTYoqD/fWpe96NuHkoS9h0ccGOTe0C4NGXdtS0iObOw==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/unist": "^3.0.0", + "ccount": "^2.0.0", + "comma-separated-tokens": "^2.0.0", + "hast-util-whitespace": "^3.0.0", + "html-void-elements": "^3.0.0", + "mdast-util-to-hast": "^13.0.0", + "property-information": "^7.0.0", + "space-separated-tokens": "^2.0.0", + "stringify-entities": "^4.0.0", + "zwitch": "^2.0.4" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-to-mdast": { + "version": "10.1.2", + "resolved": "https://registry.npmjs.org/hast-util-to-mdast/-/hast-util-to-mdast-10.1.2.tgz", + "integrity": "sha512-FiCRI7NmOvM4y+f5w32jPRzcxDIz+PUqDwEqn1A+1q2cdp3B8Gx7aVrXORdOKjMNDQsD1ogOr896+0jJHW1EFQ==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "@ungap/structured-clone": "^1.0.0", + "hast-util-phrasing": "^3.0.0", + "hast-util-to-html": "^9.0.0", + "hast-util-to-text": "^4.0.0", + "hast-util-whitespace": "^3.0.0", + "mdast-util-phrasing": "^4.0.0", + "mdast-util-to-hast": "^13.0.0", + "mdast-util-to-string": "^4.0.0", + "rehype-minify-whitespace": "^6.0.0", + "trim-trailing-lines": "^2.0.0", + "unist-util-position": "^5.0.0", + "unist-util-visit": "^5.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-to-string": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/hast-util-to-string/-/hast-util-to-string-3.0.1.tgz", + "integrity": "sha512-XelQVTDWvqcl3axRfI0xSeoVKzyIFPwsAGSLIsKdJKQMXDYJS4WYrBNF/8J7RdhIcFI2BOHgAifggsvsxp/3+A==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-to-text": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/hast-util-to-text/-/hast-util-to-text-4.0.2.tgz", + "integrity": "sha512-KK6y/BN8lbaq654j7JgBydev7wuNMcID54lkRav1P0CaE1e47P72AWWPiGKXTJU271ooYzcvTAn/Zt0REnvc7A==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/unist": "^3.0.0", + "hast-util-is-element": "^3.0.0", + "unist-util-find-after": "^5.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-whitespace": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/hast-util-whitespace/-/hast-util-whitespace-3.0.0.tgz", + "integrity": "sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hastscript": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/hastscript/-/hastscript-9.0.1.tgz", + "integrity": "sha512-g7df9rMFX/SPi34tyGCyUBREQoKkapwdY/T04Qn9TDWfHhAYt4/I0gMVirzK5wEzeUqIjEB+LXC/ypb7Aqno5w==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "comma-separated-tokens": "^2.0.0", + "hast-util-parse-selector": "^4.0.0", + "property-information": "^7.0.0", + "space-separated-tokens": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/html-escaper": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-3.0.3.tgz", + "integrity": "sha512-RuMffC89BOWQoY0WKGpIhn5gX3iI54O6nRA0yC124NYVtzjmFWBIiFd8M0x+ZdX0P9R4lADg1mgP8C7PxGOWuQ==", + "license": "MIT" + }, + "node_modules/html-void-elements": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/html-void-elements/-/html-void-elements-3.0.0.tgz", + "integrity": "sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/html-whitespace-sensitive-tag-names": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/html-whitespace-sensitive-tag-names/-/html-whitespace-sensitive-tag-names-3.0.1.tgz", + "integrity": "sha512-q+310vW8zmymYHALr1da4HyXUQ0zgiIwIicEfotYPWGN0OJVEN/58IJ3A4GBYcEq3LGAZqKb+ugvP0GNB9CEAA==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/http-cache-semantics": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.2.0.tgz", + "integrity": "sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ==", + "license": "BSD-2-Clause" + }, + "node_modules/i18next": { + "version": "26.4.2", + "resolved": "https://registry.npmjs.org/i18next/-/i18next-26.4.2.tgz", + "integrity": "sha512-RX+R0VLg13IbvRuJSxnqykUFS9vQZTl8wYpWPCIUDWVrSGjsQywB5Y+pjzrkboxGAuYfJZVH1InFTdgBdxq6ug==", + "funding": [ + { + "type": "individual", + "url": "https://www.locize.com/i18next" + }, + { + "type": "individual", + "url": "https://www.i18next.com/how-to/faq#i18next-is-awesome.-how-can-i-support-the-project" + }, + { + "type": "individual", + "url": "https://www.locize.com" + } + ], + "license": "MIT", + "peerDependencies": { + "typescript": "^5 || ^6 || ^7" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/iron-webcrypto": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/iron-webcrypto/-/iron-webcrypto-1.2.1.tgz", + "integrity": "sha512-feOM6FaSr6rEABp/eDfVseKyTMDt+KGpeB35SkVn9Tyn0CqvVsY3EwI0v5i8nMHyJnzCIQf7nsy3p41TPkJZhg==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/brc-dd" + } + }, + "node_modules/is-alphabetical": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-alphabetical/-/is-alphabetical-2.0.1.tgz", + "integrity": "sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/is-alphanumerical": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-alphanumerical/-/is-alphanumerical-2.0.1.tgz", + "integrity": "sha512-hmbYhX/9MUMF5uh7tOXyK/n0ZvWpad5caBA17GsC6vyuCqaWliRG5K1qS9inmUhEMaOBIW7/whAnSwveW/LtZw==", + "license": "MIT", + "dependencies": { + "is-alphabetical": "^2.0.0", + "is-decimal": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/is-decimal": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-decimal/-/is-decimal-2.0.1.tgz", + "integrity": "sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/is-docker": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-4.0.0.tgz", + "integrity": "sha512-LHE+wROyG/Y/0ZnbktRCoTix2c1RhgWaZraMZ8o1Q7zCh0VSrICJQO5oqIIISrcSBtrXv0o233w1IYwsWCjTzA==", + "license": "MIT", + "bin": { + "is-docker": "cli.js" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-hexadecimal": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-hexadecimal/-/is-hexadecimal-2.0.1.tgz", + "integrity": "sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-plain-obj": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz", + "integrity": "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/js-yaml": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.2.tgz", + "integrity": "sha512-SFNOvSJ+Dgf/9An904Yx+CgSlIPCkIpao4qo51lpee25TIRejdH3rhR4EZMGoNx3/TP3O+wzWuiTFl4sqbltzA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/nodeca" + } + ], + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsonc-parser": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.3.1.tgz", + "integrity": "sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ==", + "license": "MIT" + }, + "node_modules/klona": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/klona/-/klona-2.0.6.tgz", + "integrity": "sha512-dhG34DXATL5hSxJbIexCft8FChFXtmskoZYnoPWjXQuebWYCNkVeV3KkGegCK9CP1oswI/vQibS2GY7Em/sJJA==", + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/lightningcss": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.33.0.tgz", + "integrity": "sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA==", + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.33.0", + "lightningcss-darwin-arm64": "1.33.0", + "lightningcss-darwin-x64": "1.33.0", + "lightningcss-freebsd-x64": "1.33.0", + "lightningcss-linux-arm-gnueabihf": "1.33.0", + "lightningcss-linux-arm64-gnu": "1.33.0", + "lightningcss-linux-arm64-musl": "1.33.0", + "lightningcss-linux-x64-gnu": "1.33.0", + "lightningcss-linux-x64-musl": "1.33.0", + "lightningcss-win32-arm64-msvc": "1.33.0", + "lightningcss-win32-x64-msvc": "1.33.0" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.33.0.tgz", + "integrity": "sha512-gEpRTalKdosp4Bb8qWtc2iOgE5SeIHlpS1up9bFq2wAyYhl1UdTObYiHe98zEM9SQvSoqQZ1IQD0JNpg3Ml5pg==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.33.0.tgz", + "integrity": "sha512-Sciaz8eenNTKn9b3t7+xr0ipTp9YxKQY4npwQ3mrRuL0BAVHBLyZxofhaKBAVtzmtRZ/zTyo0/to4B1uWG/Djg==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.33.0.tgz", + "integrity": "sha512-Z5UPAxzrjlWNNyGy6i65cJzzvgJ5D3T6wMvs+gWpY9d7qRhANrxqAp6LhxIgZhWEw18RfJTGcRxjuLIBr+m8XQ==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.33.0.tgz", + "integrity": "sha512-QQM/Ti/hQajJwCY+RiWuCZ9sdtI/XQk7nDK5vC8kkdwixezOlDgvDx7+RT+QjK6FcFT4MpsuoBnHIo/O3StRRg==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.33.0.tgz", + "integrity": "sha512-N7FVBe6iS24MlM6R/4RBTxGhQheZGs7tiQ9U32UtF75NzP5Q7xWPRqLBCKxlRQRk3rY1jCIPLzx7WzOhuUIRLQ==", + "cpu": [ + "arm" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.33.0.tgz", + "integrity": "sha512-j2v/itmy4HlNxlc6voKXYgBqNi0Ng2LShg4z7GufpEgs05P+2suBVyi9I6YHq5uoVFx9ETin3eCEhLVyXGQnKg==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.33.0.tgz", + "integrity": "sha512-yiO5ROMuYQgXbC60yjZU5CYSFZGKXL0HFATXt9mHJn1+zW55oCtMI9NfcVhYLMFDL7gV7oBPon/EmMMGg2OvtQ==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.33.0.tgz", + "integrity": "sha512-ar+Ju7LmcN0Jo4FpL4hpFybwNG9/3A/Br5KW2n2jyODg3MEZXaDYADdemoNS+BDNfMgKvylJLj4S5tyRActuAg==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.33.0.tgz", + "integrity": "sha512-RYiYbkokw0trfKqqzfF55lginwEPrD3OJDfTuJzFs1MK6iFnDenaz1fqLLtX4ITG3OktJQXOeTaw1awrBAlZPw==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.33.0.tgz", + "integrity": "sha512-1K+MPfLSFVpphzpdbfkhlWk6wBrTObBzS2T6db10PNOZgR9GoVsAWzwNyuhUYYbTp23j+4RrncfujZ4uAzXvwA==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.33.0.tgz", + "integrity": "sha512-OlEICDx/Xl0FqSp4bry8zFnCvGpig3Gl4gCquvYwHuqJKEC1+n9NgDniFvqHGmMv1ZkqDJrDqKKSykTDX+ehuA==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/longest-streak": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/longest-streak/-/longest-streak-3.1.0.tgz", + "integrity": "sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/lru-cache": { + "version": "11.5.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.3.tgz", + "integrity": "sha512-U4N8FgzmWxc8k1VH8Kr6lQg18U7Fjvby6wXHVRX/ZZ7IwWbRMgrRbP0Wrb5q5NVinryp4SQampHKdvtecItxUg==", + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/magic-string": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-1.4.2.tgz", + "integrity": "sha512-vG+rjFRj1PqdIBozIxAGMjPlOhaVe+GXpbttY/iSK7rGcJRMlwNJO7dcUwmUqkymsFLJiNGI06t4D7Fr7yRC9g==", + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.6.0" + } + }, + "node_modules/magicast": { + "version": "0.5.5", + "resolved": "https://registry.npmjs.org/magicast/-/magicast-0.5.5.tgz", + "integrity": "sha512-UicdXN8zQ3JHlxVq+28afMXPr1z7WNY6+7EJnzTdQWkTAlMLF5fNCCKxJHBQwGaNGR11581EiQmQzx73+MvszA==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.7", + "@babel/types": "^7.29.7", + "source-map-js": "^1.2.1" + } + }, + "node_modules/markdown-table": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/markdown-table/-/markdown-table-3.0.4.tgz", + "integrity": "sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/mdast-util-directive": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mdast-util-directive/-/mdast-util-directive-3.1.0.tgz", + "integrity": "sha512-I3fNFt+DHmpWCYAT7quoM6lHf9wuqtI+oCOfvILnoicNIqjh5E3dEJWiXuYME2gNe8vl1iMQwyUHa7bgFmak6Q==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "@types/unist": "^3.0.0", + "ccount": "^2.0.0", + "devlop": "^1.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0", + "parse-entities": "^4.0.0", + "stringify-entities": "^4.0.0", + "unist-util-visit-parents": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-find-and-replace": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mdast-util-find-and-replace/-/mdast-util-find-and-replace-3.0.2.tgz", + "integrity": "sha512-Tmd1Vg/m3Xz43afeNxDIhWRtFZgM2VLyaf4vSTYwudTyeuTneoL3qtWMA5jeLyz/O1vDJmmV4QuScFCA2tBPwg==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "escape-string-regexp": "^5.0.0", + "unist-util-is": "^6.0.0", + "unist-util-visit-parents": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-from-markdown": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/mdast-util-from-markdown/-/mdast-util-from-markdown-2.0.3.tgz", + "integrity": "sha512-W4mAWTvSlKvf8L6J+VN9yLSqQ9AOAAvHuoDAmPkz4dHf553m5gVj2ejadHJhoJmcmxEnOv6Pa8XJhpxE93kb8Q==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "@types/unist": "^3.0.0", + "decode-named-character-reference": "^1.0.0", + "devlop": "^1.0.0", + "mdast-util-to-string": "^4.0.0", + "micromark": "^4.0.0", + "micromark-util-decode-numeric-character-reference": "^2.0.0", + "micromark-util-decode-string": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0", + "unist-util-stringify-position": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm/-/mdast-util-gfm-3.1.0.tgz", + "integrity": "sha512-0ulfdQOM3ysHhCJ1p06l0b0VKlhU0wuQs3thxZQagjcjPrlFRqY215uZGHHJan9GEAXd9MbfPjFJz+qMkVR6zQ==", + "license": "MIT", + "dependencies": { + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-gfm-autolink-literal": "^2.0.0", + "mdast-util-gfm-footnote": "^2.0.0", + "mdast-util-gfm-strikethrough": "^2.0.0", + "mdast-util-gfm-table": "^2.0.0", + "mdast-util-gfm-task-list-item": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-autolink-literal": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-autolink-literal/-/mdast-util-gfm-autolink-literal-2.0.1.tgz", + "integrity": "sha512-5HVP2MKaP6L+G6YaxPNjuL0BPrq9orG3TsrZ9YXbA3vDw/ACI4MEsnoDpn6ZNm7GnZgtAcONJyPhOP8tNJQavQ==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "ccount": "^2.0.0", + "devlop": "^1.0.0", + "mdast-util-find-and-replace": "^3.0.0", + "micromark-util-character": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-footnote": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-footnote/-/mdast-util-gfm-footnote-2.1.0.tgz", + "integrity": "sha512-sqpDWlsHn7Ac9GNZQMeUzPQSMzR6Wv0WKRNvQRg0KqHh02fpTz69Qc1QSseNX29bhz1ROIyNyxExfawVKTm1GQ==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "devlop": "^1.1.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-strikethrough": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-strikethrough/-/mdast-util-gfm-strikethrough-2.0.0.tgz", + "integrity": "sha512-mKKb915TF+OC5ptj5bJ7WFRPdYtuHv0yTRxK2tJvi+BDqbkiG7h7u/9SI89nRAYcmap2xHQL9D+QG/6wSrTtXg==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-table": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-table/-/mdast-util-gfm-table-2.0.0.tgz", + "integrity": "sha512-78UEvebzz/rJIxLvE7ZtDd/vIQ0RHv+3Mh5DR96p7cS7HsBhYIICDBCu8csTNWNO6tBWfqXPWekRuj2FNOGOZg==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "markdown-table": "^3.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-task-list-item": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-task-list-item/-/mdast-util-gfm-task-list-item-2.0.0.tgz", + "integrity": "sha512-IrtvNvjxC1o06taBAVJznEnkiHxLFTzgonUdy8hzFVeDun0uTjxxrRGVaNFqkU1wJR3RBPEfsxmU6jDWPofrTQ==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-phrasing": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/mdast-util-phrasing/-/mdast-util-phrasing-4.1.0.tgz", + "integrity": "sha512-TqICwyvJJpBwvGAMZjj4J2n0X8QWp21b9l0o7eXyVJ25YNWYbJDVIyD1bZXE6WtV6RmKJVYmQAKWa0zWOABz2w==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "unist-util-is": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-to-hast": { + "version": "13.2.1", + "resolved": "https://registry.npmjs.org/mdast-util-to-hast/-/mdast-util-to-hast-13.2.1.tgz", + "integrity": "sha512-cctsq2wp5vTsLIcaymblUriiTcZd0CwWtCbLvrOzYCDZoWyMNV8sZ7krj09FSnsiJi3WVsHLM4k6Dq/yaPyCXA==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "@ungap/structured-clone": "^1.0.0", + "devlop": "^1.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "trim-lines": "^3.0.0", + "unist-util-position": "^5.0.0", + "unist-util-visit": "^5.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-to-markdown": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/mdast-util-to-markdown/-/mdast-util-to-markdown-2.1.2.tgz", + "integrity": "sha512-xj68wMTvGXVOKonmog6LwyJKrYXZPvlwabaryTjLh9LuvovB/KAH+kvi8Gjj+7rJjsFi23nkUxRQv1KqSroMqA==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "@types/unist": "^3.0.0", + "longest-streak": "^3.0.0", + "mdast-util-phrasing": "^4.0.0", + "mdast-util-to-string": "^4.0.0", + "micromark-util-classify-character": "^2.0.0", + "micromark-util-decode-string": "^2.0.0", + "unist-util-visit": "^5.0.0", + "zwitch": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-to-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-to-string/-/mdast-util-to-string-4.0.0.tgz", + "integrity": "sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdn-data": { + "version": "2.27.1", + "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.27.1.tgz", + "integrity": "sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ==", + "license": "CC0-1.0" + }, + "node_modules/micromark": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/micromark/-/micromark-4.0.2.tgz", + "integrity": "sha512-zpe98Q6kvavpCr1NPVSCMebCKfD7CA2NqZ+rykeNhONIJBpc1tFKt9hucLGwha3jNTNI8lHpctWJWoimVF4PfA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "@types/debug": "^4.0.0", + "debug": "^4.0.0", + "decode-named-character-reference": "^1.0.0", + "devlop": "^1.0.0", + "micromark-core-commonmark": "^2.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-combine-extensions": "^2.0.0", + "micromark-util-decode-numeric-character-reference": "^2.0.0", + "micromark-util-encode": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-resolve-all": "^2.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "micromark-util-subtokenize": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-core-commonmark": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/micromark-core-commonmark/-/micromark-core-commonmark-2.0.3.tgz", + "integrity": "sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "decode-named-character-reference": "^1.0.0", + "devlop": "^1.0.0", + "micromark-factory-destination": "^2.0.0", + "micromark-factory-label": "^2.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-factory-title": "^2.0.0", + "micromark-factory-whitespace": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-classify-character": "^2.0.0", + "micromark-util-html-tag-name": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-resolve-all": "^2.0.0", + "micromark-util-subtokenize": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-extension-directive": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/micromark-extension-directive/-/micromark-extension-directive-4.0.0.tgz", + "integrity": "sha512-/C2nqVmXXmiseSSuCdItCMho7ybwwop6RrrRPk0KbOHW21JKoCldC+8rFOaundDoRBUWBnJJcxeA/Kvi34WQXg==", + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-factory-whitespace": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0", + "parse-entities": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm/-/micromark-extension-gfm-3.0.0.tgz", + "integrity": "sha512-vsKArQsicm7t0z2GugkCKtZehqUm31oeGBV/KVSorWSy8ZlNAv7ytjFhvaryUiCUJYqs+NoE6AFhpQvBTM6Q4w==", + "license": "MIT", + "dependencies": { + "micromark-extension-gfm-autolink-literal": "^2.0.0", + "micromark-extension-gfm-footnote": "^2.0.0", + "micromark-extension-gfm-strikethrough": "^2.0.0", + "micromark-extension-gfm-table": "^2.0.0", + "micromark-extension-gfm-tagfilter": "^2.0.0", + "micromark-extension-gfm-task-list-item": "^2.0.0", + "micromark-util-combine-extensions": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-autolink-literal": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-autolink-literal/-/micromark-extension-gfm-autolink-literal-2.1.0.tgz", + "integrity": "sha512-oOg7knzhicgQ3t4QCjCWgTmfNhvQbDDnJeVu9v81r7NltNCVmhPy1fJRX27pISafdjL+SVc4d3l48Gb6pbRypw==", + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-footnote": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-footnote/-/micromark-extension-gfm-footnote-2.1.0.tgz", + "integrity": "sha512-/yPhxI1ntnDNsiHtzLKYnE3vf9JZ6cAisqVDauhp4CEHxlb4uoOTxOCJ+9s51bIB8U1N1FJ1RXOKTIlD5B/gqw==", + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-core-commonmark": "^2.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-strikethrough": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-strikethrough/-/micromark-extension-gfm-strikethrough-2.1.0.tgz", + "integrity": "sha512-ADVjpOOkjz1hhkZLlBiYA9cR2Anf8F4HqZUO6e5eDcPQd0Txw5fxLzzxnEkSkfnD0wziSGiv7sYhk/ktvbf1uw==", + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-classify-character": "^2.0.0", + "micromark-util-resolve-all": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-table": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-table/-/micromark-extension-gfm-table-2.1.2.tgz", + "integrity": "sha512-pRzm4kDTu0MjlmBkxmS9yYhw60nncfcEwu9NNdPFSQEFXS95ZKyIIyTSHu/o3ReBUrLKYEq+7YaXCRn/bPB4MA==", + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-tagfilter": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-tagfilter/-/micromark-extension-gfm-tagfilter-2.0.0.tgz", + "integrity": "sha512-xHlTOmuCSotIA8TW1mDIM6X2O1SiX5P9IuDtqGonFhEK0qgRI4yeC6vMxEV2dgyr2TiD+2PQ10o+cOhdVAcwfg==", + "license": "MIT", + "dependencies": { + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-task-list-item": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-task-list-item/-/micromark-extension-gfm-task-list-item-2.1.0.tgz", + "integrity": "sha512-qIBZhqxqI6fjLDYFTBIa4eivDMnP+OZqsNwmQ3xNLE4Cxwc+zfQEfbs6tzAo2Hjq+bh6q5F+Z8/cksrLFYWQQw==", + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-factory-destination": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-destination/-/micromark-factory-destination-2.0.1.tgz", + "integrity": "sha512-Xe6rDdJlkmbFRExpTOmRj9N3MaWmbAgdpSrBQvCFqhezUn4AHqJHbaEnfbVYYiexVSs//tqOdY/DxhjdCiJnIA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-label": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-label/-/micromark-factory-label-2.0.1.tgz", + "integrity": "sha512-VFMekyQExqIW7xIChcXn4ok29YE3rnuyveW3wZQWWqF4Nv9Wk5rgJ99KzPvHjkmPXF93FXIbBp6YdW3t71/7Vg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-space": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-space/-/micromark-factory-space-2.0.1.tgz", + "integrity": "sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-title": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-title/-/micromark-factory-title-2.0.1.tgz", + "integrity": "sha512-5bZ+3CjhAd9eChYTHsjy6TGxpOFSKgKKJPJxr293jTbfry2KDoWkhBb6TcPVB4NmzaPhMs1Frm9AZH7OD4Cjzw==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-whitespace": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-whitespace/-/micromark-factory-whitespace-2.0.1.tgz", + "integrity": "sha512-Ob0nuZ3PKt/n0hORHyvoD9uZhr+Za8sFoP+OnMcnWK5lngSzALgQYKMr9RJVOWLqQYuyn6ulqGWSXdwf6F80lQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-character": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz", + "integrity": "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-chunked": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-chunked/-/micromark-util-chunked-2.0.1.tgz", + "integrity": "sha512-QUNFEOPELfmvv+4xiNg2sRYeS/P84pTW0TCgP5zc9FpXetHY0ab7SxKyAQCNCc1eK0459uoLI1y5oO5Vc1dbhA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-classify-character": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-classify-character/-/micromark-util-classify-character-2.0.1.tgz", + "integrity": "sha512-K0kHzM6afW/MbeWYWLjoHQv1sgg2Q9EccHEDzSkxiP/EaagNzCm7T/WMKZ3rjMbvIpvBiZgwR3dKMygtA4mG1Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-combine-extensions": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-combine-extensions/-/micromark-util-combine-extensions-2.0.1.tgz", + "integrity": "sha512-OnAnH8Ujmy59JcyZw8JSbK9cGpdVY44NKgSM7E9Eh7DiLS2E9RNQf0dONaGDzEG9yjEl5hcqeIsj4hfRkLH/Bg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-chunked": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-decode-numeric-character-reference": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/micromark-util-decode-numeric-character-reference/-/micromark-util-decode-numeric-character-reference-2.0.2.tgz", + "integrity": "sha512-ccUbYk6CwVdkmCQMyr64dXz42EfHGkPQlBj5p7YVGzq8I7CtjXZJrubAYezf7Rp+bjPseiROqe7G6foFd+lEuw==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-decode-string": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-decode-string/-/micromark-util-decode-string-2.0.1.tgz", + "integrity": "sha512-nDV/77Fj6eH1ynwscYTOsbK7rR//Uj0bZXBwJZRfaLEJ1iGBR6kIfNmlNqaqJf649EP0F3NWNdeJi03elllNUQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "decode-named-character-reference": "^1.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-decode-numeric-character-reference": "^2.0.0", + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-encode": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-encode/-/micromark-util-encode-2.0.1.tgz", + "integrity": "sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-util-html-tag-name": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-html-tag-name/-/micromark-util-html-tag-name-2.0.1.tgz", + "integrity": "sha512-2cNEiYDhCWKI+Gs9T0Tiysk136SnR13hhO8yW6BGNyhOC4qYFnwF1nKfD3HFAIXA5c45RrIG1ub11GiXeYd1xA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-util-normalize-identifier": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-normalize-identifier/-/micromark-util-normalize-identifier-2.0.1.tgz", + "integrity": "sha512-sxPqmo70LyARJs0w2UclACPUUEqltCkJ6PhKdMIDuJ3gSf/Q+/GIe3WKl0Ijb/GyH9lOpUkRAO2wp0GVkLvS9Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-resolve-all": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-resolve-all/-/micromark-util-resolve-all-2.0.1.tgz", + "integrity": "sha512-VdQyxFWFT2/FGJgwQnJYbe1jjQoNTS4RjglmSjTUlpUMa95Htx9NHeYW4rGDJzbjvCsl9eLjMQwGeElsqmzcHg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-sanitize-uri": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-sanitize-uri/-/micromark-util-sanitize-uri-2.0.1.tgz", + "integrity": "sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-encode": "^2.0.0", + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-subtokenize": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-subtokenize/-/micromark-util-subtokenize-2.1.0.tgz", + "integrity": "sha512-XQLu552iSctvnEcgXw6+Sx75GflAPNED1qx7eBJ+wydBb2KCbRZe+NwvIEEMM83uml1+2WSXpBAcp9IUCgCYWA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-symbol": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", + "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-util-types": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/micromark-util-types/-/micromark-util-types-2.0.2.tgz", + "integrity": "sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/micromatch/node_modules/picomatch": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/mrmime": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.1.tgz", + "integrity": "sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.19", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.19.tgz", + "integrity": "sha512-Y2tUNy4ouw6tq5oDSKeQYGOyhkUBhNOcGV/02KC+6kd9eDGqdZd++mjMiIDilrBYvjEnCYvVtsuHCuP+okSfug==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/neotraverse": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/neotraverse/-/neotraverse-1.0.1.tgz", + "integrity": "sha512-WmmLty1YWwJl9yZi77v2dVIV6X2kuYV8YYBI/G3LWGKdGHmHUvL1z7FW0iDvEvGAwNEoc5x1tOOOyDnf5jJw/w==", + "license": "MIT", + "engines": { + "node": ">= 10" + } + }, + "node_modules/nlcst-to-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/nlcst-to-string/-/nlcst-to-string-4.0.0.tgz", + "integrity": "sha512-YKLBCcUYKAg0FNlOBT6aI91qFmSiFKiluk655WzPF+DDMA02qIyy8uiRqI8QXtcFpEvll12LpL5MXqEmAZ+dcA==", + "license": "MIT", + "dependencies": { + "@types/nlcst": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/node-fetch-native": { + "version": "1.6.7", + "resolved": "https://registry.npmjs.org/node-fetch-native/-/node-fetch-native-1.6.7.tgz", + "integrity": "sha512-g9yhqoedzIUm0nTnTqAQvueMPVOuIY16bqgAJJC8XOOubYFNwz6IER9qs0Gq2Xd0+CecCKFjtdDTMA4u4xG06Q==", + "license": "MIT" + }, + "node_modules/node-mock-http": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/node-mock-http/-/node-mock-http-1.0.5.tgz", + "integrity": "sha512-KQyt/wLjG3TAc7DOUhpqWzgd4ERxR80JOlTK5VE5R1S12IaPVN5qkj4klBce9HPG1Njuup4Sb5bljaT34lIyjw==", + "license": "MIT" + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/nth-check": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", + "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==", + "license": "BSD-2-Clause", + "dependencies": { + "boolbase": "^1.0.0" + }, + "funding": { + "url": "https://github.com/fb55/nth-check?sponsor=1" + } + }, + "node_modules/obug": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/obug/-/obug-3.0.0.tgz", + "integrity": "sha512-5vvB5+W7ePv+p3uqxi+RcW1XAzLW0/hxt3/4X4Lc4qHudzOhmBiBwOY6DRob4WnanAEGvNcLjF+KNOufrUoEQw==", + "funding": [ + "https://github.com/sponsors/sxzz", + "https://opencollective.com/debug" + ], + "license": "MIT", + "engines": { + "node": ">=12.20.0" + } + }, + "node_modules/ofetch": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/ofetch/-/ofetch-1.5.1.tgz", + "integrity": "sha512-2W4oUZlVaqAPAil6FUg/difl6YhqhUR7x2eZY4bQCko22UXg3hptq9KLQdqFClV+Wu85UX7hNtdGTngi/1BxcA==", + "license": "MIT", + "dependencies": { + "destr": "^2.0.5", + "node-fetch-native": "^1.6.7", + "ufo": "^1.6.1" + } + }, + "node_modules/ohash": { + "version": "2.0.12", + "resolved": "https://registry.npmjs.org/ohash/-/ohash-2.0.12.tgz", + "integrity": "sha512-65S/5gk9YSsaRjcyf7Nfa6h/d3E8/1gslpXfI4W7Dxn/oap8IKRuNT5VXkLQ1YFKIEg4apRY4Pj6aiwFzrDdmw==", + "license": "MIT" + }, + "node_modules/oniguruma-parser": { + "version": "0.12.2", + "resolved": "https://registry.npmjs.org/oniguruma-parser/-/oniguruma-parser-0.12.2.tgz", + "integrity": "sha512-6HVa5oIrgMC6aA6WF6XyyqbhRPJrKR02L20+2+zpDtO5QAzGHAUGw5TKQvwi5vctNnRHkJYmjAhRVQF2EKdTQw==", + "license": "MIT" + }, + "node_modules/oniguruma-to-es": { + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/oniguruma-to-es/-/oniguruma-to-es-4.3.6.tgz", + "integrity": "sha512-csuQ9x3Yr0cEIs/Zgx/OEt9iBw9vqIunAPQkx19R/fiMq2oGVTgcMqO/V3Ybqefr1TBvosI6jU539ksaBULJyA==", + "license": "MIT", + "dependencies": { + "oniguruma-parser": "^0.12.2", + "regex": "^6.1.0", + "regex-recursion": "^6.0.2" + } + }, + "node_modules/p-limit": { + "version": "7.3.3", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-7.3.3.tgz", + "integrity": "sha512-SKgVvDU5TNIxQ4r30U75BNLC6a+6GOEjngrk/ysM5+Zu3oJuk1SC5ApzIzY/7PjCEjoVPMWJ0zho/4QUiEB7TQ==", + "license": "MIT", + "dependencies": { + "yocto-queue": "^1.2.1" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-queue": { + "version": "9.3.3", + "resolved": "https://registry.npmjs.org/p-queue/-/p-queue-9.3.3.tgz", + "integrity": "sha512-NXAOdnEe5FsZJfT4oK84lE1Y5cFFdWlRuOo5tww8DyNMxyRXwn39fIkUtNLKppcPC+UYU/bXujNCUGDv01y7CA==", + "license": "MIT", + "dependencies": { + "eventemitter3": "^5.0.4", + "p-timeout": "^7.0.0" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-timeout": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/p-timeout/-/p-timeout-7.0.2.tgz", + "integrity": "sha512-prbX4Z3YszrFNgH+MW5Zoeq3baXrMtP/MQnFeET90UB/GtGcGDQ5Usg9OCy6ETjTTntOw1SL2z9fMPUppN3Guw==", + "license": "MIT", + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/package-manager-detector": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/package-manager-detector/-/package-manager-detector-1.8.0.tgz", + "integrity": "sha512-yQA4H19AmPEoMUeavPMDIe1higySl/gH/yaQrkT/s07Qp+7pp2hYz30N3z2l5BkjVkF9Ow6o0wjJamm2y7Sn0A==", + "license": "MIT" + }, + "node_modules/pagefind": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/pagefind/-/pagefind-1.5.2.tgz", + "integrity": "sha512-XTUaK0hXMCu2jszWE584JGQT7y284TmMV9l/HX3rnG5uo3rHI/uHU56XTyyyPFjeWEBxECbAi0CaFDJOONtG0Q==", + "license": "MIT", + "bin": { + "pagefind": "lib/runner/bin.cjs" + }, + "optionalDependencies": { + "@pagefind/darwin-arm64": "1.5.2", + "@pagefind/darwin-x64": "1.5.2", + "@pagefind/freebsd-x64": "1.5.2", + "@pagefind/linux-arm64": "1.5.2", + "@pagefind/linux-x64": "1.5.2", + "@pagefind/windows-arm64": "1.5.2", + "@pagefind/windows-x64": "1.5.2" + } + }, + "node_modules/parse-entities": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/parse-entities/-/parse-entities-4.0.2.tgz", + "integrity": "sha512-GG2AQYWoLgL877gQIKeRPGO1xF9+eG1ujIb5soS5gPvLQ1y2o8FL90w2QWNdf9I361Mpp7726c+lj3U0qK1uGw==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2.0.0", + "character-entities-legacy": "^3.0.0", + "character-reference-invalid": "^2.0.0", + "decode-named-character-reference": "^1.0.0", + "is-alphanumerical": "^2.0.0", + "is-decimal": "^2.0.0", + "is-hexadecimal": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/parse-entities/node_modules/@types/unist": { + "version": "2.0.11", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.11.tgz", + "integrity": "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==", + "license": "MIT" + }, + "node_modules/parse5": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz", + "integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==", + "license": "MIT", + "dependencies": { + "entities": "^6.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/piccolore": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/piccolore/-/piccolore-0.1.3.tgz", + "integrity": "sha512-o8bTeDWjE086iwKrROaDf31K0qC/BENdm15/uH9usSC/uZjJOKb2YGiVHfLY4GhwsERiPI1jmwI2XrA7ACOxVw==", + "license": "ISC" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.7.tgz", + "integrity": "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.5.28", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.28.tgz", + "integrity": "sha512-RRuzqDtt5Y9h3quz5hWhK+TPnsmVs6WwSU6LkJMeY4HstUEDuYTG8UJSdawMRzmzAtV+KEoG8N3Qg2qLy5vM/A==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "peer": true, + "dependencies": { + "nanoid": "^3.3.18", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/postcss-nested": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.2.0.tgz", + "integrity": "sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "postcss-selector-parser": "^6.1.1" + }, + "engines": { + "node": ">=12.0" + }, + "peerDependencies": { + "postcss": "^8.2.14" + } + }, + "node_modules/postcss-selector-parser": { + "version": "6.1.4", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.4.tgz", + "integrity": "sha512-bIoJLOmjCO1S9XdY/DcnR5hJxvrDir1PbGChrzXG3vw0/FOliy/fA3dmdhQ441kah4gKv+TwckGzex6wNS5cnQ==", + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/prismjs": { + "version": "1.30.0", + "resolved": "https://registry.npmjs.org/prismjs/-/prismjs-1.30.0.tgz", + "integrity": "sha512-DEvV2ZF2r2/63V+tK8hQvrR2ZGn10srHbXviTlcv7Kpzw8jWiNTqbVgjO3IY8RxrrOUF8VPMQQFysYYYv0YZxw==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/process-ancestry": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/process-ancestry/-/process-ancestry-0.1.0.tgz", + "integrity": "sha512-tGqJW/UnclpYASFcM6Xh8D8l/BMtaQ9+CSG0vlJSJTcdMM4lDRv4c6H0Pdcsfted+bVczdYSfk2fdukg2gQkZg==", + "license": "MIT", + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/property-information": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/property-information/-/property-information-7.2.0.tgz", + "integrity": "sha512-IAtzIB6sUiWaJYrX9smp3V46pBGbBeLFRGdh25kg1334VcBlD8HzhPeNIWQH9zhGmo2itIe25EHt9dQP7G5hmg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/radix3": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/radix3/-/radix3-1.1.2.tgz", + "integrity": "sha512-b484I/7b8rDEdSDKckSSBA8knMpcdsXudlE/LNL639wFoHKwLbEkQFZHWEYwDC0wa0FKUcCY+GAF73Z7wxNVFA==", + "license": "MIT" + }, + "node_modules/readdirp": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-5.1.1.tgz", + "integrity": "sha512-Kko+Y5XQ6fM+Ce3dq3m9YGxnacYZYl9cA1wZjaF3Vbry2L3i1qVg8+CAgNPsXRArPMUMCaOR7oa9Nqntc43JKA==", + "license": "MIT", + "engines": { + "node": ">= 20.19.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/regex": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/regex/-/regex-6.1.0.tgz", + "integrity": "sha512-6VwtthbV4o/7+OaAF9I5L5V3llLEsoPyq9P1JVXkedTP33c7MfCG0/5NOPcSJn0TzXcG9YUrR0gQSWioew3LDg==", + "license": "MIT", + "dependencies": { + "regex-utilities": "^2.3.0" + } + }, + "node_modules/regex-recursion": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/regex-recursion/-/regex-recursion-6.0.2.tgz", + "integrity": "sha512-0YCaSCq2VRIebiaUviZNs0cBz1kg5kVS2UKUfNIx8YVs1cN3AV7NTctO5FOKBA+UT2BPJIWZauYHPqJODG50cg==", + "license": "MIT", + "dependencies": { + "regex-utilities": "^2.3.0" + } + }, + "node_modules/regex-utilities": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/regex-utilities/-/regex-utilities-2.3.0.tgz", + "integrity": "sha512-8VhliFJAWRaUiVvREIiW2NXXTmHs4vMNnSzuJVhscgmGav3g9VDxLrQndI3dZZVVdp0ZO/5v0xmX516/7M9cng==", + "license": "MIT" + }, + "node_modules/rehype-expressive-code": { + "version": "0.44.2", + "resolved": "https://registry.npmjs.org/rehype-expressive-code/-/rehype-expressive-code-0.44.2.tgz", + "integrity": "sha512-jvjQpsfWKBquDrEl5YQA3CJybRRIuDOYsui1BJTs5oKDUK8pPkJSSxY8FjxNlIFIdtgrTjC1zuahuY4Cc1j9SQ==", + "license": "MIT", + "dependencies": { + "expressive-code": "^0.44.2" + } + }, + "node_modules/rehype-minify-whitespace": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/rehype-minify-whitespace/-/rehype-minify-whitespace-6.0.2.tgz", + "integrity": "sha512-Zk0pyQ06A3Lyxhe9vGtOtzz3Z0+qZ5+7icZ/PL/2x1SHPbKao5oB/g/rlc6BCTajqBb33JcOe71Ye1oFsuYbnw==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "hast-util-minify-whitespace": "^1.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/rehype-parse": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/rehype-parse/-/rehype-parse-9.0.1.tgz", + "integrity": "sha512-ksCzCD0Fgfh7trPDxr2rSylbwq9iYDkSn8TCDmEJ49ljEUBxDVCzCHv7QNzZOfODanX4+bWQ4WZqLCRWYLfhag==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "hast-util-from-html": "^2.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/rehype-remark": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/rehype-remark/-/rehype-remark-10.0.1.tgz", + "integrity": "sha512-EmDndlb5NVwXGfUa4c9GPK+lXeItTilLhE6ADSaQuHr4JUlKw9MidzGzx4HpqZrNCt6vnHmEifXQiiA+CEnjYQ==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "hast-util-to-mdast": "^10.0.0", + "unified": "^11.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-directive": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/remark-directive/-/remark-directive-4.0.0.tgz", + "integrity": "sha512-7sxn4RfF1o3izevPV1DheyGDD6X4c9hrGpfdUpm7uC++dqrnJxIZVkk7CoKqcLm0VUMAuOol7Mno3m6g8cfMuA==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "mdast-util-directive": "^3.0.0", + "micromark-extension-directive": "^4.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-gfm": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/remark-gfm/-/remark-gfm-4.0.1.tgz", + "integrity": "sha512-1quofZ2RQ9EWdeN34S79+KExV1764+wCUGop5CPL1WGdD0ocPpu91lzPGbwWMECpEpd42kJGQwzRfyov9j4yNg==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "mdast-util-gfm": "^3.0.0", + "micromark-extension-gfm": "^3.0.0", + "remark-parse": "^11.0.0", + "remark-stringify": "^11.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-parse": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/remark-parse/-/remark-parse-11.0.0.tgz", + "integrity": "sha512-FCxlKLNGknS5ba/1lmpYijMUzX2esxW5xQqjWxw2eHFfS2MSdaHVINFmhjo+qN1WhZhNimq0dZATN9pH0IDrpA==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "mdast-util-from-markdown": "^2.0.0", + "micromark-util-types": "^2.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-stringify": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/remark-stringify/-/remark-stringify-11.0.0.tgz", + "integrity": "sha512-1OSmLd3awB/t8qdoEOMazZkNsfVTeY4fTsgzcQFdXNq8ToTN4ZGwrMnlda4K6smTFKD+GRV6O48i6Z4iKgPPpw==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "mdast-util-to-markdown": "^2.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/resolve-pkg-maps": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", + "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", + "license": "MIT", + "funding": { + "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" + } + }, + "node_modules/retext-smartypants": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/retext-smartypants/-/retext-smartypants-6.2.0.tgz", + "integrity": "sha512-kk0jOU7+zGv//kfjXEBjdIryL1Acl4i9XNkHxtM7Tm5lFiCog576fjNC9hjoR7LTKQ0DsPWy09JummSsH1uqfQ==", + "license": "MIT", + "dependencies": { + "@types/nlcst": "^2.0.0", + "nlcst-to-string": "^4.0.0", + "unist-util-visit": "^5.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/rolldown": { + "version": "1.2.11", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.2.11.tgz", + "integrity": "sha512-qpSwIyz0jHQq5qXBTNxFmE6664rJ7O+4TvPFOiOaBSrz8IOHc1koKKSqTM2H6u1UG1+TveuC6vaDHKXFOvb1Kw==", + "license": "MIT", + "dependencies": { + "@oxc-project/types": "=0.151.0", + "@rolldown/pluginutils": "^1.0.0" + }, + "bin": { + "rolldown": "bin/cli.mjs" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "optionalDependencies": { + "@rolldown/binding-android-arm-eabi": "1.2.11", + "@rolldown/binding-android-arm64": "1.2.11", + "@rolldown/binding-darwin-arm64": "1.2.11", + "@rolldown/binding-darwin-x64": "1.2.11", + "@rolldown/binding-freebsd-x64": "1.2.11", + "@rolldown/binding-linux-arm-gnueabihf": "1.2.11", + "@rolldown/binding-linux-arm64-gnu": "1.2.11", + "@rolldown/binding-linux-arm64-musl": "1.2.11", + "@rolldown/binding-linux-ppc64-gnu": "1.2.11", + "@rolldown/binding-linux-s390x-gnu": "1.2.11", + "@rolldown/binding-linux-x64-gnu": "1.2.11", + "@rolldown/binding-linux-x64-musl": "1.2.11", + "@rolldown/binding-openharmony-arm64": "1.2.11", + "@rolldown/binding-win32-arm64-msvc": "1.2.11", + "@rolldown/binding-win32-x64-msvc": "1.2.11" + } + }, + "node_modules/satteri": { + "version": "0.10.5", + "resolved": "https://registry.npmjs.org/satteri/-/satteri-0.10.5.tgz", + "integrity": "sha512-Ao1LKpAEa9Wdg0otgbVKViZHEq9ebdXe4DMrp3s9vQAU0HNIuHnFEuMuOcm0ZIXyV0Yzxj91NvhLpvXZJO/5ZQ==", + "license": "MIT", + "dependencies": { + "@types/estree-jsx": "^1.0.5", + "@types/hast": "^3.0.5", + "@types/mdast": "^4.0.4", + "@types/unist": "^3.0.3" + }, + "optionalDependencies": { + "@bruits/satteri-darwin-arm64": "0.10.5", + "@bruits/satteri-darwin-x64": "0.10.5", + "@bruits/satteri-linux-arm64-gnu": "0.10.5", + "@bruits/satteri-linux-arm64-musl": "0.10.5", + "@bruits/satteri-linux-x64-gnu": "0.10.5", + "@bruits/satteri-linux-x64-musl": "0.10.5", + "@bruits/satteri-wasm32-wasi": "0.10.5", + "@bruits/satteri-win32-arm64-msvc": "0.10.5", + "@bruits/satteri-win32-x64-msvc": "0.10.5" + } + }, + "node_modules/sax": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.6.1.tgz", + "integrity": "sha512-42tBVwLWnaQvW5zc4HbZrTuWccECCZfBi92FDuwtqxasH+JbPB3/FOKb1m222K42R4WxuxzzMsTswfzgtSu64Q==", + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=11.0.0" + } + }, + "node_modules/semver": { + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/sharp": { + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.35.4.tgz", + "integrity": "sha512-n++8XWcj+jCOr2IOl7h8LbKnGBDY4aPbmprMONBNFdn0ImXqpGVv5zliDs0V9HbmbCQLpbuo2ej9rAoOQTvMDA==", + "license": "Apache-2.0", + "dependencies": { + "@img/colour": "^1.1.0", + "detect-libc": "^2.1.2", + "semver": "^7.8.5" + }, + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-darwin-arm64": "0.35.4", + "@img/sharp-darwin-x64": "0.35.4", + "@img/sharp-freebsd-wasm32": "0.35.4", + "@img/sharp-libvips-darwin-arm64": "1.3.3", + "@img/sharp-libvips-darwin-x64": "1.3.3", + "@img/sharp-libvips-linux-arm": "1.3.3", + "@img/sharp-libvips-linux-arm64": "1.3.3", + "@img/sharp-libvips-linux-ppc64": "1.3.3", + "@img/sharp-libvips-linux-riscv64": "1.3.3", + "@img/sharp-libvips-linux-s390x": "1.3.3", + "@img/sharp-libvips-linux-x64": "1.3.3", + "@img/sharp-libvips-linuxmusl-arm64": "1.3.3", + "@img/sharp-libvips-linuxmusl-x64": "1.3.3", + "@img/sharp-linux-arm": "0.35.4", + "@img/sharp-linux-arm64": "0.35.4", + "@img/sharp-linux-ppc64": "0.35.4", + "@img/sharp-linux-riscv64": "0.35.4", + "@img/sharp-linux-s390x": "0.35.4", + "@img/sharp-linux-x64": "0.35.4", + "@img/sharp-linuxmusl-arm64": "0.35.4", + "@img/sharp-linuxmusl-x64": "0.35.4", + "@img/sharp-webcontainers-wasm32": "0.35.4", + "@img/sharp-win32-arm64": "0.35.4", + "@img/sharp-win32-ia32": "0.35.4", + "@img/sharp-win32-x64": "0.35.4" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/shiki": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/shiki/-/shiki-4.4.3.tgz", + "integrity": "sha512-Mb/GvXPHBAXdgGIcnfU5L3ldpn1XcxrGkPHwqgRx17/I2XRfqlFKk2vGkHWINn1kdXvzJZeuO3is6I9KLPFm0g==", + "license": "MIT", + "dependencies": { + "@shikijs/core": "4.4.3", + "@shikijs/engine-javascript": "4.4.3", + "@shikijs/engine-oniguruma": "4.4.3", + "@shikijs/langs": "4.4.3", + "@shikijs/themes": "4.4.3", + "@shikijs/types": "4.4.3", + "@shikijs/vscode-textmate": "^10.0.2", + "@types/hast": "^3.0.5" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/sisteransi": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", + "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==", + "license": "MIT" + }, + "node_modules/sitemap": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/sitemap/-/sitemap-9.0.1.tgz", + "integrity": "sha512-S6hzjGJSG3d6if0YoF5kTyeRJvia6FSTBroE5fQ0bu1QNxyJqhhinfUsXi9fH3MgtXODWvwo2BDyQSnhPQ88uQ==", + "license": "MIT", + "dependencies": { + "@types/node": "^24.9.2", + "@types/sax": "^1.2.1", + "arg": "^5.0.0", + "sax": "^1.4.1" + }, + "bin": { + "sitemap": "dist/esm/cli.js" + }, + "engines": { + "node": ">=20.19.5", + "npm": ">=10.8.2" + } + }, + "node_modules/smol-toml": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/smol-toml/-/smol-toml-1.9.0.tgz", + "integrity": "sha512-hpd+HLON7HdZXqYchMM/+LaTTbdK0AU3NngIJ4KVyWbY9bfQqdL9cD+4yf6dUoU2Ap4VsU0JkQi6FxAI1B2mXQ==", + "license": "BSD-3-Clause", + "engines": { + "node": ">= 18" + }, + "funding": { + "url": "https://github.com/sponsors/cyyynthia" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/space-separated-tokens": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/space-separated-tokens/-/space-separated-tokens-2.0.2.tgz", + "integrity": "sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/starlight-llms-txt": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/starlight-llms-txt/-/starlight-llms-txt-0.12.0.tgz", + "integrity": "sha512-ya4c0ZoiPUXuHDzd3AYCKh78Y3II3EToSE9Yp6uVN0c2UUKoNpqTjijhW86W/4toZzSUhWSnxeYs7QBcXNIPGg==", + "license": "MIT", + "dependencies": { + "@astrojs/mdx": "^8.0.1", + "@types/hast": "^3.0.5", + "@types/micromatch": "^4.0.10", + "github-slugger": "^2.0.0", + "hast-util-select": "^6.0.4", + "micromatch": "^4.0.8", + "rehype-parse": "^9.0.1", + "rehype-remark": "^10.0.1", + "remark-gfm": "^4.0.1", + "remark-stringify": "^11.0.0", + "unified": "^11.0.5", + "unist-util-remove": "^4.0.0" + }, + "peerDependencies": { + "@astrojs/starlight": ">=0.41.0", + "astro": "^7.0.0" + } + }, + "node_modules/stringify-entities": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/stringify-entities/-/stringify-entities-4.0.4.tgz", + "integrity": "sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==", + "license": "MIT", + "dependencies": { + "character-entities-html4": "^2.0.0", + "character-entities-legacy": "^3.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/svgo": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/svgo/-/svgo-4.1.0.tgz", + "integrity": "sha512-bkxnTg1kSU0guhIBmibA6UUhrQmPVA1XsQLN+ylCd+UWzbnLkySOcXpyk1mrl05f+pcaCx2eHb+sp6BgMZWX+Q==", + "license": "MIT", + "dependencies": { + "commander": "^11.1.0", + "css-select": "^6.0.0", + "css-tree": "^3.0.1", + "css-what": "^7.0.0", + "csso": "^5.0.5", + "picocolors": "^1.1.1", + "sax": "1.6.1" + }, + "bin": { + "svgo": "bin/svgo.js" + }, + "engines": { + "node": ">=16" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/svgo" + } + }, + "node_modules/tiny-inflate": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/tiny-inflate/-/tiny-inflate-1.0.3.tgz", + "integrity": "sha512-pkY1fj1cKHb2seWDy0B16HeWyczlJA9/WW3u3c4z/NiWDsO3DOU5D7nhTLE9CF0yXv/QZFY7sEJmj24dK+Rrqw==", + "license": "MIT" + }, + "node_modules/tinyclip": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/tinyclip/-/tinyclip-1.0.3.tgz", + "integrity": "sha512-nTv2raKFMzGG6JJp0lpNDsRPdfFE6UHlu2rVEjXo8vkx1ZS3LW2CVXqIaLu8xZlu+6tbFs9V58Qc/Jmk/Ii7wA==", + "license": "MIT", + "engines": { + "node": "^16.14.0 || >= 17.3.0" + } + }, + "node_modules/tinyexec": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.3.1.tgz", + "integrity": "sha512-GCvB3aoys96IuDFBMcTB46JOR6mdMtAToqwiW8JlWhsoh1mhHi/xn9ss/Dg7N555GiJyEt2qzoG/NHCwM6h1EA==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/trim-lines": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/trim-lines/-/trim-lines-3.0.1.tgz", + "integrity": "sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/trim-trailing-lines": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/trim-trailing-lines/-/trim-trailing-lines-2.1.0.tgz", + "integrity": "sha512-5UR5Biq4VlVOtzqkm2AZlgvSlDJtME46uV0br0gENbwN4l5+mMKT4b9gJKqWtuL2zAIqajGJGuvbCbcAJUZqBg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/trough": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/trough/-/trough-2.2.0.tgz", + "integrity": "sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD", + "optional": true + }, + "node_modules/ufo": { + "version": "1.6.4", + "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.6.4.tgz", + "integrity": "sha512-JFNbkD1Svwe0KvGi8GOeLcP4kAWQ609twvCdcHxq1oSL8svv39ZuSvajcD8B+5D0eL4+s1Is2D/O6KN3qcTeRA==", + "license": "MIT" + }, + "node_modules/ultrahtml": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/ultrahtml/-/ultrahtml-1.7.0.tgz", + "integrity": "sha512-2xRd0VHoAQE4M+vF/DvFFB7pUV0ZxTW1TLi7lHQWnF/Sb5TPeEUV/l+hxcNnGO00ZXGnR0voCMmYRKQf+rvJ2g==", + "license": "MIT" + }, + "node_modules/uncrypto": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/uncrypto/-/uncrypto-0.1.3.tgz", + "integrity": "sha512-Ql87qFHB3s/De2ClA9e0gsnS6zXG27SkTiSJwjCc9MebbfapQfuPzumMIUMi38ezPZVNFcHI9sUIepeQfw8J8Q==", + "license": "MIT" + }, + "node_modules/undici": { + "version": "8.11.2", + "resolved": "https://registry.npmjs.org/undici/-/undici-8.11.2.tgz", + "integrity": "sha512-u4UB2/IrKdU6lFxumHmmo1a3fCQO5tzQllRorfoRS63txhrB7xTpSn1PftwC4qEHkOaqP95fCWW4lJzwErwzhQ==", + "license": "MIT", + "engines": { + "node": ">=22.19.0" + } + }, + "node_modules/undici-types": { + "version": "7.18.2", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.18.2.tgz", + "integrity": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==", + "license": "MIT" + }, + "node_modules/unified": { + "version": "11.0.5", + "resolved": "https://registry.npmjs.org/unified/-/unified-11.0.5.tgz", + "integrity": "sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "bail": "^2.0.0", + "devlop": "^1.0.0", + "extend": "^3.0.0", + "is-plain-obj": "^4.0.0", + "trough": "^2.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unifont": { + "version": "0.7.5", + "resolved": "https://registry.npmjs.org/unifont/-/unifont-0.7.5.tgz", + "integrity": "sha512-ULe/Cs+ZIsq+dcFofNkhqielCrUJnb5mr+Yc4EBM2VlL+6OZR6+cjtI2mT1bJvRBrVncqHAbLURxmPLcCXzWMg==", + "license": "MIT", + "dependencies": { + "css-tree": "^3.1.0", + "ohash": "^2.0.11", + "undici": "^8.0.0" + } + }, + "node_modules/unist-util-find-after": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/unist-util-find-after/-/unist-util-find-after-5.0.0.tgz", + "integrity": "sha512-amQa0Ep2m6hE2g72AugUItjbuM8X8cGQnFoHk0pGfrFeT9GZhzN5SW8nRsiGKK7Aif4CrACPENkA6P/Lw6fHGQ==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-is": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-is": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-6.0.1.tgz", + "integrity": "sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-position": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/unist-util-position/-/unist-util-position-5.0.0.tgz", + "integrity": "sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-remove": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/unist-util-remove/-/unist-util-remove-4.0.0.tgz", + "integrity": "sha512-b4gokeGId57UVRX/eVKej5gXqGlc9+trkORhFJpu9raqZkZhU0zm8Doi05+HaiBsMEIJowL+2WtQ5ItjsngPXg==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-is": "^6.0.0", + "unist-util-visit-parents": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-stringify-position": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-4.0.0.tgz", + "integrity": "sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-visit": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-5.1.0.tgz", + "integrity": "sha512-m+vIdyeCOpdr/QeQCu2EzxX/ohgS8KbnPDgFni4dQsfSCtpz8UqDyY5GjRru8PDKuYn7Fq19j1CQ+nJSsGKOzg==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-is": "^6.0.0", + "unist-util-visit-parents": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-visit-parents": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-6.0.2.tgz", + "integrity": "sha512-goh1s1TBrqSqukSc8wrjwWhL0hiJxgA8m4kFxGlQ+8FYQ3C/m11FcTs4YYem7V664AhHVvgoQLk890Ssdsr2IQ==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-is": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unstorage": { + "version": "1.17.5", + "resolved": "https://registry.npmjs.org/unstorage/-/unstorage-1.17.5.tgz", + "integrity": "sha512-0i3iqvRfx29hkNntHyQvJTpf5W9dQ9ZadSoRU8+xVlhVtT7jAX57fazYO9EHvcRCfBCyi5YRya7XCDOsbTgkPg==", + "license": "MIT", + "dependencies": { + "anymatch": "^3.1.3", + "chokidar": "^5.0.0", + "destr": "^2.0.5", + "h3": "^1.15.10", + "lru-cache": "^11.2.7", + "node-fetch-native": "^1.6.7", + "ofetch": "^1.5.1", + "ufo": "^1.6.3" + }, + "peerDependencies": { + "@azure/app-configuration": "^1.8.0", + "@azure/cosmos": "^4.2.0", + "@azure/data-tables": "^13.3.0", + "@azure/identity": "^4.6.0", + "@azure/keyvault-secrets": "^4.9.0", + "@azure/storage-blob": "^12.26.0", + "@capacitor/preferences": "^6 || ^7 || ^8", + "@deno/kv": ">=0.9.0", + "@netlify/blobs": "^6.5.0 || ^7.0.0 || ^8.1.0 || ^9.0.0 || ^10.0.0", + "@planetscale/database": "^1.19.0", + "@upstash/redis": "^1.34.3", + "@vercel/blob": ">=0.27.1", + "@vercel/functions": "^2.2.12 || ^3.0.0", + "@vercel/kv": "^1 || ^2 || ^3", + "aws4fetch": "^1.0.20", + "db0": ">=0.2.1", + "idb-keyval": "^6.2.1", + "ioredis": "^5.4.2", + "uploadthing": "^7.4.4" + }, + "peerDependenciesMeta": { + "@azure/app-configuration": { + "optional": true + }, + "@azure/cosmos": { + "optional": true + }, + "@azure/data-tables": { + "optional": true + }, + "@azure/identity": { + "optional": true + }, + "@azure/keyvault-secrets": { + "optional": true + }, + "@azure/storage-blob": { + "optional": true + }, + "@capacitor/preferences": { + "optional": true + }, + "@deno/kv": { + "optional": true + }, + "@netlify/blobs": { + "optional": true + }, + "@planetscale/database": { + "optional": true + }, + "@upstash/redis": { + "optional": true + }, + "@vercel/blob": { + "optional": true + }, + "@vercel/functions": { + "optional": true + }, + "@vercel/kv": { + "optional": true + }, + "aws4fetch": { + "optional": true + }, + "db0": { + "optional": true + }, + "idb-keyval": { + "optional": true + }, + "ioredis": { + "optional": true + }, + "uploadthing": { + "optional": true + } + } + }, + "node_modules/url-extras": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/url-extras/-/url-extras-0.1.0.tgz", + "integrity": "sha512-8tzwTeXFPuX/5PHuCDQE5Dd9Ts4rwoq2t9aIT+HS4iAVpmj5l4Ao7Q+BuuFjvWRqrLswBhQDk8O96ZicgCqQqw==", + "license": "MIT", + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "license": "MIT" + }, + "node_modules/verkit": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/verkit/-/verkit-0.4.1.tgz", + "integrity": "sha512-mMVzj0TXExtVdlDEq+Mzp0eyOuyznNpFobNM3uAqe3HVm/Ps2c+u17BligMkZjZCA6EiXpoon8iRTer3iu40SQ==", + "license": "MIT", + "engines": { + "node": ">=18.12.0" + }, + "funding": { + "url": "https://github.com/sponsors/sxzz" + } + }, + "node_modules/vfile": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/vfile/-/vfile-6.0.3.tgz", + "integrity": "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/vfile-location": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/vfile-location/-/vfile-location-5.0.3.tgz", + "integrity": "sha512-5yXvWDEgqeiYiBe1lbxYF7UMAIm/IcopxMHrMQDq3nvKcjPKIhZklUKL+AE7J7uApI4kwe2snsK+eI6UTj9EHg==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/vfile-message": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-4.0.3.tgz", + "integrity": "sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-stringify-position": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/vite": { + "version": "8.3.1", + "resolved": "https://registry.npmjs.org/vite/-/vite-8.3.1.tgz", + "integrity": "sha512-/bvH9E9tmCXRGp2uXY3WbOldqpTwFkbha/8ANaEQ6VkxhH60KyqLwgZq6lG2y+4uT55x9+9eUHMpQ7uGnOCKjA==", + "license": "MIT", + "peer": true, + "dependencies": { + "lightningcss": "^1.33.0", + "picomatch": "^4.0.7", + "postcss": "^8.5.28", + "rolldown": "~1.2.9", + "tinyglobby": "^0.2.17" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "@vitejs/devtools": "^0.7.1", + "esbuild": "^0.27.0 || ^0.28.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "@vitejs/devtools": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vitefu": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/vitefu/-/vitefu-1.1.3.tgz", + "integrity": "sha512-ub4okH7Z5KLjb6hDyjqrGXqWtWvoYdU3IGm/NorpgHncKoLTCfRIbvlhBm7r0YstIaQRYlp4yEbFqDcKSzXSSg==", + "license": "MIT", + "workspaces": [ + "tests/deps/*", + "tests/projects/*", + "tests/projects/workspace/packages/*" + ], + "peerDependencies": { + "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "vite": { + "optional": true + } + } + }, + "node_modules/web-namespaces": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/web-namespaces/-/web-namespaces-2.0.1.tgz", + "integrity": "sha512-bKr1DkiNa2krS7qxNtdrtHAmzuYGFQLiQ13TsorsdT6ULTkPLKuu5+GsFpDlg6JFjUTwX2DyhMPG2be8uPrqsQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/xxhash-wasm": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/xxhash-wasm/-/xxhash-wasm-1.1.0.tgz", + "integrity": "sha512-147y/6YNh+tlp6nd/2pWq38i9h6mz/EuQ6njIrmW8D1BS5nCqs0P6DG+m6zTGnNz5I+uhZ0SHxBs9BsPrwcKDA==", + "license": "MIT" + }, + "node_modules/yargs-parser": { + "version": "22.0.0", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-22.0.0.tgz", + "integrity": "sha512-rwu/ClNdSMpkSrUb+d6BRsSkLUq1fmfsY6TOpYzTwvwkg1/NRG85KBy3kq++A8LKQwX6lsu+aWad+2khvuXrqw==", + "license": "ISC", + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=23" + } + }, + "node_modules/yocto-queue": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-1.2.2.tgz", + "integrity": "sha512-4LCcse/U2MHZ63HAJVE+v71o7yOdIe4cZ70Wpf8D/IyjDKYQLV5GD46B+hSTjJsvV5PztjvHoU580EftxjDZFQ==", + "license": "MIT", + "engines": { + "node": ">=12.20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/zod": { + "version": "4.6.5", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.6.5.tgz", + "integrity": "sha512-v5l/aFXZQeai4awLbOpSoHecE9UiMrnfx75tEXLjNonXVARxQ5mOeipTjROUchszUNCqnE+hqAMujRsRHsut2Q==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/zwitch": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/zwitch/-/zwitch-2.0.4.tgz", + "integrity": "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + } + } +} diff --git a/website/package.json b/website/package.json new file mode 100644 index 0000000000..0576825801 --- /dev/null +++ b/website/package.json @@ -0,0 +1,20 @@ +{ + "name": "sqlbuild-docs-site", + "private": true, + "type": "module", + "engines": { + "node": ">=22.12.0" + }, + "scripts": { + "dev": "astro dev", + "build": "astro build", + "preview": "astro preview" + }, + "dependencies": { + "@astrojs/starlight": "^0.42.4", + "@lucide/astro": "^1.48.0", + "astro": "^7.3.5", + "sharp": "^0.35.4", + "starlight-llms-txt": "^0.12.0" + } +} diff --git a/website/public/CNAME b/website/public/CNAME new file mode 100644 index 0000000000..7a292cb212 --- /dev/null +++ b/website/public/CNAME @@ -0,0 +1 @@ +sqlbuild.com diff --git a/website/public/assets/icon.png b/website/public/assets/icon.png new file mode 100644 index 0000000000..0e8ce0c2af Binary files /dev/null and b/website/public/assets/icon.png differ diff --git a/website/public/assets/logo-dark.png b/website/public/assets/logo-dark.png new file mode 100644 index 0000000000..9597c390cf Binary files /dev/null and b/website/public/assets/logo-dark.png differ diff --git a/website/public/assets/logo-light.png b/website/public/assets/logo-light.png new file mode 100644 index 0000000000..f2eb8b64dd Binary files /dev/null and b/website/public/assets/logo-light.png differ diff --git a/website/public/assets/og-icon.png b/website/public/assets/og-icon.png new file mode 100644 index 0000000000..68eed1855b Binary files /dev/null and b/website/public/assets/og-icon.png differ diff --git a/website/public/dlt-declarative.png b/website/public/dlt-declarative.png new file mode 100644 index 0000000000..795e810d23 Binary files /dev/null and b/website/public/dlt-declarative.png differ diff --git a/website/public/favicon.png b/website/public/favicon.png new file mode 100644 index 0000000000..0e8ce0c2af Binary files /dev/null and b/website/public/favicon.png differ diff --git a/website/public/fonts/LICENSE-DejaVu.txt b/website/public/fonts/LICENSE-DejaVu.txt new file mode 100644 index 0000000000..b3d93a1235 --- /dev/null +++ b/website/public/fonts/LICENSE-DejaVu.txt @@ -0,0 +1,78 @@ +Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/ +Upstream-Name: DejaVu fonts +Upstream-Author: Stepan Roh (original author), + see /usr/share/doc/fonts-dejavu-core/AUTHORS for full list +Source: https://dejavu-fonts.github.io/ + +Files: * +Copyright: Copyright (c) 2003 by Bitstream, Inc. All Rights Reserved. + Bitstream Vera is a trademark of Bitstream, Inc. + DejaVu changes are in public domain. +License: bitstream-vera + Permission is hereby granted, free of charge, to any person obtaining a copy + of the fonts accompanying this license ("Fonts") and associated + documentation files (the "Font Software"), to reproduce and distribute the + Font Software, including without limitation the rights to use, copy, merge, + publish, distribute, and/or sell copies of the Font Software, and to permit + persons to whom the Font Software is furnished to do so, subject to the + following conditions: + . + The above copyright and trademark notices and this permission notice shall + be included in all copies of one or more of the Font Software typefaces. + . + The Font Software may be modified, altered, or added to, and in particular + the designs of glyphs or characters in the Fonts may be modified and + additional glyphs or characters may be added to the Fonts, only if the fonts + are renamed to names not containing either the words "Bitstream" or the word + "Vera". + . + This License becomes null and void to the extent applicable to Fonts or Font + Software that has been modified and is distributed under the "Bitstream + Vera" names. + . + The Font Software may be sold as part of a larger software package but no + copy of one or more of the Font Software typefaces may be sold by itself. + . + THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS + OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT OF COPYRIGHT, PATENT, + TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL BITSTREAM OR THE GNOME + FOUNDATION BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, INCLUDING + ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL DAMAGES, + WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF + THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM OTHER DEALINGS IN THE + FONT SOFTWARE. + . + Except as contained in this notice, the names of Gnome, the Gnome + Foundation, and Bitstream Inc., shall not be used in advertising or + otherwise to promote the sale, use or other dealings in this Font Software + without prior written authorization from the Gnome Foundation or Bitstream + Inc., respectively. For further information, contact: fonts at gnome dot + org. + +Files: debian/* +Copyright: (C) 2005-2006 Peter Cernak + (C) 2006-2011 Davide Viti + (C) 2011-2013 Christian Perrier + (C) 2013 Fabian Greffrath +License: GPL-2+ + This program is free software; you can redistribute it + and/or modify it under the terms of the GNU General Public + License as published by the Free Software Foundation; either + version 2 of the License, or (at your option) any later + version. + . + This program is distributed in the hope that it will be + useful, but WITHOUT ANY WARRANTY; without even the implied + warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR + PURPOSE. See the GNU General Public License for more + details. + . + You should have received a copy of the GNU General Public + License along with this package; if not, write to the Free + Software Foundation, Inc., 51 Franklin St, Fifth Floor, + Boston, MA 02110-1301 USA + . + On Debian systems, the full text of the GNU General Public + License version 2 can be found in the file + /usr/share/common-licenses/GPL-2'. diff --git a/website/public/fonts/LICENSE-IBM-Plex-Mono.txt b/website/public/fonts/LICENSE-IBM-Plex-Mono.txt new file mode 100644 index 0000000000..83eac8278b --- /dev/null +++ b/website/public/fonts/LICENSE-IBM-Plex-Mono.txt @@ -0,0 +1,93 @@ +Copyright 2017 IBM Corp. All rights reserved. IBMPlexMono-ThinItalic.ttf: Copyright 2017 IBM Corp. All rights reserved. IBMPlexMono-ExtraLight.ttf: Copyright 2017 IBM Corp. All rights reserved. IBMPlexMono-ExtraLightItalic.ttf: Copyright 2017 IBM Corp. All rights reserved. IBMPlexMono-Light.ttf: Copyright 2017 IBM Corp. All rights reserved. IBMPlexMono-LightItalic.ttf: Copyright 2017 IBM Corp. All rights reserved. IBMPlexMono-Regular.ttf: Copyright 2017 IBM Corp. All rights reserved. IBMPlexMono-Italic.ttf: Copyright 2017 IBM Corp. All rights reserved. IBMPlexMono-Medium.ttf: Copyright 2017 IBM Corp. All rights reserved. IBMPlexMono-MediumItalic.ttf: Copyright 2017 IBM Corp. All rights reserved. IBMPlexMono-SemiBold.ttf: Copyright 2017 IBM Corp. All rights reserved. IBMPlexMono-SemiBoldItalic.ttf: Copyright 2017 IBM Corp. All rights reserved. IBMPlexMono-Bold.ttf: Copyright 2017 IBM Corp. All rights reserved. IBMPlexMono-BoldItalic.ttf: Copyright 2017 IBM Corp. All rights reserved. + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +http://scripts.sil.org/OFL + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/website/public/fonts/LICENSE-Inter.txt b/website/public/fonts/LICENSE-Inter.txt new file mode 100644 index 0000000000..40589daa9d --- /dev/null +++ b/website/public/fonts/LICENSE-Inter.txt @@ -0,0 +1,93 @@ +Copyright 2016 The Inter Project Authors (https://github.com/rsms/inter) Inter-Italic[opsz,wght].ttf: Copyright 2016 The Inter Project Authors (https://github.com/rsms/inter) + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +http://scripts.sil.org/OFL + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/website/public/fonts/dejavu-sans-mono-diagram.woff2 b/website/public/fonts/dejavu-sans-mono-diagram.woff2 new file mode 100644 index 0000000000..de6e29f14f Binary files /dev/null and b/website/public/fonts/dejavu-sans-mono-diagram.woff2 differ diff --git a/website/public/fonts/ibm-plex-mono-latin-400-normal.woff2 b/website/public/fonts/ibm-plex-mono-latin-400-normal.woff2 new file mode 100644 index 0000000000..0804aaff92 Binary files /dev/null and b/website/public/fonts/ibm-plex-mono-latin-400-normal.woff2 differ diff --git a/website/public/fonts/ibm-plex-mono-latin-500-normal.woff2 b/website/public/fonts/ibm-plex-mono-latin-500-normal.woff2 new file mode 100644 index 0000000000..090f82f7ec Binary files /dev/null and b/website/public/fonts/ibm-plex-mono-latin-500-normal.woff2 differ diff --git a/website/public/fonts/inter-latin-wght-normal.woff2 b/website/public/fonts/inter-latin-wght-normal.woff2 new file mode 100644 index 0000000000..d15208de03 Binary files /dev/null and b/website/public/fonts/inter-latin-wght-normal.woff2 differ diff --git a/website/public/icon-light.png b/website/public/icon-light.png new file mode 100644 index 0000000000..b4c9ef7984 Binary files /dev/null and b/website/public/icon-light.png differ diff --git a/website/public/icon.png b/website/public/icon.png new file mode 100644 index 0000000000..4f12585b30 Binary files /dev/null and b/website/public/icon.png differ diff --git a/website/public/index.html b/website/public/index.html new file mode 100644 index 0000000000..6cb6d90a1f --- /dev/null +++ b/website/public/index.html @@ -0,0 +1,466 @@ + + + + + + SQLBuild · The refactorable warehouse + + + + + + + + + + + + + + +
+
+
+

Free and open source · Apache-2.0

+

The refactorable warehouse

+

Verify early, test properly, and refactor safely.

+

+ Change your warehouse as often as your code. SQLBuild brings compile-time checks, tests and diffs to + your SQL, so change is safe. +

+
+
+ $ pip install sqlbuild + +
+ Read the docs + View on GitHub +
+
+ +
+ +
+
+
+

Why warehouses resist change

+
+
+
+

Valid isn't the same as correct

+

SQL can run cleanly and still return the wrong number.

+
+
+

Lessons get forgotten

+

Hard-won rules live in review comments, until someone misses one.

+
+
+

Code review can't see the data

+

A pull request shows what the SQL changed, not what the numbers did.

+
+
+

Names are tied to data

+

Rename an incremental model and you rebuild its history from scratch.

+
+
+
+
+ +
+
+
+

One refactor, start to finish

+

+ Rewrite a few models, rename an incremental rollup and move a model between folders. Each step + catches something the one before it can't. +

+
+ +
+

Before anything runs

+
+
+ 01 · Verify early +

Catch errors before anything runs

+

+ + Every column and type is checked offline against your declared contracts. A missing column fails in seconds, pointing at the line. +

+
+
sqb compile
+error[B002]: Unknown column 'qty' in table 'stg_orders' (context: SELECT)
+  model: fact_orders
+  --> models/marts/fact_orders.sql:16:5
+     |
+  16 |   o.qty,
+     |     ^
+  = help: review the SQL expression, input types, and authoritative schema
+
+error[B217]: Incompatible comparison between timestamp and integer
+  model: fact_orders
+  --> models/marts/fact_orders.sql:27:7
+     |
+  27 | WHERE o.ordered_at > 5
+     |       ^
+  = help: review the SQL expression, input types, and authoritative schema
+
+✗ Project compiled  12 models, 1 seed, 2 functions, 4 errors, 0 warnings
+
+ +
+
+ 02 · Make lessons permanent +

Turn review comments into compile errors

+

+ + A few lines of Python turn a review comment into a compile error, for people and agents alike. Here a mart reads a raw source directly. +

+
+
# rules/layers.py
+@rule(
+    code="XSQBRARCH001",
+    message="Marts must read sources through staging",
+    remediation="Reference a staging model with __ref() instead.",
+)
+def marts_use_staging(*, model: Model, ctx: RuleContext) -> list[Finding]:
+    layer = ctx.project.tree.relative_parts(path=model.path, under="models")[0]
+    sql = ctx.sql.for_model(model).authored.source
+    if layer != "marts" or "__source(" not in sql:
+        return []
+    line = sql[: sql.index("__source(")].count("\n") + 1
+    return [ctx.finding(subject=model, line=line)]
+
+sqb compile
+error[XSQBRARCH001]: Marts must read sources through staging
+  --> models/marts/daily_revenue.sql:24:1
+     |
+  24 | INNER JOIN __source("raw__payments") p ON o.order_id = p.order_id AND p.status = 'success'
+     | ^
+  = help: Reference a staging model with __ref() instead.
+
+ +
+
+ 03 · Move models safely +

Reorganise without breaking anything

+

+ + Declarations live next to the models that use them, and the compiler enforces who can see them. sqb scope shows what a move would break. +

+
+
models/marts/
+├── _sqlbuild/
+│   ├── _enums/                # "_" = visible only to models directly in marts/
+│   │   └── payment_status.sql
+│   └── _macros/
+│       ├── currency.py        # cents_to_dollars
+│       └── datetime.py        # timestamp_trunc
+└── daily_revenue.sql          # uses payment_status and cents_to_dollars
+sqb scope model:daily_revenue --as-path models/intermediate/daily_revenue.sql
+Move preview
+  Resource: model:daily_revenue
+  Destination: models/intermediate/daily_revenue.sql
+  Lost (3)
+    ├─ ● enum:payment_status     models/marts/_sqlbuild/_enums/payment_status.sql:1:1
+    ├─ ● macro:cents_to_dollars  models/marts/_sqlbuild/_macros/currency.py:4:1
+    └─ ○ macro:timestamp_trunc   models/marts/_sqlbuild/_macros/datetime.py:4:1
+  Invalidated usages (2)
+    - enum:payment_status
+    - macro:cents_to_dollars
+
+ +

Before production changes

+
+
+ 04 · Test properly +

Test the logic, not just the columns

+

+ + Tests mock the sources and check the result across every model in between. A dropped filter fails, so the table is left alone. +

+
+
-- tests/unit/test_daily_revenue_chain.sql (trimmed)
+TEST();
+
+WITH
+__source__raw__payments AS (
+  SELECT 10 AS id, 1 AS order_id, 2850 AS amount_cents, 'success' AS status
+  UNION ALL
+  SELECT 11 AS id, 2 AS order_id, 850 AS amount_cents, 'failed' AS status
+),
+__expected__daily_revenue AS (
+  SELECT
+    CAST('2026-04-01' AS DATE) AS revenue_date,
+    1 AS order_count,
+    2850 AS total_revenue_cents
+),
+__assert__daily_revenue_is_non_negative AS (
+  SELECT * FROM __ref("daily_revenue") WHERE total_revenue_cents < 0
+)
+SELECT 1
+
+sqb build --select daily_revenue
+test  test_daily_revenue_chain           FAIL
+├── expect  expected daily_revenue    FAIL  unexpected=1; missing=1
+│     differing column total_revenue_cents: actual=3700, expected=2850
+└── error   [T003] test 'test_daily_revenue_chain' failed
+✗ Completed with errors  PASS=0  FAIL=1  SKIP=1
+
+ +
+
+ 05 · Plan every change +

Know what will rebuild, and why

+

+ + sqb plan shows why each model will run and how much it will rebuild. You choose per model how far a change replays: forward only, a set window like 14 days, or the full history. +

+
+
sqb plan --select daily_order_rollup+
+Query changed (1)
+└── daily_order_rollup                 rebuild last 14d, add column
+    ├── policy  replay_on_change=bounded-14d
+    ├── schema diff:
+      + avg_order_cents   (added)
+    └── query diff:
+      @@ -2,6 +2,7 @@
+         SUM(waffles_ordered) AS waffles_ordered,
+      -  SUM(revenue_cents) AS revenue_cents
+      +  SUM(revenue_cents) AS revenue_cents,
+      +  SUM(revenue_cents) / NULLIF(SUM(orders_placed), 0) AS avg_order_cents
+       FROM __ref("hourly_order_activity")
+
+Upstream changed (1)
+└── hourly_activity_with_daily_context rebuild last 14d
+    └── cause  daily_order_rollup (query changed)
+
+ +
+
+ 06 · Compare real data +

See what the tests missed

+

+ + Tests only cover the cases someone wrote down. Here a tidy-up passed its build, but a customer disappeared and two lost an order. +

+
+
sqb diff prod:dev --select dim_customers --full
+Rows
+prod count             dev count
+    5       (-20.00%)      4
+
+┏━┯━┯━┯━┯━┓              ╭──┬──────────┬─────────╮
+┃-│-│-│-│-┃              │ 1│ prod only│ (20.00%)│
+┠─┼─┼─┼─┼─┨╌╌╌┏━┯━┯━┯━┯━┓├──┼──────────┼─────────┤╌╮
+┃ │ │ │ │ ┃ = ┃ │ │ │ │ ┃│ 2│ equal    │ (50.00%)│ │
+┠─┼─┼─┼─┼─┨╌╌╌┠─┼─┼─┼─┼─┨├──┼──────────┼─────────┤╌├╴  4  joined
+┃ │ │ │ │ ┃ ≠ ┃ │ │ │ │ ┃│ 2│ unequal  │ (50.00%)│ │
+┗━┷━┷━┷━┷━┛╌╌╌┗━┷━┷━┷━┷━┛├──┼──────────┼─────────┤╌╯
+                         │ 0│ dev only │  (0.00%)│
+                         ╰──┴──────────┴─────────╯
+Changed Columns
+┌─────────────────┬──────────────┬──────────────┐
+│ lifetime_orders │ mismatches=2 │ match=60.00% │
+└─────────────────┴──────────────┴──────────────┘
+lifetime_orders
+  - customer_id=2 | 2 -> 1
+  - customer_id=3 | 2 -> 1
+
+prod only
+  - customer_id=5
+
+ +

When production changes

+
+
+ 07 · Refactor safely +

Rename models without rebuilding history

+

+ + A renamed incremental model keeps its table instead of starting again. If the logic changed too, declare migrate_from. +

+
+
sqb plan
+Migrations (1)
+└── daily_order_rollup  migrate  dev.daily_activity_rollup -> dev.daily_order_rollup
+    ├── compatibility  compatible
+    ├── transfer  physical copy, promote by staged rename
+    └── discovery  automatic
+
+
+
+ 08 · Tidy up safely +

Archive old tables before deleting them

+

+ The janitor renames tables your project no longer builds, and only drops them once the archive expires, 14 days later by default. +

+
+
sqb janitor
+Janitor preview  prod
+  relations to archive   1
+  archives to delete     0
+  archive retention      14 days
+
+Relations to archive
+  prod.weekly_revenue  ->  prod._sqb_archive__20260925t155417z__weekly_revenue  …
+
+
+

+ Real output from SQLBuild's example project, trimmed to the relevant lines. During a migration the old + table is never modified, archives can be renamed back until they expire, and every move is recorded. + How migrations work · + How the janitor works +

+
+
+ +
+
+
+

Built for safe change

+

The rest of what it takes to change a warehouse with confidence.

+
+
+
+

Audits that block bad data

+

Failing audits stop bad data before it reaches production, for full builds and incremental batches alike.

+
+
+

Data contracts

+

Declared columns and types, checked at compile time and again before the table replaces production.

+
+
+

Column lineage

+

See where every column comes from and which models use it, worked out at compile time.

+
+
+

End-to-end scenarios

+

Run the real model graph against test data, then replay it locally on DuckDB in CI.

+
+
+

Python macros

+

Logic lives in plain, testable Python functions, not Jinja blocks in your SQL.

+
+
+

Python in the same graph

+

Loaders, tasks, assets and checks run in the same build as your SQL.

+
+
+
+
+ +
+
+
+

Start simple

+

+ SQLBuild runs as a normal build tool. Its state lives in append-only tables in your warehouse, + next to your data. +

+
    +
  • No external state service, manifest files or account.
  • +
  • Try it locally on DuckDB, with no warehouse credentials.
  • +
  • Free and open source, with no paid tier.
  • +
  • Virtual environments for isolated previews and rollback, when you need them (alpha).
  • +
+
+ DuckDB + MotherDuck + Snowflake + BigQuery + Databricks + PostgreSQL + SQL Server +
+
+
pip install sqlbuild
+sqb playground waffle-shop
+cd waffle-shop
+sqb build
+✓ Completed successfully  PASS=95  FAIL=0
+sqb test
+
+
+ +
+
+

Change your warehouse like you change code

+

The docs cover models, testing, diffs, migrations and every command.

+ +
+
+
+ + + + + + diff --git a/website/public/ingestr-example.png b/website/public/ingestr-example.png new file mode 100644 index 0000000000..f23f075765 Binary files /dev/null and b/website/public/ingestr-example.png differ diff --git a/website/public/sqlbuild_diff.png b/website/public/sqlbuild_diff.png new file mode 100644 index 0000000000..39de024dca Binary files /dev/null and b/website/public/sqlbuild_diff.png differ diff --git a/website/public/sqlbuild_diff_hq.png b/website/public/sqlbuild_diff_hq.png new file mode 100644 index 0000000000..95d0abb72f Binary files /dev/null and b/website/public/sqlbuild_diff_hq.png differ diff --git a/website/public/styles.css b/website/public/styles.css new file mode 100644 index 0000000000..c9e560fee7 --- /dev/null +++ b/website/public/styles.css @@ -0,0 +1,965 @@ +@font-face { + font-family: 'Inter'; + font-style: normal; + font-weight: 100 900; + font-display: swap; + src: url('/fonts/inter-latin-wght-normal.woff2') format('woff2'); +} + +@font-face { + font-family: 'IBM Plex Mono'; + font-style: normal; + font-weight: 400; + font-display: swap; + src: url('/fonts/ibm-plex-mono-latin-400-normal.woff2') format('woff2'); +} + +@font-face { + font-family: 'IBM Plex Mono'; + font-style: normal; + font-weight: 500; + font-display: swap; + src: url('/fonts/ibm-plex-mono-latin-500-normal.woff2') format('woff2'); +} + +/* IBM Plex Mono has no box-drawing glyphs. DejaVu Sans Mono fills them in, and draws whole + diagrams so every cell has the same advance width. */ +@font-face { + font-family: 'SQLBuild Box Drawing'; + font-style: normal; + font-weight: 400; + font-display: swap; + src: url('/fonts/dejavu-sans-mono-diagram.woff2') format('woff2'); + unicode-range: U+2500-259F, U+2260; +} + +@font-face { + font-family: 'SQLBuild Diagram'; + font-style: normal; + font-weight: 400; + font-display: swap; + src: url('/fonts/dejavu-sans-mono-diagram.woff2') format('woff2'); +} + +/* Tokens mirror the SQLBuild UI theme so the site and the app read as one product. */ +:root { + color-scheme: light; + --radius: 2px; + --background: #ffffff; + --foreground: #171a1f; + --muted-foreground: #626a76; + --faint: #98a0ad; + --primary: #2677d9; + --primary-hover: #1f68c2; + --primary-foreground: #ffffff; + --panel: #ffffff; + --panel-border: rgba(28, 32, 38, 0.2); + --inner-line: rgba(28, 32, 38, 0.08); + --code-bg: #fbfcfe; + --code-fg: #2b3038; + --success: #16825c; + --warning: #aa6c16; + --error: #c74757; + --stale: #7756b9; + --accent-soft: rgba(38, 119, 217, 0.08); + --font-sans: 'Inter', system-ui, sans-serif; + --font-mono: 'IBM Plex Mono', 'SQLBuild Box Drawing', ui-monospace, monospace; +} + +:root[data-theme='dark'] { + color-scheme: dark; + --background: #0d0f12; + --foreground: #e9ebef; + --muted-foreground: #9ba1ac; + --faint: #626975; + --primary: #3788e8; + --primary-hover: #4f98ee; + --panel: #171a1f; + --panel-border: transparent; + --inner-line: rgba(176, 184, 198, 0.1); + --code-bg: #111418; + --code-fg: #c9cdd4; + --success: #54cf8e; + --warning: #f0c267; + --error: #f47089; + --stale: #a78bfa; + --accent-soft: rgba(55, 136, 232, 0.12); +} + +*, +*::before, +*::after { + box-sizing: border-box; +} + +html { + -webkit-text-size-adjust: 100%; + scroll-behavior: smooth; +} + +body { + margin: 0; + background: var(--background); + color: var(--foreground); + font-family: var(--font-sans); + font-size: 16px; + line-height: 1.6; + -webkit-font-smoothing: antialiased; +} + +a { + color: var(--primary); + text-decoration: none; +} + +a:hover { + text-decoration: underline; +} + +code, +pre { + font-family: var(--font-mono); +} + +.wrap { + width: min(1140px, 100% - 48px); + margin: 0 auto; +} + +/* Header */ +.site-header { + position: sticky; + top: 0; + z-index: 10; + background: var(--background); + border-bottom: 1px solid var(--inner-line); +} + +.site-header .wrap { + display: flex; + align-items: center; + justify-content: space-between; + height: 60px; +} + +.brand { + display: inline-flex; + align-items: center; + gap: 10px; + color: var(--foreground); + font-weight: 650; + font-size: 18px; + letter-spacing: -0.01em; +} + +.brand:hover { + text-decoration: none; +} + +.brand img { + width: auto; + height: 30px; +} + +.brand .logo-for-dark, +:root[data-theme='dark'] .brand .logo-for-light { + display: none; +} + +:root[data-theme='dark'] .brand .logo-for-dark { + display: block; +} + +.nav { + display: flex; + align-items: center; + gap: 22px; + font-size: 14px; +} + +.nav a { + color: var(--foreground); + font-weight: 500; + opacity: 0.8; + text-decoration: underline; + text-decoration-color: transparent; + text-decoration-thickness: 2px; + text-underline-offset: 6px; + transition: + opacity 120ms, + text-decoration-color 120ms; +} + +.nav a:hover { + opacity: 1; + text-decoration-color: var(--primary); +} + +.nav .nav-icon { + display: inline-grid; + place-items: center; + color: var(--muted-foreground); +} + +.nav .nav-icon:hover { + color: var(--foreground); + opacity: 1; +} + +.nav-icon svg { + width: 18px; + height: 18px; +} + +.theme-toggle { + display: inline-grid; + place-items: center; + width: 32px; + height: 32px; + padding: 0; + border: 1px solid var(--inner-line); + border-radius: var(--radius); + background: transparent; + color: var(--muted-foreground); + cursor: pointer; +} + +.theme-toggle:hover { + color: var(--foreground); +} + +.theme-toggle svg { + width: 16px; + height: 16px; +} + +.icon-sun { + display: none; +} + +:root[data-theme='dark'] .icon-sun { + display: block; +} + +:root[data-theme='dark'] .icon-moon { + display: none; +} + +/* Hero */ +.hero { + padding: 96px 0 72px; +} + +.eyebrow { + margin: 0 0 20px; + color: var(--muted-foreground); + font-size: 14px; +} + +.hero h1 { + margin: 0; + max-width: 14ch; + font-size: clamp(44px, 7vw, 76px); + font-weight: 700; + line-height: 1.02; + letter-spacing: -0.035em; +} + +.tagline { + margin: 24px 0 0; + font-size: clamp(20px, 2.4vw, 26px); + font-weight: 550; + letter-spacing: -0.015em; +} + +.lede { + max-width: 62ch; + margin: 20px 0 0; + color: var(--muted-foreground); + font-size: 18px; +} + +.actions { + display: flex; + flex-wrap: wrap; + align-items: center; + gap: 12px; + margin-top: 36px; +} + +.button { + display: inline-flex; + align-items: center; + height: 44px; + padding: 0 18px; + border: 1px solid var(--panel-border); + border-radius: var(--radius); + background: var(--panel); + color: var(--foreground); + font-size: 15px; + font-weight: 550; +} + +.button:hover { + text-decoration: none; + border-color: var(--faint); +} + +.button.primary { + border-color: var(--primary); + background: var(--primary); + color: var(--primary-foreground); +} + +.button.primary:hover { + border-color: var(--primary-hover); + background: var(--primary-hover); +} + +.install { + display: inline-flex; + align-items: center; + gap: 12px; + height: 44px; + padding: 0 8px 0 16px; + border: 1px solid var(--panel-border); + border-radius: var(--radius); + background: var(--code-bg); + font-family: var(--font-mono); + font-size: 14px; +} + +.install .prompt { + color: var(--faint); +} + +.copy { + height: 28px; + padding: 0 10px; + border: 1px solid var(--inner-line); + border-radius: var(--radius); + background: transparent; + color: var(--muted-foreground); + font-family: var(--font-sans); + font-size: 12px; + cursor: pointer; +} + +.copy:hover { + color: var(--foreground); +} + +/* Sections */ +section { + padding: 72px 0; + border-top: 1px solid var(--inner-line); +} + +.section-head { + max-width: 68ch; + margin-bottom: 40px; +} + +h2 { + margin: 0; + font-size: clamp(28px, 3.4vw, 38px); + font-weight: 680; + line-height: 1.15; + letter-spacing: -0.025em; +} + +.section-head p { + margin: 14px 0 0; + color: var(--muted-foreground); + font-size: 17px; +} + +h3 { + margin: 0; + font-size: 18px; + font-weight: 650; + letter-spacing: -0.01em; +} + +/* Problem */ +.problem { + display: grid; + grid-template-columns: repeat(2, 1fr); + gap: 16px; +} + +.card { + padding: 24px; + border: 1px solid var(--panel-border); + border-radius: var(--radius); + background: var(--panel); +} + +.card p { + margin: 10px 0 0; + color: var(--muted-foreground); + font-size: 15px; +} + +/* Walkthrough */ +.steps { + display: grid; + gap: 16px; + counter-reset: step; +} + +.step { + display: grid; + grid-template-columns: minmax(0, 4fr) minmax(0, 8fr); + border: 1px solid var(--panel-border); + border-radius: var(--radius); + background: var(--panel); + overflow: hidden; +} + +.step-text { + padding: 28px; +} + +.step-text .num { + display: block; + margin-bottom: 12px; + color: var(--primary); + font-family: var(--font-mono); + font-size: 13px; + font-weight: 500; +} + +.step-text p { + margin: 10px 0 0; + color: var(--muted-foreground); + font-size: 15px; +} + +.terminal { + margin: 0; + padding: 22px 24px; + border-left: 1px solid var(--inner-line); + background: var(--code-bg); + color: var(--code-fg); + font-size: 12px; + line-height: 1.65; + overflow-x: auto; + white-space: pre; +} + +.terminal .cmd { + color: var(--foreground); + font-weight: 500; +} + +.terminal .cmd::before { + content: '$ '; + color: var(--faint); + font-weight: 400; +} + +.terminal .boxes { + display: block; + font-family: 'SQLBuild Diagram', var(--font-mono); + line-height: 1.15; +} + +.terminal .tree { + display: block; + font-family: 'SQLBuild Diagram', var(--font-mono); +} + +.terminal .dim { + color: var(--faint); +} + +.terminal .ok { + color: var(--success); +} + +.terminal .bad { + color: var(--error); +} + +.terminal .warn { + color: var(--warning); +} + +.terminal .note { + color: var(--stale); +} + +.terminal .info { + color: var(--primary); +} + +.terminal .strong { + font-weight: 500; +} + +.step-text code, +.grid code, +.caption code { + padding: 1px 4px; + border-radius: var(--radius); + background: var(--accent-soft); + color: var(--foreground); + font-size: 0.86em; + word-spacing: -0.2em; +} + +.caption { + margin: 16px 0 0; + color: var(--faint); + font-size: 13px; +} + +/* Feature grid */ +.grid { + display: grid; + grid-template-columns: repeat(3, 1fr); + border: 1px solid var(--panel-border); + border-radius: var(--radius); + background: var(--panel); +} + +.grid .item { + padding: 24px; + border-right: 1px solid var(--inner-line); + border-bottom: 1px solid var(--inner-line); +} + +.grid .item:nth-child(3n) { + border-right: none; +} + +.grid .item:nth-last-child(-n + 3) { + border-bottom: none; +} + +.grid .item p { + margin: 8px 0 0; + color: var(--muted-foreground); + font-size: 15px; +} + +/* Start */ +.start { + display: grid; + grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); + gap: 40px; + align-items: start; +} + +.start ul { + margin: 20px 0 0; + padding: 0; + list-style: none; +} + +.start li { + position: relative; + padding: 10px 0 10px 22px; + border-top: 1px solid var(--inner-line); + color: var(--muted-foreground); + font-size: 15px; +} + +.start li::before { + content: ''; + position: absolute; + top: 20px; + left: 2px; + width: 7px; + height: 7px; + border-radius: 50%; + background: var(--success); +} + +.start .terminal { + border: 1px solid var(--panel-border); + border-radius: var(--radius); +} + +.adapters { + display: flex; + flex-wrap: wrap; + gap: 8px; + margin-top: 28px; +} + +.adapters span { + padding: 4px 10px; + border: 1px solid var(--panel-border); + border-radius: var(--radius); + background: var(--panel); + font-size: 13px; +} + +/* Closing call to action */ +.closing { + padding: 88px 0 96px; + text-align: center; +} + +.closing h2 { + max-width: 22ch; + margin: 0 auto; +} + +.closing p { + max-width: 56ch; + margin: 16px auto 0; + color: var(--muted-foreground); + font-size: 17px; +} + +.closing .actions { + justify-content: center; +} + +footer { + padding: 28px 0; + border-top: 1px solid var(--inner-line); + color: var(--faint); + font-size: 13px; +} + +footer .wrap { + display: flex; + flex-wrap: wrap; + justify-content: space-between; + gap: 12px; +} + +footer a { + color: var(--muted-foreground); +} + +@media (max-width: 880px) { + .problem, + .grid, + .start, + .step { + grid-template-columns: 1fr; + } + + .terminal { + border-left: none; + border-top: 1px solid var(--inner-line); + } + + .grid .item, + .grid .item:nth-child(3n), + .grid .item:nth-last-child(-n + 3) { + border-right: none; + border-bottom: 1px solid var(--inner-line); + } + + .grid .item:last-child { + border-bottom: none; + } + + .hero { + padding: 64px 0 48px; + } + + .nav .hide-sm { + display: none; + } +} + +/* Syntax colours mirror the SQLBuild UI's SQL tokens. */ +:root { + --syn-keyword: #1a56c4; + --syn-string: #0a7a3f; + --syn-number: #9a6700; + --syn-comment: #8a909c; + --syn-function: #7c3aed; + --syn-type: #0f766e; +} + +:root[data-theme='dark'] { + --syn-keyword: #5aa9e6; + --syn-string: #79c98a; + --syn-number: #e3b341; + --syn-comment: #5c606b; + --syn-function: #c79bf0; + --syn-type: #4dd6c4; +} + +.terminal .kw { + color: var(--syn-keyword); +} + +.terminal .str { + color: var(--syn-string); +} + +.terminal .num { + color: var(--syn-number); +} + +.terminal .cm { + color: var(--syn-comment); +} + +.terminal .fn { + color: var(--syn-function); +} + +.terminal .ty { + color: var(--syn-type); +} + +.terminal .arg { + color: var(--muted-foreground); +} + +.step-group { + margin: 24px 0 0; + color: var(--muted-foreground); + font-size: 13px; + font-weight: 600; + letter-spacing: 0.01em; +} + +.step-group:first-child { + margin-top: 0; +} + +/* Hero visual: a model is moved and renamed, then sqb plan shows the migration. */ +.hero { + display: grid; + grid-template-columns: minmax(0, 1fr) auto; + gap: 48px; + align-items: center; +} + +.hero-visual.terminal { + border: 1px solid var(--panel-border); + border-left: 1px solid var(--inner-line); + border-radius: var(--radius); + min-height: 236px; +} + +.hero-visual { + font-family: var(--font-mono); +} + +.hero-visual .tree { + display: inline; +} + +.hero-visual .hv-stack { + display: grid; +} + +.hero-visual .hv-stack > * { + grid-area: 1 / 1; +} + +.hero-visual .hv-l { + display: block; +} + +.hero-visual .hv-a, +.hero-visual .hv-b, +.hero-visual .hv-l { + animation-duration: 10s; + animation-iteration-count: infinite; + animation-fill-mode: both; + animation-timing-function: ease-out; +} + +.hero-visual .hv-a { + animation-name: hv-a; +} + +.hero-visual .hv-b { + animation-name: hv-b; +} + +@keyframes hv-a { + 0%, + 15% { + opacity: 1; + } + 18%, + 96% { + opacity: 0; + } + 100% { + opacity: 1; + } +} + +@keyframes hv-b { + 0%, + 15% { + opacity: 0; + } + 18%, + 92% { + opacity: 1; + } + 96%, + 100% { + opacity: 0; + } +} + +@keyframes hv-l1 { + 0%, + 24% { + opacity: 0; + transform: translateY(3px); + } + 27%, + 92% { + opacity: 1; + transform: none; + } + 96%, + 100% { + opacity: 0; + } +} + +.hv-l1 { + animation-name: hv-l1; +} + +@keyframes hv-l2 { + 0%, + 30% { + opacity: 0; + transform: translateY(3px); + } + 33%, + 92% { + opacity: 1; + transform: none; + } + 96%, + 100% { + opacity: 0; + } +} + +.hv-l2 { + animation-name: hv-l2; +} + +@keyframes hv-l3 { + 0%, + 33% { + opacity: 0; + transform: translateY(3px); + } + 36%, + 92% { + opacity: 1; + transform: none; + } + 96%, + 100% { + opacity: 0; + } +} + +.hv-l3 { + animation-name: hv-l3; +} + +@keyframes hv-l4 { + 0%, + 36% { + opacity: 0; + transform: translateY(3px); + } + 39%, + 92% { + opacity: 1; + transform: none; + } + 96%, + 100% { + opacity: 0; + } +} + +.hv-l4 { + animation-name: hv-l4; +} + +@keyframes hv-l5 { + 0%, + 39% { + opacity: 0; + transform: translateY(3px); + } + 42%, + 92% { + opacity: 1; + transform: none; + } + 96%, + 100% { + opacity: 0; + } +} + +.hv-l5 { + animation-name: hv-l5; +} + +@keyframes hv-l6 { + 0%, + 42% { + opacity: 0; + transform: translateY(3px); + } + 45%, + 92% { + opacity: 1; + transform: none; + } + 96%, + 100% { + opacity: 0; + } +} + +.hv-l6 { + animation-name: hv-l6; +} + +@media (prefers-reduced-motion: reduce) { + .hero-visual .hv-a, + .hero-visual .hv-b, + .hero-visual .hv-l { + animation: none; + } + + .hero-visual .hv-a { + opacity: 0; + } +} + +@media (max-width: 1060px) { + .hero { + grid-template-columns: minmax(0, 1fr); + } + + .hero-visual { + justify-self: start; + } +} + +@media (max-width: 620px) { + .hero-visual { + display: none; + } +} diff --git a/website/scripts/check_links.py b/website/scripts/check_links.py new file mode 100644 index 0000000000..1cb1e887db --- /dev/null +++ b/website/scripts/check_links.py @@ -0,0 +1,65 @@ +"""Fail when a built page links to a missing page or anchor on the same site.""" + +import sys +from html.parser import HTMLParser +from pathlib import Path +from urllib.parse import unquote, urlsplit + + +class PageParser(HTMLParser): + def __init__(self) -> None: + super().__init__() + self.hrefs: list[str] = [] + self.ids: set[str] = set() + + def handle_starttag(self, tag: str, attrs: list[tuple[str, str | None]]) -> None: + values = dict(attrs) + if values.get("id"): + self.ids.add(values["id"] or "") + if tag == "a" and values.get("href"): + self.hrefs.append(values["href"] or "") + + +def route_for(dist: Path, file: Path) -> str: + relative = "/" + file.relative_to(dist).as_posix() + return relative.removesuffix("index.html") + + +def main() -> int: + dist = Path(sys.argv[1] if len(sys.argv) > 1 else "dist") + pages: dict[str, PageParser] = {} + for file in dist.rglob("*.html"): + parser = PageParser() + parser.feed(file.read_text()) + pages[route_for(dist, file)] = parser + + broken: list[str] = [] + for route, page in sorted(pages.items()): + if route.startswith("/404"): + continue + for href in page.hrefs: + url = urlsplit(href) + if url.scheme or url.netloc: + continue + path = unquote(url.path) or route + if not path.startswith("/"): + path = route + path + if not path.endswith("/") and "." not in path.rsplit("/", 1)[-1]: + path += "/" + target = pages.get(path) + if target is None: + if not (dist / path.lstrip("/")).exists(): + broken.append(f"{route}: missing page {href}") + continue + fragment = unquote(url.fragment) + if fragment and fragment != "_top" and fragment not in target.ids: + broken.append(f"{route}: missing anchor {href}") + + for line in broken: + print(line) + print(f"{len(pages)} pages, {len(broken)} broken links") + return 1 if broken else 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/website/src/assets/logo-dark.png b/website/src/assets/logo-dark.png new file mode 100644 index 0000000000..9597c390cf Binary files /dev/null and b/website/src/assets/logo-dark.png differ diff --git a/website/src/assets/logo-light.png b/website/src/assets/logo-light.png new file mode 100644 index 0000000000..f2eb8b64dd Binary files /dev/null and b/website/src/assets/logo-light.png differ diff --git a/website/src/code-themes.mjs b/website/src/code-themes.mjs new file mode 100644 index 0000000000..2d8ae451be --- /dev/null +++ b/website/src/code-themes.mjs @@ -0,0 +1,69 @@ +// Code colours mirror the SQLBuild UI's --syn-* tokens. +function theme(name, type, c) { + return { + name, + type, + colors: { + 'editor.background': c.bg, + 'editor.foreground': c.fg, + 'editorGroupHeader.tabsBackground': c.bg, + 'tab.activeBackground': c.bg, + 'tab.activeForeground': c.fg, + 'tab.border': c.line, + 'tab.activeBorderTop': c.primary, + 'editorGroupHeader.tabsBorder': c.line, + 'panel.border': c.line, + 'titleBar.border': c.line, + 'terminal.background': c.bg, + 'terminal.foreground': c.fg, + }, + tokenColors: [ + { scope: ['comment', 'punctuation.definition.comment'], settings: { foreground: c.comment } }, + { + scope: ['keyword', 'storage', 'storage.type', 'keyword.control', 'keyword.other'], + settings: { foreground: c.keyword }, + }, + { scope: ['keyword.operator'], settings: { foreground: c.fg } }, + { scope: ['string', 'string.quoted', 'markup.inline.raw'], settings: { foreground: c.string } }, + { scope: ['constant.numeric', 'constant.language', 'constant'], settings: { foreground: c.number } }, + { + scope: ['entity.name.function', 'support.function', 'meta.function-call.generic'], + settings: { foreground: c.function }, + }, + { + scope: ['entity.name.type', 'support.type', 'support.class', 'entity.name.class', 'entity.name.tag'], + settings: { foreground: c.type }, + }, + { scope: ['variable.parameter'], settings: { foreground: c.fg } }, + { scope: ['variable.other.key', 'support.type.property-name'], settings: { foreground: c.keyword } }, + { scope: ['entity.name.section', 'entity.name.tag'], settings: { foreground: c.type } }, + { scope: ['entity.other.attribute-name', 'support.type.property-name'], settings: { foreground: c.type } }, + ], + }; +} + +export const sqlbuildDark = theme('sqlbuild-dark', 'dark', { + bg: '#111418', + fg: '#c9cdd4', + line: '#1f232a', + primary: '#3788e8', + keyword: '#5aa9e6', + string: '#79c98a', + number: '#e3b341', + comment: '#5c606b', + function: '#c79bf0', + type: '#4dd6c4', +}); + +export const sqlbuildLight = theme('sqlbuild-light', 'light', { + bg: '#fbfcfe', + fg: '#2b3038', + line: '#e6e8eb', + primary: '#2677d9', + keyword: '#1a56c4', + string: '#0a7a3f', + number: '#9a6700', + comment: '#8a909c', + function: '#7c3aed', + type: '#0f766e', +}); diff --git a/website/src/components/Header.astro b/website/src/components/Header.astro new file mode 100644 index 0000000000..2263dadbfd --- /dev/null +++ b/website/src/components/Header.astro @@ -0,0 +1,63 @@ +--- +import Search from '@astrojs/starlight/components/Search.astro'; +import SiteTitle from './SiteTitle.astro'; +import SocialIcons from '@astrojs/starlight/components/SocialIcons.astro'; +import ThemeSelect from './ThemeSelect.astro'; +--- + +
+ + +
+ + +
+
+ + diff --git a/website/src/components/PageFrame.astro b/website/src/components/PageFrame.astro new file mode 100644 index 0000000000..e50c41a9eb --- /dev/null +++ b/website/src/components/PageFrame.astro @@ -0,0 +1,135 @@ +--- +import MobileMenuToggle from '@astrojs/starlight/components/MobileMenuToggle.astro'; + +const { hasSidebar } = Astro.locals.starlightRoute; +--- + +
+ +
+ { + hasSidebar && ( + + ) + } +
+
+
+ + diff --git a/website/src/components/PageTitle.astro b/website/src/components/PageTitle.astro new file mode 100644 index 0000000000..41cd6a0167 --- /dev/null +++ b/website/src/components/PageTitle.astro @@ -0,0 +1,54 @@ +--- +const { entry, sidebar } = Astro.locals.starlightRoute; + +type Entry = (typeof sidebar)[number]; + +function currentGroup(entries: Entry[], parent?: string): string | undefined { + for (const item of entries) { + if (item.type === 'link' && item.isCurrent) return parent; + if (item.type === 'group') { + const found = currentGroup(item.entries, item.label); + if (found) return found; + } + } + return undefined; +} + +const group = currentGroup(sidebar); +const { title, description } = entry.data; +--- + +
+ {group &&

{group}

} +

{title}

+ {description &&

{description}

} +
+ + diff --git a/website/src/components/Pagination.astro b/website/src/components/Pagination.astro new file mode 100644 index 0000000000..3cef1d2704 --- /dev/null +++ b/website/src/components/Pagination.astro @@ -0,0 +1,61 @@ +--- +const { prev, next } = Astro.locals.starlightRoute.pagination; +--- + +{ + (prev || next) && ( + + ) +} + + diff --git a/website/src/components/SiteTitle.astro b/website/src/components/SiteTitle.astro new file mode 100644 index 0000000000..3e72003ade --- /dev/null +++ b/website/src/components/SiteTitle.astro @@ -0,0 +1,58 @@ +--- +import logoDark from '../assets/logo-dark.png'; +import logoLight from '../assets/logo-light.png'; +--- + + + + diff --git a/website/src/components/ThemeSelect.astro b/website/src/components/ThemeSelect.astro new file mode 100644 index 0000000000..74d4a4cce2 --- /dev/null +++ b/website/src/components/ThemeSelect.astro @@ -0,0 +1,53 @@ + + + + + + + diff --git a/website/src/components/TwoColumnContent.astro b/website/src/components/TwoColumnContent.astro new file mode 100644 index 0000000000..359489186e --- /dev/null +++ b/website/src/components/TwoColumnContent.astro @@ -0,0 +1,43 @@ +--- +const { toc } = Astro.locals.starlightRoute; +--- + +
+
+ { + toc && ( + + ) + } +
+ + diff --git a/website/src/components/mintlify/Card.astro b/website/src/components/mintlify/Card.astro new file mode 100644 index 0000000000..61b3c49715 --- /dev/null +++ b/website/src/components/mintlify/Card.astro @@ -0,0 +1,65 @@ +--- +import ArrowRight from '@lucide/astro/icons/arrow-right'; +import BookOpen from '@lucide/astro/icons/book-open'; +import Code from '@lucide/astro/icons/code'; +import Compass from '@lucide/astro/icons/compass'; +import Eye from '@lucide/astro/icons/eye'; +import FlaskConical from '@lucide/astro/icons/flask-conical'; +import FolderTree from '@lucide/astro/icons/folder-tree'; +import Gauge from '@lucide/astro/icons/gauge'; +import GitBranch from '@lucide/astro/icons/git-branch'; +import Hash from '@lucide/astro/icons/hash'; +import Layers from '@lucide/astro/icons/layers'; +import List from '@lucide/astro/icons/list'; +import Lock from '@lucide/astro/icons/lock'; +import MapPin from '@lucide/astro/icons/map-pin'; +import Network from '@lucide/astro/icons/network'; +import Rocket from '@lucide/astro/icons/rocket'; +import ShieldCheck from '@lucide/astro/icons/shield-check'; +import SlidersHorizontal from '@lucide/astro/icons/sliders-horizontal'; +import Terminal from '@lucide/astro/icons/terminal'; +import TestTube from '@lucide/astro/icons/test-tube'; +import TriangleAlert from '@lucide/astro/icons/triangle-alert'; + +interface Props { + title: string; + href?: string; + icon?: string; +} + +// Mintlify card icon names (Font Awesome) mapped to the Lucide icons the SQLBuild UI uses. +const icons = { + 'arrow-right': ArrowRight, + book: BookOpen, + code: Code, + 'code-branch': GitBranch, + compass: Compass, + 'diagram-project': Network, + eye: Eye, + flask: FlaskConical, + 'flask-vial': TestTube, + 'folder-tree': FolderTree, + 'gauge-high': Gauge, + hashtag: Hash, + 'layer-group': Layers, + list: List, + 'location-dot': MapPin, + lock: Lock, + rocket: Rocket, + 'shield-check': ShieldCheck, + sliders: SlidersHorizontal, + terminal: Terminal, + 'triangle-exclamation': TriangleAlert, +} as const; + +const { title, href, icon } = Astro.props; +const Icon = icon ? icons[icon as keyof typeof icons] : undefined; +if (icon && !Icon) throw new Error(`Unknown card icon: ${icon}`); +const Tag = href ? 'a' : 'div'; +--- + + + {Icon && diff --git a/website/src/components/mintlify/CardGroup.astro b/website/src/components/mintlify/CardGroup.astro new file mode 100644 index 0000000000..b751e91468 --- /dev/null +++ b/website/src/components/mintlify/CardGroup.astro @@ -0,0 +1,8 @@ +--- +interface Props { + cols?: number; +} +const { cols = 2 } = Astro.props; +--- + +
diff --git a/website/src/components/mintlify/Frame.astro b/website/src/components/mintlify/Frame.astro new file mode 100644 index 0000000000..651911221a --- /dev/null +++ b/website/src/components/mintlify/Frame.astro @@ -0,0 +1 @@ +
diff --git a/website/src/components/mintlify/Note.astro b/website/src/components/mintlify/Note.astro new file mode 100644 index 0000000000..c095f07de3 --- /dev/null +++ b/website/src/components/mintlify/Note.astro @@ -0,0 +1,8 @@ +--- +import Icon from '@lucide/astro/icons/info'; +--- + + diff --git a/website/src/components/mintlify/Warning.astro b/website/src/components/mintlify/Warning.astro new file mode 100644 index 0000000000..d51c2ed9d0 --- /dev/null +++ b/website/src/components/mintlify/Warning.astro @@ -0,0 +1,8 @@ +--- +import Icon from '@lucide/astro/icons/triangle-alert'; +--- + + diff --git a/website/src/content.config.ts b/website/src/content.config.ts new file mode 100644 index 0000000000..d9ee8c9d1c --- /dev/null +++ b/website/src/content.config.ts @@ -0,0 +1,7 @@ +import { defineCollection } from 'astro:content'; +import { docsLoader } from '@astrojs/starlight/loaders'; +import { docsSchema } from '@astrojs/starlight/schema'; + +export const collections = { + docs: defineCollection({ loader: docsLoader(), schema: docsSchema() }), +}; diff --git a/website/src/content/docs/docs/benchmarks.mdx b/website/src/content/docs/docs/benchmarks.mdx new file mode 100644 index 0000000000..139f4286ea --- /dev/null +++ b/website/src/content/docs/docs/benchmarks.mdx @@ -0,0 +1,115 @@ +--- +title: "Benchmarks" +description: "Complete compiler performance on projects with thousands of models, SQL tests, and audit attachments." +--- + +SQLBuild compiles a 5,000-model project with nearly 5,000 SQL tests and more than 8,000 audit +attachments from scratch in **19 seconds** in our CI benchmark. An unchanged recompile takes +**13 seconds**. + +These measurements run the complete `sqb compile` command, including project discovery, dependency +resolution, SQL expansion and analysis, contract validation, test and audit preparation, configured +Rules, cache publication, and artifact generation. + +## Compiler performance + +| Compile time | **3k models**
2.9k tests · 5.1k audit attachments | **5k models**
4.9k tests · 8.4k audit attachments | +|---|---:|---:| +| First compile / cache fill | **12.48s** | **19.01s** | +| Unchanged recompile | **8.79s** | **12.76s** | + +Timings include test and audit preparation, not warehouse execution. + +The 5,000-model compiler process used approximately **1.3 GiB peak resident memory** on its first +compile and **0.7 GiB** on its unchanged recompile. + +### Workload details + +The deterministic generated projects exercise compiler work at scale, with synthetic SQL, +contracts, macros, functions, tests, and audits. + +| Resource | 3k-model project | 5k-model project | +|---|---:|---:| +| Models | 3,000 | 5,000 | +| Sources | 713 | 1,189 | +| Seeds | 141 | 236 | +| SQL functions | 71 | 118 | +| Python macros | 37 | 61 | +| SQL test cases | 2,945 | 4,908 | +| Audit attachments | 5,056 | 8,427 | + +The projects use repeated generated structures. Individual project timings depend on SQL +complexity, macros, configuration, and hardware as well as model count. + +### Incremental compilation + +SQLBuild reuses cached analysis for unaffected models. In the 5,000-model benchmark, a single-model +file edit reused **4,999 model analyses**. A scoped Python-macro edit required fresh analysis for +**7 models**. Both scenarios completed in approximately **9.8 seconds**. + +The single-model scenario appends a SQL comment to exercise file-change invalidation. Separate +regression coverage checks expression-changing edits against uncached compilation results. +Scenarios run in a fixed sequence and use different cache-loading paths; an edit timing below the +initial unchanged timing does not mean editing inherently requires less work. + +## Rules performance + +Rules participate in ordinary compilation. Model Rules are cached per model, and SQLBuild tracks +their dependencies so unaffected findings can be reused after edits. Project Rules evaluate the +complete project view and have broader invalidation. + +A separate custom-Rules benchmark exercises 20- and 100-Rule packs over 5,000 models. It measures +first compilation, unchanged compilation, model edits, and Rule implementation changes. This is a +different workload from the compiler table above; its results should be compared within that profile. + +See [Execution and caching](/docs/concepts/rules/execution-and-caching) for the Rules lifecycle, or run +the publication profile below to collect current custom-Rules measurements. + +## Methodology + +The compiler results above are individual scenario measurements from the +[passing CI run on September 22, 2026](https://github.com/chio-labs/sqlbuild/actions/runs/35764207007), +using the compiler code released in **SQLBuild 0.113.3** and Python 3.12 on Linux. They are not +multi-run medians. + +Each scenario launches a fresh process. The first compile starts with an empty project cache; +subsequent scenarios reuse that cache. The CI job enforces a **4 GiB combined memory limit**. +Reported peak resident memory is for the compiler process, not the entire CI job. + +CI checks cache-hit and miss expectations and pinned semantic fingerprints alongside timing and +memory budgets. The varied 1,000-model guard additionally compares SQL-expression and macro edits +with uncached analysis and Rules results. + +These measurements cover compilation and static analysis. SQL tests and audits are prepared, not +executed against a warehouse. + +## Reproduce the benchmarks + +Run the fresh-process compiler cache guards from the +[SQLBuild repository](https://github.com/chio-labs/sqlbuild): + +```bash +make test-e2e-cache-compile-performance SQLBUILD_BENCHMARK_MODELS=3000 +make test-e2e-cache-compile-performance SQLBUILD_BENCHMARK_MODELS=5000 +``` + +Run the five-iteration custom-Rules publication profile: + +```bash +uv run python -m scripts.benchmark_rules \ + --models 5000 \ + --rule-counts 20 100 \ + --iterations 5 \ + --output rules-5000.json +``` + +Run the bounded Rules profile used by required CI: + +```bash +uv run python -m scripts.benchmark_rules_ci \ + --output rules-performance-ci.json \ + --summary-output rules-performance-ci.md +``` + +The benchmark generators, guard thresholds, and CI workflow are maintained in the public +[SQLBuild repository](https://github.com/chio-labs/sqlbuild). diff --git a/website/src/content/docs/docs/cli/audit.mdx b/website/src/content/docs/docs/cli/audit.mdx new file mode 100644 index 0000000000..ef530c0f20 --- /dev/null +++ b/website/src/content/docs/docs/cli/audit.mdx @@ -0,0 +1,67 @@ +--- +title: "sqb audit" +description: "Run data quality audits in isolation." +sidebar: + label: "audit" +--- + +Runs all attached audits without rebuilding models. Useful for verifying data quality on existing warehouse state. + +## Usage + +```bash +sqb --project-dir audit [flags] +``` + +## Flags + +| Flag | Description | +|------|-------------| +| `--no-sql-analysis` | Disable compile-time SQL analysis (`--no-sql-validation` is an alias) | +| `--defer-to` | Resolve model references against another target | +| `--select`, `-s` | Select audits attached to specific models | +| `--select-file` | Read selectors from a file | +| `--exclude` | Exclude audits attached to specific models | +| `--concurrency` | Maximum number of audits to run concurrently (default: `1`) | +| `--json` | Print structured audit results | +| `--json-output` | Write structured audit results to a file | + +## Concurrency + +Standalone audits run serially by default. Use `--concurrency` to opt into parallel warehouse +queries: + +```bash +sqb audit --concurrency 8 +``` + +SQLBuild resolves the limit in this order: an explicit `--concurrency` value, the +`SQLBUILD_CONCURRENCY` environment variable, the effective project `settings.concurrency`, then +the product default of `1`. Values must be at least `1`. + +The physical worker count is bounded by the number of selected audits. Each active worker opens +and exclusively uses one warehouse connection, so higher concurrency can increase warehouse load +and cost. Results and final JSON remain in plan order even when queries finish in a different +order. + +If an audit query raises a runtime, adapter, or framework error, SQLBuild records that failure and +continues running the remaining independent audits. After all selected audits have been attempted, +it reports every query failure in plan order and exits nonzero. Failed queries do not produce +fabricated audit results. + +Interruption and cancellation are different: SQLBuild stops scheduling new audits, drains queries +that have already started, and then closes every worker connection. Generic adapters cannot +guarantee immediate cancellation of a query already running in the warehouse. + +## Examples + +```bash +# Run all audits +sqb audit + +# Run audits for marts only +sqb audit --select path:models/marts + +# Run up to 8 audit queries concurrently +sqb audit --concurrency 8 +``` diff --git a/website/src/content/docs/docs/cli/build.mdx b/website/src/content/docs/docs/cli/build.mdx new file mode 100644 index 0000000000..6797718071 --- /dev/null +++ b/website/src/content/docs/docs/cli/build.mdx @@ -0,0 +1,114 @@ +--- +title: "sqb build" +description: "Compile, plan, and execute the selected build lifecycle." +sidebar: + label: "build" +--- + +Compiles, plans, and executes the selected build lifecycle, running the full selected scope. Use +`--no-tests` and `--no-audits` to skip validation for fast iteration. + +## Usage + +```bash +sqb --project-dir build [flags] +``` + +## Flags + +| Flag | Description | +|------|-------------| +| `--target` | Build against a configured target instead of the active/default target | +| `--no-tests` | Skip SQL unit tests | +| `--no-audits` | Skip audits | +| `--no-python` | Skip read-side Python tasks and assets (loader-side Python still runs for selected sources) | +| `--no-sql-analysis` | Disable compile-time SQL analysis (`--no-sql-validation` is an alias) | +| `--no-cache` | Bypass the reusable compile-analysis cache for this invocation | +| `--full-refresh` | Drop and rebuild selected models unless a model sets `full_refresh false`; `full_refresh true` forces a model even without this flag | +| `--defer-to` | Resolve unselected model references against another target | +| `--defer-sources-to` | Read managed source data from another target | +| `--fail-fast` | Stop on first failure and skip remaining nodes | +| `--concurrency` | Number of worker connections (default: 1) | +| `--verbose`, `-v` | Show lifecycle SQL inline after each model | +| `--start-cursor-ts` | Override start cursor for timestamp incremental models (ISO format) | +| `--end-cursor-ts` | Override end cursor for timestamp incremental models (ISO format) | +| `--start-cursor-int` | Override start cursor for integer incremental models | +| `--end-cursor-int` | Override end cursor for integer incremental models | +| `--load` | Explicitly load managed sources before building | +| `--no-load` | Skip automatic source loading | +| `--reload` | Reload managed sources (passes `is_reload=True` to loaders) | +| `--include-stale-upstreams` | Expand selection to include stale upstream models needed for coherence | +| `--manifest` | Generate `target/manifest.json` with plan-aware project metadata | +| `--select`, `-s` | Select specific models | +| `--exclude` | Exclude specific models | + +## Fast iteration + +Use `--no-tests` and `--no-audits` to skip validation when you only want to materialize models: + +```bash +sqb build --no-tests --no-audits +``` + +This replaces the former `sqb run` command. The full lifecycle (tests + audits) is always the default; skip flags opt out of specific phases when you need speed. + +## Execution order + +1. Managed sources are loaded (unless `--no-load`) +2. Seeds are loaded (if changed) +3. Source audits run before their dependent models (unless `--no-audits`) +4. SQL unit tests run before their target model (unless `--no-tests`) +5. Models are materialized in DAG topological order +6. Error-severity audits run against the staging table before promotion to the target (unless `--no-audits`) + +## Output + +``` +Execution sqb build (concurrency: 1) + + 1/13 seed waffle_types OK 0.09s + 2/13 view stg_customers OK 0.05s + audit not_null (customer_id) PASS + audit unique (customer_id) PASS + 3/13 view stg_orders OK 0.03s + test test_stg_orders PASS + audit not_null (order_id) PASS + audit unique (order_id) PASS + 10/13 table hourly_order_activity (delete_insert) OK 0.16s + audit (d) expression_is_true PASS 4/4 + audit (d) not_null (activity_hour) PASS 4/4 + audit (f) expression_is_true PASS + audit (f) not_null (activity_hour) PASS + +Completed successfully. +PASS=66 WARN=0 FAIL=0 SKIP=0 TOTAL=66 (1.09s) +``` + +## Deferred builds + +Use `--defer-to` to resolve unselected model references against another target. This lets you build a subset of models in dev while referencing production tables for everything else: + +```bash +sqb build --select fact_orders --defer-to prod +``` + +No `manifest.json` is required. `--defer-to` selects only the namespace used for unselected +references. SQLBuild resolves those relations through the active target's sole physical +connection and never opens the deferred target's connection. The deferred namespace must be +visible and readable through the active connection; deferral is not a cross-account, +cross-server, or cross-file transfer mechanism. + +## Failure behavior + +When a model fails: +- Downstream models are automatically blocked and skipped +- Staging/delta tables are retained for inspection +- Failure details show the model name, failed phase, and error message + +## Fingerprints + +After a successful build, SQLBuild writes version identities to `_sqlbuild_fingerprints` in the target schema. These are used on subsequent runs to detect changes and skip unchanged work. See [Planning and Change Detection](/docs/concepts/planning) for details. + +## Runtime artifacts + +Build writes executed lifecycle SQL to `target/run/models/`. These files contain the actual SQL that was executed, including resolved cursor bounds and runtime substitutions. diff --git a/website/src/content/docs/docs/cli/check.mdx b/website/src/content/docs/docs/cli/check.mdx new file mode 100644 index 0000000000..f712d13f59 --- /dev/null +++ b/website/src/content/docs/docs/cli/check.mdx @@ -0,0 +1,53 @@ +--- +title: "sqb check" +description: "Run Python checks against tasks, assets, and loaders." +sidebar: + label: "check" +--- + +Runs Python [checks](/docs/concepts/python-nodes/checks) in isolation. Checks validate the output of tasks, assets, and loaders. For SQL relation validation, use [`sqb audit`](/docs/cli/audit) instead. + +## Usage + +```bash +sqb --project-dir check [flags] +``` + +## Flags + +| Flag | Description | +|------|-------------| +| `--select`, `-s` | Select checks to run (by name, `check:`, `tag:`, or graph expansion) | +| `--exclude` | Exclude checks from the selection | +| `--no-sql-analysis` | Disable compile-time SQL analysis (`--no-sql-validation` is an alias) | +| `--json` | Print check results as JSON | +| `--json-output` | Write check results JSON to a file path | +| `--vars` | Override project variables | + +Selecting a non-check node is rejected; use [`sqb build`](/docs/cli/build) to run tasks and assets. + +## Examples + +```bash +# Run all checks +sqb check + +# Run one check and its required dependencies +sqb check --select +check_orders_exported + +# Run checks by tag +sqb check --select tag:exports + +# Print results as JSON +sqb check --json +``` + +## Output + +Checks report PASS, FAIL, or WARN per check. A failing `error`-severity check exits non-zero; `warn`-severity failures are reported without failing the command. + +Results are written to `target/run/checks/python_checks.json`. + +## Relationship to build + +`sqb build` runs relevant Python checks by default after the tasks, assets, and loaders they depend on have run. `sqb build --no-audits` skips checks alongside audits. `sqb check` runs only checks, on demand. diff --git a/website/src/content/docs/docs/cli/clean.mdx b/website/src/content/docs/docs/cli/clean.mdx new file mode 100644 index 0000000000..fe8dc33caf --- /dev/null +++ b/website/src/content/docs/docs/cli/clean.mdx @@ -0,0 +1,16 @@ +--- +title: "sqb clean" +description: "Remove compiled artifacts from the target directory." +sidebar: + label: "clean" +--- + +Removes the `target/` directory containing compiled artifacts, runtime SQL recordings, and other build outputs. + +## Usage + +```bash +sqb --project-dir clean +``` + +No flags. This command has no confirmation prompt since it only removes local build artifacts, not warehouse data. diff --git a/website/src/content/docs/docs/cli/clone.mdx b/website/src/content/docs/docs/cli/clone.mdx new file mode 100644 index 0000000000..d7bc526222 --- /dev/null +++ b/website/src/content/docs/docs/cli/clone.mdx @@ -0,0 +1,88 @@ +--- +title: "sqb clone" +description: "Copy model relations between configured targets." +sidebar: + label: "clone" +--- + +Copies selected relations from one target to another. It uses adapter-native cloning where supported and physical copies where required; `--hard-copy` forces a physical copy on adapters that support both. + +No `manifest.json` generation or artifact management is required. Clone works directly against live targets. + +When `--to` is omitted, the destination is the active target selected by `--target`, +`sqlbuild_local.toml`, or `default_target`, in that order. + +`--from` selects only the origin namespace and its clone-origin policy. `--to`, or the active +target when it is omitted, selects the sole physical connection used by the operation. The +origin namespace must therefore be visible and readable through the destination connection. +SQLBuild never resolves or opens the origin target's connection, including for direct and +deferred clone. Clone cannot perform a cross-account, cross-server, or cross-file +transfer by opening a second connection. + +## Usage + +```bash +sqb --project-dir clone --from [--to ] [flags] +``` + +## Flags + +| Flag | Description | +|------|-------------| +| `--from` | Source target (required) | +| `--to` | Destination target; defaults to the active target | +| `--hard-copy` | Force physical table copies instead of zero-copy cloning | +| `--no-sql-analysis` | Disable compile-time SQL analysis (`--no-sql-validation` is an alias) | +| `--select`, `-s` | Select specific models to clone | +| `--exclude` | Exclude specific models from cloning | + +## Examples + +```bash +# Clone all models from prod to dev +sqb clone --from prod --to dev + +# Equivalent when dev is the active target +sqb clone --from prod + +# Clone only marts to dev +sqb clone --from prod --to dev --select path:models/marts + +# Force physical copies +sqb clone --from prod --to dev --hard-copy +``` + +## Managed sources + +Clone includes selected managed physical sources when the destination target reads its own +loader namespace. Sources are copied before seeds and models so views can be recreated in a +destination that has never been built. + +Source locations respect target configuration: + +- The origin relation comes from the target that the origin uses for managed source reads. +- The destination relation uses the destination target's `loader_schema`, falling back to + its model `schema`. +- If the destination defers source reads to another target, clone reuses that target's source + relation and does not copy or overwrite it. +- Expression sources and unmanaged external sources are not cloned. + +`--hard-copy` controls how included relations are copied. It does not change which managed +sources are selected. + +## Clone policies + +Targets deny cloning by default. Enable the origin and destination explicitly in `sqlbuild_project.toml`: + +```toml +[targets.prod.clone] +allow_as_clone_origin = true + +[targets.dev.clone] +allow_as_clone_destination = true +``` + +The origin target must already contain the built relations being cloned. The destination +connection's credentials must be able to read the origin namespace and create relations in the +destination. Managed physical sources may bootstrap a destination that has not been built. See +[Project Configuration](/docs/concepts/project-configuration) for details. diff --git a/website/src/content/docs/docs/cli/compile.mdx b/website/src/content/docs/docs/cli/compile.mdx new file mode 100644 index 0000000000..e6a26bc945 --- /dev/null +++ b/website/src/content/docs/docs/cli/compile.mdx @@ -0,0 +1,176 @@ +--- +title: "sqb compile" +description: "Compile models into resolved SQL, validate contracts, and write target artifacts - fully offline." +sidebar: + label: "compile" +--- + +import Note from '../../../../components/mintlify/Note.astro'; + +Discovers the complete project, resolves references, expands macros, validates SQL, checks column contracts, computes column lineage, and writes compiled artifacts to `target/`. The compile command is fully offline—it does not connect to the warehouse. + +## Usage + +```bash +sqb --project-dir compile [flags] +``` + +## Flags + +| Flag | Description | +|------|-------------| +| `--no-sql-analysis` | Disable SQL syntax, binding, type inference, and semantic validation (`--no-sql-validation` remains an alias) | +| `--no-cache` | Bypass the reusable compile-analysis cache for this invocation | +| `--defer-to` | Resolve unselected model references against another target | +| `--json` | Output the full compile report as JSON | +| `--manifest` | Generate `target/manifest.json` with project metadata | +| `--lineage-mode` | Column lineage mode: `fast` (default), `rich` (slower, more detail), or `none` | +| `--select`, `-s` | Analyze and report selected resources | +| `--select-file` | Read selectors from a file, one selector per line | +| `--exclude` | Remove resources from the selected scope | + +## What compile does + +1. **Discovery** - finds `sqlbuild_project.toml`, scans for models, sources, seeds, functions, audits, tests, and macros +2. **Graph resolution** - resolves `ref()` and `source()` calls, expands macros, orders models by dependency +3. **SQL validation** - validates SQL syntax (when SQL analysis is enabled) +4. **Column lineage** - analyzes column-level dependencies across models (fast mode by default) +5. **Contract validation** - checks declared column contracts against inferred query output +6. **Rules** - evaluates selected native built-ins, then selected custom Python Rules +7. **Artifact write** - writes compiled SQL to `target/compiled/` when Rules pass + +## Focused compilation + +Use normal selectors to limit expensive analysis and reporting while retaining complete project +discovery and reference integrity: + +```bash +sqb compile --select fact_orders daily_revenue +sqb compile --select-file selected-models.txt +sqb compile --exclude deprecated_model +``` + +SQLBuild deeply analyzes the selected resources and the upstream closure needed to understand them. +The text report contains only the selected scope. Project totals remain available in JSON, together +with selected model, seed, and function counts. + + +Manifest and DAG outputs remain full-project artifacts. Focused compilation changes analysis and +reporting scope; it does not produce a partial project graph. + + +## Static analysis + +When SQL analysis is enabled (default), compile performs static analysis on your models without connecting to the warehouse: + +- **Column inference**: Infers output columns from each model's SQL, including through CTEs, subqueries, and JOINs +- **Column contract validation**: Under the default `settings.column_contract_mode = "implicit"`, a model with declared columns and no model-level `contract` declaration checks that every declared column exists in the statically inferred query output. `explicit` mode requires `contract enforced` to activate shape checks. Explicit type enforcement remains independent and verifies inferred types when possible +- **Column lineage**: Traces which source columns flow into each output column, including transform classification. See [Column Lineage](/docs/concepts/column-lineage) for details + + +`sqb compile` is authoritative for mandatory correctness, contracts, lineage, and configured Rules. +Rules findings block artifact completion. Use [`sqb rules run`](/docs/cli/rules) to focus on one exact +code or family without changing project configuration. + + +### Contract diagnostics + +When a contract violation is found, compile reports it with source-annotated diagnostics: + +``` +error[K001]: declared column 'total_cents' was not found in statically inferred output for model 'fact_orders' + model: fact_orders + --> models/marts/fact_orders.sql:6:5 + 6 | total_cents (), + | ^^^^^^^^^^^ + = help: add total_cents to the SELECT list or correct/remove MODEL(columns (...)); MODEL(columns (...)) is validated using static SQL analysis because settings.column_contract_mode is "implicit" (the default). If this project intentionally uses columns only for metadata and audits, set [settings] column_contract_mode = "explicit"; models with contract enforced remain validated +``` + +The configuration guidance is an intentional project-policy choice, not a general error suppression. Fix the query or declaration when the model is intended to have a column contract. Diagnostics for `contract enforced` models do not recommend changing the project mode because explicit model contracts remain authoritative. + +Diagnostic codes: + +| Code | Meaning | +|------|---------| +| `K001` | A declared column is missing from the model's query output | +| `K002` | A column's inferred type does not match the declared type | +| `K003` | A column's type could not be proven (with `type_enforcement` enabled) | + +Compile returns exit code `1` when any error-severity diagnostic is found, making it suitable for CI checks. + +## Output + +### Text output (default) + +```bash +sqb compile +``` + +``` +Compile ready (12 models) + + stg_customers OK 3 columns + stg_orders OK 5 columns + stg_payments OK 4 columns + fact_orders OK 6 columns + dim_customers OK 4 columns + daily_revenue OK 3 columns + ... + + Compiled: 12 models, 1 seed, 5 functions, 0 errors, 0 warnings + Wrote: target/compiled/ +``` + +Each model shows its name, status (OK or FAIL), and column count. Models with contract errors are marked FAIL. + +### JSON output + +```bash +sqb compile --json +``` + +Returns a structured report including: + +- `summary` - model, seed, function, audit, test, error, and warning counts +- `resources` - per-model details including column count, dependencies, lineage summary, and compiled SQL +- `diagnostics` - all contract violations with source locations +- `compile_timings` - timing breakdown for discovery, graph, lineage, contracts, and write phases +- `lineage_mode` - which lineage mode was used +- `artifacts` - paths to written files + +## Column lineage modes + +The `--lineage-mode` flag controls how column lineage is computed during compile: + +| Mode | Description | +|------|-------------| +| `fast` | Default. Lightweight column extraction using SQL model metadata. | +| `rich` | Full SQL analysis with transform classification and deeper tracing. Slower on large projects. | +| `none` | Skip column lineage entirely. | + +The JSON compile report includes a lineage summary for each model, not the full column graph. Use `sqb lineage [.]` to inspect lineage as a tree, edge list, or JSON. See [Column Lineage](/docs/concepts/column-lineage) for details on analysis modes and transform types. + +## Examples + +```bash +# Basic compile +sqb compile + +# Compile with full JSON report +sqb compile --json + +# Compile with rich column lineage +sqb compile --lineage-mode rich + +# Skip column lineage +sqb compile --lineage-mode none + +# Generate manifest +sqb compile --manifest + +# Compile only selected models +sqb compile --select fact_orders daily_revenue + +# Disable SQL analysis +sqb compile --no-sql-analysis +``` diff --git a/website/src/content/docs/docs/cli/contract.mdx b/website/src/content/docs/docs/cli/contract.mdx new file mode 100644 index 0000000000..d228191976 --- /dev/null +++ b/website/src/content/docs/docs/cli/contract.mdx @@ -0,0 +1,41 @@ +--- +title: "sqb contract" +description: "Compare or generate repository contracts from physical warehouse schemas." +sidebar: + label: "contract" +--- + +Existing-schema comparison is explicitly online and read-only with respect to the warehouse. + +## diff + +```bash +sqb contract diff --from [flags] +``` + +Reports declaration differences. Exit code `0` means agreement and `1` means differences exist. + +## generate + +```bash +sqb contract generate --from [flags] +``` + +Prints proposed declaration updates. Add `--write` to change repository files and `--overwrite` to +replace conflicts rather than preserving authored declarations. + +## Flags + +| Flag | Commands | Description | +|------|----------|-------------| +| `--from` | both | Target namespace to inspect (required) | +| `--json` | both | Print structured findings | +| `--select`, `-s` | both | Inspect selected models or sources | +| `--select-file` | both | Read selectors from a file | +| `--exclude` | both | Exclude resources from the selected scope | +| `--vars` | both | Override project variables | +| `--write` | generate | Apply generated repository changes | +| `--overwrite` | generate | Replace conflicting declarations; requires `--write` | + +See [Contracts](/docs/concepts/models/contracts#adopting-an-existing-schema) for ownership and safety +guarantees. diff --git a/website/src/content/docs/docs/cli/dag.mdx b/website/src/content/docs/docs/cli/dag.mdx new file mode 100644 index 0000000000..57b1ed5aa3 --- /dev/null +++ b/website/src/content/docs/docs/cli/dag.mdx @@ -0,0 +1,161 @@ +--- +title: "sqb dag" +description: "Generate the static DAG artifact for Dagster and other integrations." +sidebar: + label: "dag" +--- + +Compiles the project and outputs the static DAG artifact. The artifact contains every node (source, seed, model, function), dependency edge, and check (test, audit, scenario) in your project as structured JSON. It is the bridge between SQLBuild and external orchestrators like Dagster. + +This artifact describes resource dependencies, not column-level lineage. Use [`sqb lineage`](/docs/cli/lineage) to inspect or export column traces. + +## Usage + +```bash +sqb --project-dir dag [flags] +``` + +## Flags + +| Flag | Description | +|------|-------------| +| `--json` | Print the full DAG artifact as JSON to stdout | +| `--no-sql-analysis` | Disable compile-time SQL analysis (`--no-sql-validation` is an alias) | +| `--vars` | JSON object of project variable overrides | + +Without `--json`, the command prints a summary: + +``` +DAG ready (24 nodes, 18 edges, 15 checks) +``` + +## Generating via compile + +You can also generate the DAG artifact as part of a compile: + +```bash +# Write to default location (target/sqlbuild_dag.json) +sqb compile --dag + +# Write to a specific path +sqb compile --dag target/my_dag.json +``` + +This is useful when you want to compile and generate the DAG in one step. The `SqlBuildProject.prepare()` method in the Dagster integration uses this path. + +## Output format + +The JSON artifact has this structure: + +```json +{ + "version": 1, + "project_name": "waffle_shop", + "nodes": [...], + "edges": [...], + "checks": [...] +} +``` + +### Nodes + +Each node represents a source, seed, model, or function: + +```json +{ + "id": "model:fact_orders", + "kind": "model", + "name": "fact_orders", + "asset_key": ["dev", "fact_orders"], + "target": { + "database": null, + "schema": "dev", + "name": "fact_orders", + "qualified_name": "dev.fact_orders" + }, + "path": "models/marts/fact_orders.sql", + "description": "Order fact table with waffle and payment details.", + "tags": ["marts"], + "columns": [ + {"name": "order_id", "type": "INTEGER"}, + {"name": "customer_id"} + ], + "materialization_type": "table" +} +``` + +| Field | Description | +|-------|-------------| +| `id` | Unique identifier (`{kind}:{name}`) | +| `kind` | `source`, `seed`, `model`, or `function` | +| `name` | Resource name | +| `asset_key` | Tuple used as the Dagster asset key (typically `[schema, name]` or `[database, schema, name]`) | +| `target` | Warehouse identity (database, schema, name, qualified_name) | +| `path` | Relative file path in the project | +| `description` | Model or source description, if declared | +| `tags` | Model tags | +| `columns` | Column metadata (name, type, nullable, description) | +| `materialization_type` | For models: `view`, `table`, `incremental`, or custom name | +| `language` | For functions: `sql` or `python` | +| `return_kind` | For functions: `scalar` or `table` | +| `arguments` | For functions: argument name and type pairs | + +### Edges + +Each edge is a dependency between two nodes: + +```json +{ + "from_id": "model:stg_orders", + "to_id": "model:fact_orders" +} +``` + +### Checks + +Each check represents a test, audit, or scenario: + +```json +{ + "id": "audit:not_null:model:fact_orders:order_id", + "kind": "audit", + "name": "not_null", + "checked_asset_ids": ["model:fact_orders"], + "path": "audits/generic/not_null.sql", + "severity": "error", + "attached_target_name": "fact_orders", + "attached_column_name": "order_id" +} +``` + +| Field | Description | +|-------|-------------| +| `id` | Unique check identifier | +| `kind` | `sql_test`, `audit`, or `scenario` | +| `name` | Check name | +| `checked_asset_ids` | Node IDs this check is attached to | +| `path` | Relative file path | +| `severity` | For audits: `error` or `warn` | +| `mode` | For tests: `model`, `macro`, `udf`, or `table_fn` | +| `assertion_names` | For scenarios: names of `__assert__` CTEs | +| `expected_model_names` | For scenarios: names of `__expected__` models | +| `fixture_refs` | For scenarios: names of fixture sources, refs, and seeds | + +## Examples + +```bash +# Summary output +sqb dag + +# Full JSON to stdout +sqb dag --json + +# Generate as part of compile +sqb compile --dag + +# Generate to a specific path +sqb compile --dag target/sqlbuild_dag.json + +# Pipe to jq for inspection +sqb dag --json | jq '.nodes | length' +``` diff --git a/website/src/content/docs/docs/cli/dbt.mdx b/website/src/content/docs/docs/cli/dbt.mdx new file mode 100644 index 0000000000..c99381b9a0 --- /dev/null +++ b/website/src/content/docs/docs/cli/dbt.mdx @@ -0,0 +1,81 @@ +--- +title: "sqb dbt" +description: "Coordinate dbt and SQLBuild projects." +sidebar: + label: "dbt" +--- + +Orchestrate dbt and SQLBuild together. Each subcommand runs dbt first, then SQLBuild, with selection logic across both project graphs. dbt remains responsible for dbt-owned models; SQLBuild validates and executes SQLBuild-owned models downstream. See [Using SQLBuild with dbt](/docs/concepts/dbt-compatibility/overview) for scope and selection behavior. + +## sqb dbt plan + +Preview combined dbt and SQLBuild work without executing. + +```bash +sqb dbt plan [--select ...] [--exclude ...] [--json] [--verbose] +``` + +Shows which dbt models will run, which SQLBuild models will run, and the dbt/SQLBuild commands that would be executed. + +## sqb dbt run + +Run dbt models first, then SQLBuild models. + +```bash +sqb dbt run [--select ...] [--exclude ...] +``` + +## sqb dbt build + +Build dbt models first (including dbt tests), then SQLBuild models with audits. + +```bash +sqb dbt build [--select ...] [--exclude ...] +``` + +## sqb dbt debug + +Run dbt diagnostics followed by SQLBuild diagnostics. + +```bash +sqb dbt debug [--no-connection] +``` + +## Selectors + +All `sqb dbt` commands accept `--select` and `--exclude`. Selectors work across both dbt and SQLBuild: + +```bash +# SQLBuild model with dbt dependencies +sqb dbt build --select downstream_orders + +# Full upstream chain including dbt +sqb dbt build --select +downstream_orders + +# Downstream of modified dbt models +sqb dbt build --select state:modified+ + +# SQLBuild models by tag +sqb dbt build --select tag:nightly + +# SQLBuild models by path +sqb dbt build --select path:models/marts + +# Exclude by tag +sqb dbt build --select fact_orders+ --exclude tag:nightly +``` + +See [Selection](/docs/concepts/dbt-compatibility/selection) for full details on how selectors route work between dbt and SQLBuild. + +## Configuration + +Configure the dbt project location in `sqlbuild_project.toml`: + +```toml +[dbt] +project_dir = "../dbt_project" +profiles_dir = "../profiles" +target_path = "../dbt_project/target" +``` + +See [Project Configuration](/docs/concepts/project-configuration#dbt) for all fields. diff --git a/website/src/content/docs/docs/cli/debug.mdx b/website/src/content/docs/docs/cli/debug.mdx new file mode 100644 index 0000000000..1c83543315 --- /dev/null +++ b/website/src/content/docs/docs/cli/debug.mdx @@ -0,0 +1,68 @@ +--- +title: "debug" +description: "Validate project configuration and test the warehouse connection." +--- + +Checks that your project config is valid, the adapter is resolvable, and the warehouse connection works. Useful for diagnosing setup issues. + +## Usage + +```bash +sqb debug [flags] +``` + +## What it checks + +The command runs three groups of checks: + +**Runtime** - SQLBuild version, Python version, Python path, OS info. + +**Configuration** - Finds and validates `sqlbuild_project.toml`, loads `sqlbuild_local.toml` if present, resolves the adapter and active target. + +**Connection** - Displays connection settings (secrets are masked), attempts to connect to the warehouse, and runs `SELECT 1` to verify query execution. + +## Flags + +| Flag | Description | +|------|-------------| +| `--no-connection` | Skip the connection and query tests. Useful for validating config without warehouse access. | +| `--json` | Output results as JSON instead of formatted text. | + +## Example + +```bash +sqb debug +``` + +``` +SQLBuild Diagnostics + +Runtime: + sqlbuild version: 0.2.1 + python version: 3.14.3 + python path: /home/user/.venv/bin/python3 + os info: Linux-6.6.87 + +Configuration: + project file: /home/user/waffle_shop/sqlbuild_project.toml [OK found and valid] + local config: /home/user/waffle_shop/sqlbuild_local.toml [OK found] + project: waffle_shop [OK loaded] + adapter: snowflake [OK found] + target: dev [OK resolved] + +Connection: + account: FJMQFQV-OJ66172 + authenticator: programmatic_access_token + database: SQB_DB + role: role_sqb_test + schema: TEST + token: **** + user: svc_sqb_test + warehouse: SQB_WH + connection test: [OK connected] + query test: [OK SELECT 1] +``` + +## Exit codes + +Returns `0` when all checks pass, `1` when any check fails (e.g. connection refused, invalid config). diff --git a/website/src/content/docs/docs/cli/diff.mdx b/website/src/content/docs/docs/cli/diff.mdx new file mode 100644 index 0000000000..9b1687ba80 --- /dev/null +++ b/website/src/content/docs/docs/cli/diff.mdx @@ -0,0 +1,63 @@ +--- +title: "diff" +description: "Compare schemas and data between targets." +--- + +Compares schemas and optionally row-level data between two targets (e.g. `prod:dev`). See [Data Diffs](/docs/concepts/diff) for detailed usage. + +## Usage + +```bash +sqb diff : [flags] +``` + +The first argument is a positional `FROM:TO` range. Exactly one mode is required: `--full`, `--schema-only`, or `--bounded `. + +`FROM` and `TO` are configured target names. Their database/schema namespaces remain +authoritative, while the `TO` target's named connection executes the complete comparison and must +be able to read both namespaces. + +Full and bounded row comparisons require the model to define `unique_key`. Bounded mode uses the model's cursor and falls back to a full row comparison when no cursor is configured. + +## Flags + +| Flag | Description | +|------|-------------| +| `--full` | Compare both schema and all row data | +| `--schema-only` | Compare column names and types only | +| `--bounded` | Compare row data within a recent window (e.g. `14d`, `6h`) | +| `--verbose`, `-v` | Show more example rows (default: 3, verbose: 10) | +| `--max-column-examples` | Override maximum examples per changed column | +| `--max-row-only-examples` | Override maximum examples for side-only rows | +| `--sample-rows` | Override the deterministic unique-key sample size | +| `--sample-seed` | Override the deterministic sample seed | +| `--exhaustive` | Disable inherited sampling for this invocation | +| `--json-output` | Write structured comparison scope, coverage, and results to a JSON file | +| `--max-models` | Fail when the selected scope contains more models than this limit | +| `--max-columns` | Fail when either side of a model has more columns than this limit | +| `--no-sql-analysis` | Disable compile-time SQL analysis (`--no-sql-validation` is an alias) | +| `--select`, `-s` | Select specific models to diff (required in v1) | +| `--exclude` | Exclude specific models from diffing | + +## Examples + +```bash +# Full diff of a specific model +sqb diff prod:dev --full --select customer_status_snapshot + +# Schema-only diff of all marts +sqb diff prod:dev --schema-only --select path:models/marts + +# Bounded diff of last 14 days +sqb diff prod:dev --bounded 14d --select hourly_order_activity + +# Deterministic bounded sample +sqb diff prod:dev --bounded 14d --sample-rows 50000 --sample-seed 7 --select order_lines + +# Force exhaustive comparison despite inherited sampling defaults +sqb diff prod:dev --bounded 14d --exhaustive --select order_lines +``` + +## Exit codes + +Returns `0` when all selected models have no differences, `1` when any model has schema or row differences. diff --git a/website/src/content/docs/docs/cli/format.mdx b/website/src/content/docs/docs/cli/format.mdx new file mode 100644 index 0000000000..6d182ca19e --- /dev/null +++ b/website/src/content/docs/docs/cli/format.mdx @@ -0,0 +1,30 @@ +--- +title: "sqb format" +description: "Apply canonical SQLBuild SQL formatting." +sidebar: + label: "format" +--- + +Formats SQL files using SQLBuild's canonical, dialect-aware representation. + +```bash +sqb format [flags] +``` + +| Flag | Description | +|------|-------------| +| `--check` | Fail when formatting changes are needed without writing | +| `--diff` | Print the proposed diff without writing | +| `--json` | Print structured results | +| `--select`, `-s` | Format selected models | +| `--select-file` | Read selectors from a file | +| `--exclude` | Exclude models from the selected scope | + +```bash +sqb format +sqb format --check +sqb format --diff --select tag:marts +``` + +Formatting owns presentation and source rewriting. Compiler-integrated diagnostics belong to +configured [Rules](/docs/concepts/rules), which never mutate source. diff --git a/website/src/content/docs/docs/cli/freshness.mdx b/website/src/content/docs/docs/cli/freshness.mdx new file mode 100644 index 0000000000..b36a34b56a --- /dev/null +++ b/website/src/content/docs/docs/cli/freshness.mdx @@ -0,0 +1,154 @@ +--- +title: "sqb freshness" +description: "Observe source freshness without writing state." +sidebar: + label: "freshness" +--- + +Observes the current data version of each source and reports whether the data has changed since the last build. Does not write any state or trigger builds. + +## Usage + +```bash +sqb --project-dir freshness [flags] +``` + +## Flags + +| Flag | Description | +|------|-------------| +| `--state` | Compare observations against stored freshness state from the last build | +| `--fail-on-error` | Exit with code 1 if any source observation fails or is unknown | +| `--fail-on-stale` | Exit with code 1 if any source has changed, is unknown, or errored (requires `--state`) | +| `--json` | Output as JSON instead of human-readable text | +| `--json-output` | Write JSON output to a file path (also prints text to stdout unless `--json` is set) | +| `--no-sql-analysis` | Disable compile-time SQL analysis (`--no-sql-validation` is an alias) | +| `--select`, `-s` | Select specific sources or models (sources upstream of selected models are included) | +| `--exclude` | Exclude specific sources or models | + +## Source selection + +Without `--select`, all sources in the project are observed. When `--select` is provided, selectors work like other commands: you can select sources by name, or select models and their upstream sources are automatically included: + +```bash +# Observe all sources +sqb freshness + +# Observe a specific source +sqb freshness --select raw_orders + +# Observe sources upstream of a model +sqb freshness --select fact_orders +``` + +## Observation without state + +By default, `sqb freshness` observes the current data version of each source and reports what it found. No comparison is made against previous observations: + +```bash +sqb freshness +``` + +``` +Source freshness + +Observed (3) + raw_customers timestamp 2026-06-05T14:30:00 adapter + raw_orders timestamp 2026-06-05T15:45:00 column tolerance 15m + raw_payments integer 42871 column + +Summary: observed=3 changed=0 unchanged=0 tolerated=0 unknown=0 errors=0 +``` + +Sources without explicit `freshness:` config are auto-observed using the `adapter` strategy if the adapter supports table metadata. Sources that can't be observed (expression sources, managed sources without freshness config on unsupported adapters) show as `unknown`. + +## Comparing against state + +Use `--state` to compare current observations against the freshness state stored from the last successful build: + +```bash +sqb freshness --state +``` + +``` +Source freshness + +Changed (1) + raw_orders previous 2026-06-05T12:00:00 current 2026-06-05T15:45:00 tolerance 15m + +Unchanged (1) + raw_customers previous 2026-06-05T14:30:00 current 2026-06-05T14:30:00 + +Tolerated (1) + raw_payments previous 2026-06-05T14:28:00 current 2026-06-05T14:30:00 tolerance 15m + +Summary: observed=0 changed=1 unchanged=1 tolerated=1 unknown=0 errors=0 +``` + +### Statuses + +| Status | Meaning | +|--------|---------| +| `observed` | Successfully observed (no state comparison) | +| `changed` | Data version differs from the stored state | +| `unchanged` | Data version matches the stored state exactly | +| `tolerated` | Data version differs but is within the `lag_tolerance` threshold | +| `unknown` | No freshness config and adapter metadata unavailable, or no previous state to compare against | +| `error` | Observation failed (e.g. source table does not exist, query error) | + +## CI integration + +Use `--fail-on-error` to fail the pipeline if any source can't be observed: + +```bash +sqb freshness --fail-on-error +``` + +Use `--fail-on-stale` with `--state` to fail if any source has new data that hasn't been built yet: + +```bash +sqb freshness --state --fail-on-stale +``` + +This is useful for CI gates that should block if source data has changed but the pipeline hasn't run yet. + +## JSON output + +```bash +sqb freshness --json +``` + +```json +{ + "sources": [ + { + "name": "raw_orders", + "status": "observed", + "strategy": "column", + "value_kind": "timestamp", + "current_data_version": "2026-06-05T15:45:00", + "previous_data_version": null, + "lag_tolerance": "15m", + "target": { + "database": null, + "schema": "raw", + "name": "orders" + }, + "message": null + } + ], + "summary": { + "observed": 1, + "changed": 0, + "unchanged": 0, + "tolerated": 0, + "unknown": 0, + "errors": 0 + } +} +``` + +## See also + +- [Sources: Source freshness](/docs/concepts/sources#source-freshness) for freshness configuration +- [Planning and Change Detection](/docs/concepts/planning) for how freshness feeds into change-aware builds diff --git a/website/src/content/docs/docs/cli/init.mdx b/website/src/content/docs/docs/cli/init.mdx new file mode 100644 index 0000000000..ae2787f5ba --- /dev/null +++ b/website/src/content/docs/docs/cli/init.mdx @@ -0,0 +1,76 @@ +--- +title: "sqb init" +description: "Scaffold a new SQLBuild project." +sidebar: + label: "init" +--- + +Creates a new SQLBuild project with a minimal directory structure and configuration files. + +## Usage + +```bash +sqb init +``` + +No flags. Run in the directory where you want to create the project. + +## Project layout + +`sqb init` creates the configuration, linter settings, and empty resource directories needed for a standalone project: + +```text +my-project/ + sqlbuild_project.toml + .sqruff + models/ + staging/ + marts/ + schemas/ + sources/ + seeds/ + loaders/ + tasks/ + assets/ + checks/ + hooks/ + sql/ + python/ + tests/ + unit/ + scenarios/ + functions/ + sql/ + python/ + macros/ + audits/ + generic/ + singular/ +``` + +Empty directories contain `.gitkeep` files so the scaffold can be committed. Reusable attached +audit definitions belong in `audits/generic/`; standalone audits belong in `audits/singular/`. +Add reusable SQL lifecycle hooks to `hooks/sql/` and decorated Python lifecycle hooks to +`hooks/python/`; see [Hooks](/docs/concepts/models/hooks). + +The generated project uses DuckDB, creates a named `local` connection shared by `dev` and +`prod`, and defaults models to table materialization. Its configuration follows this shape: + +```toml +adapter = "duckdb" +default_target = "dev" + +[connections.local] +database = "my_project.duckdb" + +[targets.dev] +connection = "local" +schema = "dev" + +[targets.prod] +connection = "local" +schema = "prod" +``` + +The project name is derived from the current directory name, with hyphens converted to +underscores. diff --git a/website/src/content/docs/docs/cli/janitor.mdx b/website/src/content/docs/docs/cli/janitor.mdx new file mode 100644 index 0000000000..ab9a63931a --- /dev/null +++ b/website/src/content/docs/docs/cli/janitor.mdx @@ -0,0 +1,110 @@ +--- +title: "sqb janitor" +description: "Archive and then delete stale warehouse relations." +sidebar: + label: "janitor" +--- + +Finds stale warehouse relations that are no longer part of the project, archives them, and deletes archives once they expire. Requires `janitor.enabled = true` in `sqlbuild_project.toml`. + +## Usage + +```bash +sqb --project-dir janitor [flags] +``` + +## Flags + +| Flag | Description | +|------|-------------| +| `--auto-approve` | Skip the confirmation prompt and run the planned actions immediately | +| `--retention-days` | Override the configured retention period (days) before a stale relation is archived | +| `--direct-state-history-versions` | Override how many state-history versions are kept per identity | + +## Lifecycle + +The janitor never drops a live relation directly. Each stale relation goes through two stages: + +1. **Archive.** Once a relation has been stale for `retention_days`, the janitor renames it in place, in the same schema, to: + + ``` + _SQB_ARCHIVE____ + ``` + + For example, `stg_orders` becomes `_SQB_ARCHIVE__20260924T101500Z__stg_orders`. A rename copies no data, and keeps the relation's table type, grants and time travel. +2. **Delete.** Once an archive is older than `archive_retention_days`, the janitor drops it. + +Stale relations are identified exactly as before. A relation is a candidate only if it is in a schema the project writes to, it is not produced by the current complete project, it was tracked by SQLBuild (when `delete_tracked_only = true`), it matches no exclude pattern, and it is older than the retention period. + +The age of an archive comes from the timestamp in its name, so deletion needs no other state. The janitor only deletes relations whose names **strictly** match the archive format and that are in a schema the project writes to. Names that only look similar are reported and never touched. The janitor also only acts on relations whose names are plain lowercase identifiers (letters, digits and underscores), and that don't collide case-insensitively with another relation in the same schema. Anything else is reported and left alone. + +Archive names are written as unquoted lowercase identifiers, so warehouses that fold unquoted names show them in their own case. On Snowflake, for example, the archive appears as `_SQB_ARCHIVE__20260924T101500Z__STG_ORDERS`. + +Long names are shortened with a short hash so they fit the adapter's identifier limit. The prefix and timestamp are never shortened. + +Setting both `retention_days` and `archive_retention_days` to `0` archives and deletes stale relations in the same run. + +## Relation age + +`retention_days` is measured from the last time a relation was written, as reported by the warehouse. For a relation the project no longer builds, that is roughly when it stopped being built. + +| Adapter | Age source | +|---------|------------| +| Snowflake | `INFORMATION_SCHEMA.TABLES` `CREATED` and `LAST_ALTERED`, which change on every write | +| Databricks | `DESCRIBE DETAIL` `lastModified` for tables, and `INFORMATION_SCHEMA.TABLES` for views | +| BigQuery | `__TABLES__` `creation_time` and `last_modified_time` | +| PostgreSQL, SQL Server, DuckDB, MotherDuck | Not available | + +When the warehouse can't report a relation's age, the janitor skips that relation whenever `retention_days` is greater than `0`. SQL Server is in this group because its modification date ignores inserts and updates, so a recently written table could look old. + +Ages are only looked up for relations that are otherwise candidates, and not at all when `retention_days` is `0`. + +## Audit record + +Every archive and every delete is recorded in an append-only `_sqlbuild_janitor_events` table in the affected schema. It records the original name, the archive name, the time and the invocation. The janitor never reads this table to decide what to do. It exists for auditing, and for restoring archives whose names were shortened. + +## Restoring an archive + +Rename the archive back to its original name before it expires, using your warehouse's rename statement. For example, on Snowflake: + +```sql +ALTER TABLE analytics._SQB_ARCHIVE__20260924T101500Z__stg_orders RENAME TO analytics.stg_orders; +``` + +If the archive's name was shortened, look up the original name in `_sqlbuild_janitor_events`. After an archive has been deleted, your warehouse's own time travel or undrop feature may still be able to recover it within its retention window. + +## Configuration + +Configure janitor behavior in `sqlbuild_project.toml`: + +```toml +[janitor] +enabled = true +retention_days = 14 +archive_retention_days = 14 +delete_tracked_only = true +exclude_patterns = ["audit_*", "tmp_*"] +``` + +When `retention_days` is not set, it defaults to `14`. See [Project Configuration](/docs/concepts/project-configuration#janitor) for every janitor setting. + +## Examples + +```bash +# Interactive mode (shows the plan and prompts for confirmation) +sqb janitor + +# Run the planned archives and deletes without prompting +sqb janitor --auto-approve + +# Override retention to 7 days +sqb janitor --retention-days 7 +``` + +## State history pruning + +In addition to archiving stale relations, the janitor prunes old rows from `_sqlbuild_fingerprints` and `_sqlbuild_source_freshness`, keeping the latest `direct_state_history_versions` rows per identity (default `20`). This is configured separately from relation retention. + +## Safety + +The janitor shows every planned archive and delete, with exact names, ages and expiry, then asks you to type an exact confirmation phrase. Relations whose names differ only by case from another relation in the same schema are reported and never dropped, and exclude patterns match regardless of case. Use `--auto-approve` only in automation, or when you're certain. diff --git a/website/src/content/docs/docs/cli/lineage.mdx b/website/src/content/docs/docs/cli/lineage.mdx new file mode 100644 index 0000000000..9979bf7cd8 --- /dev/null +++ b/website/src/content/docs/docs/cli/lineage.mdx @@ -0,0 +1,246 @@ +--- +title: "lineage" +description: "Explore model and column-level dependency graphs from the command line." +--- + +Inspect upstream and downstream dependencies for any model, source, seed, or function in your project. Supports both model-level lineage (dependency graph) and column-level lineage (tracing individual columns through transformations). Outputs as a tree, edge list, or structured JSON. + +`sqb compile` computes lineage for validation and includes per-model summaries in its JSON report. Use this command when you need to inspect or export the actual lineage graph. + +## Usage + +```bash +# Model lineage: dependency graph around a single resource +sqb lineage [flags] + +# Column lineage: trace a specific column +sqb lineage . [flags] + +# Selector mode: lineage for a selected scope +sqb lineage --select [flags] +``` + +Exactly one of a positional target or `--select` is required. + +## Flags + +| Flag | Description | +|------|-------------| +| `--direction` | `upstream` (default), `downstream`, or `both`. `both` is only available for model lineage. | +| `--depth` | How many hops to traverse. An integer or `all` (default: `all`). | +| `--format` | Output format: `tree` (default), `list`, or `json`. | +| `--mode` | Column lineage analysis mode: `rich` (default) or `fast`. | +| `--no-sql-analysis` | Disable compile-time SQL analysis (`--no-sql-validation` is an alias). | +| `--select`, `-s` | Select resources using standard selector syntax. | +| `--exclude` | Exclude resources from the selection. | + +## Model lineage + +When the target is a plain resource name, lineage shows the model-level dependency graph. + +### Tree + +The default. Shows an indented dependency tree with resource types and file paths: + +```bash +sqb lineage fact_orders --direction both +``` + +``` +Lineage model fact_orders models/marts/fact_orders.sql both +upstream +├── model stg_orders models/staging/stg_orders.sql +│ └── source raw__orders sources/raw.yml +├── model stg_payments models/staging/stg_payments.sql +│ └── source raw__payments sources/raw.yml +├── seed waffle_types seeds/waffle_types.csv +└── function udf__is_completed_order functions/sql/udf__is_completed_order.sql +downstream +├── model customer_status_snapshot models/intermediate/customer_status_snapshot.sql +├── model hourly_order_activity models/marts/hourly_order_activity.sql +│ ├── model daily_activity_rollup models/marts/daily_activity_rollup.sql +│ │ └── model hourly_activity_with_daily_context models/marts/hourly_activity_with_daily_context.sql +│ │ └── model hourly_order_activity (already shown) +│ └── model hourly_activity_with_daily_context (already shown) +└── model order_status_index models/intermediate/order_status_index.sql +``` + +Cycles and repeated nodes are annotated with "(already shown)" to avoid infinite recursion. + +### List + +An edge list showing each dependency as a directed pair: + +```bash +sqb lineage fact_orders --format list +``` + +``` +source:raw__orders -> model:stg_orders +source:raw__payments -> model:stg_payments +model:stg_orders -> model:fact_orders +model:stg_payments -> model:fact_orders +seed:waffle_types -> model:fact_orders +``` + +### JSON + +Structured output with nodes, edges, and metadata: + +```bash +sqb lineage fact_orders --format json +``` + +```json +{ + "nodes": [ + { + "id": "model:fact_orders", + "name": "fact_orders", + "resource_type": "model", + "relative_path": "models/marts/fact_orders.sql", + "qualified_name": "dev.fact_orders" + }, + { + "id": "source:raw__orders", + "name": "raw__orders", + "resource_type": "source", + "relative_path": "sources/raw.yml" + } + ], + "edges": [ + {"from": "source:raw__orders", "to": "model:stg_orders"}, + {"from": "model:stg_orders", "to": "model:fact_orders"} + ], + "focus": ["model:fact_orders"], + "direction": "upstream" +} +``` + +## Column lineage + +When the target uses `model.column` syntax, lineage traces the specific column through upstream or downstream transformations. Each edge is annotated with a transform type (`direct`, `expression`, `aggregation`, `cast`, `star`, `constant`) and a confidence level. See [Column Lineage](/docs/concepts/column-lineage) for a full explanation of transform types, confidence levels, and analysis modes. + +```bash +# Where does fact_orders.total_cents come from? +sqb lineage fact_orders.total_cents + +# What consumes fact_orders.order_id downstream? +sqb lineage fact_orders.order_id --direction downstream +``` + +Column lineage supports `upstream` and `downstream` directions (not `both`). The `--mode` flag selects the analysis mode: `rich` (default, full SQL analysis) or `fast` (lightweight, faster on large projects). + +### Tree + +```bash +sqb lineage fact_orders.total_cents +``` + +``` +Column trace fact_orders.total_cents upstream + + <- stg_payments.amount_cents (expression) + <- raw__payments.amount_cents (direct) +``` + +### List + +```bash +sqb lineage fact_orders.total_cents --format list +``` + +``` +Column dependencies + +stg_payments.amount_cents -> fact_orders.total_cents expression +raw__payments.amount_cents -> stg_payments.amount_cents direct +``` + +### JSON + +```bash +sqb lineage fact_orders.total_cents --format json +``` + +```json +{ + "target": { + "resource_type": "model", + "resource_name": "fact_orders", + "column_name": "total_cents" + }, + "direction": "upstream", + "metadata": { + "mode": "rich", + "max_depth": null, + "analyzed_models": 5, + "truncated": false + }, + "trace": [ + { + "source": { + "resource_type": "model", + "resource_name": "stg_payments", + "column_name": "amount_cents" + }, + "target": { + "resource_type": "model", + "resource_name": "fact_orders", + "column_name": "total_cents" + }, + "transform": "expression", + "confidence": "high" + } + ] +} +``` + +## Examples + +```bash +# Upstream dependencies of a model (default) +sqb lineage fact_orders + +# Downstream dependents +sqb lineage fact_orders --direction downstream + +# Both directions, limited to 1 hop +sqb lineage fact_orders --direction both --depth 1 + +# Column lineage: trace a specific column upstream +sqb lineage fact_orders.total_cents + +# Column lineage: trace downstream consumers +sqb lineage fact_orders.order_id --direction downstream + +# Column lineage with depth limit +sqb lineage fact_orders.total_cents --depth 1 + +# Lineage for all models in a path +sqb lineage --select path:models/marts + +# Lineage between two models (path-between) +sqb lineage --select "stg_orders~daily_activity_rollup" + +# Upstream expansion from a model +sqb lineage --select "+fact_orders" + +# JSON output for programmatic consumption +sqb lineage fact_orders --format json --direction upstream +``` + +## Depth limiting + +`--depth` controls how many hops from the focus node(s) to include. In selector mode, `--depth` requires name, source, seed, or path-between selectors - it cannot be combined with tag or path selectors or comma-intersection selectors. + +```bash +# Only immediate parents +sqb lineage fact_orders --depth 1 + +# Parents and grandparents +sqb lineage fact_orders --depth 2 + +# Only direct column dependencies +sqb lineage fact_orders.total_cents --depth 1 +``` diff --git a/website/src/content/docs/docs/cli/load.mdx b/website/src/content/docs/docs/cli/load.mdx new file mode 100644 index 0000000000..9b2c6bdbd2 --- /dev/null +++ b/website/src/content/docs/docs/cli/load.mdx @@ -0,0 +1,92 @@ +--- +title: "sqb load" +description: "Load managed sources into the warehouse." +sidebar: + label: "load" +--- + +Runs source loader functions and writes data into their target tables using the configured write strategy. + +## Usage + +```bash +sqb --project-dir load [flags] +``` + +## Flags + +| Flag | Description | +|------|-------------| +| `--select`, `-s` | Select specific sources or loaders by name | +| `--exclude` | Exclude specific sources or loaders | +| `--reload` | Pass `is_reload=True` to loader functions | +| `--concurrency` | Number of worker connections (default: from settings or 1) | +| `--start-cursor-ts` | Override start cursor for timestamp-based loaders (ISO format) | +| `--end-cursor-ts` | Override end cursor for timestamp-based loaders (ISO format) | +| `--start-cursor-int` | Override start cursor for integer-based loaders | +| `--end-cursor-int` | Override end cursor for integer-based loaders | +| `--json` | Output results as JSON | +| `--json-output` | Write JSON results to a file path | +| `--var` | Set project variables (`--var key=value`) | + +## Examples + +```bash +# Load all managed sources +sqb load + +# Load a specific source +sqb load --select raw_customers + +# Load with concurrency +sqb load --concurrency 4 + +# Reload all sources (full refresh behavior) +sqb load --reload + +# Override cursor bounds +sqb load --start-cursor-ts "2026-05-01T00:00:00" +``` + +## Execution order + +Loaders are executed in DAG topological order based on `depends_on` declarations. Independent loaders run concurrently when `--concurrency` is greater than 1. + +Intermediate loaders (those referenced only via `depends_on` without a direct source binding) run first, followed by the source-bound loaders that depend on them. + +## Output + +``` +Load ready (3 selected) + +Sources (3) + raw_customers + raw_orders + raw_payments + +Execution sqb load (concurrency: 1) + + 1/3 source raw_customers OK 0.05s rows=5 + 2/3 source raw_orders OK 0.03s rows=10 + 3/3 source raw_payments OK 0.02s rows=8 + +Completed successfully. +PASS=3 WARN=0 FAIL=0 SKIP=0 TOTAL=3 (0.12s) +``` + +## Auto-load during builds + +`sqb build` automatically loads managed sources before building dependent models. This is controlled by the `auto_load_sources` setting (default: `true`) and the `--load` / `--no-load` / `--reload` flags: + +```bash +# Default: auto-load is on +sqb build + +# Explicitly skip loading +sqb build --no-load + +# Force reload +sqb build --reload +``` + +See [Loaders](/docs/concepts/python-nodes/loaders) for full documentation on write strategies, the loader context API, and source deferral. diff --git a/website/src/content/docs/docs/cli/plan.mdx b/website/src/content/docs/docs/cli/plan.mdx new file mode 100644 index 0000000000..755b3ecc43 --- /dev/null +++ b/website/src/content/docs/docs/cli/plan.mdx @@ -0,0 +1,86 @@ +--- +title: "sqb plan" +description: "Preview what SQLBuild will do before executing." +sidebar: + label: "plan" +--- + +Shows the execution plan without making any changes. Useful for inspecting change detection, backfill policies, and selector scope before building. + +## Usage + +```bash +sqb --project-dir plan [flags] +``` + +## Flags + +| Flag | Description | +|------|-------------| +| `--no-sql-analysis` | Disable compile-time SQL analysis (`--no-sql-validation` is an alias) | +| `--no-cache` | Bypass the reusable compile-analysis cache for this invocation | +| `--no-python` | Exclude read-side Python tasks and assets from the plan | +| `--defer-to` | Resolve unselected model references against another target | +| `--as` | Compile for another target and show its plan, including [model migration](/docs/concepts/models/migrations) decisions, using the current connection. Inspection only. | +| `--json` | Output the plan as JSON | +| `--full-refresh` | Plan a full rebuild for selected models unless a model sets `full_refresh false`; `full_refresh true` forces a model without this flag | +| `--start-cursor-ts` | Override start cursor for timestamp incremental models (ISO format) | +| `--end-cursor-ts` | Override end cursor for timestamp incremental models (ISO format) | +| `--start-cursor-int` | Override start cursor for integer incremental models | +| `--end-cursor-int` | Override end cursor for integer incremental models | +| `--select`, `-s` | Select specific models | +| `--exclude` | Exclude specific models | + +## Output + +First run: + +``` +Plan ready (13 selected) + +First run (12) + stg_customers view + stg_payments view + stg_orders view + daily_order_partitioned partition_tracked (custom) + daily_revenue table + dim_customers table + fact_orders table + customer_status_snapshot merge (timestamp) + hourly_order_activity delete_insert (timestamp, microbatch) + daily_activity_rollup delete_insert (timestamp, microbatch) + hourly_activity_with_daily_context delete_insert (timestamp, microbatch) + order_status_index delete_insert (integer) + +Seeds (1) + waffle_types +``` + +Steady state: + +``` +Plan ready (13 selected) + +Normal (12) + 3 view + 3 table + 3 delete_insert (timestamp, microbatch) + 1 partition_tracked (custom) + 1 merge (timestamp) + 1 delete_insert (integer) + +Seeds (1) + waffle_types +``` + +When query or schema changes are detected, the plan shows the affected models with backfill actions and cascade information. + +## Missing upstream dependencies + +Planning a scoped selection whose upstream inputs were never built in the target fails with `S301` rather than producing a plan that would break at build time: + +``` +error[S301]: cannot build selected scope: 3 missing upstream dependencies (stg_orders, stg_payments, waffle_types) +``` + +Build the upstream chain first, or select it along with the model: `sqb build --select +fact_orders`. diff --git a/website/src/content/docs/docs/cli/playground.mdx b/website/src/content/docs/docs/cli/playground.mdx new file mode 100644 index 0000000000..4b94e7d775 --- /dev/null +++ b/website/src/content/docs/docs/cli/playground.mdx @@ -0,0 +1,83 @@ +--- +title: "sqb playground" +description: "Create a self-contained SQLBuild project to explore locally." +sidebar: + label: "playground" +--- + +Creates a self-contained waffle shop project with DuckDB. No warehouse credentials, no git clone, no external data - just a working project you can compile, build, test, and explore immediately. + +## Usage + +```bash +sqb playground [name] +``` + +The positional argument is the directory to create (default `sqlbuild-playground`). The template is chosen with `--template` (default `waffle_shop`). + +## Templates + +| Template | Description | +|----------|-------------| +| `waffle_shop` | Default. DuckDB-backed project with models, tests, scenarios, and macros. | +| `dagster` | Waffle shop project plus a `dagster/` directory with a ready-to-run `definitions.py`. | +| `rivers` | Waffle shop project plus a `rivers_pipeline/` directory with a Rivers repository definition. | +| `python_nodes` | A small DuckDB project demonstrating [Python nodes](/docs/concepts/python-nodes/overview): a task feeding a loader, a model read by a Python asset through `ctx.relation(model(...))`, a soft-skip fan-in, `materialized=False`, and a Python check. | + +## What it creates + +A complete DuckDB-backed project with: + +- Staging views, fact/dimension tables, and incremental models +- Sources with inline expression data (no external setup) +- Seeds, SQL functions, and a custom materialization +- Built-in and custom audits +- SQL unit tests and multi-model tests +- E2E scenario tests +- Python macros +- AI agent skill files (auto-installed for OpenCode, Claude Code, and other agents) + +The `dagster` template adds: + +- `dagster/definitions.py` - Dagster definitions with `sqlbuild_assets`, `sqlbuild_scenario_checks`, and `SqlBuildCliResource` +- `dagster/README.md` - Setup instructions + +The `python_nodes` template instead creates a focused Python-nodes project: + +- `tasks/orders.py`, `loaders/orders.py`, `assets/orders_export.py`, `checks/orders_export.py` +- A `fact_orders` SQL model over a managed `raw_orders` source +- Examples of the SQL boundary, `ctx.relation(model(...))`, soft-skip fan-in, and a Python check + +## Examples + +```bash +# Default waffle shop +sqb playground waffle-shop +cd waffle-shop +sqb build + +# With Dagster integration +sqb playground --template dagster +cd sqlbuild-playground +uv pip install 'sqlbuild[dagster]' +dagster dev -f dagster/definitions.py + +# With Rivers integration +sqb playground --template rivers +cd sqlbuild-playground +uv pip install 'sqlbuild[rivers]' +uv run rivers dev rivers_pipeline.definitions + +# With Python nodes +sqb playground --template python_nodes +cd sqlbuild-playground +sqb build --select +fact_orders --select +orders_export +sqb check --select +check_orders_export +``` + +## Notes + +- The target directory must not already exist +- DuckDB is included as a core dependency - no extra installation needed +- The local DuckDB database file is created on the first build +- The Dagster template uses `prepare_if_dev()` to auto-generate the DAG artifact when Dagster starts in dev mode diff --git a/website/src/content/docs/docs/cli/query.mdx b/website/src/content/docs/docs/cli/query.mdx new file mode 100644 index 0000000000..4f5fea8cba --- /dev/null +++ b/website/src/content/docs/docs/cli/query.mdx @@ -0,0 +1,99 @@ +--- +title: "sqb query" +description: "Run ad hoc SQL queries against the project database." +sidebar: + label: "query" +--- + +Execute ad hoc SQL against the active project connection. Useful for inspecting data, debugging models, or running one-off queries without leaving the SQLBuild CLI. + +## Usage + +```bash +sqb --project-dir query "SELECT * FROM dev.fact_orders LIMIT 5" +``` + +Or from a file: + +```bash +sqb --project-dir query --file my_query.sql +``` + +File paths are resolved from the current working directory. + +## Flags + +| Flag | Description | +|------|-------------| +| `sql` | SQL to execute (positional argument) | +| `--file` | Read SQL from a file instead of the command line | +| `--format` | Output format: `long` (default), `table`, `json`, or `csv` | +| `--limit` | Maximum rows to return (default: 20) | +| `--no-limit` | Disable the row limit | + +## Output formats + +### long (default) + +Vertical record format, one field per line: + +``` +-[ RECORD 1 ]---------------------------+ +order_id | 1 +customer_id | 100 +status | completed + +-[ RECORD 2 ]---------------------------+ +order_id | 2 +customer_id | 200 +status | completed + +2 rows +``` + +### table + +Horizontal table format: + +``` +order_id | customer_id | status +-------- | ----------- | --------- +1 | 100 | completed +2 | 200 | completed + +2 rows +``` + +### json + +JSON array of objects: + +```json +[{"order_id": 1, "customer_id": 100, "status": "completed"}] +``` + +### csv + +Standard CSV with headers: + +```csv +order_id,customer_id,status +1,100,completed +2,200,completed +``` + +## Examples + +```bash +# Quick inspection with default format +sqb query "SELECT * FROM dev.fact_orders" + +# Table format with higher limit +sqb query "SELECT * FROM dev.dim_customers" --format table --limit 50 + +# Export to JSON +sqb query "SELECT * FROM dev.daily_revenue" --format json --no-limit + +# Run SQL from a file +sqb query --file debug_query.sql +``` diff --git a/website/src/content/docs/docs/cli/rules.mdx b/website/src/content/docs/docs/cli/rules.mdx new file mode 100644 index 0000000000..e41bc4f703 --- /dev/null +++ b/website/src/content/docs/docs/cli/rules.mdx @@ -0,0 +1,49 @@ +--- +title: "sqb rules" +description: "List, inspect, or run compiler-integrated Rules and generate project guidance." +sidebar: + label: "rules" +--- + +Configured Rules run automatically through `sqb compile` and build planning. The `rules` command +provides focused catalogue, inspection, execution, and guidance operations. + +## Usage + +```bash +sqb rules list +sqb rules show +sqb rules run [--select ] [--exclude ] +sqb rules skills [--check] +``` + +Add `--json` after `rules` for machine-readable list or run output: + +```bash +sqb rules --json list +sqb rules --json run SQBRSQL +``` + +Lifecycle progress is written to stderr so stdout remains valid JSON. + +## Commands + +- `list`: print built-in and repository-defined Rules. +- `show`: print metadata for one exact code. +- `run`: compile and evaluate only one exact code or family prefix. +- `skills`: install guidance derived from configured Rules; `--check` verifies freshness without writing. + +`run` accepts normal SQLBuild model selectors. Native built-ins execute before custom Python Rules. + +## Exit codes + +| Command | Code | Meaning | +|---|---:|---| +| `rules list` | 0 | Catalogue printed | +| `rules show` | 0 | Exact code found | +| `rules show` | 2 | Unknown code | +| `rules run` | 0 | No findings | +| `rules run` | 1 | Findings reported or compilation failed | +| `rules run` | 2 | Unknown code or family | +| `rules skills --check` | 0 | Guidance is fresh | +| `rules skills --check` | 1 | Guidance is missing or stale | diff --git a/website/src/content/docs/docs/cli/scenario.mdx b/website/src/content/docs/docs/cli/scenario.mdx new file mode 100644 index 0000000000..1956c499f1 --- /dev/null +++ b/website/src/content/docs/docs/cli/scenario.mdx @@ -0,0 +1,159 @@ +--- +title: "sqb scenario" +description: "Run end-to-end scenario tests against the warehouse or locally with DuckDB." +sidebar: + label: "scenario" +--- + +Run end-to-end scenario tests. Scenarios materialize fixture inputs as physical relations, build the real project graph against them, and evaluate expected outputs and assertions. See [Scenarios](/docs/concepts/scenarios) for concepts and authoring details. + +## sqb scenario test + +Run scenario tests against the warehouse. + +```bash +sqb scenario test [flags] +``` + +### Flags + +| Flag | Description | +|------|-------------| +| `--select`, `-s` | Select scenarios to run | +| `--exclude` | Exclude scenarios from the selection | +| `--retain` | Keep scenario-owned warehouse artifacts for inspection | +| `--local` | Run locally against DuckDB using captured JSONL snapshots | +| `--strict` | Treat missing/stale local snapshots as errors instead of skips | +| `--sync-snapshots` | Capture missing/stale snapshots before local run (requires `--local`) | +| `--refresh` | Recapture all selected snapshots before local run (requires `--local`) | +| `--force` | Bypass snapshot capture safety limits | +| `--max-snapshot-rows` | Override per-relation row limit for capture | +| `--max-snapshot-total-rows` | Override total row limit for capture | +| `--max-snapshot-bytes` | Override per-relation byte limit for capture | +| `--max-snapshot-total-bytes` | Override total byte limit for capture | +| `--no-sql-analysis` | Disable compile-time SQL analysis (`--no-sql-validation` is an alias) | + +### Selectors + +Scenarios are selected with `--select`. `--exclude` removes scenarios from the selection. Without any selectors, all discovered scenarios run. + +| Selector | Example | +|----------|---------| +| Scenario name | `sqb scenario test --select daily_revenue_minimal` | +| Multiple names | `sqb scenario test --select daily_revenue_minimal --select daily_revenue_multi_order` | +| `.sql` file path | `sqb scenario test --select tests/scenarios/revenue/daily_revenue_minimal.sql` | +| Folder | `sqb scenario test --select tests/scenarios/revenue` | +| Scenario-root-relative folder | `sqb scenario test --select revenue` | +| Exclude | `sqb scenario test --select revenue --exclude daily_revenue_multi_order` | + +Mixed selector types are supported and the result is de-duplicated by scenario name. + +### Remote examples + +```bash +# Run all scenarios +sqb scenario test + +# Run one scenario +sqb scenario test --select daily_revenue_minimal + +# Run and retain warehouse artifacts +sqb scenario test --select daily_revenue_minimal --retain + +# Run all scenarios in a folder +sqb scenario test --select revenue +``` + +### Local examples + +```bash +# Run locally (requires prior capture) +sqb scenario test --local + +# Run locally, capture missing/stale snapshots first +sqb scenario test --local --sync-snapshots + +# Run locally, recapture everything first +sqb scenario test --local --refresh + +# Fail on missing/stale snapshots instead of skipping +sqb scenario test --local --strict +``` + +### Output + +Remote scenarios report per-scenario PASS/FAIL with nested check rows: + +``` +daily_revenue_minimal PASS + check expected daily_revenue PASS + check assertion no_negative_revenue PASS + +PASS=1 FAIL=0 TOTAL=1 +``` + +Local scenarios add ERROR and SKIP statuses: + +``` +PASS=2 FAIL=0 ERROR=0 SKIP=1 TOTAL=3 +``` + +Failed scenarios suggest rerunning with `--retain` for inspection. Local runs always keep the DuckDB file at `target/run/scenarios//local.duckdb`. + +## sqb scenario capture + +Capture scenario input fixtures from the warehouse as JSONL snapshots for local replay. + +```bash +sqb scenario capture [flags] +``` + +### Flags + +| Flag | Description | +|------|-------------| +| `--select`, `-s` | Select scenarios to capture | +| `--exclude` | Exclude scenarios from the selection | +| `--retain` | Keep warehouse fixture artifacts after capture | +| `--force` | Bypass snapshot capture safety limits | +| `--max-snapshot-rows` | Override per-relation row limit | +| `--max-snapshot-total-rows` | Override total row limit | +| `--max-snapshot-bytes` | Override per-relation byte limit | +| `--max-snapshot-total-bytes` | Override total byte limit | +| `--no-sql-analysis` | Disable compile-time SQL analysis (`--no-sql-validation` is an alias) | + +### Examples + +```bash +# Capture all scenarios +sqb scenario capture + +# Capture one scenario +sqb scenario capture --select daily_revenue_minimal + +# Capture and retain warehouse artifacts +sqb scenario capture --select daily_revenue_minimal --retain +``` + +Snapshots are written to `tests/_scenario_snapshots//` with a `scenario.json` manifest and JSONL files for each fixture relation. These files can be committed to version control. + +## Runtime artifacts + +Both remote and local scenario runs write runtime artifacts to `target/run/scenarios//`: + +``` +target/run/scenarios/daily_revenue_minimal/ + cleanup/ + prepare.sql + final.sql + fixtures/ + ref__stg_orders.sql + ref__stg_payments.sql + models/ + marts/daily_revenue.sql + checks/ + expected__daily_revenue.sql + assertion__no_negative_revenue.sql +``` + +Local runs additionally write to a `local/` subdirectory and create `local.duckdb`. diff --git a/website/src/content/docs/docs/cli/scope.mdx b/website/src/content/docs/docs/cli/scope.mdx new file mode 100644 index 0000000000..3cd67cba3d --- /dev/null +++ b/website/src/content/docs/docs/cli/scope.mdx @@ -0,0 +1,395 @@ +--- +title: "scope" +description: "Inspect declaration visibility, usage, placement, and move impact offline." +--- + +`sqb scope` explains the lexical environment around native SQLBuild macros, enums, and constants. Use it to see what a resource can access, what it actually uses, why a declaration is visible or inaccessible, and whether a move would cross a scope boundary. + +The command is read-only and offline. It does not connect to a warehouse, require warehouse credentials, or inspect relations. It reads the compiler-owned declaration-scope index and reuses a deterministic cache when the relevant source and configuration fingerprint has not changed. Building a cold cache uses the normal compiler expansion path, which loads the configured adapter and may execute authored Python macros; a warm cache hit reconstructs scope facts without importing macro modules. + +Text output is deterministic, bounded, and requires no interactive pager. + +## Usage + +```bash +# Inspect an existing resource, declaration, or exact resource path +sqb scope TARGET [flags] + +# Inspect the scope at a prospective file or directory +sqb scope --at PATH [flags] + +# Preview moving an existing resource to a new path +sqb scope TARGET --as-path PATH [flags] +``` + +`TARGET` is either a kind-qualified identity or an exact project-relative resource path: + +```bash +sqb scope model:stg_orders +sqb scope test:stg_orders__excludes_cancelled +sqb scope scenario:daily_revenue +sqb scope hook:grant_select +sqb scope function:normalize_status +sqb scope audit:positive_order_value +sqb scope source:raw__orders + +sqb scope macro:normalize_order_status +sqb scope enum:order_status +sqb scope constant:minimum_order_value + +sqb scope models/staging/orders/stg_orders.sql +``` + +Bare names such as `stg_orders` and `order_status` are rejected even if they are currently unique. Qualification keeps commands stable when names collide across resource or declaration kinds. Public declaration identities are `macro:`, `enum:`, and `constant:`. + +Model-private constants and enums include their owner in the introspection identity: + +```text +enum:model:stg_orders._state +constant:model:stg_orders._minimum_value +``` + +These identities are for inspection. SQL authored inside the owning model continues to use `@enum("_state")` and `@const("_minimum_value")`. + +## Flags + +| Flag | Description | +|------|-------------| +| `--at PATH` | Inspect path-derived visibility for a prospective file or directory. A trailing `/` denotes a directory whose direct children are being considered. | +| `--as-path PATH` | Preview moving the existing target resource to `PATH`. No file or configuration is changed. | +| `--browse PATH` | Show direct child declaration folders and recursive counts without listing declarations. | +| `--list PATH` | Recursively list declarations under one declaration folder. | +| `--defined-under PATH` | Keep declarations whose definitions are under the project-relative path. | +| `--kind KIND` | Keep `macro`, `enum`, or `constant` declarations. Repeat to include multiple kinds. | +| `--match GLOB` | Match declaration names or qualified identities with a deterministic, case-sensitive glob. | +| `--used-only` | Keep declarations used by the target. | +| `--include-nearby` | Include bounded nearby declarations that are unavailable to the target. | +| `--nearby-depth N` | Set filesystem proximity for nearby descendant and sibling discovery (default: `1`). | +| `--dependency-depth N` | Expand dependencies from declarations in the used section by `N` edges (default: `0`). | +| `--explain QUALIFIED_NAME` | Explain one qualified declaration in detail at the target. | +| `--globals POLICY` | Control global declarations in the main report: `summary` (default), `used`, or `all`. | +| `--page-size N` | Return at most `N` declarations per paged section (default: `100`). | +| `--after CURSOR` | Continue after a declaration identity returned as the previous section's next cursor. | +| `--paths MODE` | Render paths as `relative`, `compact`, or `none`. | +| `--json` | Write the canonical versioned JSON report instead of the text tree. | + +Filters combine in a fixed order: definition path, kind, glob, then usage. `--dependency-depth` +expands the filtered used declarations afterward. Parent-folder visibility is never depth-limited: +every unprefixed declaration role above the resource contributes to its scope. + +## Reading A Report + +The default text report keeps distinct facts in distinct sections: + +- **Available** contains declarations visible through the resource's own lexical path. +- **Used** contains declarations consumed by the resource, including tracked declaration dependencies. +- **Relationship grants** contains eligible file-based enums and constants made available through expected-output sections in a test or scenario. +- **Nearby unavailable** is opt-in and explains close declarations that are outside the target's scope. +- **Scope chain** starts with the resource's owner folder, then shows parent folders and the project + declaration roles. Its compact labels identify the access rule checked at each path. + +Ordinary text rows show each declaration's qualified identity, definition location, visibility, +reason for appearing, declaration-role root, and safe type or signature metadata where useful. +`--explain` additionally shows its owning path, narrowest required placement, current consumers, +dependencies, and the consumers affected by a placement mismatch. JSON includes the complete +structured declaration report. + +### See the whole scope at once + +Suppose orders and returns models use a macro published from their shared `commerce` folder, and +that macro composes two project-wide macros: + +```text +project/ +├── macros/ +│ └── currency.py add_tax, round_money +└── models/ + └── commerce/ + ├── macros/ + │ └── orders.py formatted_order_total + ├── orders.sql + └── returns/ + └── returns.sql +``` + +```python +# models/commerce/macros/orders.py +from macros.currency import add_tax, round_money + + +def formatted_order_total(expression: str) -> str: + return round_money(add_tax(expression)) +``` + +```sql +-- models/commerce/orders.sql +MODEL(); + +SELECT @formatted_order_total("subtotal") AS total +``` + +`returns/returns.sql` calls the same macro, so the declaration must remain available throughout +`models/commerce/` and its descendants. + +The default report puts actual usage next to the complete directory-derived scope. Unused globals +stay collapsed, so the useful facts remain visible even in a large project: + +```console +$ sqb scope model:orders +Scope + Target: model:orders + Resource: model:orders + Path: models/commerce/orders.sql + +Used (1) + └─ ● macro:formatted_order_total [macro; descendant-public; inherited_ancestor; params 1; role models/commerce/macros] models/commerce/macros/orders.py:5:1 + +Scope chain + ├─ exact-owner-private models/commerce (1) + ├─ descendant-public models (0) + └─ project global (2) + +Available (1 of 3, 2 collapsed) + └─ ● macro:formatted_order_total [macro; descendant-public; inherited_ancestor; params 1; role models/commerce/macros] models/commerce/macros/orders.py:5:1 + … 2 globals collapsed; run sqb scope model:orders --globals all + +Relationship grants (0 of 0) + (none) + +Nearby unavailable (0 of 0) + (none) + +Diagnostics (0) + (none) +Completeness: complete +``` + +This answers several questions without opening declaration files: what the model actually uses, +where that declaration came from, which owner and parent folders SQLBuild checks, how many +project-wide declarations are available, and whether the result is complete. The scope-chain count +at `models/commerce` includes declarations owned there; the `exact-owner-private` chain label does +not mean every declaration in that count is private. + +### Follow composed macro dependencies + +`Used` is direct by default. Add `--dependency-depth` to follow declarations used by those +declarations. In this example, the model calls `formatted_order_total`, whose Python body calls +`add_tax` and `round_money`: + +```console +$ sqb scope model:orders --used-only --dependency-depth 1 +Scope + Target: model:orders + Resource: model:orders + Path: models/commerce/orders.sql + +Used (3) + ├─ ● macro:add_tax [macro; project; dependency; params 1; role macros] macros/currency.py:1:1 + ├─ ● macro:formatted_order_total [macro; descendant-public; inherited_ancestor; params 1; role models/commerce/macros] models/commerce/macros/orders.py:5:1 + └─ ● macro:round_money [macro; project; dependency; params 1; role macros] macros/currency.py:5:1 +``` + +The remaining report sections follow the `Used` section as usual. The dependency graph comes from +actual Python calls, including nested calls and calls reached through private helpers. Merely +importing a macro does not make it used. Increase the depth to follow longer chains. + +Explain the composed macro to inspect its direct graph and placement facts: + +```console +$ sqb scope model:orders --explain macro:formatted_order_total +Explanation + └─ ● macro:formatted_order_total [macro; descendant-public; inherited_ancestor; params 1; role models/commerce/macros] models/commerce/macros/orders.py:5:1 + Owner: (none) + Owning path: models/commerce + Consumers: model:orders, model:returns + Dependencies: macro:add_tax, macro:round_money + Grants: (none) + Required scope: descendant-public + Required path: models/commerce + Promotion impact: (none) +``` + +The complete command also prints the ordinary report sections above the `Explanation` section. +Use `--json` when another tool needs the same graph and placement facts as structured data. + +Global declarations are an intentional project-wide API and can be numerous. The default `--globals summary` always retains globals used by the target but collapses the unused global inventory with exact counts. Use `--globals used` for only used globals or `--globals all` when a bounded full list is appropriate. + +```bash +# Include every global declaration in the paged report +sqb scope model:stg_orders --globals all + +# Focus on constants and enums defined in finance +sqb scope model:stg_orders \ + --kind constant \ + --kind enum \ + --defined-under models/finance + +# Find visible or used settlement declarations +sqb scope model:stg_orders --match '*settlement*' +sqb scope model:stg_orders --used-only +``` + +## Expected-output access + +Tests and scenarios have two independent ways to reach declarations. Their own folder paths provide +ordinary visibility. For tests, each explicit `__expected__` section adds the eligible +file-based macros, enums, and constants available to that model. Scenarios receive the model's +file-based enums and constants but not its macros. These relationships include eligible +exact-owner-private declarations. + +`sqb scope` reports this additional access separately under **Relationship grants** and names the +model that provides it. With multiple expected models, SQLBuild makes the deterministic union of +their eligible declarations available while compiling the whole test or scenario. It never includes +declarations defined inside a model's `MODEL()` header. A test filename, mirrored path, or mock does +not provide this access by itself. + +For macro-mode tests, **Relationship grants** also reports +`tested_macro through macro:`. This grants the tested macro and the scoped file-based +declarations available from its production owner, as inferred from calls in `__macro_actual__`. + +```bash +sqb scope test:orders__completed_only +sqb scope scenario:daily_revenue --used-only +``` + +## Nearby And Explain + +Nearby discovery is deliberately opt-in and bounded. It considers relevant declarations in the +same authored resource tree, parent folders, close neighboring branches and child folders, and +folders connected through expected-output relationships. It does not dump every private +declaration in the project. + +```bash +# Find declarations just outside the model's effective scope +sqb scope model:stg_orders --include-nearby + +# Include descendants and siblings two directory levels away +sqb scope model:stg_orders --include-nearby --nearby-depth 2 + +# Explain one declaration's visibility and placement at this model +sqb scope model:stg_orders --explain enum:customer_status +``` + +An explanation distinguishes a known but unavailable declaration from an unknown name. It reports +where the declaration is defined, why it is or is not available, which files use it, access through +expected output, required placement, and consumers affected by a placement mismatch. It does not +move or rewrite the declaration. + +`--dependency-depth` is separate from `--nearby-depth`: it follows tracked declaration dependencies from the used section rather than filesystem proximity. + +```bash +sqb scope model:stg_orders --used-only --dependency-depth 2 +``` + +## Prospective Paths + +Use `--at` before creating a resource. A prospective file receives the declarations implied by that exact authored path. A prospective directory describes what a direct child resource would receive. + +```bash +sqb scope --at models/staging/orders/new_model.sql +sqb scope --at tests/unit/staging/orders/ +``` + +The path must be project-relative, below a configured authored resource root, and use the appropriate resource suffix (`.sql`, or `.yml`/`.yaml` for sources). Paths outside those roots produce a diagnostic rather than borrowing scope from a nearby directory. + +Prospective reports are intentionally partial: static path visibility is available, but runtime +usage and expected-output relationships do not exist yet. The report marks those sections +incomplete and exits nonzero while preserving the useful static result. + +## Move Preview + +`--as-path` calculates the visibility delta for moving one existing resource. It reports retained, +gained, and lost declarations; direct usages that the move would invalidate; the new resource-tree +root; declarations private to the resource's owner folder; and expected-output access retained +independently of folder visibility. + +```bash +sqb scope model:stg_orders \ + --as-path models/marts/orders/stg_orders.sql +``` + +The destination must be a valid project-relative file path for that resource kind. This is a pure preview: `sqb scope` never moves files, edits declarations, or changes configuration. + +## Folder Browsing + +Browse and list are separate so a project with 10,000 or more declarations remains safe to explore. `--browse` returns only direct child declaration folders. Each folder has exact recursive declaration, usage, and kind counts plus its direct child-folder count; no arbitrary alphabetical prefix of declarations is printed. + +Global roots appear in the browse namespace as `global/macros`, `global/constants`, and `global/enums`, regardless of how files are organized recursively beneath the top-level declaration roots. + +```bash +# Start with folder summaries only +sqb scope model:stg_orders --browse . + +# Walk down without listing declarations +sqb scope model:stg_orders --browse global +sqb scope model:stg_orders --browse global/macros/finance + +# List declarations only after choosing a bounded domain +sqb scope model:stg_orders --list global/macros/finance/payments +``` + +`--list` is recursive and supports `--kind`, `--match`, `--defined-under`, `--used-only`, `--page-size`, and `--after`. Browse output itself stays folder-first. + +## Pagination + +Flat lists and report sections use qualified declaration identities as stable lexical cursors, not page numbers. In canonical JSON, every paged section reports its total, returned count, completeness, truncation state, and `next_cursor`. Repeat the same command and filters with that identity as `--after`: + +```bash +# First page +sqb scope model:stg_orders \ + --list global/macros/legacy \ + --page-size 50 + +# Continue with the next_cursor from the first result +sqb scope model:stg_orders \ + --list global/macros/legacy \ + --page-size 50 \ + --after macro:legacy_batch_0049 +``` + +Keep all semantic filters unchanged while continuing. Qualified cursors avoid cross-kind ambiguity. A bare cursor is accepted only when it resolves uniquely; invalid or ambiguous cursors produce a diagnostic and nonzero status rather than silently selecting a different page. + +For automation, read each section's `next_cursor` from JSON and continue until it is `null`: + +```bash +sqb scope model:stg_orders \ + --list global/macros/legacy \ + --page-size 50 \ + --json > scope-page.json +``` + +## Paths And JSON + +`--paths relative` shows normalized project-relative paths. `compact` shortens repeated path context +in text output. `none` omits declaration definition locations and replaces structural and header +paths with `(hidden)`. Paths in stable machine output never expose an absolute workspace root. + +`--json` emits the canonical schema rather than serializing the visual tree. The top-level `schema_version` is currently `1`. Reports include the target, scope chain, declaration sections, applied filters, section totals, collapsed and truncated flags, cursors, diagnostics, and aggregate and section-level completeness. Move previews and explanations appear when requested. + +JSON is deterministically ordered, ASCII, newline-terminated, and byte-stable for identical inputs. It contains no ANSI formatting. Consumers should check `schema_version` before relying on fields. + +## Partial Projects + +Scope inspection remains useful while a project is broken. SQLBuild retains valid facts and marks +affected report sections complete or incomplete. Text and JSON never present a partial section as +complete, and diagnostics identify faults that prevented the missing facts from being indexed. + +Diagnostics are stable and include project-relative source locations when available. A partial result, invalid target or cursor, or any error diagnostic produces a nonzero exit status after the available report is written. This lets an editor or agent consume path visibility while still treating incomplete analysis as a failed check. + +## Output Safety + +Scope reports describe declarations without exposing authored values or runtime secrets: + +- Constants show logical type, nullability, collection kind, item count, and rendering mode, not values. +- Enums show scalar type, member count, and a bounded preview of member names, not member values. +- Macros show parameters and tracked declaration dependencies, not source bodies, callables, or source digests. +- Credentials, connection fields, environment variables, warehouse data, and absolute machine paths are not inspected or emitted. + +There is no `--show-values` option. + +## Native SQLBuild Only + +`sqb scope` inspects native SQLBuild authored resources and declarations. It does not discover or emulate dbt models, dbt or Jinja macros, package dispatch, dbt manifests, dbt selectors, dbt tests, or dbt schema YAML visibility. An external dbt graph dependency does not contribute declarations or lexical scope. + +For declaration directory rules, placement checks, test access through expected output, and scoped +macro imports, see [Declarations and Scopes](/docs/concepts/declaration-scopes). diff --git a/website/src/content/docs/docs/cli/seed.mdx b/website/src/content/docs/docs/cli/seed.mdx new file mode 100644 index 0000000000..739fc20003 --- /dev/null +++ b/website/src/content/docs/docs/cli/seed.mdx @@ -0,0 +1,31 @@ +--- +title: "sqb seed" +description: "Load seed CSV files into the warehouse." +sidebar: + label: "seed" +--- + +Loads seed CSV files into the warehouse as tables. Seeds are fully replaced on every run. + +## Usage + +```bash +sqb --project-dir seed [flags] +``` + +## Flags + +| Flag | Description | +|------|-------------| +| `--select`, `-s` | Select specific seeds by name | +| `--exclude` | Exclude specific seeds | + +## Examples + +```bash +# Load all seeds +sqb seed + +# Load a specific seed +sqb seed --select seed:waffle_types +``` diff --git a/website/src/content/docs/docs/cli/skills.mdx b/website/src/content/docs/docs/cli/skills.mdx new file mode 100644 index 0000000000..6c628669c6 --- /dev/null +++ b/website/src/content/docs/docs/cli/skills.mdx @@ -0,0 +1,102 @@ +--- +title: "sqb skills" +description: "Install SQLBuild skill files for AI coding agents." +sidebar: + label: "skills" +--- + +Install or update SQLBuild skill files so AI coding agents (Claude Code, OpenCode, Cursor, etc.) understand your project's framework, syntax, and conventions. + +Write the packaged SQLBuild skill file to agent-specific locations in your project. + +```bash +sqb skills [flags] +``` + +## Flags + +| Flag | Description | +|------|-------------| +| `--target` | Specify agent targets to install for. Can be passed multiple times. | +| `--global` | Install to global agent config directories instead of project-local | +| `--force` | Overwrite existing skill files even if they were not generated by SQLBuild | + +## Targets + +Three agent targets are supported: + +| Target | Local path | Global path | +|--------|-----------|-------------| +| `opencode` | `.opencode/skills/sqlbuild/SKILL.md` | `~/.config/opencode/skills/sqlbuild/SKILL.md` | +| `claude` | `.claude/skills/sqlbuild/SKILL.md` | `~/.claude/skills/sqlbuild/SKILL.md` | +| `agents` | `.agents/skills/sqlbuild/SKILL.md` | `~/.agents/skills/sqlbuild/SKILL.md` | + +By default, SQLBuild installs `agents` and `claude`. OpenCode consumes the shared `.agents` +skill, while Claude requires its `.claude` copy. The `opencode` target remains available as an +explicit override for installations that require `.opencode`: + +```bash +# Install the agents and claude targets (default) +sqb skills + +# Install for Claude Code only +sqb skills --target claude + +# Install for OpenCode and Claude +sqb skills --target opencode --target claude + +# Install globally +sqb skills --global +``` + +## Overwrite behavior + +Generated skill files include an ownership marker. `sqb skills` will: + +- Overwrite files it previously generated (safe to rerun) +- Refuse to overwrite files that were manually created or edited (no marker) +- Overwrite any file when `--force` is passed + +Normal project commands append a non-blocking notice when an installed, SQLBuild-owned generated +skill is stale. The command still runs normally. + +## Configuration + +Default targets can be set in `sqlbuild_project.toml` so the team shares the same agent config: + +```toml +[skills] +targets = ["agents", "claude"] +auto_update = true +``` + +CLI `--target` flags override the TOML config. + +Set `auto_update = true` to refresh stale generated skills during normal project commands. Auto +update reads the skill bundled with the installed SQLBuild package and rewrites only files marked +as SQLBuild-owned. It never overwrites a custom file or an unowned path collision. + +## Playground + +The playground command automatically runs `sqb skills` after creating the project, so AI agents are ready to use immediately: + +```bash +sqb playground waffle-shop +cd waffle-shop +# Agent skill files are already installed +``` + +## Project Rules skills + +`sqb skills` installs general SQLBuild framework guidance. `sqb rules skills` generates +project-specific guidance from the active Rules, options, thresholds, naming vocabulary, and scoped +deviations: + +```bash +sqb rules skills +sqb rules skills --check +``` + +Use `--check` in CI to detect missing or stale Rules guidance without rewriting files. Rules use +the separate `sqlbuild-rules` skill path and refuse to overwrite divergent or unowned content. See +[Compiler-integrated Rules](/docs/concepts/rules) and the [Rules CLI reference](/docs/cli/rules). diff --git a/website/src/content/docs/docs/cli/test.mdx b/website/src/content/docs/docs/cli/test.mdx new file mode 100644 index 0000000000..0d1e73d882 --- /dev/null +++ b/website/src/content/docs/docs/cli/test.mdx @@ -0,0 +1,41 @@ +--- +title: "sqb test" +description: "Run SQL unit tests and multi-model tests in isolation." +sidebar: + label: "test" +--- + +Runs SQL unit tests, independently reported parameterized cases, and multi-model tests without +building models. Useful for validating test logic independently. + +## Usage + +```bash +sqb --project-dir test [flags] +``` + +## Flags + +| Flag | Description | +|------|-------------| +| `--no-sql-analysis` | Disable compile-time SQL analysis (`--no-sql-validation` is an alias) | +| `--select`, `-s` | Select tests targeting specific models | +| `--exclude` | Exclude tests targeting specific models | + +Parameterized cases use parent-level selection. Selecting a target model includes every case in +each matching `TEST` template; case names are not model selectors. + +Text output identifies each case as ` []` and includes its source path and safe typed +parameters. Structured JSON emits one check per case with stable source/block/case identity, +declared parameter types and nullability, typed values, and a content fingerprint. Exact decimals +are strings in JSON so their scale is preserved. + +## Examples + +```bash +# Run all tests +sqb test + +# Run tests for a specific model +sqb test --select stg_orders +``` diff --git a/website/src/content/docs/docs/concepts/adapters.mdx b/website/src/content/docs/docs/concepts/adapters.mdx new file mode 100644 index 0000000000..fd0e6a7b1b --- /dev/null +++ b/website/src/content/docs/docs/concepts/adapters.mdx @@ -0,0 +1,113 @@ +--- +title: "Overview" +description: "Supported database engines and their connection configuration." +--- + +SQLBuild uses adapters to connect to different database engines. + +| Adapter | Status | Install | +|---------|--------|---------| +| [DuckDB](/docs/concepts/adapters/duckdb) | Supported | included by default | +| [MotherDuck](/docs/concepts/adapters/motherduck) | Supported | included by default (uses DuckDB) | +| [Snowflake](/docs/concepts/adapters/snowflake) | Supported | `sqlbuild[snowflake]` | +| [BigQuery](/docs/concepts/adapters/bigquery) | Supported | `sqlbuild[bigquery]` | +| [Databricks](/docs/concepts/adapters/databricks) | Supported | `sqlbuild[databricks]` | +| [PostgreSQL](/docs/concepts/adapters/postgres) | Supported | `sqlbuild[postgres]` | +| [SQL Server](/docs/concepts/adapters/sqlserver) | Supported | `sqlbuild[sqlserver]` | +| ClickHouse | Coming soon | | +| Redshift | Coming soon | | +| Trino | Coming soon | | +| Spark | Coming soon | | +| Athena | Coming soon | | + +Set the adapter in `sqlbuild_project.toml`: + +```toml +name = "my_project" +adapter = "duckdb" +``` + +Or override it per developer in `sqlbuild_local.toml`: + +```toml +adapter = "duckdb" +``` + +## Custom adapters + +You can write your own adapter for any database engine by placing Python files under `adapters/` in your project directory. + +### Extending a built-in adapter + +The most common case is extending an existing adapter with custom behavior. Subclass the built-in adapter, set a new `adapter_name`, and override what you need: + +```python +# adapters/duckdb_plus.py +from sqlbuild.integrations.duckdb.client import DuckDbAdapter + +class DuckDbPlusAdapter(DuckDbAdapter): + adapter_name = "duckdb_plus" + + def connect(self, config): + connection = super().connect(config) + # custom setup - load extensions, set pragmas, etc. + self.execute(connection, "SET enable_progress_bar = true") + return connection +``` + +```toml +adapter = "duckdb_plus" +``` + +### Writing an adapter from scratch + +For a database engine with no built-in support, subclass `BaseAdapter`. It provides ANSI SQL defaults for most methods - you only need to implement `connect`, `execute`, `close`, and any methods where your engine differs from standard SQL: + +```python +# adapters/my_database.py +from sqlbuild.adapter.base.base_adapter import BaseAdapter + +class MyDatabaseAdapter(BaseAdapter): + adapter_name = "my_database" + sql_analysis_dialect_name = "postgres" # SQL dialect for validation and lineage + + def connect(self, config): + ... + + def execute(self, connection, sql): + ... + + def close(self, connection): + ... +``` + +Set `sql_analysis_dialect_name` to the SQL dialect name that matches your engine's SQL syntax. This enables compile-time SQL validation, column inference, column lineage, and local scenario replay for your adapter. If omitted, SQLBuild uses generic SQL parsing. SQL analysis is powered by [Polyglot](https://github.com/tobilg/polyglot), a Rust reimplementation of SQLGlot supporting 32+ dialects. + +For full control with no inherited defaults, subclass `StrictAdapter` instead. Every method is abstract and must be implemented explicitly. SQLBuild raises a clear error listing any unimplemented methods. + +### Discovery rules + +- SQLBuild discovers all `.py` files under `adapters/` recursively (excluding `__init__.py` and files starting with `_`) +- Each file is scanned for classes that define a string `adapter_name` and subclass `StrictAdapter` (or any of its subclasses like `BaseAdapter` or a built-in adapter) +- Adapter names must be unique across all adapter files - duplicates raise an error +- Custom adapter names cannot shadow built-in names (`duckdb`, `snowflake`, `bigquery`, `databricks`, `postgres`, `sqlserver`) + +### Adapter class hierarchy + +``` +StrictAdapter (fully abstract - all methods must be implemented) + └── BaseAdapter (ANSI SQL defaults - override only what differs) + ├── DuckDbAdapter + ├── SnowflakeAdapter + ├── BigQueryAdapter + ├── DatabricksAdapter + ├── PostgresAdapter + └── SqlServerAdapter +``` + +`StrictAdapter` composes four mixins that define the full adapter contract: + +- **ConnectionMixin** - `connect`, `close`, `begin`, `commit`, `rollback` +- **SchemaMixin** - `relation_exists`, `list_relations`, `describe_relation`, `ensure_schema` +- **MaterializationMixin** - `create_table_as`, `create_view`, `drop`, `rename`, `swap`, `load_seed` +- **DiffMixin** - `diff_schema`, `diff_rows`, `sample_unequal_rows`, `sample_side_only_rows` diff --git a/website/src/content/docs/docs/concepts/adapters/bigquery.mdx b/website/src/content/docs/docs/concepts/adapters/bigquery.mdx new file mode 100644 index 0000000000..8804c3c5e7 --- /dev/null +++ b/website/src/content/docs/docs/concepts/adapters/bigquery.mdx @@ -0,0 +1,62 @@ +--- +title: "BigQuery" +description: "BigQuery adapter configuration for SQLBuild." +--- + +BigQuery requires the optional `google-cloud-bigquery` dependency: + +```bash +pip install 'sqlbuild[bigquery]' +# or +uv pip install 'sqlbuild[bigquery]' +``` + +## Connection config + +```toml +adapter = "bigquery" +default_target = "dev" + +[connections.gcp] +project = "my-gcp-project" +location = "europe-west2" + +[targets.dev] +connection = "gcp" +database = "my-gcp-project" +schema = "analytics_dev" +``` + +| Field | Description | +|-------|-------------| +| `project` | GCP project ID (required) | +| `location` | BigQuery dataset location - a region (`europe-west2`, `us-east1`) or multi-region (`US`, `EU`) | +| `credentials_path` | Path to a service account JSON key file. If omitted, uses Application Default Credentials (ADC). | + +## Authentication + +BigQuery supports two authentication methods: + +**Application Default Credentials (default):** Uses the credentials from `gcloud auth application-default login` or the environment's service account. No config needed beyond `project`. + +**Service account file:** + +```toml +[connections.gcp] +project = "my-gcp-project" +credentials_path = "/path/to/service-account.json" +``` + +## Typed constants + +BigQuery renders typed constant arrays with bracket syntax, objects as native JSON expressions, and exact decimals as `NUMERIC` expressions. For example: + +```sql +['GB', 'FR', 'HK'] +JSON '{"GB":"Great Britain"}' +NUMERIC '2.4700' +``` + +BigQuery does not support arrays of arrays. A nested list or set requested with `render_as array` fails at compile time rather than producing invalid BigQuery SQL. Parenthesized `value_list` rendering remains available for ordinary scalar collections used with `IN`. + +See [Collections and Rendering](/docs/concepts/constants/collections-and-rendering#native-array-rendering) for declarations, rendering precedence, and the cross-adapter matrix. diff --git a/website/src/content/docs/docs/concepts/adapters/databricks.mdx b/website/src/content/docs/docs/concepts/adapters/databricks.mdx new file mode 100644 index 0000000000..dcf8504fef --- /dev/null +++ b/website/src/content/docs/docs/concepts/adapters/databricks.mdx @@ -0,0 +1,42 @@ +--- +title: "Databricks" +description: "Databricks adapter configuration for SQLBuild." +--- + +Databricks requires the optional `databricks-sql-connector` dependency: + +```bash +pip install 'sqlbuild[databricks]' +# or +uv pip install 'sqlbuild[databricks]' +``` + +## Connection config + +```toml +adapter = "databricks" +default_target = "dev" + +[connections.workspace] +server_hostname = "my-workspace.cloud.databricks.com" +http_path = "/sql/1.0/warehouses/abc123" +token = "dapi_my_access_token" + +[targets.dev] +connection = "workspace" +database = "my_catalog" +schema = "my_schema" +``` + +| Field | Description | +|-------|-------------| +| `server_hostname` | Databricks workspace hostname (required) | +| `http_path` | SQL warehouse or cluster HTTP path (required) | +| `token` | Personal access token (required) | + +The target's `database` selects the Unity Catalog catalog, and its `schema` selects the schema. + +## Session initialization + +On connect, SQLBuild runs `USE CATALOG` and `USE SCHEMA` statements from the active target to set +the authoritative session namespace. diff --git a/website/src/content/docs/docs/concepts/adapters/duckdb.mdx b/website/src/content/docs/docs/concepts/adapters/duckdb.mdx new file mode 100644 index 0000000000..c42ac865a7 --- /dev/null +++ b/website/src/content/docs/docs/concepts/adapters/duckdb.mdx @@ -0,0 +1,59 @@ +--- +title: "DuckDB" +description: "DuckDB adapter configuration for SQLBuild." +--- + +DuckDB is included as a core dependency. No extra installation needed. + +## Connection config + +```toml +adapter = "duckdb" +default_target = "dev" + +[connections.local] +database = "my_project.duckdb" + +[targets.dev] +connection = "local" +schema = "dev" +``` + +| Field | Description | +|-------|-------------| +| `database` | Path to the DuckDB database file. Use `:memory:` for in-memory databases. | +| `extensions` | List of DuckDB extensions to install and load on connect. | +| `settings` | Key-value pairs passed as `SET` statements on connect. | +| `attach` | List of additional databases to attach. | + +## Extensions and settings + +```toml +[connections.local] +database = "my_project.duckdb" +extensions = ["httpfs", "parquet"] + +[connections.local.settings] +memory_limit = "4GB" +``` + +## Attaching additional databases + +```toml +[connections.local] +database = "my_project.duckdb" + +[[connections.local.attach]] +path = "external_data.duckdb" +alias = "external" +read_only = true +``` + +## Table promotion mode + +DuckDB defaults to `staged` promotion: tables are materialized into a staging table, audited, then swapped into the target. This is configurable in `settings`: + +```toml +[settings] +table_promotion_mode = "staged" +``` diff --git a/website/src/content/docs/docs/concepts/adapters/motherduck.mdx b/website/src/content/docs/docs/concepts/adapters/motherduck.mdx new file mode 100644 index 0000000000..380ae3bf0c --- /dev/null +++ b/website/src/content/docs/docs/concepts/adapters/motherduck.mdx @@ -0,0 +1,76 @@ +--- +title: "MotherDuck" +description: "MotherDuck adapter configuration for SQLBuild." +--- + +MotherDuck uses DuckDB's built-in `md:` connection support. No extra installation needed beyond the core DuckDB dependency. + +## Connection config + +```toml +adapter = "motherduck" +default_target = "dev" + +[connections.motherduck] +database = "my_database" +token = "your_motherduck_token" + +[targets.dev] +connection = "motherduck" +database = "my_database" +schema = "dev" +``` + +| Field | Description | +|-------|-------------| +| `database` | MotherDuck database name. Automatically prefixed with `md:` if not already present. Defaults to `md:` (your default MotherDuck database). | +| `token` | MotherDuck access token. Can also be set via environment variable. | + +## Authentication + +MotherDuck requires an access token. Generate one from the MotherDuck UI and pass it via the connection config or an environment variable: + +```toml +[connections.motherduck] +database = "my_database" +token = "${ENV:MOTHERDUCK_TOKEN}" +``` + +## Shared connections across targets + +Use targets to separate production and development databases on MotherDuck: + +```toml +adapter = "motherduck" + +[connections.motherduck] +token = "${ENV:MOTHERDUCK_TOKEN}" +database = "my_database" + +[targets.prod] +connection = "motherduck" +database = "prod_db" +schema = "prod" + +[targets.dev] +connection = "motherduck" +database = "dev_db" +schema = "dev" +``` + +## Local development with DuckDB + +Use `sqlbuild_local.toml` to override the adapter for local development against a plain DuckDB file: + +```toml +adapter = "duckdb" + +[connections.local] +database = "local_dev.duckdb" + +[targets.dev] +connection = "local" +schema = "dev" +``` + +This lets you develop and test locally with zero MotherDuck compute cost, then deploy to MotherDuck in production. SQLBuild's [scenario replay](/docs/concepts/scenarios) also runs locally in DuckDB regardless of the production adapter. diff --git a/website/src/content/docs/docs/concepts/adapters/postgres.mdx b/website/src/content/docs/docs/concepts/adapters/postgres.mdx new file mode 100644 index 0000000000..66752859c7 --- /dev/null +++ b/website/src/content/docs/docs/concepts/adapters/postgres.mdx @@ -0,0 +1,63 @@ +--- +title: "PostgreSQL" +description: "PostgreSQL adapter configuration for SQLBuild." +--- + +PostgreSQL requires the optional `psycopg` dependency: + +```bash +pip install 'sqlbuild[postgres]' +# or +uv pip install 'sqlbuild[postgres]' +``` + +## Connection config + +```toml +adapter = "postgres" +default_target = "dev" + +[connections.postgres] +host = "localhost" +port = 5432 +user = "my_user" +password = "my_password" +dbname = "my_database" + +[targets.dev] +connection = "postgres" +database = "my_database" +schema = "analytics_dev" +``` + +| Field | Description | +|-------|-------------| +| `host` | PostgreSQL server hostname (default: `localhost`) | +| `port` | PostgreSQL server port (default: `5432`) | +| `user` | Database user | +| `password` | Database password | +| `dbname` | Database name | + +Connection fields are passed to `psycopg.connect()`. See the [psycopg documentation](https://www.psycopg.org/psycopg3/docs/api/connections.html) for all available options. The target remains authoritative for database and schema qualification. + +## Shared connections across targets + +```toml +adapter = "postgres" + +[connections.postgres] +host = "localhost" +user = "my_user" +password = "${ENV:PG_PASSWORD}" +dbname = "analytics" + +[targets.prod] +connection = "postgres" +database = "analytics" +schema = "prod" + +[targets.dev] +connection = "postgres" +database = "analytics" +schema = "dev" +``` diff --git a/website/src/content/docs/docs/concepts/adapters/snowflake.mdx b/website/src/content/docs/docs/concepts/adapters/snowflake.mdx new file mode 100644 index 0000000000..1e955f1e31 --- /dev/null +++ b/website/src/content/docs/docs/concepts/adapters/snowflake.mdx @@ -0,0 +1,65 @@ +--- +title: "Snowflake" +description: "Snowflake adapter configuration for SQLBuild." +--- + +Snowflake requires the optional `snowflake-connector-python` dependency: + +```bash +pip install 'sqlbuild[snowflake]' +# or +uv pip install 'sqlbuild[snowflake]' +``` + +## Connection config + +```toml +adapter = "snowflake" +default_target = "dev" + +[connections.warehouse] +account = "my_org-my_account" +user = "my_user" +password = "my_password" +role = "TRANSFORM_ROLE" +warehouse = "TRANSFORM_WH" + +[targets.dev] +connection = "warehouse" +database = "ANALYTICS" +schema = "RAW" +``` + +Connection fields are passed directly to `snowflake.connector.connect()`. See the [Snowflake Connector documentation](https://docs.snowflake.com/en/developer-guide/python-connector/python-connector-connect) for all available options, including key-pair authentication, OAuth, and SSO. Put the authoritative database and schema on the target. + +## Session initialization + +On connect, SQLBuild applies the connection's role and warehouse, then applies the active +target's authoritative database and schema. These ensure the session context is set correctly +regardless of the user's default settings. + +## Shared connections across targets + +Use multiple targets to reuse one Snowflake connection while selecting different authoritative +databases and schemas: + +```toml +adapter = "snowflake" + +[connections.warehouse] +account = "my_org-my_account" +user = "my_user" +password = "my_password" +role = "TRANSFORM_ROLE" +warehouse = "TRANSFORM_WH" + +[targets.prod] +connection = "warehouse" +database = "PROD_DB" +schema = "prod" + +[targets.dev] +connection = "warehouse" +database = "DEV_DB" +schema = "dev" +``` diff --git a/website/src/content/docs/docs/concepts/adapters/sqlserver.mdx b/website/src/content/docs/docs/concepts/adapters/sqlserver.mdx new file mode 100644 index 0000000000..0d746ed3fe --- /dev/null +++ b/website/src/content/docs/docs/concepts/adapters/sqlserver.mdx @@ -0,0 +1,77 @@ +--- +title: "SQL Server" +description: "Microsoft SQL Server adapter configuration for SQLBuild." +--- + +SQL Server requires the optional `pymssql` dependency: + +```bash +pip install 'sqlbuild[sqlserver]' +# or +uv pip install 'sqlbuild[sqlserver]' +``` + +## Connection config + +```toml +adapter = "sqlserver" +default_target = "dev" + +[connections.sqlserver] +host = "localhost" +port = 1433 +user = "sa" +password = "my_password" +database = "my_database" + +[targets.dev] +connection = "sqlserver" +database = "my_database" +schema = "analytics_dev" +``` + +| Field | Description | +|-------|-------------| +| `host` | SQL Server hostname (default: `localhost`). Also accepts `server` as an alias. | +| `port` | SQL Server port (default: `1433`) | +| `user` | Database user (default: `sa`). Also accepts `username` as an alias. | +| `password` | Database password | +| `database` | Database name (default: `master`). Also accepts `dbname` as an alias. | + +Connection fields are passed to `pymssql.connect()`. See the [pymssql documentation](https://pymssql.readthedocs.io/en/stable/ref/pymssql.html) for all available options. The target remains authoritative for database and schema qualification. + +SQL Server supports schema-only, full-row, and bounded `sqb diff` comparisons. + +## Typed constants + +SQL Server supports scalar and parenthesized `value_list` constants. Strings use Unicode literals where required, and objects render through a JSON expression such as: + +```sql +JSON_QUERY(N'{"GB":"Great Britain"}') +``` + +SQL Server has no supported first-class native array expression. Any list or set that resolves to `render_as array`, whether through its declaration or `[constants].collection_rendering`, fails at compile time. SQLBuild does not silently substitute a value list or quoted JSON string. + +See [Collections and Rendering](/docs/concepts/constants/collections-and-rendering#native-array-rendering) for declarations, rendering precedence, and the cross-adapter matrix. + +## Shared connections across targets + +```toml +adapter = "sqlserver" + +[connections.sqlserver] +host = "localhost" +user = "sa" +password = "${ENV:MSSQL_PASSWORD}" +database = "analytics" + +[targets.prod] +connection = "sqlserver" +database = "analytics" +schema = "prod" + +[targets.dev] +connection = "sqlserver" +database = "analytics" +schema = "dev" +``` diff --git a/website/src/content/docs/docs/concepts/audits.mdx b/website/src/content/docs/docs/concepts/audits.mdx new file mode 100644 index 0000000000..df6c74bf6f --- /dev/null +++ b/website/src/content/docs/docs/concepts/audits.mdx @@ -0,0 +1,301 @@ +--- +title: "Audits" +description: "Violation and measurement checks that gate data and record quality outcomes." +--- + +Audits are SQL queries that verify data quality. Violation audits return invalid rows. Measurement +audits return a value that SQLBuild evaluates against authored thresholds and sample policy. +SQLBuild can run audits before table promotion or incremental DML so error-severity failures do not +reach the target. + +## How audits work + +Violation audits pass when their query returns zero rows. Measurement audits produce one value and +optionally a sample count; their outcome is `pass`, `warn`, `fail`, or `insufficient`. + +For `error` severity audits: +- **Full table builds:** SQLBuild materializes into a staging table, runs audits against it, and only promotes to the target if all audits pass. If any fail, the staging table is kept for inspection and the production table is untouched. +- **Incremental models:** Delta-phase audits validate each batch before DML is applied. If an audit fails, the batch is not applied. + +For `warn` severity audits, the build continues and the failure is reported in the output. + +## Measurement audits + +A reusable measurement audit separates the aggregate query from optional bounded evidence: + +```sql +-- audits/generic/valid_order_rate.sql +AUDIT ( + evaluation measurement, + value valid_rate, + sample_count total_rows, + sample_unit rows +); + +MEASURE ( + SELECT + COUNT(*) AS total_rows, + 100.0 * AVG(CASE WHEN @condition THEN 1 ELSE 0 END) AS valid_rate + FROM @relation +); + +EVIDENCE ( + SELECT * FROM @relation WHERE NOT (@condition) +); +``` + +Attach threshold and sample policy where the audit is used: + +```sql +MODEL ( + audits [ + valid_order_rate ( + condition "order_id IS NOT NULL", + minimum_samples 100, + evidence_limit 20, + thresholds (warn (below 99.9), error (below 99)) + ) + ] +); +``` + +`minimum_samples` keeps low-volume measurements distinct as `insufficient` rather than inventing a +pass or failure. Evidence is diagnostic and bounded by `evidence_limit`; the measurement and +threshold determine the outcome. + +## Audit factories + +Use a Python audit factory when many related audit instances should be generated from one reviewed +declaration: + +```python +from sqlbuild.audits import AuditCase, AuditSeverity, audit_factory + + +@audit_factory +def order_quality(): + return [ + AuditCase( + name="positive_amount", + definition="expression_is_true", + arguments={"expression": "amount > 0"}, + severity=AuditSeverity.ERROR, + ) + ] +``` + +Attach it with `MODEL (audit_factories [order_quality])`. Generated cases compile to the same audit +contract as directly authored instances. + +## Result history + +Native warehouse adapters best-effort append confirmed audit outcomes to +`_sqlbuild_audit_results`. Rows are immutable and use deterministic IDs, so retrying the same result +is idempotent. Projection failure is reported separately and does not change the audit outcome or +command exit code. Lifecycle sinks can also consume the corresponding `audit_completed` fact, which +is published as each audit finishes. + +## Built-in audits + +SQLBuild includes four generic audits out of the box. You do not need to define these in `audits/generic/` - they are available automatically: + +| Audit | Description | Parameters | +|-------|-------------|------------| +| `not_null` | Fails if any row has a NULL value in the column | Column-level only | +| `unique` | Fails if any non-NULL value appears more than once | Column-level only | +| `accepted_values` | Fails if any non-NULL value is not in the allowed list | `values` - list of allowed values | +| `relationships` | Fails if any non-NULL value does not exist in the referenced column | `to` - target relation, `field` - target column | + +### Using built-in audits + +Attach them in the `MODEL()` header like any generic audit: + +```sql +MODEL ( + materialized view, + tags [staging], + columns ( + order_id (audits [not_null, unique]), + customer_id (audits [not_null]), + status ( + audits [ + accepted_values (values ["placed", "preparing", "ready", "completed", "cancelled"]), + ], + ), + payment_method ( + audits [ + relationships (to "stg_payments", field "method"), + ], + ), + ), +); +``` + +### Overriding built-in audits + +If you define a generic audit with the same name as a built-in (e.g. `audits/generic/not_null.sql`), your definition takes precedence. SQLBuild emits a warning so you're aware of the override: + +``` +warning[P003]: project audit 'not_null' overrides built-in audit 'not_null' +``` + +## Custom generic audits + +Beyond the built-ins, you can define reusable SQL templates under `audits/generic/`. They use `@parameter` placeholders that are resolved by the audit engine at compile time. + +```sql +-- audits/generic/expression_is_true.sql +AUDIT (); + +SELECT * +FROM @relation +WHERE NOT (@expression) +``` + +### Audit parameters + +Generic audit SQL uses `@name` for parameter placeholders. These are resolved by the audit engine, not the general SQL interpolation system: + +| Parameter | Description | +|-----------|-------------| +| `@column` | The column name (auto-populated for column-level audits) | +| `@relation` | The target relation (auto-populated from the attached model or source) | +| `@'name'` | A quoted parameter passed from the audit declaration (e.g. `@'values'`) | +| `@name` | An unquoted parameter passed from the audit declaration (e.g. `@expression`) | + +Generic and singular audit SQL uses macros, constants, and enums available from the audit file under +`audits/`, not from a model or source that uses the audit. See +[How Visibility Works](/docs/concepts/declaration-scopes/visibility). + +### Attaching custom generic audits + +```sql +MODEL ( + materialized table, + audits [ + expression_is_true ( + name "revenue_is_non_negative", + expression "total_revenue_cents >= 0", + ), + ], +); +``` + +## Singular audits + +Singular audits are standalone SQL files. Their canonical home is `audits/singular/`, and they +reference models directly. They're useful for one-off checks that don't fit a reusable template. +For backward compatibility, singular audits directly under `audits/` or another non-`generic` +child directory continue to compile. + +```sql +-- audits/singular/orders_have_payments.sql +AUDIT ( + name "completed_orders_have_payments", + severity error +); + +SELECT o.order_id +FROM __ref("fact_orders") o +LEFT JOIN __ref("stg_payments") p ON o.order_id = p.order_id +WHERE p.payment_id IS NULL + AND o.order_status = 'completed' +``` + +SQLBuild automatically infers which model a singular audit attaches to based on the `__ref()` calls in the query. If the audit references a single model, it attaches to that model. If it references multiple models, SQLBuild attaches it to the latest (most downstream) model in the DAG. If attachment can't be inferred, the audit runs at the end of the build. + +## Source audits + +Sources support the same audit system as models. Audits attached to sources run *before* any dependent model is built: + +```yaml +sources: + - name: raw__orders + columns: + - name: id + audits: + - not_null + - unique + audits: + - expression_is_true: + name: no_future_orders + expression: "ordered_at <= CURRENT_TIMESTAMP" +``` + +If a source audit with `error` severity fails, all downstream models that depend on that source are blocked. This lets you catch data quality issues at the source before any transformations run. + +## Severity + +| Severity | Behavior | +|----------|----------| +| `error` | Blocks the build. Staging table is not promoted, DML is not applied. | +| `warn` | Reports a warning but allows the build to continue. | + +Set the default severity in `sqlbuild_project.toml`: + +```toml +[settings] +default_audit_severity = "warn" +``` + +Override per audit instance in the `MODEL()` header: + +```sql +columns ( + order_id (audits [not_null (severity error)]), +), +``` + +## Run scope + +Audits on incremental models can run at different lifecycle phases: + +| Scope | Behavior | +|-------|----------| +| `final` | Run once against the staged table before promotion (default). | +| `delta_and_final` | Run against each delta batch before DML, then again against the target after all batches complete. | + +```sql +MODEL ( + materialized incremental, + ... + columns ( + activity_hour (audits [not_null (run_scope delta_and_final)]), + ), + audits [ + expression_is_true ( + name "orders_placed_is_non_negative", + expression "orders_placed >= 0", + run_scope delta_and_final, + ), + ], +); +``` + +Delta-phase audits with `error` severity block DML before the target is updated. This is visible in the build output as `audit (d)` for delta-phase and `audit (f)` for final-phase: + +``` + 10/13 table hourly_order_activity (delete_insert) OK 0.16s + audit (d) expression_is_true PASS 4/4 + audit (d) not_null (activity_hour) PASS 4/4 + audit (f) expression_is_true PASS + audit (f) not_null (activity_hour) PASS +``` + +The `4/4` indicates the audit passed for all 4 microbatch batches. + +If a model is not incremental, `delta_and_final` degrades to `final` automatically. + +## Running audits standalone + +```bash +sqb audit +``` + +This runs all audits without rebuilding any models. + +Standalone audits run serially unless concurrency is configured explicitly, through +`SQLBUILD_CONCURRENCY`, or in project settings. For example, `sqb audit --concurrency 8` runs up +to eight selected audits at once, using one warehouse connection per active worker. Increase this +limit deliberately because parallel queries can increase warehouse load and cost. See +[`sqb audit`](/docs/cli/audit) for precedence, ordering, and cancellation details. diff --git a/website/src/content/docs/docs/concepts/column-lineage.mdx b/website/src/content/docs/docs/concepts/column-lineage.mdx new file mode 100644 index 0000000000..f43b220f69 --- /dev/null +++ b/website/src/content/docs/docs/concepts/column-lineage.mdx @@ -0,0 +1,177 @@ +--- +title: "Column Lineage" +description: "Trace individual columns through your SQL pipeline - understand where data comes from and where it goes." +--- + +## Why column lineage matters + +**Impact analysis** - Before changing a source column, see exactly which downstream models and columns are affected. A rename or type change in `raw__orders.id` can be traced through every model that consumes it, even indirectly. + +**Debugging data issues** - When a column has unexpected values, trace it upstream to find where the data originates and what transformations it passes through. Instead of reading SQL files and mentally joining dependencies, ask SQLBuild to show the path. + +**Documentation** - Column lineage provides machine-readable metadata about your pipeline. The JSON output can feed data catalogs, governance tools, or custom dashboards. + +## How it works + +SQLBuild analyzes column lineage statically at compile time. No warehouse connection is needed. The analyzer parses each model's SQL, resolves `ref()` and `source()` calls, and traces columns through `SELECT` lists, CTEs, JOINs, subqueries, and expressions. + +Column lineage requires SQL analysis to be enabled in project settings (it is by default). + +## Transform types + +Each lineage edge is classified by how the column is transformed: + +| Transform | Description | Example | +|-----------|-------------|---------| +| `direct` | Column passes through unchanged | `SELECT order_id FROM ...` | +| `cast` | Column is explicitly cast to a different type | `SELECT CAST(id AS BIGINT)` | +| `expression` | Column is used in a computed expression | `SELECT amount * 100 AS amount_cents` | +| `aggregation` | Column is used inside an aggregate function | `SELECT SUM(amount) AS total` | +| `star` | Column is included via `SELECT *` | `SELECT * FROM ...` | +| `constant` | Output column is a literal value with no upstream dependency | `SELECT 'active' AS status` | + +Transform classification helps you understand the nature of each dependency. A `direct` edge means the column is a simple passthrough - safe to rename if you rename the source. An `expression` or `aggregation` edge means the column is computed - the upstream value is an input to a calculation, not a 1:1 mapping. + +## Confidence levels + +Each edge also carries a confidence level indicating how certain the analyzer is about the traced dependency: + +| Confidence | Meaning | +|------------|---------| +| `high` | The lineage path is fully resolved through known SQL constructs | +| `medium` | The path is likely correct but involves constructs the analyzer handles with heuristics | +| `low` | The path is best-effort - complex SQL patterns or unsupported constructs may reduce accuracy | + +## Analysis modes + +Column lineage supports two analysis modes that trade off speed against depth of analysis. + +**Rich mode** uses the SQL analysis optimizer to resolve columns through CTEs, subqueries, and multi-level nesting with full transform classification. Thorough, but slower because the optimizer runs per column per model. + +**Fast mode** parses the SQL AST directly to extract column mappings, resolve CTE references, and classify transforms. It handles the same SQL patterns that most column lineage tools support and is fast enough to run on every compile. + +`sqb compile` defaults to fast mode because it runs frequently and analyzes the entire project. `sqb lineage` defaults to rich mode because it targets a specific column in a scoped slice of the DAG, where the deeper analysis is worth the cost. Both are overridable: + +```bash +sqb compile --lineage-mode rich +sqb lineage fact_orders.payment_amount_cents --mode fast +``` + +## Using column lineage + +### Interactive tracing with `sqb lineage` + +The target syntax is `model_name.column_name`. Trace a column upstream to see where its values come from: + +```bash +sqb lineage daily_revenue.total_revenue_cents +``` + +``` +Column trace daily_revenue.total_revenue_cents upstream + +└── stg_payments.amount_cents (aggregation) + └── raw__payments.amount_cents (direct) +``` + +Each hop is annotated with how the value was derived. Here `total_revenue_cents` is an aggregation of `stg_payments.amount_cents`, which is a direct passthrough from the source. + +Use `--direction downstream` to trace the other way - every column derived from this one: + +```bash +sqb lineage stg_payments.amount_cents --direction downstream +``` + +``` +Column trace stg_payments.amount_cents downstream + +├── daily_revenue.avg_order_value_cents (aggregation) +├── daily_revenue.total_revenue_cents (aggregation) +├── daily_revenue.total_revenue_dollars (aggregation) +├── dim_customers.lifetime_spend_cents (aggregation) +└── fact_orders.payment_amount_cents (direct) +``` + +Column lineage supports `upstream` (default) and `downstream` directions (not `both` - model lineage supports `both`). + +### Model lineage + +`sqb lineage` also traces model-level dependencies when the target has no column (no dot): + +```bash +sqb lineage fact_orders +``` + +``` +Lineage model fact_orders models/marts/fact_orders.sql upstream + +├── model stg_orders models/staging/stg_orders.sql +│ └── source raw__orders sources/raw.yml +├── model stg_payments models/staging/stg_payments.sql +│ └── source raw__payments sources/raw.yml +├── seed waffle_types seeds/waffle_types.csv +└── udf udf__is_completed_order +``` + +Each node is tagged with its resource type (`model`, `source`, `seed`, `udf`) and file path. Use `--direction both` to show upstream and downstream together: + +```bash +sqb lineage daily_revenue --direction both +``` + +``` +Lineage model daily_revenue models/marts/daily_revenue.sql both + +upstream +├── model stg_orders models/staging/stg_orders.sql +│ └── source raw__orders sources/raw.yml +└── model stg_payments models/staging/stg_payments.sql + └── source raw__payments sources/raw.yml +downstream +``` + +### Options + +| Flag | Description | +| --------------- | --------------------------------------------------------------------------------- | +| `--direction` | `upstream` (default), `downstream`, or `both`. `both` is model lineage only. | +| `--depth` | How many hops to traverse: an integer or `all` (default `all`). | +| `--format` | `tree` (default), `list` (an edge list of `a -> b` pairs), or `json`. | +| `--mode` | Column lineage mode: `rich` (default) or `fast`. | + +See the [lineage CLI reference](/docs/cli/lineage) for full flag documentation and output format examples. + +### Batch analysis with `sqb compile` + +Every compile run computes column lineage for analysis and reports a summary: + +```bash +# Default: fast column lineage +sqb compile + +# Skip column lineage +sqb compile --lineage-mode none + +# JSON report includes per-model lineage summary +sqb compile --json +``` + +In the JSON compile report, each model includes a `lineage` field with `column_count`, `edge_count`, and `has_star` metadata. It does not contain the full edge graph; use `sqb lineage [.] --format json` when you need structured lineage details. + +See the [compile CLI reference](/docs/cli/compile) for details on the compile report format. + +### Integration with contract validation + +Column lineage feeds into compile-time contract validation. When a model declares columns in its `MODEL()` header, the compiler uses inferred column information to check that: + +- Every declared column exists in the query output +- Column types match the declared types (when `type_enforcement` is enabled) + +These checks run automatically during `sqb compile` and report diagnostics with source-annotated error messages. + +## Limitations + +- Column lineage requires SQL analysis to be enabled (`sql_analysis = true` in settings, which is the default) +- Complex SQL patterns (deeply nested correlated subqueries, dynamic SQL, adapter-specific functions) may reduce accuracy or confidence +- `SELECT *` is tracked as a `star` transform - the analyzer knows the column passes through but the mapping is less precise than explicit column references +- Column lineage is computed statically from SQL text. Runtime-only column additions (e.g. from dynamic UDFs) are not tracked diff --git a/website/src/content/docs/docs/concepts/constants.mdx b/website/src/content/docs/docs/concepts/constants.mdx new file mode 100644 index 0000000000..3d606bfd29 --- /dev/null +++ b/website/src/content/docs/docs/concepts/constants.mdx @@ -0,0 +1,90 @@ +--- +title: "Constants" +description: "Define reusable compiler-validated values and reference them safely from SQL." +--- + +import Card from '../../../../components/mintlify/Card.astro'; +import CardGroup from '../../../../components/mintlify/CardGroup.astro'; + +Constants give a name to a value used in SQL. SQLBuild validates the value during compilation and +asks the active adapter to render it safely for its SQL dialect. Constant values are data, never raw +SQL snippets. + +## Create a constant + +Put project-wide constants under the top-level `constants/` directory. Files are discovered +recursively, and one file may contain more than one declaration. + +```text +my_project/ +├── constants/ +│ ├── commerce/ +│ │ └── thresholds.sql +│ └── reporting_day.sql +├── models/ +└── sqlbuild_project.toml +``` + +```sql +-- constants/commerce/thresholds.sql +CONSTANT (name min_items, value 7); +CONSTANT (name fallback_source, value "web"); +CONSTANT (name enabled, value true); +CONSTANT (name ratio, value 0.75); +CONSTANT (name missing_value, value null); +``` + +Folders below `constants/` are organizational. They do not change where project-wide constants are +available. + +## Use a constant + +Reference a constant with `@const("name")`: + +```sql +SELECT * +FROM order_batches +WHERE item_count >= @const("min_items") + AND source = @const("fallback_source") +``` + +References work in model queries, SQL hooks, SQL functions, audits, unit tests, scenarios, and +inline source expressions. An unknown constant fails compilation. + +## Scalar values + +Constants support strings, signed integers, booleans, finite floating-point numbers, exact +decimals, and `null`. Integers use a portable signed 64-bit range. `NaN` and positive or negative +infinity are rejected. + +Use `type decimal` with a quoted value when decimal precision must be exact: + +```sql +CONSTANT ( + name usd_rate, + type decimal, + value "2.4700", +); +``` + +SQLBuild parses the quoted value directly as a decimal rather than first converting it to a binary +float. An incompatible `type` and `value` fails compilation. + +## Naming rules + +Public constant names must be unique among public constants and cannot begin with `_`. Enum, +constant, and macro names use separate namespaces, so an enum and a constant may share a name. + +## More constant features + + + + Define lists, sets, and objects, then choose value-list or native-array rendering. + + + Keep a constant inside one model when no other resource should use it. + + + +To limit a constant to one folder, or to that folder and its child folders, see +[Declarations and Scopes](/docs/concepts/declaration-scopes). diff --git a/website/src/content/docs/docs/concepts/constants/collections-and-rendering.mdx b/website/src/content/docs/docs/concepts/constants/collections-and-rendering.mdx new file mode 100644 index 0000000000..aa7be69457 --- /dev/null +++ b/website/src/content/docs/docs/concepts/constants/collections-and-rendering.mdx @@ -0,0 +1,153 @@ +--- +title: "Collections and Rendering" +description: "Define list, set, and object constants and control their adapter-specific SQL rendering." +--- + +import Warning from '../../../../../components/mintlify/Warning.astro'; + +Constants can hold collections as well as scalar values. Use this page when a constant represents +a reusable value list, native array, set, or structured object. + +## Lists + +Square brackets declare an ordered list. Lists preserve authored order and allow duplicates: + +```sql +CONSTANT ( + name supported_countries, + value ["GB", "FR", "HK"], +); +``` + +## Sets + +Curly braces declare a set. Sets reject duplicate typed values and use a stable order when SQLBuild +renders or fingerprints them: + +```sql +CONSTANT ( + name unique_countries, + value {"GB", "FR", "HK"}, +); +``` + +`{true, 1}` is valid because a boolean and an integer are different logical types. +`{"GB", "FR", "GB"}` fails compilation instead of silently discarding the duplicate. + +## Objects + +Parenthesized key-value entries declare a string-keyed object. Values may be scalars, lists, sets, +or other objects: + +```sql +CONSTANT ( + name country_rules, + value ( + GB ( + label "Great Britain", + threshold 2.47, + enabled true, + regions ["ENG", "SCT", "WLS"], + ), + FR ( + label "France", + threshold 2.5, + enabled true, + regions ["IDF", "NAQ"], + ), + ), +); +``` + +Object keys must be unique. Objects are logical JSON values rather than portable homogeneous SQL +maps or structs. + +## Collection rules + +Lists and sets must be non-empty and have one compatible element type. Nullable elements do not +determine the type, so `[1, null, 2]` is valid, while these declarations fail: + +```sql +CONSTANT (name empty_values, value []); -- no element type +CONSTANT (name unknown_values, value [null]); -- no non-null element type +CONSTANT (name mixed_values, value [1, "two"]); -- incompatible types +``` + +Objects may contain different value types because each key is checked independently. SQLBuild also +applies nesting-depth, element-count, and rendered-size safety limits. + +## Value-list rendering + +Lists and sets render as a parenthesized value list by default. This is designed for `IN`: + +```sql +WHERE country_code IN @const("supported_countries") +``` + +```sql +WHERE country_code IN ('GB', 'FR', 'HK') +``` + +Every element is escaped by the active adapter. + + + A value-list constant is intended for a value-list position such as `IN (...)`. It is not a + portable standalone projection. Use native-array rendering when the constant must be an array + expression. + + +## Native-array rendering + +Set `render_as array` to request an adapter-native array: + +```sql +CONSTANT ( + name supported_countries_array, + value ["GB", "FR", "HK"], + render_as array, +); +``` + +SQLBuild does not rewrite array membership operations. Use the operators and functions provided by +your adapter. + +Sets support the same `value_list` and `array` modes as lists. Scalar and object constants reject +`render_as` because those rendering modes do not apply to them. + +| Adapter | Native array expression | Object/JSON expression | +|---------|-------------------------|------------------------| +| DuckDB | `['GB', 'FR', 'HK']` | `json('{"GB":"Great Britain"}')` | +| MotherDuck | `['GB', 'FR', 'HK']` | `json('{"GB":"Great Britain"}')` | +| Snowflake | `ARRAY_CONSTRUCT('GB', 'FR', 'HK')` | `PARSE_JSON('{"GB":"Great Britain"}')` | +| BigQuery | `['GB', 'FR', 'HK']` | `JSON '{"GB":"Great Britain"}'` | +| Databricks | `array('GB', 'FR', 'HK')` | `parse_json('{"GB":"Great Britain"}')` | +| PostgreSQL | `ARRAY['GB', 'FR', 'HK']` | `'{"GB":"Great Britain"}'::JSONB` | +| SQL Server | Unsupported | `JSON_QUERY(N'{"GB":"Great Britain"}')` | + +BigQuery does not support arrays containing arrays. SQL Server has no native array representation. +Unsupported requests fail compilation rather than silently changing representation. + +## Project default + +Set the default for list and set constants in `sqlbuild_project.toml`: + +```toml +[constants] +collection_rendering = "array" +``` + +SQLBuild chooses the rendering mode in this order: + +1. The declaration's `render_as` field +2. Project `[constants].collection_rendering` +3. The `value_list` default + +One constant has one representation throughout a compilation. Changing its value or rendering mode +changes the identity of SQL that uses it. + +## Stable values + +- List order and duplicates are preserved. +- Set order is ignored; membership is stored in a stable order. +- Object key order is ignored; keys are stored in a stable order. +- Changing set membership or object values changes dependent query identity. diff --git a/website/src/content/docs/docs/concepts/dbt-compatibility/adding-sqlbuild-models.mdx b/website/src/content/docs/docs/concepts/dbt-compatibility/adding-sqlbuild-models.mdx new file mode 100644 index 0000000000..32f9ff8f05 --- /dev/null +++ b/website/src/content/docs/docs/concepts/dbt-compatibility/adding-sqlbuild-models.mdx @@ -0,0 +1,56 @@ +--- +title: "Adding SQLBuild models" +description: "Grow into SQLBuild's own models, tests, audits, and scenarios downstream of your dbt project." +--- + +The dbt compatibility bridge does not require SQLBuild models. You can optionally add SQLBuild models, tests, audits, and scenarios downstream of dbt outputs. + +This is purely additive. Your dbt models stay in dbt, and the layout gains a `models/` directory in the SQLBuild project. + +``` +my-workspace/ + analytics/ # your existing dbt project, untouched + dbt_project.yml + models/ + target/ + manifest.json + sqlbuild_project/ # created by SQLBuild + sqlbuild_project.toml + models/ + marts/downstream_orders.sql # references dbt models via __dbt_ref + tests/ + unit/test_downstream_orders.sql +``` + +## Referencing dbt models + +SQLBuild models reference dbt model outputs with `__dbt_ref("package", "model")`: + +```sql +MODEL ( + tags [finance], + columns (order_id (audits [not_null])), +); + +SELECT order_id FROM __dbt_ref("analytics", "fact_orders") +``` + +This resolves to the qualified warehouse table name from the dbt manifest (e.g. `analytics.fact_orders`). The dbt model becomes an upstream dependency in the combined graph, so SQLBuild models downstream of it build against its output. + +SQLBuild models can also reference other SQLBuild models with `__ref()` as usual: + +```sql +MODEL (tags [marts]); + +SELECT order_id FROM __ref("downstream_orders") +``` + +## Tests, audits, and scenarios + +SQLBuild models added downstream of dbt get SQLBuild's full validation surface: + +- **Unit tests** can mock dbt model dependencies with `__dbt_ref__` fixture CTEs, so you can test a SQLBuild model without a warehouse connection or a compiled dbt manifest. See [Testing](/docs/concepts/testing). +- **Audits** declared inline in the `MODEL()` header block promotion when they fail, the same as in a standalone SQLBuild project. See [Audits](/docs/concepts/audits). +- **Scenarios** run the combined graph against fixture inputs for end-to-end checks. See [Scenarios](/docs/concepts/scenarios). + +For the full model authoring reference (materializations, incremental strategies, contracts, and more), see [Models](/docs/concepts/models). diff --git a/website/src/content/docs/docs/concepts/dbt-compatibility/overview.mdx b/website/src/content/docs/docs/concepts/dbt-compatibility/overview.mdx new file mode 100644 index 0000000000..ae81b97805 --- /dev/null +++ b/website/src/content/docs/docs/concepts/dbt-compatibility/overview.mdx @@ -0,0 +1,142 @@ +--- +title: "Using SQLBuild with dbt" +description: "Run dbt and SQLBuild side by side with coordinated selection and SQLBuild models downstream." +--- + +import Note from '../../../../../components/mintlify/Note.astro'; + +Use the dbt compatibility bridge to coordinate dbt selections with SQLBuild-owned models downstream of their outputs. + + +SQLBuild reads the dbt manifest and drives the `dbt` CLI as a subprocess. dbt remains responsible for compiling and executing dbt-owned models; SQLBuild statically analyzes and executes only SQLBuild-owned models downstream. Change-aware execution is not supported by the bridge. + + +## Start with your existing dbt project + +From inside your dbt project, run a `sqb dbt` command. Scope dbt work with familiar `--select` values, or omit selection to plan the whole project. + +```bash +sqb dbt plan --select path:models/marts +``` + +The first time you do this, SQLBuild bootstraps itself. If there is no `sqlbuild_project.toml`, it reads your `dbt_project.yml` and profile and creates a minimal twin project in a `sqlbuild_project/` directory next to your dbt project. It reuses your dbt profile for the warehouse connection, so there are no separate credentials to configure. + +``` +my-workspace/ + analytics/ # your existing dbt project, untouched + dbt_project.yml + models/ + target/ + manifest.json + sqlbuild_project/ # created by SQLBuild + sqlbuild_project.toml # points at the dbt project, no models of its own +``` + +The generated `sqlbuild_project.toml` looks like this: + +```toml +name = "analytics" +adapter = "snowflake" +default_target = "dev" + +[dbt] +project_dir = "../analytics" +profiles_dir = "/Users/you/.dbt" +target_path = "../analytics/target" +target = "dev" + +[connections.dbt_dev] +source = "dbt_profile" +profile = "analytics" +target = "dev" + +[targets.dev] +connection = "dbt_dev" +database = "ANALYTICS" +schema = "analytics" +``` + +`source = "dbt_profile"` on the named connection tells SQLBuild to connect using your dbt +profile, so it talks to the same warehouse dbt does. The target references that connection and +remains authoritative for its database and schema. + +`sqb dbt build --select path:models/marts` compiles the project, resolves the selection, runs the selected dbt models, then runs any SQLBuild models you have added against the dbt outputs. + +## How it works + +1. SQLBuild runs `dbt compile` to produce a `manifest.json` with model metadata +2. SQLBuild reads the manifest to understand dbt model names and their qualified warehouse tables +3. SQLBuild resolves your `--select`/`--exclude` against dbt by running `dbt ls`, so dbt-native selectors like `state:modified` and `package:` are evaluated by dbt itself, not reimplemented +4. `sqb dbt plan/run/build` orchestrates the run: dbt executes the selected dbt work +5. (Optional) any SQLBuild models you have added run last, against the dbt outputs + +Each step calls the `dbt` CLI directly: `dbt compile` for the manifest, `dbt ls` for selection, and `dbt build`/`dbt run` for execution. + +### Flags + +`sqb dbt plan/run/build` declare the common flags directly. Anything declared goes **before** a `--` separator; any other raw dbt flag goes **after** it and is forwarded verbatim. A flag placed on the wrong side errors rather than silently reaching dbt. + +```bash +sqb dbt build --select path:models/marts --full-refresh +``` + +Declared flags are routed to the right place automatically: + +| Flag | Routed to | Notes | +| ---- | --------- | ----- | +| `--select` / `--exclude` | dbt | dbt resolves selection (see [Selection](/docs/concepts/dbt-compatibility/selection)). | +| `--vars` | dbt **and** SQLBuild | The same vars feed dbt's compile and SQLBuild's own variable resolution, so both sides see identical values. | +| `--full-refresh` | dbt **and** SQLBuild | Requests a full rebuild on both sides. dbt and native SQLBuild models independently apply their model-level `full_refresh` setting, including `false` opt-outs. | +| `--threads` | dbt (`--threads`) and SQLBuild (`--concurrency`) | | +| `--target` / `--project-dir` / `--profiles-dir` / `--profile` / `--target-path` | dbt | Standard dbt locators. | + +#### `--vars` + +`--vars` accepts the same JSON object dbt accepts, and SQLBuild passes it to **both** the underlying `dbt` invocation and its own variable resolution. That means a value referenced as `@@my_var` in SQLBuild model SQL (see [Interpolation](/docs/concepts/interpolation)) and as `{{ var('my_var') }}` in a dbt model both resolve to the value you passed. CLI vars take precedence over project and local config vars. You do not need to declare vars twice. + +```bash +sqb dbt build --select path:models/marts --vars '{"my_var": 1}' +``` + +#### Forwarding other dbt flags + +For any native dbt flag SQLBuild does not declare, put it after `--` and it is passed straight to the `dbt` invocation untouched: + +```bash +sqb dbt build -- --log-level debug +``` + +## Configuration + +The auto-generated project above is editable, and you can write `sqlbuild_project.toml` by hand. The `[dbt]` block (shown in the generated project above) accepts: + +| Field | Description | +|-------|-------------| +| `project_dir` | Path to the dbt project root (where `dbt_project.yml` lives) | +| `profiles_dir` | Path to the directory containing `profiles.yml` | +| `target_path` | Path to dbt's `target/` directory (where `manifest.json` is written) | +| `target` | dbt target name override (optional) | + +Paths can be absolute or relative to the SQLBuild project root. + +## Prerequisites + +- dbt must be installed and available on `PATH` as `dbt` +- Both projects must target the same warehouse and schema/database context + +SQLBuild uses your own `dbt` install; it does not bundle or install dbt. If your `dbt` is not reachable as a bare `dbt` on `PATH` (for example, you run it via `uv`, `poetry`, or a wrapper), set the `DBT_EXECUTABLE` environment variable to the executable SQLBuild should call. + +SQLBuild runs `dbt compile` automatically as part of `sqb dbt plan/run/build` to produce the manifest. You do not need to compile the dbt project manually. + +## Debugging + +`sqb dbt debug` runs both projects' diagnostics: `dbt debug` (verifying the dbt project config and warehouse connection) followed by `sqb debug` (verifying the SQLBuild project config and connection). + +```bash +sqb dbt debug +``` + +## On this topic + +- [Selection](/docs/concepts/dbt-compatibility/selection) - how `--select` and `--exclude` route work across both graphs. +- [Adding SQLBuild models](/docs/concepts/dbt-compatibility/adding-sqlbuild-models) - optionally write SQLBuild models, tests, audits, and scenarios downstream of dbt. diff --git a/website/src/content/docs/docs/concepts/dbt-compatibility/selection.mdx b/website/src/content/docs/docs/concepts/dbt-compatibility/selection.mdx new file mode 100644 index 0000000000..0dafaf2e2c --- /dev/null +++ b/website/src/content/docs/docs/concepts/dbt-compatibility/selection.mdx @@ -0,0 +1,72 @@ +--- +title: "Selection" +description: "How select and exclude flags route work across the dbt and SQLBuild graphs." +--- + +The `sqb dbt` commands use `--select` and `--exclude` to scope what runs. Selectors work across both dbt and SQLBuild, with the system determining which side owns each selector and how to route work. + +## SQLBuild-recognized selectors + +These selectors match SQLBuild models directly: + +| Selector | Example | Behavior | +|----------|---------|----------| +| Model name | `fact_orders` | Selects that SQLBuild model. Auto-includes its immediate dbt upstream dependencies. | +| Leading `+` | `+fact_orders` | Selects the model plus walks upstream through both SQLBuild and dbt models. | +| Trailing `+` | `fact_orders+` | Selects the model plus all downstream SQLBuild models. | +| Both `+` | `+fact_orders+` | Full upstream (including dbt) and downstream expansion. | +| Tag | `tag:nightly` | Selects all SQLBuild models with that tag. Auto-includes dbt dependencies. | +| Tag with `+` | `+tag:nightly` | Tag match plus upstream expansion through the combined graph. | +| Path | `path:models/marts` | Selects SQLBuild models under that project-relative directory - the same syntax as dbt's `path:` selector. | +| Path with `+` | `+path:models/marts` | Path match plus upstream/downstream expansion. | + +When a SQLBuild model is selected, its immediate dbt upstream dependencies are always included so dbt can build the tables that SQLBuild models read from. + +## dbt-only selectors + +Selectors that SQLBuild does not recognize (like `state:modified`, `package:stripe`, `source:stripe.charges`) are passed to `dbt ls` to resolve: + +| Selector | Example | Behavior | +|----------|---------|----------| +| Without `+` | `state:modified` | dbt-only work. No SQLBuild models selected. | +| With trailing `+` | `state:modified+` | SQLBuild runs `dbt ls` to find which dbt models match, then walks downstream into SQLBuild territory. | +| With both `+` | `+state:modified+` | Same downstream expansion, plus upstream dbt expansion. | + +This means you can use dbt-native selectors like `state:modified+` to trigger rebuilds of SQLBuild models that depend on changed dbt models. If `dbt ls` returns no matching models, no SQLBuild work is triggered. + +## Exclude + +`--exclude` removes matching SQLBuild models from the final selection: + +```bash +sqb dbt build --select fact_orders+ --exclude tag:nightly +``` + +## Examples + +```bash +# Build a specific SQLBuild model and its dbt dependencies +sqb dbt build --select downstream_orders + +# Build everything downstream of a SQLBuild model +sqb dbt build --select downstream_orders+ + +# Build a SQLBuild model with full upstream dbt chain +sqb dbt build --select +downstream_orders + +# Build SQLBuild models downstream of modified dbt models +sqb dbt build --select state:modified+ + +# Build all SQLBuild models tagged "nightly" with their dbt dependencies +sqb dbt build --select tag:nightly + +# Build SQLBuild models under a path +sqb dbt build --select path:models/marts +``` + +## Execution order + +For `sqb dbt run` and `sqb dbt build`: + +1. **dbt runs** - a single `dbt run/build` command executes with the user's selectors merged with any additional dbt models required by selected SQLBuild models +2. **SQLBuild runs** - selected SQLBuild models execute against the now-built dbt tables diff --git a/website/src/content/docs/docs/concepts/declaration-scopes.mdx b/website/src/content/docs/docs/concepts/declaration-scopes.mdx new file mode 100644 index 0000000000..2f6455facc --- /dev/null +++ b/website/src/content/docs/docs/concepts/declaration-scopes.mdx @@ -0,0 +1,116 @@ +--- +title: "Overview" +description: "Limit enums, constants, and macros to the parts of a project that use them." +--- + +import Card from '../../../../components/mintlify/Card.astro'; +import CardGroup from '../../../../components/mintlify/CardGroup.astro'; + +Most projects can keep enums, constants, and macros in their ordinary top-level directories. Those +declarations are available throughout the project. + +When an enum, constant, or macro should be available only within one folder or its child folders, +you can keep it near the SQL that uses it. SQLBuild uses the declaration directory's location to +decide which files can access it. No TOML configuration is required. + +| Where the declaration lives | Who can use it | +|-------------------------------------|--------------------| +| Top-level `macros/`, `constants/`, or `enums/` | The whole project | +| Nested `_sqlbuild/macros/`, `_sqlbuild/constants/`, or `_sqlbuild/enums/` | Files in the owner folder and folders below it | +| Nested `_sqlbuild/_macros/`, `_sqlbuild/_constants/`, or `_sqlbuild/_enums/` | Files directly in the owner folder only | +| An underscored constant or enum inside `MODEL()` | That model and its inline SQL hooks only | + +Macro declarations are Python files (`.py`). Constant and enum declarations are SQL files +(`.sql`). + +## Example + +The annotation beside each directory shows where its contents are available: + +```text +models/ +├── constants/ published throughout models/ +│ └── warehouse.sql +└── commerce/ + ├── _sqlbuild/ + │ ├── macros/ published throughout commerce/ + │ │ └── currency.py + │ └── _enums/ exact commerce/ directory only + │ └── grain.sql + ├── orders.sql sees warehouse, currency, and grain + ├── finance/ + │ ├── _sqlbuild/ + │ │ └── macros/ published throughout finance/ + │ │ └── tax.py + │ └── revenue.sql sees warehouse, currency, and tax + └── fulfillment/ + └── shipments.sql sees warehouse and currency +``` + +The folder containing `_sqlbuild/` is the declaration owner. An unprefixed role applies to that +owner and everything below it. An underscored role applies only to files directly in the owner. +Legacy declaration roles directly below an owner remain supported. + +`_sqlbuild/` is reserved for the six declaration-role directories shown above. Other direct files or +folders are rejected. It must also sit below a concrete owner directory. A project-root +`_sqlbuild/` or authored-root path such as `models/_sqlbuild/` is invalid; declarations at that +boundary belong in the project-wide `macros/`, `enums/`, or `constants/` roots. + +| Resource | Visible from the example tree | Not visible | +|----------|-------------------------------|-------------| +| `orders.sql` | Warehouse constant, commerce macro, commerce-folder-only enum | Finance macro | +| `finance/revenue.sql` | Warehouse constant, commerce macro, finance macro | Commerce-folder-only enum | +| `fulfillment/shipments.sql` | Warehouse constant, commerce macro | Commerce-folder-only enum, finance macro | + +Declarations do not flow upward or sideways into sibling directories. + +## Terms used in text output + +The CLI's text output uses short labels for the same four rules: + +| Canonical label | Plain-language meaning | +|-----------------|------------------------| +| `project` | Available throughout the project | +| `descendant-public` | Available in this folder and folders below it | +| `exact-owner-private` | Available directly in this folder only | +| `model-private` | Available to one model only | + +Here, an **owner folder** is simply the folder directly containing a model, test, hook, function, or +other authored resource. The [Scope Explorer](/docs/concepts/declaration-scopes/explorer) shows the owner +folder, its parents, and project-wide declarations separately. + +JSON keeps the underlying enum values for integrations. Its `scope` field uses `global`, +`inherited`, `local`, or `private`, while derived `visibility` values use underscores, such as +`descendant_public` and `exact_owner_private`. Consumers should use the fields documented in the +versioned JSON schema rather than parsing text labels. + +## Choose a location + +| Where the value is needed | Placement | +|---------------------------|-----------| +| One model only | In that model's `MODEL()` header | +| Files directly in one folder | In an underscored role under the owner's `_sqlbuild/` folder | +| Files across one folder tree | In an unprefixed role under the nearest shared owner's `_sqlbuild/` folder | +| Different resource trees, such as models and tests | In a top-level declaration role | + +SQLBuild computes the lowest common owner of every declaration's runtime consumers. A project-wide +declaration is valid only when no narrower supported owner contains all consumers. This keeps the +top-level roles as a genuine project API instead of a neutral dumping ground. + +## Explore the feature + + + + See which declarations a model, test, hook, or function can use. + + + Choose the narrowest folder that contains every real use. + + + Ask SQLBuild what a file can use and preview how moving it would change that answer. + + + +Learn the features themselves in [Enums](/docs/concepts/enums), [Constants](/docs/concepts/constants), and +[Writing Macros](/docs/concepts/macros). See [Interpolation](/docs/concepts/interpolation) for project, +environment, and runtime context values, which are separate from declarations. diff --git a/website/src/content/docs/docs/concepts/declaration-scopes/explorer.mdx b/website/src/content/docs/docs/concepts/declaration-scopes/explorer.mdx new file mode 100644 index 0000000000..102f93afc1 --- /dev/null +++ b/website/src/content/docs/docs/concepts/declaration-scopes/explorer.mdx @@ -0,0 +1,340 @@ +--- +title: "Scope Explorer" +description: "Inspect visibility, explain resolution, browse declarations, and preview moves offline." +--- + +import Card from '../../../../../components/mintlify/Card.astro'; +import CardGroup from '../../../../../components/mintlify/CardGroup.astro'; + +Scope Explorer answers questions about declaration visibility through `sqb scope`. It is read-only +and offline: it never connects to the warehouse, moves files, or edits configuration. + +Use this page for common workflows. See the [`sqb scope` CLI reference](/docs/cli/scope) for every flag, +filter, text section, and JSON field. + +## Example project + +The examples below use two domains with project-wide currency helpers: + +```text +project/ +├── macros/ +│ └── currency.py add_tax, round_money +├── models/ +│ ├── commerce/ +│ │ ├── constants/limits.sql minimum_order_value +│ │ ├── enums/status.sql order_status +│ │ ├── macros/orders.py formatted_order_total +│ │ ├── orders.sql +│ │ └── returns/returns.sql +│ └── finance/ +│ ├── enums/status.sql finance_status +│ ├── macros/margin.py calculate_margin +│ ├── finance_summary.sql +│ └── reports/margin_report.sql +└── tests/ + └── unit/orders__completed_only.sql +``` + +`formatted_order_total` composes the project-wide functions through ordinary Python calls: + +```python +from macros.currency import add_tax, round_money + + +def formatted_order_total(expression: str) -> str: + return round_money(add_tax(expression)) +``` + +`orders.sql` uses the commerce constant and macro. The returns model also uses all three commerce +declarations, so they are correctly published throughout that domain. The two finance models use +both finance declarations. This gives every scoped declaration a real consumer and valid placement. + +## See available and used declarations + +Start with a model, test, scenario, hook, function, audit, source, or authored resource path to +inspect its directory-derived scope. A declaration identity instead opens its explanation. This +excerpt of the main report sections puts actual usage beside the complete directory-derived scope: + +```console +$ sqb scope model:orders +Scope + Target: model:orders + Resource: model:orders + Path: models/commerce/orders.sql + +Used (2) + ├─ ● constant:minimum_order_value [constant; descendant-public; inherited_ancestor; type integer; role models/commerce/constants] models/commerce/constants/limits.sql:1:1 + └─ ● macro:formatted_order_total [macro; descendant-public; inherited_ancestor; params 1; role models/commerce/macros] models/commerce/macros/orders.py:4:1 + +Scope chain + ├─ exact-owner-private models/commerce (3) + ├─ descendant-public models (0) + └─ project global (2) + +Available (3 of 5, 2 collapsed) + ├─ ● constant:minimum_order_value [constant; descendant-public; inherited_ancestor; type integer; role models/commerce/constants] models/commerce/constants/limits.sql:1:1 + ├─ ○ enum:order_status [enum; descendant-public; inherited_ancestor; members 4; type VARCHAR; role models/commerce/enums] models/commerce/enums/status.sql:1:1 + └─ ● macro:formatted_order_total [macro; descendant-public; inherited_ancestor; params 1; role models/commerce/macros] models/commerce/macros/orders.py:4:1 + … 2 globals collapsed; run sqb scope model:orders --globals all + +Diagnostics (0) + (none) +Completeness: complete +``` + +`●` marks a declaration included in `Used`: a direct usage by default, or a followed declaration +dependency when `--dependency-depth` is nonzero. `○` marks a declaration present in the section but +not in `Used`. + +The compact labels translate to ordinary folder rules: + +| Output label | Meaning in this report | +|--------------|------------------------| +| `project` | Available throughout the project | +| `descendant-public` | Available from a folder at or above this resource | +| `exact-owner-private` | Available directly in this resource's owner folder only | +| `inherited_ancestor` | Visible because an unprefixed role publishes it down the folder tree | +| `role models/commerce/macros` | The declaration role begins at this path | + +In the scope chain, the first path is the resource's owner folder, followed by parent folders and +the project declaration roles. The counts show declarations defined at each path; the chain label +describes how SQLBuild reaches that path, not the visibility of every declaration counted there. +Unused project-wide declarations stay collapsed by default so a large project API does not hide the +owner-folder facts. + +## Follow composed dependencies + +`Used` is direct by default. Add `--dependency-depth` to follow declarations used by those +declarations. The expanded `Used` section is: + +```console +$ sqb scope model:orders --used-only --dependency-depth 1 +Used (4) + ├─ ● constant:minimum_order_value [constant; descendant-public; inherited_ancestor; type integer; role models/commerce/constants] models/commerce/constants/limits.sql:1:1 + ├─ ● macro:add_tax [macro; project; dependency; params 1; role macros] macros/currency.py:1:1 + ├─ ● macro:formatted_order_total [macro; descendant-public; inherited_ancestor; params 1; role models/commerce/macros] models/commerce/macros/orders.py:4:1 + └─ ● macro:round_money [macro; project; dependency; params 1; role macros] macros/currency.py:5:1 +``` + +SQLBuild derives these edges from actual Python calls, including nested calls and calls reached +through private helpers. Merely importing a macro does not make it used. Increase the depth to +follow longer chains. + +Explain the composed macro to see its direct dependencies, consumers, and required placement. The +ordinary report appears first; its `Explanation` section is: + +```console +$ sqb scope model:orders --explain macro:formatted_order_total +Explanation + └─ ● macro:formatted_order_total [macro; descendant-public; inherited_ancestor; params 1; role models/commerce/macros] models/commerce/macros/orders.py:4:1 + Owner: (none) + Owning path: models/commerce + Consumers: model:orders, model:returns + Dependencies: macro:add_tax, macro:round_money + Grants: (none) + Required scope: descendant-public + Required path: models/commerce + Promotion impact: (none) +``` + +### Catch placement that is too broad + +If `returns.sql` stopped using `formatted_order_total`, only `orders.sql` would consume it. The same +explanation would then show that descendant-public placement is broader than necessary: + +```console +Explanation + └─ ● macro:formatted_order_total [macro; descendant-public; inherited_ancestor; params 1; role models/commerce/macros] models/commerce/macros/orders.py:4:1 + Owner: (none) + Owning path: models/commerce + Consumers: model:orders + Dependencies: macro:add_tax, macro:round_money + Grants: (none) + Required scope: exact-owner-private + Required path: models/commerce + Promotion impact: model:orders + +Diagnostics (1) + ERROR S008 models/commerce/macros/orders.py: Declaration 'macro:formatted_order_total' is currently descendant-public at 'models/commerce' (models/commerce/macros/orders.py); required exact-owner-private at 'models/commerce'. Consumers: model:orders. Move it to 'models/commerce/_sqlbuild/_macros/' +Completeness: complete +``` + +This is actionable placement guidance, not only a visibility lookup. + +## Find declarations outside the scope + +Nearby discovery is useful when you know a declaration exists but not why the current resource +cannot use it. The relevant section is: + +```console +$ sqb scope model:orders --include-nearby +Nearby unavailable (2 of 2) + ├─ ○ enum:finance_status [enum; descendant-public; sibling_scope; members 3; type VARCHAR; role models/finance/enums] models/finance/enums/status.sql:1:1 + └─ ○ macro:calculate_margin [macro; descendant-public; sibling_scope; params 2; role models/finance/macros] models/finance/macros/margin.py:4:1 +``` + +The declarations are known, but they belong to a different folder branch. Here, the inspected +resource belongs to `models/commerce`, while the declarations belong to `models/finance`. +`sibling_scope` is the compact output label for that situation. Ask for one complete explanation +when you know a declaration's identity. After the ordinary report, the command adds: + +```console +$ sqb scope model:orders --explain macro:calculate_margin +Explanation + └─ ○ macro:calculate_margin [macro; descendant-public; sibling_scope; params 2; role models/finance/macros] models/finance/macros/margin.py:4:1 + Owner: (none) + Owning path: models/finance + Consumers: model:finance_summary, model:margin_report + Dependencies: (none) + Grants: (none) + Required scope: descendant-public + Required path: models/finance + Promotion impact: (none) +``` + +An explanation distinguishes a known but inaccessible declaration from an unknown name. It also +shows whether the declaration is placed more broadly than its real consumers require. + +## See declarations available through expected output + +Tests can use file-based macros, enums, and constants available to a model when they define that +model's expected output. Scenarios receive the model's file-based enums and constants but not its +macros. Neither receives declarations inside `MODEL()`. The access applies to the whole test or +scenario and remains separate from declarations visible through its own folder tree. The relevant +report sections are: + +```console +$ sqb scope test:orders__completed_only --used-only +Used (1) + └─ ● enum:order_status [enum; descendant-public; expected_model through model:orders; members 4; type VARCHAR; role models/commerce/enums] models/commerce/enums/status.sql:1:1 + +Relationship grants (1 of 1) + └─ ● enum:order_status [enum; descendant-public; expected_model through model:orders; members 4; type VARCHAR; role models/commerce/enums] models/commerce/enums/status.sql:1:1 +``` + +The compact reason `expected_model through model:orders` means **available through expected output +for model `orders`**. With multiple expected models, SQLBuild combines their eligible file-based +declarations into one deterministic set. Tests include macros in that union; scenarios include only +enums and constants. Neither receives declarations defined inside a model's `MODEL()` header. + +Macro-mode tests also report `tested_macro through macro:` relationships. Those grants contain +the tested macro and scoped file-based declarations available from its production owner. + +## Preview a resource move + +Use `--as-path` to see the scope delta before moving an existing model, test, hook, or function. The +ordinary report is followed by: + +```console +$ sqb scope model:orders --as-path models/finance/orders.sql +Move preview + Resource: model:orders + Destination: models/finance/orders.sql + Ownership root: models + Retained (2) + ├─ ○ macro:add_tax [macro; project; global; params 1; role macros] macros/currency.py:1:1 + └─ ○ macro:round_money [macro; project; global; params 1; role macros] macros/currency.py:5:1 + Gained (2) + ├─ ○ enum:finance_status [enum; descendant-public; inherited_ancestor; members 3; type VARCHAR; role models/finance/enums] models/finance/enums/status.sql:1:1 + └─ ○ macro:calculate_margin [macro; descendant-public; inherited_ancestor; params 2; role models/finance/macros] models/finance/macros/margin.py:4:1 + Lost (3) + ├─ ● constant:minimum_order_value [constant; descendant-public; inherited_ancestor; type integer; role models/commerce/constants] models/commerce/constants/limits.sql:1:1 + ├─ ○ enum:order_status [enum; descendant-public; inherited_ancestor; members 4; type VARCHAR; role models/commerce/enums] models/commerce/enums/status.sql:1:1 + └─ ● macro:formatted_order_total [macro; descendant-public; inherited_ancestor; params 1; role models/commerce/macros] models/commerce/macros/orders.py:4:1 + Private retained (0) + (none) + Relationship retained (0) + (none) + Invalidated usages (2) + - constant:minimum_order_value + - macro:formatted_order_total +``` + +The move would gain finance declarations and lose commerce declarations. More importantly, +`Invalidated usages` separates the losses that break the model from declarations that happened to +be available but were never used. The preview does not move or rewrite any file. + +## Check a prospective path + +Inspect visibility before a resource exists: + +```console +$ sqb scope --at models/commerce/returns/new_return.sql +Scope + Target: models/commerce/returns/new_return.sql + Path: models/commerce/returns/new_return.sql + Status: prospective + +Used (0) + (none) + +Scope chain + ├─ exact-owner-private models/commerce/returns (0) + ├─ descendant-public models/commerce (3) + ├─ descendant-public models (0) + └─ project global (2) + +Available (3 of 5, 2 collapsed) + ├─ ○ constant:minimum_order_value [constant; descendant-public; inherited_ancestor; type integer; role models/commerce/constants] models/commerce/constants/limits.sql:1:1 + ├─ ○ enum:order_status [enum; descendant-public; inherited_ancestor; members 4; type VARCHAR; role models/commerce/enums] models/commerce/enums/status.sql:1:1 + └─ ○ macro:formatted_order_total [macro; descendant-public; inherited_ancestor; params 1; role models/commerce/macros] models/commerce/macros/orders.py:4:1 + … 2 globals collapsed; run sqb scope --at models/commerce/returns/new_return.sql --globals all + +Relationship grants (0 of 0) + (none) + +Nearby unavailable (0 of 0) + (none) + +Diagnostics (1) + ERROR S013 models/commerce/returns/new_return.sql: Runtime usage and relationship facts are unavailable for a prospective path +Completeness: partial +``` + +Visibility is available from the proposed path. Actual usage and expected-output relationships do +not exist yet, so the command preserves the useful static result while clearly marking it partial. + +## Browse large declaration sets + +In a larger project, browse returns folder summaries rather than an arbitrary prefix of the +declaration inventory. The `used` counts are direct usages by this target; browse does not apply +dependency-depth expansion: + +```console +$ sqb scope model:orders --browse global +Scope folders + Path: global + + ├─ constants/ 146 declarations, 18 used, 4 children; constant 146 + sqb scope model:orders --browse global/constants + sqb scope model:orders --list global/constants + ├─ enums/ 84 declarations, 11 used, 3 children; enum 84 + sqb scope model:orders --browse global/enums + sqb scope model:orders --list global/enums + └─ macros/ 231 declarations, 27 used, 8 children; macro 231 + sqb scope model:orders --browse global/macros + sqb scope model:orders --list global/macros + +Diagnostics (0) + (none) +Completeness: complete +``` + +Choose a bounded folder, then use `--list`. Definition-path, kind, glob, and actual-usage filters +can be combined, and paged sections use stable declaration identities as cursors. + +## Automation + +Use `--json` for editor integrations and repository tooling. Output has a versioned schema, stable +ordering, filters, pagination, and move-preview results. Constant values, credentials, and secret +connection settings are not included. + + + + Review the directory rules behind the report. + + + See all selectors, filters, pagination options, output sections, and JSON behavior. + + diff --git a/website/src/content/docs/docs/concepts/declaration-scopes/placement.mdx b/website/src/content/docs/docs/concepts/declaration-scopes/placement.mdx new file mode 100644 index 0000000000..53e6af885e --- /dev/null +++ b/website/src/content/docs/docs/concepts/declaration-scopes/placement.mdx @@ -0,0 +1,97 @@ +--- +title: "Where to Put Declarations" +description: "Choose the narrowest folder that contains every real use." +--- + +import Card from '../../../../../components/mintlify/Card.astro'; + +Start with the ordinary project-wide directories unless you have a reason to limit access: + +```text +macros/ +enums/ +constants/ +``` + +Move a declaration closer to its users when it should be available only in one folder or one folder +tree. + +## Choose a location + +| Where it is needed | Location | +|--------------------|----------| +| One model only | Inside that model's `MODEL()` header, for enums and constants | +| SQL files directly in one directory | `_sqlbuild/_macros/`, `_sqlbuild/_enums/`, or `_sqlbuild/_constants/` | +| A directory and its descendants | `_sqlbuild/macros/`, `_sqlbuild/enums/`, or `_sqlbuild/constants/` | +| Different trees, such as models and tests | Top-level `macros/`, `enums/`, or `constants/` | + +## One directory + +These two models use the same constant and both sit directly in `commerce/`: + +```text +models/ +└── commerce/ + ├── _sqlbuild/ + │ └── _constants/ + │ └── minimum_value.sql + ├── orders.sql + └── customers.sql +``` + +Use `_sqlbuild/_constants/` because no descendant directory needs the value. + +## One directory tree + +These models use the same constant across two child directories: + +```text +models/ +└── commerce/ + ├── _sqlbuild/ + │ └── constants/ + │ └── reporting_day.sql + ├── finance/ + │ └── revenue.sql + └── fulfillment/ + └── shipments.sql +``` + +Use `_sqlbuild/constants/` in `commerce/` so both child directories can use it. + +## Different resource trees + +When a declaration is used directly from unrelated trees, make it project-wide: + +```text +my_project/ +├── constants/ +│ └── order_status.sql +├── models/ +│ └── commerce/orders.sql +└── tests/ + └── scenarios/order_lifecycle.sql +``` + +Top-level placement is valid when consumers genuinely cross resource trees or otherwise have no +shared owner folder. SQLBuild applies the same nearest-shared-folder analysis to project-wide +declarations, so a declaration used only under one narrower folder must move closer to those users. + +## What SQLBuild checks + +Every declaration must be used by real compiled SQL. A name appearing only in a comment, quoted +string, mock identity, or documentation does not count as use. + +For every declaration, including project-wide declarations, SQLBuild finds the nearest shared owner +folder of the files that use it. If the declaration is in a broader location, the error shows: + +- Where the declaration is now +- Which files use it +- Which directory form is required +- The destination directory + +These checks use the complete project rather than only the models selected by the current command. + + + Use Scope Explorer to see what a file can access or preview moving the file. + diff --git a/website/src/content/docs/docs/concepts/declaration-scopes/visibility.mdx b/website/src/content/docs/docs/concepts/declaration-scopes/visibility.mdx new file mode 100644 index 0000000000..4ccec4cea3 --- /dev/null +++ b/website/src/content/docs/docs/concepts/declaration-scopes/visibility.mdx @@ -0,0 +1,180 @@ +--- +title: "How Visibility Works" +description: "See which enums, constants, and macros are available to each SQL file." +--- + +import Card from '../../../../../components/mintlify/Card.astro'; + +For most SQL, the rule is simple: + +> A file can use declarations available to the whole project, declarations in unprefixed roles +> under `_sqlbuild/` folders owned beside or above it, and declarations in an underscored role owned +> directly beside it. + +## Start from the SQL file + +SQLBuild starts from the file containing the SQL and walks up that file's directory tree. The folder +directly containing the file is its **owner folder**. + +```text +models/ +├── constants/ available throughout models/ +│ └── warehouse.sql +└── commerce/ + ├── _sqlbuild/ + │ ├── enums/ available throughout commerce/ + │ │ └── order_status.sql + │ └── _constants/ available directly in commerce/ only + │ └── minimum_value.sql + ├── orders.sql + └── history/ + └── archived_orders.sql +``` + +From this tree: + +| File | Can use | +|------|---------| +| `orders.sql` | `warehouse`, `order_status`, and `minimum_value` | +| `history/archived_orders.sql` | `warehouse` and `order_status` | + +`minimum_value` is not available in `history/` because `_sqlbuild/_constants/` applies only to files +directly in the `_sqlbuild/` owner's folder. + +## Supported resource trees + +Grouped declaration directories can be placed below these SQL resource roots: + +| Root | Contents | +|------|----------| +| `models/` | Models and inline model hooks | +| `tests/unit/` | Unit tests | +| `tests/scenarios/` | Scenarios | +| `hooks/sql/` | Named SQL hooks | +| `functions/sql/` | SQL functions | +| `audits/` | Audits | +| `sources/` | Inline source expressions | + +Each root is a separate tree. For example, `models/constants/` does not make declarations available +under `tests/`. Put a declaration in the top-level `constants/`, `enums/`, or `macros/` directory +when it must be available across different resource trees. Project-root `_constants/`, `_enums/`, +and `_macros/` directories are invalid because there is no owner folder at the project root for +them to be private to. + +Existing scoped declaration roles directly below an owner remain supported. `_sqlbuild/` is the +preferred layout because it keeps all declarations together without changing their visibility. +Only the six public/private declaration-role directories are valid directly under `_sqlbuild/`; +other entries are rejected. `_sqlbuild/` must sit below a concrete owner directory. A project-root +`_sqlbuild/` or authored-root path such as `models/_sqlbuild/` is invalid; use the project-wide +`macros/`, `enums/`, or `constants/` roots at that boundary. + +## Which file controls visibility? + +| SQL being compiled | SQLBuild starts from | +|--------------------|----------------------| +| Model query | The model file | +| Inline SQL hook in a model | The model file | +| Unit test SQL | The test file, plus inferred tested-resource relationships | +| Scenario SQL | The scenario file, plus expected-model enum and constant relationships | +| Named SQL hook | The hook file under `hooks/sql/` | +| SQL function | The function file under `functions/sql/` | +| Audit | The audit file | +| Inline source expression | The source definition | + +This means a reusable named hook does not change meaning depending on which model calls it. The +hook uses declarations available where the hook itself is stored. + +## Tests and expected output + +A test first sees declarations available from its own folder tree. It may also use file-based +macros, enums, and constants available to a model for which it defines expected output. + +```sql +TEST(); + +WITH +__expected__orders AS ( + SELECT + 1 AS order_id, + @normalize_order_status("'completed'") AS normalized_status, + @enum("order_status").COMPLETED AS status +) +SELECT 1 +``` + +Because the test defines `__expected__orders`, the test may use file-based macros, enums, and +constants available to `orders`, including declarations in exact-owner-private roles. +Scope Explorer describes this as **available through expected output for model `orders`**. This is +an additional relationship, not another visibility level. + +This additional access does **not** include: + +- Enums or constants declared privately inside the model's `MODEL()` header +- Declarations from a model merely mentioned by filename or directory layout + +Only an explicit `__expected__model_name` section adds the model's eligible file-based declarations. +When a test checks several models, SQLBuild combines the declarations available through all +expected models and makes that deterministic union available while compiling the entire test. + +Macro-mode tests receive the tested macro and the file-based declarations available from that +macro's production owner. SQLBuild infers tested macros from calls in `__macro_actual__`; the test's +filename or directory does not grant production visibility. + +```text +models/orders/_sqlbuild/_macros/order_policy.py +tests/unit/macros/models/orders/test_order_policy__returns_quantity.sql +``` + +The macro test can call `order_policy` without promoting it to the project-wide `macros/` root. A +test can also use helper macros scoped to its own test directory: + +```text +tests/unit/orders/ +├── _sqlbuild/ +│ └── macros/ +│ └── fixtures.py +└── test_orders__calculates_total.sql +``` + +Those helpers remain unavailable to production resources. Mocked model, source, seed, and dbt +fixture CTEs do not grant declarations from the mocked resource's production scope. + +## Macros importing macros + +A macro file may import another project macro file when the imported macro is available from the +importing file's location. The same directory rules apply as they do to SQL. + +```python +# models/commerce/macros/orders.py +from macros.currency import round_money + + +def formatted_total(expression: str) -> str: + return round_money(expression) +``` + +A broadly available macro cannot import a macro from a narrower child or sibling directory. See +[Composition and Context](/docs/concepts/macros/composition-and-context) for complete examples. + +## Names do not shadow + +Public names must be unique within their feature: + +- One public macro name cannot be defined twice. +- One public enum name cannot be defined twice. +- One public constant name cannot be defined twice. + +Moving a declaration into a narrower directory changes where it is available; it does not create a +second version that overrides another declaration. + +Macros, enums, and constants use separate namespaces, so these three references may coexist: + +```sql +@format_currency() +@const("format_currency") +@enum("format_currency").USD +``` + + + Choose the simplest directory that matches where a declaration is used. + diff --git a/website/src/content/docs/docs/concepts/diff.mdx b/website/src/content/docs/docs/concepts/diff.mdx new file mode 100644 index 0000000000..d50aec53e6 --- /dev/null +++ b/website/src/content/docs/docs/concepts/diff.mdx @@ -0,0 +1,226 @@ +--- +title: "Data Diffs" +description: "Compare schemas and data between targets to validate changes before they reach production." +--- + +import Frame from '../../../../components/mintlify/Frame.astro'; + +SQLBuild can compare schemas and row-level data between two targets. This lets you validate that changes produce the expected results before they reach production. + +`sqb diff FROM:TO` compares two targets, for example `prod:dev`. + +`FROM` and `TO` resolve the authoritative database/schema namespaces to compare. +SQLBuild uses the `TO` target's named connection for the complete comparison and accesses both +namespaces through fully qualified relations. It does not resolve or open the `FROM` target's +connection. The `TO` connection must therefore be able to read both namespaces; missing credentials +for that execution connection still fail before warehouse inspection. + +```bash +sqb diff prod:dev --full --select customer_status_snapshot +``` + + + SQLBuild diff output showing schema comparison, row-level differences, and changed column values between two build contexts + + +## Comparison modes + +Every diff requires exactly one mode: + +### Full diff + +Compares both schema and row-level data for the selected models: + +```bash +sqb diff prod:dev --full --select fact_orders +``` + +Rows are joined on the model's `unique_key` and compared column by column. The output shows: +- Row counts for each side +- How many rows are equal, unequal, or only in one side +- Which columns have mismatches with match percentages +- Example values showing what changed + +### Schema-only diff + +Compares column names and types without looking at row data: + +```bash +sqb diff prod:dev --schema-only --select fact_orders +``` + +Useful for quick structural checks or when row comparison would be too expensive. + +### Bounded diff + +Compares only a recent window of data using the model's cursor: + +```bash +sqb diff prod:dev --bounded 14d --select hourly_order_activity +``` + +For timestamp cursors, the bound is a duration (`14d`, `6h`, `30m`). For integer cursors, the bound is an integer value. If the model has no cursor configured, the diff falls back to a full row comparison. + +## Deterministic key sampling + +A cursor bound limits the range of data but does not guarantee a predictable number of rows. Use +deterministic key sampling to cap the wide value comparison for a high-volume model: + +```toml +[defaults] +row_diff_sample_rows = 100000 +row_diff_sample_seed = 0 + +[path_defaults.intermediate] +row_diff_sample_rows = 25000 +``` + +The settings use the normal configuration precedence: project `defaults`, matching +`path_defaults`, the model's `MODEL()` header, and finally CLI overrides. A model can disable an +inherited sample and request exhaustive comparison with `row_diff_sample_rows 0`: + +```sql +MODEL ( + materialized table, + unique_key [order_id, line_id], + row_diff_sample_rows 0, +); +``` + +Override the effective policy for one invocation with `--sample-rows` and `--sample-seed`, or force +an exhaustive comparison with `--exhaustive`: + +```bash +sqb diff prod:dev --bounded 14d --sample-rows 50000 --sample-seed 7 --select order_lines +sqb diff prod:dev --bounded 14d --exhaustive --select order_lines +``` + +SQLBuild applies the complete cursor bound first. It then selects the lowest deterministic hashes +from the union of unique keys found on either side and uses that same key set for both relations. +This avoids false side-only rows caused by independently sampling each side. Composite keys are +encoded with component lengths before hashing, and key columns break hash ties deterministically. + +Schema comparison, bounded row counts, cursor minimum/maximum values, and null/duplicate key checks +remain exhaustive. Only the wide column-by-column comparison is sampled. If the bounded union has +no more keys than the configured limit, SQLBuild reports the comparison as exhaustive. + +Sampled success means that no differences were found in the sampled keys; it is never reported as +complete table equality. The output shows the bounded key population, compared key count, seed, and +percentage evaluated. + +## Cursor coverage + +Before comparing values, SQLBuild reports exact bounded `COUNT`, `MIN(cursor)`, and `MAX(cursor)` +for each side. If cursor extents differ, it warns and continues with the requested comparison. It +does not ask for confirmation, silently narrow to the overlap, or hide rows that exist on only one +side. + +## Row matching + +Rows are matched between the two sides using the model's `unique_key`. Models without a `unique_key` can use schema-only diff but cannot run full or bounded row comparisons. + +The diff output categorises rows as: +- **Equal** - same key, same values on both sides +- **Unequal** - same key, different values (with per-column breakdown) +- **Left only** - exists in the FROM side but not TO +- **Right only** - exists in the TO side but not FROM + +## Tolerances + +Numeric columns can have tolerance rules to avoid false positives from floating-point differences or acceptable variance. Configure tolerances in the model's `MODEL()` header: + +```sql +MODEL ( + materialized incremental, + ... + row_diff_tolerances ( + by_column ( + total_revenue_cents ( + absolute 1, + ), + ), + ), +); +``` + +Tolerance rules support: +- **`absolute`** - maximum allowed absolute difference (e.g. `1` means values differing by 1 or less are treated as equal) +- **`relative`** - maximum allowed relative difference as a decimal (e.g. `0.01` for 1%) + +Tolerances can be set per-column (`by_column`) or per-type (`by_type`). + +## Excluded columns + +Columns that are expected to differ between the two sides (like timestamps or context-specific values) can be excluded from the row comparison: + +```sql +MODEL ( + materialized incremental, + ... + row_diff_exclude_columns [latest_order_status], +); +``` + +Excluded columns are still shown in the schema comparison but skipped during row-level diffing. A column cannot be in both `row_diff_exclude_columns` and `unique_key`. + +## Verbose output + +Add `--verbose` or `-v` to see more example rows for mismatches and side-only rows: + +```bash +sqb diff prod:dev --full --select customer_status_snapshot --verbose +``` + +Default example limits are 3 per category. Verbose mode increases this to 10. You can also set exact limits: + +```bash +sqb diff prod:dev --full --select customer_status_snapshot --max-column-examples 20 --max-row-only-examples 5 +``` + +Example limits only control diagnostic values printed after comparison. They are separate from +`row_diff_sample_rows`, which controls how many unique keys receive the wide value comparison. + +## Structured output + +Use `--json-output PATH` to write a stable structured result while retaining the normal terminal +summary: + +```bash +sqb diff prod:dev --bounded 14d --sample-rows 50000 --select order_lines --json-output diff.json +``` + +Each model records `schema_only`, `exhaustive`, or `sampled` comparison scope; requested and observed +cursor coverage; bounded population and compared key counts; seed and configured limit; row result +counts; and changed-column counts. The top-level status distinguishes `no_differences_found` from +`differences_found`. + +## Invocation safety limits + +Use `--max-models` and `--max-columns` to put explicit hard limits around a broad selector. SQLBuild +fails visibly instead of truncating the selected models or compared columns: + +```bash +sqb diff prod:dev --bounded 14d --sample-rows 50000 --max-models 10 --max-columns 80 --select path:models/intermediate +``` + +These limits are optional and apply to the complete invocation. `--max-columns` checks the larger +observed schema for each model before starting its row comparison. + +## Selectors + +Diff requires `--select` in the current version. You can use any selector syntax: + +```bash +# Diff a single model +sqb diff prod:dev --full --select customer_status_snapshot + +# Diff all models in a path +sqb diff prod:dev --schema-only --select path:models/marts + +# Diff models with a specific tag +sqb diff prod:dev --full --select tag:acceptance +``` + +## Exit codes + +`sqb diff` returns exit code `0` when all selected models have no differences, and `1` when any model has schema or row differences. This makes it usable in CI pipelines as a validation gate. diff --git a/website/src/content/docs/docs/concepts/enums-and-constants.mdx b/website/src/content/docs/docs/concepts/enums-and-constants.mdx new file mode 100644 index 0000000000..75c26e5fd1 --- /dev/null +++ b/website/src/content/docs/docs/concepts/enums-and-constants.mdx @@ -0,0 +1,62 @@ +--- +title: "Enums and Constants Have Moved" +description: "Find the new focused guides for enums, constants, collections, contracts, and private values." +--- + +import Card from '../../../../components/mintlify/Card.astro'; +import CardGroup from '../../../../components/mintlify/CardGroup.astro'; + +Enums and constants now have separate guides so each feature is easier to find and learn. This page +remains available for existing links and bookmarks but is not part of the main navigation. + + + + Define fixed string or integer domains and reference validated members. + + + Define reusable scalar values and reference them safely from SQL. + + + Work with lists, sets, objects, value lists, and native arrays. + + + Keep enums and constants inside one model. + + + +## Public declarations + +See [Enums](/docs/concepts/enums) and [Constants](/docs/concepts/constants) for project-wide declarations. + +## Constant values + +See [Constants](/docs/concepts/constants#scalar-values) for scalars and +[Collections and Rendering](/docs/concepts/constants/collections-and-rendering) for collections. + +## References and rendering + +See [Collections and Rendering](/docs/concepts/constants/collections-and-rendering). + +## Adapter matrix + +See the [native-array rendering matrix](/docs/concepts/constants/collections-and-rendering#native-array-rendering). + +## Rendering configuration + +See [Project default](/docs/concepts/constants/collections-and-rendering#project-default). + +## Model-local declarations + +See [Model-Private Values](/docs/concepts/model-private-values). + +## Model-private declarations + +See [Model-Private Values](/docs/concepts/model-private-values). + +## Enum validation + +See [Enum validation rules](/docs/concepts/enums#validation-rules). + +## Enum-typed contracts + +See [Enum Model Contracts](/docs/concepts/enums/model-contracts). diff --git a/website/src/content/docs/docs/concepts/enums.mdx b/website/src/content/docs/docs/concepts/enums.mdx new file mode 100644 index 0000000000..2f1769d2c6 --- /dev/null +++ b/website/src/content/docs/docs/concepts/enums.mdx @@ -0,0 +1,97 @@ +--- +title: "Enums" +description: "Define a fixed set of named string or integer values and use them safely in SQL." +--- + +import Card from '../../../../components/mintlify/Card.astro'; +import CardGroup from '../../../../components/mintlify/CardGroup.astro'; + +Enums give a name to a fixed set of allowed values. SQLBuild checks the enum and every member +reference during compilation, then renders the selected value as a safe SQL literal. + +## Create an enum + +Put project-wide enums under the top-level `enums/` directory. Files are discovered recursively, so +subdirectories can organize a large enum library without changing where the enums are available. + +```text +my_project/ +├── enums/ +│ ├── fulfillment/ +│ │ └── fulfillment_method.sql +│ └── order_status.sql +├── models/ +└── sqlbuild_project.toml +``` + +```sql +-- enums/fulfillment/fulfillment_method.sql +ENUM ( + name fulfillment_method, + members [DELIVERY, PICKUP, SHIPPING], +); +``` + +The shorthand above uses each member name as its string value. Use explicit values when the name +used in SQLBuild should differ from the stored value: + +```sql +ENUM ( + name source, + members ( + WEB "web", + PARTNER "partner", + ), +); +``` + +Integer enums always use explicit values: + +```sql +ENUM ( + name priority, + members (LOW 1, HIGH 3), +); +``` + +## Use an enum member + +Reference one member with `@enum("name").MEMBER`: + +```sql +SELECT * +FROM orders +WHERE fulfillment_method = @enum("fulfillment_method").DELIVERY + AND source = @enum("source").WEB +``` + +SQLBuild validates the enum name and member name before the query runs. The active adapter safely +renders the underlying string or integer value. + +Enum references work in model queries, SQL hooks, SQL functions, audits, unit tests, scenarios, and +inline source expressions. + +## Validation rules + +- An enum must contain at least one member. +- Every member must use the same value type: all strings or all integers. +- Enum names and member names must be SQL identifiers. +- Member names must be uppercase and lookup is case-sensitive. +- Enum names must be unique across all public enums in the project. +- Project-wide names cannot begin with `_`; that prefix is reserved for model-private values. + +Invalid declarations, unknown enums, and unknown members fail compilation. + +## More enum features + + + + Use an enum as a portable model-column domain and generate accepted-value validation. + + + Keep an enum inside one model when no other resource should use it. + + + +To limit an enum to one folder, or to that folder and its child folders, see +[Declarations and Scopes](/docs/concepts/declaration-scopes). diff --git a/website/src/content/docs/docs/concepts/enums/model-contracts.mdx b/website/src/content/docs/docs/concepts/enums/model-contracts.mdx new file mode 100644 index 0000000000..e9a1a579d4 --- /dev/null +++ b/website/src/content/docs/docs/concepts/enums/model-contracts.mdx @@ -0,0 +1,47 @@ +--- +title: "Enum Model Contracts" +description: "Use an enum as a portable model-column domain with generated accepted-value validation." +--- + +An enum can describe the allowed domain of a model column as well as provide individual SQL +literals. + +These are separate uses: + +- `@enum("fulfillment_method").DELIVERY` inserts one validated value into SQL. +- `type fulfillment_method` declares that a model column uses the complete enum domain. + +## Declare an enum-typed column + +Use the enum name as the column type: + +```sql +MODEL ( + contract enforced, + columns ( + fulfillment_method (type fulfillment_method), + ), +); +``` + +SQLBuild does not send `fulfillment_method` to the warehouse as a physical type and does not create a +warehouse-native enum. It translates the enum into portable column metadata: + +| Enum values | Physical column type | Generated validation | +|-------------|----------------------|----------------------| +| Strings such as `DELIVERY` and `PICKUP` | `VARCHAR` | `accepted_values` for the enum strings | +| Integers such as `1` and `3` | `INTEGER` | `accepted_values` for the enum integers | + +## Contract behavior + +With `contract enforced`, SQLBuild runs the generated `accepted_values` audit with the model's other +audits. Its severity and timing follow the model's ordinary audit and materialization settings. + +With `contract none`, SQLBuild still resolves the enum to its portable scalar column type but does +not generate the domain audit. + +This behavior is the same on adapters with and without native enum support. The physical warehouse +column remains an ordinary string or integer column. + +Changing the members of an enum-typed contract changes the model's contract identity and generated +validation. diff --git a/website/src/content/docs/docs/concepts/functions.mdx b/website/src/content/docs/docs/concepts/functions.mdx new file mode 100644 index 0000000000..68233e8632 --- /dev/null +++ b/website/src/content/docs/docs/concepts/functions.mdx @@ -0,0 +1,169 @@ +--- +title: "Functions" +description: "User-defined functions and table functions managed as part of your project." +--- + +Functions are SQL or Python definitions under `functions/` that SQLBuild compiles and deploys to the warehouse alongside your models. They participate in the DAG - if a function definition changes, every model that uses it is rebuilt. + +## Scalar UDFs + +Scalar UDFs return a single value per row. They can be written in SQL or Python. + +### SQL UDFs + +Place SQL function files under `functions/sql/`. Each file has a `FUNCTION()` header declaring arguments and return type, followed by a SQL expression body: + +```sql +-- functions/sql/udf__is_completed_order.sql +FUNCTION ( + arguments (order_status STRING), + returns BOOLEAN, +); + +order_status = 'completed' +``` + +### Python UDFs + +Place Python function files under `functions/python/`. Each file has exactly one function decorated with `@udf(...)`: + +```python +# functions/python/is_completed_order_py.py +from sqlbuild.functions import udf + +@udf( + arguments={"order_status": "STRING"}, + returns="BOOLEAN", + runtime_version="3.11", +) +def main(order_status: str | None) -> bool: + return order_status == "completed" +``` + +Python UDFs are deployed as warehouse-native functions (Snowflake Python UDFs, BigQuery remote functions, etc.). The `@udf` decorator is used for static discovery only - SQLBuild parses the AST without importing your code. + +### Using scalar UDFs + +Reference scalar UDFs in models with `__udf("name")`: + +```sql +SELECT + order_id, + __udf("udf__is_completed_order")(status) AS is_completed +FROM __ref("stg_orders") +``` + +The function name is the file stem (filename without extension). `__udf()` resolves to the adapter-native function call at compile time. + +## Table functions + +Table functions return multiple rows and columns. They are written in SQL under `functions/sql/` with a `returns table(...)` declaration: + +```sql +-- functions/sql/table_fn__customer_orders.sql +FUNCTION ( + arguments (p_customer_id INTEGER), + returns table ( + order_id INTEGER, + ordered_at TIMESTAMP, + waffle_name VARCHAR, + line_total_cents INTEGER, + order_status VARCHAR, + is_completed_order BOOLEAN + ) +); + +SELECT + order_id, + ordered_at, + waffle_name, + line_total_cents, + order_status, + is_completed_order +FROM __ref("fact_orders") +WHERE customer_id = p_customer_id +``` + +### Why table functions exist + +Table functions are designed as an alternative to final-layer views for cases where views don't push predicates efficiently. A view over `fact_orders` with a `WHERE customer_id = ?` filter may scan the entire table if the engine doesn't push the predicate down. A table function accepts the filter as an argument and guarantees the predicate is applied at execution time. + +### Managed table function dependencies + +Models call managed table functions with `__table_fn("name")(arguments...)`: + +```sql +SELECT customer_id, order_id +FROM __table_fn("table_fn__customer_orders")(42) +``` + +The name must be double quoted and the second argument list is required, including for a zero-argument function. SQLBuild validates that the target exists, is a table function, and receives the declared number of arguments. + +The reference creates a typed dependency in the project DAG. Selecting a consuming model also selects the required function, function changes propagate to consumers, and cycles through models and functions are rejected. SQLBuild treats the call as an opaque relation for SQL analysis and uses the function's declared `returns table(...)` columns for star expansion and terminal lineage. + +Incremental state still belongs to the consuming model. A table function does not own a cursor interval or retain execution state. + +### Using table functions + +Applications and analysts can call the deployed warehouse function directly: + +```sql +SELECT * FROM table_fn__customer_orders(42) +``` + +## References inside functions + +SQL functions can reference the same resources as models: + +| Reference | Syntax | +|-----------|--------| +| Model | `__ref("model_name")` | +| Seed | `__seed("seed_name")` | +| Source | `__source("source_name")` | +| Scalar UDF | `__udf("function_name")` | +| Table function | `__table_fn("function_name")(arguments...)` | + +These references are resolved at compile time and create DAG edges. If a referenced model changes, the function is redeployed. + +A SQL function uses macros, constants, and enums available from its file under `functions/sql/`. +See [How Visibility Works](/docs/concepts/declaration-scopes/visibility) to limit declarations to one +function folder or to that folder and its children. + +## Change propagation + +Functions participate in fingerprint-based change detection. Their identity includes dependencies and declared return contracts in addition to the function body and runtime metadata. If a function changes, SQLBuild redeploys it and marks dependent models as changed. + +## Project layout + +``` +functions/ + sql/ + udf__is_completed_order.sql # scalar SQL UDF + table_fn__customer_orders.sql # table function (returns table) + python/ + is_completed_order_py.py # scalar Python UDF +``` + +## Python UDF options + +The `@udf` decorator accepts these keyword arguments: + +| Argument | Required | Description | +|----------|----------|-------------| +| `arguments` | Yes | Dict mapping argument names to SQL types | +| `returns` | Yes | SQL return type string | +| `runtime_version` | No | Python runtime version (e.g. `"3.11"`) | +| `entry_point` | No | Function name to use as the entry point (defaults to the decorated function name) | +| `packages` | No | List of Python packages required at runtime | + +## Adapter support + +All four supported adapters implement SQL UDFs, Python UDFs, and table functions: + +| Feature | DuckDB | Snowflake | BigQuery | Databricks | +|---------|--------|-----------|----------|------------| +| SQL UDFs | Yes | Yes | Yes | Yes | +| Python UDFs | Yes | Yes | Yes | Yes | +| Table functions | Yes | Yes | Yes | Yes | + +Future adapters may not support all function types. SQLBuild raises a clear error if a function type is unsupported by the configured adapter. diff --git a/website/src/content/docs/docs/concepts/incremental.mdx b/website/src/content/docs/docs/concepts/incremental.mdx new file mode 100644 index 0000000000..f72936a98a --- /dev/null +++ b/website/src/content/docs/docs/concepts/incremental.mdx @@ -0,0 +1,384 @@ +--- +title: "Incremental Models" +description: "Cursor-based incremental strategies, microbatch execution, and backfill policies." +--- + +Incremental models process only new or changed data instead of rebuilding the entire table. In the +default sequential path, SQLBuild works out where to resume from the current target and input +relations with no separate checkpoint store. A retry recomputes its interval from current warehouse +state rather than reusing the exact interval of a failed attempt. Opt-in concurrent microbatching +adds immutable coordination facts as described below. + +## Strategies + +### append + +Inserts new rows without modifying existing data. Optionally uses a cursor (read from the target table's highest value) to avoid reprocessing the full source on every run. + +```sql +MODEL ( + materialized incremental, + incremental_strategy append, + cursor created_at, + cursor_type timestamp, + cursor_grain second, + append_cursor_inclusive true, +); + +SELECT id, customer_id, created_at +FROM __source("raw_events") +``` + +When `append_cursor_inclusive` is `true` (the default), the lower bound uses `>=`, which may duplicate the boundary row but avoids missing late-arriving data with the same cursor value. Set to `false` for an exclusive (`>`) lower bound if your cursor values are guaranteed unique. + +Append without a cursor is also valid. The model simply inserts all rows from the source query on every run. + +### delete_insert + +Deletes rows in the cursor range, then inserts the new delta. Requires either `cursor` or `unique_key`. + +```sql +MODEL ( + materialized incremental, + incremental_strategy delete_insert, + unique_key [order_id], + cursor order_id, + cursor_type integer, +); + +SELECT order_id, customer_id, order_status, ordered_at, line_total_cents +FROM __ref("fact_orders") +``` + +With a cursor, `delete_insert` removes rows where the cursor column falls within the replay window, then inserts the new delta. With a `unique_key` only, it deletes matching rows by key before inserting. + +### merge + +Upserts rows using a unique key. Matched rows are updated; unmatched rows are inserted. + +```sql +MODEL ( + materialized incremental, + incremental_strategy merge, + unique_key [customer_id], + cursor last_ordered_at, + cursor_type timestamp, + cursor_grain second, + cursor_inputs ( + fact_orders ordered_at, + ), +); + +SELECT + customer_id, + MAX(ordered_at) AS last_ordered_at, + COUNT(*) AS total_orders, + SUM(line_total_cents) AS total_revenue_cents +FROM __ref("fact_orders") +GROUP BY customer_id +``` + +`merge` always requires `unique_key`. The cursor controls which upstream rows are scanned; the unique key determines how they're matched against the target. + +## Full-refresh overrides + +Incremental models can override command-level full-refresh behavior with `full_refresh`: + +| Model setting | Normal command | Command with `--full-refresh` | +|---------------|----------------|-------------------------------| +| omitted | incremental | full refresh | +| `full_refresh false` | incremental | incremental | +| `full_refresh true` | full refresh | full refresh | + +Use `full_refresh false` for models that must retain their normal cursor or microbatch execution even when a broader job requests full refresh: + +```sql +MODEL ( + materialized incremental, + incremental_strategy delete_insert, + full_refresh false, + cursor event_date, + cursor_type timestamp, + cursor_grain day, +); +``` + +The override is evaluated per model, so one selection can contain incrementally executed opt-outs and full-refreshed models. It controls execution mode rather than acting as a safety rejection: an opted-out model does not abort the rest of the build. + +This does not skip initial loading. If the destination relation does not exist, an incremental or microbatch model still builds the history required by its cursor policy. Cloning an existing destination before the build can provide a current watermark and avoid a first-run historical replay. + +## Cursors + +Cursors define the incremental replay boundary. SQLBuild queries `MAX(cursor)` from the target table and `MIN/MAX` from upstream inputs to compute the replay window automatically. Observed maxima are inclusive warehouse values; SQLBuild advances them once to produce an effective exclusive end bound. + +| Field | Description | +|-------|-------------| +| `cursor` | Output column used to track incremental position | +| `cursor_type` | `timestamp` or `integer` | +| `cursor_grain` | Time grain for timestamp cursors: `second`, `minute`, `hour`, `day`, `month`, `year` | +| `cursor_start` | Lower bound floor; the cursor will never replay before this value | +| `cursor_inputs` | Map of upstream ref/source names to their cursor columns | + +### cursor_inputs + +When a model references multiple upstream inputs, `cursor_inputs` is required to tell SQLBuild which column on each input carries the cursor: + +```sql +MODEL ( + materialized incremental, + incremental_strategy delete_insert, + cursor activity_hour, + cursor_type timestamp, + cursor_grain hour, + cursor_inputs ( + fact_orders ordered_at, + ), +); +``` + +SQLBuild uses these to determine the replay window. With multiple listed inputs, the end is the conservative common watermark: the minimum of their maxima. This prevents a faster input from advancing the model beyond data available from a slower input. + +On a first build, SQLBuild derives the interval from the declared cursor inputs and cursor policy. If it cannot establish a valid interval, the build fails before mutating the destination. + +### Cursor bounds in model SQL + +Cursor-based incremental models can read their effective interval with zero-argument intrinsics: + +```sql +MODEL ( + materialized incremental, + incremental_strategy delete_insert, + cursor activity_hour, + cursor_type timestamp, + cursor_grain hour, + cursor_inputs ( + fact_orders ordered_at, + ), +); + +SELECT customer_id, ordered_at AS activity_hour +FROM __ref("fact_orders") +WHERE ordered_at >= __cursor_start() + AND ordered_at < __cursor_end() +``` + +`__cursor_start()` is the effective inclusive start and `__cursor_end()` is the effective exclusive end after cursor floors, lookback, replay policy, and command-line overrides have been applied. The intrinsics accept no arguments and are only valid in built-in cursor incremental model query SQL. They are rejected in functions, hooks, audits, SQL tests and scenarios, source expressions, non-incremental or cursorless models, custom materializations, and non-microbatch full refreshes. + +In microbatch mode, the intrinsics resolve to each batch's concrete bounds. A microbatch full refresh discovers its range from current inputs while ignoring the old destination watermark. + +#### Listed inputs bound the window; unlisted inputs do not + +Only the inputs you list in `cursor_inputs` bound the replay window. This is an explicit choice, and it has two consequences worth understanding: + +- **Listed inputs** drive the window. Their new data advances the `MAX`, which is what tells SQLBuild how far to reprocess and which rows of the target to rewrite. +- **Unlisted inputs are read in full.** SQLBuild does not add a cursor filter to them, and they do not bound the window. This is correct for lookup or dimension tables that have no meaningful cursor column: you do not list them, and SQLBuild reads them whole rather than trying to filter on a column that may not exist. + +The implication for `delete_insert` and `merge`: the target rows that get rewritten are the ones whose cursor falls inside the window derived from the listed inputs. If an unlisted input changes in a way that should affect target rows outside that window, those rows are not rewritten on a normal incremental run. List every input whose new data should drive reprocessing; leave unlisted only the inputs you intend to read in full. + +To capture changes that fall outside the normal forward window, see [Lookback](#lookback) for late-arriving data and [Replay on change](#replay-on-change) for model changes. + +### Lookback + +Lookback extends the start of the replay window backwards to re-process recent data. The cursor is forward-moving, so use lookback to capture late-arriving or backfilled records that land just behind the current position: + +```sql +MODEL ( + materialized incremental, + incremental_strategy delete_insert, + cursor event_date, + cursor_type timestamp, + cursor_grain day, + lookback 3d, +); +``` + +With `lookback 3d`, the replay window starts 3 days before the normal cursor position, ensuring that any late-arriving data within that window is picked up. + +## Microbatch execution + +For large incremental ranges, microbatch mode splits the replay window into configurable batches. +Choose an explicit strategy: + +| Strategy | Window source | +|----------|---------------| +| `watermark` | Declared input availability; supports timestamp and integer cursors | +| `rolling_window` | A timestamp window relative to the current run rather than input maxima | + +Each batch has its own audit cycle: create delta, run delta audits, apply DML, and clean up. + +```sql +MODEL ( + materialized incremental, + incremental_strategy delete_insert, + cursor activity_hour, + cursor_type timestamp, + cursor_grain hour, + incremental_mode microbatch, + microbatch_strategy watermark, + cursor_watermark_mode all, + cursor_inputs ( + fact_orders (column ordered_at, roles [filter, watermark]), + ), + batch_size 1d, +); +``` + +Without microbatch mode, the entire replay range is processed in one pass. + +### Batch size + +`batch_size` controls the window size for each batch. For timestamp cursors, use duration strings like `1d`, `6h`, `1mo`. For integer cursors, use an integer value. + +For watermark models, each `cursor_inputs` entry declares whether the input filters model SQL, +contributes an availability watermark, or does both. `cursor_watermark_mode all` uses the +conservative common watermark; `any` permits progress from any watermark input. A filter-only input +does not claim that its intervals are available. + +### Concurrent batches + +Sequential execution is the default and does not read or write `_sqlbuild_microbatches`. To opt into +concurrent `delete_insert` batches, enable the project gate and choose a model limit: + +```toml +[settings] +microbatch_concurrency = true +``` + +```sql +MODEL ( + materialized incremental, + incremental_strategy delete_insert, + incremental_mode microbatch, + microbatch_strategy watermark, + batch_concurrency 4, + -- cursor configuration omitted +); +``` + +Concurrent execution uses immutable requirement and completion facts to coordinate dependencies and +reconcile retries. It does not update one mutable status row through planned/running/complete +states. Increase concurrency deliberately because every active batch can consume warehouse work. + +### Watermark batch limits + +Watermark microbatch models can declare what to do when their resolved range contains more batches than an ordinary run should process: + +```sql +MODEL ( + materialized incremental, + incremental_strategy delete_insert, + incremental_mode microbatch, + microbatch_strategy watermark, + cursor_watermark_mode all, + cursor event_date, + cursor_type timestamp, + cursor_grain day, + cursor_inputs ( + raw_events (column event_date, roles [filter, watermark]), + ), + batch_size 1d, + microbatch_limit ( + max_batches 7, + action cap_from_end, + ), +); +``` + +| Action | Behavior when the range exceeds `max_batches` | +|--------|------------------------------------------------| +| `error` | Fail before hooks or model mutation | +| `warn` | Emit a prominent warning and execute the full range | +| `cap_from_start` | Execute the earliest N aligned batches and defer later work | +| `cap_from_end` | Execute the latest N aligned batches ending at the resolved watermark and defer older work | + +A cap changes only the work selected for that invocation. Deferred batches are not recorded as complete. `cap_from_end` is useful for feeds where keeping the latest projection current is more important than catching up oldest-first; `cap_from_start` is the oldest-first catch-up policy. + +Downstream watermark models consume durable completion facts from capped upstream models. A target +table's physical minimum/maximum does not prove that deferred or intervening intervals were built; +if completion evidence is unavailable, SQLBuild fails closed rather than inventing availability. + +The project can also set an outer safety policy: + +```toml +[microbatches.limits] +max_batches = 100 +action = "error" # or "warn" +``` + +Project limits support only `error` and `warn`; they never silently cap work. When a model has a nested `microbatch_limit`, the project limit checks the full resolved range first and the model policy then applies. + +`--max-microbatches N` is an invocation-wide, hard `error` ceiling and an explicit one-run authorization. It takes precedence over project and model limits, applies to models without a declared limit, and never inherits `cap_from_start` or `cap_from_end`. For example, passing a value large enough for an intentional backfill authorizes the full range instead of retaining the model's ordinary-run cap. + +The legacy scalar `max_microbatches` model field remains supported as a fail/warn guard. New models should use the nested form when the action is part of the model's execution policy. + +### Mixed-grain chains + +When a downstream microbatch model reads from an upstream model with a coarser time grain, SQLBuild aligns the replay to the coarsest participating grain (the model's own grain and its cursor-input grains). This happens on every run that resolves cursor bounds from upstream models, not only when something changes. It is independent of the `replay_on_change` cascade behavior described below. + +Alignment does two things: it floors the replay window edges to the coarsest grain, and it coarsens the batch size to that grain. For example, an hourly model downstream of a daily model processes in day-sized batches, so each batch lines up with a unit of upstream data that actually advances instead of producing empty or boundary-straddling windows: + +```sql +-- Upstream: daily grain, 2d batches +MODEL ( + materialized incremental, + incremental_strategy delete_insert, + cursor activity_day, + cursor_type timestamp, + cursor_grain day, + cursor_inputs ( + hourly_order_activity activity_hour, + ), + incremental_mode microbatch, + batch_size 2d, +); + +-- Downstream: hourly grain, but aligns to day automatically +MODEL ( + materialized incremental, + incremental_strategy delete_insert, + cursor activity_hour, + cursor_type timestamp, + cursor_grain hour, + cursor_inputs ( + daily_activity_rollup activity_day, + ), + incremental_mode microbatch, + batch_size 6h, +); +``` + +## Replay on change + +When a model's version identity changes (query, config, upstream cascade, or any other change reason), `replay_on_change` is the explicit, per-model policy for how much data to reprocess. Reprocessing is a policy you set, not an automatic forced rebuild, so a definition change does not silently trigger a full rebuild of large downstream tables. You choose the cost per model: + +| Value | Effect | +|-------|--------| +| `forward` | Run the normal incremental delta from the cursor (default) | +| `full` | Full table rebuild | +| `bounded-14d` | Replay the last 14 days of data | + +The bounded duration supports `d` (days), `h` (hours), `m` (minutes), and `s` (seconds). For example: `bounded-7d`, `bounded-24h`, `bounded-30m`. + +```sql +MODEL ( + materialized incremental, + incremental_strategy delete_insert, + cursor activity_hour, + cursor_type timestamp, + cursor_grain hour, + replay_on_change full, +); +``` + +See [Cascade propagation](/docs/concepts/planning/cascade-propagation) for how replay policies propagate through the DAG and how downstream models can override inherited replay behavior. + +### on_schema_change + +Controls how schema differences are handled at execution time when the incremental delta has different columns than the target table: + +| Value | Effect | +|-------|--------| +| `append_new_columns` | Add new columns to the target table (default) | +| `sync_all_columns` | Add, drop, and alter columns to match the delta | +| `ignore` | Log and continue without schema changes | +| `fail` | Reject the build with an error | diff --git a/website/src/content/docs/docs/concepts/interpolation.mdx b/website/src/content/docs/docs/concepts/interpolation.mdx new file mode 100644 index 0000000000..6d0eef70ca --- /dev/null +++ b/website/src/content/docs/docs/concepts/interpolation.mdx @@ -0,0 +1,196 @@ +--- +title: "Interpolation" +description: "How SQLBuild processes variables, context, and dynamic content in SQL and config." +--- + +SQLBuild uses two syntax layers for dynamic content: + +- **`@` syntax** is for any executable SQL - model queries, SQL hooks, tests, audits, and inline source expressions +- **`${...}` syntax** is for config values - project TOML config, MODEL() header fields (excluding SQL hooks), and source/seed YAML declarations + +The rule is simple: if it's any SQL that will be executed, it uses `@`. If it's a config value, it uses `${...}`. These layers never mix. + +## Syntax reference + +| Syntax | Where | Resolved | +|--------|-------|----------| +| `@enum("name").MEMBER` | Executable SQL | Compile time - expands to the validated enum member value | +| `@const("name")` | Executable SQL | Compile time - expands to the named typed scalar, value list, native array, or object expression | +| `@macro(args)` | Model SQL, SQL hooks, tests, audits, inline source expressions | Compile time - expands to macro return value | +| `@@name` | Model SQL, SQL hooks, tests, audits, inline source expressions | Compile time - project variable substitution | +| `@@ENV:NAME` | Model SQL, SQL hooks, tests, audits, inline source expressions | Compile time - environment variable | +| `@@CTX:name` | SQL hooks only | Compile time - destination relation, target, run ID | +| `@@@name` | Model SQL | Preserved for runtime (custom materializations) | +| `@name` / `@'name'` | Named SQL hook bodies | Raw / SQL-literal invocation argument, resolved at compile time | +| `@name` / `@'name'` | Generic audit SQL | Raw / SQL-literal audit argument | +| `${CTX:...}` | TOML/YAML config values | Config compilation | +| `${ENV:...}` | TOML/YAML config values | Config compilation | + +`@@CTX:` is intentionally SQL-hook-only. Model SQL describes a relation's data and should not reference its own destination identity. SQL hooks are the operational SQL layer where destination context is useful - grants, logging, post-materialization DDL. Python hooks access the same information through `ctx.destination` on the [`HookContext`](/docs/concepts/models/hooks/python#hook-context) object. + +See [Enums](/docs/concepts/enums) and [Constants](/docs/concepts/constants) for reusable validated values. See +[Collections and Rendering](/docs/concepts/constants/collections-and-rendering) for lists, sets, objects, +and adapter-specific rendering. + +SQLBuild uses the location of a SQL file to determine which macros, enums, and constants it can +use. Named SQL hooks use their own file location rather than the location of the calling model. See +[How Visibility Works](/docs/concepts/declaration-scopes/visibility) for the complete directory rules. + +## Project variables + +Project variables use `@@name` syntax in SQL and are defined in `sqlbuild_project.toml` or per-target: + +```toml +# sqlbuild_project.toml +[vars] +schema_prefix = "analytics" + +[targets.prod.vars] +schema_prefix = "prod_analytics" +``` + +```sql +SELECT * FROM @@schema_prefix.customers +``` + +Variables can also be set in `sqlbuild_local.toml` for developer-specific overrides, or passed via the CLI using `--vars` with a JSON object: + +```bash +sqb build --vars '{"schema_prefix": "staging_analytics"}' +``` + +CLI vars take precedence over local and project config vars. + +### JSON vars and nested values + +`--vars` accepts full JSON objects. Values can be strings, numbers, booleans, null, arrays, or nested objects: + +```bash +sqb build --vars '{"schema_prefix": "staging", "grants": {"primary_role": "analyst"}, "enabled": true}' +``` + +In SQL interpolation (`@@name`), only top-level scalar values can be used directly. `null` renders as an empty string. If a variable resolves to an array or object, SQLBuild raises a clear error suggesting you use a macro instead. + +For nested or complex values, use `ctx.vars` in a macro: + +```python +def grant_role(ctx): + return ctx.vars["grants"]["primary_role"] +``` + +The macro context receives the full native JSON structure including nested dicts, lists, booleans, numbers, and `None`. + +## Environment variables + +Environment variables use `@@ENV:NAME` syntax to inject values from the shell environment: + +```sql +SELECT * +FROM @@schema_prefix.customers +WHERE source_system = '@@ENV:SOURCE_SYSTEM' +``` + +If the environment variable is not set, SQLBuild raises a compile error. + +## Context variables + +Context variables provide access to the current model's destination relation, active target, and run metadata. + +**In SQL hooks** (`@@CTX:` syntax): + +```sql +post_hooks [inline_sql('GRANT SELECT ON @@CTX:destination.qualified TO analyst_role')], +``` + +Context components can appear directly beside qualification dots. SQLBuild resolves the longest +available context key before treating the dot as SQL punctuation: + +```sql +CREATE FUNCTION @@CTX:destination.database.@@CTX:destination.schema.reconstruct_book() +RETURNS INTEGER +LANGUAGE SQL +AS 'SELECT 1' +``` + +**In TOML/YAML config values** (`${CTX:...}` syntax): + +```toml +[targets.prod] +schema = "${CTX:destination.schema}" +``` + +Available context variables: + +| Variable | Value | +|----------|-------| +| `destination.qualified` | Fully qualified destination relation name | +| `destination.schema` | Destination schema | +| `destination.database` | Destination database | +| `destination.table` | Destination relation name | +| `model.name` | Model name | +| `model.database` | Model database | +| `model.schema` | Model schema | +| `model.alias` | Model alias | +| `run.target` | Active target name | +| `run.id` | Current run ID | + +**In macros**, the `MacroContext` object is passed as the first argument when a macro function accepts a `ctx` parameter: + +```python +def timestamp_trunc(ctx, grain: str, expr: str) -> str: + if ctx.adapter_name == "bigquery": + return f"TIMESTAMP_TRUNC({expr}, {grain.upper()})" + return f"DATE_TRUNC('{grain}', {expr})" +``` + +The macro context provides `adapter_name`, `sql_analysis_enabled`, `target_name`, and `vars`. + +## Deferred placeholders + +Custom materializations can define runtime placeholders using `@@@name` syntax. These are preserved through compilation and resolved by the materialization at execution time: + +```sql +WHERE CAST(ordered_at AS DATE) >= CAST(@@@partition_start AS DATE) + AND CAST(ordered_at AS DATE) < CAST(@@@partition_end AS DATE) +``` + +## Audit parameters + +Generic audit SQL uses `@name` (single `@`, no parentheses) for raw SQL placeholders and `@'name'` for escaped SQL-literal placeholders. These are resolved when the audit is attached: + +```sql +SELECT @column +FROM @relation +WHERE @column IS NULL +``` + +Use the quoted form for values rather than SQL identifiers or expressions: + +```sql +WHERE status = @'expected_status' +``` + +This is distinct from `@@name` (project variables) and `@macro()` (macro calls), so there is no ambiguity. See [Audits](/docs/concepts/audits) for details on generic audit parameters. + +## Named SQL hook parameters + +Reusable hooks under `hooks/sql/` use `@name` for raw SQL substitution and `@'name'` for escaped SQL-literal substitution. These parameters are supplied by `sql("name", args...)` and resolved before the rest of the hook SQL is compiled. See [SQL hooks](/docs/concepts/models/hooks/sql#sql-hook-arguments) for value rendering and validation rules. + +## Compilation order + +SQLBuild processes authored SQL in this order: + +1. **Config templates** (`${CTX:...}`, `${ENV:...}`) in TOML/YAML config values are resolved during config compilation +2. **Named SQL hook arguments** (`@name` and `@'name'`) are substituted into reusable hook bodies +3. **Project variables** (`@@name`), **environment variables** (`@@ENV:NAME`), and **context variables** (`@@CTX:name` in SQL hooks) are substituted +4. **Enum and constant references** are validated, normalized, and expanded through the active adapter +5. **Macro calls** (`@name(args)`) are expanded +6. **SQL analysis validation** runs against the fully expanded SQL + +This means: +- Config templates resolve first, before any SQL processing +- Named SQL hook arguments can contain `@@CTX:...`, `@@ENV:...`, project-variable, declaration, or macro text; model context values describe the invoking model, while declarations and macros in the resulting named hook body resolve from the hook definition path +- Macros see already-substituted variable values in the SQL +- `@@CTX:destination.qualified` in SQL hooks sees the final target-overridden destination name because hooks are expanded after destination naming is fully resolved +- SQL analysis validates the final expanded SQL, catching syntax errors from both vars and macros +- Python hook arguments are not SQL and remain unexpanded diff --git a/website/src/content/docs/docs/concepts/macros.mdx b/website/src/content/docs/docs/concepts/macros.mdx new file mode 100644 index 0000000000..b18be33927 --- /dev/null +++ b/website/src/content/docs/docs/concepts/macros.mdx @@ -0,0 +1,175 @@ +--- +title: "Writing Macros" +description: "Write Python functions that generate reusable SQL fragments at compile time." +--- + +import Card from '../../../../components/mintlify/Card.astro'; +import CardGroup from '../../../../components/mintlify/CardGroup.astro'; + +Macros are Python functions that generate SQL during compilation. They provide reusable, +parameterized SQL without introducing a separate template language. + +## Create a macro + +Put project-wide macros in Python files under the top-level `macros/` directory: + +```text +my_project/ +├── macros/ +│ ├── currency.py +│ └── test_helpers.py +├── models/ +└── sqlbuild_project.toml +``` + +Every public function defined by a macro file becomes callable from SQL: + +```python +# macros/currency.py +def cents_to_dollars(expression: str) -> str: + """Convert a cents expression to dollars.""" + return f"ROUND(CAST({expression} AS DOUBLE) / 100, 2)" +``` + +## Call a macro from SQL + +Use `@macro_name(...)` in a model query: + +```sql +MODEL ( + materialized table, + tags [marts], +); + +SELECT + order_id, + @cents_to_dollars("amount_cents") AS amount_dollars +FROM __ref("stg_orders") +``` + +During compilation, SQLBuild replaces the call with the function's returned SQL: + +```sql +ROUND(CAST(amount_cents AS DOUBLE) / 100, 2) +``` + +A macro used directly in SQL must return a string. + +## Arguments + +Macro calls accept Python literal values: + +- Strings: `"value"` or `'value'` +- Numbers: `42`, `3.14`, `-1` +- Booleans: `True`, `False` +- Lists: `[1, 2, 3]` +- Dictionaries: `{"key": "value"}` +- `None` +- The result of another macro call + +Positional and keyword arguments are supported: + +```sql +@mock_orders(count=5, status="completed") +``` + +Use quoted strings when passing SQL expressions such as column names. The macro decides how to +place that text into its returned SQL. + +## Keep implementation details private + +Only public functions owned by the file are exported as macros. Prefix helpers, constants, classes, +and type aliases with `_`: + +```python +# macros/orders.py +_DEFAULT_STATUS = "completed" + + +def _status_filter(status: str) -> str: + return f"status = '{status}'" + + +def completed_orders() -> str: + return _status_filter(_DEFAULT_STATUS) +``` + +Here, SQL may call `@completed_orders()`. `_status_filter` and `_DEFAULT_STATUS` remain ordinary +Python implementation details. + +Imported functions are not re-exported as new macros from the importing file. + +## Use macros in tests + +Tests are SQL, so they can use macros as reusable fixture generators: + +```python +# macros/test_helpers.py +def mock_orders(count: int = 1) -> str: + rows = [ + f"SELECT {i} AS id, {i * 100} AS customer_id, 'completed' AS status" + for i in range(1, count + 1) + ] + return " UNION ALL ".join(rows) +``` + +```sql +TEST(); + +WITH +__source__raw__orders AS ( + @mock_orders(3) +), +__expected__stg_orders AS ( + SELECT 1 AS order_id, 100 AS customer_id, 'completed' AS status + UNION ALL + SELECT 2 AS order_id, 200 AS customer_id, 'completed' AS status + UNION ALL + SELECT 3 AS order_id, 300 AS customer_id, 'completed' AS status +) +SELECT 1 +``` + +## Use macros in hooks + +Macros work inside inline and named SQL hooks: + +```python +# macros/permissions.py +def grant_target(target: str) -> str: + return f"GRANT SELECT ON {target} TO analyst_role" +``` + +```sql +MODEL ( + post_hooks [inline_sql('@grant_target(@@CTX:destination.qualified)')], +); + +SELECT 1 AS id +``` + +See [SQL Hooks](/docs/concepts/models/hooks/sql) for hook lifecycle and context syntax. + +## Where macros work + +Macros are supported in: + +- Model query SQL +- Inline and named SQL hooks +- Unit tests and scenarios +- Standalone audit SQL +- SQL functions and supported inline source expressions + +Macros are not accepted in ordinary `MODEL()` configuration fields. SQL hook entries are the +exception because their contents are SQL. + +## Next steps + + + + Compose macros through Python imports and use adapter or target context. + + + Limit a macro to one folder, or to that folder and its child folders. + + diff --git a/website/src/content/docs/docs/concepts/macros/composition-and-context.mdx b/website/src/content/docs/docs/concepts/macros/composition-and-context.mdx new file mode 100644 index 0000000000..9d947485f3 --- /dev/null +++ b/website/src/content/docs/docs/concepts/macros/composition-and-context.mdx @@ -0,0 +1,133 @@ +--- +title: "Composition and Context" +description: "Compose macros through Python, use compile context, and understand scoped imports." +--- + +Macros are Python functions, so reusable macros should compose through ordinary Python calls. A +macro returns final SQL; SQLBuild does not treat its output as another layer of macro source. + +## Compose helpers in one file + +Use underscore-prefixed helpers for implementation details that should not be callable from SQL: + +```python +# macros/currency.py +def _divide_by_100(expression: str) -> str: + return f"({expression} / 100.0)" + + +def cents_to_dollars(expression: str) -> str: + return f"ROUND({_divide_by_100(expression)}, 2)" +``` + +Only `cents_to_dollars` is exported as a SQLBuild macro. + +Public macros in the same file are also ordinary Python functions and may call one another: + +```python +def add_tax(expression: str) -> str: + return f"({expression} * 1.2)" + + +def round_money(expression: str) -> str: + return f"ROUND({expression}, 2)" + + +def formatted_total(expression: str) -> str: + return round_money(add_tax(expression)) +``` + +## Compose macros from different files + +Import another project macro when it is visible from the importing macro file: + +```python +# macros/orders.py +from macros.currency import add_tax, round_money + + +def formatted_order_total(expression: str) -> str: + return round_money(add_tax(expression)) +``` + +SQLBuild records the imported macro files as dependencies. It rejects an import when the target +macro is outside the importing file's declaration scope or when imports form a cycle. + +Imported functions do not become duplicate exports from the importing file. In the example above, +`add_tax` and `round_money` retain their original identities; only `formatted_order_total` is newly +exported by `orders.py`. + +The same visibility direction applies to scoped macros: + +- A scoped macro may import a project-wide macro. +- A scoped macro may import a macro available from its own or an ancestor directory. +- A project-wide macro cannot import a narrower macro. +- A macro cannot import from a sibling or unrelated scope. + +See [Declarations and Scopes](/docs/concepts/declaration-scopes) when macros are stored under `macros/` +or `_macros/`. + +## Macro output is final SQL + +Do not return SQL containing another `@macro()` call: + +```python +# Invalid: creates another macro-expansion layer. +def formatted_order_total(expression: str) -> str: + return f"@round_money(@add_tax({expression!r}))" +``` + +SQLBuild rejects this output. Import and call the Python functions instead: + +```python +from macros.currency import add_tax, round_money + + +def formatted_order_total(expression: str) -> str: + return round_money(add_tax(expression)) +``` + +This keeps macro behavior readable in Python and ensures one expansion produces final SQL. + +## Nested calls written in SQL + +The SQL author may explicitly pass one macro's result to another: + +```sql +SELECT @round_money(@add_tax("subtotal")) AS order_total +``` + +SQLBuild evaluates `add_tax` first and passes its returned string to `round_money`. This is not a +second expansion of generated output: both calls are visible in the SQL source. + +Inner macros may return any Python value accepted by the outer macro. A macro used directly in SQL +must return a string. + +## Macro context + +When the first parameter is named `ctx`, SQLBuild passes a `MacroContext` with adapter, target, and +project-variable information: + +```python +# macros/datetime.py +def timestamp_trunc(ctx, grain: str, expression: str) -> str: + if ctx.adapter_name == "bigquery": + return f"TIMESTAMP_TRUNC({expression}, {grain.upper()})" + return f"DATE_TRUNC('{grain}', {expression})" +``` + +| Field | Description | +|-------|-------------| +| `adapter_name` | Active adapter, such as `duckdb` or `snowflake` | +| `sql_analysis_enabled` | Whether SQL analysis is enabled | +| `target_name` | Active target name, when selected | +| `vars` | Effective project variables after project, target, local, and CLI merging | + +```python +def schema_qualified(ctx, table: str) -> str: + schema = ctx.vars.get("schema_prefix", "public") + return f"{schema}.{table}" +``` + +Use context when generated SQL genuinely differs by adapter or target. Prefer ordinary parameters +for values that the SQL caller should choose explicitly. diff --git a/website/src/content/docs/docs/concepts/model-private-values.mdx b/website/src/content/docs/docs/concepts/model-private-values.mdx new file mode 100644 index 0000000000..886f2a1670 --- /dev/null +++ b/website/src/content/docs/docs/concepts/model-private-values.mdx @@ -0,0 +1,68 @@ +--- +title: "Model-Private Values" +description: "Keep enums and constants inside one model when no other resource should use them." +--- + +Put an enum or constant directly in `MODEL()` when it belongs to one model and should not enter the +project-wide namespace. + +```sql +MODEL ( + enums ( + _state [OPEN, CLOSED], + ), + constants ( + _min_items 2, + _supported_countries ["GB", "FR", "DE"], + ), +); + +SELECT * +FROM order_batches +WHERE state = @enum("_state").OPEN + AND item_count > @const("_min_items") + AND country_code IN @const("_supported_countries") +``` + +## Where private values work + +A model-private value is available in: + +- The owning model's query +- Inline SQL hooks written in that model + +It is not available in: + +- Another model +- A child or sibling model directory +- A unit test or scenario +- A named SQL hook stored under `hooks/sql/` + +Private names begin with exactly one `_`. Names beginning with `__` are reserved for SQLBuild. +Because the model owns the name, different models may each define `_state` without creating a +collision. + +## Explicit types and rendering + +Use `constant(...)` when a private constant needs an exact type or rendering choice: + +```sql +MODEL ( + constants ( + _usd_rate constant( + type decimal, + value "2.4700", + ), + _supported_countries constant( + value ["GB", "FR", "HK"], + render_as array, + ), + ), +); +``` + +## When a value becomes shared + +Move the declaration out of `MODEL()` when another resource needs it. Remove the `_` prefix and put +it in the narrowest suitable enum or constant directory. See +[Declarations and Scopes](/docs/concepts/declaration-scopes) for those advanced placement options. diff --git a/website/src/content/docs/docs/concepts/models.mdx b/website/src/content/docs/docs/concepts/models.mdx new file mode 100644 index 0000000000..4e8c952c2c --- /dev/null +++ b/website/src/content/docs/docs/concepts/models.mdx @@ -0,0 +1,54 @@ +--- +title: "Overview" +description: "SQL model anatomy, references, dependencies, and the model documentation guide." +--- + +A model is a SQL file that defines one transformation step and produces a table or view in the warehouse. + +## Model anatomy + +Every model starts with a `MODEL()` header followed by its query: + +```sql +MODEL ( + materialized table, + tags [marts], + description "Order fact table", + columns ( + order_id (type INTEGER, audits [not_null]), + ), +); + +SELECT + o.order_id, + o.customer_id, + p.amount_cents +FROM __ref("stg_orders") o +JOIN __ref("stg_payments") p USING (order_id) +``` + +The header controls how SQLBuild builds, validates, and documents the model. The query remains ordinary SQL apart from SQLBuild reference and macro calls. + +## References + +| Reference | Syntax | Resolves to | +|-----------|--------|-------------| +| Model | `__ref("name")` | Another model | +| Seed | `__seed("name")` | A seed CSV table | +| Source | `__source("name")` | An external source | +| Scalar UDF | `__udf("name")` | A user-defined function | + +SQLBuild discovers the dependency graph from these calls and orders selected work topologically. Among selected models, upstream models run before downstream dependents. An unselected upstream is read from its existing warehouse relation; use an upstream-expanding selector such as `+fact_orders` when it should also be built. Seeds use `__seed()`, not `__ref()`. + +See [Functions](/docs/concepts/functions) for scalar UDF and table-function references. + +## Model guide + +- [Materializations](/docs/concepts/models/materializations): views, tables, incrementals, snapshots, and custom materializations. +- [Schemas](/docs/concepts/models/schemas): inline columns, reusable schemas, inheritance, audits, and model-local extensions. +- [Type Enforcement](/docs/concepts/models/type-enforcement): static type checks and runtime cast behavior by materialization. +- [Contracts](/docs/concepts/models/contracts): exact and open output validation, runtime guarantees, nullability, and enum-backed columns. +- [Hooks](/docs/concepts/models/hooks): SQL and Python lifecycle hooks. +- [Configuration](/docs/concepts/models/configuration): `MODEL()` field reference and SQL-validation controls. + +For deeper execution behavior, see [Incremental](/docs/concepts/incremental), [Snapshots](/docs/concepts/snapshots), and [Audits](/docs/concepts/audits). diff --git a/website/src/content/docs/docs/concepts/models/configuration.mdx b/website/src/content/docs/docs/concepts/models/configuration.mdx new file mode 100644 index 0000000000..4f6bd23352 --- /dev/null +++ b/website/src/content/docs/docs/concepts/models/configuration.mdx @@ -0,0 +1,111 @@ +--- +title: "Configuration" +description: "MODEL() header fields and SQL-analysis controls." +--- + +## Common fields + +| Field | Description | +|-------|-------------| +| `materialized` | `view`, `table`, `incremental`, `snapshot`, or a custom materialization name | +| `tags` | Tags used by selectors | +| `description` | Human-readable model description | +| `columns` | Model-local column declarations or inherited-column audit augmentation | +| `model_schema` | Reusable column schema name | +| `audits` | Model-level audit instances | +| `enums` | Model-local enum declarations; names must begin with `_` | +| `constants` | Model-local scalar, list, set, or object constants; names must begin with `_`. Use `constant(...)` for an explicit decimal type or collection rendering override. | +| `schema` | Destination warehouse schema override | +| `database` | Destination database override | +| `alias` | Destination relation-name override | +| `pre_hooks` | Ordered `inline_sql(...)`, `sql("name", ...)`, or `python("name", ...)` hooks before materialization | +| `post_hooks` | Ordered `inline_sql(...)`, `sql("name", ...)`, or `python("name", ...)` hooks after materialization | +| `enabled` | Set to `false` to disable the model | +| `contract` | `none` for an open statically checked declaration, or `enforced` for an exact declaration | +| `sql_analysis` | Per-model SQL-analysis override | + +## SQL analysis + +SQL analysis has project, invocation, and model gates: + +1. `settings.sql_analysis` must be enabled. +2. `--no-sql-analysis` must not be present. +3. `MODEL (sql_analysis false)` can disable analysis for that model. If omitted, the project setting + is used. + +A model cannot re-enable analysis when either broader gate disables it. The older `sql_validation` +and `--no-sql-validation` spellings remain compatibility aliases; use `sql_analysis` for new +configuration. + +## Table fields + +| Field | Description | +|-------|-------------| +| `run_despite_unchanged` | Table-only change-aware policy. `always` rebuilds whenever selected. A duration such as `30d`, `12h`, or `90m` rebuilds while the newest timestamp-based upstream source observation is within that age; it is not a periodic schedule. | + +Table promotion mode is a project setting rather than a `MODEL()` field. Staged promotion is the default. Immediate promotion is incompatible with model type enforcement and exact contracts; see [Materializations](/docs/concepts/models/materializations#table). + +## Incremental fields + +| Field | Description | +|-------|-------------| +| `incremental_strategy` | `append`, `delete_insert`, or `merge` | +| `cursor` | Output column used to track incremental position | +| `cursor_type` | `timestamp` or `integer` | +| `cursor_grain` | Timestamp grain such as `second`, `hour`, or `day` | +| `cursor_start` | Lower cursor bound | +| `cursor_inputs` | Upstream names mapped to cursor columns | +| `unique_key` | Merge or delete/insert matching columns | +| `incremental_mode` | Set to `microbatch` for batched execution | +| `microbatch_strategy` | Required in microbatch mode: `watermark` or `rolling_window` | +| `cursor_watermark_mode` | Watermark strategy policy: `all` or `any` | +| `batch_size` | Timestamp duration string such as `1d` or `1h`; use a numeric string such as `"1000"` for an integer cursor | +| `batch_concurrency` | Concurrent batch workers; values above `1` require `delete_insert` and the project concurrency gate | +| `microbatch_limit` | Nested `max_batches` and `action` policy for watermark execution | +| `lookback` | Backward replay extension | +| `append_cursor_inclusive` | Include (`true`, default) or exclude (`false`) the current append-cursor boundary | +| `merge_exclude_columns` | Columns left unchanged by matched-row merge updates | +| `full_refresh` | Optional model execution override: `false` always runs incrementally, `true` always full-refreshes, and omission follows the command | +| `on_schema_change` | `append_new_columns`, `sync_all_columns`, `ignore`, or `fail` | +| `replay_on_change` | `forward`, `full`, or `bounded-` | +| `migrate_from` | Old model name (or `schema.name`) whose data this model takes over; see [Model migrations](/docs/concepts/models/migrations). Also valid on snapshot models | +| `migrate_force` | `true` replaces a destination that already has its own build history during a migration | + +See [Incremental](/docs/concepts/incremental) for full semantics. + +Current compatibility behavior treats an unrecognized `on_schema_change` value as the default `append_new_columns` policy and an unrecognized `replay_on_change` value as `forward`. Use the documented values exactly; future validation may reject unknown values instead of applying these fallbacks. + +## Snapshot fields + +| Field | Description | +|-------|-------------| +| `unique_key` | Columns identifying one logical record | +| `snapshot_strategy` | `timestamp` or `check` | +| `updated_at` | Source update timestamp for the timestamp strategy | +| `check_columns` | Columns compared by the check strategy, or `[*]` | +| `observed_at` | Observation timestamp for historical inputs | +| `historical_input` | `snapshot` or `changes` | +| `initial_valid_from` | Initial validity policy for first-seen rows | +| `invalidate_hard_deletes` | Close records that disappear from current-state input | +| `valid_from_column` | Override the generated valid-from column name | +| `valid_to_column` | Override the generated valid-to column name | +| `snapshot_full_refresh` | Snapshot full-refresh safety policy | +| `snapshot_schema_change` | Snapshot schema-change policy | + +See [Snapshots](/docs/concepts/snapshots) for strategy combinations, defaults, and safety behavior. + +## Custom materialization fields + +| Field | Description | +|-------|-------------| +| `config` | Arbitrary values passed to `ctx.config` | +| `placeholders` | Defaults for runtime `@@@placeholder` tokens | + +## Diff fields + +| Field | Description | +|-------|-------------| +| `row_diff_exclude_columns` | Columns excluded from row-level comparison | +| `row_diff_tolerances` | Numeric comparison tolerances | +| `row_diff_sample_rows` | Deterministic unique-key sample size; `0` disables inherited sampling | +| `row_diff_sample_seed` | Integer seed used for deterministic key hashing | diff --git a/website/src/content/docs/docs/concepts/models/contracts.mdx b/website/src/content/docs/docs/concepts/models/contracts.mdx new file mode 100644 index 0000000000..9a738a7620 --- /dev/null +++ b/website/src/content/docs/docs/concepts/models/contracts.mdx @@ -0,0 +1,159 @@ +--- +title: "Contracts" +description: "Validate required or exact model output schemas." +--- + +A model's declared columns provide output metadata, type enforcement, and column audit attachment. The project `column_contract_mode` and model `contract` policy determine whether those declarations also define a statically validated output shape. + +| Model declaration | Behavior | +|-------------------|----------| +| Unspecified | Follow `settings.column_contract_mode`: `implicit` validates declared columns as an open shape; `explicit` treats them as metadata and audit attachment only. | +| `none` | Do not activate declared-shape or nullability contract validation for this model. Explicit type enforcement remains active. | +| `enforced` | Treat declared columns as the complete authoritative output shape and enable runtime exact-schema checks on supported materializations. | + +The project default is `column_contract_mode = "implicit"`, which preserves SQLBuild's original behavior. Projects that use column declarations primarily for metadata and audits, including migrations from dbt, can opt into explicit contracts: + +```toml +[settings] +column_contract_mode = "explicit" +``` + +Under explicit mode, a model must declare `contract enforced` to activate shape validation. A model-level `contract enforced` or `contract none` always overrides the project mode. + +```sql +MODEL ( + materialized table, + contract enforced, + columns ( + order_id (type INTEGER, nullable false), + customer_id (type INTEGER, nullable false), + amount_cents (type INTEGER), + status (type VARCHAR), + ), +); +``` + +## Validation + +### Compile time + +When declared-shape validation is active and SQL analysis can infer the model's output: + +- A missing declared column fails. +- A proven type mismatch fails when type enforcement or an exact contract is active. +- A declared non-null column fails when its expression is proven nullable. +- Additional inferred columns fail only for `contract enforced`. + +If SQLBuild cannot infer the output shape, these static shape checks cannot run. An enforced contract additionally requires a non-empty column declaration and adds runtime requirements on supported materializations. + +Type enforcement remains independent. Typed columns continue to receive static type checks and supported runtime casts under explicit mode and `contract none`. + +Configuration fields that reference output columns, including `unique_key`, `cursor`, `updated_at`, and `check_columns`, are checked against an enforced declaration. + +### Runtime + +For supported materialization paths, `contract enforced` inspects the staged relation and rejects: + +- Missing declared columns. +- Additional undeclared columns. +- Warehouse types that differ from declared types. + +Runtime contract validation does not scan data for nulls and does not inspect warehouse nullability metadata. `nullable false` participates in static nullability analysis; add a `not_null` audit when null values must be checked at runtime. + +Runtime exact-schema validation currently runs for: + +- Staged full-table builds. +- Non-microbatch incremental deltas. +- Snapshot deltas. + +Views and custom materializations rely on compile-time contract analysis. Microbatch incrementals currently apply type enforcement and audits but do not perform the framework runtime exact-schema validation step. + +Staged table validation happens before promotion, so a failure leaves the existing destination untouched. Immediate table promotion is incompatible with `contract enforced` because SQLBuild cannot validate output before replacing the destination. + +## Type enforcement + +Declaring a model column type enables type enforcement automatically, independently of the contract policy. Type enforcement controls static type compatibility and runtime casts on supported table and incremental paths; contracts control output shape. See [Type Enforcement](/docs/concepts/models/type-enforcement) for the materialization matrix. + +## Reusable schemas + +Contracts apply to the effective declaration, not only the reusable base: + +```sql +MODEL ( + model_schema order, + columns ( + ingestion_batch_id (type VARCHAR, nullable false), + ), + contract enforced, +); +``` + +With `contract enforced`, this requires exactly the resolved `order` columns plus `ingestion_batch_id`. With `contract none`, the resolved columns remain metadata, audit attachment points, and inputs to explicit type enforcement without activating shape validation. See [Schemas](/docs/concepts/models/schemas). + +## Enum columns + +A column may use a declared enum as a portable logical domain type: + +```sql +MODEL ( + contract enforced, + columns ( + fulfillment_method (type fulfillment_method), + ), +); +``` + +This does not require, create, or reference a warehouse-native enum type. SQLBuild resolves string-valued enums to `VARCHAR` and integer-valued enums to `INTEGER`. Under `contract enforced`, it also generates an `accepted_values` audit for the declared members. Audit severity and timing follow normal audit configuration and materialization behavior. With the default error severity, it gates staged-table promotion and pre-DML delta paths; views and custom materializations may already have changed their relation when the audit runs. + +Enum member references such as `@enum("fulfillment_method").DELIVERY` are a separate feature that render one validated SQL literal. See [Enum Model Contracts](/docs/concepts/enums/model-contracts) for the complete distinction and lowering behavior. + +## Adopting an existing schema + +Ordinary `sqb compile` validates authored contracts offline. To compare declarations with existing +warehouse relations, use the explicitly online, read-only contract commands: + +```bash +sqb contract diff --from prod --select tag:commerce +sqb contract generate --from prod --select tag:commerce +``` + +The target named by `--from` supplies the database and schema namespace to inspect. SQLBuild uses +the active project connection and never resolves a second set of origin credentials. + +### Preview and write + +`contract diff` returns exit code `1` when declarations and physical relations disagree. +`contract generate` prints proposed declaration changes without changing files: + +```bash +# Fill missing types and append physical columns missing from code +sqb contract generate --from prod --select tag:commerce --write + +# Replace conflicting types/names and remove declarations absent physically +sqb contract generate --from prod --select tag:commerce --write --overwrite +``` + +Contract commands never mutate warehouse relations, lifecycle state, or fingerprints. + +### Safety rules + +- Additive generation preserves existing types on conflict, repository-only columns, comments, + descriptions, audits, nullability, enums, tags, freshness, and loader configuration. +- `--overwrite` is an explicit repository replacement policy, not a warehouse write. +- Generation does not add `contract enforced`; activation remains an authored decision. +- A shared `SCHEMA()` is not changed from one model's evidence. Ownership conflicts are reported. +- Writes are atomic and SQLBuild recompiles the project, restoring prior contents if validation + fails. +- Sources are first-class selectable resources, for example `--select source:raw_orders`. + +Enforced upstream contracts are authoritative compile-time interfaces. When input evidence is +complete, missing or ambiguous columns fail in projections, joins, filters, grouping, windows, and +ordering. Partial or opaque schemas remain open; SQLBuild does not turn missing metadata into a +false error. Use `sqb lineage --include-uses --format json` to inspect direct +non-projection uses such as `join_on`, `where`, `group_by`, and `window_order_by`. + +## Related policies + +The project default is implicit open-shape validation for models that omit `contract`. A repository can select explicit opt-in contracts with `settings.column_contract_mode = "explicit"` or configure a custom [Rule](/docs/concepts/rules/custom-rules/overview) that requires enforced contracts for selected model families. + +Contracts also constrain schema-change behavior. For example, `snapshot_schema_change append_new_columns` is incompatible with `contract enforced` because an unannounced appended column would violate the exact declaration. diff --git a/website/src/content/docs/docs/concepts/models/hooks.mdx b/website/src/content/docs/docs/concepts/models/hooks.mdx new file mode 100644 index 0000000000..b2dcbd0a98 --- /dev/null +++ b/website/src/content/docs/docs/concepts/models/hooks.mdx @@ -0,0 +1,107 @@ +--- +title: "Hooks" +description: "Run SQL or Python lifecycle hooks around model materialization." +--- + +Pre-hooks and post-hooks run before and after model materialization. Use SQL hooks for warehouse statements and Python hooks for runtime control flow, providers, queries, and skip decisions. + +## Project layout + +SQL and Python hooks live in language-specific directories: + +```text +my_project/ + models/ + marts/ + orders.sql + hooks/ + sql/ + permissions/ + grant_access.sql + record_access.sql + python/ + notifications.py +``` + +Directories below `hooks/sql/` and `hooks/python/` are organizational. They do not namespace resource names: `hooks/sql/permissions/grant_access.sql` is still invoked as `sql("grant_access")`. + +## Choose a hook type + + + + + + + + + + +Hook lists use three explicit entry types: + +| Entry | Definition | Use | +|-------|------------|-----| +| [`inline_sql("...")`](/docs/concepts/models/hooks/sql#inline-sql-hooks) | SQL written directly in the model header | Short, model-specific SQL | +| [`sql("name", args...)`](/docs/concepts/models/hooks/sql) | A reusable resource under `hooks/sql/` | Shared, parameterized SQL | +| [`python("name", args...)`](/docs/concepts/models/hooks/python) | A decorated function under `hooks/python/` | Control flow, providers, queries, or skips | + +Bare SQL strings are not hook entries. The singular `pre_hook` and `post_hook` fields are also invalid; use `pre_hooks` and `post_hooks` with one of the three typed forms above. `sql("...")` always means a named SQL hook and never falls back to inline SQL. + +## Configure model hooks + +Models may mix SQL and Python entries in one ordered hook list: + +```sql +MODEL ( + materialized table, + pre_hooks [ + inline_sql("INSERT INTO audit.build_log VALUES ('starting')"), + ], + post_hooks [ + sql( + "grant_access", + relation: "@@CTX:destination.qualified", + role: "analyst_role", + ), + python( + "notify_complete", + channel: "#data-builds", + ), + ], +); + +SELECT 1 AS id +``` + +Entries execute in the order authored, including when SQL and Python entries are mixed. + + + +## Lifecycle and failure timing + +Pre-hooks run before materialization. A failed pre-hook prevents materialization, and a Python pre-hook that returns `ctx.skip(...)` stops the remaining pre-hooks and prevents the model from running. + +Post-hooks run after warehouse mutation and audits. They are not promotion gates: a failed post-hook marks the model run as failed, but the relation has already been created, promoted, or incrementally updated. Put logic that must prevent materialization in a pre-hook, contract, pre-promotion audit, or the materialization itself. + +A Python post-hook that returns `ctx.skip(...)` stops the remaining post-hooks and changes the reported model result. `mode="hard"` blocks downstream nodes. A soft skip does not automatically block every downstream node; scheduler propagation also depends on the other upstream results. + +## Names and identity + +SQL and Python hook names share the project-wide resource namespace with models, sources, seeds, functions, loaders, tasks, assets, checks, and providers. Any collision fails discovery, including two same-stem SQL hook files in different directories or a SQL and Python hook with the same name. + +The ordered hook list participates in each model's version identity: + +- Inline and named SQL hooks contribute their fully rendered statements. Named hooks also retain their resource name, definition, arguments, description, and source path in identity metadata. +- Python hooks contribute the invocation name, configured arguments, and a version hash derived from the decorated function, its transitive first-party dependencies, and decorator configuration. +- Changing a hook body, arguments, or order changes the consuming model's identity and makes it stale for change-aware planning. +- Executed Python hooks record their own hook fingerprints after successful completion or an explicit skip. + +Named SQL hooks compile into their consuming models rather than becoming independently scheduled nodes. Python hooks also run as part of their model's lifecycle phase rather than as independently selected Python nodes. + +## Diagnostics + +Discovery and compilation fail early for malformed definitions and invocations. Diagnostics include the resource path and, for model entries, the model name and indexed label such as `post_hooks[1] sql("grant_access")`. + +See the language-specific pages for definition, argument, validation, and runtime diagnostics: + +- [SQL hooks](/docs/concepts/models/hooks/sql) +- [Python hooks](/docs/concepts/models/hooks/python) diff --git a/website/src/content/docs/docs/concepts/models/hooks/python.mdx b/website/src/content/docs/docs/concepts/models/hooks/python.mdx new file mode 100644 index 0000000000..7e30ca0713 --- /dev/null +++ b/website/src/content/docs/docs/concepts/models/hooks/python.mdx @@ -0,0 +1,104 @@ +--- +title: "Python Hooks" +description: "Define Python lifecycle hooks with runtime context, providers, SQL access, and skips." +--- + +Python hooks run model lifecycle logic that needs Python control flow, providers, warehouse queries, logging, or explicit skip decisions. + +For shared lifecycle ordering, failure timing, naming, and identity rules, see the [Hooks overview](/docs/concepts/models/hooks). + +## Define a Python hook + +SQLBuild discovers decorated functions recursively from `.py` files under `hooks/python/`: + +**`hooks/python/notifications.py`** + +```python +from sqlbuild.hooks import hook + + +@hook +def notify_complete(ctx, channel="#data-builds"): + ctx.log( + f"Notify {channel}: " + f"{ctx.model_name} completed during {ctx.phase}" + ) +``` + +By default, the hook name is the function name. The decorator accepts optional `name` and `description` arguments. A Python file may define multiple decorated hooks. + +Files named `__init__.py` or beginning with `_` are skipped. Imported decorated functions are not registered again from the importing module. + +## Invoke a Python hook + +Reference the hook by name and optionally pass keyword arguments: + +```sql +MODEL ( + materialized table, + post_hooks [ + python( + "notify_complete", + channel: "#model-alerts", + ), + ], +); + +SELECT 1 AS id +``` + +Python hook arguments are ordinary configuration values and are not SQL-expanded. An argument containing `@@CTX:...` or `@macro()` reaches the function unchanged. + +## Signature validation + +Unknown hooks, unknown keyword arguments, missing required arguments, required positional-only arguments, and arguments that conflict with context or provider injection fail compilation. A function with `**kwargs` can accept additional configured arguments. + +Python hooks must return `None` or `ctx.skip(...)`. Any other return value fails execution. + +## Hook context + +Python hooks declare a `HookContext` parameter named `ctx`, `context`, `_ctx`, or `hook_context`. It need not be the first parameter when providers or configured arguments are also present: + +| Field | Description | +|-------|-------------| +| `ctx.model_name` | Model being built | +| `ctx.phase` | `pre_hooks` or `post_hooks` | +| `ctx.hook_name` | Invoked hook name | +| `ctx.hook_index` | Zero-based position in the authored hook list | +| `ctx.run_id` | Current run ID | +| `ctx.target` | Active target | +| `ctx.vars` | Effective project variables | +| `ctx.destination` | Destination relation metadata | +| `ctx.adapter_name` | Active adapter name | +| `ctx.adapter` | Adapter instance | +| `ctx.connection` | Live connection | +| `ctx.execute_sql(sql)` | Execute SQL | +| `ctx.query(sql)` | Execute SQL and return rows | +| `ctx.log(message)` | Write run output | +| `ctx.skip(reason="...", mode="soft")` | Return a soft or hard skip result; arguments are keyword-only | +| `ctx.providers` | Access discovered [providers](/docs/concepts/python-nodes/providers) | + +## Providers + +Providers may be injected into Python hook parameters by name or accessed through `ctx.providers`. They are resolved lazily using the same lifecycle as loaders, tasks, assets, and checks. SQL hooks cannot use providers because they are compiled SQL statements rather than Python callables. + +```python +from sqlbuild.hooks import hook + + +@hook +def notify_complete(ctx, slack_notifier): + slack_notifier.send( + f"Model {ctx.model_name} built successfully" + ) +``` + +## Skips and failures + +Returning `ctx.skip(...)` stops the remaining hooks in the current phase. Runtime exceptions fail that lifecycle phase. See [Lifecycle and failure timing](/docs/concepts/models/hooks#lifecycle-and-failure-timing) for the effects of pre-hook and post-hook skips, soft and hard modes, and failures after warehouse mutation. + +## Identity and diagnostics + +Python hook invocations and version hashes participate in model identity as described in [Names and identity](/docs/concepts/models/hooks#names-and-identity). Executed hooks also record their own fingerprints after successful completion or an explicit skip. + +Compilation reports unknown hooks and invalid signatures with the model name and indexed invocation label, such as `post_hooks[1] python("notify_complete")`. Runtime output preserves the authored index and hook name. Exceptions and unsupported return values fail with the Python hook identity attached. diff --git a/website/src/content/docs/docs/concepts/models/hooks/sql.mdx b/website/src/content/docs/docs/concepts/models/hooks/sql.mdx new file mode 100644 index 0000000000..f4ab80d0f8 --- /dev/null +++ b/website/src/content/docs/docs/concepts/models/hooks/sql.mdx @@ -0,0 +1,165 @@ +--- +title: "SQL Hooks" +description: "Define, parameterize, compile, and invoke reusable or inline SQL lifecycle hooks." +--- + +SQL hooks submit one rendered SQL payload to the adapter before or after model materialization. Use a named SQL hook for reusable behavior and `inline_sql(...)` for short model-specific payloads. + +For shared lifecycle ordering, failure timing, naming, and identity rules, see the [Hooks overview](/docs/concepts/models/hooks). + +## Reusable SQL hooks + +SQLBuild discovers `.sql` files recursively under `hooks/sql/`. Each file defines exactly one hook and must start with a `HOOK(...)` header as its first non-whitespace content. + +**`hooks/sql/permissions/grant_access.sql`** + +```sql +HOOK ( + description "Grant a warehouse role access to the model relation" +); + +GRANT SELECT ON @relation TO @role +``` + +The hook name is always the filename stem, so this resource is invoked as `sql("grant_access", ...)`. Nested directories organize files but do not namespace names: `hooks/sql/admin/grant_access.sql` is still named `grant_access`. + +`HOOK()` accepts only an optional, non-empty `description`. It does not accept a `name`; rename the file to rename the hook. The content after the header must be non-empty and becomes the SQL payload for each invocation. + +Files beginning with `_` are skipped. All other `.sql` files under `hooks/sql/` are parsed as hook resources and must have a valid `HOOK()` header. + +## Invoke a named hook + +Pass the hook name and its arguments from a model's `pre_hooks` or `post_hooks` list: + +**`models/marts/orders.sql`** + +```sql +MODEL ( + materialized table, + post_hooks [ + sql( + "grant_access", + relation: "@@CTX:destination.qualified", + role: "analyst_role", + ), + ], +); + +SELECT 1 AS id +``` + +SQLBuild resource-header fields use native `key value` syntax, as in `TEST (mode macro)`, `SCENARIO (tags ["revenue"])`, `AUDIT (severity error)`, and `HOOK (description "...")`. The `relation: ...` and `role: ...` entries above intentionally retain `key: value` syntax because they are nested named arguments to the `sql(...)` hook call, not resource-header fields. The same distinction applies to named arguments passed to `python(...)`. + +## SQL hook arguments + +Named SQL hooks declare parameters by using them in the SQL body. Arguments are supplied as named values in `sql("name", args...)`: + +| Syntax | Behavior | +|--------|----------| +| `@name` | Raw substitution. Strings are inserted verbatim for relations, identifiers, keywords, or SQL fragments. | +| `@'name'` | SQL-literal substitution. Strings are single-quoted and embedded quotes are escaped. | + +**`hooks/sql/record_access.sql`** + +```sql +HOOK ( + description "Record access configuration" +); + +INSERT INTO audit.access_log (relation_name, role_name) +VALUES (@'relation', @'role') +``` + +```sql +MODEL ( + post_hooks [ + sql( + "record_access", + relation: "@@CTX:destination.qualified", + role: "O'Brien", + ), + ], +); + +SELECT 1 AS id +``` + +For both forms, booleans render as `TRUE` or `FALSE`, numbers render directly, and `null` renders as `NULL`. Lists render as comma-separated values, applying raw or quoted behavior to each item. For example, `@'roles'` with `roles: ["reader", "writer"]` renders as `'reader', 'writer'`. + +Every referenced argument is required and every supplied argument must be used. Missing arguments, unused arguments, and unsupported values such as maps fail compilation. Raw string arguments are not escaped; use `@'name'` for data values and reserve `@name` for trusted SQL structure. + +## Inline SQL hooks + +Use `inline_sql("...")` for SQL that is specific to one model: + +```sql +MODEL ( + materialized table, + post_hooks [ + inline_sql("GRANT SELECT ON @@CTX:destination.qualified TO analyst_role"), + ], +); + +SELECT 1 AS id +``` + +An inline hook accepts exactly one quoted SQL string and no additional arguments. That string becomes one adapter execution payload. + +## Compile-time context + +Both named and inline SQL hooks receive the invoking model's runtime context. They support: + +- Project variables such as `@@audit_schema` +- Environment variables such as `@@ENV:DEPLOY_ROLE` +- Hook context variables such as `@@CTX:destination.qualified` +- Enums and constants such as `@enum("role").ANALYST` and `@const("retention_days")` +- Python macros such as `@grant_target("@@CTX:destination.qualified")` + +For named hooks, SQLBuild first substitutes `@name` and `@'name'` arguments into the hook body. A supplied argument such as `relation: "@@CTX:destination.qualified"` therefore resolves to the invoking model's final target-overridden destination. + +An inline hook uses macros, constants, and enums available to its model file. A named hook uses those +available to its own file under `hooks/sql/`. See +[How Visibility Works](/docs/concepts/declaration-scopes/visibility#which-file-controls-visibility). + +`${...}` config-template syntax is not valid in SQL hooks. + +| Variable | Value | +|----------|-------| +| `@@CTX:destination.qualified` | Fully qualified destination relation | +| `@@CTX:destination.schema` | Destination schema | +| `@@CTX:destination.database` | Destination database | +| `@@CTX:destination.table` | Destination relation name | +| `@@CTX:model.name` | Model name | +| `@@CTX:model.database` | Model database | +| `@@CTX:model.schema` | Model schema | +| `@@CTX:model.alias` | Model alias | +| `@@CTX:run.target` | Active target name | +| `@@CTX:run.id` | Current run ID | + +Context components can be combined into qualified identifiers without whitespace or a Python +wrapper: + +```sql +HOOK (); + +CREATE FUNCTION @@CTX:destination.database.@@CTX:destination.schema.reconstruct_book() +RETURNS INTEGER +LANGUAGE SQL +AS 'SELECT 1' +``` + +## Validation and diagnostics + +SQLBuild strictly validates hook resource syntax, arguments, interpolation, macros, metadata, and non-empty payloads. It does not classify executable statement kinds or implement vendor SQL grammar. Each rendered hook payload is passed to the adapter in one `execute` call; whether a driver accepts multiple statements or client-side batch separators is adapter and warehouse behavior. Use separate hook entries when portable ordering between statements matters. + +After expansion, Polyglot validates the complete payload using the active adapter's analysis dialect when the model's effective `sql_analysis` setting is enabled, which it is by default. The `--no-sql-analysis` flag disables this analysis for the invocation. Polyglot does not understand every administrative or procedural command supported by every warehouse. If it cannot parse valid vendor-specific hook SQL, disable SQL analysis for that model or invocation and let the adapter and warehouse provide the authoritative result. SQLBuild does not compensate with keyword allowlists or handwritten parser fallbacks. + +Common errors include: + +- Missing or malformed leading `HOOK(...)` headers +- Unsupported header keys, empty descriptions, and missing SQL bodies +- Missing or unused arguments and unsupported argument values +- Unknown named hooks, unquoted hook names, and bare hook strings +- SQL rejected by Polyglot while optional SQL validation is enabled + +Definition errors point to the hook file. Invocation and argument errors also identify the consuming model entry, such as `post_hooks[1] sql("grant_access")`. Runtime output preserves the authored hook index and identifies named and inline SQL entries. diff --git a/website/src/content/docs/docs/concepts/models/materializations.mdx b/website/src/content/docs/docs/concepts/models/materializations.mdx new file mode 100644 index 0000000000..397857d106 --- /dev/null +++ b/website/src/content/docs/docs/concepts/models/materializations.mdx @@ -0,0 +1,97 @@ +--- +title: "Materializations" +description: "Choose how SQLBuild persists model output." +--- + +The `materialized` field selects how a model becomes a warehouse relation. + +## View + +Creates or replaces a database view on each build: + +```sql +MODEL (materialized view); + +SELECT id AS order_id, customer_id, status +FROM __source("raw__orders") +``` + +View audits run after the view has been replaced. A failing audit marks the build as failed but cannot preserve the previous view. Views use compile-time contract and type analysis; SQLBuild does not rewrite the view with runtime casts or run the staged runtime-contract step. + +## Table + +By default, creates a staging table, applies supported type and contract enforcement, runs blocking audits, and only then promotes it to the destination. A pre-promotion failure leaves the previous destination unchanged. + +```sql +MODEL (materialized table); + +SELECT customer_id, COUNT(*) AS total_orders +FROM __ref("stg_orders") +GROUP BY customer_id +``` + +Projects may opt into `settings.table_promotion_mode = "immediate"`. Immediate promotion replaces the destination before audits, so a failed audit does not restore the old table. Immediate promotion rejects models that require declared-type enforcement or `contract enforced`; use staged promotion for those guarantees. + +## Incremental + +On a normal incremental run, applies append, delete/insert, or merge DML from a staged delta. Delete/insert removes matching keys or cursor ranges before inserting replacement rows. Cursor configuration controls replay bounds and may split work into microbatches. + +```sql +MODEL ( + materialized incremental, + incremental_strategy delete_insert, + cursor activity_hour, + cursor_type timestamp, + cursor_grain hour, + unique_key [activity_hour], +); +``` + +For non-microbatch models, the first run, `--full-refresh`, and a full replay-on-change rebuild use the full-table path rather than incremental DML. Microbatch models retain batched execution: a full rebuild drops the existing target, creates its replacement from the first batch, and applies later batches with incremental DML. A failed full-refresh microbatch does not preserve the previous target. + +See [Incremental](/docs/concepts/incremental) for cursor semantics, replay, schema changes, and microbatch execution. + +## Snapshot + +Maintains historical row versions with SCD Type 2 semantics: + +```sql +MODEL ( + materialized snapshot, + unique_key [customer_id], + snapshot_strategy timestamp, + updated_at updated_at, +); + +SELECT customer_id, name, plan, status, updated_at +FROM __source("customers") +``` + +See [Snapshots](/docs/concepts/snapshots) for timestamp and check strategies, historical inputs, and full-refresh policies. + +## Custom + +A project-local Python materialization can manage specialized persistence with adapter access, schema findings, query-change state, declared columns, and `ctx.run_audits`. + +```sql +MODEL ( + materialized partition_tracked, + placeholders ( + partition_start "'2026-04-01'", + partition_end "'2026-04-05'", + ), + config ( + tracking_table partition_state, + partition_column order_date, + ), +); + +SELECT * +FROM __ref("stg_orders") +WHERE ordered_at >= @@@partition_start + AND ordered_at < @@@partition_end +``` + +The `config` block is passed to the Python function through `ctx.config`. Runtime-owned `@@@placeholder` values remain unresolved until materialization execution. + +The custom function owns staging, runtime type or contract enforcement, audit timing, promotion, and rollback. Framework final audits run after the function returns unless `MaterializationResult.audit_results` is populated. A custom materialization can call `ctx.run_audits` against a staging relation before applying changes and return those results through `MaterializationResult.audit_results`. diff --git a/website/src/content/docs/docs/concepts/models/migrations.mdx b/website/src/content/docs/docs/concepts/models/migrations.mdx new file mode 100644 index 0000000000..cee105fb62 --- /dev/null +++ b/website/src/content/docs/docs/concepts/models/migrations.mdx @@ -0,0 +1,85 @@ +--- +title: "Model migrations" +description: "Keep the history of an incremental or snapshot model when you rename it." +--- + +Renaming an incremental or snapshot model normally means rebuilding it from scratch under the new name, which loses any history the source data can no longer reproduce. A model migration moves the existing relation's data to the new name instead, then continues building incrementally. + +Migrations apply to incremental and snapshot models. + +## Declaring a migration + +Add `migrate_from` to the renamed model's header, naming the old model: + +```sql +MODEL ( + materialized incremental, + migrate_from stg_orders, + cursor order_date, + ... +); +``` + +Use `schema.name` when the old relation is in a different schema of the same database. + +On the next build, SQLBuild copies the old relation's data to the new name through a staging table (see [How a move runs](#how-a-move-runs)), records the move, and builds the model incrementally from the migrated state. There is no first-run rebuild, and no `replay_on_change` replay caused by the rename. The old relation is never modified or dropped. Once it is no longer part of the project, [janitor](/docs/cli/janitor) archives it like any other stale relation. + +After the move is recorded, `migrate_from` has no further effect, and SQLBuild tells you it can be removed. Keep it until every target that needs the move has built. + +## Decisions + +`sqb plan` shows the decision for every migration: + +| Decision | Meaning | +|----------|---------| +| `migrate` | The new relation doesn't exist. Move the old one's data to it. | +| `done` | The move is already recorded. Nothing to do. | +| `redo` | The new relation exists but has no build history and no recorded move, which is what an interrupted migration leaves behind. Move it again. | +| `superseded replace` | The new relation was itself the origin of the latest recorded move, for example after renaming a model back. Replace it with the newer data. | +| `forced replace` | The new relation has its own build history and `migrate_force true` is set. Replace it. | +| `conflict` | The new relation has its own build history. The build stops (`M103`) until you set `migrate_force true` or remove `migrate_from`. | +| `origin missing` | The old relation doesn't exist and no move was recorded. The build stops (`M102`). | + +The origin and destination must be compatible under the model's normal `on_schema_change` rules; otherwise the build stops (`M104`). + +`migrate_force true` only affects the `conflict` case and is safe to leave in the header. + +## Automatic discovery + +If a selected model has never been built, SQLBuild compares it with models that were removed from the project but whose relations still exist. When exactly one removed model has equivalent logic, it is migrated automatically. Equivalent means the same query and configuration, ignoring the model's own name, CTE and table alias names, comments, formatting, and storage-only settings. Renamed upstream models are matched first, so a renamed chain of models is migrated together. Removed models whose data was already moved on by a recorded migration are not candidates, so a model renamed several times matches its latest table. + +Renamed tables and views are matched the same way. They are still rebuilt under the new name, since they hold no history, but they keep their identity for change detection, so incremental models downstream of a renamed view are not replayed or rebuilt. These renames are recorded as `renamed` events. + +Automatic discovery never guesses. If a match is ambiguous, SQLBuild warns (`M107`) and builds from scratch; declare `migrate_from` to choose. An explicit `migrate_from` always wins. Automatic discovery covers renames within the project's schemas; use `migrate_from` to move a model to another schema. Relations last built before this feature carry no stored fingerprint and aren't matched automatically. + +## How a move runs + +A move never overwrites the destination directly: + +1. The old relation is cloned or copied into a new staging table named `_sqb_archive____migration_stage__`, with the destination's configured table type. +2. The staging table is checked: it must exist, have the expected columns and table type, and, for a physical copy, the same row count as the old relation. +3. The staging table is swapped in as the destination. If a destination already existed, for example after `superseded replace` or `forced replace`, it is kept as `_sqb_archive____migration_previous__` rather than dropped. +4. The move is recorded, then the model builds as usual. + +The old relation is never modified. Staging and replaced tables use the [janitor](/docs/cli/janitor) archive format, so an abandoned staging table or a replaced destination is deleted by janitor after `archive_retention_days`. A staging table left by an interrupted run is never reused; the next run stages again. Don't run janitor at the same time as a build in the same schema. + +`sqb plan` shows how each move will run, for example `physical copy, transient -> permanent, promote by swap`. + +## State + +Each move is recorded in an append-only `_sqlbuild_migrations` table in the destination schema. A missing record means completion is unrecorded; staging or promotion may already have happened. Re-running after an interruption recovers through the migration decisions above. + +## Adapter support + +| Adapter | Staging | Promotion | +|---------|---------|-----------| +| Snowflake | Zero-copy clone with `COPY GRANTS`. Transient → permanent is a physical copy, because Snowflake can't clone a transient table into a permanent one. | `ALTER TABLE … SWAP WITH` for an existing destination; rename for a missing destination | +| BigQuery | `CREATE TABLE … CLONE`, or `CREATE TABLE … COPY` when BigQuery refuses the clone | Two renames; the destination is briefly missing between them, and an interrupted run recovers on retry | +| Databricks | `DEEP CLONE` | Two renames, as for BigQuery | +| PostgreSQL, SQL Server, DuckDB, MotherDuck | Physical copy | Renames and the migration record in one transaction | + +On PostgreSQL, views that depend on the destination are re-pointed to the new table in the same transaction, keeping their options, owner and grants. Materialized views are not re-pointed. + +## Previewing another target + +`sqb plan --as ` compiles the project for another target, for example production, and shows its migration decisions using your current connection. It only inspects; nothing is written. diff --git a/website/src/content/docs/docs/concepts/models/schemas.mdx b/website/src/content/docs/docs/concepts/models/schemas.mdx new file mode 100644 index 0000000000..cad21fc591 --- /dev/null +++ b/website/src/content/docs/docs/concepts/models/schemas.mdx @@ -0,0 +1,124 @@ +--- +title: "Schemas" +description: "Declare model columns inline or reuse canonical inherited schemas." +--- + +Schema metadata defines column names, types, nullability, descriptions, and column audits. It can live inline in one `MODEL()` header or in a reusable `SCHEMA()` declaration. + +## Inline columns + +Use inline columns for metadata owned by one model: + +```sql +MODEL ( + materialized view, + description "Cleaned order records", + columns ( + order_id (type INTEGER, nullable false, audits [not_null, unique]), + customer_id (type INTEGER, nullable false, audits [not_null]), + status ( + type VARCHAR, + audits [accepted_values (values ["placed", "completed", "cancelled"])], + ), + ), +); +``` + +See [Audits](/docs/concepts/audits) for built-in audits, custom audits, arguments, severity, and incremental run scope. + +## Reusable schemas + +When multiple models implement the same relation shape, declare it once under `schemas/`. SQLBuild discovers schema files recursively and makes public names available throughout the project. + +```sql +-- schemas/orders/order.sql +SCHEMA ( + name order, + description "Canonical staged order shape", + columns ( + order_id (type INTEGER, nullable false, audits [not_null]), + customer_id (type INTEGER, nullable false, audits [not_null]), + status (type VARCHAR), + ), +); +``` + +Bind a model with `model_schema`: + +```sql +MODEL ( + materialized view, + schema staging, + model_schema order, + contract enforced, +); +``` + +`schema staging` selects the warehouse destination schema. `model_schema order` selects reusable column metadata. + +The reusable description becomes the model description when the model does not declare one. A model-owned description takes precedence. + +## Model-local columns + +A bound model may add output columns that are not part of the reusable shape: + +```sql +MODEL ( + model_schema order, + columns ( + ingestion_batch_id (type VARCHAR, nullable false), + ), + contract enforced, +); +``` + +Resolved schema columns retain their order and new model-local columns follow them. Use a named child schema when an extension is reusable; use inline columns for an extension owned by one model. + +## Model-specific column audits + +Audits in a reusable schema apply to every bound model. A model can add stricter audits to an inherited column by naming that column and declaring only `audits`: + +```sql +MODEL ( + model_schema order, + columns ( + order_id (audits [unique]), + ), +); +``` + +The effective `order_id` keeps the reusable type, nullability, description, and `not_null` audit, then adds `unique`. A model cannot remove reusable audits or override inherited metadata. An inherited-column entry containing `type`, `nullable`, or `description` fails compilation. Identical audit instances are deduplicated. + +## Inheritance + +A reusable schema may extend one parent with additional columns: + +```sql +SCHEMA ( + name sourced_order, + extends order, + columns ( + source (type VARCHAR, nullable false), + ), +); +``` + +Inheritance may be transitive. Parent columns resolve before child columns. An inherited column cannot be redeclared or overridden in a child schema. SQLBuild rejects unknown parents, cycles, case-insensitive duplicates, and multiple parents. + +Physical SQL output order is not currently enforced. Static contract analysis matches names and checks declared types and proven non-nullability. Runtime exact-contract validation matches names and types but does not validate nullability. + +## Contracts and planning + +`contract enforced` treats the complete named-plus-local declaration as the exact output shape. An unspecified model follows the project's `column_contract_mode`; `contract none` explicitly keeps the resolved columns as metadata, audit attachment, and type-enforcement inputs without activating shape validation. See [Contracts](/docs/concepts/models/contracts). + +For models bound to a reusable schema, effective column names, types, nullability, and enum members participate in model version identity. Changing those fields on a parent therefore affects models bound through descendants. Descriptions do not change model identity. Audits have their own audit-gate identities, so audit changes invalidate reusable audit results without changing the model version itself. + +## Limitations + +Reusable schemas intentionally support a narrow ownership model: + +- One optional parent, with transitive inheritance. +- Additive child and model-local output columns. +- Audit-only model augmentation of inherited columns. +- No general column overrides, multiple inheritance, composition, mixins, parameters, or generated projections. +- No physical output ordinal enforcement. diff --git a/website/src/content/docs/docs/concepts/models/type-enforcement.mdx b/website/src/content/docs/docs/concepts/models/type-enforcement.mdx new file mode 100644 index 0000000000..7bcbeea1f0 --- /dev/null +++ b/website/src/content/docs/docs/concepts/models/type-enforcement.mdx @@ -0,0 +1,71 @@ +--- +title: "Type Enforcement" +description: "Understand declared model types, static checks, and runtime casting." +--- + +Type enforcement makes authored column types operational. It is a model feature as well as a related source feature, and it is separate from whether a model uses an exact contract. + +## Enabling it + +For a SQL model, declaring a type on any inline or reusable-schema column enables type enforcement automatically: + +```sql +MODEL ( + materialized table, + columns ( + order_id (type INTEGER), + ordered_at (type TIMESTAMP), + source_system (), + ), +); +``` + +There is no `MODEL(type_enforcement ...)` switch. In this example, `order_id` and `ordered_at` are typed; `source_system` remains untyped and passes through unchanged. + +Sources have related behavior but different configuration. A source can explicitly set `type_enforcement: false`; see [Sources](/docs/concepts/sources#type-enforcement). + +## Compile-time checks + +When SQL analysis can infer an output type, SQLBuild compares it with the declared type using adapter-aware normalization. A proven mismatch is a compile error when type enforcement is active. If SQLBuild identifies an output column but cannot prove its expression type, it reports an unproven-type warning rather than guessing. If the entire output shape cannot be inferred, static type checks and warnings cannot run. + +Static analysis does not rewrite the authored query. It determines whether the inferred output is compatible before warehouse execution. + +## Runtime casting + +Runtime enforcement inspects a staged relation. If a typed output column does not already have its declared type, SQLBuild rebuilds the staging or delta relation with an explicit cast: + +```sql +CAST(order_id AS INTEGER) AS order_id +``` + +Only columns with declared types are cast. Untyped output columns are preserved. If a cast fails, the model fails during the type-enforcement phase before the staged table is promoted or the incremental delta is applied. + +Runtime cast support currently depends on materialization: + +| Materialization path | Framework runtime casts | +|----------------------|-------------------------| +| Full table with staged promotion | Yes, on the staging table | +| Non-microbatch incremental | Yes, on the staged delta | +| Microbatch incremental | Yes, on each staged batch | +| View | No; the view query defines the warehouse output type | +| Snapshot | No framework cast reconstruction | +| Custom materialization | Owned by the custom materialization | +| Full table with immediate promotion | Rejected when type enforcement is required | + +Staged promotion is the default table mode. If a project selects immediate table promotion, a typed model fails with guidance to use staged promotion because SQLBuild cannot inspect and reconstruct output before mutating the destination. + +## Type enforcement versus contracts + +These controls answer different questions: + +| Feature | Question | +|---------|----------| +| Type enforcement | Should typed columns be checked and, on supported paths, cast to their declared physical types? | +| `contract none` | Which declared columns can SQLBuild verify statically while still allowing additional output columns? | +| `contract enforced` | Must the output have exactly the complete declared shape? | + +Type enforcement can be active with `contract none`. Conversely, an enforced contract may contain untyped columns: exact shape validation still checks their names, but there is no declared physical type to cast or compare. + +## Schema changes + +Declared types are used in schema-change detection. With type enforcement active, the declared type is authoritative for typed columns when SQLBuild compares a planned model with the warehouse relation. diff --git a/website/src/content/docs/docs/concepts/observability.mdx b/website/src/content/docs/docs/concepts/observability.mdx new file mode 100644 index 0000000000..d747362233 --- /dev/null +++ b/website/src/content/docs/docs/concepts/observability.mdx @@ -0,0 +1,83 @@ +--- +title: "Execution Observability" +description: "Choose authoritative lifecycle facts, readable logs, or command-output records." +--- + +SQLBuild keeps execution facts separate from human output. Do not parse terminal text to determine +whether work started, completed, failed, skipped, or retried. + +| Record | Purpose | Authority | +|--------|---------|-----------| +| `LifecycleEvent` | Immutable invocation, run, resource-attempt, operation, statement, retry, and audit facts | Canonical lifecycle evidence once durably stored | +| `DiagnosticLog` | Structured framework diagnostics | Explanatory only | +| `stdout.log` / `stderr.log` | Exact host-local process output | Troubleshooting transcript | +| `CommandOutputRecord` | Bounded, ANSI-free remote stdout/stderr chunks | Potentially sensitive and lossy troubleshooting data | +| Final JSON output | End-of-command aggregate | Result projection, not event history | + +Human-readable CLI output remains the normal operator interface. Structured records are a separate +contract for integrations and durable consumers. + +## Lifecycle envelope + +New events use lifecycle schema version 2: + +```json +{ + "event_id": "9fb6d899754d4bd79c703067a8d5046c", + "event_type": "statement_completed", + "schema_version": 2, + "producer": "sqlbuild", + "producer_version": "0.91.0", + "occurred_at": "2026-09-07T18:15:30.125000Z", + "invocation_id": "4cc757dd93fe466aad2a220f1e76625e", + "invocation_sequence": 17, + "run_id": "run-20260907-181500", + "resource_id": "model:orders", + "resource_attempt_id": "97933c4aa29441cda133641345515e20", + "operation_id": "c978e47367494051bdde68ad03e56db4", + "statement_id": "5113ebc5afe247eb9c41dc0b27ef1a9a", + "external_context": { + "integration": { + "name": "dagster", + "run_id": "dagster-run-1", + "job_name": "daily_models", + "step_key": "sqlbuild_assets", + "retry_number": 0, + "partition_key": "2026-09-07" + } + }, + "payload": { + "adapter": "snowflake", + "duration_ms": 318.4, + "query_id": "01b6f1c2-0000-0000-0000-000000000000", + "sql_digest": "881b77b00a75e07936d7f20ea308a55e9ef98f2f1fd42c144a3a998d32094c1b" + } +} +``` + +`event_id` is the immutable deduplication identity. `invocation_sequence` is unique and increasing +in synchronous publication order within one invocation, including concurrent workers. A sink may +prioritize delivery, so consumers reconstruct invocation chronology by sorting this field rather +than relying on arrival timestamps. + +Schema-version-1 events remain readable. Unknown event names and future schema versions are retained +as opaque envelopes instead of being assigned invented semantics. + +## Correlation and privacy + +Every fact has an invocation ID. Nested IDs identify a run, logical resource and attempt, non-SQL +operation, and SQL statement. Orchestrators may provide bounded JSON-compatible +`external_context`; the Dagster integration supplies run, job, step, retry, and partition values +when its execution context exposes them. + +Lifecycle payloads exclude full SQL, parameter values, credentials, arbitrary user messages, and +raw process output. Command output and local logs can contain sensitive data and require an explicit +retention and access policy. + +## Failure semantics + +A start without a terminal fact is unknown or presumed lost. Consumers must not fabricate success +or failure. Sink delivery is bounded and best effort; destination failure does not change successful +warehouse work, and successful delivery does not prove the command succeeded. + +See [Typed Sinks](/docs/concepts/observability/sinks) to export lifecycle and command-output records. diff --git a/website/src/content/docs/docs/concepts/observability/sinks.mdx b/website/src/content/docs/docs/concepts/observability/sinks.mdx new file mode 100644 index 0000000000..667d788bc0 --- /dev/null +++ b/website/src/content/docs/docs/concepts/observability/sinks.mdx @@ -0,0 +1,104 @@ +--- +title: "Typed Sinks" +description: "Export lifecycle facts and command output through project-owned providers." +--- + +Project sinks consume one declared record type. Lifecycle and command-output streams remain separate +even when they share a provider and destination. + +## Lifecycle sink + +```python +# sinks/publish.py +from providers.destination_client import DestinationClient +from sqlbuild.sinks import ( + LifecycleEvent, + LifecycleEventKind, + lifecycle_event_sink, + lifecycle_event_to_json, +) + + +@lifecycle_event_sink( + event_kinds={ + LifecycleEventKind.INVOCATION, + LifecycleEventKind.RUN, + LifecycleEventKind.RESOURCE, + LifecycleEventKind.AUDIT, + } +) +def publish_lifecycle(event: LifecycleEvent, destination_client: DestinationClient) -> None: + destination_client.publish( + route="sqlbuild.lifecycle.v2", + key=event.invocation_id, + payload=lifecycle_event_to_json(event).encode("utf-8"), + ) +``` + +Using `invocation_id` as a partition key keeps one invocation on one Kafka partition. Keep +`event_id` in the payload as the deduplication identity. Confirm the destination topic is not +compacted when complete lifecycle history must remain replayable. + +## Command-output sink + +```python +from sqlbuild.sinks import CommandOutputRecord, command_output_sink, command_output_to_json + + +@command_output_sink(streams={"stdout", "stderr"}) +def publish_output(record: CommandOutputRecord, destination_client: DestinationClient) -> None: + destination_client.publish( + route="sqlbuild.command_output.v1", + key=record.record_id, + payload=command_output_to_json(record).encode("utf-8"), + ) +``` + +Command output groups adjacent text into bounded byte chunks and flushes on elapsed time, stream +change, size, or close. A loss record reports bounded-queue drops. This stream is useful for remote +transcripts but is not lifecycle evidence. + +## Configuration + +Runtime configuration can narrow declaration filters: + +```toml +[sinks.lifecycle] +event_kinds = ["run", "resource", "operation", "statement", "audit"] +min_severity = "info" + +[sinks.lifecycle.named.publish_lifecycle] +event_kinds = ["resource", "statement"] +min_severity = "warning" +``` + +`shutdown_timeout` sets how long a command waits at exit for lifecycle sinks to drain queued +events. It is a fixed duration from `"0s"` to `"10m"`, defaults to `"2s"`, and is accepted only +under `[sinks.lifecycle]`, not per named sink. `"0s"` drops anything still queued at exit. The +setting does not change the one-second timeout for each sink call. + +```toml +[sinks.lifecycle] +shutdown_timeout = "30s" +``` + +SQLBuild owns envelope validation, local queueing, filtering, and bounded dispatch. The project owns +destination credentials, serialization, routes/topics, acknowledgements, retries, retention, and +durability. SQLBuild core does not provide a Kafka or ClickHouse implementation. + +## Delivery behavior + +- Lifecycle dispatch prioritizes failures and terminal facts, remaining FIFO within equal priority. +- Queue overflow can displace or drop lower-priority records and is reflected in sink accounting. +- A failing or timed-out sink is isolated from command correctness. +- `audit_completed` facts are published as each audit finishes (model audits in `sqb build` when + their model finishes), not in one burst at the end of the run. +- When lifecycle events are dropped or fail, the command prints one warning to stderr with the + counts and affected sinks, for example + `Warning: command completed successfully, but lifecycle event export was incomplete: 140 of 248 events dropped, 0 failed (sink 'orders_events'). Increase sinks.lifecycle.shutdown_timeout or check sink health.` + The exit code and stdout, including `--json` output, are unchanged. +- Provider setup and declaration errors fail before execution because the project configuration is + invalid. +- Providers shared by multiple sinks are set up and torn down once per command. + +Use lifecycle facts—not command-output records—to build execution state and timelines. diff --git a/website/src/content/docs/docs/concepts/planning.mdx b/website/src/content/docs/docs/concepts/planning.mdx new file mode 100644 index 0000000000..5904a668a2 --- /dev/null +++ b/website/src/content/docs/docs/concepts/planning.mdx @@ -0,0 +1,130 @@ +--- +title: "Planning and change detection" +description: "How SQLBuild explains build work, detects changes, and chooses safe model actions." +sidebar: + label: "Overview" +--- + +`sqb plan` previews the work for a build without executing it. `sqb build` uses the same planner +before running the selected resources. + +Planning and selection are separate concerns: + +- **Selection** determines which resources are in scope. By default, that is the whole project; use + `--select` and `--exclude` to choose a smaller scope. +- **Change detection** compares compiled resources with recorded warehouse state. It explains why a + resource needs work, chooses the correct action for incremental models, propagates upstream + changes, and warns when a partial selection would be incoherent. + +A build runs its selected scope. Change detection still matters: the plan can show query and +configuration differences, calculate replay or full-refresh actions, and identify stale upstreams. +It does not filter a build to stale models. + +## Reading a plan + +Run the plan command with the same target and selectors you intend to build: + +```bash +sqb plan +sqb --target dev plan --select +fact_orders +``` + +A plan answers four questions: + +1. Which resources are selected? +2. What does SQLBuild know about their current warehouse state? +3. Why does each selected model need work? +4. What materialization or incremental action will the build take? + +Plans may include query diffs, schema changes, backfill ranges, source freshness signals, cascade +reasons, and warnings about stale dependencies. Planning does not modify model relations. + +## What SQLBuild compares + +### Models and functions + +Each model and function has a version identity derived from the inputs that affect its result: + +- normalized SQL after macro expansion and reference resolution; +- materialization settings, contracts, rendered SQL hooks, Python hook versions, and other + version-relevant configuration; +- referenced function identities; and +- upstream version identities where dependency changes must propagate. + +A function change therefore appears on the function itself and on dependent models. + +### Seeds + +Seeds use a content hash plus load-affecting configuration. SQLBuild can distinguish an unchanged +seed from one whose file or loading behavior changed. + +### Python nodes + +Loaders, tasks, assets, checks, and Python hooks have identities derived from project-owned source, +transitive project dependencies, and decorator configuration. Python hook identities also +participate in the identity of models that invoke them. + +For standalone Python nodes, this identity is primarily planning evidence. SQLBuild cannot observe +arbitrary external inputs such as APIs or files, so a node controls its own no-work decision with +`ctx.skip()` rather than relying on SQL identity alone. + +### Sources + +[Source freshness](/docs/concepts/planning/source-freshness) records an observable data version for a +source. A new observation can mark downstream models as affected even when their SQL is unchanged. + +## Change reasons + +The plan assigns reasons to explain detected work: + +| Reason | Meaning | +|--------|---------| +| First run | No previous identity exists for the resource | +| Query changed / checksum changed | Compiled SQL or seed content differs from the recorded identity | +| Config changed | Version-relevant configuration differs | +| Schema changed | A relevant warehouse or upstream schema changed | +| Function changed | A referenced function has a different identity | +| Upstream changed | A dependency change propagated to this model | +| Run despite unchanged | A table's explicit policy requires another run despite an unchanged identity | + +These reasons explain the plan; they are not a promise that a build will skip entries labelled +current. Builds execute the selected scope. + +## Cascades and incremental actions + +Changes propagate through the DAG in dependency order. The resulting action depends on the +materialization: + +- views are recreated when selected; +- tables rebuild when selected; +- incremental models use their cursor state, backfill policy, and `replay_on_change` setting to + determine the affected range; and +- a changed upstream can alter the action of downstream models even when their own SQL is unchanged. + +See [Cascade propagation](/docs/concepts/planning/cascade-propagation) for the detailed materialization +rules. + +## Selection and coherence + +SQLBuild reasons about dependencies outside a scoped selection without silently adding them to the +plan. If building the selected resources would use a stale or missing upstream, the planner warns or +blocks instead of presenting an incoherent partial build as current. + +See [Selection and staleness](/docs/concepts/planning/selection-and-staleness) for closure selectors and +stale-upstream handling. + +## Recorded state + +SQLBuild records append-only planning evidence in the warehouse: + +- `_sqlbuild_fingerprints` stores resource identities; +- `_sqlbuild_source_freshness` stores source observations; and +- `_sqlbuild_node_results` stores Python node outcomes. + +The planner reads the latest applicable facts and appends new facts after successful work. + +## Related topics + +- [Cascade propagation](/docs/concepts/planning/cascade-propagation) +- [Source freshness](/docs/concepts/planning/source-freshness) +- [Selection and staleness](/docs/concepts/planning/selection-and-staleness) diff --git a/website/src/content/docs/docs/concepts/planning/cascade-propagation.mdx b/website/src/content/docs/docs/concepts/planning/cascade-propagation.mdx new file mode 100644 index 0000000000..e77461cba7 --- /dev/null +++ b/website/src/content/docs/docs/concepts/planning/cascade-propagation.mdx @@ -0,0 +1,38 @@ +--- +title: "Cascade propagation" +description: "How a change signal propagates downstream through the DAG, and how each materialization type responds." +--- + +When a model or function changes, the change signal propagates downstream through the DAG. Every model downstream of a changed node is marked `Upstream changed` in the plan, even if its own SQL and config are identical. + +The cascade walk is topological: it processes models in dependency order, so each model sees the resolved state of all its upstreams before deciding its own effective action. + +## What cascades + +- A query, config, or schema change on any model cascades to all its downstream dependents. +- A function change cascades to every model that calls it (directly or transitively). +- Source freshness changes propagate downstream the same way. See [Source freshness](/docs/concepts/planning/source-freshness). + +## How materialization types respond + +- **Views** are recreated on every build regardless, so a cascade has no extra cost. +- **Tables** are fully rebuilt, same as if they had changed themselves. +- **Incremental models** receive a replay window from the cascade. A `full` replay always cascades. A `bounded` replay only cascades when the upstream and downstream models share the same `cursor_type` (e.g. both use `timestamp`), so unrelated cursor types don't inherit bounded windows that don't apply. The downstream model's own `replay_on_change` policy takes precedence over any cascaded signal. + +## Resolution when multiple upstreams are stale + +If a model has multiple stale upstreams with different replay actions, the most aggressive action wins. `full` beats `bounded`, and among bounded actions, the longer duration wins. Ties are broken alphabetically by model name for determinism. + +## Overriding cascaded replay + +Downstream incremental models can set their own `replay_on_change` policy to override the cascaded signal. If a downstream model declares its own policy, that policy applies instead of the upstream's. If it has no policy, it inherits the upstream's replay scope. + +## Replay on change + +When a change is detected on an incremental model, `replay_on_change` controls how much data to reprocess: + +- **`forward`** (default) - run the normal incremental delta from the cursor. No reprocessing. +- **`bounded-`** (e.g. `bounded-14d`) - replay the specified window of data. +- **`full`** - drop and rebuild the entire table. + +See [Incremental Models: Replay on change](/docs/concepts/incremental#replay-on-change) for configuration details. diff --git a/website/src/content/docs/docs/concepts/planning/selection-and-staleness.mdx b/website/src/content/docs/docs/concepts/planning/selection-and-staleness.mdx new file mode 100644 index 0000000000..b3f6a086ac --- /dev/null +++ b/website/src/content/docs/docs/concepts/planning/selection-and-staleness.mdx @@ -0,0 +1,70 @@ +--- +title: "Selection and staleness" +description: "How selection interacts with change detection, and the stale warnings that stop silent partial rebuilds." +--- + +Change detection is selection-aware. When you scope a run with `--select`, SQLBuild only runs (and only displays) the resources you selected, but it still reasons about the *whole* graph to keep the result honest. The key case it handles: a selected model whose upstream changed but is **not** in the selection. + +## Only selected resources appear in the plan + +For a scoped run like `sqb dbt build --select agg_daily_revenue`, the plan shows only the selected resources, not their passive upstream closure. SQLBuild still walks upstreams internally for change propagation (a selected model flips to run when an upstream changed), but those passive upstreams are not listed as plan entries or counted in the header. + +`--full-refresh` is likewise scoped to the selected models, not the expanded upstream closure. + +## Stale warnings instead of silent partial rebuilds + +If a selected model has an upstream that changed but is **outside the selection**, rebuilding the selected model alone would run it on top of a stale upstream and silently produce a result that looks current but isn't. SQLBuild does not do that. It leaves the selected model current (it is not rebuilt) and emits a stale warning naming the changed upstreams, with the selector you can use to incorporate them. + +Suppose `stg_orders` (upstream) changed, and you select only the downstream `agg_daily_revenue`: + +```bash +sqb dbt plan --select agg_daily_revenue +``` + +``` +Plan ready (1 selected resources) + +dbt (1 selected resources) + planned models: 0 run, 1 current, 0 blocked + + Model plan + Current (1) + model.jaffle_analytics.agg_daily_revenue no change + +Warnings (1) + - selected dbt model 'agg_daily_revenue' is stale: upstream stg_orders changed + but will not be rebuilt or is stale; rebuild with a closure selector + (e.g. +model) to incorporate it +``` + +The model stays current rather than being rebuilt on a stale input, and the warning tells you exactly which upstreams changed and how to pull them in. + +## Incorporating the changed upstreams + +Use a closure selector to include the changed upstreams in the run. With `+agg_daily_revenue`, SQLBuild pulls in the changed `stg_orders`, cascades the change through the intermediate models, and rebuilds: + +```bash +sqb dbt plan --select +agg_daily_revenue +``` + +``` +Plan ready (8 selected resources) + planned models: 5 run, 1 current, 0 blocked + + Model plan + Checksum changed (1) + model.jaffle_analytics.stg_orders checksum changed + Upstream changed (4) + model.jaffle_analytics.agg_daily_revenue upstream changed + model.jaffle_analytics.fct_orders upstream changed + model.jaffle_analytics.int_order_payments upstream changed + model.jaffle_analytics.stg_order_statuses upstream changed +``` + +## Seeds follow the same rule + +Seed changes are tracked the same selection-aware way. A changed seed that is outside the current selection does not trigger a partial rebuild of its dependents; instead the dependents are left current and a stale warning is emitted, exactly as for models. This keeps a scoped run from quietly building on a seed that the run didn't reload. + +## Why this matters + +A run that reports success but silently built a model on stale inputs is a correctness hazard - the data looks fresh but isn't. Surfacing staleness as an explicit warning (and refusing the misleading partial rebuild) keeps scoped runs honest: you either get a result built on current inputs, or a clear warning telling you it would not be, with the fix. diff --git a/website/src/content/docs/docs/concepts/planning/source-freshness.mdx b/website/src/content/docs/docs/concepts/planning/source-freshness.mdx new file mode 100644 index 0000000000..da72f9a6e6 --- /dev/null +++ b/website/src/content/docs/docs/concepts/planning/source-freshness.mdx @@ -0,0 +1,37 @@ +--- +title: "Source freshness" +description: "Observe external source changes and propagate them through planning." +--- + +Source freshness lets SQLBuild observe whether external source data changed between runs. A changed +observation propagates through the dependency graph, so plans can explain which downstream models +are affected and choose the correct incremental action. + +## Configuration + +Source freshness is configured per source in `sources/*.yml` with a `freshness:` block. See [Sources: Source freshness](/docs/concepts/sources#source-freshness) for the full configuration reference. + +## How observations work + +During planning, SQLBuild observes the current data version of each source that has freshness configured (or that the adapter can observe automatically): + +1. **Observe** - query the source's current data version using the configured strategy. +2. **Compare** - compare the observed version against the last recorded observation from `_sqlbuild_source_freshness` in the target schema. +3. **Propagate** - walk the DAG downstream from changed or unknown sources to identify which models are affected. + +Sources without explicit `freshness:` config are auto-observed using the `adapter` strategy if the adapter supports table metadata and the source has a physical table (not an expression source, not a managed source). + +## Lag tolerance + +For timestamp-based freshness, `lag_tolerance` controls how much the observed value can drift before being considered a real change. If the current timestamp is within the tolerance of the previous observation, the source is treated as unchanged. This is useful for sources where the freshness timestamp moves by seconds or minutes on every query but the underlying data hasn't meaningfully changed. + +## State storage + +Source freshness observations are appended to `_sqlbuild_source_freshness` in each target schema. +Failed work does not replace the prior successful observation, so the next plan still +sees the pending source change. + +Direct observations are resolved across all target schemas in the project, so a source referenced by +models in different schemas is tracked consistently. + +Use [`sqb freshness`](/docs/cli/freshness) to observe source freshness on demand without triggering a build. diff --git a/website/src/content/docs/docs/concepts/project-configuration.mdx b/website/src/content/docs/docs/concepts/project-configuration.mdx new file mode 100644 index 0000000000..fdfc8c2726 --- /dev/null +++ b/website/src/content/docs/docs/concepts/project-configuration.mdx @@ -0,0 +1,482 @@ +--- +title: "Project Configuration" +description: "Configure your SQLBuild project with sqlbuild_project.toml and sqlbuild_local.toml." +--- + +SQLBuild projects are configured with two files in the project root: + +- **`sqlbuild_project.toml`** - shared project configuration, committed to version control +- **`sqlbuild_local.toml`** - local developer overrides, gitignored + +Macro, constant, and enum visibility is not configured in either file. Declaration scopes use filesystem conventions; see [Declaration Scopes](/docs/concepts/declaration-scopes). + +Most projects need only one committed `sqlbuild_project.toml`. Define shared targets such +as `dev` and `prod` there, including clone policies and team-wide defaults. Do not maintain +separate complete project files for each environment. + +Create `sqlbuild_local.toml` only when a developer or execution environment needs different +target selection, credentials, schemas, adapter settings, or variables. SQLBuild loads it +automatically and merges its explicitly configured values over the shared project config. + +```text +project/ + sqlbuild_project.toml # committed: shared targets and behavior + sqlbuild_local.toml # gitignored: this developer's overrides +``` + +Add the local file to `.gitignore`: + +```gitignore +sqlbuild_local.toml +``` + +## sqlbuild_project.toml + +A complete example: + +```toml +name = "waffle_shop" +adapter = "duckdb" +default_target = "dev" + +[connections.local] +database = "waffle_shop_control.duckdb" + +[settings] +default_audit_severity = "warn" + +[defaults] +materialized = "table" + +[constants] +collection_rendering = "value_list" + +[targets.prod] +connection = "local" +schema = "prod" + +[targets.dev] +connection = "local" +schema = "dev" + +[path_defaults.staging] +materialized = "view" +``` + +### Required fields + +| Field | Description | +|-------|-------------| +| `name` | Project name. Used in fingerprint tracking and manifest generation. | +| `adapter` | Database adapter: `duckdb`, `motherduck`, `snowflake`, `bigquery`, `databricks`, `postgres`, or `sqlserver`. See [Adapters](/docs/concepts/adapters). | +| `default_target` | Name of the target to build against when none is selected (see [Targets](#targets)). | + +### Named connections + +Define reusable connections under `[connections.]`, then reference one by name from +each target. Connections own endpoint, authentication, and compute settings. Targets own the +authoritative `database`, `schema`, variables, and operational policy. + +```toml +[connections.local] +database = "my_project.duckdb" + +[targets.dev] +connection = "local" +schema = "dev" +``` + +Multiple targets can reuse one connection while keeping separate namespaces and policies: + +```toml +[connections.warehouse] +account = "my_org-my_account" +user = "${ENV:SNOWFLAKE_USER}" +password = "${ENV:SNOWFLAKE_PASSWORD}" +warehouse = "TRANSFORM_WH" + +[targets.prod] +connection = "warehouse" +database = "ANALYTICS" +schema = "PROD" + +[targets.dev] +connection = "warehouse" +database = "ANALYTICS" +schema = "DEV_ALICE" +``` + +A `database` or `schema` present in a named connection is connection/session metadata only; +it does not satisfy the mandatory namespace strategy for a named target. Put the target's +authoritative database and schema on `[targets.]`. SQLBuild validates connection +references while loading configuration, without opening a warehouse connection, and reports +an unknown `targets..connection` name as an offline configuration error. + +For migration only, SQLBuild still maps legacy `[connection]` to an implicit connection and +legacy `[targets..connection]` blocks to target-specific implicit connections. These +forms are compatibility syntax, not the canonical format for new or updated projects. + +## Targets + +A target is a named build context - the database and schema you build into, plus execution policy (for example `dev` and `prod`). Each target references a named connection and can configure: + +| Field | Description | +|-------|-------------| +| `schema` | Schema for all models in this target; required for named targets | +| `loader_schema` | Default write schema for managed source loaders; falls back to `schema` | +| `database` | Database for all models in this target | +| `connection` | Name from `[connections.]` used for endpoint, authentication, and compute | +| `vars` | Target-specific project variables | +| `defer_sources_to` | Target name to read managed source data from (see [Loaders](/docs/concepts/python-nodes/loaders#source-deferral)) | +| `clone` | Clone policy (see below) | + +```toml +[connections.warehouse] +account = "my_org-my_account" +warehouse = "TRANSFORM_WH" + +[targets.prod] +connection = "warehouse" +database = "analytics" +schema = "analytics_prod" +loader_schema = "raw_prod" +defer_sources_to = "prod" + +[targets.dev] +connection = "warehouse" +database = "analytics" +schema = "analytics_dev" +loader_schema = "raw_dev" +defer_sources_to = "prod" + +[targets.staging] +connection = "warehouse" +database = "analytics" +schema = "staging" + +``` + +Managed loader writes use the active target's `loader_schema`, falling back to its model +`schema`. Managed source reads use the target named by `defer_sources_to`, or the active +target itself when deferral is omitted. In the example, `load --target dev` writes to +`raw_dev`, while models built in dev read from `raw_prod`. + +SQLBuild rejects targets on the same warehouse/database when their managed loader writes +resolve to the same schema. Two targets may read the same schema through deferral, but they +cannot both own loader writes there. + +### Selecting a target + +The active target is determined by (in order of precedence): + +1. `--target` on the command line (highest priority) +2. `sqlbuild_local.toml` `target` field +3. `default_target` in `sqlbuild_project.toml` +4. No target (models build to the default schema) + +A typical developer keeps shared target definitions in `sqlbuild_project.toml` and selects +their normal target once in the optional local file: + +```toml +# sqlbuild_local.toml +target = "dev" + +[targets.dev] +connection = "warehouse" +schema = "dev_alice" +loader_schema = "raw_alice" + +[connections.warehouse] +user = "alice" +password = "${ENV:SNOWFLAKE_PASSWORD}" +``` + +Commands then use `dev` automatically. An explicit command such as `sqb build --target prod` +still takes precedence for that invocation. + +### Clone policies + +Targets can declare whether they allow cloning to or from: + +```toml +[targets.prod] +schema = "prod" + +[targets.prod.clone] +allow_as_clone_origin = true +allow_as_clone_destination = false + +[targets.dev] +schema = "dev" + +[targets.dev.clone] +allow_as_clone_origin = false +allow_as_clone_destination = true +``` + +Both policies default to `false`. `sqb clone --from prod --to dev` requires `allow_as_clone_origin = true` on `prod` and `allow_as_clone_destination = true` on `dev`. + +## Defaults + +Project-wide model defaults. Any field you can set in a `MODEL()` header can be set here as a default: + +```toml +[defaults] +materialized = "table" +incremental_strategy = "delete_insert" +replay_on_change = "full" +tags = ["managed"] +``` + +These apply to all models unless overridden by path defaults or the model's own `MODEL()` header. + +## Constants + +Collection constants default to parenthesized SQL value lists. Set a project-wide default when lists and sets should instead compile to first-class adapter-native arrays: + +```toml +[constants] +collection_rendering = "array" +``` + +`collection_rendering` accepts `value_list` (the SQLBuild default) or `array`. A public constant's `render_as` field or a model-local `constant(...)` wrapper overrides the project setting. The complete precedence order is declaration override, project setting, then `value_list`. + +This setting does not make unsupported adapter features portable. In particular, SQL Server rejects native arrays, and BigQuery rejects nested arrays. See [Collections and Rendering](/docs/concepts/constants/collections-and-rendering#project-default) for syntax, adapter output, and value-list usage constraints. + +## Path defaults + +Per-directory model defaults. Useful for applying different config to different parts of your project: + +```toml +[path_defaults."models/staging"] +materialized = "view" +tags = ["staging"] + +[path_defaults.marts] +materialized = "table" +tags = ["marts"] +replay_on_change = "full" +``` + +Path matching uses the path below `models/`. A model at `models/staging/stg_orders.sql` matches the +`staging` path default. + +### Config layering order + +Configuration is layered in this order, with later layers overriding earlier ones: + +1. **Project defaults** (`defaults`) +2. **Path defaults** (`path_defaults`) - if the model's path matches +3. **MODEL() header** - the model's own config + +Most keys are overridden by the more specific layer, but three merge instead: + +- `tags` are *unioned* across layers. A model with `tags [marts]` in its header that matches a path default with `tags [managed]` will have both tags. +- `row_diff_exclude_columns` lists are unioned across layers. +- `row_diff_tolerances` mappings are deep-merged across layers, so a header tolerance for one column adds to (rather than replaces) tolerances declared in defaults or path defaults. + +Diff sampling values use ordinary replacement precedence. `row_diff_sample_rows = 0` explicitly +disables an inherited sample for a path or model. CLI `--sample-rows`, `--sample-seed`, and +`--exhaustive` override the compiled configuration for one invocation. + +## Settings + +Global feature toggles: + +```toml +[settings] +sql_analysis = true +query_change_tracking = true +column_contract_mode = "implicit" +concurrency = 1 +microbatch_concurrency = false +auto_load_sources = true +table_promotion_mode = "staged" +default_audit_severity = "warn" +default_audit_run_scope = "final" +``` + +| Field | Default | Description | +|-------|---------|-------------| +| `sql_analysis` | `true` | Enable SQL syntax analysis, column binding, type inference, output inference, and semantic validation at compile time | +| `query_change_tracking` | `true` | Track query fingerprints for change detection | +| `column_contract_mode` | `implicit` | Controls whether column declarations on models without a `contract` declaration activate static shape/nullability validation. `implicit` preserves that validation; `explicit` treats columns as metadata and audit attachment unless the model declares `contract enforced`. Model-level `contract enforced` and `contract none` override this setting. Explicit type enforcement remains independent. See [Contracts](/docs/concepts/models/contracts). | +| `concurrency` | `1` | Maximum parallel model execution (currently serial only) | +| `microbatch_concurrency` | `false` | Explicitly permit models with `batch_concurrency > 1`; concurrent batches use immutable coordination facts | +| `auto_load_sources` | `true` | Automatically run source loaders before building dependent models during `sqb build`. See [Loaders](/docs/concepts/python-nodes/loaders). | +| `table_promotion_mode` | adapter default | `staged` (CTAS to staging, audit, then promote) or `immediate` (CTAS directly to target, then audit) | +| `default_audit_severity` | `warn` | Default severity for audits: `warn` or `error` | +| `default_audit_run_scope` | `final` | Default run scope for audits: `final` or `delta_and_final` | + +### Table promotion mode + +- **`staged`** (default for most adapters): Materializes into a staging table, runs audits, then swaps into the target. If audits fail, the production table is untouched. +- **`immediate`**: Creates the table directly at the target location. Audits run after materialization. Simpler but no pre-promotion safety net. + +## Rules + +Rules configuration belongs in the shared `sqlbuild_project.toml` so local and CI compilation use +the same checks: + +```toml +[rules] +select = ["SQBRSQL", "SQBRGRAPH", "XSQBRARCH"] +``` + +Rules are opt-in. Exact codes activate individual checks and prefixes activate a family. Built-in +codes begin with `SQBR`; repository-defined codes begin with `XSQBR`. See +[Compiler-integrated Rules](/docs/concepts/rules) for configuration, authoring, and suppressions. + +## Project variables + +Variables are simple string substitutions available in model SQL via the `@@name` syntax: + +```toml +[vars] +schema_prefix = "analytics" +retention_days = "90" +``` + +Target-specific variables override project-level ones: + +```toml +[vars] +schema_prefix = "analytics" + +[targets.prod.vars] +schema_prefix = "prod_analytics" +``` + +See [Macros](/docs/concepts/macros) for details on variable substitution and how variables interact with macros. + +## Janitor + +Configuration for the `sqb janitor` command, which archives and then deletes stale warehouse relations: + +```toml +[janitor] +enabled = false +retention_days = 14 +archive_retention_days = 14 +delete_tracked_only = true +exclude_patterns = ["audit_*", "tmp_*"] +``` + +| Field | Default | Description | +|-------|---------|-------------| +| `enabled` | `false` | Whether janitor is active | +| `retention_days` | `14` | How many days a relation must be stale before it is archived | +| `archive_retention_days` | `14` | How many days an archive is kept before it is deleted. `0` deletes it in the same run. | +| `delete_tracked_only` | `true` | Only clean relations that appear in fingerprint tracking | +| `exclude_patterns` | `[]` | Glob patterns for relations to skip | +| `direct_state_history_versions` | `20` | How many state-history rows to keep per identity in `_sqlbuild_fingerprints` and `_sqlbuild_source_freshness` | + +See [janitor](/docs/cli/janitor) for the archive and delete lifecycle. + +## Scenario + +Configuration for scenario snapshot capture safety limits and local type overrides: + +```toml +[scenario.snapshot_limits] +max_rows_per_relation = 10000 +max_total_rows = 50000 +max_bytes_per_relation = 10485760 +max_total_bytes = 52428800 +``` + +| Field | Default | Description | +|-------|---------|-------------| +| `max_rows_per_relation` | none | Maximum rows per captured relation | +| `max_total_rows` | none | Maximum total rows across all relations in one scenario | +| `max_bytes_per_relation` | none | Maximum JSONL bytes per relation file | +| `max_total_bytes` | none | Maximum total JSONL bytes per scenario | + +Local type overrides for DuckDB replay are configured per adapter dialect: + +```toml +[scenario.local_type_overrides.snowflake] +"OBJECT" = "JSON" +"ARRAY" = "JSON" +``` + +See [Scenarios](/docs/concepts/scenarios) for details on local type overrides and capture limits. + +## dbt + +Configuration for running SQLBuild alongside an existing dbt project: + +```toml +[dbt] +project_dir = "../dbt_project" +profiles_dir = "../profiles" +target_path = "../dbt_project/target" +target = "dev" +``` + +| Field | Description | +|-------|-------------| +| `project_dir` | Path to the dbt project root (where `dbt_project.yml` lives) | +| `profiles_dir` | Path to the directory containing `profiles.yml` | +| `target_path` | Path to dbt's `target/` directory (where `manifest.json` is written) | +| `target` | dbt target name override (optional) | + +Paths can be absolute or relative to the SQLBuild project root. See [Using SQLBuild with dbt](/docs/concepts/dbt-compatibility/overview) for setup and usage details. + +## Skills + +Configuration for AI agent skill file installation: + +```toml +[skills] +targets = ["agents", "claude"] +auto_update = false +``` + +| Field | Default | Description | +|-------|---------|-------------| +| `targets` | `["agents", "claude"]` | Agent targets to install. OpenCode consumes `.agents`; use `opencode` only as an explicit override. | +| `auto_update` | `false` | Refresh stale SQLBuild-owned generated files from the installed package during normal commands; custom collisions are never overwritten. | + +See [skills CLI reference](/docs/cli/skills) for usage details. + +## sqlbuild_local.toml + +Local developer overrides. This optional file is loaded automatically and should be +gitignored. Only put values that differ from the shared project configuration here. + +```toml +target = "dev" + +[targets.dev] +schema = "dev_alice" +loader_schema = "raw_alice" + +[connections.local] +database = "my_local.duckdb" + +[settings] +sql_analysis = false +concurrency = 4 + +[vars] +debug_mode = "true" +``` + +| Field | Description | +|-------|-------------| +| `target` | Override which target is active for this developer | +| `adapter` | Override the database adapter (e.g. use DuckDB locally while prod uses Snowflake) | +| `connections` | Override named connection fields; entries merge by connection name | +| `settings` | Override global settings (only explicitly set fields take effect) | +| `vars` | Developer-specific variable overrides (merged on top of project + target vars) | + +Project and local configuration merge named connections by name and merge their explicitly +configured fields. Target blocks merge the same way and may override the connection reference, +`database`, `schema`, `loader_schema`, variables, source deferral, and policy fields. Unspecified +values continue to come from `sqlbuild_project.toml`; a local reference to an unknown merged +connection still fails offline during configuration loading. + +This replaces the common dbt pattern of switching profiles or setting environment variables +to change targets. Each developer sets their target, named connection, and preferences once in +`sqlbuild_local.toml` and it persists across sessions. diff --git a/website/src/content/docs/docs/concepts/python-nodes/assets.mdx b/website/src/content/docs/docs/concepts/python-nodes/assets.mdx new file mode 100644 index 0000000000..286c431533 --- /dev/null +++ b/website/src/content/docs/docs/concepts/python-nodes/assets.mdx @@ -0,0 +1,137 @@ +--- +title: "Assets" +description: "Produce or observe external artifacts as nodes in the SQLBuild graph." +--- + +Assets are Python nodes that produce or observe an external artifact - an exported file, a published dataset, a dashboard refresh, an ML model. They behave like [tasks](/docs/concepts/python-nodes/tasks) but add dataset-like metadata (columns, column lineage) and a materialization flag. See [Python Nodes](/docs/concepts/python-nodes/overview) for the shared model and the SQL boundary rules. + +## Defining an asset + +Place Python files under `assets/` and decorate functions with `@asset`: + +```python +# assets/exports.py +from sqlbuild.assets import asset, AssetContext + + +@asset +def orders_export(ctx: AssetContext): + path = write_orders_csv() + return ctx.result(metadata={"path": path, "rows": 1200}, materialized=True) +``` + +The asset receives an `AssetContext` and returns through `ctx.result(...)`. + +## Materialization + +Assets record whether they actually produced an artifact via `materialized`: + +```python +@asset +def orders_export(ctx: AssetContext): + if nothing_changed(): + return ctx.result(metadata={"status": "unchanged"}, materialized=False) + return ctx.result(metadata={"path": export()}, materialized=True) +``` + +- `materialized=True` - the artifact was produced this run. +- `materialized=False` - the asset ran but produced nothing (e.g. it only observed state). This is **not** a skip; the node still succeeded. + +Only assets have `materialized`; tasks do not. + +## Dependencies + +Assets declare dependencies with `depends_on` (a single function, tuple, or list), and may depend on tasks, assets, and loaders: + +```python +from sqlbuild.assets import asset +from tasks.orders import export_orders + + +@asset(depends_on=export_orders) +def orders_dashboard(ctx): + result = ctx.result_of(export_orders) + return ctx.result(metadata={"rows": result.payload["rows"]}, materialized=True) +``` + +To read a SQL model or source, declare a typed reference and resolve it at runtime - see [SQL references](/docs/concepts/python-nodes/sql-references): + +```python +from sqlbuild.assets import asset +from sqlbuild.refs import model + + +@asset(depends_on=model("fact_orders")) +def orders_extract(ctx): + relation = ctx.relation(model("fact_orders")) + rows = ctx.query(f"SELECT count(*) FROM {relation}").fetchone()[0] + return ctx.result(metadata={"rows": rows}, materialized=True) +``` + +## Columns and column lineage + +Assets can declare a schema for catalog and lineage purposes. This does not enforce anything at runtime; it describes the artifact: + +```python +@asset( + columns=[ + {"name": "order_id", "type": "INTEGER"}, + {"name": "total_cents", "type": "INTEGER", "nullable": False}, + ], + column_lineage={ + "order_id": [{"node": "fact_orders", "column": "order_id"}], + "total_cents": [{"node": "fact_orders", "column": "total_cents"}], + }, +) +def orders_export(ctx): + ... +``` + +- `columns` - column declarations with `name`, optional `type`, `nullable`, `description`, and `meta`. +- `column_lineage` - maps each asset column to upstream `{node, column}` references, surfaced in the DAG artifact and integrations. + +Tasks and checks do not support `columns` or `column_lineage`. + +## Skipping and retries + +Assets support the same `ctx.skip(...)` and `retry` behavior as tasks: + +```python +from sqlbuild.assets import asset, SkipMode +from sqlbuild.retries import RetryPolicy + + +@asset(retry=RetryPolicy(max_attempts=3, retry_on=(IOError,))) +def export(ctx): + if not ready(): + return ctx.skip("upstream not ready", mode="soft") # or SkipMode.SOFT + return ctx.result(metadata={"path": do_export()}, materialized=True) +``` + +See [Tasks](/docs/concepts/python-nodes/tasks#retries) for the full retry policy fields. + +## Decorator parameters + +| Parameter | Description | +|-----------|-------------| +| `name` | Override the node name (defaults to the function name) | +| `depends_on` | Upstream nodes (function, tuple, or list); `model()`/`source()` for read-only SQL refs | +| `columns` | Column declarations for the produced artifact | +| `column_lineage` | Map of asset column to upstream `{node, column}` references | +| `tags` | Labels for selection and grouping | +| `group` | Display/catalog grouping | +| `description` | Docs (defaults to docstring) | +| `meta` | Freeform JSON metadata | +| `retry` | A `RetryPolicy` | + +## Running assets + +```bash +# Run an asset and its required graph +sqb build --select orders_export --no-tests --no-audits + +# Include in a full build with its upstreams +sqb build --select +orders_export +``` + +Assets run during `sqb build`. Use `--no-python` to suppress read-side assets while still loading sources. diff --git a/website/src/content/docs/docs/concepts/python-nodes/checks.mdx b/website/src/content/docs/docs/concepts/python-nodes/checks.mdx new file mode 100644 index 0000000000..051290c618 --- /dev/null +++ b/website/src/content/docs/docs/concepts/python-nodes/checks.mdx @@ -0,0 +1,116 @@ +--- +title: "Checks" +description: "Validate tasks, assets, and loaders with Python checks." +--- + +Checks are Python nodes that validate other Python nodes. They are the Python analog of SQL [audits](/docs/concepts/audits): audits validate SQL relations, checks validate the output of tasks, assets, and loaders. See [Python Nodes](/docs/concepts/python-nodes/overview) for the shared model. + +Checks are separate graph nodes, not callbacks embedded in a task or asset. A check declares what it validates through `depends_on`. + +## Defining a check + +Place Python files under `checks/` and decorate functions with `@check`. `depends_on` is required: + +```python +# checks/orders.py +from sqlbuild.checks import check, CheckContext +from tasks.orders import export_orders + + +@check(depends_on=export_orders) +def check_orders_exported(ctx: CheckContext): + result = ctx.result_of(export_orders) + if result.metadata.get("rows", 0) == 0: + return ctx.fail("no orders exported") + return ctx.pass_("orders exported") +``` + +The check receives a `CheckContext` and reads its dependencies' persisted results with `ctx.result_of(...)`. + +## Results + +Return a result through the context helpers, or a bool shorthand: + +```python +@check(depends_on=orders_asset) +def rows_present(ctx): + return ctx.result_of(orders_asset).payload["rows"] > 0 # True -> pass, False -> fail +``` + +| Return | Meaning | +|--------|---------| +| `ctx.pass_(message=None, metadata=None)` | Passing | +| `ctx.fail(message, metadata=None)` | Failing, using the check's severity | +| `ctx.warn(message, metadata=None)` | Warning, regardless of severity | +| `True` | Pass | +| `False` | Fail | + +Returning `None` is not allowed - checks must be explicit. + +## Severity + +`@check` takes a `severity` of `error` (default) or `warn`: + +```python +@check(depends_on=export_orders, severity="warn") +def orders_freshness(ctx): + if stale(): + return ctx.fail("export is stale") # recorded as a warning, does not fail the build + return ctx.pass_() +``` + +- `error` (default) - a failing check fails `sqb build`. +- `warn` - a failing check is reported but does not fail the build. + +`ctx.warn(...)` always produces a warning regardless of the declared severity. + +## What checks can depend on + +- Checks may depend on **tasks, assets, and loaders**. +- Checks may **not** depend on SQL models, sources, seeds, or functions. Use SQL [audits](/docs/concepts/audits) to validate SQL relations. +- Checks may **not** depend on other checks. +- Checks may **not** depend on a terminal source loader directly. Validate loaded source data with a source audit instead. + +A check that depends on a single node is displayed grouped under that node. Multi-dependency checks are shown as standalone validation nodes, grouped by `group`, tags, or path. + +## Decorator parameters + +| Parameter | Description | +|-----------|-------------| +| `depends_on` | Required. Tasks/assets/loaders to validate (function, tuple, or list) | +| `name` | Override the node name (defaults to the function name) | +| `severity` | `error` (default) or `warn` | +| `tags` | Labels for selection and grouping | +| `group` | Display/catalog grouping | +| `description` | Docs (defaults to docstring) | +| `meta` | Freeform JSON metadata | + +Checks do not support `columns`, `column_lineage`, or `retry`. + +## Running checks + +Checks run automatically during `sqb build` when their Python dependencies run. They are skipped when `--no-audits` is passed. To run checks on their own, use [`sqb check`](/docs/cli/check): + +```bash +# Run all checks +sqb check + +# Run a specific check (and its required dependencies) +sqb check --select +check_orders_exported + +# Run checks by tag +sqb check --select tag:exports +``` + +`sqb check` rejects selecting non-check nodes; use `sqb build` to run tasks and assets. Check results are written to `target/run/checks/python_checks.json`, and `sqb check --json` prints them to stdout. + +## Checks vs audits + +| | Checks | Audits | +|---|--------|--------| +| Validates | Python tasks, assets, loaders | SQL relations | +| Authored in | `checks/` (Python) | `MODEL()` headers / `audits/` (SQL) | +| Run by | `sqb build`, `sqb check` | `sqb build`, `sqb audit` | +| Severity | `error`, `warn` | `error`, `warn` | + +`sqb build` runs both. `sqb audit` runs SQL audits only; `sqb check` runs Python checks only. diff --git a/website/src/content/docs/docs/concepts/python-nodes/factories.mdx b/website/src/content/docs/docs/concepts/python-nodes/factories.mdx new file mode 100644 index 0000000000..22972af2ca --- /dev/null +++ b/website/src/content/docs/docs/concepts/python-nodes/factories.mdx @@ -0,0 +1,96 @@ +--- +title: "Factories" +description: "Generate Python nodes programmatically with @factory." +--- + +A factory is a function that **generates** Python nodes instead of authoring them one at a time. Use `@factory` when you want to create many similar [tasks](/docs/concepts/python-nodes/tasks), [assets](/docs/concepts/python-nodes/assets), [loaders](/docs/concepts/python-nodes/loaders), or [checks](/docs/concepts/python-nodes/checks) from a list, a config, or a loop, rather than hand-writing each one. + +Factories are an advanced feature. Most projects author nodes directly; reach for a factory when you find yourself copy-pasting near-identical node definitions. + +## Defining a factory + +A `@factory` function takes **no arguments** and returns one or more decorated node functions: + +```python +# factories/exports.py +from sqlbuild.factories import factory +from sqlbuild.assets import asset + +TABLES = ["orders", "customers", "payments"] + + +@factory +def export_assets(): + nodes = [] + for table in TABLES: + @asset(name=f"export_{table}", tags=("export",)) + def export(ctx, table=table): + return ctx.result(metadata={"table": table}, materialized=True) + nodes.append(export) + return nodes +``` + +The returned nodes are discovered and added to the graph exactly as if you had written them by hand. They participate in selection, lifecycle, the DAG artifact, and integrations like any other node. + +A factory may return a single node or a list, tuple, or set of nodes. Every returned item must be a decorated `@task`, `@asset`, `@loader`, or `@check` function. + +## Folder rules + +Where a factory lives determines what it is allowed to emit: + +| Location | May emit | +|----------|----------| +| `loaders/` | loaders only | +| `tasks/` | tasks only | +| `assets/` | assets only | +| `checks/` | checks only | +| `factories/` | any kind, including a mix | + +- A **single-kind** factory can live in that kind's folder (e.g. an asset-only factory in `assets/`), keeping it next to the nodes it generates. It can also live in `factories/`. +- A factory that emits **more than one kind** must live in `factories/`. + +This keeps each kind folder honest: everything in `assets/`, whether hand-written or factory-generated, is an asset. + +SQLBuild enforces this at discovery time. A factory in a kind folder that returns a foreign kind raises an error pointing you to `factories/`: + +``` +Factory export_pipeline in assets/ returned a loader 'raw_orders'; +mixed-kind factories must live in factories/. +``` + +The `factories/` folder is not created by `sqb init` - add it when you need it. + +## Mixed-kind factories + +A factory in `factories/` can generate a whole related pipeline at once - a loader, the asset that reads it, and a check on the result: + +```python +# factories/orders.py +from sqlbuild.factories import factory +from sqlbuild.loaders import loader +from sqlbuild.assets import asset +from sqlbuild.checks import check + + +@factory +def orders_pipeline(): + @loader(name="raw_orders") + def load(ctx): + return fetch_orders() + + @asset(name="orders_export", depends_on=load) + def export(ctx): + return ctx.result(materialized=True) + + @check(depends_on=export) + def orders_export_check(ctx): + return ctx.pass_("export ready") + + return [load, export, orders_export_check] +``` + +Generated nodes follow the same rules as directly-authored ones, including the [SQL boundary](/docs/concepts/python-nodes/overview#the-sql-boundary): a factory-generated loader still binds to a managed source, and a factory-generated check still may not depend on SQL models. + +## Naming + +Generated nodes need unique names across the project. Pass an explicit `name=` to each node a factory creates (factories almost always generate names from a loop variable or config), since relying on the function's own name would produce duplicates. diff --git a/website/src/content/docs/docs/concepts/python-nodes/loaders.mdx b/website/src/content/docs/docs/concepts/python-nodes/loaders.mdx new file mode 100644 index 0000000000..f0d928638b --- /dev/null +++ b/website/src/content/docs/docs/concepts/python-nodes/loaders.mdx @@ -0,0 +1,372 @@ +--- +title: "Loaders" +description: "Load external data into source tables with Python functions." +--- + +Loaders are Python functions that load data into source tables. They replace expression sources and manual ETL scripts with code that lives inside your project, runs as part of the build, and supports incremental write strategies. Loaders are one of the four [Python node](/docs/concepts/python-nodes/overview) kinds, and the only one that writes into a SQL source. + +## How it works + +1. Write a Python function under `loaders/` decorated with `@loader` +2. Declare a managed source in `sources/*.yml` with `managed: true` and **the same name as the loader function** +3. SQLBuild calls the function, writes returned rows to a staging table, then applies the configured write strategy to the target + +Loaders participate in the build lifecycle. When `sqb build` runs, managed sources are loaded before any dependent model is materialized. + +## Defining a loader + +Place Python files under `loaders/` in your project directory. Each file can contain one or more loader functions: + +```python +# loaders/raw_sources.py +from sqlbuild.loaders import loader +from sqlbuild.executor.load.models import LoaderContext + +@loader +def raw_customers(ctx: LoaderContext) -> list[dict[str, object]]: + return [ + {"id": 1, "name": "Leslie Knope", "email": "leslie@pawnee.gov"}, + {"id": 2, "name": "Ron Swanson", "email": "ron@pawnee.gov"}, + ] +``` + +The function receives a `LoaderContext` and returns rows as a list of dicts, an iterator of dicts, or `None` for self-managed loaders. + +## Binding to a source + +Declare a managed source in `sources/*.yml`. A managed source is bound to the loader function **with the same name** - there is no separate `loader` field: + +```yaml +sources: + - name: raw_customers + managed: true + write_strategy: table + columns: + - name: id + type: INTEGER + - name: name + type: VARCHAR + - name: email + type: VARCHAR +``` + +Setting `managed: true` makes this a **managed source** - SQLBuild owns both the loading and the schema. The binding is by name: the source `raw_customers` is populated by the `@loader` function named `raw_customers`. SQLBuild raises an error if a managed source has no loader function of the same name. + +Models reference managed sources the same way as any other source: + +```sql +SELECT id, name FROM __source("raw_customers") +``` + +## Write strategies + +The `write_strategy` field controls how returned rows are written to the target table. + +### table + +Full replace. The target is dropped and recreated from the loader output on every run. + +```yaml +sources: + - name: raw_countries + managed: true + write_strategy: table + columns: + - name: country_id + type: INTEGER + - name: country_code + type: VARCHAR +``` + +### append + +Insert all returned rows into the target. No deduplication. + +```yaml +sources: + - name: raw_webhook_events + managed: true + write_strategy: append + columns: + - name: event_id + type: INTEGER + - name: event_name + type: VARCHAR +``` + +### delete_insert + +Delete rows in the cursor range, then insert replacements. Requires `cursor_column`. + +```yaml +sources: + - name: raw_order_events + managed: true + write_strategy: delete_insert + cursor_column: event_at + columns: + - name: event_id + type: INTEGER + - name: event_at + type: TIMESTAMP + - name: amount_cents + type: INTEGER +``` + +The loader receives `ctx.current_cursor_value` with the current `MAX(cursor_column)` from the target, so it can fetch only new or updated data. Its function name matches the source name (`raw_order_events`): + +```python +@loader +def raw_order_events(ctx: LoaderContext) -> list[dict[str, object]]: + if ctx.current_cursor_value is None: + return fetch_all_events() + return fetch_events_since(ctx.current_cursor_value) +``` + +### merge + +Upsert based on `unique_key`. Requires both `unique_key` and `cursor_column`. + +```yaml +sources: + - name: raw_customers + managed: true + write_strategy: merge + unique_key: customer_id + cursor_column: updated_at + columns: + - name: customer_id + type: INTEGER + - name: plan_name + type: VARCHAR + - name: updated_at + type: TIMESTAMP +``` + +Existing rows matching the unique key are updated; new rows are inserted. + +## Self-managed loaders + +If a loader returns `None`, SQLBuild skips its row-writing pipeline. The loader is responsible for writing data to the target itself, using whatever approach makes sense - `ctx.execute_sql()`, an external library, a subprocess, or anything else: + +```python +@loader +def raw_status(ctx: LoaderContext) -> None: + ctx.execute_sql(f"DROP TABLE IF EXISTS {ctx.destination}") + ctx.execute_sql( + f"CREATE TABLE {ctx.destination} AS " + "SELECT 1 AS status_id, 'loaded' AS status_name" + ) +``` + +The source is still declared as managed, just without a `write_strategy`: + +```yaml +sources: + - name: raw_status + managed: true + columns: + - name: status_id + type: INTEGER + - name: status_name + type: VARCHAR +``` + +Self-managed loaders must not declare a `write_strategy`. They are useful when you want to use adapter-specific SQL (e.g. `COPY INTO`, external tables), call an external ingestion tool like [dlt](/docs/integrations/dlt), or handle writes in a way that doesn't fit the dict-return pattern. + +## Loader context + +Every loader function receives a `LoaderContext` as its first argument. It provides access to the destination relation, cursor state, active target, and helper methods. + +### Properties + +| Property | Type | Description | +|----------|------|-------------| +| `destination` | `str` | Fully-qualified destination relation name (where rows are written) | +| `destination_database` | `str \| None` | Destination database | +| `destination_schema` | `str \| None` | Destination schema | +| `destination_name` | `str` | Unqualified destination table name | +| `current_cursor_value` | `object \| None` | Current `MAX(cursor_column)` from the destination, or `None` if the table does not exist or has no cursor column | +| `run_id` | `str` | Unique identifier for this execution run | +| `target` | `str \| None` | Active target name (e.g. `dev`, `prod`) | +| `vars` | `dict` | Project variables (merged from project, target, and local config) | +| `is_reload` | `bool` | `True` when `--reload` was passed | +| `start_cursor_ts` | `datetime \| None` | Timestamp cursor start override from `--start-cursor-ts` | +| `end_cursor_ts` | `datetime \| None` | Timestamp cursor end override from `--end-cursor-ts` | +| `start_cursor_int` | `int \| None` | Integer cursor start override from `--start-cursor-int` | +| `end_cursor_int` | `int \| None` | Integer cursor end override from `--end-cursor-int` | +| `adapter` | `BaseAdapter` | The database adapter instance | +| `connection` | `object` | The active database connection | +| `logger` | `Logger` | Python logger scoped to the loader | + +### Methods + +| Method | Description | +|--------|-------------| +| `execute_sql(sql)` | Execute a SQL statement against the connection | +| `query(sql)` | Execute a SQL query and return the cursor | +| `log(message)` | Log a message to the execution lifecycle output | +| `qualify_name(name)` | Return a fully-qualified relation name in the destination database/schema | +| `skip(reason, mode=...)` | Skip this loader. `mode` is `"soft"` (default, skip only this loader) or `"hard"` (also block dependents) | +| `result(payload=, metadata=, materialized=)` | Return a structured result for a self-managed loader | +| `result_of(node_fn)` | Read the latest persisted result of an upstream node (current or previous run) | +| `results_of(node_fn, limit=N)` | Read the last N successful results of an upstream node, newest first | +| `loader(loader_fn)` | Return a `LoaderRelationRef` for an upstream loader dependency | +| `source(source_name)` | Return a `LoaderRelationRef` for a project source by YAML name | + +### LoaderRelationRef + +Returned by `ctx.loader()` and `ctx.source()`. Provides access to an upstream relation: + +| Property / Method | Description | +|-------------------|-------------| +| `destination` | Fully-qualified relation name | +| `current_cursor_value` | Current `MAX(cursor_column)` from the relation | +| `max(column)` | Return the `MAX` of any column from the relation | + +## Loader dependencies + +Loaders can depend on other loaders using `depends_on`. Dependencies are executed first, and their destination relations are available via `ctx.loader()`: + +```python +from sqlbuild.loaders import loader +from sqlbuild.executor.load.models import LoaderContext + +@loader +def raw_accounts(ctx: LoaderContext) -> list[dict[str, object]]: + return [ + {"account_id": 1, "account_name": "Pawnee Parks"}, + {"account_id": 2, "account_name": "Eagleton"}, + ] + +@loader(depends_on=[raw_accounts]) +def raw_account_metrics(ctx: LoaderContext) -> list[dict[str, object]]: + accounts = ctx.loader(raw_accounts) + rows = ctx.query(f"SELECT account_id FROM {accounts.destination}") + return [ + {"account_id": row[0], "metric": "active"} + for row in rows.fetchall() + ] +``` + +Dependencies form a DAG. SQLBuild schedules loaders in topological order and executes independent loaders concurrently when `--concurrency` is set. + +Intermediate loaders (those referenced only via `depends_on`, with no managed source of the same name) are given synthetic source entries and write to `__loader__` tables by default. Only the terminal loader - the one whose name matches a managed source - populates that source; intermediate loaders feed it. Use the `destination` parameter on the decorator to override the intermediate relation: + +```python +@loader(destination="staging.shared_accounts") +def raw_accounts(ctx: LoaderContext): + ... +``` + +## Decorator parameters + +The `@loader` decorator accepts optional parameters that can also be set in the source YAML. When both are specified, the YAML takes precedence. + +| Parameter | Description | +|-----------|-------------| +| `depends_on` | List of loader functions this loader depends on | +| `destination` | Override the destination relation name (can include schema or database) | +| `write_strategy` | `table`, `append`, `delete_insert`, or `merge` | +| `cursor_column` | Column used for incremental cursor tracking | +| `unique_key` | Column(s) used as the merge key (string or list of strings) | +| `columns` | Column specifications with name, type, nullable, and description | +| `contract` | `enforced` or `none` | + +## Auto-load during builds + +By default, `sqb build` automatically loads managed sources before building dependent models. This is controlled by the `auto_load_sources` setting: + +```toml +[settings] +auto_load_sources = true # default +``` + +You can also control this per-run with CLI flags: + +```bash +# Explicitly load sources before building +sqb build --load + +# Skip source loading +sqb build --no-load + +# Reload sources (passes is_reload=True to loaders) +sqb build --reload +``` + +When `--reload` is passed, `ctx.is_reload` is `True` in the loader function. This lets loaders implement different behavior for full reloads versus normal incremental loads. + +## Source deferral + +Loader writes and managed source reads are resolved separately. `loader_schema` controls +where a target's loaders write; `defer_sources_to` optionally selects another target whose +managed sources models should read. + +```toml +[targets.dev] +schema = "analytics_dev" +loader_schema = "raw_dev" +defer_sources_to = "prod" + +[targets.prod] +schema = "analytics_prod" +loader_schema = "raw_prod" +``` + +With this config: + +- `sqb load --target dev` writes to `raw_dev`. +- `sqb load --target prod` writes to `raw_prod`. +- Models built in dev read from `raw_prod` because dev defers source reads to prod. +- Models built in prod read from `raw_prod` because omitted deferral defaults to the active target. + +If `loader_schema` is omitted, loaders fall back to the active target's model `schema`, then +the adapter default schema. An explicit `schema` on a managed source overrides the target +default. SQLBuild validates final managed-loader write namespaces and rejects two targets +on the same warehouse/database that could write to the same schema. + +`defer_sources_to` never redirects loader writes. It only changes `__source()` and Python +`source()` reads. This lets developers test loaders in an isolated schema without making +development models consume partial development source data. + +## Schema evolution + +When a loader returns rows with columns not present in the existing target table, SQLBuild detects the schema change and adds the new columns automatically. Type mismatches between the staging table and the existing target raise an error. + +## Project structure + +``` +my-project/ + loaders/ + raw_sources.py # loader functions + api_sources.py # more loader functions + sources/ + raw.yml # managed source declarations (managed: true) + models/ + staging/ + stg_customers.sql # __source("raw_customers") +``` + +SQLBuild discovers all `.py` files under `loaders/` recursively (excluding `__init__.py` and files starting with `_`). Each file is scanned for functions decorated with `@loader`. + +## Config reference + +### Source YAML fields for managed sources + +| Field | Description | +|-------|-------------| +| `managed` | Set to `true` to bind the source to the `@loader` function of the same name | +| `write_strategy` | `table`, `append`, `delete_insert`, or `merge` (requires `managed: true`) | +| `cursor_column` | Column for incremental cursor tracking (required for `delete_insert` and `merge`) | +| `unique_key` | Merge key column(s) (required for `merge`) | +| `columns` | Column declarations with types | +| `contract` | `enforced` or `none` | + +### Validation rules + +- `append` cannot have `unique_key` +- `merge` requires `unique_key` +- `table` cannot have `cursor_column` or `unique_key` +- `delete_insert` requires `cursor_column` and cannot have `unique_key` +- `cursor_column` requires one of `append`, `delete_insert`, or `merge` diff --git a/website/src/content/docs/docs/concepts/python-nodes/overview.mdx b/website/src/content/docs/docs/concepts/python-nodes/overview.mdx new file mode 100644 index 0000000000..d051238541 --- /dev/null +++ b/website/src/content/docs/docs/concepts/python-nodes/overview.mdx @@ -0,0 +1,162 @@ +--- +title: "Overview" +description: "Tasks, assets, loaders, and checks as first-class nodes in the SQLBuild graph." +--- + +Python nodes let your project grow beyond warehouse-only SQL while keeping the SQL graph clean. They are ordinary Python functions, decorated to become nodes in the same DAG as your SQL models, and they run as part of `sqb build`. + +There are four kinds, authored in dedicated top-level folders: + +| Kind | Decorator | Folder | Purpose | +|------|-----------|--------|---------| +| Loader | `@loader` | `loaders/` | Load external data into a managed source | +| Task | `@task` | `tasks/` | Run Python computation or side effects | +| Asset | `@asset` | `assets/` | Produce or observe an external artifact | +| Check | `@check` | `checks/` | Validate tasks, assets, and loaders | + +All four share the same decorator conventions, dependency model, selection syntax, and runtime context helpers. The pages for each kind cover their specifics: + +- [Loaders](/docs/concepts/python-nodes/loaders) - load data into managed sources +- [Tasks](/docs/concepts/python-nodes/tasks) - Python computation and side effects +- [Assets](/docs/concepts/python-nodes/assets) - external artifacts +- [Checks](/docs/concepts/python-nodes/checks) - Python validations +- [Factories](/docs/concepts/python-nodes/factories) - generate nodes programmatically with `@factory` +- [SQL references](/docs/concepts/python-nodes/sql-references) - read SQL models and sources from Python + +Nodes can also be generated programmatically with [`@factory`](/docs/concepts/python-nodes/factories) instead of authored one at a time. + +## The SQL boundary + +The most important rule: **SQL models never depend on Python nodes.** The dependency direction is strictly one way. + +- SQL models depend only on other SQL resources (models, sources, seeds, functions). +- Python nodes may depend on other Python nodes. +- The only way Python data reaches SQL is through a **loader populating a source**: `loader -> source -> model`. +- Python nodes may **read** SQL models and sources at runtime through typed references (see [SQL references](/docs/concepts/python-nodes/sql-references)), but reading a model does not make it a SQL dependency. + +This keeps the SQL graph fully analyzable and testable on its own, while letting Python participate around the edges. + +``` + depends_on + task ----------------> task + | | + | (read only) v + | asset + v | + source <-- loader | check (validates tasks/assets/loaders) + | + v + model (SQL) ----> model (SQL) +``` + +## Decorators + +Every decorator accepts the same organizational metadata: + +| Argument | Description | +|----------|-------------| +| `depends_on` | A single function, tuple, or list of upstream nodes (and, where allowed, `model()`/`source()` references) | +| `tags` | Labels for selection, filtering, and catalog grouping | +| `group` | A display/catalog grouping string | +| `description` | Human-readable docs (defaults to the function docstring) | +| `meta` | Freeform JSON metadata for catalogs and integrations | + +`@task` and `@asset` also accept a `retry` policy. `@asset` additionally accepts `columns` and `column_lineage`. Node kind is inferred from the decorator - you never pass `kind=`. + +## Identity tracking + +Every Python node is fingerprinted by source code hash, decorator config hash, and transitive dependency hashes (scoped to the git root, so third-party package updates don't affect identities). The plan shows source and dependency diffs when a node's identity changes, giving you visibility into what changed in your Python code. + +Unlike SQL models, Python nodes may depend on external inputs the framework cannot observe (APIs, files, third-party services). Skip/run decisions are therefore user-controlled via `ctx.skip()`: the node's own logic decides whether it needs to run. See [Planning and Change Detection](/docs/concepts/planning#python-nodes) for details. + +## Runtime context + +Each node receives a context object as its first argument (`TaskContext`, `AssetContext`, `CheckContext`, or `LoaderContext`). They share these helpers: + +| Helper | Description | +|--------|-------------| +| `ctx.run_id` | Unique identifier for this run | +| `ctx.target` | Active target name | +| `ctx.vars` | Project variables | +| `ctx.is_reload` | `True` when `--reload` was passed | +| `ctx.adapter` / `ctx.connection` | Adapter and live connection | +| `ctx.log(message)` | Log to the run output | +| `ctx.query(sql)` / `ctx.execute_sql(sql)` | Run SQL on the connection | +| `ctx.qualify_name(name)` | Qualify a relation name | +| `ctx.relation(ref)` | Resolve a declared `model()`/`source()` reference to a relation | +| `ctx.result_of(node_fn)` | Read the latest persisted result of an upstream node (current or previous run) | +| `ctx.results_of(node_fn, limit=N)` | Read the last N successful results of an upstream node, newest first | +| `ctx.providers` | Access discovered [providers](/docs/concepts/python-nodes/providers) by name | + +Task and asset contexts add `ctx.result(...)` and `ctx.skip(...)`. Check contexts add `ctx.pass_(...)`, `ctx.fail(...)`, and `ctx.warn(...)`. + +Providers can also be injected directly as function parameters by name. See [Providers](/docs/concepts/python-nodes/providers) for details. + +## Returns and skips + +Tasks and assets return through `ctx.result(...)`: + +```python +@task +def export_orders(ctx): + return ctx.result(payload={"rows": 120}, metadata={"rows": 120}) +``` + +- A plain value or `None` is also accepted and normalized to a successful result. +- `ctx.skip(reason, mode=...)` skips the node. `mode` accepts `"soft"` (default, skips only this node) or `"hard"` (also blocks dependents), as a string or the `SkipMode` enum from `sqlbuild.tasks`/`sqlbuild.assets`. +- Assets may pass `materialized=True`/`False` to record whether an artifact was produced. + +Downstream nodes run only if at least one upstream succeeded. If all upstreams are skipped, the downstream is skipped. A failed or hard-skipped upstream blocks its dependents. + +## Result persistence + +Node results (payload, metadata, status, errors) are persisted after each execution. Results are stored in `_sqlbuild_node_results` in the warehouse alongside your data. Results persist across runs, so they are available for observability, debugging, and downstream consumption. + +## Selection + +Python nodes are selected like SQL resources, by bare name or typed selector: + +```bash +sqb build --select export_orders # bare name +sqb build --select task:export_orders # typed +sqb build --select asset:orders_export +sqb build --select check:check_orders +sqb build --select tag:exports # by tag +sqb build --select +orders_export # with upstreams +``` + +Names are globally unique across models, sources, seeds, functions, loaders, tasks, assets, checks, providers, and hooks. + +## Lifecycle: run, build, check + +Python nodes run in two phases relative to SQL: + +- **Ingress (pre-SQL):** loaders, and tasks/assets that feed sources, run before SQL models are built. +- **Read-side (post-SQL):** tasks/assets that read SQL run after their SQL dependencies are built. + +The commands differ in what they include by default: + +| Command | SQL | Loaders / tasks / assets | Checks | Audits | +|---------|-----|--------------------------|--------|--------| +| `sqb build` | Yes | Yes | Yes | Yes | +| `sqb build --no-tests --no-audits` | Yes | Yes | No | No | +| `sqb check` | No | No | Selected checks only | No | + +- `sqb build` is the complete build-and-validate command: it runs SQL, the required Python nodes, SQL audits, and Python checks. +- `sqb build --no-tests --no-audits` executes the DAG without validation, for fast iteration. +- `sqb check` runs Python checks only. See [`sqb check`](/docs/cli/check). + +Use `--no-python` on `plan` and `build` to suppress read-side tasks/assets. Loader-side Python required to populate selected sources still runs (use `--no-load` to skip source loading). See the [`sqb build`](/docs/cli/build) reference. + +## Try it + +The `python_nodes` playground is a small working project with a task, loader, model, asset, and check: + +```bash +sqb playground --template python_nodes +cd sqlbuild-playground +sqb build --select +fact_orders --select +orders_export +sqb check --select +check_orders_export +``` + +See [`sqb playground`](/docs/cli/playground). diff --git a/website/src/content/docs/docs/concepts/python-nodes/providers.mdx b/website/src/content/docs/docs/concepts/python-nodes/providers.mdx new file mode 100644 index 0000000000..3d049b2715 --- /dev/null +++ b/website/src/content/docs/docs/concepts/python-nodes/providers.mdx @@ -0,0 +1,193 @@ +--- +title: "Providers" +description: "Shared runtime services for Python nodes and hooks." +--- + +Providers are shared runtime services that SQLBuild discovers, configures, and injects into your Python nodes and hooks. Use them for external connections, API clients, or any stateful service that multiple nodes need access to. + +## Defining a provider + +Create a Python file under `providers/` in your project. A provider is a class that subclasses `Provider` from `sqlbuild.providers`: + +```python +# providers/warehouse_client.py +from sqlbuild.providers import Provider + +class WarehouseClient(Provider): + api_key: str + endpoint: str = "https://api.example.com" + + def setup(self, ctx): + self.session = create_session(self.api_key, self.endpoint) + + def teardown(self): + self.session.close() +``` + +`Provider` extends [pydantic-settings `BaseSettings`](https://docs.pydantic.dev/latest/concepts/pydantic_settings/), so provider fields are validated and can be populated from environment variables automatically. + +## Provider name + +Each provider has a runtime name used for injection. By default, the name is derived from the class name by converting to `lower_snake_case`: + +- `WarehouseClient` becomes `warehouse_client` +- `SlackNotifier` becomes `slack_notifier` + +Override the name explicitly with `provider_name`: + +```python +class WarehouseClient(Provider): + provider_name = "warehouse" + api_key: str +``` + +Provider names must be valid Python identifiers (`lower_snake_case`). They share the global project resource namespace with models, sources, seeds, functions, loaders, tasks, assets, checks, and hooks. + +## Using providers in Python nodes + +Providers are injected into Python node functions by **parameter name**. Add a parameter whose name matches the provider's runtime name: + +```python +from sqlbuild.tasks import task + +@task +def export_orders(ctx, warehouse_client): + warehouse_client.session.upload(ctx.query("SELECT * FROM orders")) +``` + +SQLBuild matches the parameter name `warehouse_client` to the discovered provider with that name, sets it up if it hasn't been already, and passes it to the function. + +You can also type-annotate the parameter for IDE support and compile-time validation: + +```python +from sqlbuild.tasks import task +from providers.warehouse_client import WarehouseClient + +@task +def export_orders(ctx, warehouse_client: WarehouseClient): + warehouse_client.session.upload(ctx.query("SELECT * FROM orders")) +``` + +When a type annotation is present, SQLBuild validates that the discovered provider is an instance of the annotated class. A mismatch raises a compile-time error. + +Provider injection works in all Python node types: + +- **Loaders** (`@loader`) +- **Tasks** (`@task`) +- **Assets** (`@asset`) +- **Checks** (`@check`) + +## Using providers in hooks + +Python lifecycle hooks also support provider injection by parameter name: + +```python +# hooks/python/notify.py +from sqlbuild.hooks import hook + +@hook +def notify_complete(ctx, slack_notifier): + slack_notifier.send(f"Model {ctx.model_name} built successfully") +``` + +```sql +MODEL ( + materialized table, + post_hooks [python("notify_complete")], +); +``` + +Providers are also available on the `HookContext` via `ctx.providers`: + +```python +@hook +def notify_complete(ctx): + notifier = ctx.providers.slack_notifier + notifier.send(f"Model {ctx.model_name} built successfully") +``` + +## Using providers via context + +All Python node contexts (`TaskContext`, `AssetContext`, `CheckContext`, `LoaderContext`) and `HookContext` expose a `ctx.providers` container for name-based access: + +```python +@task +def export_orders(ctx): + client = ctx.providers.warehouse_client + client.session.upload(ctx.query("SELECT * FROM orders")) +``` + +Both approaches (parameter injection and `ctx.providers`) are equivalent. Parameter injection is more explicit and enables type checking; `ctx.providers` is useful when provider access is conditional or dynamic. + +## Lifecycle + +Providers follow a lazy setup, reverse-teardown lifecycle scoped to the command invocation: + +1. **Discovery** - on compile, SQLBuild discovers all `Provider` subclasses under `providers/` and validates their settings (from environment variables or field defaults). +2. **Lazy setup** - `setup(ctx)` is called the first time a provider is accessed during a build, not at startup. Providers that are never used are never set up. +3. **Teardown** - after the command completes, `teardown()` is called on all providers that were set up, in reverse setup order. Teardown runs even if the build failed. + +```python +class WarehouseClient(Provider): + api_key: str + + def setup(self, ctx): + # Called once, the first time any node accesses this provider + self.connection = connect(self.api_key) + + def teardown(self): + # Called after the command completes + self.connection.close() +``` + +Both `setup` and `teardown` are optional. A provider with only field declarations and no lifecycle methods is valid; it acts as a validated configuration object. + +## Configuration from environment variables + +Because `Provider` extends `pydantic-settings BaseSettings`, fields without defaults are read from environment variables. The environment variable name matches the field name in uppercase: + +```python +class SlackNotifier(Provider): + slack_token: str # reads SLACK_TOKEN from environment + channel: str = "#builds" # has a default, environment variable is optional +``` + +See the [pydantic-settings documentation](https://docs.pydantic.dev/latest/concepts/pydantic_settings/) for advanced configuration like custom env prefixes, `.env` file support, and nested settings. + +## Discovery rules + +- Provider classes are discovered from `.py` files under `providers/` recursively +- Files named `__init__.py` or starting with `_` are skipped +- Each concrete (non-abstract) subclass of `Provider` is registered +- Provider names must be globally unique across project resources +- Settings are validated at discovery time. Missing required fields (without environment variables set) raise a discovery error immediately, not at runtime + +## Plan output + +When providers are used by Python nodes or hooks, `sqb plan` shows a Providers section listing each provider and its consumers: + +``` +Providers + warehouse_client 2 nodes + slack_notifier 1 node +``` + +Use `--verbose` to see which specific nodes consume each provider. + +## Project layout + +``` +my-project/ + providers/ + warehouse_client.py + slack_notifier.py + hooks/ + sql/ + record_notification.sql + python/ + notify.py + loaders/ + load_orders.py + tasks/ + export_orders.py +``` diff --git a/website/src/content/docs/docs/concepts/python-nodes/sql-references.mdx b/website/src/content/docs/docs/concepts/python-nodes/sql-references.mdx new file mode 100644 index 0000000000..761ea84051 --- /dev/null +++ b/website/src/content/docs/docs/concepts/python-nodes/sql-references.mdx @@ -0,0 +1,67 @@ +--- +title: "SQL References" +description: "Read SQL models and sources from Python nodes without creating SQL dependencies." +--- + +import Warning from '../../../../../components/mintlify/Warning.astro'; + +Python nodes can **read** SQL models and sources at runtime, but they cannot depend on them as graph edges (see the [SQL boundary](/docs/concepts/python-nodes/overview#the-sql-boundary)). Typed references make this read-only access explicit and safe. + +## Declaring a reference + +Use `model()` and `source()` from `sqlbuild.refs` in a node's `depends_on`: + +```python +from sqlbuild.refs import model, source +from sqlbuild.tasks import task + + +@task(depends_on=model("fact_orders")) +def export_orders(ctx): + ... + + +@task(depends_on=source("raw_orders")) +def inspect_raw(ctx): + ... +``` + +Declaring the reference does two things: + +1. It tells SQLBuild the node reads that SQL resource, so the node is scheduled **after** the resource is built (read-side). +2. It authorizes `ctx.relation(...)` to resolve that reference at runtime. + +It does **not** make the SQL resource depend on the Python node. The dependency is one-way: Python reads SQL, never the reverse. + +## Resolving with ctx.relation + +Always resolve a reference to its concrete relation with `ctx.relation(...)` instead of hardcoding the table name: + +```python +@task(depends_on=model("fact_orders")) +def export_orders(ctx): + relation = ctx.relation(model("fact_orders")) + rows = ctx.query(f"SELECT * FROM {relation}").fetchall() + return ctx.result(metadata={"rows": len(rows)}) +``` + +`ctx.relation(...)` returns the adapter-qualified relation name for the current run. Passing a reference that was not declared in `depends_on` raises an error. + + +Do not hardcode model or source names in raw SQL. A model's relation depends on the target, its schema and any deferral, so a query like `ctx.query("SELECT * FROM fact_orders")` can fail or read the wrong relation. Always use `ctx.relation(model("fact_orders"))`. + + +## model vs source + +| Reference | Resolves to | +|-----------|-------------| +| `model("name")` | The built model relation | +| `source("name")` | The source read relation, following deferred-source semantics | + +`source(...)` respects `defer_sources_to`, so a node reading a source in `dev` can read the deferred target's data just like SQL models do. + +## Where references are allowed + +- **Tasks** and **assets** may declare `model()` and `source()` references and read them with `ctx.relation(...)`. +- **Checks** may not reference SQL resources. Validate SQL with [audits](/docs/concepts/audits). +- A Python node referencing a SQL resource never turns into a SQL dependency; selector expansion will not pull Python outputs into SQL model dependencies. diff --git a/website/src/content/docs/docs/concepts/python-nodes/tasks.mdx b/website/src/content/docs/docs/concepts/python-nodes/tasks.mdx new file mode 100644 index 0000000000..5531605fad --- /dev/null +++ b/website/src/content/docs/docs/concepts/python-nodes/tasks.mdx @@ -0,0 +1,134 @@ +--- +title: "Tasks" +description: "Run Python computation and side effects as nodes in the SQLBuild graph." +--- + +Tasks are Python functions that run as part of the DAG. Use them for computation, side effects, and orchestration steps that are not loading data into a source and not producing a tracked artifact. See [Python Nodes](/docs/concepts/python-nodes/overview) for the shared model and the SQL boundary rules. + +## Defining a task + +Place Python files under `tasks/` and decorate functions with `@task`: + +```python +# tasks/orders.py +from sqlbuild.tasks import task, TaskContext + + +@task +def export_orders(ctx: TaskContext): + rows = fetch_orders() + return ctx.result(payload={"rows": len(rows)}, metadata={"rows": len(rows)}) +``` + +The task receives a `TaskContext` and returns through `ctx.result(...)`. A plain return value or `None` is also accepted and normalized to a successful result. + +## Dependencies + +Tasks declare dependencies with `depends_on`, accepting a single function, a tuple, or a list: + +```python +@task +def fetch_orders(ctx: TaskContext): + return ctx.result(payload=download_orders()) + + +@task(depends_on=fetch_orders) +def summarize_orders(ctx: TaskContext): + result = ctx.result_of(fetch_orders) + return ctx.result(metadata={"count": len(result.payload)}) +``` + +`ctx.result_of(node_fn)` reads the latest persisted result of an upstream node, returning a `NodeResultEnvelope` with `payload`, `metadata`, `status`, and `ts` fields. Results persist across runs. Use `ctx.results_of(node_fn, limit=N)` to read result history. Reading a missing or unsuccessful upstream raises unless you pass `default=`. + +Tasks may depend on other tasks, assets, and loaders. They may **not** depend on SQL models or sources as graph dependencies, but they can read them at runtime with typed references - see [SQL references](/docs/concepts/python-nodes/sql-references). + +## Returning results + +```python +@task +def build_export(ctx: TaskContext): + return ctx.result( + payload={"path": "/exports/orders.csv"}, + metadata={"rows": 1200}, + ) +``` + +- `payload` is the value downstream nodes read with `ctx.result_of(...)`. +- `metadata` is structured JSON for catalogs and downstream reads. +- Tasks cannot set `materialized` - that is for [assets](/docs/concepts/python-nodes/assets). + +## Skipping + +Return `ctx.skip(...)` to skip a task: + +```python +@task +def export_if_present(ctx: TaskContext): + if not new_files_available(): + return ctx.skip("no new files") + return ctx.result(payload=do_export()) +``` + +- `"soft"` (default) skips only this task; dependents may still run if another upstream succeeded. +- `"hard"` skips this task and blocks its dependents. + +`mode` accepts either a plain string or the `SkipMode` enum: + +```python +from sqlbuild.tasks import task, SkipMode + + +@task +def optional_step(ctx): + return ctx.skip("nothing to do", mode=SkipMode.HARD) # or mode="hard" +``` + +## Retries + +`@task` accepts a `retry` policy for transient failures: + +```python +from sqlbuild.retries import RetryPolicy +from sqlbuild.tasks import task + + +@task(retry=RetryPolicy(max_attempts=3, retry_on=(ConnectionError,))) +def call_api(ctx): + return ctx.result(payload=fetch_from_flaky_api()) +``` + +| Field | Default | Description | +|-------|---------|-------------| +| `max_attempts` | `3` | Total attempts including the first | +| `retry_on` | `Exception` | Exception class, tuple, or list to retry on | +| `initial_delay_seconds` | `1.0` | Delay before the first retry | +| `backoff_multiplier` | `2.0` | Exponential backoff multiplier | +| `max_delay_seconds` | `30.0` | Cap on any single delay | +| `max_elapsed_seconds` | `None` | Overall time bound for retries | +| `jitter` | `True` | Randomize delays to avoid synchronized retries | + +The default is no retry. Set `retry_on` explicitly rather than relying on the broad default when you can. The original exception is preserved if all attempts fail. + +## Decorator parameters + +| Parameter | Description | +|-----------|-------------| +| `name` | Override the node name (defaults to the function name) | +| `depends_on` | Upstream nodes (function, tuple, or list); `model()`/`source()` for read-only SQL refs | +| `tags` | Labels for selection and grouping | +| `group` | Display/catalog grouping | +| `description` | Docs (defaults to docstring) | +| `meta` | Freeform JSON metadata | +| `retry` | A `RetryPolicy` | + +## Running tasks + +```bash +# Run a task and its required graph +sqb build --select export_orders --no-tests --no-audits + +# Include in a full build +sqb build --select export_orders +``` + +Tasks run during `sqb build`. They are not validated by checks unless you also write a [check](/docs/concepts/python-nodes/checks) that depends on them. diff --git a/website/src/content/docs/docs/concepts/resource-identities.mdx b/website/src/content/docs/docs/concepts/resource-identities.mdx new file mode 100644 index 0000000000..4510522e4f --- /dev/null +++ b/website/src/content/docs/docs/concepts/resource-identities.mdx @@ -0,0 +1,54 @@ +--- +title: "Resource Identities" +description: "Canonical names for SQLBuild resources, selectors, state, and integrations." +--- + +SQLBuild resource identities use lowercase ASCII snake_case. A name must start with a lowercase +letter, end with a lowercase letter or digit, and contain only lowercase letters, digits, and +underscores. Consecutive underscores are valid, so established names such as +`commerce__mart_v_order` remain canonical. + +This contract applies to models, seeds, sources, SQL and Python functions, generic and singular +audits, attached audit definitions and instance names, SQL tests and parameterized cases, +scenarios, SQL and Python hooks, macros, model schemas, enums, constants, materializations, +providers, event exporters, loaders, tasks, assets, and checks. Private scoped declarations use +exactly one leading underscore, such as `_country_codes`; public declarations must not use a +leading underscore. + +Provider classes are the one conventional derivation: when `provider_name` is omitted, +`AnalyticsApiProvider` resolves to `analytics_api_provider`. An explicit `provider_name` is an +authored identity and must already be canonical snake_case; SQLBuild does not normalize it. + +Names derived from files use the filename stem. For example, `models/daily_orders.sql` defines +the model identity `daily_orders`, while `audits/generic/expression_is_true.sql` defines the +generic audit identity `expression_is_true`. Directories organize resources but do not change +their names. + +Physical warehouse identifiers are separate from SQLBuild resource identities. Database, schema, +table, model alias, column, tag, group, and directory names retain their existing adapter-specific +contracts and do not need to follow this profile. + +## Invalid names + +Compilation fails during discovery with `D016` when an authored identity is not canonical: + +```text +error[D016]: Invalid model identity 'DailyOrders' in models/DailyOrders.sql; +use snake_case 'daily_orders' +``` + +SQLBuild suggests a corrected spelling but never silently normalizes an identity. Silent +normalization would make selectors, manifests, persisted execution state, and integration keys +disagree about which resource ran. + +## Migrating existing projects + +1. Rename file-derived resources and explicit `name` values to snake_case. +2. Update `__ref`, `__seed`, `__source`, function, hook, macro, audit, test, and dependency + references. +3. Update selectors and external integrations, including Dagster asset/check keys, that use the + old identity. +4. Compile before building. + +A rename intentionally creates a new resource identity. Existing fingerprints, audit history, +and other persisted state under the old name are not silently reassigned to the new resource. diff --git a/website/src/content/docs/docs/concepts/rules.mdx b/website/src/content/docs/docs/concepts/rules.mdx new file mode 100644 index 0000000000..3e45134ac5 --- /dev/null +++ b/website/src/content/docs/docs/concepts/rules.mdx @@ -0,0 +1,237 @@ +--- +title: "Overview" +description: "Turn repeated SQL and project review decisions into compiler-enforced requirements." +--- + +import Card from '../../../../components/mintlify/Card.astro'; +import CardGroup from '../../../../components/mintlify/CardGroup.astro'; + +Code review is an expensive place to keep enforcing a decision the team has already made. + +Comments such as these are useful once, but repetitive after that: + +- “This query needs deterministic ordering before `LIMIT`." +- “Move this model into the agreed area and layer." +- “Final models cannot depend on another area's private intermediate models." +- “This incremental output needs a declared grain, an enforced contract, and a focused test." +- “External sources must enter through staging." + +Rules turn those decisions into configurable compile-time requirements. They run over the same +project representation SQLBuild uses to produce executable artifacts, so a failed Rule is a failed +compile rather than an optional report somebody must remember to run. + +## What Rules are for + +Start with built-in Rules, then add custom Rules for requirements specific to your project. + +- **Built-in Rules** are opinionated, reusable checks for areas such as SQL safety, contracts, and + dependency boundaries. Select or ignore each Rule individually. +- **Custom Rules** encode local decisions such as directory structure, architectural boundaries, + public interfaces, or the contract and test coverage required for a particular kind of model. + +Both evaluate authored SQL and the compiled project without querying warehouse data. They use +compiler-owned facts about SQL, models, paths, configuration, columns, contracts, tests, audits, +declarations, and dependencies. + +That context can be combined in one check—for example, allowing `SELECT *` in staging while requiring +contracted outputs in final models, or allowing cross-area dependencies only through an `interface` +model. + +## How Rules work + +Use `sqb rules show` to understand a Rule before enabling it: + +```bash +sqb rules show SQBRSQL004 +``` + +```text +SQBRSQL004: Row selection is nondeterministic +Family: SQBRSQL +Slug: unordered-limit +Subject: model +Enabled by default: yes +Remediation: Add ORDER BY with a deterministic tie-breaker before LIMIT or OFFSET. +``` + +`Enabled by default` means the Rule is included when a matching family is selected. Configurable +Rules do not run unless `select` contains an exact code or matching family. + +Select Rules in `sqlbuild_project.toml`. This example enables the complete built-in SQL family: + +```toml +[rules] +select = ["SQBRSQL"] +``` + +An exact code such as `SQBRSQL004` selects one Rule. A family code such as `SQBRSQL` selects every +Rule in that family. An empty `select` disables configurable Rules, but mandatory compiler +correctness still applies. + +Now consider a model that returns an arbitrary row because its `LIMIT` has no ordering: + +```sql +-- models/orders.sql +SELECT order_id FROM orders LIMIT 1 +``` + +Run normal compilation: + +```bash +sqb compile +``` + +The selected Rule reports the source location, problem, and remediation: + +```text +error[SQBRSQL004]: Row selection is nondeterministic + --> models/orders.sql:2:36 + | + 2 | SELECT order_id FROM orders LIMIT 1 + | ^ + = help: Add ORDER BY with a deterministic tie-breaker before LIMIT or OFFSET. +``` + +Compilation fails before SQLBuild completes artifacts or opens a warehouse connection. Add +deterministic ordering and compile again: + +```sql +SELECT order_id FROM orders ORDER BY order_id LIMIT 1 +``` + +Use focused commands when discovering or adopting Rules: + +```bash +sqb rules list +sqb rules run SQBRSQL004 +``` + +A focused run checks only the requested Rule or family. Run `sqb compile` before treating the whole +project as valid. + +## Where SQL linting went + +SQL linting is part of the Rules system rather than a separate command. + +The `SQBRSQL` built-in family covers deterministic SQL diagnostics such as: + +- unsafe `NULL` comparisons; +- implicit cartesian joins; +- unordered `LIMIT` and `OFFSET`; +- unused CTEs; +- duplicate aliases; +- unstable window ordering; +- risky set-operation shapes; +- selected SQL structure and convention requirements. + +These checks run through ordinary compilation. There is intentionally no separate `sqb lint` +lifecycle. If an SQL requirement affects whether the project is acceptable, the compiler enforces +it with the rest of the configured Rules. + +Not every traditional lint concern should become a diagnostic. SQLBuild separates three kinds of +SQL responsibility: + +| Responsibility | Owner | Example | +|---|---|---| +| Correctness required to understand the project | Compiler | Invalid syntax, unresolved references, incompatible output shape | +| Configurable, deterministic requirements | `SQBRSQL` Rules (executed by the compiler) | Unordered `LIMIT`, unused CTE, implicit cartesian join | +| Canonical presentation | [`sqb format`](/docs/cli/format) (separate from the compiler) | Capitalization, indentation, spacing, comma and clause layout | + +Rules never rewrite source. `sqb format` rewrites source and does not decide whether a project +passes its configured Rules. This keeps diagnostics and source mutation separate. + +## Built-in and custom Rules + +SQLBuild maintains native built-in Rules for reusable requirements. Their codes begin with `SQBR`, +for example `SQBRSQL004` and `SQBRGRAPH101`. + +Projects can define custom Python Rules under `rules/**/*.py`. Their codes begin with `XSQBR`, for +example `XSQBRARCH001`. Custom Rules use the same selection, findings, exceptions, execution order, +and cache system as built-ins. + +```toml +[rules] +select = [ + "SQBRSQL", + "SQBRGRAPH", + "XSQBRARCH", +] +``` + + + Define typed model or project checks over compiler-owned facts with `@rule`, `RuleContext`, and + `Finding`. + + +## One definition of project validity + +`sqb compile` is authoritative. SQLBuild evaluates a project in this order: + +```text +mandatory compiler correctness +→ selected native built-in Rules +→ selected custom Python Rules +→ artifact completion +``` + +Build and planning commands use the same compiler path. Configured findings stop execution before a +warehouse connection is opened. Compile artifacts are not completed from a project that failed its +Rules. + +## Rules compared with tests, audits, and contracts + +Rules do not replace every form of validation: + +| Capability | Question it answers | +|---|---| +| Mandatory compiler correctness | Can SQLBuild construct a trustworthy project and executable representation? | +| Rules | Does the authored and compiled project satisfy selected static requirements? | +| Contracts | Does a model declare and preserve the required output shape? | +| SQL tests | Does SQL logic produce an expected result for controlled inputs? | +| Audits | Does warehouse data satisfy a runtime quality requirement? | +| Runtime checks | Are external resources and execution state valid now? | + +Use a Rule when the answer can be determined from source and compiler facts. Use a test when you +need to execute SQL against controlled data. Use an audit when the answer depends on warehouse data. + +## Findings and intentional exceptions + +A finding contains a stable code, project-relative path, line, column, explanation, and remediation. +This makes the same requirement usable in a terminal, JSON output, CI annotation, or agent workflow. + +Intentional departures remain explicit. Exact exceptions and path- or resource-scoped ignores +require a reason; exact exceptions are stale-checked so obsolete suppressions do not silently +accumulate. Mandatory compiler correctness cannot be suppressed. + +See [Findings and exceptions](/docs/concepts/rules/findings-and-exceptions) for configuration examples. + +## Performance is part of the design + +Model Rules are evaluated and cached per model. Editing one model invalidates affected subjects +rather than every invocation. Project Rules run once over the complete project view and are the +explicit choice for genuinely project-wide requirements, with broader invalidation. SQLBuild tracks +the inputs each Rule depends on so cached findings are reused only while those inputs remain valid. + +Required CI exercises custom Rule packs over generated projects, checking compilation time, +memory, and cache invalidation. Separate 20- and 100-Rule profiles measure the cost of custom +validation at 5,000-model scale. + +See [Benchmarks](/docs/benchmarks#rules-performance) for the benchmark profiles and reproduction commands, +and [Execution and caching](/docs/concepts/rules/execution-and-caching) for the Rules lifecycle. + +## Continue + + + + Select exact Rules or families and configure project-owned options. + + + Understand diagnostics, exact exceptions, path ignores, and stale checks. + + + Learn evaluation order, focused execution, and dependency-aware reuse. + + + List, inspect, run, and generate guidance for configured Rules. + + diff --git a/website/src/content/docs/docs/concepts/rules/configuration-and-selection.mdx b/website/src/content/docs/docs/concepts/rules/configuration-and-selection.mdx new file mode 100644 index 0000000000..36f6b8ea1c --- /dev/null +++ b/website/src/content/docs/docs/concepts/rules/configuration-and-selection.mdx @@ -0,0 +1,108 @@ +--- +title: "Configuration and selection" +description: "Select built-in and custom Rules by exact code or family prefix." +--- + +import Card from '../../../../../components/mintlify/Card.astro'; + +Configure built-in and custom Rules together: + +```toml +[rules] +select = ["SQBRSQL", "SQBRGRAPH", "XSQBRARCH"] +ignore = ["SQBRSQL004"] +``` + +An empty `select` disables configurable Rules. It does not disable mandatory compiler correctness. + +## Codes and families + +Built-in codes use `SQBR`, such as `SQBRSQL004`, `SQBRMODEL101`, and +`SQBRGRAPH101`. Custom codes use `XSQBR`, such as +`XSQBRARCH001`. + +A family is always the code with its final three digits removed: + +```text +SQBRSQL004 → SQBRSQL +XSQBRARCH001 → XSQBRARCH +XSQBR001 → XSQBR +``` + +Exact codes select one Rule. Prefixes select every matching Rule. `ignore` uses the same matching +semantics and takes precedence over `select`. + +Use the catalogue to inspect available codes and metadata: + +```bash +sqb rules list +sqb rules show SQBRSQL004 +``` + +## Rule options + +Built-in and Custom Rules can declare typed options. Configure them by exact code: + +```toml +[rules.rule_options.XSQBRNAME001] +required_prefix = "order" + +[rules.rule_options.SQBRTEST203] +allowed_tests = ["customers__reviewed_empty_input"] +``` + +`SQBRTEST203` (`empty-input-only-test`) accepts `allowed_tests`, a list of SQL test names that are +reviewed exceptions. Listed tests are not flagged and still count toward `SQBRTEST202`'s +`min_tests_per_model`. Entries that name no existing test are reported as stale. See +[Empty-input tests](/docs/concepts/testing#empty-input-tests). + +Unknown Rule codes, option names, and invalid option values fail configuration. + +## Focused selection + +Run one exact Rule or family while developing or adopting it: + +```bash +sqb rules run SQBRSQL004 +sqb rules run SQBRSQL +sqb rules run XSQBRARCH --select customer_orders +``` + +Focused execution constructs the compiler facts needed by the selected Rules. It does not replace +the complete configured enforcement performed by `sqb compile`. + + + See every `sqb rules` command and exit code. + + +## Join keys and final CTE names + +`SQBRSQL040` checks every `JOIN ... ON` in the SQL covered by SQL Rules, including joins inside CTEs +and subqueries. `USING (...)` is accepted. Allowed predicates are: + +- Plain column comparisons, including equality, inequality, range and null-safe comparisons. +- A plain column compared with a literal, including a signed numeric literal. +- Literal-only predicates such as `ON TRUE`, `ON FALSE` and `ON 1 = 1`, including + `LEFT JOIN LATERAL FLATTEN(...) AS f ON TRUE`. Functions or casts applied to literals are still + computed expressions. +- `BETWEEN` / `NOT BETWEEN` on a plain column with plain-column or literal bounds. +- `IN` / `NOT IN` on a plain column with a literal list. +- `IS NULL` / `IS NOT NULL` on a plain column. +- Parenthesized predicates, and `AND` or `OR` combinations in which every branch is allowed. + +Functions, casts (including `::`), arithmetic, concatenation, `CASE`, subqueries and other computed +operands are findings. For example, replace `ON LOWER(o.category) = c.category_key` with a CTE that +projects `LOWER(category) AS category_key`, then join `ON o.category_key = c.category_key`. Snowflake +variant path access such as `ON a.data:key = b.id` is also computed: project the value as a named +column before joining. + +`SQBRSQL041` checks CTE naming: the last top-level CTE must be named `final` (compared +case-insensitively, including quoted identifiers), and no other CTE, including a nested one, may use +that name. Queries without CTEs are unaffected, and SQL-test fixture CTEs are exempt. `SQBRSQL035` +owns the terminal `SELECT`'s plain projection from that last CTE, so select both, normally through +the `SQBRSQL` family, for the complete convention. Parentheses around the entire query do not create +a nested CTE scope. + +Family selectors such as `select = ["SQBRSQL", "SQBRMODEL", "SQBRGRAPH"]` pick up new built-in Rules +on upgrade, so a release that adds `SQBRSQL040` and `SQBRSQL041` can expose computed join keys or +differently named final CTEs that were accepted before. diff --git a/website/src/content/docs/docs/concepts/rules/custom-rules/overview.mdx b/website/src/content/docs/docs/concepts/rules/custom-rules/overview.mdx new file mode 100644 index 0000000000..a16536dd50 --- /dev/null +++ b/website/src/content/docs/docs/concepts/rules/custom-rules/overview.mdx @@ -0,0 +1,93 @@ +--- +title: "Overview" +description: "Define repository-owned checks with one typed Python API." +--- + +import Card from '../../../../../../components/mintlify/Card.astro'; +import CardGroup from '../../../../../../components/mintlify/CardGroup.astro'; + +Custom Rules are ordinary Python under `rules/**/*.py`. Only functions decorated with `@rule` +register; helpers, constants, dataclasses, and classes remain ordinary Python. + +## Model-subject Rules + +```python +from sqlbuild.rules import Finding, Model, RuleContext, rule + + +@rule( + code="XSQBRARCH001", + message="Final models must declare an order identifier", + remediation="Declare order_id in the model contract.", +) +def final_order_identifier(*, model: Model, ctx: RuleContext) -> list[Finding]: + declared = {column.name for column in ctx.columns.declared(model)} + return [] if "order_id" in declared else [ctx.finding(subject=model)] +``` + +Annotations determine invocation. The signature must contain exactly one typed `Model` or `Project` +subject and one typed `RuleContext`, all keyword-only. Parameter names and position are not semantic. +A model-subject Rule can still inspect project-wide facts. + +## Project-subject Rules + +Use `Project` when an invariant has no natural model subject: + +```python +from sqlbuild.rules import Finding, Project, RuleContext, rule + + +@rule( + code="XSQBRARCH002", + message="The project must contain a final model directory", + remediation="Add models/final and place final outputs beneath it.", +) +def final_directory(*, ctx: RuleContext, project: Project) -> list[Finding]: + del project + paths = ctx.project.tree.glob("models/final/**") + return [] if paths else [ctx.finding(subject="models")] +``` + +Rules cannot mutate SQL, resources, adapter lowering, or compiler output. They return diagnostics +only. + +## Typed options + +```python +from sqlbuild.rules import Finding, Model, RuleContext, RuleOption, rule + +REQUIRED_PREFIX = RuleOption.string( + name="required_prefix", + default="customer", + description="Required model-name prefix.", +) + + +@rule( + code="XSQBRNAME001", + message="The model name uses the wrong prefix", + remediation="Rename the model with the configured prefix.", + options=(REQUIRED_PREFIX,), +) +def required_prefix(*, model: Model, ctx: RuleContext) -> list[Finding]: + prefix = ctx.option(REQUIRED_PREFIX) + return [] if model.name.startswith(prefix) else [ctx.finding(subject=model)] +``` + +```toml +[rules.rule_options.XSQBRNAME001] +required_prefix = "order" +``` + +Unknown codes, option names, or invalid values fail configuration. + +## Continue + + + + Inspect SQL, graph, contracts, tests, audits, declarations, and project structure. + + + Test findings and keep helper and project observations cacheable. + + diff --git a/website/src/content/docs/docs/concepts/rules/custom-rules/rule-context.mdx b/website/src/content/docs/docs/concepts/rules/custom-rules/rule-context.mdx new file mode 100644 index 0000000000..18c35f43ff --- /dev/null +++ b/website/src/content/docs/docs/concepts/rules/custom-rules/rule-context.mdx @@ -0,0 +1,71 @@ +--- +title: "RuleContext and compiler facts" +description: "Inspect stable compiler-owned facts and opt into lazy SQL AST access." +--- + +`RuleContext` exposes typed, read-only views over the compiled project: + +- `ctx.sql`: authored and expanded SQL +- `ctx.graph`: compiler-resolved dependencies and dependents +- `ctx.columns`: declared and inferred output columns +- `ctx.contracts`: enforcement and grain facts +- `ctx.tests` and `ctx.audits`: checks associated with a model +- `ctx.declarations`: public and model-scoped enums and constants +- `ctx.project`: compiled resources and deterministic project-tree observations + +## Authored and expanded SQL + +Choose the SQL representation that matches the requirement: + +```python +sql = ctx.sql.for_model(model) + +authored_text = sql.authored.source +expanded_text = sql.expanded.source +``` + +An authoring convention may inspect what a developer wrote. A compiler-output convention may need +the expanded SQL after interpolation, declarations, and macros. + +## Common SQL structures + +Common structures are available through typed source nodes: + +```python +for cte in sql.expanded.ctes(): + ... + +for star in sql.authored.star_projections(): + ... +``` + +## Lazy Polyglot access + +Use the full Polyglot AST only when common projections are insufficient: + +```python +ast = sql.expanded.polyglot_ast() +for node in ast.walk(): + ... +``` + +AST construction is lazy. A path or contract Rule does not pay for SQL parsing merely because AST +access exists elsewhere. + +## Project structure + +Use the compiler-owned tree rather than direct filesystem calls: + +```python +parts = ctx.project.tree.relative_parts(path=model.path, under="models") +models = ctx.project.tree.resources_under("models/orders") +config = ctx.project.tree.read_text("rules/requirements.yaml") +``` + +Paths remain project-relative, and observations participate in cache invalidation. + +## Choose the subject deliberately + +A model-subject Rule is evaluated and cached per model. A project-subject Rule runs once and can +iterate `ctx.project.models`. Use a project subject only when the invariant genuinely needs a +project-wide view; its cache invalidation is intentionally broader. diff --git a/website/src/content/docs/docs/concepts/rules/custom-rules/testing-and-determinism.mdx b/website/src/content/docs/docs/concepts/rules/custom-rules/testing-and-determinism.mdx new file mode 100644 index 0000000000..9198c43b4a --- /dev/null +++ b/website/src/content/docs/docs/concepts/rules/custom-rules/testing-and-determinism.mdx @@ -0,0 +1,62 @@ +--- +title: "Testing and deterministic Rules" +description: "Test custom findings and keep implementations safe for dependency-aware caching." +--- + +## Test Rules + +Use the public harness to exercise discovery, compilation, and Rules evaluation: + +```python +from sqlbuild.rules.testing import RuleCase, evaluate_rule +from rules.final_outputs import final_order_identifier + + +def test_given_missing_order_id_when_evaluating_then_reports_finding() -> None: + result = evaluate_rule( + rule=final_order_identifier, + test_case=RuleCase( + description="missing order identifier", + source='MODEL (description "Orders");\nSELECT 1 AS customer_id\n', + path="models/final/customer_orders.sql", + expected_finding_count=1, + ), + ) + + assert result.finding_count == 1 +``` + +`RuleCase.files` adds supporting project files. `RuleCase.config` supplies option values. Include +passing cases, failing cases, near misses, and exact source-position assertions. + +Repository pytest files remain outside the SQLBuild project's SQL `tests/` directory. + +## Helpers + +Custom Rules can import repository-owned helpers under `rules/` and supported pure Python modules. +Changing an imported helper invalidates the Rules that depend on it. + +Only decorated functions register. Ordinary functions, constants, dataclasses, and classes remain +helpers. + +## Deterministic inputs + +Direct filesystem, environment, clock, randomness, network, and subprocess access is rejected. +Those inputs cannot be reproduced safely by the Rules cache. + +Read supported project text and structure through `ctx.project.tree`: + +```python +text = ctx.project.tree.read_text("rules/requirements.yaml") +matches = ctx.project.tree.glob("models/*/interface/*.sql") +``` + +Both positive and negative observations are tracked. If `glob` returns no paths, adding a matching +path invalidates the cached result. + +## Cache granularity + +Cache identity incorporates the Rule implementation, imported helper closure, configured options, +subject, accessed compiler facts, tracked project observations, dialect, and compatibility +versions. Prefer model subjects for independent per-model checks and project subjects for genuine +cross-project invariants. diff --git a/website/src/content/docs/docs/concepts/rules/execution-and-caching.mdx b/website/src/content/docs/docs/concepts/rules/execution-and-caching.mdx new file mode 100644 index 0000000000..0d180f6d43 --- /dev/null +++ b/website/src/content/docs/docs/concepts/rules/execution-and-caching.mdx @@ -0,0 +1,53 @@ +--- +title: "Execution and caching" +description: "Understand Rule enforcement order, focused runs, and dependency-aware reuse." +--- + +SQLBuild evaluates a project in this order: + +1. discovery, expansion, and mandatory compiler correctness +2. selected native built-in Rules +3. selected custom Python Rules +4. artifact completion + +Native built-ins always run before custom Rules. Structured compile output reports built-in and +custom durations separately, together with cache hits and misses. + +## Authoritative enforcement + +`sqb compile` runs the complete configured Rules set. Build and execution commands use the same +compiler path and reject Rule findings before opening a warehouse connection. + +`sqb rules run` is intentionally focused. Use it to inspect one exact code or family, but run the +normal compiler before treating the complete project as valid. + +## Dependency-aware caching + +Rules cache under `target/rules-cache`. A cache identity includes the Rule code, implementation and +imported helpers, options, invocation subject, accessed compiler facts, dialect, tracked project +observations, and compatibility versions. + +Model-subject Rules support fine-grained reuse. Editing one model invalidates affected model +subjects rather than every Rule invocation. Project-subject Rules run once and are appropriate for +genuinely project-wide invariants, with correspondingly broader invalidation. + +Positive and negative project-tree observations are dependencies. If a Rule observes that a glob +has no matches, adding a matching file invalidates that result. + +## Built-in-only execution + +When no custom Rule is selected, SQLBuild does not initialize the custom Python host. When every +custom result is cached, the host is not launched for evaluation. + +## Agent guidance + +Generate project-specific guidance from the configured Rules: + +```bash +sqb rules skills +sqb rules skills --check +``` + +SQLBuild manages `.agents/skills/sqlbuild-rules/SKILL.md`, +`.claude/skills/sqlbuild-rules/SKILL.md`, and `.opencode/skills/sqlbuild-rules/SKILL.md` without +overwriting divergent or unowned content. diff --git a/website/src/content/docs/docs/concepts/rules/findings-and-exceptions.mdx b/website/src/content/docs/docs/concepts/rules/findings-and-exceptions.mdx new file mode 100644 index 0000000000..30b43386bf --- /dev/null +++ b/website/src/content/docs/docs/concepts/rules/findings-and-exceptions.mdx @@ -0,0 +1,74 @@ +--- +title: "Findings and exceptions" +description: "Understand Rule diagnostics and record intentional exceptions safely." +--- + +A Rule finding identifies an exact code, project-relative path, line, column, message, and +remediation. Error findings make compilation fail and prevent artifact completion. + +## Exact exceptions + +Use an exact exception for one known finding. Exceptions require a reason and are stale-checked: + +```toml +[[rules.rule_exceptions]] +rule = "SQBRSQL004" +path = "models/examples/sample_orders.sql" +reason = "This example intentionally demonstrates one sampled row." +``` + +An exception refers to one exact Rule code and path. If the finding disappears, SQLBuild reports +the stale exception so obsolete configuration does not accumulate silently. + +## Path- and resource-scoped ignores + +Use a path ignore when a documented project area intentionally follows a different convention: + +```toml +[[rules.rule_ignores]] +rules = ["SQBRSQL"] +paths = ["models/examples/**"] +reason = "Examples retain intentionally minimal SQL." +``` + +Path ignores accept exact codes and family prefixes. Keep their scope narrow and explain why the +project differs from the selected requirement. + +Use `selectors` when the exception follows graph-resource identities or lineage rather than files: + +```toml +[[rules.rule_ignores]] +rules = ["SQBRSQL021"] +selectors = ["intermediate_*"] +reason = "These intermediate interfaces intentionally preserve upstream columns." +``` + +Selectors use the same grammar as SQLBuild commands, including exact names, name globs, tags, +resource paths, and graph expansion such as `+intermediate_*`. Use `paths` for authored-file glob +matching, including SQL tests and audits that are not graph resources: + +```toml +[[rules.rule_ignores]] +rules = ["SQBRSQL021"] +paths = ["models/**/intermediate_*.sql"] +reason = "These intermediate SQL files intentionally preserve upstream columns." +``` + +`paths` and `selectors` may be combined in one scoped ignore. Both forms require a reason. + +## Mandatory correctness + +Mandatory compiler correctness is not configurable and cannot be suppressed. A project must first +compile into a trustworthy representation before any selected Rule can run. + +## Machine-readable findings + +Use JSON when another tool consumes findings: + +```bash +sqb compile --json +sqb rules --json run SQBRSQL +``` + +Machine-readable output remains on stdout. Lifecycle progress and terminal status are written to +stderr. diff --git a/website/src/content/docs/docs/concepts/scenarios.mdx b/website/src/content/docs/docs/concepts/scenarios.mdx new file mode 100644 index 0000000000..bd38f97f2d --- /dev/null +++ b/website/src/content/docs/docs/concepts/scenarios.mdx @@ -0,0 +1,278 @@ +--- +title: "Scenarios" +description: "End-to-end tests that build real project graphs against coherent fixture data." +--- + +Scenarios are end-to-end tests for coherent slices of your warehouse. You define fixture inputs, SQLBuild builds the real project graph against them in isolated physical relations, and you assert that the result is correct. + +## When to use scenarios vs unit tests + +[SQL unit tests](/docs/concepts/testing) compile to a single comparison query. They are fast, inline, and work well for checking individual model transformations. But when you need to test across many models with related entities - customers that have orders, orders that have payments, refunds that reference original orders - a single query can become unwieldy or hit SQL size limits. + +Scenarios materialize physical relations in the warehouse and build the actual model graph. This means: + +- Fixture inputs are real tables, not CTEs inside one query +- Models execute with real SQL, real materializations, and real dependency ordering +- You can test end-to-end business logic across an entire pipeline slice +- Debugging is easier because you can inspect intermediate tables with `--retain` + +Use unit tests for transformation logic. Use scenarios for coherent end-to-end validation. + +## Authoring scenarios + +Scenario files live under `tests/scenarios/` and use one file per scenario: + +``` +tests/ + scenarios/ + revenue/ + daily_revenue_minimal.sql + daily_revenue_multi_order.sql + fulfillment/ + fulfillment__late_shipment.sql +``` + +Folders are organizational only. The scenario name is the filename stem and must be globally unique across all discovered scenario files. + +### Scenario format + +Each file has a `SCENARIO()` header followed by CTEs that define inputs, expected outputs, and assertions: + +```sql +SCENARIO ( + description "Daily revenue includes only successful payments", + tags ["revenue", "example"] +); + +WITH +__ref__stg_orders AS ( + SELECT + 1 AS order_id, 10 AS customer_id, 1 AS waffle_type_id, + 2 AS quantity, CAST('2026-04-01 09:15:00' AS TIMESTAMP) AS ordered_at, + 'completed' AS status +), + +__ref__stg_payments AS ( + SELECT + 1 AS payment_id, 1 AS order_id, 1700 AS amount_cents, + 'credit_card' AS payment_method, + CAST('2026-04-01 09:16:00' AS TIMESTAMP) AS paid_at, + 'success' AS payment_status + UNION ALL + SELECT + 2 AS payment_id, 2 AS order_id, 1050 AS amount_cents, + 'credit_card' AS payment_method, + CAST('2026-04-01 10:01:00' AS TIMESTAMP) AS paid_at, + 'failed' AS payment_status +), + +__expected__daily_revenue AS ( + SELECT + CAST('2026-04-01' AS DATE) AS revenue_date, + 1 AS order_count, 2 AS waffles_sold, + 1700 AS total_revenue_cents +), + +__assert__all_orders_have_payments AS ( + SELECT * + FROM __ref("fact_orders") + WHERE payment_amount_cents IS NULL +) + +SELECT 1 +``` + +### CTE conventions + +| Prefix | Purpose | +|--------|---------| +| `__source__` | Fixture source input. Materializes as a scenario table. | +| `__ref__` | Fixture model boundary. Prevents SQLBuild from building that model upstream. | +| `__seed__` | Fixture seed data. | +| `__expected__` | Full expected output. Compared order-insensitively against the scenario-built model. | +| `__assert__` | Zero-row assertion. Passes if the query returns no rows; fails with the returned rows as diagnostics. | + +For sources with two-part identity, use double underscores: `__source__raw__orders`. + +Every scenario must have at least one fixture CTE and at least one `__expected__` or `__assert__` CTE. + +Scenario SQL uses macros, enums, and constants available from the scenario file's directory under +`tests/scenarios/`. Public enums and constants available to a model are also available when the +scenario defines `__expected__model_name` output for that model. Model-private values and macros +available only to the model are not included. See +[How Visibility Works](/docs/concepts/declaration-scopes/visibility#tests-and-expected-output). + +### SCENARIO() header + +The header is metadata only: + +| Field | Description | +|-------|-------------| +| `description` | Optional description string | +| `tags` | Optional list of string tags | + +## How scenarios execute + +### Graph inference + +SQLBuild infers which models to build from your scenario: + +1. Target models come from `__expected__` CTE names and `__ref(...)` calls inside `__assert__` CTEs +2. SQLBuild walks upstream from targets, building every required model +3. `__ref__` fixtures act as boundaries - upstream traversal stops there +4. All required sources must be provided by `__source__` fixtures +5. Required project seeds are loaded automatically unless overridden by `__seed__` fixtures + +### Isolation + +Scenario artifacts are physically isolated from production: + +- Every scenario-owned relation uses a deterministic prefixed name (based on a hash of the project and scenario identity) +- All `ref()`, `source()`, and `seed()` calls resolve to scenario-owned relations, never production tables +- Fixture CTEs can read from existing warehouse relations if you choose, but downstream models always read the materialized scenario fixture, not the original + +### Execution flow + +1. Clean any existing scenario artifacts from a previous run +2. Materialize source, ref, and seed fixtures as physical tables +3. Load required project seeds +4. Build required models in dependency order (incremental models run as full-refresh in scenarios) +5. Run expected-output comparisons (order-insensitive) +6. Run zero-row assertions +7. Clean up all scenario-owned artifacts (unless `--retain`) + +### Inspecting with `--retain` + +When a scenario fails or you want to inspect intermediate state: + +```bash +sqb scenario test --select daily_revenue_minimal --retain +``` + +This keeps all scenario-owned relations in the warehouse and prints a relation map showing the logical-to-physical name mapping. You can then query the scenario tables directly to debug. + +Runtime artifacts (fixture SQL, model lifecycle SQL, check SQL, cleanup SQL) are always written to `target/run/scenarios//` regardless of `--retain`. + +## Local scenario testing + +Scenarios can run locally against DuckDB using captured JSONL snapshots - no warehouse connection needed. This is useful for CI pipelines and fast developer iteration. + +### Capture + +First, capture scenario inputs from the real warehouse: + +```bash +sqb scenario capture --select daily_revenue_minimal +``` + +This: + +1. Materializes scenario input fixtures in the warehouse +2. Runs a preflight `COUNT(*)` on each fixture to check row counts against safety limits +3. Inspects column types and maps them to DuckDB-compatible types +4. Downloads rows as JSONL files (enforcing byte limits during writing) +5. Writes a `scenario.json` manifest with column metadata, types, row counts, and an input fingerprint +6. Cleans up warehouse artifacts + +Snapshots are written to `tests/_scenario_snapshots//` and can be committed to version control. JSONL is human-readable and git-diffable. + +### Capture safety limits + +Capture enforces row and byte limits to prevent accidentally saving large datasets. Limits can be set in `sqlbuild_project.toml`: + +```toml +[scenario.snapshot_limits] +max_rows_per_relation = 10000 +max_total_rows = 50000 +max_bytes_per_relation = 10485760 # 10 MB +max_total_bytes = 52428800 # 50 MB +``` + +Or per-command via CLI flags: + +```bash +sqb scenario capture --max-snapshot-rows 5000 --max-snapshot-total-rows 20000 +sqb scenario capture --max-snapshot-bytes 5242880 +``` + +CLI flags override TOML config. Use `--force` to bypass all limits: + +```bash +sqb scenario capture --force +``` + +When a limit is exceeded, capture fails with a clear error and suggests narrowing the fixture query, raising the limit, or using `--force`. + +### Local replay + +Run scenarios locally against DuckDB: + +```bash +sqb scenario test --local +``` + +This: + +1. Checks snapshot freshness via the input fingerprint (missing/stale snapshots are skipped by default) +2. Creates a temporary DuckDB database at `target/run/scenarios//local.duckdb` +3. Loads JSONL snapshots into typed DuckDB tables using column metadata from `scenario.json` +4. Transpiles model and check SQL from the project adapter dialect to DuckDB +5. Builds functions, models, and runs expected/assertion checks in DuckDB +6. Keeps the local DuckDB file for inspection (it lives under `target/`, so it's always retained) + +### Sync snapshots + +Instead of running capture and test separately, sync snapshots in one command: + +```bash +# Capture missing/stale snapshots, then run locally +sqb scenario test --local --sync-snapshots + +# Recapture all snapshots, then run locally +sqb scenario test --local --refresh +``` + +With `--sync-snapshots`, fresh snapshots are reused. With `--refresh`, all selected snapshots are recaptured even if fresh. + +### Strict mode + +By default, missing or stale snapshots are skipped with a warning. Use `--strict` to treat them as errors: + +```bash +sqb scenario test --local --strict +``` + +### Local type overrides + +When the automatic warehouse-to-DuckDB type conversion produces an incompatible type, you can override it in `sqlbuild_project.toml`: + +```toml +[scenario.local_type_overrides.snowflake] +"OBJECT" = "JSON" +"ARRAY" = "JSON" +"NUMBER(*,0)" = "BIGINT" +``` + +Override keys are structural type patterns. Values can reference matched type arguments with `{1}`, `{2}`, etc: + +```toml +[scenario.local_type_overrides.bigquery] +"NUMERIC({1},{2})" = "DECIMAL({1},{2})" +``` + +These overrides are written into `scenario.json` during capture and used when loading snapshots into DuckDB. + +## Cleanup + +By default, remote scenario artifacts are dropped after each run (pass or fail). The janitor command also recognizes scenario artifacts and can clean retained or orphaned scenario relations: + +```bash +sqb janitor +``` + +See the [CLI reference](/docs/cli/scenario) for full command documentation. + +## Limitations + +- Custom materializations are not supported in scenarios yet. Scenario models using custom materializations will fail with a clear error. +- Local replay transpiles SQL from the project adapter dialect to DuckDB. Adapter-specific SQL that cannot be translated will produce a clear error with the failing resource name and reason. diff --git a/website/src/content/docs/docs/concepts/seeds.mdx b/website/src/content/docs/docs/concepts/seeds.mdx new file mode 100644 index 0000000000..5a0d067ed9 --- /dev/null +++ b/website/src/content/docs/docs/concepts/seeds.mdx @@ -0,0 +1,122 @@ +--- +title: "Seeds" +description: "Load static CSV data into your pipeline as tables." +--- + +Seeds are CSV files under `seeds/` that SQLBuild loads as tables in the warehouse. They're useful for small, static reference data like lookup tables, category mappings, or configuration values. + +## Defining a seed + +Place a CSV file in your `seeds/` directory: + +```csv +waffle_type_id,waffle_name,category,price_cents +1,Classic Belgian,sweet,850 +2,Liege,sweet,950 +3,Brussels,sweet,750 +4,Cheddar Herb,savory,1050 +``` + +Declare the seed's column types in any `.yml` file under `seeds/`. You can use any filename and organize declarations however you like - one file per seed, or group related seeds together: + +```yaml +# seeds/lookups.yml +seeds: + - name: waffle_types + description: Waffle menu items with pricing. + columns: + - name: waffle_type_id + type: INTEGER + - name: waffle_name + type: VARCHAR + - name: category + type: VARCHAR + - name: price_cents + type: INTEGER +``` + +Every seed must have a YAML declaration with at least one typed column. CSV filenames must be unique across the entire `seeds/` directory (including subdirectories). The seed name in the YAML must match the CSV filename (without the `.csv` extension). + +## Referencing seeds + +Seeds are referenced in models with `__seed()`: + +```sql +SELECT + o.order_id, + w.waffle_name, + w.price_cents * o.quantity AS line_total_cents +FROM __ref("stg_orders") o +LEFT JOIN __seed("waffle_types") w ON o.waffle_type_id = w.waffle_type_id +``` + +`__seed()` is distinct from `__ref()`. Using `__ref()` with a seed name raises a compile error with a message pointing you to `__seed()`. + +## Loading seeds + +Seeds are loaded automatically during `sqb build`. You can also load them standalone: + +```bash +sqb seed +``` + +Seeds are fully replaced on every run. If the CSV changes, the table is recreated with the new data. + +## Target overrides + +Seeds inherit the project's default database and schema. You can override these per seed: + +```yaml +seeds: + - name: waffle_types + database: analytics + schema: lookups + columns: + - name: waffle_type_id + type: INTEGER + - name: waffle_name + type: VARCHAR +``` + +## CSV settings + +For non-standard CSV formats, configure parsing behavior with `csv_settings`: + +```yaml +seeds: + - name: european_prices + csv_settings: + delimiter: ";" + encoding: utf-8 + quotechar: '"' + columns: + - name: product_id + type: INTEGER + - name: price + type: VARCHAR +``` + +### Available CSV settings + +| Setting | Type | Description | +|---------|------|-------------| +| `delimiter` | string | Field delimiter (default: `,`) | +| `quotechar` | string | Character used to quote fields (default: `"`) | +| `doublequote` | boolean | Whether the quotechar is doubled to escape itself | +| `escapechar` | string | Character used to escape the delimiter or quotechar | +| `skipinitialspace` | boolean | Skip whitespace after the delimiter | +| `lineterminator` | string | Line terminator character | +| `encoding` | string | File encoding (default: `utf-8`) | +| `na_values` | list or mapping | Values to treat as null - a global list or per-column mapping | +| `keep_default_na` | boolean | Whether to use the default set of NA values in addition to `na_values` | + +### Per-column NA values + +`na_values` can be a flat list (applied to all columns) or a mapping from column names to their NA values: + +```yaml +csv_settings: + na_values: + price: ["N/A", ""] + category: ["unknown"] +``` diff --git a/website/src/content/docs/docs/concepts/selectors.mdx b/website/src/content/docs/docs/concepts/selectors.mdx new file mode 100644 index 0000000000..f9b0ee178e --- /dev/null +++ b/website/src/content/docs/docs/concepts/selectors.mdx @@ -0,0 +1,158 @@ +--- +title: "Selectors" +description: "Target specific models, paths, tags, or DAG subsets with select and exclude flags." +--- + +Selectors let you scope commands to specific subsets of your project. They work with `plan`, `build`, `test`, `audit`, `seed`, `clone`, and `diff`. + +## Basic usage + +```bash +sqb build --select daily_revenue +sqb build --select daily_revenue customer_status_snapshot +sqb build --exclude stg_customers +``` + +`--select` (or `-s` for short) accepts one or more names. Multiple values are unioned. Space-separated names within one `--select` are also unioned. `--exclude` subtracts from the selected set. + +When no `--select` is provided, all models are selected. + +## Selector types + +### Name + +Select a single resource by name: + +```bash +sqb build --select fact_orders +``` + +Bare names accept glob patterns. This selects every resource whose name starts with +`intermediate_`: + +```bash +sqb build --select "intermediate_*" +``` + +Name globs compose with graph expansion. For example, `+intermediate_*` selects every matching +resource and all of their upstream dependencies. Quote patterns in shell commands so your shell +does not expand `*` against files in the current directory. + +### Tag + +Select all models with a specific tag: + +```bash +sqb build --select tag:staging +sqb build --select tag:acceptance +``` + +### Path + +Select all models under a directory path: + +```bash +sqb build --select path:models/marts +sqb build --select models/marts +sqb build --select path:models/intermediate +``` + +Any name containing `/` is treated as a path selector, so `path:models/marts` and a bare `models/marts` work the same way. Path selectors require an explicit root directory: `models/`, `tasks/`, `assets/`, `checks/`, or `loaders/`. Nested paths work too: `models/staging/orders`. + +### Seed and source + +```bash +sqb build --select seed:waffle_types +sqb build --select source:raw__orders +``` + +## Graph expansion + +### Upstream + +Select a model plus all its upstream dependencies: + +```bash +sqb build --select +daily_activity_rollup +``` + +### Downstream + +Select a model plus all its downstream dependents: + +```bash +sqb build --select fact_orders+ +``` + +### Bidirectional + +```bash +sqb build --select +fact_orders+ +``` + +Graph expansion works with all selector types: + +```bash +sqb build --select +tag:marts +sqb build --select path:models/staging+ +``` + +## Path-between selectors + +Select all models on the shortest path between two nodes: + +```bash +sqb build --select fact_orders~daily_activity_rollup +``` + +With endpoint expansion: + +```bash +sqb build --select +fact_orders~daily_activity_rollup+ +``` + +This selects: +- All upstreams of `fact_orders` +- Every model on the path between `fact_orders` and `daily_activity_rollup` +- All downstreams of `daily_activity_rollup` + +This is useful for rebuilding a specific slice of the DAG without manually listing every model in between. + +## Intersection + +Use commas to intersect selector results: + +```bash +sqb build --select "tag:staging,path:models/finance" +``` + +This selects only models that match *both* conditions - in this case, models tagged `staging` that are also under the `models/finance` directory. + +## Combining select and exclude + +```bash +# Build all marts except daily_revenue +sqb build --select path:models/marts --exclude daily_revenue + +# Build everything upstream of fact_orders, excluding staging models +sqb build --select +fact_orders --exclude tag:staging +``` + +## Error handling + +Unknown resource names, name patterns with no matches, empty paths, and malformed selectors produce +clear error messages: + +``` +unknown selector name 'nonexistent_model' +unknown selector pattern 'missing_*' +no models found under path 'models/nonexistent'. +no models found with tag 'nonexistent_tag' +path selector 'fact_orders~' requires names on both sides of '~' +``` + +If a path selector omits the root directory, SQLBuild asks for the explicit form: + +``` +path selectors require an explicit root: use 'models/', 'tasks/', 'assets/', 'checks/', or 'loaders/' +``` diff --git a/website/src/content/docs/docs/concepts/snapshots.mdx b/website/src/content/docs/docs/concepts/snapshots.mdx new file mode 100644 index 0000000000..e051dfd163 --- /dev/null +++ b/website/src/content/docs/docs/concepts/snapshots.mdx @@ -0,0 +1,352 @@ +--- +title: "Snapshots (SCD Type 2)" +description: "Preserve row history over time using SCD Type 2 semantics with timestamp or check-based change detection." +--- + +Snapshot models maintain historical row versions with validity windows. They answer questions like: what does this entity look like now, what did it look like before, when did it change, and was it absent during a period. + +## How snapshots work + +SQLBuild adds two generated columns to the target table: + +| Column | Meaning | +|--------|---------| +| `valid_from` | When this version became valid (inclusive) | +| `valid_to` | When this version stopped being valid (exclusive). `NULL` means currently active. | + +A point-in-time query uses the interval `valid_from <= point_in_time < valid_to`. + +## Change detection strategies + +### Timestamp strategy + +Use when your source has a reliable column recording when the entity changed. + +```sql +MODEL ( + materialized snapshot, + unique_key [customer_id], + snapshot_strategy timestamp, + updated_at updated_at, +); + +SELECT + customer_id, + name, + plan, + status, + updated_at +FROM __source("customers") +``` + +If the source `updated_at` is newer than the active target row's `updated_at`, SQLBuild closes the old version and inserts the new one. If `updated_at` is unchanged or older, nothing happens. + +### Check strategy + +Use when the source does not have a reliable update timestamp. + +```sql +MODEL ( + materialized snapshot, + unique_key [customer_id], + snapshot_strategy check, + check_columns [name, plan, status], +); + +SELECT + customer_id, + name, + plan, + status +FROM __source("customers") +``` + +SQLBuild compares `check_columns` between source and active target rows. If any checked value differs, a new version is created. Changes to unchecked columns are ignored. + +`check_columns [*]` checks all output columns except `unique_key` and the generated validity columns. Explicit columns are recommended for important models to avoid noisy history from volatile metadata columns. + +## Historical input + +By default, SQLBuild treats the model query as returning the current state of each entity (one row per `unique_key`). When your source contains historical observations over time, add `observed_at` to switch to historical mode. + +### Historical check snapshot + +Use for daily full exports or periodic snapshots without a business update timestamp. + +```sql +MODEL ( + materialized snapshot, + unique_key [customer_id], + snapshot_strategy check, + check_columns [plan, status], + observed_at snapshot_date, +); + +SELECT + customer_id, + plan, + status, + snapshot_date +FROM __source("customers_daily_snapshot") +``` + +Each `observed_at` group is treated as a complete picture of the source at that time. Consecutive unchanged observations are collapsed into a single version. + +### Historical timestamp snapshot + +Use for historical observations that include a business update timestamp. + +```sql +MODEL ( + materialized snapshot, + unique_key [customer_id], + snapshot_strategy timestamp, + updated_at updated_at, + observed_at extract_date, + historical_input snapshot, +); + +SELECT + customer_id, + plan, + status, + updated_at, + extract_date +FROM __source("customers_historical_extracts") +``` + +Each row means: "at `extract_date`, the source's current state for this key had this `updated_at`." Validity windows use `updated_at`, not `observed_at`. + +### Historical change records + +Use for CDC tables, audit logs, or historical backfills where rows are individual version records. + +```sql +MODEL ( + materialized snapshot, + unique_key [customer_id], + snapshot_strategy timestamp, + updated_at updated_at, + observed_at loaded_at, + historical_input changes, +); + +SELECT + customer_id, + plan, + status, + updated_at, + loaded_at +FROM __source("customers_cdc") +``` + +Multiple changes for the same key in one batch are allowed. `updated_at` determines version ordering. `observed_at` is arrival/load time, not validity time. + +### Historical input rules + +| Strategy | `historical_input` | Source shape | Uniqueness | Hard deletes | +|----------|-------------------|-------------|-----------|-------------| +| `check` | `snapshot` (default) | Complete observations over time | `unique_key + observed_at` | Allowed | +| `timestamp` | `snapshot` | Complete observations with update timestamp | `unique_key + observed_at` | Allowed | +| `timestamp` | `changes` | Individual change/version records | `unique_key + updated_at` | Not allowed | +| `check` | `changes` | Not supported | - | - | + +Timestamp snapshots with `observed_at` require `historical_input` to be set explicitly. + +## Hard deletes + +```sql +invalidate_hard_deletes true, +``` + +When enabled, active target rows whose keys are missing from the source are closed: + +- **Current-state input**: closed at execution time +- **Historical input** (`historical_input snapshot`): closed at the `observed_at` time of the group where the key is missing + +Hard deletes are not allowed with `historical_input changes` because change-record batches are not complete source snapshots - a missing key just means no change, not deletion. + +Reappearing keys create a new active version. + +## Configuration reference + +| Field | Required | Description | +|-------|----------|-------------| +| `materialized snapshot` | Yes | Enables snapshot lifecycle | +| `unique_key` | Yes | Column(s) identifying one entity | +| `snapshot_strategy` | Yes | `timestamp` or `check` | +| `updated_at` | Timestamp only | Source column with business update time | +| `check_columns` | Check only | Columns compared to detect changes. Use `[*]` for all non-key columns. | +| `observed_at` | No | Source column with observation/extract time. Presence enables historical mode. | +| `historical_input` | Conditional | `snapshot` or `changes`. Required for timestamp with `observed_at`. Defaults to `snapshot` for check with `observed_at`. | +| `invalidate_hard_deletes` | No | Close active rows missing from source. Default `false`. | +| `valid_from_column` | No | Override generated column name. Default `valid_from`. | +| `valid_to_column` | No | Override generated column name. Default `valid_to`. | +| `initial_valid_from` | No | First-version start time: `updated_at`, `observed_at`, or `execution_time`. See defaults below. | +| `snapshot_full_refresh` | No | Model-level full-refresh safety: `deny`, `require_confirmation`, or `allow`. | + +### Initial valid_from defaults + +| Case | Default | +|------|---------| +| Timestamp, no `observed_at` | `updated_at` | +| Timestamp, with `observed_at` | `updated_at` | +| Check, no `observed_at` | `execution_time` | +| Check, with `observed_at` | `observed_at` | + +## Full refresh safety + +Snapshot full refresh can permanently discard history that cannot be reconstructed from the source. SQLBuild guards against this with configurable safety policies. + +### Project config + +```toml +[snapshots] +current_state_full_refresh = "deny" +historical_full_refresh = "require_confirmation" +``` + +| Policy | Behavior | +|--------|----------| +| `deny` | Full refresh is blocked regardless of CLI flags | +| `require_confirmation` | Requires `--allow-snapshot-full-refresh` flag or interactive confirmation | +| `allow` | No snapshot-specific confirmation required | + +### Defaults + +| Snapshot type | Default policy | Reason | +|--------------|---------------|--------| +| Current-state (no `observed_at`) | `deny` | Cannot reconstruct older history from current-state source | +| Historical (with `observed_at`) | `require_confirmation` | Can reconstruct if query returns full history, but SQLBuild cannot prove that | + +### Model override + +The model `snapshot_full_refresh` field can only make the policy **stricter** than the project setting. A model cannot weaken `deny` to `allow`. + +```sql +MODEL ( + materialized snapshot, + ... + snapshot_full_refresh deny, +); +``` + +### CLI usage + +```bash +# Fails if any selected snapshot has effective policy 'deny' +sqb build --full-refresh + +# Satisfies 'require_confirmation' policy (cannot override 'deny') +sqb build --full-refresh --allow-snapshot-full-refresh +``` + +## Audits + +Snapshot models support the same audit system as other materializations. Audits with `delta_and_final` run scope execute against the snapshot delta relation before target mutation, blocking promotion if an error-severity audit fails. Final audits run after target mutation. + +```sql +MODEL ( + materialized snapshot, + unique_key [customer_id], + snapshot_strategy timestamp, + updated_at updated_at, + columns ( + customer_id (audits [not_null (run_scope delta_and_final)]), + ), +); +``` + +## Duplicate handling + +SQLBuild fails with an actionable error if the source query produces duplicate rows at the snapshot identity grain: + +- Current-state: duplicate `unique_key` +- Historical snapshot: duplicate `unique_key + observed_at` +- Historical changes: duplicate `unique_key + updated_at` + +Deduplicate in your model SQL: + +```sql +WITH ranked AS ( + SELECT + *, + ROW_NUMBER() OVER ( + PARTITION BY customer_id, snapshot_date + ORDER BY loaded_at DESC + ) AS rn + FROM __source("customer_daily") +) + +SELECT customer_id, plan, status, snapshot_date +FROM ranked +WHERE rn = 1 +``` + +## Querying snapshots + +### Current rows + +```sql +SELECT * FROM customer_snapshot WHERE valid_to IS NULL +``` + +### Point-in-time + +```sql +SELECT * +FROM customer_snapshot +WHERE customer_id = 1 + AND TIMESTAMP '2026-02-15' >= valid_from + AND (valid_to IS NULL OR TIMESTAMP '2026-02-15' < valid_to) +``` + +### Fact-to-dimension historical join + +```sql +SELECT + o.order_id, + o.ordered_at, + c.plan +FROM orders o +JOIN customer_snapshot c + ON o.customer_id = c.customer_id + AND o.ordered_at >= c.valid_from + AND (c.valid_to IS NULL OR o.ordered_at < c.valid_to) +``` + +## Examples + +### Composite key + +```sql +MODEL ( + materialized snapshot, + unique_key [user_id, role_id], + snapshot_strategy check, + check_columns [role_name, role_status], + observed_at snapshot_date, + invalidate_hard_deletes true, +); + +SELECT + user_id, role_id, role_name, role_status, snapshot_date +FROM __source("user_role_daily") +``` + +### Custom validity column names + +```sql +MODEL ( + materialized snapshot, + unique_key [product_id], + snapshot_strategy timestamp, + updated_at modified_at, + valid_from_column effective_from, + valid_to_column effective_to, +); + +SELECT + product_id, name, price, modified_at +FROM __source("products") +``` diff --git a/website/src/content/docs/docs/concepts/sources.mdx b/website/src/content/docs/docs/concepts/sources.mdx new file mode 100644 index 0000000000..0a2509e583 --- /dev/null +++ b/website/src/content/docs/docs/concepts/sources.mdx @@ -0,0 +1,234 @@ +--- +title: "Sources" +description: "Declare external data inputs for your pipeline." +--- + +Sources declare external data that your models depend on. They are defined in YAML files under `sources/` in your project directory. + +## Table sources + +Point at an existing table or view in your warehouse: + +```yaml +sources: + - name: raw_events + database: analytics + schema: raw + table: events +``` + +Reference it in models with `__source("raw_events")`. + +## Expression sources + +Define source data inline as a SQL expression. No external tables or setup scripts needed: + +```yaml +sources: + - name: raw__customers + expression: | + SELECT * FROM (VALUES + (1, 'Leslie', 'Knope', 'leslie@pawnee.gov', TIMESTAMP '2026-01-15 09:00:00'), + (2, 'Ron', 'Swanson', 'ron@pawnee.gov', TIMESTAMP '2026-02-01 08:00:00') + ) AS raw__customers(id, first_name, last_name, email, created_at) +``` + +Expression sources are resolved at compile time. They're the escape hatch for anything the framework doesn't natively model: external tables, warehouse-specific syntax, function calls, or any other relation type that doesn't fit a standard table reference. + +An inline source expression uses macros, constants, and enums available from its source definition +under `sources/`. Declarations limited to one source folder are not available in sibling folders. +See [How Visibility Works](/docs/concepts/declaration-scopes/visibility). + +## Source audits + +Sources support the same audit system as models. Audits attached to sources run before any dependent model is built: + +```yaml +sources: + - name: raw_orders + columns: + - name: id + audits: + - not_null + - unique + audits: + - expression_is_true: + name: no future orders + expression: "ordered_at <= CURRENT_TIMESTAMP" +``` + +If a source audit with `error` severity fails, all downstream models that depend on that source are blocked. + +## Type enforcement + +Type enforcement is implicit for sources. If any column declares a `type`, SQLBuild activates source type enforcement, casts that column during source resolution, and uses declared types for schema-change detection: + +```yaml +sources: + - name: raw__customers + expression: | + SELECT 1 AS id, 'Leslie' AS first_name, 'Knope' AS last_name + columns: + - name: id + type: INTEGER + - name: first_name + - name: last_name +``` + +In this example, only `id` has a type declared, so only `id` is cast. Columns without a `type` are passed through unchanged. + +For expression sources, SQLBuild probes the expression's output columns and builds a projection that casts typed columns while preserving the rest. For table sources, it uses warehouse metadata to validate that declared column names exist and applies casts accordingly. + +You can explicitly set `type_enforcement: false` on a source to disable casting even when column types are declared. + +## Managed sources (loaders) + +Sources can be loaded by Python functions instead of pointing at existing tables or inline expressions. Set `managed: true` to bind a source to the `@loader` function **of the same name**, and SQLBuild will call it to populate the source table: + +```yaml +sources: + - name: raw_customers + managed: true + write_strategy: table + columns: + - name: id + type: INTEGER + - name: name + type: VARCHAR +``` + +Managed sources support incremental write strategies (`table`, `append`, `delete_insert`, `merge`), cursor-based loading, and concurrent execution. + +See [Loaders](/docs/concepts/python-nodes/loaders) for the full guide on writing loader functions, write strategies, the loader context API, and auto-load behavior during builds. + +### Declarative integrations (no Python) + +For common ingestion you can declare the source entirely in YAML, with no `@loader` function. SQLBuild generates the loader for you and runs it during the build: + +- [dlt](/docs/integrations/dlt) - declare `dlt_sources` for `rest_api`, `sql_database`, and `filesystem` sources. +- [ingestr](/docs/integrations/ingestr) - add an `ingestr` block to a source to pull from 50+ sources. + +## Source freshness + +Source freshness lets SQLBuild observe whether a source's data changed between runs. This feeds into +[planning and change detection](/docs/concepts/planning): changed observations propagate to downstream +models and can alter their planned action. + +Configure freshness per source with a `freshness:` block: + +```yaml +sources: + - name: raw_events + schema: raw + table: events + freshness: + strategy: column + column: updated_at + type: timestamp + lag_tolerance: 15m +``` + +### Strategies + +| Strategy | Description | Required fields | +|----------|-------------|-----------------| +| `adapter` | Uses warehouse metadata (e.g. Snowflake `LAST_ALTERED`). No query against the source table. | None | +| `column` | Reads `MAX(column)` from the source table. | `column`, `type` | +| `sql` | Runs a custom query that returns a single scalar value. | `query`, `type` | + +#### adapter + +```yaml +freshness: + strategy: adapter +``` + +Uses adapter-level table metadata. Supported on Snowflake, BigQuery, Databricks, PostgreSQL, DuckDB, and SQL Server. Does not support `type`, `column`, or `query`. + +#### column + +```yaml +freshness: + strategy: column + column: updated_at + type: timestamp +``` + +Queries `MAX(column)` from the source table. The column must be a plain column name (no expressions; use `sql` strategy for those). Requires `type`. + +#### sql + +```yaml +freshness: + strategy: sql + query: "SELECT MAX(version_id) FROM raw.events" + type: integer +``` + +Runs an arbitrary SQL query that returns a single scalar. Requires `type`. Does not support `column`. + +### Type + +The `type` field declares the value kind for comparison: + +| Type | Description | +|------|-------------| +| `timestamp` | Datetime value. Supports `lag_tolerance`. | +| `integer` | Integer value. Change detected by exact comparison. | +| `string` | String value. Change detected by exact comparison. | + +### Lag tolerance + +`lag_tolerance` is optional and only valid with `type: timestamp`. It declares how much the observed timestamp can drift from the previous observation before being treated as a change: + +```yaml +freshness: + strategy: column + column: updated_at + type: timestamp + lag_tolerance: 2h +``` + +Accepts positive durations: `15m` (minutes), `2h` (hours), `1d` (days). If the current observation is within the tolerance of the previous one, the source is treated as unchanged. + +### Auto-observation + +Sources without an explicit `freshness:` block are auto-observed using the `adapter` strategy if: + +- The source has a physical table (not an expression source) +- The source is not managed +- The adapter supports table freshness metadata + +This means most unmanaged table sources get freshness tracking automatically on adapters that support it, with no configuration needed. + +Use [`sqb freshness`](/docs/cli/freshness) to observe source freshness on demand without triggering a build. See [Source freshness](/docs/concepts/planning/source-freshness) for how freshness feeds into change-aware builds. + +### Freshness config reference + +| Field | Description | +|-------|-------------| +| `strategy` | Observation strategy: `adapter`, `column`, or `sql` | +| `type` | Value kind: `timestamp`, `integer`, or `string` | +| `column` | Column name for `column` strategy | +| `query` | SQL query for `sql` strategy | +| `lag_tolerance` | Duration tolerance for timestamp comparisons (e.g. `15m`, `2h`, `1d`). Only valid with `type: timestamp`. | + +## Config reference + +| Field | Description | +|-------|-------------| +| `name` | Source name, used in `__source("name")` references | +| `database` | Target database (optional) | +| `schema` | Physical source schema (optional). For managed sources, overrides target `loader_schema`; otherwise managed loaders fall back to `loader_schema`, then target `schema`. | +| `table` | Target table name (defaults to `name` if omitted) | +| `expression` | Inline SQL expression (alternative to table reference) | +| `managed` | Set to `true` to bind the source to the `@loader` function of the same name (see [Loaders](/docs/concepts/python-nodes/loaders)) | +| `write_strategy` | How the loader writes data: `table`, `append`, `delete_insert`, or `merge` (requires `managed: true`) | +| `cursor_column` | Column for incremental cursor tracking (required for `delete_insert` and `merge`) | +| `unique_key` | Merge key column(s) (required for `merge`) | +| `freshness` | Source freshness observation config (see [Source freshness](#source-freshness)) | +| `description` | Human-readable description | +| `type_enforcement` | Override implicit type enforcement (`true`/`false`). Defaults to `true` when any column declares a type. | +| `contract` | `enforced` or `none`. When enforced, downstream models validate configured column references against source columns. | +| `columns` | Column declarations with optional types and audits | +| `audits` | Source-level audits | diff --git a/website/src/content/docs/docs/concepts/testing.mdx b/website/src/content/docs/docs/concepts/testing.mdx new file mode 100644 index 0000000000..5a7dedb77d --- /dev/null +++ b/website/src/content/docs/docs/concepts/testing.mdx @@ -0,0 +1,667 @@ +--- +title: "Testing" +description: "SQL unit tests and multi-model tests with macro support, assertions, and model chaining." +--- + +SQLBuild supports SQL-native unit tests that validate model logic by comparing actual query results against expected values. Tests can chain across multiple models (multi-model tests), use macros for reusable mock data, and include zero-row assertions. + +For end-to-end testing across many models with physical warehouse relations, see [Scenarios](/docs/concepts/scenarios). + +## How tests work + +A test file defines mock inputs and expected outputs using CTEs. SQLBuild substitutes the mock data into the real model SQL, executes it, and compares the result against the expected CTE using `EXCEPT` queries. Zero mismatched rows means the test passes. + +```sql +-- tests/unit/test_stg_orders.sql +TEST(); + +WITH +__source__raw__orders AS ( + SELECT + 1 AS id, + 100 AS customer_id, + 2 AS waffle_type_id, + 3 AS quantity, + '2026-04-01 10:00:00' AS ordered_at, + 'completed' AS status +), +__expected__stg_orders AS ( + SELECT + 1 AS order_id, + 100 AS customer_id, + 2 AS waffle_type_id, + 3 AS quantity, + '2026-04-01 10:00:00' AS ordered_at, + 'completed' AS status +) +SELECT 1 +``` + +The test: +1. Mocks the `raw__orders` source with the `__source__raw__orders` CTE +2. Runs the real `stg_orders` model SQL with the mock substituted in +3. Compares the output against `__expected__stg_orders` +4. Passes if row counts match and there are zero mismatched rows + +When an expected-output comparison fails, SQLBuild reports the unexpected and missing row counts +and best-effort samples from each direction. Samples are deliberately bounded to three rows, 12 +columns, and 120 characters per value, and pass through diagnostic redaction. They are also present +as structured `unexpected_samples` and `missing_samples` in JSON output. Sampling failure never +replaces the known test failure. + +When exactly one row exists in each difference direction and the bounded samples have the same +columns, SQLBuild also aligns the rows and reports only changed columns with their redacted actual +and expected values. It does not guess an alignment when several rows differ or sampling is +incomplete. + +Before opening a warehouse connection, SQLBuild statically checks fixture shapes when SQL analysis +is enabled. Missing columns are reported together with the test path, fixture resource, and models +that read them. Statically provable collection-versus-scalar type conflicts identify the affected +column and suggest explicit `CAST`, `ARRAY_CONSTRUCT`, or `PARSE_JSON` expressions. SQLBuild never +invents missing values. + +The trailing `SELECT 1` is required as a ceremonial closing statement. + +## CTE conventions + +| Prefix | Purpose | +|--------|---------| +| `__source__` | Mock a source. Replaces `__source("")` in the model SQL. | +| `__ref__` | Mock a model. Replaces `__ref("")` in the model SQL. | +| `__seed__` | Mock a seed. Replaces `__seed("")` in the model SQL. | +| `__table_fn__` | Mock a table function in model mode. Replaces the complete `__table_fn("")(...)` invocation with the fixture relation. | +| `__expected__` | Define expected output for a model. SQLBuild resolves the model's real SQL and compares both ways on the listed columns. | +| `__assert__` | Zero-row assertion. Passes if the query returns no rows; fails with the returned rows as diagnostics. | +| `__macro__` | Mock a macro. Replaces every `@(...)` call with the mock value. | + +Any CTE without one of these prefixes is treated as a helper CTE, available to mock, model, +`__expected__`, and `__assert__` SQL in the test. + +An `__expected__` CTE compares only the columns it lists, matched by name, so column order +does not matter and unlisted model columns are ignored. Row counts are always compared. Listing a +column the model does not output is a test error. When the expected CTE's columns are not explicit +(for example `SELECT *` from another CTE), SQLBuild compares every column by position. + +Helper CTEs can read other helpers and mocks by CTE name, such as `__ref__stg_orders`. Shared +expected rows can therefore live in one helper that both an `__expected__` CTE and an assertion +read: + +```sql +TEST(); + +WITH +__source__raw__orders AS ( + SELECT 1 AS id, 3 AS quantity UNION ALL SELECT 2 AS id, 5 AS quantity +), +expected_orders AS ( + SELECT 1 AS order_id, 3 AS quantity UNION ALL SELECT 2 AS order_id, 5 AS quantity +), +__expected__stg_orders AS ( + SELECT order_id, quantity FROM expected_orders +), +__assert__no_unexpected_order_ids AS ( + SELECT order_id FROM __ref("stg_orders") + EXCEPT + SELECT order_id FROM expected_orders +) +SELECT 1 +``` + +SQLBuild emits each helper and mock a query needs once, in dependency order, including mocks that +a helper reaches through another mock. + +## Multi-model tests + +Tests can span multiple models in a single file. Mock your sources, define an expected output for the model you care about, and SQLBuild automatically resolves every intermediate model using its real SQL. + +```sql +-- Mock two sources, assert on the final mart. +-- stg_orders and stg_payments resolve automatically from their real SQL. +TEST(); + +WITH +__source__raw__orders AS ( + SELECT 1 AS id, 100 AS customer_id, 2 AS waffle_type_id, 3 AS quantity, + '2026-04-01 10:00:00' AS ordered_at, 'completed' AS status +), +__source__raw__payments AS ( + SELECT 1 AS payment_id, 1 AS order_id, 1500 AS amount_cents, + 'credit_card' AS method, '2026-04-01 10:01:00' AS paid_at, 'success' AS status +), +__expected__fact_orders AS ( + SELECT 1 AS order_id, 100 AS customer_id, 1500 AS payment_amount_cents, + 'credit_card' AS payment_method +) +SELECT 1 +``` + +SQLBuild topologically sorts the expected models, resolves each intermediate model's real SQL with mocks substituted, and chains the outputs forward. Every model between the mocked sources and the expected model is computed automatically. + +Inspect that boundary without opening a warehouse connection: + +```bash +sqb test --select fact_orders --inspect +``` + +The resolved plan lists mocked sources, refs, seeds, real models in execution order, expected +models, and any unsatisfied leaf dependencies. A mocked ref is called out explicitly because its +real model SQL will not execute. Inspection exits non-zero when the plan contains unresolved +dependency errors. + +## Mocking refs and seeds + +You can mock models directly with `__ref__` and seeds with `__seed__`, not just sources. This skips the model's real SQL (or the seed's real CSV data) and provides controlled data instead: + +```sql +TEST(); + +WITH +__ref__stg_orders AS ( + SELECT + 1 AS order_id, + 100 AS customer_id, + 2 AS waffle_type_id, + 3 AS quantity, + CAST('2026-04-01 10:00:00' AS TIMESTAMP) AS ordered_at, + 'completed' AS status +), +__ref__stg_payments AS ( + SELECT + 10 AS payment_id, + 1 AS order_id, + 2850 AS amount_cents, + 'card' AS payment_method, + CAST('2026-04-01 10:05:00' AS TIMESTAMP) AS paid_at, + 'success' AS payment_status +), +__seed__waffle_types AS ( + SELECT + 2 AS waffle_type_id, + 'Liege' AS waffle_name, + 'sweet' AS category, + 950 AS price_cents +), +__expected__fact_orders AS ( + SELECT + 1 AS order_id, + 100 AS customer_id, + 2 AS waffle_type_id, + 'Liege' AS waffle_name, + 'sweet' AS waffle_category, + 3 AS quantity, + 2850 AS line_total_cents, + CAST('2026-04-01 10:00:00' AS TIMESTAMP) AS ordered_at, + 'completed' AS order_status, + TRUE AS is_completed_order, + 'card' AS payment_method, + 'success' AS payment_status, + 2850 AS payment_amount_cents +) +SELECT 1 +``` + +You can mix `__source__`, `__ref__`, and `__seed__` mocks in the same test. As long as every leaf dependency is satisfied (either by a source mock, a ref mock, a seed mock, or by being in the expected chain), the test resolves. + +## Mocking table functions in model tests + +Use `__table_fn__` when the model under test reads a managed table function but the test +should provide rows directly. SQLBuild replaces the complete invocation, including its argument +expressions, with the fixture relation. The mocked function is not deployed as a test dependency. + +```sql +-- models/customer_order_totals.sql reads: +-- __table_fn("customer_orders")(42) +TEST(); + +WITH +fixture_orders AS ( + SELECT 101 AS order_id, 42 AS customer_id, 2500 AS amount_cents +), +__table_fn__customer_orders AS ( + SELECT order_id, customer_id, amount_cents FROM fixture_orders +), +__expected__customer_order_totals AS ( + SELECT 42 AS customer_id, 2500 AS total_cents +) +SELECT 1 +``` + +The fixture name must match a declared table function. Helper CTEs are available inside the fixture, +just as they are for source, ref, and seed mocks. A table-function fixture can also be used directly +inside a zero-row assertion. + +This is separate from `TEST (mode table_fn)`, which tests the table function implementation itself. +Model-mode fixtures isolate a model from that implementation; table-function mode executes and +compares the real function. + +## Multiple expected models + +A single test can assert on multiple models. SQLBuild resolves and compares each one independently: + +```sql +TEST(); + +WITH +__source__raw__orders AS ( + SELECT 1 AS id, 100 AS customer_id, 2 AS waffle_type_id, 3 AS quantity, + '2026-04-01 10:00:00' AS ordered_at, 'completed' AS status +), +__source__raw__payments AS ( + SELECT 1 AS payment_id, 1 AS order_id, 1500 AS amount_cents, + 'credit_card' AS method, '2026-04-01 10:01:00' AS paid_at, 'success' AS status +), +__source__raw__customers AS ( + SELECT 100 AS id, 'Leslie' AS first_name, 'Knope' AS last_name, + 'leslie@pawnee.gov' AS email, '2026-01-15 09:00:00' AS created_at +), +__expected__fact_orders AS ( + SELECT 1 AS order_id, 100 AS customer_id, 1500 AS payment_amount_cents, + 'credit_card' AS payment_method +), +__expected__dim_customers AS ( + SELECT 100 AS customer_id, 1 AS total_orders, 1500 AS lifetime_spend_cents +) +SELECT 1 +``` + +If the expected models form a chain (e.g. `stg_orders` feeds into `fact_orders` which feeds into `dim_customers`), SQLBuild resolves them in dependency order, using the output of earlier steps as input to later ones. + +When a test defines `__expected__model_name` output, it may also use the public enums and constants +available to that model. A test that checks several models may use the public enums and constants +available to each of them. Public names are unique, so those values cannot conflict. + +Only explicit expected CTEs create grants. A matching test filename, `__ref__` mock, or nearby model path does not. Model-private declarations and macros are never granted through expected models. + +## Macro-powered mocks + +Because unit tests are written in SQL, they support macro calls. This lets you write reusable mock generators instead of copy-pasting mock data across test files: + +```sql +TEST(); + +WITH +__source__raw__orders AS ( + @mock_orders() +), +__source__raw__payments AS ( + SELECT 1 AS payment_id, 1 AS order_id, 1500 AS amount_cents, 'credit_card' AS method +), +__expected__fact_orders AS ( + SELECT 1 AS order_id, 100 AS customer_id, 1500 AS total_cents, + 'credit_card' AS payment_method +) +SELECT 1 +``` + +The `@mock_orders()` call expands at compile time to whatever SQL the Python macro function returns. + +Test SQL uses macros, enums, and constants available from the test file's directory under +`tests/unit/`. Public enums and constants available to a model are also available when the test +defines `__expected__model_name` output for that model. Model-private values are not available to +tests. See [How Visibility Works](/docs/concepts/declaration-scopes/visibility#tests-and-expected-output). + +## Macro mocking + +When a model uses macros that you want to control in tests (e.g. target-specific logic, dynamic SQL generation), you can override their output with `__macro__` CTEs: + +```sql +TEST(); + +WITH +__macro__country_filter AS ( + SELECT 'country_code = ''US''' +), +__source__raw__orders AS ( + SELECT 1 AS id, 100 AS customer_id, 'US' AS country_code, 'completed' AS status +), +__expected__stg_orders AS ( + SELECT 1 AS order_id, 100 AS customer_id, 'completed' AS status +) +SELECT 1 +``` + +When a `__macro__` mock is defined, every call to `@country_filter(...)` in any model SQL resolved by the test is replaced with the mock value (`country_code = 'US'`). The macro's actual Python function is not called, and the arguments are ignored. + +The mock value must be a single `SELECT` with one string literal. Use doubled single quotes for quotes within the value (standard SQL escaping). + +This is useful for: +- Testing models that use target-specific macros without depending on target config +- Controlling dynamic SQL generation to produce predictable test inputs +- Isolating model logic from macro implementation details + +## Assertions + +Unit tests can include `__assert__` CTEs for property-based checks. An assertion passes if the query returns zero rows - any returned rows are failing examples. + +```sql +TEST(); + +WITH +__ref__stg_orders AS ( + SELECT 1 AS order_id, 100 AS customer_id, 3 AS quantity, + CAST('2026-04-01 10:00:00' AS TIMESTAMP) AS ordered_at, 'completed' AS status +), +__assert__order_ids_are_not_null AS ( + SELECT * FROM __ref("stg_orders") WHERE order_id IS NULL +) +SELECT 1 +``` + +Assertions can be mixed with `__expected__` CTEs in the same test, or used on their own. They are useful when the natural check is "no rows should violate this rule" rather than "the output should exactly equal these rows" - for example, duplicate checks, negative-value constraints, or conditional business rules. + +During `sqb test` and `sqb build`, assertion results appear as nested check rows alongside expected comparisons. + +## Cursor windows in tests + +Models that call `__cursor_start()` and `__cursor_end()` run in a test over one wide window, never +the model's configured `cursor_start` and `cursor_end`: `1900-01-01 00:00:00` to +`2999-12-31 00:00:00` for timestamp cursors, and `-1000000000000000` to `1000000000000000` for +integer cursors. The bounds render as the same typed literals as a real run, so date arithmetic +such as `DATEADD('day', -7, __cursor_start()::DATE)` keeps working and mocked rows are not filtered +out. Microbatch models run once over the whole window, not once per batch. + +To test windowing itself, declare the window in the header. Either key is optional. As in model +configuration, `cursor_start` is inclusive and `cursor_end` is exclusive: + +```sql +TEST (name "daily_orders_skips_rows_outside_window", cursor_start "2026-02-01", cursor_end "2026-02-03"); +``` + +The window applies to every model the test evaluates that uses the intrinsics. Values are checked +against each model's `cursor_type` and `cursor_grain` at compile time. Invalid, misaligned, or +inverted bounds, and a window on a test whose models do not use the intrinsics, are compile errors. +The compiled test SQL shows the rendered window. + +## Empty-input tests + +Do not write a test whose mocks are all empty (`WHERE FALSE`, `WHERE 1 = 0`, `LIMIT 0`, or +`__EMPTY_FIXTURE()`) and whose only checks are an empty `__expected__` CTE or a bare +`SELECT ... FROM __ref("")` assertion. It cannot fail for a model whose rows come from its +inputs. Rule `SQBRTEST203` rejects it, and `SQBRTEST202` does not count it toward +`min_tests_per_model`. Mock representative rows and assert concrete output instead. + +An empty-input test is legitimate when it asserts concrete output, for example a global aggregate +that must return one zero-valued summary row: + +```sql +TEST (name "order_summary__empty_inputs_return_zero_row"); + +WITH +__source__raw__orders AS ( + SELECT CAST(NULL AS INTEGER) AS id, CAST(NULL AS INTEGER) AS amount WHERE FALSE +), +__expected__order_summary AS ( + SELECT 0 AS order_count, 0 AS total_amount +) +SELECT 1 +``` + +Assertions that filter for invalid rows, compare against expected rows, aggregate, or limit their +result are not treated as bare existence checks. Reviewed exceptions are listed by test name in the +project file; see [Rule options](/docs/concepts/rules/configuration-and-selection#rule-options). + +## Test modes + +By default, `TEST()` runs in model mode - mocking sources/refs and comparing model outputs. Three additional modes let you test reusable logic directly without needing a model chain. + +### Macro tests + +Test macro output by calling the macro in `__macro_actual__` and comparing against `__macro_expected__`: + +```sql +TEST (mode macro, name "calculates_line_total_cents"); + +WITH +input_values AS ( + SELECT 950 AS price_cents, 3 AS quantity +), +__macro_actual__ AS ( + SELECT @line_total_cents("price_cents", "quantity") AS line_total_cents + FROM input_values +), +__macro_expected__ AS ( + SELECT 2850 AS line_total_cents +) +SELECT 1 +``` + +Macros are compile-time code, so macro tests expand the macro at compile time and compare the results. During `sqb build`, macro tests run before any model that uses the tested macro. + +### UDF tests + +Test scalar UDFs by calling them in `__udf_actual__` and comparing against `__udf_expected__`: + +```sql +TEST (mode udf, name "detects_completed_orders"); + +WITH +input_values AS ( + SELECT 'completed' AS order_status + UNION ALL + SELECT 'pending' AS order_status +), +__udf_actual__ AS ( + SELECT + order_status, + __udf("udf__is_completed_order")(order_status) AS is_completed_order + FROM input_values +), +__udf_expected__ AS ( + SELECT 'completed' AS order_status, TRUE AS is_completed_order + UNION ALL + SELECT 'pending' AS order_status, FALSE AS is_completed_order +) +SELECT 1 +``` + +UDFs are warehouse objects, so the function is created before the test runs. During `sqb build`, UDF tests run after the function is created but before any model that uses it. + +### Table function tests + +Test table functions by calling them in `__table_fn_actual__` and comparing against `__table_fn_expected__`: + +```sql +TEST (mode table_fn, name "returns_customer_orders"); + +WITH +__table_fn_actual__ AS ( + SELECT order_id, order_status, is_completed_order + FROM __table_fn("table_fn__customer_orders")(1) +), +__table_fn_expected__ AS ( + SELECT 1 AS order_id, 'completed' AS order_status, TRUE AS is_completed_order + UNION ALL + SELECT 2 AS order_id, 'completed' AS order_status, TRUE AS is_completed_order +) +SELECT 1 +``` + +Table function tests run after the function is created. Since table functions are terminal (models cannot depend on them), these tests validate the function independently. + +### Mode rules + +Each mode has strict CTE validation: + +| Mode | Actual CTE | Expected CTE | Allowed | +|------|-----------|--------------|---------| +| `model` (default) | existing model-chain syntax | `__expected__` | `__source__`, `__ref__`, `__seed__`, `__assert__`, `__macro__` | +| `macro` | `__macro_actual__` | `__macro_expected__` | Helper CTEs, `@macro()` calls in actual | +| `udf` | `__udf_actual__` | `__udf_expected__` | Helper CTEs, `__udf()` calls in actual | +| `table_fn` | `__table_fn_actual__` | `__table_fn_expected__` | Helper CTEs, `__table_fn()` calls in actual | + +CTE prefixes from other modes are not allowed. For example, `__source__` in a macro test or `__macro_actual__` in a model test will produce a clear error pointing you to the right mode. Expected CTEs must not call macros, UDFs, or table functions - they should be independent, inspectable expected data. + +## Multiple tests per file + +A single test file can contain multiple `TEST()` blocks. Each block must have a unique `name`: + +```sql +TEST (name "completed_orders_only"); + +WITH +__source__raw__orders AS ( + SELECT 1 AS id, 100 AS customer_id, 'completed' AS status +), +__expected__stg_orders AS ( + SELECT 1 AS order_id, 100 AS customer_id, 'completed' AS status +) +SELECT 1 + +TEST (name "cancelled_orders_excluded"); + +WITH +__source__raw__orders AS ( + SELECT 1 AS id, 100 AS customer_id, 'cancelled' AS status +), +__expected__stg_orders AS ( + SELECT 1 AS order_id, 100 AS customer_id, 'cancelled' AS status +) +SELECT 1 +``` + +A file with a single test can omit the `name` field. Files with multiple tests require names on every block. + +## Repeating test logic + +SQLBuild supports three repetition patterns: + +| Pattern | Use when | Result identity | +|---------|----------|-----------------| +| Multiple `TEST` blocks | Cases need different SQL shapes | One result per block | +| Macro or `VALUES` case table | One aggregate comparison is sufficient | One result for the table | +| Native parameters and cases | Cases share a template but need independent status | One result per named case | + +### Native independent cases + +Declare a typed schema and ordered named cases in one header: + +```sql +TEST ( + name "order_status_maps_source_states", + parameters ( + source_status string, + expected_status string, + ), + cases ( + completed (source_status "completed", expected_status "completed"), + cancelled (source_status "cancelled", expected_status "excluded"), + pending (source_status "pending", expected_status "open"), + ), +); + +WITH +__source__raw__orders AS ( + SELECT 1 AS id, @param("source_status") AS status +), +__expected__stg_orders AS ( + SELECT 1 AS id, @param("expected_status") AS status +) +SELECT 1 +``` + +The cases report independently as `order status: maps source states [completed]`, `[cancelled]`, +and `[pending]`. Authored order controls display order. Selecting `stg_orders` selects every case by +default. Add `--case` to run one named case without changing the test file: + +```bash +sqb test --select stg_orders --case cancelled +``` + +If the case does not exist within the parent/model selection, SQLBuild reports the available case +names. + +Supported scalar types are `string`, `integer`, `boolean`, `float`, and exact `decimal`. Decimal +values are quoted, such as `tax_rate "0.2000"`, so they never pass through binary float. Boolean +and integer remain distinct. Raw SQL and collection parameters are rejected. + +Nullable parameters use an expanded declaration and are valid where SQL can infer the null type: + +```sql +parameters ( + cancellation_reason (type string, nullable true), +), +cases ( + completed (cancellation_reason null), +) +``` + +Every case provides exactly the declared parameters, and every declaration must appear as +`@param("name")` in the template. SQLBuild rejects missing, extra, incompatible, undeclared, and +unused values before execution. + +Expansion is deterministic: + +```text +parse TEST template and cases + -> render typed @param values with the active adapter + -> expand @const and @enum references + -> expand macros + -> validate SQL and compile expected models/assertions +``` + +Values can therefore feed macro arguments, for example +`@order_fixture(@param("source_status"))`. Parameters work in model, macro, UDF, and table-function +modes and preserve every expected model in multi-model tests. + +Text and JSON output include parent/case identity and safe typed values. Compile JSON, manifests, +DAG checks, and compiled/runtime SQL artifacts retain source, block, case, and content-fingerprint +provenance. Changing one case changes that case's fingerprint without reassigning another case's +stable identity. + +Scenarios are intentionally not parameterized. Keep one scenario per coherent business world so +capture and replay identity remains explicit. + +### Aggregate SQL case tables + +When independent status is unnecessary, a normal SQL case table remains concise: + +```sql +WITH cases(case_name, source_status, expected_status) AS ( + VALUES + ('completed', 'completed', 'completed'), + ('cancelled', 'cancelled', 'excluded') +), +__expected__status_mapping AS ( + SELECT case_name, expected_status FROM cases +) +SELECT 1 +``` + +This produces one aggregate test result. Use native cases when each row must pass or fail +independently. + +Quoted strings in `VALUES` rows may contain commas, brackets, and parentheses. SQLBuild preserves +these literals while extracting test CTEs; use normal SQL quote escaping for embedded quotes. + +## Test placement + +Place unit test files under `tests/unit/` in your project directory. SQLBuild discovers all `.sql` files in this directory recursively. + +``` +tests/ + unit/ + test_stg_orders.sql # model test + test_fact_orders.sql # model test with assertion + test_daily_revenue_chain.sql # multi-model test + test_line_total_cents_macro.sql # macro test + test_is_completed_order_udf.sql # UDF test + test_customer_orders_table_fn.sql # table function test + scenarios/ + ... +``` + +## Running tests + +Tests run automatically during `sqb build` in DAG order, before their target model is materialized. + +Run tests standalone: + +```bash +sqb test +``` + +Scope to specific models: + +```bash +sqb test --select stg_orders +``` diff --git a/website/src/content/docs/docs/feature-comparison.mdx b/website/src/content/docs/docs/feature-comparison.mdx new file mode 100644 index 0000000000..0470d2c666 --- /dev/null +++ b/website/src/content/docs/docs/feature-comparison.mdx @@ -0,0 +1,129 @@ +--- +title: "Feature Comparison" +description: "Feature comparison between SQLBuild, dbt, and SQLMesh." +--- + +SQLBuild, dbt, and SQLMesh are all SQL pipeline frameworks. They share common ground but differ in design philosophy and feature focus. + +## Feature comparison + +### Testing + +| Feature | SQLBuild | dbt | SQLMesh | +|---------|----------|-----|---------| +| Multi-model tests | Chain across multiple models | YAML-stub, single model | CTE-based, single model | +| Typed parameterized unit tests | Independent named cases with adapter-rendered scalar values | No | No | +| Macros as test helpers | Tests are SQL - macros work as reusable fixture generators | No (YAML stubs) | No | +| E2E scenario tests | Fixture worlds with real graph execution | No | No | +| Local E2E replay | Capture from warehouse, replay in DuckDB | No | No | +| Macro / UDF / table function tests | `TEST(mode macro)`, `TEST(mode udf)`, or `TEST(mode table_fn)` | No | No | +| Zero-row assertions | `__assert__` CTEs in tests and scenarios | No | No | +| Failure diagnostics | Bounded, redacted unexpected and missing row samples in text and JSON | Adapter/tool dependent | Row diffs | + +### Audits + +| Feature | SQLBuild | dbt | SQLMesh | +|---------|----------|-----|---------| +| Built-in audits | not_null, unique, accepted_values, relationships | not_null, unique, accepted_values, relationships | Extensive (statistical, string pattern, etc.) | +| Blocking audits | Block promotion from staging table | Tests run after materialization | Audits gate plan application; run-time audits execute after the interval is materialized | +| Delta/interval-scoped audits | Per-microbatch audit cycle before DML | No | Audit query filtered to processed intervals for time-range models | +| Measurement audits | Thresholds, minimum samples, bounded evidence, and immutable result history | Package/custom test patterns | Custom audits | +| Audit factories | Typed Python factories generate equivalent reviewed audit instances | Macros/packages | Python audit definitions | + +### Compilation + +| Feature | SQLBuild | dbt | SQLMesh | +|---------|----------|-----|---------| +| SQL analysis | Offline syntax, binding, type inference, semantic validation, and lineage (Polyglot) | dbt Core: none; dbt Fusion engine: compile-time (proprietary; built on Apache-2.0 dbt Core v2) | Compile-time (SQLGlot) | +| Focused compilation | Full graph integrity with deep analysis limited to selection and required upstream closure | Selected compilation | Selected planning | +| Column-level lineage | Compile-time, fast and rich modes | dbt Core: post-hoc via docs; dbt Fusion engine: compile-time | Compile-time | +| Column contract validation | Compile-time inference plus runtime enforcement with `contract enforced` | YAML schema contracts at runtime | Schema contracts via plan | +| Existing-schema contracts | Online read-only diff and safe repository generation | Codegen/packages | External schema tooling | +| Rules and formatting | Compiler-integrated native and custom diagnostics, reasoned suppressions, and separate canonical formatting | External tools | Built-in formatter plus external linting | +| Compiler-integrated Rules | Native and custom rules over compiler-owned SQL, models, dependencies, contracts, tests, and project paths | Project conventions through packages and external tooling | Built-in audits and external linting | +| SQL transpilation | For local E2E replay into DuckDB | No | For cross-dialect model execution | +| Python macros | `@macro()` syntax | No (Jinja only) | SQLMesh macro syntax | +| Compiler-enforced declaration scopes | Project, descendant-public, exact-owner-private, and model-private tiers with offline `sqb scope` inspection | No lexical declaration scopes | No lexical declaration scopes | +| Jinja support | No (Python macros instead) | Yes (core templating) | Yes | + +### Incremental + +| Feature | SQLBuild | dbt | SQLMesh | +|---------|----------|-----|---------| +| Incremental strategies | append, delete_insert, merge, SCD Type 2 | append, delete_insert, merge, snapshots | delete_insert (time-range), merge (unique-key), SCD Type 2, partition | +| Microbatch execution | Watermark and rolling-window strategies, per-batch audits, limits, and opt-in concurrency | Microbatch | Batch size support | +| Interval progress | Sequential runs derive progress from target/input cursors; concurrent runs coordinate with append-only facts | No | Tracks intervals in a state store | +| SCD Type 2 models | Timestamp and check strategies, historical input, hard deletes | Snapshots (timestamp and check strategies) | `SCD_TYPE_2` model kind (timestamp and check strategies) | + +### Planning and change detection + +| Feature | SQLBuild | dbt | SQLMesh | +|---------|----------|-----|---------| +| Warehouse-native state | Append-only tables in the warehouse; no external state database | manifest.json artifacts | Requires external state store (SQLite/PostgreSQL) | +| Source freshness | `sqb freshness` with adapter/column/sql strategies, lag tolerance, and CI gating | `dbt source freshness` | No dedicated freshness command; `signals` gate model evaluation until external data is ready | +| Cascade propagation | Topological walk with `replay_on_change` policy inheritance and override | No cascade control | Cascades through version hashes | + +### Environments + +| Feature | SQLBuild | dbt | SQLMesh | +|---------|----------|-----|---------| +| Virtual environments | No. Build branches into their own schema, and copy relations between targets with `sqb clone` | No | Pointer swaps, no compute cost | +| Data diffs | Full row-level data comparison across targets | No | Table diff | +| Zero-copy cloning | `sqb clone` | No | No | + +### Models + +| Feature | SQLBuild | dbt | SQLMesh | +|---------|----------|-----|---------| +| SQL models | `MODEL()` header with inline config | Jinja-templated SQL + YAML sidecar | `MODEL` DDL | +| Python models | Coming soon | Pandas, PySpark, Snowpark, BigFrames | Pandas, PySpark, Snowpark, BigFrames | +| Custom materializations | Python with full framework hooks | Jinja-based | Python-based custom model kinds | +| Lifecycle hooks | Typed inline SQL, reusable parameterized SQL resources, and Python hooks with compile-time validation and `HookContext` | Jinja pre/post hooks | Python pre/post hooks | + +### Python nodes + +| Feature | SQLBuild | dbt | SQLMesh | +|---------|----------|-----|---------| +| Tasks | `@task` - Python computation as DAG nodes | No | No | +| Assets | `@asset` - external artifact production/observation | No | No | +| Checks | `@check` - Python validation of tasks, assets, and loaders | No | No | +| Factories | `@factory` - programmatic node generation | No | No | +| Providers | Shared runtime services with name-based injection into nodes and hooks | No | No | + +### dbt interoperability + +| Feature | SQLBuild | dbt | SQLMesh | +|---------|----------|-----|---------| +| dbt compatibility | Reads dbt manifests, coordinates dbt and SQLBuild selection, and supports SQLBuild models downstream | N/A | Jinja compatibility layer plus own macro system | + +### Sources + +| Feature | SQLBuild | dbt | SQLMesh | +|---------|----------|-----|---------| +| Source loaders | Python `@loader` functions with table/append/delete_insert/merge strategies | No (external to dbt) | No (external to SQLMesh) | +| Declarative ingestion | dlt and ingestr integrations - YAML-only source config, no Python | No | No | +| Auto-load during builds | Managed sources loaded before dependent models | No | No | +| Source deferral | `--defer-sources-to` reads source data from another target | No | No | + +### Other + +| Feature | SQLBuild | dbt | SQLMesh | +|---------|----------|-----|---------| +| Reference syntax | `__ref()` - parses as valid SQL | `{{ ref() }}` - Jinja template | `model_name` with dependency tracking | +| Adapters | DuckDB, MotherDuck, Snowflake, BigQuery, Databricks, PostgreSQL, SQL Server | 30+ (community adapters) | DuckDB, Snowflake, BigQuery, Databricks, Spark, Redshift, Postgres, Trino, MySQL | +| State requirements | Stateless by default | manifest.json + target/ | Requires state store (local database or PostgreSQL for production) | +| Playground | `sqb playground` | Clone example repo | Example project | +| AI agent skills | General guidance with `sqb skills`; Rules guidance with `sqb rules skills` | No | No | +| Execution observability | Immutable schema-versioned lifecycle facts, invocation-local ordering, orchestration context, and typed project sinks | Events/artifacts plus external observability | Plans, state, and external observability | + +## Where each tool fits + +| Tool | Best for | +|------|----------| +| **SQLBuild** | Rigor-first SQL pipelines: compile-time verification, pre-promotion audit gating, multi-model tests, and local E2E replay by default. Opt into change-aware builds and warehouse-native state when full rebuilds get expensive, plus ingestion and Python nodes as the project grows. | +| **dbt** | The most widely adopted SQL transformation framework with the largest adapter and community ecosystem. | +| **SQLMesh** | State-managed pipelines with virtual environments, interval tracking, and cross-dialect transpilation. | + +## Not yet in SQLBuild + +- **Broader adapter support** - ClickHouse, Redshift, Trino, Spark, Athena diff --git a/website/src/content/docs/docs/index.mdx b/website/src/content/docs/docs/index.mdx new file mode 100644 index 0000000000..ebce1dab2d --- /dev/null +++ b/website/src/content/docs/docs/index.mdx @@ -0,0 +1,69 @@ +--- +title: "Introduction" +description: "The refactorable warehouse. Verify early, test properly, and refactor safely." +--- + +import Card from '../../../components/mintlify/Card.astro'; +import CardGroup from '../../../components/mintlify/CardGroup.astro'; + +SQLBuild is a free, open-source framework for SQL and Python data pipelines. It brings compile-time +checks, tests and diffs to your warehouse, so you can change it as often as you change your code. + +Valid SQL isn't the same as correct SQL. A query can compile, run and return rows, and still produce +a wrong number that people already trust. SQLBuild is built to catch that before it reaches +production. + +## Verify early + +Before anything runs, `sqb compile` analyses the whole project offline, with no warehouse +connection. It checks SQL, model references and declared [contracts](/docs/concepts/models/contracts), +and traces [column lineage](/docs/concepts/column-lineage). [Rules](/docs/concepts/rules) turn +decisions from code review into compile errors, and +[declaration scopes](/docs/concepts/declaration-scopes) keep enums, constants and macros visible only +where they are used. + +## Test properly + +[SQL unit tests](/docs/concepts/testing) mock your sources, resolve every model in between from its +real SQL, and compare the output with what you expect. [Scenarios](/docs/concepts/scenarios) build the +real model graph against test data, then replay it locally on DuckDB in CI. +[Audits](/docs/concepts/audits) check each build before it replaces production. + +## Refactor safely + +[`sqb plan`](/docs/concepts/planning) shows why each model will run and how much it will rebuild. +[`sqb diff`](/docs/concepts/diff) compares real data between targets before a change ships. When you +rename a model, a [migration](/docs/concepts/models/migrations) moves its table instead of rebuilding +its history, and the [janitor](/docs/cli/janitor) archives tables the project no longer builds before +it deletes them. + +## How it fits together + +| Step | Command | What it does | +|------|---------|--------------| +| Compile | [`sqb compile`](/docs/cli/compile) | Checks the project offline | +| Test | [`sqb test`](/docs/cli/test) | Runs SQL unit tests | +| Plan | [`sqb plan`](/docs/cli/plan) | Shows what will run, why, and how much it will rebuild | +| Build | [`sqb build`](/docs/cli/build) | Runs tests, builds models in dependency order, and blocks bad data with audits | +| Compare | [`sqb diff`](/docs/cli/diff) | Compares schemas and rows between two targets | + +SQLBuild works with DuckDB, MotherDuck, Snowflake, BigQuery, Databricks, PostgreSQL and SQL Server. +DuckDB runs locally, so you can try everything without warehouse credentials. See +[Adapters](/docs/concepts/adapters) for connection setup. + +## Where to start + + + + Build a complete example project locally in a minute. + + + Run SQLBuild alongside an existing dbt project. + + + Models, tests, audits, planning and the rest. + + + Every command and flag. + + diff --git a/website/src/content/docs/docs/integrations/dagster-reference.mdx b/website/src/content/docs/docs/integrations/dagster-reference.mdx new file mode 100644 index 0000000000..94310410bc --- /dev/null +++ b/website/src/content/docs/docs/integrations/dagster-reference.mdx @@ -0,0 +1,249 @@ +--- +title: "API Reference" +description: "Dagster integration classes, decorators, and translator hooks." +--- + +## SqlBuildProject + +Project metadata and DAG artifact preparation. + +```python +from sqlbuild.integrations.dagster import SqlBuildProject + +project = SqlBuildProject( + project_dir=".", + target_path="target", + dag_filename="sqlbuild_dag.json", + sqb_command=("sqb",), + prepare_project_cli_args=("compile", "--dag"), +) +``` + +| Parameter | Default | Description | +|-----------|---------|-------------| +| `project_dir` | (required) | Path to the SQLBuild project root | +| `target_path` | `"target"` | Relative path to the target directory | +| `dag_filename` | `"sqlbuild_dag.json"` | DAG artifact filename | +| `sqb_command` | `("sqb",)` | Command to invoke SQLBuild CLI | +| `prepare_project_cli_args` | `("compile", "--dag")` | CLI args used by `prepare()` to generate the DAG | + +### Methods + +| Method | Description | +|--------|-------------| +| `dag_path` | Property. Returns the full path to the DAG artifact (`project_dir / target_path / dag_filename`). | +| `prepare()` | Runs `sqb compile --dag ` to generate the DAG artifact. Raises `DagsterProjectPrepareError` on failure. | +| `prepare_if_dev()` | Calls `prepare()` only when the `DAGSTER_IS_DEV_CLI` environment variable is set. | + +## SqlBuildCliResource + +Dagster `ConfigurableResource` that shells out to the SQLBuild CLI. + +```python +from sqlbuild.integrations.dagster import SqlBuildCliResource, SqlBuildProject + +# From a project +resource = SqlBuildCliResource(SqlBuildProject(project_dir=".")) + +# Or with explicit paths +resource = SqlBuildCliResource( + project_dir=".", + sqb_command=["sqb"], + dag_path="target/sqlbuild_dag.json", +) +``` + +| Parameter | Default | Description | +|-----------|---------|-------------| +| `project_dir` | `"."` | Path to the SQLBuild project root, or a `SqlBuildProject` instance | +| `sqb_command` | `["sqb"]` | Command to invoke SQLBuild CLI | +| `dag_path` | `None` | Path to the DAG artifact. When set, enables asset selection bridging and structured event emission. | + +### cli() + +Create a CLI invocation for the provided command arguments. + +```python +invocation = resource.cli( + ["build"], + context=context, # Dagster execution context (optional) + raise_on_error=True, # raise Failure on non-zero exit (default) +) +``` + +Returns a `SqlBuildCliInvocation`. + +## SqlBuildCliInvocation + +A running or completed SQLBuild CLI subprocess. + +### Methods + +| Method | Description | +|--------|-------------| +| `wait()` | Wait for the process to complete. Streams stdout/stderr in real time. Returns `self`. | +| `stream()` | Wait for the process, then yield Dagster `MaterializeResult` and `AssetCheckResult` events parsed from execution JSON. | +| `is_successful()` | Returns `True` if exit code is 0. | +| `get_error()` | Returns a Dagster `Failure` if the process failed, `None` otherwise. | +| `get_artifact(name)` | Read a JSON artifact from the project's `target/` directory. | + +### Properties + +| Property | Description | +|----------|-------------| +| `stdout` | Captured stdout after `wait()` or `stream()`. | +| `stderr` | Captured stderr after `wait()` or `stream()`. | +| `returncode` | Process exit code after completion. | +| `execution_payload` | Parsed execution JSON, if available. | + +### stream() behavior + +`stream()` is the primary way to emit Dagster events from a SQLBuild execution: + +1. Waits for the subprocess to complete while streaming output to stdout/stderr +2. Reads the structured execution JSON (from `--json-output` tempfile) +3. Maps each completed asset to a `MaterializeResult` with metadata (status, duration, row counts) +4. Maps each check result to an `AssetCheckResult` with pass/fail, severity, and check metadata +5. If the DAG artifact is available, results are matched to the correct Dagster asset keys +6. Raises `Failure` if the process exited non-zero and `raise_on_error` is set + +## sqlbuild_assets + +Decorator that creates a Dagster multi-asset definition from a SQLBuild DAG artifact. + +```python +from sqlbuild.integrations.dagster import sqlbuild_assets + +@sqlbuild_assets( + project=project, # or dag=path_or_dict + translator=MyTranslator(), # optional + name="my_sqlbuild_assets", # optional + include_scenario_checks=True, # default: True + required_resource_keys={"sqb"}, # optional +) +def my_assets(context, sqb): + yield from sqb.cli(["build"], context=context).stream() +``` + +| Parameter | Default | Description | +|-----------|---------|-------------| +| `project` | `None` | `SqlBuildProject` instance. Mutually exclusive with `dag`. | +| `dag` | `None` | DAG artifact as a path, string, or pre-loaded dict. Mutually exclusive with `project`. | +| `translator` | `SqlBuildDagsterTranslator()` | Translator for asset keys, groups, tags, and metadata. | +| `name` | `None` | Override the Dagster multi-asset name. | +| `include_scenario_checks` | `True` | Whether to include scenario checks alongside test and audit checks. | +| `required_resource_keys` | `None` | Additional Dagster resource keys to require. | + +If neither `project` nor `dag` is provided, the decorator looks for `target/sqlbuild_dag.json` in the current directory. + +## sqlbuild_scenario_checks + +Decorator that creates a Dagster multi-asset-check definition for SQLBuild scenarios only. Useful when you want to run scenarios separately from the main build. + +```python +from sqlbuild.integrations.dagster import sqlbuild_scenario_checks + +@sqlbuild_scenario_checks(project=project) +def my_scenario_checks(context, sqb): + yield from sqb.cli(["scenario", "test"], context=context).stream() +``` + +Parameters are the same as `sqlbuild_assets` except there is no `include_scenario_checks` flag. + +## SqlBuildDagsterTranslator + +Customise how SQLBuild DAG nodes map to Dagster asset metadata. Subclass and override any method. + +### Methods + +| Method | Arguments | Returns | Default behavior | +|--------|-----------|---------|------------------| +| `get_asset_key` | `node` | `AssetKey` | Authored source `meta.dagster.asset_key`, otherwise `AssetKey(node["asset_key"])` | +| `is_asset_node` | `node` | `bool` | Include the node as a Dagster asset | +| `is_asset_check` | `check` | `bool` | Include the check in the multi-asset definition | +| `get_group_name` | `node` | `str \| None` | Authored group, project name, then `sqlbuild` | +| `get_tags` | `node` | `dict[str, str]` | `sqlbuild/kind` tag plus any model tags | +| `get_metadata` | `node` | `dict[str, Any]` | SQLBuild identity/selector plus available path, target, SQL, columns, lineage, group, language, materialization, loader, and authored metadata | +| `get_description` | `node` | `str \| None` | Node description if present | +| `get_check_name` | `check` | `str` | `"{kind}__{name}"` with optional column/target suffix | +| `get_check_metadata` | `check` | `dict[str, Any]` | Check ID, kind, name, and selector | + +### Node structure + +Each `node` dict passed to translator methods contains: + +```json +{ + "id": "model:fact_orders", + "kind": "model", + "name": "fact_orders", + "asset_key": ["dev", "fact_orders"], + "target": { + "database": null, + "schema": "dev", + "name": "fact_orders", + "qualified_name": "dev.fact_orders" + }, + "path": "models/marts/fact_orders.sql", + "description": "Order fact table with waffle and payment details.", + "tags": ["marts"], + "columns": [ + {"name": "order_id", "type": "INTEGER"} + ], + "materialization_type": "table" +} +``` + +Python node asset keys use a two-part key with the node kind as prefix: + +| Kind | Asset key example | +|------|-------------------| +| Task | `("task", "prepare_orders")` | +| Asset | `("asset", "orders_export")` | +| Loader | `("loader", "raw_orders")` | + +A source can provide a stable Dagster-native key without replacing the translator: + +```yaml +sources: + - name: raw__orders + meta: + dagster: + asset_key: [warehouse, raw, orders] +``` + +Override `is_asset_node` or `is_asset_check` when one SQLBuild project intentionally exposes only a +subset through a particular Dagster definition. Filtering changes Dagster ownership; it does not +remove the resource from SQLBuild's compiled graph. + +### Check structure + +Each `check` dict passed to check translator methods contains: + +```json +{ + "id": "audit:not_null:model:fact_orders:order_id", + "kind": "audit", + "name": "not_null", + "checked_asset_ids": ["model:fact_orders"], + "path": "audits/generic/not_null.sql", + "severity": "error", + "attached_target_name": "fact_orders", + "attached_column_name": "order_id" +} +``` + +Scenario checks include additional fields: + +```json +{ + "id": "scenario:daily_revenue_minimal", + "kind": "scenario", + "name": "daily_revenue_minimal", + "checked_asset_ids": ["model:daily_revenue"], + "path": "tests/scenarios/revenue/daily_revenue_minimal.sql", + "assertion_names": ["no_negative_revenue"], + "expected_model_names": ["daily_revenue"], + "fixture_refs": ["stg_orders", "stg_payments"] +} +``` diff --git a/website/src/content/docs/docs/integrations/dagster.mdx b/website/src/content/docs/docs/integrations/dagster.mdx new file mode 100644 index 0000000000..7e3b61b4e4 --- /dev/null +++ b/website/src/content/docs/docs/integrations/dagster.mdx @@ -0,0 +1,159 @@ +--- +title: "Overview" +description: "Orchestrate SQLBuild pipelines with Dagster scheduling, retries, and asset UI." +--- + +SQLBuild includes a Dagster integration that maps your project's models, sources, seeds, functions, loaders, tasks, assets, tests, audits, scenarios, and Python checks into Dagster assets and asset checks. SQLBuild handles the SQL transformation layer. Dagster handles scheduling, retries, alerting, and the asset-centric UI. + +## Install + +```bash +uv pip install 'sqlbuild[dagster]' +# or +pip install 'sqlbuild[dagster]' +``` + +This installs `dagster` and `dagster-webserver` alongside SQLBuild. + +## Try it + +```bash +sqb playground --template dagster +cd sqlbuild-playground +dagster dev -f dagster/definitions.py +``` + +This creates the waffle shop project with a `dagster/definitions.py` that includes asset definitions, scenario checks, and a configured resource. Open the Dagster UI, materialize the assets, then run the scenario checks. + +## How it works + +1. `sqb compile --dag` generates a static `sqlbuild_dag.json` artifact with your project's full graph (nodes, edges, checks) +2. `@sqlbuild_assets()` reads the artifact and creates one Dagster `AssetSpec` per source, seed, model, function, loader, task, and asset, with dependency edges preserved +3. `SqlBuildCliResource` shells out to `sqb build`, `sqb test`, `sqb scenario test`, etc. as subprocesses +4. Execution results (materializations, audit pass/fail, scenario outcomes) are read from SQLBuild's structured integration-result stream and emitted as Dagster `MaterializeResult` and `AssetCheckResult` events + +SQLBuild tests, audits, and Python checks become Dagster asset checks. Scenarios become asset checks attached to the models they exercise. + +## Quickstart + +```python +# definitions.py +from sqlbuild.integrations.dagster import ( + SqlBuildCliResource, + SqlBuildProject, + sqlbuild_assets, +) +import dagster as dg + +project = SqlBuildProject(project_dir=".") +project.prepare_if_dev() # auto-generates DAG artifact in dagster dev + +@sqlbuild_assets(project=project) +def my_sqlbuild_assets(context: dg.AssetExecutionContext, sqb: SqlBuildCliResource): + yield from sqb.cli(["build"], context=context).stream() + +defs = dg.Definitions( + assets=[my_sqlbuild_assets], + resources={"sqb": SqlBuildCliResource(project)}, +) +``` + +```bash +dagster dev -f definitions.py +``` + +Dagster discovers every SQLBuild model, loader, task, and asset as a Dagster asset. Selecting a subset in the Dagster UI automatically scopes the `sqb build` invocation to those nodes via `--select`. + +For production deployments, use `project.prepare()` or `sqb compile --dag` to generate the DAG artifact explicitly in your CI pipeline. + +## Asset selection + +When you select a subset of assets in the Dagster UI, the integration automatically: + +1. Maps selected Dagster asset keys back to SQLBuild node names using the DAG artifact +2. Writes the selectors to a temporary file +3. Passes `--select-file` to the `sqb` CLI so only the selected models are built + +This means Dagster's asset subsetting works naturally with SQLBuild's selector system. + +An explicit `--select`, `--select-file`, or `--exclude` supplied by application code takes +precedence; SQLBuild does not silently combine it with Dagster's implicit asset selection. + +## Checks + +SQLBuild tests, audits, scenarios, and Python checks are registered as Dagster asset checks: + +- **Unit tests** become checks attached to the models they test +- **Audits** become checks attached to the model or source they audit, with severity mapped to `AssetCheckSeverity.ERROR` or `AssetCheckSeverity.WARN` +- **Scenarios** become checks attached to the models they exercise +- **Python checks** (`@check`) become checks attached to the tasks, assets, or loaders they validate + +Check results are emitted with pass/fail status and metadata from the execution JSON. + +## Execution correlation + +When a Dagster execution context is passed to `sqb.cli`, SQLBuild propagates bounded orchestration +metadata to command-output and lifecycle records: + +- integration name (`dagster`) +- Dagster run ID and job name +- step/op key +- retry number +- partition key, when the run is partitioned + +Lifecycle events remain SQLBuild's immutable execution facts. Human stdout/stderr remains a +correlated troubleshooting transcript; the integration does not parse terminal text to establish +resource status. See [Execution Observability](/docs/concepts/observability). + +## Scenarios as checks + +Scenarios can be included as asset checks alongside tests and audits (the default), or run separately: + +```python +from sqlbuild.integrations.dagster import sqlbuild_assets, sqlbuild_scenario_checks + +# Include scenario checks with other assets (default) +@sqlbuild_assets(project=project, include_scenario_checks=True) +def my_assets(context, sqb): + yield from sqb.cli(["build"], context=context).stream() + +# Or run scenarios separately +@sqlbuild_scenario_checks(project=project) +def my_scenario_checks(context, sqb): + yield from sqb.cli(["scenario", "test"], context=context).stream() +``` + +## Project preparation + +`SqlBuildProject.prepare()` regenerates the DAG artifact by running `sqb compile --dag`. Use `prepare_if_dev()` to only regenerate during local development: + +```python +project = SqlBuildProject(project_dir=".") +project.prepare_if_dev() # only runs when DAGSTER_IS_DEV_CLI is set +``` + +This keeps the Dagster UI in sync with your latest model changes during development without regenerating in production. + +## Translator + +Customise how SQLBuild nodes map to Dagster assets by subclassing `SqlBuildDagsterTranslator`: + +```python +from sqlbuild.integrations.dagster import SqlBuildDagsterTranslator +import dagster as dg + +class MyTranslator(SqlBuildDagsterTranslator): + def get_asset_key(self, node): + # Prefix all asset keys with the project name + return dg.AssetKey(["my_project", *node["asset_key"]]) + + def get_group_name(self, node): + # Group by materialization type instead of kind + return node.get("materialization_type", "other") + +@sqlbuild_assets(project=project, translator=MyTranslator()) +def my_assets(context, sqb): + yield from sqb.cli(["build"], context=context).stream() +``` + +See the [API reference](/docs/integrations/dagster-reference) for all translator methods. diff --git a/website/src/content/docs/docs/integrations/dlt.mdx b/website/src/content/docs/docs/integrations/dlt.mdx new file mode 100644 index 0000000000..6ed0539b12 --- /dev/null +++ b/website/src/content/docs/docs/integrations/dlt.mdx @@ -0,0 +1,222 @@ +--- +title: "dlt" +description: "Declarative dlt sources in YAML, or full dlt pipelines inside Python source loaders." +--- + +import Frame from '../../../../components/mintlify/Frame.astro'; + +[dlt](https://dlthub.com) is an open-source Python library for loading data from APIs, databases, cloud storage, and more. SQLBuild integrates with dlt two ways: + +- **Declarative dlt sources** - configure dlt directly in your source YAML, no Python code. SQLBuild runs the dlt pipeline as part of the build lifecycle. Best for the common REST API, SQL database, and filesystem cases. +- **dlt inside a Python loader** - wrap any `dlt.pipeline(...)` in a SQLBuild [source loader](/docs/concepts/python-nodes/loaders) for full control when you need dlt features beyond the declarative surface. + +## Install + +```bash +pip install 'sqlbuild[dlt]' +# or +uv pip install 'sqlbuild[dlt]' +``` + +This installs dlt with the `duckdb`, `filesystem`, and `sql_database` extras alongside SQLBuild. + +## Declarative dlt sources (YAML) + +Declare a `dlt_sources` list in a `sources/*.yml` file. Each entry has a `type`, a `config` block passed to the dlt source, and a list of `resources` that become SQLBuild managed sources. SQLBuild generates a synthetic loader per resource and writes into the database its adapter manages, no Python and no destination setup required. + +Supported source types: `rest_api`, `sql_database`, `filesystem`. + + + sqb build loading three declarative dlt sources (raw_customers, raw_orders, raw_payments) as external (dlt) sources, each showing dlt pipeline extract/normalize/load progress before the models build + + +### REST API + +```yaml +# sources/github.yml +dlt_sources: + - type: rest_api + config: + client: + base_url: https://api.github.com/ + headers: + Authorization: "Bearer ${github_token}" + paginator: + type: header_link + resources: + - name: raw_github_issues + write_disposition: append + endpoint: + path: "repos/${github_owner}/${github_repo}/issues" + params: + state: all + per_page: 100 +``` + +`rest_api` requires `client.base_url` in `config`. Each resource needs an `endpoint` mapping; the dlt resource name comes from `endpoint.name` if present, otherwise the resource `name`. + +### SQL database + +```yaml +# sources/postgres.yml +dlt_sources: + - type: sql_database + config: + credentials: "${postgres_connection_string}" + resources: + - name: raw_customers + table: customers + write_disposition: merge + primary_key: id +``` + +`sql_database` requires `credentials` in `config`, and each resource requires a `table`. + +### Filesystem + +```yaml +# sources/files.yml +dlt_sources: + - type: filesystem + config: + bucket_url: "s3://my-bucket/events/" + resources: + - name: raw_events + write_disposition: append +``` + +`filesystem` requires `bucket_url` in `config`. + +### Resource options + +| Key | Description | +|-----|-------------| +| `name` | Source name SQLBuild exposes (referenced via `__source("name")`). Required. | +| `table` | Source table to replicate (`sql_database` only). Required for that type. | +| `endpoint` | Endpoint mapping (`rest_api` only). Required for that type. | +| `write_disposition` | dlt write disposition: `replace`, `append`, or `merge`. `merge` requires `primary_key`. | +| `primary_key` / `merge_key` | dlt keys for `merge`. | +| `incremental` | dlt incremental config mapping (e.g. cursor column and start value). | +| `schema` | Per-resource schema override (falls back to the group `schema`). | + +Use SQLBuild's `write_disposition` (dlt's term), not `write_strategy`. `delete_insert` is not available declaratively; use a Python loader for that. Config values support SQLBuild's [interpolation](/docs/concepts/interpolation): `${name}` for a project variable, `${ENV:NAME}` for an environment variable, and `${CTX:...}` for context, resolved at load time, so credentials stay out of the YAML. + +### Destination + +By default each resource loads into the database SQLBuild's adapter manages, with the dataset/schema derived from your target. An optional `destination` mapping passes extra settings to the dlt destination, but it cannot set `credentials`, `dataset_name`, or `default_schema_name` (SQLBuild manages those): + +```yaml +dlt_sources: + - type: rest_api + destination: + loader_file_format: parquet + config: + client: + base_url: https://api.example.com/ + resources: + - name: raw_widgets + endpoint: + path: widgets +``` + +### Reference in models + +Declared resources are managed sources, reference them like any other source: + +```sql +SELECT id, title, state FROM __source("raw_github_issues") +``` + +## dlt inside a Python loader + +When you need dlt capabilities beyond the declarative surface (custom transforms, `delete_insert`, sources not covered above), wrap a dlt pipeline in a [source loader](/docs/concepts/python-nodes/loaders). The loader calls `dlt.pipeline(...).run(...)` and returns `None`; dlt handles the writes and SQLBuild treats the source as loaded. + +```python +# loaders/github_sources.py +import dlt +from dlt.sources.rest_api import rest_api_source +from sqlbuild.loaders import loader +from sqlbuild.executor.load.models import LoaderContext + +@loader +def raw_github_issues(ctx: LoaderContext): + source = rest_api_source({ + "client": { + "base_url": "https://api.github.com/", + "headers": {"Authorization": f"Bearer {ctx.vars['github_token']}"}, + "paginator": {"type": "header_link"}, + }, + "resources": [{ + "name": "issues", + "endpoint": { + "path": "repos/{owner}/{repo}/issues", + "params": { + "owner": ctx.vars["github_owner"], + "repo": ctx.vars["github_repo"], + "state": "all", + "per_page": 100, + }, + }, + }], + }) + + pipeline = dlt.pipeline( + pipeline_name="github_issues", + destination=dlt.destinations.duckdb(ctx.connection), + dataset_name=ctx.destination_schema or "main", + ) + pipeline.run(source) +``` + +Bind it to a source with `managed: true` in `sources/*.yml`: + +```yaml +# sources/github.yml +sources: + - name: raw_github_issues + managed: true + table: issues + columns: + - name: id + type: INTEGER + - name: title + type: VARCHAR +``` + +### DuckDB connection sharing + +With the DuckDB adapter, pass `ctx.connection` to dlt's DuckDB destination to reuse SQLBuild's open connection, so dlt writes into the same database without a separate connection string: + +```python +pipeline = dlt.pipeline( + pipeline_name="my_pipeline", + destination=dlt.destinations.duckdb(ctx.connection), + dataset_name=ctx.destination_schema or "main", +) +``` + +### Warehouse destinations + +For Snowflake, BigQuery, or Databricks, configure dlt with its own credentials. dlt writes directly to the warehouse, and SQLBuild reads the resulting tables as sources: + +```python +pipeline = dlt.pipeline( + pipeline_name="api_data", + destination="snowflake", + dataset_name=ctx.destination_schema or "public", +) +``` + +Configure dlt credentials via its own `secrets.toml` or environment variables, as in the [dlt documentation](https://dlthub.com/docs/general-usage/credentials/setup). + +## Build integration + +Whether declarative or Python, loaders run automatically during `sqb build` (when `auto_load_sources` is enabled), so dlt pipelines execute as part of the normal build lifecycle: + +```bash +sqb build # loaders run, then models build +sqb build --no-load # skip loading, use existing source data +sqb load # run loaders standalone +``` + +See [Loaders](/docs/concepts/python-nodes/loaders) for write strategies, the loader context API, auto-load behavior, and source deferral. diff --git a/website/src/content/docs/docs/integrations/ingestr.mdx b/website/src/content/docs/docs/integrations/ingestr.mdx new file mode 100644 index 0000000000..d218c3cb3e --- /dev/null +++ b/website/src/content/docs/docs/integrations/ingestr.mdx @@ -0,0 +1,158 @@ +--- +title: "ingestr" +description: "Declarative data ingestion from 50+ sources using ingestr." +--- + +import Frame from '../../../../components/mintlify/Frame.astro'; + +[ingestr](https://github.com/bruin-data/ingestr) is an open-source CLI tool by [Bruin](https://github.com/bruin-data) that copies data from any source to any destination using a single command. SQLBuild integrates with ingestr as a declarative source loader - you configure the ingestion directly in your source YAML, and SQLBuild handles execution as part of the build lifecycle. + +## Install + +```bash +pip install 'sqlbuild[ingestr]' +# or +uv pip install 'sqlbuild[ingestr]' +``` + +This installs `ingestr` alongside SQLBuild. The `ingestr` CLI must be available on `PATH`. + +## How it works + +1. Declare an `ingestr` block on a source in `sources/*.yml` +2. SQLBuild generates a synthetic loader that calls `ingestr ingest` as a subprocess +3. ingestr reads from the configured source and writes directly to the SQLBuild adapter's database +4. The destination URI is built automatically from your SQLBuild connection config - no manual destination setup + +No Python loader code is needed. The YAML declaration is the entire configuration. + + + sqb build loading three external (ingestr) sources (raw_customers, raw_orders, raw_payments); the raw_customers step shows the ingestr ingest command, pipeline source/destination, and an ingestion metrics table before the models build + + +## Example: PostgreSQL to DuckDB + +Replicate a table from PostgreSQL into your local DuckDB project: + +```yaml +# sources/raw.yml +sources: + - name: raw_orders + table: orders + ingestr: + source_uri: "postgresql://user:pass@host:5432/mydb" + source_table: "public.orders" +``` + +That's it. Run `sqb load` or `sqb build` and ingestr copies the `orders` table into your project. + +## Example: Stripe with incremental merge + +Load Stripe charges with incremental merge on a primary key: + +```yaml +sources: + - name: raw_stripe_charges + table: charges + ingestr: + source_uri: "stripe://${stripe_api_key}" + source_table: "charges" + strategy: merge + primary_key: id + incremental_key: created +``` + +On subsequent runs, ingestr merges new and updated records based on the `id` column, using `created` to detect changes. + +## Configuration reference + +The `ingestr` block on a source supports the following fields: + +| Field | Required | Description | +|-------|----------|-------------| +| `source_uri` | Yes | ingestr source connection URI (e.g. `postgresql://...`, `stripe://...`, `shopify://...`) | +| `source_table` | Yes | Source table or resource name to ingest | +| `strategy` | No | Ingestion strategy: `replace`, `append`, `merge`, `delete+insert`, or `truncate+insert` | +| `incremental_key` | No | Column used for incremental change detection | +| `primary_key` | No | Primary key column(s) for merge strategy (string or list) | +| `columns` | No | Comma-separated column list to select from the source | +| `extra_args` | No | Additional CLI arguments passed to `ingestr ingest` (list of strings) | + +### Strategy mapping + +| Strategy | Behavior | +|----------|----------| +| `replace` | Drop and recreate the destination table (default when no strategy is set) | +| `append` | Insert all rows without deduplication | +| `merge` | Upsert based on `primary_key`, using `incremental_key` for change detection | +| `delete+insert` | Delete matching rows by `incremental_key` range, then insert replacements | +| `truncate+insert` | Truncate destination table, then insert all rows | + +## Template variables + +All string fields in the `ingestr` block support SQLBuild project variable substitution and context templates: + +```yaml +sources: + - name: raw_orders + ingestr: + source_uri: "postgresql://${pg_user}:${pg_password}@${pg_host}:5432/${pg_database}" + source_table: "public.orders" +``` + +Variables are resolved from the project's merged variable config (project + target + local). + +Set sensitive values in `sqlbuild_local.toml` (gitignored): + +```toml +[vars] +pg_user = "readonly" +pg_password = "secret" +pg_host = "prod-db.example.com" +pg_database = "analytics" +stripe_api_key = "sk_live_..." +``` + +## Destination URI + +SQLBuild automatically builds the ingestr destination URI from your adapter connection config. All supported adapters work without manual destination configuration: + +| Adapter | Destination URI format | +|---------|----------------------| +| DuckDB | `duckdb:///path/to/db.duckdb` | +| MotherDuck | `motherduck://database` | +| PostgreSQL | `postgresql://user:pass@host:port/db` | +| Snowflake | `snowflake://user:pass@account/db/schema` | +| BigQuery | `bigquery://project` | +| Databricks | `databricks://token:...@host` | +| SQL Server | `mssql://user:pass@host:port/db` | + +## Reload + +When `--reload` is passed, ingestr runs with `--full-refresh`, forcing a complete reload regardless of the configured strategy: + +```bash +sqb load --reload +sqb build --reload +``` + +## Build integration + +ingestr sources are managed sources - they participate in the same lifecycle as Python loaders: + +```bash +# ingestr runs automatically before dependent models +sqb build + +# run ingestr sources standalone +sqb load + +# skip loading +sqb build --no-load +``` + +See [Loaders](/docs/concepts/python-nodes/loaders) for details on auto-load behavior, source deferral, and the `--load` / `--no-load` / `--reload` flags. + +## Supported sources + +ingestr supports 50+ sources including databases, SaaS APIs, and file systems. See the [ingestr documentation](https://bruin-data.github.io/ingestr/) for the full list of supported sources and their URI formats. diff --git a/website/src/content/docs/docs/integrations/rivers.mdx b/website/src/content/docs/docs/integrations/rivers.mdx new file mode 100644 index 0000000000..27943b33e1 --- /dev/null +++ b/website/src/content/docs/docs/integrations/rivers.mdx @@ -0,0 +1,169 @@ +--- +title: "Rivers" +description: "Orchestrate SQLBuild pipelines with Rivers scheduling, jobs, and asset tracking." +--- + +SQLBuild includes a Rivers integration that maps your project's models, sources, seeds, loaders, tasks, assets, and functions into Rivers assets with dependency edges preserved. SQLBuild handles the SQL transformation layer. Rivers handles scheduling, execution, and the asset-centric UI. + +## Install + +```bash +pip install 'sqlbuild[rivers]' +# or +uv pip install 'sqlbuild[rivers]' +``` + +This installs `rivers` alongside SQLBuild. + +## Try it + +```bash +sqb playground --template rivers +cd sqlbuild-playground +uv run rivers dev rivers_pipeline.definitions +``` + +This creates the waffle shop project with a `rivers_pipeline/definitions.py` that includes asset definitions and a configured job. Open the Rivers UI to inspect assets and trigger materializations. + +## How it works + +1. `sqb compile --dag` generates a static `sqlbuild_dag.json` artifact with your project's full graph (nodes, edges) +2. `@sqlbuild_assets()` reads the artifact and creates one Rivers `AssetDef` per source, loader, seed, model, function, task, and asset, with dependency edges preserved +3. The decorated function runs `sqb build` as a subprocess and yields `Materialization` events for each output + +## Quickstart + +```python +# definitions.py +from __future__ import annotations + +import subprocess +from collections.abc import Iterator +from pathlib import Path +from typing import Any + +import rivers as rs + +from sqlbuild.integrations.rivers import SqlBuildProject, sqlbuild_assets + +PROJECT_DIR = Path(__file__).resolve().parent.parent +SQLBUILD_PROJECT = SqlBuildProject(project_dir=PROJECT_DIR) +SQLBUILD_PROJECT.prepare_if_dev() + + +@sqlbuild_assets(project=SQLBUILD_PROJECT) +def my_assets(context: Any) -> Iterator[Any]: + completed = subprocess.run( + ["sqb", "build"], + cwd=PROJECT_DIR, + capture_output=True, + check=False, + text=True, + ) + if completed.returncode != 0: + raise RuntimeError(completed.stderr or completed.stdout) + for output_name in context.output_selection: + yield rs.Materialization(output_name=output_name) + + +repo = rs.CodeRepository( + assets=[my_assets], + jobs=[ + rs.Job( + name="sqlbuild_pipeline", + assets=[my_assets], + executor=rs.Executor.in_process(), + ), + ], +) +``` + +## SqlBuildProject + +`SqlBuildProject` manages paths and DAG artifact generation: + +```python +project = SqlBuildProject(project_dir=Path(".")) +``` + +| Field | Default | Description | +|-------|---------|-------------| +| `project_dir` | required | Path to the SQLBuild project root | +| `target_path` | `target` | Directory for build artifacts | +| `dag_filename` | `sqlbuild_dag.json` | DAG artifact filename | +| `sqb_command` | `("sqb",)` | Command to invoke SQLBuild | +| `prepare_project_cli_args` | `("compile", "--dag")` | CLI arguments for DAG generation | + +### Methods + +| Method | Description | +|--------|-------------| +| `prepare()` | Generate the DAG artifact by running `sqb compile --dag` | +| `prepare_if_dev()` | Generate the DAG artifact only when `RIVERS_DEPLOYMENT=dev` | +| `dag_path` | Property returning the full path to the DAG artifact | + +Use `prepare_if_dev()` so the DAG artifact is auto-generated during local development but not during production deployments where it should already exist. + +## sqlbuild_assets + +The `@sqlbuild_assets()` decorator creates a Rivers multi-asset definition from a SQLBuild DAG artifact: + +```python +@sqlbuild_assets(project=project) +def my_assets(context): + ... +``` + +| Parameter | Description | +|-----------|-------------| +| `project` | `SqlBuildProject` instance (generates and locates the DAG artifact) | +| `dag` | Alternative: pass a DAG path or dict directly instead of a project | +| `translator` | Optional `SqlBuildRiversTranslator` for custom name/tag/metadata mapping | +| `name` | Optional asset name override | + +Pass either `project` or `dag`, not both. + +## SqlBuildRiversTranslator + +Override the default mapping from SQLBuild DAG nodes to Rivers asset metadata: + +```python +from sqlbuild.integrations.rivers import SqlBuildRiversTranslator + +class CustomTranslator(SqlBuildRiversTranslator): + def get_group_name(self, node): + return "my_custom_group" + + def get_tags(self, node): + tags = super().get_tags(node) + tags.append("team:analytics") + return tags + +@sqlbuild_assets(project=project, translator=CustomTranslator()) +def my_assets(context): + ... +``` + +### Translator methods + +| Method | Default behavior | +|--------|-----------------| +| `get_asset_name(node)` | Joins the asset key parts with `__` (e.g. `task__prepare_orders`, `asset__orders_export`) | +| `get_group_name(node)` | Uses the project name | +| `get_tags(node)` | Includes `sqlbuild/kind:` plus model tags | +| `get_kinds(node)` | Returns `["sqlbuild", ""]` | +| `get_metadata(node)` | Includes SQLBuild name, kind, path, target, columns, etc. | + +## Running locally + +Run the pipeline directly without the Rivers UI: + +```bash +uv run python rivers_pipeline/definitions.py +``` + +Or start the Rivers development server: + +```bash +uv run rivers dev rivers_pipeline.definitions +``` diff --git a/website/src/content/docs/docs/quickstart.mdx b/website/src/content/docs/docs/quickstart.mdx new file mode 100644 index 0000000000..2d9ca32718 --- /dev/null +++ b/website/src/content/docs/docs/quickstart.mdx @@ -0,0 +1,267 @@ +--- +title: "Quickstart" +description: "Get a SQLBuild project running locally with DuckDB in under a minute." +--- + +This guide walks you through creating and running a complete transformation project - views, tables, incremental models, audits, tests, and a custom materialization - all running locally against DuckDB. No external data setup or warehouse credentials required. + +## Prerequisites + +- Python 3.12+ +- SQLBuild installed: `uv pip install sqlbuild` or `pip install sqlbuild` + +Confirm the installed release with `sqb --version`. + +## 1. Create the playground + +```bash +sqb playground waffle-shop +cd waffle-shop +``` + +This creates a self-contained waffle shop project with everything you need to explore SQLBuild. + +The playground creates the shared `sqlbuild_project.toml`; that is sufficient for this +quickstart. In a team project, you can additionally create a gitignored +`sqlbuild_local.toml` to select your usual target and override your personal development +schema or credentials without changing the committed project config. See +[Project Configuration](/docs/concepts/project-configuration#sqlbuild_localtoml). + +## 2. Plan + +Preview what SQLBuild will do: + +```bash +sqb plan +``` + +``` +Plan ready (13 selected) + +First run (12) + stg_customers view + stg_payments view + stg_orders view + daily_order_partitioned partition_tracked (custom) + daily_revenue table + dim_customers table + fact_orders table + customer_status_snapshot merge (timestamp) + hourly_order_activity delete_insert (timestamp, microbatch) + daily_activity_rollup delete_insert (timestamp, microbatch) + hourly_activity_with_daily_context delete_insert (timestamp, microbatch) + order_status_index delete_insert (integer) + +Seeds (1) + waffle_types +``` + +Source data is defined as inline expressions in `sources/raw.yml`, so SQLBuild resolves everything at compile time - no seeding step needed. + +## 3. Build + +Execute the full build: + +```bash +sqb --no-color build +``` + +SQLBuild materializes all models in DAG order. SQL unit tests run before their target models. Error-severity audits run against a staging table before promotion - if any fail, the production table is untouched: + +``` +Execution sqb build (concurrency: 1) + + 1/13 seed waffle_types OK 0.09s + 2/13 view stg_customers OK 0.05s + audit not_null (customer_id) PASS + audit unique (customer_id) PASS + audit not_null (email) PASS + 3/13 view stg_payments OK 0.02s + audit not_null (payment_id) PASS + audit unique (payment_id) PASS + audit not_null (order_id) PASS + 4/13 view stg_orders OK 0.03s + test test_stg_orders PASS + audit not_null (order_id) PASS + audit unique (order_id) PASS + audit not_null (customer_id) PASS + audit accepted_values (status) PASS + 5/13 custom daily_order_partitioned OK 0.17s + audit expression_is_true PASS 4/4 + audit not_null (order_date) PASS 4/4 + 6/13 table daily_revenue OK 0.05s + audit expression_is_true PASS + 7/13 table dim_customers OK 0.05s + audit not_null (customer_id) PASS + audit unique (customer_id) PASS + 8/13 table fact_orders OK 0.04s + audit not_null (order_id) PASS + 9/13 table customer_status_snapshot (merge) OK 0.04s + audit not_null (customer_id) PASS + audit unique (customer_id) PASS + 10/13 table hourly_order_activity (delete_insert) OK 0.16s + audit (d) expression_is_true PASS 4/4 + audit (d) not_null (activity_hour) PASS 4/4 + audit (f) expression_is_true PASS + audit (f) not_null (activity_hour) PASS + 11/13 table order_status_index (delete_insert) OK 0.03s + audit not_null (order_id) PASS + audit unique (order_id) PASS + 12/13 table daily_activity_rollup (delete_insert) OK 0.08s + audit (d) expression_is_true PASS 2/2 + audit (d) not_null (activity_day) PASS 2/2 + audit (f) expression_is_true PASS + audit (f) not_null (activity_day) PASS + 13/13 table hourly_activity_with_daily_context (delete_insert) OK 0.17s + audit (d) expression_is_true PASS 4/4 + audit (d) not_null (activity_hour) PASS 4/4 + audit (f) expression_is_true PASS + audit (f) not_null (activity_hour) PASS + +Completed successfully. +PASS=66 WARN=0 FAIL=0 SKIP=0 TOTAL=66 (1.09s) +``` + +Notice `audit (d)` and `audit (f)` on incremental models - these are delta-phase and final-phase audits. Delta audits validate each batch before DML is applied; final audits run against the target after promotion. + +## 4. Verify + +Run the plan again to see the steady-state: + +```bash +sqb plan +``` + +``` +Plan ready (13 selected) + +Normal (12) + 3 view + 3 table + 3 delete_insert (timestamp, microbatch) + 1 partition_tracked (custom) + 1 merge (timestamp) + 1 delete_insert (integer) + +Seeds (1) + waffle_types +``` + +All models now show `Normal` instead of `First run`. + +## 5. Common operations + +```bash +# Rebuild a specific model +sqb build --select daily_revenue + +# Rebuild models under a path +sqb build --select path:models/marts + +# Full refresh of everything +sqb build --full-refresh + +# Run tests only +sqb test + +# Run audits only +sqb audit + +# Inspect lineage +sqb lineage fact_orders --direction both + +# Compile and check contracts +sqb compile +``` + +## What you just built + +### Model DAG + +``` +raw__customers ──> stg_customers ──> dim_customers +raw__orders ────> stg_orders ────> fact_orders ──> customer_status_snapshot +raw__payments ──> stg_payments ──> │ order_status_index + │ hourly_order_activity ──> daily_activity_rollup + │ hourly_activity_with_daily_context + └──> daily_revenue + └──> daily_order_partitioned +``` + +### Materialization types + +The project demonstrates several materialization strategies: + +- **Views** (`stg_customers`, `stg_orders`, `stg_payments`) - lightweight staging layer +- **Tables** (`fact_orders`, `dim_customers`, `daily_revenue`) - full table rebuilds +- **Merge incremental** (`customer_status_snapshot`) - upsert with timestamp cursor +- **Delete/insert incremental** (`hourly_order_activity`, `daily_activity_rollup`, `hourly_activity_with_daily_context`) - microbatch processing with configurable batch sizes +- **Integer cursor incremental** (`order_status_index`) - non-timestamp incremental +- **Custom materialization** (`daily_order_partitioned`) - Python-defined partition-tracked strategy + +See [Incremental](/docs/concepts/incremental) for details on cursor-based strategies and microbatch processing. + +### Project structure + +``` +waffle-shop/ + sqlbuild_project.toml # project config + sources/ + raw.yml # source declarations (loader-managed and expression-backed) + loaders/ + waffle_sources.py # Python loaders for raw__customers and raw__orders + seeds/ + waffle_types.csv # seed data + lookups.yml # seed declarations + functions/ + sql/ + udf__is_completed_order.sql # scalar SQL UDF + table_fn__customer_orders.sql # table function + models/ + staging/ + stg_customers.sql # view (audits declared in MODEL() header) + stg_orders.sql # view + stg_payments.sql # view + marts/ + fact_orders.sql # table + dim_customers.sql # table + daily_revenue.sql # table + daily_order_partitioned.sql # custom materialization + hourly_order_activity.sql # microbatch incremental + hourly_activity_with_daily_context.sql # microbatch incremental + daily_activity_rollup.sql # microbatch incremental + _sqlbuild/ + _macros/ # visible only to models directly in marts/ + currency.py # cents_to_dollars macro + datetime.py # adapter-portable timestamp_trunc + intermediate/ + customer_status_snapshot.sql # merge incremental + order_status_index.sql # integer cursor incremental + audits/ + generic/ + expression_is_true.sql # custom generic audit + tests/ + unit/ + test_stg_orders.sql # model unit test + test_fact_orders.sql # multi-model test + test_daily_revenue_chain.sql # multi-model test + test_line_total_cents_macro.sql # macro test + scenarios/ + daily_revenue_minimal.sql # E2E scenario test + daily_revenue_multi_order.sql # E2E scenario test + macros/ + currency.py # project-wide line_total_cents macro + materializations/ + partition_tracked.py # custom materialization +``` + +## Next steps + +- [Models](/docs/concepts/models) - understand `MODEL()` headers and materialization types +- [Rules](/docs/concepts/rules) - add compile-time SQL, architecture, and model-shape checks +- [Functions](/docs/concepts/functions) - SQL UDFs, Python UDFs, and table functions +- [Incremental](/docs/concepts/incremental) - learn about cursor-based incremental strategies +- [Audits](/docs/concepts/audits) - configure data quality checks +- [Testing](/docs/concepts/testing) - write SQL unit tests and multi-model tests with macro support +- [Column Lineage](/docs/concepts/column-lineage) - trace individual columns through your pipeline +- [CLI Reference](/docs/cli/build) - full command reference diff --git a/website/src/navigation.mjs b/website/src/navigation.mjs new file mode 100644 index 0000000000..b0e003cb0e --- /dev/null +++ b/website/src/navigation.mjs @@ -0,0 +1,214 @@ +// Sidebar and redirects, first generated from the Mintlify docs.json. +export const sidebar = [ + { + "label": "Getting Started", + "items": [ + "docs", + "docs/quickstart", + "docs/feature-comparison", + "docs/benchmarks" + ] + }, + { + "label": "dbt Compatibility", + "items": [ + "docs/concepts/dbt-compatibility/overview", + "docs/concepts/dbt-compatibility/selection", + "docs/concepts/dbt-compatibility/adding-sqlbuild-models" + ] + }, + { + "label": "Concepts", + "items": [ + "docs/concepts/project-configuration", + "docs/concepts/resource-identities", + { + "label": "Adapters", + "collapsed": true, + "items": [ + "docs/concepts/adapters", + "docs/concepts/adapters/duckdb", + "docs/concepts/adapters/motherduck", + "docs/concepts/adapters/snowflake", + "docs/concepts/adapters/bigquery", + "docs/concepts/adapters/databricks", + "docs/concepts/adapters/postgres", + "docs/concepts/adapters/sqlserver" + ] + }, + "docs/concepts/sources", + "docs/concepts/seeds", + { + "label": "Models", + "collapsed": true, + "items": [ + "docs/concepts/models", + "docs/concepts/models/materializations", + "docs/concepts/models/schemas", + "docs/concepts/models/type-enforcement", + "docs/concepts/models/contracts", + "docs/concepts/models/migrations", + { + "label": "Hooks", + "collapsed": true, + "items": [ + { + "label": "Overview", + "slug": "docs/concepts/models/hooks" + }, + "docs/concepts/models/hooks/sql", + "docs/concepts/models/hooks/python" + ] + }, + "docs/concepts/models/configuration" + ] + }, + { + "label": "Enums and Constants", + "collapsed": true, + "items": [ + "docs/concepts/enums", + "docs/concepts/constants", + "docs/concepts/constants/collections-and-rendering", + "docs/concepts/enums/model-contracts", + "docs/concepts/model-private-values" + ] + }, + { + "label": "Python Macros", + "collapsed": true, + "items": [ + "docs/concepts/macros", + "docs/concepts/macros/composition-and-context" + ] + }, + "docs/concepts/interpolation", + "docs/concepts/functions", + "docs/concepts/incremental", + { + "label": "Planning and Change Detection", + "collapsed": true, + "items": [ + "docs/concepts/planning", + "docs/concepts/planning/cascade-propagation", + "docs/concepts/planning/source-freshness", + "docs/concepts/planning/selection-and-staleness" + ] + }, + "docs/concepts/snapshots", + "docs/concepts/audits", + { + "label": "Rules", + "collapsed": true, + "items": [ + "docs/concepts/rules", + "docs/concepts/rules/configuration-and-selection", + "docs/concepts/rules/findings-and-exceptions", + "docs/concepts/rules/execution-and-caching" + ] + }, + "docs/concepts/testing", + "docs/concepts/scenarios", + "docs/concepts/selectors", + "docs/concepts/column-lineage", + "docs/concepts/diff" + ] + }, + { + "label": "Advanced Concepts", + "items": [ + { + "label": "Observability", + "collapsed": true, + "items": [ + "docs/concepts/observability", + "docs/concepts/observability/sinks" + ] + }, + { + "label": "Declarations and Scopes", + "collapsed": true, + "items": [ + "docs/concepts/declaration-scopes", + "docs/concepts/declaration-scopes/visibility", + "docs/concepts/declaration-scopes/placement", + "docs/concepts/declaration-scopes/explorer" + ] + }, + { + "label": "Custom Rules", + "collapsed": true, + "items": [ + "docs/concepts/rules/custom-rules/overview", + "docs/concepts/rules/custom-rules/rule-context", + "docs/concepts/rules/custom-rules/testing-and-determinism" + ] + } + ] + }, + { + "label": "Python Nodes", + "items": [ + "docs/concepts/python-nodes/overview", + "docs/concepts/python-nodes/loaders", + "docs/concepts/python-nodes/tasks", + "docs/concepts/python-nodes/assets", + "docs/concepts/python-nodes/checks", + "docs/concepts/python-nodes/factories", + "docs/concepts/python-nodes/providers", + "docs/concepts/python-nodes/sql-references" + ] + }, + { + "label": "Integrations", + "items": [ + { + "label": "Dagster", + "collapsed": true, + "items": [ + "docs/integrations/dagster", + "docs/integrations/dagster-reference" + ] + }, + "docs/integrations/rivers", + "docs/integrations/dlt", + "docs/integrations/ingestr" + ] + }, + { + "label": "CLI Reference", + "items": [ + "docs/cli/init", + "docs/cli/playground", + "docs/cli/skills", + "docs/cli/compile", + "docs/cli/format", + "docs/cli/contract", + "docs/cli/scope", + "docs/cli/rules", + "docs/cli/plan", + "docs/cli/build", + "docs/cli/load", + "docs/cli/seed", + "docs/cli/test", + "docs/cli/scenario", + "docs/cli/audit", + "docs/cli/freshness", + "docs/cli/check", + "docs/cli/clone", + "docs/cli/diff", + "docs/cli/lineage", + "docs/cli/dag", + "docs/cli/query", + "docs/cli/debug", + "docs/cli/janitor", + "docs/cli/clean", + "docs/cli/dbt" + ] + } +]; + +export const redirects = { + "/docs/concepts/rules/overview": "/docs/concepts/rules", + "/docs/concepts/rules/custom-rules": "/docs/concepts/rules/custom-rules/overview" +}; diff --git a/website/src/styles/fonts.css b/website/src/styles/fonts.css new file mode 100644 index 0000000000..43693eef7f --- /dev/null +++ b/website/src/styles/fonts.css @@ -0,0 +1,42 @@ +@font-face { + font-family: 'Inter'; + font-style: normal; + font-weight: 100 900; + font-display: swap; + src: url('/fonts/inter-latin-wght-normal.woff2') format('woff2'); +} + +@font-face { + font-family: 'IBM Plex Mono'; + font-style: normal; + font-weight: 400; + font-display: swap; + src: url('/fonts/ibm-plex-mono-latin-400-normal.woff2') format('woff2'); +} + +@font-face { + font-family: 'IBM Plex Mono'; + font-style: normal; + font-weight: 500; + font-display: swap; + src: url('/fonts/ibm-plex-mono-latin-500-normal.woff2') format('woff2'); +} + +/* IBM Plex Mono has no box-drawing glyphs. DejaVu Sans Mono fills them in, and draws whole + diagrams so every cell has the same advance width. */ +@font-face { + font-family: 'SQLBuild Box Drawing'; + font-style: normal; + font-weight: 400; + font-display: swap; + src: url('/fonts/dejavu-sans-mono-diagram.woff2') format('woff2'); + unicode-range: U+2500-259F, U+2260; +} + +@font-face { + font-family: 'SQLBuild Diagram'; + font-style: normal; + font-weight: 400; + font-display: swap; + src: url('/fonts/dejavu-sans-mono-diagram.woff2') format('woff2'); +} diff --git a/website/src/styles/sqlbuild.css b/website/src/styles/sqlbuild.css new file mode 100644 index 0000000000..41c0cc080d --- /dev/null +++ b/website/src/styles/sqlbuild.css @@ -0,0 +1,446 @@ +/* Starlight theme mapped onto the SQLBuild UI tokens, matching sqlbuild.com. */ +:root { + --sl-font: 'Inter', system-ui, sans-serif; + --sl-font-mono: 'IBM Plex Mono', 'SQLBuild Box Drawing', ui-monospace, monospace; + --sl-nav-height: 60px; + --sl-content-width: 50rem; + --sl-sidebar-width: 18rem; + --sqb-toc-width: 16rem; + --sqb-frame-width: 1440px; + --sl-text-h1: 2rem; + --sl-text-h2: 1.45rem; + --sl-text-h3: 1.15rem; + --sl-text-h4: 1rem; + --sl-line-height-headings: 1.25; + + /* Dark (Starlight's default) */ + --sqb-bg: #0d0f12; + --sqb-panel: #171a1f; + --sqb-fg: #e9ebef; + --sqb-muted: #9ba1ac; + --sqb-faint: #626975; + --sqb-line: rgba(176, 184, 198, 0.1); + --sqb-line-strong: rgba(176, 184, 198, 0.18); + --sqb-primary: #3788e8; + --sqb-accent-soft: rgba(55, 136, 232, 0.12); + --sqb-code-bg: #111418; + --sqb-warning: #f0c267; + + --sl-color-accent-low: #0f2440; + --sl-color-accent: #3788e8; + --sl-color-accent-high: #a9cdf6; + --sl-color-white: #e9ebef; + --sl-color-gray-1: #d4d7dd; + --sl-color-gray-2: #b4b9c2; + --sl-color-gray-3: #9ba1ac; + --sl-color-gray-4: #626975; + --sl-color-gray-5: #2a2f37; + --sl-color-gray-6: #171a1f; + --sl-color-black: #0d0f12; +} + +:root[data-theme='light'] { + --sqb-bg: #ffffff; + --sqb-panel: #ffffff; + --sqb-fg: #171a1f; + --sqb-muted: #626a76; + --sqb-faint: #98a0ad; + --sqb-line: rgba(28, 32, 38, 0.08); + --sqb-line-strong: rgba(28, 32, 38, 0.16); + --sqb-primary: #2677d9; + --sqb-accent-soft: rgba(38, 119, 217, 0.08); + --sqb-code-bg: #fbfcfe; + --sqb-warning: #aa6c16; + + --sl-color-accent-low: #e3eefb; + --sl-color-accent: #2677d9; + --sl-color-accent-high: #174a8a; + --sl-color-white: #171a1f; + --sl-color-gray-1: #2b3038; + --sl-color-gray-2: #434a55; + --sl-color-gray-3: #626a76; + --sl-color-gray-4: #98a0ad; + --sl-color-gray-5: #dfe3e8; + --sl-color-gray-6: #f4f6f8; + --sl-color-gray-7: #fafbfc; + --sl-color-black: #ffffff; +} + +:root { + --sl-color-bg: var(--sqb-bg); + --sl-color-bg-nav: var(--sqb-bg); + --sl-color-bg-sidebar: var(--sqb-bg); + --sl-color-bg-inline-code: var(--sqb-accent-soft); + --sl-color-hairline: var(--sqb-line); + --sl-color-hairline-light: var(--sqb-line); + --sl-color-hairline-shade: var(--sqb-line); + --sl-color-text: var(--sqb-fg); + --sl-color-text-accent: var(--sqb-primary); +} + +body { + -webkit-font-smoothing: antialiased; +} + +/* Header */ +site-search button[data-open-modal] { + border-radius: 2px; + border-color: var(--sqb-line-strong); + background: transparent; + color: var(--sqb-muted); +} + +site-search button[data-open-modal]:hover { + border-color: var(--sqb-primary); + color: var(--sqb-fg); +} + +/* Sidebar: bold top-level headings, nested groups styled like links (as on Mintlify). */ +.sidebar-content ul ul li { + margin-inline-start: 0; + padding-inline-start: 0; + border-inline-start: 0; +} + +.sidebar-content .top-level > li + li { + margin-top: 1.1rem; +} + +.sidebar-content .top-level > li > details > summary { + padding: 0.2rem 0.5rem 0.35rem; + pointer-events: none; +} + +.sidebar-content .top-level > li > details > summary .caret { + display: none; +} + +.sidebar-content .top-level > li > details > summary .large { + color: var(--sqb-fg); + font-size: 14px; + font-weight: 600; +} + +.sidebar-content ul ul summary { + padding: 0.3rem 0.5rem; + border-radius: 2px; +} + +.sidebar-content ul ul summary:hover { + background: var(--sqb-accent-soft); +} + +.sidebar-content ul ul summary .large { + color: var(--sqb-muted); + font-size: 14px; + font-weight: 400; + line-height: 1.4; +} + +.sidebar-content ul ul summary:hover .large { + color: var(--sqb-fg); +} + +.sidebar-content ul ul summary .caret { + width: 1rem; + height: 1rem; + color: var(--sqb-faint); +} + +.sidebar-content ul ul ul { + margin-inline-start: 0.75rem; +} + +.sidebar-content a { + padding: 0.3rem 0.5rem; + border-radius: 2px; + font-size: 14px; + line-height: 1.4; + color: var(--sqb-muted); +} + +.sidebar-content a:hover { + background: var(--sqb-accent-soft); + color: var(--sqb-fg); +} + +.sidebar-content a[aria-current='page'], +.sidebar-content a[aria-current='page']:hover { + background: var(--sqb-accent-soft); + color: var(--sqb-primary); + font-weight: 500; +} + +/* Page title and on-this-page */ +.content-panel { + border-top-color: var(--sqb-line); +} + +h1#_top { + font-weight: 650; + letter-spacing: -0.02em; +} + +.sl-markdown-content :is(h2, h3, h4) { + font-weight: 620; + letter-spacing: -0.01em; + color: var(--sqb-fg); +} + +.sl-markdown-content .sl-heading-wrapper.level-h2 { + margin-top: 2.75rem; +} + +/* Title and body share one panel, with no dividing line. */ +.content-panel + .content-panel { + border-top: 0; + padding-top: 0; +} + +.content-panel:first-of-type { + padding-bottom: 2.25rem; +} + +.sl-markdown-content p, +.sl-markdown-content li { + color: var(--sqb-muted); +} + +.sl-markdown-content strong { + color: var(--sqb-fg); + font-weight: 600; +} + +starlight-toc h2 { + margin-bottom: 0.5rem; + color: var(--sqb-fg); + font-size: 13px; + font-weight: 600; +} + +starlight-toc li:has(> a[href='#_top']) { + display: none; +} + +starlight-toc a, +mobile-starlight-toc a { + padding-block: 0.3rem; + font-size: 13.5px; + line-height: 1.4; + color: var(--sqb-muted); +} + +starlight-toc a:hover { + color: var(--sqb-fg); +} + +starlight-toc a[aria-current='true'], +starlight-toc a[aria-current='true']:hover { + color: var(--sqb-primary); + font-weight: 500; +} + +/* Links */ +.sl-markdown-content a:not(.sqb-card) { + color: var(--sqb-fg); + font-weight: 500; + text-decoration: underline; + text-decoration-color: var(--sqb-primary); + text-decoration-thickness: 1px; + text-underline-offset: 3px; +} + +.sl-markdown-content a:not(.sqb-card):hover { + color: var(--sqb-primary); + text-decoration-thickness: 2px; +} + +/* Code blocks: a quiet copy icon that is always visible, as on Mintlify. */ +.expressive-code .copy button { + opacity: 0.6 !important; +} + +.expressive-code .copy button:hover { + opacity: 1 !important; +} + +/* Inline code */ +.sl-markdown-content :not(pre) > code { + border: 0; + border-radius: 2px; + padding: 0.1em 0.35em; + font-size: 0.86em; + color: var(--sqb-fg); +} + +/* Tables */ +.sl-markdown-content table { + display: table; + width: 100%; + border-collapse: collapse; + font-size: 14px; + line-height: 1.5; +} + +@media (max-width: 50rem) { + .sl-markdown-content table { + display: block; + overflow-x: auto; + } +} + +.sl-markdown-content :is(th, td) { + border: 0; + border-bottom: 1px solid var(--sqb-line); + padding: 0.55rem 0.9rem 0.55rem 0; + vertical-align: top; +} + +.sl-markdown-content th { + background: transparent; + color: var(--sqb-fg); + font-weight: 600; + white-space: nowrap; + border-bottom-color: var(--sqb-line-strong); +} + +.sl-markdown-content tr:nth-child(2n) { + background: transparent; +} + +.sl-markdown-content td { + color: var(--sqb-muted); +} + +.sl-markdown-content td:first-child code { + white-space: nowrap; +} + +/* Search */ +site-search { + --pagefind-ui-border-radius: 2px; +} + +site-search dialog { + border-radius: 2px; +} + +/* Callouts */ +.sqb-callout { + display: flex; + gap: 12px; + padding: 14px 16px; + border: 1px solid color-mix(in srgb, var(--sqb-callout) 30%, transparent); + border-radius: 2px; + background: color-mix(in srgb, var(--sqb-callout) 8%, transparent); + font-size: 14px; + line-height: 1.6; +} + +.sqb-callout.note { + --sqb-callout: var(--sqb-primary); +} + +.sqb-callout.warning { + --sqb-callout: var(--sqb-warning); +} + +.sl-markdown-content .sqb-callout-icon { + width: 16px; + height: 16px; +} + +.sl-markdown-content .sqb-callout > .sqb-callout-body { + margin: 0; +} + +.sqb-callout-icon { + flex: none; + margin-top: 4px; + color: var(--sqb-callout); +} + +.sl-markdown-content .sqb-callout-body > * { + margin: 0; + color: var(--sqb-fg); +} + +.sl-markdown-content .sqb-callout-body > * + * { + margin-top: 0.6rem; +} + +/* Mintlify-compatible components */ +.sqb-card-group { + display: grid; + grid-template-columns: repeat(var(--cols, 2), minmax(0, 1fr)); + gap: 12px; + margin-top: 1.25rem; +} + +@media (max-width: 50rem) { + .sqb-card-group { + grid-template-columns: 1fr; + } +} + +.sl-markdown-content .sqb-card { + display: flex; + flex-direction: column; + margin: 0; + padding: 18px 20px; + border: 1px solid var(--sqb-line-strong); + border-radius: 2px; + background: var(--sqb-panel); + color: var(--sqb-muted); + text-decoration: none; + font-size: 14px; + line-height: 1.5; +} + +.sl-markdown-content a.sqb-card:hover { + border-color: var(--sqb-primary); +} + +.sqb-card-icon { + margin-bottom: 12px; + color: var(--sqb-primary); +} + +.sqb-card-title { + color: var(--sqb-fg); + font-weight: 600; + font-size: 15px; +} + +.sqb-card-body { + margin-top: 4px; +} + +.sqb-card-body > * { + margin: 0; +} + +.sqb-frame { + margin: 1rem 0; + padding: 0; + border: 1px solid var(--sqb-line); +} + +.sqb-frame img { + display: block; + width: 100%; +} + +/* Mobile controls */ +.sl-menu-button { + border: 1px solid var(--sqb-line-strong) !important; + border-radius: 2px !important; + background: var(--sqb-bg) !important; + color: var(--sqb-muted) !important; + box-shadow: none !important; +} + +mobile-starlight-toc summary .toggle { + border-radius: 2px; + border-color: var(--sqb-line-strong); +}