Mercurial > hg > orthanc-databases
view PostgreSQL/Plugins/SQL/PrepareIndex.sql @ 715:7c0157bf749c sql-opti
added the name of the source plugin in audit logs
author | Sebastien Jodogne <s.jodogne@gmail.com> |
---|---|
date | Sun, 10 Aug 2025 15:27:45 +0200 |
parents | 2f2036e0f352 |
children | 951d2ef62f58 |
line wrap: on
line source
-- This SQL file creates a DB in Rev5 directly -- It is also run after upgrade scripts to create new tables and or create/replace triggers and functions. -- This script is self contained, it contains everything that needs to be run to create an Orthanc DB. -- Note to developers: -- - it is and must stay idempotent. -- - it is executed when the DB is "locked", only one Orthanc instance can execute it at a given time. CREATE TABLE IF NOT EXISTS GlobalProperties( property INTEGER PRIMARY KEY, value TEXT ); CREATE TABLE IF NOT EXISTS Resources( internalId BIGSERIAL NOT NULL PRIMARY KEY, resourceType INTEGER NOT NULL, publicId VARCHAR(64) NOT NULL, -- parentId BIGINT REFERENCES Resources(internalId) ON DELETE CASCADE, parentId BIGINT REFERENCES Resources(internalId), childCount INTEGER, CONSTRAINT UniquePublicId UNIQUE (publicId) ); CREATE TABLE IF NOT EXISTS MainDicomTags( id BIGINT REFERENCES Resources(internalId) ON DELETE CASCADE, tagGroup INTEGER, tagElement INTEGER, value TEXT, PRIMARY KEY(id, tagGroup, tagElement) ); CREATE TABLE IF NOT EXISTS DicomIdentifiers( id BIGINT REFERENCES Resources(internalId) ON DELETE CASCADE, tagGroup INTEGER, tagElement INTEGER, value TEXT, PRIMARY KEY(id, tagGroup, tagElement) ); CREATE TABLE IF NOT EXISTS Metadata( id BIGINT REFERENCES Resources(internalId) ON DELETE CASCADE, type INTEGER NOT NULL, value TEXT, revision INTEGER, PRIMARY KEY(id, type) ); CREATE TABLE IF NOT EXISTS AttachedFiles( id BIGINT REFERENCES Resources(internalId) ON DELETE CASCADE, fileType INTEGER, uuid VARCHAR(64) NOT NULL, compressedSize BIGINT, uncompressedSize BIGINT, compressionType INTEGER, uncompressedHash VARCHAR(40), compressedHash VARCHAR(40), revision INTEGER, customData BYTEA, -- new in schema rev 5 PRIMARY KEY(id, fileType) ); CREATE TABLE IF NOT EXISTS Changes( seq BIGSERIAL NOT NULL PRIMARY KEY, changeType INTEGER, internalId BIGINT REFERENCES Resources(internalId) ON DELETE CASCADE, resourceType INTEGER, date VARCHAR(64) ); CREATE TABLE IF NOT EXISTS ExportedResources( seq BIGSERIAL NOT NULL PRIMARY KEY, resourceType INTEGER, publicId VARCHAR(64), remoteModality TEXT, patientId VARCHAR(64), studyInstanceUid TEXT, seriesInstanceUid TEXT, sopInstanceUid TEXT, date VARCHAR(64) ); CREATE TABLE IF NOT EXISTS Labels( id BIGINT REFERENCES Resources(internalId) ON DELETE CASCADE, label TEXT, PRIMARY KEY(id, label) ); CREATE TABLE IF NOT EXISTS GlobalIntegers( key INTEGER PRIMARY KEY, value BIGINT); -- GlobalIntegers keys: -- 0: CompressedSize -- 1: UncompressedSize -- 2: PatientsCount -- 3: StudiesCount -- 4: SeriesCount -- 5: InstancesCount -- 6: ChangeSeq -- 7: PatientRecyclingOrderSeq (removed in 7.0) CREATE TABLE IF NOT EXISTS ServerProperties( server VARCHAR(64) NOT NULL, property INTEGER, value TEXT, PRIMARY KEY(server, property) ); DO $$ DECLARE pg_version text; BEGIN SELECT version() INTO pg_version; IF substring(pg_version from 'PostgreSQL (\d+)\.')::int >= 11 THEN -- PostgreSQL 11 or later -- new ChildrenIndex2 introduced in Rev3 (replacing previous ChildrenIndex) EXECUTE 'CREATE INDEX IF NOT EXISTS ChildrenIndex2 ON Resources USING btree (parentId ASC NULLS LAST) INCLUDE (publicId, internalId)'; ELSE EXECUTE 'CREATE INDEX IF NOT EXISTS ChildrenIndex2 ON Resources USING btree (parentId ASC NULLS LAST, publicId, internalId)'; END IF; END $$; CREATE INDEX IF NOT EXISTS PublicIndex ON Resources(publicId); CREATE INDEX IF NOT EXISTS ResourceTypeIndex ON Resources(resourceType); CREATE INDEX IF NOT EXISTS MainDicomTagsIndex ON MainDicomTags(id); CREATE INDEX IF NOT EXISTS DicomIdentifiersIndex1 ON DicomIdentifiers(id); CREATE INDEX IF NOT EXISTS DicomIdentifiersIndex2 ON DicomIdentifiers(tagGroup, tagElement); CREATE INDEX IF NOT EXISTS DicomIdentifiersIndex3 ON DicomIdentifiers(tagGroup, tagElement, value); CREATE INDEX IF NOT EXISTS DicomIdentifiersIndexValues ON DicomIdentifiers(value); CREATE INDEX IF NOT EXISTS ChangesIndex ON Changes(internalId); CREATE INDEX IF NOT EXISTS LabelsIndex1 ON LABELS(id); CREATE INDEX IF NOT EXISTS LabelsIndex2 ON LABELS(label); ------------------- Trigram index creation ------------------- -- Apply fix for performance issue (speed up wildcard search by using GIN trigrams). This implements the patch suggested -- in issue #47, BUT we also keep the original "DicomIdentifiersIndexValues", as it leads to better -- performance for "strict" searches (i.e. searches involving no wildcard). -- https://www.postgresql.org/docs/current/static/pgtrgm.html -- https://orthanc.uclouvain.be/bugs/show_bug.cgi?id=47 DO $body$ begin IF EXISTS (SELECT 1 FROM pg_available_extensions WHERE name='pg_trgm') THEN CREATE EXTENSION IF NOT EXISTS pg_trgm; CREATE INDEX IF NOT EXISTS DicomIdentifiersIndexValues2 ON DicomIdentifiers USING gin(value gin_trgm_ops); ELSE RAISE NOTICE 'pg_trgm extension is not available on you system'; END IF; END $body$; --------------------- PatientRecyclingOrder ------------------- -- from rev 99, we always maintain a PatientRecyclingOrder metadata, no matter if the patient is protected or not CREATE OR REPLACE FUNCTION PatientAddedOrUpdated( IN patient_id BIGINT ) RETURNS VOID AS $body$ BEGIN DECLARE newSeq BIGINT; BEGIN INSERT INTO Metadata (id, type, value, revision) VALUES (patient_id, 19, nextval('PatientRecyclingOrderSequence')::TEXT, 0) ON CONFLICT (id, type) DO UPDATE SET value = EXCLUDED.value, revision = EXCLUDED.revision; END; END; $body$ LANGUAGE plpgsql; ------------------- DeleteResource function ------------------- CREATE OR REPLACE FUNCTION DeleteResource( IN id BIGINT, OUT remaining_ancestor_resource_type INTEGER, OUT remaining_anncestor_public_id TEXT) AS $body$ DECLARE deleted_resource_row RECORD; deleted_parent_row RECORD; deleted_grand_parent_row RECORD; deleted_grand_grand_parent_row RECORD; locked_parent_row RECORD; locked_resource_row RECORD; BEGIN SET client_min_messages = warning; -- suppress NOTICE: relation "deletedresources" already exists, skipping -- note: temporary tables are created at connection level -> they are likely to exist. -- These tables are used by the triggers CREATE TEMPORARY TABLE IF NOT EXISTS DeletedResources( resourceType INTEGER NOT NULL, publicId VARCHAR(64) NOT NULL ); RESET client_min_messages; -- clear the temporary table in case it has been created earlier in the connection DELETE FROM DeletedResources; -- create/clear the DeletedFiles temporary table PERFORM CreateDeletedFilesTemporaryTable(); -- Before deleting an object, we need to lock its parent until the end of the transaction to avoid that -- 2 threads deletes the last 2 instances of a series at the same time -> none of them would realize -- that they are deleting the last instance and the parent resources would not be deleted. -- Locking only the immediate parent is sufficient to prevent from this. SELECT * INTO locked_parent_row FROM resources WHERE internalid = (SELECT parentid FROM resources WHERE internalid = id) FOR UPDATE; -- Before deleting the resource itself, we lock it to retrieve the resourceType and to make sure not 2 connections try to -- delete it at the same time SELECT * INTO locked_resource_row FROM resources WHERE internalid = id FOR UPDATE; -- before delete the resource itself, we must delete its grand-grand-children, the grand-children and its children no to violate -- the parentId referencing an existing primary key constrain. This is actually implementing the ON DELETE CASCADE that was on the parentId in previous revisions. -- If this resource has grand-grand-children, delete them if locked_resource_row.resourceType < 1 THEN WITH grand_grand_children_to_delete AS (SELECT grandGrandChildLevel.internalId, grandGrandChildLevel.resourceType, grandGrandChildLevel.publicId FROM Resources childLevel INNER JOIN Resources grandChildLevel ON childLevel.internalId = grandChildLevel.parentId INNER JOIN Resources grandGrandChildLevel ON grandChildLevel.internalId = grandGrandChildLevel.parentId WHERE childLevel.parentId = id), deleted_grand_grand_children_rows AS (DELETE FROM Resources WHERE internalId IN (SELECT internalId FROM grand_grand_children_to_delete) RETURNING resourceType, publicId) INSERT INTO DeletedResources SELECT resourceType, publicId FROM deleted_grand_grand_children_rows; END IF; -- If this resource has grand-children, delete them if locked_resource_row.resourceType < 2 THEN WITH grand_children_to_delete AS (SELECT grandChildLevel.internalId, grandChildLevel.resourceType, grandChildLevel.publicId FROM Resources childLevel INNER JOIN Resources grandChildLevel ON childLevel.internalId = grandChildLevel.parentId WHERE childLevel.parentId = id), deleted_grand_children_rows AS (DELETE FROM Resources WHERE internalId IN (SELECT internalId FROM grand_children_to_delete) RETURNING resourceType, publicId) INSERT INTO DeletedResources SELECT resourceType, publicId FROM deleted_grand_children_rows; END IF; -- If this resource has children, delete them if locked_resource_row.resourceType < 3 THEN WITH deleted_children AS (DELETE FROM Resources WHERE parentId = id RETURNING resourceType, publicId) INSERT INTO DeletedResources SELECT resourceType, publicId FROM deleted_children; END IF; -- delete the resource itself DELETE FROM Resources WHERE internalId=id RETURNING * INTO deleted_resource_row; -- keep track of the deleted resources for C++ code INSERT INTO DeletedResources VALUES (deleted_resource_row.resourceType, deleted_resource_row.publicId); -- If this resource still has siblings, keep track of the remaining parent -- (a parent that must not be deleted but whose LastUpdate must be updated) SELECT resourceType, publicId INTO remaining_ancestor_resource_type, remaining_anncestor_public_id FROM Resources WHERE internalId = deleted_resource_row.parentId AND EXISTS (SELECT 1 FROM Resources WHERE parentId = deleted_resource_row.parentId); IF deleted_resource_row.resourceType > 0 THEN -- If this resource is the latest child, delete the parent DELETE FROM Resources WHERE internalId = deleted_resource_row.parentId AND NOT EXISTS (SELECT 1 FROM Resources WHERE parentId = deleted_resource_row.parentId) RETURNING * INTO deleted_parent_row; IF FOUND THEN INSERT INTO DeletedResources VALUES (deleted_parent_row.resourceType, deleted_parent_row.publicId); IF deleted_parent_row.resourceType > 0 THEN -- If this resource is the latest child, delete the parent DELETE FROM Resources WHERE internalId = deleted_parent_row.parentId AND NOT EXISTS (SELECT 1 FROM Resources WHERE parentId = deleted_parent_row.parentId) RETURNING * INTO deleted_grand_parent_row; IF FOUND THEN INSERT INTO DeletedResources VALUES (deleted_grand_parent_row.resourceType, deleted_grand_parent_row.publicId); IF deleted_grand_parent_row.resourceType > 0 THEN -- If this resource is the latest child, delete the parent DELETE FROM Resources WHERE internalId = deleted_grand_parent_row.parentId AND NOT EXISTS (SELECT 1 FROM Resources WHERE parentId = deleted_grand_parent_row.parentId) RETURNING * INTO deleted_grand_parent_row; IF FOUND THEN INSERT INTO DeletedResources VALUES (deleted_grand_parent_row.resourceType, deleted_grand_parent_row.publicId); END IF; END IF; END IF; END IF; END IF; END IF; END; $body$ LANGUAGE plpgsql; CREATE OR REPLACE FUNCTION CreateDeletedFilesTemporaryTable( ) RETURNS VOID AS $body$ BEGIN SET client_min_messages = warning; -- suppress NOTICE: relation "DeletedFiles" already exists, skipping -- note: temporary tables created at connection level -> they are likely to exist CREATE TEMPORARY TABLE IF NOT EXISTS DeletedFiles( uuid VARCHAR(64) NOT NULL, fileType INTEGER, compressedSize BIGINT, uncompressedSize BIGINT, compressionType INTEGER, uncompressedHash VARCHAR(40), compressedHash VARCHAR(40), revision INTEGER, customData BYTEA ); RESET client_min_messages; -- clear the temporary table in case it has been created earlier in the connection DELETE FROM DeletedFiles; END; $body$ LANGUAGE plpgsql; -- Keep track of deleted files such that the C++ code knows which files have been deleted. -- Attached files are deleted by cascade when the related resource is deleted. CREATE OR REPLACE FUNCTION AttachedFileDeletedFunc() RETURNS TRIGGER AS $body$ BEGIN INSERT INTO DeletedFiles VALUES (old.uuid, old.filetype, old.compressedSize, old.uncompressedSize, old.compressionType, old.uncompressedHash, old.compressedHash, old.revision, old.customData); RETURN NULL; END; $body$ LANGUAGE plpgsql; DROP TRIGGER IF EXISTS AttachedFileDeleted on AttachedFiles; CREATE TRIGGER AttachedFileDeleted AFTER DELETE ON AttachedFiles FOR EACH ROW EXECUTE PROCEDURE AttachedFileDeletedFunc(); CREATE OR REPLACE FUNCTION DeleteAttachment( IN resource_id BIGINT, IN file_type INTEGER) RETURNS VOID AS $body$ BEGIN -- create/clear the DeletedFiles temporary table PERFORM CreateDeletedFilesTemporaryTable(); DELETE FROM AttachedFiles WHERE id = resource_id AND fileType = file_type; END; $body$ LANGUAGE plpgsql; ------------------- Fast Statistics ------------------- -- initial population of GlobalIntegers if not already there INSERT INTO GlobalIntegers SELECT 0, CAST(COALESCE(SUM(compressedSize), 0) AS BIGINT) FROM AttachedFiles ON CONFLICT DO NOTHING; INSERT INTO GlobalIntegers SELECT 1, CAST(COALESCE(SUM(uncompressedSize), 0) AS BIGINT) FROM AttachedFiles ON CONFLICT DO NOTHING; INSERT INTO GlobalIntegers SELECT 2, CAST(COALESCE(COUNT(*), 0) AS BIGINT) FROM Resources WHERE resourceType = 0 -- Count patients ON CONFLICT DO NOTHING; INSERT INTO GlobalIntegers SELECT 3, CAST(COALESCE(COUNT(*), 0) AS BIGINT) FROM Resources WHERE resourceType = 1 -- Count studies ON CONFLICT DO NOTHING; INSERT INTO GlobalIntegers SELECT 4, CAST(COALESCE(COUNT(*), 0) AS BIGINT) FROM Resources WHERE resourceType = 2 -- Count series ON CONFLICT DO NOTHING; INSERT INTO GlobalIntegers SELECT 5, CAST(COALESCE(COUNT(*), 0) AS BIGINT) FROM Resources WHERE resourceType = 3 -- Count instances ON CONFLICT DO NOTHING; -- this table stores all changes that needs to be performed to the GlobalIntegers table -- This way, each transaction can add row independently in this table without having to lock -- any row (which was the case with previous FastTotalSize). -- These changes will be applied at regular interval by an external thread or when someone -- requests the statistics CREATE TABLE IF NOT EXISTS GlobalIntegersChanges( key INTEGER, value BIGINT); CREATE OR REPLACE FUNCTION UpdateSingleStatistic( IN statistics_key INTEGER, OUT new_value BIGINT ) AS $body$ BEGIN -- Delete the current changes, sum them and update the GlobalIntegers row. -- New rows can be added in the meantime, they won't be deleted or summed. WITH deleted_rows AS ( DELETE FROM GlobalIntegersChanges WHERE GlobalIntegersChanges.key = statistics_key RETURNING value ) UPDATE GlobalIntegers SET value = value + ( SELECT COALESCE(SUM(value), 0) FROM deleted_rows ) WHERE GlobalIntegers.key = statistics_key RETURNING value INTO new_value; END; $body$ LANGUAGE plpgsql; CREATE OR REPLACE FUNCTION UpdateStatistics( OUT patients_cunt BIGINT, OUT studies_count BIGINT, OUT series_count BIGINT, OUT instances_count BIGINT, OUT total_compressed_size BIGINT, OUT total_uncompressed_size BIGINT ) AS $body$ BEGIN SELECT UpdateSingleStatistic(0) INTO total_compressed_size; SELECT UpdateSingleStatistic(1) INTO total_uncompressed_size; SELECT UpdateSingleStatistic(2) INTO patients_cunt; SELECT UpdateSingleStatistic(3) INTO studies_count; SELECT UpdateSingleStatistic(4) INTO series_count; SELECT UpdateSingleStatistic(5) INTO instances_count; END; $body$ LANGUAGE plpgsql; CREATE OR REPLACE FUNCTION IncrementResourcesTrackerFunc() RETURNS TRIGGER AS $$ BEGIN INSERT INTO GlobalIntegersChanges VALUES(new.resourceType + 2, 1); RETURN NULL; END; $$ LANGUAGE plpgsql; CREATE OR REPLACE FUNCTION DecrementResourcesTrackerFunc() RETURNS TRIGGER AS $$ BEGIN INSERT INTO GlobalIntegersChanges VALUES(old.resourceType + 2, -1); RETURN NULL; END; $$ LANGUAGE plpgsql; CREATE OR REPLACE FUNCTION AttachedFileIncrementSizeFunc() RETURNS TRIGGER AS $body$ BEGIN INSERT INTO GlobalIntegersChanges VALUES(0, new.compressedSize); INSERT INTO GlobalIntegersChanges VALUES(1, new.uncompressedSize); RETURN NULL; END; $body$ LANGUAGE plpgsql; CREATE OR REPLACE FUNCTION AttachedFileDecrementSizeFunc() RETURNS TRIGGER AS $body$ BEGIN INSERT INTO GlobalIntegersChanges VALUES(0, -old.compressedSize); INSERT INTO GlobalIntegersChanges VALUES(1, -old.uncompressedSize); RETURN NULL; END; $body$ LANGUAGE plpgsql; DROP TRIGGER IF EXISTS AttachedFileIncrementSize on AttachedFiles; CREATE TRIGGER AttachedFileIncrementSize AFTER INSERT ON AttachedFiles FOR EACH ROW EXECUTE PROCEDURE AttachedFileIncrementSizeFunc(); DROP TRIGGER IF EXISTS AttachedFileDecrementSize on AttachedFiles; CREATE TRIGGER AttachedFileDecrementSize AFTER DELETE ON AttachedFiles FOR EACH ROW EXECUTE PROCEDURE AttachedFileDecrementSizeFunc(); DROP TRIGGER IF EXISTS IncrementResourcesTracker on Resources; CREATE TRIGGER IncrementResourcesTracker AFTER INSERT ON Resources FOR EACH ROW EXECUTE PROCEDURE IncrementResourcesTrackerFunc(); DROP TRIGGER IF EXISTS DecrementResourcesTracker on Resources; CREATE TRIGGER DecrementResourcesTracker AFTER DELETE ON Resources FOR EACH ROW EXECUTE PROCEDURE DecrementResourcesTrackerFunc(); ------------------- InsertOrUpdateMetadata function ------------------- CREATE OR REPLACE FUNCTION InsertOrUpdateMetadata(resource_ids BIGINT[], metadata_types INTEGER[], metadata_values TEXT[], revisions INTEGER[]) RETURNS VOID AS $body$ BEGIN FOR i IN 1 .. ARRAY_LENGTH(resource_ids, 1) LOOP -- RAISE NOTICE 'Parameter %: % % %', i, resource_ids[i], metadata_types[i], metadata_values[i]; INSERT INTO Metadata VALUES(resource_ids[i], metadata_types[i], metadata_values[i], revisions[i]) ON CONFLICT (id, type) DO UPDATE SET value = EXCLUDED.value, revision = EXCLUDED.revision; END LOOP; END; $body$ LANGUAGE plpgsql; ------------------- GetLastChange function ------------------- DROP TRIGGER IF EXISTS InsertedChange ON Changes; -- insert the value if not already there INSERT INTO GlobalIntegers SELECT 6, CAST(COALESCE(MAX(seq), 0) AS BIGINT) FROM Changes ON CONFLICT DO NOTHING; CREATE OR REPLACE FUNCTION InsertedChangeFunc() RETURNS TRIGGER AS $body$ BEGIN UPDATE GlobalIntegers SET value = new.seq WHERE key = 6; RETURN NULL; END; $body$ LANGUAGE plpgsql; DROP TRIGGER IF EXISTS InsertedChange on Changes; CREATE TRIGGER InsertedChange AFTER INSERT ON Changes FOR EACH ROW EXECUTE PROCEDURE InsertedChangeFunc(); ------------------- CreateInstance function ------------------- CREATE OR REPLACE FUNCTION CreateInstance( IN patient_public_id TEXT, IN study_public_id TEXT, IN series_public_id TEXT, IN instance_public_id TEXT, OUT is_new_patient BIGINT, OUT is_new_study BIGINT, OUT is_new_series BIGINT, OUT is_new_instance BIGINT, OUT patient_internal_id BIGINT, OUT study_internal_id BIGINT, OUT series_internal_id BIGINT, OUT instance_internal_id BIGINT) AS $body$ BEGIN -- Assume the parent series already exists to minimize exceptions. -- Most of the instances are not the first of their series - especially when we need high performances. is_new_patient := 1; is_new_study := 1; is_new_series := 1; is_new_instance := 1; -- First, check if the series already exists SELECT internalid INTO series_internal_id FROM "resources" WHERE publicId = series_public_id; IF series_internal_id IS NOT NULL THEN -- RAISE NOTICE 'series-found %', series_internal_id; is_new_patient := 0; is_new_study := 0; is_new_series := 0; -- If the series exists, insert the instance directly BEGIN INSERT INTO "resources" VALUES (DEFAULT, 3, instance_public_id, series_internal_id, 0) RETURNING internalid INTO instance_internal_id; EXCEPTION WHEN unique_violation THEN is_new_instance := 0; SELECT internalid INTO instance_internal_id FROM "resources" WHERE publicId = instance_public_id; END; SELECT internalid INTO patient_internal_id FROM "resources" WHERE publicId = patient_public_id; SELECT internalid INTO study_internal_id FROM "resources" WHERE publicId = study_public_id; ELSE -- RAISE NOTICE 'series-not-found'; -- If the series does not exist, execute the "full" steps BEGIN INSERT INTO "resources" VALUES (DEFAULT, 0, patient_public_id, NULL, 0) RETURNING internalid INTO patient_internal_id; EXCEPTION WHEN unique_violation THEN is_new_patient := 0; SELECT internalid INTO patient_internal_id FROM "resources" WHERE publicId = patient_public_id; END; BEGIN INSERT INTO "resources" VALUES (DEFAULT, 1, study_public_id, patient_internal_id, 0) RETURNING internalid INTO study_internal_id; EXCEPTION WHEN unique_violation THEN is_new_study := 0; SELECT internalid INTO study_internal_id FROM "resources" WHERE publicId = study_public_id; END; BEGIN INSERT INTO "resources" VALUES (DEFAULT, 2, series_public_id, study_internal_id, 0) RETURNING internalid INTO series_internal_id; EXCEPTION WHEN unique_violation THEN is_new_series := 0; SELECT internalid INTO series_internal_id FROM "resources" WHERE publicId = series_public_id; END; BEGIN INSERT INTO "resources" VALUES (DEFAULT, 3, instance_public_id, series_internal_id, 0) RETURNING internalid INTO instance_internal_id; EXCEPTION WHEN unique_violation THEN is_new_instance := 0; SELECT internalid INTO instance_internal_id FROM "resources" WHERE publicId = instance_public_id; END; END IF; IF is_new_instance > 0 THEN -- Move the patient to the end of the recycling order. PERFORM PatientAddedOrUpdated(patient_internal_id); END IF; END; $body$ LANGUAGE plpgsql; -- function to compute a statistic in a ReadOnly transaction CREATE OR REPLACE FUNCTION ComputeStatisticsReadOnly( IN statistics_key INTEGER, OUT accumulated_value BIGINT ) RETURNS BIGINT AS $body$ DECLARE current_value BIGINT; BEGIN SELECT VALUE FROM GlobalIntegers INTO current_value WHERE key = statistics_key; SELECT COALESCE(SUM(value), 0) + current_value FROM GlobalIntegersChanges INTO accumulated_value WHERE key = statistics_key; END; $body$ LANGUAGE plpgsql; -- -- new in Rev3 -- Computes the childCount for a number of resources for which it has not been computed yet. -- This is actually used only after an update to Rev3. A thread will call this function -- at regular interval to update all missing values and stop once all values have been processed. CREATE OR REPLACE FUNCTION ComputeMissingChildCount( IN batch_size BIGINT, OUT updated_rows_count BIGINT ) RETURNS BIGINT AS $body$ BEGIN UPDATE Resources AS r SET childCount = (SELECT COUNT(childLevel.internalId) FROM Resources AS childLevel WHERE childLevel.parentId = r.internalId) WHERE internalId IN ( SELECT internalId FROM Resources WHERE resourceType < 3 AND childCount IS NULL LIMIT batch_size); -- Get the number of rows affected GET DIAGNOSTICS updated_rows_count = ROW_COUNT; END; $body$ LANGUAGE plpgsql; -- -- new in rev4 -- This table records all resource entries whose childCount column is currently invalid -- because of recent addition/removal of a child. -- This way, each transaction that is adding/removing a child can add row independently -- in this table without having to lock the parent resource row. -- At regular interval, the DB housekeeping thread updates the childCount column of -- resources with an entry in this table. CREATE TABLE IF NOT EXISTS InvalidChildCounts( id BIGINT REFERENCES Resources(internalId) ON DELETE CASCADE, updatedAt TIMESTAMP DEFAULT NOW()); -- Updates the Resources.childCount column with the delta that have not been committed yet. -- A thread will call this function at regular interval to update all pending values. CREATE OR REPLACE FUNCTION UpdateInvalidChildCounts( OUT updated_rows_count BIGINT ) RETURNS BIGINT AS $body$ DECLARE locked_resources_ids BIGINT[]; BEGIN -- Lock the resources rows asap to prevent deadlocks -- that will need to be retried SELECT ARRAY(SELECT internalId FROM Resources WHERE internalId IN (SELECT DISTINCT id FROM InvalidChildCounts) FOR UPDATE SKIP LOCKED) INTO locked_resources_ids; -- New rows can be added in the meantime, they won't be taken into account this time. WITH deleted_rows AS ( DELETE FROM InvalidChildCounts WHERE id = ANY(locked_resources_ids) RETURNING id ) UPDATE Resources SET childCount = (SELECT COUNT(childLevel.internalId) FROM Resources AS childLevel WHERE childLevel.parentId = Resources.internalId) WHERE internalid = ANY(locked_resources_ids); -- Get the number of rows affected GET DIAGNOSTICS updated_rows_count = ROW_COUNT; END; $body$ LANGUAGE plpgsql; DROP TRIGGER IF EXISTS IncrementChildCount on Resources; DROP TRIGGER IF EXISTS DecrementChildCount on Resources; CREATE OR REPLACE FUNCTION UpdateChildCount() RETURNS TRIGGER AS $body$ BEGIN IF TG_OP = 'INSERT' THEN IF new.parentId IS NOT NULL THEN -- mark the parent's childCount as invalid INSERT INTO InvalidChildCounts VALUES(new.parentId); END IF; ELSIF TG_OP = 'DELETE' THEN IF old.parentId IS NOT NULL THEN BEGIN -- mark the parent's childCount as invalid INSERT INTO InvalidChildCounts VALUES(old.parentId); EXCEPTION -- when deleting the last child of a parent, the insert will fail (this is expected) WHEN foreign_key_violation THEN NULL; END; END IF; END IF; RETURN NULL; END; $body$ LANGUAGE plpgsql; CREATE TRIGGER IncrementChildCount AFTER INSERT ON Resources FOR EACH ROW EXECUTE PROCEDURE UpdateChildCount(); CREATE TRIGGER DecrementChildCount AFTER DELETE ON Resources FOR EACH ROW WHEN (OLD.parentId IS NOT NULL) EXECUTE PROCEDURE UpdateChildCount(); -- new in 1.12.8 (rev 5) CREATE TABLE IF NOT EXISTS KeyValueStores( storeId TEXT NOT NULL, key TEXT NOT NULL, value BYTEA NOT NULL, PRIMARY KEY(storeId, key) -- Prevents duplicates ); CREATE TABLE IF NOT EXISTS Queues ( id BIGSERIAL NOT NULL PRIMARY KEY, queueId TEXT NOT NULL, value BYTEA NOT NULL ); CREATE INDEX IF NOT EXISTS QueuesIndex ON Queues (queueId, id); -- new in rev 6 CREATE SEQUENCE IF NOT EXISTS PatientRecyclingOrderSequence INCREMENT 1 START 1; CREATE OR REPLACE FUNCTION ProtectPatient(patient_id BIGINT) RETURNS VOID AS $$ BEGIN INSERT INTO Metadata (id, type, value, revision) -- 18 = IsProtected VALUES (patient_id, 18, 'true', 0) ON CONFLICT (id, type) DO UPDATE SET value = EXCLUDED.value, revision = EXCLUDED.revision; END; $$ LANGUAGE plpgsql; -- remove IsProtected and update PatientRecyclingOrder CREATE OR REPLACE FUNCTION UnprotectPatient(patient_id BIGINT) RETURNS VOID AS $$ BEGIN DELETE FROM Metadata WHERE id = patient_id AND type = 18; -- 18 = IsProtected INSERT INTO Metadata (id, type, value, revision) VALUES (patient_id, 19, nextval('PatientRecyclingOrderSequence')::TEXT, 0) ON CONFLICT (id, type) DO UPDATE SET value = EXCLUDED.value, revision = EXCLUDED.revision; END; $$ LANGUAGE plpgsql; CREATE TABLE IF NOT EXISTS AuditLogs ( ts TIMESTAMP DEFAULT NOW(), sourcePlugin TEXT NOT NULL, userId TEXT NOT NULL, resourceType INTEGER NOT NULL, resourceId VARCHAR(64) NOT NULL, action TEXT NOT NULL, logData BYTEA ); CREATE INDEX IF NOT EXISTS AuditLogsUserId ON AuditLogs (userId); CREATE INDEX IF NOT EXISTS AuditLogsResourceId ON AuditLogs (resourceId); CREATE INDEX IF NOT EXISTS AuditLogsAction ON AuditLogs (action); -- set the global properties that actually documents the DB version, revision and some of the capabilities DELETE FROM GlobalProperties WHERE property IN (1, 4, 6, 10, 11, 12, 13, 14); INSERT INTO GlobalProperties VALUES (1, 6); -- GlobalProperty_DatabaseSchemaVersion INSERT INTO GlobalProperties VALUES (4, 6); -- GlobalProperty_DatabasePatchLevel INSERT INTO GlobalProperties VALUES (6, 1); -- GlobalProperty_GetTotalSizeIsFast INSERT INTO GlobalProperties VALUES (10, 1); -- GlobalProperty_HasTrigramIndex INSERT INTO GlobalProperties VALUES (11, 3); -- GlobalProperty_HasCreateInstance -- this is actually the 3rd version of HasCreateInstance INSERT INTO GlobalProperties VALUES (12, 1); -- GlobalProperty_HasFastCountResources INSERT INTO GlobalProperties VALUES (13, 1); -- GlobalProperty_GetLastChangeIndex INSERT INTO GlobalProperties VALUES (14, 1); -- GlobalProperty_HasComputeStatisticsReadOnly