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 탭:
```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);
```
@ -144,7 +144,7 @@ php -r "echo bin2hex(random_bytes(32)), \"\n\";"
```
```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);
```

View file

@ -188,14 +188,14 @@ php -r "echo bin2hex(random_bytes(32)), \"\n\";"
```
```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);
```
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
`/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.
## 7. First admin account
@ -215,7 +215,7 @@ php -r "echo password_hash('CHANGE-ME-to-a-real-passphrase', PASSWORD_ARGON2ID),
`PASSWORD_ARGON2ID`.)
```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);
```

View file

@ -21,27 +21,27 @@
-- ===========================================================================
-- 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 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.
ALTER TABLE events
ALTER TABLE move_events
ADD COLUMN site VARCHAR(64) NULL AFTER device_id,
ADD KEY ix_events_site_occurred_at (site, occurred_at);
-- 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
-- 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 UNIQUE KEY uq_events_uuid (event_uuid);
-- A4) event_snapshots.created_at index — PdoSnapshotRepository::purgeOlderThan()
-- runs "SELECT ... WHERE created_at < ?" inside live ingest requests. Without
-- 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);
-- 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
-- entry", the table already has duplicate (event_id, content_hash) rows —
-- 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);
-- 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
-- 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);
-- ===========================================================================
-- 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.
-- ===========================================================================
-- UPDATE 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-a' WHERE device_id = 'pi-1';
-- UPDATE move_devices SET site = 'factory-b' WHERE device_id = 'pi-2';
-- -- ... 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
-- device's site so its past events reappear for scoped operators.
-- ===========================================================================
UPDATE events e
JOIN devices d ON d.device_id = e.device_id
UPDATE move_events e
JOIN move_devices d ON d.device_id = e.device_id
SET e.site = d.site
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
-- 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) -----------
-- SHOW COLUMNS FROM devices LIKE 'site';
-- SHOW COLUMNS FROM events LIKE 'site';
-- SHOW COLUMNS FROM events LIKE 'event_uuid';
-- SHOW INDEX FROM events WHERE Key_name = 'uq_events_uuid';
-- SHOW INDEX FROM 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 rate_limits WHERE Key_name = 'ix_rate_limits_window';
-- SHOW COLUMNS FROM move_devices LIKE 'site';
-- SHOW COLUMNS FROM move_events LIKE 'site';
-- SHOW COLUMNS FROM move_events LIKE 'event_uuid';
-- SHOW INDEX FROM move_events WHERE Key_name = 'uq_events_uuid';
-- SHOW INDEX FROM move_event_snapshots WHERE Key_name = 'ix_event_snapshots_created';
-- SHOW INDEX FROM move_event_snapshots WHERE Key_name = 'uq_event_snapshots_event_hash';
-- 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
-- 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;

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
-- 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,
username VARCHAR(190) NOT NULL,
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
-- (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,
name VARCHAR(190) NULL,
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
-- 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,
device_id VARCHAR(64) NOT 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
-- 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
device_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
-- 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_uuid CHAR(32) NULL, -- edge-generated idempotency key (dedup a re-sent event); NULL for legacy rows
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,
event_type VARCHAR(32) 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
-- before calling SnapshotRepository::add() for its snapshots.
-- ---------------------------------------------------------------------------
CREATE TABLE event_snapshots (
CREATE TABLE move_event_snapshots (
snapshot_id INT UNSIGNED NOT NULL AUTO_INCREMENT,
event_id INT UNSIGNED NOT NULL,
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),
KEY ix_event_snapshots_event (event_id),
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
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
@ -173,7 +173,7 @@ CREATE TABLE event_snapshots (
-- GET /api/config. No PdoThresholdRepository write method exists in P1-B --
-- 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,
device_id VARCHAR(64) NOT 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,
-- not the earlier seen() check).
-- ---------------------------------------------------------------------------
CREATE TABLE nonces (
CREATE TABLE move_nonces (
nonce VARCHAR(190) NOT NULL,
expires_at BIGINT UNSIGNED NOT NULL,
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;
-- ---------------------------------------------------------------------------
@ -207,7 +207,7 @@ CREATE TABLE nonces (
-- `INSERT ... ON DUPLICATE KEY UPDATE count = count + 1` upsert in
-- PdoRateLimiterRepository::hit() -- no read-modify-write race window.
-- ---------------------------------------------------------------------------
CREATE TABLE rate_limits (
CREATE TABLE move_rate_limits (
rate_key VARCHAR(190) NOT NULL,
window_start BIGINT UNSIGNED NOT NULL, -- floor(now / 60): the minute bucket
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
-- 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,
actor VARCHAR(190) NOT NULL,
action VARCHAR(64) NOT NULL,
@ -241,13 +241,18 @@ CREATE TABLE audit_log (
-- run. Apply the RUNNABLE migration instead:
--
-- 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
-- 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
-- phpMyAdmin BEFORE deploying code that reads these columns, then provision:
-- UPDATE devices SET site = 'factory-a' WHERE device_id = 'pi-1';
-- UPDATE users SET site_scope = 'factory-a' WHERE username = 'operator-a';
-- UPDATE move_devices SET site = 'factory-a' WHERE device_id = 'pi-1';
-- 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
-- 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
{
$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 (?, ?, ?, ?, ?, ?)'
);
$st->execute([

View file

@ -11,7 +11,7 @@ final class PdoDeviceRepository implements DeviceRepository
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]);
$secret = $st->fetchColumn();
return $secret === false ? null : (string)$secret;
@ -19,7 +19,7 @@ final class PdoDeviceRepository implements DeviceRepository
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]);
$site = $st->fetchColumn();
// 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
// implicitly from an inbound heartbeat.
$st = $this->pdo->prepare(
'UPDATE devices
'UPDATE move_devices
SET last_fps = ?, last_temp = ?, last_throttle = ?,
reported_config_version = ?, last_heartbeat_at = ?
WHERE device_id = ?'
@ -51,7 +51,7 @@ final class PdoDeviceRepository implements DeviceRepository
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]);
$version = $st->fetchColumn();
return $version === false ? 1 : (int)$version;

View file

@ -16,7 +16,7 @@ final class PdoEventRepository implements EventRepository
public function insert(array $event): int
{
$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,
occurred_at, duration_s, metrics_json, received_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)'
@ -24,7 +24,7 @@ final class PdoEventRepository implements EventRepository
$st->execute([
$event['event_uuid'] ?? null, // NULL when the edge sent no idempotency key (legacy)
(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['event_type'],
(string)$event['severity'],
@ -40,7 +40,7 @@ final class PdoEventRepository implements EventRepository
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]);
$id = $st->fetchColumn();
return $id === false ? null : (int)$id;
@ -51,7 +51,7 @@ final class PdoEventRepository implements EventRepository
if ($severity !== null) {
$st = $this->pdo->prepare(
'SELECT ' . self::SELECT_COLUMNS . '
FROM events WHERE severity = ?
FROM move_events WHERE severity = ?
ORDER BY occurred_at DESC, event_id DESC LIMIT ?'
);
$st->bindValue(1, $severity, PDO::PARAM_STR);
@ -59,7 +59,7 @@ final class PdoEventRepository implements EventRepository
} else {
$st = $this->pdo->prepare(
'SELECT ' . self::SELECT_COLUMNS . '
FROM events
FROM move_events
ORDER BY occurred_at DESC, event_id DESC LIMIT ?'
);
$st->bindValue(1, $limit, PDO::PARAM_INT);
@ -92,7 +92,7 @@ final class PdoEventRepository implements EventRepository
$binds[] = [$beforeTs, PDO::PARAM_STR];
$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);
$sql .= ' ORDER BY occurred_at DESC, event_id DESC LIMIT ?';
$binds[] = [$limit, PDO::PARAM_INT];
@ -106,7 +106,7 @@ final class PdoEventRepository implements EventRepository
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]);
$row = $st->fetch();
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
// already-acknowledged event by the same user would otherwise report
// 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]);
if ($exists->fetchColumn() === false) {
return false;
}
$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]);
return true;

View file

@ -20,20 +20,20 @@ final class PdoNonceRepository implements NonceRepository
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]);
return (bool)$st->fetchColumn();
}
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]);
// 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
// already-expired rows (safe: seen() only cares about unexpired ones).
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()]);
}
}

View file

@ -18,13 +18,13 @@ final class PdoRateLimiterRepository implements RateLimiterRepository
public function hit(string $key, int $windowStart): int
{
$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'
);
$upsert->execute([$key, $windowStart]);
$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]);
$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.
if (mt_rand(1, 100) === 1) {
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]);
} catch (\PDOException $e) {
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
{
$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 (?, ?, ?, ?)'
);
$st->execute([$eventId, $fileUuid, $contentHash, time()]);
@ -21,7 +21,7 @@ final class PdoSnapshotRepository implements SnapshotRepository
public function forEvent(int $eventId): array
{
$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'
);
$st->execute([$eventId]);
@ -39,7 +39,7 @@ final class PdoSnapshotRepository implements SnapshotRepository
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]);
if ($st->fetchColumn() === false) {
return null;
@ -53,7 +53,7 @@ final class PdoSnapshotRepository implements SnapshotRepository
if (strlen($uuid) !== 32 || !ctype_xdigit($uuid)) {
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]);
$id = $st->fetchColumn();
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
// PIPA leak we are closing, so never delete the row before its file.
$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(2, $limit, PDO::PARAM_INT);
$sel->execute();
@ -92,7 +92,7 @@ final class PdoSnapshotRepository implements SnapshotRepository
return 0;
}
$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);
return count($ids);
}

View file

@ -11,7 +11,7 @@ final class PdoThresholdRepository implements ThresholdRepository
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]);
$out = [];

View file

@ -13,7 +13,7 @@ final class PdoUserRepository implements UserRepository
{
$st = $this->pdo->prepare(
'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]);
$row = $st->fetch();
@ -36,14 +36,14 @@ final class PdoUserRepository implements UserRepository
// Increment in SQL (not read-then-write in PHP) so concurrent failed
// logins for the same account can't clobber each other's count.
$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]);
}
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]);
}
}

View file

@ -13,7 +13,7 @@ final class PdoZoneRepository implements ZoneRepository
public function find(string $zoneId): ?array
{
$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]);
$row = $st->fetch();
@ -23,7 +23,7 @@ final class PdoZoneRepository implements ZoneRepository
public function forDevice(string $deviceId): array
{
$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]);
return $st->fetchAll();
@ -32,7 +32,7 @@ final class PdoZoneRepository implements ZoneRepository
public function forCamera(string $cameraId): array
{
$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]);
return $st->fetchAll();
@ -52,7 +52,7 @@ final class PdoZoneRepository implements ZoneRepository
$newId = bin2hex(random_bytes(16));
$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 (?, ?, ?, ?, ?, ?)'
);
$st->execute([$newId, $deviceId, $cameraId, $level, $polygonJson, time()]);
@ -61,7 +61,7 @@ final class PdoZoneRepository implements ZoneRepository
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]);
return $st->rowCount() > 0;
}
@ -89,7 +89,7 @@ final class PdoZoneRepository implements ZoneRepository
string $polygonJson,
): string {
$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]);
if ($owns->fetchColumn() === false) {
@ -97,7 +97,7 @@ final class PdoZoneRepository implements ZoneRepository
}
$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 = ?'
);
$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
// above and this UPDATE. Disambiguate with a targeted existence
// 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]);
if ($stillExists->fetchColumn() === false) {
throw new ZoneOwnershipException('zone was deleted concurrently');

View file

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