88cb5635d35d988f398fd4f7909cf2ffe943681a
[working/Evergreen.git] / Open-ILS / src / sql / Pg / version-upgrade / 2.0-2.1-upgrade-db.sql
1 -- 0498
2 -- Rather than polluting the public schema with general Evergreen
3 -- functions, carve out a dedicated schema.  It might already exist,
4 -- so do it before the transaction starts
5 CREATE SCHEMA evergreen;
6
7 BEGIN;
8
9 -- 0425
10 ALTER TABLE permission.grp_tree
11         ADD COLUMN hold_priority INT NOT NULL DEFAULT 0;
12
13 -- 0430 and friends
14 ALTER TABLE config.hold_matrix_matchpoint
15     ADD COLUMN strict_ou_match BOOL NOT NULL DEFAULT FALSE,
16     ADD COLUMN marc_bib_level text,
17     DROP CONSTRAINT hous_once_per_grp_loc_mod_marc,
18     DROP CONSTRAINT hold_matrix_matchpoint_marc_form_fkey,
19     DROP CONSTRAINT hold_matrix_matchpoint_marc_type_fkey,
20     DROP CONSTRAINT hold_matrix_matchpoint_marc_vr_format_fkey;
21
22
23
24 -- Replace all uses of PostgreSQL's built-in LOWER() function with
25 -- a more locale-savvy PLPERLU evergreen.lowercase() function
26 CREATE OR REPLACE FUNCTION evergreen.lowercase( TEXT ) RETURNS TEXT AS $$
27     return lc(shift);
28 $$ LANGUAGE PLPERLU STRICT IMMUTABLE;
29
30 -- 0500
31 CREATE OR REPLACE FUNCTION evergreen.change_db_setting(setting_name TEXT, settings TEXT[]) RETURNS VOID AS $$
32 BEGIN
33 EXECUTE 'ALTER DATABASE ' || quote_ident(current_database()) || ' SET ' || quote_ident(setting_name) || ' = ' || array_to_string(settings, ',');
34 END;
35
36 $$ LANGUAGE plpgsql;
37
38 -- 0501
39 SELECT evergreen.change_db_setting('search_path', ARRAY['evergreen','public','pg_catalog']);
40
41 -- Fix function breakage due to short search path
42 CREATE OR REPLACE FUNCTION evergreen.force_unicode_normal_form(string TEXT, form TEXT) RETURNS TEXT AS $func$
43 use Unicode::Normalize 'normalize';
44 return normalize($_[1],$_[0]); # reverse the params
45 $func$ LANGUAGE PLPERLU;
46
47 CREATE OR REPLACE FUNCTION evergreen.facet_force_nfc() RETURNS TRIGGER AS $$
48 BEGIN
49     NEW.value := evergreen.force_unicode_normal_form(NEW.value,'NFC');
50     RETURN NEW;
51 END;
52 $$ LANGUAGE PLPGSQL;
53
54 CREATE OR REPLACE FUNCTION evergreen.xml_escape(str TEXT) RETURNS text AS $$
55     SELECT REPLACE(REPLACE(REPLACE($1,
56        '&', '&'),
57        '<', '&lt;'),
58        '>', '&gt;');
59 $$ LANGUAGE SQL IMMUTABLE;
60
61 CREATE OR REPLACE FUNCTION evergreen.maintain_901 () RETURNS TRIGGER AS $func$
62 DECLARE
63     use_id_for_tcn BOOLEAN;
64 BEGIN
65     -- Remove any existing 901 fields before we insert the authoritative one
66     NEW.marc := REGEXP_REPLACE(NEW.marc, E'<datafield[^>]*?tag="901".+?</datafield>', '', 'g');
67
68     IF TG_TABLE_SCHEMA = 'biblio' THEN
69         -- Set TCN value to record ID?
70         SELECT enabled FROM config.global_flag INTO use_id_for_tcn
71             WHERE name = 'cat.bib.use_id_for_tcn';
72
73         IF use_id_for_tcn = 't' THEN
74             NEW.tcn_value := NEW.id;
75         END IF;
76
77         NEW.marc := REGEXP_REPLACE(
78             NEW.marc,
79             E'(</(?:[^:]*?:)?record>)',
80             E'<datafield tag="901" ind1=" " ind2=" ">' ||
81                 '<subfield code="a">' || evergreen.xml_escape(NEW.tcn_value) || E'</subfield>' ||
82                 '<subfield code="b">' || evergreen.xml_escape(NEW.tcn_source) || E'</subfield>' ||
83                 '<subfield code="c">' || NEW.id || E'</subfield>' ||
84                 '<subfield code="t">' || TG_TABLE_SCHEMA || E'</subfield>' ||
85                 CASE WHEN NEW.owner IS NOT NULL THEN '<subfield code="o">' || NEW.owner || E'</subfield>' ELSE '' END ||
86                 CASE WHEN NEW.share_depth IS NOT NULL THEN '<subfield code="d">' || NEW.share_depth || E'</subfield>' ELSE '' END ||
87              E'</datafield>\\1'
88         );
89     ELSIF TG_TABLE_SCHEMA = 'authority' THEN
90         NEW.marc := REGEXP_REPLACE(
91             NEW.marc,
92             E'(</(?:[^:]*?:)?record>)',
93             E'<datafield tag="901" ind1=" " ind2=" ">' ||
94                 '<subfield code="c">' || NEW.id || E'</subfield>' ||
95                 '<subfield code="t">' || TG_TABLE_SCHEMA || E'</subfield>' ||
96              E'</datafield>\\1'
97         );
98     ELSIF TG_TABLE_SCHEMA = 'serial' THEN
99         NEW.marc := REGEXP_REPLACE(
100             NEW.marc,
101             E'(</(?:[^:]*?:)?record>)',
102             E'<datafield tag="901" ind1=" " ind2=" ">' ||
103                 '<subfield code="c">' || NEW.id || E'</subfield>' ||
104                 '<subfield code="t">' || TG_TABLE_SCHEMA || E'</subfield>' ||
105                 '<subfield code="o">' || NEW.owning_lib || E'</subfield>' ||
106                 CASE WHEN NEW.record IS NOT NULL THEN '<subfield code="r">' || NEW.record || E'</subfield>' ELSE '' END ||
107              E'</datafield>\\1'
108         );
109     ELSE
110         NEW.marc := REGEXP_REPLACE(
111             NEW.marc,
112             E'(</(?:[^:]*?:)?record>)',
113             E'<datafield tag="901" ind1=" " ind2=" ">' ||
114                 '<subfield code="c">' || NEW.id || E'</subfield>' ||
115                 '<subfield code="t">' || TG_TABLE_SCHEMA || E'</subfield>' ||
116              E'</datafield>\\1'
117         );
118     END IF;
119
120     RETURN NEW;
121 END;
122 $func$ LANGUAGE PLPGSQL;
123
124 CREATE OR REPLACE FUNCTION evergreen.array_remove_item_by_value(inp ANYARRAY, el ANYELEMENT) RETURNS anyarray AS $$ SELECT ARRAY_ACCUM(x.e) FROM UNNEST( $1 ) x(e) WHERE x.e <> $2; $$ LANGUAGE SQL;
125
126 CREATE OR REPLACE FUNCTION evergreen.lpad_number_substrings( TEXT, TEXT, INT ) RETURNS TEXT AS $$
127     my $string = shift;
128     my $pad = shift;
129     my $len = shift;
130     my $find = $len - 1;
131
132     while ($string =~ /(?:^|\D)(\d{1,$find})(?:$|\D)/) {
133         my $padded = $1;
134         $padded = $pad x ($len - length($padded)) . $padded;
135         $string =~ s/$1/$padded/sg;
136     }
137
138     return $string;
139 $$ LANGUAGE PLPERLU;
140
141 -- 0477
142 ALTER TABLE config.hard_due_date DROP CONSTRAINT hard_due_date_name_check;
143
144 -- 0478
145 CREATE OR REPLACE FUNCTION public.naco_normalize( TEXT, TEXT ) RETURNS TEXT AS $func$
146
147     use strict;
148     use Unicode::Normalize;
149     use Encode;
150
151     my $str = decode_utf8(shift);
152     my $sf = shift;
153
154     # Apply NACO normalization to input string; based on
155     # http://www.loc.gov/catdir/pcc/naco/SCA_PccNormalization_Final_revised.pdf
156     #
157     # Note that unlike a strict reading of the NACO normalization rules,
158     # output is returned as lowercase instead of uppercase for compatibility
159     # with previous versions of the Evergreen naco_normalize routine.
160
161     # Convert to upper-case first; even though final output will be lowercase, doing this will
162     # ensure that the German eszett (ß) and certain ligatures (ff, fi, ffl, etc.) will be handled correctly.
163     # If there are any bugs in Perl's implementation of upcasing, they will be passed through here.
164     $str = uc $str;
165
166     # remove non-filing strings
167     $str =~ s/\x{0098}.*?\x{009C}//g;
168
169     $str = NFKD($str);
170
171     # additional substitutions - 3.6.
172     $str =~ s/\x{00C6}/AE/g;
173     $str =~ s/\x{00DE}/TH/g;
174     $str =~ s/\x{0152}/OE/g;
175     $str =~ tr/\x{0110}\x{00D0}\x{00D8}\x{0141}\x{2113}\x{02BB}\x{02BC}]['/DDOLl/d;
176
177     # transformations based on Unicode category codes
178     $str =~ s/[\p{Cc}\p{Cf}\p{Co}\p{Cs}\p{Lm}\p{Mc}\p{Me}\p{Mn}]//g;
179
180         if ($sf && $sf =~ /^a/o) {
181                 my $commapos = index($str, ',');
182                 if ($commapos > -1) {
183                         if ($commapos != length($str) - 1) {
184                 $str =~ s/,/\x07/; # preserve first comma
185                         }
186                 }
187         }
188
189     # since we've stripped out the control characters, we can now
190     # use a few as placeholders temporarily
191     $str =~ tr/+&@\x{266D}\x{266F}#/\x01\x02\x03\x04\x05\x06/;
192     $str =~ s/[\p{Pc}\p{Pd}\p{Pe}\p{Pf}\p{Pi}\p{Po}\p{Ps}\p{Sk}\p{Sm}\p{So}\p{Zl}\p{Zp}\p{Zs}]/ /g;
193     $str =~ tr/\x01\x02\x03\x04\x05\x06\x07/+&@\x{266D}\x{266F}#,/;
194
195     # decimal digits
196     $str =~ tr/\x{0660}-\x{0669}\x{06F0}-\x{06F9}\x{07C0}-\x{07C9}\x{0966}-\x{096F}\x{09E6}-\x{09EF}\x{0A66}-\x{0A6F}\x{0AE6}-\x{0AEF}\x{0B66}-\x{0B6F}\x{0BE6}-\x{0BEF}\x{0C66}-\x{0C6F}\x{0CE6}-\x{0CEF}\x{0D66}-\x{0D6F}\x{0E50}-\x{0E59}\x{0ED0}-\x{0ED9}\x{0F20}-\x{0F29}\x{1040}-\x{1049}\x{1090}-\x{1099}\x{17E0}-\x{17E9}\x{1810}-\x{1819}\x{1946}-\x{194F}\x{19D0}-\x{19D9}\x{1A80}-\x{1A89}\x{1A90}-\x{1A99}\x{1B50}-\x{1B59}\x{1BB0}-\x{1BB9}\x{1C40}-\x{1C49}\x{1C50}-\x{1C59}\x{A620}-\x{A629}\x{A8D0}-\x{A8D9}\x{A900}-\x{A909}\x{A9D0}-\x{A9D9}\x{AA50}-\x{AA59}\x{ABF0}-\x{ABF9}\x{FF10}-\x{FF19}/0-90-90-90-90-90-90-90-90-90-90-90-90-90-90-90-90-90-90-90-90-90-90-90-90-90-90-90-90-90-90-90-90-90-9/;
197
198     # intentionally skipping step 8 of the NACO algorithm; if the string
199     # gets normalized away, that's fine.
200
201     # leading and trailing spaces
202     $str =~ s/\s+/ /g;
203     $str =~ s/^\s+//;
204     $str =~ s/\s+$//g;
205
206     return lc $str;
207 $func$ LANGUAGE 'plperlu' STRICT IMMUTABLE;
208
209 -- 0479
210 CREATE OR REPLACE FUNCTION permission.grp_ancestors_distance( INT ) RETURNS TABLE (id INT, distance INT) AS $$
211     WITH RECURSIVE grp_ancestors_distance(id, distance) AS (
212             SELECT $1, 0
213         UNION
214             SELECT pgt.parent, gad.distance+1
215             FROM permission.grp_tree pgt JOIN grp_ancestors_distance gad ON pgt.id = gad.id
216             WHERE pgt.parent IS NOT NULL
217     )
218     SELECT * FROM grp_ancestors_distance;
219 $$ LANGUAGE SQL STABLE;
220
221 CREATE OR REPLACE FUNCTION permission.grp_descendants_distance( INT ) RETURNS TABLE (id INT, distance INT) AS $$
222     WITH RECURSIVE grp_descendants_distance(id, distance) AS (
223             SELECT $1, 0
224         UNION
225             SELECT pgt.id, gdd.distance+1
226             FROM permission.grp_tree pgt JOIN grp_descendants_distance gdd ON pgt.parent = gdd.id
227     )
228     SELECT * FROM grp_descendants_distance;
229 $$ LANGUAGE SQL STABLE;
230
231 CREATE OR REPLACE FUNCTION actor.org_unit_ancestors_distance( INT ) RETURNS TABLE (id INT, distance INT) AS $$
232     WITH RECURSIVE org_unit_ancestors_distance(id, distance) AS (
233             SELECT $1, 0
234         UNION
235             SELECT ou.parent_ou, ouad.distance+1
236             FROM actor.org_unit ou JOIN org_unit_ancestors_distance ouad ON ou.id = ouad.id
237             WHERE ou.parent_ou IS NOT NULL
238     )
239     SELECT * FROM org_unit_ancestors_distance;
240 $$ LANGUAGE SQL STABLE;
241
242 CREATE OR REPLACE FUNCTION actor.org_unit_descendants_distance( INT ) RETURNS TABLE (id INT, distance INT) AS $$
243     WITH RECURSIVE org_unit_descendants_distance(id, distance) AS (
244             SELECT $1, 0
245         UNION
246             SELECT ou.id, oudd.distance+1
247             FROM actor.org_unit ou JOIN org_unit_descendants_distance oudd ON ou.parent_ou = oudd.id
248     )
249     SELECT * FROM org_unit_descendants_distance;
250 $$ LANGUAGE SQL STABLE;
251
252 CREATE TABLE config.circ_matrix_weights (
253     id                      SERIAL  PRIMARY KEY,
254     name                    TEXT    NOT NULL UNIQUE,
255     org_unit                NUMERIC(6,2)   NOT NULL,
256     grp                     NUMERIC(6,2)   NOT NULL,
257     circ_modifier           NUMERIC(6,2)   NOT NULL,
258     marc_type               NUMERIC(6,2)   NOT NULL,
259     marc_form               NUMERIC(6,2)   NOT NULL,
260     marc_vr_format          NUMERIC(6,2)   NOT NULL,
261     copy_circ_lib           NUMERIC(6,2)   NOT NULL,
262     copy_owning_lib         NUMERIC(6,2)   NOT NULL,
263     user_home_ou            NUMERIC(6,2)   NOT NULL,
264     ref_flag                NUMERIC(6,2)   NOT NULL,
265     juvenile_flag           NUMERIC(6,2)   NOT NULL,
266     is_renewal              NUMERIC(6,2)   NOT NULL,
267     usr_age_lower_bound     NUMERIC(6,2)   NOT NULL,
268     usr_age_upper_bound     NUMERIC(6,2)   NOT NULL
269 );
270
271 CREATE TABLE config.hold_matrix_weights (
272     id                      SERIAL  PRIMARY KEY,
273     name                    TEXT    NOT NULL UNIQUE,
274     user_home_ou            NUMERIC(6,2)   NOT NULL,
275     request_ou              NUMERIC(6,2)   NOT NULL,
276     pickup_ou               NUMERIC(6,2)   NOT NULL,
277     item_owning_ou          NUMERIC(6,2)   NOT NULL,
278     item_circ_ou            NUMERIC(6,2)   NOT NULL,
279     usr_grp                 NUMERIC(6,2)   NOT NULL,
280     requestor_grp           NUMERIC(6,2)   NOT NULL,
281     circ_modifier           NUMERIC(6,2)   NOT NULL,
282     marc_type               NUMERIC(6,2)   NOT NULL,
283     marc_form               NUMERIC(6,2)   NOT NULL,
284     marc_vr_format          NUMERIC(6,2)   NOT NULL,
285     juvenile_flag           NUMERIC(6,2)   NOT NULL,
286     ref_flag                NUMERIC(6,2)   NOT NULL
287 );
288
289 CREATE TABLE config.weight_assoc (
290     id                      SERIAL  PRIMARY KEY,
291     active                  BOOL    NOT NULL,
292     org_unit                INT     NOT NULL REFERENCES actor.org_unit (id) ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED,
293     circ_weights            INT     REFERENCES config.circ_matrix_weights (id) ON DELETE SET NULL DEFERRABLE INITIALLY DEFERRED,
294     hold_weights            INT     REFERENCES config.hold_matrix_weights (id) ON DELETE SET NULL DEFERRABLE INITIALLY DEFERRED
295 );
296 CREATE UNIQUE INDEX cwa_one_active_per_ou ON config.weight_assoc (org_unit) WHERE active;
297
298 INSERT INTO config.circ_matrix_weights(name, org_unit, grp, circ_modifier, marc_type, marc_form, marc_vr_format, copy_circ_lib, copy_owning_lib, user_home_ou, ref_flag, juvenile_flag, is_renewal, usr_age_upper_bound, usr_age_lower_bound) VALUES 
299     ('Default', 10.0, 11.0, 5.0, 4.0, 3.0, 2.0, 8.0, 8.0, 8.0, 1.0, 6.0, 7.0, 0.0, 0.0),
300     ('Org_Unit_First', 11.0, 10.0, 5.0, 4.0, 3.0, 2.0, 8.0, 8.0, 8.0, 1.0, 6.0, 7.0, 0.0, 0.0),
301     ('Item_Owner_First', 8.0, 8.0, 5.0, 4.0, 3.0, 2.0, 10.0, 11.0, 8.0, 1.0, 6.0, 7.0, 0.0, 0.0),
302     ('All_Equal', 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0);
303
304 INSERT INTO config.hold_matrix_weights(name, user_home_ou, request_ou, pickup_ou, item_owning_ou, item_circ_ou, usr_grp, requestor_grp, circ_modifier, marc_type, marc_form, marc_vr_format, juvenile_flag, ref_flag) VALUES
305     ('Default', 5.0, 5.0, 5.0, 5.0, 5.0, 7.0, 8.0, 4.0, 3.0, 2.0, 1.0, 4.0, 0.0),
306     ('Item_Owner_First', 5.0, 5.0, 5.0, 8.0, 7.0, 5.0, 5.0, 4.0, 3.0, 2.0, 1.0, 4.0, 0.0),
307     ('User_Before_Requestor', 5.0, 5.0, 5.0, 5.0, 5.0, 8.0, 7.0, 4.0, 3.0, 2.0, 1.0, 4.0, 0.0),
308     ('All_Equal', 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0);
309
310 INSERT INTO config.weight_assoc(active, org_unit, circ_weights, hold_weights) VALUES
311     (true, 1, 1, 1);
312
313 -- 0480
314 CREATE OR REPLACE FUNCTION actor.usr_purge_data(
315         src_usr  IN INTEGER,
316         specified_dest_usr IN INTEGER
317 ) RETURNS VOID AS $$
318 DECLARE
319         suffix TEXT;
320         renamable_row RECORD;
321         dest_usr INTEGER;
322 BEGIN
323
324         IF specified_dest_usr IS NULL THEN
325                 dest_usr := 1; -- Admin user on stock installs
326         ELSE
327                 dest_usr := specified_dest_usr;
328         END IF;
329
330         UPDATE actor.usr SET
331                 active = FALSE,
332                 card = NULL,
333                 mailing_address = NULL,
334                 billing_address = NULL
335         WHERE id = src_usr;
336
337         -- acq.*
338         UPDATE acq.fund_allocation SET allocator = dest_usr WHERE allocator = src_usr;
339         UPDATE acq.lineitem SET creator = dest_usr WHERE creator = src_usr;
340         UPDATE acq.lineitem SET editor = dest_usr WHERE editor = src_usr;
341         UPDATE acq.lineitem SET selector = dest_usr WHERE selector = src_usr;
342         UPDATE acq.lineitem_note SET creator = dest_usr WHERE creator = src_usr;
343         UPDATE acq.lineitem_note SET editor = dest_usr WHERE editor = src_usr;
344         DELETE FROM acq.lineitem_usr_attr_definition WHERE usr = src_usr;
345
346         -- Update with a rename to avoid collisions
347         FOR renamable_row in
348                 SELECT id, name
349                 FROM   acq.picklist
350                 WHERE  owner = src_usr
351         LOOP
352                 suffix := ' (' || src_usr || ')';
353                 LOOP
354                         BEGIN
355                                 UPDATE  acq.picklist
356                                 SET     owner = dest_usr, name = name || suffix
357                                 WHERE   id = renamable_row.id;
358                         EXCEPTION WHEN unique_violation THEN
359                                 suffix := suffix || ' ';
360                                 CONTINUE;
361                         END;
362                         EXIT;
363                 END LOOP;
364         END LOOP;
365
366         UPDATE acq.picklist SET creator = dest_usr WHERE creator = src_usr;
367         UPDATE acq.picklist SET editor = dest_usr WHERE editor = src_usr;
368         UPDATE acq.po_note SET creator = dest_usr WHERE creator = src_usr;
369         UPDATE acq.po_note SET editor = dest_usr WHERE editor = src_usr;
370         UPDATE acq.purchase_order SET owner = dest_usr WHERE owner = src_usr;
371         UPDATE acq.purchase_order SET creator = dest_usr WHERE creator = src_usr;
372         UPDATE acq.purchase_order SET editor = dest_usr WHERE editor = src_usr;
373         UPDATE acq.claim_event SET creator = dest_usr WHERE creator = src_usr;
374
375         -- action.*
376         DELETE FROM action.circulation WHERE usr = src_usr;
377         UPDATE action.circulation SET circ_staff = dest_usr WHERE circ_staff = src_usr;
378         UPDATE action.circulation SET checkin_staff = dest_usr WHERE checkin_staff = src_usr;
379         UPDATE action.hold_notification SET notify_staff = dest_usr WHERE notify_staff = src_usr;
380         UPDATE action.hold_request SET fulfillment_staff = dest_usr WHERE fulfillment_staff = src_usr;
381         UPDATE action.hold_request SET requestor = dest_usr WHERE requestor = src_usr;
382         DELETE FROM action.hold_request WHERE usr = src_usr;
383         UPDATE action.in_house_use SET staff = dest_usr WHERE staff = src_usr;
384         UPDATE action.non_cat_in_house_use SET staff = dest_usr WHERE staff = src_usr;
385         DELETE FROM action.non_cataloged_circulation WHERE patron = src_usr;
386         UPDATE action.non_cataloged_circulation SET staff = dest_usr WHERE staff = src_usr;
387         DELETE FROM action.survey_response WHERE usr = src_usr;
388         UPDATE action.fieldset SET owner = dest_usr WHERE owner = src_usr;
389
390         -- actor.*
391         DELETE FROM actor.card WHERE usr = src_usr;
392         DELETE FROM actor.stat_cat_entry_usr_map WHERE target_usr = src_usr;
393
394         -- The following update is intended to avoid transient violations of a foreign
395         -- key constraint, whereby actor.usr_address references itself.  It may not be
396         -- necessary, but it does no harm.
397         UPDATE actor.usr_address SET replaces = NULL
398                 WHERE usr = src_usr AND replaces IS NOT NULL;
399         DELETE FROM actor.usr_address WHERE usr = src_usr;
400         DELETE FROM actor.usr_note WHERE usr = src_usr;
401         UPDATE actor.usr_note SET creator = dest_usr WHERE creator = src_usr;
402         DELETE FROM actor.usr_org_unit_opt_in WHERE usr = src_usr;
403         UPDATE actor.usr_org_unit_opt_in SET staff = dest_usr WHERE staff = src_usr;
404         DELETE FROM actor.usr_setting WHERE usr = src_usr;
405         DELETE FROM actor.usr_standing_penalty WHERE usr = src_usr;
406         UPDATE actor.usr_standing_penalty SET staff = dest_usr WHERE staff = src_usr;
407
408         -- asset.*
409         UPDATE asset.call_number SET creator = dest_usr WHERE creator = src_usr;
410         UPDATE asset.call_number SET editor = dest_usr WHERE editor = src_usr;
411         UPDATE asset.call_number_note SET creator = dest_usr WHERE creator = src_usr;
412         UPDATE asset.copy SET creator = dest_usr WHERE creator = src_usr;
413         UPDATE asset.copy SET editor = dest_usr WHERE editor = src_usr;
414         UPDATE asset.copy_note SET creator = dest_usr WHERE creator = src_usr;
415
416         -- auditor.*
417         DELETE FROM auditor.actor_usr_address_history WHERE id = src_usr;
418         DELETE FROM auditor.actor_usr_history WHERE id = src_usr;
419         UPDATE auditor.asset_call_number_history SET creator = dest_usr WHERE creator = src_usr;
420         UPDATE auditor.asset_call_number_history SET editor  = dest_usr WHERE editor  = src_usr;
421         UPDATE auditor.asset_copy_history SET creator = dest_usr WHERE creator = src_usr;
422         UPDATE auditor.asset_copy_history SET editor  = dest_usr WHERE editor  = src_usr;
423         UPDATE auditor.biblio_record_entry_history SET creator = dest_usr WHERE creator = src_usr;
424         UPDATE auditor.biblio_record_entry_history SET editor  = dest_usr WHERE editor  = src_usr;
425
426         -- biblio.*
427         UPDATE biblio.record_entry SET creator = dest_usr WHERE creator = src_usr;
428         UPDATE biblio.record_entry SET editor = dest_usr WHERE editor = src_usr;
429         UPDATE biblio.record_note SET creator = dest_usr WHERE creator = src_usr;
430         UPDATE biblio.record_note SET editor = dest_usr WHERE editor = src_usr;
431
432         -- container.*
433         -- Update buckets with a rename to avoid collisions
434         FOR renamable_row in
435                 SELECT id, name
436                 FROM   container.biblio_record_entry_bucket
437                 WHERE  owner = src_usr
438         LOOP
439                 suffix := ' (' || src_usr || ')';
440                 LOOP
441                         BEGIN
442                                 UPDATE  container.biblio_record_entry_bucket
443                                 SET     owner = dest_usr, name = name || suffix
444                                 WHERE   id = renamable_row.id;
445                         EXCEPTION WHEN unique_violation THEN
446                                 suffix := suffix || ' ';
447                                 CONTINUE;
448                         END;
449                         EXIT;
450                 END LOOP;
451         END LOOP;
452
453         FOR renamable_row in
454                 SELECT id, name
455                 FROM   container.call_number_bucket
456                 WHERE  owner = src_usr
457         LOOP
458                 suffix := ' (' || src_usr || ')';
459                 LOOP
460                         BEGIN
461                                 UPDATE  container.call_number_bucket
462                                 SET     owner = dest_usr, name = name || suffix
463                                 WHERE   id = renamable_row.id;
464                         EXCEPTION WHEN unique_violation THEN
465                                 suffix := suffix || ' ';
466                                 CONTINUE;
467                         END;
468                         EXIT;
469                 END LOOP;
470         END LOOP;
471
472         FOR renamable_row in
473                 SELECT id, name
474                 FROM   container.copy_bucket
475                 WHERE  owner = src_usr
476         LOOP
477                 suffix := ' (' || src_usr || ')';
478                 LOOP
479                         BEGIN
480                                 UPDATE  container.copy_bucket
481                                 SET     owner = dest_usr, name = name || suffix
482                                 WHERE   id = renamable_row.id;
483                         EXCEPTION WHEN unique_violation THEN
484                                 suffix := suffix || ' ';
485                                 CONTINUE;
486                         END;
487                         EXIT;
488                 END LOOP;
489         END LOOP;
490
491         FOR renamable_row in
492                 SELECT id, name
493                 FROM   container.user_bucket
494                 WHERE  owner = src_usr
495         LOOP
496                 suffix := ' (' || src_usr || ')';
497                 LOOP
498                         BEGIN
499                                 UPDATE  container.user_bucket
500                                 SET     owner = dest_usr, name = name || suffix
501                                 WHERE   id = renamable_row.id;
502                         EXCEPTION WHEN unique_violation THEN
503                                 suffix := suffix || ' ';
504                                 CONTINUE;
505                         END;
506                         EXIT;
507                 END LOOP;
508         END LOOP;
509
510         DELETE FROM container.user_bucket_item WHERE target_user = src_usr;
511
512         -- money.*
513         DELETE FROM money.billable_xact WHERE usr = src_usr;
514         DELETE FROM money.collections_tracker WHERE usr = src_usr;
515         UPDATE money.collections_tracker SET collector = dest_usr WHERE collector = src_usr;
516
517         -- permission.*
518         DELETE FROM permission.usr_grp_map WHERE usr = src_usr;
519         DELETE FROM permission.usr_object_perm_map WHERE usr = src_usr;
520         DELETE FROM permission.usr_perm_map WHERE usr = src_usr;
521         DELETE FROM permission.usr_work_ou_map WHERE usr = src_usr;
522
523         -- reporter.*
524         -- Update with a rename to avoid collisions
525         BEGIN
526                 FOR renamable_row in
527                         SELECT id, name
528                         FROM   reporter.output_folder
529                         WHERE  owner = src_usr
530                 LOOP
531                         suffix := ' (' || src_usr || ')';
532                         LOOP
533                                 BEGIN
534                                         UPDATE  reporter.output_folder
535                                         SET     owner = dest_usr, name = name || suffix
536                                         WHERE   id = renamable_row.id;
537                                 EXCEPTION WHEN unique_violation THEN
538                                         suffix := suffix || ' ';
539                                         CONTINUE;
540                                 END;
541                                 EXIT;
542                         END LOOP;
543                 END LOOP;
544         EXCEPTION WHEN undefined_table THEN
545                 -- do nothing
546         END;
547
548         BEGIN
549                 UPDATE reporter.report SET owner = dest_usr WHERE owner = src_usr;
550         EXCEPTION WHEN undefined_table THEN
551                 -- do nothing
552         END;
553
554         -- Update with a rename to avoid collisions
555         BEGIN
556                 FOR renamable_row in
557                         SELECT id, name
558                         FROM   reporter.report_folder
559                         WHERE  owner = src_usr
560                 LOOP
561                         suffix := ' (' || src_usr || ')';
562                         LOOP
563                                 BEGIN
564                                         UPDATE  reporter.report_folder
565                                         SET     owner = dest_usr, name = name || suffix
566                                         WHERE   id = renamable_row.id;
567                                 EXCEPTION WHEN unique_violation THEN
568                                         suffix := suffix || ' ';
569                                         CONTINUE;
570                                 END;
571                                 EXIT;
572                         END LOOP;
573                 END LOOP;
574         EXCEPTION WHEN undefined_table THEN
575                 -- do nothing
576         END;
577
578         BEGIN
579                 UPDATE reporter.schedule SET runner = dest_usr WHERE runner = src_usr;
580         EXCEPTION WHEN undefined_table THEN
581                 -- do nothing
582         END;
583
584         BEGIN
585                 UPDATE reporter.template SET owner = dest_usr WHERE owner = src_usr;
586         EXCEPTION WHEN undefined_table THEN
587                 -- do nothing
588         END;
589
590         -- Update with a rename to avoid collisions
591         BEGIN
592                 FOR renamable_row in
593                         SELECT id, name
594                         FROM   reporter.template_folder
595                         WHERE  owner = src_usr
596                 LOOP
597                         suffix := ' (' || src_usr || ')';
598                         LOOP
599                                 BEGIN
600                                         UPDATE  reporter.template_folder
601                                         SET     owner = dest_usr, name = name || suffix
602                                         WHERE   id = renamable_row.id;
603                                 EXCEPTION WHEN unique_violation THEN
604                                         suffix := suffix || ' ';
605                                         CONTINUE;
606                                 END;
607                                 EXIT;
608                         END LOOP;
609                 END LOOP;
610         EXCEPTION WHEN undefined_table THEN
611         -- do nothing
612         END;
613
614         -- vandelay.*
615         -- Update with a rename to avoid collisions
616         FOR renamable_row in
617                 SELECT id, name
618                 FROM   vandelay.queue
619                 WHERE  owner = src_usr
620         LOOP
621                 suffix := ' (' || src_usr || ')';
622                 LOOP
623                         BEGIN
624                                 UPDATE  vandelay.queue
625                                 SET     owner = dest_usr, name = name || suffix
626                                 WHERE   id = renamable_row.id;
627                         EXCEPTION WHEN unique_violation THEN
628                                 suffix := suffix || ' ';
629                                 CONTINUE;
630                         END;
631                         EXIT;
632                 END LOOP;
633         END LOOP;
634
635 END;
636 $$ LANGUAGE plpgsql;
637
638 -- 0482, 0487, and parts of others
639 -- Circ matchpoint table changes
640 ALTER TABLE config.circ_matrix_matchpoint
641     ALTER COLUMN circulate DROP NOT NULL, -- Fallthrough enable
642     ALTER COLUMN circulate DROP DEFAULT, -- Stop defaulting to true to enable default to fallthrough
643     ALTER COLUMN duration_rule DROP NOT NULL, -- Fallthrough enable
644     ALTER COLUMN recurring_fine_rule DROP NOT NULL, -- Fallthrough enable
645     ALTER COLUMN max_fine_rule DROP NOT NULL, -- Fallthrough enable
646     ADD COLUMN renewals INT, -- Renewals override
647     ADD COLUMN user_home_ou INT REFERENCES actor.org_unit (id) DEFERRABLE INITIALLY DEFERRED,
648     ADD COLUMN grace_period INTERVAL,
649     ADD COLUMN marc_bib_level text,
650     DROP CONSTRAINT ep_once_per_grp_loc_mod_marc,
651     DROP CONSTRAINT circ_matrix_matchpoint_marc_form_fkey,
652     DROP CONSTRAINT circ_matrix_matchpoint_marc_type_fkey,
653     DROP CONSTRAINT circ_matrix_matchpoint_marc_vr_format_fkey;
654
655 -- Clean up tables before making normalized index
656
657 CREATE OR REPLACE FUNCTION action.cleanup_matrix_matchpoints() RETURNS void AS $func$
658 DECLARE
659     temp_row    RECORD;
660 BEGIN
661     -- Circ Matrix
662     FOR temp_row IN
663         SELECT org_unit, grp, circ_modifier, marc_type, marc_form, marc_vr_format, copy_circ_lib, copy_owning_lib, user_home_ou, ref_flag, juvenile_flag, is_renewal, usr_age_lower_bound, usr_age_upper_bound, COUNT(id) as rowcount, MIN(id) as firstrow
664         FROM config.circ_matrix_matchpoint
665         WHERE active
666         GROUP BY org_unit, grp, circ_modifier, marc_type, marc_form, marc_vr_format, copy_circ_lib, copy_owning_lib, user_home_ou, ref_flag, juvenile_flag, is_renewal, usr_age_lower_bound, usr_age_upper_bound
667         HAVING COUNT(id) > 1 LOOP
668
669         UPDATE config.circ_matrix_matchpoint SET active=false
670             WHERE id > temp_row.firstrow
671                 AND org_unit = temp_row.org_unit
672                 AND grp = temp_row.grp
673                 AND circ_modifier       IS NOT DISTINCT FROM temp_row.circ_modifier
674                 AND marc_type           IS NOT DISTINCT FROM temp_row.marc_type
675                 AND marc_form           IS NOT DISTINCT FROM temp_row.marc_form
676                 AND marc_vr_format      IS NOT DISTINCT FROM temp_row.marc_vr_format
677                 AND copy_circ_lib       IS NOT DISTINCT FROM temp_row.copy_circ_lib
678                 AND copy_owning_lib     IS NOT DISTINCT FROM temp_row.copy_owning_lib
679                 AND user_home_ou        IS NOT DISTINCT FROM temp_row.user_home_ou
680                 AND ref_flag            IS NOT DISTINCT FROM temp_row.ref_flag
681                 AND juvenile_flag       IS NOT DISTINCT FROM temp_row.juvenile_flag
682                 AND is_renewal          IS NOT DISTINCT FROM temp_row.is_renewal
683                 AND usr_age_lower_bound IS NOT DISTINCT FROM temp_row.usr_age_lower_bound
684                 AND usr_age_upper_bound IS NOT DISTINCT FROM temp_row.usr_age_upper_bound;
685     END LOOP;
686
687     -- Hold Matrix
688     FOR temp_row IN
689         SELECT user_home_ou, request_ou, pickup_ou, item_owning_ou, item_circ_ou, usr_grp, requestor_grp, circ_modifier, marc_type, marc_form, marc_vr_format, juvenile_flag, ref_flag, COUNT(id) as rowcount, MIN(id) as firstrow
690         FROM config.hold_matrix_matchpoint
691         WHERE active
692         GROUP BY user_home_ou, request_ou, pickup_ou, item_owning_ou, item_circ_ou, usr_grp, requestor_grp, circ_modifier, marc_type, marc_form, marc_vr_format, juvenile_flag, ref_flag
693         HAVING COUNT(id) > 1 LOOP
694
695         UPDATE config.hold_matrix_matchpoint SET active=false
696             WHERE id > temp_row.firstrow
697                 AND user_home_ou        IS NOT DISTINCT FROM temp_row.user_home_ou
698                 AND request_ou          IS NOT DISTINCT FROM temp_row.request_ou
699                 AND pickup_ou           IS NOT DISTINCT FROM temp_row.pickup_ou
700                 AND item_owning_ou      IS NOT DISTINCT FROM temp_row.item_owning_ou
701                 AND item_circ_ou        IS NOT DISTINCT FROM temp_row.item_circ_ou
702                 AND usr_grp             IS NOT DISTINCT FROM temp_row.usr_grp
703                 AND requestor_grp       IS NOT DISTINCT FROM temp_row.requestor_grp
704                 AND circ_modifier       IS NOT DISTINCT FROM temp_row.circ_modifier
705                 AND marc_type           IS NOT DISTINCT FROM temp_row.marc_type
706                 AND marc_form           IS NOT DISTINCT FROM temp_row.marc_form
707                 AND marc_vr_format      IS NOT DISTINCT FROM temp_row.marc_vr_format
708                 AND juvenile_flag       IS NOT DISTINCT FROM temp_row.juvenile_flag
709                 AND ref_flag            IS NOT DISTINCT FROM temp_row.ref_flag;
710     END LOOP;
711 END;
712 $func$ LANGUAGE plpgsql;
713
714 SELECT action.cleanup_matrix_matchpoints();
715
716 DROP FUNCTION IF EXISTS action.cleanup_matrix_matchpoints();
717
718 -- Create Normalized indexes
719
720 CREATE UNIQUE INDEX ccmm_once_per_paramset ON config.circ_matrix_matchpoint (org_unit, grp, COALESCE(circ_modifier, ''), COALESCE(marc_type, ''), COALESCE(marc_form, ''), COALESCE(marc_vr_format, ''), COALESCE(copy_circ_lib::TEXT, ''), COALESCE(copy_owning_lib::TEXT, ''), COALESCE(user_home_ou::TEXT, ''), COALESCE(ref_flag::TEXT, ''), COALESCE(juvenile_flag::TEXT, ''), COALESCE(is_renewal::TEXT, ''), COALESCE(usr_age_lower_bound::TEXT, ''), COALESCE(usr_age_upper_bound::TEXT, '')) WHERE active;
721
722 CREATE UNIQUE INDEX chmm_once_per_paramset ON config.hold_matrix_matchpoint (COALESCE(user_home_ou::TEXT, ''), COALESCE(request_ou::TEXT, ''), COALESCE(pickup_ou::TEXT, ''), COALESCE(item_owning_ou::TEXT, ''), COALESCE(item_circ_ou::TEXT, ''), COALESCE(usr_grp::TEXT, ''), COALESCE(requestor_grp::TEXT, ''), COALESCE(circ_modifier, ''), COALESCE(marc_type, ''), COALESCE(marc_form, ''), COALESCE(marc_vr_format, ''), COALESCE(juvenile_flag::TEXT, ''), COALESCE(ref_flag::TEXT, '')) WHERE active;
723
724 -- 0484
725 DROP FUNCTION asset.metarecord_copy_count ( INT, BIGINT, BOOL );
726 DROP FUNCTION asset.record_copy_count ( INT, BIGINT, BOOL );
727
728 DROP FUNCTION asset.opac_ou_record_copy_count (INT, BIGINT);
729 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$
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.id, t.depth FROM actor.org_unit_ancestors(org) AS u JOIN actor.org_unit_type t ON (u.ou_type = t.id) LOOP
737         RETURN QUERY
738         SELECT  ans.depth,
739                 ans.id,
740                 COUNT( av.id ),
741                 SUM( CASE WHEN cp.status IN (0,7,12) THEN 1 ELSE 0 END ),
742                 COUNT( av.id ),
743                 trans
744           FROM  
745                 actor.org_unit_descendants(ans.id) d
746                 JOIN asset.opac_visible_copies av ON (av.record = rid AND av.circ_lib = d.id)
747                 JOIN asset.copy cp ON (cp.id = av.copy_id)
748           GROUP BY 1,2,6;
749
750         IF NOT FOUND THEN
751             RETURN QUERY SELECT ans.depth, ans.id, 0::BIGINT, 0::BIGINT, 0::BIGINT, trans;
752         END IF;
753
754     END LOOP;
755
756     RETURN;
757 END;
758 $f$ LANGUAGE PLPGSQL;
759
760 DROP FUNCTION asset.opac_lasso_record_copy_count (INT, BIGINT);
761 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$
762 DECLARE
763     ans RECORD;
764     trans INT;
765 BEGIN
766     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;
767
768     FOR ans IN SELECT u.org_unit AS id FROM actor.org_lasso_map AS u WHERE lasso = i_lasso LOOP
769         RETURN QUERY
770         SELECT  -1,
771                 ans.id,
772                 COUNT( av.id ),
773                 SUM( CASE WHEN cp.status IN (0,7,12) THEN 1 ELSE 0 END ),
774                 COUNT( av.id ),
775                 trans
776           FROM  
777                 actor.org_unit_descendants(ans.id) d
778                 JOIN asset.opac_visible_copies av ON (av.record = rid AND av.circ_lib = d.id)
779                 JOIN asset.copy cp ON (cp.id = av.copy_id)
780           GROUP BY 1,2,6;
781
782         IF NOT FOUND THEN
783             RETURN QUERY SELECT ans.depth, ans.id, 0::BIGINT, 0::BIGINT, 0::BIGINT, trans;
784         END IF;
785
786     END LOOP;
787
788     RETURN;
789 END;
790 $f$ LANGUAGE PLPGSQL;
791
792 DROP FUNCTION asset.staff_ou_record_copy_count (INT, BIGINT);
793
794 DROP FUNCTION asset.staff_lasso_record_copy_count (INT, BIGINT);
795
796 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$
797 BEGIN
798     IF staff IS TRUE THEN
799         IF place > 0 THEN
800             RETURN QUERY SELECT * FROM asset.staff_ou_record_copy_count( place, rid );
801         ELSE
802             RETURN QUERY SELECT * FROM asset.staff_lasso_record_copy_count( -place, rid );
803         END IF;
804     ELSE
805         IF place > 0 THEN
806             RETURN QUERY SELECT * FROM asset.opac_ou_record_copy_count( place, rid );
807         ELSE
808             RETURN QUERY SELECT * FROM asset.opac_lasso_record_copy_count( -place, rid );
809         END IF;
810     END IF;
811
812     RETURN;
813 END;
814 $f$ LANGUAGE PLPGSQL;
815
816 DROP FUNCTION asset.opac_ou_metarecord_copy_count (INT, BIGINT);
817 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$
818 DECLARE
819     ans RECORD;
820     trans INT;
821 BEGIN
822     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;
823
824     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
825         RETURN QUERY
826         SELECT  ans.depth,
827                 ans.id,
828                 COUNT( av.id ),
829                 SUM( CASE WHEN cp.status IN (0,7,12) THEN 1 ELSE 0 END ),
830                 COUNT( av.id ),
831                 trans
832           FROM
833                 actor.org_unit_descendants(ans.id) d
834                 JOIN asset.opac_visible_copies av ON (av.record = rid AND av.circ_lib = d.id)
835                 JOIN asset.copy cp ON (cp.id = av.copy_id)
836                 JOIN metabib.metarecord_source_map m ON (m.source = av.record)
837           GROUP BY 1,2,6;
838
839         IF NOT FOUND THEN
840             RETURN QUERY SELECT ans.depth, ans.id, 0::BIGINT, 0::BIGINT, 0::BIGINT, trans;
841         END IF;
842
843     END LOOP;
844
845     RETURN;
846 END;
847 $f$ LANGUAGE PLPGSQL;
848
849 DROP FUNCTION asset.opac_lasso_metarecord_copy_count (INT, BIGINT);
850 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$
851 DECLARE
852     ans RECORD;
853     trans INT;
854 BEGIN
855     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;
856
857     FOR ans IN SELECT u.org_unit AS id FROM actor.org_lasso_map AS u WHERE lasso = i_lasso LOOP
858         RETURN QUERY
859         SELECT  -1,
860                 ans.id,
861                 COUNT( av.id ),
862                 SUM( CASE WHEN cp.status IN (0,7,12) THEN 1 ELSE 0 END ),
863                 COUNT( av.id ),
864                 trans
865           FROM
866                 actor.org_unit_descendants(ans.id) d
867                 JOIN asset.opac_visible_copies av ON (av.record = rid AND av.circ_lib = d.id)
868                 JOIN asset.copy cp ON (cp.id = av.copy_id)
869                 JOIN metabib.metarecord_source_map m ON (m.source = av.record)
870           GROUP BY 1,2,6;
871
872         IF NOT FOUND THEN
873             RETURN QUERY SELECT ans.depth, ans.id, 0::BIGINT, 0::BIGINT, 0::BIGINT, trans;
874         END IF;
875
876     END LOOP;
877
878     RETURN;
879 END;
880 $f$ LANGUAGE PLPGSQL;
881
882 DROP FUNCTION asset.staff_lasso_metarecord_copy_count (INT, BIGINT);
883
884 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$
885 BEGIN
886     IF staff IS TRUE THEN
887         IF place > 0 THEN
888             RETURN QUERY SELECT * FROM asset.staff_ou_metarecord_copy_count( place, rid );
889         ELSE
890             RETURN QUERY SELECT * FROM asset.staff_lasso_metarecord_copy_count( -place, rid );
891         END IF;
892     ELSE
893         IF place > 0 THEN
894             RETURN QUERY SELECT * FROM asset.opac_ou_metarecord_copy_count( place, rid );
895         ELSE
896             RETURN QUERY SELECT * FROM asset.opac_lasso_metarecord_copy_count( -place, rid );
897         END IF;
898     END IF;
899
900     RETURN;
901 END;
902 $f$ LANGUAGE PLPGSQL;
903
904 -- 0485
905 CREATE OR REPLACE VIEW reporter.simple_record AS
906 SELECT  r.id,
907         s.metarecord,
908         r.fingerprint,
909         r.quality,
910         r.tcn_source,
911         r.tcn_value,
912         title.value AS title,
913         uniform_title.value AS uniform_title,
914         author.value AS author,
915         publisher.value AS publisher,
916         SUBSTRING(pubdate.value FROM $$\d+$$) AS pubdate,
917         series_title.value AS series_title,
918         series_statement.value AS series_statement,
919         summary.value AS summary,
920         ARRAY_ACCUM( DISTINCT REPLACE(SUBSTRING(isbn.value FROM $$^\S+$$), '-', '') ) AS isbn,
921         ARRAY_ACCUM( DISTINCT REGEXP_REPLACE(issn.value, E'^\\S*(\\d{4})[-\\s](\\d{3,4}x?)', E'\\1 \\2') ) AS issn,
922         ARRAY((SELECT DISTINCT value FROM metabib.full_rec WHERE tag = '650' AND subfield = 'a' AND record = r.id)) AS topic_subject,
923         ARRAY((SELECT DISTINCT value FROM metabib.full_rec WHERE tag = '651' AND subfield = 'a' AND record = r.id)) AS geographic_subject,
924         ARRAY((SELECT DISTINCT value FROM metabib.full_rec WHERE tag = '655' AND subfield = 'a' AND record = r.id)) AS genre,
925         ARRAY((SELECT DISTINCT value FROM metabib.full_rec WHERE tag = '600' AND subfield = 'a' AND record = r.id)) AS name_subject,
926         ARRAY((SELECT DISTINCT value FROM metabib.full_rec WHERE tag = '610' AND subfield = 'a' AND record = r.id)) AS corporate_subject,
927         ARRAY((SELECT value FROM metabib.full_rec WHERE tag = '856' AND subfield IN ('3','y','u') AND record = r.id ORDER BY CASE WHEN subfield IN ('3','y') THEN 0 ELSE 1 END)) AS external_uri
928   FROM  biblio.record_entry r
929         JOIN metabib.metarecord_source_map s ON (s.source = r.id)
930         LEFT JOIN metabib.full_rec uniform_title ON (r.id = uniform_title.record AND uniform_title.tag = '240' AND uniform_title.subfield = 'a')
931         LEFT JOIN metabib.full_rec title ON (r.id = title.record AND title.tag = '245' AND title.subfield = 'a')
932         LEFT JOIN metabib.full_rec author ON (r.id = author.record AND author.tag = '100' AND author.subfield = 'a')
933         LEFT JOIN metabib.full_rec publisher ON (r.id = publisher.record AND publisher.tag = '260' AND publisher.subfield = 'b')
934         LEFT JOIN metabib.full_rec pubdate ON (r.id = pubdate.record AND pubdate.tag = '260' AND pubdate.subfield = 'c')
935         LEFT JOIN metabib.full_rec isbn ON (r.id = isbn.record AND isbn.tag IN ('024', '020') AND isbn.subfield IN ('a','z'))
936         LEFT JOIN metabib.full_rec issn ON (r.id = issn.record AND issn.tag = '022' AND issn.subfield = 'a')
937         LEFT JOIN metabib.full_rec series_title ON (r.id = series_title.record AND series_title.tag IN ('830','440') AND series_title.subfield = 'a')
938         LEFT JOIN metabib.full_rec series_statement ON (r.id = series_statement.record AND series_statement.tag = '490' AND series_statement.subfield = 'a')
939         LEFT JOIN metabib.full_rec summary ON (r.id = summary.record AND summary.tag = '520' AND summary.subfield = 'a')
940   GROUP BY 1,2,3,4,5,6,7,8,9,10,11,12,13,14;
941
942 CREATE OR REPLACE VIEW reporter.old_super_simple_record AS
943 SELECT  r.id,
944     r.fingerprint,
945     r.quality,
946     r.tcn_source,
947     r.tcn_value,
948     FIRST(title.value) AS title,
949     FIRST(author.value) AS author,
950     ARRAY_TO_STRING(ARRAY_ACCUM( DISTINCT publisher.value), ', ') AS publisher,
951     ARRAY_TO_STRING(ARRAY_ACCUM( DISTINCT SUBSTRING(pubdate.value FROM $$\d+$$) ), ', ') AS pubdate,
952     ARRAY_ACCUM( DISTINCT REPLACE(SUBSTRING(isbn.value FROM $$^\S+$$), '-', '') ) AS isbn,
953     ARRAY_ACCUM( DISTINCT REGEXP_REPLACE(issn.value, E'^\\S*(\\d{4})[-\\s](\\d{3,4}x?)', E'\\1 \\2') ) AS issn
954   FROM  biblio.record_entry r
955     LEFT JOIN metabib.full_rec title ON (r.id = title.record AND title.tag = '245' AND title.subfield = 'a')
956     LEFT JOIN metabib.full_rec author ON (r.id = author.record AND author.tag IN ('100','110','111') AND author.subfield = 'a')
957     LEFT JOIN metabib.full_rec publisher ON (r.id = publisher.record AND publisher.tag = '260' AND publisher.subfield = 'b')
958     LEFT JOIN metabib.full_rec pubdate ON (r.id = pubdate.record AND pubdate.tag = '260' AND pubdate.subfield = 'c')
959     LEFT JOIN metabib.full_rec isbn ON (r.id = isbn.record AND isbn.tag IN ('024', '020') AND isbn.subfield IN ('a','z'))
960     LEFT JOIN metabib.full_rec issn ON (r.id = issn.record AND issn.tag = '022' AND issn.subfield = 'a')
961   GROUP BY 1,2,3,4,5;
962
963 -- 0486
964 ALTER TABLE money.credit_card_payment ADD COLUMN cc_order_number TEXT;
965
966 -- Changing return types requires explicit dropping of old versions
967 DROP FUNCTION action.find_circ_matrix_matchpoint( context_ou INT, match_item BIGINT, match_user INT, renewal BOOL );
968 DROP FUNCTION action.item_user_circ_test( circ_ou INT, match_item BIGINT, match_user INT, renewal BOOL );
969 DROP FUNCTION action.item_user_circ_test( INT, BIGINT, INT );
970 DROP FUNCTION action.item_user_renew_test( INT, BIGINT, INT );
971
972 -- New return types
973 CREATE TYPE action.found_circ_matrix_matchpoint AS ( success BOOL, matchpoint config.circ_matrix_matchpoint, buildrows INT[] );
974
975 -- Helper function - For manual calling, it can be easier to pass in IDs instead of objects
976 CREATE OR REPLACE FUNCTION action.find_circ_matrix_matchpoint( context_ou INT, match_item BIGINT, match_user INT, renewal BOOL ) RETURNS SETOF action.found_circ_matrix_matchpoint AS $func$
977 DECLARE
978     item_object asset.copy%ROWTYPE;
979     user_object actor.usr%ROWTYPE;
980 BEGIN
981     SELECT INTO item_object * FROM asset.copy   WHERE id = match_item;
982     SELECT INTO user_object * FROM actor.usr    WHERE id = match_user;
983
984     RETURN QUERY SELECT * FROM action.find_circ_matrix_matchpoint( context_ou, item_object, user_object, renewal );
985 END;
986 $func$ LANGUAGE plpgsql;
987
988 CREATE TYPE action.circ_matrix_test_result AS ( success BOOL, fail_part TEXT, buildrows INT[], matchpoint INT, circulate BOOL, duration_rule INT, recurring_fine_rule INT, max_fine_rule INT, hard_due_date INT, renewals INT, grace_period INTERVAL );
989
990 CREATE OR REPLACE FUNCTION action.item_user_circ_test( circ_ou INT, match_item BIGINT, match_user INT, renewal BOOL ) RETURNS SETOF action.circ_matrix_test_result AS $func$
991 DECLARE
992     user_object             actor.usr%ROWTYPE;
993     standing_penalty        config.standing_penalty%ROWTYPE;
994     item_object             asset.copy%ROWTYPE;
995     item_status_object      config.copy_status%ROWTYPE;
996     item_location_object    asset.copy_location%ROWTYPE;
997     result                  action.circ_matrix_test_result;
998     circ_test               action.found_circ_matrix_matchpoint;
999     circ_matchpoint         config.circ_matrix_matchpoint%ROWTYPE;
1000     out_by_circ_mod         config.circ_matrix_circ_mod_test%ROWTYPE;
1001     circ_mod_map            config.circ_matrix_circ_mod_test_map%ROWTYPE;
1002     hold_ratio              action.hold_stats%ROWTYPE;
1003     penalty_type            TEXT;
1004     items_out               INT;
1005     context_org_list        INT[];
1006     done                    BOOL := FALSE;
1007 BEGIN
1008     -- Assume success unless we hit a failure condition
1009     result.success := TRUE;
1010
1011     -- Fail if the user is BARRED
1012     SELECT INTO user_object * FROM actor.usr WHERE id = match_user;
1013
1014     -- Fail if we couldn't find the user 
1015     IF user_object.id IS NULL THEN
1016         result.fail_part := 'no_user';
1017         result.success := FALSE;
1018         done := TRUE;
1019         RETURN NEXT result;
1020         RETURN;
1021     END IF;
1022
1023     SELECT INTO item_object * FROM asset.copy WHERE id = match_item;
1024
1025     -- Fail if we couldn't find the item 
1026     IF item_object.id IS NULL THEN
1027         result.fail_part := 'no_item';
1028         result.success := FALSE;
1029         done := TRUE;
1030         RETURN NEXT result;
1031         RETURN;
1032     END IF;
1033
1034     IF user_object.barred IS TRUE THEN
1035         result.fail_part := 'actor.usr.barred';
1036         result.success := FALSE;
1037         done := TRUE;
1038         RETURN NEXT result;
1039     END IF;
1040
1041     -- Fail if the item can't circulate
1042     IF item_object.circulate IS FALSE THEN
1043         result.fail_part := 'asset.copy.circulate';
1044         result.success := FALSE;
1045         done := TRUE;
1046         RETURN NEXT result;
1047     END IF;
1048
1049     -- Fail if the item isn't in a circulateable status on a non-renewal
1050     IF NOT renewal AND item_object.status NOT IN ( 0, 7, 8 ) THEN 
1051         result.fail_part := 'asset.copy.status';
1052         result.success := FALSE;
1053         done := TRUE;
1054         RETURN NEXT result;
1055     ELSIF renewal AND item_object.status <> 1 THEN
1056         result.fail_part := 'asset.copy.status';
1057         result.success := FALSE;
1058         done := TRUE;
1059         RETURN NEXT result;
1060     END IF;
1061
1062     -- Fail if the item can't circulate because of the shelving location
1063     SELECT INTO item_location_object * FROM asset.copy_location WHERE id = item_object.location;
1064     IF item_location_object.circulate IS FALSE THEN
1065         result.fail_part := 'asset.copy_location.circulate';
1066         result.success := FALSE;
1067         done := TRUE;
1068         RETURN NEXT result;
1069     END IF;
1070
1071     SELECT INTO circ_test * FROM action.find_circ_matrix_matchpoint(circ_ou, item_object, user_object, renewal);
1072
1073     circ_matchpoint             := circ_test.matchpoint;
1074     result.matchpoint           := circ_matchpoint.id;
1075     result.circulate            := circ_matchpoint.circulate;
1076     result.duration_rule        := circ_matchpoint.duration_rule;
1077     result.recurring_fine_rule  := circ_matchpoint.recurring_fine_rule;
1078     result.max_fine_rule        := circ_matchpoint.max_fine_rule;
1079     result.hard_due_date        := circ_matchpoint.hard_due_date;
1080     result.renewals             := circ_matchpoint.renewals;
1081     result.buildrows            := circ_test.buildrows;
1082
1083     -- Fail if we couldn't find a matchpoint
1084     IF circ_test.success = false THEN
1085         result.fail_part := 'no_matchpoint';
1086         result.success := FALSE;
1087         done := TRUE;
1088         RETURN NEXT result;
1089         RETURN; -- All tests after this point require a matchpoint. No sense in running on an incomplete or missing one.
1090     END IF;
1091
1092     -- Apparently....use the circ matchpoint org unit to determine what org units are valid.
1093     SELECT INTO context_org_list ARRAY_ACCUM(id) FROM actor.org_unit_full_path( circ_matchpoint.org_unit );
1094
1095     IF renewal THEN
1096         penalty_type = '%RENEW%';
1097     ELSE
1098         penalty_type = '%CIRC%';
1099     END IF;
1100
1101     FOR standing_penalty IN
1102         SELECT  DISTINCT csp.*
1103           FROM  actor.usr_standing_penalty usp
1104                 JOIN config.standing_penalty csp ON (csp.id = usp.standing_penalty)
1105           WHERE usr = match_user
1106                 AND usp.org_unit IN ( SELECT * FROM unnest(context_org_list) )
1107                 AND (usp.stop_date IS NULL or usp.stop_date > NOW())
1108                 AND csp.block_list LIKE penalty_type LOOP
1109
1110         result.fail_part := standing_penalty.name;
1111         result.success := FALSE;
1112         done := TRUE;
1113         RETURN NEXT result;
1114     END LOOP;
1115
1116     -- Fail if the test is set to hard non-circulating
1117     IF circ_matchpoint.circulate IS FALSE THEN
1118         result.fail_part := 'config.circ_matrix_test.circulate';
1119         result.success := FALSE;
1120         done := TRUE;
1121         RETURN NEXT result;
1122     END IF;
1123
1124     -- Fail if the total copy-hold ratio is too low
1125     IF circ_matchpoint.total_copy_hold_ratio IS NOT NULL THEN
1126         SELECT INTO hold_ratio * FROM action.copy_related_hold_stats(match_item);
1127         IF hold_ratio.total_copy_ratio IS NOT NULL AND hold_ratio.total_copy_ratio < circ_matchpoint.total_copy_hold_ratio THEN
1128             result.fail_part := 'config.circ_matrix_test.total_copy_hold_ratio';
1129             result.success := FALSE;
1130             done := TRUE;
1131             RETURN NEXT result;
1132         END IF;
1133     END IF;
1134
1135     -- Fail if the available copy-hold ratio is too low
1136     IF circ_matchpoint.available_copy_hold_ratio IS NOT NULL THEN
1137         IF hold_ratio.hold_count IS NULL THEN
1138             SELECT INTO hold_ratio * FROM action.copy_related_hold_stats(match_item);
1139         END IF;
1140         IF hold_ratio.available_copy_ratio IS NOT NULL AND hold_ratio.available_copy_ratio < circ_matchpoint.available_copy_hold_ratio THEN
1141             result.fail_part := 'config.circ_matrix_test.available_copy_hold_ratio';
1142             result.success := FALSE;
1143             done := TRUE;
1144             RETURN NEXT result;
1145         END IF;
1146     END IF;
1147
1148     -- Fail if the user has too many items with specific circ_modifiers checked out
1149     FOR out_by_circ_mod IN SELECT * FROM config.circ_matrix_circ_mod_test WHERE matchpoint = circ_matchpoint.id LOOP
1150         SELECT  INTO items_out COUNT(*)
1151           FROM  action.circulation circ
1152             JOIN asset.copy cp ON (cp.id = circ.target_copy)
1153           WHERE circ.usr = match_user
1154                AND circ.circ_lib IN ( SELECT * FROM unnest(context_org_list) )
1155             AND circ.checkin_time IS NULL
1156             AND (circ.stop_fines IN ('MAXFINES','LONGOVERDUE') OR circ.stop_fines IS NULL)
1157             AND cp.circ_modifier IN (SELECT circ_mod FROM config.circ_matrix_circ_mod_test_map WHERE circ_mod_test = out_by_circ_mod.id);
1158         IF items_out >= out_by_circ_mod.items_out THEN
1159             result.fail_part := 'config.circ_matrix_circ_mod_test';
1160             result.success := FALSE;
1161             done := TRUE;
1162             RETURN NEXT result;
1163         END IF;
1164     END LOOP;
1165
1166     -- If we passed everything, return the successful matchpoint id
1167     IF NOT done THEN
1168         RETURN NEXT result;
1169     END IF;
1170
1171     RETURN;
1172 END;
1173 $func$ LANGUAGE plpgsql;
1174
1175 CREATE OR REPLACE FUNCTION action.item_user_circ_test( INT, BIGINT, INT ) RETURNS SETOF action.circ_matrix_test_result AS $func$
1176     SELECT * FROM action.item_user_circ_test( $1, $2, $3, FALSE );
1177 $func$ LANGUAGE SQL;
1178
1179 CREATE OR REPLACE FUNCTION action.item_user_renew_test( INT, BIGINT, INT ) RETURNS SETOF action.circ_matrix_test_result AS $func$
1180     SELECT * FROM action.item_user_circ_test( $1, $2, $3, TRUE );
1181 $func$ LANGUAGE SQL;
1182
1183 -- 0490
1184 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$
1185 DECLARE         
1186     ans RECORD; 
1187     trans INT;
1188 BEGIN           
1189     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;
1190
1191     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
1192         RETURN QUERY
1193         SELECT  ans.depth,
1194                 ans.id,
1195                 COUNT( cp.id ),
1196                 SUM( CASE WHEN cp.status IN (0,7,12) THEN 1 ELSE 0 END ),
1197                 COUNT( cp.id ),
1198                 trans
1199           FROM
1200                 actor.org_unit_descendants(ans.id) d
1201                 JOIN asset.copy cp ON (cp.circ_lib = d.id AND NOT cp.deleted)
1202                 JOIN asset.call_number cn ON (cn.record = rid AND cn.id = cp.call_number AND NOT cn.deleted)
1203           GROUP BY 1,2,6;
1204
1205         IF NOT FOUND THEN
1206             RETURN QUERY SELECT ans.depth, ans.id, 0::BIGINT, 0::BIGINT, 0::BIGINT, trans;
1207         END IF;
1208
1209     END LOOP;
1210
1211     RETURN;
1212 END;
1213 $f$ LANGUAGE PLPGSQL;
1214
1215 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$
1216 DECLARE
1217     ans RECORD;
1218     trans INT;
1219 BEGIN
1220     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;
1221
1222     FOR ans IN SELECT u.org_unit AS id FROM actor.org_lasso_map AS u WHERE lasso = i_lasso LOOP
1223         RETURN QUERY
1224         SELECT  -1,
1225                 ans.id,
1226                 COUNT( cp.id ),
1227                 SUM( CASE WHEN cp.status IN (0,7,12) THEN 1 ELSE 0 END ),
1228                 COUNT( cp.id ),
1229                 trans
1230           FROM
1231                 actor.org_unit_descendants(ans.id) d
1232                 JOIN asset.copy cp ON (cp.circ_lib = d.id AND NOT cp.deleted)
1233                 JOIN asset.call_number cn ON (cn.record = rid AND cn.id = cp.call_number AND NOT cn.deleted)
1234           GROUP BY 1,2,6;
1235
1236         IF NOT FOUND THEN
1237             RETURN QUERY SELECT ans.depth, ans.id, 0::BIGINT, 0::BIGINT, 0::BIGINT, trans;
1238         END IF;
1239
1240     END LOOP;
1241
1242     RETURN;
1243 END;
1244 $f$ LANGUAGE PLPGSQL;
1245
1246 DROP FUNCTION asset.staff_ou_metarecord_copy_count (INT, BIGINT);
1247 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$
1248 DECLARE         
1249     ans RECORD; 
1250     trans INT;
1251 BEGIN
1252     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;
1253
1254     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
1255         RETURN QUERY
1256         SELECT  ans.depth,
1257                 ans.id,
1258                 COUNT( cp.id ),
1259                 SUM( CASE WHEN cp.status IN (0,7,12) THEN 1 ELSE 0 END ),
1260                 COUNT( cp.id ),
1261                 trans
1262           FROM
1263                 actor.org_unit_descendants(ans.id) d
1264                 JOIN asset.copy cp ON (cp.circ_lib = d.id AND NOT cp.deleted)
1265                 JOIN asset.call_number cn ON (cn.record = rid AND cn.id = cp.call_number AND NOT cn.deleted)
1266                 JOIN metabib.metarecord_source_map m ON (m.source = cn.record)
1267           GROUP BY 1,2,6;
1268
1269         IF NOT FOUND THEN
1270             RETURN QUERY SELECT ans.depth, ans.id, 0::BIGINT, 0::BIGINT, 0::BIGINT, trans;
1271         END IF;
1272
1273     END LOOP;
1274
1275     RETURN;
1276 END;
1277 $f$ LANGUAGE PLPGSQL;
1278
1279 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$
1280 DECLARE
1281     ans RECORD;
1282     trans INT;
1283 BEGIN
1284     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;
1285
1286     FOR ans IN SELECT u.org_unit AS id FROM actor.org_lasso_map AS u WHERE lasso = i_lasso LOOP
1287         RETURN QUERY
1288         SELECT  -1,
1289                 ans.id,
1290                 COUNT( cp.id ),
1291                 SUM( CASE WHEN cp.status IN (0,7,12) THEN 1 ELSE 0 END ),
1292                 COUNT( cp.id ),
1293                 trans
1294           FROM
1295                 actor.org_unit_descendants(ans.id) d
1296                 JOIN asset.copy cp ON (cp.circ_lib = d.id AND NOT cp.deleted)
1297                 JOIN asset.call_number cn ON (cn.record = rid AND cn.id = cp.call_number AND NOT cn.deleted)
1298                 JOIN metabib.metarecord_source_map m ON (m.source = cn.record)
1299           GROUP BY 1,2,6;
1300
1301         IF NOT FOUND THEN
1302             RETURN QUERY SELECT ans.depth, ans.id, 0::BIGINT, 0::BIGINT, 0::BIGINT, trans;
1303         END IF;
1304
1305     END LOOP;
1306
1307     RETURN;
1308 END;
1309 $f$ LANGUAGE PLPGSQL;
1310
1311
1312 -- 0493
1313 UPDATE config.org_unit_setting_type
1314     SET description = 'Amount of time before a hold expires at which point the patron should be alerted. Examples: "5 days", "1 hour"'
1315     WHERE label = 'Holds: Expire Alert Interval';
1316
1317 UPDATE config.org_unit_setting_type
1318     SET description = 'When predicting the amount of time a patron will be waiting for a hold to be fulfilled, this is the default estimated length of time to assume an item will be checked out. Examples: "3 weeks", "7 days"'
1319     WHERE label = 'Holds: Default Estimated Wait';
1320
1321 UPDATE config.org_unit_setting_type
1322     SET description = 'When predicting the amount of time a patron will be waiting for a hold to be fulfilled, this is the minimum estimated length of time to assume an item will be checked out. Examples: "1 week", "5 days"'
1323     WHERE label = 'Holds: Minimum Estimated Wait';
1324
1325 UPDATE config.org_unit_setting_type
1326     SET description = 'The purpose is to provide an interval of time after an item goes into the on-holds-shelf status before it appears to patrons that it is actually on the holds shelf.  This gives staff time to process the item before it shows as ready-for-pickup. Examples: "5 days", "1 hour"'
1327     WHERE label = 'Hold Shelf Status Delay';
1328
1329 -- 0494
1330 UPDATE config.metabib_field
1331     SET xpath = $$//mods32:mods/mods32:subject$$
1332     WHERE field_class = 'subject' AND name = 'complete';
1333
1334 UPDATE config.metabib_field
1335     SET xpath = $$//marc:datafield[@tag='099']$$
1336     WHERE field_class = 'identifier' AND name = 'bibcn';
1337
1338 -- 0495
1339 CREATE TABLE config.record_attr_definition (
1340     name        TEXT    PRIMARY KEY,
1341     label       TEXT    NOT NULL, -- I18N
1342     description TEXT,
1343     filter      BOOL    NOT NULL DEFAULT TRUE,  -- becomes QP filter if true
1344     sorter      BOOL    NOT NULL DEFAULT FALSE, -- becomes QP sort() axis if true
1345
1346 -- For pre-extracted fields. Takes the first occurance, uses naive subfield ordering
1347     tag         TEXT, -- LIKE format
1348     sf_list     TEXT, -- pile-o-values, like 'abcd' for a and b and c and d
1349
1350 -- This is used for both tag/sf and xpath entries
1351     joiner      TEXT,
1352
1353 -- For xpath-extracted attrs
1354     xpath       TEXT,
1355     format      TEXT    REFERENCES config.xml_transform (name) ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED,
1356     start_pos   INT,
1357     string_len  INT,
1358
1359 -- For fixed fields
1360     fixed_field TEXT, -- should exist in config.marc21_ff_pos_map.fixed_field
1361
1362 -- For phys-char fields
1363     phys_char_sf    INT REFERENCES config.marc21_physical_characteristic_subfield_map (id)
1364 );
1365
1366 CREATE TABLE config.record_attr_index_norm_map (
1367     id      SERIAL  PRIMARY KEY,
1368     attr    TEXT    NOT NULL REFERENCES config.record_attr_definition (name) ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED,
1369     norm    INT     NOT NULL REFERENCES config.index_normalizer (id) ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED,
1370     params  TEXT,
1371     pos     INT     NOT NULL DEFAULT 0
1372 );
1373
1374 CREATE TABLE config.coded_value_map (
1375     id          SERIAL  PRIMARY KEY,
1376     ctype       TEXT    NOT NULL REFERENCES config.record_attr_definition (name) ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED,
1377     code        TEXT    NOT NULL,
1378     value       TEXT    NOT NULL,
1379     description TEXT
1380 );
1381
1382 -- record attributes
1383 INSERT INTO config.record_attr_definition (name,label,fixed_field) values ('alph','Alph','Alph');
1384 INSERT INTO config.record_attr_definition (name,label,fixed_field) values ('audience','Audn','Audn');
1385 INSERT INTO config.record_attr_definition (name,label,fixed_field) values ('bib_level','BLvl','BLvl');
1386 INSERT INTO config.record_attr_definition (name,label,fixed_field) values ('biog','Biog','Biog');
1387 INSERT INTO config.record_attr_definition (name,label,fixed_field) values ('conf','Conf','Conf');
1388 INSERT INTO config.record_attr_definition (name,label,fixed_field) values ('control_type','Ctrl','Ctrl');
1389 INSERT INTO config.record_attr_definition (name,label,fixed_field) values ('ctry','Ctry','Ctry');
1390 INSERT INTO config.record_attr_definition (name,label,fixed_field) values ('date1','Date1','Date1');
1391 INSERT INTO config.record_attr_definition (name,label,fixed_field,sorter,filter) values ('pubdate','Pub Date','Date1',TRUE,FALSE);
1392 INSERT INTO config.record_attr_definition (name,label,fixed_field) values ('date2','Date2','Date2');
1393 INSERT INTO config.record_attr_definition (name,label,fixed_field) values ('cat_form','Desc','Desc');
1394 INSERT INTO config.record_attr_definition (name,label,fixed_field) values ('pub_status','DtSt','DtSt');
1395 INSERT INTO config.record_attr_definition (name,label,fixed_field) values ('enc_level','ELvl','ELvl');
1396 INSERT INTO config.record_attr_definition (name,label,fixed_field) values ('fest','Fest','Fest');
1397 INSERT INTO config.record_attr_definition (name,label,fixed_field) values ('item_form','Form','Form');
1398 INSERT INTO config.record_attr_definition (name,label,fixed_field) values ('gpub','GPub','GPub');
1399 INSERT INTO config.record_attr_definition (name,label,fixed_field) values ('ills','Ills','Ills');
1400 INSERT INTO config.record_attr_definition (name,label,fixed_field) values ('indx','Indx','Indx');
1401 INSERT INTO config.record_attr_definition (name,label,fixed_field) values ('item_lang','Lang','Lang');
1402 INSERT INTO config.record_attr_definition (name,label,fixed_field) values ('lit_form','LitF','LitF');
1403 INSERT INTO config.record_attr_definition (name,label,fixed_field) values ('mrec','MRec','MRec');
1404 INSERT INTO config.record_attr_definition (name,label,fixed_field) values ('ff_sl','S/L','S/L');
1405 INSERT INTO config.record_attr_definition (name,label,fixed_field) values ('type_mat','TMat','TMat');
1406 INSERT INTO config.record_attr_definition (name,label,fixed_field) values ('item_type','Type','Type');
1407 INSERT INTO config.record_attr_definition (name,label,phys_char_sf) values ('vr_format','Videorecording format',72);
1408 INSERT INTO config.record_attr_definition (name,label,sorter,filter,tag) values ('titlesort','Title',TRUE,FALSE,'tnf');
1409 INSERT INTO config.record_attr_definition (name,label,sorter,filter,tag) values ('authorsort','Author',TRUE,FALSE,'1%');
1410
1411 INSERT INTO config.upgrade_log (version) VALUES ('0624'); -- miker/tsbere
1412 -- Cont was typod as Conf. Update the old entries.
1413 UPDATE config.marc21_ff_pos_map SET fixed_field = 'Cont' WHERE fixed_field = 'Conf' AND length > 1;
1414 -- Conf thus didn't exist. Add it.
1415 INSERT INTO config.marc21_ff_pos_map (fixed_field, tag, rec_type,start_pos, length, default_val) VALUES ('Conf', '006', 'BKS', 11, 1, ' ');
1416 INSERT INTO config.marc21_ff_pos_map (fixed_field, tag, rec_type,start_pos, length, default_val) VALUES ('Conf', '006', 'SER', 11, 1, ' ');
1417 INSERT INTO config.marc21_ff_pos_map (fixed_field, tag, rec_type,start_pos, length, default_val) VALUES ('Conf', '008', 'BKS', 29, 1, ' ');
1418 INSERT INTO config.marc21_ff_pos_map (fixed_field, tag, rec_type,start_pos, length, default_val) VALUES ('Conf', '008', 'SER', 29, 1, ' ');
1419
1420 INSERT INTO config.coded_value_map (ctype,code,value,description)
1421     SELECT 'item_lang' AS ctype, code, value, NULL FROM config.language_map
1422         UNION
1423     SELECT 'bib_level' AS ctype, code, value, NULL FROM config.bib_level_map
1424         UNION
1425     SELECT 'item_form' AS ctype, code, value, NULL FROM config.item_form_map
1426         UNION
1427     SELECT 'item_type' AS ctype, code, value, NULL FROM config.item_type_map
1428         UNION
1429     SELECT 'lit_form' AS ctype, code, value, description FROM config.lit_form_map
1430         UNION
1431     SELECT 'audience' AS ctype, code, value, description FROM config.audience_map
1432         UNION
1433     SELECT 'vr_format' AS ctype, code, value, NULL FROM config.videorecording_format_map;
1434
1435 ALTER TABLE config.i18n_locale DROP CONSTRAINT i18n_locale_marc_code_fkey;
1436
1437 DROP TABLE config.language_map;
1438 DROP TABLE config.bib_level_map;
1439 DROP TABLE config.item_form_map;
1440 DROP TABLE config.item_type_map;
1441 DROP TABLE config.lit_form_map;
1442 DROP TABLE config.audience_map;
1443 DROP TABLE config.videorecording_format_map;
1444
1445 UPDATE config.i18n_core SET fq_field = 'ccvm.value', identity_value = ccvm.id FROM config.coded_value_map AS ccvm WHERE fq_field = 'clm.value' AND ccvm.ctype = 'item_lang' AND identity_value = ccvm.code;
1446 UPDATE config.i18n_core SET fq_field = 'ccvm.value', identity_value = ccvm.id FROM config.coded_value_map AS ccvm WHERE fq_field = 'cblvl.value' AND ccvm.ctype = 'bib_level' AND identity_value = ccvm.code;
1447 UPDATE config.i18n_core SET fq_field = 'ccvm.value', identity_value = ccvm.id FROM config.coded_value_map AS ccvm WHERE fq_field = 'cifm.value' AND ccvm.ctype = 'item_form' AND identity_value = ccvm.code;
1448 UPDATE config.i18n_core SET fq_field = 'ccvm.value', identity_value = ccvm.id FROM config.coded_value_map AS ccvm WHERE fq_field = 'citm.value' AND ccvm.ctype = 'item_type' AND identity_value = ccvm.code;
1449 UPDATE config.i18n_core SET fq_field = 'ccvm.value', identity_value = ccvm.id FROM config.coded_value_map AS ccvm WHERE fq_field = 'clfm.value' AND ccvm.ctype = 'lit_form' AND identity_value = ccvm.code;
1450 UPDATE config.i18n_core SET fq_field = 'ccvm.value', identity_value = ccvm.id FROM config.coded_value_map AS ccvm WHERE fq_field = 'cam.value' AND ccvm.ctype = 'audience' AND identity_value = ccvm.code;
1451 UPDATE config.i18n_core SET fq_field = 'ccvm.value', identity_value = ccvm.id FROM config.coded_value_map AS ccvm WHERE fq_field = 'cvrfm.value' AND ccvm.ctype = 'vr_format' AND identity_value = ccvm.code;
1452
1453 UPDATE config.i18n_core SET fq_field = 'ccvm.description', identity_value = ccvm.id FROM config.coded_value_map AS ccvm WHERE fq_field = 'clfm.description' AND ccvm.ctype = 'lit_form' AND identity_value = ccvm.code;
1454 UPDATE config.i18n_core SET fq_field = 'ccvm.description', identity_value = ccvm.id FROM config.coded_value_map AS ccvm WHERE fq_field = 'cam.description' AND ccvm.ctype = 'audience' AND identity_value = ccvm.code;
1455
1456 CREATE VIEW config.language_map AS SELECT code, value FROM config.coded_value_map WHERE ctype = 'item_lang';
1457 CREATE VIEW config.bib_level_map AS SELECT code, value FROM config.coded_value_map WHERE ctype = 'bib_level';
1458 CREATE VIEW config.item_form_map AS SELECT code, value FROM config.coded_value_map WHERE ctype = 'item_form';
1459 CREATE VIEW config.item_type_map AS SELECT code, value FROM config.coded_value_map WHERE ctype = 'item_type';
1460 CREATE VIEW config.lit_form_map AS SELECT code, value, description FROM config.coded_value_map WHERE ctype = 'lit_form';
1461 CREATE VIEW config.audience_map AS SELECT code, value, description FROM config.coded_value_map WHERE ctype = 'audience';
1462 CREATE VIEW config.videorecording_format_map AS SELECT code, value FROM config.coded_value_map WHERE ctype = 'vr_format';
1463
1464 CREATE TABLE metabib.record_attr (
1465        id              BIGINT  PRIMARY KEY REFERENCES biblio.record_entry (id) ON DELETE CASCADE,
1466        attrs   HSTORE  NOT NULL DEFAULT ''::HSTORE
1467 );
1468 CREATE INDEX metabib_svf_attrs_idx ON metabib.record_attr USING GIST (attrs);
1469 CREATE INDEX metabib_svf_date1_idx ON metabib.record_attr ( (attrs->'date1') );
1470 CREATE INDEX metabib_svf_dates_idx ON metabib.record_attr ( (attrs->'date1'), (attrs->'date2') );
1471
1472 INSERT INTO metabib.record_attr (id,attrs)
1473     SELECT DISTINCT ON (mrd.record) mrd.record, hstore(mrd) - '{id,record}'::TEXT[] FROM metabib.rec_descriptor mrd;
1474
1475 -- Back-compat view ... we're moving to an HSTORE world
1476 CREATE TYPE metabib.rec_desc_type AS (
1477     item_type       TEXT,
1478     item_form       TEXT,
1479     bib_level       TEXT,
1480     control_type    TEXT,
1481     char_encoding   TEXT,
1482     enc_level       TEXT,
1483     audience        TEXT,
1484     lit_form        TEXT,
1485     type_mat        TEXT,
1486     cat_form        TEXT,
1487     pub_status      TEXT,
1488     item_lang       TEXT,
1489     vr_format       TEXT,
1490     date1           TEXT,
1491     date2           TEXT
1492 );
1493
1494 DROP TABLE metabib.rec_descriptor CASCADE;
1495
1496 CREATE VIEW metabib.rec_descriptor AS
1497     SELECT  id,
1498             id AS record,
1499             (populate_record(NULL::metabib.rec_desc_type, attrs)).*
1500       FROM  metabib.record_attr;
1501
1502 CREATE OR REPLACE FUNCTION vandelay.marc21_record_type( marc TEXT ) RETURNS config.marc21_rec_type_map AS $func$
1503 DECLARE
1504     ldr         TEXT;
1505     tval        TEXT;
1506     tval_rec    RECORD;
1507     bval        TEXT;
1508     bval_rec    RECORD;
1509     retval      config.marc21_rec_type_map%ROWTYPE;
1510 BEGIN
1511     ldr := oils_xpath_string( '//*[local-name()="leader"]', marc );
1512
1513     IF ldr IS NULL OR ldr = '' THEN
1514         SELECT * INTO retval FROM config.marc21_rec_type_map WHERE code = 'BKS';
1515         RETURN retval;
1516     END IF;
1517
1518     SELECT * INTO tval_rec FROM config.marc21_ff_pos_map WHERE fixed_field = 'Type' LIMIT 1; -- They're all the same
1519     SELECT * INTO bval_rec FROM config.marc21_ff_pos_map WHERE fixed_field = 'BLvl' LIMIT 1; -- They're all the same
1520
1521
1522     tval := SUBSTRING( ldr, tval_rec.start_pos + 1, tval_rec.length );
1523     bval := SUBSTRING( ldr, bval_rec.start_pos + 1, bval_rec.length );
1524
1525     -- RAISE NOTICE 'type %, blvl %, ldr %', tval, bval, ldr;
1526
1527     SELECT * INTO retval FROM config.marc21_rec_type_map WHERE type_val LIKE '%' || tval || '%' AND blvl_val LIKE '%' || bval || '%';
1528
1529
1530     IF retval.code IS NULL THEN
1531         SELECT * INTO retval FROM config.marc21_rec_type_map WHERE code = 'BKS';
1532     END IF;
1533
1534     RETURN retval;
1535 END;
1536 $func$ LANGUAGE PLPGSQL;
1537
1538 CREATE OR REPLACE FUNCTION biblio.marc21_record_type( rid BIGINT ) RETURNS config.marc21_rec_type_map AS $func$
1539     SELECT * FROM vandelay.marc21_record_type( (SELECT marc FROM biblio.record_entry WHERE id = $1) );
1540 $func$ LANGUAGE SQL;
1541
1542 CREATE OR REPLACE FUNCTION vandelay.marc21_extract_fixed_field( marc TEXT, ff TEXT ) RETURNS TEXT AS $func$
1543 DECLARE
1544     rtype       TEXT;
1545     ff_pos      RECORD;
1546     tag_data    RECORD;
1547     val         TEXT;
1548 BEGIN
1549     rtype := (vandelay.marc21_record_type( marc )).code;
1550     FOR ff_pos IN SELECT * FROM config.marc21_ff_pos_map WHERE fixed_field = ff AND rec_type = rtype ORDER BY tag DESC LOOP
1551         FOR tag_data IN SELECT value FROM UNNEST( oils_xpath( '//*[@tag="' || UPPER(ff_pos.tag) || '"]/text()', marc ) ) x(value) LOOP
1552             val := SUBSTRING( tag_data.value, ff_pos.start_pos + 1, ff_pos.length );
1553             RETURN val;
1554         END LOOP;
1555         val := REPEAT( ff_pos.default_val, ff_pos.length );
1556         RETURN val;
1557     END LOOP;
1558
1559     RETURN NULL;
1560 END;
1561 $func$ LANGUAGE PLPGSQL;
1562
1563 CREATE OR REPLACE FUNCTION biblio.marc21_extract_fixed_field( rid BIGINT, ff TEXT ) RETURNS TEXT AS $func$
1564     SELECT * FROM vandelay.marc21_extract_fixed_field( (SELECT marc FROM biblio.record_entry WHERE id = $1), $2 );
1565 $func$ LANGUAGE SQL;
1566
1567 CREATE TYPE biblio.record_ff_map AS (record BIGINT, ff_name TEXT, ff_value TEXT);
1568 CREATE OR REPLACE FUNCTION vandelay.marc21_extract_all_fixed_fields( marc TEXT ) RETURNS SETOF biblio.record_ff_map AS $func$
1569 DECLARE
1570     tag_data    TEXT;
1571     rtype       TEXT;
1572     ff_pos      RECORD;
1573     output      biblio.record_ff_map%ROWTYPE;
1574 BEGIN
1575     rtype := (vandelay.marc21_record_type( marc )).code;
1576
1577     FOR ff_pos IN SELECT * FROM config.marc21_ff_pos_map WHERE rec_type = rtype ORDER BY tag DESC LOOP
1578         output.ff_name  := ff_pos.fixed_field;
1579         output.ff_value := NULL;
1580
1581         FOR tag_data IN SELECT value FROM UNNEST( oils_xpath( '//*[@tag="' || UPPER(tag) || '"]/text()', marc ) ) x(value) LOOP
1582             output.ff_value := SUBSTRING( tag_data.value, ff_pos.start_pos + 1, ff_pos.length );
1583             IF output.ff_value IS NULL THEN output.ff_value := REPEAT( ff_pos.default_val, ff_pos.length ); END IF;
1584             RETURN NEXT output;
1585             output.ff_value := NULL;
1586         END LOOP;
1587
1588     END LOOP;
1589
1590     RETURN;
1591 END;
1592 $func$ LANGUAGE PLPGSQL;
1593
1594 CREATE OR REPLACE FUNCTION biblio.marc21_extract_all_fixed_fields( rid BIGINT ) RETURNS SETOF biblio.record_ff_map AS $func$
1595     SELECT $1 AS record, ff_name, ff_value FROM vandelay.marc21_extract_all_fixed_fields( (SELECT marc FROM biblio.record_entry WHERE id = $1) );
1596 $func$ LANGUAGE SQL;
1597
1598 CREATE OR REPLACE FUNCTION vandelay.marc21_physical_characteristics( marc TEXT) RETURNS SETOF biblio.marc21_physical_characteristics AS $func$
1599 DECLARE
1600     rowid   INT := 0;
1601     _007    TEXT;
1602     ptype   config.marc21_physical_characteristic_type_map%ROWTYPE;
1603     psf     config.marc21_physical_characteristic_subfield_map%ROWTYPE;
1604     pval    config.marc21_physical_characteristic_value_map%ROWTYPE;
1605     retval  biblio.marc21_physical_characteristics%ROWTYPE;
1606 BEGIN
1607
1608     _007 := oils_xpath_string( '//*[@tag="007"]', marc );
1609
1610     IF _007 IS NOT NULL AND _007 <> '' THEN
1611         SELECT * INTO ptype FROM config.marc21_physical_characteristic_type_map WHERE ptype_key = SUBSTRING( _007, 1, 1 );
1612
1613         IF ptype.ptype_key IS NOT NULL THEN
1614             FOR psf IN SELECT * FROM config.marc21_physical_characteristic_subfield_map WHERE ptype_key = ptype.ptype_key LOOP
1615                 SELECT * INTO pval FROM config.marc21_physical_characteristic_value_map WHERE ptype_subfield = psf.id AND value = SUBSTRING( _007, psf.start_pos + 1, psf.length );
1616
1617                 IF pval.id IS NOT NULL THEN
1618                     rowid := rowid + 1;
1619                     retval.id := rowid;
1620                     retval.ptype := ptype.ptype_key;
1621                     retval.subfield := psf.id;
1622                     retval.value := pval.id;
1623                     RETURN NEXT retval;
1624                 END IF;
1625
1626             END LOOP;
1627         END IF;
1628     END IF;
1629
1630     RETURN;
1631 END;
1632 $func$ LANGUAGE PLPGSQL;
1633
1634 CREATE OR REPLACE FUNCTION biblio.marc21_physical_characteristics( rid BIGINT ) RETURNS SETOF biblio.marc21_physical_characteristics AS $func$
1635     SELECT id, $1 AS record, ptype, subfield, value FROM vandelay.marc21_physical_characteristics( (SELECT marc FROM biblio.record_entry WHERE id = $1) );
1636 $func$ LANGUAGE SQL;
1637
1638 CREATE OR REPLACE FUNCTION biblio.indexing_ingest_or_delete () RETURNS TRIGGER AS $func$
1639 DECLARE
1640     transformed_xml TEXT;
1641     prev_xfrm       TEXT;
1642     normalizer      RECORD;
1643     xfrm            config.xml_transform%ROWTYPE;
1644     attr_value      TEXT;
1645     new_attrs       HSTORE := ''::HSTORE;
1646     attr_def        config.record_attr_definition%ROWTYPE;
1647 BEGIN
1648
1649     IF NEW.deleted IS TRUE THEN -- If this bib is deleted
1650         DELETE FROM metabib.metarecord_source_map WHERE source = NEW.id; -- Rid ourselves of the search-estimate-killing linkage
1651         DELETE FROM metabib.record_attr WHERE id = NEW.id; -- Kill the attrs hash, useless on deleted records
1652         DELETE FROM authority.bib_linking WHERE bib = NEW.id; -- Avoid updating fields in bibs that are no longer visible
1653         RETURN NEW; -- and we're done
1654     END IF;
1655
1656     IF TG_OP = 'UPDATE' THEN -- re-ingest?
1657         PERFORM * FROM config.internal_flag WHERE name = 'ingest.reingest.force_on_same_marc' AND enabled;
1658
1659         IF NOT FOUND AND OLD.marc = NEW.marc THEN -- don't do anything if the MARC didn't change
1660             RETURN NEW;
1661         END IF;
1662     END IF;
1663
1664     -- Record authority linking
1665     PERFORM * FROM config.internal_flag WHERE name = 'ingest.disable_authority_linking' AND enabled;
1666     IF NOT FOUND THEN
1667         PERFORM biblio.map_authority_linking( NEW.id, NEW.marc );
1668     END IF;
1669
1670     -- Flatten and insert the mfr data
1671     PERFORM * FROM config.internal_flag WHERE name = 'ingest.disable_metabib_full_rec' AND enabled;
1672     IF NOT FOUND THEN
1673         PERFORM metabib.reingest_metabib_full_rec(NEW.id);
1674
1675         -- Now we pull out attribute data, which is dependent on the mfr for all but XPath-based fields
1676         PERFORM * FROM config.internal_flag WHERE name = 'ingest.disable_metabib_rec_descriptor' AND enabled;
1677         IF NOT FOUND THEN
1678             FOR attr_def IN SELECT * FROM config.record_attr_definition ORDER BY format LOOP
1679
1680                 IF attr_def.tag IS NOT NULL THEN -- tag (and optional subfield list) selection
1681                     SELECT  ARRAY_TO_STRING(ARRAY_ACCUM(value), COALESCE(attr_def.joiner,' ')) INTO attr_value
1682                       FROM  (SELECT * FROM metabib.full_rec ORDER BY tag, subfield) AS x
1683                       WHERE record = NEW.id
1684                             AND tag LIKE attr_def.tag
1685                             AND CASE
1686                                 WHEN attr_def.sf_list IS NOT NULL
1687                                     THEN POSITION(subfield IN attr_def.sf_list) > 0
1688                                 ELSE TRUE
1689                                 END
1690                       GROUP BY tag
1691                       ORDER BY tag
1692                       LIMIT 1;
1693
1694                 ELSIF attr_def.fixed_field IS NOT NULL THEN -- a named fixed field, see config.marc21_ff_pos_map.fixed_field
1695                     attr_value := biblio.marc21_extract_fixed_field(NEW.id, attr_def.fixed_field);
1696
1697                 ELSIF attr_def.xpath IS NOT NULL THEN -- and xpath expression
1698
1699                     SELECT INTO xfrm * FROM config.xml_transform WHERE name = attr_def.format;
1700
1701                     -- See if we can skip the XSLT ... it's expensive
1702                     IF prev_xfrm IS NULL OR prev_xfrm <> xfrm.name THEN
1703                         -- Can't skip the transform
1704                         IF xfrm.xslt <> '---' THEN
1705                             transformed_xml := oils_xslt_process(NEW.marc,xfrm.xslt);
1706                         ELSE
1707                             transformed_xml := NEW.marc;
1708                         END IF;
1709
1710                         prev_xfrm := xfrm.name;
1711                     END IF;
1712
1713                     IF xfrm.name IS NULL THEN
1714                         -- just grab the marcxml (empty) transform
1715                         SELECT INTO xfrm * FROM config.xml_transform WHERE xslt = '---' LIMIT 1;
1716                         prev_xfrm := xfrm.name;
1717                     END IF;
1718
1719                     attr_value := oils_xpath_string(attr_def.xpath, transformed_xml, COALESCE(attr_def.joiner,' '), ARRAY[ARRAY[xfrm.prefix, xfrm.namespace_uri]]);
1720
1721                 ELSIF attr_def.phys_char_sf IS NOT NULL THEN -- a named Physical Characteristic, see config.marc21_physical_characteristic_*_map
1722                     SELECT  value::TEXT INTO attr_value
1723                       FROM  biblio.marc21_physical_characteristics(NEW.id)
1724                       WHERE subfield = attr_def.phys_char_sf
1725                       LIMIT 1; -- Just in case ...
1726
1727                 END IF;
1728
1729                 -- apply index normalizers to attr_value
1730                 FOR normalizer IN
1731                     SELECT  n.func AS func,
1732                             n.param_count AS param_count,
1733                             m.params AS params
1734                       FROM  config.index_normalizer n
1735                             JOIN config.record_attr_index_norm_map m ON (m.norm = n.id)
1736                       WHERE attr = attr_def.name
1737                       ORDER BY m.pos LOOP
1738                         EXECUTE 'SELECT ' || normalizer.func || '(' ||
1739                             quote_literal( attr_value ) ||
1740                             CASE
1741                                 WHEN normalizer.param_count > 0
1742                                     THEN ',' || REPLACE(REPLACE(BTRIM(normalizer.params,'[]'),E'\'',E'\\\''),E'"',E'\'')
1743                                     ELSE ''
1744                                 END ||
1745                             ')' INTO attr_value;
1746
1747                 END LOOP;
1748
1749                 -- Add the new value to the hstore
1750                 new_attrs := new_attrs || hstore( attr_def.name, attr_value );
1751
1752             END LOOP;
1753
1754             IF TG_OP = 'INSERT' OR OLD.deleted THEN -- initial insert OR revivication
1755                 INSERT INTO metabib.record_attr (id, attrs) VALUES (NEW.id, new_attrs);
1756             ELSE
1757                 UPDATE metabib.record_attr SET attrs = attrs || new_attrs WHERE id = NEW.id;
1758             END IF;
1759
1760         END IF;
1761     END IF;
1762
1763     -- Gather and insert the field entry data
1764     PERFORM metabib.reingest_metabib_field_entries(NEW.id);
1765
1766     -- Located URI magic
1767     IF TG_OP = 'INSERT' THEN
1768         PERFORM * FROM config.internal_flag WHERE name = 'ingest.disable_located_uri' AND enabled;
1769         IF NOT FOUND THEN
1770             PERFORM biblio.extract_located_uris( NEW.id, NEW.marc, NEW.editor );
1771         END IF;
1772     ELSE
1773         PERFORM * FROM config.internal_flag WHERE name = 'ingest.disable_located_uri' AND enabled;
1774         IF NOT FOUND THEN
1775             PERFORM biblio.extract_located_uris( NEW.id, NEW.marc, NEW.editor );
1776         END IF;
1777     END IF;
1778
1779     -- (re)map metarecord-bib linking
1780     IF TG_OP = 'INSERT' THEN -- if not deleted and performing an insert, check for the flag
1781         PERFORM * FROM config.internal_flag WHERE name = 'ingest.metarecord_mapping.skip_on_insert' AND enabled;
1782         IF NOT FOUND THEN
1783             PERFORM metabib.remap_metarecord_for_bib( NEW.id, NEW.fingerprint );
1784         END IF;
1785     ELSE -- we're doing an update, and we're not deleted, remap
1786         PERFORM * FROM config.internal_flag WHERE name = 'ingest.metarecord_mapping.skip_on_update' AND enabled;
1787         IF NOT FOUND THEN
1788             PERFORM metabib.remap_metarecord_for_bib( NEW.id, NEW.fingerprint );
1789         END IF;
1790     END IF;
1791
1792     RETURN NEW;
1793 END;
1794 $func$ LANGUAGE PLPGSQL;
1795
1796 DROP FUNCTION metabib.reingest_metabib_rec_descriptor( bib_id BIGINT );
1797
1798 CREATE OR REPLACE FUNCTION public.approximate_date( TEXT, TEXT ) RETURNS TEXT AS $func$
1799         SELECT REGEXP_REPLACE( $1, E'\\D', $2, 'g' );
1800 $func$ LANGUAGE SQL STRICT IMMUTABLE;
1801
1802 CREATE OR REPLACE FUNCTION public.approximate_low_date( TEXT ) RETURNS TEXT AS $func$
1803         SELECT approximate_date( $1, '0');
1804 $func$ LANGUAGE SQL STRICT IMMUTABLE;
1805
1806 CREATE OR REPLACE FUNCTION public.approximate_high_date( TEXT ) RETURNS TEXT AS $func$
1807         SELECT approximate_date( $1, '9');
1808 $func$ LANGUAGE SQL STRICT IMMUTABLE;
1809
1810 CREATE OR REPLACE FUNCTION public.integer_or_null( TEXT ) RETURNS TEXT AS $func$
1811         SELECT CASE WHEN $1 ~ E'^\\d+$' THEN $1 ELSE NULL END
1812 $func$ LANGUAGE SQL STRICT IMMUTABLE;
1813
1814 CREATE OR REPLACE FUNCTION public.content_or_null( TEXT ) RETURNS TEXT AS $func$
1815         SELECT CASE WHEN $1 ~ E'^\\s*$' THEN NULL ELSE $1 END
1816 $func$ LANGUAGE SQL STRICT IMMUTABLE;
1817
1818 CREATE OR REPLACE FUNCTION public.force_to_isbn13( TEXT ) RETURNS TEXT AS $func$
1819     use Business::ISBN;
1820     use strict;
1821     use warnings;
1822
1823     # Find the first ISBN, force it to ISBN13 and return it
1824
1825     my $input = shift;
1826
1827     foreach my $word (split(/\s/, $input)) {
1828         my $isbn = Business::ISBN->new($word);
1829
1830         # First check the checksum; if it is not valid, fix it and add the original
1831         # bad-checksum ISBN to the output
1832         if ($isbn && $isbn->is_valid_checksum() == Business::ISBN::BAD_CHECKSUM) {
1833             $isbn->fix_checksum();
1834         }
1835
1836         # If we now have a valid ISBN, force it to ISBN13 and return it
1837         return $isbn->as_isbn13->isbn if ($isbn && $isbn->is_valid());
1838     }
1839     return undef;
1840 $func$ LANGUAGE PLPERLU;
1841
1842 COMMENT ON FUNCTION public.force_to_isbn13(TEXT) IS $$
1843 /*
1844  * Copyright (C) 2011 Equinox Software
1845  * Mike Rylander <mrylander@gmail.com>
1846  *
1847  * Inspired by translate_isbn1013
1848  *
1849  * The force_to_isbn13 function takes an input ISBN and returns the ISBN13
1850  * version without hypens and with a repaired checksum if the checksum was bad
1851  */
1852 $$;
1853
1854 -- 0496
1855 UPDATE config.metabib_field
1856     SET xpath = $$//marc:datafield[@tag='024' and @ind1='1']/marc:subfield[@code='a' or @code='z']$$
1857     WHERE field_class = 'identifier' AND name = 'upc';
1858
1859 UPDATE config.metabib_field
1860     SET xpath = $$//marc:datafield[@tag='024' and @ind1='2']/marc:subfield[@code='a' or @code='z']$$
1861     WHERE field_class = 'identifier' AND name = 'ismn';
1862
1863 UPDATE config.metabib_field
1864     SET xpath = $$//marc:datafield[@tag='024' and @ind1='3']/marc:subfield[@code='a' or @code='z']$$
1865     WHERE field_class = 'identifier' AND name = 'ean';
1866
1867 UPDATE config.metabib_field
1868     SET xpath = $$//marc:datafield[@tag='024' and @ind1='0']/marc:subfield[@code='a' or @code='z']$$
1869     WHERE field_class = 'identifier' AND name = 'isrc';
1870
1871 UPDATE config.metabib_field
1872     SET xpath = $$//marc:datafield[@tag='024' and @ind1='4']/marc:subfield[@code='a' or @code='z']$$
1873     WHERE field_class = 'identifier' AND name = 'sici';
1874
1875 -- 0497
1876 INSERT into config.org_unit_setting_type
1877 ( name, label, description, datatype ) VALUES
1878
1879 ( 'ui.patron.edit.au.active.show',
1880     oils_i18n_gettext('ui.patron.edit.au.active.show', 'GUI: Show active field on patron registration', 'coust', 'label'),
1881     oils_i18n_gettext('ui.patron.edit.au.active.show', 'The active field will be shown on the patron registration screen. Showing a field makes it appear with required fields even when not required. If the field is required this setting is ignored.', 'coust', 'description'),
1882     'bool'),
1883 ( 'ui.patron.edit.au.active.suggest',
1884     oils_i18n_gettext('ui.patron.edit.au.active.suggest', 'GUI: Suggest active field on patron registration', 'coust', 'label'),
1885     oils_i18n_gettext('ui.patron.edit.au.active.suggest', 'The active field will be suggested on the patron registration screen. Suggesting a field makes it appear when suggested fields are shown. If the field is shown or required this setting is ignored.', 'coust', 'description'),
1886     'bool'),
1887 ( 'ui.patron.edit.au.alert_message.show',
1888     oils_i18n_gettext('ui.patron.edit.au.alert_message.show', 'GUI: Show alert_message field on patron registration', 'coust', 'label'),
1889     oils_i18n_gettext('ui.patron.edit.au.alert_message.show', 'The alert_message field will be shown on the patron registration screen. Showing a field makes it appear with required fields even when not required. If the field is required this setting is ignored.', 'coust', 'description'),
1890     'bool'),
1891 ( 'ui.patron.edit.au.alert_message.suggest',
1892     oils_i18n_gettext('ui.patron.edit.au.alert_message.suggest', 'GUI: Suggest alert_message field on patron registration', 'coust', 'label'),
1893     oils_i18n_gettext('ui.patron.edit.au.alert_message.suggest', 'The alert_message field will be suggested on the patron registration screen. Suggesting a field makes it appear when suggested fields are shown. If the field is shown or required this setting is ignored.', 'coust', 'description'),
1894     'bool'),
1895 ( 'ui.patron.edit.au.alias.show',
1896     oils_i18n_gettext('ui.patron.edit.au.alias.show', 'GUI: Show alias field on patron registration', 'coust', 'label'),
1897     oils_i18n_gettext('ui.patron.edit.au.alias.show', 'The alias field will be shown on the patron registration screen. Showing a field makes it appear with required fields even when not required. If the field is required this setting is ignored.', 'coust', 'description'),
1898     'bool'),
1899 ( 'ui.patron.edit.au.alias.suggest',
1900     oils_i18n_gettext('ui.patron.edit.au.alias.suggest', 'GUI: Suggest alias field on patron registration', 'coust', 'label'),
1901     oils_i18n_gettext('ui.patron.edit.au.alias.suggest', 'The alias field will be suggested on the patron registration screen. Suggesting a field makes it appear when suggested fields are shown. If the field is shown or required this setting is ignored.', 'coust', 'description'),
1902     'bool'),
1903 ( 'ui.patron.edit.au.barred.show',
1904     oils_i18n_gettext('ui.patron.edit.au.barred.show', 'GUI: Show barred field on patron registration', 'coust', 'label'),
1905     oils_i18n_gettext('ui.patron.edit.au.barred.show', 'The barred field will be shown on the patron registration screen. Showing a field makes it appear with required fields even when not required. If the field is required this setting is ignored.', 'coust', 'description'),
1906     'bool'),
1907 ( 'ui.patron.edit.au.barred.suggest',
1908     oils_i18n_gettext('ui.patron.edit.au.barred.suggest', 'GUI: Suggest barred field on patron registration', 'coust', 'label'),
1909     oils_i18n_gettext('ui.patron.edit.au.barred.suggest', 'The barred field will be suggested on the patron registration screen. Suggesting a field makes it appear when suggested fields are shown. If the field is shown or required this setting is ignored.', 'coust', 'description'),
1910     'bool'),
1911 ( 'ui.patron.edit.au.claims_never_checked_out_count.show',
1912     oils_i18n_gettext('ui.patron.edit.au.claims_never_checked_out_count.show', 'GUI: Show claims_never_checked_out_count field on patron registration', 'coust', 'label'),
1913     oils_i18n_gettext('ui.patron.edit.au.claims_never_checked_out_count.show', 'The claims_never_checked_out_count field will be shown on the patron registration screen. Showing a field makes it appear with required fields even when not required. If the field is required this setting is ignored.', 'coust', 'description'),
1914     'bool'),
1915 ( 'ui.patron.edit.au.claims_never_checked_out_count.suggest',
1916     oils_i18n_gettext('ui.patron.edit.au.claims_never_checked_out_count.suggest', 'GUI: Suggest claims_never_checked_out_count field on patron registration', 'coust', 'label'),
1917     oils_i18n_gettext('ui.patron.edit.au.claims_never_checked_out_count.suggest', 'The claims_never_checked_out_count field will be suggested on the patron registration screen. Suggesting a field makes it appear when suggested fields are shown. If the field is shown or required this setting is ignored.', 'coust', 'description'),
1918     'bool'),
1919 ( 'ui.patron.edit.au.claims_returned_count.show',
1920     oils_i18n_gettext('ui.patron.edit.au.claims_returned_count.show', 'GUI: Show claims_returned_count field on patron registration', 'coust', 'label'),
1921     oils_i18n_gettext('ui.patron.edit.au.claims_returned_count.show', 'The claims_returned_count field will be shown on the patron registration screen. Showing a field makes it appear with required fields even when not required. If the field is required this setting is ignored.', 'coust', 'description'),
1922     'bool'),
1923 ( 'ui.patron.edit.au.claims_returned_count.suggest',
1924     oils_i18n_gettext('ui.patron.edit.au.claims_returned_count.suggest', 'GUI: Suggest claims_returned_count field on patron registration', 'coust', 'label'),
1925     oils_i18n_gettext('ui.patron.edit.au.claims_returned_count.suggest', 'The claims_returned_count field will be suggested on the patron registration screen. Suggesting a field makes it appear when suggested fields are shown. If the field is shown or required this setting is ignored.', 'coust', 'description'),
1926     'bool'),
1927 ( 'ui.patron.edit.au.day_phone.example',
1928     oils_i18n_gettext('ui.patron.edit.au.day_phone.example', 'GUI: Example for day_phone field on patron registration', 'coust', 'label'),
1929     oils_i18n_gettext('ui.patron.edit.au.day_phone.example', 'The Example for validation on the day_phone field in patron registration.', 'coust', 'description'),
1930     'string'),
1931 ( 'ui.patron.edit.au.day_phone.regex',
1932     oils_i18n_gettext('ui.patron.edit.au.day_phone.regex', 'GUI: Regex for day_phone field on patron registration', 'coust', 'label'),
1933     oils_i18n_gettext('ui.patron.edit.au.day_phone.regex', 'The Regular Expression for validation on the day_phone field in patron registration.', 'coust', 'description'),
1934     'string'),
1935 ( 'ui.patron.edit.au.day_phone.require',
1936     oils_i18n_gettext('ui.patron.edit.au.day_phone.require', 'GUI: Require day_phone field on patron registration', 'coust', 'label'),
1937     oils_i18n_gettext('ui.patron.edit.au.day_phone.require', 'The day_phone field will be required on the patron registration screen.', 'coust', 'description'),
1938     'bool'),
1939 ( 'ui.patron.edit.au.day_phone.show',
1940     oils_i18n_gettext('ui.patron.edit.au.day_phone.show', 'GUI: Show day_phone field on patron registration', 'coust', 'label'),
1941     oils_i18n_gettext('ui.patron.edit.au.day_phone.show', 'The day_phone field will be shown on the patron registration screen. Showing a field makes it appear with required fields even when not required. If the field is required this setting is ignored.', 'coust', 'description'),
1942     'bool'),
1943 ( 'ui.patron.edit.au.day_phone.suggest',
1944     oils_i18n_gettext('ui.patron.edit.au.day_phone.suggest', 'GUI: Suggest day_phone field on patron registration', 'coust', 'label'),
1945     oils_i18n_gettext('ui.patron.edit.au.day_phone.suggest', 'The day_phone field will be suggested on the patron registration screen. Suggesting a field makes it appear when suggested fields are shown. If the field is shown or required this setting is ignored.', 'coust', 'description'),
1946     'bool'),
1947 ( 'ui.patron.edit.au.dob.calendar',
1948     oils_i18n_gettext('ui.patron.edit.au.dob.calendar', 'GUI: Show calendar widget for dob field on patron registration', 'coust', 'label'),
1949     oils_i18n_gettext('ui.patron.edit.au.dob.calendar', 'If set the calendar widget will appear when editing the dob field on the patron registration form.', 'coust', 'description'),
1950     'bool'),
1951 ( 'ui.patron.edit.au.dob.require',
1952     oils_i18n_gettext('ui.patron.edit.au.dob.require', 'GUI: Require dob field on patron registration', 'coust', 'label'),
1953     oils_i18n_gettext('ui.patron.edit.au.dob.require', 'The dob field will be required on the patron registration screen.', 'coust', 'description'),
1954     'bool'),
1955 ( 'ui.patron.edit.au.dob.show',
1956     oils_i18n_gettext('ui.patron.edit.au.dob.show', 'GUI: Show dob field on patron registration', 'coust', 'label'),
1957     oils_i18n_gettext('ui.patron.edit.au.dob.show', 'The dob field will be shown on the patron registration screen. Showing a field makes it appear with required fields even when not required. If the field is required this setting is ignored.', 'coust', 'description'),
1958     'bool'),
1959 ( 'ui.patron.edit.au.dob.suggest',
1960     oils_i18n_gettext('ui.patron.edit.au.dob.suggest', 'GUI: Suggest dob field on patron registration', 'coust', 'label'),
1961     oils_i18n_gettext('ui.patron.edit.au.dob.suggest', 'The dob field will be suggested on the patron registration screen. Suggesting a field makes it appear when suggested fields are shown. If the field is shown or required this setting is ignored.', 'coust', 'description'),
1962     'bool'),
1963 ( 'ui.patron.edit.au.email.example',
1964     oils_i18n_gettext('ui.patron.edit.au.email.example', 'GUI: Example for email field on patron registration', 'coust', 'label'),
1965     oils_i18n_gettext('ui.patron.edit.au.email.example', 'The Example for validation on the email field in patron registration.', 'coust', 'description'),
1966     'string'),
1967 ( 'ui.patron.edit.au.email.regex',
1968     oils_i18n_gettext('ui.patron.edit.au.email.regex', 'GUI: Regex for email field on patron registration', 'coust', 'label'),
1969     oils_i18n_gettext('ui.patron.edit.au.email.regex', 'The Regular Expression for validation on the email field in patron registration.', 'coust', 'description'),
1970     'string'),
1971 ( 'ui.patron.edit.au.email.require',
1972     oils_i18n_gettext('ui.patron.edit.au.email.require', 'GUI: Require email field on patron registration', 'coust', 'label'),
1973     oils_i18n_gettext('ui.patron.edit.au.email.require', 'The email field will be required on the patron registration screen.', 'coust', 'description'),
1974     'bool'),
1975 ( 'ui.patron.edit.au.email.show',
1976     oils_i18n_gettext('ui.patron.edit.au.email.show', 'GUI: Show email field on patron registration', 'coust', 'label'),
1977     oils_i18n_gettext('ui.patron.edit.au.email.show', 'The email field will be shown on the patron registration screen. Showing a field makes it appear with required fields even when not required. If the field is required this setting is ignored.', 'coust', 'description'),
1978     'bool'),
1979 ( 'ui.patron.edit.au.email.suggest',
1980     oils_i18n_gettext('ui.patron.edit.au.email.suggest', 'GUI: Suggest email field on patron registration', 'coust', 'label'),
1981     oils_i18n_gettext('ui.patron.edit.au.email.suggest', 'The email field will be suggested on the patron registration screen. Suggesting a field makes it appear when suggested fields are shown. If the field is shown or required this setting is ignored.', 'coust', 'description'),
1982     'bool'),
1983 ( 'ui.patron.edit.au.evening_phone.example',
1984     oils_i18n_gettext('ui.patron.edit.au.evening_phone.example', 'GUI: Example for evening_phone field on patron registration', 'coust', 'label'),
1985     oils_i18n_gettext('ui.patron.edit.au.evening_phone.example', 'The Example for validation on the evening_phone field in patron registration.', 'coust', 'description'),
1986     'string'),
1987 ( 'ui.patron.edit.au.evening_phone.regex',
1988     oils_i18n_gettext('ui.patron.edit.au.evening_phone.regex', 'GUI: Regex for evening_phone field on patron registration', 'coust', 'label'),
1989     oils_i18n_gettext('ui.patron.edit.au.evening_phone.regex', 'The Regular Expression for validation on the evening_phone field in patron registration.', 'coust', 'description'),
1990     'string'),
1991 ( 'ui.patron.edit.au.evening_phone.require',
1992     oils_i18n_gettext('ui.patron.edit.au.evening_phone.require', 'GUI: Require evening_phone field on patron registration', 'coust', 'label'),
1993     oils_i18n_gettext('ui.patron.edit.au.evening_phone.require', 'The evening_phone field will be required on the patron registration screen.', 'coust', 'description'),
1994     'bool'),
1995 ( 'ui.patron.edit.au.evening_phone.show',
1996     oils_i18n_gettext('ui.patron.edit.au.evening_phone.show', 'GUI: Show evening_phone field on patron registration', 'coust', 'label'),
1997     oils_i18n_gettext('ui.patron.edit.au.evening_phone.show', 'The evening_phone field will be shown on the patron registration screen. Showing a field makes it appear with required fields even when not required. If the field is required this setting is ignored.', 'coust', 'description'),
1998     'bool'),
1999 ( 'ui.patron.edit.au.evening_phone.suggest',
2000     oils_i18n_gettext('ui.patron.edit.au.evening_phone.suggest', 'GUI: Suggest evening_phone field on patron registration', 'coust', 'label'),
2001     oils_i18n_gettext('ui.patron.edit.au.evening_phone.suggest', 'The evening_phone field will be suggested on the patron registration screen. Suggesting a field makes it appear when suggested fields are shown. If the field is shown or required this setting is ignored.', 'coust', 'description'),
2002     'bool'),
2003 ( 'ui.patron.edit.au.ident_value.show',
2004     oils_i18n_gettext('ui.patron.edit.au.ident_value.show', 'GUI: Show ident_value field on patron registration', 'coust', 'label'),
2005     oils_i18n_gettext('ui.patron.edit.au.ident_value.show', 'The ident_value field will be shown on the patron registration screen. Showing a field makes it appear with required fields even when not required. If the field is required this setting is ignored.', 'coust', 'description'),
2006     'bool'),
2007 ( 'ui.patron.edit.au.ident_value.suggest',
2008     oils_i18n_gettext('ui.patron.edit.au.ident_value.suggest', 'GUI: Suggest ident_value field on patron registration', 'coust', 'label'),
2009     oils_i18n_gettext('ui.patron.edit.au.ident_value.suggest', 'The ident_value field will be suggested on the patron registration screen. Suggesting a field makes it appear when suggested fields are shown. If the field is shown or required this setting is ignored.', 'coust', 'description'),
2010     'bool'),
2011 ( 'ui.patron.edit.au.ident_value2.show',
2012     oils_i18n_gettext('ui.patron.edit.au.ident_value2.show', 'GUI: Show ident_value2 field on patron registration', 'coust', 'label'),
2013     oils_i18n_gettext('ui.patron.edit.au.ident_value2.show', 'The ident_value2 field will be shown on the patron registration screen. Showing a field makes it appear with required fields even when not required. If the field is required this setting is ignored.', 'coust', 'description'),
2014     'bool'),
2015 ( 'ui.patron.edit.au.ident_value2.suggest',
2016     oils_i18n_gettext('ui.patron.edit.au.ident_value2.suggest', 'GUI: Suggest ident_value2 field on patron registration', 'coust', 'label'),
2017     oils_i18n_gettext('ui.patron.edit.au.ident_value2.suggest', 'The ident_value2 field will be suggested on the patron registration screen. Suggesting a field makes it appear when suggested fields are shown. If the field is shown or required this setting is ignored.', 'coust', 'description'),
2018     'bool'),
2019 ( 'ui.patron.edit.au.juvenile.show',
2020     oils_i18n_gettext('ui.patron.edit.au.juvenile.show', 'GUI: Show juvenile field on patron registration', 'coust', 'label'),
2021     oils_i18n_gettext('ui.patron.edit.au.juvenile.show', 'The juvenile field will be shown on the patron registration screen. Showing a field makes it appear with required fields even when not required. If the field is required this setting is ignored.', 'coust', 'description'),
2022     'bool'),
2023 ( 'ui.patron.edit.au.juvenile.suggest',
2024     oils_i18n_gettext('ui.patron.edit.au.juvenile.suggest', 'GUI: Suggest juvenile field on patron registration', 'coust', 'label'),
2025     oils_i18n_gettext('ui.patron.edit.au.juvenile.suggest', 'The juvenile field will be suggested on the patron registration screen. Suggesting a field makes it appear when suggested fields are shown. If the field is shown or required this setting is ignored.', 'coust', 'description'),
2026     'bool'),
2027 ( 'ui.patron.edit.au.master_account.show',
2028     oils_i18n_gettext('ui.patron.edit.au.master_account.show', 'GUI: Show master_account field on patron registration', 'coust', 'label'),
2029     oils_i18n_gettext('ui.patron.edit.au.master_account.show', 'The master_account field will be shown on the patron registration screen. Showing a field makes it appear with required fields even when not required. If the field is required this setting is ignored.', 'coust', 'description'),
2030     'bool'),
2031 ( 'ui.patron.edit.au.master_account.suggest',
2032     oils_i18n_gettext('ui.patron.edit.au.master_account.suggest', 'GUI: Suggest master_account field on patron registration', 'coust', 'label'),
2033     oils_i18n_gettext('ui.patron.edit.au.master_account.suggest', 'The master_account field will be suggested on the patron registration screen. Suggesting a field makes it appear when suggested fields are shown. If the field is shown or required this setting is ignored.', 'coust', 'description'),
2034     'bool'),
2035 ( 'ui.patron.edit.au.other_phone.example',
2036     oils_i18n_gettext('ui.patron.edit.au.other_phone.example', 'GUI: Example for other_phone field on patron registration', 'coust', 'label'),
2037     oils_i18n_gettext('ui.patron.edit.au.other_phone.example', 'The Example for validation on the other_phone field in patron registration.', 'coust', 'description'),
2038     'string'),
2039 ( 'ui.patron.edit.au.other_phone.regex',
2040     oils_i18n_gettext('ui.patron.edit.au.other_phone.regex', 'GUI: Regex for other_phone field on patron registration', 'coust', 'label'),
2041     oils_i18n_gettext('ui.patron.edit.au.other_phone.regex', 'The Regular Expression for validation on the other_phone field in patron registration.', 'coust', 'description'),
2042     'string'),
2043 ( 'ui.patron.edit.au.other_phone.require',
2044     oils_i18n_gettext('ui.patron.edit.au.other_phone.require', 'GUI: Require other_phone field on patron registration', 'coust', 'label'),
2045     oils_i18n_gettext('ui.patron.edit.au.other_phone.require', 'The other_phone field will be required on the patron registration screen.', 'coust', 'description'),
2046     'bool'),
2047 ( 'ui.patron.edit.au.other_phone.show',
2048     oils_i18n_gettext('ui.patron.edit.au.other_phone.show', 'GUI: Show other_phone field on patron registration', 'coust', 'label'),
2049     oils_i18n_gettext('ui.patron.edit.au.other_phone.show', 'The other_phone field will be shown on the patron registration screen. Showing a field makes it appear with required fields even when not required. If the field is required this setting is ignored.', 'coust', 'description'),
2050     'bool'),
2051 ( 'ui.patron.edit.au.other_phone.suggest',
2052     oils_i18n_gettext('ui.patron.edit.au.other_phone.suggest', 'GUI: Suggest other_phone field on patron registration', 'coust', 'label'),
2053     oils_i18n_gettext('ui.patron.edit.au.other_phone.suggest', 'The other_phone field will be suggested on the patron registration screen. Suggesting a field makes it appear when suggested fields are shown. If the field is shown or required this setting is ignored.', 'coust', 'description'),
2054     'bool'),
2055 ( 'ui.patron.edit.au.second_given_name.show',
2056     oils_i18n_gettext('ui.patron.edit.au.second_given_name.show', 'GUI: Show second_given_name field on patron registration', 'coust', 'label'),
2057     oils_i18n_gettext('ui.patron.edit.au.second_given_name.show', 'The second_given_name field will be shown on the patron registration screen. Showing a field makes it appear with required fields even when not required. If the field is required this setting is ignored.', 'coust', 'description'),
2058     'bool'),
2059 ( 'ui.patron.edit.au.second_given_name.suggest',
2060     oils_i18n_gettext('ui.patron.edit.au.second_given_name.suggest', 'GUI: Suggest second_given_name field on patron registration', 'coust', 'label'),
2061     oils_i18n_gettext('ui.patron.edit.au.second_given_name.suggest', 'The second_given_name field will be suggested on the patron registration screen. Suggesting a field makes it appear when suggested fields are shown. If the field is shown or required this setting is ignored.', 'coust', 'description'),
2062     'bool'),
2063 ( 'ui.patron.edit.au.suffix.show',
2064     oils_i18n_gettext('ui.patron.edit.au.suffix.show', 'GUI: Show suffix field on patron registration', 'coust', 'label'),
2065     oils_i18n_gettext('ui.patron.edit.au.suffix.show', 'The suffix field will be shown on the patron registration screen. Showing a field makes it appear with required fields even when not required. If the field is required this setting is ignored.', 'coust', 'description'),
2066     'bool'),
2067 ( 'ui.patron.edit.au.suffix.suggest',
2068     oils_i18n_gettext('ui.patron.edit.au.suffix.suggest', 'GUI: Suggest suffix field on patron registration', 'coust', 'label'),
2069     oils_i18n_gettext('ui.patron.edit.au.suffix.suggest', 'The suffix field will be suggested on the patron registration screen. Suggesting a field makes it appear when suggested fields are shown. If the field is shown or required this setting is ignored.', 'coust', 'description'),
2070     'bool'),
2071 ( 'ui.patron.edit.aua.county.require',
2072     oils_i18n_gettext('ui.patron.edit.aua.county.require', 'GUI: Require county field on patron registration', 'coust', 'label'),
2073     oils_i18n_gettext('ui.patron.edit.aua.county.require', 'The county field will be required on the patron registration screen.', 'coust', 'description'),
2074     'bool'),
2075 ( 'ui.patron.edit.aua.post_code.example',
2076     oils_i18n_gettext('ui.patron.edit.aua.post_code.example', 'GUI: Example for post_code field on patron registration', 'coust', 'label'),
2077     oils_i18n_gettext('ui.patron.edit.aua.post_code.example', 'The Example for validation on the post_code field in patron registration.', 'coust', 'description'),
2078     'string'),
2079 ( 'ui.patron.edit.aua.post_code.regex',
2080     oils_i18n_gettext('ui.patron.edit.aua.post_code.regex', 'GUI: Regex for post_code field on patron registration', 'coust', 'label'),
2081     oils_i18n_gettext('ui.patron.edit.aua.post_code.regex', 'The Regular Expression for validation on the post_code field in patron registration.', 'coust', 'description'),
2082     'string'),
2083 ( 'ui.patron.edit.default_suggested',
2084     oils_i18n_gettext('ui.patron.edit.default_suggested', 'GUI: Default showing suggested patron registration fields', 'coust', 'label'),
2085     oils_i18n_gettext('ui.patron.edit.default_suggested', 'Instead of All fields, show just suggested fields in patron registration by default.', 'coust', 'description'),
2086     'bool'),
2087 ( 'ui.patron.edit.phone.example',
2088     oils_i18n_gettext('ui.patron.edit.phone.example', 'GUI: Example for phone fields on patron registration', 'coust', 'label'),
2089     oils_i18n_gettext('ui.patron.edit.phone.example', 'The Example for validation on phone fields in patron registration. Applies to all phone fields without their own setting.', 'coust', 'description'),
2090     'string'),
2091 ( 'ui.patron.edit.phone.regex',
2092     oils_i18n_gettext('ui.patron.edit.phone.regex', 'GUI: Regex for phone fields on patron registration', 'coust', 'label'),
2093     oils_i18n_gettext('ui.patron.edit.phone.regex', 'The Regular Expression for validation on phone fields in patron registration. Applies to all phone fields without their own setting.', 'coust', 'description'),
2094     'string');
2095
2096 -- update actor.usr_address indexes
2097 DROP INDEX IF EXISTS actor.actor_usr_addr_street1_idx;
2098 DROP INDEX IF EXISTS actor.actor_usr_addr_street2_idx;
2099 DROP INDEX IF EXISTS actor.actor_usr_addr_city_idx;
2100 DROP INDEX IF EXISTS actor.actor_usr_addr_state_idx; 
2101 DROP INDEX IF EXISTS actor.actor_usr_addr_post_code_idx;
2102
2103 CREATE INDEX actor_usr_addr_street1_idx ON actor.usr_address (evergreen.lowercase(street1));
2104 CREATE INDEX actor_usr_addr_street2_idx ON actor.usr_address (evergreen.lowercase(street2));
2105 CREATE INDEX actor_usr_addr_city_idx ON actor.usr_address (evergreen.lowercase(city));
2106 CREATE INDEX actor_usr_addr_state_idx ON actor.usr_address (evergreen.lowercase(state));
2107 CREATE INDEX actor_usr_addr_post_code_idx ON actor.usr_address (evergreen.lowercase(post_code));
2108
2109 -- update actor.usr indexes
2110 DROP INDEX IF EXISTS actor.actor_usr_first_given_name_idx;
2111 DROP INDEX IF EXISTS actor.actor_usr_second_given_name_idx;
2112 DROP INDEX IF EXISTS actor.actor_usr_family_name_idx;
2113 DROP INDEX IF EXISTS actor.actor_usr_email_idx;
2114 DROP INDEX IF EXISTS actor.actor_usr_day_phone_idx;
2115 DROP INDEX IF EXISTS actor.actor_usr_evening_phone_idx;
2116 DROP INDEX IF EXISTS actor.actor_usr_other_phone_idx;
2117 DROP INDEX IF EXISTS actor.actor_usr_ident_value_idx;
2118 DROP INDEX IF EXISTS actor.actor_usr_ident_value2_idx;
2119
2120 CREATE INDEX actor_usr_first_given_name_idx ON actor.usr (evergreen.lowercase(first_given_name));
2121 CREATE INDEX actor_usr_second_given_name_idx ON actor.usr (evergreen.lowercase(second_given_name));
2122 CREATE INDEX actor_usr_family_name_idx ON actor.usr (evergreen.lowercase(family_name));
2123 CREATE INDEX actor_usr_email_idx ON actor.usr (evergreen.lowercase(email));
2124 CREATE INDEX actor_usr_day_phone_idx ON actor.usr (evergreen.lowercase(day_phone));
2125 CREATE INDEX actor_usr_evening_phone_idx ON actor.usr (evergreen.lowercase(evening_phone));
2126 CREATE INDEX actor_usr_other_phone_idx ON actor.usr (evergreen.lowercase(other_phone));
2127 CREATE INDEX actor_usr_ident_value_idx ON actor.usr (evergreen.lowercase(ident_value));
2128 CREATE INDEX actor_usr_ident_value2_idx ON actor.usr (evergreen.lowercase(ident_value2));
2129
2130 -- update actor.card indexes
2131 DROP INDEX IF EXISTS actor.actor_card_barcode_evergreen_lowercase_idx;
2132 CREATE INDEX actor_card_barcode_evergreen_lowercase_idx ON actor.card (evergreen.lowercase(barcode));
2133
2134 CREATE OR REPLACE FUNCTION vandelay.match_bib_record ( ) RETURNS TRIGGER AS $func$
2135 DECLARE
2136     attr        RECORD;
2137     attr_def    RECORD;
2138     eg_rec      RECORD;
2139     id_value    TEXT;
2140     exact_id    BIGINT;
2141 BEGIN
2142
2143     DELETE FROM vandelay.bib_match WHERE queued_record = NEW.id;
2144
2145     SELECT * INTO attr_def FROM vandelay.bib_attr_definition WHERE xpath = '//*[@tag="901"]/*[@code="c"]' ORDER BY id LIMIT 1;
2146
2147     IF attr_def IS NOT NULL AND attr_def.id IS NOT NULL THEN
2148         id_value := extract_marc_field('vandelay.queued_bib_record', NEW.id, attr_def.xpath, attr_def.remove);
2149     
2150         IF id_value IS NOT NULL AND id_value <> '' AND id_value ~ $r$^\d+$$r$ THEN
2151             SELECT id INTO exact_id FROM biblio.record_entry WHERE id = id_value::BIGINT AND NOT deleted;
2152             SELECT * INTO attr FROM vandelay.queued_bib_record_attr WHERE record = NEW.id and field = attr_def.id LIMIT 1;
2153             IF exact_id IS NOT NULL THEN
2154                 INSERT INTO vandelay.bib_match (field_type, matched_attr, queued_record, eg_record) VALUES ('id', attr.id, NEW.id, exact_id);
2155             END IF;
2156         END IF;
2157     END IF;
2158
2159     IF exact_id IS NULL THEN
2160         FOR attr IN SELECT a.* FROM vandelay.queued_bib_record_attr a JOIN vandelay.bib_attr_definition d ON (d.id = a.field) WHERE record = NEW.id AND d.ident IS TRUE LOOP
2161     
2162                 -- All numbers? check for an id match
2163                 IF (attr.attr_value ~ $r$^\d+$$r$) THEN
2164                 FOR eg_rec IN SELECT * FROM biblio.record_entry WHERE id = attr.attr_value::BIGINT AND deleted IS FALSE LOOP
2165                         INSERT INTO vandelay.bib_match (field_type, matched_attr, queued_record, eg_record) VALUES ('id', attr.id, NEW.id, eg_rec.id);
2166                         END LOOP;
2167                 END IF;
2168     
2169                 -- Looks like an ISBN? check for an isbn match
2170                 IF (attr.attr_value ~* $r$^[0-9x]+$$r$ AND character_length(attr.attr_value) IN (10,13)) THEN
2171                 FOR eg_rec IN EXECUTE $$SELECT * FROM metabib.full_rec fr WHERE fr.value LIKE evergreen.lowercase('$$ || attr.attr_value || $$%') AND fr.tag = '020' AND fr.subfield = 'a'$$ LOOP
2172                                 PERFORM id FROM biblio.record_entry WHERE id = eg_rec.record AND deleted IS FALSE;
2173                                 IF FOUND THEN
2174                                 INSERT INTO vandelay.bib_match (field_type, matched_attr, queued_record, eg_record) VALUES ('isbn', attr.id, NEW.id, eg_rec.record);
2175                                 END IF;
2176                         END LOOP;
2177     
2178                         -- subcheck for isbn-as-tcn
2179                     FOR eg_rec IN SELECT * FROM biblio.record_entry WHERE tcn_value = 'i' || attr.attr_value AND deleted IS FALSE LOOP
2180                             INSERT INTO vandelay.bib_match (field_type, matched_attr, queued_record, eg_record) VALUES ('tcn_value', attr.id, NEW.id, eg_rec.id);
2181                 END LOOP;
2182                 END IF;
2183     
2184                 -- check for an OCLC tcn_value match
2185                 IF (attr.attr_value ~ $r$^o\d+$$r$) THEN
2186                     FOR eg_rec IN SELECT * FROM biblio.record_entry WHERE tcn_value = regexp_replace(attr.attr_value,'^o','ocm') AND deleted IS FALSE LOOP
2187                             INSERT INTO vandelay.bib_match (field_type, matched_attr, queued_record, eg_record) VALUES ('tcn_value', attr.id, NEW.id, eg_rec.id);
2188                 END LOOP;
2189                 END IF;
2190     
2191                 -- check for a direct tcn_value match
2192             FOR eg_rec IN SELECT * FROM biblio.record_entry WHERE tcn_value = attr.attr_value AND deleted IS FALSE LOOP
2193                 INSERT INTO vandelay.bib_match (field_type, matched_attr, queued_record, eg_record) VALUES ('tcn_value', attr.id, NEW.id, eg_rec.id);
2194             END LOOP;
2195     
2196                 -- check for a direct item barcode match
2197             FOR eg_rec IN
2198                     SELECT  DISTINCT b.*
2199                       FROM  biblio.record_entry b
2200                             JOIN asset.call_number cn ON (cn.record = b.id)
2201                             JOIN asset.copy cp ON (cp.call_number = cn.id)
2202                       WHERE cp.barcode = attr.attr_value AND cp.deleted IS FALSE
2203             LOOP
2204                 INSERT INTO vandelay.bib_match (field_type, matched_attr, queued_record, eg_record) VALUES ('id', attr.id, NEW.id, eg_rec.id);
2205             END LOOP;
2206     
2207         END LOOP;
2208     END IF;
2209
2210     RETURN NULL;
2211 END;
2212 $func$ LANGUAGE PLPGSQL;
2213
2214
2215 -- 0499
2216 CREATE OR REPLACE FUNCTION asset.label_normalizer_generic(TEXT) RETURNS TEXT AS $func$
2217     # Created after looking at the Koha C4::ClassSortRoutine::Generic module,
2218     # thus could probably be considered a derived work, although nothing was
2219     # directly copied - but to err on the safe side of providing attribution:
2220     # Copyright (C) 2007 LibLime
2221     # Copyright (C) 2011 Equinox Software, Inc (Steve Callendar)
2222     # Licensed under the GPL v2 or later
2223
2224     use strict;
2225     use warnings;
2226
2227     # Converts the callnumber to uppercase
2228     # Strips spaces from start and end of the call number
2229     # Converts anything other than letters, digits, and periods into spaces
2230     # Collapses multiple spaces into a single underscore
2231     my $callnum = uc(shift);
2232     $callnum =~ s/^\s//g;
2233     $callnum =~ s/\s$//g;
2234     # NOTE: this previously used underscores, but this caused sorting issues
2235     # for the "before" half of page 0 on CN browse, sorting CNs containing a
2236     # decimal before "whole number" CNs
2237     $callnum =~ s/[^A-Z0-9_.]/ /g;
2238     $callnum =~ s/ {2,}/ /g;
2239
2240     return $callnum;
2241 $func$ LANGUAGE PLPERLU;
2242
2243
2244
2245 -- 0501
2246 INSERT INTO config.record_attr_definition (name,label,fixed_field) values ('language','Language (2.0 compat version)','Lang');
2247 UPDATE metabib.record_attr SET attrs = attrs || hstore('language',(attrs->'item_lang'));
2248
2249 -- 0502
2250 -- Dewey fields
2251 UPDATE asset.call_number_class
2252     SET field = '080ab,082ab,092abef'
2253     WHERE id = 2
2254 ;
2255
2256 -- LC fields
2257 UPDATE asset.call_number_class
2258     SET field = '050ab,055ab,090abef'
2259     WHERE id = 3
2260 ;
2261
2262 -- FAIR WARNING:
2263 -- Using a tool such as pgadmin to run this script may fail
2264 -- If it does, try psql command line.
2265
2266 -- Change this to FALSE to disable updating existing circs
2267 -- Otherwise will use the fine interval for the grace period
2268 \set CircGrace TRUE
2269
2270 -- 0503
2271 -- New Columns
2272
2273 ALTER TABLE config.rule_recurring_fine
2274     ADD COLUMN grace_period INTERVAL NOT NULL DEFAULT '1 day';
2275
2276 ALTER TABLE action.circulation
2277     ADD COLUMN grace_period INTERVAL NOT NULL DEFAULT '0 seconds';
2278
2279 ALTER TABLE action.aged_circulation
2280     ADD COLUMN grace_period INTERVAL NOT NULL DEFAULT '0 seconds';
2281
2282 -- Remove defaults needed to stop null complaints
2283
2284 ALTER TABLE action.circulation
2285     ALTER COLUMN grace_period DROP DEFAULT;
2286
2287 ALTER TABLE action.aged_circulation
2288     ALTER COLUMN grace_period DROP DEFAULT;
2289
2290 -- Drop Views
2291
2292 DROP VIEW action.all_circulation;
2293 DROP VIEW action.open_circulation;
2294 DROP VIEW action.billable_circulations;
2295
2296 -- Replace Views
2297
2298 CREATE OR REPLACE VIEW action.all_circulation AS
2299     SELECT  id,usr_post_code, usr_home_ou, usr_profile, usr_birth_year, copy_call_number, copy_location,
2300         copy_owning_lib, copy_circ_lib, copy_bib_record, xact_start, xact_finish, target_copy,
2301         circ_lib, circ_staff, checkin_staff, checkin_lib, renewal_remaining, grace_period, due_date,
2302         stop_fines_time, checkin_time, create_time, duration, fine_interval, recurring_fine,
2303         max_fine, phone_renewal, desk_renewal, opac_renewal, duration_rule, recurring_fine_rule,
2304         max_fine_rule, stop_fines, workstation, checkin_workstation, checkin_scan_time, parent_circ
2305       FROM  action.aged_circulation
2306             UNION ALL
2307     SELECT  DISTINCT circ.id,COALESCE(a.post_code,b.post_code) AS usr_post_code, p.home_ou AS usr_home_ou, p.profile AS usr_profile, EXTRACT(YEAR FROM p.dob)::INT AS usr_birth_year,
2308         cp.call_number AS copy_call_number, cp.location AS copy_location, cn.owning_lib AS copy_owning_lib, cp.circ_lib AS copy_circ_lib,
2309         cn.record AS copy_bib_record, circ.xact_start, circ.xact_finish, circ.target_copy, circ.circ_lib, circ.circ_staff, circ.checkin_staff,
2310         circ.checkin_lib, circ.renewal_remaining, circ.grace_period, circ.due_date, circ.stop_fines_time, circ.checkin_time, circ.create_time, circ.duration,
2311         circ.fine_interval, circ.recurring_fine, circ.max_fine, circ.phone_renewal, circ.desk_renewal, circ.opac_renewal, circ.duration_rule,
2312         circ.recurring_fine_rule, circ.max_fine_rule, circ.stop_fines, circ.workstation, circ.checkin_workstation, circ.checkin_scan_time,
2313         circ.parent_circ
2314       FROM  action.circulation circ
2315         JOIN asset.copy cp ON (circ.target_copy = cp.id)
2316         JOIN asset.call_number cn ON (cp.call_number = cn.id)
2317         JOIN actor.usr p ON (circ.usr = p.id)
2318         LEFT JOIN actor.usr_address a ON (p.mailing_address = a.id)
2319         LEFT JOIN actor.usr_address b ON (p.billing_address = a.id);
2320
2321 CREATE OR REPLACE VIEW action.open_circulation AS
2322         SELECT  *
2323           FROM  action.circulation
2324           WHERE checkin_time IS NULL
2325           ORDER BY due_date;
2326                 
2327
2328 CREATE OR REPLACE VIEW action.billable_circulations AS
2329         SELECT  *
2330           FROM  action.circulation
2331           WHERE xact_finish IS NULL;
2332
2333 -- Drop Functions that rely on types
2334
2335 DROP FUNCTION action.item_user_circ_test(INT, BIGINT, INT, BOOL);
2336 DROP FUNCTION action.item_user_circ_test(INT, BIGINT, INT);
2337 DROP FUNCTION action.item_user_renew_test(INT, BIGINT, INT);
2338
2339 CREATE OR REPLACE FUNCTION action.item_user_circ_test( circ_ou INT, match_item BIGINT, match_user INT, renewal BOOL ) RETURNS SETOF action.circ_matrix_test_result AS $func$
2340 DECLARE
2341     user_object             actor.usr%ROWTYPE;
2342     standing_penalty        config.standing_penalty%ROWTYPE;
2343     item_object             asset.copy%ROWTYPE;
2344     item_status_object      config.copy_status%ROWTYPE;
2345     item_location_object    asset.copy_location%ROWTYPE;
2346     result                  action.circ_matrix_test_result;
2347     circ_test               action.found_circ_matrix_matchpoint;
2348     circ_matchpoint         config.circ_matrix_matchpoint%ROWTYPE;
2349     out_by_circ_mod         config.circ_matrix_circ_mod_test%ROWTYPE;
2350     circ_mod_map            config.circ_matrix_circ_mod_test_map%ROWTYPE;
2351     hold_ratio              action.hold_stats%ROWTYPE;
2352     penalty_type            TEXT;
2353     items_out               INT;
2354     context_org_list        INT[];
2355     done                    BOOL := FALSE;
2356 BEGIN
2357     -- Assume success unless we hit a failure condition
2358     result.success := TRUE;
2359
2360     -- Fail if the user is BARRED
2361     SELECT INTO user_object * FROM actor.usr WHERE id = match_user;
2362
2363     -- Fail if we couldn't find the user 
2364     IF user_object.id IS NULL THEN
2365         result.fail_part := 'no_user';
2366         result.success := FALSE;
2367         done := TRUE;
2368         RETURN NEXT result;
2369         RETURN;
2370     END IF;
2371
2372     SELECT INTO item_object * FROM asset.copy WHERE id = match_item;
2373
2374     -- Fail if we couldn't find the item 
2375     IF item_object.id IS NULL THEN
2376         result.fail_part := 'no_item';
2377         result.success := FALSE;
2378         done := TRUE;
2379         RETURN NEXT result;
2380         RETURN;
2381     END IF;
2382
2383     IF user_object.barred IS TRUE THEN
2384         result.fail_part := 'actor.usr.barred';
2385         result.success := FALSE;
2386         done := TRUE;
2387         RETURN NEXT result;
2388     END IF;
2389
2390     -- Fail if the item can't circulate
2391     IF item_object.circulate IS FALSE THEN
2392         result.fail_part := 'asset.copy.circulate';
2393         result.success := FALSE;
2394         done := TRUE;
2395         RETURN NEXT result;
2396     END IF;
2397
2398     -- Fail if the item isn't in a circulateable status on a non-renewal
2399     IF NOT renewal AND item_object.status NOT IN ( 0, 7, 8 ) THEN 
2400         result.fail_part := 'asset.copy.status';
2401         result.success := FALSE;
2402         done := TRUE;
2403         RETURN NEXT result;
2404     ELSIF renewal AND item_object.status <> 1 THEN
2405         result.fail_part := 'asset.copy.status';
2406         result.success := FALSE;
2407         done := TRUE;
2408         RETURN NEXT result;
2409     END IF;
2410
2411     -- Fail if the item can't circulate because of the shelving location
2412     SELECT INTO item_location_object * FROM asset.copy_location WHERE id = item_object.location;
2413     IF item_location_object.circulate IS FALSE THEN
2414         result.fail_part := 'asset.copy_location.circulate';
2415         result.success := FALSE;
2416         done := TRUE;
2417         RETURN NEXT result;
2418     END IF;
2419
2420     SELECT INTO circ_test * FROM action.find_circ_matrix_matchpoint(circ_ou, item_object, user_object, renewal);
2421
2422     circ_matchpoint             := circ_test.matchpoint;
2423     result.matchpoint           := circ_matchpoint.id;
2424     result.circulate            := circ_matchpoint.circulate;
2425     result.duration_rule        := circ_matchpoint.duration_rule;
2426     result.recurring_fine_rule  := circ_matchpoint.recurring_fine_rule;
2427     result.max_fine_rule        := circ_matchpoint.max_fine_rule;
2428     result.hard_due_date        := circ_matchpoint.hard_due_date;
2429     result.renewals             := circ_matchpoint.renewals;
2430     result.grace_period         := circ_matchpoint.grace_period;
2431     result.buildrows            := circ_test.buildrows;
2432
2433     -- Fail if we couldn't find a matchpoint
2434     IF circ_test.success = false THEN
2435         result.fail_part := 'no_matchpoint';
2436         result.success := FALSE;
2437         done := TRUE;
2438         RETURN NEXT result;
2439         RETURN; -- All tests after this point require a matchpoint. No sense in running on an incomplete or missing one.
2440     END IF;
2441
2442     -- Apparently....use the circ matchpoint org unit to determine what org units are valid.
2443     SELECT INTO context_org_list ARRAY_ACCUM(id) FROM actor.org_unit_full_path( circ_matchpoint.org_unit );
2444
2445     IF renewal THEN
2446         penalty_type = '%RENEW%';
2447     ELSE
2448         penalty_type = '%CIRC%';
2449     END IF;
2450
2451     FOR standing_penalty IN
2452         SELECT  DISTINCT csp.*
2453           FROM  actor.usr_standing_penalty usp
2454                 JOIN config.standing_penalty csp ON (csp.id = usp.standing_penalty)
2455           WHERE usr = match_user
2456                 AND usp.org_unit IN ( SELECT * FROM unnest(context_org_list) )
2457                 AND (usp.stop_date IS NULL or usp.stop_date > NOW())
2458                 AND csp.block_list LIKE penalty_type LOOP
2459
2460         result.fail_part := standing_penalty.name;
2461         result.success := FALSE;
2462         done := TRUE;
2463         RETURN NEXT result;
2464     END LOOP;
2465
2466     -- Fail if the test is set to hard non-circulating
2467     IF circ_matchpoint.circulate IS FALSE THEN
2468         result.fail_part := 'config.circ_matrix_test.circulate';
2469         result.success := FALSE;
2470         done := TRUE;
2471         RETURN NEXT result;
2472     END IF;
2473
2474     -- Fail if the total copy-hold ratio is too low
2475     IF circ_matchpoint.total_copy_hold_ratio IS NOT NULL THEN
2476         SELECT INTO hold_ratio * FROM action.copy_related_hold_stats(match_item);
2477         IF hold_ratio.total_copy_ratio IS NOT NULL AND hold_ratio.total_copy_ratio < circ_matchpoint.total_copy_hold_ratio THEN
2478             result.fail_part := 'config.circ_matrix_test.total_copy_hold_ratio';
2479             result.success := FALSE;
2480             done := TRUE;
2481             RETURN NEXT result;
2482         END IF;
2483     END IF;
2484
2485     -- Fail if the available copy-hold ratio is too low
2486     IF circ_matchpoint.available_copy_hold_ratio IS NOT NULL THEN
2487         IF hold_ratio.hold_count IS NULL THEN
2488             SELECT INTO hold_ratio * FROM action.copy_related_hold_stats(match_item);
2489         END IF;
2490         IF hold_ratio.available_copy_ratio IS NOT NULL AND hold_ratio.available_copy_ratio < circ_matchpoint.available_copy_hold_ratio THEN
2491             result.fail_part := 'config.circ_matrix_test.available_copy_hold_ratio';
2492             result.success := FALSE;
2493             done := TRUE;
2494             RETURN NEXT result;
2495         END IF;
2496     END IF;
2497
2498     -- Fail if the user has too many items with specific circ_modifiers checked out
2499     FOR out_by_circ_mod IN SELECT * FROM config.circ_matrix_circ_mod_test WHERE matchpoint = circ_matchpoint.id LOOP
2500         SELECT  INTO items_out COUNT(*)
2501           FROM  action.circulation circ
2502             JOIN asset.copy cp ON (cp.id = circ.target_copy)
2503           WHERE circ.usr = match_user
2504                AND circ.circ_lib IN ( SELECT * FROM unnest(context_org_list) )
2505             AND circ.checkin_time IS NULL
2506             AND (circ.stop_fines IN ('MAXFINES','LONGOVERDUE') OR circ.stop_fines IS NULL)
2507             AND cp.circ_modifier IN (SELECT circ_mod FROM config.circ_matrix_circ_mod_test_map WHERE circ_mod_test = out_by_circ_mod.id);
2508         IF items_out >= out_by_circ_mod.items_out THEN
2509             result.fail_part := 'config.circ_matrix_circ_mod_test';
2510             result.success := FALSE;
2511             done := TRUE;
2512             RETURN NEXT result;
2513         END IF;
2514     END LOOP;
2515
2516     -- If we passed everything, return the successful matchpoint id
2517     IF NOT done THEN
2518         RETURN NEXT result;
2519     END IF;
2520
2521     RETURN;
2522 END;
2523 $func$ LANGUAGE plpgsql;
2524
2525 CREATE OR REPLACE FUNCTION action.item_user_circ_test( INT, BIGINT, INT ) RETURNS SETOF action.circ_matrix_test_result AS $func$
2526     SELECT * FROM action.item_user_circ_test( $1, $2, $3, FALSE );
2527 $func$ LANGUAGE SQL;
2528
2529 CREATE OR REPLACE FUNCTION action.item_user_renew_test( INT, BIGINT, INT ) RETURNS SETOF action.circ_matrix_test_result AS $func$
2530     SELECT * FROM action.item_user_circ_test( $1, $2, $3, TRUE );
2531 $func$ LANGUAGE SQL;
2532
2533 -- Update recurring fine rules
2534 UPDATE config.rule_recurring_fine SET grace_period=recurrence_interval;
2535
2536 -- Update Circulation Data
2537 -- Only update if we were told to and the circ hasn't been checked in
2538 UPDATE action.circulation SET grace_period=fine_interval WHERE :CircGrace AND (checkin_time IS NULL);
2539
2540 -- 0504
2541 CREATE TABLE biblio.monograph_part (
2542     id              SERIAL  PRIMARY KEY,
2543     record          BIGINT  NOT NULL REFERENCES biblio.record_entry (id),
2544     label           TEXT    NOT NULL,
2545     label_sortkey   TEXT    NOT NULL,
2546     CONSTRAINT record_label_unique UNIQUE (record,label)
2547 );
2548
2549 CREATE OR REPLACE FUNCTION biblio.normalize_biblio_monograph_part_sortkey () RETURNS TRIGGER AS $$
2550 BEGIN
2551     NEW.label_sortkey := REGEXP_REPLACE(
2552         evergreen.lpad_number_substrings(
2553             naco_normalize(NEW.label),
2554             '0',
2555             10
2556         ),
2557         E'\\s+',
2558         '',
2559         'g'
2560     );
2561     RETURN NEW;
2562 END;
2563 $$ LANGUAGE PLPGSQL;
2564
2565 CREATE TRIGGER norm_sort_label BEFORE INSERT OR UPDATE ON biblio.monograph_part FOR EACH ROW EXECUTE PROCEDURE biblio.normalize_biblio_monograph_part_sortkey();
2566
2567 CREATE TABLE asset.copy_part_map (
2568     id          SERIAL  PRIMARY KEY,
2569     target_copy BIGINT  NOT NULL, -- points o asset.copy
2570     part        INT     NOT NULL REFERENCES biblio.monograph_part (id) ON DELETE CASCADE
2571 );
2572 CREATE UNIQUE INDEX copy_part_map_cp_part_idx ON asset.copy_part_map (target_copy, part);
2573
2574 CREATE TABLE asset.call_number_prefix (
2575        id                      SERIAL   PRIMARY KEY,
2576        owning_lib          INT                 NOT NULL REFERENCES actor.org_unit (id),
2577        label               TEXT                NOT NULL, -- i18n
2578        label_sortkey   TEXT
2579 );
2580
2581 CREATE OR REPLACE FUNCTION asset.normalize_affix_sortkey () RETURNS TRIGGER AS $$
2582 BEGIN
2583     NEW.label_sortkey := REGEXP_REPLACE(
2584         evergreen.lpad_number_substrings(
2585             naco_normalize(NEW.label),
2586             '0',
2587             10
2588         ),
2589         E'\\s+',
2590         '',
2591         'g'
2592     );
2593     RETURN NEW;
2594 END;
2595 $$ LANGUAGE PLPGSQL;
2596
2597 CREATE TRIGGER prefix_normalize_tgr BEFORE INSERT OR UPDATE ON asset.call_number_prefix FOR EACH ROW EXECUTE PROCEDURE asset.normalize_affix_sortkey();
2598 CREATE UNIQUE INDEX asset_call_number_prefix_once_per_lib ON asset.call_number_prefix (label, owning_lib);
2599 CREATE INDEX asset_call_number_prefix_sortkey_idx ON asset.call_number_prefix (label_sortkey);
2600
2601 CREATE TABLE asset.call_number_suffix (
2602        id                      SERIAL   PRIMARY KEY,
2603        owning_lib          INT                 NOT NULL REFERENCES actor.org_unit (id),
2604        label               TEXT                NOT NULL, -- i18n
2605        label_sortkey   TEXT
2606 );
2607 CREATE TRIGGER suffix_normalize_tgr BEFORE INSERT OR UPDATE ON asset.call_number_suffix FOR EACH ROW EXECUTE PROCEDURE asset.normalize_affix_sortkey();
2608 CREATE UNIQUE INDEX asset_call_number_suffix_once_per_lib ON asset.call_number_suffix (label, owning_lib);
2609 CREATE INDEX asset_call_number_suffix_sortkey_idx ON asset.call_number_suffix (label_sortkey);
2610
2611 INSERT INTO asset.call_number_suffix (id, owning_lib, label) VALUES (-1, 1, '');
2612 INSERT INTO asset.call_number_prefix (id, owning_lib, label) VALUES (-1, 1, '');
2613
2614 DROP INDEX IF EXISTS asset.asset_call_number_label_once_per_lib;
2615
2616 ALTER TABLE asset.call_number
2617     ADD COLUMN prefix INT NOT NULL DEFAULT -1 REFERENCES asset.call_number_prefix(id) DEFERRABLE INITIALLY DEFERRED,
2618     ADD COLUMN suffix INT NOT NULL DEFAULT -1 REFERENCES asset.call_number_suffix(id) DEFERRABLE INITIALLY DEFERRED;
2619
2620 ALTER TABLE auditor.asset_call_number_history
2621     ADD COLUMN prefix INT NOT NULL DEFAULT -1,
2622     ADD COLUMN suffix INT NOT NULL DEFAULT -1;
2623
2624 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;
2625
2626 INSERT INTO config.org_unit_setting_type ( name, label, description, datatype ) VALUES (
2627     'ui.cat.volume_copy_editor.horizontal',
2628     oils_i18n_gettext(
2629         'ui.cat.volume_copy_editor.horizontal',
2630         'GUI: Horizontal layout for Volume/Copy Creator/Editor.',
2631         'coust', 'label'),
2632     oils_i18n_gettext(
2633         'ui.cat.volume_copy_editor.horizontal',
2634         'The main entry point for this interface is in Holdings Maintenance, Actions for Selected Rows, Edit Item Attributes / Call Numbers / Replace Barcodes.  This setting changes the top and bottom panes for that interface into left and right panes.',
2635         'coust', 'description'),
2636     'bool'
2637 );
2638
2639
2640
2641 -- 0506
2642 ALTER FUNCTION actor.org_unit_descendants( INT, INT ) ROWS 1;
2643 ALTER FUNCTION actor.org_unit_descendants( INT ) ROWS 1;
2644 ALTER FUNCTION actor.org_unit_descendants_distance( INT )  ROWS 1;
2645 ALTER FUNCTION actor.org_unit_ancestors( INT )  ROWS 1;
2646 ALTER FUNCTION actor.org_unit_ancestors_distance( INT )  ROWS 1;
2647 ALTER FUNCTION actor.org_unit_full_path ( INT )  ROWS 2;
2648 ALTER FUNCTION actor.org_unit_full_path ( INT, INT ) ROWS 2;
2649 ALTER FUNCTION actor.org_unit_combined_ancestors ( INT, INT ) ROWS 1;
2650 ALTER FUNCTION actor.org_unit_common_ancestors ( INT, INT ) ROWS 1;
2651 ALTER FUNCTION actor.org_unit_ancestor_setting( TEXT, INT ) ROWS 1;
2652 ALTER FUNCTION permission.grp_ancestors ( INT ) ROWS 1;
2653 ALTER FUNCTION permission.grp_ancestors_distance( INT ) ROWS 1;
2654 ALTER FUNCTION permission.grp_descendants_distance( INT ) ROWS 1;
2655 ALTER FUNCTION permission.usr_perms ( INT ) ROWS 10;
2656 ALTER FUNCTION permission.usr_has_perm_at_nd ( INT, TEXT) ROWS 1;
2657 ALTER FUNCTION permission.usr_has_perm_at_all_nd ( INT, TEXT ) ROWS 1;
2658 ALTER FUNCTION permission.usr_has_perm_at ( INT, TEXT ) ROWS 1;
2659 ALTER FUNCTION permission.usr_has_perm_at_all ( INT, TEXT ) ROWS 1;
2660
2661
2662 DROP TRIGGER IF EXISTS facet_force_nfc_tgr ON metabib.facet_entry;
2663 CREATE TRIGGER facet_force_nfc_tgr
2664     BEFORE UPDATE OR INSERT ON metabib.facet_entry
2665     FOR EACH ROW EXECUTE PROCEDURE evergreen.facet_force_nfc();
2666
2667 DROP FUNCTION IF EXISTS public.force_unicode_normal_form (TEXT,TEXT);
2668 DROP FUNCTION IF EXISTS public.facet_force_nfc ();
2669
2670 DROP TRIGGER b_maintain_901 ON biblio.record_entry;
2671 DROP TRIGGER b_maintain_901 ON authority.record_entry;
2672 DROP TRIGGER b_maintain_901 ON serial.record_entry;
2673
2674 CREATE TRIGGER b_maintain_901 BEFORE INSERT OR UPDATE ON biblio.record_entry FOR EACH ROW EXECUTE PROCEDURE evergreen.maintain_901();
2675 CREATE TRIGGER b_maintain_901 BEFORE INSERT OR UPDATE ON authority.record_entry FOR EACH ROW EXECUTE PROCEDURE evergreen.maintain_901();
2676 CREATE TRIGGER b_maintain_901 BEFORE INSERT OR UPDATE ON serial.record_entry FOR EACH ROW EXECUTE PROCEDURE evergreen.maintain_901();
2677
2678 DROP FUNCTION IF EXISTS public.maintain_901 ();
2679
2680 ------ Backporting note: 2.1+ only beyond here --------
2681
2682 CREATE SCHEMA unapi;
2683
2684 CREATE TABLE unapi.bre_output_layout (
2685     name                TEXT    PRIMARY KEY,
2686     transform           TEXT    REFERENCES config.xml_transform (name) ON DELETE CASCADE ON UPDATE CASCADE DEFERRABLE INITIALLY DEFERRED,
2687     mime_type           TEXT    NOT NULL,
2688     feed_top            TEXT    NOT NULL,
2689     holdings_element    TEXT,
2690     title_element       TEXT,
2691     description_element TEXT,
2692     creator_element     TEXT,
2693     update_ts_element   TEXT
2694 );
2695
2696 INSERT INTO unapi.bre_output_layout
2697     (name,           transform, mime_type,              holdings_element, feed_top,         title_element, description_element, creator_element, update_ts_element)
2698         VALUES
2699     ('holdings_xml', NULL,      'application/xml',      NULL,             'hxml',           NULL,          NULL,                NULL,            NULL),
2700     ('marcxml',      'marcxml', 'application/marc+xml', 'record',         'collection',     NULL,          NULL,                NULL,            NULL),
2701     ('mods32',       'mods32',  'application/mods+xml', 'mods',           'modsCollection', NULL,          NULL,                NULL,            NULL)
2702 ;
2703
2704 -- Dummy functions, so we can create the real ones out of order
2705 CREATE OR REPLACE FUNCTION unapi.aou    ( obj_id BIGINT, format TEXT, ename TEXT, includes TEXT[], org TEXT, depth INT DEFAULT NULL, slimit INT DEFAULT NULL, soffset INT DEFAULT NULL, include_xmlns BOOL DEFAULT TRUE ) RETURNS XML AS $F$ SELECT NULL::XML $F$ LANGUAGE SQL;
2706 CREATE OR REPLACE FUNCTION unapi.acnp   ( obj_id BIGINT, format TEXT, ename TEXT, includes TEXT[], org TEXT, depth INT DEFAULT NULL, slimit INT DEFAULT NULL, soffset INT DEFAULT NULL, include_xmlns BOOL DEFAULT TRUE ) RETURNS XML AS $F$ SELECT NULL::XML $F$ LANGUAGE SQL;
2707 CREATE OR REPLACE FUNCTION unapi.acns   ( obj_id BIGINT, format TEXT, ename TEXT, includes TEXT[], org TEXT, depth INT DEFAULT NULL, slimit INT DEFAULT NULL, soffset INT DEFAULT NULL, include_xmlns BOOL DEFAULT TRUE ) RETURNS XML AS $F$ SELECT NULL::XML $F$ LANGUAGE SQL;
2708 CREATE OR REPLACE FUNCTION unapi.acn    ( obj_id BIGINT, format TEXT, ename TEXT, includes TEXT[], org TEXT, depth INT DEFAULT NULL, slimit INT DEFAULT NULL, soffset INT DEFAULT NULL, include_xmlns BOOL DEFAULT TRUE ) RETURNS XML AS $F$ SELECT NULL::XML $F$ LANGUAGE SQL;
2709 CREATE OR REPLACE FUNCTION unapi.ssub   ( obj_id BIGINT, format TEXT, ename TEXT, includes TEXT[], org TEXT, depth INT DEFAULT NULL, slimit INT DEFAULT NULL, soffset INT DEFAULT NULL, include_xmlns BOOL DEFAULT TRUE ) RETURNS XML AS $F$ SELECT NULL::XML $F$ LANGUAGE SQL;
2710 CREATE OR REPLACE FUNCTION unapi.sdist  ( obj_id BIGINT, format TEXT, ename TEXT, includes TEXT[], org TEXT, depth INT DEFAULT NULL, slimit INT DEFAULT NULL, soffset INT DEFAULT NULL, include_xmlns BOOL DEFAULT TRUE ) RETURNS XML AS $F$ SELECT NULL::XML $F$ LANGUAGE SQL;
2711 CREATE OR REPLACE FUNCTION unapi.sstr   ( obj_id BIGINT, format TEXT, ename TEXT, includes TEXT[], org TEXT, depth INT DEFAULT NULL, slimit INT DEFAULT NULL, soffset INT DEFAULT NULL, include_xmlns BOOL DEFAULT TRUE ) RETURNS XML AS $F$ SELECT NULL::XML $F$ LANGUAGE SQL;
2712 CREATE OR REPLACE FUNCTION unapi.sitem  ( obj_id BIGINT, format TEXT, ename TEXT, includes TEXT[], org TEXT, depth INT DEFAULT NULL, slimit INT DEFAULT NULL, soffset INT DEFAULT NULL, include_xmlns BOOL DEFAULT TRUE ) RETURNS XML AS $F$ SELECT NULL::XML $F$ LANGUAGE SQL;
2713 CREATE OR REPLACE FUNCTION unapi.sunit  ( obj_id BIGINT, format TEXT, ename TEXT, includes TEXT[], org TEXT, depth INT DEFAULT NULL, slimit INT DEFAULT NULL, soffset INT DEFAULT NULL, include_xmlns BOOL DEFAULT TRUE ) RETURNS XML AS $F$ SELECT NULL::XML $F$ LANGUAGE SQL;
2714 CREATE OR REPLACE FUNCTION unapi.sisum  ( obj_id BIGINT, format TEXT, ename TEXT, includes TEXT[], org TEXT, depth INT DEFAULT NULL, slimit INT DEFAULT NULL, soffset INT DEFAULT NULL, include_xmlns BOOL DEFAULT TRUE ) RETURNS XML AS $F$ SELECT NULL::XML $F$ LANGUAGE SQL;
2715 CREATE OR REPLACE FUNCTION unapi.sbsum  ( obj_id BIGINT, format TEXT, ename TEXT, includes TEXT[], org TEXT, depth INT DEFAULT NULL, slimit INT DEFAULT NULL, soffset INT DEFAULT NULL, include_xmlns BOOL DEFAULT TRUE ) RETURNS XML AS $F$ SELECT NULL::XML $F$ LANGUAGE SQL;
2716 CREATE OR REPLACE FUNCTION unapi.sssum  ( obj_id BIGINT, format TEXT, ename TEXT, includes TEXT[], org TEXT, depth INT DEFAULT NULL, slimit INT DEFAULT NULL, soffset INT DEFAULT NULL, include_xmlns BOOL DEFAULT TRUE ) RETURNS XML AS $F$ SELECT NULL::XML $F$ LANGUAGE SQL;
2717 CREATE OR REPLACE FUNCTION unapi.siss   ( obj_id BIGINT, format TEXT, ename TEXT, includes TEXT[], org TEXT, depth INT DEFAULT NULL, slimit INT DEFAULT NULL, soffset INT DEFAULT NULL, include_xmlns BOOL DEFAULT TRUE ) RETURNS XML AS $F$ SELECT NULL::XML $F$ LANGUAGE SQL;
2718 CREATE OR REPLACE FUNCTION unapi.auri   ( obj_id BIGINT, format TEXT, ename TEXT, includes TEXT[], org TEXT, depth INT DEFAULT NULL, slimit INT DEFAULT NULL, soffset INT DEFAULT NULL, include_xmlns BOOL DEFAULT TRUE ) RETURNS XML AS $F$ SELECT NULL::XML $F$ LANGUAGE SQL;
2719 CREATE OR REPLACE FUNCTION unapi.acp    ( obj_id BIGINT, format TEXT, ename TEXT, includes TEXT[], org TEXT, depth INT DEFAULT NULL, slimit INT DEFAULT NULL, soffset INT DEFAULT NULL, include_xmlns BOOL DEFAULT TRUE ) RETURNS XML AS $F$ SELECT NULL::XML $F$ LANGUAGE SQL;
2720 CREATE OR REPLACE FUNCTION unapi.acpn   ( obj_id BIGINT, format TEXT, ename TEXT, includes TEXT[], org TEXT, depth INT DEFAULT NULL, slimit INT DEFAULT NULL, soffset INT DEFAULT NULL, include_xmlns BOOL DEFAULT TRUE ) RETURNS XML AS $F$ SELECT NULL::XML $F$ LANGUAGE SQL;
2721 CREATE OR REPLACE FUNCTION unapi.acl    ( obj_id BIGINT, format TEXT, ename TEXT, includes TEXT[], org TEXT, depth INT DEFAULT NULL, slimit INT DEFAULT NULL, soffset INT DEFAULT NULL, include_xmlns BOOL DEFAULT TRUE ) RETURNS XML AS $F$ SELECT NULL::XML $F$ LANGUAGE SQL;
2722 CREATE OR REPLACE FUNCTION unapi.ccs    ( obj_id BIGINT, format TEXT, ename TEXT, includes TEXT[], org TEXT, depth INT DEFAULT NULL, slimit INT DEFAULT NULL, soffset INT DEFAULT NULL, include_xmlns BOOL DEFAULT TRUE ) RETURNS XML AS $F$ SELECT NULL::XML $F$ LANGUAGE SQL;
2723 CREATE OR REPLACE FUNCTION unapi.ascecm ( obj_id BIGINT, format TEXT, ename TEXT, includes TEXT[], org TEXT, depth INT DEFAULT NULL, slimit INT DEFAULT NULL, soffset INT DEFAULT NULL, include_xmlns BOOL DEFAULT TRUE ) RETURNS XML AS $F$ SELECT NULL::XML $F$ LANGUAGE SQL;
2724 CREATE OR REPLACE FUNCTION unapi.bre    ( obj_id BIGINT, format TEXT, ename TEXT, includes TEXT[], org TEXT, depth INT DEFAULT NULL, slimit INT DEFAULT NULL, soffset INT DEFAULT NULL, include_xmlns BOOL DEFAULT TRUE ) RETURNS XML AS $F$ SELECT NULL::XML $F$ LANGUAGE SQL;
2725 CREATE OR REPLACE FUNCTION unapi.bmp    ( obj_id BIGINT, format TEXT, ename TEXT, includes TEXT[], org TEXT, depth INT DEFAULT NULL, slimit INT DEFAULT NULL, soffset INT DEFAULT NULL, include_xmlns BOOL DEFAULT TRUE ) RETURNS XML AS $F$ SELECT NULL::XML $F$ LANGUAGE SQL;
2726
2727 CREATE OR REPLACE FUNCTION unapi.holdings_xml ( bid BIGINT, ouid INT, org TEXT, depth INT DEFAULT NULL, includes TEXT[] DEFAULT NULL::TEXT[], slimit INT DEFAULT NULL, soffset INT DEFAULT NULL, include_xmlns BOOL DEFAULT TRUE) RETURNS XML AS $F$ SELECT NULL::XML $F$ LANGUAGE SQL;
2728 CREATE OR REPLACE FUNCTION unapi.biblio_record_entry_feed ( id_list BIGINT[], format TEXT, includes TEXT[], org TEXT, depth INT DEFAULT NULL, slimit INT DEFAULT NULL, soffset INT DEFAULT NULL, include_xmlns BOOL DEFAULT TRUE, title TEXT DEFAULT NULL, description TEXT DEFAULT NULL, creator TEXT DEFAULT NULL, update_ts TEXT DEFAULT NULL, unapi_url TEXT DEFAULT NULL, header_xml XML DEFAULT NULL ) RETURNS XML AS $F$ SELECT NULL::XML $F$ LANGUAGE SQL;
2729
2730 CREATE OR REPLACE FUNCTION unapi.memoize (classname TEXT, obj_id BIGINT, format TEXT,  ename TEXT, includes TEXT[], org TEXT, depth INT DEFAULT NULL, slimit INT DEFAULT NULL, soffset INT DEFAULT NULL, include_xmlns BOOL DEFAULT TRUE ) RETURNS XML AS $F$
2731 DECLARE
2732     key     TEXT;
2733     output  XML;
2734 BEGIN
2735     key :=
2736         'id'        || COALESCE(obj_id::TEXT,'') ||
2737         'format'    || COALESCE(format::TEXT,'') ||
2738         'ename'     || COALESCE(ename::TEXT,'') ||
2739         'includes'  || COALESCE(includes::TEXT,'{}'::TEXT[]::TEXT) ||
2740         'org'       || COALESCE(org::TEXT,'') ||
2741         'depth'     || COALESCE(depth::TEXT,'') ||
2742         'slimit'    || COALESCE(slimit::TEXT,'') ||
2743         'soffset'   || COALESCE(soffset::TEXT,'') ||
2744         'include_xmlns'   || COALESCE(include_xmlns::TEXT,'');
2745     -- RAISE NOTICE 'memoize key: %', key;
2746
2747     key := MD5(key);
2748     -- RAISE NOTICE 'memoize hash: %', key;
2749
2750     -- XXX cache logic ... memcached? table?
2751
2752     EXECUTE $$SELECT unapi.$$ || classname || $$( $1, $2, $3, $4, $5, $6, $7, $8, $9);$$ INTO output USING obj_id, format, ename, includes, org, depth, slimit, soffset, include_xmlns;
2753     RETURN output;
2754 END;
2755 $F$ LANGUAGE PLPGSQL;
2756
2757 CREATE OR REPLACE FUNCTION unapi.biblio_record_entry_feed ( id_list BIGINT[], format TEXT, includes TEXT[], org TEXT, depth INT DEFAULT NULL, slimit INT DEFAULT NULL, soffset INT DEFAULT NULL, include_xmlns BOOL DEFAULT TRUE, title TEXT DEFAULT NULL, description TEXT DEFAULT NULL, creator TEXT DEFAULT NULL, update_ts TEXT DEFAULT NULL, unapi_url TEXT DEFAULT NULL, header_xml XML DEFAULT NULL ) RETURNS XML AS $F$
2758 DECLARE
2759     layout          unapi.bre_output_layout%ROWTYPE;
2760     transform       config.xml_transform%ROWTYPE;
2761     item_format     TEXT;
2762     tmp_xml         TEXT;
2763     xmlns_uri       TEXT := 'http://open-ils.org/spec/feed-xml/v1';
2764     ouid            INT;
2765     element_list    TEXT[];
2766 BEGIN
2767
2768     SELECT id INTO ouid FROM actor.org_unit WHERE shortname = org;
2769     SELECT * INTO layout FROM unapi.bre_output_layout WHERE name = format;
2770
2771     IF layout.name IS NULL THEN
2772         RETURN NULL::XML;
2773     END IF;
2774
2775     SELECT * INTO transform FROM config.xml_transform WHERE name = layout.transform;
2776     xmlns_uri := COALESCE(transform.namespace_uri,xmlns_uri);
2777
2778     -- Gather the bib xml
2779     SELECT XMLAGG( unapi.bre(i, format, '', includes, org, depth, slimit, soffset, include_xmlns)) INTO tmp_xml FROM UNNEST( id_list ) i;
2780
2781     IF layout.title_element IS NOT NULL THEN
2782         EXECUTE 'SELECT XMLCONCAT( XMLELEMENT( name '|| layout.title_element ||', CASE WHEN $4 THEN XMLATTRIBUTES( $1 AS xmlns) ELSE NULL END, $3), $2)' INTO tmp_xml USING xmlns_uri, tmp_xml::XML, title, include_xmlns;
2783     END IF;
2784
2785     IF layout.description_element IS NOT NULL THEN
2786         EXECUTE 'SELECT XMLCONCAT( XMLELEMENT( name '|| layout.description_element ||', CASE WHEN $4 THEN XMLATTRIBUTES( $1 AS xmlns) ELSE NULL END, $3), $2)' INTO tmp_xml USING xmlns_uri, tmp_xml::XML, description, include_xmlns;
2787     END IF;
2788
2789     IF layout.creator_element IS NOT NULL THEN
2790         EXECUTE 'SELECT XMLCONCAT( XMLELEMENT( name '|| layout.creator_element ||', CASE WHEN $4 THEN XMLATTRIBUTES( $1 AS xmlns) ELSE NULL END, $3), $2)' INTO tmp_xml USING xmlns_uri, tmp_xml::XML, creator, include_xmlns;
2791     END IF;
2792
2793     IF layout.update_ts_element IS NOT NULL THEN
2794         EXECUTE 'SELECT XMLCONCAT( XMLELEMENT( name '|| layout.update_ts_element ||', CASE WHEN $4 THEN XMLATTRIBUTES( $1 AS xmlns) ELSE NULL END, $3), $2)' INTO tmp_xml USING xmlns_uri, tmp_xml::XML, update_ts, include_xmlns;
2795     END IF;
2796
2797     IF unapi_url IS NOT NULL THEN
2798         EXECUTE $$SELECT XMLCONCAT( XMLELEMENT( name link, XMLATTRIBUTES( 'http://www.w3.org/1999/xhtml' AS xmlns, 'unapi-server' AS rel, $1 AS href, 'unapi' AS title)), $2)$$ INTO tmp_xml USING unapi_url, tmp_xml::XML;
2799     END IF;
2800
2801     IF header_xml IS NOT NULL THEN tmp_xml := XMLCONCAT(header_xml,tmp_xml::XML); END IF;
2802
2803     element_list := regexp_split_to_array(layout.feed_top,E'\\.');
2804     FOR i IN REVERSE ARRAY_UPPER(element_list, 1) .. 1 LOOP
2805         EXECUTE 'SELECT XMLELEMENT( name '|| quote_ident(element_list[i]) ||', CASE WHEN $4 THEN XMLATTRIBUTES( $1 AS xmlns) ELSE NULL END, $2)' INTO tmp_xml USING xmlns_uri, tmp_xml::XML, include_xmlns;
2806     END LOOP;
2807
2808     RETURN tmp_xml::XML;
2809 END;
2810 $F$ LANGUAGE PLPGSQL;
2811
2812 CREATE OR REPLACE FUNCTION unapi.bre ( obj_id BIGINT, format TEXT,  ename TEXT, includes TEXT[], org TEXT, depth INT DEFAULT NULL, slimit INT DEFAULT NULL, soffset INT DEFAULT NULL, include_xmlns BOOL DEFAULT TRUE ) RETURNS XML AS $F$
2813 DECLARE
2814     me      biblio.record_entry%ROWTYPE;
2815     layout  unapi.bre_output_layout%ROWTYPE;
2816     xfrm    config.xml_transform%ROWTYPE;
2817     ouid    INT;
2818     tmp_xml TEXT;
2819     top_el  TEXT;
2820     output  XML;
2821     hxml    XML;
2822 BEGIN
2823
2824     SELECT id INTO ouid FROM actor.org_unit WHERE shortname = org;
2825
2826     IF ouid IS NULL THEN
2827         RETURN NULL::XML;
2828     END IF;
2829
2830     IF format = 'holdings_xml' THEN -- the special case
2831         output := unapi.holdings_xml( obj_id, ouid, org, depth, includes, slimit, soffset, include_xmlns);
2832         RETURN output;
2833     END IF;
2834
2835     SELECT * INTO layout FROM unapi.bre_output_layout WHERE name = format;
2836
2837     IF layout.name IS NULL THEN
2838         RETURN NULL::XML;
2839     END IF;
2840
2841     SELECT * INTO xfrm FROM config.xml_transform WHERE name = layout.transform;
2842
2843     SELECT * INTO me FROM biblio.record_entry WHERE id = obj_id;
2844
2845     -- grab hodlings if we need them
2846     IF ('holdings_xml' = ANY (includes)) THEN 
2847         hxml := unapi.holdings_xml(obj_id, ouid, org, depth, evergreen.array_remove_item_by_value(includes,'holdings_xml'), slimit, soffset, include_xmlns);
2848     ELSE
2849         hxml := NULL::XML;
2850     END IF;
2851
2852
2853     -- generate our item node
2854
2855
2856     IF format = 'marcxml' THEN
2857         tmp_xml := me.marc;
2858         IF tmp_xml !~ E'<marc:' THEN -- If we're not using the prefixed namespace in this record, then remove all declarations of it
2859            tmp_xml := REGEXP_REPLACE(tmp_xml, ' xmlns:marc="http://www.loc.gov/MARC21/slim"', '', 'g');
2860         END IF; 
2861     ELSE
2862         tmp_xml := oils_xslt_process(me.marc, xfrm.xslt)::XML;
2863     END IF;
2864
2865     top_el := REGEXP_REPLACE(tmp_xml, E'^.*?<((?:\\S+:)?' || layout.holdings_element || ').*$', E'\\1');
2866
2867     IF hxml IS NOT NULL THEN -- XXX how do we configure the holdings position?
2868         tmp_xml := REGEXP_REPLACE(tmp_xml, '</' || top_el || '>(.*?)$', hxml || '</' || top_el || E'>\\1');
2869     END IF;
2870
2871     IF ('bre.unapi' = ANY (includes)) THEN 
2872         output := REGEXP_REPLACE(
2873             tmp_xml,
2874             '</' || top_el || '>(.*?)',
2875             XMLELEMENT(
2876                 name abbr,
2877                 XMLATTRIBUTES(
2878                     'http://www.w3.org/1999/xhtml' AS xmlns,
2879                     'unapi-id' AS class,
2880                     'tag:open-ils.org:U2@bre/' || obj_id || '/' || org AS title
2881                 )
2882             )::TEXT || '</' || top_el || E'>\\1'
2883         );
2884     ELSE
2885         output := tmp_xml;
2886     END IF;
2887
2888     RETURN output;
2889 END;
2890 $F$ LANGUAGE PLPGSQL;
2891
2892 CREATE OR REPLACE FUNCTION unapi.holdings_xml (bid BIGINT, ouid INT, org TEXT, depth INT DEFAULT NULL, includes TEXT[] DEFAULT NULL::TEXT[], slimit INT DEFAULT NULL, soffset INT DEFAULT NULL, include_xmlns BOOL DEFAULT TRUE) RETURNS XML AS $F$
2893      SELECT  XMLELEMENT(
2894                  name holdings,
2895                  XMLATTRIBUTES(
2896                     CASE WHEN $8 THEN 'http://open-ils.org/spec/holdings/v1' ELSE NULL END AS xmlns,
2897                     CASE WHEN ('bre' = ANY ('{acn,auri}'::TEXT[] || $5)) THEN 'tag:open-ils.org:U2@bre/' || $1 || '/' || $3 ELSE NULL END AS id
2898                  ),
2899                  XMLELEMENT(
2900                      name counts,
2901                      (SELECT  XMLAGG(XMLELEMENT::XML) FROM (
2902                          SELECT  XMLELEMENT(
2903                                      name count,
2904                                      XMLATTRIBUTES('public' as type, depth, org_unit, coalesce(transcendant,0) as transcendant, available, visible as count, unshadow)
2905                                  )::text
2906                            FROM  asset.opac_ou_record_copy_count($2,  $1)
2907                                      UNION
2908                          SELECT  XMLELEMENT(
2909                                      name count,
2910                                      XMLATTRIBUTES('staff' as type, depth, org_unit, coalesce(transcendant,0) as transcendant, available, visible as count, unshadow)
2911                                  )::text
2912                            FROM  asset.staff_ou_record_copy_count($2, $1)
2913                                      ORDER BY 1
2914                      )x)
2915                  ),
2916                  CASE 
2917                      WHEN ('bmp' = ANY ($5)) THEN
2918                         XMLELEMENT( name monograph_parts,
2919                             XMLAGG((SELECT unapi.bmp( id, 'xml', 'monograph_part', evergreen.array_remove_item_by_value( evergreen.array_remove_item_by_value($5,'bre'), 'holdings_xml'), $3, $4, $6, $7, FALSE) FROM biblio.monograph_part WHERE record = $1))
2920                         )
2921                      ELSE NULL
2922                  END,
2923                  CASE WHEN ('acn' = ANY ('{acn,auri}'::TEXT[] || $5)) THEN 
2924                      XMLELEMENT(
2925                          name volumes,
2926                          (SELECT XMLAGG(acn) FROM (
2927                             SELECT  unapi.acn(acn.id,'xml','volume', evergreen.array_remove_item_by_value( evergreen.array_remove_item_by_value('{acn,auri}'::TEXT[] || $5,'holdings_xml'),'bre'), $3, $4, $6, $7, FALSE)
2928                               FROM  asset.call_number acn
2929                               WHERE acn.record = $1
2930                                     AND EXISTS (
2931                                         SELECT  1
2932                                           FROM  asset.copy acp
2933                                                 JOIN actor.org_unit_descendants(
2934                                                     $2,
2935                                                     (COALESCE(
2936                                                         $4,
2937                                                         (SELECT aout.depth
2938                                                           FROM  actor.org_unit_type aout
2939                                                                 JOIN actor.org_unit aou ON (aou.ou_type = aout.id AND aou.id = $2)
2940                                                         )
2941                                                     ))
2942                                                 ) aoud ON (acp.circ_lib = aoud.id)
2943                                           LIMIT 1
2944                                     )
2945                               ORDER BY label_sortkey
2946                               LIMIT $6
2947                               OFFSET $7
2948                          )x)
2949                      )
2950                  ELSE NULL END,
2951                  CASE WHEN ('ssub' = ANY ('{acn,auri}'::TEXT[] || $5)) THEN 
2952                      XMLELEMENT(
2953                          name subscriptions,
2954                          (SELECT XMLAGG(ssub) FROM (
2955                             SELECT  unapi.ssub(id,'xml','subscription','{}'::TEXT[], $3, $4, $6, $7, FALSE)
2956                               FROM  serial.subscription
2957                               WHERE record_entry = $1
2958                         )x)
2959                      )
2960                  ELSE NULL END
2961              );
2962 $F$ LANGUAGE SQL;
2963
2964 CREATE OR REPLACE FUNCTION unapi.ssub ( obj_id BIGINT, format TEXT,  ename TEXT, includes TEXT[], org TEXT, depth INT DEFAULT NULL, slimit INT DEFAULT NULL, soffset INT DEFAULT NULL, include_xmlns BOOL DEFAULT TRUE ) RETURNS XML AS $F$
2965         SELECT  XMLELEMENT(
2966                     name subscription,
2967                     XMLATTRIBUTES(
2968                         CASE WHEN $9 THEN 'http://open-ils.org/spec/holdings/v1' ELSE NULL END AS xmlns,
2969                         'tag:open-ils.org:U2@ssub/' || id AS id,
2970                         start_date AS start, end_date AS end, expected_date_offset
2971                     ),
2972                     unapi.aou( owning_lib, $2, 'owning_lib', evergreen.array_remove_item_by_value($4,'ssub'), $5, $6, $7, $8),
2973                     XMLELEMENT( name distributions,
2974                         CASE 
2975                             WHEN ('sdist' = ANY ($4)) THEN
2976                                 XMLAGG((SELECT unapi.sdist( id, 'xml', 'distribution', evergreen.array_remove_item_by_value($4,'ssub'), $5, $6, $7, $8, FALSE) FROM serial.distribution WHERE subscription = ssub.id))
2977                             ELSE NULL
2978                         END
2979                     )
2980                 )
2981           FROM  serial.subscription ssub
2982           WHERE id = $1
2983           GROUP BY id, start_date, end_date, expected_date_offset, owning_lib;
2984 $F$ LANGUAGE SQL;
2985
2986 CREATE OR REPLACE FUNCTION unapi.sdist ( obj_id BIGINT, format TEXT,  ename TEXT, includes TEXT[], org TEXT, depth INT DEFAULT NULL, slimit INT DEFAULT NULL, soffset INT DEFAULT NULL, include_xmlns BOOL DEFAULT TRUE ) RETURNS XML AS $F$
2987         SELECT  XMLELEMENT(
2988                     name distribution,
2989                     XMLATTRIBUTES(
2990                         CASE WHEN $9 THEN 'http://open-ils.org/spec/holdings/v1' ELSE NULL END AS xmlns,
2991                         'tag:open-ils.org:U2@sdist/' || id AS id,
2992                         'tag:open-ils.org:U2@acn/' || receive_call_number AS receive_call_number,
2993                         'tag:open-ils.org:U2@acn/' || bind_call_number AS bind_call_number,
2994                         unit_label_prefix, label, unit_label_suffix, summary_method
2995                     ),
2996                     unapi.aou( holding_lib, $2, 'holding_lib', evergreen.array_remove_item_by_value($4,'sdist'), $5, $6, $7, $8),
2997                     CASE WHEN subscription IS NOT NULL AND ('ssub' = ANY ($4)) THEN unapi.ssub( subscription, 'xml', 'subscription', evergreen.array_remove_item_by_value($4,'sdist'), $5, $6, $7, $8, FALSE) ELSE NULL END,
2998                     XMLELEMENT( name streams,
2999                         CASE 
3000                             WHEN ('sstr' = ANY ($4)) THEN
3001                                 XMLAGG((SELECT unapi.sstr( id, 'xml', 'stream', evergreen.array_remove_item_by_value($4,'sdist'), $5, $6, $7, $8, FALSE) FROM serial.stream WHERE distribution = sdist.id))
3002                             ELSE NULL
3003                         END
3004                     ),
3005                     XMLELEMENT( name summaries,
3006                         CASE 
3007                             WHEN ('ssum' = ANY ($4)) THEN
3008                                 XMLAGG((SELECT unapi.sbsum( id, 'xml', 'serial_summary', evergreen.array_remove_item_by_value($4,'sdist'), $5, $6, $7, $8, FALSE) FROM serial.basic_summary WHERE distribution = sdist.id))
3009                             ELSE NULL
3010                         END,
3011                         CASE 
3012                             WHEN ('ssum' = ANY ($4)) THEN
3013                                 XMLAGG((SELECT unapi.sisum( id, 'xml', 'serial_summary', evergreen.array_remove_item_by_value($4,'sdist'), $5, $6, $7, $8, FALSE) FROM serial.index_summary WHERE distribution = sdist.id))
3014                             ELSE NULL
3015                         END,
3016                         CASE 
3017                             WHEN ('ssum' = ANY ($4)) THEN
3018                                 XMLAGG((SELECT unapi.sssum( id, 'xml', 'serial_summary', evergreen.array_remove_item_by_value($4,'sdist'), $5, $6, $7, $8, FALSE) FROM serial.supplement_summary WHERE distribution = sdist.id))
3019                             ELSE NULL
3020                         END
3021                     )
3022                 )
3023           FROM  serial.distribution sdist
3024           WHERE id = $1
3025           GROUP BY id, label, unit_label_prefix, unit_label_suffix, holding_lib, summary_method, subscription, receive_call_number, bind_call_number;
3026 $F$ LANGUAGE SQL;
3027
3028 CREATE OR REPLACE FUNCTION unapi.sstr ( obj_id BIGINT, format TEXT,  ename TEXT, includes TEXT[], org TEXT, depth INT DEFAULT NULL, slimit INT DEFAULT NULL, soffset INT DEFAULT NULL, include_xmlns BOOL DEFAULT TRUE ) RETURNS XML AS $F$
3029     SELECT  XMLELEMENT(
3030                 name stream,
3031                 XMLATTRIBUTES(
3032                     CASE WHEN $9 THEN 'http://open-ils.org/spec/holdings/v1' ELSE NULL END AS xmlns,
3033                     'tag:open-ils.org:U2@sstr/' || id AS id,
3034                     routing_label
3035                 ),
3036                 CASE WHEN distribution IS NOT NULL AND ('sdist' = ANY ($4)) THEN unapi.sssum( distribution, 'xml', 'distribtion', evergreen.array_remove_item_by_value($4,'sstr'), $5, $6, $7, $8, FALSE) ELSE NULL END,
3037                 XMLELEMENT( name items,
3038                     CASE 
3039                         WHEN ('sitem' = ANY ($4)) THEN
3040                             XMLAGG((SELECT unapi.sitem( id, 'xml', 'serial_item', evergreen.array_remove_item_by_value($4,'sstr'), $5, $6, $7, $8, FALSE) FROM serial.item WHERE stream = sstr.id))
3041                         ELSE NULL
3042                     END
3043                 )
3044             )
3045       FROM  serial.stream sstr
3046       WHERE id = $1
3047       GROUP BY id, routing_label, distribution;
3048 $F$ LANGUAGE SQL;
3049
3050 CREATE OR REPLACE FUNCTION unapi.siss ( obj_id BIGINT, format TEXT,  ename TEXT, includes TEXT[], org TEXT, depth INT DEFAULT NULL, slimit INT DEFAULT NULL, soffset INT DEFAULT NULL, include_xmlns BOOL DEFAULT TRUE ) RETURNS XML AS $F$
3051     SELECT  XMLELEMENT(
3052                 name issuance,
3053                 XMLATTRIBUTES(
3054                     CASE WHEN $9 THEN 'http://open-ils.org/spec/holdings/v1' ELSE NULL END AS xmlns,
3055                     'tag:open-ils.org:U2@siss/' || id AS id,
3056                     create_date, edit_date, label, date_published,
3057                     holding_code, holding_type, holding_link_id
3058                 ),
3059                 CASE WHEN subscription IS NOT NULL AND ('ssub' = ANY ($4)) THEN unapi.ssub( subscription, 'xml', 'subscription', evergreen.array_remove_item_by_value($4,'siss'), $5, $6, $7, $8, FALSE) ELSE NULL END,
3060                 XMLELEMENT( name items,
3061                     CASE 
3062                         WHEN ('sitem' = ANY ($4)) THEN
3063                             XMLAGG((SELECT unapi.sitem( id, 'xml', 'serial_item', evergreen.array_remove_item_by_value($4,'siss'), $5, $6, $7, $8, FALSE) FROM serial.item WHERE issuance = sstr.id))
3064                         ELSE NULL
3065                     END
3066                 )
3067             )
3068       FROM  serial.issuance sstr
3069       WHERE id = $1
3070       GROUP BY id, create_date, edit_date, label, date_published, holding_code, holding_type, holding_link_id, subscription;
3071 $F$ LANGUAGE SQL;
3072
3073 CREATE OR REPLACE FUNCTION unapi.sitem ( obj_id BIGINT, format TEXT,  ename TEXT, includes TEXT[], org TEXT, depth INT DEFAULT NULL, slimit INT DEFAULT NULL, soffset INT DEFAULT NULL, include_xmlns BOOL DEFAULT TRUE ) RETURNS XML AS $F$
3074         SELECT  XMLELEMENT(
3075                     name serial_item,
3076                     XMLATTRIBUTES(
3077                         CASE WHEN $9 THEN 'http://open-ils.org/spec/holdings/v1' ELSE NULL END AS xmlns,
3078                         'tag:open-ils.org:U2@sitem/' || id AS id,
3079                         'tag:open-ils.org:U2@siss/' || issuance AS issuance,
3080                         date_expected, date_received
3081                     ),
3082                     CASE WHEN issuance IS NOT NULL AND ('siss' = ANY ($4)) THEN unapi.siss( issuance, $2, 'issuance', evergreen.array_remove_item_by_value($4,'sitem'), $5, $6, $7, $8, FALSE) ELSE NULL END,
3083                     CASE WHEN stream IS NOT NULL AND ('sstr' = ANY ($4)) THEN unapi.sstr( stream, $2, 'stream', evergreen.array_remove_item_by_value($4,'sitem'), $5, $6, $7, $8, FALSE) ELSE NULL END,
3084                     CASE WHEN unit IS NOT NULL AND ('sunit' = ANY ($4)) THEN unapi.sunit( stream, $2, 'serial_unit', evergreen.array_remove_item_by_value($4,'sitem'), $5, $6, $7, $8, FALSE) ELSE NULL END,
3085                     CASE WHEN uri IS NOT NULL AND ('auri' = ANY ($4)) THEN unapi.auri( uri, $2, 'uri', evergreen.array_remove_item_by_value($4,'sitem'), $5, $6, $7, $8, FALSE) ELSE NULL END
3086 --                    XMLELEMENT( name notes,
3087 --                        CASE 
3088 --                            WHEN ('acpn' = ANY ($4)) THEN
3089 --                                XMLAGG((SELECT unapi.acpn( id, 'xml', 'copy_note', evergreen.array_remove_item_by_value($4,'acp'), $5, $6, $7, $8) FROM asset.copy_note WHERE owning_copy = cp.id AND pub))
3090 --                            ELSE NULL
3091 --                        END
3092 --                    )
3093                 )
3094           FROM  serial.item sitem
3095           WHERE id = $1;
3096 $F$ LANGUAGE SQL;
3097
3098
3099 CREATE OR REPLACE FUNCTION unapi.sssum ( obj_id BIGINT, format TEXT,  ename TEXT, includes TEXT[], org TEXT, depth INT DEFAULT NULL, slimit INT DEFAULT NULL, soffset INT DEFAULT NULL, include_xmlns BOOL DEFAULT TRUE ) RETURNS XML AS $F$
3100     SELECT  XMLELEMENT(
3101                 name serial_summary,
3102                 XMLATTRIBUTES(
3103                     CASE WHEN $9 THEN 'http://open-ils.org/spec/holdings/v1' ELSE NULL END AS xmlns,
3104                     'tag:open-ils.org:U2@sbsum/' || id AS id,
3105                     'sssum' AS type, generated_coverage, textual_holdings, show_generated
3106                 ),
3107                 CASE WHEN ('sdist' = ANY ($4)) THEN unapi.sdist( distribution, 'xml', 'distribtion', evergreen.array_remove_item_by_value($4,'ssum'), $5, $6, $7, $8, FALSE) ELSE NULL END
3108             )
3109       FROM  serial.supplement_summary ssum
3110       WHERE id = $1
3111       GROUP BY id, generated_coverage, textual_holdings, distribution, show_generated;
3112 $F$ LANGUAGE SQL;
3113
3114 CREATE OR REPLACE FUNCTION unapi.sbsum ( obj_id BIGINT, format TEXT,  ename TEXT, includes TEXT[], org TEXT, depth INT DEFAULT NULL, slimit INT DEFAULT NULL, soffset INT DEFAULT NULL, include_xmlns BOOL DEFAULT TRUE ) RETURNS XML AS $F$
3115     SELECT  XMLELEMENT(
3116                 name serial_summary,
3117                 XMLATTRIBUTES(
3118                     CASE WHEN $9 THEN 'http://open-ils.org/spec/holdings/v1' ELSE NULL END AS xmlns,
3119                     'tag:open-ils.org:U2@sbsum/' || id AS id,
3120                     'sbsum' AS type, generated_coverage, textual_holdings, show_generated
3121                 ),
3122                 CASE WHEN ('sdist' = ANY ($4)) THEN unapi.sdist( distribution, 'xml', 'distribtion', evergreen.array_remove_item_by_value($4,'ssum'), $5, $6, $7, $8, FALSE) ELSE NULL END
3123             )
3124       FROM  serial.basic_summary ssum
3125       WHERE id = $1
3126       GROUP BY id, generated_coverage, textual_holdings, distribution, show_generated;
3127 $F$ LANGUAGE SQL;
3128
3129 CREATE OR REPLACE FUNCTION unapi.sisum ( obj_id BIGINT, format TEXT,  ename TEXT, includes TEXT[], org TEXT, depth INT DEFAULT NULL, slimit INT DEFAULT NULL, soffset INT DEFAULT NULL, include_xmlns BOOL DEFAULT TRUE ) RETURNS XML AS $F$
3130     SELECT  XMLELEMENT(
3131                 name serial_summary,
3132                 XMLATTRIBUTES(
3133                     CASE WHEN $9 THEN 'http://open-ils.org/spec/holdings/v1' ELSE NULL END AS xmlns,
3134                     'tag:open-ils.org:U2@sbsum/' || id AS id,
3135                     'sisum' AS type, generated_coverage, textual_holdings, show_generated
3136                 ),
3137                 CASE WHEN ('sdist' = ANY ($4)) THEN unapi.sdist( distribution, 'xml', 'distribtion', evergreen.array_remove_item_by_value($4,'ssum'), $5, $6, $7, $8, FALSE) ELSE NULL END
3138             )
3139       FROM  serial.index_summary ssum
3140       WHERE id = $1
3141       GROUP BY id, generated_coverage, textual_holdings, distribution, show_generated;
3142 $F$ LANGUAGE SQL;
3143
3144
3145 CREATE OR REPLACE FUNCTION unapi.aou ( obj_id BIGINT, format TEXT,  ename TEXT, includes TEXT[], org TEXT, depth INT DEFAULT NULL, slimit INT DEFAULT NULL, soffset INT DEFAULT NULL, include_xmlns BOOL DEFAULT TRUE ) RETURNS XML AS $F$
3146 DECLARE
3147     output XML;
3148 BEGIN
3149     IF ename = 'circlib' THEN
3150         SELECT  XMLELEMENT(
3151                     name circlib,
3152                     XMLATTRIBUTES(
3153                         'http://open-ils.org/spec/actors/v1' AS xmlns,
3154                         id AS ident
3155                     ),
3156                     name
3157                 ) INTO output
3158           FROM  actor.org_unit aou
3159           WHERE id = obj_id;
3160     ELSE