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