]> git.evergreen-ils.org Git - Evergreen.git/blob - Open-ILS/src/sql/Pg/011.schema.authority.sql
Use control sets to drive the tracing links view
[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     sf_list     TEXT    NOT NULL,
37     name        TEXT    NOT NULL, -- i18n
38     description TEXT              -- i18n
39 );
40
41 CREATE TABLE authority.control_set_bib_field (
42     id              SERIAL  PRIMARY KEY,
43     authority_field INT     NOT NULL REFERENCES authority.control_set_authority_field (id) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED,
44     tag             CHAR(3) NOT NULL
45 );
46
47 CREATE TABLE authority.thesaurus (
48     code        TEXT    PRIMARY KEY,     -- MARC21 thesaurus code
49     control_set INT     NOT NULL REFERENCES authority.control_set (id) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED,
50     name        TEXT    NOT NULL UNIQUE, -- i18n
51     description TEXT                     -- i18n
52 );
53
54 CREATE TABLE authority.record_entry (
55     id              BIGSERIAL    PRIMARY KEY,
56     create_date     TIMESTAMP WITH TIME ZONE    NOT NULL DEFAULT now(),
57     edit_date       TIMESTAMP WITH TIME ZONE    NOT NULL DEFAULT now(),
58     creator         INT     NOT NULL DEFAULT 1,
59     editor          INT     NOT NULL DEFAULT 1,
60     active          BOOL    NOT NULL DEFAULT TRUE,
61     deleted         BOOL    NOT NULL DEFAULT FALSE,
62     source          INT,
63     control_set     INT     REFERENCES authority.control_set (id) ON UPDATE CASCADE DEFERRABLE INITIALLY DEFERRED,
64     marc            TEXT    NOT NULL,
65     last_xact_id    TEXT    NOT NULL,
66     owner           INT
67 );
68 CREATE INDEX authority_record_entry_creator_idx ON authority.record_entry ( creator );
69 CREATE INDEX authority_record_entry_editor_idx ON authority.record_entry ( editor );
70 CREATE INDEX authority_record_deleted_idx ON authority.record_entry(deleted) WHERE deleted IS FALSE OR deleted = false;
71 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();
72 CREATE TRIGGER b_maintain_901 BEFORE INSERT OR UPDATE ON authority.record_entry FOR EACH ROW EXECUTE PROCEDURE evergreen.maintain_901();
73 CREATE TRIGGER c_maintain_control_numbers BEFORE INSERT OR UPDATE ON authority.record_entry FOR EACH ROW EXECUTE PROCEDURE maintain_control_numbers();
74 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);
75
76 CREATE TABLE authority.bib_linking (
77     id          BIGSERIAL   PRIMARY KEY,
78     bib         BIGINT      NOT NULL REFERENCES biblio.record_entry (id),
79     authority   BIGINT      NOT NULL REFERENCES authority.record_entry (id)
80 );
81 CREATE INDEX authority_bl_bib_idx ON authority.bib_linking ( bib );
82 CREATE UNIQUE INDEX authority_bl_bib_authority_once_idx ON authority.bib_linking ( authority, bib );
83
84 CREATE TABLE authority.record_note (
85     id          BIGSERIAL   PRIMARY KEY,
86     record      BIGINT      NOT NULL REFERENCES authority.record_entry (id) DEFERRABLE INITIALLY DEFERRED,
87     value       TEXT        NOT NULL,
88     creator     INT         NOT NULL DEFAULT 1,
89     editor      INT         NOT NULL DEFAULT 1,
90     create_date TIMESTAMP WITH TIME ZONE    NOT NULL DEFAULT now(),
91     edit_date   TIMESTAMP WITH TIME ZONE    NOT NULL DEFAULT now()
92 );
93 CREATE INDEX authority_record_note_record_idx ON authority.record_note ( record );
94 CREATE INDEX authority_record_note_creator_idx ON authority.record_note ( creator );
95 CREATE INDEX authority_record_note_editor_idx ON authority.record_note ( editor );
96
97 CREATE TABLE authority.rec_descriptor (
98     id              BIGSERIAL PRIMARY KEY,
99     record          BIGINT,
100     record_status   TEXT,
101     encoding_level  TEXT,
102     thesaurus       TEXT
103 );
104 CREATE INDEX authority_rec_descriptor_record_idx ON authority.rec_descriptor (record);
105
106 CREATE TABLE authority.full_rec (
107     id              BIGSERIAL   PRIMARY KEY,
108     record          BIGINT      NOT NULL,
109     tag             CHAR(3)     NOT NULL,
110     ind1            TEXT,
111     ind2            TEXT,
112     subfield        TEXT,
113     value           TEXT        NOT NULL,
114     index_vector    tsvector    NOT NULL
115 );
116 CREATE INDEX authority_full_rec_record_idx ON authority.full_rec (record);
117 CREATE INDEX authority_full_rec_tag_subfield_idx ON authority.full_rec (tag, subfield);
118 CREATE INDEX authority_full_rec_tag_part_idx ON authority.full_rec (SUBSTRING(tag FROM 2));
119 CREATE INDEX authority_full_rec_subfield_a_idx ON authority.full_rec (value) WHERE subfield = 'a';
120 CREATE TRIGGER authority_full_rec_fti_trigger
121     BEFORE UPDATE OR INSERT ON authority.full_rec
122     FOR EACH ROW EXECUTE PROCEDURE tsearch2(index_vector, value);
123
124 CREATE INDEX authority_full_rec_index_vector_idx ON authority.full_rec USING GIST (index_vector);
125 /* Enable LIKE to use an index for database clusters with locales other than C or POSIX */
126 CREATE INDEX authority_full_rec_value_tpo_index ON authority.full_rec (value text_pattern_ops);
127
128 -- Adding indexes using oils_xpath_string() for the main entry tags described in
129 -- authority.control_set_authority_field would speed this up, if we ever want to use it, though
130 -- the existing index on authority.normalize_heading() helps already with a record in hand
131 CREATE OR REPLACE VIEW authority.tracing_links AS
132     SELECT  main.record AS record,
133             main.id AS main_id,
134             main.tag AS main_tag,
135             oils_xpath_string('//*[@datafield="'||main.tag||'"]/*[local-name="subfield"]', are.marc) AS main_value,
136             authority.normalize_heading(are.marc) AS normalized_main_value,
137             substr(link.value,1,1) AS relationship,
138             substr(link.value,2,1) AS use_restriction,
139             substr(link.value,3,1) AS deprecation,
140             substr(link.value,4,1) AS display_restriction,
141             link.id AS link_id,
142             link.tag AS link_tag,
143             extract_marc_field('authority.record_entry',link.record,'//*[@datafield="'||link.tag||'"]') AS link_value
144       FROM  authority.full_rec main
145             JOIN authority.record_entry are ON (main.record = are.id)
146             JOIN authority.control_set_authority_field main_entry
147                 ON (main_entry.tag = main.tag
148                     AND main_entry.main_entry IS NULL
149                     AND main.subfield = 'a' )
150             JOIN authority.control_set_authority_field sub_entry
151                 ON (main_entry.id = sub_entry.main_entry)
152             JOIN authority.full_rec link
153                 ON (link.record = main.record
154                     AND link.tag = sub_entry.tag
155                     AND link.subfield = 'w' );
156
157
158 -- Function to generate an ephemeral overlay template from an authority record
159 CREATE OR REPLACE FUNCTION authority.generate_overlay_template ( TEXT, BIGINT ) RETURNS TEXT AS $func$
160
161     use MARC::Record;
162     use MARC::File::XML (BinaryEncoding => 'UTF-8');
163     use MARC::Charset;
164
165     MARC::Charset->assume_unicode(1);
166
167     my $xml = shift;
168     my $r = MARC::Record->new_from_xml( $xml );
169
170     return undef unless ($r);
171
172     my $id = shift() || $r->subfield( '901' => 'c' );
173     $id =~ s/^\s*(?:\([^)]+\))?\s*(.+)\s*?$/$1/;
174     return undef unless ($id); # We need an ID!
175
176     my $tmpl = MARC::Record->new();
177     $tmpl->encoding( 'UTF-8' );
178
179     my @rule_fields;
180     for my $field ( $r->field( '1..' ) ) { # Get main entry fields from the authority record
181
182         my $tag = $field->tag;
183         my $i1 = $field->indicator(1);
184         my $i2 = $field->indicator(2);
185         my $sf = join '', map { $_->[0] } $field->subfields;
186         my @data = map { @$_ } $field->subfields;
187
188         my @replace_them;
189
190         # Map the authority field to bib fields it can control.
191         if ($tag >= 100 and $tag <= 111) {       # names
192             @replace_them = map { $tag + $_ } (0, 300, 500, 600, 700);
193         } elsif ($tag eq '130') {                # uniform title
194             @replace_them = qw/130 240 440 730 830/;
195         } elsif ($tag >= 150 and $tag <= 155) {  # subjects
196             @replace_them = ($tag + 500);
197         } elsif ($tag >= 180 and $tag <= 185) {  # floating subdivisions
198             @replace_them = qw/100 400 600 700 800 110 410 610 710 810 111 411 611 711 811 130 240 440 730 830 650 651 655/;
199         } else {
200             next;
201         }
202
203         # Dummy up the bib-side data
204         $tmpl->append_fields(
205             map {
206                 MARC::Field->new( $_, $i1, $i2, @data )
207             } @replace_them
208         );
209
210         # Construct some 'replace' rules
211         push @rule_fields, map { $_ . $sf . '[0~\)' .$id . '$]' } @replace_them;
212     }
213
214     # Insert the replace rules into the template
215     $tmpl->append_fields(
216         MARC::Field->new( '905' => ' ' => ' ' => 'r' => join(',', @rule_fields ) )
217     );
218
219     $xml = $tmpl->as_xml_record;
220     $xml =~ s/^<\?.+?\?>$//mo;
221     $xml =~ s/\n//sgo;
222     $xml =~ s/>\s+</></sgo;
223
224     return $xml;
225
226 $func$ LANGUAGE PLPERLU;
227
228 CREATE OR REPLACE FUNCTION authority.generate_overlay_template ( BIGINT ) RETURNS TEXT AS $func$
229     SELECT authority.generate_overlay_template( marc, id ) FROM authority.record_entry WHERE id = $1;
230 $func$ LANGUAGE SQL;
231
232 CREATE OR REPLACE FUNCTION authority.generate_overlay_template ( TEXT ) RETURNS TEXT AS $func$
233     SELECT authority.generate_overlay_template( $1, NULL );
234 $func$ LANGUAGE SQL;
235
236 CREATE OR REPLACE FUNCTION authority.merge_records ( target_record BIGINT, source_record BIGINT ) RETURNS INT AS $func$
237 DECLARE
238     moved_objects INT := 0;
239     bib_id        INT := 0;
240     bib_rec       biblio.record_entry%ROWTYPE;
241     auth_link     authority.bib_linking%ROWTYPE;
242     ingest_same   boolean;
243 BEGIN
244
245     -- Defining our terms:
246     -- "target record" = the record that will survive the merge
247     -- "source record" = the record that is sacrifing its existence and being
248     --   replaced by the target record
249
250     -- 1. Update all bib records with the ID from target_record in their $0
251     FOR bib_rec IN
252             SELECT  bre.*
253               FROM  biblio.record_entry bre 
254                     JOIN authority.bib_linking abl ON abl.bib = bre.id
255               WHERE abl.authority = source_record
256         LOOP
257
258         UPDATE  biblio.record_entry
259           SET   marc = REGEXP_REPLACE(
260                     marc,
261                     E'(<subfield\\s+code="0"\\s*>[^<]*?\\))' || source_record || '<',
262                     E'\\1' || target_record || '<',
263                     'g'
264                 )
265           WHERE id = bib_rec.id;
266
267           moved_objects := moved_objects + 1;
268     END LOOP;
269
270     -- 2. Grab the current value of reingest on same MARC flag
271     SELECT  enabled INTO ingest_same
272       FROM  config.internal_flag
273       WHERE name = 'ingest.reingest.force_on_same_marc'
274     ;
275
276     -- 3. Temporarily set reingest on same to TRUE
277     UPDATE  config.internal_flag
278       SET   enabled = TRUE
279       WHERE name = 'ingest.reingest.force_on_same_marc'
280     ;
281
282     -- 4. Make a harmless update to target_record to trigger auto-update
283     --    in linked bibliographic records
284     UPDATE  authority.record_entry
285       SET   deleted = FALSE
286       WHERE id = target_record;
287
288     -- 5. "Delete" source_record
289     DELETE FROM authority.record_entry WHERE id = source_record;
290
291     -- 6. Set "reingest on same MARC" flag back to initial value
292     UPDATE  config.internal_flag
293       SET   enabled = ingest_same
294       WHERE name = 'ingest.reingest.force_on_same_marc'
295     ;
296
297     RETURN moved_objects;
298 END;
299 $func$ LANGUAGE plpgsql;
300
301 COMMIT;