]> git.evergreen-ils.org Git - working/Evergreen.git/blob - Open-ILS/src/sql/Pg/040.schema.asset.sql
Fix Dewey call number sorting
[working/Evergreen.git] / Open-ILS / src / sql / Pg / 040.schema.asset.sql
1 /*
2  * Copyright (C) 2004-2008  Georgia Public Library Service
3  * Copyright (C) 2007-2008  Equinox Software, Inc.
4  * Mike Rylander <miker@esilibrary.com> 
5  *
6  * This program is free software; you can redistribute it and/or
7  * modify it under the terms of the GNU General Public License
8  * as published by the Free Software Foundation; either version 2
9  * of the License, or (at your option) any later version.
10  *
11  * This program is distributed in the hope that it will be useful,
12  * but WITHOUT ANY WARRANTY; without even the implied warranty of
13  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14  * GNU General Public License for more details.
15  *
16  */
17
18 DROP SCHEMA IF EXISTS asset CASCADE;
19
20 BEGIN;
21
22 CREATE SCHEMA asset;
23
24 CREATE TABLE asset.copy_location (
25         id              SERIAL  PRIMARY KEY,
26         name            TEXT    NOT NULL,
27         owning_lib      INT     NOT NULL REFERENCES actor.org_unit (id) DEFERRABLE INITIALLY DEFERRED,
28         holdable        BOOL    NOT NULL DEFAULT TRUE,
29         hold_verify     BOOL    NOT NULL DEFAULT FALSE,
30         opac_visible    BOOL    NOT NULL DEFAULT TRUE,
31         circulate       BOOL    NOT NULL DEFAULT TRUE,
32         label_prefix    TEXT,
33         label_suffix    TEXT,
34         CONSTRAINT acl_name_once_per_lib UNIQUE (name, owning_lib)
35 );
36
37 CREATE TABLE asset.copy_location_order
38 (
39         id              SERIAL           PRIMARY KEY,
40         location        INT              NOT NULL
41                                              REFERENCES asset.copy_location
42                                              ON DELETE CASCADE
43                                              DEFERRABLE INITIALLY DEFERRED,
44         org             INT              NOT NULL
45                                              REFERENCES actor.org_unit
46                                              ON DELETE CASCADE
47                                              DEFERRABLE INITIALLY DEFERRED,
48         position        INT              NOT NULL DEFAULT 0,
49         CONSTRAINT acplo_once_per_org UNIQUE ( location, org )
50 );
51
52 CREATE TABLE asset.copy (
53         id              BIGSERIAL                       PRIMARY KEY,
54         circ_lib        INT                             NOT NULL REFERENCES actor.org_unit (id) DEFERRABLE INITIALLY DEFERRED,
55         creator         BIGINT                          NOT NULL,
56         call_number     BIGINT                          NOT NULL,
57         editor          BIGINT                          NOT NULL,
58         create_date     TIMESTAMP WITH TIME ZONE        DEFAULT NOW(),
59         edit_date       TIMESTAMP WITH TIME ZONE        DEFAULT NOW(),
60         copy_number     INT,
61         status          INT                             NOT NULL DEFAULT 0 REFERENCES config.copy_status (id) DEFERRABLE INITIALLY DEFERRED,
62         location        INT                             NOT NULL DEFAULT 1 REFERENCES asset.copy_location (id) DEFERRABLE INITIALLY DEFERRED,
63         loan_duration   INT                             NOT NULL CHECK ( loan_duration IN (1,2,3) ),
64         fine_level      INT                             NOT NULL CHECK ( fine_level IN (1,2,3) ),
65         age_protect     INT,
66         circulate       BOOL                            NOT NULL DEFAULT TRUE,
67         deposit         BOOL                            NOT NULL DEFAULT FALSE,
68         ref             BOOL                            NOT NULL DEFAULT FALSE,
69         holdable        BOOL                            NOT NULL DEFAULT TRUE,
70         deposit_amount  NUMERIC(6,2)                    NOT NULL DEFAULT 0.00,
71         price           NUMERIC(8,2),
72         barcode         TEXT                            NOT NULL,
73         circ_modifier   TEXT,
74         circ_as_type    TEXT,
75         dummy_title     TEXT,
76         dummy_author    TEXT,
77         alert_message   TEXT,
78         opac_visible    BOOL                            NOT NULL DEFAULT TRUE,
79         deleted         BOOL                            NOT NULL DEFAULT FALSE,
80         floating                BOOL                            NOT NULL DEFAULT FALSE,
81         dummy_isbn      TEXT,
82         status_changed_time TIMESTAMP WITH TIME ZONE,
83         active_date TIMESTAMP WITH TIME ZONE,
84         mint_condition      BOOL        NOT NULL DEFAULT TRUE,
85     cost    NUMERIC(8,2)
86 );
87 CREATE UNIQUE INDEX copy_barcode_key ON asset.copy (barcode) WHERE deleted = FALSE OR deleted IS FALSE;
88 CREATE INDEX cp_cn_idx ON asset.copy (call_number);
89 CREATE INDEX cp_avail_cn_idx ON asset.copy (call_number);
90 CREATE INDEX cp_creator_idx  ON asset.copy ( creator );
91 CREATE INDEX cp_editor_idx   ON asset.copy ( editor );
92 CREATE INDEX cp_create_date  ON asset.copy (create_date);
93 CREATE RULE protect_copy_delete AS ON DELETE TO asset.copy DO INSTEAD UPDATE asset.copy SET deleted = TRUE WHERE OLD.id = asset.copy.id;
94
95 CREATE TABLE asset.copy_part_map (
96     id          SERIAL  PRIMARY KEY,
97     target_copy BIGINT  NOT NULL, -- points o asset.copy
98     part        INT     NOT NULL REFERENCES biblio.monograph_part (id) ON DELETE CASCADE
99 );
100 CREATE UNIQUE INDEX copy_part_map_cp_part_idx ON asset.copy_part_map (target_copy, part);
101
102 CREATE TABLE asset.opac_visible_copies (
103   id        BIGSERIAL primary key,
104   copy_id   BIGINT, -- copy id
105   record    BIGINT,
106   circ_lib  INTEGER
107 );
108 COMMENT ON TABLE asset.opac_visible_copies IS $$
109 Materialized view of copies that are visible in the OPAC, used by
110 search.query_parser_fts() to speed up OPAC visibility checks on large
111 databases.  Contents are maintained by a set of triggers.
112 $$;
113 CREATE INDEX opac_visible_copies_idx1 on asset.opac_visible_copies (record, circ_lib);
114 CREATE INDEX opac_visible_copies_copy_id_idx on asset.opac_visible_copies (copy_id);
115 CREATE UNIQUE INDEX opac_visible_copies_once_per_record_idx on asset.opac_visible_copies (copy_id, record);
116
117 CREATE OR REPLACE FUNCTION asset.acp_status_changed()
118 RETURNS TRIGGER AS $$
119 BEGIN
120         IF NEW.status <> OLD.status AND NOT (NEW.status = 0 AND OLD.status = 7) THEN
121         NEW.status_changed_time := now();
122         IF NEW.active_date IS NULL AND NEW.status IN (SELECT id FROM config.copy_status WHERE copy_active = true) THEN
123             NEW.active_date := now();
124         END IF;
125     END IF;
126     RETURN NEW;
127 END;
128 $$ LANGUAGE plpgsql;
129
130 -- Need to check on initial create. Fast adds, manual edit of status at create, etc.
131 CREATE OR REPLACE FUNCTION asset.acp_created()
132 RETURNS TRIGGER AS $$
133 BEGIN
134     IF NEW.active_date IS NULL AND NEW.status IN (SELECT id FROM config.copy_status WHERE copy_active = true) THEN
135         NEW.active_date := now();
136     END IF;
137     IF NEW.status_changed_time IS NULL THEN
138         NEW.status_changed_time := now();
139     END IF;
140     RETURN NEW;
141 END;
142 $$ LANGUAGE plpgsql;
143
144 CREATE TRIGGER acp_status_changed_trig
145     BEFORE UPDATE ON asset.copy
146     FOR EACH ROW EXECUTE PROCEDURE asset.acp_status_changed();
147
148 CREATE TRIGGER acp_created_trig
149     BEFORE INSERT ON asset.copy
150     FOR EACH ROW EXECUTE PROCEDURE asset.acp_created();
151
152 CREATE TABLE asset.stat_cat_sip_fields (
153     field   CHAR(2) PRIMARY KEY,
154     name    TEXT    NOT NULL,
155     one_only BOOL    NOT NULL DEFAULT FALSE
156 );
157 COMMENT ON TABLE asset.stat_cat_sip_fields IS $$
158 Asset Statistical Category SIP Fields
159
160 Contains the list of valid SIP Field identifiers for
161 Statistical Categories.
162 $$;
163
164 CREATE TABLE asset.stat_cat_entry_transparency_map (
165         id                      BIGSERIAL       PRIMARY KEY,
166         stat_cat                INT             NOT NULL, -- needs ON DELETE CASCADE
167         stat_cat_entry          INT             NOT NULL, -- needs ON DELETE CASCADE
168         owning_transparency     INT             NOT NULL, -- needs ON DELETE CASCADE
169         CONSTRAINT scte_once_per_trans UNIQUE (owning_transparency,stat_cat)
170 );
171
172 CREATE TABLE asset.stat_cat (
173         id              SERIAL  PRIMARY KEY,
174         owner           INT     NOT NULL,
175         opac_visible    BOOL    NOT NULL DEFAULT FALSE,
176         name            TEXT    NOT NULL,
177         required        BOOL    NOT NULL DEFAULT FALSE,
178     sip_field   CHAR(2) REFERENCES asset.stat_cat_sip_fields(field) ON UPDATE CASCADE ON DELETE SET NULL DEFERRABLE INITIALLY DEFERRED,
179     sip_format  TEXT,
180         CONSTRAINT sc_once_per_owner UNIQUE (owner,name)
181 );
182
183 CREATE TABLE asset.stat_cat_entry (
184         id              SERIAL  PRIMARY KEY,
185         stat_cat        INT     NOT NULL,
186         owner           INT     NOT NULL,
187         value           TEXT    NOT NULL,
188         CONSTRAINT sce_once_per_owner UNIQUE (stat_cat,owner,value)
189 );
190
191 CREATE TABLE asset.stat_cat_entry_copy_map (
192         id              BIGSERIAL       PRIMARY KEY,
193         stat_cat        INT             NOT NULL,
194         stat_cat_entry  INT             NOT NULL,
195         owning_copy     BIGINT          NOT NULL,
196         CONSTRAINT sce_once_per_copy UNIQUE (owning_copy,stat_cat)
197 );
198 CREATE INDEX scecm_owning_copy_idx ON asset.stat_cat_entry_copy_map(owning_copy);
199
200 CREATE FUNCTION asset.stat_cat_check() RETURNS trigger AS $func$
201 DECLARE
202     sipfield asset.stat_cat_sip_fields%ROWTYPE;
203     use_count INT;
204 BEGIN
205     IF NEW.sip_field IS NOT NULL THEN
206         SELECT INTO sipfield * FROM asset.stat_cat_sip_fields WHERE field = NEW.sip_field;
207         IF sipfield.one_only THEN
208             SELECT INTO use_count count(id) FROM asset.stat_cat WHERE sip_field = NEW.sip_field AND id != NEW.id;
209             IF use_count > 0 THEN
210                 RAISE EXCEPTION 'Sip field cannot be used twice';
211             END IF;
212         END IF;
213     END IF;
214     RETURN NEW;
215 END;
216 $func$ LANGUAGE PLPGSQL;
217
218 CREATE TRIGGER asset_stat_cat_sip_update_trigger
219     BEFORE INSERT OR UPDATE ON asset.stat_cat FOR EACH ROW
220     EXECUTE PROCEDURE asset.stat_cat_check();
221
222 CREATE TABLE asset.copy_note (
223         id              BIGSERIAL                       PRIMARY KEY,
224         owning_copy     BIGINT                          NOT NULL,
225         creator         BIGINT                          NOT NULL,
226         create_date     TIMESTAMP WITH TIME ZONE        DEFAULT NOW(),
227         pub             BOOL                            NOT NULL DEFAULT FALSE,
228         title           TEXT                            NOT NULL,
229         value           TEXT                            NOT NULL
230 );
231 CREATE INDEX asset_copy_note_creator_idx ON asset.copy_note ( creator );
232 CREATE INDEX asset_copy_note_owning_copy_idx ON asset.copy_note ( owning_copy );
233
234 CREATE TABLE asset.uri (
235     id  SERIAL  PRIMARY KEY,
236     href    TEXT    NOT NULL,
237     label   TEXT,
238     use_restriction TEXT,
239     active  BOOL    NOT NULL DEFAULT TRUE
240 );
241
242 CREATE TABLE asset.call_number_class (
243     id             bigserial     PRIMARY KEY,
244     name           TEXT          NOT NULL,
245     normalizer     TEXT          NOT NULL DEFAULT 'asset.normalize_generic',
246     field          TEXT          NOT NULL DEFAULT '050ab,055ab,060ab,070ab,080ab,082ab,086ab,088ab,090,092,096,098,099'
247 );
248 COMMENT ON TABLE asset.call_number_class IS $$
249 Defines the call number normalization database functions in the "normalizer"
250 column and the tag/subfield combinations to use to lookup the call number in
251 the "field" column for a given classification scheme. Tag/subfield combinations
252 are delimited by commas.
253 $$;
254
255 CREATE OR REPLACE FUNCTION asset.label_normalizer() RETURNS TRIGGER AS $func$
256 DECLARE
257     sortkey        TEXT := '';
258 BEGIN
259     sortkey := NEW.label_sortkey;
260
261     IF NEW.label_class IS NULL THEN
262             NEW.label_class := COALESCE(
263             (
264                 SELECT substring(value from E'\\d+')::integer
265                 FROM actor.org_unit_ancestor_setting('cat.default_classification_scheme', NEW.owning_lib)
266             ), 1
267         );
268     END IF;
269
270     EXECUTE 'SELECT ' || acnc.normalizer || '(' || 
271        quote_literal( NEW.label ) || ')'
272        FROM asset.call_number_class acnc
273        WHERE acnc.id = NEW.label_class
274        INTO sortkey;
275     NEW.label_sortkey = sortkey;
276     RETURN NEW;
277 END;
278 $func$ LANGUAGE PLPGSQL;
279
280 CREATE OR REPLACE FUNCTION asset.label_normalizer_generic(TEXT) RETURNS TEXT AS $func$
281     # Created after looking at the Koha C4::ClassSortRoutine::Generic module,
282     # thus could probably be considered a derived work, although nothing was
283     # directly copied - but to err on the safe side of providing attribution:
284     # Copyright (C) 2007 LibLime
285     # Copyright (C) 2011 Equinox Software, Inc (Steve Callendar)
286     # Licensed under the GPL v2 or later
287
288     use strict;
289     use warnings;
290
291     # Converts the callnumber to uppercase
292     # Strips spaces from start and end of the call number
293     # Converts anything other than letters, digits, and periods into spaces
294     # Collapses multiple spaces into a single underscore
295     my $callnum = uc(shift);
296     $callnum =~ s/^\s//g;
297     $callnum =~ s/\s$//g;
298     # NOTE: this previously used underscores, but this caused sorting issues
299     # for the "before" half of page 0 on CN browse, sorting CNs containing a
300     # decimal before "whole number" CNs
301     $callnum =~ s/[^A-Z0-9_.]/ /g;
302     $callnum =~ s/ {2,}/ /g;
303
304     return $callnum;
305 $func$ LANGUAGE PLPERLU;
306
307 CREATE OR REPLACE FUNCTION asset.label_normalizer_dewey(TEXT) RETURNS TEXT AS $func$
308     # Derived from the Koha C4::ClassSortRoutine::Dewey module
309     # Copyright (C) 2007 LibLime
310     # Licensed under the GPL v2 or later
311
312     use strict;
313     use warnings;
314
315     my $init = uc(shift);
316     $init =~ s/^\s+//;
317     $init =~ s/\s+$//;
318     $init =~ s!/!!g;
319     $init =~ s/^([\p{IsAlpha}]+)/$1 /;
320     my @tokens = split /\.|\s+/, $init;
321     my $digit_group_count = 0;
322     for (my $i = 0; $i <= $#tokens; $i++) {
323         if ($tokens[$i] =~ /^\d+$/) {
324             $digit_group_count++;
325             if (2 == $digit_group_count) {
326                 $tokens[$i] = sprintf("%-15.15s", $tokens[$i]);
327                 $tokens[$i] =~ tr/ /0/;
328             }
329         }
330     }
331     # Pad the first digit_group if there was only one
332     if (1 == $digit_group_count) {
333         $tokens[0] .= '_000000000000000'
334     }
335     my $key = join("_", @tokens);
336     $key =~ s/[^\p{IsAlnum}_]//g;
337
338     return $key;
339
340 $func$ LANGUAGE PLPERLU;
341
342 CREATE OR REPLACE FUNCTION asset.label_normalizer_lc(TEXT) RETURNS TEXT AS $func$
343     use strict;
344     use warnings;
345
346     # Library::CallNumber::LC is currently hosted at http://code.google.com/p/library-callnumber-lc/
347     # The author hopes to upload it to CPAN some day, which would make our lives easier
348     use Library::CallNumber::LC;
349
350     my $callnum = Library::CallNumber::LC->new(shift);
351     return $callnum->normalize();
352
353 $func$ LANGUAGE PLPERLU;
354
355 INSERT INTO asset.call_number_class (name, normalizer, field) VALUES 
356     ('Generic', 'asset.label_normalizer_generic', '050ab,055ab,060ab,070ab,080ab,082ab,086ab,088ab,090,092,096,098,099'),
357     ('Dewey (DDC)', 'asset.label_normalizer_dewey', '080ab,082ab,092abef'),
358     ('Library of Congress (LC)', 'asset.label_normalizer_lc', '050ab,055ab,090abef')
359 ;
360
361 CREATE OR REPLACE FUNCTION asset.normalize_affix_sortkey () RETURNS TRIGGER AS $$
362 BEGIN
363     NEW.label_sortkey := REGEXP_REPLACE(
364         evergreen.lpad_number_substrings(
365             naco_normalize(NEW.label),
366             '0',
367             10
368         ),
369         E'\\s+',
370         '',
371         'g'
372     );
373     RETURN NEW;
374 END;
375 $$ LANGUAGE PLPGSQL;
376
377 CREATE TABLE asset.call_number_prefix (
378         id                      SERIAL   PRIMARY KEY,
379         owning_lib          INT                 NOT NULL REFERENCES actor.org_unit (id),
380         label               TEXT                NOT NULL, -- i18n
381         label_sortkey   TEXT
382 );
383 CREATE TRIGGER prefix_normalize_tgr BEFORE INSERT OR UPDATE ON asset.call_number_prefix FOR EACH ROW EXECUTE PROCEDURE asset.normalize_affix_sortkey();
384 CREATE UNIQUE INDEX asset_call_number_prefix_once_per_lib ON asset.call_number_prefix (label, owning_lib);
385 CREATE INDEX asset_call_number_prefix_sortkey_idx ON asset.call_number_prefix (label_sortkey);
386
387 CREATE TABLE asset.call_number_suffix (
388         id                      SERIAL   PRIMARY KEY,
389         owning_lib          INT                 NOT NULL REFERENCES actor.org_unit (id),
390         label               TEXT                NOT NULL, -- i18n
391         label_sortkey   TEXT
392 );
393 CREATE TRIGGER suffix_normalize_tgr BEFORE INSERT OR UPDATE ON asset.call_number_suffix FOR EACH ROW EXECUTE PROCEDURE asset.normalize_affix_sortkey();
394 CREATE UNIQUE INDEX asset_call_number_suffix_once_per_lib ON asset.call_number_suffix (label, owning_lib);
395 CREATE INDEX asset_call_number_suffix_sortkey_idx ON asset.call_number_suffix (label_sortkey);
396
397 CREATE TABLE asset.call_number (
398         id              bigserial PRIMARY KEY,
399         creator         BIGINT                          NOT NULL,
400         create_date     TIMESTAMP WITH TIME ZONE        DEFAULT NOW(),
401         editor          BIGINT                          NOT NULL,
402         edit_date       TIMESTAMP WITH TIME ZONE        DEFAULT NOW(),
403         record          bigint                          NOT NULL,
404         owning_lib      INT                                 NOT NULL,
405         label           TEXT                            NOT NULL,
406         deleted         BOOL                            NOT NULL DEFAULT FALSE,
407         prefix          INT                                 NOT NULL DEFAULT -1 REFERENCES asset.call_number_prefix(id) DEFERRABLE INITIALLY DEFERRED,
408         suffix          INT                                 NOT NULL DEFAULT -1 REFERENCES asset.call_number_suffix(id) DEFERRABLE INITIALLY DEFERRED,
409         label_class     BIGINT                          NOT NULL
410                                                         REFERENCES asset.call_number_class(id)
411                                                         DEFERRABLE INITIALLY DEFERRED,
412         label_sortkey   TEXT
413 );
414 CREATE INDEX asset_call_number_record_idx ON asset.call_number (record);
415 CREATE INDEX asset_call_number_creator_idx ON asset.call_number (creator);
416 CREATE INDEX asset_call_number_editor_idx ON asset.call_number (editor);
417 CREATE INDEX asset_call_number_dewey_idx ON asset.call_number (public.call_number_dewey(label));
418 CREATE INDEX asset_call_number_upper_label_id_owning_lib_idx ON asset.call_number (oils_text_as_bytea(label),id,owning_lib);
419 CREATE INDEX asset_call_number_label_sortkey ON asset.call_number(oils_text_as_bytea(label_sortkey));
420 CREATE UNIQUE INDEX asset_call_number_label_once_per_lib ON asset.call_number (record, owning_lib, label, prefix, suffix) WHERE deleted = FALSE OR deleted IS FALSE;
421 CREATE INDEX asset_call_number_label_sortkey_browse ON asset.call_number(oils_text_as_bytea(label_sortkey), oils_text_as_bytea(label), id, owning_lib) WHERE deleted IS FALSE OR deleted = FALSE;
422 CREATE RULE protect_cn_delete AS ON DELETE TO asset.call_number DO INSTEAD UPDATE asset.call_number SET deleted = TRUE WHERE OLD.id = asset.call_number.id;
423 CREATE TRIGGER asset_label_sortkey_trigger
424     BEFORE UPDATE OR INSERT ON asset.call_number
425     FOR EACH ROW EXECUTE PROCEDURE asset.label_normalizer();
426
427 CREATE TABLE asset.uri_call_number_map (
428     id          BIGSERIAL   PRIMARY KEY,
429     uri         INT         NOT NULL REFERENCES asset.uri (id),
430     call_number INT         NOT NULL REFERENCES asset.call_number (id),
431     CONSTRAINT uri_cn_once UNIQUE (uri,call_number)
432 );
433 CREATE INDEX asset_uri_call_number_map_cn_idx ON asset.uri_call_number_map (call_number);
434
435 CREATE TABLE asset.call_number_note (
436         id              BIGSERIAL                       PRIMARY KEY,
437         call_number     BIGINT                          NOT NULL,
438         creator         BIGINT                          NOT NULL,
439         create_date     TIMESTAMP WITH TIME ZONE        DEFAULT NOW(),
440         pub             BOOL                            NOT NULL DEFAULT FALSE,
441         title           TEXT                            NOT NULL,
442         value           TEXT                            NOT NULL
443 );
444 CREATE INDEX asset_call_number_note_creator_idx ON asset.call_number_note ( creator );
445
446 CREATE TABLE asset.copy_template (
447         id             SERIAL   PRIMARY KEY,
448         owning_lib     INT      NOT NULL
449                                 REFERENCES actor.org_unit (id)
450                                 DEFERRABLE INITIALLY DEFERRED,
451         creator        BIGINT   NOT NULL
452                                 REFERENCES actor.usr (id)
453                                 DEFERRABLE INITIALLY DEFERRED,
454         editor         BIGINT   NOT NULL
455                                 REFERENCES actor.usr (id)
456                                 DEFERRABLE INITIALLY DEFERRED,
457         create_date    TIMESTAMP WITH TIME ZONE    DEFAULT NOW(),
458         edit_date      TIMESTAMP WITH TIME ZONE    DEFAULT NOW(),
459         name           TEXT     NOT NULL,
460         -- columns above this point are attributes of the template itself
461         -- columns after this point are attributes of the copy this template modifies/creates
462         circ_lib       INT      REFERENCES actor.org_unit (id)
463                                 DEFERRABLE INITIALLY DEFERRED,
464         status         INT      REFERENCES config.copy_status (id)
465                                 DEFERRABLE INITIALLY DEFERRED,
466         location       INT      REFERENCES asset.copy_location (id)
467                                 DEFERRABLE INITIALLY DEFERRED,
468         loan_duration  INT      CONSTRAINT valid_loan_duration CHECK (
469                                     loan_duration IS NULL OR loan_duration IN (1,2,3)),
470         fine_level     INT      CONSTRAINT valid_fine_level CHECK (
471                                     fine_level IS NULL OR loan_duration IN (1,2,3)),
472         age_protect    INT,
473         circulate      BOOL,
474         deposit        BOOL,
475         ref            BOOL,
476         holdable       BOOL,
477         deposit_amount NUMERIC(6,2),
478         price          NUMERIC(8,2),
479         circ_modifier  TEXT,
480         circ_as_type   TEXT,
481         alert_message  TEXT,
482         opac_visible   BOOL,
483         floating       BOOL,
484         mint_condition BOOL
485 );
486
487 CREATE OR REPLACE FUNCTION asset.opac_ou_record_copy_count (org INT, rid BIGINT) RETURNS TABLE (depth INT, org_unit INT, visible BIGINT, available BIGINT, unshadow BIGINT, transcendant INT) AS $f$
488 DECLARE
489     ans RECORD;
490     trans INT;
491 BEGIN
492     SELECT 1 INTO trans FROM biblio.record_entry b JOIN config.bib_source src ON (b.source = src.id) WHERE src.transcendant AND b.id = rid;
493
494     FOR ans IN SELECT u.id, t.depth FROM actor.org_unit_ancestors(org) AS u JOIN actor.org_unit_type t ON (u.ou_type = t.id) LOOP
495         RETURN QUERY
496         SELECT  ans.depth,
497                 ans.id,
498                 COUNT( av.id ),
499                 SUM( CASE WHEN cp.status IN (0,7,12) THEN 1 ELSE 0 END ),
500                 COUNT( av.id ),
501                 trans
502           FROM  
503                 actor.org_unit_descendants(ans.id) d
504                 JOIN asset.opac_visible_copies av ON (av.record = rid AND av.circ_lib = d.id)
505                 JOIN asset.copy cp ON (cp.id = av.copy_id)
506           GROUP BY 1,2,6;
507
508         IF NOT FOUND THEN
509             RETURN QUERY SELECT ans.depth, ans.id, 0::BIGINT, 0::BIGINT, 0::BIGINT, trans;
510         END IF;
511
512     END LOOP;
513
514     RETURN;
515 END;
516 $f$ LANGUAGE PLPGSQL;
517
518 CREATE OR REPLACE FUNCTION asset.opac_lasso_record_copy_count (i_lasso INT, rid BIGINT) RETURNS TABLE (depth INT, org_unit INT, visible BIGINT, available BIGINT, unshadow BIGINT, transcendant INT) AS $f$
519 DECLARE
520     ans RECORD;
521     trans INT;
522 BEGIN
523     SELECT 1 INTO trans FROM biblio.record_entry b JOIN config.bib_source src ON (b.source = src.id) WHERE src.transcendant AND b.id = rid;
524
525     FOR ans IN SELECT u.org_unit AS id FROM actor.org_lasso_map AS u WHERE lasso = i_lasso LOOP
526         RETURN QUERY
527         SELECT  -1,
528                 ans.id,
529                 COUNT( av.id ),
530                 SUM( CASE WHEN cp.status IN (0,7,12) THEN 1 ELSE 0 END ),
531                 COUNT( av.id ),
532                 trans
533           FROM
534                 actor.org_unit_descendants(ans.id) d
535                 JOIN asset.opac_visible_copies av ON (av.record = rid AND av.circ_lib = d.id)
536                 JOIN asset.copy cp ON (cp.id = av.copy_id)
537           GROUP BY 1,2,6;
538
539         IF NOT FOUND THEN
540             RETURN QUERY SELECT -1, ans.id, 0::BIGINT, 0::BIGINT, 0::BIGINT, trans;
541         END IF;
542
543     END LOOP;   
544                 
545     RETURN;     
546 END;            
547 $f$ LANGUAGE PLPGSQL;
548
549 CREATE OR REPLACE FUNCTION asset.staff_ou_record_copy_count (org INT, rid BIGINT) RETURNS TABLE (depth INT, org_unit INT, visible BIGINT, available BIGINT, unshadow BIGINT, transcendant INT) AS $f$
550 DECLARE         
551     ans RECORD; 
552     trans INT;
553 BEGIN           
554     SELECT 1 INTO trans FROM biblio.record_entry b JOIN config.bib_source src ON (b.source = src.id) WHERE src.transcendant AND b.id = rid;
555
556     FOR ans IN SELECT u.id, t.depth FROM actor.org_unit_ancestors(org) AS u JOIN actor.org_unit_type t ON (u.ou_type = t.id) LOOP
557         RETURN QUERY
558         SELECT  ans.depth,
559                 ans.id,
560                 COUNT( cp.id ),
561                 SUM( CASE WHEN cp.status IN (0,7,12) THEN 1 ELSE 0 END ),
562                 SUM( CASE WHEN cl.opac_visible AND cp.opac_visible THEN 1 ELSE 0 END),
563                 trans
564           FROM
565                 actor.org_unit_descendants(ans.id) d
566                 JOIN asset.copy cp ON (cp.circ_lib = d.id AND NOT cp.deleted)
567                 JOIN asset.copy_location cl ON (cp.location = cl.id)
568                 JOIN asset.call_number cn ON (cn.record = rid AND cn.id = cp.call_number AND NOT cn.deleted)
569           GROUP BY 1,2,6;
570
571         IF NOT FOUND THEN
572             RETURN QUERY SELECT ans.depth, ans.id, 0::BIGINT, 0::BIGINT, 0::BIGINT, trans;
573         END IF;
574
575     END LOOP;
576
577     RETURN;
578 END;
579 $f$ LANGUAGE PLPGSQL;
580
581 CREATE OR REPLACE FUNCTION asset.staff_lasso_record_copy_count (i_lasso INT, rid BIGINT) RETURNS TABLE (depth INT, org_unit INT, visible BIGINT, available BIGINT, unshadow BIGINT, transcendant INT) AS $f$
582 DECLARE
583     ans RECORD;
584     trans INT;
585 BEGIN
586     SELECT 1 INTO trans FROM biblio.record_entry b JOIN config.bib_source src ON (b.source = src.id) WHERE src.transcendant AND b.id = rid;
587
588     FOR ans IN SELECT u.org_unit AS id FROM actor.org_lasso_map AS u WHERE lasso = i_lasso LOOP
589         RETURN QUERY
590         SELECT  -1,
591                 ans.id,
592                 COUNT( cp.id ),
593                 SUM( CASE WHEN cp.status IN (0,7,12) THEN 1 ELSE 0 END ),
594                 SUM( CASE WHEN cl.opac_visible AND cp.opac_visible THEN 1 ELSE 0 END),
595                 trans
596           FROM
597                 actor.org_unit_descendants(ans.id) d
598                 JOIN asset.copy cp ON (cp.circ_lib = d.id AND NOT cp.deleted)
599                 JOIN asset.copy_location cl ON (cp.location = cl.id)
600                 JOIN asset.call_number cn ON (cn.record = rid AND cn.id = cp.call_number AND NOT cn.deleted)
601           GROUP BY 1,2,6;
602
603         IF NOT FOUND THEN
604             RETURN QUERY SELECT -1, ans.id, 0::BIGINT, 0::BIGINT, 0::BIGINT, trans;
605         END IF;
606
607     END LOOP;
608
609     RETURN;
610 END;
611 $f$ LANGUAGE PLPGSQL;
612
613 CREATE OR REPLACE FUNCTION asset.record_copy_count ( place INT, rid BIGINT, staff BOOL) RETURNS TABLE (depth INT, org_unit INT, visible BIGINT, available BIGINT, unshadow BIGINT, transcendant INT) AS $f$
614 BEGIN
615     IF staff IS TRUE THEN
616         IF place > 0 THEN
617             RETURN QUERY SELECT * FROM asset.staff_ou_record_copy_count( place, rid );
618         ELSE
619             RETURN QUERY SELECT * FROM asset.staff_lasso_record_copy_count( -place, rid );
620         END IF;
621     ELSE
622         IF place > 0 THEN
623             RETURN QUERY SELECT * FROM asset.opac_ou_record_copy_count( place, rid );
624         ELSE
625             RETURN QUERY SELECT * FROM asset.opac_lasso_record_copy_count( -place, rid );
626         END IF;
627     END IF;
628
629     RETURN;
630 END;
631 $f$ LANGUAGE PLPGSQL;
632
633 CREATE OR REPLACE FUNCTION asset.opac_ou_metarecord_copy_count (org INT, rid BIGINT) RETURNS TABLE (depth INT, org_unit INT, visible BIGINT, available BIGINT, unshadow BIGINT, transcendant INT) AS $f$
634 DECLARE
635     ans RECORD;
636     trans INT;
637 BEGIN
638     SELECT 1 INTO trans FROM biblio.record_entry b JOIN config.bib_source src ON (b.source = src.id) WHERE src.transcendant AND b.id = rid;
639
640     FOR ans IN SELECT u.id, t.depth FROM actor.org_unit_ancestors(org) AS u JOIN actor.org_unit_type t ON (u.ou_type = t.id) LOOP
641         RETURN QUERY
642         SELECT  ans.depth,
643                 ans.id,
644                 COUNT( av.id ),
645                 SUM( CASE WHEN cp.status IN (0,7,12) THEN 1 ELSE 0 END ),
646                 COUNT( av.id ),
647                 trans
648           FROM  
649                 actor.org_unit_descendants(ans.id) d
650                 JOIN asset.opac_visible_copies av ON (av.record = rid AND av.circ_lib = d.id)
651                 JOIN asset.copy cp ON (cp.id = av.copy_id)
652                 JOIN metabib.metarecord_source_map m ON (m.source = av.record)
653           GROUP BY 1,2,6;
654
655         IF NOT FOUND THEN
656             RETURN QUERY SELECT ans.depth, ans.id, 0::BIGINT, 0::BIGINT, 0::BIGINT, trans;
657         END IF;
658
659     END LOOP;
660
661     RETURN;
662 END;
663 $f$ LANGUAGE PLPGSQL;
664
665 CREATE OR REPLACE FUNCTION asset.opac_lasso_metarecord_copy_count (i_lasso INT, rid BIGINT) RETURNS TABLE (depth INT, org_unit INT, visible BIGINT, available BIGINT, unshadow BIGINT, transcendant INT) AS $f$
666 DECLARE
667     ans RECORD;
668     trans INT;
669 BEGIN
670     SELECT 1 INTO trans FROM biblio.record_entry b JOIN config.bib_source src ON (b.source = src.id) WHERE src.transcendant AND b.id = rid;
671
672     FOR ans IN SELECT u.org_unit AS id FROM actor.org_lasso_map AS u WHERE lasso = i_lasso LOOP
673         RETURN QUERY
674         SELECT  -1,
675                 ans.id,
676                 COUNT( av.id ),
677                 SUM( CASE WHEN cp.status IN (0,7,12) THEN 1 ELSE 0 END ),
678                 COUNT( av.id ),
679                 trans
680           FROM
681                 actor.org_unit_descendants(ans.id) d
682                 JOIN asset.opac_visible_copies av ON (av.record = rid AND av.circ_lib = d.id)
683                 JOIN asset.copy cp ON (cp.id = av.copy_id)
684                 JOIN metabib.metarecord_source_map m ON (m.source = av.record)
685           GROUP BY 1,2,6;
686
687         IF NOT FOUND THEN
688             RETURN QUERY SELECT ans.depth, ans.id, 0::BIGINT, 0::BIGINT, 0::BIGINT, trans;
689         END IF;
690
691     END LOOP;   
692                 
693     RETURN;     
694 END;            
695 $f$ LANGUAGE PLPGSQL;
696
697 CREATE OR REPLACE FUNCTION asset.staff_ou_metarecord_copy_count (org INT, rid BIGINT) RETURNS TABLE (depth INT, org_unit INT, visible BIGINT, available BIGINT, unshadow BIGINT, transcendant INT) AS $f$
698 DECLARE         
699     ans RECORD; 
700     trans INT;
701 BEGIN
702     SELECT 1 INTO trans FROM biblio.record_entry b JOIN config.bib_source src ON (b.source = src.id) WHERE src.transcendant AND b.id = rid;
703
704     FOR ans IN SELECT u.id, t.depth FROM actor.org_unit_ancestors(org) AS u JOIN actor.org_unit_type t ON (u.ou_type = t.id) LOOP
705         RETURN QUERY
706         SELECT  ans.depth,
707                 ans.id,
708                 COUNT( cp.id ),
709                 SUM( CASE WHEN cp.status IN (0,7,12) THEN 1 ELSE 0 END ),
710                 COUNT( cp.id ),
711                 trans
712           FROM
713                 actor.org_unit_descendants(ans.id) d
714                 JOIN asset.copy cp ON (cp.circ_lib = d.id AND NOT cp.deleted)
715                 JOIN asset.call_number cn ON (cn.record = rid AND cn.id = cp.call_number AND NOT cn.deleted)
716                 JOIN metabib.metarecord_source_map m ON (m.source = cn.record)
717           GROUP BY 1,2,6;
718
719         IF NOT FOUND THEN
720             RETURN QUERY SELECT ans.depth, ans.id, 0::BIGINT, 0::BIGINT, 0::BIGINT, trans;
721         END IF;
722
723     END LOOP;
724
725     RETURN;
726 END;
727 $f$ LANGUAGE PLPGSQL;
728
729 CREATE OR REPLACE FUNCTION asset.staff_lasso_metarecord_copy_count (i_lasso INT, rid BIGINT) RETURNS TABLE (depth INT, org_unit INT, visible BIGINT, available BIGINT, unshadow BIGINT, transcendant INT) AS $f$
730 DECLARE
731     ans RECORD;
732     trans INT;
733 BEGIN
734     SELECT 1 INTO trans FROM biblio.record_entry b JOIN config.bib_source src ON (b.source = src.id) WHERE src.transcendant AND b.id = rid;
735
736     FOR ans IN SELECT u.org_unit AS id FROM actor.org_lasso_map AS u WHERE lasso = i_lasso LOOP
737         RETURN QUERY
738         SELECT  -1,
739                 ans.id,
740                 COUNT( cp.id ),
741                 SUM( CASE WHEN cp.status IN (0,7,12) THEN 1 ELSE 0 END ),
742                 COUNT( cp.id ),
743                 trans
744           FROM
745                 actor.org_unit_descendants(ans.id) d
746                 JOIN asset.copy cp ON (cp.circ_lib = d.id AND NOT cp.deleted)
747                 JOIN asset.call_number cn ON (cn.record = rid AND cn.id = cp.call_number AND NOT cn.deleted)
748                 JOIN metabib.metarecord_source_map m ON (m.source = cn.record)
749           GROUP BY 1,2,6;
750
751         IF NOT FOUND THEN
752             RETURN QUERY SELECT ans.depth, ans.id, 0::BIGINT, 0::BIGINT, 0::BIGINT, trans;
753         END IF;
754
755     END LOOP;
756
757     RETURN;
758 END;
759 $f$ LANGUAGE PLPGSQL;
760
761 CREATE OR REPLACE FUNCTION asset.metarecord_copy_count ( place INT, rid BIGINT, staff BOOL) RETURNS TABLE (depth INT, org_unit INT, visible BIGINT, available BIGINT, unshadow BIGINT, transcendant INT) AS $f$
762 BEGIN
763     IF staff IS TRUE THEN
764         IF place > 0 THEN
765             RETURN QUERY SELECT * FROM asset.staff_ou_metarecord_copy_count( place, rid );
766         ELSE
767             RETURN QUERY SELECT * FROM asset.staff_lasso_metarecord_copy_count( -place, rid );
768         END IF;
769     ELSE
770         IF place > 0 THEN
771             RETURN QUERY SELECT * FROM asset.opac_ou_metarecord_copy_count( place, rid );
772         ELSE
773             RETURN QUERY SELECT * FROM asset.opac_lasso_metarecord_copy_count( -place, rid );
774         END IF;
775     END IF;
776
777     RETURN;
778 END;
779 $f$ LANGUAGE PLPGSQL;
780
781 CREATE OR REPLACE FUNCTION asset.autogenerate_placeholder_barcode ( ) RETURNS TRIGGER AS $f$
782 BEGIN
783         IF NEW.barcode LIKE '@@%' THEN
784                 NEW.barcode := '@@' || NEW.id;
785         END IF;
786         RETURN NEW;
787 END;
788 $f$ LANGUAGE PLPGSQL;
789
790 CREATE TRIGGER autogenerate_placeholder_barcode
791         BEFORE INSERT OR UPDATE ON asset.copy
792         FOR EACH ROW EXECUTE PROCEDURE asset.autogenerate_placeholder_barcode();
793
794 CREATE OR REPLACE FUNCTION evergreen.fake_fkey_tgr () RETURNS TRIGGER AS $F$
795 DECLARE
796     copy_id BIGINT;
797 BEGIN
798     EXECUTE 'SELECT ($1).' || quote_ident(TG_ARGV[0]) INTO copy_id USING NEW;
799     PERFORM * FROM asset.copy WHERE id = copy_id;
800     IF NOT FOUND THEN
801         RAISE EXCEPTION 'Key (%.%=%) does not exist in asset.copy', TG_TABLE_SCHEMA, TG_TABLE_NAME, copy_id;
802     END IF;
803     RETURN NULL;
804 END;
805 $F$ LANGUAGE PLPGSQL;
806
807 COMMIT;
808