]> git.evergreen-ils.org Git - working/Evergreen.git/blob - Open-ILS/src/sql/Pg/011.schema.authority.sql
Avoid problems when auth recs are missing the 901c
[working/Evergreen.git] / Open-ILS / src / sql / Pg / 011.schema.authority.sql
1 /*
2  * Copyright (C) 2004-2008  Georgia Public Library Service
3  * Copyright (C) 2008  Equinox Software, Inc.
4  * Copyright (C) 2010  Laurentian University
5  * Mike Rylander <miker@esilibrary.com> 
6  * Dan Scott <dscott@laurentian.ca>
7  *
8  * This program is free software; you can redistribute it and/or
9  * modify it under the terms of the GNU General Public License
10  * as published by the Free Software Foundation; either version 2
11  * of the License, or (at your option) any later version.
12  *
13  * This program is distributed in the hope that it will be useful,
14  * but WITHOUT ANY WARRANTY; without even the implied warranty of
15  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
16  * GNU General Public License for more details.
17  *
18  */
19
20 DROP SCHEMA IF EXISTS authority CASCADE;
21
22 BEGIN;
23 CREATE SCHEMA authority;
24
25 CREATE TABLE authority.control_set (
26     id          SERIAL  PRIMARY KEY,
27     name        TEXT    NOT NULL UNIQUE, -- i18n
28     description TEXT                     -- i18n
29 );
30
31 CREATE TABLE authority.control_set_authority_field (
32     id          SERIAL  PRIMARY KEY,
33     main_entry  INT     REFERENCES authority.control_set_authority_field (id) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED,
34     control_set INT     NOT NULL REFERENCES authority.control_set (id) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED,
35     tag         CHAR(3) NOT NULL,
36     nfi         CHAR(1),          -- non-filing indicator
37     sf_list     TEXT    NOT NULL,
38     name        TEXT    NOT NULL, -- i18n
39     description TEXT              -- i18n
40 );
41
42 CREATE TABLE authority.control_set_bib_field (
43     id              SERIAL  PRIMARY KEY,
44     authority_field INT     NOT NULL REFERENCES authority.control_set_authority_field (id) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED,
45     tag             CHAR(3) NOT NULL
46 );
47
48 CREATE TABLE authority.thesaurus (
49     code        TEXT    PRIMARY KEY,     -- MARC21 thesaurus code
50     control_set INT     REFERENCES authority.control_set (id) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED,
51     name        TEXT    NOT NULL UNIQUE, -- i18n
52     description TEXT                     -- i18n
53 );
54
55 CREATE TABLE authority.browse_axis (
56     code        TEXT    PRIMARY KEY,
57     name        TEXT    UNIQUE NOT NULL, -- i18n
58     sorter      TEXT    REFERENCES config.record_attr_definition (name) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED,
59     description TEXT
60 );
61
62 CREATE TABLE authority.browse_axis_authority_field_map (
63     id          SERIAL  PRIMARY KEY,
64     axis        TEXT    NOT NULL REFERENCES authority.browse_axis (code) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED,
65     field       INT     NOT NULL REFERENCES authority.control_set_authority_field (id) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED
66 );
67
68 CREATE TABLE authority.record_entry (
69     id              BIGSERIAL    PRIMARY KEY,
70     create_date     TIMESTAMP WITH TIME ZONE    NOT NULL DEFAULT now(),
71     edit_date       TIMESTAMP WITH TIME ZONE    NOT NULL DEFAULT now(),
72     creator         INT     NOT NULL DEFAULT 1,
73     editor          INT     NOT NULL DEFAULT 1,
74     active          BOOL    NOT NULL DEFAULT TRUE,
75     deleted         BOOL    NOT NULL DEFAULT FALSE,
76     source          INT,
77     control_set     INT     REFERENCES authority.control_set (id) ON UPDATE CASCADE DEFERRABLE INITIALLY DEFERRED,
78     marc            TEXT    NOT NULL,
79     last_xact_id    TEXT    NOT NULL,
80     owner           INT
81 );
82 CREATE INDEX authority_record_entry_creator_idx ON authority.record_entry ( creator );
83 CREATE INDEX authority_record_entry_editor_idx ON authority.record_entry ( editor );
84 CREATE INDEX authority_record_deleted_idx ON authority.record_entry(deleted) WHERE deleted IS FALSE OR deleted = false;
85 CREATE TRIGGER a_marcxml_is_well_formed BEFORE INSERT OR UPDATE ON authority.record_entry FOR EACH ROW EXECUTE PROCEDURE biblio.check_marcxml_well_formed();
86 CREATE TRIGGER b_maintain_901 BEFORE INSERT OR UPDATE ON authority.record_entry FOR EACH ROW EXECUTE PROCEDURE evergreen.maintain_901();
87 CREATE TRIGGER c_maintain_control_numbers BEFORE INSERT OR UPDATE ON authority.record_entry FOR EACH ROW EXECUTE PROCEDURE maintain_control_numbers();
88
89 CREATE TABLE authority.bib_linking (
90     id          BIGSERIAL   PRIMARY KEY,
91     bib         BIGINT      NOT NULL REFERENCES biblio.record_entry (id),
92     authority   BIGINT      NOT NULL REFERENCES authority.record_entry (id)
93 );
94 CREATE INDEX authority_bl_bib_idx ON authority.bib_linking ( bib );
95 CREATE UNIQUE INDEX authority_bl_bib_authority_once_idx ON authority.bib_linking ( authority, bib );
96
97 CREATE TABLE authority.record_note (
98     id          BIGSERIAL   PRIMARY KEY,
99     record      BIGINT      NOT NULL REFERENCES authority.record_entry (id) DEFERRABLE INITIALLY DEFERRED,
100     value       TEXT        NOT NULL,
101     creator     INT         NOT NULL DEFAULT 1,
102     editor      INT         NOT NULL DEFAULT 1,
103     create_date TIMESTAMP WITH TIME ZONE    NOT NULL DEFAULT now(),
104     edit_date   TIMESTAMP WITH TIME ZONE    NOT NULL DEFAULT now()
105 );
106 CREATE INDEX authority_record_note_record_idx ON authority.record_note ( record );
107 CREATE INDEX authority_record_note_creator_idx ON authority.record_note ( creator );
108 CREATE INDEX authority_record_note_editor_idx ON authority.record_note ( editor );
109
110 CREATE TABLE authority.rec_descriptor (
111     id              BIGSERIAL PRIMARY KEY,
112     record          BIGINT,
113     record_status   TEXT,
114     encoding_level  TEXT,
115     thesaurus       TEXT
116 );
117 CREATE INDEX authority_rec_descriptor_record_idx ON authority.rec_descriptor (record);
118
119 CREATE TABLE authority.full_rec (
120     id              BIGSERIAL   PRIMARY KEY,
121     record          BIGINT      NOT NULL,
122     tag             CHAR(3)     NOT NULL,
123     ind1            TEXT,
124     ind2            TEXT,
125     subfield        TEXT,
126     value           TEXT        NOT NULL,
127     index_vector    tsvector    NOT NULL
128 );
129 CREATE INDEX authority_full_rec_record_idx ON authority.full_rec (record);
130 CREATE INDEX authority_full_rec_tag_subfield_idx ON authority.full_rec (tag, subfield);
131 CREATE INDEX authority_full_rec_tag_part_idx ON authority.full_rec (SUBSTRING(tag FROM 2));
132 CREATE INDEX authority_full_rec_subfield_a_idx ON authority.full_rec (value) WHERE subfield = 'a';
133 CREATE TRIGGER authority_full_rec_fti_trigger
134     BEFORE UPDATE OR INSERT ON authority.full_rec
135     FOR EACH ROW EXECUTE PROCEDURE oils_tsearch2('keyword');
136
137 CREATE INDEX authority_full_rec_index_vector_idx ON authority.full_rec USING GIST (index_vector);
138 /* Enable LIKE to use an index for database clusters with locales other than C or POSIX */
139 CREATE INDEX authority_full_rec_value_tpo_index ON authority.full_rec (value text_pattern_ops);
140 /* But we still need this (boooo) for paging using >, <, etc */
141 CREATE INDEX authority_full_rec_value_index ON authority.full_rec (value);
142
143 CREATE RULE protect_authority_rec_delete AS ON DELETE TO authority.record_entry DO INSTEAD (UPDATE authority.record_entry SET deleted = TRUE WHERE OLD.id = authority.record_entry.id; DELETE FROM authority.full_rec WHERE record = OLD.id);
144
145 -- Intended to be used in a unique index on authority.record_entry like so:
146 -- CREATE UNIQUE INDEX unique_by_heading_and_thesaurus
147 --   ON authority.record_entry (authority.normalize_heading(marc))
148 --   WHERE deleted IS FALSE or deleted = FALSE;
149 CREATE OR REPLACE FUNCTION authority.normalize_heading( marcxml TEXT, no_thesaurus BOOL ) RETURNS TEXT AS $func$
150 DECLARE
151     acsaf           authority.control_set_authority_field%ROWTYPE;
152     tag_used        TEXT;
153     nfi_used        TEXT;
154     sf              TEXT;
155     thes_code       TEXT;
156     cset            INT;
157     heading_text    TEXT;
158     tmp_text        TEXT;
159     first_sf        BOOL;
160     auth_id         INT DEFAULT COALESCE(NULLIF(oils_xpath_string('//*[@tag="901"]/*[local-name()="subfield" and @code="c"]', marcxml), ''), '0')::INT; 
161 BEGIN
162     SELECT control_set INTO cset FROM authority.record_entry WHERE id = auth_id;
163
164     IF cset IS NULL THEN
165         SELECT  control_set INTO cset
166           FROM  authority.control_set_authority_field
167           WHERE tag IN ( SELECT  UNNEST(XPATH('//*[starts-with(@tag,"1")]/@tag',marcxml::XML)::TEXT[]))
168           LIMIT 1;
169     END IF;
170
171     thes_code := vandelay.marc21_extract_fixed_field(marcxml,'Subj');
172     IF thes_code IS NULL THEN
173         thes_code := '|';
174     ELSIF thes_code = 'z' THEN
175         thes_code := COALESCE( oils_xpath_string('//*[@tag="040"]/*[@code="f"][1]', marcxml), '' );
176     END IF;
177
178     heading_text := '';
179     FOR acsaf IN SELECT * FROM authority.control_set_authority_field WHERE control_set = cset AND main_entry IS NULL LOOP
180         tag_used := acsaf.tag;
181         nfi_used := acsaf.nfi;
182         first_sf := TRUE;
183         FOR sf IN SELECT * FROM regexp_split_to_table(acsaf.sf_list,'') LOOP
184             tmp_text := oils_xpath_string('//*[@tag="'||tag_used||'"]/*[@code="'||sf||'"]', marcxml);
185
186             IF first_sf AND tmp_text IS NOT NULL AND nfi_used IS NOT NULL THEN
187
188                 tmp_text := SUBSTRING(
189                     tmp_text FROM
190                     COALESCE(
191                         NULLIF(
192                             REGEXP_REPLACE(
193                                 oils_xpath_string('//*[@tag="'||tag_used||'"]/@ind'||nfi_used, marcxml),
194                                 $$\D+$$,
195                                 '',
196                                 'g'
197                             ),
198                             ''
199                         )::INT,
200                         0
201                     ) + 1
202                 );
203
204             END IF;
205
206             first_sf := FALSE;
207
208             IF tmp_text IS NOT NULL AND tmp_text <> '' THEN
209                 heading_text := heading_text || E'\u2021' || sf || ' ' || tmp_text;
210             END IF;
211         END LOOP;
212         EXIT WHEN heading_text <> '';
213     END LOOP;
214
215     IF heading_text <> '' THEN
216         IF no_thesaurus IS TRUE THEN
217             heading_text := tag_used || ' ' || public.naco_normalize(heading_text);
218         ELSE
219             heading_text := tag_used || '_' || COALESCE(nfi_used,'-') || '_' || thes_code || ' ' || public.naco_normalize(heading_text);
220         END IF;
221     ELSE
222         heading_text := 'NOHEADING_' || thes_code || ' ' || MD5(marcxml);
223     END IF;
224
225     RETURN heading_text;
226 END;
227 $func$ LANGUAGE PLPGSQL IMMUTABLE;
228
229 CREATE TABLE authority.simple_heading (
230     id              BIGSERIAL   PRIMARY KEY,
231     record          BIGINT      NOT NULL REFERENCES authority.record_entry (id),
232     atag            INT         NOT NULL REFERENCES authority.control_set_authority_field (id),
233     value           TEXT        NOT NULL,
234     sort_value      TEXT        NOT NULL,
235     index_vector    tsvector    NOT NULL
236 );
237 CREATE TRIGGER authority_simple_heading_fti_trigger
238     BEFORE UPDATE OR INSERT ON authority.simple_heading
239     FOR EACH ROW EXECUTE PROCEDURE oils_tsearch2('keyword');
240
241 CREATE INDEX authority_simple_heading_index_vector_idx ON authority.simple_heading USING GIST (index_vector);
242 CREATE INDEX authority_simple_heading_value_idx ON authority.simple_heading (value);
243 CREATE INDEX authority_simple_heading_sort_value_idx ON authority.simple_heading (sort_value);
244
245 CREATE OR REPLACE FUNCTION authority.simple_heading_set( marcxml TEXT ) RETURNS SETOF authority.simple_heading AS $func$
246 DECLARE
247     res             authority.simple_heading%ROWTYPE;
248     acsaf           authority.control_set_authority_field%ROWTYPE;
249     tag_used        TEXT;
250     nfi_used        TEXT;
251     sf              TEXT;
252     cset            INT;
253     heading_text    TEXT;
254     sort_text       TEXT;
255     tmp_text        TEXT;
256     tmp_xml         TEXT;
257     first_sf        BOOL;
258     auth_id         INT DEFAULT oils_xpath_string('//*[@tag="901"]/*[local-name()="subfield" and @code="c"]', marcxml)::INT;
259 BEGIN
260
261     res.record := auth_id;
262
263     SELECT  control_set INTO cset
264       FROM  authority.control_set_authority_field
265       WHERE tag IN ( SELECT UNNEST(XPATH('//*[starts-with(@tag,"1")]/@tag',marcxml::XML)::TEXT[]) )
266       LIMIT 1;
267
268     FOR acsaf IN SELECT * FROM authority.control_set_authority_field WHERE control_set = cset LOOP
269
270         res.atag := acsaf.id;
271         tag_used := acsaf.tag;
272         nfi_used := acsaf.nfi;
273
274         FOR tmp_xml IN SELECT UNNEST(XPATH('//*[@tag="'||tag_used||'"]', marcxml::XML)) LOOP
275             heading_text := '';
276
277             FOR sf IN SELECT * FROM regexp_split_to_table(acsaf.sf_list,'') LOOP
278                 heading_text := heading_text || COALESCE( ' ' || oils_xpath_string('//*[@code="'||sf||'"]',tmp_xml::TEXT), '');
279             END LOOP;
280
281             heading_text := public.naco_normalize(heading_text);
282             
283             IF nfi_used IS NOT NULL THEN
284
285                 sort_text := SUBSTRING(
286                     heading_text FROM
287                     COALESCE(
288                         NULLIF(
289                             REGEXP_REPLACE(
290                                 oils_xpath_string('//*[@tag="'||tag_used||'"]/@ind'||nfi_used, marcxml),
291                                 $$\D+$$,
292                                 '',
293                                 'g'
294                             ),
295                             ''
296                         )::INT,
297                         0
298                     ) + 1
299                 );
300
301             ELSE
302                 sort_text := heading_text;
303             END IF;
304
305             IF heading_text IS NOT NULL AND heading_text <> '' THEN
306                 res.value := heading_text;
307                 res.sort_value := sort_text;
308                 RETURN NEXT res;
309             END IF;
310
311         END LOOP;
312
313     END LOOP;
314
315     RETURN;
316 END;
317 $func$ LANGUAGE PLPGSQL IMMUTABLE;
318
319 CREATE OR REPLACE FUNCTION authority.simple_normalize_heading( marcxml TEXT ) RETURNS TEXT AS $func$
320     SELECT authority.normalize_heading($1, TRUE);
321 $func$ LANGUAGE SQL IMMUTABLE;
322
323 CREATE OR REPLACE FUNCTION authority.normalize_heading( marcxml TEXT ) RETURNS TEXT AS $func$
324     SELECT authority.normalize_heading($1, FALSE);
325 $func$ LANGUAGE SQL IMMUTABLE;
326
327 COMMENT ON FUNCTION authority.normalize_heading( TEXT ) IS $$
328 Extract the authority heading, thesaurus, and NACO-normalized values
329 from an authority record. The primary purpose is to build a unique
330 index to defend against duplicated authority records from the same
331 thesaurus.
332 $$;
333
334 -- Adding indexes using oils_xpath_string() for the main entry tags described in
335 -- authority.control_set_authority_field would speed this up, if we ever want to use it, though
336 -- the existing index on authority.normalize_heading() helps already with a record in hand
337 CREATE OR REPLACE VIEW authority.tracing_links AS
338     SELECT  main.record AS record,
339             main.id AS main_id,
340             main.tag AS main_tag,
341             oils_xpath_string('//*[@tag="'||main.tag||'"]/*[local-name()="subfield"]', are.marc) AS main_value,
342             substr(link.value,1,1) AS relationship,
343             substr(link.value,2,1) AS use_restriction,
344             substr(link.value,3,1) AS deprecation,
345             substr(link.value,4,1) AS display_restriction,
346             link.id AS link_id,
347             link.tag AS link_tag,
348             oils_xpath_string('//*[@tag="'||link.tag||'"]/*[local-name()="subfield"]', are.marc) AS link_value,
349             authority.normalize_heading(are.marc) AS normalized_main_value
350       FROM  authority.full_rec main
351             JOIN authority.record_entry are ON (main.record = are.id)
352             JOIN authority.control_set_authority_field main_entry
353                 ON (main_entry.tag = main.tag
354                     AND main_entry.main_entry IS NULL
355                     AND main.subfield = 'a' )
356             JOIN authority.control_set_authority_field sub_entry
357                 ON (main_entry.id = sub_entry.main_entry)
358             JOIN authority.full_rec link
359                 ON (link.record = main.record
360                     AND link.tag = sub_entry.tag
361                     AND link.subfield = 'w' );
362
363 -- Function to generate an ephemeral overlay template from an authority record
364 CREATE OR REPLACE FUNCTION authority.generate_overlay_template (source_xml TEXT) RETURNS TEXT AS $f$
365 DECLARE
366     cset                INT;
367     main_entry          authority.control_set_authority_field%ROWTYPE;
368     bib_field           authority.control_set_bib_field%ROWTYPE;
369     auth_id             INT DEFAULT oils_xpath_string('//*[@tag="901"]/*[local-name()="subfield" and @code="c"]', source_xml)::INT;
370     replace_data        XML[] DEFAULT '{}'::XML[];
371     replace_rules       TEXT[] DEFAULT '{}'::TEXT[];
372     auth_field          XML[];
373 BEGIN
374     IF auth_id IS NULL THEN
375         RETURN NULL;
376     END IF;
377
378     -- Default to the LoC controll set
379     SELECT control_set INTO cset FROM authority.record_entry WHERE id = auth_id;
380
381     -- if none, make a best guess
382     IF cset IS NULL THEN
383         SELECT  control_set INTO cset
384           FROM  authority.control_set_authority_field
385           WHERE tag IN (
386                     SELECT  UNNEST(XPATH('//*[starts-with(@tag,"1")]/@tag',marc::XML)::TEXT[])
387                       FROM  authority.record_entry
388                       WHERE id = auth_id
389                 )
390           LIMIT 1;
391     END IF;
392
393     -- if STILL none, no-op change
394     IF cset IS NULL THEN
395         RETURN XMLELEMENT(
396             name record,
397             XMLATTRIBUTES('http://www.loc.gov/MARC21/slim' AS xmlns),
398             XMLELEMENT( name leader, '00881nam a2200193   4500'),
399             XMLELEMENT(
400                 name datafield,
401                 XMLATTRIBUTES( '905' AS tag, ' ' AS ind1, ' ' AS ind2),
402                 XMLELEMENT(
403                     name subfield,
404                     XMLATTRIBUTES('d' AS code),
405                     '901c'
406                 )
407             )
408         )::TEXT;
409     END IF;
410
411     FOR main_entry IN SELECT * FROM authority.control_set_authority_field acsaf WHERE acsaf.control_set = cset AND acsaf.main_entry IS NULL LOOP
412         auth_field := XPATH('//*[@tag="'||main_entry.tag||'"][1]',source_xml::XML);
413         IF ARRAY_LENGTH(auth_field,1) > 0 THEN
414             FOR bib_field IN SELECT * FROM authority.control_set_bib_field WHERE authority_field = main_entry.id LOOP
415                 replace_data := replace_data || XMLELEMENT( name datafield, XMLATTRIBUTES(bib_field.tag AS tag), XPATH('//*[local-name()="subfield"]',auth_field[1])::XML[]);
416                 replace_rules := replace_rules || ( bib_field.tag || main_entry.sf_list || E'[0~\\)' || auth_id || '$]' );
417             END LOOP;
418             EXIT;
419         END IF;
420     END LOOP;
421
422     RETURN XMLELEMENT(
423         name record,
424         XMLATTRIBUTES('http://www.loc.gov/MARC21/slim' AS xmlns),
425         XMLELEMENT( name leader, '00881nam a2200193   4500'),
426         replace_data,
427         XMLELEMENT(
428             name datafield,
429             XMLATTRIBUTES( '905' AS tag, ' ' AS ind1, ' ' AS ind2),
430             XMLELEMENT(
431                 name subfield,
432                 XMLATTRIBUTES('r' AS code),
433                 ARRAY_TO_STRING(replace_rules,',')
434             )
435         )
436     )::TEXT;
437 END;
438 $f$ STABLE LANGUAGE PLPGSQL;
439
440 CREATE OR REPLACE FUNCTION authority.generate_overlay_template ( BIGINT ) RETURNS TEXT AS $func$
441     SELECT authority.generate_overlay_template( marc ) FROM authority.record_entry WHERE id = $1;
442 $func$ LANGUAGE SQL;
443
444 CREATE OR REPLACE FUNCTION authority.merge_records ( target_record BIGINT, source_record BIGINT ) RETURNS INT AS $func$
445 DECLARE
446     moved_objects INT := 0;
447     bib_id        INT := 0;
448     bib_rec       biblio.record_entry%ROWTYPE;
449     auth_link     authority.bib_linking%ROWTYPE;
450     ingest_same   boolean;
451 BEGIN
452
453     -- Defining our terms:
454     -- "target record" = the record that will survive the merge
455     -- "source record" = the record that is sacrifing its existence and being
456     --   replaced by the target record
457
458     -- 1. Update all bib records with the ID from target_record in their $0
459     FOR bib_rec IN
460             SELECT  bre.*
461               FROM  biblio.record_entry bre 
462                     JOIN authority.bib_linking abl ON abl.bib = bre.id
463               WHERE abl.authority = source_record
464         LOOP
465
466         UPDATE  biblio.record_entry
467           SET   marc = REGEXP_REPLACE(
468                     marc,
469                     E'(<subfield\\s+code="0"\\s*>[^<]*?\\))' || source_record || '<',
470                     E'\\1' || target_record || '<',
471                     'g'
472                 )
473           WHERE id = bib_rec.id;
474
475           moved_objects := moved_objects + 1;
476     END LOOP;
477
478     -- 2. Grab the current value of reingest on same MARC flag
479     SELECT  enabled INTO ingest_same
480       FROM  config.internal_flag
481       WHERE name = 'ingest.reingest.force_on_same_marc'
482     ;
483
484     -- 3. Temporarily set reingest on same to TRUE
485     UPDATE  config.internal_flag
486       SET   enabled = TRUE
487       WHERE name = 'ingest.reingest.force_on_same_marc'
488     ;
489
490     -- 4. Make a harmless update to target_record to trigger auto-update
491     --    in linked bibliographic records
492     UPDATE  authority.record_entry
493       SET   deleted = FALSE
494       WHERE id = target_record;
495
496     -- 5. "Delete" source_record
497     DELETE FROM authority.record_entry WHERE id = source_record;
498
499     -- 6. Set "reingest on same MARC" flag back to initial value
500     UPDATE  config.internal_flag
501       SET   enabled = ingest_same
502       WHERE name = 'ingest.reingest.force_on_same_marc'
503     ;
504
505     RETURN moved_objects;
506 END;
507 $func$ LANGUAGE plpgsql;
508
509
510 -- Support function used to find the pivot for alpha-heading-browse style searching
511 CREATE OR REPLACE FUNCTION authority.simple_heading_find_pivot( a INT[], q TEXT ) RETURNS TEXT AS $$
512 DECLARE
513     sort_value_row  RECORD;
514     value_row       RECORD;
515     t_term          TEXT;
516 BEGIN
517
518     t_term := public.naco_normalize(q);
519
520     SELECT  CASE WHEN ash.sort_value LIKE t_term || '%' THEN 1 ELSE 0 END
521                 + CASE WHEN ash.value LIKE t_term || '%' THEN 1 ELSE 0 END AS rank,
522             ash.sort_value
523       INTO  sort_value_row
524       FROM  authority.simple_heading ash
525       WHERE ash.atag = ANY (a)
526             AND ash.sort_value >= t_term
527       ORDER BY rank DESC, ash.sort_value
528       LIMIT 1;
529
530     SELECT  CASE WHEN ash.sort_value LIKE t_term || '%' THEN 1 ELSE 0 END
531                 + CASE WHEN ash.value LIKE t_term || '%' THEN 1 ELSE 0 END AS rank,
532             ash.sort_value
533       INTO  value_row
534       FROM  authority.simple_heading ash
535       WHERE ash.atag = ANY (a)
536             AND ash.value >= t_term
537       ORDER BY rank DESC, ash.sort_value
538       LIMIT 1;
539
540     IF value_row.rank > sort_value_row.rank THEN
541         RETURN value_row.sort_value;
542     ELSE
543         RETURN sort_value_row.sort_value;
544     END IF;
545 END;
546 $$ LANGUAGE PLPGSQL;
547
548 CREATE OR REPLACE FUNCTION authority.simple_heading_browse_center( atag_list INT[], q TEXT, page INT DEFAULT 0, pagesize INT DEFAULT 9 ) RETURNS SETOF BIGINT AS $$
549 DECLARE
550     pivot_sort_value    TEXT;
551     boffset             INT DEFAULT 0;
552     aoffset             INT DEFAULT 0;
553     blimit              INT DEFAULT 0;
554     alimit              INT DEFAULT 0;
555 BEGIN
556
557     pivot_sort_value := authority.simple_heading_find_pivot(atag_list,q);
558
559     IF page = 0 THEN
560         blimit := pagesize / 2;
561         alimit := blimit;
562
563         IF pagesize % 2 <> 0 THEN
564             alimit := alimit + 1;
565         END IF;
566     ELSE
567         blimit := pagesize;
568         alimit := blimit;
569
570         boffset := pagesize / 2;
571         aoffset := boffset;
572
573         IF pagesize % 2 <> 0 THEN
574             boffset := boffset + 1;
575         END IF;
576     END IF;
577
578     IF page <= 0 THEN
579         RETURN QUERY
580             -- "bottom" half of the browse results
581             SELECT id FROM (
582                 SELECT  ash.id,
583                         row_number() over ()
584                   FROM  authority.simple_heading ash
585                   WHERE ash.atag = ANY (atag_list)
586                         AND ash.sort_value < pivot_sort_value
587                   ORDER BY ash.sort_value DESC
588                   LIMIT blimit
589                   OFFSET ABS(page) * pagesize - boffset
590             ) x ORDER BY row_number DESC;
591     END IF;
592
593     IF page >= 0 THEN
594         RETURN QUERY
595             -- "bottom" half of the browse results
596             SELECT  ash.id
597               FROM  authority.simple_heading ash
598               WHERE ash.atag = ANY (atag_list)
599                     AND ash.sort_value >= pivot_sort_value
600               ORDER BY ash.sort_value
601               LIMIT alimit
602               OFFSET ABS(page) * pagesize - aoffset;
603     END IF;
604 END;
605 $$ LANGUAGE PLPGSQL ROWS 10;
606
607 CREATE OR REPLACE FUNCTION authority.axis_authority_tags(a TEXT) RETURNS INT[] AS $$
608     SELECT ARRAY_ACCUM(field) FROM authority.browse_axis_authority_field_map WHERE axis = $1;
609 $$ LANGUAGE SQL;
610
611
612 CREATE OR REPLACE FUNCTION authority.axis_authority_tags_refs(a TEXT) RETURNS INT[] AS $$
613     SELECT ARRAY_AGG(y) from (
614        SELECT  unnest(ARRAY_CAT(
615                  ARRAY[a.field],
616                  (SELECT ARRAY_ACCUM(x.id) FROM authority.control_set_authority_field x WHERE x.main_entry = a.field)
617              )) y
618        FROM  authority.browse_axis_authority_field_map a
619        WHERE axis = $1) x
620 $$ LANGUAGE SQL;
621
622
623 CREATE OR REPLACE FUNCTION authority.btag_authority_tags(btag TEXT) RETURNS INT[] AS $$
624     SELECT ARRAY_ACCUM(authority_field) FROM authority.control_set_bib_field WHERE tag = $1
625 $$ LANGUAGE SQL;
626
627
628 CREATE OR REPLACE FUNCTION authority.btag_authority_tags_refs(btag TEXT) RETURNS INT[] AS $$
629     SELECT ARRAY_AGG(y) from (
630         SELECT  unnest(ARRAY_CAT(
631                     ARRAY[a.authority_field],
632                     (SELECT ARRAY_ACCUM(x.id) FROM authority.control_set_authority_field x WHERE x.main_entry = a.authority_field)
633                 )) y
634       FROM  authority.control_set_bib_field a
635       WHERE a.tag = $1) x
636 $$ LANGUAGE SQL;
637
638
639 CREATE OR REPLACE FUNCTION authority.atag_authority_tags(atag TEXT) RETURNS INT[] AS $$
640     SELECT ARRAY_ACCUM(id) FROM authority.control_set_authority_field WHERE tag = $1
641 $$ LANGUAGE SQL;
642
643 CREATE OR REPLACE FUNCTION authority.atag_authority_tags_refs(atag TEXT) RETURNS INT[] AS $$
644     SELECT ARRAY_AGG(y) from (
645         SELECT  unnest(ARRAY_CAT(
646                     ARRAY[a.id],
647                     (SELECT ARRAY_ACCUM(x.id) FROM authority.control_set_authority_field x WHERE x.main_entry = a.id)
648                 )) y
649       FROM  authority.control_set_authority_field a
650       WHERE a.tag = $1) x
651 $$ LANGUAGE SQL;
652
653
654 CREATE OR REPLACE FUNCTION authority.axis_browse_center( a TEXT, q TEXT, page INT DEFAULT 0, pagesize INT DEFAULT 9 ) RETURNS SETOF BIGINT AS $$
655     SELECT * FROM authority.simple_heading_browse_center(authority.axis_authority_tags($1), $2, $3, $4)
656 $$ LANGUAGE SQL ROWS 10;
657
658 CREATE OR REPLACE FUNCTION authority.btag_browse_center( a TEXT, q TEXT, page INT DEFAULT 0, pagesize INT DEFAULT 9 ) RETURNS SETOF BIGINT AS $$
659     SELECT * FROM authority.simple_heading_browse_center(authority.btag_authority_tags($1), $2, $3, $4)
660 $$ LANGUAGE SQL ROWS 10;
661
662 CREATE OR REPLACE FUNCTION authority.atag_browse_center( a TEXT, q TEXT, page INT DEFAULT 0, pagesize INT DEFAULT 9 ) RETURNS SETOF BIGINT AS $$
663     SELECT * FROM authority.simple_heading_browse_center(authority.atag_authority_tags($1), $2, $3, $4)
664 $$ LANGUAGE SQL ROWS 10;
665
666 CREATE OR REPLACE FUNCTION authority.axis_browse_center_refs( a TEXT, q TEXT, page INT DEFAULT 0, pagesize INT DEFAULT 9 ) RETURNS SETOF BIGINT AS $$
667     SELECT * FROM authority.simple_heading_browse_center(authority.axis_authority_tags_refs($1), $2, $3, $4)
668 $$ LANGUAGE SQL ROWS 10;
669
670 CREATE OR REPLACE FUNCTION authority.btag_browse_center_refs( a TEXT, q TEXT, page INT DEFAULT 0, pagesize INT DEFAULT 9 ) RETURNS SETOF BIGINT AS $$
671     SELECT * FROM authority.simple_heading_browse_center(authority.btag_authority_tags_refs($1), $2, $3, $4)
672 $$ LANGUAGE SQL ROWS 10;
673
674 CREATE OR REPLACE FUNCTION authority.atag_browse_center_refs( a TEXT, q TEXT, page INT DEFAULT 0, pagesize INT DEFAULT 9 ) RETURNS SETOF BIGINT AS $$
675     SELECT * FROM authority.simple_heading_browse_center(authority.atag_authority_tags_refs($1), $2, $3, $4)
676 $$ LANGUAGE SQL ROWS 10;
677
678
679 CREATE OR REPLACE FUNCTION authority.simple_heading_browse_top( atag_list INT[], q TEXT, page INT DEFAULT 0, pagesize INT DEFAULT 10 ) RETURNS SETOF BIGINT AS $$
680 DECLARE
681     pivot_sort_value    TEXT;
682 BEGIN
683
684     pivot_sort_value := authority.simple_heading_find_pivot(atag_list,q);
685
686     IF page < 0 THEN
687         RETURN QUERY
688             -- "bottom" half of the browse results
689             SELECT id FROM (
690                 SELECT  ash.id,
691                         row_number() over ()
692                   FROM  authority.simple_heading ash
693                   WHERE ash.atag = ANY (atag_list)
694                         AND ash.sort_value < pivot_sort_value
695                   ORDER BY ash.sort_value DESC
696                   LIMIT pagesize
697                   OFFSET (ABS(page) - 1) * pagesize
698             ) x ORDER BY row_number DESC;
699     END IF;
700
701     IF page >= 0 THEN
702         RETURN QUERY
703             -- "bottom" half of the browse results
704             SELECT  ash.id
705               FROM  authority.simple_heading ash
706               WHERE ash.atag = ANY (atag_list)
707                     AND ash.sort_value >= pivot_sort_value
708               ORDER BY ash.sort_value
709               LIMIT pagesize
710               OFFSET ABS(page) * pagesize ;
711     END IF;
712 END;
713 $$ LANGUAGE PLPGSQL ROWS 10;
714
715 CREATE OR REPLACE FUNCTION authority.axis_browse_top( a TEXT, q TEXT, page INT DEFAULT 0, pagesize INT DEFAULT 10 ) RETURNS SETOF BIGINT AS $$
716     SELECT * FROM authority.simple_heading_browse_top(authority.axis_authority_tags($1), $2, $3, $4)
717 $$ LANGUAGE SQL ROWS 10;
718
719 CREATE OR REPLACE FUNCTION authority.btag_browse_top( a TEXT, q TEXT, page INT DEFAULT 0, pagesize INT DEFAULT 10 ) RETURNS SETOF BIGINT AS $$
720     SELECT * FROM authority.simple_heading_browse_top(authority.btag_authority_tags($1), $2, $3, $4)
721 $$ LANGUAGE SQL ROWS 10;
722
723 CREATE OR REPLACE FUNCTION authority.atag_browse_top( a TEXT, q TEXT, page INT DEFAULT 0, pagesize INT DEFAULT 10 ) RETURNS SETOF BIGINT AS $$
724     SELECT * FROM authority.simple_heading_browse_top(authority.atag_authority_tags($1), $2, $3, $4)
725 $$ LANGUAGE SQL ROWS 10;
726
727 CREATE OR REPLACE FUNCTION authority.axis_browse_top_refs( a TEXT, q TEXT, page INT DEFAULT 0, pagesize INT DEFAULT 10 ) RETURNS SETOF BIGINT AS $$
728     SELECT * FROM authority.simple_heading_browse_top(authority.axis_authority_tags_refs($1), $2, $3, $4)
729 $$ LANGUAGE SQL ROWS 10;
730
731 CREATE OR REPLACE FUNCTION authority.btag_browse_top_refs( a TEXT, q TEXT, page INT DEFAULT 0, pagesize INT DEFAULT 10 ) RETURNS SETOF BIGINT AS $$
732     SELECT * FROM authority.simple_heading_browse_top(authority.btag_authority_tags_refs($1), $2, $3, $4)
733 $$ LANGUAGE SQL ROWS 10;
734
735 CREATE OR REPLACE FUNCTION authority.atag_browse_top_refs( a TEXT, q TEXT, page INT DEFAULT 0, pagesize INT DEFAULT 10 ) RETURNS SETOF BIGINT AS $$
736     SELECT * FROM authority.simple_heading_browse_top(authority.atag_authority_tags_refs($1), $2, $3, $4)
737 $$ LANGUAGE SQL ROWS 10;
738
739
740 CREATE OR REPLACE FUNCTION authority.simple_heading_search_rank( atag_list INT[], q TEXT, page INT DEFAULT 0, pagesize INT DEFAULT 10 ) RETURNS SETOF BIGINT AS $$
741     SELECT  ash.id
742       FROM  authority.simple_heading ash,
743             public.naco_normalize($2) t(term),
744             plainto_tsquery('keyword'::regconfig,$2) ptsq(term)
745       WHERE ash.atag = ANY ($1)
746             AND ash.index_vector @@ ptsq.term
747       ORDER BY ts_rank_cd(ash.index_vector,ptsq.term,14)::numeric
748                     + CASE WHEN ash.sort_value LIKE t.term || '%' THEN 2 ELSE 0 END
749                     + CASE WHEN ash.value LIKE t.term || '%' THEN 1 ELSE 0 END DESC
750       LIMIT $4
751       OFFSET $4 * $3;
752 $$ LANGUAGE SQL ROWS 10;
753
754 CREATE OR REPLACE FUNCTION authority.axis_search_rank( a TEXT, q TEXT, page INT DEFAULT 0, pagesize INT DEFAULT 10 ) RETURNS SETOF BIGINT AS $$
755     SELECT * FROM authority.simple_heading_search_rank(authority.axis_authority_tags($1), $2, $3, $4)
756 $$ LANGUAGE SQL ROWS 10;
757
758 CREATE OR REPLACE FUNCTION authority.btag_search_rank( a TEXT, q TEXT, page INT DEFAULT 0, pagesize INT DEFAULT 10 ) RETURNS SETOF BIGINT AS $$
759     SELECT * FROM authority.simple_heading_search_rank(authority.btag_authority_tags($1), $2, $3, $4)
760 $$ LANGUAGE SQL ROWS 10;
761
762 CREATE OR REPLACE FUNCTION authority.atag_search_rank( a TEXT, q TEXT, page INT DEFAULT 0, pagesize INT DEFAULT 10 ) RETURNS SETOF BIGINT AS $$
763     SELECT * FROM authority.simple_heading_search_rank(authority.atag_authority_tags($1), $2, $3, $4)
764 $$ LANGUAGE SQL ROWS 10;
765
766 CREATE OR REPLACE FUNCTION authority.axis_search_rank_refs( a TEXT, q TEXT, page INT DEFAULT 0, pagesize INT DEFAULT 10 ) RETURNS SETOF BIGINT AS $$
767     SELECT * FROM authority.simple_heading_search_rank(authority.axis_authority_tags_refs($1), $2, $3, $4)
768 $$ LANGUAGE SQL ROWS 10;
769
770 CREATE OR REPLACE FUNCTION authority.btag_search_rank_refs( a TEXT, q TEXT, page INT DEFAULT 0, pagesize INT DEFAULT 10 ) RETURNS SETOF BIGINT AS $$
771     SELECT * FROM authority.simple_heading_search_rank(authority.btag_authority_tags_refs($1), $2, $3, $4)
772 $$ LANGUAGE SQL ROWS 10;
773
774 CREATE OR REPLACE FUNCTION authority.atag_search_rank_refs( a TEXT, q TEXT, page INT DEFAULT 0, pagesize INT DEFAULT 10 ) RETURNS SETOF BIGINT AS $$
775     SELECT * FROM authority.simple_heading_search_rank(authority.atag_authority_tags_refs($1), $2, $3, $4)
776 $$ LANGUAGE SQL ROWS 10;
777
778
779 CREATE OR REPLACE FUNCTION authority.simple_heading_search_heading( atag_list INT[], q TEXT, page INT DEFAULT 0, pagesize INT DEFAULT 10 ) RETURNS SETOF BIGINT AS $$
780     SELECT  ash.id
781       FROM  authority.simple_heading ash,
782             public.naco_normalize($2) t(term),
783             plainto_tsquery('keyword'::regconfig,$2) ptsq(term)
784       WHERE ash.atag = ANY ($1)
785             AND ash.index_vector @@ ptsq.term
786       ORDER BY ash.sort_value
787       LIMIT $4
788       OFFSET $4 * $3;
789 $$ LANGUAGE SQL ROWS 10;
790
791 CREATE OR REPLACE FUNCTION authority.axis_search_heading( a TEXT, q TEXT, page INT DEFAULT 0, pagesize INT DEFAULT 10 ) RETURNS SETOF BIGINT AS $$
792     SELECT * FROM authority.simple_heading_search_heading(authority.axis_authority_tags($1), $2, $3, $4)
793 $$ LANGUAGE SQL ROWS 10;
794
795 CREATE OR REPLACE FUNCTION authority.btag_search_heading( a TEXT, q TEXT, page INT DEFAULT 0, pagesize INT DEFAULT 10 ) RETURNS SETOF BIGINT AS $$
796     SELECT * FROM authority.simple_heading_search_heading(authority.btag_authority_tags($1), $2, $3, $4)
797 $$ LANGUAGE SQL ROWS 10;
798
799 CREATE OR REPLACE FUNCTION authority.atag_search_heading( a TEXT, q TEXT, page INT DEFAULT 0, pagesize INT DEFAULT 10 ) RETURNS SETOF BIGINT AS $$
800     SELECT * FROM authority.simple_heading_search_heading(authority.atag_authority_tags($1), $2, $3, $4)
801 $$ LANGUAGE SQL ROWS 10;
802
803 CREATE OR REPLACE FUNCTION authority.axis_search_heading_refs( a TEXT, q TEXT, page INT DEFAULT 0, pagesize INT DEFAULT 10 ) RETURNS SETOF BIGINT AS $$
804     SELECT * FROM authority.simple_heading_search_heading(authority.axis_authority_tags_refs($1), $2, $3, $4)
805 $$ LANGUAGE SQL ROWS 10;
806
807 CREATE OR REPLACE FUNCTION authority.btag_search_heading_refs( a TEXT, q TEXT, page INT DEFAULT 0, pagesize INT DEFAULT 10 ) RETURNS SETOF BIGINT AS $$
808     SELECT * FROM authority.simple_heading_search_heading(authority.btag_authority_tags_refs($1), $2, $3, $4)
809 $$ LANGUAGE SQL ROWS 10;
810
811 CREATE OR REPLACE FUNCTION authority.atag_search_heading_refs( a TEXT, q TEXT, page INT DEFAULT 0, pagesize INT DEFAULT 10 ) RETURNS SETOF BIGINT AS $$
812     SELECT * FROM authority.simple_heading_search_heading(authority.atag_authority_tags_refs($1), $2, $3, $4)
813 $$ LANGUAGE SQL ROWS 10;
814
815
816 COMMIT;
817