Skip to content

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.

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
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.

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 Hire
FROM test_hires
INTO simulated_hire_events;
SELECT stream_id, event_type, data
FROM 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.

CREATE TABLE test_opens AS VALUES
('ACC-100', 500.00),
('ACC-101', 0.00),
('ACC-102', 1500.00);
INSPECT DECISION Open
FROM test_opens
INTO simulated_open_events;
SELECT stream_id, event_type, data
FROM simulated_open_events;

Re-running INSPECT ... INTO overwrites the target table:

INSPECT DECISION Open
FROM test_opens
INTO simulated_open_events;
SELECT COUNT(*) AS event_count FROM simulated_open_events;

Inspection can be integrated into build pipelines by preparing known input tables and asserting on the output:

-- test_banking_decisions.deql
-- Setup: known test commands
CREATE TABLE test_deposits AS VALUES
('ACC-001', 500.00),
('ACC-002', 0.00),
('ACC-003', 100.00);
-- Run inspection
INSPECT DECISION DepositFunds
FROM test_deposits
INTO test_deposit_results;
-- Assert: check results
SELECT stream_id, event_type, data
FROM test_deposit_results;

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.

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:

ClausePurpose
OFFSETResume replay from a specific event sequence position
WHEREGuard condition — filter which events are processed
LIMITCap 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 events
CREATE TABLE test_opens AS VALUES
('ACC-100', 500.00),
('ACC-101', 0.00),
('ACC-102', 1500.00);
INSPECT DECISION Open
FROM test_opens
INTO simulated_open_events;
-- Step 2: Feed those events through the AccountBalance projection
INSPECT PROJECTION AccountBalance
FROM simulated_open_events
INTO simulated_balances;
-- Step 3: Verify the projected balances
SELECT * 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.

-- Simulate hiring employees
CREATE TABLE test_hires AS VALUES
('EMP-100', 'Charlie', 'L3'),
('EMP-101', 'Diana', 'L5');
INSPECT DECISION Hire
FROM test_hires
INTO simulated_hire_events;
-- Verify the employee roster projection
INSPECT PROJECTION EmployeeRoster
FROM simulated_hire_events
INTO simulated_roster;
SELECT * FROM simulated_roster;

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 opening
CREATE TABLE test_opens AS VALUES
('ACC-200', 500.00),
('ACC-201', 1000.00);
INSPECT DECISION Open
FROM test_opens
INTO opened_events;
SELECT stream_id, event_type, data
FROM opened_events;
-- 2. Project balances from simulated events
INSPECT PROJECTION AccountBalance
FROM opened_events
INTO projected_balances;
-- 3. Assert: balances should match initial_balance
SELECT * FROM projected_balances;

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_model

No real events are written. No real projections are updated. The entire chain is side-effect-free.

ScenarioWhat It Catches
Projection logic errorsWrong aggregation, missing event types, bad filters
Schema mismatchesProjection expects fields the event doesn’t carry
Regression testingProjection output changes after event schema evolution
Read model designValidate the shape before deploying to production
Full pipeline verificationCommand → event → read model correctness in one test

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.

Drop and rebuild a projection from the beginning of the event stream:

INSPECT PROJECTION AccountBalance
FROM 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.

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 500000
INSPECT PROJECTION AccountBalance
FROM 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

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 account
INSPECT PROJECTION AccountBalance
FROM DeReg."BankAccount$Events"
INTO DeReg."AccountBalance"
WHERE stream_id = 'ACC-001';
-- Rebuild only from recent events
INSPECT PROJECTION AccountBalance
FROM DeReg."BankAccount$Events"
INTO DeReg."AccountBalance"
WHERE timestamp >= '2026-01-01';
-- Rebuild only for specific event types
INSPECT PROJECTION AccountBalance
FROM DeReg."BankAccount$Events"
INTO DeReg."AccountBalance"
WHERE event_type IN ('AccountOpened', 'Deposited');

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 time
INSPECT PROJECTION EmployeeRoster
FROM DeReg."Employee$Events"
INTO DeReg."EmployeeRoster"
OFFSET 0
LIMIT 10000;
-- Next batch
INSPECT PROJECTION EmployeeRoster
FROM DeReg."Employee$Events"
INTO DeReg."EmployeeRoster"
OFFSET 10000
LIMIT 10000;

All clauses compose naturally:

-- Resume from sequence 250000, only for a specific stream, 5000 events at a time
INSPECT PROJECTION EmployeeRoster
FROM DeReg."Employee$Events"
INTO DeReg."EmployeeRoster"
OFFSET 250000
WHERE stream_id = 'EMP-001'
LIMIT 5000;

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 v1
INSPECT PROJECTION AccountBalanceV2
FROM DeReg."BankAccount$Events"
INTO AccountBalanceV2_staging
OFFSET 0;
-- Once validated, swap
-- DROP PROJECTION AccountBalance;
-- ALTER PROJECTION AccountBalanceV2_staging RENAME TO AccountBalance;
PatternSyntaxUse Case
Full rebuildFROM DeReg."<Aggregate>$Events" INTO DeReg."Projection"New projection, schema change
Incremental catch-upOFFSET <n>Resume after downtime
Scoped rebuildWHERE stream_id = ...Fix a single entity’s projection
Time-boundedWHERE timestamp >= ...Rebuild recent data only
Batched replayOFFSET <n> LIMIT <m>Large stores, controlled throughput
Shadow buildINTO staging_tableZero-downtime projection migration
PropertyDescription
Same logicUses identical evaluation as production decisions and projections
No side effectsNothing is written to the event store (projection target is explicit)
Table-drivenInput from any table, query, or live event stream
ComposableDecision output chains directly into projection input
ResumableOFFSET enables incremental and batched replay
GuardedWHERE filters events before they reach the projection
BoundedLIMIT caps throughput per pass
DeterministicSame inputs always produce the same inspection result
CI-friendlyCan be executed as automated tests in pipelines