diff --git a/.changeset/create-chkit-example-prompt.md b/.changeset/create-chkit-example-prompt.md new file mode 100644 index 0000000..44d003e --- /dev/null +++ b/.changeset/create-chkit-example-prompt.md @@ -0,0 +1,5 @@ +--- +"create-chkit": patch +--- + +Prompt for the example to scaffold from a bundled manifest instead of silently defaulting to `clickbench`. The list of examples ships with the package and is kept in sync with `examples/` at build time. diff --git a/.changeset/detect-streamed-clickhouse-exceptions.md b/.changeset/detect-streamed-clickhouse-exceptions.md new file mode 100644 index 0000000..ac0de6f --- /dev/null +++ b/.changeset/detect-streamed-clickhouse-exceptions.md @@ -0,0 +1,8 @@ +--- +"@chkit/clickhouse": patch +"chkit": patch +--- + +Detect ClickHouse exceptions that arrive in the `x-clickhouse-exception-code` response header on an HTTP 200 response. When `send_progress_in_http_headers=1` is set (chkit's default for long-running migrations), ClickHouse commits to a 200 status before the query completes; if the query then errors, the exception is reported via response headers rather than as an HTTP error code. `@clickhouse/client` does not surface this as a thrown error, so previously `chkit migrate` could record a failed INSERT migration as applied while the data never landed. + +`@chkit/clickhouse` now inspects `result.response_headers` after every `command`/`query`/`queryJson`/`insert` call and throws a new `ClickHouseStreamedException` (with `code`, `exceptionTag`, and `query_id`) when a non-zero exception code is present. Migrations that fail this way now exit with a non-zero status and remain pending so the operator can fix the underlying issue and re-apply. diff --git a/.changeset/migration-mode-async.md b/.changeset/migration-mode-async.md new file mode 100644 index 0000000..7a3ab12 --- /dev/null +++ b/.changeset/migration-mode-async.md @@ -0,0 +1,26 @@ +--- +"chkit": patch +--- + +Add `mode=async` annotation for long-running migration operations. + +Mark an operation as async by adding `mode=async` to its `-- operation:` header line, for example: + +```sql +-- operation: load_table_data key=table:default.hits risk=caution mode=async +INSERT INTO default.hits SELECT * FROM s3(...); +``` + +When `chkit migrate --apply` encounters an async operation it: + +1. Computes a deterministic `query_id` from `sha256(migration_filename + ':' + statement_index)`. +2. Checks `system.processes` / `system.query_log` for any prior attempt with that id. +3. Fires the INSERT via the existing `submit()` path without blocking on its HTTP response, and polls `queryStatus(query_id)` every 5 seconds — printing a one-line update (`written=N.NM rows (N.N GiB), elapsed Ns`) so the operator sees the load advance. +4. On `QueryFinish` → records the journal entry and proceeds. On `ExceptionWhileProcessing` → throws with the server's exception. On any prior run's failure → resubmits (retry semantics). + +This unblocks two scenarios chkit could not previously handle: + +- **Long INSERTs through a proxy/LB with an HTTP request-duration ceiling**: the operator sees progress, and a connection drop mid-poll no longer cancels the work — the deterministic id lets a re-run attach to the in-flight query on the server. +- **Transient client-side errors during a multi-minute load**: re-running chkit picks up where it left off rather than starting over. + +Existing migrations without `mode=async` continue to use the synchronous path; the annotation is opt-in and forward-compatible (an unknown mode value falls back to sync). diff --git a/CLAUDE.md b/CLAUDE.md index d694764..d2adff3 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -35,6 +35,12 @@ This is a monorepo managed with Bun workspaces and Turborepo. - Internal packages (`@chkit/clickhouse`, `@chkit/codegen`) are not meant to be installed directly by users. - Plugins extend the CLI and are registered in `clickhouse.config.ts` via the `plugins` array. +## Examples + +The `examples/` directory holds curated starter projects scaffolded by `create-chkit` (`bun create chkit@latest`). The list shown in the interactive prompt is sourced from `examples/manifest.json`, which is bundled into the `create-chkit` package at build time. + +**When you add a new example under `examples//`, you MUST also add an entry to `examples/manifest.json`.** The `verify` job runs `scripts/check-examples-manifest.ts` (via `create-chkit`'s build step), which fails CI if any directory under `examples/` is missing from the manifest, any manifest entry has no matching directory, or the `default` field doesn't match a listed entry. + ## CLI Commands `init`, `generate`, `migrate`, `status`, `drift`, `check`, `codegen`, `pull`, `plugin` diff --git a/apps/docs/astro.config.mjs b/apps/docs/astro.config.mjs index 08d69ad..5b65935 100644 --- a/apps/docs/astro.config.mjs +++ b/apps/docs/astro.config.mjs @@ -1,8 +1,11 @@ // @ts-check import { defineConfig } from 'astro/config'; import starlight from '@astrojs/starlight'; +import react from '@astrojs/react'; import rawMarkdown from './src/integrations/raw-markdown'; +const isDev = process.argv.includes('dev'); + // https://astro.build/config export default defineConfig({ integrations: [ @@ -10,6 +13,11 @@ export default defineConfig({ title: 'chkit Docs', description: 'Public documentation for chkit, the ClickHouse schema and migration CLI.', customCss: ['./src/styles/custom.css'], + ...(isDev && { + components: { + Footer: './src/components/Footer.astro', + }, + }), sidebar: [ { label: 'Overview', @@ -41,12 +49,17 @@ export default defineConfig({ label: 'Schema', autogenerate: { directory: 'schema' }, }, + { + label: 'ObsessionDB', + autogenerate: { directory: 'obsessiondb' }, + }, { label: 'Plugins', autogenerate: { directory: 'plugins' }, }, ], }), + ...(isDev ? [react()] : []), rawMarkdown(), ], }); diff --git a/apps/docs/package.json b/apps/docs/package.json index 8e52954..b169dfa 100644 --- a/apps/docs/package.json +++ b/apps/docs/package.json @@ -17,6 +17,12 @@ "sharp": "^0.34.2" }, "devDependencies": { + "@astrojs/react": "^5.0.5", + "@types/react": "^19.2.15", + "@types/react-dom": "^19.2.3", + "agentation": "^3.0.2", + "react": "^19.2.6", + "react-dom": "^19.2.6", "wrangler": "^4.65.0" } } diff --git a/apps/docs/src/components/AgentationDev.tsx b/apps/docs/src/components/AgentationDev.tsx new file mode 100644 index 0000000..07d42ad --- /dev/null +++ b/apps/docs/src/components/AgentationDev.tsx @@ -0,0 +1,5 @@ +import { Agentation } from 'agentation'; + +export default function AgentationDev() { + return ; +} diff --git a/apps/docs/src/components/Command.astro b/apps/docs/src/components/Command.astro new file mode 100644 index 0000000..fead08d --- /dev/null +++ b/apps/docs/src/components/Command.astro @@ -0,0 +1,51 @@ +--- +interface Props { + command: string; +} + +const { command } = Astro.props; +--- + +
+
+
$ {command}
+
+ +
+ + diff --git a/apps/docs/src/components/Footer.astro b/apps/docs/src/components/Footer.astro new file mode 100644 index 0000000..e235a9b --- /dev/null +++ b/apps/docs/src/components/Footer.astro @@ -0,0 +1,8 @@ +--- +import Default from '@astrojs/starlight/components/Footer.astro'; +import AgentationDev from './AgentationDev'; +--- + + + +{import.meta.env.DEV && } diff --git a/apps/docs/src/components/PackagedCommand.astro b/apps/docs/src/components/PackagedCommand.astro new file mode 100644 index 0000000..d2bffb9 --- /dev/null +++ b/apps/docs/src/components/PackagedCommand.astro @@ -0,0 +1,187 @@ +--- +type PackageManager = 'bun' | 'npm' | 'pnpm' | 'yarn'; + +interface Props { + install?: string; + exec?: string; + create?: string; + args?: string; + dev?: boolean; +} + +const { install, exec, create, args, dev } = Astro.props; + +const pms: PackageManager[] = ['bun', 'npm', 'pnpm', 'yarn']; + +function buildInstall(pm: PackageManager, packages: string, isDev: boolean): string { + const devFlag = isDev ? (pm === 'bun' ? '-d ' : '-D ') : ''; + const verb = pm === 'npm' ? 'install' : 'add'; + return `${pm} ${verb} ${devFlag}${packages}`; +} + +function buildExec(pm: PackageManager, command: string): string { + switch (pm) { + case 'bun': + return `bunx ${command}`; + case 'npm': + return `npx ${command}`; + case 'pnpm': + return `pnpm dlx ${command}`; + case 'yarn': + return `yarn dlx ${command}`; + } +} + +function buildCreate(pm: PackageManager, pkg: string, rawArgs?: string): string { + const yarnPkg = pkg.replace(/@latest$/, ''); + const target = pm === 'yarn' ? yarnPkg : pkg; + + if (!rawArgs) { + return `${pm} create ${target}`; + } + + if (pm !== 'npm') { + return `${pm} create ${target} ${rawArgs}`; + } + + const parts = rawArgs.split(/\s+/); + const firstFlagIdx = parts.findIndex((p) => p.startsWith('-')); + if (firstFlagIdx === -1) { + return `npm create ${target} ${rawArgs}`; + } + const positional = parts.slice(0, firstFlagIdx).join(' '); + const flags = parts.slice(firstFlagIdx).join(' '); + return positional + ? `npm create ${target} ${positional} -- ${flags}` + : `npm create ${target} -- ${flags}`; +} + +function commandFor(pm: PackageManager): string { + if (install !== undefined) return buildInstall(pm, install, dev ?? false); + if (exec !== undefined) return buildExec(pm, exec); + if (create !== undefined) return buildCreate(pm, create, args); + throw new Error('PackagedCommand requires one of: install, exec, create'); +} + +const entries = pms.map((pm) => ({ pm, command: commandFor(pm) })); +--- + +
+
+
+ { + entries.map(({ pm }) => ( + + )) + } +
+ +
+
+ { + entries.map(({ pm, command }) => ( + + )) + } +
+
+ + diff --git a/apps/docs/src/content/docs/cli/query.md b/apps/docs/src/content/docs/cli/query.md index ff8b914..dcfcc3b 100644 --- a/apps/docs/src/content/docs/cli/query.md +++ b/apps/docs/src/content/docs/cli/query.md @@ -19,9 +19,9 @@ The SQL must be a single positional argument. Wrap it in quotes if it contains s No command-specific flags. See [global flags](/cli/overview/#global-flags). -When the [ObsessionDB plugin](/plugins/obsessiondb/) is loaded, `chkit query` +When the [ObsessionDB plugin](/obsessiondb/overview/) is loaded, `chkit query` also accepts `--service ` to route the query to a specific service for -this invocation (see [Per-command service override](/plugins/obsessiondb/#per-command-service-override)). +this invocation (see [Per-command override](/obsessiondb/services/#per-command-override)). ## Behavior diff --git a/apps/docs/src/content/docs/getting-started/add-to-existing-project.mdx b/apps/docs/src/content/docs/getting-started/add-to-existing-project.mdx index c116577..593286a 100644 --- a/apps/docs/src/content/docs/getting-started/add-to-existing-project.mdx +++ b/apps/docs/src/content/docs/getting-started/add-to-existing-project.mdx @@ -5,7 +5,8 @@ sidebar: order: 2 --- -import { Tabs, TabItem } from '@astrojs/starlight/components'; +import PackagedCommand from '../../../components/PackagedCommand.astro'; +import Command from '../../../components/Command.astro'; Install chkit alongside your existing application code, drop a minimal config and starter schema into the current directory, and produce your first migration. Use this path when you already have a TypeScript project that talks to ClickHouse. @@ -18,55 +19,13 @@ Install chkit alongside your existing application code, drop a minimal config an Add `chkit` and `@chkit/core` as dev dependencies: - - - ```sh - bun add -d chkit @chkit/core - ``` - - - ```sh - npm install -D chkit @chkit/core - ``` - - - ```sh - pnpm add -D chkit @chkit/core - ``` - - - ```sh - yarn add -D chkit @chkit/core - ``` - - + ## 2. Initialize the config `chkit init` writes `clickhouse.config.ts` (config with sensible defaults) and `src/db/schema/example.ts` (a starter `MergeTree` table). It's idempotent — running it on an existing project leaves both files untouched. - - - ```sh - bunx chkit init - ``` - - - ```sh - npx chkit init - ``` - - - ```sh - pnpm dlx chkit init - ``` - - - ```sh - yarn dlx chkit init - ``` - - + Edit `src/db/schema/example.ts` to match the table you actually want before moving on. @@ -74,86 +33,20 @@ Edit `src/db/schema/example.ts` to match the table you actually want before movi `chkit generate` diffs your schema definitions against the previous snapshot and writes migration SQL into `chkit/migrations/`. Pass `--name` to label the migration file. - - - ```sh - bunx chkit generate --name init - ``` - - - ```sh - npx chkit generate --name init - ``` - - - ```sh - pnpm dlx chkit generate --name init - ``` - - - ```sh - yarn dlx chkit generate --name init - ``` - - + ## 4. Apply the migration `chkit migrate --apply` runs unapplied migrations against the ClickHouse endpoint from your environment. Make sure `CLICKHOUSE_URL` (and any auth env vars) are set before running this. - - - ```sh - bunx chkit migrate --apply - ``` - - - ```sh - npx chkit migrate --apply - ``` - - - ```sh - pnpm dlx chkit migrate --apply - ``` - - - ```sh - yarn dlx chkit migrate --apply - ``` - - + ## 5. Verify `chkit status` shows which migrations have been applied. `chkit check` confirms the live schema matches your TypeScript definitions. - - - ```sh - bunx chkit status - bunx chkit check - ``` - - - ```sh - npx chkit status - npx chkit check - ``` - - - ```sh - pnpm dlx chkit status - pnpm dlx chkit check - ``` - - - ```sh - yarn dlx chkit status - yarn dlx chkit check - ``` - - + + ## AI agent skill @@ -161,9 +54,7 @@ chkit ships an agent skill so AI coding assistants (Claude Code, Cursor, GitHub On the first interactive run, chkit detects your agent and prompts to install the skill. To install it manually at any time: -```sh -npx skills add obsessiondb/chkit -``` + The skill is installed into your project's agent directory (e.g. `.claude/skills/chkit/`, `.cursor/skills/chkit/`). diff --git a/apps/docs/src/content/docs/getting-started/with-an-example.mdx b/apps/docs/src/content/docs/getting-started/with-an-example.mdx index ff773f7..50248cc 100644 --- a/apps/docs/src/content/docs/getting-started/with-an-example.mdx +++ b/apps/docs/src/content/docs/getting-started/with-an-example.mdx @@ -5,7 +5,8 @@ sidebar: order: 1 --- -import { Tabs, TabItem } from '@astrojs/starlight/components'; +import PackagedCommand from '../../../components/PackagedCommand.astro'; +import Command from '../../../components/Command.astro'; `create-chkit` scaffolds a working chkit project by downloading a curated example from the chkit repository and wiring it up against your chosen package manager. Use this path when you want a known-good project as a starting point. @@ -16,62 +17,20 @@ import { Tabs, TabItem } from '@astrojs/starlight/components'; ## Scaffold a project -Run the package without arguments to be prompted for a project name and to scaffold the default `clickbench` example. - - - - ```sh - bun create chkit@latest - ``` - - - ```sh - npm create chkit@latest - ``` - - - ```sh - pnpm create chkit@latest - ``` - - - ```sh - yarn create chkit - ``` - - +Run the package without arguments to be prompted for a project name and to pick from the list of bundled examples. + + Pass a project directory and pick an example explicitly: - - - ```sh - bun create chkit@latest my-app --example clickbench - ``` - - - ```sh - npm create chkit@latest my-app -- --example clickbench - ``` - - - ```sh - pnpm create chkit@latest my-app --example clickbench - ``` - - - ```sh - yarn create chkit my-app --example clickbench - ``` - - + ## Options | Flag | Description | | --- | --- | | `[project-directory]` | Target directory. Prompted if omitted. | -| `-e, --example ` | Example name or full GitHub URL. Defaults to `clickbench`. | +| `-e, --example ` | Example name or full GitHub URL. Prompted with the list of bundled examples if omitted. | | `-m, --package-manager ` | `npm`, `pnpm`, `yarn`, or `bun`. Auto-detected from the invoking package manager. | | `--skip-install` | Skip installing dependencies after scaffolding. | @@ -88,7 +47,7 @@ More examples will land in the [`examples/` directory](https://github.com/obsess Once the scaffold completes: ```sh -cd my-app +cd my-chkit-app export CLICKHOUSE_URL=http://localhost:8123 # export CLICKHOUSE_PASSWORD=... bun run migrate diff --git a/apps/docs/src/content/docs/obsessiondb/engine-rewriting.md b/apps/docs/src/content/docs/obsessiondb/engine-rewriting.md new file mode 100644 index 0000000..0bdc7c3 --- /dev/null +++ b/apps/docs/src/content/docs/obsessiondb/engine-rewriting.md @@ -0,0 +1,50 @@ +--- +title: Engine Rewriting +description: How chkit strips Shared engine prefixes when the target isn't ObsessionDB so one set of schema files works against both. +sidebar: + order: 3 +--- + +ObsessionDB uses `Shared` engine variants — `SharedMergeTree`, `SharedReplacingMergeTree`, `SharedAggregatingMergeTree` — to deliver managed replication without operator intervention. These engines do not exist in regular ClickHouse. Writing your schema with `Shared*` engines and then running it against a local Docker ClickHouse or self-hosted staging would fail at apply time. + +The plugin intercepts schema definitions before diff and planning, strips the `Shared` prefix when the target is not ObsessionDB, and leaves it intact when the target is ObsessionDB. One set of schema files works against both. + +## Auto-detection + +By default the plugin inspects `clickhouse.url`. If the host ends with `.obsessiondb.com`, `Shared` engines are preserved. Otherwise, the `Shared` prefix is stripped. + +This means a config pointing at `https://my-service.obsessiondb.com` keeps the managed engines, while `http://localhost:8123` automatically downgrades to the standard equivalents — no flags needed. + +## CLI flag overrides + +Two flags override auto-detection for a single command: + +- `--force-shared-engines` — keep `Shared` engine prefixes even against regular ClickHouse. +- `--no-shared-engines` — strip the prefix even against ObsessionDB. + +```sh +# Force stripping even when targeting ObsessionDB +chkit generate --no-shared-engines + +# Force keeping Shared engines even on regular ClickHouse +chkit migrate --force-shared-engines +``` + +## Rewrite table + +| Schema engine | Regular ClickHouse | ObsessionDB | +|---|---|---| +| `SharedMergeTree` | `MergeTree` | `SharedMergeTree` | +| `SharedReplacingMergeTree(ts)` | `ReplacingMergeTree(ts)` | `SharedReplacingMergeTree(ts)` | +| `SharedAggregatingMergeTree` | `AggregatingMergeTree` | `SharedAggregatingMergeTree` | +| `MergeTree` | `MergeTree` | `MergeTree` | + +Only table engine definitions are rewritten. Views and materialized views pass through unchanged. + +The plugin also strips ObsessionDB-only table settings (such as `storage_policy`) when the target is regular ClickHouse, so settings that only make sense in the managed environment don't leak into local migrations. + +## Related + +- [Services](/obsessiondb/services/) — the runtime counterpart: route a single command at a specific service with `--service`. +- [`chkit drift`](/cli/drift/) — normalizes `SharedMergeTree` to `MergeTree` when comparing engines, so managed-side engine substitutions don't show up as drift. +- [Schema Overview](/schema/overview/) — where engines are declared in the DSL. diff --git a/apps/docs/src/content/docs/obsessiondb/getting-started.md b/apps/docs/src/content/docs/obsessiondb/getting-started.md new file mode 100644 index 0000000..353f91f --- /dev/null +++ b/apps/docs/src/content/docs/obsessiondb/getting-started.md @@ -0,0 +1,80 @@ +--- +title: Getting Started with ObsessionDB +description: Sign up, install the plugin, authenticate, and route chkit commands at your first ObsessionDB service. +sidebar: + order: 2 +--- + +Five steps from nothing to a working ObsessionDB target: sign up, install the plugin, authenticate, select a service, and run your first query. + +## 1. Sign up for a free instance + +Create an account and spin up a service at [console.obsessiondb.com](https://console.obsessiondb.com). The free tier is enough to follow this guide end-to-end. Once your service is provisioned, note its name — you'll select it from a list in step 4, so you don't need to copy any URLs or tokens by hand. + +## 2. Install the plugin + +```sh +bun add -d @chkit/plugin-obsessiondb +``` + +Register it in your `clickhouse.config.ts`: + +```ts +import { defineConfig } from '@chkit/core' +import { obsessiondb } from '@chkit/plugin-obsessiondb' + +export default defineConfig({ + schema: './src/db/schema/**/*.ts', + outDir: './chkit', + plugins: [obsessiondb()], +}) +``` + +You don't need a `clickhouse` block at this point — once a service is selected, the plugin routes SQL through the ObsessionDB API. + +## 3. Authenticate + +```sh +chkit obsessiondb login +``` + +This opens an interactive browser flow against the ObsessionDB API. Credentials are stored locally so subsequent commands don't re-prompt. + +Verify the login: + +```sh +chkit obsessiondb whoami +``` + +If you're on a non-default ObsessionDB region, pass `--api-url` to `login` to point at the correct endpoint. See [Services](/obsessiondb/services/) for credential storage details and how to switch regions. + +## 4. Select a service + +List services across the organizations you belong to: + +```sh +chkit obsessiondb service list +``` + +Then pick one interactively: + +```sh +chkit obsessiondb service select +``` + +The selection is written to `.chkit/obsessiondb.json` next to your config file and becomes the default target for every `chkit` command after that. + +## 5. Run your first command + +Confirm the routing works: + +```sh +chkit query "SELECT 1" +``` + +The query goes through the ObsessionDB API to your selected service. If it returns a row, you're done — your schema commands (`generate`, `migrate`, `status`, `drift`, `check`) will use the same target from here on. + +## Next + +- [Engine Rewriting](/obsessiondb/engine-rewriting/) — what the plugin does to `Shared*` engines when you also target regular ClickHouse. +- [Services](/obsessiondb/services/) — managing multiple services, per-command overrides, and aliases. diff --git a/apps/docs/src/content/docs/obsessiondb/overview.md b/apps/docs/src/content/docs/obsessiondb/overview.md new file mode 100644 index 0000000..f35fbb0 --- /dev/null +++ b/apps/docs/src/content/docs/obsessiondb/overview.md @@ -0,0 +1,46 @@ +--- +title: ObsessionDB Overview +description: First-class integration with ObsessionDB — engine rewriting, service selection, and remote execution from one schema. +sidebar: + order: 1 +--- + +chkit ships a dedicated integration with [ObsessionDB](https://obsessiondb.com), the managed ClickHouse-compatible database that provides `Shared` engine variants and a hosted API for queries and backfills. + +## What it gives you + +- **One schema, two targets** — write `Shared*` engines once and run them against ObsessionDB as-is, or against regular ClickHouse with the `Shared` prefix stripped automatically. +- **Service selection** — list services across your organizations, pick a default per project, and override per command without touching config. +- **Remote query execution** — once a service is selected, `chkit query` and other SQL-emitting commands route through the ObsessionDB API instead of a local ClickHouse connection. +- **Remote backfills** — `chkit backfill` can submit jobs to ObsessionDB rather than streaming chunks from your machine. + +## Install + +```sh +bun add -d @chkit/plugin-obsessiondb +``` + +Register it in your `clickhouse.config.ts`: + +```ts +import { defineConfig } from '@chkit/core' +import { obsessiondb } from '@chkit/plugin-obsessiondb' + +export default defineConfig({ + schema: './src/db/schema/**/*.ts', + outDir: './chkit', + plugins: [obsessiondb()], + clickhouse: { + url: process.env.CLICKHOUSE_URL ?? 'http://localhost:8123', + }, +}) +``` + +The plugin hooks into `generate`, `migrate`, `status`, `drift`, `check`, and `query`. + +## Next + +- [Getting Started](/obsessiondb/getting-started/) — sign up, authenticate, and select your first service. +- [Engine Rewriting](/obsessiondb/engine-rewriting/) — how `Shared*` engines are stripped for non-ObsessionDB targets. +- [Services](/obsessiondb/services/) — list, select, alias, and override services per command. +- [Backfill Plugin](/plugins/backfill/) — for backfills against ObsessionDB once a service is selected. diff --git a/apps/docs/src/content/docs/obsessiondb/services.md b/apps/docs/src/content/docs/obsessiondb/services.md new file mode 100644 index 0000000..005806a --- /dev/null +++ b/apps/docs/src/content/docs/obsessiondb/services.md @@ -0,0 +1,81 @@ +--- +title: Services +description: List, select, alias, and override ObsessionDB services per command — and how queries route through the ObsessionDB API. +sidebar: + order: 4 +--- + +Once you're authenticated, the plugin manages which ObsessionDB service each command talks to. You set a default per project, override it per command, and define short aliases for the services you switch between most. + +## List and select + +List all services across the organizations your account belongs to: + +```sh +chkit obsessiondb service list +``` + +Output is grouped by organization, with the currently selected service marked. + +Pick one as the project default: + +```sh +chkit obsessiondb service select +``` + +The selection is persisted to `.chkit/obsessiondb.json` next to your config file. Every `chkit` command after that uses it unless you override (see below). + +Credentials and selection live in two locations: + +- **Project** — `.chkit/obsessiondb.json` (next to `clickhouse.config.ts`). Selected service and project-scoped aliases. +- **Profile** — `~/.config/chkit/obsessiondb.json`. Credentials and profile-scoped aliases shared across projects. + +## Per-command override + +Any command that hits ClickHouse accepts `--service ` to target a different service for one invocation without changing the saved selection: + +```sh +# Ad-hoc query against a different service +chkit query "SELECT count() FROM users" --service customer-b + +# Apply migrations against a one-off service +chkit migrate --apply --service staging +``` + +`--service` is available on `generate`, `migrate`, `status`, `drift`, `check`, and `query`. + +The lookup tries service names first (as shown in `chkit obsessiondb service list`), then saved aliases. If nothing matches, the command fails fast with the list of available services and aliases. + +## Aliases + +Aliases give your services short, memorable names. Set, list, and remove them with `service alias`: + +```sh +# Define a short alias for a service +chkit obsessiondb service alias set prod customer-production + +# Use the alias anywhere --service is accepted +chkit query "SELECT count() FROM users" --service prod + +# List or remove aliases +chkit obsessiondb service alias list +chkit obsessiondb service alias remove prod +``` + +Project aliases live in `.chkit/obsessiondb.json`; profile-scoped aliases live in `~/.config/chkit/obsessiondb.json`. The project file wins when both define the same alias. + +Aliases can't shadow real service names — if `prod` is already the name of a service, setting it as an alias is rejected. + +## Remote query routing + +When a service is selected, chkit doesn't open a direct ClickHouse connection. SQL from `chkit query` and any plugin command that runs queries is submitted through the ObsessionDB API to the selected service, and results are normalized back into chkit's executor interface. + +This is why no `clickhouse` block is required in `clickhouse.config.ts` once you're authenticated — the plugin supplies the executor. + +See [`chkit query`](/cli/query/) for the command reference, including the `--service` flag and JSON output. + +## Related + +- [`chkit query`](/cli/query/) — ad-hoc SQL execution against the selected service. +- [Engine Rewriting](/obsessiondb/engine-rewriting/) — how `Shared*` engines are handled depending on the active target. +- [Backfill Plugin](/plugins/backfill/) — backfills can submit jobs to ObsessionDB once a service is selected. diff --git a/apps/docs/src/content/docs/plugins/obsessiondb.md b/apps/docs/src/content/docs/plugins/obsessiondb.md deleted file mode 100644 index 55e9148..0000000 --- a/apps/docs/src/content/docs/plugins/obsessiondb.md +++ /dev/null @@ -1,123 +0,0 @@ ---- -title: ObsessionDB Plugin -description: Run a single set of schema files across both ObsessionDB and regular ClickHouse by auto-rewriting Shared engine variants. ---- - -The `@chkit/plugin-obsessiondb` plugin lets you keep one set of schema files -that target both [ObsessionDB](https://obsessiondb.com) and standard ClickHouse -instances (e.g. local Docker, self-hosted staging). - -## Why - -ObsessionDB uses `Shared` engine variants -(`SharedMergeTree`, `SharedReplacingMergeTree`, `SharedAggregatingMergeTree`) -to deliver managed replication without operator intervention. These engines -do not exist in regular ClickHouse. If you define schemas with `Shared` -engines but apply them against a standard ClickHouse instance, migrations -will fail. - -This plugin intercepts schema definitions before diff/planning and strips the -`Shared` prefix when the target is not ObsessionDB. - -## Install - -```bash -bun add -d @chkit/plugin-obsessiondb -``` - -## Usage - -Register the plugin in your `clickhouse.config.ts`: - -```ts -import { defineConfig } from '@chkit/core' -import { obsessiondb } from '@chkit/plugin-obsessiondb' - -export default defineConfig({ - schema: './src/db/schema/**/*.ts', - outDir: './chkit', - plugins: [obsessiondb()], - clickhouse: { - url: process.env.CLICKHOUSE_URL ?? 'http://localhost:8123', - }, -}) -``` - -The plugin hooks into `generate`, `migrate`, `status`, `drift`, `check`, and `query`. - -## Service selection - -Authenticate and pick the ObsessionDB service this project should route through: - -```bash -chkit obsessiondb login -chkit obsessiondb service list -chkit obsessiondb service select -``` - -The selection is stored in `.chkit/obsessiondb.json` next to your config file and used as the default target for every command after that. - -### Per-command service override - -Once authenticated, any command that hits ClickHouse accepts `--service ` to target a different service for one invocation without changing the saved selection. The flag takes the service **name** as shown in `chkit obsessiondb service list`, or a saved alias. - -```bash -# Run an ad-hoc query against a different service -chkit query "SELECT count() FROM users" --service customer-b - -# Apply migrations against a one-off service -chkit migrate --apply --service staging -``` - -`--service` is available on `generate`, `migrate`, `status`, `drift`, `check`, and `query`. The lookup tries service names first, then saved aliases, and fails fast with the list of available services and aliases if nothing matches. - -### Service aliases - -Aliases are stored with the ObsessionDB service selection: project aliases live in `.chkit/obsessiondb.json` next to your config, while profile-mode aliases live in `~/.config/chkit/obsessiondb.json`. - -```bash -# Define a short alias for a service name -chkit obsessiondb service alias set prod customer-production - -# Use the alias in a query -chkit query "SELECT count() FROM users" --service prod - -# List or remove aliases -chkit obsessiondb service alias list -chkit obsessiondb service alias remove prod -``` - -## How it works - -1. **Auto-detection (default)** — the plugin inspects `clickhouse.url`. If the - host matches `.obsessiondb.com`, `Shared` engines are preserved. Otherwise, - the `Shared` prefix is stripped. -2. **CLI flag overrides** — override auto-detection per command: - - `--force-shared-engines` keeps `Shared` engine prefixes even against - regular ClickHouse. - - `--no-shared-engines` strips the prefix even against ObsessionDB. - -## Engine rewriting - -| Schema engine | Regular ClickHouse | ObsessionDB | -|---|---|---| -| `SharedMergeTree` | `MergeTree` | `SharedMergeTree` | -| `SharedReplacingMergeTree(ts)` | `ReplacingMergeTree(ts)` | `SharedReplacingMergeTree(ts)` | -| `SharedAggregatingMergeTree` | `AggregatingMergeTree` | `SharedAggregatingMergeTree` | -| `MergeTree` | `MergeTree` | `MergeTree` | - -Only table engine definitions are rewritten. Views and materialized views pass -through unchanged. - -## Examples - -```bash -# Auto-detect based on clickhouse.url -bunx chkit generate - -# Force stripping even when targeting ObsessionDB -bunx chkit generate --no-shared-engines - -# Force keeping Shared engines even on regular ClickHouse -bunx chkit migrate --force-shared-engines -``` diff --git a/apps/docs/src/content/docs/plugins/overview.md b/apps/docs/src/content/docs/plugins/overview.md index 05b59ac..cec518b 100644 --- a/apps/docs/src/content/docs/plugins/overview.md +++ b/apps/docs/src/content/docs/plugins/overview.md @@ -29,7 +29,8 @@ You can author your own plugins; the existing official plugins are the reference ## Official plugins +If you deploy to [ObsessionDB](https://obsessiondb.com), start at the dedicated [ObsessionDB section](/obsessiondb/overview/) — `@chkit/plugin-obsessiondb` is documented there as a first-class integration rather than as a plain plugin. + - [`@chkit/plugin-codegen`](/plugins/codegen/) — TypeScript row types and optional Zod schemas, generated from your schema files. - [`@chkit/plugin-pull`](/plugins/pull/) — introspect a live ClickHouse database into local schema files. Useful for adopting chkit on an existing database. - [`@chkit/plugin-backfill`](/plugins/backfill/) — time-windowed data backfill with checkpoints, for materialized views and historical data loads. -- [`@chkit/plugin-obsessiondb`](/plugins/obsessiondb/) — run a single set of schema files against both [ObsessionDB](https://obsessiondb.com) (with `Shared` engines) and regular ClickHouse. Recommended if you deploy to ObsessionDB. diff --git a/apps/docs/src/content/docs/schema/dsl-reference.md b/apps/docs/src/content/docs/schema/dsl-reference.md index f92f3e0..65dfdc4 100644 --- a/apps/docs/src/content/docs/schema/dsl-reference.md +++ b/apps/docs/src/content/docs/schema/dsl-reference.md @@ -1,6 +1,8 @@ --- title: Schema DSL Reference description: Complete reference for chkit schema definition functions, column types, and table options. +sidebar: + order: 3 --- Schema files are TypeScript files that export definitions using functions from `@chkit/core`. All exported definitions are collected when chkit loads schema files matched by the `schema` glob in your [configuration](/configuration/overview/). diff --git a/apps/docs/src/content/docs/schema/overview.md b/apps/docs/src/content/docs/schema/overview.md index 55be13e..de7656d 100644 --- a/apps/docs/src/content/docs/schema/overview.md +++ b/apps/docs/src/content/docs/schema/overview.md @@ -1,6 +1,8 @@ --- title: Schema Overview description: How chkit thinks about ClickHouse schema and where to learn each piece of the DSL. +sidebar: + order: 1 --- In chkit, your ClickHouse schema lives in TypeScript files. You declare tables, views, and materialized views as plain values using functions from `@chkit/core`, group them with `schema()`, and let chkit handle the rest — diffing them against the database, generating migration SQL, and applying it safely. @@ -46,3 +48,4 @@ chkit discovers schema files using the `schema` glob in your [configuration](/co - [Configuration Overview](/configuration/overview/) — where the `schema` glob is set. - [CLI: `chkit generate`](/cli/generate/) — how schema changes become migration SQL. - [CLI: `chkit pull`](/cli/pull/) — bootstrap schema files from an existing ClickHouse database. +- [ObsessionDB: Engine Rewriting](/obsessiondb/engine-rewriting/) — how `Shared*` engines are stripped when the target isn't ObsessionDB. diff --git a/apps/docs/src/content/docs/schema/refreshable-views.md b/apps/docs/src/content/docs/schema/refreshable-views.md index 3165f50..e613452 100644 --- a/apps/docs/src/content/docs/schema/refreshable-views.md +++ b/apps/docs/src/content/docs/schema/refreshable-views.md @@ -1,6 +1,8 @@ --- title: Refreshable Materialized Views description: Schedule ClickHouse materialized views to refresh on a cron-like cadence, with APPEND mode, DEPENDS ON chains, and the three hard rules chkit enforces. +sidebar: + order: 2 --- ClickHouse [refreshable materialized views](https://clickhouse.com/docs/materialized-view/refreshable-materialized-view) (RMVs) periodically re-execute a SELECT on a schedule, rather than firing per-INSERT like regular incremental MVs. They've been production-ready since ClickHouse 24.10. diff --git a/apps/docs/src/styles/custom.css b/apps/docs/src/styles/custom.css index e0ed54d..0466b8c 100644 --- a/apps/docs/src/styles/custom.css +++ b/apps/docs/src/styles/custom.css @@ -211,6 +211,132 @@ pre { border-radius: 10px; } +/* ── PackagedCommand & Command components ───────── */ + +.pkg-cmd { + background: #141414; + border: 1px solid rgba(255, 255, 255, 0.06); + border-radius: 10px; + overflow: hidden; + margin: 1rem 0; + position: relative; + font-family: var(--sl-font-mono); + font-size: 0.875rem; +} + +.pkg-cmd-header { + display: flex; + align-items: stretch; + justify-content: space-between; + border-bottom: 1px solid rgba(255, 255, 255, 0.06); + background: rgba(255, 255, 255, 0.015); +} + +.pkg-cmd-tabs { + display: flex; + align-items: stretch; +} + +.pkg-cmd-tab { + background: transparent; + border: 0; + color: var(--sl-color-gray-3); + cursor: pointer; + padding: 0.55rem 0.95rem; + font-family: var(--sl-font-mono); + font-size: 0.8125rem; + line-height: 1; + letter-spacing: 0; + position: relative; + transition: color 120ms ease; +} + +.pkg-cmd-tab:hover { + color: var(--sl-color-gray-1); +} + +.pkg-cmd-tab[aria-selected='true'] { + color: var(--sl-color-white); +} + +.pkg-cmd-tab[aria-selected='true']::after { + content: ''; + position: absolute; + left: 0.95rem; + right: 0.95rem; + bottom: -1px; + height: 1px; + background: var(--sl-color-white); +} + +.pkg-cmd-copy { + background: transparent; + border: 0; + color: var(--sl-color-gray-3); + cursor: pointer; + padding: 0 0.85rem; + display: inline-flex; + align-items: center; + justify-content: center; + transition: color 120ms ease; +} + +.pkg-cmd-copy:hover { + color: var(--sl-color-white); +} + +.pkg-cmd-copy.is-copied { + color: var(--sl-color-white); +} + +.pkg-cmd-body { + padding: 0; +} + +.pkg-cmd-pre { + background: transparent !important; + border: 0 !important; + border-radius: 0 !important; + margin: 0; + padding: 0.85rem 1rem; + overflow-x: auto; +} + +.pkg-cmd-pre code { + background: transparent; + color: var(--sl-color-white); + font-family: var(--sl-font-mono); + font-size: 0.875rem; + line-height: 1.6; + white-space: pre; +} + +.pkg-cmd-prompt { + color: var(--sl-color-gray-4); + user-select: none; + margin-right: 0.25rem; +} + +/* Plain (no-tabs) variant — floating copy button in top-right */ +.pkg-cmd--plain .pkg-cmd-copy--floating { + position: absolute; + top: 0.35rem; + right: 0.35rem; + padding: 0.35rem; + border-radius: 6px; + opacity: 0; + transition: opacity 120ms ease, color 120ms ease, background-color 120ms ease; +} + +.pkg-cmd--plain:hover .pkg-cmd-copy--floating, +.pkg-cmd--plain:focus-within .pkg-cmd-copy--floating { + opacity: 1; +} + +.pkg-cmd--plain .pkg-cmd-copy--floating:hover { + background: rgba(255, 255, 255, 0.06); +} + /* ── Link styling ─────────────────────────────────── */ a:not([class]) { diff --git a/bun.lock b/bun.lock index 9eeb8a4..54d4484 100644 --- a/bun.lock +++ b/bun.lock @@ -19,19 +19,33 @@ }, "apps/docs": { "name": "@chkit/docs", - "version": "0.0.2-beta.13", + "version": "0.0.2-beta.15", "dependencies": { "@astrojs/starlight": "^0.37.6", "astro": "^5.6.1", "sharp": "^0.34.2", }, "devDependencies": { + "@astrojs/react": "^5.0.5", + "@types/react": "^19.2.15", + "@types/react-dom": "^19.2.3", + "agentation": "^3.0.2", + "react": "^19.2.6", + "react-dom": "^19.2.6", "wrangler": "^4.65.0", }, }, + "examples/clickbench": { + "name": "chkit-example-clickbench", + "devDependencies": { + "@chkit/core": "workspace:*", + "@chkit/plugin-obsessiondb": "workspace:*", + "chkit": "workspace:*", + }, + }, "packages/cli": { "name": "chkit", - "version": "0.1.0-beta.24", + "version": "0.1.0-beta.26", "bin": { "chkit": "./dist/bin/chkit.js", }, @@ -48,7 +62,7 @@ }, "packages/clickhouse": { "name": "@chkit/clickhouse", - "version": "0.1.0-beta.24", + "version": "0.1.0-beta.26", "dependencies": { "@chkit/core": "workspace:*", "@clickhouse/client": "^1.11.0", @@ -58,14 +72,14 @@ }, "packages/codegen": { "name": "@chkit/codegen", - "version": "0.1.0-beta.24", + "version": "0.1.0-beta.26", "dependencies": { - "@chkit/core": "workspace:*", + "@chkit/core": "0.1.0-beta.26", }, }, "packages/core": { "name": "@chkit/core", - "version": "0.1.0-beta.24", + "version": "0.1.0-beta.26", "dependencies": { "fast-glob": "^3.3.2", }, @@ -75,7 +89,7 @@ }, "packages/create-chkit": { "name": "create-chkit", - "version": "0.1.0-beta.24", + "version": "0.1.0-beta.25", "bin": { "create-chkit": "./dist/bin/create-chkit.js", }, @@ -91,7 +105,7 @@ }, "packages/plugin-backfill": { "name": "@chkit/plugin-backfill", - "version": "0.1.0-beta.24", + "version": "0.1.0-beta.26", "dependencies": { "@chkit/clickhouse": "workspace:*", "@chkit/core": "workspace:*", @@ -102,7 +116,7 @@ }, "packages/plugin-codegen": { "name": "@chkit/plugin-codegen", - "version": "0.1.0-beta.24", + "version": "0.1.0-beta.26", "dependencies": { "@chkit/core": "workspace:*", "zod": "^4.3.6", @@ -110,7 +124,7 @@ }, "packages/plugin-obsessiondb": { "name": "@chkit/plugin-obsessiondb", - "version": "0.1.0-beta.24", + "version": "0.1.0-beta.26", "dependencies": { "@chkit/clickhouse": "workspace:*", "@chkit/core": "workspace:*", @@ -121,7 +135,7 @@ }, "packages/plugin-pull": { "name": "@chkit/plugin-pull", - "version": "0.1.0-beta.24", + "version": "0.1.0-beta.26", "dependencies": { "@chkit/clickhouse": "workspace:*", "@chkit/core": "workspace:*", @@ -132,7 +146,7 @@ "packages": { "@astrojs/compiler": ["@astrojs/compiler@2.13.1", "", {}, "sha512-f3FN83d2G/v32ipNClRKgYv30onQlMZX1vCeZMjPsMMPl1mDpmbl0+N5BYo4S/ofzqJyS5hvwacEo0CCVDn/Qg=="], - "@astrojs/internal-helpers": ["@astrojs/internal-helpers@0.7.5", "", {}, "sha512-vreGnYSSKhAjFJCWAwe/CNhONvoc5lokxtRoZims+0wa3KbHBdPHSSthJsKxPd8d/aic6lWKpRTYGY/hsgK6EA=="], + "@astrojs/internal-helpers": ["@astrojs/internal-helpers@0.9.1", "", { "dependencies": { "picomatch": "^4.0.4" } }, "sha512-1pWuARqYom/TzuU3+0ZugsTrKlUydWKuULmDqSMTuonY+9IRDUEGKX/8PXQ1nBxRq3w85uGtd9q9SXfqEldMIQ=="], "@astrojs/markdown-remark": ["@astrojs/markdown-remark@6.3.10", "", { "dependencies": { "@astrojs/internal-helpers": "0.7.5", "@astrojs/prism": "3.3.0", "github-slugger": "^2.0.0", "hast-util-from-html": "^2.0.3", "hast-util-to-text": "^4.0.2", "import-meta-resolve": "^4.2.0", "js-yaml": "^4.1.1", "mdast-util-definitions": "^6.0.0", "rehype-raw": "^7.0.0", "rehype-stringify": "^10.0.1", "remark-gfm": "^4.0.1", "remark-parse": "^11.0.0", "remark-rehype": "^11.1.2", "remark-smartypants": "^3.0.2", "shiki": "^3.19.0", "smol-toml": "^1.5.2", "unified": "^11.0.5", "unist-util-remove-position": "^5.0.0", "unist-util-visit": "^5.0.0", "unist-util-visit-parents": "^6.0.2", "vfile": "^6.0.3" } }, "sha512-kk4HeYR6AcnzC4QV8iSlOfh+N8TZ3MEStxPyenyCtemqn8IpEATBFMTJcfrNW32dgpt6MY3oCkMM/Tv3/I4G3A=="], @@ -140,20 +154,52 @@ "@astrojs/prism": ["@astrojs/prism@3.3.0", "", { "dependencies": { "prismjs": "^1.30.0" } }, "sha512-q8VwfU/fDZNoDOf+r7jUnMC2//H2l0TuQ6FkGJL8vD8nw/q5KiL3DS1KKBI3QhI9UQhpJ5dc7AtqfbXWuOgLCQ=="], + "@astrojs/react": ["@astrojs/react@5.0.5", "", { "dependencies": { "@astrojs/internal-helpers": "0.9.1", "@vitejs/plugin-react": "^5.2.0", "devalue": "^5.6.4", "ultrahtml": "^1.6.0", "vite": "^7.3.2" }, "peerDependencies": { "@types/react": "^17.0.50 || ^18.0.21 || ^19.0.0", "@types/react-dom": "^17.0.17 || ^18.0.6 || ^19.0.0", "react": "^17.0.2 || ^18.0.0 || ^19.0.0", "react-dom": "^17.0.2 || ^18.0.0 || ^19.0.0" } }, "sha512-5jSFDqWqLdEyp7CEVD66A7AQEEuwLkCGR25NJ4FR5EjziZQqZTGc7hJOFZ97qb98BiU6vElrS70R8iI+HhufGQ=="], + "@astrojs/sitemap": ["@astrojs/sitemap@3.7.0", "", { "dependencies": { "sitemap": "^8.0.2", "stream-replace-string": "^2.0.0", "zod": "^3.25.76" } }, "sha512-+qxjUrz6Jcgh+D5VE1gKUJTA3pSthuPHe6Ao5JCxok794Lewx8hBFaWHtOnN0ntb2lfOf7gvOi9TefUswQ/ZVA=="], "@astrojs/starlight": ["@astrojs/starlight@0.37.6", "", { "dependencies": { "@astrojs/markdown-remark": "^6.3.1", "@astrojs/mdx": "^4.2.3", "@astrojs/sitemap": "^3.3.0", "@pagefind/default-ui": "^1.3.0", "@types/hast": "^3.0.4", "@types/js-yaml": "^4.0.9", "@types/mdast": "^4.0.4", "astro-expressive-code": "^0.41.1", "bcp-47": "^2.1.0", "hast-util-from-html": "^2.0.1", "hast-util-select": "^6.0.2", "hast-util-to-string": "^3.0.0", "hastscript": "^9.0.0", "i18next": "^23.11.5", "js-yaml": "^4.1.0", "klona": "^2.0.6", "magic-string": "^0.30.17", "mdast-util-directive": "^3.0.0", "mdast-util-to-markdown": "^2.1.0", "mdast-util-to-string": "^4.0.0", "pagefind": "^1.3.0", "rehype": "^13.0.1", "rehype-format": "^5.0.0", "remark-directive": "^3.0.0", "ultrahtml": "^1.6.0", "unified": "^11.0.5", "unist-util-visit": "^5.0.0", "vfile": "^6.0.2" }, "peerDependencies": { "astro": "^5.5.0" } }, "sha512-wQrKwH431q+8FsLBnNQeG+R36TMtEGxTQ2AuiVpcx9APcazvL3n7wVW8mMmYyxX0POjTnxlcWPkdMGR3Yj1L+w=="], "@astrojs/telemetry": ["@astrojs/telemetry@3.3.0", "", { "dependencies": { "ci-info": "^4.2.0", "debug": "^4.4.0", "dlv": "^1.1.3", "dset": "^3.1.4", "is-docker": "^3.0.0", "is-wsl": "^3.1.0", "which-pm-runs": "^1.1.0" } }, "sha512-UFBgfeldP06qu6khs/yY+q1cDAaArM2/7AEIqQ9Cuvf7B1hNLq0xDrZkct+QoIGyjq56y8IaE2I3CTvG99mlhQ=="], + "@babel/code-frame": ["@babel/code-frame@7.29.7", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.29.7", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw=="], + + "@babel/compat-data": ["@babel/compat-data@7.29.7", "", {}, "sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg=="], + + "@babel/core": ["@babel/core@7.29.7", "", { "dependencies": { "@babel/code-frame": "^7.29.7", "@babel/generator": "^7.29.7", "@babel/helper-compilation-targets": "^7.29.7", "@babel/helper-module-transforms": "^7.29.7", "@babel/helpers": "^7.29.7", "@babel/parser": "^7.29.7", "@babel/template": "^7.29.7", "@babel/traverse": "^7.29.7", "@babel/types": "^7.29.7", "@jridgewell/remapping": "^2.3.5", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", "json5": "^2.2.3", "semver": "^6.3.1" } }, "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA=="], + + "@babel/generator": ["@babel/generator@7.29.7", "", { "dependencies": { "@babel/parser": "^7.29.7", "@babel/types": "^7.29.7", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-DkXD5OJQaAQIdZ1bt3UZdEnHAn9Imd3IVBdX03UFe+ony9Ojw5pzr9YVKGDY1jt+Gcn/FnGkNf8r+Vj5NOJWtQ=="], + + "@babel/helper-compilation-targets": ["@babel/helper-compilation-targets@7.29.7", "", { "dependencies": { "@babel/compat-data": "^7.29.7", "@babel/helper-validator-option": "^7.29.7", "browserslist": "^4.24.0", "lru-cache": "^5.1.1", "semver": "^6.3.1" } }, "sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g=="], + + "@babel/helper-globals": ["@babel/helper-globals@7.29.7", "", {}, "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA=="], + + "@babel/helper-module-imports": ["@babel/helper-module-imports@7.29.7", "", { "dependencies": { "@babel/traverse": "^7.29.7", "@babel/types": "^7.29.7" } }, "sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g=="], + + "@babel/helper-module-transforms": ["@babel/helper-module-transforms@7.29.7", "", { "dependencies": { "@babel/helper-module-imports": "^7.29.7", "@babel/helper-validator-identifier": "^7.29.7", "@babel/traverse": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg=="], + + "@babel/helper-plugin-utils": ["@babel/helper-plugin-utils@7.29.7", "", {}, "sha512-G7sHYigPY17oO5SYWnfD/0MTBwVR781S/JI643e/JhUYgVgWE/61SoW3NH9KWUKyKq5LVh3npif99Wkt6j86Jw=="], + "@babel/helper-string-parser": ["@babel/helper-string-parser@7.27.1", "", {}, "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA=="], "@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.28.5", "", {}, "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q=="], + "@babel/helper-validator-option": ["@babel/helper-validator-option@7.29.7", "", {}, "sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw=="], + + "@babel/helpers": ["@babel/helpers@7.29.7", "", { "dependencies": { "@babel/template": "^7.29.7", "@babel/types": "^7.29.7" } }, "sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg=="], + "@babel/parser": ["@babel/parser@7.29.0", "", { "dependencies": { "@babel/types": "^7.29.0" }, "bin": "./bin/babel-parser.js" }, "sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww=="], + "@babel/plugin-transform-react-jsx-self": ["@babel/plugin-transform-react-jsx-self@7.29.7", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-TL0hMc9xzy86VD31nUiwzd5otRAcyEPcsegCxolO0PvcXuH1v0kECe/UIznYFihpkvU5wg/jk4v0TTEFfm53fw=="], + + "@babel/plugin-transform-react-jsx-source": ["@babel/plugin-transform-react-jsx-source@7.29.7", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-06IyK09H3wi4cGbhDBwp5gUGo0IKtnYa8tyTiephirPCK6fbobVGiXMMI5zLQ4aKEYP3wZ3ArU44o+8KMrSG/Q=="], + "@babel/runtime": ["@babel/runtime@7.28.6", "", {}, "sha512-05WQkdpL9COIMz4LjTxGpPNCdlpyimKppYNoJ5Di5EUObifl8t4tuLuUBBZEpoLYOmfvIWrsp9fCl0HoPRVTdA=="], + "@babel/template": ["@babel/template@7.29.7", "", { "dependencies": { "@babel/code-frame": "^7.29.7", "@babel/parser": "^7.29.7", "@babel/types": "^7.29.7" } }, "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg=="], + + "@babel/traverse": ["@babel/traverse@7.29.7", "", { "dependencies": { "@babel/code-frame": "^7.29.7", "@babel/generator": "^7.29.7", "@babel/helper-globals": "^7.29.7", "@babel/parser": "^7.29.7", "@babel/template": "^7.29.7", "@babel/types": "^7.29.7", "debug": "^4.3.1" } }, "sha512-EhlfNQtZ+NK22w5BM61ciuiq1m58ed33Wr1Xan//ZRTy6hgjnwyCffRYwzsGXdASJSUJ1guZILsErh1eQcl+zw=="], + "@babel/types": ["@babel/types@7.29.0", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A=="], "@biomejs/biome": ["@biomejs/biome@2.4.0", "", { "optionalDependencies": { "@biomejs/cli-darwin-arm64": "2.4.0", "@biomejs/cli-darwin-x64": "2.4.0", "@biomejs/cli-linux-arm64": "2.4.0", "@biomejs/cli-linux-arm64-musl": "2.4.0", "@biomejs/cli-linux-x64": "2.4.0", "@biomejs/cli-linux-x64-musl": "2.4.0", "@biomejs/cli-win32-arm64": "2.4.0", "@biomejs/cli-win32-x64": "2.4.0" }, "bin": { "biome": "bin/biome" } }, "sha512-iluT61cORUDIC5i/y42ljyQraCemmmcgbMLLCnYO+yh+2hjTmcMFcwY8G0zTzWCsPb3t3AyKc+0t/VuhPZULUg=="], @@ -366,6 +412,10 @@ "@inquirer/external-editor": ["@inquirer/external-editor@1.0.3", "", { "dependencies": { "chardet": "^2.1.1", "iconv-lite": "^0.7.0" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-RWbSrDiYmO4LbejWY7ttpxczuwQyZLBUyygsA9Nsv95hpzUWwnNTVQmAq3xuh7vNwCp07UTmE5i11XAEExx4RA=="], + "@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.13", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA=="], + + "@jridgewell/remapping": ["@jridgewell/remapping@2.3.5", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ=="], + "@jridgewell/resolve-uri": ["@jridgewell/resolve-uri@3.1.2", "", {}, "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw=="], "@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.5", "", {}, "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og=="], @@ -420,6 +470,8 @@ "@poppinss/exception": ["@poppinss/exception@1.2.3", "", {}, "sha512-dCED+QRChTVatE9ibtoaxc+WkdzOSjYTKi/+uacHWIsfodVfpsueo3+DKpgU5Px8qXjgmXkSvhXvSCz3fnP9lw=="], + "@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-rc.3", "", {}, "sha512-eybk3TjzzzV97Dlj5c+XrBFW57eTNhzod66y9HrBlzJ6NsCrWCp/2kaPS3K9wJmurBC0Tdw4yPjXKZqlznim3Q=="], + "@rollup/pluginutils": ["@rollup/pluginutils@5.3.0", "", { "dependencies": { "@types/estree": "^1.0.0", "estree-walker": "^2.0.2", "picomatch": "^4.0.2" }, "peerDependencies": { "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" }, "optionalPeers": ["rollup"] }, "sha512-5EdhGZtnu3V88ces7s53hhfK5KSASnJZv8Lulpc04cWO3REESroJXg73DFsOmgbU2BhwV0E20bu2IDZb3VKW4Q=="], "@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.57.1", "", { "os": "android", "cpu": "arm" }, "sha512-A6ehUVSiSaaliTxai040ZpZ2zTevHYbvu/lDoeAteHI8QnaosIzm4qwtezfRg1jOYaUmnzLX1AOD6Z+UJjtifg=="], @@ -504,6 +556,14 @@ "@turbo/windows-arm64": ["@turbo/windows-arm64@2.8.21", "", { "os": "win32", "cpu": "arm64" }, "sha512-95tMA/ZbIidJFUUtkmqioQ1gf3n3I1YbRP3ZgVdWTVn2qVbkodcIdGXBKRHHrIbRsLRl99SiHi/L7IxhpZDagQ=="], + "@types/babel__core": ["@types/babel__core@7.20.5", "", { "dependencies": { "@babel/parser": "^7.20.7", "@babel/types": "^7.20.7", "@types/babel__generator": "*", "@types/babel__template": "*", "@types/babel__traverse": "*" } }, "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA=="], + + "@types/babel__generator": ["@types/babel__generator@7.27.0", "", { "dependencies": { "@babel/types": "^7.0.0" } }, "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg=="], + + "@types/babel__template": ["@types/babel__template@7.4.4", "", { "dependencies": { "@babel/parser": "^7.1.0", "@babel/types": "^7.0.0" } }, "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A=="], + + "@types/babel__traverse": ["@types/babel__traverse@7.28.0", "", { "dependencies": { "@babel/types": "^7.28.2" } }, "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q=="], + "@types/debug": ["@types/debug@4.1.12", "", { "dependencies": { "@types/ms": "*" } }, "sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ=="], "@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="], @@ -524,16 +584,24 @@ "@types/node": ["@types/node@24.10.13", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-oH72nZRfDv9lADUBSo104Aq7gPHpQZc4BTx38r9xf9pg5LfP6EzSyH2n7qFmmxRQXh7YlUXODcYsg6PuTDSxGg=="], + "@types/react": ["@types/react@19.2.15", "", { "dependencies": { "csstype": "^3.2.2" } }, "sha512-eRwcGNHve+E8qtEQSSRl6urh+rFop4v8gm6O8rGv25CodbvFdLjA1vVQ1KkiFE0w0UPOnb8tDiFKL5lp0rtY5Q=="], + + "@types/react-dom": ["@types/react-dom@19.2.3", "", { "peerDependencies": { "@types/react": "^19.2.0" } }, "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ=="], + "@types/sax": ["@types/sax@1.2.7", "", { "dependencies": { "@types/node": "*" } }, "sha512-rO73L89PJxeYM3s3pPPjiPgVVcymqU490g0YO5n5By0k2Erzj6tay/4lr1CHAAU4JyOWd1rpQ8bCf6cZfHU96A=="], "@types/unist": ["@types/unist@3.0.3", "", {}, "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q=="], "@ungap/structured-clone": ["@ungap/structured-clone@1.3.0", "", {}, "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g=="], + "@vitejs/plugin-react": ["@vitejs/plugin-react@5.2.0", "", { "dependencies": { "@babel/core": "^7.29.0", "@babel/plugin-transform-react-jsx-self": "^7.27.1", "@babel/plugin-transform-react-jsx-source": "^7.27.1", "@rolldown/pluginutils": "1.0.0-rc.3", "@types/babel__core": "^7.20.5", "react-refresh": "^0.18.0" }, "peerDependencies": { "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-YmKkfhOAi3wsB1PhJq5Scj3GXMn3WvtQ/JC0xoopuHoXSdmtdStOpFrYaT1kie2YgFBcIe64ROzMYRjCrYOdYw=="], + "acorn": ["acorn@8.15.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg=="], "acorn-jsx": ["acorn-jsx@5.3.2", "", { "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ=="], + "agentation": ["agentation@3.0.2", "", { "peerDependencies": { "react": ">=18.0.0", "react-dom": ">=18.0.0" }, "optionalPeers": ["react", "react-dom"] }, "sha512-iGzBxFVTuZEIKzLY6AExSLAQH6i6SwxV4pAu7v7m3X6bInZ7qlZXAwrEqyc4+EfP4gM7z2RXBF6SF4DeH0f2lA=="], + "ansi-align": ["ansi-align@3.0.1", "", { "dependencies": { "string-width": "^4.1.0" } }, "sha512-IOfwwBF5iczOjp/WeY4YxyjqAFMQoZufdQWDd19SEExbVLNXqvpzSJ/M7Za4/sCPmQ0+GRquoA7bGcINcxew6w=="], "ansi-colors": ["ansi-colors@4.1.3", "", {}, "sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw=="], @@ -566,6 +634,8 @@ "base-64": ["base-64@1.0.0", "", {}, "sha512-kwDPIFCGx0NZHog36dj+tHiwP4QMzsZ3AgMViUBKI0+V5n4U0ufTCUMhnQ04diaRI8EX/QcPfql7zlhZ7j4zgg=="], + "baseline-browser-mapping": ["baseline-browser-mapping@2.10.32", "", { "bin": { "baseline-browser-mapping": "dist/cli.cjs" } }, "sha512-wbPvpyjJPC0zdfdKXxqEL3Ea+bOMD/87X4lftiJkkaBiuG6ALQy1SLmEd7BSmVCuwCQsBrCamgBoLyfFDD1EPg=="], + "bcp-47": ["bcp-47@2.1.0", "", { "dependencies": { "is-alphabetical": "^2.0.0", "is-alphanumerical": "^2.0.0", "is-decimal": "^2.0.0" } }, "sha512-9IIS3UPrvIa1Ej+lVDdDwO7zLehjqsaByECw0bu2RRGP73jALm6FYbzI5gWbgHLvNdkvfXB5YrSbocZdOS0c0w=="], "bcp-47-match": ["bcp-47-match@2.0.3", "", {}, "sha512-JtTezzbAibu8G0R9op9zb3vcWZd9JF6M0xOYGPn0fNCd7wOpRB1mU2mH9T8gaBGbAAyIIVgB2G7xG0GP98zMAQ=="], @@ -580,8 +650,12 @@ "braces": ["braces@3.0.3", "", { "dependencies": { "fill-range": "^7.1.1" } }, "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA=="], + "browserslist": ["browserslist@4.28.2", "", { "dependencies": { "baseline-browser-mapping": "^2.10.12", "caniuse-lite": "^1.0.30001782", "electron-to-chromium": "^1.5.328", "node-releases": "^2.0.36", "update-browserslist-db": "^1.2.3" }, "bin": { "browserslist": "cli.js" } }, "sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg=="], + "camelcase": ["camelcase@8.0.0", "", {}, "sha512-8WB3Jcas3swSvjIeA2yvCJ+Miyz5l1ZmB6HFb9R1317dt9LCQoswg/BGrmAmkWVEszSrrg4RwmO46qIm2OEnSA=="], + "caniuse-lite": ["caniuse-lite@1.0.30001793", "", {}, "sha512-iwSsYWaCOoh26cV8NwNRViHlrfUvYsHDfRVcbtmw0Kg6PJIZZXwMkj1442FYLBGkeUf1juAsU3DTfxW579mrPA=="], + "ccount": ["ccount@2.0.1", "", {}, "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg=="], "chalk": ["chalk@5.6.2", "", {}, "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA=="], @@ -598,6 +672,8 @@ "chkit": ["chkit@workspace:packages/cli"], + "chkit-example-clickbench": ["chkit-example-clickbench@workspace:examples/clickbench"], + "chokidar": ["chokidar@5.0.0", "", { "dependencies": { "readdirp": "^5.0.0" } }, "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw=="], "ci-info": ["ci-info@3.9.0", "", {}, "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ=="], @@ -614,6 +690,8 @@ "common-ancestor-path": ["common-ancestor-path@1.0.1", "", {}, "sha512-L3sHRo1pXXEqX8VU28kfgUY+YGsk09hPqZiZmLacNib6XNTCM8ubYeT7ryXQw8asB1sKgcU5lkB7ONug08aB8w=="], + "convert-source-map": ["convert-source-map@2.0.0", "", {}, "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg=="], + "cookie": ["cookie@1.1.1", "", {}, "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ=="], "cookie-es": ["cookie-es@1.2.2", "", {}, "sha512-+W7VmiVINB+ywl1HGXJXmrqkOhpKrIiVZV6tQuV54ZyQC7MMuBt81Vc336GMLoHBq5hV/F9eXgt5Mnx0Rha5Fg=="], @@ -636,6 +714,8 @@ "csso": ["csso@5.0.5", "", { "dependencies": { "css-tree": "~2.2.0" } }, "sha512-0LrrStPOdJj+SPCCrGhzryycLjwcgUSHBtxNA8aIDxf0GLsRh1cKYhB00Gd1lDOS4yGH69+SNn13+TWbVHETFQ=="], + "csstype": ["csstype@3.2.3", "", {}, "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ=="], + "debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], "decode-named-character-reference": ["decode-named-character-reference@1.3.0", "", { "dependencies": { "character-entities": "^2.0.0" } }, "sha512-GtpQYB283KrPp6nRw50q3U9/VfOutZOe103qlN7BPP6Ad27xYnOIWv4lPzo8HCAL+mMZofJ9KEy30fq6MfaK6Q=="], @@ -652,7 +732,7 @@ "deterministic-object-hash": ["deterministic-object-hash@2.0.2", "", { "dependencies": { "base-64": "^1.0.0" } }, "sha512-KxektNH63SrbfUyDiwXqRb1rLwKt33AmMv+5Nhsw1kqZ13SJBRTgZHtGbE+hH3a1mVW1cz+4pqSWVPAtLVXTzQ=="], - "devalue": ["devalue@5.6.2", "", {}, "sha512-nPRkjWzzDQlsejL1WVifk5rvcFi/y1onBRxjaFMjZeR9mFpqu2gmAZ9xUB9/IEanEP/vBtGeGganC/GO1fmufg=="], + "devalue": ["devalue@5.8.1", "", {}, "sha512-4CXDYRBGqN+57wVJkuXBYmpAVUSg3L6JAQa/DFqm238G73E1wuyc/JhGQJzN7vUf/CMphYau2zXbfWzDR5aTEw=="], "devlop": ["devlop@1.1.0", "", { "dependencies": { "dequal": "^2.0.0" } }, "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA=="], @@ -674,6 +754,8 @@ "dset": ["dset@3.1.4", "", {}, "sha512-2QF/g9/zTaPDc3BjNcVTGoBbXBgYfMTTceLaYcFJ/W9kggFUkhxD/hMEeuLKbugyef9SqAx8cpgwlIP/jinUTA=="], + "electron-to-chromium": ["electron-to-chromium@1.5.361", "", {}, "sha512-Q6Hts7N9FnJc5LeGRINFvLhCI9xZmNtTDe5ZbcVezQz7cU4a8Aua3GH1b8J2XY8Al9PF+OCwYqhgsOOheMdvkA=="], + "emoji-regex": ["emoji-regex@10.6.0", "", {}, "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A=="], "enquirer": ["enquirer@2.4.1", "", { "dependencies": { "ansi-colors": "^4.1.1", "strip-ansi": "^6.0.1" } }, "sha512-rRqJg/6gd538VHvR3PSrdRBb/1Vy2YfzHqzvbhGIQpDRKIa4FgV/54b5Q1xYSxOOwKvjXweS26E0Q+nAMwp2pQ=="], @@ -690,6 +772,8 @@ "esbuild": ["esbuild@0.27.3", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.27.3", "@esbuild/android-arm": "0.27.3", "@esbuild/android-arm64": "0.27.3", "@esbuild/android-x64": "0.27.3", "@esbuild/darwin-arm64": "0.27.3", "@esbuild/darwin-x64": "0.27.3", "@esbuild/freebsd-arm64": "0.27.3", "@esbuild/freebsd-x64": "0.27.3", "@esbuild/linux-arm": "0.27.3", "@esbuild/linux-arm64": "0.27.3", "@esbuild/linux-ia32": "0.27.3", "@esbuild/linux-loong64": "0.27.3", "@esbuild/linux-mips64el": "0.27.3", "@esbuild/linux-ppc64": "0.27.3", "@esbuild/linux-riscv64": "0.27.3", "@esbuild/linux-s390x": "0.27.3", "@esbuild/linux-x64": "0.27.3", "@esbuild/netbsd-arm64": "0.27.3", "@esbuild/netbsd-x64": "0.27.3", "@esbuild/openbsd-arm64": "0.27.3", "@esbuild/openbsd-x64": "0.27.3", "@esbuild/openharmony-arm64": "0.27.3", "@esbuild/sunos-x64": "0.27.3", "@esbuild/win32-arm64": "0.27.3", "@esbuild/win32-ia32": "0.27.3", "@esbuild/win32-x64": "0.27.3" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg=="], + "escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="], + "escape-string-regexp": ["escape-string-regexp@5.0.0", "", {}, "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw=="], "esprima": ["esprima@4.0.1", "", { "bin": { "esparse": "./bin/esparse.js", "esvalidate": "./bin/esvalidate.js" } }, "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A=="], @@ -742,6 +826,8 @@ "fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="], + "gensync": ["gensync@1.0.0-beta.2", "", {}, "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg=="], + "get-east-asian-width": ["get-east-asian-width@1.4.0", "", {}, "sha512-QZjmEOC+IT1uk6Rx0sX22V6uHWVwbdbxf1faPqJ1QhLdGgsRGCZoyaQBm/piRdJy/D2um6hM1UP7ZEeQ4EkP+Q=="], "giget": ["giget@3.2.0", "", { "bin": { "giget": "dist/cli.mjs" } }, "sha512-GvHTWcykIR/fP8cj8dMpuMMkvaeJfPvYnhq0oW+chSeIr+ldX21ifU2Ms6KBoyKZQZmVaUAAhQ2EZ68KJF8a7A=="], @@ -850,8 +936,14 @@ "isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="], + "js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="], + "js-yaml": ["js-yaml@4.1.1", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA=="], + "jsesc": ["jsesc@3.1.0", "", { "bin": { "jsesc": "bin/jsesc" } }, "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA=="], + + "json5": ["json5@2.2.3", "", { "bin": { "json5": "lib/cli.js" } }, "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg=="], + "jsonfile": ["jsonfile@4.0.0", "", { "optionalDependencies": { "graceful-fs": "^4.1.6" } }, "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg=="], "kleur": ["kleur@3.0.3", "", {}, "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w=="], @@ -1006,6 +1098,8 @@ "node-mock-http": ["node-mock-http@1.0.4", "", {}, "sha512-8DY+kFsDkNXy1sJglUfuODx1/opAGJGyrTuFqEoN90oRc2Vk0ZbD4K2qmKXBBEhZQzdKHIVfEJpDU8Ak2NJEvQ=="], + "node-releases": ["node-releases@2.0.46", "", {}, "sha512-GYVXHE2KnrzAfsAjl4uP++evGFCrAU1jta4ubEjIG7YWt/64Gqv66a30yKwWczVjA6j3bM4nBwH7Pk1JmDHaxQ=="], + "normalize-path": ["normalize-path@3.0.0", "", {}, "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA=="], "nth-check": ["nth-check@2.1.1", "", { "dependencies": { "boolbase": "^1.0.0" } }, "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w=="], @@ -1088,6 +1182,12 @@ "radix3": ["radix3@1.1.2", "", {}, "sha512-b484I/7b8rDEdSDKckSSBA8knMpcdsXudlE/LNL639wFoHKwLbEkQFZHWEYwDC0wa0FKUcCY+GAF73Z7wxNVFA=="], + "react": ["react@19.2.6", "", {}, "sha512-sfWGGfavi0xr8Pg0sVsyHMAOziVYKgPLNrS7ig+ivMNb3wbCBw3KxtflsGBAwD3gYQlE/AEZsTLgToRrSCjb0Q=="], + + "react-dom": ["react-dom@19.2.6", "", { "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { "react": "^19.2.6" } }, "sha512-0prMI+hvBbPjsWnxDLxlCGyM8PN6UuWjEUCYmZhO67xIV9Xasa/r/vDnq+Xyq4Lo27g8QSbO5YzARu0D1Sps3g=="], + + "react-refresh": ["react-refresh@0.18.0", "", {}, "sha512-QgT5//D3jfjJb6Gsjxv0Slpj23ip+HtOpnNgnb2S5zU3CB26G/IDPGoy4RJB42wzFE46DRsstbW6tKHoKbhAxw=="], + "read-yaml-file": ["read-yaml-file@1.1.0", "", { "dependencies": { "graceful-fs": "^4.1.5", "js-yaml": "^3.6.1", "pify": "^4.0.1", "strip-bom": "^3.0.0" } }, "sha512-VIMnQi/Z4HT2Fxuwg5KrY174U1VdUIASQVWXXyqtNRtxSr9IYkn1rsI6Tb6HsrHCmB7gVpNwX6JxPTHcH6IoTA=="], "readdirp": ["readdirp@5.0.0", "", {}, "sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ=="], @@ -1154,6 +1254,8 @@ "sax": ["sax@1.4.4", "", {}, "sha512-1n3r/tGXO6b6VXMdFT54SHzT9ytu9yr7TaELowdYpMqY/Ao7EnlQGmAQ1+RatX7Tkkdm6hONI2owqNx2aZj5Sw=="], + "scheduler": ["scheduler@0.27.0", "", {}, "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q=="], + "semver": ["semver@7.7.4", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA=="], "sharp": ["sharp@0.34.5", "", { "dependencies": { "@img/colour": "^1.0.0", "detect-libc": "^2.1.2", "semver": "^7.7.3" }, "optionalDependencies": { "@img/sharp-darwin-arm64": "0.34.5", "@img/sharp-darwin-x64": "0.34.5", "@img/sharp-libvips-darwin-arm64": "1.2.4", "@img/sharp-libvips-darwin-x64": "1.2.4", "@img/sharp-libvips-linux-arm": "1.2.4", "@img/sharp-libvips-linux-arm64": "1.2.4", "@img/sharp-libvips-linux-ppc64": "1.2.4", "@img/sharp-libvips-linux-riscv64": "1.2.4", "@img/sharp-libvips-linux-s390x": "1.2.4", "@img/sharp-libvips-linux-x64": "1.2.4", "@img/sharp-libvips-linuxmusl-arm64": "1.2.4", "@img/sharp-libvips-linuxmusl-x64": "1.2.4", "@img/sharp-linux-arm": "0.34.5", "@img/sharp-linux-arm64": "0.34.5", "@img/sharp-linux-ppc64": "0.34.5", "@img/sharp-linux-riscv64": "0.34.5", "@img/sharp-linux-s390x": "0.34.5", "@img/sharp-linux-x64": "0.34.5", "@img/sharp-linuxmusl-arm64": "0.34.5", "@img/sharp-linuxmusl-x64": "0.34.5", "@img/sharp-wasm32": "0.34.5", "@img/sharp-win32-arm64": "0.34.5", "@img/sharp-win32-ia32": "0.34.5", "@img/sharp-win32-x64": "0.34.5" } }, "sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg=="], @@ -1268,6 +1370,8 @@ "unstorage": ["unstorage@1.17.4", "", { "dependencies": { "anymatch": "^3.1.3", "chokidar": "^5.0.0", "destr": "^2.0.5", "h3": "^1.15.5", "lru-cache": "^11.2.0", "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" }, "optionalPeers": ["@azure/app-configuration", "@azure/cosmos", "@azure/data-tables", "@azure/identity", "@azure/keyvault-secrets", "@azure/storage-blob", "@capacitor/preferences", "@deno/kv", "@netlify/blobs", "@planetscale/database", "@upstash/redis", "@vercel/blob", "@vercel/functions", "@vercel/kv", "aws4fetch", "db0", "idb-keyval", "ioredis", "uploadthing"] }, "sha512-fHK0yNg38tBiJKp/Vgsq4j0JEsCmgqH58HAn707S7zGkArbZsVr/CwINoi+nh3h98BRCwKvx1K3Xg9u3VV83sw=="], + "update-browserslist-db": ["update-browserslist-db@1.2.3", "", { "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.1" }, "peerDependencies": { "browserslist": ">= 4.21.0" }, "bin": { "update-browserslist-db": "cli.js" } }, "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w=="], + "util-deprecate": ["util-deprecate@1.0.2", "", {}, "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="], "vfile": ["vfile@6.0.3", "", { "dependencies": { "@types/unist": "^3.0.0", "vfile-message": "^4.0.0" } }, "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q=="], @@ -1276,7 +1380,7 @@ "vfile-message": ["vfile-message@4.0.3", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-stringify-position": "^4.0.0" } }, "sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw=="], - "vite": ["vite@6.4.1", "", { "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.4.4", "picomatch": "^4.0.2", "postcss": "^8.5.3", "rollup": "^4.34.9", "tinyglobby": "^0.2.13" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", "jiti": ">=1.21.0", "less": "*", "lightningcss": "^1.21.0", "sass": "*", "sass-embedded": "*", "stylus": "*", "sugarss": "*", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g=="], + "vite": ["vite@7.3.3", "", { "dependencies": { "esbuild": "^0.27.0", "fdir": "^6.5.0", "picomatch": "^4.0.3", "postcss": "^8.5.6", "rollup": "^4.43.0", "tinyglobby": "^0.2.15" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "jiti": ">=1.21.0", "less": "^4.0.0", "lightningcss": "^1.21.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" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-/4XH147Ui7OGTjg3HbdWe5arnZQSbfuRzdr9Ec7TQi5I7R+ir0Rlc9GIvD4v0XZurELqA035KVXJXpR61xhiTA=="], "vitefu": ["vitefu@1.1.1", "", { "peerDependencies": { "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0-beta.0" }, "optionalPeers": ["vite"] }, "sha512-B/Fegf3i8zh0yFbpzZ21amWzHmuNlLlmJT6n7bu5e+pCHUKQIfXSYokrqOBGEMMe9UG2sostKQF9mml/vYaWJQ=="], @@ -1298,6 +1402,8 @@ "xxhash-wasm": ["xxhash-wasm@1.1.0", "", {}, "sha512-147y/6YNh+tlp6nd/2pWq38i9h6mz/EuQ6njIrmW8D1BS5nCqs0P6DG+m6zTGnNz5I+uhZ0SHxBs9BsPrwcKDA=="], + "yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="], + "yargs-parser": ["yargs-parser@21.1.1", "", {}, "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw=="], "yocto-queue": ["yocto-queue@1.2.2", "", {}, "sha512-4LCcse/U2MHZ63HAJVE+v71o7yOdIe4cZ70Wpf8D/IyjDKYQLV5GD46B+hSTjJsvV5PztjvHoU580EftxjDZFQ=="], @@ -1318,14 +1424,54 @@ "zwitch": ["zwitch@2.0.4", "", {}, "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A=="], + "@astrojs/internal-helpers/picomatch": ["picomatch@4.0.4", "", {}, "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A=="], + + "@astrojs/markdown-remark/@astrojs/internal-helpers": ["@astrojs/internal-helpers@0.7.5", "", {}, "sha512-vreGnYSSKhAjFJCWAwe/CNhONvoc5lokxtRoZims+0wa3KbHBdPHSSthJsKxPd8d/aic6lWKpRTYGY/hsgK6EA=="], + "@astrojs/telemetry/ci-info": ["ci-info@4.4.0", "", {}, "sha512-77PSwercCZU2Fc4sX94eF8k8Pxte6JAwL4/ICZLFjJLqegs7kCuAsqqj/70NQF6TvDpgFjkubQB2FW2ZZddvQg=="], + "@babel/code-frame/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.29.7", "", {}, "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg=="], + + "@babel/core/@babel/parser": ["@babel/parser@7.29.7", "", { "dependencies": { "@babel/types": "^7.29.7" }, "bin": "./bin/babel-parser.js" }, "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg=="], + + "@babel/core/@babel/types": ["@babel/types@7.29.7", "", { "dependencies": { "@babel/helper-string-parser": "^7.29.7", "@babel/helper-validator-identifier": "^7.29.7" } }, "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA=="], + + "@babel/core/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], + + "@babel/generator/@babel/parser": ["@babel/parser@7.29.7", "", { "dependencies": { "@babel/types": "^7.29.7" }, "bin": "./bin/babel-parser.js" }, "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg=="], + + "@babel/generator/@babel/types": ["@babel/types@7.29.7", "", { "dependencies": { "@babel/helper-string-parser": "^7.29.7", "@babel/helper-validator-identifier": "^7.29.7" } }, "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA=="], + + "@babel/generator/@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="], + + "@babel/helper-compilation-targets/lru-cache": ["lru-cache@5.1.1", "", { "dependencies": { "yallist": "^3.0.2" } }, "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w=="], + + "@babel/helper-compilation-targets/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], + + "@babel/helper-module-imports/@babel/types": ["@babel/types@7.29.7", "", { "dependencies": { "@babel/helper-string-parser": "^7.29.7", "@babel/helper-validator-identifier": "^7.29.7" } }, "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA=="], + + "@babel/helper-module-transforms/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.29.7", "", {}, "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg=="], + + "@babel/helpers/@babel/types": ["@babel/types@7.29.7", "", { "dependencies": { "@babel/helper-string-parser": "^7.29.7", "@babel/helper-validator-identifier": "^7.29.7" } }, "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA=="], + + "@babel/template/@babel/parser": ["@babel/parser@7.29.7", "", { "dependencies": { "@babel/types": "^7.29.7" }, "bin": "./bin/babel-parser.js" }, "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg=="], + + "@babel/template/@babel/types": ["@babel/types@7.29.7", "", { "dependencies": { "@babel/helper-string-parser": "^7.29.7", "@babel/helper-validator-identifier": "^7.29.7" } }, "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA=="], + + "@babel/traverse/@babel/parser": ["@babel/parser@7.29.7", "", { "dependencies": { "@babel/types": "^7.29.7" }, "bin": "./bin/babel-parser.js" }, "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg=="], + + "@babel/traverse/@babel/types": ["@babel/types@7.29.7", "", { "dependencies": { "@babel/helper-string-parser": "^7.29.7", "@babel/helper-validator-identifier": "^7.29.7" } }, "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA=="], + "@chkit/plugin-backfill/zod": ["zod@4.3.6", "", {}, "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg=="], "@chkit/plugin-codegen/zod": ["zod@4.3.6", "", {}, "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg=="], "@chkit/plugin-pull/zod": ["zod@4.3.6", "", {}, "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg=="], + "@jridgewell/gen-mapping/@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="], + + "@jridgewell/remapping/@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="], + "@manypkg/find-root/@types/node": ["@types/node@12.20.55", "", {}, "sha512-J8xLz7q2OFulZ2cyGTLE1TbbZcjpno7FaN6zdJNrgAdrJ+DZzh/uFR6YrTb4C+nXakvud8Q4+rbhoIWlYQbUFQ=="], "@manypkg/find-root/fs-extra": ["fs-extra@8.1.0", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^4.0.0", "universalify": "^0.1.0" } }, "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g=="], @@ -1338,16 +1484,34 @@ "@rollup/pluginutils/estree-walker": ["estree-walker@2.0.2", "", {}, "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w=="], + "@types/babel__core/@babel/parser": ["@babel/parser@7.29.7", "", { "dependencies": { "@babel/types": "^7.29.7" }, "bin": "./bin/babel-parser.js" }, "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg=="], + + "@types/babel__core/@babel/types": ["@babel/types@7.29.7", "", { "dependencies": { "@babel/helper-string-parser": "^7.29.7", "@babel/helper-validator-identifier": "^7.29.7" } }, "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA=="], + + "@types/babel__generator/@babel/types": ["@babel/types@7.29.7", "", { "dependencies": { "@babel/helper-string-parser": "^7.29.7", "@babel/helper-validator-identifier": "^7.29.7" } }, "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA=="], + + "@types/babel__template/@babel/parser": ["@babel/parser@7.29.7", "", { "dependencies": { "@babel/types": "^7.29.7" }, "bin": "./bin/babel-parser.js" }, "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg=="], + + "@types/babel__template/@babel/types": ["@babel/types@7.29.7", "", { "dependencies": { "@babel/helper-string-parser": "^7.29.7", "@babel/helper-validator-identifier": "^7.29.7" } }, "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA=="], + + "@types/babel__traverse/@babel/types": ["@babel/types@7.29.7", "", { "dependencies": { "@babel/helper-string-parser": "^7.29.7", "@babel/helper-validator-identifier": "^7.29.7" } }, "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA=="], + "ansi-align/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], "anymatch/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="], + "astro/@astrojs/internal-helpers": ["@astrojs/internal-helpers@0.7.5", "", {}, "sha512-vreGnYSSKhAjFJCWAwe/CNhONvoc5lokxtRoZims+0wa3KbHBdPHSSthJsKxPd8d/aic6lWKpRTYGY/hsgK6EA=="], + "astro/ci-info": ["ci-info@4.4.0", "", {}, "sha512-77PSwercCZU2Fc4sX94eF8k8Pxte6JAwL4/ICZLFjJLqegs7kCuAsqqj/70NQF6TvDpgFjkubQB2FW2ZZddvQg=="], + "astro/devalue": ["devalue@5.6.2", "", {}, "sha512-nPRkjWzzDQlsejL1WVifk5rvcFi/y1onBRxjaFMjZeR9mFpqu2gmAZ9xUB9/IEanEP/vBtGeGganC/GO1fmufg=="], + "astro/p-limit": ["p-limit@6.2.0", "", { "dependencies": { "yocto-queue": "^1.1.1" } }, "sha512-kuUqqHNUqoIWp/c467RI4X6mmyuojY5jGutNU0wVTmEOOfcuwLqyMVoAi9MKi2Ak+5i9+nhmrK4ufZE8069kHA=="], "astro/package-manager-detector": ["package-manager-detector@1.6.0", "", {}, "sha512-61A5ThoTiDG/C8s8UMZwSorAGwMJ0ERVGj2OjoW5pAalsNOg15+iQiPzrLJ4jhZ1HJzmC2PIHT2oEiH3R5fzNA=="], + "astro/vite": ["vite@6.4.1", "", { "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.4.4", "picomatch": "^4.0.2", "postcss": "^8.5.3", "rollup": "^4.34.9", "tinyglobby": "^0.2.13" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", "jiti": ">=1.21.0", "less": "*", "lightningcss": "^1.21.0", "sass": "*", "sass-embedded": "*", "stylus": "*", "sugarss": "*", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g=="], + "boxen/type-fest": ["type-fest@4.41.0", "", {}, "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA=="], "create-chkit/@types/node": ["@types/node@20.19.41", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-ECymXOukMnOoVkC2bb1Vc/w/836DXncOg5m8Xj1RH7xSHZJWNYY6Zh7EH477vcnD5egKNNfy2RpNOmuChhFPgQ=="], @@ -1370,12 +1534,54 @@ "svgo/commander": ["commander@11.1.0", "", {}, "sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ=="], - "vite/esbuild": ["esbuild@0.25.12", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.25.12", "@esbuild/android-arm": "0.25.12", "@esbuild/android-arm64": "0.25.12", "@esbuild/android-x64": "0.25.12", "@esbuild/darwin-arm64": "0.25.12", "@esbuild/darwin-x64": "0.25.12", "@esbuild/freebsd-arm64": "0.25.12", "@esbuild/freebsd-x64": "0.25.12", "@esbuild/linux-arm": "0.25.12", "@esbuild/linux-arm64": "0.25.12", "@esbuild/linux-ia32": "0.25.12", "@esbuild/linux-loong64": "0.25.12", "@esbuild/linux-mips64el": "0.25.12", "@esbuild/linux-ppc64": "0.25.12", "@esbuild/linux-riscv64": "0.25.12", "@esbuild/linux-s390x": "0.25.12", "@esbuild/linux-x64": "0.25.12", "@esbuild/netbsd-arm64": "0.25.12", "@esbuild/netbsd-x64": "0.25.12", "@esbuild/openbsd-arm64": "0.25.12", "@esbuild/openbsd-x64": "0.25.12", "@esbuild/openharmony-arm64": "0.25.12", "@esbuild/sunos-x64": "0.25.12", "@esbuild/win32-arm64": "0.25.12", "@esbuild/win32-ia32": "0.25.12", "@esbuild/win32-x64": "0.25.12" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg=="], + "vite/picomatch": ["picomatch@4.0.4", "", {}, "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A=="], "wrap-ansi/strip-ansi": ["strip-ansi@7.1.2", "", { "dependencies": { "ansi-regex": "^6.0.1" } }, "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA=="], + "@babel/core/@babel/types/@babel/helper-string-parser": ["@babel/helper-string-parser@7.29.7", "", {}, "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw=="], + + "@babel/core/@babel/types/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.29.7", "", {}, "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg=="], + + "@babel/generator/@babel/types/@babel/helper-string-parser": ["@babel/helper-string-parser@7.29.7", "", {}, "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw=="], + + "@babel/generator/@babel/types/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.29.7", "", {}, "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg=="], + + "@babel/helper-module-imports/@babel/types/@babel/helper-string-parser": ["@babel/helper-string-parser@7.29.7", "", {}, "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw=="], + + "@babel/helper-module-imports/@babel/types/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.29.7", "", {}, "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg=="], + + "@babel/helpers/@babel/types/@babel/helper-string-parser": ["@babel/helper-string-parser@7.29.7", "", {}, "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw=="], + + "@babel/helpers/@babel/types/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.29.7", "", {}, "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg=="], + + "@babel/template/@babel/types/@babel/helper-string-parser": ["@babel/helper-string-parser@7.29.7", "", {}, "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw=="], + + "@babel/template/@babel/types/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.29.7", "", {}, "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg=="], + + "@babel/traverse/@babel/types/@babel/helper-string-parser": ["@babel/helper-string-parser@7.29.7", "", {}, "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw=="], + + "@babel/traverse/@babel/types/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.29.7", "", {}, "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg=="], + + "@types/babel__core/@babel/types/@babel/helper-string-parser": ["@babel/helper-string-parser@7.29.7", "", {}, "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw=="], + + "@types/babel__core/@babel/types/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.29.7", "", {}, "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg=="], + + "@types/babel__generator/@babel/types/@babel/helper-string-parser": ["@babel/helper-string-parser@7.29.7", "", {}, "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw=="], + + "@types/babel__generator/@babel/types/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.29.7", "", {}, "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg=="], + + "@types/babel__template/@babel/types/@babel/helper-string-parser": ["@babel/helper-string-parser@7.29.7", "", {}, "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw=="], + + "@types/babel__template/@babel/types/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.29.7", "", {}, "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg=="], + + "@types/babel__traverse/@babel/types/@babel/helper-string-parser": ["@babel/helper-string-parser@7.29.7", "", {}, "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw=="], + + "@types/babel__traverse/@babel/types/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.29.7", "", {}, "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg=="], + "ansi-align/string-width/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], + "astro/vite/esbuild": ["esbuild@0.25.12", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.25.12", "@esbuild/android-arm": "0.25.12", "@esbuild/android-arm64": "0.25.12", "@esbuild/android-x64": "0.25.12", "@esbuild/darwin-arm64": "0.25.12", "@esbuild/darwin-x64": "0.25.12", "@esbuild/freebsd-arm64": "0.25.12", "@esbuild/freebsd-x64": "0.25.12", "@esbuild/linux-arm": "0.25.12", "@esbuild/linux-arm64": "0.25.12", "@esbuild/linux-ia32": "0.25.12", "@esbuild/linux-loong64": "0.25.12", "@esbuild/linux-mips64el": "0.25.12", "@esbuild/linux-ppc64": "0.25.12", "@esbuild/linux-riscv64": "0.25.12", "@esbuild/linux-s390x": "0.25.12", "@esbuild/linux-x64": "0.25.12", "@esbuild/netbsd-arm64": "0.25.12", "@esbuild/netbsd-x64": "0.25.12", "@esbuild/openbsd-arm64": "0.25.12", "@esbuild/openbsd-x64": "0.25.12", "@esbuild/openharmony-arm64": "0.25.12", "@esbuild/sunos-x64": "0.25.12", "@esbuild/win32-arm64": "0.25.12", "@esbuild/win32-ia32": "0.25.12", "@esbuild/win32-x64": "0.25.12" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg=="], + "create-chkit/@types/node/undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="], "csso/css-tree/mdn-data": ["mdn-data@2.0.28", "", {}, "sha512-aylIc7Z9y4yzHYAJNuESG3hfhC+0Ibp/MAMiaOZgNv4pmEdFyfZhhhny4MNiAfWdBQ1RQ2mfDWmM1x8SvGyp8g=="], @@ -1384,58 +1590,58 @@ "string-width/strip-ansi/ansi-regex": ["ansi-regex@6.2.2", "", {}, "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg=="], - "vite/esbuild/@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.25.12", "", { "os": "aix", "cpu": "ppc64" }, "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA=="], + "wrap-ansi/strip-ansi/ansi-regex": ["ansi-regex@6.2.2", "", {}, "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg=="], - "vite/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.25.12", "", { "os": "android", "cpu": "arm" }, "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg=="], + "astro/vite/esbuild/@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.25.12", "", { "os": "aix", "cpu": "ppc64" }, "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA=="], - "vite/esbuild/@esbuild/android-arm64": ["@esbuild/android-arm64@0.25.12", "", { "os": "android", "cpu": "arm64" }, "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg=="], + "astro/vite/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.25.12", "", { "os": "android", "cpu": "arm" }, "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg=="], - "vite/esbuild/@esbuild/android-x64": ["@esbuild/android-x64@0.25.12", "", { "os": "android", "cpu": "x64" }, "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg=="], + "astro/vite/esbuild/@esbuild/android-arm64": ["@esbuild/android-arm64@0.25.12", "", { "os": "android", "cpu": "arm64" }, "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg=="], - "vite/esbuild/@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.25.12", "", { "os": "darwin", "cpu": "arm64" }, "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg=="], + "astro/vite/esbuild/@esbuild/android-x64": ["@esbuild/android-x64@0.25.12", "", { "os": "android", "cpu": "x64" }, "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg=="], - "vite/esbuild/@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.25.12", "", { "os": "darwin", "cpu": "x64" }, "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA=="], + "astro/vite/esbuild/@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.25.12", "", { "os": "darwin", "cpu": "arm64" }, "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg=="], - "vite/esbuild/@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.25.12", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg=="], + "astro/vite/esbuild/@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.25.12", "", { "os": "darwin", "cpu": "x64" }, "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA=="], - "vite/esbuild/@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.25.12", "", { "os": "freebsd", "cpu": "x64" }, "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ=="], + "astro/vite/esbuild/@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.25.12", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg=="], - "vite/esbuild/@esbuild/linux-arm": ["@esbuild/linux-arm@0.25.12", "", { "os": "linux", "cpu": "arm" }, "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw=="], + "astro/vite/esbuild/@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.25.12", "", { "os": "freebsd", "cpu": "x64" }, "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ=="], - "vite/esbuild/@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.25.12", "", { "os": "linux", "cpu": "arm64" }, "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ=="], + "astro/vite/esbuild/@esbuild/linux-arm": ["@esbuild/linux-arm@0.25.12", "", { "os": "linux", "cpu": "arm" }, "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw=="], - "vite/esbuild/@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.25.12", "", { "os": "linux", "cpu": "ia32" }, "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA=="], + "astro/vite/esbuild/@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.25.12", "", { "os": "linux", "cpu": "arm64" }, "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ=="], - "vite/esbuild/@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng=="], + "astro/vite/esbuild/@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.25.12", "", { "os": "linux", "cpu": "ia32" }, "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA=="], - "vite/esbuild/@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw=="], + "astro/vite/esbuild/@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng=="], - "vite/esbuild/@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.25.12", "", { "os": "linux", "cpu": "ppc64" }, "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA=="], + "astro/vite/esbuild/@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw=="], - "vite/esbuild/@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w=="], + "astro/vite/esbuild/@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.25.12", "", { "os": "linux", "cpu": "ppc64" }, "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA=="], - "vite/esbuild/@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.25.12", "", { "os": "linux", "cpu": "s390x" }, "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg=="], + "astro/vite/esbuild/@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w=="], - "vite/esbuild/@esbuild/linux-x64": ["@esbuild/linux-x64@0.25.12", "", { "os": "linux", "cpu": "x64" }, "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw=="], + "astro/vite/esbuild/@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.25.12", "", { "os": "linux", "cpu": "s390x" }, "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg=="], - "vite/esbuild/@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.25.12", "", { "os": "none", "cpu": "arm64" }, "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg=="], + "astro/vite/esbuild/@esbuild/linux-x64": ["@esbuild/linux-x64@0.25.12", "", { "os": "linux", "cpu": "x64" }, "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw=="], - "vite/esbuild/@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.25.12", "", { "os": "none", "cpu": "x64" }, "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ=="], + "astro/vite/esbuild/@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.25.12", "", { "os": "none", "cpu": "arm64" }, "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg=="], - "vite/esbuild/@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.25.12", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A=="], + "astro/vite/esbuild/@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.25.12", "", { "os": "none", "cpu": "x64" }, "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ=="], - "vite/esbuild/@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.25.12", "", { "os": "openbsd", "cpu": "x64" }, "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw=="], + "astro/vite/esbuild/@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.25.12", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A=="], - "vite/esbuild/@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.25.12", "", { "os": "none", "cpu": "arm64" }, "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg=="], + "astro/vite/esbuild/@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.25.12", "", { "os": "openbsd", "cpu": "x64" }, "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw=="], - "vite/esbuild/@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.25.12", "", { "os": "sunos", "cpu": "x64" }, "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w=="], + "astro/vite/esbuild/@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.25.12", "", { "os": "none", "cpu": "arm64" }, "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg=="], - "vite/esbuild/@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.25.12", "", { "os": "win32", "cpu": "arm64" }, "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg=="], + "astro/vite/esbuild/@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.25.12", "", { "os": "sunos", "cpu": "x64" }, "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w=="], - "vite/esbuild/@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.25.12", "", { "os": "win32", "cpu": "ia32" }, "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ=="], + "astro/vite/esbuild/@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.25.12", "", { "os": "win32", "cpu": "arm64" }, "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg=="], - "vite/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.25.12", "", { "os": "win32", "cpu": "x64" }, "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA=="], + "astro/vite/esbuild/@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.25.12", "", { "os": "win32", "cpu": "ia32" }, "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ=="], - "wrap-ansi/strip-ansi/ansi-regex": ["ansi-regex@6.2.2", "", {}, "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg=="], + "astro/vite/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.25.12", "", { "os": "win32", "cpu": "x64" }, "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA=="], } } diff --git a/examples/clickbench/README.md b/examples/clickbench/README.md index 1685981..6981750 100644 --- a/examples/clickbench/README.md +++ b/examples/clickbench/README.md @@ -1,6 +1,6 @@ # ClickBench CHKit example -This example creates the ClickBench `hits` schema and loads the full public ClickBench dataset from `datasets.clickhouse.com`. +This example creates the ClickBench `hits` schema and loads the full public ClickBench dataset from the ObsessionDB-hosted mirror at `fsn1.your-objectstorage.com/obsessiondb-datasets/clickbench/`. The data load migration truncates `default.hits` before inserting so an interrupted load can be retried by clearing the migration journal entry or using a fresh database. Run it against a disposable ClickHouse database. @@ -25,6 +25,6 @@ bun run migrate -- --service ## Migrations - `20260525133129_create_clickbench_schema.sql` creates the ClickBench `hits` table. -- `20260525133130_load_clickbench_data.sql` truncates `hits` and loads the full partitioned Parquet dataset from `https://datasets.clickhouse.com/hits_compatible/athena_partitioned/` via ClickHouse's `url()` table function. +- `20260525133130_load_clickbench_data.sql` truncates `hits` and loads the full partitioned Parquet dataset from `https://fsn1.your-objectstorage.com/obsessiondb-datasets/clickbench/` via ClickHouse's `s3()` table function. The benchmark query set is intentionally not included yet; this example focuses on schema creation and dataset loading. diff --git a/examples/clickbench/chkit/migrations/20260525133130_load_clickbench_data.sql b/examples/clickbench/chkit/migrations/20260525133130_load_clickbench_data.sql index 0afa209..9c68e04 100644 --- a/examples/clickbench/chkit/migrations/20260525133130_load_clickbench_data.sql +++ b/examples/clickbench/chkit/migrations/20260525133130_load_clickbench_data.sql @@ -11,9 +11,13 @@ TRUNCATE TABLE default.hits SETTINGS max_table_size_to_drop = 0, max_partition_size_to_drop = 0; -- Load the full ClickBench dataset (100 Parquet files, ~100M rows) in a single --- INSERT. We use the s3() table function against the public dataset bucket --- (datasets.clickhouse.com is a CloudFront alias for clickhouse-public-datasets) --- because s3() does native partitioned-Parquet parallelism that url() does not. +-- INSERT. We use the s3() table function against the ObsessionDB-hosted mirror +-- of the dataset (Hetzner Object Storage, FSN1 region) because s3() does +-- native partitioned-Parquet parallelism that url() does not. +-- +-- The executable TRUNCATE above clears the target before the first attempt. +-- The `before-retry:` line below repeats that compensation before any retry +-- where a prior async INSERT may have partially loaded rows. -- -- Tuning (measured against an ObsessionDB customer-benchmark instance): -- * max_download_threads = 64, max_insert_threads = 16 lands at ~178s for @@ -26,15 +30,16 @@ TRUNCATE TABLE default.hits SETTINGS max_table_size_to_drop = 0, max_partition_s -- * max_execution_time = 0 lifts the server-side query timer (the load is -- intentionally long-running). --- operation: load_table_data key=table:default.hits risk=caution +-- operation: load_table_data key=table:default.hits risk=caution mode=async +-- before-retry: TRUNCATE TABLE default.hits SETTINGS max_table_size_to_drop = 0, max_partition_size_to_drop = 0; INSERT INTO default.hits SELECT * FROM s3( - 'https://clickhouse-public-datasets.s3.amazonaws.com/hits_compatible/athena_partitioned/hits_{0..99}.parquet', + 'https://fsn1.your-objectstorage.com/obsessiondb-datasets/clickbench/hits_{0..99}.parquet', 'Parquet' ) SETTINGS max_execution_time = 0, max_memory_usage = 6500000000, - max_download_threads = 64, - max_insert_threads = 16; + max_download_threads = 2, + max_insert_threads = 2; diff --git a/examples/clickbench/package.json b/examples/clickbench/package.json index 3159487..2d40533 100644 --- a/examples/clickbench/package.json +++ b/examples/clickbench/package.json @@ -9,8 +9,8 @@ "check": "chkit check" }, "devDependencies": { - "@chkit/core": "0.1.0-beta.24", - "@chkit/plugin-obsessiondb": "0.1.0-beta.24", - "chkit": "0.1.0-beta.24" + "@chkit/core": "0.1.0-beta.26", + "@chkit/plugin-obsessiondb": "0.1.0-beta.26", + "chkit": "0.1.0-beta.26" } } diff --git a/examples/manifest.json b/examples/manifest.json new file mode 100644 index 0000000..19f5c63 --- /dev/null +++ b/examples/manifest.json @@ -0,0 +1,9 @@ +{ + "default": "clickbench", + "examples": [ + { + "name": "clickbench", + "description": "Full ClickBench schema and dataset load against ObsessionDB or ClickHouse." + } + ] +} diff --git a/fallow-plugin-chkit-conventions.jsonc b/fallow-plugin-chkit-conventions.jsonc index f0c217a..5a82d6a 100644 --- a/fallow-plugin-chkit-conventions.jsonc +++ b/fallow-plugin-chkit-conventions.jsonc @@ -10,6 +10,7 @@ ], "alwaysUsed": [ "apps/docs/functions/**/*.ts", + "apps/docs/src/components/AgentationDev.tsx", "apps/docs/src/styles/custom.css", "packages/cli/src/e2e-testkit.ts" ], diff --git a/package.json b/package.json index e6ce9cd..0207bda 100644 --- a/package.json +++ b/package.json @@ -13,7 +13,8 @@ "packageManager": "bun@1.3.5", "workspaces": [ "packages/*", - "apps/*" + "apps/*", + "examples/clickbench" ], "scripts": { "build": "turbo run build", @@ -33,6 +34,7 @@ "changeset": "changeset", "version-packages": "changeset version", "release": "bun run ./scripts/manual-release.ts", + "check:examples": "bun run scripts/check-examples-manifest.ts", "clean": "turbo run clean", "chkit": "bun run ./packages/cli/src/bin/chkit.ts", "deploy": "turbo run deploy" diff --git a/packages/cli/src/commands/migrate/apply.ts b/packages/cli/src/commands/migrate/apply.ts index fc1895a..a17fd62 100644 --- a/packages/cli/src/commands/migrate/apply.ts +++ b/packages/cli/src/commands/migrate/apply.ts @@ -12,6 +12,7 @@ import { extractExecutableStatements, extractMigrationOperationSummaries, } from '../../runtime/safety-markers.js' +import { applyAsyncStatement } from './async-apply.js' type JournalStore = ReturnType @@ -43,10 +44,28 @@ export async function applyMigration(input: { statements: parsedStatements, }) + const migrationChecksum = checksumSQL(sql) + for (let i = 0; i < statements.length; i++) { const statement = statements[i] as string - await db.command(statement) const operation = operationSummaries[i] + if (operation?.mode === 'async') { + await applyAsyncStatement({ + db, + journalStore, + sql: statement, + migrationName: file, + migrationChecksum, + statementIndex: i, + operationType: operation.type, + operationKey: operation.key, + beforeRetry: operation.beforeRetry, + log: (line) => console.log(line), + }) + // Async ops are DML (loads, backfills) — no DDL propagation to wait on. + continue + } + await db.command(statement) if (operation) { await waitForDDLPropagation(db, operation.type, operation.key) } @@ -55,7 +74,7 @@ export async function applyMigration(input: { const entry: MigrationJournalEntry = { name: file, appliedAt: new Date().toISOString().replace('Z', ''), - checksum: checksumSQL(sql), + checksum: migrationChecksum, } await journalStore.appendEntry(entry) diff --git a/packages/cli/src/commands/migrate/async-apply.ts b/packages/cli/src/commands/migrate/async-apply.ts new file mode 100644 index 0000000..1cbcde3 --- /dev/null +++ b/packages/cli/src/commands/migrate/async-apply.ts @@ -0,0 +1,386 @@ +import { createHash } from 'node:crypto' + +import type { ClickHouseExecutor, QueryStatus } from '@chkit/clickhouse' + +import { debug } from '../../runtime/debug.js' +import type { + JournalStore, + MigrationRowState, + OperationState, +} from '../../runtime/journal-store.js' + +const POLL_INTERVAL_MS = 5_000 + +export interface AsyncApplyInput { + db: ClickHouseExecutor + journalStore: JournalStore + sql: string + migrationName: string + migrationChecksum: string + statementIndex: number + operationType: string + operationKey: string + beforeRetry: string | null + log: (line: string) => void + pollIntervalMs?: number + sleep?: (ms: number) => Promise + now?: () => number +} + +export type AsyncApplyResult = + | { kind: 'completed'; operation: OperationState } + | { kind: 'skipped'; operation: OperationState } + +export async function applyAsyncStatement(input: AsyncApplyInput): Promise { + const { + db, + journalStore, + sql, + migrationName, + migrationChecksum, + statementIndex, + operationType, + operationKey, + beforeRetry, + log, + pollIntervalMs = POLL_INTERVAL_MS, + sleep = defaultSleep, + now = Date.now, + } = input + + const queryId = makeDeterministicQueryId(migrationName, statementIndex) + debug( + 'migrate:async', + `${migrationName}#${statementIndex} query_id=${queryId} type=${operationType}`, + ) + + const initialMigrationState = await journalStore.readMigrationState(migrationName) + if ( + initialMigrationState !== null && + !initialMigrationState.migrationCompleted && + initialMigrationState.checksum !== migrationChecksum + ) { + throw new Error( + `Migration ${migrationName} has in-progress async journal state for checksum ${initialMigrationState.checksum}, but the current file checksum is ${migrationChecksum}. Restore the original migration file or clear the in-progress journal state before retrying.`, + ) + } + const priorOpState = initialMigrationState?.operations.find( + (op) => op.operationIndex === statementIndex, + ) + + // 1. Already completed → skip entirely + if (priorOpState?.status === 'completed') { + log( + ` ${operationType}: query_id=${queryId} already completed in prior run — skipping`, + ) + return { kind: 'skipped', operation: priorOpState } + } + + // 2. Currently in flight on the server → attach + const inFlight = await db.queryStatus(queryId) + if (inFlight.status === 'running') { + log( + ` ${operationType}: query_id=${queryId} already running on server — attaching to in-flight query`, + ) + return await pollUntilTerminal({ + db, + journalStore, + migrationState: initialMigrationState, + migrationName, + migrationChecksum, + statementIndex, + operationType, + operationKey, + queryId, + pollAfterTime: '1970-01-01 00:00:00', + log, + pollIntervalMs, + sleep, + now, + // No submit promise — query was started by a prior chkit run that's + // now gone. We only observe its eventual completion. + submitPromise: null, + startedAt: priorOpState?.startedAt ?? isoWithoutZone(new Date(now())), + }) + } + + // 3. Prior in-progress row exists but query is not in system.processes → + // RETRY. Run before-retry compensation, then resubmit forward. + // 4. No prior row → FIRST attempt. Skip compensation, submit forward. + // (priorOpState.status === 'completed' was already returned above.) + if (priorOpState !== undefined) { + const errorTail = priorOpState.lastError + ? `: ${firstLine(priorOpState.lastError)}` + : '' + log( + ` ${operationType}: previous attempt of query_id=${queryId} is no longer running (status=${priorOpState.status}${errorTail}) — running before-retry then resubmitting`, + ) + if (beforeRetry !== null) { + log(` ${operationType}: running before-retry SQL`) + await db.command(beforeRetry) + } + } else { + log(` ${operationType}: submitting async (query_id=${queryId})`) + } + + const submitAfterTime = + priorOpState === undefined ? undefined : isoWithoutZone(new Date(now() - 60_000)) + const startedAt = isoWithoutZone(new Date(now())) + + // Persist the "started" intent BEFORE submitting, so a crash between here + // and the next event still leaves chkit able to detect "this op was tried." + await journalStore.writeMigrationState( + upsertOperation( + initialMigrationState ?? freshMigrationState(migrationName, migrationChecksum), + { + operationIndex: statementIndex, + operationKey, + operationType, + queryId, + status: 'started', + startedAt, + finishedAt: null, + lastError: '', + }, + now, + ), + ) + + // Re-read so we have the row's actual stored applied_at (matters for + // subsequent ReplacingMergeTree writes during polling). + const stateAfterStart = await journalStore.readMigrationState(migrationName) + + const submitPromise = db.submit(sql, queryId) + + return await pollUntilTerminal({ + db, + journalStore, + migrationState: stateAfterStart, + migrationName, + migrationChecksum, + statementIndex, + operationType, + operationKey, + queryId, + pollAfterTime: submitAfterTime, + log, + pollIntervalMs, + sleep, + now, + submitPromise, + startedAt, + }) +} + +interface PollUntilTerminalInput { + db: ClickHouseExecutor + journalStore: JournalStore + migrationState: MigrationRowState | null + migrationName: string + migrationChecksum: string + statementIndex: number + operationType: string + operationKey: string + queryId: string + pollAfterTime: string | undefined + log: (line: string) => void + pollIntervalMs: number + sleep: (ms: number) => Promise + now: () => number + submitPromise: Promise | null + startedAt: string +} + +async function pollUntilTerminal(input: PollUntilTerminalInput): Promise { + const { + db, + journalStore, + migrationState, + migrationName, + migrationChecksum, + statementIndex, + operationType, + operationKey, + queryId, + pollAfterTime, + log, + pollIntervalMs, + sleep, + now, + submitPromise, + startedAt, + } = input + + let submitError: Error | null = null + // Capture the submit promise rejection so it doesn't surface as an + // unhandled rejection. Polling is the source of truth for outcome. + const submitPromiseGuarded = + submitPromise === null + ? null + : submitPromise.catch((error: unknown) => { + submitError = error instanceof Error ? error : new Error(String(error)) + }) + + const pollStartedAt = now() + try { + for (;;) { + await sleep(pollIntervalMs) + const status = await db.queryStatus( + queryId, + pollAfterTime === undefined ? undefined : { afterTime: pollAfterTime }, + ) + const elapsedSec = Math.floor((now() - pollStartedAt) / 1000) + + if (status.status === 'finished') { + const finishedSec = Math.round((status.durationMs ?? 0) / 1000) + const finishedOp: OperationState = { + operationIndex: statementIndex, + operationKey, + operationType, + queryId, + status: 'completed', + startedAt, + finishedAt: isoWithoutZone(new Date(now())), + lastError: '', + } + const baseState = + migrationState ?? freshMigrationState(migrationName, migrationChecksum) + await journalStore.writeMigrationState( + upsertOperation(baseState, finishedOp, now), + ) + log( + ` ${operationType}: finished — written=${formatRows(status.writtenRows)} (${formatBytes(status.writtenBytes)}) in ${finishedSec}s`, + ) + return { kind: 'completed', operation: finishedOp } + } + + if (status.status === 'failed') { + const failedOp: OperationState = { + operationIndex: statementIndex, + operationKey, + operationType, + queryId, + status: 'failed', + startedAt, + finishedAt: isoWithoutZone(new Date(now())), + lastError: status.error ?? '', + } + const baseState = + migrationState ?? freshMigrationState(migrationName, migrationChecksum) + await journalStore.writeMigrationState( + upsertOperation(baseState, failedOp, now), + ) + throw new Error( + `Async migration step ${operationType} failed (query_id ${queryId}): ${status.error ?? ''}`, + ) + } + + if (status.status === 'running') { + log(progressLine(operationType, status, elapsedSec)) + continue + } + + // status === 'unknown' + // If submit has already rejected, the query never made it server-side + // (e.g. SQL parse error) — surface that error. Otherwise it's a + // transient gap (just-submitted or just-finished); loop. + if (submitError) { + throw submitError + } + log( + ` ${operationType}: status unknown — still polling (elapsed ${elapsedSec}s)`, + ) + } + } finally { + if (submitPromiseGuarded) { + await submitPromiseGuarded.catch(() => {}) + } + } +} + +function upsertOperation( + state: MigrationRowState, + op: OperationState, + now: () => number, +): MigrationRowState { + const others = state.operations.filter( + (existing) => existing.operationIndex !== op.operationIndex, + ) + const operations = [...others, op].sort( + (a, b) => a.operationIndex - b.operationIndex, + ) + return { + ...state, + appliedAt: isoWithoutZone(new Date(now())), + operations, + // migrationCompleted stays false until applyMigration explicitly flips it + migrationCompleted: state.migrationCompleted, + } +} + +function freshMigrationState( + migrationName: string, + checksum: string, +): MigrationRowState { + return { + name: migrationName, + appliedAt: '1970-01-01 00:00:00.000', + checksum, + chkitVersion: '', + migrationCompleted: false, + operations: [], + } +} + +export function makeDeterministicQueryId( + migrationName: string, + statementIndex: number, +): string { + const hex = createHash('sha256') + .update(`chkit:${migrationName}:${statementIndex}`) + .digest('hex') + return [ + hex.slice(0, 8), + hex.slice(8, 12), + hex.slice(12, 16), + hex.slice(16, 20), + hex.slice(20, 32), + ].join('-') +} + +function progressLine( + operationLabel: string, + status: QueryStatus, + elapsedSec: number, +): string { + const rows = formatRows(status.writtenRows) + const bytes = formatBytes(status.writtenBytes) + return ` ${operationLabel}: written=${rows} (${bytes}), elapsed ${elapsedSec}s` +} + +function formatRows(value: number | undefined): string { + if (value === undefined || value === 0) return '0 rows' + if (value >= 1_000_000) return `${(value / 1_000_000).toFixed(2)}M rows` + if (value >= 1_000) return `${(value / 1_000).toFixed(1)}K rows` + return `${value} rows` +} + +function formatBytes(value: number | undefined): string { + if (value === undefined || value === 0) return '0 B' + if (value >= 1024 ** 3) return `${(value / 1024 ** 3).toFixed(2)} GiB` + if (value >= 1024 ** 2) return `${(value / 1024 ** 2).toFixed(1)} MiB` + if (value >= 1024) return `${(value / 1024).toFixed(1)} KiB` + return `${value} B` +} + +function isoWithoutZone(date: Date): string { + return date.toISOString().replace('Z', '') +} + +function firstLine(value: string): string { + return value.split('\n')[0] ?? value +} + +function defaultSleep(ms: number): Promise { + return new Promise((resolve) => setTimeout(resolve, ms)) +} diff --git a/packages/cli/src/runtime/journal-store.ts b/packages/cli/src/runtime/journal-store.ts index 56ec054..bf13271 100644 --- a/packages/cli/src/runtime/journal-store.ts +++ b/packages/cli/src/runtime/journal-store.ts @@ -4,8 +4,32 @@ import type { MigrationJournal, MigrationJournalEntry } from './migration-store. import { CLI_VERSION } from './version.js' import { debug } from './debug.js' -interface JournalStore { +export type OperationStatus = 'started' | 'completed' | 'failed' + +export interface OperationState { + operationIndex: number + operationKey: string + operationType: string + queryId: string + status: OperationStatus + startedAt: string + finishedAt: string | null + lastError: string +} + +export interface MigrationRowState { + name: string + appliedAt: string + checksum: string + chkitVersion: string + migrationCompleted: boolean + operations: OperationState[] +} + +export interface JournalStore { readJournal(): Promise + readMigrationState(migrationName: string): Promise + writeMigrationState(state: MigrationRowState): Promise appendEntry(entry: MigrationJournalEntry): Promise readonly databaseMissing: boolean } @@ -28,14 +52,68 @@ interface MigrationRow extends Record { applied_at: string checksum: string chkit_version: string + migration_completed?: boolean | number + // ClickHouse JSONEachRow returns named-tuple columns as objects keyed by the + // tuple field names, not as positional arrays. + operations?: OperationTupleRow[] } +interface OperationTupleRow { + operation_index: number | string + operation_key: string + operation_type: string + query_id: string + status: string + started_at: string + finished_at: string | null + last_error: string +} + +const OPERATIONS_TUPLE_TYPE = + 'Array(Tuple(operation_index Int32, operation_key String, operation_type String, query_id String, status LowCardinality(String), started_at DateTime64(3, \'UTC\'), finished_at Nullable(DateTime64(3, \'UTC\')), last_error String))' + function isRetryableInsertRace(error: unknown): boolean { if (!(error instanceof Error)) return false const message = error.message return message.includes('INSERT race condition') || message.includes('Please retry the INSERT') } +function escapeSqlString(value: string): string { + return value.replace(/\\/g, '\\\\').replace(/'/g, "\\'") +} + +function operationToTupleLiteral(op: OperationState): string { + const parts = [ + String(op.operationIndex), + `'${escapeSqlString(op.operationKey)}'`, + `'${escapeSqlString(op.operationType)}'`, + `'${escapeSqlString(op.queryId)}'`, + `'${escapeSqlString(op.status)}'`, + `'${escapeSqlString(op.startedAt)}'`, + op.finishedAt === null ? 'NULL' : `'${escapeSqlString(op.finishedAt)}'`, + `'${escapeSqlString(op.lastError)}'`, + ] + return `(${parts.join(',')})` +} + +function operationsArrayLiteral(operations: OperationState[]): string { + if (operations.length === 0) return '[]' + return `[${operations.map(operationToTupleLiteral).join(',')}]` +} + +function operationFromTuple(row: OperationTupleRow): OperationState { + return { + operationIndex: Number(row.operation_index), + operationKey: row.operation_key, + operationType: row.operation_type, + queryId: row.query_id, + status: row.status as OperationStatus, + startedAt: row.started_at, + finishedAt: row.finished_at, + lastError: row.last_error, + } +} + export function createJournalStore(db: ClickHouseExecutor): JournalStore { const journalTable = resolveJournalTableName() debug('journal', `journal table: ${journalTable}${process.env.CHKIT_JOURNAL_TABLE ? ' (from CHKIT_JOURNAL_TABLE)' : ''}`) @@ -43,10 +121,13 @@ export function createJournalStore(db: ClickHouseExecutor): JournalStore { name String, applied_at DateTime64(3, 'UTC'), checksum String, - chkit_version String + chkit_version String, + migration_completed Bool DEFAULT true, + operations ${OPERATIONS_TUPLE_TYPE} DEFAULT [] ) ENGINE = ReplacingMergeTree(applied_at) ORDER BY (name) SETTINGS index_granularity = 1` + let bootstrapped = false let _databaseMissing = false @@ -55,7 +136,8 @@ SETTINGS index_granularity = 1` debug('journal', `probing journal table "${journalTable}"`) try { await db.query(`SELECT name FROM ${journalTable} LIMIT 0`) - debug('journal', 'journal table exists') + debug('journal', 'journal table exists — checking schema') + await ensureSchemaUpgraded() bootstrapped = true return } catch (error) { @@ -90,10 +172,32 @@ SETTINGS index_granularity = 1` bootstrapped = true } + async function ensureSchemaUpgraded(): Promise { + // Pre-existing journal tables predate per-operation tracking. Add the + // columns idempotently so older deployments pick them up on first run + // of the new chkit. ALTER ADD COLUMN IF NOT EXISTS is a metadata op, + // no data rewrite. + await db.command( + `ALTER TABLE ${journalTable} ADD COLUMN IF NOT EXISTS migration_completed Bool DEFAULT true`, + ) + await db.command( + `ALTER TABLE ${journalTable} ADD COLUMN IF NOT EXISTS operations ${OPERATIONS_TUPLE_TYPE} DEFAULT []`, + ) + } + + async function trySyncReplica(): Promise { + try { + await db.command(`SYSTEM SYNC REPLICA ${journalTable}`) + } catch { + // Non-replicated or single-node setups don't support SYSTEM SYNC REPLICA. + } + } + return { get databaseMissing() { return _databaseMissing }, + async readJournal(): Promise { debug('journal', 'reading journal') await ensureTable() @@ -101,13 +205,9 @@ SETTINGS index_granularity = 1` debug('journal', 'database missing — returning empty journal') return { version: 1, applied: [] } } - try { - await db.command(`SYSTEM SYNC REPLICA ${journalTable}`) - } catch { - // Non-replicated or single-node setups don't support SYSTEM SYNC REPLICA. - } + await trySyncReplica() const rows = await db.query( - `SELECT name, applied_at, checksum, chkit_version FROM ${journalTable} ORDER BY name SETTINGS select_sequential_consistency = 1` + `SELECT name, applied_at, checksum, chkit_version FROM ${journalTable} FINAL WHERE migration_completed = true ORDER BY name SETTINGS select_sequential_consistency = 1`, ) debug('journal', `journal has ${rows.length} applied entries`) return { @@ -120,16 +220,33 @@ SETTINGS index_granularity = 1` } }, - async appendEntry(entry: MigrationJournalEntry): Promise { - debug('journal', `appending entry: ${entry.name} (checksum: ${entry.checksum})`) + async readMigrationState(migrationName: string): Promise { + await ensureTable() + if (_databaseMissing) return null + await trySyncReplica() + const rows = await db.query( + `SELECT name, applied_at, checksum, chkit_version, migration_completed, operations FROM ${journalTable} FINAL WHERE name = '${escapeSqlString(migrationName)}' LIMIT 1 SETTINGS select_sequential_consistency = 1`, + ) + const row = rows[0] + if (!row) return null + return { + name: row.name, + appliedAt: row.applied_at, + checksum: row.checksum, + chkitVersion: row.chkit_version, + migrationCompleted: Boolean(row.migration_completed), + operations: (row.operations ?? []).map(operationFromTuple), + } + }, + + async writeMigrationState(state: MigrationRowState): Promise { if (_databaseMissing) { debug('journal', 'resetting databaseMissing flag — migration may have created the database') _databaseMissing = false bootstrapped = false } await ensureTable() - const esc = (s: string) => s.replace(/\\/g, '\\\\').replace(/'/g, "\\'") - const insertSql = `INSERT INTO ${journalTable} (name, applied_at, checksum, chkit_version) VALUES ('${esc(entry.name)}', '${esc(entry.appliedAt)}', '${esc(entry.checksum)}', '${esc(CLI_VERSION)}')` + const insertSql = `INSERT INTO ${journalTable} (name, applied_at, checksum, chkit_version, migration_completed, operations) VALUES ('${escapeSqlString(state.name)}', '${escapeSqlString(state.appliedAt)}', '${escapeSqlString(state.checksum)}', '${escapeSqlString(state.chkitVersion || CLI_VERSION)}', ${state.migrationCompleted ? 'true' : 'false'}, ${operationsArrayLiteral(state.operations)})` const maxAttempts = 5 for (let attempt = 1; attempt <= maxAttempts; attempt += 1) { try { @@ -143,11 +260,22 @@ SETTINGS index_granularity = 1` await new Promise((r) => setTimeout(r, attempt * 150)) } } - try { - await db.command(`SYSTEM SYNC REPLICA ${journalTable}`) - } catch { - // Non-replicated or single-node setups don't support SYSTEM SYNC REPLICA. - } + await trySyncReplica() + }, + + async appendEntry(entry: MigrationJournalEntry): Promise { + debug('journal', `appending entry: ${entry.name} (checksum: ${entry.checksum})`) + // Preserve any per-operation history that async statements wrote + // earlier in this migration's apply — we just flip migrationCompleted. + const existing = await this.readMigrationState(entry.name) + await this.writeMigrationState({ + name: entry.name, + appliedAt: entry.appliedAt, + checksum: entry.checksum, + chkitVersion: CLI_VERSION, + migrationCompleted: true, + operations: existing?.operations ?? [], + }) }, } } diff --git a/packages/cli/src/runtime/safety-markers.ts b/packages/cli/src/runtime/safety-markers.ts index 1f498c5..32bd5b1 100644 --- a/packages/cli/src/runtime/safety-markers.ts +++ b/packages/cli/src/runtime/safety-markers.ts @@ -14,10 +14,21 @@ export interface DestructiveOperationMarker { summary: string } -interface MigrationOperationSummary { +export type MigrationOperationMode = 'sync' | 'async' + +export interface MigrationOperationSummary { type: string key: string risk: string + mode: MigrationOperationMode + /** + * SQL to run BEFORE re-attempting this operation, parsed from a + * `-- before-retry: ` header line that follows the `-- operation:` + * line. Empty / null on first attempt; runs on every retry. Compensates + * for partial side effects left behind by a failed prior attempt + * (e.g. TRUNCATE a partially-loaded table before re-INSERTing). + */ + beforeRetry: string | null summary: string } @@ -33,25 +44,58 @@ function extractDestructiveOperationSummaries(sql: string): string[] { .map((line) => line.replace(/^-- operation:\s*/, '')) } -function parseOperationLine(summary: string): MigrationOperationSummary | null { - const match = summary.match(/^([a-z_]+)\s+key=([^\s]+)\s+risk=([a-z_]+)$/) +function parseOperationLine( + summary: string, + beforeRetry: string | null, +): MigrationOperationSummary | null { + const match = summary.match( + /^([a-z_]+)\s+key=([^\s]+)\s+risk=([a-z_]+)(?:\s+mode=([a-z_]+))?$/, + ) if (!match) return null + const rawMode = match[4] + const mode: MigrationOperationMode = rawMode === 'async' ? 'async' : 'sync' return { type: match[1] ?? 'unknown', key: match[2] ?? 'unknown', risk: match[3] ?? 'unknown', + mode, + beforeRetry, summary, } } +const BEFORE_RETRY_PREFIX = '-- before-retry:' + +function stripTrailingSemicolon(value: string): string { + return value.replace(/;\s*$/, '').trim() +} + export function extractMigrationOperationSummaries(sql: string): MigrationOperationSummary[] { - return sql - .split('\n') - .map((line) => line.trim()) - .filter((line) => line.startsWith('-- operation:')) - .map((line) => line.replace(/^-- operation:\s*/, '')) - .map((summary) => parseOperationLine(summary)) - .filter((item): item is MigrationOperationSummary => item !== null) + const lines = sql.split('\n').map((line) => line.trim()) + const summaries: MigrationOperationSummary[] = [] + for (let i = 0; i < lines.length; i++) { + const line = lines[i] + if (!line?.startsWith('-- operation:')) continue + const summary = line.replace(/^-- operation:\s*/, '') + // A `-- before-retry: ` line is associated with the operation if it + // appears in the comment block immediately following the operation line + // (allowing blank/comment lines in between, but not executable SQL). + let beforeRetry: string | null = null + for (let j = i + 1; j < lines.length; j++) { + const next = lines[j] + if (next === undefined) break + if (next === '') continue + if (!next.startsWith('--')) break + if (next.startsWith(BEFORE_RETRY_PREFIX)) { + beforeRetry = stripTrailingSemicolon(next.slice(BEFORE_RETRY_PREFIX.length).trim()) + if (beforeRetry === '') beforeRetry = null + break + } + } + const parsed = parseOperationLine(summary, beforeRetry) + if (parsed !== null) summaries.push(parsed) + } + return summaries } function describeDestructiveOperation(type: string): { @@ -98,7 +142,7 @@ export function collectDestructiveOperationMarkers( sql: string ): DestructiveOperationMarker[] { return extractDestructiveOperationSummaries(sql).map((summary) => { - const parsed = parseOperationLine(summary) + const parsed = parseOperationLine(summary, null) const type = parsed?.type ?? 'unknown' const key = parsed?.key ?? 'unknown' const risk = parsed?.risk ?? 'danger' diff --git a/packages/cli/src/test/commands/migrate/async-apply.test.ts b/packages/cli/src/test/commands/migrate/async-apply.test.ts new file mode 100644 index 0000000..6cd3c3e --- /dev/null +++ b/packages/cli/src/test/commands/migrate/async-apply.test.ts @@ -0,0 +1,383 @@ +import { describe, expect, test } from 'bun:test' + +import type { ClickHouseExecutor, QueryStatus } from '@chkit/clickhouse' + +import { + applyAsyncStatement, + makeDeterministicQueryId, +} from '../../../commands/migrate/async-apply.js' +import type { + JournalStore, + MigrationRowState, + OperationState, +} from '../../../runtime/journal-store.js' + +type StatusCall = { queryId: string; afterTime?: string } +type SubmitCall = { sql: string; queryId?: string } +type CommandCall = { sql: string } + +interface FakeExecutor { + db: ClickHouseExecutor + statusCalls: StatusCall[] + submitCalls: SubmitCall[] + commandCalls: CommandCall[] +} + +function createFakeExecutor( + statuses: QueryStatus[], + options: { failSubmit?: () => Error } = {}, +): FakeExecutor { + const statusCalls: StatusCall[] = [] + const submitCalls: SubmitCall[] = [] + const commandCalls: CommandCall[] = [] + const queue = [...statuses] + const db = { + async submit(sql: string, queryId?: string) { + submitCalls.push({ sql, queryId }) + if (options.failSubmit) throw options.failSubmit() + return queryId ?? 'fallback-id' + }, + async queryStatus(queryId: string, opts?: { afterTime?: string }) { + statusCalls.push({ queryId, afterTime: opts?.afterTime }) + const next = queue.shift() + if (!next) { + throw new Error('queryStatus called more times than fake has answers for') + } + return next + }, + async command(sql: string) { + commandCalls.push({ sql }) + }, + } as unknown as ClickHouseExecutor + return { db, statusCalls, submitCalls, commandCalls } +} + +interface FakeStore { + store: JournalStore + writes: MigrationRowState[] +} + +function createFakeJournalStore(initial: MigrationRowState | null = null): FakeStore { + let current: MigrationRowState | null = initial + const writes: MigrationRowState[] = [] + const store: JournalStore = { + databaseMissing: false, + async readJournal() { + return { version: 1, applied: [] } + }, + async readMigrationState() { + return current + }, + async writeMigrationState(state) { + writes.push(state) + current = state + }, + async appendEntry() { + // not used in these tests + }, + } + return { store, writes } +} + +const NO_SLEEP = (_ms: number) => Promise.resolve() +const FIXED_NOW = () => 1_700_000_000_000 + +const BASE_INPUT = { + sql: 'INSERT INTO t SELECT 1', + migrationName: 'm.sql', + migrationChecksum: 'deadbeef', + statementIndex: 0, + operationType: 'load_table_data', + operationKey: 'table:t', + beforeRetry: null, +} as const + +function freshStateWith(op: OperationState): MigrationRowState { + return { + name: BASE_INPUT.migrationName, + appliedAt: '1970-01-01 00:00:00.000', + checksum: BASE_INPUT.migrationChecksum, + chkitVersion: '', + migrationCompleted: false, + operations: [op], + } +} + +describe('applyAsyncStatement', () => { + test('produces a deterministic UUID-shaped query_id from (migration, statement_index)', () => { + const a = makeDeterministicQueryId('20260526_load.sql', 0) + const b = makeDeterministicQueryId('20260526_load.sql', 0) + const c = makeDeterministicQueryId('20260526_load.sql', 1) + expect(a).toBe(b) + expect(a).not.toBe(c) + expect(a).toMatch(/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/) + }) + + test('first attempt: writes started, submits, polls to completion', async () => { + const queryId = makeDeterministicQueryId(BASE_INPUT.migrationName, BASE_INPUT.statementIndex) + const { db, statusCalls, submitCalls, commandCalls } = createFakeExecutor([ + { status: 'unknown' }, // initial in-flight check + { status: 'running', writtenRows: 50_000 }, + { status: 'finished', writtenRows: 100_000, durationMs: 8000 }, + ]) + const { store, writes } = createFakeJournalStore(null) + const lines: string[] = [] + + const result = await applyAsyncStatement({ + ...BASE_INPUT, + db, + journalStore: store, + log: (line) => lines.push(line), + sleep: NO_SLEEP, + now: FIXED_NOW, + }) + + expect(result.kind).toBe('completed') + expect(submitCalls).toHaveLength(1) + expect(submitCalls[0]?.queryId).toBe(queryId) + expect(commandCalls).toEqual([]) // no before-retry on first attempt + expect(statusCalls).toHaveLength(3) + expect(statusCalls.every((call) => call.afterTime === undefined)).toBe(true) + // Two writes: started + completed. + expect(writes).toHaveLength(2) + expect(writes[0]?.operations[0]?.status).toBe('started') + expect(writes[1]?.operations[0]?.status).toBe('completed') + expect(lines.some((line) => line.includes('submitting async'))).toBe(true) + }) + + test('refuses to resume in-progress async state when the migration checksum changed', async () => { + const queryId = makeDeterministicQueryId(BASE_INPUT.migrationName, BASE_INPUT.statementIndex) + const { db, statusCalls, submitCalls, commandCalls } = createFakeExecutor([]) + const { store, writes } = createFakeJournalStore( + freshStateWith({ + operationIndex: 0, + operationKey: BASE_INPUT.operationKey, + operationType: BASE_INPUT.operationType, + queryId, + status: 'started', + startedAt: '2026-05-26 12:00:00.000', + finishedAt: null, + lastError: '', + }), + ) + + await expect( + applyAsyncStatement({ + ...BASE_INPUT, + migrationChecksum: 'changed-checksum', + beforeRetry: 'TRUNCATE TABLE t', + db, + journalStore: store, + log: () => {}, + sleep: NO_SLEEP, + now: FIXED_NOW, + }), + ).rejects.toThrow(/in-progress async journal state/) + + expect(statusCalls).toEqual([]) + expect(submitCalls).toEqual([]) + expect(commandCalls).toEqual([]) + expect(writes).toEqual([]) + }) + + test('completed-skip: prior operation marked completed → no submit, no command', async () => { + const queryId = makeDeterministicQueryId(BASE_INPUT.migrationName, BASE_INPUT.statementIndex) + const { db, statusCalls, submitCalls, commandCalls } = createFakeExecutor([]) + const { store, writes } = createFakeJournalStore( + freshStateWith({ + operationIndex: 0, + operationKey: BASE_INPUT.operationKey, + operationType: BASE_INPUT.operationType, + queryId, + status: 'completed', + startedAt: '2026-05-26 12:00:00.000', + finishedAt: '2026-05-26 12:01:00.000', + lastError: '', + }), + ) + const lines: string[] = [] + + const result = await applyAsyncStatement({ + ...BASE_INPUT, + db, + journalStore: store, + log: (line) => lines.push(line), + sleep: NO_SLEEP, + now: FIXED_NOW, + }) + + expect(result.kind).toBe('skipped') + expect(submitCalls).toEqual([]) + expect(statusCalls).toEqual([]) // didn't even consult system.processes + expect(commandCalls).toEqual([]) + expect(writes).toEqual([]) // nothing changed + expect(lines.some((line) => line.includes('already completed'))).toBe(true) + }) + + test('in-flight attach: query already running on server → poll without resubmit', async () => { + const queryId = makeDeterministicQueryId(BASE_INPUT.migrationName, BASE_INPUT.statementIndex) + const { db, statusCalls, submitCalls, commandCalls } = createFakeExecutor([ + { status: 'running', writtenRows: 50 }, // initial check sees it running + { status: 'running', writtenRows: 75 }, + { status: 'finished', writtenRows: 100, durationMs: 5000 }, + ]) + const { store, writes } = createFakeJournalStore( + freshStateWith({ + operationIndex: 0, + operationKey: BASE_INPUT.operationKey, + operationType: BASE_INPUT.operationType, + queryId, + status: 'started', + startedAt: '2026-05-26 11:00:00.000', + finishedAt: null, + lastError: '', + }), + ) + const lines: string[] = [] + + const result = await applyAsyncStatement({ + ...BASE_INPUT, + beforeRetry: 'TRUNCATE TABLE t', + db, + journalStore: store, + log: (line) => lines.push(line), + sleep: NO_SLEEP, + now: FIXED_NOW, + }) + + expect(result.kind).toBe('completed') + expect(submitCalls).toEqual([]) // never resubmitted + expect(commandCalls).toEqual([]) // before-retry NOT run on attach + expect(statusCalls).toHaveLength(3) + expect(lines.some((line) => line.includes('attaching'))).toBe(true) + expect(writes).toHaveLength(1) + expect(writes[0]?.operations[0]?.status).toBe('completed') + }) + + test('retry: prior failed op + query no longer running → run before-retry, then resubmit', async () => { + const queryId = makeDeterministicQueryId(BASE_INPUT.migrationName, BASE_INPUT.statementIndex) + const { db, statusCalls, submitCalls, commandCalls } = createFakeExecutor([ + { status: 'unknown' }, // initial check: not running on server anymore + { status: 'running', writtenRows: 25_000 }, + { status: 'finished', writtenRows: 100_000, durationMs: 3000 }, + ]) + const { store, writes } = createFakeJournalStore( + freshStateWith({ + operationIndex: 0, + operationKey: BASE_INPUT.operationKey, + operationType: BASE_INPUT.operationType, + queryId, + status: 'failed', + startedAt: '2026-05-26 10:00:00.000', + finishedAt: '2026-05-26 10:05:00.000', + lastError: 'Memory limit exceeded', + }), + ) + const lines: string[] = [] + + const result = await applyAsyncStatement({ + ...BASE_INPUT, + beforeRetry: 'TRUNCATE TABLE t SETTINGS max_table_size_to_drop = 0', + db, + journalStore: store, + log: (line) => lines.push(line), + sleep: NO_SLEEP, + now: FIXED_NOW, + }) + + expect(result.kind).toBe('completed') + expect(commandCalls).toEqual([ + { sql: 'TRUNCATE TABLE t SETTINGS max_table_size_to_drop = 0' }, + ]) + expect(submitCalls).toHaveLength(1) + expect(submitCalls[0]?.queryId).toBe(queryId) + expect(statusCalls).toHaveLength(3) + expect(statusCalls[1]?.afterTime).toBe('2023-11-14T22:12:20.000') + expect(statusCalls[2]?.afterTime).toBe('2023-11-14T22:12:20.000') + expect(lines.some((line) => line.includes('running before-retry SQL'))).toBe(true) + expect(lines.some((line) => line.includes('Memory limit exceeded'))).toBe(true) + // started (overwrite prior failed) + completed + expect(writes).toHaveLength(2) + expect(writes[0]?.operations[0]?.status).toBe('started') + expect(writes[1]?.operations[0]?.status).toBe('completed') + }) + + test('retry without before-retry SQL: still resubmits forward', async () => { + const queryId = makeDeterministicQueryId(BASE_INPUT.migrationName, BASE_INPUT.statementIndex) + const { db, submitCalls, commandCalls } = createFakeExecutor([ + { status: 'unknown' }, + { status: 'finished', writtenRows: 1, durationMs: 100 }, + ]) + const { store } = createFakeJournalStore( + freshStateWith({ + operationIndex: 0, + operationKey: BASE_INPUT.operationKey, + operationType: BASE_INPUT.operationType, + queryId, + status: 'failed', + startedAt: '2026-05-26 10:00:00.000', + finishedAt: '2026-05-26 10:05:00.000', + lastError: 'NETWORK_ERROR', + }), + ) + + await applyAsyncStatement({ + ...BASE_INPUT, + beforeRetry: null, + db, + journalStore: store, + log: () => {}, + sleep: NO_SLEEP, + now: FIXED_NOW, + }) + + expect(commandCalls).toEqual([]) // no before-retry SQL → no command + expect(submitCalls).toHaveLength(1) + }) + + test('polling-failure: query transitions to failed → write failed state and throw', async () => { + const { db } = createFakeExecutor([ + { status: 'unknown' }, + { status: 'running' }, + { status: 'failed', error: 'NETWORK_ERROR: broken pipe' }, + ]) + const { store, writes } = createFakeJournalStore(null) + + await expect( + applyAsyncStatement({ + ...BASE_INPUT, + db, + journalStore: store, + log: () => {}, + sleep: NO_SLEEP, + now: FIXED_NOW, + }), + ).rejects.toThrow(/NETWORK_ERROR: broken pipe/) + + // started + failed + expect(writes).toHaveLength(2) + expect(writes[0]?.operations[0]?.status).toBe('started') + expect(writes[1]?.operations[0]?.status).toBe('failed') + expect(writes[1]?.operations[0]?.lastError).toContain('NETWORK_ERROR: broken pipe') + }) + + test('surfaces submit error when status remains unknown (SQL parse failure case)', async () => { + const { db } = createFakeExecutor( + [{ status: 'unknown' }, { status: 'unknown' }, { status: 'unknown' }], + { failSubmit: () => new Error('Syntax error: failed at position 1') }, + ) + const { store } = createFakeJournalStore(null) + + await expect( + applyAsyncStatement({ + ...BASE_INPUT, + sql: 'NOT VALID SQL', + db, + journalStore: store, + log: () => {}, + sleep: NO_SLEEP, + now: FIXED_NOW, + }), + ).rejects.toThrow(/Syntax error/) + }) +}) diff --git a/packages/cli/src/test/runtime/journal-store.test.ts b/packages/cli/src/test/runtime/journal-store.test.ts new file mode 100644 index 0000000..43c3692 --- /dev/null +++ b/packages/cli/src/test/runtime/journal-store.test.ts @@ -0,0 +1,211 @@ +import { describe, expect, test } from 'bun:test' + +import type { ClickHouseExecutor } from '@chkit/clickhouse' + +import { createJournalStore } from '../../runtime/journal-store.js' + +interface ScriptedExecutor { + db: ClickHouseExecutor + commandCalls: string[] +} + +function createScriptedExecutor( + queryAnswers: Map, +): ScriptedExecutor { + const commandCalls: string[] = [] + const db = { + async command(sql: string) { + commandCalls.push(sql) + }, + async query(sql: string): Promise { + for (const [key, value] of queryAnswers) { + if (key instanceof RegExp) { + if (key.test(sql)) return value as T[] + } else if (sql.includes(key)) { + return value as T[] + } + } + return [] + }, + async queryStatus() { + throw new Error('queryStatus not implemented in this fake') + }, + async submit() { + throw new Error('submit not implemented in this fake') + }, + async insert() { + throw new Error('insert not implemented in this fake') + }, + } as unknown as ClickHouseExecutor + return { db, commandCalls } +} + +describe('createJournalStore', () => { + test('readMigrationState parses named-tuple operations returned as objects (JSONEachRow shape)', async () => { + // Regression: ClickHouse's JSONEachRow format returns named Tuple columns + // as objects keyed by tuple field names, NOT positional arrays. An earlier + // implementation read by index (`tuple[0]`) and silently produced + // `operationIndex: NaN`, which made retry detection fail and corrupted + // subsequent writes. Make sure named-tuple object-shape is parsed by name. + const { db } = createScriptedExecutor( + new Map([ + // ensureTable probe: pretend the table exists with operations column already. + [/SELECT name FROM .* LIMIT 0/, []], + // The actual SELECT for readMigrationState — return JSONEachRow shape: + [ + /FROM .* FINAL WHERE name = /, + [ + { + name: 'm.sql', + applied_at: '2026-05-26 12:00:00.000', + checksum: 'deadbeef', + chkit_version: '0.1.0-test', + migration_completed: false, + operations: [ + { + operation_index: 0, + operation_key: 'table:default.hits', + operation_type: 'load_table_data', + query_id: '17977426-2184-de85-8142-3b6b04a1fded', + status: 'started', + started_at: '2026-05-26 12:00:00.000', + finished_at: null, + last_error: '', + }, + ], + }, + ], + ], + ]), + ) + + const store = createJournalStore(db) + const state = await store.readMigrationState('m.sql') + + expect(state).not.toBeNull() + expect(state?.name).toBe('m.sql') + expect(state?.migrationCompleted).toBe(false) + expect(state?.operations).toHaveLength(1) + const op = state?.operations[0] + expect(op?.operationIndex).toBe(0) + expect(Number.isNaN(op?.operationIndex)).toBe(false) + expect(op?.operationKey).toBe('table:default.hits') + expect(op?.operationType).toBe('load_table_data') + expect(op?.queryId).toBe('17977426-2184-de85-8142-3b6b04a1fded') + expect(op?.status).toBe('started') + expect(op?.startedAt).toBe('2026-05-26 12:00:00.000') + expect(op?.finishedAt).toBeNull() + expect(op?.lastError).toBe('') + }) + + test('readMigrationState handles operations column missing entirely (legacy row pre-ALTER)', async () => { + const { db } = createScriptedExecutor( + new Map([ + [/SELECT name FROM .* LIMIT 0/, []], + [ + /FROM .* FINAL WHERE name = /, + [ + { + name: 'legacy.sql', + applied_at: '2026-04-01 09:00:00.000', + checksum: 'cafebabe', + chkit_version: '0.0.9', + migration_completed: true, + // operations field omitted on purpose — legacy journal row. + }, + ], + ], + ]), + ) + + const store = createJournalStore(db) + const state = await store.readMigrationState('legacy.sql') + + expect(state).not.toBeNull() + expect(state?.migrationCompleted).toBe(true) + expect(state?.operations).toEqual([]) + }) + + test('readMigrationState returns null when no row exists', async () => { + const { db } = createScriptedExecutor( + new Map([ + [/SELECT name FROM .* LIMIT 0/, []], + [/FROM .* FINAL WHERE name = /, []], + ]), + ) + + const store = createJournalStore(db) + const state = await store.readMigrationState('absent.sql') + expect(state).toBeNull() + }) + + test('writeMigrationState serializes operations as a tuple array literal in INSERT VALUES', async () => { + const { db, commandCalls } = createScriptedExecutor( + new Map([ + [/SELECT name FROM .* LIMIT 0/, []], + ]), + ) + + const store = createJournalStore(db) + await store.writeMigrationState({ + name: 'm.sql', + appliedAt: '2026-05-26 12:00:00.000', + checksum: 'deadbeef', + chkitVersion: '0.1.0-test', + migrationCompleted: false, + operations: [ + { + operationIndex: 0, + operationKey: 'table:default.hits', + operationType: 'load_table_data', + queryId: '17977426-2184-de85-8142-3b6b04a1fded', + status: 'started', + startedAt: '2026-05-26 12:00:00.000', + finishedAt: null, + lastError: '', + }, + ], + }) + + const insert = commandCalls.find((sql) => sql.startsWith('INSERT INTO')) + expect(insert).toBeDefined() + // Operations encoded as a positional tuple-array literal, with NULL for + // finished_at when the op is still in flight. + expect(insert).toMatch( + /\[\(0,'table:default\.hits','load_table_data','17977426-2184-de85-8142-3b6b04a1fded','started','2026-05-26 12:00:00\.000',NULL,''\)\]/, + ) + }) + + test('writeMigrationState escapes single quotes inside lastError so SQL stays valid', async () => { + const { db, commandCalls } = createScriptedExecutor( + new Map([ + [/SELECT name FROM .* LIMIT 0/, []], + ]), + ) + + const store = createJournalStore(db) + await store.writeMigrationState({ + name: 'm.sql', + appliedAt: '2026-05-26 12:00:00.000', + checksum: 'cs', + chkitVersion: 'v', + migrationCompleted: false, + operations: [ + { + operationIndex: 0, + operationKey: 'k', + operationType: 't', + queryId: 'q', + status: 'failed', + startedAt: '2026-05-26 12:00:00.000', + finishedAt: '2026-05-26 12:01:00.000', + lastError: "It's broken: 'unterminated", + }, + ], + }) + + const insert = commandCalls.find((sql) => sql.startsWith('INSERT INTO')) + expect(insert).toBeDefined() + expect(insert).toContain("It\\'s broken: \\'unterminated") + }) +}) diff --git a/packages/cli/src/test/runtime/safety-markers.test.ts b/packages/cli/src/test/runtime/safety-markers.test.ts index 23a2ae5..8c3ba24 100644 --- a/packages/cli/src/test/runtime/safety-markers.test.ts +++ b/packages/cli/src/test/runtime/safety-markers.test.ts @@ -1,6 +1,9 @@ import { describe, expect, test } from 'bun:test' -import { extractExecutableStatements } from '../../runtime/safety-markers.js' +import { + extractExecutableStatements, + extractMigrationOperationSummaries, +} from '../../runtime/safety-markers.js' describe('extractExecutableStatements', () => { test('splits simple statement batches', () => { @@ -42,6 +45,100 @@ describe('extractExecutableStatements', () => { ]) }) + test('extractMigrationOperationSummaries defaults mode to sync and beforeRetry to null', () => { + const sql = ` + -- operation: create_table key=table:app.events risk=safe + CREATE TABLE app.events (id UInt64) ENGINE = MergeTree() ORDER BY id; + ` + + expect(extractMigrationOperationSummaries(sql)).toEqual([ + { + type: 'create_table', + key: 'table:app.events', + risk: 'safe', + mode: 'sync', + beforeRetry: null, + summary: 'create_table key=table:app.events risk=safe', + }, + ]) + }) + + test('extractMigrationOperationSummaries parses mode=async', () => { + const sql = ` + -- operation: load_table_data key=table:app.events risk=caution mode=async + INSERT INTO app.events SELECT * FROM s3('https://example.com/file.parquet','Parquet'); + ` + + expect(extractMigrationOperationSummaries(sql)).toEqual([ + { + type: 'load_table_data', + key: 'table:app.events', + risk: 'caution', + mode: 'async', + beforeRetry: null, + summary: + 'load_table_data key=table:app.events risk=caution mode=async', + }, + ]) + }) + + test('extractMigrationOperationSummaries parses before-retry SQL attached to an operation', () => { + const sql = ` + -- operation: load_table_data key=table:app.events risk=caution mode=async + -- before-retry: TRUNCATE TABLE app.events SETTINGS max_table_size_to_drop = 0; + INSERT INTO app.events SELECT * FROM s3('…','Parquet'); + ` + + const ops = extractMigrationOperationSummaries(sql) + expect(ops).toHaveLength(1) + expect(ops[0]?.beforeRetry).toBe( + 'TRUNCATE TABLE app.events SETTINGS max_table_size_to_drop = 0', + ) + }) + + test('extractMigrationOperationSummaries does not pick up before-retry separated by SQL', () => { + const sql = ` + -- operation: load_table_data key=table:app.events risk=caution mode=async + INSERT INTO app.events SELECT 1; + -- before-retry: TRUNCATE TABLE app.events; + ` + + // The before-retry line comes AFTER an executable statement — that's + // for a different (later) operation, not this one. This operation's + // beforeRetry should be null. + const ops = extractMigrationOperationSummaries(sql) + expect(ops).toHaveLength(1) + expect(ops[0]?.beforeRetry).toBeNull() + }) + + test('extractMigrationOperationSummaries handles mixed sync + async ops', () => { + const sql = ` + -- operation: truncate_table key=table:app.events risk=caution + TRUNCATE TABLE app.events; + -- operation: load_table_data key=table:app.events risk=caution mode=async + INSERT INTO app.events SELECT 1; + ` + + const ops = extractMigrationOperationSummaries(sql) + expect(ops.map((op) => ({ type: op.type, mode: op.mode }))).toEqual([ + { type: 'truncate_table', mode: 'sync' }, + { type: 'load_table_data', mode: 'async' }, + ]) + }) + + test('extractMigrationOperationSummaries treats unrecognized mode as sync (forward-compat)', () => { + const sql = ` + -- operation: load_table_data key=table:app.events risk=caution mode=batched + INSERT INTO app.events SELECT 1; + ` + + // Forward compat: a future mode value an older chkit doesn't know about + // should fall back to sync execution rather than silently dropping the op. + const ops = extractMigrationOperationSummaries(sql) + expect(ops).toHaveLength(1) + expect(ops[0]?.mode).toBe('sync') + }) + test('ignores full-line comments while preserving executable statements', () => { const sql = ` -- operation: alter_table_drop_column key=table:app.events:column:old_col risk=danger diff --git a/packages/clickhouse/src/index.test.ts b/packages/clickhouse/src/index.test.ts index 163cc7e..859e75f 100644 --- a/packages/clickhouse/src/index.test.ts +++ b/packages/clickhouse/src/index.test.ts @@ -1,6 +1,8 @@ import { describe, expect, test } from 'bun:test' import { + assertStreamedQuerySucceeded, + ClickHouseStreamedException, createClickHouseExecutor, createExecutorWithClient, createSessionClickHouseClient, @@ -23,15 +25,28 @@ type InsertCall = { values: Array> } -function createMockClient(name: string, calls: InsertCall[]) { +type MockClientOptions = { + commandHeaders?: Record + queryHeaders?: Record + insertHeaders?: Record +} + +function createMockClient( + name: string, + calls: InsertCall[], + opts: MockClientOptions = {}, +) { return { async command() { - return { query_id: `${name}-command` } + return { + query_id: `${name}-command`, + response_headers: opts.commandHeaders ?? {}, + } }, async query() { return { query_id: `${name}-query`, - response_headers: {}, + response_headers: opts.queryHeaders ?? {}, async json() { return [] }, @@ -39,7 +54,10 @@ function createMockClient(name: string, calls: InsertCall[]) { }, async insert(params: { table: string; values: Array> }) { calls.push({ client: name, table: params.table, values: params.values }) - return { query_id: `${name}-insert` } + return { + query_id: `${name}-insert`, + response_headers: opts.insertHeaders ?? {}, + } }, async close() {}, } as unknown as ReturnType @@ -187,6 +205,156 @@ SETTINGS index_granularity = 8192;` expect(parseEngineFromCreateTableQuery(query)).toBe('MergeTree()') }) + test('assertStreamedQuerySucceeded throws on non-zero exception code header', () => { + const call = () => + assertStreamedQuerySucceeded({ + response_headers: { + 'x-clickhouse-exception-code': '241', + 'x-clickhouse-exception-tag': 'tagvalue', + }, + query_id: 'qid-1', + sql: 'INSERT INTO t SELECT 1', + }) + + expect(call).toThrow(ClickHouseStreamedException) + expect(call).toThrow(/241/) + expect(call).toThrow(/qid-1/) + expect(call).toThrow(/tagvalue/) + }) + + test('assertStreamedQuerySucceeded carries structured fields', () => { + let caught: unknown + try { + assertStreamedQuerySucceeded({ + response_headers: { + 'x-clickhouse-exception-code': '241', + 'x-clickhouse-exception-tag': 'tagvalue', + }, + query_id: 'qid-1', + sql: 'INSERT INTO t SELECT 1', + }) + } catch (error) { + caught = error + } + expect(caught).toBeInstanceOf(ClickHouseStreamedException) + const err = caught as ClickHouseStreamedException + expect(err.code).toBe('241') + expect(err.exceptionTag).toBe('tagvalue') + expect(err.query_id).toBe('qid-1') + }) + + test("assertStreamedQuerySucceeded does not throw when code is '0'", () => { + assertStreamedQuerySucceeded({ + response_headers: { 'x-clickhouse-exception-code': '0' }, + query_id: 'qid', + sql: undefined, + }) + }) + + test('assertStreamedQuerySucceeded does not throw on missing header', () => { + assertStreamedQuerySucceeded({ + response_headers: { 'content-type': 'text/plain' }, + query_id: 'qid', + sql: undefined, + }) + }) + + test('assertStreamedQuerySucceeded does not throw on undefined response_headers', () => { + assertStreamedQuerySucceeded({ + response_headers: undefined, + query_id: 'qid', + sql: undefined, + }) + }) + + test('executor.command throws when streamed exception is reported via headers', async () => { + const calls: InsertCall[] = [] + const executor = createExecutorWithClient( + { + url: 'http://localhost:8123', + username: 'default', + password: '', + database: 'default', + secure: false, + }, + createMockClient('plain', calls, { + commandHeaders: { + 'x-clickhouse-exception-code': '241', + 'x-clickhouse-exception-tag': 'memlim', + }, + }), + ) + + await expect(executor.command('INSERT INTO hits SELECT 1')).rejects.toBeInstanceOf( + ClickHouseStreamedException, + ) + await expect(executor.command('INSERT INTO hits SELECT 1')).rejects.toThrow(/241/) + await executor.close() + }) + + test('executor.query throws when streamed exception is reported via headers', async () => { + const calls: InsertCall[] = [] + const executor = createExecutorWithClient( + { + url: 'http://localhost:8123', + username: 'default', + password: '', + database: 'default', + secure: false, + }, + createMockClient('plain', calls, { + queryHeaders: { + 'x-clickhouse-exception-code': '159', + }, + }), + ) + + await expect(executor.query('SELECT 1')).rejects.toBeInstanceOf( + ClickHouseStreamedException, + ) + await executor.close() + }) + + test('executor.insert throws when streamed exception is reported via headers', async () => { + const calls: InsertCall[] = [] + const executor = createExecutorWithClient( + { + url: 'http://localhost:8123', + username: 'default', + password: '', + database: 'default', + secure: false, + }, + createMockClient('plain', calls, { + insertHeaders: { + 'x-clickhouse-exception-code': '60', + }, + }), + ) + + await expect( + executor.insert({ table: 'hits', values: [{ id: 1 }] }), + ).rejects.toBeInstanceOf(ClickHouseStreamedException) + await executor.close() + }) + + test('executor.command succeeds when response_headers carry no exception code', async () => { + const calls: InsertCall[] = [] + const executor = createExecutorWithClient( + { + url: 'http://localhost:8123', + username: 'default', + password: '', + database: 'default', + secure: false, + }, + createMockClient('plain', calls), + ) + + await executor.command('CREATE TABLE noop (x UInt64) ENGINE = Memory') + await executor.close() + }) + test('parses projection definitions from create table query', () => { const query = `CREATE TABLE app.events ( diff --git a/packages/clickhouse/src/index.ts b/packages/clickhouse/src/index.ts index 5316a21..3daa75b 100644 --- a/packages/clickhouse/src/index.ts +++ b/packages/clickhouse/src/index.ts @@ -353,6 +353,64 @@ export function isUnknownDatabaseError(error: unknown): boolean { return String(error.code) === '81' } +/** + * Thrown when a ClickHouse query failed mid-flight after the server already + * committed an HTTP 200 response by emitting progress headers. In that + * scenario the error is reported via the `x-clickhouse-exception-code` + * response header rather than as an HTTP error — @clickhouse/client does not + * surface it as a thrown error, so we must detect it ourselves and throw. + */ +export class ClickHouseStreamedException extends Error { + readonly code: string + readonly exceptionTag: string | undefined + readonly query_id: string | undefined + constructor(input: { + code: string + exceptionTag: string | undefined + query_id: string | undefined + sql: string | undefined + }) { + const idPart = input.query_id ? ` (query_id ${input.query_id})` : '' + const tagPart = input.exceptionTag ? `, exception_tag ${input.exceptionTag}` : '' + const sqlPreview = input.sql + ? `\n SQL: ${input.sql.length > 200 ? `${input.sql.slice(0, 200)}…` : input.sql}` + : '' + super( + `ClickHouse query failed with exception code ${input.code}${tagPart}${idPart}.${sqlPreview}`, + ) + this.name = 'ClickHouseStreamedException' + this.code = input.code + this.exceptionTag = input.exceptionTag + this.query_id = input.query_id + } +} + +/** + * Throws if the response carries a non-zero `x-clickhouse-exception-code` + * header. This happens when ClickHouse sends progress headers (committing + * HTTP 200) and then the query errors out — the error is reported in + * headers, not by HTTP status, and @clickhouse/client does not raise it. + */ +export function assertStreamedQuerySucceeded(input: { + response_headers: Record | undefined + query_id: string + sql: string | undefined +}): void { + const headers = input.response_headers + if (!headers) return + const rawCode = headers['x-clickhouse-exception-code'] + const code = Array.isArray(rawCode) ? rawCode[0] : rawCode + if (!code || code === '0') return + const rawTag = headers['x-clickhouse-exception-tag'] + const tag = Array.isArray(rawTag) ? rawTag[0] : rawTag + throw new ClickHouseStreamedException({ + code, + exceptionTag: tag, + query_id: input.query_id, + sql: input.sql, + }) +} + export { waitForColumn, waitForDDLPropagation, @@ -484,6 +542,11 @@ export function createExecutorWithClient( query: sql, http_headers: { 'X-DDL': '1' }, }) + assertStreamedQuerySucceeded({ + response_headers: result.response_headers, + query_id: result.query_id, + sql, + }) logProfiling(profiler, sql, result.query_id, result.summary) } catch (error) { if (isUnknownDatabaseError(error)) { @@ -496,10 +559,15 @@ export function createExecutorWithClient( clickhouse_settings: { wait_end_of_query: 1, async_insert: 0 }, }) try { - await fallback.command({ + const fallbackResult = await fallback.command({ query: sql, http_headers: { 'X-DDL': '1' }, }) + assertStreamedQuerySucceeded({ + response_headers: fallbackResult.response_headers, + query_id: fallbackResult.query_id, + sql, + }) } finally { await fallback.close() } @@ -517,6 +585,11 @@ export function createExecutorWithClient( ...(settings ? { clickhouse_settings: settings } : {}), }) const rows = await result.json() + assertStreamedQuerySucceeded({ + response_headers: result.response_headers, + query_id: result.query_id, + sql, + }) logProfiling( profiler, sql, @@ -539,7 +612,12 @@ export function createExecutorWithClient( http_headers: { 'X-DDL': '1' }, ...(settings ? { clickhouse_settings: settings } : {}), }) - const payload = (await result.json()) as ClickHouseJsonQueryResult + const payload = (await result.json()) as ClickHouseJsonQueryResult + assertStreamedQuerySucceeded({ + response_headers: result.response_headers, + query_id: result.query_id, + sql, + }) logProfiling( profiler, sql, @@ -576,6 +654,11 @@ export function createExecutorWithClient( values: params.values, format: 'JSONEachRow', }) + assertStreamedQuerySucceeded({ + response_headers: result.response_headers, + query_id: result.query_id, + sql: `INSERT INTO ${params.table}`, + }) logProfiling( profiler, `INSERT INTO ${params.table}`, diff --git a/packages/create-chkit/README.md b/packages/create-chkit/README.md index 6ebb1c0..259deca 100644 --- a/packages/create-chkit/README.md +++ b/packages/create-chkit/README.md @@ -25,7 +25,7 @@ bun create chkit@latest my-app --example clickbench | Flag | Description | | --- | --- | | `[project-directory]` | Target directory. Prompted if omitted. | -| `-e, --example ` | Example to scaffold. Bare name (`clickbench`) or full GitHub URL. Defaults to `clickbench`. | +| `-e, --example ` | Example to scaffold. Bare name (`clickbench`) or full GitHub URL. Prompted with the list of bundled examples if omitted. | | `-m, --package-manager ` | `npm`, `pnpm`, `yarn`, or `bun`. Auto-detected from the invoking package manager. | | `--skip-install` | Skip installing dependencies after scaffolding. | | `-v, --version` | Print version. | diff --git a/packages/create-chkit/package.json b/packages/create-chkit/package.json index e5663bf..d6c3847 100644 --- a/packages/create-chkit/package.json +++ b/packages/create-chkit/package.json @@ -30,7 +30,7 @@ "dist" ], "scripts": { - "build": "tsc -p tsconfig.json", + "build": "bun run ../../scripts/check-examples-manifest.ts && tsc -p tsconfig.json && bun run scripts/copy-manifest.ts", "dev": "bun run src/bin/create-chkit.ts", "typecheck": "tsc -p tsconfig.json --noEmit", "lint": "biome lint src", diff --git a/packages/create-chkit/scripts/copy-manifest.ts b/packages/create-chkit/scripts/copy-manifest.ts new file mode 100644 index 0000000..6de9614 --- /dev/null +++ b/packages/create-chkit/scripts/copy-manifest.ts @@ -0,0 +1,12 @@ +#!/usr/bin/env bun +import { copyFileSync, mkdirSync } from 'node:fs' +import { dirname, resolve } from 'node:path' +import { fileURLToPath } from 'node:url' + +const here = dirname(fileURLToPath(import.meta.url)) +const source = resolve(here, '..', '..', '..', 'examples', 'manifest.json') +const target = resolve(here, '..', 'dist', 'manifest.json') + +mkdirSync(dirname(target), { recursive: true }) +copyFileSync(source, target) +console.log(`Copied ${source} -> ${target}`) diff --git a/packages/create-chkit/src/bin/create-chkit.ts b/packages/create-chkit/src/bin/create-chkit.ts index 69d50f6..1950c1b 100644 --- a/packages/create-chkit/src/bin/create-chkit.ts +++ b/packages/create-chkit/src/bin/create-chkit.ts @@ -12,13 +12,13 @@ program .description('Scaffold a new chkit project from an example') .version(CREATE_CHKIT_VERSION, '-v, --version', 'output the version number') .argument('[project-directory]', 'target directory for the new project') - .option('-e, --example ', 'example name or full GitHub URL', 'clickbench') + .option('-e, --example ', 'example name or full GitHub URL (prompted if omitted)') .option('-m, --package-manager ', 'npm | pnpm | yarn | bun (overrides auto-detection)') .option('--skip-install', 'skip installing dependencies after scaffolding') .action(async (projectDirectory: string | undefined, options) => { await runCreate({ projectDirectory, - example: options.example as string, + example: options.example as string | undefined, packageManager: options.packageManager as string | undefined, skipInstall: options.skipInstall === true, }) diff --git a/packages/create-chkit/src/create.ts b/packages/create-chkit/src/create.ts index 9a3a376..a7ce0af 100644 --- a/packages/create-chkit/src/create.ts +++ b/packages/create-chkit/src/create.ts @@ -5,6 +5,7 @@ import { confirm, intro, log, outro, select, spinner, text } from '@clack/prompt import pc from 'picocolors' import { downloadExample } from './download.js' +import { DEFAULT_EXAMPLE, EXAMPLES } from './examples.js' import { type PackageManager, detectPackageManager, @@ -17,7 +18,7 @@ import { CREATE_CHKIT_VERSION } from './version.js' export type CreateOptions = { projectDirectory: string | undefined - example: string + example: string | undefined packageManager: string | undefined skipInstall: boolean } @@ -29,9 +30,11 @@ export async function runCreate(options: CreateOptions): Promise { const targetDir = resolve(process.cwd(), projectName) await assertDirIsEmpty(targetDir) + const example = await resolveExample(options.example) + const downloadSpinner = spinner() - downloadSpinner.start(`Downloading ${pc.cyan(options.example)} example`) - const { source } = await downloadExample(options.example, targetDir) + downloadSpinner.start(`Downloading ${pc.cyan(example)} example`) + const { source } = await downloadExample(example, targetDir) downloadSpinner.stop(`Downloaded ${pc.dim(source)}`) const packageManager = await resolvePackageManager(options.packageManager) @@ -50,6 +53,20 @@ export async function runCreate(options: CreateOptions): Promise { outro(pc.green('Done.')) } +async function resolveExample(explicit: string | undefined): Promise { + if (explicit && explicit.trim().length > 0) return explicit.trim() + const chosen = await select({ + message: 'Which example do you want to scaffold?', + initialValue: DEFAULT_EXAMPLE, + options: EXAMPLES.map((entry) => ({ + value: entry.name, + label: entry.name, + hint: entry.description, + })), + }) + return unwrap(chosen) as string +} + async function resolveProjectName(initial: string | undefined): Promise { if (initial && initial.trim().length > 0) return initial.trim() const value = await text({ diff --git a/packages/create-chkit/src/examples.ts b/packages/create-chkit/src/examples.ts new file mode 100644 index 0000000..0595bcf --- /dev/null +++ b/packages/create-chkit/src/examples.ts @@ -0,0 +1,35 @@ +import { existsSync, readFileSync } from 'node:fs' +import { dirname, join } from 'node:path' +import { fileURLToPath } from 'node:url' + +export type ExampleEntry = { + name: string + description: string +} + +type ExamplesManifest = { + default: string + examples: ExampleEntry[] +} + +const MANIFEST = loadManifest() + +export const EXAMPLES: ReadonlyArray = MANIFEST.examples +export const DEFAULT_EXAMPLE: string = MANIFEST.default + +function loadManifest(): ExamplesManifest { + const here = dirname(fileURLToPath(import.meta.url)) + const candidates = [ + join(here, 'manifest.json'), + join(here, '..', '..', '..', 'examples', 'manifest.json'), + ] + for (const candidate of candidates) { + if (existsSync(candidate)) { + const raw = readFileSync(candidate, 'utf8') + return JSON.parse(raw) as ExamplesManifest + } + } + throw new Error( + `create-chkit: examples manifest not found. Looked in:\n${candidates.map((c) => ` - ${c}`).join('\n')}`, + ) +} diff --git a/scripts/check-examples-manifest.ts b/scripts/check-examples-manifest.ts new file mode 100644 index 0000000..d377427 --- /dev/null +++ b/scripts/check-examples-manifest.ts @@ -0,0 +1,55 @@ +#!/usr/bin/env bun +import { readFileSync, readdirSync, statSync } from 'node:fs' +import { dirname, resolve } from 'node:path' +import { fileURLToPath } from 'node:url' +import process from 'node:process' + +type Manifest = { + default: string + examples: { name: string; description: string }[] +} + +const here = dirname(fileURLToPath(import.meta.url)) +const examplesDir = resolve(here, '..', 'examples') +const manifestPath = resolve(examplesDir, 'manifest.json') + +const manifest = JSON.parse(readFileSync(manifestPath, 'utf8')) as Manifest + +const dirNames = readdirSync(examplesDir) + .filter((entry) => statSync(resolve(examplesDir, entry)).isDirectory()) + .sort() + +const manifestNames = manifest.examples.map((entry) => entry.name).sort() + +const missingFromManifest = dirNames.filter((name) => !manifestNames.includes(name)) +const missingFromDisk = manifestNames.filter((name) => !dirNames.includes(name)) +const defaultMissing = !manifestNames.includes(manifest.default) + +const errors: string[] = [] + +if (missingFromManifest.length > 0) { + errors.push( + `Directories under examples/ are missing from manifest.json:\n${missingFromManifest.map((n) => ` - ${n}`).join('\n')}`, + ) +} + +if (missingFromDisk.length > 0) { + errors.push( + `manifest.json lists examples with no matching directory under examples/:\n${missingFromDisk.map((n) => ` - ${n}`).join('\n')}`, + ) +} + +if (defaultMissing) { + errors.push( + `manifest.json default "${manifest.default}" does not match any entry in examples[].`, + ) +} + +if (errors.length > 0) { + console.error('examples/manifest.json is out of sync:\n') + for (const error of errors) console.error(`${error}\n`) + console.error('Edit examples/manifest.json so it matches the contents of examples/.') + process.exit(1) +} + +console.log(`examples/manifest.json is in sync (${manifestNames.length} example(s)).`)