krz/omaha-metro-blotter

clone: git clone https://gitbay.org/krz/omaha-metro-blotter.git

main: schema.sql · raw

  1-- Incidents as first observed. Rows here are never updated: when a feed serves a
  2-- changed version of a record it goes to incident_amendments instead, so the
  3-- original survives a reclassification, a reopened case or a withdrawn record.
  4-- occurred_at is local time (America/Chicago); the services return UTC epochs
  5-- and ingest.py converts on the way in.
  6CREATE TABLE IF NOT EXISTS incidents (
  7    source       TEXT NOT NULL,  -- opd | sarpy | cbpd | opd_csv
  8    source_key   TEXT NOT NULL,  -- PK (opd) | IncidentId (sarpy) | cfs_number (cbpd)
  9    agency       TEXT NOT NULL,
 10    case_id      TEXT,
 11    occurred_at  TEXT NOT NULL,  -- ISO 8601, no offset
 12    category     TEXT,           -- each feed's own taxonomy, not comparable
 13    call_type    TEXT,           -- CadTypeDesc (sarpy) | incident_code (cbpd)
 14    disposition  TEXT,           -- CAD disposition, sarpy and cbpd
 15    offense_desc TEXT,           -- StatuteDesc (sarpy) | statute text (opd_csv)
 16    is_stop      INTEGER NOT NULL DEFAULT 0,  -- officer-initiated vehicle stop
 17    address      TEXT,
 18    lat          REAL,
 19    lon          REAL,
 20    digest       TEXT NOT NULL,  -- hash of the payload, for change detection
 21    first_seen   TEXT,           -- when ingest first saw it; NULL if pre-dating
 22    PRIMARY KEY (source, source_key)
 23);
 24
 25CREATE INDEX IF NOT EXISTS incidents_occurred ON incidents (occurred_at);
 26CREATE INDEX IF NOT EXISTS incidents_agency   ON incidents (agency, occurred_at);
 27CREATE INDEX IF NOT EXISTS incidents_category ON incidents (category);
 28CREATE INDEX IF NOT EXISTS incidents_stop     ON incidents (is_stop, occurred_at);
 29
 30-- Every distinct later version of a record, one row per version. Keyed on the
 31-- payload digest, so a version is stored once no matter how many runs serve it.
 32-- A record that reverts to a payload already on file is therefore not recorded
 33-- again: this holds the set of distinct states observed, not a strict timeline.
 34CREATE TABLE IF NOT EXISTS incident_amendments (
 35    source       TEXT NOT NULL,
 36    source_key   TEXT NOT NULL,
 37    agency       TEXT NOT NULL,
 38    case_id      TEXT,
 39    occurred_at  TEXT NOT NULL,
 40    category     TEXT,
 41    call_type    TEXT,
 42    disposition  TEXT,
 43    offense_desc TEXT,
 44    is_stop      INTEGER NOT NULL DEFAULT 0,
 45    address      TEXT,
 46    lat          REAL,
 47    lon          REAL,
 48    digest       TEXT NOT NULL,
 49    seen_at      TEXT NOT NULL,  -- when this version was first observed
 50    PRIMARY KEY (source, source_key, digest)
 51);
 52
 53CREATE INDEX IF NOT EXISTS amendments_key  ON incident_amendments (source, source_key);
 54CREATE INDEX IF NOT EXISTS amendments_seen ON incident_amendments (seen_at);
 55
 56-- The newest known version of each record: its latest amendment, or the
 57-- original where a record has never been amended.
 58CREATE VIEW IF NOT EXISTS incidents_current AS
 59SELECT source, source_key, agency, case_id, occurred_at, category, call_type,
 60       disposition, offense_desc, is_stop, address, lat, lon, observed_at,
 61       amended
 62FROM (
 63    SELECT *, ROW_NUMBER() OVER (PARTITION BY source, source_key
 64                                 ORDER BY amended DESC, observed_at DESC) AS rn
 65    FROM (
 66        SELECT source, source_key, agency, case_id, occurred_at, category,
 67               call_type, disposition, offense_desc, is_stop, address, lat, lon,
 68               first_seen AS observed_at, 0 AS amended
 69        FROM incidents
 70        UNION ALL
 71        SELECT source, source_key, agency, case_id, occurred_at, category,
 72               call_type, disposition, offense_desc, is_stop, address, lat, lon,
 73               seen_at AS observed_at, 1 AS amended
 74        FROM incident_amendments
 75    )
 76)
 77WHERE rn = 1;
 78
 79-- What the feed actually served, one row per version, keyed the same way
 80-- incident_amendments is. A parse that turns out wrong or a field a feed adds
 81-- later can only be applied to history if the bytes were kept, and the feeds
 82-- age out, so there is no second chance to fetch them.
 83CREATE TABLE IF NOT EXISTS raw_records (
 84    source     TEXT NOT NULL,
 85    source_key TEXT NOT NULL,
 86    digest     TEXT NOT NULL,  -- the version of the record this payload produced
 87    fetched_at TEXT NOT NULL,
 88    payload    TEXT NOT NULL,  -- the feature object as served, JSON
 89    PRIMARY KEY (source, source_key, digest)
 90);
 91
 92-- Searches run against an agency's ALPR network, from its Flock transparency
 93-- portal. The portal serves a rolling 30 days and the export is already the
 94-- whole record: no field here is derived, so there is no separate raw copy.
 95-- userId is redacted to *** by Flock, so searches cannot be attributed to an
 96-- officer, and most carry no reason despite the portals' stated access policy.
 97CREATE TABLE IF NOT EXISTS alpr_searches (
 98    agency        TEXT NOT NULL,  -- portal slug the export came from
 99    search_id     TEXT NOT NULL,  -- Flock's own UUID, stable across exports
100    searched_at   TEXT NOT NULL,  -- ISO 8601 UTC, as published
101    network_count INTEGER,        -- camera networks the search reached across
102    reason        TEXT,           -- free text, blank on most searches
103    user_id       TEXT,           -- redacted upstream
104    imported_at   TEXT NOT NULL,
105    PRIMARY KEY (agency, search_id)
106);
107
108CREATE INDEX IF NOT EXISTS searches_when ON alpr_searches (searched_at);
109
110-- ALPR cameras from OpenStreetMap (ODbL). first_seen/last_seen track when a node
111-- entered and was last present in the Overpass result, so cameras that appear or
112-- are removed are visible over time.
113CREATE TABLE IF NOT EXISTS alpr_cameras (
114    osm_id       INTEGER PRIMARY KEY,
115    lat          REAL NOT NULL,
116    lon          REAL NOT NULL,
117    manufacturer TEXT,
118    operator     TEXT,
119    direction    TEXT,
120    first_seen   TEXT NOT NULL,
121    last_seen    TEXT NOT NULL,
122    tags         TEXT NOT NULL  -- full OSM tag dict as JSON
123);