]> git.evergreen-ils.org Git - working/Evergreen.git/blob - Open-ILS/src/sql/Pg/011.schema.authority.sql
LP#1638299: reify schema and seed data for new databases
[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 -- subset of types listed in https://www.loc.gov/marc/authority/ad1xx3xx.html
26 -- for now, ignoring subdivisions
27 CREATE TYPE authority.heading_type AS ENUM (
28     'personal_name',
29     'corporate_name',
30     'meeting_name',
31     'uniform_title',
32     'named_event',
33     'chronological_term',
34     'topical_term',
35     'geographic_name',
36     'genre_form_term',
37     'medium_of_performance_term'
38 );
39
40 CREATE TYPE authority.variant_heading_type AS ENUM (
41     'abbreviation',
42     'acronym',
43     'translation',
44     'expansion',
45     'other',
46     'hidden'
47 );
48
49 CREATE TYPE authority.related_heading_type AS ENUM (
50     'earlier',
51     'later',
52     'parent organization',
53     'broader',
54     'narrower',
55     'equivalent',
56     'other'
57 );
58
59 CREATE TYPE authority.heading_purpose AS ENUM (
60     'main',
61     'variant',
62     'related'
63 );
64
65 CREATE TABLE authority.heading_field (
66     id              SERIAL                      PRIMARY KEY,
67     heading_type    authority.heading_type      NOT NULL,
68     heading_purpose authority.heading_purpose   NOT NULL,
69     label           TEXT                        NOT NULL,
70     format          TEXT                        NOT NULL REFERENCES config.xml_transform (name) DEFAULT 'mads21',
71     heading_xpath   TEXT                        NOT NULL,
72     component_xpath TEXT                        NOT NULL,
73     type_xpath      TEXT                        NULL, -- to extract related or variant type
74     thesaurus_xpath TEXT                        NULL,
75     thesaurus_override_xpath TEXT               NULL,
76     joiner          TEXT                        NULL
77 );
78
79 CREATE TABLE authority.heading_field_norm_map (
80         id      SERIAL  PRIMARY KEY,
81         field   INT     NOT NULL REFERENCES authority.heading_field (id) ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED,
82         norm    INT     NOT NULL REFERENCES config.index_normalizer (id) ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED,
83         params  TEXT,
84         pos     INT     NOT NULL DEFAULT 0
85 );
86
87 CREATE TYPE authority.heading AS (
88     field               INT,
89     type                authority.heading_type,
90     purpose             authority.heading_purpose,
91     variant_type        authority.variant_heading_type,
92     related_type        authority.related_heading_type,
93     thesaurus           TEXT,
94     heading             TEXT,
95     normalized_heading  TEXT
96 );
97
98 CREATE TABLE authority.control_set (
99     id          SERIAL  PRIMARY KEY,
100     name        TEXT    NOT NULL UNIQUE, -- i18n
101     description TEXT                     -- i18n
102 );
103
104 CREATE TABLE authority.control_set_authority_field (
105     id          SERIAL  PRIMARY KEY,
106     main_entry  INT     REFERENCES authority.control_set_authority_field (id) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED,
107     control_set INT     NOT NULL REFERENCES authority.control_set (id) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED,
108     tag         CHAR(3) NOT NULL,
109     nfi         CHAR(1),          -- non-filing indicator
110     sf_list     TEXT    NOT NULL,
111     display_sf_list     TEXT NOT NULL,
112     name        TEXT    NOT NULL, -- i18n
113     description TEXT,             -- i18n
114     joiner      TEXT,
115     linking_subfield CHAR(1),
116     heading_field INTEGER REFERENCES authority.heading_field(id)
117 );
118
119 CREATE TABLE authority.control_set_bib_field (
120     id              SERIAL  PRIMARY KEY,
121     authority_field INT     NOT NULL REFERENCES authority.control_set_authority_field (id) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED,
122     tag             CHAR(3) NOT NULL
123 );
124
125 -- Seed data will be generated from class <-> axis mapping
126 CREATE TABLE authority.control_set_bib_field_metabib_field_map (
127     id              SERIAL  PRIMARY KEY,
128     bib_field       INT     NOT NULL REFERENCES authority.control_set_bib_field (id) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED,
129     metabib_field   INT     NOT NULL REFERENCES config.metabib_field (id) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED,
130     CONSTRAINT a_bf_mf_map_once UNIQUE (bib_field, metabib_field)
131 );
132
133 CREATE VIEW authority.control_set_auth_field_metabib_field_map_main AS
134     SELECT  DISTINCT b.authority_field, m.metabib_field
135       FROM  authority.control_set_bib_field_metabib_field_map m JOIN authority.control_set_bib_field b ON (b.id = m.bib_field);
136 COMMENT ON VIEW authority.control_set_auth_field_metabib_field_map_main IS $$metabib fields for main entry auth fields$$;
137
138 CREATE VIEW authority.control_set_auth_field_metabib_field_map_refs_only AS
139     SELECT  DISTINCT a.id AS authority_field, m.metabib_field
140       FROM  authority.control_set_authority_field a
141             JOIN authority.control_set_authority_field ame ON (a.main_entry = ame.id)
142             JOIN authority.control_set_bib_field b ON (b.authority_field = ame.id)
143             JOIN authority.control_set_bib_field_metabib_field_map mf ON (mf.bib_field = b.id)
144             JOIN authority.control_set_auth_field_metabib_field_map_main m ON (ame.id = m.authority_field);
145 COMMENT ON VIEW authority.control_set_auth_field_metabib_field_map_refs_only IS $$metabib fields for NON-main entry auth fields$$;
146
147 CREATE VIEW authority.control_set_auth_field_metabib_field_map_refs AS
148     SELECT * FROM authority.control_set_auth_field_metabib_field_map_main
149         UNION
150     SELECT * FROM authority.control_set_auth_field_metabib_field_map_refs_only;
151 COMMENT ON VIEW authority.control_set_auth_field_metabib_field_map_refs IS $$metabib fields for all auth fields$$;
152
153
154 -- blind refs only is probably what we want for lookup in bib/auth browse
155 CREATE VIEW authority.control_set_auth_field_metabib_field_map_blind_refs_only AS
156     SELECT  r.*
157       FROM  authority.control_set_auth_field_metabib_field_map_refs_only r
158             JOIN authority.control_set_authority_field a ON (r.authority_field = a.id)
159       WHERE linking_subfield IS NULL;
160 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$$; -- '
161
162 CREATE VIEW authority.control_set_auth_field_metabib_field_map_blind_refs AS
163     SELECT  r.*
164       FROM  authority.control_set_auth_field_metabib_field_map_refs r
165             JOIN authority.control_set_authority_field a ON (r.authority_field = a.id)
166       WHERE linking_subfield IS NULL;
167 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$$; -- '
168
169 CREATE VIEW authority.control_set_auth_field_metabib_field_map_blind_main AS
170     SELECT  r.*
171       FROM  authority.control_set_auth_field_metabib_field_map_main r
172             JOIN authority.control_set_authority_field a ON (r.authority_field = a.id)
173       WHERE linking_subfield IS NULL;
174 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$$; -- '
175
176 CREATE TABLE authority.thesaurus (
177     code        TEXT    PRIMARY KEY,     -- MARC21 thesaurus code
178     control_set INT     REFERENCES authority.control_set (id) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED,
179     name        TEXT    NOT NULL UNIQUE, -- i18n
180     description TEXT,                     -- i18n
181     short_code  TEXT,
182     uri         TEXT
183 );
184
185 CREATE TRIGGER thes_code_tracking_trigger
186     AFTER UPDATE ON authority.thesaurus
187     FOR EACH ROW EXECUTE PROCEDURE oils_i18n_code_tracking('at');
188
189 CREATE TABLE authority.browse_axis (
190     code        TEXT    PRIMARY KEY,
191     name        TEXT    UNIQUE NOT NULL, -- i18n
192     sorter      TEXT    REFERENCES config.record_attr_definition (name) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED,
193     description TEXT
194 );
195
196 CREATE TABLE authority.browse_axis_authority_field_map (
197     id          SERIAL  PRIMARY KEY,
198     axis        TEXT    NOT NULL REFERENCES authority.browse_axis (code) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED,
199     field       INT     NOT NULL REFERENCES authority.control_set_authority_field (id) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED
200 );
201
202 CREATE TABLE authority.record_entry (
203     id              BIGSERIAL    PRIMARY KEY,
204     create_date     TIMESTAMP WITH TIME ZONE    NOT NULL DEFAULT now(),
205     edit_date       TIMESTAMP WITH TIME ZONE    NOT NULL DEFAULT now(),
206     creator         INT     NOT NULL DEFAULT 1,
207     editor          INT     NOT NULL DEFAULT 1,
208     active          BOOL    NOT NULL DEFAULT TRUE,
209     deleted         BOOL    NOT NULL DEFAULT FALSE,
210     source          INT,
211     control_set     INT     REFERENCES authority.control_set (id) ON UPDATE CASCADE DEFERRABLE INITIALLY DEFERRED,
212     marc            TEXT    NOT NULL,
213     last_xact_id    TEXT    NOT NULL,
214     owner           INT,
215     heading         TEXT,
216     simple_heading  TEXT
217 );
218 CREATE INDEX authority_record_entry_creator_idx ON authority.record_entry ( creator );
219 CREATE INDEX authority_record_entry_editor_idx ON authority.record_entry ( editor );
220 CREATE INDEX authority_record_entry_create_date_idx ON authority.record_entry ( create_date );
221 CREATE INDEX authority_record_entry_edit_date_idx ON authority.record_entry ( edit_date );
222 CREATE INDEX authority_record_deleted_idx ON authority.record_entry(deleted) WHERE deleted IS FALSE OR deleted = false;
223 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();
224 CREATE TRIGGER b_maintain_901 BEFORE INSERT OR UPDATE ON authority.record_entry FOR EACH ROW EXECUTE PROCEDURE evergreen.maintain_901();
225 CREATE TRIGGER c_maintain_control_numbers BEFORE INSERT OR UPDATE ON authority.record_entry FOR EACH ROW EXECUTE PROCEDURE maintain_control_numbers();
226
227 CREATE TABLE authority.authority_linking (
228     id      BIGSERIAL PRIMARY KEY,
229     source  BIGINT REFERENCES authority.record_entry (id) NOT NULL,
230     target  BIGINT REFERENCES authority.record_entry (id) NOT NULL,
231     field   INT REFERENCES authority.control_set_authority_field (id) NOT NULL
232 );
233
234 CREATE TABLE authority.bib_linking (
235     id          BIGSERIAL   PRIMARY KEY,
236     bib         BIGINT      NOT NULL REFERENCES biblio.record_entry (id),
237     authority   BIGINT      NOT NULL REFERENCES authority.record_entry (id)
238 );
239 CREATE INDEX authority_bl_bib_idx ON authority.bib_linking ( bib );
240 CREATE UNIQUE INDEX authority_bl_bib_authority_once_idx ON authority.bib_linking ( authority, bib );
241
242 CREATE TABLE authority.record_note (
243     id          BIGSERIAL   PRIMARY KEY,
244     record      BIGINT      NOT NULL REFERENCES authority.record_entry (id) DEFERRABLE INITIALLY DEFERRED,
245     value       TEXT        NOT NULL,
246     creator     INT         NOT NULL DEFAULT 1,
247     editor      INT         NOT NULL DEFAULT 1,
248     create_date TIMESTAMP WITH TIME ZONE    NOT NULL DEFAULT now(),
249     edit_date   TIMESTAMP WITH TIME ZONE    NOT NULL DEFAULT now()
250 );
251 CREATE INDEX authority_record_note_record_idx ON authority.record_note ( record );
252 CREATE INDEX authority_record_note_creator_idx ON authority.record_note ( creator );
253 CREATE INDEX authority_record_note_editor_idx ON authority.record_note ( editor );
254
255 CREATE TABLE authority.rec_descriptor (
256     id              BIGSERIAL PRIMARY KEY,
257     record          BIGINT,
258     record_status   TEXT,
259     encoding_level  TEXT,
260     thesaurus       TEXT
261 );
262 CREATE INDEX authority_rec_descriptor_record_idx ON authority.rec_descriptor (record);
263
264 CREATE TABLE authority.full_rec (
265     id              BIGSERIAL   PRIMARY KEY,
266     record          BIGINT      NOT NULL,
267     tag             CHAR(3)     NOT NULL,
268     ind1            TEXT,
269     ind2            TEXT,
270     subfield        TEXT,
271     value           TEXT        NOT NULL,
272     index_vector    tsvector    NOT NULL
273 );
274 CREATE INDEX authority_full_rec_record_idx ON authority.full_rec (record);
275 CREATE INDEX authority_full_rec_tag_subfield_idx ON authority.full_rec (tag, subfield);
276 CREATE INDEX authority_full_rec_tag_part_idx ON authority.full_rec (SUBSTRING(tag FROM 2));
277 CREATE INDEX authority_full_rec_subfield_a_idx ON authority.full_rec (value) WHERE subfield = 'a';
278 CREATE TRIGGER authority_full_rec_fti_trigger
279     BEFORE UPDATE OR INSERT ON authority.full_rec
280     FOR EACH ROW EXECUTE PROCEDURE oils_tsearch2('keyword');
281
282 CREATE INDEX authority_full_rec_index_vector_idx ON authority.full_rec USING GIST (index_vector);
283 /* Enable LIKE to use an index for database clusters with locales other than C or POSIX */
284 CREATE INDEX authority_full_rec_value_tpo_index ON authority.full_rec (value text_pattern_ops);
285 /* But we still need this (boooo) for paging using >, <, etc */
286 CREATE INDEX authority_full_rec_value_index ON authority.full_rec (value);
287
288 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);
289
290 CREATE OR REPLACE FUNCTION authority.extract_thesaurus( marcxml TEXT ) RETURNS TEXT AS $func$
291 DECLARE
292     thes_code TEXT;
293 BEGIN
294     thes_code := vandelay.marc21_extract_fixed_field(marcxml,'Subj');
295     IF thes_code IS NULL THEN
296         thes_code := '|';
297     ELSIF thes_code = 'z' THEN
298         thes_code := COALESCE( oils_xpath_string('//*[@tag="040"]/*[@code="f"][1]', marcxml), '' );
299     ELSE
300         SELECT code INTO thes_code FROM authority.thesaurus WHERE short_code = thes_code;
301         IF NOT FOUND THEN
302             thes_code := '|'; -- default
303         END IF;
304     END IF;
305     RETURN thes_code;
306 END;
307 $func$ LANGUAGE PLPGSQL STABLE STRICT;
308
309 -- Intended to be used in a unique index on authority.record_entry like so:
310 -- CREATE UNIQUE INDEX unique_by_heading_and_thesaurus
311 --   ON authority.record_entry (heading)
312 --   WHERE deleted IS FALSE or deleted = FALSE;
313 CREATE OR REPLACE FUNCTION authority.normalize_heading( marcxml TEXT, no_thesaurus BOOL ) RETURNS TEXT AS $func$
314 DECLARE
315     acsaf           authority.control_set_authority_field%ROWTYPE;
316     tag_used        TEXT;
317     nfi_used        TEXT;
318     sf              TEXT;
319     sf_node         TEXT;
320     tag_node        TEXT;
321     thes_code       TEXT;
322     cset            INT;
323     heading_text    TEXT;
324     tmp_text        TEXT;
325     first_sf        BOOL;
326     auth_id         INT DEFAULT COALESCE(NULLIF(oils_xpath_string('//*[@tag="901"]/*[local-name()="subfield" and @code="c"]', marcxml), ''), '0')::INT; 
327 BEGIN
328     SELECT control_set INTO cset FROM authority.record_entry WHERE id = auth_id;
329
330     IF cset IS NULL THEN
331         SELECT  control_set INTO cset
332           FROM  authority.control_set_authority_field
333           WHERE tag IN ( SELECT  UNNEST(XPATH('//*[starts-with(@tag,"1")]/@tag',marcxml::XML)::TEXT[]))
334           LIMIT 1;
335     END IF;
336
337     heading_text := '';
338     FOR acsaf IN SELECT * FROM authority.control_set_authority_field WHERE control_set = cset AND main_entry IS NULL LOOP
339         tag_used := acsaf.tag;
340         nfi_used := acsaf.nfi;
341         first_sf := TRUE;
342
343         FOR tag_node IN SELECT unnest(oils_xpath('//*[@tag="'||tag_used||'"]',marcxml)) LOOP
344             FOR sf_node IN SELECT unnest(oils_xpath('./*[contains("'||acsaf.sf_list||'",@code)]',tag_node)) LOOP
345
346                 tmp_text := oils_xpath_string('.', sf_node);
347                 sf := oils_xpath_string('./@code', sf_node);
348
349                 IF first_sf AND tmp_text IS NOT NULL AND nfi_used IS NOT NULL THEN
350
351                     tmp_text := SUBSTRING(
352                         tmp_text FROM
353                         COALESCE(
354                             NULLIF(
355                                 REGEXP_REPLACE(
356                                     oils_xpath_string('./@ind'||nfi_used, tag_node),
357                                     $$\D+$$,
358                                     '',
359                                     'g'
360                                 ),
361                                 ''
362                             )::INT,
363                             0
364                         ) + 1
365                     );
366
367                 END IF;
368
369                 first_sf := FALSE;
370
371                 IF tmp_text IS NOT NULL AND tmp_text <> '' THEN
372                     heading_text := heading_text || E'\u2021' || sf || ' ' || tmp_text;
373                 END IF;
374             END LOOP;
375
376             EXIT WHEN heading_text <> '';
377         END LOOP;
378
379         EXIT WHEN heading_text <> '';
380     END LOOP;
381
382     IF heading_text <> '' THEN
383         IF no_thesaurus IS TRUE THEN
384             heading_text := tag_used || ' ' || public.naco_normalize(heading_text);
385         ELSE
386             thes_code := authority.extract_thesaurus(marcxml);
387             heading_text := tag_used || '_' || COALESCE(nfi_used,'-') || '_' || thes_code || ' ' || public.naco_normalize(heading_text);
388         END IF;
389     ELSE
390         heading_text := 'NOHEADING_' || thes_code || ' ' || MD5(marcxml);
391     END IF;
392
393     RETURN heading_text;
394 END;
395 $func$ LANGUAGE PLPGSQL STABLE STRICT;
396
397 CREATE TABLE authority.simple_heading (
398     id              BIGSERIAL   PRIMARY KEY,
399     record          BIGINT      NOT NULL REFERENCES authority.record_entry (id),
400     atag            INT         NOT NULL REFERENCES authority.control_set_authority_field (id),
401     value           TEXT        NOT NULL,
402     sort_value      TEXT        NOT NULL,
403     index_vector    tsvector    NOT NULL,
404     thesaurus       TEXT
405 );
406 CREATE TRIGGER authority_simple_heading_fti_trigger
407     BEFORE UPDATE OR INSERT ON authority.simple_heading
408     FOR EACH ROW EXECUTE PROCEDURE oils_tsearch2('keyword');
409
410 CREATE INDEX authority_simple_heading_index_vector_idx ON authority.simple_heading USING GIST (index_vector);
411 CREATE INDEX authority_simple_heading_value_idx ON authority.simple_heading (value);
412 CREATE INDEX authority_simple_heading_sort_value_idx ON authority.simple_heading (sort_value);
413 CREATE INDEX authority_simple_heading_record_idx ON authority.simple_heading (record);
414 CREATE INDEX authority_simple_heading_thesaurus_idx ON authority.simple_heading (thesaurus);
415
416 CREATE OR REPLACE FUNCTION authority.simple_heading_set( marcxml TEXT ) RETURNS SETOF authority.simple_heading AS $func$
417 DECLARE
418     res             authority.simple_heading%ROWTYPE;
419     acsaf           authority.control_set_authority_field%ROWTYPE;
420     tag_used        TEXT;
421     nfi_used        TEXT;
422     sf              TEXT;
423     cset            INT;
424     heading_text    TEXT;
425     joiner_text     TEXT;
426     sort_text       TEXT;
427     tmp_text        TEXT;
428     tmp_xml         TEXT;
429     first_sf        BOOL;
430     auth_id         INT DEFAULT COALESCE(NULLIF(oils_xpath_string('//*[@tag="901"]/*[local-name()="subfield" and @code="c"]', marcxml), ''), '0')::INT; 
431 BEGIN
432
433     SELECT control_set INTO cset FROM authority.record_entry WHERE id = auth_id;
434
435     IF cset IS NULL THEN
436         SELECT  control_set INTO cset
437           FROM  authority.control_set_authority_field
438           WHERE tag IN ( SELECT  UNNEST(XPATH('//*[starts-with(@tag,"1")]/@tag',marcxml::XML)::TEXT[]))
439           LIMIT 1;
440     END IF;
441
442     res.record := auth_id;
443     res.thesaurus := authority.extract_thesaurus(marcxml);
444
445     FOR acsaf IN SELECT * FROM authority.control_set_authority_field WHERE control_set = cset LOOP
446
447         res.atag := acsaf.id;
448         tag_used := acsaf.tag;
449         nfi_used := acsaf.nfi;
450         joiner_text := COALESCE(acsaf.joiner, ' ');
451
452         FOR tmp_xml IN SELECT UNNEST(XPATH('//*[@tag="'||tag_used||'"]', marcxml::XML)::TEXT[]) LOOP
453
454             heading_text := COALESCE(
455                 oils_xpath_string('./*[contains("'||acsaf.display_sf_list||'",@code)]', tmp_xml, joiner_text),
456                 ''
457             );
458
459             IF nfi_used IS NOT NULL THEN
460
461                 sort_text := SUBSTRING(
462                     heading_text FROM
463                     COALESCE(
464                         NULLIF(
465                             REGEXP_REPLACE(
466                                 oils_xpath_string('./@ind'||nfi_used, tmp_xml::TEXT),
467                                 $$\D+$$,
468                                 '',
469                                 'g'
470                             ),
471                             ''
472                         )::INT,
473                         0
474                     ) + 1
475                 );
476
477             ELSE
478                 sort_text := heading_text;
479             END IF;
480
481             IF heading_text IS NOT NULL AND heading_text <> '' THEN
482                 res.value := heading_text;
483                 res.sort_value := public.naco_normalize(sort_text);
484                 res.index_vector = to_tsvector('keyword'::regconfig, res.sort_value);
485                 RETURN NEXT res;
486             END IF;
487
488         END LOOP;
489
490     END LOOP;
491
492     RETURN;
493 END;
494 $func$ LANGUAGE PLPGSQL STABLE STRICT;
495
496 CREATE OR REPLACE FUNCTION authority.simple_normalize_heading( marcxml TEXT ) RETURNS TEXT AS $func$
497     SELECT authority.normalize_heading($1, TRUE);
498 $func$ LANGUAGE SQL STABLE STRICT;
499
500 CREATE OR REPLACE FUNCTION authority.normalize_heading( marcxml TEXT ) RETURNS TEXT AS $func$
501     SELECT authority.normalize_heading($1, FALSE);
502 $func$ LANGUAGE SQL STABLE STRICT;
503
504 COMMENT ON FUNCTION authority.normalize_heading( TEXT ) IS $$
505 Extract the authority heading, thesaurus, and NACO-normalized values
506 from an authority record. The primary purpose is to build a unique
507 index to defend against duplicated authority records from the same
508 thesaurus.
509 $$;
510
511 -- Store these in line with the MARC for easier indexing
512 CREATE OR REPLACE FUNCTION authority.normalize_heading_for_upsert () RETURNS TRIGGER AS $f$
513 BEGIN
514     NEW.heading := authority.normalize_heading( NEW.marc );
515     NEW.simple_heading := authority.simple_normalize_heading( NEW.marc );
516     RETURN NEW;
517 END;
518 $f$ LANGUAGE PLPGSQL;
519
520 CREATE TRIGGER update_headings_tgr BEFORE INSERT OR UPDATE ON authority.record_entry FOR EACH ROW EXECUTE PROCEDURE authority.normalize_heading_for_upsert();
521
522 -- Adding indexes using oils_xpath_string() for the main entry tags described in
523 -- authority.control_set_authority_field would speed this up, if we ever want to use it, though
524 -- the existing index on authority.normalize_heading() helps already with a record in hand
525 CREATE OR REPLACE VIEW authority.tracing_links AS
526     SELECT  main.record AS record,
527             main.id AS main_id,
528             main.tag AS main_tag,
529             oils_xpath_string('//*[@tag="'||main.tag||'"]/*[local-name()="subfield"]', are.marc) AS main_value,
530             substr(link.value,1,1) AS relationship,
531             substr(link.value,2,1) AS use_restriction,
532             substr(link.value,3,1) AS deprecation,
533             substr(link.value,4,1) AS display_restriction,
534             link.id AS link_id,
535             link.tag AS link_tag,
536             oils_xpath_string('//*[@tag="'||link.tag||'"]/*[local-name()="subfield"]', are.marc) AS link_value,
537             are.heading AS normalized_main_value
538       FROM  authority.full_rec main
539             JOIN authority.record_entry are ON (main.record = are.id)
540             JOIN authority.control_set_authority_field main_entry
541                 ON (main_entry.tag = main.tag
542                     AND main_entry.main_entry IS NULL
543                     AND main.subfield = 'a' )
544             JOIN authority.control_set_authority_field sub_entry
545                 ON (main_entry.id = sub_entry.main_entry)
546             JOIN authority.full_rec link
547                 ON (link.record = main.record
548                     AND link.tag = sub_entry.tag
549                     AND link.subfield = 'w' );
550
551 -- Function to generate an ephemeral overlay template from an authority record
552 CREATE OR REPLACE FUNCTION authority.generate_overlay_template (source_xml TEXT) RETURNS TEXT AS $f$
553 DECLARE
554     cset                INT;
555     main_entry          authority.control_set_authority_field%ROWTYPE;
556     bib_field           authority.control_set_bib_field%ROWTYPE;
557     auth_id             INT DEFAULT oils_xpath_string('//*[@tag="901"]/*[local-name()="subfield" and @code="c"]', source_xml)::INT;
558     tmp_data            XML;
559     replace_data        XML[] DEFAULT '{}'::XML[];
560     replace_rules       TEXT[] DEFAULT '{}'::TEXT[];
561     auth_field          XML[];
562     auth_i1             TEXT;
563     auth_i2             TEXT;
564 BEGIN
565     IF auth_id IS NULL THEN
566         RETURN NULL;
567     END IF;
568
569     -- Default to the LoC controll set
570     SELECT control_set INTO cset FROM authority.record_entry WHERE id = auth_id;
571
572     -- if none, make a best guess
573     IF cset IS NULL THEN
574         SELECT  control_set INTO cset
575           FROM  authority.control_set_authority_field
576           WHERE tag IN (
577                     SELECT  UNNEST(XPATH('//*[starts-with(@tag,"1")]/@tag',marc::XML)::TEXT[])
578                       FROM  authority.record_entry
579                       WHERE id = auth_id
580                 )
581           LIMIT 1;
582     END IF;
583
584     -- if STILL none, no-op change
585     IF cset IS NULL THEN
586         RETURN XMLELEMENT(
587             name record,
588             XMLATTRIBUTES('http://www.loc.gov/MARC21/slim' AS xmlns),
589             XMLELEMENT( name leader, '00881nam a2200193   4500'),
590             XMLELEMENT(
591                 name datafield,
592                 XMLATTRIBUTES( '905' AS tag, ' ' AS ind1, ' ' AS ind2),
593                 XMLELEMENT(
594                     name subfield,
595                     XMLATTRIBUTES('d' AS code),
596                     '901c'
597                 )
598             )
599         )::TEXT;
600     END IF;
601
602     FOR main_entry IN SELECT * FROM authority.control_set_authority_field acsaf WHERE acsaf.control_set = cset AND acsaf.main_entry IS NULL LOOP
603         auth_field := XPATH('//*[@tag="'||main_entry.tag||'"][1]',source_xml::XML);
604         auth_i1 = (XPATH('@ind1',auth_field[1]))[1];
605         auth_i2 = (XPATH('@ind2',auth_field[1]))[1];
606         IF ARRAY_LENGTH(auth_field,1) > 0 THEN
607             FOR bib_field IN SELECT * FROM authority.control_set_bib_field WHERE authority_field = main_entry.id LOOP
608                 SELECT XMLELEMENT( -- XMLAGG avoids magical <element> creation, but requires unnest subquery
609                     name datafield,
610                     XMLATTRIBUTES(bib_field.tag AS tag, auth_i1 AS ind1, auth_i2 AS ind2),
611                     XMLAGG(UNNEST)
612                 ) INTO tmp_data FROM UNNEST(XPATH('//*[local-name()="subfield"]', auth_field[1]));
613                 replace_data := replace_data || tmp_data;
614                 replace_rules := replace_rules || ( bib_field.tag || main_entry.sf_list || E'[0~\\)' || auth_id || '$]' );
615                 tmp_data = NULL;
616             END LOOP;
617             EXIT;
618         END IF;
619     END LOOP;
620
621     SELECT XMLAGG(UNNEST) INTO tmp_data FROM UNNEST(replace_data);
622
623     RETURN XMLELEMENT(
624         name record,
625         XMLATTRIBUTES('http://www.loc.gov/MARC21/slim' AS xmlns),
626         XMLELEMENT( name leader, '00881nam a2200193   4500'),
627         tmp_data,
628         XMLELEMENT(
629             name datafield,
630             XMLATTRIBUTES( '905' AS tag, ' ' AS ind1, ' ' AS ind2),
631             XMLELEMENT(
632                 name subfield,
633                 XMLATTRIBUTES('r' AS code),
634                 ARRAY_TO_STRING(replace_rules,',')
635             )
636         )
637     )::TEXT;
638 END;
639 $f$ STABLE LANGUAGE PLPGSQL;
640
641 CREATE OR REPLACE FUNCTION authority.generate_overlay_template ( BIGINT ) RETURNS TEXT AS $func$
642     SELECT authority.generate_overlay_template( marc ) FROM authority.record_entry WHERE id = $1;
643 $func$ LANGUAGE SQL;
644
645 CREATE OR REPLACE FUNCTION authority.merge_records ( target_record BIGINT, source_record BIGINT ) RETURNS INT AS $func$
646 DECLARE
647     moved_objects INT := 0;
648     bib_id        INT := 0;
649     bib_rec       biblio.record_entry%ROWTYPE;
650     auth_link     authority.bib_linking%ROWTYPE;
651     ingest_same   boolean;
652 BEGIN
653
654     -- Defining our terms:
655     -- "target record" = the record that will survive the merge
656     -- "source record" = the record that is sacrifing its existence and being
657     --   replaced by the target record
658
659     -- 1. Update all bib records with the ID from target_record in their $0
660     FOR bib_rec IN
661             SELECT  bre.*
662               FROM  biblio.record_entry bre 
663                     JOIN authority.bib_linking abl ON abl.bib = bre.id
664               WHERE abl.authority = source_record
665         LOOP
666
667         UPDATE  biblio.record_entry
668           SET   marc = REGEXP_REPLACE(
669                     marc,
670                     E'(<subfield\\s+code="0"\\s*>[^<]*?\\))' || source_record || '<',
671                     E'\\1' || target_record || '<',
672                     'g'
673                 )
674           WHERE id = bib_rec.id;
675
676           moved_objects := moved_objects + 1;
677     END LOOP;
678
679     -- 2. Grab the current value of reingest on same MARC flag
680     SELECT  enabled INTO ingest_same
681       FROM  config.internal_flag
682       WHERE name = 'ingest.reingest.force_on_same_marc'
683     ;
684
685     -- 3. Temporarily set reingest on same to TRUE
686     UPDATE  config.internal_flag
687       SET   enabled = TRUE
688       WHERE name = 'ingest.reingest.force_on_same_marc'
689     ;
690
691     -- 4. Make a harmless update to target_record to trigger auto-update
692     --    in linked bibliographic records
693     UPDATE  authority.record_entry
694       SET   deleted = FALSE
695       WHERE id = target_record;
696
697     -- 5. "Delete" source_record
698     DELETE FROM authority.record_entry WHERE id = source_record;
699
700     -- 6. Set "reingest on same MARC" flag back to initial value
701     UPDATE  config.internal_flag
702       SET   enabled = ingest_same
703       WHERE name = 'ingest.reingest.force_on_same_marc'
704     ;
705
706     RETURN moved_objects;
707 END;
708 $func$ LANGUAGE plpgsql;
709
710
711 -- Support function used to find the pivot for alpha-heading-browse style searching
712 CREATE OR REPLACE FUNCTION authority.simple_heading_find_pivot( a INT[], q TEXT, thesauruses TEXT DEFAULT '' ) RETURNS TEXT AS $$
713 DECLARE
714     sort_value_row  RECORD;
715     value_row       RECORD;
716     t_term          TEXT;
717 BEGIN
718
719     t_term := public.naco_normalize(q);
720
721     SELECT  CASE WHEN ash.sort_value LIKE t_term || '%' THEN 1 ELSE 0 END
722                 + CASE WHEN ash.value LIKE t_term || '%' THEN 1 ELSE 0 END AS rank,
723             ash.sort_value
724       INTO  sort_value_row
725       FROM  authority.simple_heading ash
726       WHERE ash.atag = ANY (a)
727             AND ash.sort_value >= t_term
728             AND CASE thesauruses
729                 WHEN '' THEN TRUE
730                 ELSE ash.thesaurus = ANY(regexp_split_to_array(thesauruses, ','))
731                 END
732       ORDER BY rank DESC, ash.sort_value
733       LIMIT 1;
734
735     SELECT  CASE WHEN ash.sort_value LIKE t_term || '%' THEN 1 ELSE 0 END
736                 + CASE WHEN ash.value LIKE t_term || '%' THEN 1 ELSE 0 END AS rank,
737             ash.sort_value
738       INTO  value_row
739       FROM  authority.simple_heading ash
740       WHERE ash.atag = ANY (a)
741             AND ash.value >= t_term
742             AND CASE thesauruses
743                 WHEN '' THEN TRUE
744                 ELSE ash.thesaurus = ANY(regexp_split_to_array(thesauruses, ','))
745                 END
746       ORDER BY rank DESC, ash.sort_value
747       LIMIT 1;
748
749     IF value_row.rank > sort_value_row.rank THEN
750         RETURN value_row.sort_value;
751     ELSE
752         RETURN sort_value_row.sort_value;
753     END IF;
754 END;
755 $$ LANGUAGE PLPGSQL;
756
757 CREATE OR REPLACE FUNCTION authority.simple_heading_browse_center( atag_list INT[], q TEXT, page INT DEFAULT 0, pagesize INT DEFAULT 9, thesauruses TEXT DEFAULT '' ) RETURNS SETOF BIGINT AS $$
758 DECLARE
759     pivot_sort_value    TEXT;
760     boffset             INT DEFAULT 0;
761     aoffset             INT DEFAULT 0;
762     blimit              INT DEFAULT 0;
763     alimit              INT DEFAULT 0;
764 BEGIN
765
766     pivot_sort_value := authority.simple_heading_find_pivot(atag_list,q,thesauruses);
767
768     IF page = 0 THEN
769         blimit := pagesize / 2;
770         alimit := blimit;
771
772         IF pagesize % 2 <> 0 THEN
773             alimit := alimit + 1;
774         END IF;
775     ELSE
776         blimit := pagesize;
777         alimit := blimit;
778
779         boffset := pagesize / 2;
780         aoffset := boffset;
781
782         IF pagesize % 2 <> 0 THEN
783             boffset := boffset + 1;
784         END IF;
785     END IF;
786
787     IF page <= 0 THEN
788         RETURN QUERY
789             -- "bottom" half of the browse results
790             SELECT id FROM (
791                 SELECT  ash.id,
792                         row_number() over ()
793                   FROM  authority.simple_heading ash
794                   WHERE ash.atag = ANY (atag_list)
795                         AND CASE thesauruses
796                             WHEN '' THEN TRUE
797                             ELSE ash.thesaurus = ANY(regexp_split_to_array(thesauruses, ','))
798                             END
799                         AND ash.sort_value < pivot_sort_value
800                   ORDER BY ash.sort_value DESC
801                   LIMIT blimit
802                   OFFSET ABS(page) * pagesize - boffset
803             ) x ORDER BY row_number DESC;
804     END IF;
805
806     IF page >= 0 THEN
807         RETURN QUERY
808             -- "bottom" half of the browse results
809             SELECT  ash.id
810               FROM  authority.simple_heading ash
811               WHERE ash.atag = ANY (atag_list)
812                     AND CASE thesauruses
813                         WHEN '' THEN TRUE
814                         ELSE ash.thesaurus = ANY(regexp_split_to_array(thesauruses, ','))
815                         END
816                     AND ash.sort_value >= pivot_sort_value
817               ORDER BY ash.sort_value
818               LIMIT alimit
819               OFFSET ABS(page) * pagesize - aoffset;
820     END IF;
821 END;
822 $$ LANGUAGE PLPGSQL ROWS 10;
823
824 CREATE OR REPLACE FUNCTION authority.axis_authority_tags(a TEXT) RETURNS INT[] AS $$
825     SELECT ARRAY_AGG(field) FROM authority.browse_axis_authority_field_map WHERE axis = $1;
826 $$ LANGUAGE SQL;
827
828
829 CREATE OR REPLACE FUNCTION authority.axis_authority_tags_refs(a TEXT) RETURNS INT[] AS $$
830     SELECT ARRAY_AGG(y) from (
831        SELECT  unnest(ARRAY_CAT(
832                  ARRAY[a.field],
833                  (SELECT ARRAY_AGG(x.id) FROM authority.control_set_authority_field x WHERE x.main_entry = a.field)
834              )) y
835        FROM  authority.browse_axis_authority_field_map a
836        WHERE axis = $1) x
837 $$ LANGUAGE SQL;
838
839
840 CREATE OR REPLACE FUNCTION authority.btag_authority_tags(btag TEXT) RETURNS INT[] AS $$
841     SELECT ARRAY_AGG(authority_field) FROM authority.control_set_bib_field WHERE tag = $1
842 $$ LANGUAGE SQL;
843
844
845 CREATE OR REPLACE FUNCTION authority.btag_authority_tags_refs(btag TEXT) RETURNS INT[] AS $$
846     SELECT ARRAY_AGG(y) from (
847         SELECT  unnest(ARRAY_CAT(
848                     ARRAY[a.authority_field],
849                     (SELECT ARRAY_AGG(x.id) FROM authority.control_set_authority_field x WHERE x.main_entry = a.authority_field)
850                 )) y
851       FROM  authority.control_set_bib_field a
852       WHERE a.tag = $1) x
853 $$ LANGUAGE SQL;
854
855
856 CREATE OR REPLACE FUNCTION authority.atag_authority_tags(atag TEXT) RETURNS INT[] AS $$
857     SELECT ARRAY_AGG(id) FROM authority.control_set_authority_field WHERE tag = $1
858 $$ LANGUAGE SQL;
859
860 CREATE OR REPLACE FUNCTION authority.atag_authority_tags_refs(atag TEXT) RETURNS INT[] AS $$
861     SELECT ARRAY_AGG(y) from (
862         SELECT  unnest(ARRAY_CAT(
863                     ARRAY[a.id],
864                     (SELECT ARRAY_AGG(x.id) FROM authority.control_set_authority_field x WHERE x.main_entry = a.id)
865                 )) y
866       FROM  authority.control_set_authority_field a
867       WHERE a.tag = $1) x
868 $$ LANGUAGE SQL;
869
870
871 CREATE OR REPLACE FUNCTION authority.axis_browse_center( a TEXT, q TEXT, page INT DEFAULT 0, pagesize INT DEFAULT 9, thesauruses TEXT DEFAULT '' ) RETURNS SETOF BIGINT AS $$
872     SELECT * FROM authority.simple_heading_browse_center(authority.axis_authority_tags($1), $2, $3, $4, $5)
873 $$ LANGUAGE SQL ROWS 10;
874
875 CREATE OR REPLACE FUNCTION authority.btag_browse_center( a TEXT, q TEXT, page INT DEFAULT 0, pagesize INT DEFAULT 9, thesauruses TEXT DEFAULT '' ) RETURNS SETOF BIGINT AS $$
876     SELECT * FROM authority.simple_heading_browse_center(authority.btag_authority_tags($1), $2, $3, $4, $5)
877 $$ LANGUAGE SQL ROWS 10;
878
879 CREATE OR REPLACE FUNCTION authority.atag_browse_center( a TEXT, q TEXT, page INT DEFAULT 0, pagesize INT DEFAULT 9, thesauruses TEXT DEFAULT '' ) RETURNS SETOF BIGINT AS $$
880     SELECT * FROM authority.simple_heading_browse_center(authority.atag_authority_tags($1), $2, $3, $4, $5)
881 $$ LANGUAGE SQL ROWS 10;
882
883 CREATE OR REPLACE FUNCTION authority.axis_browse_center_refs( a TEXT, q TEXT, page INT DEFAULT 0, pagesize INT DEFAULT 9, thesauruses TEXT DEFAULT '' ) RETURNS SETOF BIGINT AS $$
884     SELECT * FROM authority.simple_heading_browse_center(authority.axis_authority_tags_refs($1), $2, $3, $4, $5)
885 $$ LANGUAGE SQL ROWS 10;
886
887 CREATE OR REPLACE FUNCTION authority.btag_browse_center_refs( a TEXT, q TEXT, page INT DEFAULT 0, pagesize INT DEFAULT 9, thesauruses TEXT DEFAULT '' ) RETURNS SETOF BIGINT AS $$
888     SELECT * FROM authority.simple_heading_browse_center(authority.btag_authority_tags_refs($1), $2, $3, $4, $5)
889 $$ LANGUAGE SQL ROWS 10;
890
891 CREATE OR REPLACE FUNCTION authority.atag_browse_center_refs( a TEXT, q TEXT, page INT DEFAULT 0, pagesize INT DEFAULT 9, thesauruses TEXT DEFAULT '' ) RETURNS SETOF BIGINT AS $$
892     SELECT * FROM authority.simple_heading_browse_center(authority.atag_authority_tags_refs($1), $2, $3, $4, $5)
893 $$ LANGUAGE SQL ROWS 10;
894
895
896 CREATE OR REPLACE FUNCTION authority.simple_heading_browse_top( atag_list INT[], q TEXT, page INT DEFAULT 0, pagesize INT DEFAULT 10, thesauruses TEXT DEFAULT '' ) RETURNS SETOF BIGINT AS $$
897 DECLARE
898     pivot_sort_value    TEXT;
899 BEGIN
900
901     pivot_sort_value := authority.simple_heading_find_pivot(atag_list,q,thesauruses);
902
903     IF page < 0 THEN
904         RETURN QUERY
905             -- "bottom" half of the browse results
906             SELECT id FROM (
907                 SELECT  ash.id,
908                         row_number() over ()
909                   FROM  authority.simple_heading ash
910                   WHERE ash.atag = ANY (atag_list)
911                         AND CASE thesauruses
912                             WHEN '' THEN TRUE
913                             ELSE ash.thesaurus = ANY(regexp_split_to_array(thesauruses, ','))
914                             END
915                         AND ash.sort_value < pivot_sort_value
916                   ORDER BY ash.sort_value DESC
917                   LIMIT pagesize
918                   OFFSET (ABS(page) - 1) * pagesize
919             ) x ORDER BY row_number DESC;
920     END IF;
921
922     IF page >= 0 THEN
923         RETURN QUERY
924             -- "bottom" half of the browse results
925             SELECT  ash.id
926               FROM  authority.simple_heading ash
927               WHERE ash.atag = ANY (atag_list)
928                 AND CASE thesauruses
929                     WHEN '' THEN TRUE
930                     ELSE ash.thesaurus = ANY(regexp_split_to_array(thesauruses, ','))
931                     END
932                     AND ash.sort_value >= pivot_sort_value
933               ORDER BY ash.sort_value
934               LIMIT pagesize
935               OFFSET ABS(page) * pagesize ;
936     END IF;
937 END;
938 $$ LANGUAGE PLPGSQL ROWS 10;
939
940 CREATE OR REPLACE FUNCTION authority.axis_browse_top( a TEXT, q TEXT, page INT DEFAULT 0, pagesize INT DEFAULT 10, thesauruses TEXT DEFAULT '' ) RETURNS SETOF BIGINT AS $$
941     SELECT * FROM authority.simple_heading_browse_top(authority.axis_authority_tags($1), $2, $3, $4, $5)
942 $$ LANGUAGE SQL ROWS 10;
943
944 CREATE OR REPLACE FUNCTION authority.btag_browse_top( a TEXT, q TEXT, page INT DEFAULT 0, pagesize INT DEFAULT 10, thesauruses TEXT DEFAULT '' ) RETURNS SETOF BIGINT AS $$
945     SELECT * FROM authority.simple_heading_browse_top(authority.btag_authority_tags($1), $2, $3, $4, $5)
946 $$ LANGUAGE SQL ROWS 10;
947
948 CREATE OR REPLACE FUNCTION authority.atag_browse_top( a TEXT, q TEXT, page INT DEFAULT 0, pagesize INT DEFAULT 10, thesauruses TEXT DEFAULT '' ) RETURNS SETOF BIGINT AS $$
949     SELECT * FROM authority.simple_heading_browse_top(authority.atag_authority_tags($1), $2, $3, $4, $5)
950 $$ LANGUAGE SQL ROWS 10;
951
952 CREATE OR REPLACE FUNCTION authority.axis_browse_top_refs( a TEXT, q TEXT, page INT DEFAULT 0, pagesize INT DEFAULT 10, thesauruses TEXT DEFAULT '' ) RETURNS SETOF BIGINT AS $$
953     SELECT * FROM authority.simple_heading_browse_top(authority.axis_authority_tags_refs($1), $2, $3, $4, $5)
954 $$ LANGUAGE SQL ROWS 10;
955
956 CREATE OR REPLACE FUNCTION authority.btag_browse_top_refs( a TEXT, q TEXT, page INT DEFAULT 0, pagesize INT DEFAULT 10, thesauruses TEXT DEFAULT '' ) RETURNS SETOF BIGINT AS $$
957     SELECT * FROM authority.simple_heading_browse_top(authority.btag_authority_tags_refs($1), $2, $3, $4, $5)
958 $$ LANGUAGE SQL ROWS 10;
959
960 CREATE OR REPLACE FUNCTION authority.atag_browse_top_refs( a TEXT, q TEXT, page INT DEFAULT 0, pagesize INT DEFAULT 10, thesauruses TEXT DEFAULT '' ) RETURNS SETOF BIGINT AS $$
961     SELECT * FROM authority.simple_heading_browse_top(authority.atag_authority_tags_refs($1), $2, $3, $4, $5)
962 $$ LANGUAGE SQL ROWS 10;
963
964
965 CREATE OR REPLACE FUNCTION authority.simple_heading_search_rank( atag_list INT[], q TEXT, page INT DEFAULT 0, pagesize INT DEFAULT 10, thesauruses TEXT DEFAULT '' ) RETURNS SETOF BIGINT AS $$
966     SELECT  ash.id
967       FROM  authority.simple_heading ash,
968             public.naco_normalize($2) t(term),
969             plainto_tsquery('keyword'::regconfig,$2) ptsq(term)
970       WHERE ash.atag = ANY ($1)
971             AND ash.index_vector @@ ptsq.term
972             AND CASE $5
973                 WHEN '' THEN TRUE
974                 ELSE ash.thesaurus = ANY(regexp_split_to_array($5, ','))
975                 END
976       ORDER BY ts_rank_cd(ash.index_vector,ptsq.term,14)::numeric
977                     + CASE WHEN ash.sort_value LIKE t.term || '%' THEN 2 ELSE 0 END
978                     + CASE WHEN ash.value LIKE t.term || '%' THEN 1 ELSE 0 END DESC
979       LIMIT $4
980       OFFSET $4 * $3;
981 $$ LANGUAGE SQL ROWS 10;
982
983 CREATE OR REPLACE FUNCTION authority.axis_search_rank( a TEXT, q TEXT, page INT DEFAULT 0, pagesize INT DEFAULT 10, thesauruses TEXT DEFAULT '' ) RETURNS SETOF BIGINT AS $$
984     SELECT * FROM authority.simple_heading_search_rank(authority.axis_authority_tags($1), $2, $3, $4, $5)
985 $$ LANGUAGE SQL ROWS 10;
986
987 CREATE OR REPLACE FUNCTION authority.btag_search_rank( a TEXT, q TEXT, page INT DEFAULT 0, pagesize INT DEFAULT 10, thesauruses TEXT DEFAULT '' ) RETURNS SETOF BIGINT AS $$
988     SELECT * FROM authority.simple_heading_search_rank(authority.btag_authority_tags($1), $2, $3, $4, $5)
989 $$ LANGUAGE SQL ROWS 10;
990
991 CREATE OR REPLACE FUNCTION authority.atag_search_rank( a TEXT, q TEXT, page INT DEFAULT 0, pagesize INT DEFAULT 10, thesauruses TEXT DEFAULT '' ) RETURNS SETOF BIGINT AS $$
992     SELECT * FROM authority.simple_heading_search_rank(authority.atag_authority_tags($1), $2, $3, $4, $5)
993 $$ LANGUAGE SQL ROWS 10;
994
995 CREATE OR REPLACE FUNCTION authority.axis_search_rank_refs( a TEXT, q TEXT, page INT DEFAULT 0, pagesize INT DEFAULT 10, thesauruses TEXT DEFAULT '' ) RETURNS SETOF BIGINT AS $$
996     SELECT * FROM authority.simple_heading_search_rank(authority.axis_authority_tags_refs($1), $2, $3, $4, $5)
997 $$ LANGUAGE SQL ROWS 10;
998
999 CREATE OR REPLACE FUNCTION authority.btag_search_rank_refs( a TEXT, q TEXT, page INT DEFAULT 0, pagesize INT DEFAULT 10, thesauruses TEXT DEFAULT '' ) RETURNS SETOF BIGINT AS $$
1000     SELECT * FROM authority.simple_heading_search_rank(authority.btag_authority_tags_refs($1), $2, $3, $4, $5)
1001 $$ LANGUAGE SQL ROWS 10;
1002
1003 CREATE OR REPLACE FUNCTION authority.atag_search_rank_refs( a TEXT, q TEXT, page INT DEFAULT 0, pagesize INT DEFAULT 10, thesauruses TEXT DEFAULT '' ) RETURNS SETOF BIGINT AS $$
1004     SELECT * FROM authority.simple_heading_search_rank(authority.atag_authority_tags_refs($1), $2, $3, $4, $5)
1005 $$ LANGUAGE SQL ROWS 10;
1006
1007
1008 CREATE OR REPLACE FUNCTION authority.simple_heading_search_heading( atag_list INT[], q TEXT, page INT DEFAULT 0, pagesize INT DEFAULT 10, thesauruses TEXT DEFAULT '' ) RETURNS SETOF BIGINT AS $$
1009     SELECT  ash.id
1010       FROM  authority.simple_heading ash,
1011             public.naco_normalize($2) t(term),
1012             plainto_tsquery('keyword'::regconfig,$2) ptsq(term)
1013       WHERE ash.atag = ANY ($1)
1014             AND ash.index_vector @@ ptsq.term
1015             AND CASE $5
1016                 WHEN '' THEN TRUE
1017                 ELSE ash.thesaurus = ANY(regexp_split_to_array($5, ','))
1018                 END
1019       ORDER BY ash.sort_value
1020       LIMIT $4
1021       OFFSET $4 * $3;
1022 $$ LANGUAGE SQL ROWS 10;
1023
1024 CREATE OR REPLACE FUNCTION authority.axis_search_heading( a TEXT, q TEXT, page INT DEFAULT 0, pagesize INT DEFAULT 10, thesauruses TEXT DEFAULT '' ) RETURNS SETOF BIGINT AS $$
1025     SELECT * FROM authority.simple_heading_search_heading(authority.axis_authority_tags($1), $2, $3, $4, $5)
1026 $$ LANGUAGE SQL ROWS 10;
1027
1028 CREATE OR REPLACE FUNCTION authority.btag_search_heading( a TEXT, q TEXT, page INT DEFAULT 0, pagesize INT DEFAULT 10, thesauruses TEXT DEFAULT '' ) RETURNS SETOF BIGINT AS $$
1029     SELECT * FROM authority.simple_heading_search_heading(authority.btag_authority_tags($1), $2, $3, $4, $5)
1030 $$ LANGUAGE SQL ROWS 10;
1031
1032 CREATE OR REPLACE FUNCTION authority.atag_search_heading( a TEXT, q TEXT, page INT DEFAULT 0, pagesize INT DEFAULT 10, thesauruses TEXT DEFAULT '' ) RETURNS SETOF BIGINT AS $$
1033     SELECT * FROM authority.simple_heading_search_heading(authority.atag_authority_tags($1), $2, $3, $4, $5)
1034 $$ LANGUAGE SQL ROWS 10;
1035
1036 CREATE OR REPLACE FUNCTION authority.axis_search_heading_refs( a TEXT, q TEXT, page INT DEFAULT 0, pagesize INT DEFAULT 10, thesauruses TEXT DEFAULT '' ) RETURNS SETOF BIGINT AS $$
1037     SELECT * FROM authority.simple_heading_search_heading(authority.axis_authority_tags_refs($1), $2, $3, $4, $5)
1038 $$ LANGUAGE SQL ROWS 10;
1039
1040 CREATE OR REPLACE FUNCTION authority.btag_search_heading_refs( a TEXT, q TEXT, page INT DEFAULT 0, pagesize INT DEFAULT 10, thesauruses TEXT DEFAULT '' ) RETURNS SETOF BIGINT AS $$
1041     SELECT * FROM authority.simple_heading_search_heading(authority.btag_authority_tags_refs($1), $2, $3, $4, $5)
1042 $$ LANGUAGE SQL ROWS 10;
1043
1044 CREATE OR REPLACE FUNCTION authority.atag_search_heading_refs( a TEXT, q TEXT, page INT DEFAULT 0, pagesize INT DEFAULT 10, thesauruses TEXT DEFAULT '' ) RETURNS SETOF BIGINT AS $$
1045     SELECT * FROM authority.simple_heading_search_heading(authority.atag_authority_tags_refs($1), $2, $3, $4, $5)
1046 $$ LANGUAGE SQL ROWS 10;
1047
1048 CREATE OR REPLACE FUNCTION authority.extract_headings(marc TEXT, restrict INT[] DEFAULT NULL) RETURNS SETOF authority.heading AS $func$
1049 DECLARE
1050     idx         authority.heading_field%ROWTYPE;
1051     xfrm        config.xml_transform%ROWTYPE;
1052     prev_xfrm   TEXT;
1053     transformed_xml TEXT;
1054     heading_node    TEXT;
1055     heading_node_list   TEXT[];
1056     component_node    TEXT;
1057     component_node_list   TEXT[];
1058     raw_text    TEXT;
1059     normalized_text    TEXT;
1060     normalizer  RECORD;
1061     curr_text   TEXT;
1062     joiner      TEXT;
1063     type_value  TEXT;
1064     base_thesaurus TEXT := NULL;
1065     output_row  authority.heading;
1066 BEGIN
1067
1068     -- Loop over the indexing entries
1069     FOR idx IN SELECT * FROM authority.heading_field WHERE restrict IS NULL OR id = ANY (restrict) ORDER BY format LOOP
1070
1071         output_row.field   := idx.id;
1072         output_row.type    := idx.heading_type;
1073         output_row.purpose := idx.heading_purpose;
1074
1075         joiner := COALESCE(idx.joiner, ' ');
1076
1077         SELECT INTO xfrm * from config.xml_transform WHERE name = idx.format;
1078
1079         -- See if we can skip the XSLT ... it's expensive
1080         IF prev_xfrm IS NULL OR prev_xfrm <> xfrm.name THEN
1081             -- Can't skip the transform
1082             IF xfrm.xslt <> '---' THEN
1083                 transformed_xml := oils_xslt_process(marc, xfrm.xslt);
1084             ELSE
1085                 transformed_xml := marc;
1086             END IF;
1087
1088             prev_xfrm := xfrm.name;
1089         END IF;
1090
1091         IF idx.thesaurus_xpath IS NOT NULL THEN
1092             base_thesaurus := ARRAY_TO_STRING(oils_xpath(idx.thesaurus_xpath, transformed_xml, ARRAY[ARRAY[xfrm.prefix, xfrm.namespace_uri]]), '');
1093         END IF;
1094
1095         heading_node_list := oils_xpath( idx.heading_xpath, transformed_xml, ARRAY[ARRAY[xfrm.prefix, xfrm.namespace_uri]] );
1096
1097         FOR heading_node IN SELECT x FROM unnest(heading_node_list) AS x LOOP
1098
1099             CONTINUE WHEN heading_node !~ E'^\\s*<';
1100
1101             output_row.variant_type := NULL;
1102             output_row.related_type := NULL;
1103             output_row.thesaurus    := NULL;
1104             output_row.heading      := NULL;
1105
1106             IF idx.heading_purpose = 'variant' AND idx.type_xpath IS NOT NULL THEN
1107                 type_value := ARRAY_TO_STRING(oils_xpath(idx.type_xpath, heading_node, ARRAY[ARRAY[xfrm.prefix, xfrm.namespace_uri]]), '');
1108                 BEGIN
1109                     output_row.variant_type := type_value;
1110                 EXCEPTION WHEN invalid_text_representation THEN
1111                     RAISE NOTICE 'Do not recognize variant heading type %', type_value;
1112                 END;
1113             END IF;
1114             IF idx.heading_purpose = 'related' AND idx.type_xpath IS NOT NULL THEN
1115                 type_value := ARRAY_TO_STRING(oils_xpath(idx.type_xpath, heading_node, ARRAY[ARRAY[xfrm.prefix, xfrm.namespace_uri]]), '');
1116                 BEGIN
1117                     output_row.related_type := type_value;
1118                 EXCEPTION WHEN invalid_text_representation THEN
1119                     RAISE NOTICE 'Do not recognize related heading type %', type_value;
1120                 END;
1121             END IF;
1122  
1123             IF idx.thesaurus_override_xpath IS NOT NULL THEN
1124                 output_row.thesaurus := ARRAY_TO_STRING(oils_xpath(idx.thesaurus_override_xpath, heading_node, ARRAY[ARRAY[xfrm.prefix, xfrm.namespace_uri]]), '');
1125             END IF;
1126             IF output_row.thesaurus IS NULL THEN
1127                 output_row.thesaurus := base_thesaurus;
1128             END IF;
1129
1130             raw_text := NULL;
1131
1132             -- now iterate over components of heading
1133             component_node_list := oils_xpath( idx.component_xpath, heading_node, ARRAY[ARRAY[xfrm.prefix, xfrm.namespace_uri]] );
1134             FOR component_node IN SELECT x FROM unnest(component_node_list) AS x LOOP
1135             -- XXX much of this should be moved into oils_xpath_string...
1136                 curr_text := ARRAY_TO_STRING(evergreen.array_remove_item_by_value(evergreen.array_remove_item_by_value(
1137                     oils_xpath( '//text()', -- get the content of all the nodes within the main selected node
1138                         REGEXP_REPLACE( component_node, E'\\s+', ' ', 'g' ) -- Translate adjacent whitespace to a single space
1139                     ), ' '), ''),  -- throw away morally empty (bankrupt?) strings
1140                     joiner
1141                 );
1142
1143                 CONTINUE WHEN curr_text IS NULL OR curr_text = '';
1144
1145                 IF raw_text IS NOT NULL THEN
1146                     raw_text := raw_text || joiner;
1147                 END IF;
1148
1149                 raw_text := COALESCE(raw_text,'') || curr_text;
1150             END LOOP;
1151
1152             IF raw_text IS NOT NULL THEN
1153                 output_row.heading := raw_text;
1154                 normalized_text := raw_text;
1155
1156                 FOR normalizer IN
1157                     SELECT  n.func AS func,
1158                             n.param_count AS param_count,
1159                             m.params AS params
1160                     FROM  config.index_normalizer n
1161                             JOIN authority.heading_field_norm_map m ON (m.norm = n.id)
1162                     WHERE m.field = idx.id
1163                     ORDER BY m.pos LOOP
1164             
1165                         EXECUTE 'SELECT ' || normalizer.func || '(' ||
1166                             quote_literal( normalized_text ) ||
1167                             CASE
1168                                 WHEN normalizer.param_count > 0
1169                                     THEN ',' || REPLACE(REPLACE(BTRIM(normalizer.params,'[]'),E'\'',E'\\\''),E'"',E'\'')
1170                                     ELSE ''
1171                                 END ||
1172                             ')' INTO normalized_text;
1173             
1174                 END LOOP;
1175             
1176                 output_row.normalized_heading := normalized_text;
1177             
1178                 RETURN NEXT output_row;
1179             END IF;
1180         END LOOP;
1181
1182     END LOOP;
1183 END;
1184 $func$ LANGUAGE PLPGSQL;
1185
1186 CREATE OR REPLACE FUNCTION authority.extract_headings(rid BIGINT, restrict INT[] DEFAULT NULL) RETURNS SETOF authority.heading AS $func$
1187 DECLARE
1188     auth        authority.record_entry%ROWTYPE;
1189     output_row  authority.heading;
1190 BEGIN
1191     -- Get the record
1192     SELECT INTO auth * FROM authority.record_entry WHERE id = rid;
1193
1194     RETURN QUERY SELECT * FROM authority.extract_headings(auth.marc, restrict);
1195 END;
1196 $func$ LANGUAGE PLPGSQL;
1197
1198 COMMIT;
1199