openvibers/openvibe.media Commits

Media: the frozen paste tables are dropped (contract after 0001); getAppStats carries no paste keys (#89)

OP OpenVibers committed 11 Oct 2026 · c5b2ca3cfc · 3 files, +64 −4

Media's frozen paste tables go. Pastes moved to OpenVibe.Community on 2026-09-22, and Media#75 (deployed 2026-10-10 13:38 UTC) stopped every read and write of `pastes`, `paste_likes` and `paste_comments` (`test/paste-tables-unread.test.js`).

**Checked in production first:**
- Two `pg_stat_user_tables` readings a minute apart showed no insert, update or delete, and no scan.
- `pastes` last showed an index scan at 2026-10-10 13:36 UTC, before Media#75.
- Its last sequential scan (2026-10-11 17:27 UTC) was an operator's database-wide URL survey, not Media.
- `paste_likes` and `paste_comments` were last read on 10-07.

**Backup:** `pg_dump -Fc` of the three tables is at `/root/ov_media-pastes-pre-drop-2026-10-11.dump` on the host (891 pastes, 48 likes, 60 comments; restorable with `pg_restore`).

**Migration `0007_drop_frozen_paste_tables.sql`** (`-- phase: contract`, `-- after: 0001`, the migration that created them, applied 2026-09-28, so the N-1 window has passed):
- Drops the three tables, their two trigger functions and the seven `paste_*` settings. Nothing reads any of them.
- The legacy screenshot and avatar objects stay: they are `media_objects` found by `legacy_ref`, and `/p/:slug/screenshot` 301s to `/o/<id>`.
- `DROP TABLE` fires no row trigger, so `trg_pastes_object_deleted` cannot mark them deleted.

**`getAppStats`:** the `pastes`, `pasteImages` and `pasteText` null keys are gone (they stayed one release for N-1 clients). Live reads paste counts from Community only; that change is in OpenVibe.Live.

**Test:** new `test/paste-tables-dropped.test.js`, run on a database migrated to 0006 that holds a legacy screenshot paste tied to its object and a like:
- 0007 is held inside its window, with the tables still there.
- Eight days later it applies: the tables, functions and settings are gone.
- The object is still `ready`.

The N-1 seed inserts no paste rows, so it is unchanged. `ov test`: 83 of 83 covering files pass.

Changes

ddiff --git a/migrations/0007_drop_frozen_paste_tables.sql b/migrations/0007_drop_frozen_paste_tables.sql
nnew file mode 100644
iindex 0000000..6324238
---- /dev/null
++++ b/migrations/0007_drop_frozen_paste_tables.sql
@@@ -0,0 +1,16 @@
++-- phase: contract
++-- after: 0001
++-- OpenVibe.Media: the frozen paste tables go. Pastes moved to OpenVibe.Community on 2026-09-22; Media#75
++-- (2026-10-10 13:38 UTC) stopped every read and write of `pastes`, `paste_likes` and `paste_comments`
++-- (test/paste-tables-unread.test.js), and production showed no scan or write of them after it. They were created by
++-- 0001, so the N-1 window has long passed. The legacy screenshot and avatar objects stay: they are media_objects
++-- found by legacy_ref (/p/:slug/screenshot 301s to /o/<id>), and DROP TABLE fires no row trigger, so dropping
++-- pastes marks no object deleted. A pg_dump of the three tables was kept before this ran
++-- (/root/ov_media-pastes-pre-drop-2026-10-11.dump on the host).
++DROP TABLE IF EXISTS paste_comments;
++DROP TABLE IF EXISTS paste_likes;
++DROP TABLE IF EXISTS pastes;
++DROP FUNCTION IF EXISTS trg_pastes_hold_guard_v2();
++DROP FUNCTION IF EXISTS trg_pastes_object_deleted();
++DELETE FROM media_settings WHERE key IN ('paste_max_size_kb', 'paste_screenshot_max_size_mb', 'paste_cooldown_seconds',
++ 'paste_max_per_user_per_day', 'paste_comment_cooldown_seconds', 'paste_comment_max_length', 'paste_comment_anon_allowed');
ddiff --git a/server/db/database.js b/server/db/database.js
iindex 2690fc8..c0943ef 100644
---- a/server/db/database.js
++++ b/server/db/database.js
@@@ -332,10 +332,6 @@ async function getAppStats(appId) {
return {
vods: await c(`SELECT COUNT(*) n ${vodBase}`, [appId]),
clips: await c(`SELECT COUNT(*) n ${clipBase}`, [appId]),
-- // Pastes belong to Community, and Live reads them there; null keys remain for one release for N-1 clients.
-- pastes: null,
-- pasteImages: null,
-- pasteText: null,
durationSeconds: await c(`SELECT COALESCE(SUM(duration_seconds),0) n ${vodBase}`, [appId]),
recent: {
vods: await win(`SELECT COUNT(*) n ${vodBase} AND created_at >= datetime('now', ?)`, [appId]),
ddiff --git a/test/paste-tables-dropped.test.js b/test/paste-tables-dropped.test.js
nnew file mode 100644
iindex 0000000..2c671b9
---- /dev/null
++++ b/test/paste-tables-dropped.test.js
@@@ -0,0 +1,48 @@
++'use strict';
++// Migration 0007 drops the frozen paste tables (pastes moved to OpenVibe.Community; Media#75 stopped every read and
++// write). On a database that already holds them, it waits out its N-1 window after 0001 and then drops the three
++// tables, their trigger functions and the paste settings — and leaves the legacy screenshot objects alone: DROP TABLE
++// fires no row trigger, so trg_pastes_object_deleted never marks an object deleted.
++const assert = require('assert');
++const fs = require('fs');
++const os = require('os');
++const path = require('path');
++const { createDb } = require('openvibe-sdk/db');
++
++const MIGRATIONS = path.join(__dirname, '..', 'migrations');
++const quiet = { log() {}, warn() {}, error() {} };
++
++(async () => {
++ // The schema as production had it before this release: every migration up to 0006.
++ const before = fs.mkdtempSync(path.join(os.tmpdir(), 'ov-media-0006-'));
++ for (const f of fs.readdirSync(MIGRATIONS)) if (/^000[1-6]_/.test(f)) fs.copyFileSync(path.join(MIGRATIONS, f), path.join(before, f));
++ const db = createDb({ pglite: true, service: 'paste-drop-test' });
++ await db.migrate({ dir: before, log: quiet });
++
++ await db.exec(`INSERT INTO media_objects (id, app_id, namespace, kind, visibility, lifecycle_status, legacy_ref)
++ VALUES ('med_01JZ0000000000000000000001', 'live', 'live', 'screenshot', 'public', 'ready', 'legacy:live:paste:old-shot')`);
++ await db.exec(`INSERT INTO pastes (slug, type, title, object_id) VALUES ('old-shot', 'screenshot', 'An old screenshot', 'med_01JZ0000000000000000000001')`);
++ await db.exec(`INSERT INTO paste_likes (paste_id, user_id) SELECT id, 7 FROM pastes WHERE slug = 'old-shot'`);
++
++ // Within the window after 0001 the drop waits, and nothing after it runs.
++ const held = await db.migrate({ dir: MIGRATIONS, log: quiet });
++ assert.deepStrictEqual(held.held.map((h) => h.id), ['0007'], 'the contract waits for its N-1 window');
++ assert.ok(await db.value("SELECT to_regclass('pastes')::text"), 'the tables are still there while it waits');
++
++ // A week later it runs.
++ const later = await db.migrate({ dir: MIGRATIONS, now: () => Date.now() + 8 * 86400000, log: quiet });
++ assert.deepStrictEqual(later.applied.map((a) => a.id), ['0007']);
++ for (const t of ['pastes', 'paste_likes', 'paste_comments']) {
++ assert.strictEqual(await db.value(`SELECT to_regclass('${t}')::text`), null, `${t} is dropped`);
++ }
++ const fns = await db.many("SELECT proname FROM pg_proc WHERE proname IN ('trg_pastes_hold_guard_v2', 'trg_pastes_object_deleted')");
++ assert.deepStrictEqual(fns, [], 'their trigger functions go too');
++ const settings = await db.many("SELECT key FROM media_settings WHERE key LIKE 'paste%'");
++ assert.deepStrictEqual(settings, [], 'and the paste settings');
++ const obj = await db.maybe("SELECT lifecycle_status FROM media_objects WHERE id = 'med_01JZ0000000000000000000001'");
++ assert.strictEqual(obj.lifecycle_status, 'ready', 'the legacy screenshot object is untouched (no row trigger fired)');
++
++ await db.close();
++ fs.rmSync(before, { recursive: true, force: true });
++ console.log('paste tables dropped: all checks passed');
++})().catch((err) => { console.error(err); process.exit(1); });