34224b194515a6cfec77c6af32b82ca94ec07a24
[working/Evergreen.git] / Open-ILS / src / sql / Pg / 002.schema.config.sql
1 /*
2  * Copyright (C) 2004-2008  Georgia Public Library Service
3  * Copyright (C) 2008-2011  Equinox Software, Inc.
4  * Mike Rylander <miker@esilibrary.com>
5  * Copyright (C) 2010 Merrimack Valley Library Consortium
6  * Jason Stephenson <jstephenson@mvlc.org>
7  * Copyright (C) 2010 Laurentian University
8  * Dan Scott <dscott@laurentian.ca>
9  *
10  * This program is free software; you can redistribute it and/or
11  * modify it under the terms of the GNU General Public License
12  * as published by the Free Software Foundation; either version 2
13  * of the License, or (at your option) any later version.
14  *
15  * This program is distributed in the hope that it will be useful,
16  * but WITHOUT ANY WARRANTY; without even the implied warranty of
17  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
18  * GNU General Public License for more details.
19  *
20  */
21
22
23
24 DROP SCHEMA IF EXISTS stats CASCADE;
25 DROP SCHEMA IF EXISTS config CASCADE;
26
27 BEGIN;
28 CREATE SCHEMA stats;
29
30 CREATE SCHEMA config;
31 COMMENT ON SCHEMA config IS $$
32 The config schema holds static configuration data for the
33 Evergreen installation.
34 $$;
35
36 CREATE TABLE config.internal_flag (
37     name    TEXT    PRIMARY KEY,
38     value   TEXT,
39     enabled BOOL    NOT NULL DEFAULT FALSE
40 );
41 INSERT INTO config.internal_flag (name) VALUES ('ingest.metarecord_mapping.skip_on_insert');
42 INSERT INTO config.internal_flag (name) VALUES ('ingest.metarecord_mapping.skip_on_update');
43 INSERT INTO config.internal_flag (name) VALUES ('ingest.reingest.force_on_same_marc');
44 INSERT INTO config.internal_flag (name) VALUES ('ingest.disable_located_uri');
45 INSERT INTO config.internal_flag (name) VALUES ('ingest.disable_metabib_full_rec');
46 INSERT INTO config.internal_flag (name) VALUES ('ingest.disable_metabib_rec_descriptor');
47 INSERT INTO config.internal_flag (name) VALUES ('ingest.disable_metabib_field_entry');
48 INSERT INTO config.internal_flag (name) VALUES ('ingest.assume_inserts_only');
49 INSERT INTO config.internal_flag (name) VALUES ('serial.rematerialize_on_same_holding_code');
50
51 CREATE TABLE config.global_flag (
52     label   TEXT    NOT NULL
53 ) INHERITS (config.internal_flag);
54 ALTER TABLE config.global_flag ADD PRIMARY KEY (name);
55
56 CREATE TABLE config.upgrade_log (
57     version         TEXT    PRIMARY KEY,
58     install_date    TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
59     applied_to      TEXT
60 );
61
62 CREATE TABLE config.db_patch_dependencies (
63   db_patch      TEXT PRIMARY KEY,
64   supersedes    TEXT[],
65   deprecates    TEXT[]
66 );
67
68 CREATE OR REPLACE FUNCTION evergreen.array_overlap_check (/* field */) RETURNS TRIGGER AS $$
69 DECLARE
70     fld     TEXT;
71     cnt     INT;
72 BEGIN
73     fld := TG_ARGV[1];
74     EXECUTE 'SELECT COUNT(*) FROM '|| TG_TABLE_SCHEMA ||'.'|| TG_TABLE_NAME ||' WHERE '|| fld ||' && ($1).'|| fld INTO cnt USING NEW;
75     IF cnt > 0 THEN
76         RAISE EXCEPTION 'Cannot insert duplicate array into field % of table %', fld, TG_TABLE_SCHEMA ||'.'|| TG_TABLE_NAME;
77     END IF;
78     RETURN NEW;
79 END;
80 $$ LANGUAGE PLPGSQL;
81
82 CREATE TRIGGER no_overlapping_sups
83     BEFORE INSERT OR UPDATE ON config.db_patch_dependencies
84     FOR EACH ROW EXECUTE PROCEDURE evergreen.array_overlap_check ('supersedes');
85
86 CREATE TRIGGER no_overlapping_deps
87     BEFORE INSERT OR UPDATE ON config.db_patch_dependencies
88     FOR EACH ROW EXECUTE PROCEDURE evergreen.array_overlap_check ('deprecates');
89
90 INSERT INTO config.upgrade_log (version, applied_to) VALUES ('0701', :eg_version); -- sprater/bshum/tsbere
91
92 CREATE TABLE config.bib_source (
93         id              SERIAL  PRIMARY KEY,
94         quality         INT     CHECK ( quality BETWEEN 0 AND 100 ),
95         source          TEXT    NOT NULL UNIQUE,
96         transcendant    BOOL    NOT NULL DEFAULT FALSE,
97         can_have_copies BOOL    NOT NULL DEFAULT TRUE
98 );
99 COMMENT ON TABLE config.bib_source IS $$
100 This is table is used to set up the relative "quality" of each
101 MARC source, such as OCLC.  Also identifies "transcendant" sources,
102 i.e., sources of bib records that should display in the OPAC
103 even if no copies or located URIs are attached. Also indicates if
104 the source is allowed to have actual copies on its bibs. Volumes
105 for targeted URIs are unaffected by this setting.
106 $$;
107
108 CREATE TABLE config.standing (
109         id              SERIAL  PRIMARY KEY,
110         value           TEXT    NOT NULL UNIQUE
111 );
112 COMMENT ON TABLE config.standing IS $$
113 Patron Standings
114
115 This table contains the values that can be applied to a patron
116 by a staff member.  These values should not be changed, other
117 than for translation, as the ID column is currently a "magic
118 number" in the source. :(
119 $$;
120
121 CREATE TABLE config.standing_penalty (
122         id                      SERIAL  PRIMARY KEY,
123         name            TEXT    NOT NULL UNIQUE,
124         label           TEXT    NOT NULL,
125         block_list      TEXT,
126         staff_alert     BOOL    NOT NULL DEFAULT FALSE,
127         org_depth       INTEGER
128 );
129
130 CREATE TABLE config.xml_transform (
131         name            TEXT    PRIMARY KEY,
132         namespace_uri   TEXT    NOT NULL,
133         prefix          TEXT    NOT NULL,
134         xslt            TEXT    NOT NULL
135 );
136
137 CREATE TABLE config.biblio_fingerprint (
138         id                      SERIAL  PRIMARY KEY,
139         name            TEXT    NOT NULL, 
140         xpath           TEXT    NOT NULL,
141     first_word  BOOL    NOT NULL DEFAULT FALSE,
142         format          TEXT    NOT NULL DEFAULT 'marcxml'
143 );
144
145 INSERT INTO config.biblio_fingerprint (name, xpath, format)
146     VALUES (
147         'Title',
148         '//marc:datafield[@tag="700"]/marc:subfield[@code="t"]|' ||
149             '//marc:datafield[@tag="240"]/marc:subfield[@code="a"]|' ||
150             '//marc:datafield[@tag="242"]/marc:subfield[@code="a"]|' ||
151             '//marc:datafield[@tag="246"]/marc:subfield[@code="a"]|' ||
152             '//marc:datafield[@tag="245"]/marc:subfield[@code="a"]',
153         'marcxml'
154     );
155
156 INSERT INTO config.biblio_fingerprint (name, xpath, format, first_word)
157     VALUES (
158         'Author',
159         '//marc:datafield[@tag="700" and ./*[@code="t"]]/marc:subfield[@code="a"]|'
160             '//marc:datafield[@tag="100"]/marc:subfield[@code="a"]|'
161             '//marc:datafield[@tag="110"]/marc:subfield[@code="a"]|'
162             '//marc:datafield[@tag="111"]/marc:subfield[@code="a"]|'
163             '//marc:datafield[@tag="260"]/marc:subfield[@code="b"]',
164         'marcxml',
165         TRUE
166     );
167
168 CREATE TABLE config.metabib_class (
169     name     TEXT    PRIMARY KEY,
170     label    TEXT    NOT NULL UNIQUE,
171     buoyant  BOOL    DEFAULT FALSE NOT NULL,
172     restrict BOOL    DEFAULT FALSE NOT NULL
173 );
174
175 CREATE TABLE config.metabib_field (
176         id              SERIAL  PRIMARY KEY,
177         field_class     TEXT    NOT NULL REFERENCES config.metabib_class (name),
178         name            TEXT    NOT NULL,
179         label           TEXT    NOT NULL,
180         xpath           TEXT    NOT NULL,
181         weight          INT     NOT NULL DEFAULT 1,
182         format          TEXT    NOT NULL REFERENCES config.xml_transform (name) DEFAULT 'mods33',
183         search_field    BOOL    NOT NULL DEFAULT TRUE,
184         facet_field     BOOL    NOT NULL DEFAULT FALSE,
185         browse_field    BOOL    NOT NULL DEFAULT TRUE,
186         browse_xpath   TEXT,
187         facet_xpath     TEXT,
188         restrict        BOOL    DEFAULT FALSE NOT NULL
189 );
190 COMMENT ON TABLE config.metabib_field IS $$
191 XPath used for record indexing ingest
192
193 This table contains the XPath used to chop up MODS into its
194 indexable parts.  Each XPath entry is named and assigned to
195 a "class" of either title, subject, author, keyword, series
196 or identifier.
197 $$;
198
199 CREATE UNIQUE INDEX config_metabib_field_class_name_idx ON config.metabib_field (field_class, name);
200
201 CREATE TABLE config.metabib_search_alias (
202     alias       TEXT    PRIMARY KEY,
203     field_class TEXT    NOT NULL REFERENCES config.metabib_class (name),
204     field       INT     REFERENCES config.metabib_field (id)
205 );
206
207 CREATE TABLE config.non_cataloged_type (
208         id              SERIAL          PRIMARY KEY,
209         owning_lib      INT             NOT NULL, -- REFERENCES actor.org_unit (id),
210         name            TEXT            NOT NULL,
211         circ_duration   INTERVAL        NOT NULL DEFAULT '14 days'::INTERVAL,
212         in_house        BOOL            NOT NULL DEFAULT FALSE,
213         CONSTRAINT noncat_once_per_lib UNIQUE (owning_lib,name)
214 );
215 COMMENT ON TABLE config.non_cataloged_type IS $$
216 Types of valid non-cataloged items.
217 $$;
218
219 CREATE TABLE config.identification_type (
220         id              SERIAL  PRIMARY KEY,
221         name            TEXT    NOT NULL UNIQUE
222 );
223 COMMENT ON TABLE config.identification_type IS $$
224 Types of valid patron identification.
225
226 Each patron must display at least one valid form of identification
227 in order to get a library card.  This table lists those forms.
228 $$;
229
230 CREATE TABLE config.rule_circ_duration (
231         id              SERIAL          PRIMARY KEY,
232         name            TEXT            NOT NULL UNIQUE CHECK ( name ~ E'^\\w+$' ),
233         extended        INTERVAL        NOT NULL,
234         normal          INTERVAL        NOT NULL,
235         shrt            INTERVAL        NOT NULL,
236         max_renewals    INT             NOT NULL
237 );
238 COMMENT ON TABLE config.rule_circ_duration IS $$
239 Circulation Duration rules
240
241 Each circulation is given a duration based on one of these rules.
242 $$;
243
244 CREATE TABLE config.hard_due_date (
245     id                  SERIAL      PRIMARY KEY,
246     name                TEXT        NOT NULL UNIQUE,
247     ceiling_date        TIMESTAMPTZ NOT NULL,
248     forceto             BOOL        NOT NULL,
249     owner               INT         NOT NULL   -- REFERENCES actor.org_unit (id) DEFERRABLE INITIALLY DEFERRED,
250 );
251
252 CREATE TABLE config.hard_due_date_values (
253     id                  SERIAL      PRIMARY KEY,
254     hard_due_date       INT         NOT NULL REFERENCES config.hard_due_date (id)
255                                     DEFERRABLE INITIALLY DEFERRED,
256     ceiling_date        TIMESTAMPTZ NOT NULL,
257     active_date         TIMESTAMPTZ NOT NULL
258 );
259
260 CREATE OR REPLACE FUNCTION config.update_hard_due_dates () RETURNS INT AS $func$
261 DECLARE
262     temp_value  config.hard_due_date_values%ROWTYPE;
263     updated     INT := 0;
264 BEGIN
265     FOR temp_value IN
266       SELECT  DISTINCT ON (hard_due_date) *
267         FROM  config.hard_due_date_values
268         WHERE active_date <= NOW() -- We've passed (or are at) the rollover time
269         ORDER BY hard_due_date, active_date DESC -- Latest (nearest to us) active time
270    LOOP
271         UPDATE  config.hard_due_date
272           SET   ceiling_date = temp_value.ceiling_date
273           WHERE id = temp_value.hard_due_date
274                 AND ceiling_date <> temp_value.ceiling_date; -- Time is equal if we've already updated the chdd
275
276         IF FOUND THEN
277             updated := updated + 1;
278         END IF;
279     END LOOP;
280
281     RETURN updated;
282 END;
283 $func$ LANGUAGE plpgsql;
284
285 CREATE TABLE config.rule_max_fine (
286     id          SERIAL          PRIMARY KEY,
287     name        TEXT            NOT NULL UNIQUE CHECK ( name ~ E'^\\w+$' ),
288     amount      NUMERIC(6,2)    NOT NULL,
289     is_percent  BOOL            NOT NULL DEFAULT FALSE
290 );
291 COMMENT ON TABLE config.rule_max_fine IS $$
292 Circulation Max Fine rules
293
294 Each circulation is given a maximum fine based on one of
295 these rules.
296 $$;
297
298 CREATE TABLE config.rule_recurring_fine (
299         id                      SERIAL          PRIMARY KEY,
300         name                    TEXT            NOT NULL UNIQUE CHECK ( name ~ E'^\\w+$' ),
301         high                    NUMERIC(6,2)    NOT NULL,
302         normal                  NUMERIC(6,2)    NOT NULL,
303         low                     NUMERIC(6,2)    NOT NULL,
304         recurrence_interval     INTERVAL        NOT NULL DEFAULT '1 day'::INTERVAL,
305     grace_period       INTERVAL         NOT NULL DEFAULT '1 day'::INTERVAL
306 );
307 COMMENT ON TABLE config.rule_recurring_fine IS $$
308 Circulation Recurring Fine rules
309
310 Each circulation is given a recurring fine amount based on one of
311 these rules.  Note that it is recommended to run the fine generator
312 (from cron) at least as frequently as the lowest recurrence interval
313 used by your circulation rules so that accrued fines will be up
314 to date.
315 $$;
316
317
318 CREATE TABLE config.rule_age_hold_protect (
319         id      SERIAL          PRIMARY KEY,
320         name    TEXT            NOT NULL UNIQUE CHECK ( name ~ E'^\\w+$' ),
321         age     INTERVAL        NOT NULL,
322         prox    INT             NOT NULL
323 );
324 COMMENT ON TABLE config.rule_age_hold_protect IS $$
325 Hold Item Age Protection rules
326
327 A hold request can only capture new(ish) items when they are
328 within a particular proximity of the pickup_lib of the request.
329 The proximity ('prox' column) is calculated by counting
330 the number of tree edges between the pickup_lib and either the
331 owning_lib or circ_lib of the copy that could fulfill the hold,
332 as determined by the distance_is_from_owner value of the hold matrix
333 rule controlling the hold request.
334 $$;
335
336 CREATE TABLE config.copy_status (
337         id              SERIAL  PRIMARY KEY,
338         name            TEXT    NOT NULL UNIQUE,
339         holdable        BOOL    NOT NULL DEFAULT FALSE,
340         opac_visible    BOOL    NOT NULL DEFAULT FALSE,
341     copy_active  BOOL    NOT NULL DEFAULT FALSE,
342         restrict_copy_delete BOOL         NOT NULL DEFAULT FALSE
343 );
344 COMMENT ON TABLE config.copy_status IS $$
345 Copy Statuses
346
347 The available copy statuses, and whether a copy in that
348 status is available for hold request capture.  0 (zero) is
349 the only special number in this set, meaning that the item
350 is available for immediate checkout, and is counted as available
351 in the OPAC.
352
353 Statuses with an ID below 100 are not removable, and have special
354 meaning in the code.  Do not change them except to translate the
355 textual name.
356
357 You may add and remove statuses above 100, and these can be used
358 to remove items from normal circulation without affecting the rest
359 of the copy's values or its location.
360 $$;
361
362 CREATE TABLE config.net_access_level (
363         id      SERIAL          PRIMARY KEY,
364         name    TEXT            NOT NULL UNIQUE
365 );
366 COMMENT ON TABLE config.net_access_level IS $$
367 Patron Network Access level
368
369 This will be used to inform the in-library firewall of how much
370 internet access the using patron should be allowed.
371 $$;
372
373
374 CREATE TABLE config.remote_account (
375     id          SERIAL  PRIMARY KEY,
376     label       TEXT    NOT NULL,
377     host        TEXT    NOT NULL,   -- name or IP, :port optional
378     username    TEXT,               -- optional, since we could default to $USER
379     password    TEXT,               -- optional, since we could use SSH keys, or anonymous login.
380     account     TEXT,               -- aka profile or FTP "account" command
381     path        TEXT,               -- aka directory
382     owner       INT     NOT NULL,   -- REFERENCES actor.org_unit (id) DEFERRABLE INITIALLY DEFERRED,
383     last_activity TIMESTAMP WITH TIME ZONE
384 );
385
386 CREATE TABLE config.marc21_rec_type_map (
387     code        TEXT    PRIMARY KEY,
388     type_val    TEXT    NOT NULL,
389     blvl_val    TEXT    NOT NULL
390 );
391
392 CREATE TABLE config.marc21_ff_pos_map (
393     id          SERIAL  PRIMARY KEY,
394     fixed_field TEXT    NOT NULL,
395     tag         TEXT    NOT NULL,
396     rec_type    TEXT    NOT NULL,
397     start_pos   INT     NOT NULL,
398     length      INT     NOT NULL,
399     default_val TEXT    NOT NULL DEFAULT ' '
400 );
401
402 CREATE TABLE config.marc21_physical_characteristic_type_map (
403     ptype_key   TEXT    PRIMARY KEY,
404     label       TEXT    NOT NULL -- I18N
405 );
406
407 CREATE TABLE config.marc21_physical_characteristic_subfield_map (
408     id          SERIAL  PRIMARY KEY,
409     ptype_key   TEXT    NOT NULL REFERENCES config.marc21_physical_characteristic_type_map (ptype_key) ON DELETE CASCADE ON UPDATE CASCADE,
410     subfield    TEXT    NOT NULL,
411     start_pos   INT     NOT NULL,
412     length      INT     NOT NULL,
413     label       TEXT    NOT NULL -- I18N
414 );
415
416 CREATE TABLE config.marc21_physical_characteristic_value_map (
417     id              SERIAL  PRIMARY KEY,
418     value           TEXT    NOT NULL,
419     ptype_subfield  INT     NOT NULL REFERENCES config.marc21_physical_characteristic_subfield_map (id),
420     label           TEXT    NOT NULL -- I18N
421 );
422
423
424 CREATE TABLE config.z3950_source (
425     name                TEXT    PRIMARY KEY,
426     label               TEXT    NOT NULL UNIQUE,
427     host                TEXT    NOT NULL,
428     port                INT     NOT NULL,
429     db                  TEXT    NOT NULL,
430     record_format       TEXT    NOT NULL DEFAULT 'FI',
431     transmission_format TEXT    NOT NULL DEFAULT 'usmarc',
432     auth                BOOL    NOT NULL DEFAULT TRUE,
433     use_perm            INT     -- REFERENCES permission.perm_list (id)
434 );
435
436 COMMENT ON TABLE config.z3950_source IS $$
437 Z39.50 Sources
438
439 Each row in this table represents a database searchable via Z39.50.
440 $$;
441
442 COMMENT ON COLUMN config.z3950_source.record_format IS $$
443 Z39.50 element set.
444 $$;
445
446 COMMENT ON COLUMN config.z3950_source.transmission_format IS $$
447 Z39.50 preferred record syntax..
448 $$;
449
450 COMMENT ON COLUMN config.z3950_source.use_perm IS $$
451 If set, this permission is required for the source to be listed in the staff
452 client Z39.50 interface.  Similar to permission.grp_tree.application_perm.
453 $$;
454
455 CREATE TABLE config.z3950_attr (
456     id          SERIAL  PRIMARY KEY,
457     source      TEXT    NOT NULL REFERENCES config.z3950_source (name) DEFERRABLE INITIALLY DEFERRED,
458     name        TEXT    NOT NULL,
459     label       TEXT    NOT NULL,
460     code        INT     NOT NULL,
461     format      INT     NOT NULL,
462     truncation  INT     NOT NULL DEFAULT 0,
463     CONSTRAINT z_code_format_once_per_source UNIQUE (code,format,source)
464 );
465
466 CREATE TABLE config.i18n_locale (
467     code        TEXT    PRIMARY KEY,
468     marc_code   TEXT    NOT NULL, -- should exist in config.coded_value_map WHERE ctype = 'item_lang'
469     name        TEXT    UNIQUE NOT NULL,
470     description TEXT
471 );
472
473 CREATE TABLE config.i18n_core (
474     id              BIGSERIAL   PRIMARY KEY,
475     fq_field        TEXT        NOT NULL,
476     identity_value  TEXT        NOT NULL,
477     translation     TEXT        NOT NULL    REFERENCES config.i18n_locale (code) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED,
478     string          TEXT        NOT NULL
479 );
480
481 CREATE UNIQUE INDEX i18n_identity ON config.i18n_core (fq_field,identity_value,translation);
482
483 CREATE OR REPLACE FUNCTION oils_i18n_update_apply(old_ident TEXT, new_ident TEXT, hint TEXT) RETURNS VOID AS $_$
484 BEGIN
485
486     EXECUTE $$
487         UPDATE  config.i18n_core
488           SET   identity_value = $$ || quote_literal(new_ident) || $$ 
489           WHERE fq_field LIKE '$$ || hint || $$.%' 
490                 AND identity_value = $$ || quote_literal(old_ident) || $$::TEXT;$$;
491
492     RETURN;
493
494 END;
495 $_$ LANGUAGE PLPGSQL;
496
497 CREATE OR REPLACE FUNCTION oils_i18n_id_tracking(/* hint */) RETURNS TRIGGER AS $_$
498 BEGIN
499     PERFORM oils_i18n_update_apply( OLD.id::TEXT, NEW.id::TEXT, TG_ARGV[0]::TEXT );
500     RETURN NEW;
501 END;
502 $_$ LANGUAGE PLPGSQL;
503
504 CREATE OR REPLACE FUNCTION oils_i18n_code_tracking(/* hint */) RETURNS TRIGGER AS $_$
505 BEGIN
506     PERFORM oils_i18n_update_apply( OLD.code::TEXT, NEW.code::TEXT, TG_ARGV[0]::TEXT );
507     RETURN NEW;
508 END;
509 $_$ LANGUAGE PLPGSQL;
510
511 CREATE TABLE config.billing_type (
512     id              SERIAL  PRIMARY KEY,
513     name            TEXT    NOT NULL,
514     owner           INT     NOT NULL, -- REFERENCES actor.org_unit (id)
515     default_price   NUMERIC(6,2),
516     CONSTRAINT billing_type_once_per_lib UNIQUE (name, owner)
517 );
518
519 CREATE TABLE config.settings_group (
520     name    TEXT PRIMARY KEY,
521     label   TEXT UNIQUE NOT NULL -- I18N
522 );
523
524 CREATE TABLE config.org_unit_setting_type (
525     name            TEXT    PRIMARY KEY,
526     label           TEXT    UNIQUE NOT NULL,
527     grp             TEXT    REFERENCES config.settings_group (name),
528     description     TEXT,
529     datatype        TEXT    NOT NULL DEFAULT 'string',
530     fm_class        TEXT,
531     view_perm       INT,
532     update_perm     INT,
533     --
534     -- define valid datatypes
535     --
536     CONSTRAINT coust_valid_datatype CHECK ( datatype IN
537     ( 'bool', 'integer', 'float', 'currency', 'interval',
538       'date', 'string', 'object', 'array', 'link' ) ),
539     --
540     -- fm_class is meaningful only for 'link' datatype
541     --
542     CONSTRAINT coust_no_empty_link CHECK
543     ( ( datatype =  'link' AND fm_class IS NOT NULL ) OR
544       ( datatype <> 'link' AND fm_class IS NULL ) )
545 );
546
547 CREATE TABLE config.usr_setting_type (
548
549     name TEXT PRIMARY KEY,
550     opac_visible BOOL NOT NULL DEFAULT FALSE,
551     label TEXT UNIQUE NOT NULL,
552     description TEXT,
553     grp             TEXT    REFERENCES config.settings_group (name),
554     datatype TEXT NOT NULL DEFAULT 'string',
555     fm_class TEXT,
556
557     --
558     -- define valid datatypes
559     --
560     CONSTRAINT coust_valid_datatype CHECK ( datatype IN
561     ( 'bool', 'integer', 'float', 'currency', 'interval',
562         'date', 'string', 'object', 'array', 'link' ) ),
563
564     --
565     -- fm_class is meaningful only for 'link' datatype
566     --
567     CONSTRAINT coust_no_empty_link CHECK
568     ( ( datatype = 'link' AND fm_class IS NOT NULL ) OR
569         ( datatype <> 'link' AND fm_class IS NULL ) )
570
571 );
572
573 -- Some handy functions, based on existing ones, to provide optional ingest normalization
574
575 CREATE OR REPLACE FUNCTION public.left_trunc( TEXT, INT ) RETURNS TEXT AS $func$
576         SELECT SUBSTRING($1,$2);
577 $func$ LANGUAGE SQL STRICT IMMUTABLE;
578
579 CREATE OR REPLACE FUNCTION public.right_trunc( TEXT, INT ) RETURNS TEXT AS $func$
580         SELECT SUBSTRING($1,1,$2);
581 $func$ LANGUAGE SQL STRICT IMMUTABLE;
582
583 CREATE OR REPLACE FUNCTION public.split_date_range( TEXT ) RETURNS TEXT AS $func$
584         SELECT REGEXP_REPLACE( $1, E'(\\d{4})-(\\d{4})', E'\\1 \\2', 'g' );
585 $func$ LANGUAGE SQL STRICT IMMUTABLE;
586
587 CREATE OR REPLACE FUNCTION public.approximate_date( TEXT, TEXT ) RETURNS TEXT AS $func$
588         SELECT REGEXP_REPLACE( $1, E'\\D', $2, 'g' );
589 $func$ LANGUAGE SQL STRICT IMMUTABLE;
590
591 CREATE OR REPLACE FUNCTION public.approximate_low_date( TEXT ) RETURNS TEXT AS $func$
592         SELECT approximate_date( $1, '0');
593 $func$ LANGUAGE SQL STRICT IMMUTABLE;
594
595 CREATE OR REPLACE FUNCTION public.approximate_high_date( TEXT ) RETURNS TEXT AS $func$
596         SELECT approximate_date( $1, '9');
597 $func$ LANGUAGE SQL STRICT IMMUTABLE;
598
599 CREATE OR REPLACE FUNCTION public.content_or_null( TEXT ) RETURNS TEXT AS $func$
600         SELECT CASE WHEN $1 ~ E'^\\s*$' THEN NULL ELSE $1 END
601 $func$ LANGUAGE SQL STRICT IMMUTABLE;
602
603 CREATE OR REPLACE FUNCTION public.integer_or_null( TEXT ) RETURNS TEXT AS $func$
604         SELECT CASE WHEN $1 ~ E'^\\d+$' THEN $1 ELSE NULL END
605 $func$ LANGUAGE SQL STRICT IMMUTABLE;
606
607 CREATE OR REPLACE FUNCTION public.force_to_isbn13( TEXT ) RETURNS TEXT AS $func$
608     use Business::ISBN;
609     use strict;
610     use warnings;
611
612     # Find the first ISBN, force it to ISBN13 and return it
613
614     my $input = shift;
615
616     foreach my $word (split(/\s/, $input)) {
617         my $isbn = Business::ISBN->new($word);
618
619         # First check the checksum; if it is not valid, fix it and add the original
620         # bad-checksum ISBN to the output
621         if ($isbn && $isbn->is_valid_checksum() == Business::ISBN::BAD_CHECKSUM) {
622             $isbn->fix_checksum();
623         }
624
625         # If we now have a valid ISBN, force it to ISBN13 and return it
626         return $isbn->as_isbn13->isbn if ($isbn && $isbn->is_valid());
627     }
628     return undef;
629 $func$ LANGUAGE PLPERLU;
630
631 COMMENT ON FUNCTION public.force_to_isbn13(TEXT) IS $$
632 Inspired by translate_isbn1013
633
634 The force_to_isbn13 function takes an input ISBN and returns the ISBN13
635 version without hypens and with a repaired checksum if the checksum was bad
636 $$;
637
638
639 CREATE OR REPLACE FUNCTION public.translate_isbn1013( TEXT ) RETURNS TEXT AS $func$
640     use Business::ISBN;
641     use strict;
642     use warnings;
643
644     # For each ISBN found in a single string containing a set of ISBNs:
645     #   * Normalize an incoming ISBN to have the correct checksum and no hyphens
646     #   * Convert an incoming ISBN10 or ISBN13 to its counterpart and return
647
648     my $input = shift;
649     my $output = '';
650
651     foreach my $word (split(/\s/, $input)) {
652         my $isbn = Business::ISBN->new($word);
653
654         # First check the checksum; if it is not valid, fix it and add the original
655         # bad-checksum ISBN to the output
656         if ($isbn && $isbn->is_valid_checksum() == Business::ISBN::BAD_CHECKSUM) {
657             $output .= $isbn->isbn() . " ";
658             $isbn->fix_checksum();
659         }
660
661         # If we now have a valid ISBN, convert it to its counterpart ISBN10/ISBN13
662         # and add the normalized original ISBN to the output
663         if ($isbn && $isbn->is_valid()) {
664             my $isbn_xlated = ($isbn->type eq "ISBN13") ? $isbn->as_isbn10 : $isbn->as_isbn13;
665             $output .= $isbn->isbn . " ";
666
667             # If we successfully converted the ISBN to its counterpart, add the
668             # converted ISBN to the output as well
669             $output .= ($isbn_xlated->isbn . " ") if ($isbn_xlated);
670         }
671     }
672     return $output if $output;
673
674     # If there were no valid ISBNs, just return the raw input
675     return $input;
676 $func$ LANGUAGE PLPERLU;
677
678 COMMENT ON FUNCTION public.translate_isbn1013(TEXT) IS $$
679 The translate_isbn1013 function takes an input ISBN and returns the
680 following in a single space-delimited string if the input ISBN is valid:
681   - The normalized input ISBN (hyphens stripped)
682   - The normalized input ISBN with a fixed checksum if the checksum was bad
683   - The ISBN converted to its ISBN10 or ISBN13 counterpart, if possible
684 $$;
685
686 -- And ... a table in which to register them
687
688 CREATE TABLE config.index_normalizer (
689         id              SERIAL  PRIMARY KEY,
690         name            TEXT    UNIQUE NOT NULL,
691         description     TEXT,
692         func            TEXT    NOT NULL,
693         param_count     INT     NOT NULL DEFAULT 0
694 );
695
696 CREATE TABLE config.metabib_field_index_norm_map (
697         id      SERIAL  PRIMARY KEY,
698         field   INT     NOT NULL REFERENCES config.metabib_field (id) ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED,
699         norm    INT     NOT NULL REFERENCES config.index_normalizer (id) ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED,
700         params  TEXT,
701         pos     INT     NOT NULL DEFAULT 0
702 );
703
704 CREATE TABLE config.record_attr_definition (
705     name        TEXT    PRIMARY KEY,
706     label       TEXT    NOT NULL, -- I18N
707     description TEXT,
708     filter      BOOL    NOT NULL DEFAULT TRUE,  -- becomes QP filter if true
709     sorter      BOOL    NOT NULL DEFAULT FALSE, -- becomes QP sort() axis if true
710
711 -- For pre-extracted fields. Takes the first occurance, uses naive subfield ordering
712     tag         TEXT, -- LIKE format
713     sf_list     TEXT, -- pile-o-values, like 'abcd' for a and b and c and d
714
715 -- This is used for both tag/sf and xpath entries
716     joiner      TEXT,
717
718 -- For xpath-extracted attrs
719     xpath       TEXT,
720     format      TEXT    REFERENCES config.xml_transform (name) ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED,
721     start_pos   INT,
722     string_len  INT,
723
724 -- For fixed fields
725     fixed_field TEXT, -- should exist in config.marc21_ff_pos_map.fixed_field
726
727 -- For phys-char fields
728     phys_char_sf    INT REFERENCES config.marc21_physical_characteristic_subfield_map (id)
729 );
730
731 CREATE TABLE config.record_attr_index_norm_map (
732     id      SERIAL  PRIMARY KEY,
733     attr    TEXT    NOT NULL REFERENCES config.record_attr_definition (name) ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED,
734     norm    INT     NOT NULL REFERENCES config.index_normalizer (id) ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED,
735     params  TEXT,
736     pos     INT     NOT NULL DEFAULT 0
737 );
738
739 CREATE TABLE config.coded_value_map (
740     id          SERIAL  PRIMARY KEY,
741     ctype       TEXT    NOT NULL REFERENCES config.record_attr_definition (name) ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED,
742     code        TEXT    NOT NULL,
743     value       TEXT    NOT NULL,
744     description TEXT
745 );
746
747 CREATE VIEW config.language_map AS SELECT code, value FROM config.coded_value_map WHERE ctype = 'item_lang';
748 CREATE VIEW config.bib_level_map AS SELECT code, value FROM config.coded_value_map WHERE ctype = 'bib_level';
749 CREATE VIEW config.item_form_map AS SELECT code, value FROM config.coded_value_map WHERE ctype = 'item_form';
750 CREATE VIEW config.item_type_map AS SELECT code, value FROM config.coded_value_map WHERE ctype = 'item_type';
751 CREATE VIEW config.lit_form_map AS SELECT code, value, description FROM config.coded_value_map WHERE ctype = 'lit_form';
752 CREATE VIEW config.audience_map AS SELECT code, value, description FROM config.coded_value_map WHERE ctype = 'audience';
753 CREATE VIEW config.videorecording_format_map AS SELECT code, value FROM config.coded_value_map WHERE ctype = 'vr_format';
754
755 CREATE OR REPLACE FUNCTION oils_tsearch2 () RETURNS TRIGGER AS $$
756 DECLARE
757     normalizer      RECORD;
758     value           TEXT := '';
759 BEGIN
760
761     value := NEW.value;
762
763     IF TG_TABLE_NAME::TEXT ~ 'field_entry$' THEN
764         FOR normalizer IN
765             SELECT  n.func AS func,
766                     n.param_count AS param_count,
767                     m.params AS params
768               FROM  config.index_normalizer n
769                     JOIN config.metabib_field_index_norm_map m ON (m.norm = n.id)
770               WHERE field = NEW.field AND m.pos < 0
771               ORDER BY m.pos LOOP
772                 EXECUTE 'SELECT ' || normalizer.func || '(' ||
773                     quote_literal( value ) ||
774                     CASE
775                         WHEN normalizer.param_count > 0
776                             THEN ',' || REPLACE(REPLACE(BTRIM(normalizer.params,'[]'),E'\'',E'\\\''),E'"',E'\'')
777                             ELSE ''
778                         END ||
779                     ')' INTO value;
780
781         END LOOP;
782
783         NEW.value := value;
784     END IF;
785
786     IF NEW.index_vector = ''::tsvector THEN
787         RETURN NEW;
788     END IF;
789
790     IF TG_TABLE_NAME::TEXT ~ 'field_entry$' THEN
791         FOR normalizer IN
792             SELECT  n.func AS func,
793                     n.param_count AS param_count,
794                     m.params AS params
795               FROM  config.index_normalizer n
796                     JOIN config.metabib_field_index_norm_map m ON (m.norm = n.id)
797               WHERE field = NEW.field AND m.pos >= 0
798               ORDER BY m.pos LOOP
799                 EXECUTE 'SELECT ' || normalizer.func || '(' ||
800                     quote_literal( value ) ||
801                     CASE
802                         WHEN normalizer.param_count > 0
803                             THEN ',' || REPLACE(REPLACE(BTRIM(normalizer.params,'[]'),E'\'',E'\\\''),E'"',E'\'')
804                             ELSE ''
805                         END ||
806                     ')' INTO value;
807
808         END LOOP;
809     END IF;
810
811     IF TG_TABLE_NAME::TEXT ~ 'browse_entry$' THEN
812         value :=  ARRAY_TO_STRING(
813             evergreen.regexp_split_to_array(value, E'\\W+'), ' '
814         );
815         value := public.search_normalize(value);
816     END IF;
817
818     NEW.index_vector = to_tsvector((TG_ARGV[0])::regconfig, value);
819
820     RETURN NEW;
821 END;
822 $$ LANGUAGE PLPGSQL;
823
824 -- List applied db patches that are deprecated by (and block the application of) my_db_patch
825 CREATE OR REPLACE FUNCTION evergreen.upgrade_list_applied_deprecates ( my_db_patch TEXT ) RETURNS SETOF evergreen.patch AS $$
826     SELECT  DISTINCT l.version
827       FROM  config.upgrade_log l
828             JOIN config.db_patch_dependencies d ON (l.version::TEXT[] && d.deprecates)
829       WHERE d.db_patch = $1
830 $$ LANGUAGE SQL;
831
832 -- List applied db patches that are superseded by (and block the application of) my_db_patch
833 CREATE OR REPLACE FUNCTION evergreen.upgrade_list_applied_supersedes ( my_db_patch TEXT ) RETURNS SETOF evergreen.patch AS $$
834     SELECT  DISTINCT l.version
835       FROM  config.upgrade_log l
836             JOIN config.db_patch_dependencies d ON (l.version::TEXT[] && d.supersedes)
837       WHERE d.db_patch = $1
838 $$ LANGUAGE SQL;
839
840 -- List applied db patches that deprecates (and block the application of) my_db_patch
841 CREATE FUNCTION evergreen.upgrade_list_applied_deprecated ( my_db_patch TEXT ) RETURNS SETOF TEXT AS $$
842     SELECT  db_patch
843       FROM  config.db_patch_dependencies
844       WHERE ARRAY[$1]::TEXT[] && deprecates
845 $$ LANGUAGE SQL;
846
847 -- List applied db patches that supersedes (and block the application of) my_db_patch
848 CREATE FUNCTION evergreen.upgrade_list_applied_superseded ( my_db_patch TEXT ) RETURNS SETOF TEXT AS $$
849     SELECT  db_patch
850       FROM  config.db_patch_dependencies
851       WHERE ARRAY[$1]::TEXT[] && supersedes
852 $$ LANGUAGE SQL;
853
854 -- Make sure that no deprecated or superseded db patches are currently applied
855 CREATE OR REPLACE FUNCTION evergreen.upgrade_verify_no_dep_conflicts ( my_db_patch TEXT ) RETURNS BOOL AS $$
856     SELECT  COUNT(*) = 0
857       FROM  (SELECT * FROM evergreen.upgrade_list_applied_deprecates( $1 )
858                 UNION
859              SELECT * FROM evergreen.upgrade_list_applied_supersedes( $1 )
860                 UNION
861              SELECT * FROM evergreen.upgrade_list_applied_deprecated( $1 )
862                 UNION
863              SELECT * FROM evergreen.upgrade_list_applied_superseded( $1 ))x
864 $$ LANGUAGE SQL;
865
866 -- Raise an exception if there are, in fact, dep/sup conflict
867 CREATE OR REPLACE FUNCTION evergreen.upgrade_deps_block_check ( my_db_patch TEXT, my_applied_to TEXT ) RETURNS BOOL AS $$
868 DECLARE 
869     deprecates TEXT;
870     supersedes TEXT;
871 BEGIN
872     IF NOT evergreen.upgrade_verify_no_dep_conflicts( my_db_patch ) THEN
873         SELECT  STRING_AGG(patch, ', ') INTO deprecates FROM evergreen.upgrade_list_applied_deprecates(my_db_patch);
874         SELECT  STRING_AGG(patch, ', ') INTO supersedes FROM evergreen.upgrade_list_applied_supersedes(my_db_patch);
875         RAISE EXCEPTION '
876 Upgrade script % can not be applied:
877   applied deprecated scripts %
878   applied superseded scripts %
879   deprecated by %
880   superseded by %',
881             my_db_patch,
882             ARRAY_AGG(evergreen.upgrade_list_applied_deprecates(my_db_patch)),
883             ARRAY_AGG(evergreen.upgrade_list_applied_supersedes(my_db_patch)),
884             evergreen.upgrade_list_applied_deprecated(my_db_patch),
885             evergreen.upgrade_list_applied_superseded(my_db_patch);
886     END IF;
887
888     INSERT INTO config.upgrade_log (version, applied_to) VALUES (my_db_patch, my_applied_to);
889     RETURN TRUE;
890 END;
891 $$ LANGUAGE PLPGSQL;
892
893 CREATE TABLE config.barcode_completion (
894     id          SERIAL PRIMARY KEY,
895     active      BOOL NOT NULL DEFAULT true,
896     org_unit    INT NOT NULL, -- REFERENCES actor.org_unit(id) DEFERRABLE INITIALLY DEFERRED,
897     prefix      TEXT,
898     suffix      TEXT,
899     length      INT NOT NULL DEFAULT 0,
900     padding     TEXT,
901     padding_end BOOL NOT NULL DEFAULT false,
902     asset       BOOL NOT NULL DEFAULT true,
903     actor       BOOL NOT NULL DEFAULT true
904 );
905
906 CREATE TYPE evergreen.barcode_set AS (type TEXT, id BIGINT, barcode TEXT);
907
908 -- Add support for logging, only keep the most recent five rows for each category. 
909
910
911 CREATE TABLE config.org_unit_setting_type_log (
912     id              BIGSERIAL   PRIMARY KEY,
913     date_applied    TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
914     org             INT,   --REFERENCES actor.org_unit (id),
915     original_value  TEXT,
916     new_value       TEXT,
917     field_name      TEXT      REFERENCES config.org_unit_setting_type (name)
918 );
919
920 COMMENT ON TABLE config.org_unit_setting_type_log IS $$
921 Org Unit setting Logs
922
923 This table contains the most recent changes to each setting 
924 in actor.org_unit_setting, allowing for mistakes to be undone.
925 This is NOT meant to be an auditor, but rather an undo/redo.
926 $$;
927
928 CREATE OR REPLACE FUNCTION limit_oustl() RETURNS TRIGGER AS $oustl_limit$
929     BEGIN
930         -- Only keeps the most recent five settings changes.
931         DELETE FROM config.org_unit_setting_type_log WHERE field_name = NEW.field_name AND date_applied NOT IN 
932         (SELECT date_applied FROM config.org_unit_setting_type_log WHERE field_name = NEW.field_name ORDER BY date_applied DESC LIMIT 4);
933         
934         IF (TG_OP = 'UPDATE') THEN
935             RETURN NEW;
936         ELSIF (TG_OP = 'INSERT') THEN
937             RETURN NEW;
938         END IF;
939         RETURN NULL;
940     END;
941 $oustl_limit$ LANGUAGE plpgsql;
942
943 CREATE TRIGGER limit_logs_oust
944     BEFORE INSERT OR UPDATE ON config.org_unit_setting_type_log
945     FOR EACH ROW EXECUTE PROCEDURE limit_oustl();
946
947 CREATE TABLE config.sms_carrier (
948     id              SERIAL PRIMARY KEY,
949     region          TEXT,
950     name            TEXT,
951     email_gateway   TEXT,
952     active          BOOLEAN DEFAULT TRUE
953 );
954
955 CREATE TYPE config.usr_activity_group AS ENUM ('authen','authz','circ','hold','search');
956
957 CREATE TABLE config.usr_activity_type (
958     id          SERIAL                      PRIMARY KEY, 
959     ewho        TEXT,
960     ewhat       TEXT,
961     ehow        TEXT,
962     label       TEXT                        NOT NULL, -- i18n
963     egroup      config.usr_activity_group   NOT NULL,
964     enabled     BOOL                        NOT NULL DEFAULT TRUE,
965     transient   BOOL                        NOT NULL DEFAULT FALSE,
966     CONSTRAINT  one_of_wwh CHECK (COALESCE(ewho,ewhat,ehow) IS NOT NULL)
967 );
968
969 CREATE UNIQUE INDEX unique_wwh ON config.usr_activity_type 
970     (COALESCE(ewho,''), COALESCE (ewhat,''), COALESCE(ehow,''));
971
972
973 COMMIT;