Files
directus/.planning/phase-1-slice-1-schema/03-initial-migrations.md
T
julian a8e808e71c Scaffold directus service planning structure
Initial commit. Establishes the .planning/ tree mirroring processor's
shape (ROADMAP.md as nav hub + per-phase folders with READMEs and
granular task files).

Six phases:

1. Slice 1 schema + deploy pipeline — what Rally Albania 2026 needs.
   Org catalog (orgs, users, vehicles, devices) + event participation
   (events, classes, entries, entry_crew, entry_devices). db-init/
   for the positions hypertable + faulty column. snapshot/apply
   tooling. Gitea CI dry-run. Dogfood seed of Rally Albania 2026.
   Nine task files with full Goal / Deliverables / Specification /
   Acceptance criteria / Risks / Done sections.

2. Course definition — stages, segments, geofences, waypoints, SLZs.
   PostGIS extension introduced here.

3. Timing & penalty tables — co-developed with processor Phase 2.
   entry_segment_starts, entry_crossings, entry_penalties,
   stage_results, penalty_formulas.

4. Permissions & policies — Directus 11 dynamic-filter Policies per
   logical role. Deployment-time work, deferred to keep early phases
   focused on the data model.

5. Custom extensions — TypeScript hooks/endpoints implementing the
   cross-plane workflows the schema implies (faulty-flag → Redis
   stream emit, stage-open materializer, etc.).

6. Future / optional — retroactivity preview UI, command-routing
   Flows, audit trails, federation rule import. Not committed.

Non-negotiable design rules captured in ROADMAP.md: schema authority
in Directus + snapshot-as-code + db-init for non-Directus DDL +
sequential idempotent migrations + entrypoint apply order + no
application logic in Flows + permissions deferred to Phase 4.

Architectural anchors point at the wiki at ../docs/wiki/ — the schema
draft, the Rally Albania 2025 source page, plus the existing
processor/postgres-timescaledb/live-channel pages. Each task file
calls out the wiki refs an implementing agent should read first.

README.md mirrors the processor service README structure: quick start,
local Docker test, prod/stage deployment notes, env vars, CI behavior.
2026-05-01 20:42:44 +02:00

5.1 KiB

Task 1.3 — Initial migrations

Phase: 1 — Slice 1 schema + deploy pipeline Status: Not started Depends on: 1.2 Wiki refs: docs/wiki/entities/postgres-timescaledb.md, docs/wiki/concepts/position-record.md, docs/wiki/entities/processor.md (Faulty position handling)

Goal

Author the three Phase 1 migrations under db-init/: the TimescaleDB extension, the positions hypertable creation, and the faulty boolean column. Each is internally idempotent so that environments where they were applied ad-hoc (e.g. existing stage) absorb them as no-ops.

Deliverables

  • db-init/001_extensions.sql:
    CREATE EXTENSION IF NOT EXISTS timescaledb CASCADE;
    
  • db-init/002_positions_hypertable.sql:
    CREATE TABLE IF NOT EXISTS positions (
      device_id   TEXT        NOT NULL,
      ts          TIMESTAMPTZ NOT NULL,
      latitude    DOUBLE PRECISION NOT NULL,
      longitude   DOUBLE PRECISION NOT NULL,
      altitude    DOUBLE PRECISION,
      angle       SMALLINT,
      speed       SMALLINT,
      satellites  SMALLINT,
      priority    SMALLINT,
      attributes  JSONB        NOT NULL DEFAULT '{}'::jsonb,
      PRIMARY KEY (device_id, ts)
    );
    
    -- Idempotent hypertable creation: if_not_exists => true
    SELECT create_hypertable(
      'positions', 'ts',
      chunk_time_interval => INTERVAL '7 days',
      if_not_exists => TRUE
    );
    
    CREATE INDEX IF NOT EXISTS positions_device_ts_idx
      ON positions (device_id, ts DESC);
    
  • db-init/003_faulty_column.sql:
    ALTER TABLE positions
      ADD COLUMN IF NOT EXISTS faulty BOOLEAN NOT NULL DEFAULT FALSE;
    
    CREATE INDEX IF NOT EXISTS positions_faulty_idx
      ON positions (device_id, ts DESC) WHERE faulty = FALSE;
    

Specification

  • Schema must match what processor writes. Cross-check column names, types, nullability against docs/wiki/concepts/position-record.md and the actual processor writer code (processor/src/db/migrations/0001_positions.sql). If any field differs, this task is blocked until directus-schema-draft and the processor's existing migration are reconciled — fix the divergence in the doc first, then this task.
  • attributes is JSONB NOT NULL DEFAULT '{}' — never null, always an object. Keeps query plans simple.
  • (device_id, ts) primary key — natural key, idempotent for the processor's ON CONFLICT DO NOTHING writer.
  • Chunk interval = 7 days. Tunable later; 7 days is a reasonable default for hundreds of devices emitting at multi-Hz.
  • Faulty index uses a partial-index WHERE faulty = FALSE. Optimizes the processor hot-path read which always filters faulty out. Operator queries that select faulty rows specifically use the broader (device_id, ts DESC) index.
  • CASCADE on CREATE EXTENSION so that any dependent extensions install transparently. TimescaleDB has no required deps so CASCADE is a no-op for now, but harmless and future-proof.
  • No IF EXISTS shortcuts that hide schema drift. The migrations are idempotent at the DDL level (IF NOT EXISTS), but if a column type already differs from what the file declares, the migration silently passes — leaving stage in an inconsistent state. Add a final DO $$ ... $$ block per file that asserts the table shape is what the migration intends:
    -- end of 002_positions_hypertable.sql
    DO $$ BEGIN
      IF NOT EXISTS (
        SELECT 1 FROM information_schema.columns
        WHERE table_name = 'positions' AND column_name = 'attributes' AND data_type = 'jsonb'
      ) THEN
        RAISE EXCEPTION 'positions.attributes is not JSONB — schema drift';
      END IF;
    END $$;
    
    One assertion per critical column shape. Catches the case where stage has the table but with subtly different types.

Acceptance criteria

  • Against a fresh Postgres + TimescaleDB image, apply-db-init.sh runs all three files cleanly.
  • \d positions shows the expected columns (including faulty).
  • SELECT * FROM timescaledb_information.hypertables WHERE hypertable_name = 'positions'; returns one row.
  • Both indexes (positions_device_ts_idx, positions_faulty_idx) exist (\di+).
  • Re-running the script is a no-op (verified via migrations_applied table contents).
  • Against a Postgres that already has positions from a prior ad-hoc run, the migration absorbs it as a no-op (provided the existing schema matches; otherwise the assertion blocks deploy).
  • Cross-checked against processor/src/db/migrations/0001_positions.sql — column names, types, indexes match.

Risks / open questions

  • Existing stage Postgres may have a slightly different schema. Run pg_dump --schema-only -t positions on stage before this task lands and compare to the migration above. Reconcile differences in this file (or document them as known-divergent).
  • Hypertable was created before — create_hypertable with if_not_exists should accept it, but the chunk interval can't be retroactively changed via this call. If stage's chunk interval differs from 7 days, that's a non-blocking divergence (functional, just suboptimal). Don't try to migrate it via SQL; leave it as a follow-up.

Done

(Fill in commit SHA + one-line note when this lands.)