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