]> git.evergreen-ils.org Git - working/Evergreen.git/blob - Open-ILS/src/sql/Pg/012.schema.vandelay.sql
Structure match set points as a tree
[working/Evergreen.git] / Open-ILS / src / sql / Pg / 012.schema.vandelay.sql
1 DROP SCHEMA IF EXISTS vandelay CASCADE;
2
3 BEGIN;
4
5 CREATE OR REPLACE FUNCTION 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;
6
7 CREATE SCHEMA vandelay;
8
9 CREATE TABLE vandelay.match_set (
10     id      SERIAL  PRIMARY KEY,
11     name    TEXT        NOT NULL,
12     owner   INT     NOT NULL REFERENCES actor.org_unit (id) ON DELETE CASCADE,
13     mtype   TEXT        NOT NULL DEFAULT 'biblio', -- 'biblio','authority','mfhd'?, others?
14     CONSTRAINT name_once_per_owner_mtype UNIQUE (name, owner, mtype)
15 );
16
17 -- Table to define match points, either FF via SVF or tag+subfield
18 CREATE TABLE vandelay.match_set_point (
19     id          SERIAL  PRIMARY KEY,
20     match_set   INT     REFERENCES vandelay.match_set (id),
21     parent      INT     REFERENCES vandelay.match_set_point (id),
22     bool_op     TEXT    CHECK (bool_op IS NULL OR (bool_op IN ('AND','OR','NOT'))),
23     svf         TEXT    REFERENCES config.record_attr_definition (name),
24     tag         TEXT,
25     subfield    TEXT,
26     quality     INT     NOT NULL DEFAULT 1, -- higher is better
27     CONSTRAINT vmsp_need_a_subfield_with_a_tag CHECK ((tag IS NOT NULL AND subfield IS NOT NULL) OR tag IS NULL),
28     CONSTRAINT vmsp_need_a_tag_or_a_ff_or_a_bo CHECK (
29         (tag IS NOT NULL AND svf IS NULL AND bool_op IS NULL) OR
30         (tag IS NULL AND svf IS NOT NULL AND bool_op IS NULL) OR
31         (tag IS NULL AND svf IS NULL AND bool_op IS NOT NULL)
32     )
33 );
34
35 CREATE TABLE vandelay.match_set_quality (
36     id          SERIAL  PRIMARY KEY,
37     match_set   INT     NOT NULL REFERENCES vandelay.match_set (id),
38     svf         TEXT    REFERENCES config.record_attr_definition,
39     tag         TEXT,
40     subfield    TEXT,
41     value       TEXT    NOT NULL,
42     quality     INT     NOT NULL DEFAULT 1, -- higher is better
43     CONSTRAINT vmsq_need_a_subfield_with_a_tag CHECK ((tag IS NOT NULL AND subfield IS NOT NULL) OR tag IS NULL),
44     CONSTRAINT vmsq_need_a_tag_or_a_ff CHECK ((tag IS NOT NULL AND svf IS NULL) OR (tag IS NULL AND svf IS NOT NULL))
45 );
46 CREATE UNIQUE INDEX vmsq_def_once_per_set ON vandelay.match_set_quality (match_set, COALESCE(tag,''), COALESCE(subfield,''), COALESCE(svf,''));
47
48
49 CREATE TABLE vandelay.queue (
50         id                              BIGSERIAL       PRIMARY KEY,
51         owner                   INT                     NOT NULL REFERENCES actor.usr (id) DEFERRABLE INITIALLY DEFERRED,
52         name                    TEXT            NOT NULL,
53         complete                BOOL            NOT NULL DEFAULT FALSE,
54         queue_type              TEXT            NOT NULL DEFAULT 'bib' CHECK (queue_type IN ('bib','authority')),
55     match_set       INT         REFERENCES vandelay.match_set (id) ON UPDATE CASCADE ON DELETE SET NULL DEFERRABLE INITIALLY DEFERRED,
56         CONSTRAINT vand_queue_name_once_per_owner_const UNIQUE (owner,name,queue_type)
57 );
58
59 CREATE TABLE vandelay.queued_record (
60     id                  BIGSERIAL                   PRIMARY KEY,
61     create_time TIMESTAMP WITH TIME ZONE    NOT NULL DEFAULT NOW(),
62     import_time TIMESTAMP WITH TIME ZONE,
63         purpose         TEXT                                            NOT NULL DEFAULT 'import' CHECK (purpose IN ('import','overlay')),
64     marc                TEXT                        NOT NULL,
65     quality     INT                         NOT NULL DEFAULT 0
66 );
67
68
69
70 /* Bib stuff at the top */
71 ----------------------------------------------------
72
73 CREATE TABLE vandelay.bib_attr_definition (
74         id                      SERIAL  PRIMARY KEY,
75         code            TEXT    UNIQUE NOT NULL,
76         description     TEXT,
77         xpath           TEXT    NOT NULL,
78         remove          TEXT    NOT NULL DEFAULT ''
79 );
80
81 -- Each TEXT field (other than 'name') should hold an XPath predicate for pulling the data needed
82 -- DROP TABLE vandelay.import_item_attr_definition CASCADE;
83 CREATE TABLE vandelay.import_item_attr_definition (
84     id              BIGSERIAL   PRIMARY KEY,
85     owner           INT         NOT NULL REFERENCES actor.org_unit (id) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED,
86     name            TEXT        NOT NULL,
87     tag             TEXT        NOT NULL,
88     keep            BOOL        NOT NULL DEFAULT FALSE,
89     owning_lib      TEXT,
90     circ_lib        TEXT,
91     call_number     TEXT,
92     copy_number     TEXT,
93     status          TEXT,
94     location        TEXT,
95     circulate       TEXT,
96     deposit         TEXT,
97     deposit_amount  TEXT,
98     ref             TEXT,
99     holdable        TEXT,
100     price           TEXT,
101     barcode         TEXT,
102     circ_modifier   TEXT,
103     circ_as_type    TEXT,
104     alert_message   TEXT,
105     opac_visible    TEXT,
106     pub_note_title  TEXT,
107     pub_note        TEXT,
108     priv_note_title TEXT,
109     priv_note       TEXT,
110         CONSTRAINT vand_import_item_attr_def_idx UNIQUE (owner,name)
111 );
112
113 CREATE TABLE vandelay.import_error (
114     code        TEXT    PRIMARY KEY,
115     description TEXT    NOT NULL -- i18n
116 );
117
118 CREATE TABLE vandelay.bib_queue (
119         queue_type          TEXT        NOT NULL DEFAULT 'bib' CHECK (queue_type = 'bib'),
120         item_attr_def   BIGINT REFERENCES vandelay.import_item_attr_definition (id) ON DELETE SET NULL DEFERRABLE INITIALLY DEFERRED,
121         CONSTRAINT vand_bib_queue_name_once_per_owner_const UNIQUE (owner,name,queue_type)
122 ) INHERITS (vandelay.queue);
123 ALTER TABLE vandelay.bib_queue ADD PRIMARY KEY (id);
124
125 CREATE TABLE vandelay.queued_bib_record (
126         queue               INT         NOT NULL REFERENCES vandelay.bib_queue (id) ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED,
127         bib_source          INT         REFERENCES config.bib_source (id) DEFERRABLE INITIALLY DEFERRED,
128         imported_as     BIGINT  REFERENCES biblio.record_entry (id) DEFERRABLE INITIALLY DEFERRED,
129         import_error    TEXT    REFERENCES vandelay.import_error (code) ON DELETE SET NULL ON UPDATE CASCADE DEFERRABLE INITIALLY DEFERRED,
130         error_detail    TEXT
131 ) INHERITS (vandelay.queued_record);
132 ALTER TABLE vandelay.queued_bib_record ADD PRIMARY KEY (id);
133 CREATE INDEX queued_bib_record_queue_idx ON vandelay.queued_bib_record (queue);
134
135 CREATE TABLE vandelay.queued_bib_record_attr (
136         id                      BIGSERIAL       PRIMARY KEY,
137         record          BIGINT          NOT NULL REFERENCES vandelay.queued_bib_record (id) DEFERRABLE INITIALLY DEFERRED,
138         field           INT                     NOT NULL REFERENCES vandelay.bib_attr_definition (id) DEFERRABLE INITIALLY DEFERRED,
139         attr_value      TEXT            NOT NULL
140 );
141 CREATE INDEX queued_bib_record_attr_record_idx ON vandelay.queued_bib_record_attr (record);
142
143 CREATE TABLE vandelay.bib_match (
144         id                              BIGSERIAL       PRIMARY KEY,
145         matched_set     INT                     REFERENCES vandelay.match_set (id) ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED,
146         queued_record   BIGINT          REFERENCES vandelay.queued_bib_record (id) ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED,
147         eg_record               BIGINT          REFERENCES biblio.record_entry (id) ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED,
148     quality         INT         NOT NULL DEFAULT 0
149 );
150
151 CREATE TABLE vandelay.import_item (
152     id              BIGSERIAL   PRIMARY KEY,
153     record          BIGINT      NOT NULL REFERENCES vandelay.queued_bib_record (id) ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED,
154     definition      BIGINT      NOT NULL REFERENCES vandelay.import_item_attr_definition (id) ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED,
155         import_error    TEXT        REFERENCES vandelay.import_error (code) ON DELETE SET NULL ON UPDATE CASCADE DEFERRABLE INITIALLY DEFERRED,
156         error_detail    TEXT,
157     owning_lib      INT,
158     circ_lib        INT,
159     call_number     TEXT,
160     copy_number     INT,
161     status          INT,
162     location        INT,
163     circulate       BOOL,
164     deposit         BOOL,
165     deposit_amount  NUMERIC(8,2),
166     ref             BOOL,
167     holdable        BOOL,
168     price           NUMERIC(8,2),
169     barcode         TEXT,
170     circ_modifier   TEXT,
171     circ_as_type    TEXT,
172     alert_message   TEXT,
173     pub_note        TEXT,
174     priv_note       TEXT,
175     opac_visible    BOOL
176 );
177  
178 CREATE TABLE vandelay.import_bib_trash_fields (
179     id              BIGSERIAL   PRIMARY KEY,
180     owner           INT         NOT NULL REFERENCES actor.org_unit (id) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED,
181     field           TEXT        NOT NULL,
182         CONSTRAINT vand_import_bib_trash_fields_idx UNIQUE (owner,field)
183 );
184
185 CREATE TABLE vandelay.merge_profile (
186     id              BIGSERIAL   PRIMARY KEY,
187     owner           INT         NOT NULL REFERENCES actor.org_unit (id) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED,
188     name            TEXT        NOT NULL,
189     add_spec        TEXT,
190     replace_spec    TEXT,
191     strip_spec      TEXT,
192     preserve_spec   TEXT,
193         CONSTRAINT vand_merge_prof_owner_name_idx UNIQUE (owner,name),
194         CONSTRAINT add_replace_strip_or_preserve CHECK ((preserve_spec IS NOT NULL OR replace_spec IS NOT NULL) OR (preserve_spec IS NULL AND replace_spec IS NULL))
195 );
196
197 CREATE OR REPLACE FUNCTION vandelay.marc21_record_type( marc TEXT ) RETURNS config.marc21_rec_type_map AS $func$
198 DECLARE
199     ldr         TEXT;
200     tval        TEXT;
201     tval_rec    RECORD;
202     bval        TEXT;
203     bval_rec    RECORD;
204     retval      config.marc21_rec_type_map%ROWTYPE;
205 BEGIN
206     ldr := oils_xpath_string( '//*[local-name()="leader"]', marc );
207
208     IF ldr IS NULL OR ldr = '' THEN
209         SELECT * INTO retval FROM config.marc21_rec_type_map WHERE code = 'BKS';
210         RETURN retval;
211     END IF;
212
213     SELECT * INTO tval_rec FROM config.marc21_ff_pos_map WHERE fixed_field = 'Type' LIMIT 1; -- They're all the same
214     SELECT * INTO bval_rec FROM config.marc21_ff_pos_map WHERE fixed_field = 'BLvl' LIMIT 1; -- They're all the same
215
216
217     tval := SUBSTRING( ldr, tval_rec.start_pos + 1, tval_rec.length );
218     bval := SUBSTRING( ldr, bval_rec.start_pos + 1, bval_rec.length );
219
220     -- RAISE NOTICE 'type %, blvl %, ldr %', tval, bval, ldr;
221
222     SELECT * INTO retval FROM config.marc21_rec_type_map WHERE type_val LIKE '%' || tval || '%' AND blvl_val LIKE '%' || bval || '%';
223
224
225     IF retval.code IS NULL THEN
226         SELECT * INTO retval FROM config.marc21_rec_type_map WHERE code = 'BKS';
227     END IF;
228
229     RETURN retval;
230 END;
231 $func$ LANGUAGE PLPGSQL;
232
233 CREATE OR REPLACE FUNCTION vandelay.marc21_extract_fixed_field( marc TEXT, ff TEXT ) RETURNS TEXT AS $func$
234 DECLARE
235     rtype       TEXT;
236     ff_pos      RECORD;
237     tag_data    RECORD;
238     val         TEXT;
239 BEGIN
240     rtype := (vandelay.marc21_record_type( marc )).code;
241     FOR ff_pos IN SELECT * FROM config.marc21_ff_pos_map WHERE fixed_field = ff AND rec_type = rtype ORDER BY tag DESC LOOP
242         FOR tag_data IN SELECT value FROM UNNEST( oils_xpath( '//*[@tag="' || UPPER(ff_pos.tag) || '"]/text()', marc ) ) x(value) LOOP
243             val := SUBSTRING( tag_data.value, ff_pos.start_pos + 1, ff_pos.length );
244             RETURN val;
245         END LOOP;
246         val := REPEAT( ff_pos.default_val, ff_pos.length );
247         RETURN val;
248     END LOOP;
249
250     RETURN NULL;
251 END;
252 $func$ LANGUAGE PLPGSQL;
253
254 CREATE TYPE biblio.record_ff_map AS (record BIGINT, ff_name TEXT, ff_value TEXT);
255 CREATE OR REPLACE FUNCTION vandelay.marc21_extract_all_fixed_fields( marc TEXT ) RETURNS SETOF biblio.record_ff_map AS $func$
256 DECLARE
257     tag_data    TEXT;
258     rtype       TEXT;
259     ff_pos      RECORD;
260     output      biblio.record_ff_map%ROWTYPE;
261 BEGIN
262     rtype := (vandelay.marc21_record_type( marc )).code;
263
264     FOR ff_pos IN SELECT * FROM config.marc21_ff_pos_map WHERE rec_type = rtype ORDER BY tag DESC LOOP
265         output.ff_name  := ff_pos.fixed_field;
266         output.ff_value := NULL;
267
268         FOR tag_data IN SELECT value FROM UNNEST( oils_xpath( '//*[@tag="' || UPPER(tag) || '"]/text()', marc ) ) x(value) LOOP
269             output.ff_value := SUBSTRING( tag_data.value, ff_pos.start_pos + 1, ff_pos.length );
270             IF output.ff_value IS NULL THEN output.ff_value := REPEAT( ff_pos.default_val, ff_pos.length ); END IF;
271             RETURN NEXT output;
272             output.ff_value := NULL;
273         END LOOP;
274
275     END LOOP;
276
277     RETURN;
278 END;
279 $func$ LANGUAGE PLPGSQL;
280
281 CREATE TYPE biblio.marc21_physical_characteristics AS ( id INT, record BIGINT, ptype TEXT, subfield INT, value INT );
282 CREATE OR REPLACE FUNCTION vandelay.marc21_physical_characteristics( marc TEXT) RETURNS SETOF biblio.marc21_physical_characteristics AS $func$
283 DECLARE
284     rowid   INT := 0;
285     _007    TEXT;
286     ptype   config.marc21_physical_characteristic_type_map%ROWTYPE;
287     psf     config.marc21_physical_characteristic_subfield_map%ROWTYPE;
288     pval    config.marc21_physical_characteristic_value_map%ROWTYPE;
289     retval  biblio.marc21_physical_characteristics%ROWTYPE;
290 BEGIN
291
292     _007 := oils_xpath_string( '//*[@tag="007"]', marc );
293
294     IF _007 IS NOT NULL AND _007 <> '' THEN
295         SELECT * INTO ptype FROM config.marc21_physical_characteristic_type_map WHERE ptype_key = SUBSTRING( _007, 1, 1 );
296
297         IF ptype.ptype_key IS NOT NULL THEN
298             FOR psf IN SELECT * FROM config.marc21_physical_characteristic_subfield_map WHERE ptype_key = ptype.ptype_key LOOP
299                 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 );
300
301                 IF pval.id IS NOT NULL THEN
302                     rowid := rowid + 1;
303                     retval.id := rowid;
304                     retval.ptype := ptype.ptype_key;
305                     retval.subfield := psf.id;
306                     retval.value := pval.id;
307                     RETURN NEXT retval;
308                 END IF;
309
310             END LOOP;
311         END IF;
312     END IF;
313
314     RETURN;
315 END;
316 $func$ LANGUAGE PLPGSQL;
317
318 CREATE TYPE vandelay.flat_marc AS ( tag CHAR(3), ind1 TEXT, ind2 TEXT, subfield TEXT, value TEXT );
319 CREATE OR REPLACE FUNCTION vandelay.flay_marc ( TEXT ) RETURNS SETOF vandelay.flat_marc AS $func$
320
321 use MARC::Record;
322 use MARC::File::XML (BinaryEncoding => 'UTF-8');
323
324 my $xml = shift;
325 my $r = MARC::Record->new_from_xml( $xml );
326
327 return_next( { tag => 'LDR', value => $r->leader } );
328
329 for my $f ( $r->fields ) {
330     if ($f->is_control_field) {
331         return_next({ tag => $f->tag, value => $f->data });
332     } else {
333         for my $s ($f->subfields) {
334             return_next({
335                 tag      => $f->tag,
336                 ind1     => $f->indicator(1),
337                 ind2     => $f->indicator(2),
338                 subfield => $s->[0],
339                 value    => $s->[1]
340             });
341
342             if ( $f->tag eq '245' and $s->[0] eq 'a' ) {
343                 my $trim = $f->indicator(2) || 0;
344                 return_next({
345                     tag      => 'tnf',
346                     ind1     => $f->indicator(1),
347                     ind2     => $f->indicator(2),
348                     subfield => 'a',
349                     value    => substr( $s->[1], $trim )
350                 });
351             }
352         }
353     }
354 }
355
356 return undef;
357
358 $func$ LANGUAGE PLPERLU;
359
360 CREATE OR REPLACE FUNCTION vandelay.flatten_marc ( marc TEXT ) RETURNS SETOF vandelay.flat_marc AS $func$
361 DECLARE
362     output  vandelay.flat_marc%ROWTYPE;
363     field   RECORD;
364 BEGIN
365     FOR field IN SELECT * FROM vandelay.flay_marc( marc ) LOOP
366         output.ind1 := field.ind1;
367         output.ind2 := field.ind2;
368         output.tag := field.tag;
369         output.subfield := field.subfield;
370         IF field.subfield IS NOT NULL AND field.tag NOT IN ('020','022','024') THEN -- exclude standard numbers and control fields
371             output.value := naco_normalize(field.value, field.subfield);
372         ELSE
373             output.value := field.value;
374         END IF;
375
376         CONTINUE WHEN output.value IS NULL;
377
378         RETURN NEXT output;
379     END LOOP;
380 END;
381 $func$ LANGUAGE PLPGSQL;
382
383 CREATE OR REPLACE FUNCTION vandelay.extract_rec_attrs ( xml TEXT, attr_defs TEXT[]) RETURNS hstore AS $_$
384 DECLARE
385     transformed_xml TEXT;
386     prev_xfrm       TEXT;
387     normalizer      RECORD;
388     xfrm            config.xml_transform%ROWTYPE;
389     attr_value      TEXT;
390     new_attrs       HSTORE := ''::HSTORE;
391     attr_def        config.record_attr_definition%ROWTYPE;
392 BEGIN
393
394     FOR attr_def IN SELECT * FROM config.record_attr_definition WHERE name IN (SELECT * FROM UNNEST(attr_defs)) ORDER BY format LOOP
395
396         IF attr_def.tag IS NOT NULL THEN -- tag (and optional subfield list) selection
397             SELECT  ARRAY_TO_STRING(ARRAY_ACCUM(x.value), COALESCE(attr_def.joiner,' ')) INTO attr_value
398               FROM  vandelay.flatten_marc(xml) AS x
399               WHERE x.tag LIKE attr_def.tag
400                     AND CASE
401                         WHEN attr_def.sf_list IS NOT NULL
402                             THEN POSITION(x.subfield IN attr_def.sf_list) > 0
403                         ELSE TRUE
404                         END
405               GROUP BY x.tag
406               ORDER BY x.tag
407               LIMIT 1;
408
409         ELSIF attr_def.fixed_field IS NOT NULL THEN -- a named fixed field, see config.marc21_ff_pos_map.fixed_field
410             attr_value := vandelay.marc21_extract_fixed_field(xml, attr_def.fixed_field);
411
412         ELSIF attr_def.xpath IS NOT NULL THEN -- and xpath expression
413
414             SELECT INTO xfrm * FROM config.xml_transform WHERE name = attr_def.format;
415
416             -- See if we can skip the XSLT ... it's expensive
417             IF prev_xfrm IS NULL OR prev_xfrm <> xfrm.name THEN
418                 -- Can't skip the transform
419                 IF xfrm.xslt <> '---' THEN
420                     transformed_xml := oils_xslt_process(xml,xfrm.xslt);
421                 ELSE
422                     transformed_xml := xml;
423                 END IF;
424
425                 prev_xfrm := xfrm.name;
426             END IF;
427
428             IF xfrm.name IS NULL THEN
429                 -- just grab the marcxml (empty) transform
430                 SELECT INTO xfrm * FROM config.xml_transform WHERE xslt = '---' LIMIT 1;
431                 prev_xfrm := xfrm.name;
432             END IF;
433
434             attr_value := oils_xpath_string(attr_def.xpath, transformed_xml, COALESCE(attr_def.joiner,' '), ARRAY[ARRAY[xfrm.prefix, xfrm.namespace_uri]]);
435
436         ELSIF attr_def.phys_char_sf IS NOT NULL THEN -- a named Physical Characteristic, see config.marc21_physical_characteristic_*_map
437             SELECT  value::TEXT INTO attr_value
438               FROM  vandelay.marc21_physical_characteristics(xml)
439               WHERE subfield = attr_def.phys_char_sf
440               LIMIT 1; -- Just in case ...
441
442         END IF;
443
444         -- apply index normalizers to attr_value
445         FOR normalizer IN
446             SELECT  n.func AS func,
447                     n.param_count AS param_count,
448                     m.params AS params
449               FROM  config.index_normalizer n
450                     JOIN config.record_attr_index_norm_map m ON (m.norm = n.id)
451               WHERE attr = attr_def.name
452               ORDER BY m.pos LOOP
453                 EXECUTE 'SELECT ' || normalizer.func || '(' ||
454                     quote_literal( attr_value ) ||
455                     CASE
456                         WHEN normalizer.param_count > 0
457                             THEN ',' || REPLACE(REPLACE(BTRIM(normalizer.params,'[]'),E'\'',E'\\\''),E'"',E'\'')
458                             ELSE ''
459                         END ||
460                     ')' INTO attr_value;
461
462         END LOOP;
463
464         -- Add the new value to the hstore
465         new_attrs := new_attrs || hstore( attr_def.name, attr_value );
466
467     END LOOP;
468
469     RETURN new_attrs;
470 END;
471 $_$ LANGUAGE PLPGSQL;
472
473 CREATE OR REPLACE FUNCTION vandelay.extract_rec_attrs ( xml TEXT ) RETURNS hstore AS $_$
474     SELECT vandelay.extract_rec_attrs( $1, (SELECT ARRAY_ACCUM(name) FROM config.record_attr_definition));
475 $_$ LANGUAGE SQL;
476
477 CREATE OR REPLACE FUNCTION vandelay.match_bib_record ( ) RETURNS TRIGGER AS $func$
478 DECLARE
479     incoming_existing_id    TEXT;
480     my_bib_queue            vandelay.bib_queue%ROWTYPE;
481     my_match_set            vandelay.match_set%ROWTYPE;
482     test                    vandelay.match_set_point%ROWTYPE;
483     potential_matches       BIGINT[];
484     matches                 BIGINT[];
485     rvalue                  TEXT;
486     quality_set             hstore;
487     tmp_rec                 BIGINT;
488     tmp_quality             INT;
489     first_round             BOOL;
490 BEGIN
491     DELETE FROM vandelay.bib_match WHERE queued_record = NEW.id;
492
493     incoming_existing_id := oils_xpath_string('//*[@tag="901"]/*[@code="c"][1]',NEW.marc);
494
495     IF incoming_existing_id IS NOT NULL THEN
496         SELECT id INTO tmp_rec FROM biblio.record_entry WHERE id = exact_id;
497         IF tmp_rec IS NOT NULL THEN
498             INSERT INTO vandelay.bib_match (queued_record, eg_record, quality) VALUES ( NEW.id, exact_id, 9999);
499             RETURN NEW;
500         END IF;
501     END IF;
502
503     SELECT * INTO my_bib_queue FROM vandelay.bib_queue WHERE id = NEW.queue;
504
505     first_round := TRUE;
506     -- whew ... here we go ...
507
508
509     -- Commented out until replaced by tree-ish version
510 /*
511     FOR test IN SELECT * FROM vandelay.match_set_point WHERE match_set = my_bib_queue.match_set ORDER BY required DESC LOOP
512         IF test.tag IS NOT NULL THEN
513             FOR rvalue IN SELECT value FROM vandelay.flatten_marc( xml ) WHERE tag = test.tag AND subfield = test.subfield LOOP
514                 SELECT ARRAY_ACCUM(DISTINCT record) INTO potential_matches FROM metabib.real_full_rec WHERE tag = test.tag AND subfield = test.subfield AND value = rvalue;
515
516                 IF first_round THEN
517                     matches := potential_matches;
518                     first_round := FALSE;
519                 ELSIF test.required THEN
520                     FOR tmp_rec IN SELECT * FROM UNNEST(matches) LOOP
521                         IF tmp_rec NOT IN (SELECT * FROM UNNEST(potential_matches)) THEN
522                             matches := array_remove_item_by_value(matches, tmp_rec);
523                             potential_matches := array_remove_item_by_value(potential_matches, tmp_rec);
524                         END IF;
525                     END LOOP;
526                 END IF;
527
528                 -- add the quality for this match
529                 FOR tmp_rec IN SELECT * FROM UNNEST(potential_matches) LOOP
530                     tmp_quality := COALESCE((quality_set -> tmp_rec::TEXT)::INT, 0);
531                     quality_set := quality_set || hstore(tmp_rec::TEXT, (tmp_quality + test.quality)::TEXT);
532                 END LOOP;
533
534             END LOOP;
535         ELSE
536             rvalue := vandelay.vandelay.extract_rec_attrs(xml, ARRAY[test.svf]);
537
538             IF first_round THEN
539                 matches := potential_matches;
540                 first_round := FALSE;
541             ELSIF test.required THEN
542                 FOR tmp_rec IN SELECT * FROM UNNEST(matches) LOOP
543                     IF tmp_rec NOT IN (SELECT * FROM UNNEST(potential_matches)) THEN
544                         matches := array_remove_item_by_value(matches, tmp_rec);
545                         potential_matches := array_remove_item_by_value(potential_matches, tmp_rec);
546                     END IF;
547                 END LOOP;
548             END IF;
549
550             -- add the quality for this match
551             FOR tmp_rec IN SELECT * FROM UNNEST(potential_matches) LOOP
552                 tmp_quality := COALESCE((quality_set -> tmp_rec::TEXT)::INT, 0);
553                 quality_set := quality_set || hstore(tmp_rec::TEXT, (tmp_quality + test.quality)::TEXT);
554             END LOOP;
555
556         END IF;
557     END LOOP;
558
559     FOR tmp_rec IN SELECT * FROM UNNEST(matches) LOOP
560         INSERT INTO vandelay.bib_match (matched_set, queued_record, eg_record, quality) VALUES (my_bib_queue.match_set, NEW.id, tmp_rec, (quality_set -> tmp_rec::TEXT));
561     END LOOP;
562 */
563
564     RETURN NEW;
565 END;
566 $func$ LANGUAGE PLPGSQL;
567
568 CREATE OR REPLACE FUNCTION vandelay.incoming_record_quality ( xml TEXT, match_set_id INT ) RETURNS INT AS $_$
569 DECLARE
570     out_q   INT := 0;
571     rvalue  TEXT;
572     test    vandelay.match_set_quality%ROWTYPE;
573 BEGIN
574
575     FOR test IN SELECT * FROM vandelay.match_set_quality WHERE match_set = match_set_id LOOP
576         IF test.tag IS NOT NULL THEN
577             FOR rvalue IN SELECT value FROM vandelay.flatten_marc( xml ) WHERE tag = test.tag AND subfield = test.subfield LOOP
578                 IF test.value = rvalue THEN
579                     out_q := out_q + test.quality;
580                 END IF;
581             END LOOP;
582         ELSE
583             IF test.value = vandelay.extract_rec_attrs(xml, ARRAY[test.svf]) THEN
584                 out_q := out_q + test.quality;
585             END IF;
586         END IF;
587     END LOOP;
588
589     RETURN out_q;
590 END;
591 $_$ LANGUAGE PLPGSQL;
592
593 CREATE TYPE vandelay.tcn_data AS (tcn TEXT, tcn_source TEXT, used BOOL);
594 CREATE OR REPLACE FUNCTION vandelay.find_bib_tcn_data ( xml TEXT ) RETURNS SETOF vandelay.tcn_data AS $_$
595 DECLARE
596     eg_tcn          TEXT;
597     eg_tcn_source   TEXT;
598     output          vandelay.tcn_data%ROWTYPE;
599 BEGIN
600
601     -- 001/003
602     eg_tcn := BTRIM((oils_xpath('//*[@tag="001"]/text()',xml))[1]);
603     IF eg_tcn IS NOT NULL AND eg_tcn <> '' THEN
604
605         eg_tcn_source := BTRIM((oils_xpath('//*[@tag="003"]/text()',xml))[1]);
606         IF eg_tcn_source IS NULL OR eg_tcn_source = '' THEN
607             eg_tcn_source := 'System Local';
608         END IF;
609
610         PERFORM id FROM biblio.record_entry WHERE tcn_value = eg_tcn  AND NOT deleted;
611
612         IF NOT FOUND THEN
613             output.used := FALSE;
614         ELSE
615             output.used := TRUE;
616         END IF;
617
618         output.tcn := eg_tcn;
619         output.tcn_source := eg_tcn_source;
620         RETURN NEXT output;
621
622     END IF;
623
624     -- 901 ab
625     eg_tcn := BTRIM((oils_xpath('//*[@tag="901"]/*[@code="a"]/text()',xml))[1]);
626     IF eg_tcn IS NOT NULL AND eg_tcn <> '' THEN
627
628         eg_tcn_source := BTRIM((oils_xpath('//*[@tag="901"]/*[@code="b"]/text()',xml))[1]);
629         IF eg_tcn_source IS NULL OR eg_tcn_source = '' THEN
630             eg_tcn_source := 'System Local';
631         END IF;
632
633         PERFORM id FROM biblio.record_entry WHERE tcn_value = eg_tcn  AND NOT deleted;
634
635         IF NOT FOUND THEN
636             output.used := FALSE;
637         ELSE
638             output.used := TRUE;
639         END IF;
640
641         output.tcn := eg_tcn;
642         output.tcn_source := eg_tcn_source;
643         RETURN NEXT output;
644
645     END IF;
646
647     -- 039 ab
648     eg_tcn := BTRIM((oils_xpath('//*[@tag="039"]/*[@code="a"]/text()',xml))[1]);
649     IF eg_tcn IS NOT NULL AND eg_tcn <> '' THEN
650
651         eg_tcn_source := BTRIM((oils_xpath('//*[@tag="039"]/*[@code="b"]/text()',xml))[1]);
652         IF eg_tcn_source IS NULL OR eg_tcn_source = '' THEN
653             eg_tcn_source := 'System Local';
654         END IF;
655
656         PERFORM id FROM biblio.record_entry WHERE tcn_value = eg_tcn  AND NOT deleted;
657
658         IF NOT FOUND THEN
659             output.used := FALSE;
660         ELSE
661             output.used := TRUE;
662         END IF;
663
664         output.tcn := eg_tcn;
665         output.tcn_source := eg_tcn_source;
666         RETURN NEXT output;
667
668     END IF;
669
670     -- 020 a
671     eg_tcn := REGEXP_REPLACE((oils_xpath('//*[@tag="020"]/*[@code="a"]/text()',xml))[1], $re$^(\w+).*?$$re$, $re$\1$re$);
672     IF eg_tcn IS NOT NULL AND eg_tcn <> '' THEN
673
674         eg_tcn_source := 'ISBN';
675
676         PERFORM id FROM biblio.record_entry WHERE tcn_value = eg_tcn  AND NOT deleted;
677
678         IF NOT FOUND THEN
679             output.used := FALSE;
680         ELSE
681             output.used := TRUE;
682         END IF;
683
684         output.tcn := eg_tcn;
685         output.tcn_source := eg_tcn_source;
686         RETURN NEXT output;
687
688     END IF;
689
690     -- 022 a
691     eg_tcn := REGEXP_REPLACE((oils_xpath('//*[@tag="022"]/*[@code="a"]/text()',xml))[1], $re$^(\w+).*?$$re$, $re$\1$re$);
692     IF eg_tcn IS NOT NULL AND eg_tcn <> '' THEN
693
694         eg_tcn_source := 'ISSN';
695
696         PERFORM id FROM biblio.record_entry WHERE tcn_value = eg_tcn  AND NOT deleted;
697
698         IF NOT FOUND THEN
699             output.used := FALSE;
700         ELSE
701             output.used := TRUE;
702         END IF;
703
704         output.tcn := eg_tcn;
705         output.tcn_source := eg_tcn_source;
706         RETURN NEXT output;
707
708     END IF;
709
710     -- 010 a
711     eg_tcn := REGEXP_REPLACE((oils_xpath('//*[@tag="010"]/*[@code="a"]/text()',xml))[1], $re$^(\w+).*?$$re$, $re$\1$re$);
712     IF eg_tcn IS NOT NULL AND eg_tcn <> '' THEN
713
714         eg_tcn_source := 'LCCN';
715
716         PERFORM id FROM biblio.record_entry WHERE tcn_value = eg_tcn  AND NOT deleted;
717
718         IF NOT FOUND THEN
719             output.used := FALSE;
720         ELSE
721             output.used := TRUE;
722         END IF;
723
724         output.tcn := eg_tcn;
725         output.tcn_source := eg_tcn_source;
726         RETURN NEXT output;
727
728     END IF;
729
730     -- 035 a
731     eg_tcn := REGEXP_REPLACE((oils_xpath('//*[@tag="035"]/*[@code="a"]/text()',xml))[1], $re$^.*?(\w+)$$re$, $re$\1$re$);
732     IF eg_tcn IS NOT NULL AND eg_tcn <> '' THEN
733
734         eg_tcn_source := 'System Legacy';
735
736         PERFORM id FROM biblio.record_entry WHERE tcn_value = eg_tcn  AND NOT deleted;
737
738         IF NOT FOUND THEN
739             output.used := FALSE;
740         ELSE
741             output.used := TRUE;
742         END IF;
743
744         output.tcn := eg_tcn;
745         output.tcn_source := eg_tcn_source;
746         RETURN NEXT output;
747
748     END IF;
749
750     RETURN;
751 END;
752 $_$ LANGUAGE PLPGSQL;
753
754 CREATE OR REPLACE FUNCTION vandelay.add_field ( target_xml TEXT, source_xml TEXT, field TEXT, force_add INT ) RETURNS TEXT AS $_$
755
756     use MARC::Record;
757     use MARC::File::XML (BinaryEncoding => 'UTF-8');
758     use MARC::Charset;
759     use strict;
760
761     MARC::Charset->assume_unicode(1);
762
763     my $target_xml = shift;
764     my $source_xml = shift;
765     my $field_spec = shift;
766     my $force_add = shift || 0;
767
768     my $target_r = MARC::Record->new_from_xml( $target_xml );
769     my $source_r = MARC::Record->new_from_xml( $source_xml );
770
771     return $target_xml unless ($target_r && $source_r);
772
773     my @field_list = split(',', $field_spec);
774
775     my %fields;
776     for my $f (@field_list) {
777         $f =~ s/^\s*//; $f =~ s/\s*$//;
778         if ($f =~ /^(.{3})(\w*)(?:\[([^]]*)\])?$/) {
779             my $field = $1;
780             $field =~ s/\s+//;
781             my $sf = $2;
782             $sf =~ s/\s+//;
783             my $match = $3;
784             $match =~ s/^\s*//; $match =~ s/\s*$//;
785             $fields{$field} = { sf => [ split('', $sf) ] };
786             if ($match) {
787                 my ($msf,$mre) = split('~', $match);
788                 if (length($msf) > 0 and length($mre) > 0) {
789                     $msf =~ s/^\s*//; $msf =~ s/\s*$//;
790                     $mre =~ s/^\s*//; $mre =~ s/\s*$//;
791                     $fields{$field}{match} = { sf => $msf, re => qr/$mre/ };
792                 }
793             }
794         }
795     }
796
797     for my $f ( keys %fields) {
798         if ( @{$fields{$f}{sf}} ) {
799             for my $from_field ($source_r->field( $f )) {
800                 my @tos = $target_r->field( $f );
801                 if (!@tos) {
802                     next if (exists($fields{$f}{match}) and !$force_add);
803                     my @new_fields = map { $_->clone } $source_r->field( $f );
804                     $target_r->insert_fields_ordered( @new_fields );
805                 } else {
806                     for my $to_field (@tos) {
807                         if (exists($fields{$f}{match})) {
808                             next unless (grep { $_ =~ $fields{$f}{match}{re} } $to_field->subfield($fields{$f}{match}{sf}));
809                         }
810                         my @new_sf = map { ($_ => $from_field->subfield($_)) } @{$fields{$f}{sf}};
811                         $to_field->add_subfields( @new_sf );
812                     }
813                 }
814             }
815         } else {
816             my @new_fields = map { $_->clone } $source_r->field( $f );
817             $target_r->insert_fields_ordered( @new_fields );
818         }
819     }
820
821     $target_xml = $target_r->as_xml_record;
822     $target_xml =~ s/^<\?.+?\?>$//mo;
823     $target_xml =~ s/\n//sgo;
824     $target_xml =~ s/>\s+</></sgo;
825
826     return $target_xml;
827
828 $_$ LANGUAGE PLPERLU;
829
830 CREATE OR REPLACE FUNCTION vandelay.add_field ( target_xml TEXT, source_xml TEXT, field TEXT ) RETURNS TEXT AS $_$
831     SELECT vandelay.add_field( $1, $2, $3, 0 );
832 $_$ LANGUAGE SQL;
833
834 CREATE OR REPLACE FUNCTION vandelay.strip_field ( xml TEXT, field TEXT ) RETURNS TEXT AS $_$
835
836     use MARC::Record;
837     use MARC::File::XML (BinaryEncoding => 'UTF-8');
838     use MARC::Charset;
839     use strict;
840
841     MARC::Charset->assume_unicode(1);
842
843     my $xml = shift;
844     my $r = MARC::Record->new_from_xml( $xml );
845
846     return $xml unless ($r);
847
848     my $field_spec = shift;
849     my @field_list = split(',', $field_spec);
850
851     my %fields;
852     for my $f (@field_list) {
853         $f =~ s/^\s*//; $f =~ s/\s*$//;
854         if ($f =~ /^(.{3})(\w*)(?:\[([^]]*)\])?$/) {
855             my $field = $1;
856             $field =~ s/\s+//;
857             my $sf = $2;
858             $sf =~ s/\s+//;
859             my $match = $3;
860             $match =~ s/^\s*//; $match =~ s/\s*$//;
861             $fields{$field} = { sf => [ split('', $sf) ] };
862             if ($match) {
863                 my ($msf,$mre) = split('~', $match);
864                 if (length($msf) > 0 and length($mre) > 0) {
865                     $msf =~ s/^\s*//; $msf =~ s/\s*$//;
866                     $mre =~ s/^\s*//; $mre =~ s/\s*$//;
867                     $fields{$field}{match} = { sf => $msf, re => qr/$mre/ };
868                 }
869             }
870         }
871     }
872
873     for my $f ( keys %fields) {
874         for my $to_field ($r->field( $f )) {
875             if (exists($fields{$f}{match})) {
876                 next unless (grep { $_ =~ $fields{$f}{match}{re} } $to_field->subfield($fields{$f}{match}{sf}));
877             }
878
879             if ( @{$fields{$f}{sf}} ) {
880                 $to_field->delete_subfield(code => $fields{$f}{sf});
881             } else {
882                 $r->delete_field( $to_field );
883             }
884         }
885     }
886
887     $xml = $r->as_xml_record;
888     $xml =~ s/^<\?.+?\?>$//mo;
889     $xml =~ s/\n//sgo;
890     $xml =~ s/>\s+</></sgo;
891
892     return $xml;
893
894 $_$ LANGUAGE PLPERLU;
895
896 CREATE OR REPLACE FUNCTION vandelay.replace_field ( target_xml TEXT, source_xml TEXT, field TEXT ) RETURNS TEXT AS $_$
897 DECLARE
898     xml_output TEXT;
899     parsed_target TEXT;
900     curr_field TEXT;
901 BEGIN
902
903     parsed_target := vandelay.strip_field( target_xml, ''); -- this dance normalizes the format of the xml for the IF below
904
905     FOR curr_field IN SELECT UNNEST( STRING_TO_ARRAY(field, ',') ) LOOP -- naive split, but it's the same we use in the perl
906
907         xml_output := vandelay.strip_field( parsed_target, curr_field);
908
909         IF xml_output <> parsed_target  AND curr_field ~ E'~' THEN
910             -- we removed something, and there was a regexp restriction in the curr_field definition, so proceed
911             xml_output := vandelay.add_field( xml_output, source_xml, curr_field, 1 );
912         ELSIF curr_field !~ E'~' THEN
913             -- No regexp restriction, add the curr_field
914             xml_output := vandelay.add_field( xml_output, source_xml, curr_field, 0 );
915         END IF;
916
917         parsed_target := xml_output; -- in prep for any following loop iterations
918
919     END LOOP;
920
921     RETURN xml_output;
922 END;
923 $_$ LANGUAGE PLPGSQL;
924
925 CREATE OR REPLACE FUNCTION vandelay.merge_record_xml ( target_xml TEXT, source_xml TEXT, add_rule TEXT, replace_preserve_rule TEXT, strip_rule TEXT ) RETURNS TEXT AS $_$
926     SELECT vandelay.replace_field( vandelay.add_field( vandelay.strip_field( $1, $5) , $2, $3 ), $2, $4);
927 $_$ LANGUAGE SQL;
928
929 CREATE TYPE vandelay.compile_profile AS (add_rule TEXT, replace_rule TEXT, preserve_rule TEXT, strip_rule TEXT);
930 CREATE OR REPLACE FUNCTION vandelay.compile_profile ( incoming_xml TEXT ) RETURNS vandelay.compile_profile AS $_$
931 DECLARE
932     output              vandelay.compile_profile%ROWTYPE;
933     profile             vandelay.merge_profile%ROWTYPE;
934     profile_tmpl        TEXT;
935     profile_tmpl_owner  TEXT;
936     add_rule            TEXT := '';
937     strip_rule          TEXT := '';
938     replace_rule        TEXT := '';
939     preserve_rule       TEXT := '';
940
941 BEGIN
942
943     profile_tmpl := (oils_xpath('//*[@tag="905"]/*[@code="t"]/text()',incoming_xml))[1];
944     profile_tmpl_owner := (oils_xpath('//*[@tag="905"]/*[@code="o"]/text()',incoming_xml))[1];
945
946     IF profile_tmpl IS NOT NULL AND profile_tmpl <> '' AND profile_tmpl_owner IS NOT NULL AND profile_tmpl_owner <> '' THEN
947         SELECT  p.* INTO profile
948           FROM  vandelay.merge_profile p
949                 JOIN actor.org_unit u ON (u.id = p.owner)
950           WHERE p.name = profile_tmpl
951                 AND u.shortname = profile_tmpl_owner;
952
953         IF profile.id IS NOT NULL THEN
954             add_rule := COALESCE(profile.add_spec,'');
955             strip_rule := COALESCE(profile.strip_spec,'');
956             replace_rule := COALESCE(profile.replace_spec,'');
957             preserve_rule := COALESCE(profile.preserve_spec,'');
958         END IF;
959     END IF;
960
961     add_rule := add_rule || ',' || COALESCE(ARRAY_TO_STRING(oils_xpath('//*[@tag="905"]/*[@code="a"]/text()',incoming_xml),','),'');
962     strip_rule := strip_rule || ',' || COALESCE(ARRAY_TO_STRING(oils_xpath('//*[@tag="905"]/*[@code="d"]/text()',incoming_xml),','),'');
963     replace_rule := replace_rule || ',' || COALESCE(ARRAY_TO_STRING(oils_xpath('//*[@tag="905"]/*[@code="r"]/text()',incoming_xml),','),'');
964     preserve_rule := preserve_rule || ',' || COALESCE(ARRAY_TO_STRING(oils_xpath('//*[@tag="905"]/*[@code="p"]/text()',incoming_xml),','),'');
965
966     output.add_rule := BTRIM(add_rule,',');
967     output.replace_rule := BTRIM(replace_rule,',');
968     output.strip_rule := BTRIM(strip_rule,',');
969     output.preserve_rule := BTRIM(preserve_rule,',');
970
971     RETURN output;
972 END;
973 $_$ LANGUAGE PLPGSQL;
974
975 CREATE OR REPLACE FUNCTION vandelay.template_overlay_bib_record ( v_marc TEXT, eg_id BIGINT, merge_profile_id INT ) RETURNS BOOL AS $$
976 DECLARE
977     merge_profile   vandelay.merge_profile%ROWTYPE;
978     dyn_profile     vandelay.compile_profile%ROWTYPE;
979     editor_string   TEXT;
980     editor_id       INT;
981     source_marc     TEXT;
982     target_marc     TEXT;
983     eg_marc         TEXT;
984     replace_rule    TEXT;
985     match_count     INT;
986 BEGIN
987
988     SELECT  b.marc INTO eg_marc
989       FROM  biblio.record_entry b
990       WHERE b.id = eg_id
991       LIMIT 1;
992
993     IF eg_marc IS NULL OR v_marc IS NULL THEN
994         -- RAISE NOTICE 'no marc for template or bib record';
995         RETURN FALSE;
996     END IF;
997
998     dyn_profile := vandelay.compile_profile( v_marc );
999
1000     IF merge_profile_id IS NOT NULL THEN
1001         SELECT * INTO merge_profile FROM vandelay.merge_profile WHERE id = merge_profile_id;
1002         IF FOUND THEN
1003             dyn_profile.add_rule := BTRIM( dyn_profile.add_rule || ',' || COALESCE(merge_profile.add_spec,''), ',');
1004             dyn_profile.strip_rule := BTRIM( dyn_profile.strip_rule || ',' || COALESCE(merge_profile.strip_spec,''), ',');
1005             dyn_profile.replace_rule := BTRIM( dyn_profile.replace_rule || ',' || COALESCE(merge_profile.replace_spec,''), ',');
1006             dyn_profile.preserve_rule := BTRIM( dyn_profile.preserve_rule || ',' || COALESCE(merge_profile.preserve_spec,''), ',');
1007         END IF;
1008     END IF;
1009
1010     IF dyn_profile.replace_rule <> '' AND dyn_profile.preserve_rule <> '' THEN
1011         -- RAISE NOTICE 'both replace [%] and preserve [%] specified', dyn_profile.replace_rule, dyn_profile.preserve_rule;
1012         RETURN FALSE;
1013     END IF;
1014
1015     IF dyn_profile.replace_rule <> '' THEN
1016         source_marc = v_marc;
1017         target_marc = eg_marc;
1018         replace_rule = dyn_profile.replace_rule;
1019     ELSE
1020         source_marc = eg_marc;
1021         target_marc = v_marc;
1022         replace_rule = dyn_profile.preserve_rule;
1023     END IF;
1024
1025     UPDATE  biblio.record_entry
1026       SET   marc = vandelay.merge_record_xml( target_marc, source_marc, dyn_profile.add_rule, replace_rule, dyn_profile.strip_rule )
1027       WHERE id = eg_id;
1028
1029     IF NOT FOUND THEN
1030         -- RAISE NOTICE 'update of biblio.record_entry failed';
1031         RETURN FALSE;
1032     END IF;
1033
1034     RETURN TRUE;
1035
1036 END;
1037 $$ LANGUAGE PLPGSQL;
1038
1039 CREATE OR REPLACE FUNCTION vandelay.merge_record_xml ( target_marc TEXT, template_marc TEXT ) RETURNS TEXT AS $$
1040 DECLARE
1041     dyn_profile     vandelay.compile_profile%ROWTYPE;
1042     replace_rule    TEXT;
1043     tmp_marc        TEXT;
1044     trgt_marc        TEXT;
1045     tmpl_marc        TEXT;
1046     match_count     INT;
1047 BEGIN
1048
1049     IF target_marc IS NULL OR template_marc IS NULL THEN
1050         -- RAISE NOTICE 'no marc for target or template record';
1051         RETURN NULL;
1052     END IF;
1053
1054     dyn_profile := vandelay.compile_profile( template_marc );
1055
1056     IF dyn_profile.replace_rule <> '' AND dyn_profile.preserve_rule <> '' THEN
1057         -- RAISE NOTICE 'both replace [%] and preserve [%] specified', dyn_profile.replace_rule, dyn_profile.preserve_rule;
1058         RETURN NULL;
1059     END IF;
1060
1061     IF dyn_profile.replace_rule <> '' THEN
1062         trgt_marc = target_marc;
1063         tmpl_marc = template_marc;
1064         replace_rule = dyn_profile.replace_rule;
1065     ELSE
1066         tmp_marc = target_marc;
1067         trgt_marc = template_marc;
1068         tmpl_marc = tmp_marc;
1069         replace_rule = dyn_profile.preserve_rule;
1070     END IF;
1071
1072     RETURN vandelay.merge_record_xml( trgt_marc, tmpl_marc, dyn_profile.add_rule, replace_rule, dyn_profile.strip_rule );
1073
1074 END;
1075 $$ LANGUAGE PLPGSQL;
1076
1077 CREATE OR REPLACE FUNCTION vandelay.template_overlay_bib_record ( v_marc TEXT, eg_id BIGINT) RETURNS BOOL AS $$
1078     SELECT vandelay.template_overlay_bib_record( $1, $2, NULL);
1079 $$ LANGUAGE SQL;
1080
1081 CREATE OR REPLACE FUNCTION vandelay.overlay_bib_record ( import_id BIGINT, eg_id BIGINT, merge_profile_id INT ) RETURNS BOOL AS $$
1082 DECLARE
1083     merge_profile   vandelay.merge_profile%ROWTYPE;
1084     dyn_profile     vandelay.compile_profile%ROWTYPE;
1085     editor_string   TEXT;
1086     editor_id       INT;
1087     source_marc     TEXT;
1088     target_marc     TEXT;
1089     eg_marc         TEXT;
1090     v_marc          TEXT;
1091     replace_rule    TEXT;
1092     match_count     INT;
1093 BEGIN
1094
1095     SELECT  q.marc INTO v_marc
1096       FROM  vandelay.queued_record q
1097             JOIN vandelay.bib_match m ON (m.queued_record = q.id AND q.id = import_id)
1098       LIMIT 1;
1099
1100     IF v_marc IS NULL THEN
1101         -- RAISE NOTICE 'no marc for vandelay or bib record';
1102         RETURN FALSE;
1103     END IF;
1104
1105     IF vandelay.template_overlay_bib_record( v_marc, eg_id, merge_profile_id) THEN
1106         UPDATE  vandelay.queued_bib_record
1107           SET   imported_as = eg_id,
1108                 import_time = NOW()
1109           WHERE id = import_id;
1110
1111         editor_string := (oils_xpath('//*[@tag="905"]/*[@code="u"]/text()',v_marc))[1];
1112
1113         IF editor_string IS NOT NULL AND editor_string <> '' THEN
1114             SELECT usr INTO editor_id FROM actor.card WHERE barcode = editor_string;
1115
1116             IF editor_id IS NULL THEN
1117                 SELECT id INTO editor_id FROM actor.usr WHERE usrname = editor_string;
1118             END IF;
1119
1120             IF editor_id IS NOT NULL THEN
1121                 UPDATE biblio.record_entry SET editor = editor_id WHERE id = eg_id;
1122             END IF;
1123         END IF;
1124
1125         RETURN TRUE;
1126     END IF;
1127
1128     -- RAISE NOTICE 'update of biblio.record_entry failed';
1129
1130     RETURN FALSE;
1131
1132 END;
1133 $$ LANGUAGE PLPGSQL;
1134
1135 CREATE OR REPLACE FUNCTION vandelay.auto_overlay_bib_record ( import_id BIGINT, merge_profile_id INT ) RETURNS BOOL AS $$
1136 DECLARE
1137     eg_id           BIGINT;
1138     match_count     INT;
1139     match_attr      vandelay.bib_attr_definition%ROWTYPE;
1140 BEGIN
1141
1142     PERFORM * FROM vandelay.queued_bib_record WHERE import_time IS NOT NULL AND id = import_id;
1143
1144     IF FOUND THEN
1145         -- RAISE NOTICE 'already imported, cannot auto-overlay'
1146         RETURN FALSE;
1147     END IF;
1148
1149     SELECT COUNT(*) INTO match_count FROM vandelay.bib_match WHERE queued_record = import_id;
1150
1151     IF match_count <> 1 THEN
1152         -- RAISE NOTICE 'not an exact match';
1153         RETURN FALSE;
1154     END IF;
1155
1156     SELECT  d.* INTO match_attr
1157       FROM  vandelay.bib_attr_definition d
1158             JOIN vandelay.queued_bib_record_attr a ON (a.field = d.id)
1159             JOIN vandelay.bib_match m ON (m.matched_attr = a.id)
1160       WHERE m.queued_record = import_id;
1161
1162     IF NOT (match_attr.xpath ~ '@tag="901"' AND match_attr.xpath ~ '@code="c"') THEN
1163         -- RAISE NOTICE 'not a 901c match: %', match_attr.xpath;
1164         RETURN FALSE;
1165     END IF;
1166
1167     SELECT  m.eg_record INTO eg_id
1168       FROM  vandelay.bib_match m
1169       WHERE m.queued_record = import_id
1170       LIMIT 1;
1171
1172     IF eg_id IS NULL THEN
1173         RETURN FALSE;
1174     END IF;
1175
1176     RETURN vandelay.overlay_bib_record( import_id, eg_id, merge_profile_id );
1177 END;
1178 $$ LANGUAGE PLPGSQL;
1179
1180 CREATE OR REPLACE FUNCTION vandelay.auto_overlay_bib_queue ( queue_id BIGINT, merge_profile_id INT ) RETURNS SETOF BIGINT AS $$
1181 DECLARE
1182     queued_record   vandelay.queued_bib_record%ROWTYPE;
1183 BEGIN
1184
1185     FOR queued_record IN SELECT * FROM vandelay.queued_bib_record WHERE queue = queue_id AND import_time IS NULL LOOP
1186
1187         IF vandelay.auto_overlay_bib_record( queued_record.id, merge_profile_id ) THEN
1188             RETURN NEXT queued_record.id;
1189         END IF;
1190
1191     END LOOP;
1192
1193     RETURN;
1194     
1195 END;
1196 $$ LANGUAGE PLPGSQL;
1197
1198 CREATE OR REPLACE FUNCTION vandelay.auto_overlay_bib_queue ( queue_id BIGINT ) RETURNS SETOF BIGINT AS $$
1199     SELECT * FROM vandelay.auto_overlay_bib_queue( $1, NULL );
1200 $$ LANGUAGE SQL;
1201
1202 CREATE OR REPLACE FUNCTION vandelay.ingest_items ( import_id BIGINT, attr_def_id BIGINT ) RETURNS SETOF vandelay.import_item AS $$
1203 DECLARE
1204
1205     owning_lib      TEXT;
1206     circ_lib        TEXT;
1207     call_number     TEXT;
1208     copy_number     TEXT;
1209     status          TEXT;
1210     location        TEXT;
1211     circulate       TEXT;
1212     deposit         TEXT;
1213     deposit_amount  TEXT;
1214     ref             TEXT;
1215     holdable        TEXT;
1216     price           TEXT;
1217     barcode         TEXT;
1218     circ_modifier   TEXT;
1219     circ_as_type    TEXT;
1220     alert_message   TEXT;
1221     opac_visible    TEXT;
1222     pub_note        TEXT;
1223     priv_note       TEXT;
1224
1225     attr_def        RECORD;
1226     tmp_attr_set    RECORD;
1227     attr_set        vandelay.import_item%ROWTYPE;
1228
1229     xpath           TEXT;
1230
1231 BEGIN
1232
1233     SELECT * INTO attr_def FROM vandelay.import_item_attr_definition WHERE id = attr_def_id;
1234
1235     IF FOUND THEN
1236
1237         attr_set.definition := attr_def.id; 
1238     
1239         -- Build the combined XPath
1240     
1241         owning_lib :=
1242             CASE
1243                 WHEN attr_def.owning_lib IS NULL THEN 'null()'
1244                 WHEN LENGTH( attr_def.owning_lib ) = 1 THEN '//*[@tag="' || attr_def.tag || '"]/*[@code="' || attr_def.owning_lib || '"]'
1245                 ELSE '//*[@tag="' || attr_def.tag || '"]/*' || attr_def.owning_lib
1246             END;
1247     
1248         circ_lib :=
1249             CASE
1250                 WHEN attr_def.circ_lib IS NULL THEN 'null()'
1251                 WHEN LENGTH( attr_def.circ_lib ) = 1 THEN '//*[@tag="' || attr_def.tag || '"]/*[@code="' || attr_def.circ_lib || '"]'
1252                 ELSE '//*[@tag="' || attr_def.tag || '"]/*' || attr_def.circ_lib
1253             END;
1254     
1255         call_number :=
1256             CASE
1257                 WHEN attr_def.call_number IS NULL THEN 'null()'
1258                 WHEN LENGTH( attr_def.call_number ) = 1 THEN '//*[@tag="' || attr_def.tag || '"]/*[@code="' || attr_def.call_number || '"]'
1259                 ELSE '//*[@tag="' || attr_def.tag || '"]/*' || attr_def.call_number
1260             END;
1261     
1262         copy_number :=
1263             CASE
1264                 WHEN attr_def.copy_number IS NULL THEN 'null()'
1265                 WHEN LENGTH( attr_def.copy_number ) = 1 THEN '//*[@tag="' || attr_def.tag || '"]/*[@code="' || attr_def.copy_number || '"]'
1266                 ELSE '//*[@tag="' || attr_def.tag || '"]/*' || attr_def.copy_number
1267             END;
1268     
1269         status :=
1270             CASE
1271                 WHEN attr_def.status IS NULL THEN 'null()'
1272                 WHEN LENGTH( attr_def.status ) = 1 THEN '//*[@tag="' || attr_def.tag || '"]/*[@code="' || attr_def.status || '"]'
1273                 ELSE '//*[@tag="' || attr_def.tag || '"]/*' || attr_def.status
1274             END;
1275     
1276         location :=
1277             CASE
1278                 WHEN attr_def.location IS NULL THEN 'null()'
1279                 WHEN LENGTH( attr_def.location ) = 1 THEN '//*[@tag="' || attr_def.tag || '"]/*[@code="' || attr_def.location || '"]'
1280                 ELSE '//*[@tag="' || attr_def.tag || '"]/*' || attr_def.location
1281             END;
1282     
1283         circulate :=
1284             CASE
1285                 WHEN attr_def.circulate IS NULL THEN 'null()'
1286                 WHEN LENGTH( attr_def.circulate ) = 1 THEN '//*[@tag="' || attr_def.tag || '"]/*[@code="' || attr_def.circulate || '"]'
1287                 ELSE '//*[@tag="' || attr_def.tag || '"]/*' || attr_def.circulate
1288             END;
1289     
1290         deposit :=
1291             CASE
1292                 WHEN attr_def.deposit IS NULL THEN 'null()'
1293                 WHEN LENGTH( attr_def.deposit ) = 1 THEN '//*[@tag="' || attr_def.tag || '"]/*[@code="' || attr_def.deposit || '"]'
1294                 ELSE '//*[@tag="' || attr_def.tag || '"]/*' || attr_def.deposit
1295             END;
1296     
1297         deposit_amount :=
1298             CASE
1299                 WHEN attr_def.deposit_amount IS NULL THEN 'null()'
1300                 WHEN LENGTH( attr_def.deposit_amount ) = 1 THEN '//*[@tag="' || attr_def.tag || '"]/*[@code="' || attr_def.deposit_amount || '"]'
1301                 ELSE '//*[@tag="' || attr_def.tag || '"]/*' || attr_def.deposit_amount
1302             END;
1303     
1304         ref :=
1305             CASE
1306                 WHEN attr_def.ref IS NULL THEN 'null()'
1307                 WHEN LENGTH( attr_def.ref ) = 1 THEN '//*[@tag="' || attr_def.tag || '"]/*[@code="' || attr_def.ref || '"]'
1308                 ELSE '//*[@tag="' || attr_def.tag || '"]/*' || attr_def.ref
1309             END;
1310     
1311         holdable :=
1312             CASE
1313                 WHEN attr_def.holdable IS NULL THEN 'null()'
1314                 WHEN LENGTH( attr_def.holdable ) = 1 THEN '//*[@tag="' || attr_def.tag || '"]/*[@code="' || attr_def.holdable || '"]'
1315                 ELSE '//*[@tag="' || attr_def.tag || '"]/*' || attr_def.holdable
1316             END;
1317     
1318         price :=
1319             CASE
1320                 WHEN attr_def.price IS NULL THEN 'null()'
1321                 WHEN LENGTH( attr_def.price ) = 1 THEN '//*[@tag="' || attr_def.tag || '"]/*[@code="' || attr_def.price || '"]'
1322                 ELSE '//*[@tag="' || attr_def.tag || '"]/*' || attr_def.price
1323             END;
1324     
1325         barcode :=
1326             CASE
1327                 WHEN attr_def.barcode IS NULL THEN 'null()'
1328                 WHEN LENGTH( attr_def.barcode ) = 1 THEN '//*[@tag="' || attr_def.tag || '"]/*[@code="' || attr_def.barcode || '"]'
1329                 ELSE '//*[@tag="' || attr_def.tag || '"]/*' || attr_def.barcode
1330             END;
1331     
1332         circ_modifier :=
1333             CASE
1334                 WHEN attr_def.circ_modifier IS NULL THEN 'null()'
1335                 WHEN LENGTH( attr_def.circ_modifier ) = 1 THEN '//*[@tag="' || attr_def.tag || '"]/*[@code="' || attr_def.circ_modifier || '"]'
1336                 ELSE '//*[@tag="' || attr_def.tag || '"]/*' || attr_def.circ_modifier
1337             END;
1338     
1339         circ_as_type :=
1340             CASE
1341                 WHEN attr_def.circ_as_type IS NULL THEN 'null()'
1342                 WHEN LENGTH( attr_def.circ_as_type ) = 1 THEN '//*[@tag="' || attr_def.tag || '"]/*[@code="' || attr_def.circ_as_type || '"]'
1343                 ELSE '//*[@tag="' || attr_def.tag || '"]/*' || attr_def.circ_as_type
1344             END;
1345     
1346         alert_message :=
1347             CASE
1348                 WHEN attr_def.alert_message IS NULL THEN 'null()'
1349                 WHEN LENGTH( attr_def.alert_message ) = 1 THEN '//*[@tag="' || attr_def.tag || '"]/*[@code="' || attr_def.alert_message || '"]'
1350                 ELSE '//*[@tag="' || attr_def.tag || '"]/*' || attr_def.alert_message
1351             END;
1352     
1353         opac_visible :=
1354             CASE
1355                 WHEN attr_def.opac_visible IS NULL THEN 'null()'
1356                 WHEN LENGTH( attr_def.opac_visible ) = 1 THEN '//*[@tag="' || attr_def.tag || '"]/*[@code="' || attr_def.opac_visible || '"]'
1357                 ELSE '//*[@tag="' || attr_def.tag || '"]/*' || attr_def.opac_visible
1358             END;
1359
1360         pub_note :=
1361             CASE
1362                 WHEN attr_def.pub_note IS NULL THEN 'null()'
1363                 WHEN LENGTH( attr_def.pub_note ) = 1 THEN '//*[@tag="' || attr_def.tag || '"]/*[@code="' || attr_def.pub_note || '"]'
1364                 ELSE '//*[@tag="' || attr_def.tag || '"]/*' || attr_def.pub_note
1365             END;
1366         priv_note :=
1367             CASE
1368                 WHEN attr_def.priv_note IS NULL THEN 'null()'
1369                 WHEN LENGTH( attr_def.priv_note ) = 1 THEN '//*[@tag="' || attr_def.tag || '"]/*[@code="' || attr_def.priv_note || '"]'
1370                 ELSE '//*[@tag="' || attr_def.tag || '"]/*' || attr_def.priv_note
1371             END;
1372     
1373     
1374         xpath := 
1375             owning_lib      || '|' || 
1376             circ_lib        || '|' || 
1377             call_number     || '|' || 
1378             copy_number     || '|' || 
1379             status          || '|' || 
1380             location        || '|' || 
1381             circulate       || '|' || 
1382             deposit         || '|' || 
1383             deposit_amount  || '|' || 
1384             ref             || '|' || 
1385             holdable        || '|' || 
1386             price           || '|' || 
1387             barcode         || '|' || 
1388             circ_modifier   || '|' || 
1389             circ_as_type    || '|' || 
1390             alert_message   || '|' || 
1391             pub_note        || '|' || 
1392             priv_note       || '|' || 
1393             opac_visible;
1394
1395         -- RAISE NOTICE 'XPath: %', xpath;
1396         
1397         FOR tmp_attr_set IN
1398                 SELECT  *
1399                   FROM  oils_xpath_table( 'id', 'marc', 'vandelay.queued_bib_record', xpath, 'id = ' || import_id )
1400                             AS t( id INT, ol TEXT, clib TEXT, cn TEXT, cnum TEXT, cs TEXT, cl TEXT, circ TEXT,
1401                                   dep TEXT, dep_amount TEXT, r TEXT, hold TEXT, pr TEXT, bc TEXT, circ_mod TEXT,
1402                                   circ_as TEXT, amessage TEXT, note TEXT, pnote TEXT, opac_vis TEXT )
1403         LOOP
1404     
1405             tmp_attr_set.pr = REGEXP_REPLACE(tmp_attr_set.pr, E'[^0-9\\.]', '', 'g');
1406             tmp_attr_set.dep_amount = REGEXP_REPLACE(tmp_attr_set.dep_amount, E'[^0-9\\.]', '', 'g');
1407
1408             tmp_attr_set.pr := NULLIF( tmp_attr_set.pr, '' );
1409             tmp_attr_set.dep_amount := NULLIF( tmp_attr_set.dep_amount, '' );
1410     
1411             SELECT id INTO attr_set.owning_lib FROM actor.org_unit WHERE shortname = UPPER(tmp_attr_set.ol); -- INT
1412             SELECT id INTO attr_set.circ_lib FROM actor.org_unit WHERE shortname = UPPER(tmp_attr_set.clib); -- INT
1413             SELECT id INTO attr_set.status FROM config.copy_status WHERE LOWER(name) = LOWER(tmp_attr_set.cs); -- INT
1414     
1415             SELECT  id INTO attr_set.location
1416               FROM  asset.copy_location
1417               WHERE LOWER(name) = LOWER(tmp_attr_set.cl)
1418                     AND asset.copy_location.owning_lib = COALESCE(attr_set.owning_lib, attr_set.circ_lib); -- INT
1419     
1420             attr_set.circulate      :=
1421                 LOWER( SUBSTRING( tmp_attr_set.circ, 1, 1)) IN ('t','y','1')
1422                 OR LOWER(tmp_attr_set.circ) = 'circulating'; -- BOOL
1423
1424             attr_set.deposit        :=
1425                 LOWER( SUBSTRING( tmp_attr_set.dep, 1, 1 ) ) IN ('t','y','1')
1426                 OR LOWER(tmp_attr_set.dep) = 'deposit'; -- BOOL
1427
1428             attr_set.holdable       :=
1429                 LOWER( SUBSTRING( tmp_attr_set.hold, 1, 1 ) ) IN ('t','y','1')
1430                 OR LOWER(tmp_attr_set.hold) = 'holdable'; -- BOOL
1431
1432             attr_set.opac_visible   :=
1433                 LOWER( SUBSTRING( tmp_attr_set.opac_vis, 1, 1 ) ) IN ('t','y','1')
1434                 OR LOWER(tmp_attr_set.opac_vis) = 'visible'; -- BOOL
1435
1436             attr_set.ref            :=
1437                 LOWER( SUBSTRING( tmp_attr_set.r, 1, 1 ) ) IN ('t','y','1')
1438                 OR LOWER(tmp_attr_set.r) = 'reference'; -- BOOL
1439     
1440             attr_set.copy_number    := tmp_attr_set.cnum::INT; -- INT,
1441             attr_set.deposit_amount := tmp_attr_set.dep_amount::NUMERIC(6,2); -- NUMERIC(6,2),
1442             attr_set.price          := tmp_attr_set.pr::NUMERIC(8,2); -- NUMERIC(8,2),
1443     
1444             attr_set.call_number    := tmp_attr_set.cn; -- TEXT
1445             attr_set.barcode        := tmp_attr_set.bc; -- TEXT,
1446             attr_set.circ_modifier  := tmp_attr_set.circ_mod; -- TEXT,
1447             attr_set.circ_as_type   := tmp_attr_set.circ_as; -- TEXT,
1448             attr_set.alert_message  := tmp_attr_set.amessage; -- TEXT,
1449             attr_set.pub_note       := tmp_attr_set.note; -- TEXT,
1450             attr_set.priv_note      := tmp_attr_set.pnote; -- TEXT,
1451             attr_set.alert_message  := tmp_attr_set.amessage; -- TEXT,
1452     
1453             RETURN NEXT attr_set;
1454     
1455         END LOOP;
1456     
1457     END IF;
1458
1459     RETURN;
1460
1461 END;
1462 $$ LANGUAGE PLPGSQL;
1463
1464
1465 CREATE OR REPLACE FUNCTION vandelay.ingest_bib_marc ( ) RETURNS TRIGGER AS $$
1466 DECLARE
1467     value   TEXT;
1468     atype   TEXT;
1469     adef    RECORD;
1470 BEGIN
1471     FOR adef IN SELECT * FROM vandelay.bib_attr_definition LOOP
1472
1473         SELECT extract_marc_field('vandelay.queued_bib_record', id, adef.xpath, adef.remove) INTO value FROM vandelay.queued_bib_record WHERE id = NEW.id;
1474         IF (value IS NOT NULL AND value <> '') THEN
1475             INSERT INTO vandelay.queued_bib_record_attr (record, field, attr_value) VALUES (NEW.id, adef.id, value);
1476         END IF;
1477
1478     END LOOP;
1479
1480     RETURN NULL;
1481 END;
1482 $$ LANGUAGE PLPGSQL;
1483
1484 CREATE OR REPLACE FUNCTION vandelay.ingest_bib_items ( ) RETURNS TRIGGER AS $func$
1485 DECLARE
1486     attr_def    BIGINT;
1487     item_data   vandelay.import_item%ROWTYPE;
1488 BEGIN
1489
1490     SELECT item_attr_def INTO attr_def FROM vandelay.bib_queue WHERE id = NEW.queue;
1491
1492     FOR item_data IN SELECT * FROM vandelay.ingest_items( NEW.id::BIGINT, attr_def ) LOOP
1493         INSERT INTO vandelay.import_item (
1494             record,
1495             definition,
1496             owning_lib,
1497             circ_lib,
1498             call_number,
1499             copy_number,
1500             status,
1501             location,
1502             circulate,
1503             deposit,
1504             deposit_amount,
1505             ref,
1506             holdable,
1507             price,
1508             barcode,
1509             circ_modifier,
1510             circ_as_type,
1511             alert_message,
1512             pub_note,
1513             priv_note,
1514             opac_visible
1515         ) VALUES (
1516             NEW.id,
1517             item_data.definition,
1518             item_data.owning_lib,
1519             item_data.circ_lib,
1520             item_data.call_number,
1521             item_data.copy_number,
1522             item_data.status,
1523             item_data.location,
1524             item_data.circulate,
1525             item_data.deposit,
1526             item_data.deposit_amount,
1527             item_data.ref,
1528             item_data.holdable,
1529             item_data.price,
1530             item_data.barcode,
1531             item_data.circ_modifier,
1532             item_data.circ_as_type,
1533             item_data.alert_message,
1534             item_data.pub_note,
1535             item_data.priv_note,
1536             item_data.opac_visible
1537         );
1538     END LOOP;
1539
1540     RETURN NULL;
1541 END;
1542 $func$ LANGUAGE PLPGSQL;
1543
1544 CREATE OR REPLACE FUNCTION vandelay.match_bib_record ( ) RETURNS TRIGGER AS $func$
1545 DECLARE
1546     attr        RECORD;
1547     attr_def    RECORD;
1548     eg_rec      RECORD;
1549     id_value    TEXT;
1550     exact_id    BIGINT;
1551 BEGIN
1552
1553     DELETE FROM vandelay.bib_match WHERE queued_record = NEW.id;
1554
1555     SELECT * INTO attr_def FROM vandelay.bib_attr_definition WHERE xpath = '//*[@tag="901"]/*[@code="c"]' ORDER BY id LIMIT 1;
1556
1557     IF attr_def IS NOT NULL AND attr_def.id IS NOT NULL THEN
1558         id_value := extract_marc_field('vandelay.queued_bib_record', NEW.id, attr_def.xpath, attr_def.remove);
1559     
1560         IF id_value IS NOT NULL AND id_value <> '' AND id_value ~ $r$^\d+$$r$ THEN
1561             SELECT id INTO exact_id FROM biblio.record_entry WHERE id = id_value::BIGINT AND NOT deleted;
1562             SELECT * INTO attr FROM vandelay.queued_bib_record_attr WHERE record = NEW.id and field = attr_def.id LIMIT 1;
1563             IF exact_id IS NOT NULL THEN
1564                 INSERT INTO vandelay.bib_match (field_type, matched_attr, queued_record, eg_record) VALUES ('id', attr.id, NEW.id, exact_id);
1565             END IF;
1566         END IF;
1567     END IF;
1568
1569     IF exact_id IS NULL THEN
1570         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
1571     
1572                 -- All numbers? check for an id match
1573                 IF (attr.attr_value ~ $r$^\d+$$r$) THEN
1574                 FOR eg_rec IN SELECT * FROM biblio.record_entry WHERE id = attr.attr_value::BIGINT AND deleted IS FALSE LOOP
1575                         INSERT INTO vandelay.bib_match (field_type, matched_attr, queued_record, eg_record) VALUES ('id', attr.id, NEW.id, eg_rec.id);
1576                         END LOOP;
1577                 END IF;
1578     
1579                 -- Looks like an ISBN? check for an isbn match
1580                 IF (attr.attr_value ~* $r$^[0-9x]+$$r$ AND character_length(attr.attr_value) IN (10,13)) THEN
1581                 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
1582                                 PERFORM id FROM biblio.record_entry WHERE id = eg_rec.record AND deleted IS FALSE;
1583                                 IF FOUND THEN
1584                                 INSERT INTO vandelay.bib_match (field_type, matched_attr, queued_record, eg_record) VALUES ('isbn', attr.id, NEW.id, eg_rec.record);
1585                                 END IF;
1586                         END LOOP;
1587     
1588                         -- subcheck for isbn-as-tcn
1589                     FOR eg_rec IN SELECT * FROM biblio.record_entry WHERE tcn_value = 'i' || attr.attr_value AND deleted IS FALSE LOOP
1590                             INSERT INTO vandelay.bib_match (field_type, matched_attr, queued_record, eg_record) VALUES ('tcn_value', attr.id, NEW.id, eg_rec.id);
1591                 END LOOP;
1592                 END IF;
1593     
1594                 -- check for an OCLC tcn_value match
1595                 IF (attr.attr_value ~ $r$^o\d+$$r$) THEN
1596                     FOR eg_rec IN SELECT * FROM biblio.record_entry WHERE tcn_value = regexp_replace(attr.attr_value,'^o','ocm') AND deleted IS FALSE LOOP
1597                             INSERT INTO vandelay.bib_match (field_type, matched_attr, queued_record, eg_record) VALUES ('tcn_value', attr.id, NEW.id, eg_rec.id);
1598                 END LOOP;
1599                 END IF;
1600     
1601                 -- check for a direct tcn_value match
1602             FOR eg_rec IN SELECT * FROM biblio.record_entry WHERE tcn_value = attr.attr_value AND deleted IS FALSE LOOP
1603                 INSERT INTO vandelay.bib_match (field_type, matched_attr, queued_record, eg_record) VALUES ('tcn_value', attr.id, NEW.id, eg_rec.id);
1604             END LOOP;
1605     
1606                 -- check for a direct item barcode match
1607             FOR eg_rec IN
1608                     SELECT  DISTINCT b.*
1609                       FROM  biblio.record_entry b
1610                             JOIN asset.call_number cn ON (cn.record = b.id)
1611                             JOIN asset.copy cp ON (cp.call_number = cn.id)
1612                       WHERE cp.barcode = attr.attr_value AND cp.deleted IS FALSE
1613             LOOP
1614                 INSERT INTO vandelay.bib_match (field_type, matched_attr, queued_record, eg_record) VALUES ('id', attr.id, NEW.id, eg_rec.id);
1615             END LOOP;
1616     
1617         END LOOP;
1618     END IF;
1619
1620     RETURN NULL;
1621 END;
1622 $func$ LANGUAGE PLPGSQL;
1623
1624 CREATE OR REPLACE FUNCTION vandelay.cleanup_bib_marc ( ) RETURNS TRIGGER AS $$
1625 BEGIN
1626     DELETE FROM vandelay.queued_bib_record_attr WHERE record = OLD.id;
1627     DELETE FROM vandelay.import_item WHERE record = OLD.id;
1628
1629     IF TG_OP = 'UPDATE' THEN
1630         RETURN NEW;
1631     END IF;
1632     RETURN OLD;
1633 END;
1634 $$ LANGUAGE PLPGSQL;
1635
1636 CREATE TRIGGER cleanup_bib_trigger
1637     BEFORE UPDATE OR DELETE ON vandelay.queued_bib_record
1638     FOR EACH ROW EXECUTE PROCEDURE vandelay.cleanup_bib_marc();
1639
1640 CREATE TRIGGER ingest_bib_trigger
1641     AFTER INSERT OR UPDATE ON vandelay.queued_bib_record
1642     FOR EACH ROW EXECUTE PROCEDURE vandelay.ingest_bib_marc();
1643
1644 CREATE TRIGGER ingest_item_trigger
1645     AFTER INSERT OR UPDATE ON vandelay.queued_bib_record
1646     FOR EACH ROW EXECUTE PROCEDURE vandelay.ingest_bib_items();
1647
1648 CREATE TRIGGER zz_match_bibs_trigger
1649     AFTER INSERT OR UPDATE ON vandelay.queued_bib_record
1650     FOR EACH ROW EXECUTE PROCEDURE vandelay.match_bib_record();
1651
1652
1653 /* Authority stuff down here */
1654 ---------------------------------------
1655 CREATE TABLE vandelay.authority_attr_definition (
1656         id                      SERIAL  PRIMARY KEY,
1657         code            TEXT    UNIQUE NOT NULL,
1658         description     TEXT,
1659         xpath           TEXT    NOT NULL,
1660         remove          TEXT    NOT NULL DEFAULT ''
1661 );
1662
1663 CREATE TABLE vandelay.authority_queue (
1664         queue_type      TEXT            NOT NULL DEFAULT 'authority' CHECK (queue_type = 'authority'),
1665         CONSTRAINT vand_authority_queue_name_once_per_owner_const UNIQUE (owner,name,queue_type)
1666 ) INHERITS (vandelay.queue);
1667 ALTER TABLE vandelay.authority_queue ADD PRIMARY KEY (id);
1668
1669 CREATE TABLE vandelay.queued_authority_record (
1670         queue           INT     NOT NULL REFERENCES vandelay.authority_queue (id) ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED,
1671         imported_as     INT     REFERENCES authority.record_entry (id) DEFERRABLE INITIALLY DEFERRED,
1672         import_error    TEXT    REFERENCES vandelay.import_error (code) ON DELETE SET NULL ON UPDATE CASCADE DEFERRABLE INITIALLY DEFERRED,
1673         error_detail    TEXT
1674 ) INHERITS (vandelay.queued_record);
1675 ALTER TABLE vandelay.queued_authority_record ADD PRIMARY KEY (id);
1676 CREATE INDEX queued_authority_record_queue_idx ON vandelay.queued_authority_record (queue);
1677
1678 CREATE TABLE vandelay.queued_authority_record_attr (
1679         id                      BIGSERIAL       PRIMARY KEY,
1680         record          BIGINT          NOT NULL REFERENCES vandelay.queued_authority_record (id) DEFERRABLE INITIALLY DEFERRED,
1681         field           INT                     NOT NULL REFERENCES vandelay.authority_attr_definition (id) DEFERRABLE INITIALLY DEFERRED,
1682         attr_value      TEXT            NOT NULL
1683 );
1684 CREATE INDEX queued_authority_record_attr_record_idx ON vandelay.queued_authority_record_attr (record);
1685
1686 CREATE TABLE vandelay.authority_match (
1687         id                              BIGSERIAL       PRIMARY KEY,
1688         matched_set     INT                     REFERENCES vandelay.match_set (id) ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED,
1689         queued_record   BIGINT          REFERENCES vandelay.queued_authority_record (id) ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED,
1690         eg_record               BIGINT          REFERENCES authority.record_entry (id) DEFERRABLE INITIALLY DEFERRED,
1691     quality         INT         NOT NULL DEFAULT 0
1692 );
1693
1694 CREATE OR REPLACE FUNCTION vandelay.ingest_authority_marc ( ) RETURNS TRIGGER AS $$
1695 DECLARE
1696     value   TEXT;
1697     atype   TEXT;
1698     adef    RECORD;
1699 BEGIN
1700     FOR adef IN SELECT * FROM vandelay.authority_attr_definition LOOP
1701
1702         SELECT extract_marc_field('vandelay.queued_authority_record', id, adef.xpath, adef.remove) INTO value FROM vandelay.queued_authority_record WHERE id = NEW.id;
1703         IF (value IS NOT NULL AND value <> '') THEN
1704             INSERT INTO vandelay.queued_authority_record_attr (record, field, attr_value) VALUES (NEW.id, adef.id, value);
1705         END IF;
1706
1707     END LOOP;
1708
1709     RETURN NULL;
1710 END;
1711 $$ LANGUAGE PLPGSQL;
1712
1713 CREATE OR REPLACE FUNCTION vandelay.cleanup_authority_marc ( ) RETURNS TRIGGER AS $$
1714 BEGIN
1715     DELETE FROM vandelay.queued_authority_record_attr WHERE record = OLD.id;
1716     IF TG_OP = 'UPDATE' THEN
1717         RETURN NEW;
1718     END IF;
1719     RETURN OLD;
1720 END;
1721 $$ LANGUAGE PLPGSQL;
1722
1723 CREATE TRIGGER cleanup_authority_trigger
1724     BEFORE UPDATE OR DELETE ON vandelay.queued_authority_record
1725     FOR EACH ROW EXECUTE PROCEDURE vandelay.cleanup_authority_marc();
1726
1727 CREATE TRIGGER ingest_authority_trigger
1728     AFTER INSERT OR UPDATE ON vandelay.queued_authority_record
1729     FOR EACH ROW EXECUTE PROCEDURE vandelay.ingest_authority_marc();
1730
1731 CREATE OR REPLACE FUNCTION vandelay.overlay_authority_record ( import_id BIGINT, eg_id BIGINT, merge_profile_id INT ) RETURNS BOOL AS $$
1732 DECLARE
1733     merge_profile   vandelay.merge_profile%ROWTYPE;
1734     dyn_profile     vandelay.compile_profile%ROWTYPE;
1735     source_marc     TEXT;
1736     target_marc     TEXT;
1737     eg_marc         TEXT;
1738     v_marc          TEXT;
1739     replace_rule    TEXT;
1740     match_count     INT;
1741 BEGIN
1742
1743     SELECT  b.marc INTO eg_marc
1744       FROM  authority.record_entry b
1745             JOIN vandelay.authority_match m ON (m.eg_record = b.id AND m.queued_record = import_id)
1746       LIMIT 1;
1747
1748     SELECT  q.marc INTO v_marc
1749       FROM  vandelay.queued_record q
1750             JOIN vandelay.authority_match m ON (m.queued_record = q.id AND q.id = import_id)
1751       LIMIT 1;
1752
1753     IF eg_marc IS NULL OR v_marc IS NULL THEN
1754         -- RAISE NOTICE 'no marc for vandelay or authority record';
1755         RETURN FALSE;
1756     END IF;
1757
1758     dyn_profile := vandelay.compile_profile( v_marc );
1759
1760     IF merge_profile_id IS NOT NULL THEN
1761         SELECT * INTO merge_profile FROM vandelay.merge_profile WHERE id = merge_profile_id;
1762         IF FOUND THEN
1763             dyn_profile.add_rule := BTRIM( dyn_profile.add_rule || ',' || COALESCE(merge_profile.add_spec,''), ',');
1764             dyn_profile.strip_rule := BTRIM( dyn_profile.strip_rule || ',' || COALESCE(merge_profile.strip_spec,''), ',');
1765             dyn_profile.replace_rule := BTRIM( dyn_profile.replace_rule || ',' || COALESCE(merge_profile.replace_spec,''), ',');
1766             dyn_profile.preserve_rule := BTRIM( dyn_profile.preserve_rule || ',' || COALESCE(merge_profile.preserve_spec,''), ',');
1767         END IF;
1768     END IF;
1769
1770     IF dyn_profile.replace_rule <> '' AND dyn_profile.preserve_rule <> '' THEN
1771         -- RAISE NOTICE 'both replace [%] and preserve [%] specified', dyn_profile.replace_rule, dyn_profile.preserve_rule;
1772         RETURN FALSE;
1773     END IF;
1774
1775     IF dyn_profile.replace_rule <> '' THEN
1776         source_marc = v_marc;
1777         target_marc = eg_marc;
1778         replace_rule = dyn_profile.replace_rule;
1779     ELSE
1780         source_marc = eg_marc;
1781         target_marc = v_marc;
1782         replace_rule = dyn_profile.preserve_rule;
1783     END IF;
1784
1785     UPDATE  authority.record_entry
1786       SET   marc = vandelay.merge_record_xml( target_marc, source_marc, dyn_profile.add_rule, replace_rule, dyn_profile.strip_rule )
1787       WHERE id = eg_id;
1788
1789     IF FOUND THEN
1790         UPDATE  vandelay.queued_authority_record
1791           SET   imported_as = eg_id,
1792                 import_time = NOW()
1793           WHERE id = import_id;
1794         RETURN TRUE;
1795     END IF;
1796
1797     -- RAISE NOTICE 'update of authority.record_entry failed';
1798
1799     RETURN FALSE;
1800
1801 END;
1802 $$ LANGUAGE PLPGSQL;
1803
1804 CREATE OR REPLACE FUNCTION vandelay.auto_overlay_authority_record ( import_id BIGINT, merge_profile_id INT ) RETURNS BOOL AS $$
1805 DECLARE
1806     eg_id           BIGINT;
1807     match_count     INT;
1808 BEGIN
1809     SELECT COUNT(*) INTO match_count FROM vandelay.authority_match WHERE queued_record = import_id;
1810
1811     IF match_count <> 1 THEN
1812         -- RAISE NOTICE 'not an exact match';
1813         RETURN FALSE;
1814     END IF;
1815
1816     SELECT  m.eg_record INTO eg_id
1817       FROM  vandelay.authority_match m
1818       WHERE m.queued_record = import_id
1819       LIMIT 1;
1820
1821     IF eg_id IS NULL THEN
1822         RETURN FALSE;
1823     END IF;
1824
1825     RETURN vandelay.overlay_authority_record( import_id, eg_id, merge_profile_id );
1826 END;
1827 $$ LANGUAGE PLPGSQL;
1828
1829 CREATE OR REPLACE FUNCTION vandelay.auto_overlay_authority_queue ( queue_id BIGINT, merge_profile_id INT ) RETURNS SETOF BIGINT AS $$
1830 DECLARE
1831     queued_record   vandelay.queued_authority_record%ROWTYPE;
1832 BEGIN
1833
1834     FOR queued_record IN SELECT * FROM vandelay.queued_authority_record WHERE queue = queue_id AND import_time IS NULL LOOP
1835
1836         IF vandelay.auto_overlay_authority_record( queued_record.id, merge_profile_id ) THEN
1837             RETURN NEXT queued_record.id;
1838         END IF;
1839
1840     END LOOP;
1841
1842     RETURN;
1843     
1844 END;
1845 $$ LANGUAGE PLPGSQL;
1846
1847 CREATE OR REPLACE FUNCTION vandelay.auto_overlay_authority_queue ( queue_id BIGINT ) RETURNS SETOF BIGINT AS $$
1848     SELECT * FROM vandelay.auto_overlay_authority_queue( $1, NULL );
1849 $$ LANGUAGE SQL;
1850
1851
1852 -- Vandelay (for importing and exporting records) 012.schema.vandelay.sql 
1853 --INSERT INTO vandelay.bib_attr_definition ( id, code, description, xpath ) VALUES (1, 'title', oils_i18n_gettext(1, 'vqbrad', 'Title of work', 'description'),'//*[@tag="245"]/*[contains("abcmnopr",@code)]');
1854 --INSERT INTO vandelay.bib_attr_definition ( id, code, description, xpath ) VALUES (2, 'author', oils_i18n_gettext(1, 'vqbrad', 'Author of work', 'description'),'//*[@tag="100" or @tag="110" or @tag="113"]/*[contains("ad",@code)]');
1855 --INSERT INTO vandelay.bib_attr_definition ( id, code, description, xpath ) VALUES (3, 'language', oils_i18n_gettext(3, 'vqbrad', 'Language of work', 'description'),'//*[@tag="240"]/*[@code="l"][1]');
1856 --INSERT INTO vandelay.bib_attr_definition ( id, code, description, xpath ) VALUES (4, 'pagination', oils_i18n_gettext(4, 'vqbrad', 'Pagination', 'description'),'//*[@tag="300"]/*[@code="a"][1]');
1857 --INSERT INTO vandelay.bib_attr_definition ( id, code, description, xpath, ident, remove ) VALUES (5, 'isbn',oils_i18n_gettext(5, 'vqbrad', 'ISBN', 'description'),'//*[@tag="020"]/*[@code="a"]', TRUE, $r$(?:-|\s.+$)$r$);
1858 --INSERT INTO vandelay.bib_attr_definition ( id, code, description, xpath, ident, remove ) VALUES (6, 'issn',oils_i18n_gettext(6, 'vqbrad', 'ISSN', 'description'),'//*[@tag="022"]/*[@code="a"]', TRUE, $r$(?:-|\s.+$)$r$);
1859 --INSERT INTO vandelay.bib_attr_definition ( id, code, description, xpath ) VALUES (7, 'price',oils_i18n_gettext(7, 'vqbrad', 'Price', 'description'),'//*[@tag="020" or @tag="022"]/*[@code="c"][1]');
1860 --INSERT INTO vandelay.bib_attr_definition ( id, code, description, xpath, ident ) VALUES (8, 'rec_identifier',oils_i18n_gettext(8, 'vqbrad', 'Accession Number', 'description'),'//*[@tag="001"]', TRUE);
1861 --INSERT INTO vandelay.bib_attr_definition ( id, code, description, xpath, ident ) VALUES (9, 'eg_tcn',oils_i18n_gettext(9, 'vqbrad', 'TCN Value', 'description'),'//*[@tag="901"]/*[@code="a"]', TRUE);
1862 --INSERT INTO vandelay.bib_attr_definition ( id, code, description, xpath, ident ) VALUES (10, 'eg_tcn_source',oils_i18n_gettext(10, 'vqbrad', 'TCN Source', 'description'),'//*[@tag="901"]/*[@code="b"]', TRUE);
1863 --INSERT INTO vandelay.bib_attr_definition ( id, code, description, xpath, ident ) VALUES (11, 'eg_identifier',oils_i18n_gettext(11, 'vqbrad', 'Internal ID', 'description'),'//*[@tag="901"]/*[@code="c"]', TRUE);
1864 --INSERT INTO vandelay.bib_attr_definition ( id, code, description, xpath ) VALUES (12, 'publisher',oils_i18n_gettext(12, 'vqbrad', 'Publisher', 'description'),'//*[@tag="260"]/*[@code="b"][1]');
1865 --INSERT INTO vandelay.bib_attr_definition ( id, code, description, xpath, remove ) VALUES (13, 'pubdate',oils_i18n_gettext(13, 'vqbrad', 'Publication Date', 'description'),'//*[@tag="260"]/*[@code="c"][1]',$r$\D$r$);
1866 --INSERT INTO vandelay.bib_attr_definition ( id, code, description, xpath ) VALUES (14, 'edition',oils_i18n_gettext(14, 'vqbrad', 'Edition', 'description'),'//*[@tag="250"]/*[@code="a"][1]');
1867 --
1868 --INSERT INTO vandelay.import_item_attr_definition (
1869 --    owner, name, tag, owning_lib, circ_lib, location,
1870 --    call_number, circ_modifier, barcode, price, copy_number,
1871 --    circulate, ref, holdable, opac_visible, status
1872 --) VALUES (
1873 --    1,
1874 --    'Evergreen 852 export format',
1875 --    '852',
1876 --    '[@code = "b"][1]',
1877 --    '[@code = "b"][2]',
1878 --    'c',
1879 --    'j',
1880 --    'g',
1881 --    'p',
1882 --    'y',
1883 --    't',
1884 --    '[@code = "x" and text() = "circulating"]',
1885 --    '[@code = "x" and text() = "reference"]',
1886 --    '[@code = "x" and text() = "holdable"]',
1887 --    '[@code = "x" and text() = "visible"]',
1888 --    'z'
1889 --);
1890 --
1891 --INSERT INTO vandelay.import_item_attr_definition (
1892 --    owner,
1893 --    name,
1894 --    tag,
1895 --    owning_lib,
1896 --    location,
1897 --    call_number,
1898 --    circ_modifier,
1899 --    barcode,
1900 --    price,
1901 --    status
1902 --) VALUES (
1903 --    1,
1904 --    'Unicorn Import format -- 999',
1905 --    '999',
1906 --    'm',
1907 --    'l',
1908 --    'a',
1909 --    't',
1910 --    'i',
1911 --    'p',
1912 --    'k'
1913 --);
1914 --
1915 --INSERT INTO vandelay.authority_attr_definition ( code, description, xpath, ident ) VALUES ('rec_identifier','Identifier','//*[@tag="001"]', TRUE);
1916
1917 COMMIT;
1918