Inspection
DeQL supports inspection as a first-class capability. INSPECT statements evaluate decisions and projections in a side-effect-free mode — using the same logic as production but without writing to the event store, updating projections, or triggering any side effects.
Why Inspection Matters
Section titled “Why Inspection Matters”In traditional CQRS/ES systems, the only way to know what a command will do is to execute it. This makes testing, debugging, and validation expensive and risky.
DeQL’s inspection enables:
- Simulation — “What would happen if these commands were processed?”
- Validation — “Would these commands be accepted or rejected?”
- Projection verification — “Does the read model produce the expected output from these events?”
- Debugging — “Why was this command rejected?”
- Automated verification — CI/CD pipelines that test decisions and projections without side effects
Decision Inspection Syntax
Section titled “Decision Inspection Syntax”INSPECT DECISION <DecisionName>FROM <source_table>INTO <output_table>;The FROM clause specifies a table (or view) containing the commands to simulate. The INTO clause specifies where the simulated events are written — as data, not as persisted events.
Example: Inspecting Hires
Section titled “Example: Inspecting Hires”Given a table of test hire commands (positional columns auto-mapped to command fields):
CREATE TABLE test_hires AS VALUES ('EMP-100', 'Charlie', 'L3'), ('EMP-101', 'Diana', 'L5');
INSPECT DECISION HireFROM test_hiresINTO simulated_hire_events;
SELECT stream_id, event_type, dataFROM simulated_hire_events;The simulated_hire_events table contains the events that would have been emitted for each row in test_hires, without actually modifying the event store.
Example: Inspecting Account Opens
Section titled “Example: Inspecting Account Opens”CREATE TABLE test_opens AS VALUES ('ACC-100', 500.00), ('ACC-101', 0.00), ('ACC-102', 1500.00);
INSPECT DECISION OpenFROM test_opensINTO simulated_open_events;
SELECT stream_id, event_type, dataFROM simulated_open_events;Example: Overwrite Semantics
Section titled “Example: Overwrite Semantics”Re-running INSPECT ... INTO overwrites the target table:
INSPECT DECISION OpenFROM test_opensINTO simulated_open_events;
SELECT COUNT(*) AS event_count FROM simulated_open_events;Inspection in CI/CD
Section titled “Inspection in CI/CD”Inspection can be integrated into build pipelines by preparing known input tables and asserting on the output:
-- test_banking_decisions.deql
-- Setup: known test commandsCREATE TABLE test_deposits AS VALUES ('ACC-001', 500.00), ('ACC-002', 0.00), ('ACC-003', 100.00);
-- Run inspectionINSPECT DECISION DepositFundsFROM test_depositsINTO test_deposit_results;
-- Assert: check resultsSELECT stream_id, event_type, dataFROM test_deposit_results;Projection Inspection
Section titled “Projection Inspection”Decisions are only half the story. Projections transform events into read models, and those transformations need validation too. INSPECT PROJECTION feeds a set of events through a projection’s logic and writes the resulting read model into an output table — without updating the real projection.
Projection Inspection Syntax
Section titled “Projection Inspection Syntax”INSPECT PROJECTION <ProjectionName>FROM <event_source_table>INTO <output_table>[OFFSET <position>][WHERE <guard_condition>][LIMIT <max_events>];The FROM clause provides the events to process — either the output of a prior INSPECT DECISION (for testing) or the real event store (for production replay). The INTO clause receives the projected read model rows.
Optional clauses:
| Clause | Purpose |
|---|---|
OFFSET | Resume replay from a specific event sequence position |
WHERE | Guard condition — filter which events are processed |
LIMIT | Cap the number of events processed in a single pass |
Example: Chaining Decision → Projection Inspection
Section titled “Example: Chaining Decision → Projection Inspection”Chain a decision inspection into a projection inspection to verify the full pipeline — command → events → read model:
-- Step 1: Simulate account opens to get eventsCREATE TABLE test_opens AS VALUES ('ACC-100', 500.00), ('ACC-101', 0.00), ('ACC-102', 1500.00);
INSPECT DECISION OpenFROM test_opensINTO simulated_open_events;
-- Step 2: Feed those events through the AccountBalance projectionINSPECT PROJECTION AccountBalanceFROM simulated_open_eventsINTO simulated_balances;
-- Step 3: Verify the projected balancesSELECT * FROM simulated_balances;The simulated_balances table contains the read model rows that AccountBalance would produce if those events were real — without touching the actual projection.
Example: Verifying Employee Roster
Section titled “Example: Verifying Employee Roster”-- Simulate hiring employeesCREATE TABLE test_hires AS VALUES ('EMP-100', 'Charlie', 'L3'), ('EMP-101', 'Diana', 'L5');
INSPECT DECISION HireFROM test_hiresINTO simulated_hire_events;
-- Verify the employee roster projectionINSPECT PROJECTION EmployeeRosterFROM simulated_hire_eventsINTO simulated_roster;
SELECT * FROM simulated_roster;Example: End-to-End Pipeline Test
Section titled “Example: End-to-End Pipeline Test”Projection inspection shines in end-to-end tests where you validate the entire flow from command to read model:
-- test_full_pipeline.deql
-- 1. Simulate account openingCREATE TABLE test_opens AS VALUES ('ACC-200', 500.00), ('ACC-201', 1000.00);
INSPECT DECISION OpenFROM test_opensINTO opened_events;
SELECT stream_id, event_type, dataFROM opened_events;
-- 2. Project balances from simulated eventsINSPECT PROJECTION AccountBalanceFROM opened_eventsINTO projected_balances;
-- 3. Assert: balances should match initial_balanceSELECT * FROM projected_balances;Chaining Decisions and Projections
Section titled “Chaining Decisions and Projections”The INTO output of INSPECT DECISION is a table of simulated events. That table can be fed directly as the FROM input to INSPECT PROJECTION. This creates a clean, composable test pipeline:
INSPECT DECISION → simulated_events → INSPECT PROJECTION → simulated_read_modelNo real events are written. No real projections are updated. The entire chain is side-effect-free.
Why Inspect Projections?
Section titled “Why Inspect Projections?”| Scenario | What It Catches |
|---|---|
| Projection logic errors | Wrong aggregation, missing event types, bad filters |
| Schema mismatches | Projection expects fields the event doesn’t carry |
| Regression testing | Projection output changes after event schema evolution |
| Read model design | Validate the shape before deploying to production |
| Full pipeline verification | Command → event → read model correctness in one test |
Production Replay
Section titled “Production Replay”INSPECT PROJECTION is not limited to testing. In production, it serves as the mechanism for rebuilding projections from the real event store — replaying all (or a subset of) events through the projection logic.
Full Rebuild
Section titled “Full Rebuild”Drop and rebuild a projection from the beginning of the event stream:
INSPECT PROJECTION AccountBalanceFROM DeReg."BankAccount$Events"INTO DeReg."AccountBalance";When FROM points at a real event stream (DeReg."<Aggregate>$Events") and INTO targets the projection itself, this performs a full rebuild.
Offset: Resume From a Position
Section titled “Offset: Resume From a Position”Large event stores make full replays expensive. The OFFSET clause lets you resume from where a previous replay left off, using the event sequence number:
-- Resume from event sequence 500000INSPECT PROJECTION AccountBalanceFROM DeReg."BankAccount$Events"INTO DeReg."AccountBalance"OFFSET 500000;The projection processes only events with sequence > 500000. This is essential for:
- Incremental catch-up after downtime
- Resuming a failed rebuild without starting over
- Keeping a secondary projection in sync
Guards: Filter What Gets Replayed
Section titled “Guards: Filter What Gets Replayed”The WHERE clause acts as a guard — only events matching the condition are fed through the projection. This enables targeted replays:
-- Rebuild only for a specific accountINSPECT PROJECTION AccountBalanceFROM DeReg."BankAccount$Events"INTO DeReg."AccountBalance"WHERE stream_id = 'ACC-001';
-- Rebuild only from recent eventsINSPECT PROJECTION AccountBalanceFROM DeReg."BankAccount$Events"INTO DeReg."AccountBalance"WHERE timestamp >= '2026-01-01';
-- Rebuild only for specific event typesINSPECT PROJECTION AccountBalanceFROM DeReg."BankAccount$Events"INTO DeReg."AccountBalance"WHERE event_type IN ('AccountOpened', 'Deposited');Limit: Controlled Batch Replay
Section titled “Limit: Controlled Batch Replay”The LIMIT clause caps the number of events processed in a single pass. Combined with OFFSET, this enables batched replay for large event stores:
-- Process 10,000 events at a timeINSPECT PROJECTION EmployeeRosterFROM DeReg."Employee$Events"INTO DeReg."EmployeeRoster"OFFSET 0LIMIT 10000;
-- Next batchINSPECT PROJECTION EmployeeRosterFROM DeReg."Employee$Events"INTO DeReg."EmployeeRoster"OFFSET 10000LIMIT 10000;Combining Offset, Guards, and Limit
Section titled “Combining Offset, Guards, and Limit”All clauses compose naturally:
-- Resume from sequence 250000, only for a specific stream, 5000 events at a timeINSPECT PROJECTION EmployeeRosterFROM DeReg."Employee$Events"INTO DeReg."EmployeeRoster"OFFSET 250000WHERE stream_id = 'EMP-001'LIMIT 5000;Shadow Projection (Side-by-Side Rebuild)
Section titled “Shadow Projection (Side-by-Side Rebuild)”Use INTO with a different target to build a new version of a projection alongside the existing one — zero downtime:
-- Build v2 of the projection without touching v1INSPECT PROJECTION AccountBalanceV2FROM DeReg."BankAccount$Events"INTO AccountBalanceV2_stagingOFFSET 0;
-- Once validated, swap-- DROP PROJECTION AccountBalance;-- ALTER PROJECTION AccountBalanceV2_staging RENAME TO AccountBalance;Production Replay Patterns
Section titled “Production Replay Patterns”| Pattern | Syntax | Use Case |
|---|---|---|
| Full rebuild | FROM DeReg."<Aggregate>$Events" INTO DeReg."Projection" | New projection, schema change |
| Incremental catch-up | OFFSET <n> | Resume after downtime |
| Scoped rebuild | WHERE stream_id = ... | Fix a single entity’s projection |
| Time-bounded | WHERE timestamp >= ... | Rebuild recent data only |
| Batched replay | OFFSET <n> LIMIT <m> | Large stores, controlled throughput |
| Shadow build | INTO staging_table | Zero-downtime projection migration |
Hybrid Environment Inspection
Section titled “Hybrid Environment Inspection”Because INSPECT is side-effect-free, the CLI is well-suited for scenarios where the data you want to read lives in one environment and the destination belongs to another. Named event stores make this explicit: each environment can have its own declared event store, and inspection FROM/INTO clauses choose which one to use.
Two common patterns illustrate this:
Simulated Commands Against Live Production State
Section titled “Simulated Commands Against Live Production State”When a decision has a STATE AS query, it reads from the active aggregate state. If the event store is pointing at production data, STATE AS reads real production state. This lets you ask: “Would these commands succeed against real production data?” — without executing anything:
-- dev table holds the commands to testCREATE TABLE test_promotions AS VALUES ('EMP-001', 'L6'), ('EMP-002', 'L4');
INSPECT DECISION PromoteFROM test_promotions -- simulated commands (dev)INTO simulated_results; -- output lands in a dev/local temp table
-- The WHERE guard in Promote reads Employee$Agg from production.-- Results show which promotions would be accepted or rejected given real state.SELECT stream_id, event_type, data FROM simulated_results;The aggregate lives in production. The simulated commands and the event destination are local. No production state is mutated.
Production Events Through a SIT Projection
Section titled “Production Events Through a SIT Projection”When developing or validating a new projection, you can feed it real events from production and write the results to a SIT (system integration test) staging table — without touching the live projection:
-- Events stay in production; projection logic runs against them-- and output lands in a SIT-local staging tableINSPECT PROJECTION NewHireReportFROM DeReg."Employee$Events" -- production event streamINTO sit_new_hire_report; -- SIT staging target
-- Validate shape and correctness before promoting the projectionSELECT * FROM sit_new_hire_report ORDER BY employee_id;Real production events flow through the new projection logic. The live projection is not touched. Once the output is verified, the projection can be deployed to SIT or production.
Summary
Section titled “Summary”| Pattern | Reads from | Writes to | What it validates |
|---|---|---|---|
| Command safety check | Production aggregate state ($Agg) | Dev temp table | Would these commands succeed against live state? |
| Projection validation | Production event stream ($Events) | SIT staging table | Does new projection logic produce correct output? |
| Full pipeline on real data | Production (state + events) | Dev / SIT tables | Full command → event → read model correctness on real data |
The CLI is the natural tool for these patterns because it connects directly to named event stores, supports iterative refinement, and the inspection is side-effect-free by design. The REST API is designed for single-environment application integration, not cross-environment inspection.
Key Properties
Section titled “Key Properties”| Property | Description |
|---|---|
| Same logic | Uses identical evaluation as production decisions and projections |
| No side effects | Nothing is written to the event store (projection target is explicit) |
| Table-driven | Input from any table, query, or live event stream |
| Composable | Decision output chains directly into projection input |
| Resumable | OFFSET enables incremental and batched replay |
| Guarded | WHERE filters events before they reach the projection |
| Bounded | LIMIT caps throughput per pass |
| Deterministic | Same inputs always produce the same inspection result |
| CI-friendly | Can be executed as automated tests in pipelines |