-- Mutable metadata feature, milestone 2.3: document_custom_metadata CRUD -- queries plus document custom_schema_id accessors. -- -- See plan Section 8.2 / tracking §2.3 of -- plans/mutable.metadata.plan.combo.v4.md for the canonical list. -- -- v4 notes: -- - No schema_id parameter on the metadata insert. The schema binding -- lives on the document row; metadata rows don't duplicate it. -- - GetCurrentDocumentCustomMetadata and GetDocumentCustomMetadataByVersion -- join documents + client_metadata_schemas so the handler can populate -- schemaId/schemaName/schemaVersion in one round-trip. -- - LockDocumentForMetadataWrite takes a parent-row lock on documents. -- Replaces v3's LockDocumentCustomMetadataForVersion. -- - DeleteDocumentCustomMetadata / NullifyDocumentCustomSchemaId are -- intentionally absent: FK cascades handle document deletion and the -- reset-metadata workstream writes a direct UPDATE instead. -- name: CreateDocumentCustomMetadata :one -- Insert a new metadata version for a document. Caller supplies an -- explicit version computed from GetMaxDocumentMetadataVersion + 1 under -- the parent-row lock from LockDocumentForMetadataWrite. No schema_id -- column exists on this table in v4. INSERT INTO document_custom_metadata ( document_id, metadata, version, created_by ) VALUES ( @document_id, @metadata, @version, @created_by ) RETURNING *; -- name: GetCurrentDocumentCustomMetadata :one -- Return the most-recent metadata row for a document, decorated with the -- current schema name and version pulled from the bound schema row. -- Joins documents (for the binding) and client_metadata_schemas (for -- name/version). idx_dcm_doc_version_desc backs the ORDER BY ... LIMIT 1. SELECT dcm.id, dcm.document_id, dcm.metadata, dcm.version, dcm.created_at, dcm.created_by, cms.id AS schema_id, cms.name AS schema_name, cms.version AS schema_version FROM document_custom_metadata dcm JOIN documents d ON d.id = dcm.document_id LEFT JOIN client_metadata_schemas cms ON cms.id = d.custom_schema_id WHERE dcm.document_id = @document_id ORDER BY dcm.version DESC LIMIT 1; -- name: GetDocumentCustomMetadataByVersion :one -- Return a specific historical version with the same schema decoration -- as the current-version query above. SELECT dcm.id, dcm.document_id, dcm.metadata, dcm.version, dcm.created_at, dcm.created_by, cms.id AS schema_id, cms.name AS schema_name, cms.version AS schema_version FROM document_custom_metadata dcm JOIN documents d ON d.id = dcm.document_id LEFT JOIN client_metadata_schemas cms ON cms.id = d.custom_schema_id WHERE dcm.document_id = @document_id AND dcm.version = @version; -- name: GetDocumentCustomMetadataHistory :many -- Return a paged list of version summaries for a document, newest first. -- No schema join: the history response is a terse list of -- (version, created_at, created_by) tuples. SELECT version, created_at, created_by FROM document_custom_metadata WHERE document_id = @document_id ORDER BY version DESC LIMIT @limit_val OFFSET @offset_val; -- name: LockDocumentForMetadataWrite :one -- Parent-row lock for metadata write ordering. Must be called inside a -- transaction as the FIRST DML statement. Serializes with AssignSchema -- (which also takes FOR UPDATE on documents) so the mutual-exclusivity -- invariant in plan Section 7.3 holds. SELECT id FROM documents WHERE id = @document_id FOR UPDATE; -- name: GetMaxDocumentMetadataVersion :one -- Return MAX(version) for a document's metadata rows, or 0 when none -- exist. Caller adds 1 to compute the next version to insert. Must run -- inside the transaction that holds the LockDocumentForMetadataWrite -- lock; otherwise two concurrent writers can both see the same max. SELECT COALESCE(MAX(version), 0)::int AS max_version FROM document_custom_metadata WHERE document_id = @document_id; -- name: SetDocumentCustomSchemaId :exec -- Bind (or clear) a document's schema. Nullable parameter: passing NULL -- clears the binding. The composite FK in migration 128 prevents binding -- a schema owned by a different client, so this query does not re-check. UPDATE documents SET custom_schema_id = @custom_schema_id WHERE id = @id; -- name: GetDocumentCustomSchemaId :one -- Read a document's current schema binding. Returns NULL when unbound. SELECT custom_schema_id FROM documents WHERE id = @id; -- name: GetDocumentClientID :one -- Read a document's clientId for the AssignSchema cross-client check. -- documents.clientId is the unquoted (lowercased) varchar column. SELECT clientId FROM documents WHERE id = @id; -- name: HasDocumentCustomMetadata :one -- EXISTS probe for any metadata row belonging to this document. Used by -- AssignSchema to reject re-binding once metadata has been written. SELECT EXISTS( SELECT 1 FROM document_custom_metadata WHERE document_id = @document_id ) AS has_metadata; -- name: LockDocumentForReset :one -- Milestone 3 parent-row lock acquired by ResetDocumentMetadata as the -- FIRST DML statement inside its transaction. Same FOR UPDATE shape as -- LockDocumentForMetadataWrite / LockDocumentForLegacyExtractionWrite so -- the reset path serializes correctly against concurrent assign and write -- paths for the mutual-exclusivity invariant (plan Section 7.3). -- Returns id, clientid, and current custom_schema_id so the caller can -- short-circuit and/or decorate the result without a second round-trip. SELECT id, clientId, custom_schema_id FROM documents WHERE id = @document_id FOR UPDATE; -- name: GetDocumentSchemaBindingForReset :one -- Read the document's current schema binding along with the joined -- schema row's name and version so ResetDocumentMetadata can populate the -- previousSchema* fields on its response. The LEFT JOIN returns NULL for -- name/version when the document has no schema bound — the reset endpoint -- treats that as the idempotent no-op case and still succeeds. SELECT d.custom_schema_id, cms.name AS schema_name, cms.version AS schema_version FROM documents d LEFT JOIN client_metadata_schemas cms ON cms.id = d.custom_schema_id WHERE d.id = @document_id; -- name: CountDocumentCustomMetadataVersions :one -- Return the number of metadata version rows for a document. Used by -- ResetDocumentMetadata to populate metadataVersionsDeleted on its -- response. Run under the parent-row lock from LockDocumentForReset so -- the count is authoritative at commit time. SELECT COUNT(*)::int AS version_count FROM document_custom_metadata WHERE document_id = @document_id; -- name: DeleteDocumentCustomMetadataForReset :exec -- @sqlc-vet-disable -- Delete every metadata version row for a document. Owned by the -- reset-metadata path — the Delete cascade for document deletion is -- handled by ON DELETE CASCADE on document_custom_metadata.document_id, -- not by this query. Name disambiguates from any future cascade helper. DELETE FROM document_custom_metadata WHERE document_id = @document_id; -- name: NullifyDocumentCustomSchemaIdForReset :exec -- Clear the document's schema binding as the final DML of the reset -- transaction. Trigger 1 (trg_prevent_schema_reassignment) fires on this -- UPDATE; the preceding DeleteDocumentCustomMetadataForReset has already -- removed every metadata row and the transaction has already rejected any -- document carrying legacy field extractions, so the trigger's EXISTS -- checks both return false and the UPDATE succeeds. UPDATE documents SET custom_schema_id = NULL WHERE id = @document_id;