refactor(cloud): prefix all DB tables with move_

Renames the 10 P1-B tables (users, devices, cameras, zones, events,
event_snapshots, thresholds, nonces, rate_limits, audit_log) to a
move_ prefix so the schema can co-tenant a shared-hosting database.

- schema.sql + all App\Repository\Pdo\* queries + tests + deploy docs
- new migrations/2026-07-20-move-table-prefix.sql: atomic RENAME TABLE
  for existing installs; must run BEFORE deploying this code
- index/constraint names left unchanged (no cross-table collision)

Tests: 109 pass, 4 skipped.
This commit is contained in:
유창욱 2026-07-20 11:23:06 +09:00
parent 3a8a5106cd
commit 5d511b04f2
15 changed files with 106 additions and 82 deletions

View file

@ -129,7 +129,7 @@ php -r "echo password_hash('여기에_진짜_비밀번호', PASSWORD_ARGON2ID),
phpMyAdmin → SQL 탭: phpMyAdmin → SQL 탭:
```sql ```sql
INSERT INTO users (username, password_hash, role, site_scope, failed_attempts, locked_until) INSERT INTO move_users (username, password_hash, role, site_scope, failed_attempts, locked_until)
VALUES ('admin', '여기에_해시_붙여넣기', 'admin', NULL, 0, 0); VALUES ('admin', '여기에_해시_붙여넣기', 'admin', NULL, 0, 0);
``` ```
@ -144,7 +144,7 @@ php -r "echo bin2hex(random_bytes(32)), \"\n\";"
``` ```
```sql ```sql
INSERT INTO devices (device_id, name, secret, config_version) INSERT INTO move_devices (device_id, name, secret, config_version)
VALUES ('pi-01', '3라인 낙상 카메라', '여기에_비밀키_붙여넣기', 1); VALUES ('pi-01', '3라인 낙상 카메라', '여기에_비밀키_붙여넣기', 1);
``` ```

View file

@ -188,14 +188,14 @@ php -r "echo bin2hex(random_bytes(32)), \"\n\";"
``` ```
```sql ```sql
INSERT INTO devices (device_id, name, secret, config_version) INSERT INTO move_devices (device_id, name, secret, config_version)
VALUES ('pi-01', 'Line 3 — Fall Cam', '<paste the generated secret here>', 1); VALUES ('pi-01', 'Line 3 — Fall Cam', '<paste the generated secret here>', 1);
``` ```
Deliver the secret to the device provisioning process out-of-band (not email/chat) and Deliver the secret to the device provisioning process out-of-band (not email/chat) and
configure it as the device's `HMAC` signing key for `X-Signature` on configure it as the device's `HMAC` signing key for `X-Signature` on
`/api/events`, `/api/heartbeat`, and `/api/config`. Rotate a compromised secret by `/api/events`, `/api/heartbeat`, and `/api/config`. Rotate a compromised secret by
`UPDATE devices SET secret = ? WHERE device_id = ?` with a freshly generated value — the `UPDATE move_devices SET secret = ? WHERE device_id = ?` with a freshly generated value — the
old value stops working immediately since `secretFor()` always reads the current row. old value stops working immediately since `secretFor()` always reads the current row.
## 7. First admin account ## 7. First admin account
@ -215,7 +215,7 @@ php -r "echo password_hash('CHANGE-ME-to-a-real-passphrase', PASSWORD_ARGON2ID),
`PASSWORD_ARGON2ID`.) `PASSWORD_ARGON2ID`.)
```sql ```sql
INSERT INTO users (username, password_hash, role, site_scope, failed_attempts, locked_until) INSERT INTO move_users (username, password_hash, role, site_scope, failed_attempts, locked_until)
VALUES ('admin', '<paste the argon2id hash here>', 'admin', NULL, 0, 0); VALUES ('admin', '<paste the argon2id hash here>', 'admin', NULL, 0, 0);
``` ```

View file

@ -21,27 +21,27 @@
-- =========================================================================== -- ===========================================================================
-- A1) devices.site — the access-control site each device belongs to. -- A1) devices.site — the access-control site each device belongs to.
ALTER TABLE devices ALTER TABLE move_devices
ADD COLUMN site VARCHAR(64) NULL AFTER name, ADD COLUMN site VARCHAR(64) NULL AFTER name,
ADD KEY ix_devices_site (site); ADD KEY ix_devices_site (site);
-- A2) events.site — denormalized from devices.site at ingest, for the dashboard's -- A2) events.site — denormalized from move_devices.site at ingest, for the dashboard's
-- site-scoped event list. -- site-scoped event list.
ALTER TABLE events ALTER TABLE move_events
ADD COLUMN site VARCHAR(64) NULL AFTER device_id, ADD COLUMN site VARCHAR(64) NULL AFTER device_id,
ADD KEY ix_events_site_occurred_at (site, occurred_at); ADD KEY ix_events_site_occurred_at (site, occurred_at);
-- A3) events.event_uuid — edge-generated idempotency key (P1-9). The UNIQUE key -- A3) events.event_uuid — edge-generated idempotency key (P1-9). The UNIQUE key
-- makes a re-sent event collide (dedup) instead of duplicating. MySQL allows -- makes a re-sent event collide (dedup) instead of duplicating. MySQL allows
-- multiple NULLs in a UNIQUE index, so legacy rows without a uuid are fine. -- multiple NULLs in a UNIQUE index, so legacy rows without a uuid are fine.
ALTER TABLE events ALTER TABLE move_events
ADD COLUMN event_uuid CHAR(32) NULL AFTER event_id, ADD COLUMN event_uuid CHAR(32) NULL AFTER event_id,
ADD UNIQUE KEY uq_events_uuid (event_uuid); ADD UNIQUE KEY uq_events_uuid (event_uuid);
-- A4) event_snapshots.created_at index — PdoSnapshotRepository::purgeOlderThan() -- A4) event_snapshots.created_at index — PdoSnapshotRepository::purgeOlderThan()
-- runs "SELECT ... WHERE created_at < ?" inside live ingest requests. Without -- runs "SELECT ... WHERE created_at < ?" inside live ingest requests. Without
-- this index it full-scans the snapshot table on every triggered purge. -- this index it full-scans the snapshot table on every triggered purge.
ALTER TABLE event_snapshots ALTER TABLE move_event_snapshots
ADD KEY ix_event_snapshots_created (created_at); ADD KEY ix_event_snapshots_created (created_at);
-- A5) event_snapshots (event_id, content_hash) UNIQUE — lets the ingest dedup path -- A5) event_snapshots (event_id, content_hash) UNIQUE — lets the ingest dedup path
@ -50,21 +50,21 @@ ALTER TABLE event_snapshots
-- collides instead of writing a second copy). If this fails on "Duplicate -- collides instead of writing a second copy). If this fails on "Duplicate
-- entry", the table already has duplicate (event_id, content_hash) rows — -- entry", the table already has duplicate (event_id, content_hash) rows —
-- remove the dups, then re-run this one statement. -- remove the dups, then re-run this one statement.
ALTER TABLE event_snapshots ALTER TABLE move_event_snapshots
ADD UNIQUE KEY uq_event_snapshots_event_hash (event_id, content_hash); ADD UNIQUE KEY uq_event_snapshots_event_hash (event_id, content_hash);
-- A6) rate_limits.window_start index — the reap "DELETE ... WHERE window_start < ?" -- A6) rate_limits.window_start index — the reap "DELETE ... WHERE window_start < ?"
-- cannot use the (rate_key, window_start) PRIMARY KEY (window_start is the -- cannot use the (rate_key, window_start) PRIMARY KEY (window_start is the
-- trailing column), so it full-scans and locks rows on the pre-auth hot path. -- trailing column), so it full-scans and locks rows on the pre-auth hot path.
ALTER TABLE rate_limits ALTER TABLE move_rate_limits
ADD KEY ix_rate_limits_window (window_start); ADD KEY ix_rate_limits_window (window_start);
-- =========================================================================== -- ===========================================================================
-- PART B — provisioning (EDIT the values for your deployment, then run). -- PART B — provisioning (EDIT the values for your deployment, then run).
-- Assign every device to its site here; the backfill in Part C reads it. -- Assign every device to its site here; the backfill in Part C reads it.
-- =========================================================================== -- ===========================================================================
-- UPDATE devices SET site = 'factory-a' WHERE device_id = 'pi-1'; -- UPDATE move_devices SET site = 'factory-a' WHERE device_id = 'pi-1';
-- UPDATE devices SET site = 'factory-b' WHERE device_id = 'pi-2'; -- UPDATE move_devices SET site = 'factory-b' WHERE device_id = 'pi-2';
-- -- ... one row per device ... -- -- ... one row per device ...
-- =========================================================================== -- ===========================================================================
@ -72,8 +72,8 @@ ALTER TABLE rate_limits
-- RUN THIS ONLY AFTER Part B. Re-run it whenever you add/relabel a -- RUN THIS ONLY AFTER Part B. Re-run it whenever you add/relabel a
-- device's site so its past events reappear for scoped operators. -- device's site so its past events reappear for scoped operators.
-- =========================================================================== -- ===========================================================================
UPDATE events e UPDATE move_events e
JOIN devices d ON d.device_id = e.device_id JOIN move_devices d ON d.device_id = e.device_id
SET e.site = d.site SET e.site = d.site
WHERE e.site IS NULL AND d.site IS NOT NULL; WHERE e.site IS NULL AND d.site IS NOT NULL;
@ -82,17 +82,17 @@ UPDATE events e
-- see their backfilled history). NULL = global operator; '' is NOT -- see their backfilled history). NULL = global operator; '' is NOT
-- global (matches nothing — an accidental blank fails closed). -- global (matches nothing — an accidental blank fails closed).
-- =========================================================================== -- ===========================================================================
-- UPDATE users SET site_scope = 'factory-a' WHERE username = 'operator-a'; -- UPDATE move_users SET site_scope = 'factory-a' WHERE username = 'operator-a';
-- ---- Verification (run these; each must confirm the object/state) ----------- -- ---- Verification (run these; each must confirm the object/state) -----------
-- SHOW COLUMNS FROM devices LIKE 'site'; -- SHOW COLUMNS FROM move_devices LIKE 'site';
-- SHOW COLUMNS FROM events LIKE 'site'; -- SHOW COLUMNS FROM move_events LIKE 'site';
-- SHOW COLUMNS FROM events LIKE 'event_uuid'; -- SHOW COLUMNS FROM move_events LIKE 'event_uuid';
-- SHOW INDEX FROM events WHERE Key_name = 'uq_events_uuid'; -- SHOW INDEX FROM move_events WHERE Key_name = 'uq_events_uuid';
-- SHOW INDEX FROM event_snapshots WHERE Key_name = 'ix_event_snapshots_created'; -- SHOW INDEX FROM move_event_snapshots WHERE Key_name = 'ix_event_snapshots_created';
-- SHOW INDEX FROM event_snapshots WHERE Key_name = 'uq_event_snapshots_event_hash'; -- SHOW INDEX FROM move_event_snapshots WHERE Key_name = 'uq_event_snapshots_event_hash';
-- SHOW INDEX FROM rate_limits WHERE Key_name = 'ix_rate_limits_window'; -- SHOW INDEX FROM move_rate_limits WHERE Key_name = 'ix_rate_limits_window';
-- Events still unscoped after Part C (expect 0, or only rows whose device has no -- Events still unscoped after Part C (expect 0, or only rows whose device has no
-- site assigned yet — go back and finish Part B for those devices): -- site assigned yet — go back and finish Part B for those devices):
-- SELECT COUNT(*) FROM events e JOIN devices d ON d.device_id = e.device_id -- SELECT COUNT(*) FROM move_events e JOIN move_devices d ON d.device_id = e.device_id
-- WHERE e.site IS NULL AND d.site IS NOT NULL; -- WHERE e.site IS NULL AND d.site IS NOT NULL;

View file

@ -0,0 +1,19 @@
-- cloud/migrations/2026-07-20-move-table-prefix.sql
-- Rename every P1-B table to the `move_` prefix (shared-hosting DB co-tenancy).
-- Run ONCE in phpMyAdmin BEFORE deploying code that reads the prefixed names.
-- Fresh installs: skip this, cloud/schema.sql already creates prefixed tables.
--
-- RENAME TABLE is atomic across all pairs and preserves indexes, constraints
-- and FKs (fk_event_snapshots_event follows its parent automatically).
RENAME TABLE
users TO move_users,
devices TO move_devices,
cameras TO move_cameras,
zones TO move_zones,
events TO move_events,
event_snapshots TO move_event_snapshots,
thresholds TO move_thresholds,
nonces TO move_nonces,
rate_limits TO move_rate_limits,
audit_log TO move_audit_log;

View file

@ -36,7 +36,7 @@ SET NAMES utf8mb4;
-- No PdoUserRepository::create() exists in P1-B -- the first admin account is -- No PdoUserRepository::create() exists in P1-B -- the first admin account is
-- provisioned out-of-band via the CLI snippet in cloud/docs/cloud-deploy.md. -- provisioned out-of-band via the CLI snippet in cloud/docs/cloud-deploy.md.
-- --------------------------------------------------------------------------- -- ---------------------------------------------------------------------------
CREATE TABLE users ( CREATE TABLE move_users (
id INT UNSIGNED NOT NULL AUTO_INCREMENT, id INT UNSIGNED NOT NULL AUTO_INCREMENT,
username VARCHAR(190) NOT NULL, username VARCHAR(190) NOT NULL,
password_hash VARCHAR(255) NOT NULL, -- Argon2id, see App\Security\Password::hash() password_hash VARCHAR(255) NOT NULL, -- Argon2id, see App\Security\Password::hash()
@ -55,7 +55,7 @@ CREATE TABLE users (
-- No PdoDeviceRepository::create() exists -- rows are provisioned by an admin -- No PdoDeviceRepository::create() exists -- rows are provisioned by an admin
-- (per-device secret issuance) before the device is allowed to sync/ingest. -- (per-device secret issuance) before the device is allowed to sync/ingest.
-- --------------------------------------------------------------------------- -- ---------------------------------------------------------------------------
CREATE TABLE devices ( CREATE TABLE move_devices (
device_id VARCHAR(64) NOT NULL, device_id VARCHAR(64) NOT NULL,
name VARCHAR(190) NULL, name VARCHAR(190) NULL,
site VARCHAR(64) NULL, -- access-control scope; matches users.site_scope (NULL = unassigned) site VARCHAR(64) NULL, -- access-control scope; matches users.site_scope (NULL = unassigned)
@ -77,7 +77,7 @@ CREATE TABLE devices (
-- directly as a free-form string. Kept for documentation/future per-camera -- directly as a free-form string. Kept for documentation/future per-camera
-- config and to avoid a breaking migration when that repository is added. -- config and to avoid a breaking migration when that repository is added.
-- --------------------------------------------------------------------------- -- ---------------------------------------------------------------------------
CREATE TABLE cameras ( CREATE TABLE move_cameras (
camera_id VARCHAR(64) NOT NULL, camera_id VARCHAR(64) NOT NULL,
device_id VARCHAR(64) NOT NULL, device_id VARCHAR(64) NOT NULL,
name VARCHAR(190) NULL, name VARCHAR(190) NULL,
@ -94,7 +94,7 @@ CREATE TABLE cameras (
-- `cameras` row exists for a given camera_id (no camera-registration flow in -- `cameras` row exists for a given camera_id (no camera-registration flow in
-- P1-B), so a FK here would spuriously reject valid app writes. -- P1-B), so a FK here would spuriously reject valid app writes.
-- --------------------------------------------------------------------------- -- ---------------------------------------------------------------------------
CREATE TABLE zones ( CREATE TABLE move_zones (
zone_id CHAR(32) NOT NULL, -- bin2hex(random_bytes(16)), same convention as snapshot uuids zone_id CHAR(32) NOT NULL, -- bin2hex(random_bytes(16)), same convention as snapshot uuids
device_id VARCHAR(64) NOT NULL, device_id VARCHAR(64) NOT NULL,
camera_id VARCHAR(64) NOT NULL, camera_id VARCHAR(64) NOT NULL,
@ -112,11 +112,11 @@ CREATE TABLE zones (
-- App\Ingest\EventValidator before insert(); the CHECK constraints below are -- App\Ingest\EventValidator before insert(); the CHECK constraints below are
-- defense-in-depth only, not the primary validation layer. -- defense-in-depth only, not the primary validation layer.
-- --------------------------------------------------------------------------- -- ---------------------------------------------------------------------------
CREATE TABLE events ( CREATE TABLE move_events (
event_id INT UNSIGNED NOT NULL AUTO_INCREMENT, event_id INT UNSIGNED NOT NULL AUTO_INCREMENT,
event_uuid CHAR(32) NULL, -- edge-generated idempotency key (dedup a re-sent event); NULL for legacy rows event_uuid CHAR(32) NULL, -- edge-generated idempotency key (dedup a re-sent event); NULL for legacy rows
device_id VARCHAR(64) NOT NULL, device_id VARCHAR(64) NOT NULL,
site VARCHAR(64) NULL, -- denormalized from devices.site at ingest; access-control scope site VARCHAR(64) NULL, -- denormalized from move_devices.site at ingest; access-control scope
camera_id VARCHAR(64) NOT NULL, camera_id VARCHAR(64) NOT NULL,
event_type VARCHAR(32) NOT NULL, event_type VARCHAR(32) NOT NULL,
severity VARCHAR(16) NOT NULL, severity VARCHAR(16) NOT NULL,
@ -150,7 +150,7 @@ CREATE TABLE events (
-- events is safe because IngestController always inserts the parent event row -- events is safe because IngestController always inserts the parent event row
-- before calling SnapshotRepository::add() for its snapshots. -- before calling SnapshotRepository::add() for its snapshots.
-- --------------------------------------------------------------------------- -- ---------------------------------------------------------------------------
CREATE TABLE event_snapshots ( CREATE TABLE move_event_snapshots (
snapshot_id INT UNSIGNED NOT NULL AUTO_INCREMENT, snapshot_id INT UNSIGNED NOT NULL AUTO_INCREMENT,
event_id INT UNSIGNED NOT NULL, event_id INT UNSIGNED NOT NULL,
file_uuid CHAR(32) NOT NULL, -- bin2hex(random_bytes(16)); on-disk filename = <uuid>.jpg file_uuid CHAR(32) NOT NULL, -- bin2hex(random_bytes(16)); on-disk filename = <uuid>.jpg
@ -164,7 +164,7 @@ CREATE TABLE event_snapshots (
UNIQUE KEY uq_event_snapshots_event_hash (event_id, content_hash), UNIQUE KEY uq_event_snapshots_event_hash (event_id, content_hash),
KEY ix_event_snapshots_event (event_id), KEY ix_event_snapshots_event (event_id),
KEY ix_event_snapshots_created (created_at), -- retention purge scans WHERE created_at < ? KEY ix_event_snapshots_created (created_at), -- retention purge scans WHERE created_at < ?
CONSTRAINT fk_event_snapshots_event FOREIGN KEY (event_id) REFERENCES events (event_id) CONSTRAINT fk_event_snapshots_event FOREIGN KEY (event_id) REFERENCES move_events (event_id)
ON DELETE CASCADE ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
@ -173,7 +173,7 @@ CREATE TABLE event_snapshots (
-- GET /api/config. No PdoThresholdRepository write method exists in P1-B -- -- GET /api/config. No PdoThresholdRepository write method exists in P1-B --
-- rows are seeded/edited directly (phpMyAdmin) until a management UI lands. -- rows are seeded/edited directly (phpMyAdmin) until a management UI lands.
-- --------------------------------------------------------------------------- -- ---------------------------------------------------------------------------
CREATE TABLE thresholds ( CREATE TABLE move_thresholds (
id INT UNSIGNED NOT NULL AUTO_INCREMENT, id INT UNSIGNED NOT NULL AUTO_INCREMENT,
device_id VARCHAR(64) NOT NULL, device_id VARCHAR(64) NOT NULL,
camera_id VARCHAR(64) NULL, camera_id VARCHAR(64) NULL,
@ -194,11 +194,11 @@ CREATE TABLE thresholds (
-- TOCTOU flagged in the Task 7 review -- the PK uniqueness is the real guard, -- TOCTOU flagged in the Task 7 review -- the PK uniqueness is the real guard,
-- not the earlier seen() check). -- not the earlier seen() check).
-- --------------------------------------------------------------------------- -- ---------------------------------------------------------------------------
CREATE TABLE nonces ( CREATE TABLE move_nonces (
nonce VARCHAR(190) NOT NULL, nonce VARCHAR(190) NOT NULL,
expires_at BIGINT UNSIGNED NOT NULL, expires_at BIGINT UNSIGNED NOT NULL,
PRIMARY KEY (nonce), PRIMARY KEY (nonce),
KEY ix_nonces_expires_at (expires_at) -- for a periodic `DELETE FROM nonces WHERE expires_at < ?` cleanup job KEY ix_nonces_expires_at (expires_at) -- for a periodic `DELETE FROM move_nonces WHERE expires_at < ?` cleanup job
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- --------------------------------------------------------------------------- -- ---------------------------------------------------------------------------
@ -207,7 +207,7 @@ CREATE TABLE nonces (
-- `INSERT ... ON DUPLICATE KEY UPDATE count = count + 1` upsert in -- `INSERT ... ON DUPLICATE KEY UPDATE count = count + 1` upsert in
-- PdoRateLimiterRepository::hit() -- no read-modify-write race window. -- PdoRateLimiterRepository::hit() -- no read-modify-write race window.
-- --------------------------------------------------------------------------- -- ---------------------------------------------------------------------------
CREATE TABLE rate_limits ( CREATE TABLE move_rate_limits (
rate_key VARCHAR(190) NOT NULL, rate_key VARCHAR(190) NOT NULL,
window_start BIGINT UNSIGNED NOT NULL, -- floor(now / 60): the minute bucket window_start BIGINT UNSIGNED NOT NULL, -- floor(now / 60): the minute bucket
count INT UNSIGNED NOT NULL DEFAULT 0, count INT UNSIGNED NOT NULL DEFAULT 0,
@ -222,7 +222,7 @@ CREATE TABLE rate_limits (
-- Tamper-evident hash-chaining (design doc §9.2) is deferred to P3 per the -- Tamper-evident hash-chaining (design doc §9.2) is deferred to P3 per the
-- Task 16 brief self-review -- this table is the plain append-only record only. -- Task 16 brief self-review -- this table is the plain append-only record only.
-- --------------------------------------------------------------------------- -- ---------------------------------------------------------------------------
CREATE TABLE audit_log ( CREATE TABLE move_audit_log (
id INT UNSIGNED NOT NULL AUTO_INCREMENT, id INT UNSIGNED NOT NULL AUTO_INCREMENT,
actor VARCHAR(190) NOT NULL, actor VARCHAR(190) NOT NULL,
action VARCHAR(64) NOT NULL, action VARCHAR(64) NOT NULL,
@ -241,13 +241,18 @@ CREATE TABLE audit_log (
-- run. Apply the RUNNABLE migration instead: -- run. Apply the RUNNABLE migration instead:
-- --
-- cloud/migrations/2026-07-17-p1-9-p3-2b.sql -- cloud/migrations/2026-07-17-p1-9-p3-2b.sql
-- cloud/migrations/2026-07-20-move-table-prefix.sql (run this one LAST)
--
-- The second migration renames every table to the `move_` prefix used above and
-- by App\Repository\Pdo\* -- an existing DB must be renamed before deploy or all
-- queries fail with "table doesn't exist".
-- --
-- It adds devices.site / events.site / events.event_uuid, the retention and -- It adds devices.site / events.site / events.event_uuid, the retention and
-- rate-limit reap indexes, AND backfills events.site from devices.site (without -- rate-limit reap indexes, AND backfills events.site from move_devices.site (without
-- that backfill, historical events vanish for a scoped operator). Run it once in -- that backfill, historical events vanish for a scoped operator). Run it once in
-- phpMyAdmin BEFORE deploying code that reads these columns, then provision: -- phpMyAdmin BEFORE deploying code that reads these columns, then provision:
-- UPDATE devices SET site = 'factory-a' WHERE device_id = 'pi-1'; -- UPDATE move_devices SET site = 'factory-a' WHERE device_id = 'pi-1';
-- UPDATE users SET site_scope = 'factory-a' WHERE username = 'operator-a'; -- UPDATE move_users SET site_scope = 'factory-a' WHERE username = 'operator-a';
-- A user with site_scope = NULL is a global operator; '' is NOT global (matches -- A user with site_scope = NULL is a global operator; '' is NOT global (matches
-- nothing — a blank scope fails closed). -- nothing — a blank scope fails closed).
-- =========================================================================== -- ===========================================================================

View file

@ -12,7 +12,7 @@ final class PdoAuditRepository implements AuditRepository
public function append(string $actor, string $action, string $target, array $before, array $after): void public function append(string $actor, string $action, string $target, array $before, array $after): void
{ {
$st = $this->pdo->prepare( $st = $this->pdo->prepare(
'INSERT INTO audit_log (actor, action, target, before_json, after_json, ts) 'INSERT INTO move_audit_log (actor, action, target, before_json, after_json, ts)
VALUES (?, ?, ?, ?, ?, ?)' VALUES (?, ?, ?, ?, ?, ?)'
); );
$st->execute([ $st->execute([

View file

@ -11,7 +11,7 @@ final class PdoDeviceRepository implements DeviceRepository
public function secretFor(string $deviceId): ?string public function secretFor(string $deviceId): ?string
{ {
$st = $this->pdo->prepare('SELECT secret FROM devices WHERE device_id = ? LIMIT 1'); $st = $this->pdo->prepare('SELECT secret FROM move_devices WHERE device_id = ? LIMIT 1');
$st->execute([$deviceId]); $st->execute([$deviceId]);
$secret = $st->fetchColumn(); $secret = $st->fetchColumn();
return $secret === false ? null : (string)$secret; return $secret === false ? null : (string)$secret;
@ -19,7 +19,7 @@ final class PdoDeviceRepository implements DeviceRepository
public function siteFor(string $deviceId): ?string public function siteFor(string $deviceId): ?string
{ {
$st = $this->pdo->prepare('SELECT site FROM devices WHERE device_id = ? LIMIT 1'); $st = $this->pdo->prepare('SELECT site FROM move_devices WHERE device_id = ? LIMIT 1');
$st->execute([$deviceId]); $st->execute([$deviceId]);
$site = $st->fetchColumn(); $site = $st->fetchColumn();
// false = no such device; null = device exists but unassigned. Both -> null (unscoped). // false = no such device; null = device exists but unassigned. Both -> null (unscoped).
@ -34,7 +34,7 @@ final class PdoDeviceRepository implements DeviceRepository
// out-of-band by an admin (see cloud/docs/cloud-deploy.md), never created // out-of-band by an admin (see cloud/docs/cloud-deploy.md), never created
// implicitly from an inbound heartbeat. // implicitly from an inbound heartbeat.
$st = $this->pdo->prepare( $st = $this->pdo->prepare(
'UPDATE devices 'UPDATE move_devices
SET last_fps = ?, last_temp = ?, last_throttle = ?, SET last_fps = ?, last_temp = ?, last_throttle = ?,
reported_config_version = ?, last_heartbeat_at = ? reported_config_version = ?, last_heartbeat_at = ?
WHERE device_id = ?' WHERE device_id = ?'
@ -51,7 +51,7 @@ final class PdoDeviceRepository implements DeviceRepository
public function configVersion(string $deviceId): int public function configVersion(string $deviceId): int
{ {
$st = $this->pdo->prepare('SELECT config_version FROM devices WHERE device_id = ? LIMIT 1'); $st = $this->pdo->prepare('SELECT config_version FROM move_devices WHERE device_id = ? LIMIT 1');
$st->execute([$deviceId]); $st->execute([$deviceId]);
$version = $st->fetchColumn(); $version = $st->fetchColumn();
return $version === false ? 1 : (int)$version; return $version === false ? 1 : (int)$version;

View file

@ -16,7 +16,7 @@ final class PdoEventRepository implements EventRepository
public function insert(array $event): int public function insert(array $event): int
{ {
$st = $this->pdo->prepare( $st = $this->pdo->prepare(
'INSERT INTO events 'INSERT INTO move_events
(event_uuid, device_id, site, camera_id, event_type, severity, track_id, zone_id, (event_uuid, device_id, site, camera_id, event_type, severity, track_id, zone_id,
occurred_at, duration_s, metrics_json, received_at) occurred_at, duration_s, metrics_json, received_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)' VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)'
@ -24,7 +24,7 @@ final class PdoEventRepository implements EventRepository
$st->execute([ $st->execute([
$event['event_uuid'] ?? null, // NULL when the edge sent no idempotency key (legacy) $event['event_uuid'] ?? null, // NULL when the edge sent no idempotency key (legacy)
(string)$event['device_id'], (string)$event['device_id'],
$event['site'] ?? null, // denormalized from devices.site by IngestController $event['site'] ?? null, // denormalized from move_devices.site by IngestController
(string)$event['camera_id'], (string)$event['camera_id'],
(string)$event['event_type'], (string)$event['event_type'],
(string)$event['severity'], (string)$event['severity'],
@ -40,7 +40,7 @@ final class PdoEventRepository implements EventRepository
public function idByUuid(string $uuid): ?int public function idByUuid(string $uuid): ?int
{ {
$st = $this->pdo->prepare('SELECT event_id FROM events WHERE event_uuid = ? LIMIT 1'); $st = $this->pdo->prepare('SELECT event_id FROM move_events WHERE event_uuid = ? LIMIT 1');
$st->execute([$uuid]); $st->execute([$uuid]);
$id = $st->fetchColumn(); $id = $st->fetchColumn();
return $id === false ? null : (int)$id; return $id === false ? null : (int)$id;
@ -51,7 +51,7 @@ final class PdoEventRepository implements EventRepository
if ($severity !== null) { if ($severity !== null) {
$st = $this->pdo->prepare( $st = $this->pdo->prepare(
'SELECT ' . self::SELECT_COLUMNS . ' 'SELECT ' . self::SELECT_COLUMNS . '
FROM events WHERE severity = ? FROM move_events WHERE severity = ?
ORDER BY occurred_at DESC, event_id DESC LIMIT ?' ORDER BY occurred_at DESC, event_id DESC LIMIT ?'
); );
$st->bindValue(1, $severity, PDO::PARAM_STR); $st->bindValue(1, $severity, PDO::PARAM_STR);
@ -59,7 +59,7 @@ final class PdoEventRepository implements EventRepository
} else { } else {
$st = $this->pdo->prepare( $st = $this->pdo->prepare(
'SELECT ' . self::SELECT_COLUMNS . ' 'SELECT ' . self::SELECT_COLUMNS . '
FROM events FROM move_events
ORDER BY occurred_at DESC, event_id DESC LIMIT ?' ORDER BY occurred_at DESC, event_id DESC LIMIT ?'
); );
$st->bindValue(1, $limit, PDO::PARAM_INT); $st->bindValue(1, $limit, PDO::PARAM_INT);
@ -92,7 +92,7 @@ final class PdoEventRepository implements EventRepository
$binds[] = [$beforeTs, PDO::PARAM_STR]; $binds[] = [$beforeTs, PDO::PARAM_STR];
$binds[] = [$beforeId, PDO::PARAM_INT]; $binds[] = [$beforeId, PDO::PARAM_INT];
} }
$sql = 'SELECT ' . self::SELECT_COLUMNS . ' FROM events'; $sql = 'SELECT ' . self::SELECT_COLUMNS . ' FROM move_events';
if ($where) $sql .= ' WHERE ' . implode(' AND ', $where); if ($where) $sql .= ' WHERE ' . implode(' AND ', $where);
$sql .= ' ORDER BY occurred_at DESC, event_id DESC LIMIT ?'; $sql .= ' ORDER BY occurred_at DESC, event_id DESC LIMIT ?';
$binds[] = [$limit, PDO::PARAM_INT]; $binds[] = [$limit, PDO::PARAM_INT];
@ -106,7 +106,7 @@ final class PdoEventRepository implements EventRepository
public function find(int $id): ?array public function find(int $id): ?array
{ {
$st = $this->pdo->prepare('SELECT ' . self::SELECT_COLUMNS . ' FROM events WHERE event_id = ?'); $st = $this->pdo->prepare('SELECT ' . self::SELECT_COLUMNS . ' FROM move_events WHERE event_id = ?');
$st->execute([$id]); $st->execute([$id]);
$row = $st->fetch(); $row = $st->fetch();
return $row === false ? null : self::mapRow($row); return $row === false ? null : self::mapRow($row);
@ -119,13 +119,13 @@ final class PdoEventRepository implements EventRepository
// "changed rows" rather than "matched rows": re-acknowledging an // "changed rows" rather than "matched rows": re-acknowledging an
// already-acknowledged event by the same user would otherwise report // already-acknowledged event by the same user would otherwise report
// 0 affected rows and this method would wrongly return false. // 0 affected rows and this method would wrongly return false.
$exists = $this->pdo->prepare('SELECT 1 FROM events WHERE event_id = ? LIMIT 1'); $exists = $this->pdo->prepare('SELECT 1 FROM move_events WHERE event_id = ? LIMIT 1');
$exists->execute([$id]); $exists->execute([$id]);
if ($exists->fetchColumn() === false) { if ($exists->fetchColumn() === false) {
return false; return false;
} }
$st = $this->pdo->prepare( $st = $this->pdo->prepare(
'UPDATE events SET acknowledged_by = ?, acknowledged_at = ? WHERE event_id = ?' 'UPDATE move_events SET acknowledged_by = ?, acknowledged_at = ? WHERE event_id = ?'
); );
$st->execute([$user, time(), $id]); $st->execute([$user, time(), $id]);
return true; return true;

View file

@ -20,20 +20,20 @@ final class PdoNonceRepository implements NonceRepository
public function seen(string $nonce): bool public function seen(string $nonce): bool
{ {
$st = $this->pdo->prepare('SELECT 1 FROM nonces WHERE nonce = ? LIMIT 1'); $st = $this->pdo->prepare('SELECT 1 FROM move_nonces WHERE nonce = ? LIMIT 1');
$st->execute([$nonce]); $st->execute([$nonce]);
return (bool)$st->fetchColumn(); return (bool)$st->fetchColumn();
} }
public function remember(string $nonce, int $expiresAt): void public function remember(string $nonce, int $expiresAt): void
{ {
$st = $this->pdo->prepare('INSERT INTO nonces (nonce, expires_at) VALUES (?, ?)'); $st = $this->pdo->prepare('INSERT INTO move_nonces (nonce, expires_at) VALUES (?, ?)');
$st->execute([$nonce, $expiresAt]); $st->execute([$nonce, $expiresAt]);
// Cron-less reap: Cafe24 has no scheduler, so expired nonces accumulate forever until // Cron-less reap: Cafe24 has no scheduler, so expired nonces accumulate forever until
// the table bloats and inserts slow/fail. Opportunistically prune a bounded batch of // the table bloats and inserts slow/fail. Opportunistically prune a bounded batch of
// already-expired rows (safe: seen() only cares about unexpired ones). // already-expired rows (safe: seen() only cares about unexpired ones).
if (mt_rand(1, 100) === 1) { if (mt_rand(1, 100) === 1) {
$del = $this->pdo->prepare('DELETE FROM nonces WHERE expires_at < ? LIMIT 5000'); $del = $this->pdo->prepare('DELETE FROM move_nonces WHERE expires_at < ? LIMIT 5000');
$del->execute([time()]); $del->execute([time()]);
} }
} }

View file

@ -18,13 +18,13 @@ final class PdoRateLimiterRepository implements RateLimiterRepository
public function hit(string $key, int $windowStart): int public function hit(string $key, int $windowStart): int
{ {
$upsert = $this->pdo->prepare( $upsert = $this->pdo->prepare(
'INSERT INTO rate_limits (rate_key, window_start, count) VALUES (?, ?, 1) 'INSERT INTO move_rate_limits (rate_key, window_start, count) VALUES (?, ?, 1)
ON DUPLICATE KEY UPDATE count = count + 1' ON DUPLICATE KEY UPDATE count = count + 1'
); );
$upsert->execute([$key, $windowStart]); $upsert->execute([$key, $windowStart]);
$select = $this->pdo->prepare( $select = $this->pdo->prepare(
'SELECT count FROM rate_limits WHERE rate_key = ? AND window_start = ?' 'SELECT count FROM move_rate_limits WHERE rate_key = ? AND window_start = ?'
); );
$select->execute([$key, $windowStart]); $select->execute([$key, $windowStart]);
$count = (int)$select->fetchColumn(); $count = (int)$select->fetchColumn();
@ -37,7 +37,7 @@ final class PdoRateLimiterRepository implements RateLimiterRepository
// count is already computed above; swallow reap failures and let the next hit retry. // count is already computed above; swallow reap failures and let the next hit retry.
if (mt_rand(1, 100) === 1) { if (mt_rand(1, 100) === 1) {
try { try {
$del = $this->pdo->prepare('DELETE FROM rate_limits WHERE window_start < ? LIMIT 5000'); $del = $this->pdo->prepare('DELETE FROM move_rate_limits WHERE window_start < ? LIMIT 5000');
$del->execute([$windowStart - 2]); $del->execute([$windowStart - 2]);
} catch (\PDOException $e) { } catch (\PDOException $e) {
error_log('rate_limits reap skipped: ' . $e->getMessage()); error_log('rate_limits reap skipped: ' . $e->getMessage());

View file

@ -12,7 +12,7 @@ final class PdoSnapshotRepository implements SnapshotRepository
public function add(int $eventId, string $fileUuid, string $contentHash): void public function add(int $eventId, string $fileUuid, string $contentHash): void
{ {
$st = $this->pdo->prepare( $st = $this->pdo->prepare(
'INSERT INTO event_snapshots (event_id, file_uuid, content_hash, created_at) 'INSERT INTO move_event_snapshots (event_id, file_uuid, content_hash, created_at)
VALUES (?, ?, ?, ?)' VALUES (?, ?, ?, ?)'
); );
$st->execute([$eventId, $fileUuid, $contentHash, time()]); $st->execute([$eventId, $fileUuid, $contentHash, time()]);
@ -21,7 +21,7 @@ final class PdoSnapshotRepository implements SnapshotRepository
public function forEvent(int $eventId): array public function forEvent(int $eventId): array
{ {
$st = $this->pdo->prepare( $st = $this->pdo->prepare(
'SELECT file_uuid, content_hash FROM event_snapshots 'SELECT file_uuid, content_hash FROM move_event_snapshots
WHERE event_id = ? ORDER BY snapshot_id ASC' WHERE event_id = ? ORDER BY snapshot_id ASC'
); );
$st->execute([$eventId]); $st->execute([$eventId]);
@ -39,7 +39,7 @@ final class PdoSnapshotRepository implements SnapshotRepository
return null; return null;
} }
$st = $this->pdo->prepare('SELECT 1 FROM event_snapshots WHERE file_uuid = ? LIMIT 1'); $st = $this->pdo->prepare('SELECT 1 FROM move_event_snapshots WHERE file_uuid = ? LIMIT 1');
$st->execute([$uuid]); $st->execute([$uuid]);
if ($st->fetchColumn() === false) { if ($st->fetchColumn() === false) {
return null; return null;
@ -53,7 +53,7 @@ final class PdoSnapshotRepository implements SnapshotRepository
if (strlen($uuid) !== 32 || !ctype_xdigit($uuid)) { if (strlen($uuid) !== 32 || !ctype_xdigit($uuid)) {
return null; return null;
} }
$st = $this->pdo->prepare('SELECT event_id FROM event_snapshots WHERE file_uuid = ? LIMIT 1'); $st = $this->pdo->prepare('SELECT event_id FROM move_event_snapshots WHERE file_uuid = ? LIMIT 1');
$st->execute([$uuid]); $st->execute([$uuid]);
$id = $st->fetchColumn(); $id = $st->fetchColumn();
return $id === false ? null : (int)$id; return $id === false ? null : (int)$id;
@ -66,7 +66,7 @@ final class PdoSnapshotRepository implements SnapshotRepository
// row (file gone, row left) is harmless; an orphan file (row gone, file left) is the // row (file gone, row left) is harmless; an orphan file (row gone, file left) is the
// PIPA leak we are closing, so never delete the row before its file. // PIPA leak we are closing, so never delete the row before its file.
$sel = $this->pdo->prepare( $sel = $this->pdo->prepare(
'SELECT snapshot_id, file_uuid FROM event_snapshots WHERE created_at < ? LIMIT ?'); 'SELECT snapshot_id, file_uuid FROM move_event_snapshots WHERE created_at < ? LIMIT ?');
$sel->bindValue(1, $cutoff, PDO::PARAM_INT); $sel->bindValue(1, $cutoff, PDO::PARAM_INT);
$sel->bindValue(2, $limit, PDO::PARAM_INT); $sel->bindValue(2, $limit, PDO::PARAM_INT);
$sel->execute(); $sel->execute();
@ -92,7 +92,7 @@ final class PdoSnapshotRepository implements SnapshotRepository
return 0; return 0;
} }
$in = implode(',', array_fill(0, count($ids), '?')); $in = implode(',', array_fill(0, count($ids), '?'));
$del = $this->pdo->prepare("DELETE FROM event_snapshots WHERE snapshot_id IN ($in)"); $del = $this->pdo->prepare("DELETE FROM move_event_snapshots WHERE snapshot_id IN ($in)");
$del->execute($ids); $del->execute($ids);
return count($ids); return count($ids);
} }

View file

@ -11,7 +11,7 @@ final class PdoThresholdRepository implements ThresholdRepository
public function forDevice(string $deviceId): array public function forDevice(string $deviceId): array
{ {
$st = $this->pdo->prepare('SELECT `key`, `value` FROM thresholds WHERE device_id = ?'); $st = $this->pdo->prepare('SELECT `key`, `value` FROM move_thresholds WHERE device_id = ?');
$st->execute([$deviceId]); $st->execute([$deviceId]);
$out = []; $out = [];

View file

@ -13,7 +13,7 @@ final class PdoUserRepository implements UserRepository
{ {
$st = $this->pdo->prepare( $st = $this->pdo->prepare(
'SELECT id, username, password_hash, role, site_scope, failed_attempts, locked_until 'SELECT id, username, password_hash, role, site_scope, failed_attempts, locked_until
FROM users WHERE username = ? LIMIT 1' FROM move_users WHERE username = ? LIMIT 1'
); );
$st->execute([$username]); $st->execute([$username]);
$row = $st->fetch(); $row = $st->fetch();
@ -36,14 +36,14 @@ final class PdoUserRepository implements UserRepository
// Increment in SQL (not read-then-write in PHP) so concurrent failed // Increment in SQL (not read-then-write in PHP) so concurrent failed
// logins for the same account can't clobber each other's count. // logins for the same account can't clobber each other's count.
$st = $this->pdo->prepare( $st = $this->pdo->prepare(
'UPDATE users SET failed_attempts = failed_attempts + 1, locked_until = ? WHERE id = ?' 'UPDATE move_users SET failed_attempts = failed_attempts + 1, locked_until = ? WHERE id = ?'
); );
$st->execute([$lockUntil, $userId]); $st->execute([$lockUntil, $userId]);
} }
public function clearFailures(int $userId): void public function clearFailures(int $userId): void
{ {
$st = $this->pdo->prepare('UPDATE users SET failed_attempts = 0, locked_until = 0 WHERE id = ?'); $st = $this->pdo->prepare('UPDATE move_users SET failed_attempts = 0, locked_until = 0 WHERE id = ?');
$st->execute([$userId]); $st->execute([$userId]);
} }
} }

View file

@ -13,7 +13,7 @@ final class PdoZoneRepository implements ZoneRepository
public function find(string $zoneId): ?array public function find(string $zoneId): ?array
{ {
$st = $this->pdo->prepare( $st = $this->pdo->prepare(
'SELECT zone_id, device_id, camera_id, level, polygon_json FROM zones WHERE zone_id = ? LIMIT 1' 'SELECT zone_id, device_id, camera_id, level, polygon_json FROM move_zones WHERE zone_id = ? LIMIT 1'
); );
$st->execute([$zoneId]); $st->execute([$zoneId]);
$row = $st->fetch(); $row = $st->fetch();
@ -23,7 +23,7 @@ final class PdoZoneRepository implements ZoneRepository
public function forDevice(string $deviceId): array public function forDevice(string $deviceId): array
{ {
$st = $this->pdo->prepare( $st = $this->pdo->prepare(
'SELECT zone_id, device_id, camera_id, level, polygon_json FROM zones WHERE device_id = ?' 'SELECT zone_id, device_id, camera_id, level, polygon_json FROM move_zones WHERE device_id = ?'
); );
$st->execute([$deviceId]); $st->execute([$deviceId]);
return $st->fetchAll(); return $st->fetchAll();
@ -32,7 +32,7 @@ final class PdoZoneRepository implements ZoneRepository
public function forCamera(string $cameraId): array public function forCamera(string $cameraId): array
{ {
$st = $this->pdo->prepare( $st = $this->pdo->prepare(
'SELECT zone_id, device_id, camera_id, level, polygon_json FROM zones WHERE camera_id = ?' 'SELECT zone_id, device_id, camera_id, level, polygon_json FROM move_zones WHERE camera_id = ?'
); );
$st->execute([$cameraId]); $st->execute([$cameraId]);
return $st->fetchAll(); return $st->fetchAll();
@ -52,7 +52,7 @@ final class PdoZoneRepository implements ZoneRepository
$newId = bin2hex(random_bytes(16)); $newId = bin2hex(random_bytes(16));
$st = $this->pdo->prepare( $st = $this->pdo->prepare(
'INSERT INTO zones (zone_id, device_id, camera_id, level, polygon_json, updated_at) 'INSERT INTO move_zones (zone_id, device_id, camera_id, level, polygon_json, updated_at)
VALUES (?, ?, ?, ?, ?, ?)' VALUES (?, ?, ?, ?, ?, ?)'
); );
$st->execute([$newId, $deviceId, $cameraId, $level, $polygonJson, time()]); $st->execute([$newId, $deviceId, $cameraId, $level, $polygonJson, time()]);
@ -61,7 +61,7 @@ final class PdoZoneRepository implements ZoneRepository
public function delete(string $zoneId): bool public function delete(string $zoneId): bool
{ {
$st = $this->pdo->prepare('DELETE FROM zones WHERE zone_id = ?'); $st = $this->pdo->prepare('DELETE FROM move_zones WHERE zone_id = ?');
$st->execute([$zoneId]); $st->execute([$zoneId]);
return $st->rowCount() > 0; return $st->rowCount() > 0;
} }
@ -89,7 +89,7 @@ final class PdoZoneRepository implements ZoneRepository
string $polygonJson, string $polygonJson,
): string { ): string {
$owns = $this->pdo->prepare( $owns = $this->pdo->prepare(
'SELECT 1 FROM zones WHERE zone_id = ? AND device_id = ? AND camera_id = ? LIMIT 1' 'SELECT 1 FROM move_zones WHERE zone_id = ? AND device_id = ? AND camera_id = ? LIMIT 1'
); );
$owns->execute([$zoneId, $deviceId, $cameraId]); $owns->execute([$zoneId, $deviceId, $cameraId]);
if ($owns->fetchColumn() === false) { if ($owns->fetchColumn() === false) {
@ -97,7 +97,7 @@ final class PdoZoneRepository implements ZoneRepository
} }
$st = $this->pdo->prepare( $st = $this->pdo->prepare(
'UPDATE zones SET level = ?, polygon_json = ?, updated_at = ? 'UPDATE move_zones SET level = ?, polygon_json = ?, updated_at = ?
WHERE zone_id = ? AND device_id = ? AND camera_id = ?' WHERE zone_id = ? AND device_id = ? AND camera_id = ?'
); );
$st->execute([$level, $polygonJson, time(), $zoneId, $deviceId, $cameraId]); $st->execute([$level, $polygonJson, time(), $zoneId, $deviceId, $cameraId]);
@ -108,7 +108,7 @@ final class PdoZoneRepository implements ZoneRepository
// error) or (b) the zone was deleted between the ownership check // error) or (b) the zone was deleted between the ownership check
// above and this UPDATE. Disambiguate with a targeted existence // above and this UPDATE. Disambiguate with a targeted existence
// check instead of assuming either case. // check instead of assuming either case.
$stillExists = $this->pdo->prepare('SELECT 1 FROM zones WHERE zone_id = ? LIMIT 1'); $stillExists = $this->pdo->prepare('SELECT 1 FROM move_zones WHERE zone_id = ? LIMIT 1');
$stillExists->execute([$zoneId]); $stillExists->execute([$zoneId]);
if ($stillExists->fetchColumn() === false) { if ($stillExists->fetchColumn() === false) {
throw new ZoneOwnershipException('zone was deleted concurrently'); throw new ZoneOwnershipException('zone was deleted concurrently');

View file

@ -148,12 +148,12 @@ final class PdoIntegrationTest extends TestCase
private static function sqliteSchema(): string private static function sqliteSchema(): string
{ {
return <<<'SQL' return <<<'SQL'
CREATE TABLE nonces ( CREATE TABLE move_nonces (
nonce TEXT PRIMARY KEY, nonce TEXT PRIMARY KEY,
expires_at INTEGER NOT NULL expires_at INTEGER NOT NULL
); );
CREATE TABLE events ( CREATE TABLE move_events (
event_id INTEGER PRIMARY KEY AUTOINCREMENT, event_id INTEGER PRIMARY KEY AUTOINCREMENT,
event_uuid TEXT UNIQUE, event_uuid TEXT UNIQUE,
device_id TEXT NOT NULL, device_id TEXT NOT NULL,
@ -171,7 +171,7 @@ final class PdoIntegrationTest extends TestCase
received_at INTEGER NOT NULL received_at INTEGER NOT NULL
); );
CREATE TABLE zones ( CREATE TABLE move_zones (
zone_id TEXT PRIMARY KEY, zone_id TEXT PRIMARY KEY,
device_id TEXT NOT NULL, device_id TEXT NOT NULL,
camera_id TEXT NOT NULL, camera_id TEXT NOT NULL,