Files
narl-workspace/migrations/0010_files.sql
T
nvrl 2103edc299 files: a node kind of their own, in the tree beside the notes
A file node points at one media row and owns it the way a document owns
what it embeds, so folder deletes, the media browser and exports keep
working. POST /api/nodes/{id}/files streams uploads into a folder;
imports turn non-markdown files into file nodes; exports put them at
their tree path. The browser previews images, video, audio and text,
offers the rest as a download, and takes files dropped on the list.

Migration 0010 widens the kind CHECK by editing the stored schema, as
SQLite documents for loosening a constraint: sqlx runs migrations in a
transaction, where foreign keys cannot be switched off for a rebuild.
2026-10-19 15:44:30 +02:00

47 lines
2.4 KiB
SQL

-- Files join the tree. A PDF or a zip used to exist only as media embedded in
-- a document; now it can sit in a folder beside the notes about it, as a node
-- of its own kind that points at one row of `media`.
-- Files only: the bytes. Deleting the media row — `rm` in the media browser —
-- takes the file out of the tree with it.
ALTER TABLE nodes ADD COLUMN media_id TEXT REFERENCES media (id) ON DELETE CASCADE
CHECK ((kind = 'file') = (media_id IS NOT NULL));
-- `kind` has to admit 'file', and SQLite cannot alter a CHECK constraint. The
-- usual way round is rebuilding the table with foreign keys off, but sqlx runs
-- every migration inside a transaction, where that pragma does nothing — and
-- dropping `nodes` with them on cascades into every card, list and attachment.
--
-- Widening a CHECK changes nothing on disk, though, so SQLite documents editing
-- the stored definition instead. RESET re-reads the schema on this connection;
-- the CREATE INDEX after it bumps the schema cookie, which makes every other
-- connection re-read it too. The INSERT further down is the proof: it fails,
-- and with it the whole migration, unless the new constraint is in force.
PRAGMA writable_schema = ON;
UPDATE sqlite_schema
SET sql = replace(sql, '''folder'', ''document'')', '''folder'', ''document'', ''file'')')
WHERE type = 'table' AND name = 'nodes';
PRAGMA writable_schema = RESET;
CREATE INDEX idx_nodes_media ON nodes (media_id) WHERE media_id IS NOT NULL;
-- An import used to hang a file that no document linked to on the folder it
-- arrived in, where only the media browser could see it. Each of those becomes
-- a file node in that folder. The slug cannot be derived from the name in SQL,
-- so it is the start of the media id: unique, and renaming fixes it for good.
INSERT INTO nodes (parent_id, kind, name, slug, created_at, updated_at, media_id)
SELECT nm.node_id, 'file', m.filename, 'file-' || substr(m.id, 1, 8),
m.created_at, m.created_at, m.id
FROM node_media nm
JOIN nodes n ON n.id = nm.node_id
JOIN media m ON m.id = nm.media_id
WHERE n.kind IN ('space', 'folder');
-- The file node owns its bytes the same way a document owns what it embeds,
-- so deleting a folder still finds every file that goes with it.
DELETE FROM node_media
WHERE node_id IN (SELECT id FROM nodes WHERE kind IN ('space', 'folder'));
INSERT INTO node_media (node_id, media_id)
SELECT id, media_id FROM nodes WHERE kind = 'file';