comparison PostgreSQL/Plugins/SQL/PrepareIndexV2.sql @ 437:d979f25e60cf pg-transactions

Re-organized DB creation/upgrade into standalone files
author Alain Mazy <am@osimis.io>
date Mon, 18 Dec 2023 18:50:01 +0100
parents
children f790232b77ef
comparison
equal deleted inserted replaced
436:f16faa1fdc46 437:d979f25e60cf
1 -- This SQL file creates a DB in revision 2 directly (version is being used from v 6.0 of the PostgreSQL plugin)
2 -- It is also run after upgrade scripts to create new tables and or create/replace triggers and functions.
3 -- This script is self contained, it contains everything that needs to be run to create an Orthanc DB.
4 -- Note: it is and must be idempotent.
5
6 CREATE TABLE IF NOT EXISTS GlobalProperties(
7 property INTEGER PRIMARY KEY,
8 value TEXT
9 );
10
11 CREATE TABLE IF NOT EXISTS Resources(
12 internalId BIGSERIAL NOT NULL PRIMARY KEY,
13 resourceType INTEGER NOT NULL,
14 publicId VARCHAR(64) NOT NULL,
15 parentId BIGINT REFERENCES Resources(internalId) ON DELETE CASCADE,
16 CONSTRAINT UniquePublicId UNIQUE (publicId)
17 );
18
19 CREATE TABLE IF NOT EXISTS MainDicomTags(
20 id BIGINT REFERENCES Resources(internalId) ON DELETE CASCADE,
21 tagGroup INTEGER,
22 tagElement INTEGER,
23 value TEXT,
24 PRIMARY KEY(id, tagGroup, tagElement)
25 );
26
27 CREATE TABLE IF NOT EXISTS DicomIdentifiers(
28 id BIGINT REFERENCES Resources(internalId) ON DELETE CASCADE,
29 tagGroup INTEGER,
30 tagElement INTEGER,
31 value TEXT,
32 PRIMARY KEY(id, tagGroup, tagElement)
33 );
34
35 CREATE TABLE IF NOT EXISTS Metadata(
36 id BIGINT REFERENCES Resources(internalId) ON DELETE CASCADE,
37 type INTEGER NOT NULL,
38 value TEXT,
39 revision INTEGER,
40 PRIMARY KEY(id, type)
41 );
42
43 CREATE TABLE IF NOT EXISTS AttachedFiles(
44 id BIGINT REFERENCES Resources(internalId) ON DELETE CASCADE,
45 fileType INTEGER,
46 uuid VARCHAR(64) NOT NULL,
47 compressedSize BIGINT,
48 uncompressedSize BIGINT,
49 compressionType INTEGER,
50 uncompressedHash VARCHAR(40),
51 compressedHash VARCHAR(40),
52 revision INTEGER,
53 PRIMARY KEY(id, fileType)
54 );
55
56 CREATE TABLE IF NOT EXISTS Changes(
57 seq BIGSERIAL NOT NULL PRIMARY KEY,
58 changeType INTEGER,
59 internalId BIGINT REFERENCES Resources(internalId) ON DELETE CASCADE,
60 resourceType INTEGER,
61 date VARCHAR(64)
62 );
63
64 CREATE TABLE IF NOT EXISTS ExportedResources(
65 seq BIGSERIAL NOT NULL PRIMARY KEY,
66 resourceType INTEGER,
67 publicId VARCHAR(64),
68 remoteModality TEXT,
69 patientId VARCHAR(64),
70 studyInstanceUid TEXT,
71 seriesInstanceUid TEXT,
72 sopInstanceUid TEXT,
73 date VARCHAR(64)
74 );
75
76 CREATE TABLE IF NOT EXISTS PatientRecyclingOrder(
77 seq BIGSERIAL NOT NULL PRIMARY KEY,
78 patientId BIGINT REFERENCES Resources(internalId) ON DELETE CASCADE,
79 CONSTRAINT UniquePatientId UNIQUE (patientId)
80 );
81
82 CREATE TABLE IF NOT EXISTS Labels(
83 id BIGINT REFERENCES Resources(internalId) ON DELETE CASCADE,
84 label TEXT,
85 PRIMARY KEY(id, label)
86 );
87
88 CREATE TABLE IF NOT EXISTS GlobalIntegers(
89 key INTEGER PRIMARY KEY,
90 value BIGINT);
91 -- GlobalIntegers keys:
92 -- 0: CompressedSize
93 -- 1: UncompressedSize
94 -- 2: PatientsCount
95 -- 3: StudiesCount
96 -- 4: SeriesCount
97 -- 5: InstancesCount
98 -- 6: ChangeSeq
99 -- 7: PatientRecyclingOrderSeq
100
101 CREATE TABLE IF NOT EXISTS ServerProperties(
102 server VARCHAR(64) NOT NULL,
103 property INTEGER, value TEXT,
104 PRIMARY KEY(server, property)
105 );
106
107 CREATE INDEX IF NOT EXISTS ChildrenIndex ON Resources(parentId);
108 CREATE INDEX IF NOT EXISTS PublicIndex ON Resources(publicId);
109 CREATE INDEX IF NOT EXISTS ResourceTypeIndex ON Resources(resourceType);
110 CREATE INDEX IF NOT EXISTS PatientRecyclingIndex ON PatientRecyclingOrder(patientId);
111
112 CREATE INDEX IF NOT EXISTS MainDicomTagsIndex ON MainDicomTags(id);
113 CREATE INDEX IF NOT EXISTS DicomIdentifiersIndex1 ON DicomIdentifiers(id);
114 CREATE INDEX IF NOT EXISTS DicomIdentifiersIndex2 ON DicomIdentifiers(tagGroup, tagElement);
115 CREATE INDEX IF NOT EXISTS DicomIdentifiersIndexValues ON DicomIdentifiers(value);
116
117 CREATE INDEX IF NOT EXISTS ChangesIndex ON Changes(internalId);
118 CREATE INDEX IF NOT EXISTS LabelsIndex1 ON LABELS(id);
119 CREATE INDEX IF NOT EXISTS LabelsIndex2 ON LABELS(label);
120
121 ------------------- Trigram index creation -------------------
122
123
124 -- Apply fix for performance issue (speed up wildcard search by using GIN trigrams). This implements the patch suggested
125 -- in issue #47, BUT we also keep the original "DicomIdentifiersIndexValues", as it leads to better
126 -- performance for "strict" searches (i.e. searches involving no wildcard).
127 -- https://www.postgresql.org/docs/current/static/pgtrgm.html
128 -- https://orthanc.uclouvain.be/bugs/show_bug.cgi?id=47
129
130 DO $body$
131 begin
132 IF EXISTS (SELECT 1 FROM pg_available_extensions WHERE name='pg_trgm') THEN
133 CREATE EXTENSION IF NOT EXISTS pg_trgm;
134 CREATE INDEX IF NOT EXISTS DicomIdentifiersIndexValues2 ON DicomIdentifiers USING gin(value gin_trgm_ops);
135 ELSE
136 RAISE NOTICE 'pg_trgm extension is not available on you system';
137 END IF;
138 END $body$;
139
140
141 ------------------- PatientAdded trigger & PatientRecyclingOrder -------------------
142 DROP TRIGGER IF EXISTS PatientAdded ON Resources;
143
144 CREATE OR REPLACE FUNCTION PatientAddedOrUpdated(
145 IN patient_id BIGINT,
146 IN is_update BIGINT
147 )
148 RETURNS VOID AS $body$
149 BEGIN
150 DECLARE
151 newSeq BIGINT;
152 BEGIN
153 UPDATE GlobalIntegers SET value = value + 1 WHERE key = 7 RETURNING value INTO newSeq;
154 IF is_update > 0 THEN
155 -- Note: Protected patient are not listed in this table ! So, they won't be updated
156 UPDATE PatientRecyclingOrder SET seq = newSeq WHERE PatientRecyclingOrder.patientId = patient_id;
157 ELSE
158 INSERT INTO PatientRecyclingOrder VALUES (newSeq, patient_id);
159 -- ON CONFLICT (patientId) DO UPDATE SET seq = newSeq;
160 END IF;
161 END;
162 END;
163 $body$ LANGUAGE plpgsql;
164
165 CREATE OR REPLACE FUNCTION PatientAddedFunc()
166 RETURNS TRIGGER AS $body$
167 BEGIN
168 -- The "0" corresponds to "OrthancPluginResourceType_Patient"
169 IF new.resourceType = 0 THEN
170 PERFORM PatientAddedOrUpdated(new.internalId, 0);
171 -- UPDATE GlobalIntegers WHERE key = 7 SET value = value + 1 RETURNING value
172 -- INSERT INTO PatientRecyclingOrder VALUES ((SELECT value FROM GlobalIntegers WHERE key = 7), new.internalId)
173 -- ON CONFLICT ;
174 END IF;
175 RETURN NULL;
176 END;
177 $body$ LANGUAGE plpgsql;
178
179 CREATE TRIGGER PatientAdded
180 AFTER INSERT ON Resources
181 FOR EACH ROW
182 EXECUTE PROCEDURE PatientAddedFunc();
183
184 -- initial value for PatientRecyclingOrderSeq
185 INSERT INTO GlobalIntegers
186 SELECT 7, CAST(COALESCE(MAX(seq), 0) AS BIGINT) FROM PatientRecyclingOrder
187 ON CONFLICT DO NOTHING;
188
189
190 ------------------- ResourceDeleted trigger -------------------
191 DROP TRIGGER IF EXISTS ResourceDeleted ON Resources;
192
193 -- The following trigger combines 2 triggers from SQLite:
194 -- ResourceDeleted + ResourceDeletedParentCleaning
195 CREATE OR REPLACE FUNCTION ResourceDeletedFunc()
196 RETURNS TRIGGER AS $body$
197 BEGIN
198 -- RAISE NOTICE 'ResourceDeletedFunc %', old.publicId;
199 INSERT INTO DeletedResources VALUES (old.resourceType, old.publicId);
200
201 -- If this resource is the latest child, delete the parent
202 DELETE FROM Resources WHERE internalId = old.parentId
203 AND NOT EXISTS (SELECT 1 FROM Resources WHERE parentId = old.parentId);
204 RETURN NULL;
205 END;
206 $body$ LANGUAGE plpgsql;
207
208 CREATE TRIGGER ResourceDeleted
209 AFTER DELETE ON Resources
210 FOR EACH ROW
211 EXECUTE PROCEDURE ResourceDeletedFunc();
212
213
214 ------------------- DeleteResource function -------------------
215
216 CREATE OR REPLACE FUNCTION DeleteResource(
217 IN id BIGINT,
218 OUT remaining_ancestor_resource_type INTEGER,
219 OUT remaining_anncestor_public_id TEXT) AS $body$
220
221 DECLARE
222 deleted_row RECORD;
223
224 BEGIN
225
226 SET client_min_messages = warning; -- suppress NOTICE: relation "deletedresources" already exists, skipping
227
228 -- note: temporary tables are created at session (connection) level -> they are likely to exist
229 -- these tables are used by the triggers
230 CREATE TEMPORARY TABLE IF NOT EXISTS DeletedResources(
231 resourceType INTEGER NOT NULL,
232 publicId VARCHAR(64) NOT NULL
233 );
234
235 RESET client_min_messages;
236
237 -- clear the temporary table in case it has been created earlier in the session
238 DELETE FROM DeletedResources;
239
240 -- create/clear the DeletedFiles temporary table
241 PERFORM CreateDeletedFilesTemporaryTable();
242
243 -- delete the resource itself
244 DELETE FROM Resources WHERE internalId=id RETURNING * INTO deleted_row;
245 -- note: there is a ResourceDeletedFunc trigger that will execute here and delete the parents if there are no remaining children +
246
247 -- If this resource still has siblings, keep track of the remaining parent
248 -- (a parent that must not be deleted but whose LastUpdate must be updated)
249 SELECT resourceType, publicId INTO remaining_ancestor_resource_type, remaining_anncestor_public_id
250 FROM Resources
251 WHERE internalId = deleted_row.parentId
252 AND EXISTS (SELECT 1 FROM Resources WHERE parentId = deleted_row.parentId);
253
254 END;
255
256 $body$ LANGUAGE plpgsql;
257
258 CREATE OR REPLACE FUNCTION CreateDeletedFilesTemporaryTable(
259 ) RETURNS VOID AS $body$
260
261 BEGIN
262
263 SET client_min_messages = warning; -- suppress NOTICE: relation "deletedresources" already exists, skipping
264
265 -- note: temporary tables are created at session (connection) level -> they are likely to exist
266 CREATE TEMPORARY TABLE IF NOT EXISTS DeletedFiles(
267 uuid VARCHAR(64) NOT NULL,
268 fileType INTEGER,
269 compressedSize BIGINT,
270 uncompressedSize BIGINT,
271 compressionType INTEGER,
272 uncompressedHash VARCHAR(40),
273 compressedHash VARCHAR(40)
274 );
275
276 RESET client_min_messages;
277
278 -- clear the temporary table in case it has been created earlier in the session
279 DELETE FROM DeletedFiles;
280 END;
281
282 $body$ LANGUAGE plpgsql;
283
284
285 DROP TRIGGER IF EXISTS AttachedFileDeleted ON AttachedFiles;
286
287 CREATE OR REPLACE FUNCTION AttachedFileDeletedFunc()
288 RETURNS TRIGGER AS $body$
289 BEGIN
290 INSERT INTO DeletedFiles VALUES
291 (old.uuid, old.filetype, old.compressedSize,
292 old.uncompressedSize, old.compressionType,
293 old.uncompressedHash, old.compressedHash);
294 RETURN NULL;
295 END;
296 $body$ LANGUAGE plpgsql;
297
298 CREATE TRIGGER AttachedFileDeleted
299 AFTER DELETE ON AttachedFiles
300 FOR EACH ROW
301 EXECUTE PROCEDURE AttachedFileDeletedFunc();
302
303
304 ------------------- Fast Statistics -------------------
305
306 -- initialize values if not already theere
307 INSERT INTO GlobalIntegers
308 SELECT 0, CAST(COALESCE(SUM(compressedSize), 0) AS BIGINT) FROM AttachedFiles
309 ON CONFLICT DO NOTHING;
310
311 INSERT INTO GlobalIntegers
312 SELECT 1, CAST(COALESCE(SUM(uncompressedSize), 0) AS BIGINT) FROM AttachedFiles
313 ON CONFLICT DO NOTHING;
314
315 INSERT INTO GlobalIntegers
316 SELECT 2, CAST(COALESCE(COUNT(*), 0) AS BIGINT) FROM Resources WHERE resourceType = 0 -- Count patients
317 ON CONFLICT DO NOTHING;
318
319 INSERT INTO GlobalIntegers
320 SELECT 3, CAST(COALESCE(COUNT(*), 0) AS BIGINT) FROM Resources WHERE resourceType = 1 -- Count studies
321 ON CONFLICT DO NOTHING;
322
323 INSERT INTO GlobalIntegers
324 SELECT 4, CAST(COALESCE(COUNT(*), 0) AS BIGINT) FROM Resources WHERE resourceType = 2 -- Count series
325 ON CONFLICT DO NOTHING;
326
327 INSERT INTO GlobalIntegers
328 SELECT 5, CAST(COALESCE(COUNT(*), 0) AS BIGINT) FROM Resources WHERE resourceType = 3 -- Count instances
329 ON CONFLICT DO NOTHING;
330
331
332 -- this table stores all changes that needs to be performed to the GlobalIntegers table
333 -- This way, each transaction can add row independently in this table without having to lock
334 -- any row (which was the case with previous FastTotalSize).
335 -- These changes will be applied at regular interval by an external thread or when someone
336 -- requests the statistics
337 CREATE TABLE IF NOT EXISTS GlobalIntegersChanges(
338 key INTEGER,
339 value BIGINT);
340
341 CREATE OR REPLACE FUNCTION UpdateSingleStatistic(
342 IN statistics_key INTEGER,
343 OUT new_value BIGINT
344 ) AS $body$
345 BEGIN
346
347 -- Delete the current changes, sum them and update the GlobalIntegers row.
348 -- New rows can be added in the meantime, they won't be deleted or summed.
349 WITH deleted_rows AS (
350 DELETE FROM GlobalIntegersChanges
351 WHERE GlobalIntegersChanges.key = statistics_key
352 RETURNING value
353 )
354 UPDATE GlobalIntegers
355 SET value = value + (
356 SELECT COALESCE(SUM(value), 0)
357 FROM deleted_rows
358 )
359 WHERE GlobalIntegers.key = statistics_key
360 RETURNING value INTO new_value;
361
362 END;
363 $body$ LANGUAGE plpgsql;
364
365
366 CREATE OR REPLACE FUNCTION UpdateStatistics(
367 OUT patients_cunt BIGINT,
368 OUT studies_count BIGINT,
369 OUT series_count BIGINT,
370 OUT instances_count BIGINT,
371 OUT total_compressed_size BIGINT,
372 OUT total_uncompressed_size BIGINT
373 ) AS $body$
374 BEGIN
375
376 SELECT UpdateSingleStatistic(0) INTO total_compressed_size;
377 SELECT UpdateSingleStatistic(1) INTO total_uncompressed_size;
378 SELECT UpdateSingleStatistic(2) INTO patients_cunt;
379 SELECT UpdateSingleStatistic(3) INTO studies_count;
380 SELECT UpdateSingleStatistic(4) INTO series_count;
381 SELECT UpdateSingleStatistic(5) INTO instances_count;
382
383 END;
384 $body$ LANGUAGE plpgsql;
385
386
387 CREATE OR REPLACE FUNCTION IncrementResourcesTrackerFunc()
388 RETURNS TRIGGER AS $$
389 BEGIN
390 INSERT INTO GlobalIntegersChanges VALUES(new.resourceType + 2, 1);
391 RETURN NULL;
392 END;
393 $$ LANGUAGE plpgsql;
394
395 CREATE OR REPLACE FUNCTION DecrementResourcesTrackerFunc()
396 RETURNS TRIGGER AS $$
397 BEGIN
398 INSERT INTO GlobalIntegersChanges VALUES(old.resourceType + 2, -1);
399 RETURN NULL;
400 END;
401 $$ LANGUAGE plpgsql;
402
403
404 CREATE OR REPLACE FUNCTION AttachedFileIncrementSizeFunc()
405 RETURNS TRIGGER AS $body$
406 BEGIN
407 INSERT INTO GlobalIntegersChanges VALUES(0, new.compressedSize);
408 INSERT INTO GlobalIntegersChanges VALUES(1, new.uncompressedSize);
409 RETURN NULL;
410 END;
411 $body$ LANGUAGE plpgsql;
412
413 CREATE OR REPLACE FUNCTION AttachedFileDecrementSizeFunc()
414 RETURNS TRIGGER AS $body$
415 BEGIN
416 INSERT INTO GlobalIntegersChanges VALUES(0, -old.compressedSize);
417 INSERT INTO GlobalIntegersChanges VALUES(1, -old.uncompressedSize);
418 RETURN NULL;
419 END;
420 $body$ LANGUAGE plpgsql;
421
422 DROP TRIGGER IF EXISTS AttachedFileIncrementSize on AttachedFiles;
423 CREATE TRIGGER AttachedFileIncrementSize
424 AFTER INSERT ON AttachedFiles
425 FOR EACH ROW
426 EXECUTE PROCEDURE AttachedFileIncrementSizeFunc();
427
428 DROP TRIGGER IF EXISTS AttachedFileDecrementSize on AttachedFiles;
429 CREATE TRIGGER AttachedFileDecrementSize
430 AFTER DELETE ON AttachedFiles
431 FOR EACH ROW
432 EXECUTE PROCEDURE AttachedFileDecrementSizeFunc();
433
434 DROP TRIGGER IF EXISTS IncrementResourcesTracker on Resources;
435 CREATE TRIGGER IncrementResourcesTracker
436 AFTER INSERT ON Resources
437 FOR EACH ROW
438 EXECUTE PROCEDURE IncrementResourcesTrackerFunc();
439
440 DROP TRIGGER IF EXISTS DecrementResourcesTracker on Resources;
441 CREATE TRIGGER DecrementResourcesTracker
442 AFTER DELETE ON Resources
443 FOR EACH ROW
444 EXECUTE PROCEDURE DecrementResourcesTrackerFunc();
445
446
447 ------------------- InsertOrUpdateMetadata function -------------------
448 CREATE OR REPLACE FUNCTION InsertOrUpdateMetadata(resource_ids BIGINT[],
449 metadata_types INTEGER[],
450 metadata_values TEXT[],
451 revisions INTEGER[])
452 RETURNS VOID AS $body$
453 BEGIN
454 FOR i IN 1 .. ARRAY_LENGTH(resource_ids, 1) LOOP
455 -- RAISE NOTICE 'Parameter %: % % %', i, resource_ids[i], metadata_types[i], metadata_values[i];
456 INSERT INTO Metadata VALUES(resource_ids[i], metadata_types[i], metadata_values[i], revisions[i]) ON CONFLICT DO NOTHING;
457 END LOOP;
458
459 END;
460 $body$ LANGUAGE plpgsql;
461
462
463 ------------------- GetLastChange function -------------------
464 DROP TRIGGER IF EXISTS InsertedChange ON Changes;
465
466 -- insert the value if not already there
467 INSERT INTO GlobalIntegers
468 SELECT 6, CAST(COALESCE(MAX(seq), 0) AS BIGINT) FROM Changes
469 ON CONFLICT DO NOTHING;
470
471 CREATE OR REPLACE FUNCTION InsertedChangeFunc()
472 RETURNS TRIGGER AS $body$
473 BEGIN
474 UPDATE GlobalIntegers SET value = new.seq WHERE key = 6;
475 RETURN NULL;
476 END;
477 $body$ LANGUAGE plpgsql;
478
479 CREATE TRIGGER InsertedChange
480 AFTER INSERT ON Changes
481 FOR EACH ROW
482 EXECUTE PROCEDURE InsertedChangeFunc();
483
484
485 ------------------- CreateInstance function -------------------
486 CREATE OR REPLACE FUNCTION CreateInstance(
487 IN patient_public_id TEXT,
488 IN study_public_id TEXT,
489 IN series_public_id TEXT,
490 IN instance_public_id TEXT,
491 OUT is_new_patient BIGINT,
492 OUT is_new_study BIGINT,
493 OUT is_new_series BIGINT,
494 OUT is_new_instance BIGINT,
495 OUT patient_internal_id BIGINT,
496 OUT study_internal_id BIGINT,
497 OUT series_internal_id BIGINT,
498 OUT instance_internal_id BIGINT) AS $body$
499
500 DECLARE
501 patientSeq BIGINT;
502 countRecycling BIGINT;
503
504 BEGIN
505 is_new_patient := 1;
506 is_new_study := 1;
507 is_new_series := 1;
508 is_new_instance := 1;
509
510 BEGIN
511 INSERT INTO "resources" VALUES (DEFAULT, 0, patient_public_id, NULL);
512 EXCEPTION
513 WHEN unique_violation THEN
514 is_new_patient := 0;
515 END;
516 SELECT internalid INTO patient_internal_id FROM "resources" WHERE publicId = patient_public_id AND resourcetype = 0;
517
518 BEGIN
519 INSERT INTO "resources" VALUES (DEFAULT, 1, study_public_id, patient_internal_id);
520 EXCEPTION
521 WHEN unique_violation THEN
522 is_new_study := 0;
523 END;
524 SELECT internalid INTO study_internal_id FROM "resources" WHERE publicId = study_public_id AND resourcetype = 1;
525
526 BEGIN
527 INSERT INTO "resources" VALUES (DEFAULT, 2, series_public_id, study_internal_id);
528 EXCEPTION
529 WHEN unique_violation THEN
530 is_new_series := 0;
531 END;
532 SELECT internalid INTO series_internal_id FROM "resources" WHERE publicId = series_public_id AND resourcetype = 2;
533
534 BEGIN
535 INSERT INTO "resources" VALUES (DEFAULT, 3, instance_public_id, series_internal_id);
536 EXCEPTION
537 WHEN unique_violation THEN
538 is_new_instance := 0;
539 END;
540 SELECT internalid INTO instance_internal_id FROM "resources" WHERE publicId = instance_public_id AND resourcetype = 3;
541
542 IF is_new_instance > 0 THEN
543 -- Move the patient to the end of the recycling order.
544 PERFORM PatientAddedOrUpdated(patient_internal_id, 1);
545 END IF;
546 END;
547
548 $body$ LANGUAGE plpgsql;