1f53ad17d31886bcf3786c30fec91e746a66b322
[Evergreen.git] / Open-ILS / src / sql / Pg / 012.schema.vandelay.sql
1 DROP SCHEMA IF EXISTS vandelay CASCADE;
2
3 BEGIN;
4
5 CREATE SCHEMA vandelay;
6
7 CREATE TABLE vandelay.match_set (
8     id      SERIAL  PRIMARY KEY,
9     name    TEXT        NOT NULL,
10     owner   INT     NOT NULL REFERENCES actor.org_unit (id) ON DELETE CASCADE,
11     mtype   TEXT        NOT NULL DEFAULT 'biblio', -- 'biblio','authority','mfhd'?, others?
12     CONSTRAINT name_once_per_owner_mtype UNIQUE (name, owner, mtype)
13 );
14
15 -- Table to define match points, either FF via SVF or tag+subfield
16 CREATE TABLE vandelay.match_set_point (
17     id          SERIAL  PRIMARY KEY,
18     match_set   INT     REFERENCES vandelay.match_set (id) ON DELETE CASCADE,
19     parent      INT     REFERENCES vandelay.match_set_point (id),
20     bool_op     TEXT    CHECK (bool_op IS NULL OR (bool_op IN ('AND','OR','NOT'))),
21     svf         TEXT    REFERENCES config.record_attr_definition (name),
22     tag         TEXT,
23     subfield    TEXT,
24     negate      BOOL    DEFAULT FALSE,
25     quality     INT     NOT NULL DEFAULT 1, -- higher is better
26     heading     BOOLEAN NOT NULL DEFAULT FALSE, -- match on authority heading
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 heading IS FALSE AND bool_op IS NULL) OR 
30         (tag IS NULL AND svf IS NOT NULL AND heading IS FALSE AND bool_op IS NULL) OR 
31         (tag IS NULL AND svf IS NULL AND heading IS TRUE AND bool_op IS NULL) OR 
32         (tag IS NULL AND svf IS NULL AND heading IS FALSE AND bool_op IS NOT NULL)
33     )
34 );
35
36 CREATE TABLE vandelay.match_set_quality (
37     id          SERIAL  PRIMARY KEY,
38     match_set   INT     NOT NULL REFERENCES vandelay.match_set (id) ON DELETE CASCADE,
39     svf         TEXT    REFERENCES config.record_attr_definition,
40     tag         TEXT,
41     subfield    TEXT,
42     value       TEXT    NOT NULL,
43     quality     INT     NOT NULL DEFAULT 1, -- higher is better
44     CONSTRAINT vmsq_need_a_subfield_with_a_tag CHECK ((tag IS NOT NULL AND subfield IS NOT NULL) OR tag IS NULL),
45     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))
46 );
47 CREATE UNIQUE INDEX vmsq_def_once_per_set ON vandelay.match_set_quality (match_set, COALESCE(tag,''), COALESCE(subfield,''), COALESCE(svf,''), value);
48
49
50 CREATE TABLE vandelay.queue (
51         id                              BIGSERIAL       PRIMARY KEY,
52         owner                   INT                     NOT NULL REFERENCES actor.usr (id) DEFERRABLE INITIALLY DEFERRED,
53         name                    TEXT            NOT NULL,
54         complete                BOOL            NOT NULL DEFAULT FALSE,
55     match_set       INT         REFERENCES vandelay.match_set (id) ON UPDATE CASCADE ON DELETE SET NULL DEFERRABLE INITIALLY DEFERRED
56 );
57
58 CREATE TABLE vandelay.queued_record (
59     id                  BIGSERIAL                   PRIMARY KEY,
60     create_time TIMESTAMP WITH TIME ZONE    NOT NULL DEFAULT NOW(),
61     import_time TIMESTAMP WITH TIME ZONE,
62         purpose         TEXT                                            NOT NULL DEFAULT 'import' CHECK (purpose IN ('import','overlay')),
63     marc                TEXT                        NOT NULL,
64     quality     INT                         NOT NULL DEFAULT 0
65 );
66
67
68
69 /* Bib stuff at the top */
70 ----------------------------------------------------
71
72 CREATE TABLE vandelay.bib_attr_definition (
73         id                      SERIAL  PRIMARY KEY,
74         code            TEXT    UNIQUE NOT NULL,
75         description     TEXT,
76         xpath           TEXT    NOT NULL,
77         remove          TEXT    NOT NULL DEFAULT ''
78 );
79
80 -- Each TEXT field (other than 'name') should hold an XPath predicate for pulling the data needed
81 -- DROP TABLE vandelay.import_item_attr_definition CASCADE;
82 CREATE TABLE vandelay.import_item_attr_definition (
83     id              BIGSERIAL   PRIMARY KEY,
84     owner           INT         NOT NULL REFERENCES actor.org_unit (id) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED,
85     name            TEXT        NOT NULL,
86     tag             TEXT        NOT NULL,
87     keep            BOOL        NOT NULL DEFAULT FALSE,
88     owning_lib      TEXT,
89     circ_lib        TEXT,
90     call_number     TEXT,
91     copy_number     TEXT,
92     status          TEXT,
93     location        TEXT,
94     circulate       TEXT,
95     deposit         TEXT,
96     deposit_amount  TEXT,
97     ref             TEXT,
98     holdable        TEXT,
99     price           TEXT,
100     barcode         TEXT,
101     circ_modifier   TEXT,
102     circ_as_type    TEXT,
103     alert_message   TEXT,
104     opac_visible    TEXT,
105     pub_note_title  TEXT,
106     pub_note        TEXT,
107     priv_note_title TEXT,
108     priv_note       TEXT,
109     internal_id     TEXT,
110     stat_cat_data   TEXT,
111     parts_data      TEXT,
112         CONSTRAINT vand_import_item_attr_def_idx UNIQUE (owner,name)
113 );
114
115 CREATE TABLE vandelay.import_error (
116     code        TEXT    PRIMARY KEY,
117     description TEXT    NOT NULL -- i18n
118 );
119
120 CREATE TYPE vandelay.bib_queue_queue_type AS ENUM ('bib', 'acq');
121
122 CREATE TABLE vandelay.bib_queue (
123         queue_type          vandelay.bib_queue_queue_type       NOT NULL DEFAULT 'bib',
124         item_attr_def   BIGINT REFERENCES vandelay.import_item_attr_definition (id) ON DELETE SET NULL DEFERRABLE INITIALLY DEFERRED,
125     match_bucket    INTEGER, -- REFERENCES container.biblio_record_entry_bucket(id);
126         CONSTRAINT vand_bib_queue_name_once_per_owner_const UNIQUE (owner,name,queue_type)
127 ) INHERITS (vandelay.queue);
128 ALTER TABLE vandelay.bib_queue ADD PRIMARY KEY (id);
129
130 CREATE TABLE vandelay.queued_bib_record (
131         queue               INT         NOT NULL REFERENCES vandelay.bib_queue (id) ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED,
132         bib_source          INT         REFERENCES config.bib_source (id) DEFERRABLE INITIALLY DEFERRED,
133         imported_as     BIGINT  REFERENCES biblio.record_entry (id) DEFERRABLE INITIALLY DEFERRED,
134         import_error    TEXT    REFERENCES vandelay.import_error (code) ON DELETE SET NULL ON UPDATE CASCADE DEFERRABLE INITIALLY DEFERRED,
135         error_detail    TEXT
136 ) INHERITS (vandelay.queued_record);
137 ALTER TABLE vandelay.queued_bib_record ADD PRIMARY KEY (id);
138 CREATE INDEX queued_bib_record_queue_idx ON vandelay.queued_bib_record (queue);
139
140 CREATE TABLE vandelay.queued_bib_record_attr (
141         id                      BIGSERIAL       PRIMARY KEY,
142         record          BIGINT          NOT NULL REFERENCES vandelay.queued_bib_record (id) DEFERRABLE INITIALLY DEFERRED,
143         field           INT                     NOT NULL REFERENCES vandelay.bib_attr_definition (id) DEFERRABLE INITIALLY DEFERRED,
144         attr_value      TEXT            NOT NULL
145 );
146 CREATE INDEX queued_bib_record_attr_record_idx ON vandelay.queued_bib_record_attr (record);
147
148 CREATE TABLE vandelay.bib_match (
149         id                              BIGSERIAL       PRIMARY KEY,
150         queued_record   BIGINT          REFERENCES vandelay.queued_bib_record (id) ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED,
151         eg_record               BIGINT          REFERENCES biblio.record_entry (id) ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED,
152     quality         INT         NOT NULL DEFAULT 1,
153     match_score     INT         NOT NULL DEFAULT 0
154 );
155 CREATE INDEX bib_match_queued_record_idx ON vandelay.bib_match (queued_record);
156
157 CREATE TABLE vandelay.import_item (
158     id              BIGSERIAL   PRIMARY KEY,
159     record          BIGINT      NOT NULL REFERENCES vandelay.queued_bib_record (id) ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED,
160     definition      BIGINT      NOT NULL REFERENCES vandelay.import_item_attr_definition (id) ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED,
161         import_error    TEXT        REFERENCES vandelay.import_error (code) ON DELETE SET NULL ON UPDATE CASCADE DEFERRABLE INITIALLY DEFERRED,
162         error_detail    TEXT,
163     imported_as     BIGINT,
164     import_time     TIMESTAMP WITH TIME ZONE,
165     owning_lib      INT,
166     circ_lib        INT,
167     call_number     TEXT,
168     copy_number     INT,
169     status          INT,
170     location        INT,
171     circulate       BOOL,
172     deposit         BOOL,
173     deposit_amount  NUMERIC(8,2),
174     ref             BOOL,
175     holdable        BOOL,
176     price           NUMERIC(8,2),
177     barcode         TEXT,
178     circ_modifier   TEXT,
179     circ_as_type    TEXT,
180     alert_message   TEXT,
181     pub_note        TEXT,
182     priv_note       TEXT,
183     stat_cat_data   TEXT,
184     parts_data      TEXT,
185     opac_visible    BOOL,
186     internal_id     BIGINT -- queue_type == 'acq' ? acq.lineitem_detail.id : asset.copy.id
187 );
188 CREATE INDEX import_item_record_idx ON vandelay.import_item (record);
189
190 CREATE TABLE vandelay.import_bib_trash_group(
191     id           SERIAL  PRIMARY KEY,
192     owner        INTEGER NOT NULL REFERENCES actor.org_unit(id),
193     label        TEXT    NOT NULL, --i18n
194     always_apply BOOLEAN NOT NULL DEFAULT FALSE,
195         CONSTRAINT vand_import_bib_trash_grp_owner_label UNIQUE (owner, label)
196 );
197  
198 CREATE TABLE vandelay.import_bib_trash_fields (
199     id         BIGSERIAL PRIMARY KEY,
200     grp        INTEGER   NOT NULL REFERENCES vandelay.import_bib_trash_group,
201     field      TEXT      NOT NULL,
202     CONSTRAINT vand_import_bib_trash_fields_once_per UNIQUE (grp, field)
203 );
204
205 CREATE TABLE vandelay.merge_profile (
206     id              BIGSERIAL   PRIMARY KEY,
207     owner           INT         NOT NULL REFERENCES actor.org_unit (id) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED,
208     name            TEXT        NOT NULL,
209     add_spec        TEXT,
210     replace_spec    TEXT,
211     strip_spec      TEXT,
212     preserve_spec   TEXT,
213     update_bib_source BOOLEAN   NOT NULL DEFAULT FALSE,
214     lwm_ratio       NUMERIC,
215         CONSTRAINT vand_merge_prof_owner_name_idx UNIQUE (owner,name),
216         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))
217 );
218
219 CREATE OR REPLACE FUNCTION vandelay.marc21_record_type( marc TEXT ) RETURNS config.marc21_rec_type_map AS $func$
220 DECLARE
221         ldr         TEXT;
222         tval        TEXT;
223         tval_rec    RECORD;
224         bval        TEXT;
225         bval_rec    RECORD;
226     retval      config.marc21_rec_type_map%ROWTYPE;
227 BEGIN
228     ldr := oils_xpath_string( '//*[local-name()="leader"]', marc );
229
230     IF ldr IS NULL OR ldr = '' THEN
231         SELECT * INTO retval FROM config.marc21_rec_type_map WHERE code = 'BKS';
232         RETURN retval;
233     END IF;
234
235     SELECT * INTO tval_rec FROM config.marc21_ff_pos_map WHERE fixed_field = 'Type' LIMIT 1; -- They're all the same
236     SELECT * INTO bval_rec FROM config.marc21_ff_pos_map WHERE fixed_field = 'BLvl' LIMIT 1; -- They're all the same
237
238
239     tval := SUBSTRING( ldr, tval_rec.start_pos + 1, tval_rec.length );
240     bval := SUBSTRING( ldr, bval_rec.start_pos + 1, bval_rec.length );
241
242     -- RAISE NOTICE 'type %, blvl %, ldr %', tval, bval, ldr;
243
244     SELECT * INTO retval FROM config.marc21_rec_type_map WHERE type_val LIKE '%' || tval || '%' AND blvl_val LIKE '%' || bval || '%';
245
246
247     IF retval.code IS NULL THEN
248         SELECT * INTO retval FROM config.marc21_rec_type_map WHERE code = 'BKS';
249     END IF;
250
251     RETURN retval;
252 END;
253 $func$ LANGUAGE PLPGSQL;
254
255 CREATE TYPE biblio.record_ff_map AS (record BIGINT, ff_name TEXT, ff_value TEXT);
256 CREATE OR REPLACE FUNCTION vandelay.marc21_extract_all_fixed_fields( marc TEXT, use_default BOOL DEFAULT FALSE ) RETURNS SETOF biblio.record_ff_map AS $func$
257 DECLARE
258     tag_data    TEXT;
259     rtype       TEXT;
260     ff_pos      RECORD;
261     output      biblio.record_ff_map%ROWTYPE;
262 BEGIN
263     rtype := (vandelay.marc21_record_type( marc )).code;
264
265     FOR ff_pos IN SELECT * FROM config.marc21_ff_pos_map WHERE rec_type = rtype ORDER BY tag DESC LOOP
266         output.ff_name  := ff_pos.fixed_field;
267         output.ff_value := NULL;
268
269         IF ff_pos.tag = 'ldr' THEN
270             output.ff_value := oils_xpath_string('//*[local-name()="leader"]', marc);
271             IF output.ff_value IS NOT NULL THEN
272                 output.ff_value := SUBSTRING( output.ff_value, ff_pos.start_pos + 1, ff_pos.length );
273                 RETURN NEXT output;
274                 output.ff_value := NULL;
275             END IF;
276         ELSE
277             FOR tag_data IN SELECT value FROM UNNEST( oils_xpath( '//*[@tag="' || UPPER(ff_pos.tag) || '"]/text()', marc ) ) x(value) LOOP
278                 output.ff_value := SUBSTRING( tag_data, ff_pos.start_pos + 1, ff_pos.length );
279                 CONTINUE WHEN output.ff_value IS NULL AND NOT use_default;
280                 IF output.ff_value IS NULL THEN output.ff_value := REPEAT( ff_pos.default_val, ff_pos.length ); END IF;
281                 RETURN NEXT output;
282                 output.ff_value := NULL;
283             END LOOP;
284         END IF;
285
286     END LOOP;
287
288     RETURN;
289 END;
290 $func$ LANGUAGE PLPGSQL;
291
292 CREATE OR REPLACE FUNCTION vandelay.marc21_extract_fixed_field_list( marc TEXT, ff TEXT, use_default BOOL DEFAULT FALSE ) RETURNS TEXT[] AS $func$
293 DECLARE
294     rtype       TEXT;
295     ff_pos      RECORD;
296     tag_data    RECORD;
297     val         TEXT;
298     collection  TEXT[] := '{}'::TEXT[];
299 BEGIN
300     rtype := (vandelay.marc21_record_type( marc )).code;
301     FOR ff_pos IN SELECT * FROM config.marc21_ff_pos_map WHERE fixed_field = ff AND rec_type = rtype ORDER BY tag DESC LOOP
302         IF ff_pos.tag = 'ldr' THEN
303             val := oils_xpath_string('//*[local-name()="leader"]', marc);
304             IF val IS NOT NULL THEN
305                 val := SUBSTRING( val, ff_pos.start_pos + 1, ff_pos.length );
306                 collection := collection || val;
307             END IF;
308         ELSE
309             FOR tag_data IN SELECT value FROM UNNEST( oils_xpath( '//*[@tag="' || UPPER(ff_pos.tag) || '"]/text()', marc ) ) x(value) LOOP
310                 val := SUBSTRING( tag_data.value, ff_pos.start_pos + 1, ff_pos.length );
311                 collection := collection || val;
312             END LOOP;
313         END IF;
314         CONTINUE WHEN NOT use_default;
315         CONTINUE WHEN ARRAY_UPPER(collection, 1) > 0;
316         val := REPEAT( ff_pos.default_val, ff_pos.length );
317         collection := collection || val;
318     END LOOP;
319
320     RETURN collection;
321 END;
322 $func$ LANGUAGE PLPGSQL;
323
324 CREATE OR REPLACE FUNCTION vandelay.marc21_extract_fixed_field( marc TEXT, ff TEXT, use_default BOOL DEFAULT FALSE ) RETURNS TEXT AS $func$
325 DECLARE
326     rtype       TEXT;
327     ff_pos      RECORD;
328     tag_data    RECORD;
329     val         TEXT;
330 BEGIN
331     rtype := (vandelay.marc21_record_type( marc )).code;
332     FOR ff_pos IN SELECT * FROM config.marc21_ff_pos_map WHERE fixed_field = ff AND rec_type = rtype ORDER BY tag DESC LOOP
333         IF ff_pos.tag = 'ldr' THEN
334             val := oils_xpath_string('//*[local-name()="leader"]', marc);
335             IF val IS NOT NULL THEN
336                 val := SUBSTRING( val, ff_pos.start_pos + 1, ff_pos.length );
337                 RETURN val;
338             END IF;
339         ELSE
340             FOR tag_data IN SELECT value FROM UNNEST( oils_xpath( '//*[@tag="' || UPPER(ff_pos.tag) || '"]/text()', marc ) ) x(value) LOOP
341                 val := SUBSTRING( tag_data.value, ff_pos.start_pos + 1, ff_pos.length );
342                 RETURN val;
343             END LOOP;
344         END IF;
345         CONTINUE WHEN NOT use_default;
346         val := REPEAT( ff_pos.default_val, ff_pos.length );
347         RETURN val;
348     END LOOP;
349
350     RETURN NULL;
351 END;
352 $func$ LANGUAGE PLPGSQL;
353
354 CREATE TYPE biblio.marc21_physical_characteristics AS ( id INT, record BIGINT, ptype TEXT, subfield INT, value INT );
355 CREATE OR REPLACE FUNCTION vandelay.marc21_physical_characteristics( marc TEXT) RETURNS SETOF biblio.marc21_physical_characteristics AS $func$
356 DECLARE
357     rowid   INT := 0;
358     _007    TEXT;
359     ptype   config.marc21_physical_characteristic_type_map%ROWTYPE;
360     psf     config.marc21_physical_characteristic_subfield_map%ROWTYPE;
361     pval    config.marc21_physical_characteristic_value_map%ROWTYPE;
362     retval  biblio.marc21_physical_characteristics%ROWTYPE;
363 BEGIN
364
365     FOR _007 IN SELECT oils_xpath_string('//*', value) FROM UNNEST(oils_xpath('//*[@tag="007"]', marc)) x(value) LOOP
366         IF _007 IS NOT NULL AND _007 <> '' THEN
367             SELECT * INTO ptype FROM config.marc21_physical_characteristic_type_map WHERE ptype_key = SUBSTRING( _007, 1, 1 );
368
369             IF ptype.ptype_key IS NOT NULL THEN
370                 FOR psf IN SELECT * FROM config.marc21_physical_characteristic_subfield_map WHERE ptype_key = ptype.ptype_key LOOP
371                     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 );
372
373                     IF pval.id IS NOT NULL THEN
374                         rowid := rowid + 1;
375                         retval.id := rowid;
376                         retval.ptype := ptype.ptype_key;
377                         retval.subfield := psf.id;
378                         retval.value := pval.id;
379                         RETURN NEXT retval;
380                     END IF;
381
382                 END LOOP;
383             END IF;
384         END IF;
385     END LOOP;
386
387     RETURN;
388 END;
389 $func$ LANGUAGE PLPGSQL;
390
391 CREATE TYPE vandelay.flat_marc AS ( tag CHAR(3), ind1 TEXT, ind2 TEXT, subfield TEXT, value TEXT );
392 CREATE OR REPLACE FUNCTION vandelay.flay_marc ( TEXT ) RETURNS SETOF vandelay.flat_marc AS $func$
393
394 use MARC::Record;
395 use MARC::File::XML (BinaryEncoding => 'UTF-8');
396 use MARC::Charset;
397 use strict;
398
399 MARC::Charset->assume_unicode(1);
400
401 my $xml = shift;
402 my $r = MARC::Record->new_from_xml( $xml );
403
404 return_next( { tag => 'LDR', value => $r->leader } );
405
406 for my $f ( $r->fields ) {
407     if ($f->is_control_field) {
408         return_next({ tag => $f->tag, value => $f->data });
409     } else {
410         for my $s ($f->subfields) {
411             return_next({
412                 tag      => $f->tag,
413                 ind1     => $f->indicator(1),
414                 ind2     => $f->indicator(2),
415                 subfield => $s->[0],
416                 value    => $s->[1]
417             });
418
419             if ( $f->tag eq '245' and $s->[0] eq 'a' ) {
420                 my $trim = $f->indicator(2) || 0;
421                 return_next({
422                     tag      => 'tnf',
423                     ind1     => $f->indicator(1),
424                     ind2     => $f->indicator(2),
425                     subfield => 'a',
426                     value    => substr( $s->[1], $trim )
427                 });
428             }
429         }
430     }
431 }
432
433 return undef;
434
435 $func$ LANGUAGE PLPERLU;
436
437 CREATE OR REPLACE FUNCTION vandelay.flatten_marc ( marc TEXT ) RETURNS SETOF vandelay.flat_marc AS $func$
438 DECLARE
439     output  vandelay.flat_marc%ROWTYPE;
440     field   RECORD;
441 BEGIN
442     FOR field IN SELECT * FROM vandelay.flay_marc( marc ) LOOP
443         output.ind1 := field.ind1;
444         output.ind2 := field.ind2;
445         output.tag := field.tag;
446         output.subfield := field.subfield;
447         IF field.subfield IS NOT NULL AND field.tag NOT IN ('020','022','024') THEN -- exclude standard numbers and control fields
448             output.value := naco_normalize(field.value, field.subfield);
449         ELSE
450             output.value := field.value;
451         END IF;
452
453         CONTINUE WHEN output.value IS NULL;
454
455         RETURN NEXT output;
456     END LOOP;
457 END;
458 $func$ LANGUAGE PLPGSQL;
459
460 CREATE OR REPLACE FUNCTION vandelay.extract_rec_attrs ( xml TEXT, attr_defs TEXT[]) RETURNS hstore AS $_$
461 DECLARE
462     transformed_xml TEXT;
463     prev_xfrm       TEXT;
464     normalizer      RECORD;
465     xfrm            config.xml_transform%ROWTYPE;
466     attr_value      TEXT;
467     new_attrs       HSTORE := ''::HSTORE;
468     attr_def        config.record_attr_definition%ROWTYPE;
469 BEGIN
470
471     FOR attr_def IN SELECT * FROM config.record_attr_definition WHERE name IN (SELECT * FROM UNNEST(attr_defs)) ORDER BY format LOOP
472
473         IF attr_def.tag IS NOT NULL THEN -- tag (and optional subfield list) selection
474             SELECT  STRING_AGG(x.value, COALESCE(attr_def.joiner,' ')) INTO attr_value
475               FROM  vandelay.flatten_marc(xml) AS x
476               WHERE x.tag LIKE attr_def.tag
477                     AND CASE
478                         WHEN attr_def.sf_list IS NOT NULL
479                             THEN POSITION(x.subfield IN attr_def.sf_list) > 0
480                         ELSE TRUE
481                         END
482               GROUP BY x.tag
483               ORDER BY x.tag
484               LIMIT 1;
485
486         ELSIF attr_def.fixed_field IS NOT NULL THEN -- a named fixed field, see config.marc21_ff_pos_map.fixed_field
487             attr_value := vandelay.marc21_extract_fixed_field(xml, attr_def.fixed_field);
488
489         ELSIF attr_def.xpath IS NOT NULL THEN -- and xpath expression
490
491             SELECT INTO xfrm * FROM config.xml_transform WHERE name = attr_def.format;
492
493             -- See if we can skip the XSLT ... it's expensive
494             IF prev_xfrm IS NULL OR prev_xfrm <> xfrm.name THEN
495                 -- Can't skip the transform
496                 IF xfrm.xslt <> '---' THEN
497                     transformed_xml := oils_xslt_process(xml,xfrm.xslt);
498                 ELSE
499                     transformed_xml := xml;
500                 END IF;
501
502                 prev_xfrm := xfrm.name;
503             END IF;
504
505             IF xfrm.name IS NULL THEN
506                 -- just grab the marcxml (empty) transform
507                 SELECT INTO xfrm * FROM config.xml_transform WHERE xslt = '---' LIMIT 1;
508                 prev_xfrm := xfrm.name;
509             END IF;
510
511             attr_value := oils_xpath_string(attr_def.xpath, transformed_xml, COALESCE(attr_def.joiner,' '), ARRAY[ARRAY[xfrm.prefix, xfrm.namespace_uri]]);
512
513         ELSIF attr_def.phys_char_sf IS NOT NULL THEN -- a named Physical Characteristic, see config.marc21_physical_characteristic_*_map
514             SELECT  m.value::TEXT INTO attr_value
515               FROM  vandelay.marc21_physical_characteristics(xml) v
516                     JOIN config.marc21_physical_characteristic_value_map m ON (m.id = v.value)
517               WHERE v.subfield = attr_def.phys_char_sf
518               LIMIT 1; -- Just in case ...
519
520         END IF;
521
522         -- apply index normalizers to attr_value
523         FOR normalizer IN
524             SELECT  n.func AS func,
525                     n.param_count AS param_count,
526                     m.params AS params
527               FROM  config.index_normalizer n
528                     JOIN config.record_attr_index_norm_map m ON (m.norm = n.id)
529               WHERE attr = attr_def.name
530               ORDER BY m.pos LOOP
531                 EXECUTE 'SELECT ' || normalizer.func || '(' ||
532                     quote_nullable( attr_value ) ||
533                     CASE
534                         WHEN normalizer.param_count > 0
535                             THEN ',' || REPLACE(REPLACE(BTRIM(normalizer.params,'[]'),E'\'',E'\\\''),E'"',E'\'')
536                             ELSE ''
537                         END ||
538                     ')' INTO attr_value;
539
540         END LOOP;
541
542         -- Add the new value to the hstore
543         new_attrs := new_attrs || hstore( attr_def.name, attr_value );
544
545     END LOOP;
546
547     RETURN new_attrs;
548 END;
549 $_$ LANGUAGE PLPGSQL;
550
551 CREATE OR REPLACE FUNCTION vandelay.extract_rec_attrs ( xml TEXT ) RETURNS hstore AS $_$
552     SELECT vandelay.extract_rec_attrs( $1, (SELECT ARRAY_AGG(name) FROM config.record_attr_definition));
553 $_$ LANGUAGE SQL;
554
555 -- Everything between this comment and the beginning of the definition of
556 -- vandelay.match_bib_record() is strictly in service of that function.
557 CREATE TYPE vandelay.match_set_test_result AS (record BIGINT, quality INTEGER);
558
559 CREATE OR REPLACE FUNCTION vandelay.match_set_test_marcxml(
560     match_set_id INTEGER, record_xml TEXT, bucket_id INTEGER 
561 ) RETURNS SETOF vandelay.match_set_test_result AS $$
562 DECLARE
563     tags_rstore HSTORE;
564     svf_rstore  HSTORE;
565     coal        TEXT;
566     joins       TEXT;
567     query_      TEXT;
568     wq          TEXT;
569     qvalue      INTEGER;
570     rec         RECORD;
571 BEGIN
572     tags_rstore := vandelay.flatten_marc_hstore(record_xml);
573     svf_rstore := vandelay.extract_rec_attrs(record_xml);
574
575     CREATE TEMPORARY TABLE _vandelay_tmp_qrows (q INTEGER);
576     CREATE TEMPORARY TABLE _vandelay_tmp_jrows (j TEXT);
577
578     -- generate the where clause and return that directly (into wq), and as
579     -- a side-effect, populate the _vandelay_tmp_[qj]rows tables.
580     wq := vandelay.get_expr_from_match_set(match_set_id, tags_rstore);
581
582     query_ := 'SELECT DISTINCT(record), ';
583
584     -- qrows table is for the quality bits we add to the SELECT clause
585     SELECT STRING_AGG(
586         'COALESCE(n' || q::TEXT || '.quality, 0)', ' + '
587     ) INTO coal FROM _vandelay_tmp_qrows;
588
589     -- our query string so far is the SELECT clause and the inital FROM.
590     -- no JOINs yet nor the WHERE clause
591     query_ := query_ || coal || ' AS quality ' || E'\n';
592
593     -- jrows table is for the joins we must make (and the real text conditions)
594     SELECT STRING_AGG(j, E'\n') INTO joins
595         FROM _vandelay_tmp_jrows;
596
597     -- add those joins and the where clause to our query.
598     query_ := query_ || joins || E'\n';
599
600     -- join the record bucket
601     IF bucket_id IS NOT NULL THEN
602         query_ := query_ || 'JOIN container.biblio_record_entry_bucket_item ' ||
603             'brebi ON (brebi.target_biblio_record_entry = record ' ||
604             'AND brebi.bucket = ' || bucket_id || E')\n';
605     END IF;
606
607     query_ := query_ || 'JOIN biblio.record_entry bre ON (bre.id = record) ' || 'WHERE ' || wq || ' AND not bre.deleted';
608
609     -- this will return rows of record,quality
610     FOR rec IN EXECUTE query_ USING tags_rstore, svf_rstore LOOP
611         RETURN NEXT rec;
612     END LOOP;
613
614     DROP TABLE _vandelay_tmp_qrows;
615     DROP TABLE _vandelay_tmp_jrows;
616     RETURN;
617 END;
618 $$ LANGUAGE PLPGSQL;
619
620
621 CREATE OR REPLACE FUNCTION vandelay.flatten_marc_hstore(
622     record_xml TEXT
623 ) RETURNS HSTORE AS $func$
624 BEGIN
625     RETURN (SELECT
626         HSTORE(
627             ARRAY_AGG(tag || (COALESCE(subfield, ''))),
628             ARRAY_AGG(value)
629         )
630         FROM (
631             SELECT  tag, subfield, ARRAY_AGG(value)::TEXT AS value
632               FROM  (SELECT tag,
633                             subfield,
634                             CASE WHEN tag = '020' THEN -- caseless -- isbn
635                                 LOWER((REGEXP_MATCHES(value,$$^(\S{10,17})$$))[1] || '%')
636                             WHEN tag = '022' THEN -- caseless -- issn
637                                 LOWER((REGEXP_MATCHES(value,$$^(\S{4}[- ]?\S{4})$$))[1] || '%')
638                             WHEN tag = '024' THEN -- caseless -- upc (other)
639                                 LOWER(value || '%')
640                             ELSE
641                                 value
642                             END AS value
643                       FROM  vandelay.flatten_marc(record_xml)) x
644                 GROUP BY tag, subfield ORDER BY tag, subfield
645         ) subquery
646     );
647 END;
648 $func$ LANGUAGE PLPGSQL;
649
650 -- backwards compat version so we don't have 
651 -- to modify vandelay.match_set_test_marcxml()
652 CREATE OR REPLACE FUNCTION vandelay.get_expr_from_match_set(
653     match_set_id INTEGER,
654     tags_rstore HSTORE
655 ) RETURNS TEXT AS $$
656 BEGIN
657     RETURN vandelay.get_expr_from_match_set(
658         match_set_id, tags_rstore, NULL);
659 END;
660 $$  LANGUAGE PLPGSQL;
661
662 CREATE OR REPLACE FUNCTION vandelay.get_expr_from_match_set(
663     match_set_id INTEGER,
664     tags_rstore HSTORE,
665     auth_heading TEXT
666 ) RETURNS TEXT AS $$
667 DECLARE
668     root vandelay.match_set_point;
669 BEGIN
670     SELECT * INTO root FROM vandelay.match_set_point
671         WHERE parent IS NULL AND match_set = match_set_id;
672
673     RETURN vandelay.get_expr_from_match_set_point(
674         root, tags_rstore, auth_heading);
675 END;
676 $$  LANGUAGE PLPGSQL;
677
678 CREATE OR REPLACE FUNCTION vandelay.get_expr_from_match_set_point(
679     node vandelay.match_set_point,
680     tags_rstore HSTORE,
681     auth_heading TEXT
682 ) RETURNS TEXT AS $$
683 DECLARE
684     q           TEXT;
685     i           INTEGER;
686     this_op     TEXT;
687     children    INTEGER[];
688     child       vandelay.match_set_point;
689 BEGIN
690     SELECT ARRAY_AGG(id) INTO children FROM vandelay.match_set_point
691         WHERE parent = node.id;
692
693     IF ARRAY_LENGTH(children, 1) > 0 THEN
694         this_op := vandelay._get_expr_render_one(node);
695         q := '(';
696         i := 1;
697         WHILE children[i] IS NOT NULL LOOP
698             SELECT * INTO child FROM vandelay.match_set_point
699                 WHERE id = children[i];
700             IF i > 1 THEN
701                 q := q || ' ' || this_op || ' ';
702             END IF;
703             i := i + 1;
704             q := q || vandelay.get_expr_from_match_set_point(
705                 child, tags_rstore, auth_heading);
706         END LOOP;
707         q := q || ')';
708         RETURN q;
709     ELSIF node.bool_op IS NULL THEN
710         PERFORM vandelay._get_expr_push_qrow(node);
711         PERFORM vandelay._get_expr_push_jrow(node, tags_rstore, auth_heading);
712         RETURN vandelay._get_expr_render_one(node);
713     ELSE
714         RETURN '';
715     END IF;
716 END;
717 $$  LANGUAGE PLPGSQL;
718
719 CREATE OR REPLACE FUNCTION vandelay._get_expr_push_qrow(
720     node vandelay.match_set_point
721 ) RETURNS VOID AS $$
722 DECLARE
723 BEGIN
724     INSERT INTO _vandelay_tmp_qrows (q) VALUES (node.id);
725 END;
726 $$ LANGUAGE PLPGSQL;
727
728 CREATE OR REPLACE FUNCTION vandelay._get_expr_push_jrow(
729     node vandelay.match_set_point,
730     tags_rstore HSTORE,
731     auth_heading TEXT
732 ) RETURNS VOID AS $$
733 DECLARE
734     jrow        TEXT;
735     my_alias    TEXT;
736     op          TEXT;
737     tagkey      TEXT;
738     caseless    BOOL;
739     jrow_count  INT;
740     my_using    TEXT;
741     my_join     TEXT;
742     rec_table   TEXT;
743 BEGIN
744     -- remember $1 is tags_rstore, and $2 is svf_rstore
745     -- a non-NULL auth_heading means we're matching authority records
746
747     IF auth_heading IS NOT NULL THEN
748         rec_table := 'authority.full_rec';
749     ELSE
750         rec_table := 'metabib.full_rec';
751     END IF;
752
753     caseless := FALSE;
754     SELECT COUNT(*) INTO jrow_count FROM _vandelay_tmp_jrows;
755     IF jrow_count > 0 THEN
756         my_using := ' USING (record)';
757         my_join := 'FULL OUTER JOIN';
758     ELSE
759         my_using := '';
760         my_join := 'FROM';
761     END IF;
762
763     IF node.tag IS NOT NULL THEN
764         caseless := (node.tag IN ('020', '022', '024'));
765         tagkey := node.tag;
766         IF node.subfield IS NOT NULL THEN
767             tagkey := tagkey || node.subfield;
768         END IF;
769     END IF;
770
771     IF node.negate THEN
772         IF caseless THEN
773             op := 'NOT LIKE';
774         ELSE
775             op := '<>';
776         END IF;
777     ELSE
778         IF caseless THEN
779             op := 'LIKE';
780         ELSE
781             op := '=';
782         END IF;
783     END IF;
784
785     my_alias := 'n' || node.id::TEXT;
786
787     jrow := my_join || ' (SELECT *, ';
788     IF node.tag IS NOT NULL THEN
789         jrow := jrow  || node.quality ||
790             ' AS quality FROM ' || rec_table || ' mfr WHERE mfr.tag = ''' ||
791             node.tag || '''';
792         IF node.subfield IS NOT NULL THEN
793             jrow := jrow || ' AND mfr.subfield = ''' ||
794                 node.subfield || '''';
795         END IF;
796         jrow := jrow || ' AND (';
797         jrow := jrow || vandelay._node_tag_comparisons(caseless, op, tags_rstore, tagkey);
798         jrow := jrow || ')) ' || my_alias || my_using || E'\n';
799     ELSE    -- svf
800         IF auth_heading IS NOT NULL THEN -- authority record
801             IF node.heading AND auth_heading <> '' THEN
802                 jrow := jrow || 'id AS record, ' || node.quality ||
803                 ' AS quality FROM authority.record_entry are ' ||
804                 ' WHERE are.heading = ''' || auth_heading || '''';
805                 jrow := jrow || ') ' || my_alias || my_using || E'\n';
806             END IF;
807         ELSE -- bib record
808             jrow := jrow || 'id AS record, ' || node.quality ||
809                 ' AS quality FROM metabib.record_attr_flat mraf WHERE mraf.attr = ''' ||
810                 node.svf || ''' AND mraf.value ' || op || ' $2->''' || node.svf || ''') ' ||
811                 my_alias || my_using || E'\n';
812         END IF;
813     END IF;
814     INSERT INTO _vandelay_tmp_jrows (j) VALUES (jrow);
815 END;
816 $$ LANGUAGE PLPGSQL;
817
818 CREATE OR REPLACE FUNCTION vandelay._node_tag_comparisons(
819     caseless BOOLEAN,
820     op TEXT,
821     tags_rstore HSTORE,
822     tagkey TEXT
823 ) RETURNS TEXT AS $$
824 DECLARE
825     result  TEXT;
826     i       INT;
827     vals    TEXT[];
828 BEGIN
829     i := 1;
830     vals := tags_rstore->tagkey;
831     result := '';
832
833     WHILE TRUE LOOP
834         IF i > 1 THEN
835             IF vals[i] IS NULL THEN
836                 EXIT;
837             ELSE
838                 result := result || ' OR ';
839             END IF;
840         END IF;
841
842         IF caseless THEN
843             result := result || 'LOWER(mfr.value) ' || op;
844         ELSE
845             result := result || 'mfr.value ' || op;
846         END IF;
847
848         result := result || ' ' || COALESCE('''' || vals[i] || '''', 'NULL');
849
850         IF vals[i] IS NULL THEN
851             EXIT;
852         END IF;
853         i := i + 1;
854     END LOOP;
855
856     RETURN result;
857
858 END;
859 $$ LANGUAGE PLPGSQL;
860
861 CREATE OR REPLACE FUNCTION vandelay._get_expr_render_one(
862     node vandelay.match_set_point
863 ) RETURNS TEXT AS $$
864 DECLARE
865     s           TEXT;
866 BEGIN
867     IF node.bool_op IS NOT NULL THEN
868         RETURN node.bool_op;
869     ELSE
870         RETURN '(n' || node.id::TEXT || '.id IS NOT NULL)';
871     END IF;
872 END;
873 $$ LANGUAGE PLPGSQL;
874
875 CREATE OR REPLACE FUNCTION vandelay.match_bib_record() RETURNS TRIGGER AS $func$
876 DECLARE
877     incoming_existing_id    TEXT;
878     test_result             vandelay.match_set_test_result%ROWTYPE;
879     tmp_rec                 BIGINT;
880     match_set               INT;
881     match_bucket            INT;
882 BEGIN
883     IF TG_OP IN ('INSERT','UPDATE') AND NEW.imported_as IS NOT NULL THEN
884         RETURN NEW;
885     END IF;
886
887     DELETE FROM vandelay.bib_match WHERE queued_record = NEW.id;
888
889     SELECT q.match_set INTO match_set FROM vandelay.bib_queue q WHERE q.id = NEW.queue;
890
891     IF match_set IS NOT NULL THEN
892         NEW.quality := vandelay.measure_record_quality( NEW.marc, match_set );
893     END IF;
894
895     -- Perfect matches on 901$c exit early with a match with high quality.
896     incoming_existing_id :=
897         oils_xpath_string('//*[@tag="901"]/*[@code="c"][1]', NEW.marc);
898
899     IF incoming_existing_id IS NOT NULL AND incoming_existing_id != '' THEN
900         SELECT id INTO tmp_rec FROM biblio.record_entry WHERE id = incoming_existing_id::bigint;
901         IF tmp_rec IS NOT NULL THEN
902             INSERT INTO vandelay.bib_match (queued_record, eg_record, match_score, quality) 
903                 SELECT
904                     NEW.id, 
905                     b.id,
906                     9999,
907                     -- note: no match_set means quality==0
908                     vandelay.measure_record_quality( b.marc, match_set )
909                 FROM biblio.record_entry b
910                 WHERE id = incoming_existing_id::bigint;
911         END IF;
912     END IF;
913
914     IF match_set IS NULL THEN
915         RETURN NEW;
916     END IF;
917
918     SELECT q.match_bucket INTO match_bucket FROM vandelay.bib_queue q WHERE q.id = NEW.queue;
919
920     FOR test_result IN SELECT * FROM
921         vandelay.match_set_test_marcxml(match_set, NEW.marc, match_bucket) LOOP
922
923         INSERT INTO vandelay.bib_match ( queued_record, eg_record, match_score, quality )
924             SELECT  
925                 NEW.id,
926                 test_result.record,
927                 test_result.quality,
928                 vandelay.measure_record_quality( b.marc, match_set )
929                 FROM  biblio.record_entry b
930                 WHERE id = test_result.record;
931
932     END LOOP;
933
934     RETURN NEW;
935 END;
936 $func$ LANGUAGE PLPGSQL;
937
938 CREATE OR REPLACE FUNCTION vandelay.measure_record_quality ( xml TEXT, match_set_id INT ) RETURNS INT AS $_$
939 DECLARE
940     out_q   INT := 0;
941     rvalue  TEXT;
942     test    vandelay.match_set_quality%ROWTYPE;
943 BEGIN
944
945     FOR test IN SELECT * FROM vandelay.match_set_quality WHERE match_set = match_set_id LOOP
946         IF test.tag IS NOT NULL THEN
947             FOR rvalue IN SELECT value FROM vandelay.flatten_marc( xml ) WHERE tag = test.tag AND subfield = test.subfield LOOP
948                 IF test.value = rvalue THEN
949                     out_q := out_q + test.quality;
950                 END IF;
951             END LOOP;
952         ELSE
953             IF test.value = vandelay.extract_rec_attrs(xml, ARRAY[test.svf]) -> test.svf THEN
954                 out_q := out_q + test.quality;
955             END IF;
956         END IF;
957     END LOOP;
958
959     RETURN out_q;
960 END;
961 $_$ LANGUAGE PLPGSQL;
962
963 CREATE TYPE vandelay.tcn_data AS (tcn TEXT, tcn_source TEXT, used BOOL);
964 CREATE OR REPLACE FUNCTION vandelay.find_bib_tcn_data ( xml TEXT ) RETURNS SETOF vandelay.tcn_data AS $_$
965 DECLARE
966     eg_tcn          TEXT;
967     eg_tcn_source   TEXT;
968     output          vandelay.tcn_data%ROWTYPE;
969 BEGIN
970
971     -- 001/003
972     eg_tcn := BTRIM((oils_xpath('//*[@tag="001"]/text()',xml))[1]);
973     IF eg_tcn IS NOT NULL AND eg_tcn <> '' THEN
974
975         eg_tcn_source := BTRIM((oils_xpath('//*[@tag="003"]/text()',xml))[1]);
976         IF eg_tcn_source IS NULL OR eg_tcn_source = '' THEN
977             eg_tcn_source := 'System Local';
978         END IF;
979
980         PERFORM id FROM biblio.record_entry WHERE tcn_value = eg_tcn  AND NOT deleted;
981
982         IF NOT FOUND THEN
983             output.used := FALSE;
984         ELSE
985             output.used := TRUE;
986         END IF;
987
988         output.tcn := eg_tcn;
989         output.tcn_source := eg_tcn_source;
990         RETURN NEXT output;
991
992     END IF;
993
994     -- 901 ab
995     eg_tcn := BTRIM((oils_xpath('//*[@tag="901"]/*[@code="a"]/text()',xml))[1]);
996     IF eg_tcn IS NOT NULL AND eg_tcn <> '' THEN
997
998         eg_tcn_source := BTRIM((oils_xpath('//*[@tag="901"]/*[@code="b"]/text()',xml))[1]);
999         IF eg_tcn_source IS NULL OR eg_tcn_source = '' THEN
1000             eg_tcn_source := 'System Local';
1001         END IF;
1002
1003         PERFORM id FROM biblio.record_entry WHERE tcn_value = eg_tcn  AND NOT deleted;
1004
1005         IF NOT FOUND THEN
1006             output.used := FALSE;
1007         ELSE
1008             output.used := TRUE;
1009         END IF;
1010
1011         output.tcn := eg_tcn;
1012         output.tcn_source := eg_tcn_source;
1013         RETURN NEXT output;
1014
1015     END IF;
1016
1017     -- 039 ab
1018     eg_tcn := BTRIM((oils_xpath('//*[@tag="039"]/*[@code="a"]/text()',xml))[1]);
1019     IF eg_tcn IS NOT NULL AND eg_tcn <> '' THEN
1020
1021         eg_tcn_source := BTRIM((oils_xpath('//*[@tag="039"]/*[@code="b"]/text()',xml))[1]);
1022         IF eg_tcn_source IS NULL OR eg_tcn_source = '' THEN
1023             eg_tcn_source := 'System Local';
1024         END IF;
1025
1026         PERFORM id FROM biblio.record_entry WHERE tcn_value = eg_tcn  AND NOT deleted;
1027
1028         IF NOT FOUND THEN
1029             output.used := FALSE;
1030         ELSE
1031             output.used := TRUE;
1032         END IF;
1033
1034         output.tcn := eg_tcn;
1035         output.tcn_source := eg_tcn_source;
1036         RETURN NEXT output;
1037
1038     END IF;
1039
1040     -- 020 a
1041     eg_tcn := REGEXP_REPLACE((oils_xpath('//*[@tag="020"]/*[@code="a"]/text()',xml))[1], $re$^(\w+).*?$$re$, $re$\1$re$);
1042     IF eg_tcn IS NOT NULL AND eg_tcn <> '' THEN
1043
1044         eg_tcn_source := 'ISBN';
1045
1046         PERFORM id FROM biblio.record_entry WHERE tcn_value = eg_tcn  AND NOT deleted;
1047
1048         IF NOT FOUND THEN
1049             output.used := FALSE;
1050         ELSE
1051             output.used := TRUE;
1052         END IF;
1053
1054         output.tcn := eg_tcn;
1055         output.tcn_source := eg_tcn_source;
1056         RETURN NEXT output;
1057
1058     END IF;
1059
1060     -- 022 a
1061     eg_tcn := REGEXP_REPLACE((oils_xpath('//*[@tag="022"]/*[@code="a"]/text()',xml))[1], $re$^(\w+).*?$$re$, $re$\1$re$);
1062     IF eg_tcn IS NOT NULL AND eg_tcn <> '' THEN
1063
1064         eg_tcn_source := 'ISSN';
1065
1066         PERFORM id FROM biblio.record_entry WHERE tcn_value = eg_tcn  AND NOT deleted;
1067
1068         IF NOT FOUND THEN
1069             output.used := FALSE;
1070         ELSE
1071             output.used := TRUE;
1072         END IF;
1073
1074         output.tcn := eg_tcn;
1075         output.tcn_source := eg_tcn_source;
1076         RETURN NEXT output;
1077
1078     END IF;
1079
1080     -- 010 a
1081     eg_tcn := REGEXP_REPLACE((oils_xpath('//*[@tag="010"]/*[@code="a"]/text()',xml))[1], $re$^(\w+).*?$$re$, $re$\1$re$);
1082     IF eg_tcn IS NOT NULL AND eg_tcn <> '' THEN
1083
1084         eg_tcn_source := 'LCCN';
1085
1086         PERFORM id FROM biblio.record_entry WHERE tcn_value = eg_tcn  AND NOT deleted;
1087
1088         IF NOT FOUND THEN
1089             output.used := FALSE;
1090         ELSE
1091             output.used := TRUE;
1092         END IF;
1093
1094         output.tcn := eg_tcn;
1095         output.tcn_source := eg_tcn_source;
1096         RETURN NEXT output;
1097
1098     END IF;
1099
1100     -- 035 a
1101     eg_tcn := REGEXP_REPLACE((oils_xpath('//*[@tag="035"]/*[@code="a"]/text()',xml))[1], $re$^.*?(\w+)$$re$, $re$\1$re$);
1102     IF eg_tcn IS NOT NULL AND eg_tcn <> '' THEN
1103
1104         eg_tcn_source := 'System Legacy';
1105
1106         PERFORM id FROM biblio.record_entry WHERE tcn_value = eg_tcn  AND NOT deleted;
1107
1108         IF NOT FOUND THEN
1109             output.used := FALSE;
1110         ELSE
1111             output.used := TRUE;
1112         END IF;
1113
1114         output.tcn := eg_tcn;
1115         output.tcn_source := eg_tcn_source;
1116         RETURN NEXT output;
1117
1118     END IF;
1119
1120     RETURN;
1121 END;
1122 $_$ LANGUAGE PLPGSQL;
1123
1124 CREATE OR REPLACE FUNCTION vandelay.add_field ( target_xml TEXT, source_xml TEXT, field TEXT, force_add INT ) RETURNS TEXT AS $_$
1125
1126     use MARC::Record;
1127     use MARC::File::XML (BinaryEncoding => 'UTF-8');
1128     use MARC::Charset;
1129     use strict;
1130
1131     MARC::Charset->assume_unicode(1);
1132
1133     my $target_xml = shift;
1134     my $source_xml = shift;
1135     my $field_spec = shift;
1136     my $force_add = shift || 0;
1137
1138     my $target_r = MARC::Record->new_from_xml( $target_xml );
1139     my $source_r = MARC::Record->new_from_xml( $source_xml );
1140
1141     return $target_xml unless ($target_r && $source_r);
1142
1143     my @field_list = split(',', $field_spec);
1144
1145     my %fields;
1146     for my $f (@field_list) {
1147         $f =~ s/^\s*//; $f =~ s/\s*$//;
1148         if ($f =~ /^(.{3})(\w*)(?:\[([^]]*)\])?$/) {
1149             my $field = $1;
1150             $field =~ s/\s+//;
1151             my $sf = $2;
1152             $sf =~ s/\s+//;
1153             my $match = $3;
1154             $match =~ s/^\s*//; $match =~ s/\s*$//;
1155             $fields{$field} = { sf => [ split('', $sf) ] };
1156             if ($match) {
1157                 my ($msf,$mre) = split('~', $match);
1158                 if (length($msf) > 0 and length($mre) > 0) {
1159                     $msf =~ s/^\s*//; $msf =~ s/\s*$//;
1160                     $mre =~ s/^\s*//; $mre =~ s/\s*$//;
1161                     $fields{$field}{match} = { sf => $msf, re => qr/$mre/ };
1162                 }
1163             }
1164         }
1165     }
1166
1167     for my $f ( keys %fields) {
1168         if ( @{$fields{$f}{sf}} ) {
1169             for my $from_field ($source_r->field( $f )) {
1170                 my @tos = $target_r->field( $f );
1171                 if (!@tos) {
1172                     next if (exists($fields{$f}{match}) and !$force_add);
1173                     my @new_fields = map { $_->clone } $source_r->field( $f );
1174                     $target_r->insert_fields_ordered( @new_fields );
1175                 } else {
1176                     for my $to_field (@tos) {
1177                         if (exists($fields{$f}{match})) {
1178                             next unless (grep { $_ =~ $fields{$f}{match}{re} } $to_field->subfield($fields{$f}{match}{sf}));
1179                         }
1180                         for my $old_sf ($from_field->subfields) {
1181                             $to_field->add_subfields( @$old_sf ) if grep(/$$old_sf[0]/,@{$fields{$f}{sf}});
1182                         }
1183                     }
1184                 }
1185             }
1186         } else {
1187             my @new_fields = map { $_->clone } $source_r->field( $f );
1188             $target_r->insert_fields_ordered( @new_fields );
1189         }
1190     }
1191
1192     $target_xml = $target_r->as_xml_record;
1193     $target_xml =~ s/^<\?.+?\?>$//mo;
1194     $target_xml =~ s/\n//sgo;
1195     $target_xml =~ s/>\s+</></sgo;
1196
1197     return $target_xml;
1198
1199 $_$ LANGUAGE PLPERLU;
1200
1201 CREATE OR REPLACE FUNCTION vandelay.add_field ( target_xml TEXT, source_xml TEXT, field TEXT ) RETURNS TEXT AS $_$
1202     SELECT vandelay.add_field( $1, $2, $3, 0 );
1203 $_$ LANGUAGE SQL;
1204
1205 CREATE OR REPLACE FUNCTION vandelay.strip_field ( xml TEXT, field TEXT ) RETURNS TEXT AS $_$
1206
1207     use MARC::Record;
1208     use MARC::File::XML (BinaryEncoding => 'UTF-8');
1209     use MARC::Charset;
1210     use strict;
1211
1212     MARC::Charset->assume_unicode(1);
1213
1214     my $xml = shift;
1215     my $r = MARC::Record->new_from_xml( $xml );
1216
1217     return $xml unless ($r);
1218
1219     my $field_spec = shift;
1220     my @field_list = split(',', $field_spec);
1221
1222     my %fields;
1223     for my $f (@field_list) {
1224         $f =~ s/^\s*//; $f =~ s/\s*$//;
1225         if ($f =~ /^(.{3})(\w*)(?:\[([^]]*)\])?$/) {
1226             my $field = $1;
1227             $field =~ s/\s+//;
1228             my $sf = $2;
1229             $sf =~ s/\s+//;
1230             my $match = $3;
1231             $match =~ s/^\s*//; $match =~ s/\s*$//;
1232             $fields{$field} = { sf => [ split('', $sf) ] };
1233             if ($match) {
1234                 my ($msf,$mre) = split('~', $match);
1235                 if (length($msf) > 0 and length($mre) > 0) {
1236                     $msf =~ s/^\s*//; $msf =~ s/\s*$//;
1237                     $mre =~ s/^\s*//; $mre =~ s/\s*$//;
1238                     $fields{$field}{match} = { sf => $msf, re => qr/$mre/ };
1239                 }
1240             }
1241         }
1242     }
1243
1244     for my $f ( keys %fields) {
1245         for my $to_field ($r->field( $f )) {
1246             if (exists($fields{$f}{match})) {
1247                 next unless (grep { $_ =~ $fields{$f}{match}{re} } $to_field->subfield($fields{$f}{match}{sf}));
1248             }
1249
1250             if ( @{$fields{$f}{sf}} ) {
1251                 $to_field->delete_subfield(code => $fields{$f}{sf});
1252             } else {
1253                 $r->delete_field( $to_field );
1254             }
1255         }
1256     }
1257
1258     $xml = $r->as_xml_record;
1259     $xml =~ s/^<\?.+?\?>$//mo;
1260     $xml =~ s/\n//sgo;
1261     $xml =~ s/>\s+</></sgo;
1262
1263     return $xml;
1264
1265 $_$ LANGUAGE PLPERLU;
1266
1267 CREATE OR REPLACE FUNCTION vandelay.replace_field ( target_xml TEXT, source_xml TEXT, field TEXT ) RETURNS TEXT AS $_$
1268 DECLARE
1269     xml_output TEXT;
1270     parsed_target TEXT;
1271     curr_field TEXT;
1272 BEGIN
1273
1274     parsed_target := vandelay.strip_field( target_xml, ''); -- this dance normalizes the format of the xml for the IF below
1275     xml_output := parsed_target; -- if there are no replace rules, just return the input
1276
1277     FOR curr_field IN SELECT UNNEST( STRING_TO_ARRAY(field, ',') ) LOOP -- naive split, but it's the same we use in the perl
1278
1279         xml_output := vandelay.strip_field( parsed_target, curr_field);
1280
1281         IF xml_output <> parsed_target  AND curr_field ~ E'~' THEN
1282             -- we removed something, and there was a regexp restriction in the curr_field definition, so proceed
1283             xml_output := vandelay.add_field( xml_output, source_xml, curr_field, 1 );
1284         ELSIF curr_field !~ E'~' THEN
1285             -- No regexp restriction, add the curr_field
1286             xml_output := vandelay.add_field( xml_output, source_xml, curr_field, 0 );
1287         END IF;
1288
1289         parsed_target := xml_output; -- in prep for any following loop iterations
1290
1291     END LOOP;
1292
1293     RETURN xml_output;
1294 END;
1295 $_$ LANGUAGE PLPGSQL;
1296
1297 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 $_$
1298     SELECT vandelay.replace_field( vandelay.add_field( vandelay.strip_field( $1, $5) , $2, $3 ), $2, $4);
1299 $_$ LANGUAGE SQL;
1300
1301 CREATE TYPE vandelay.compile_profile AS (add_rule TEXT, replace_rule TEXT, preserve_rule TEXT, strip_rule TEXT);
1302 CREATE OR REPLACE FUNCTION vandelay.compile_profile ( incoming_xml TEXT ) RETURNS vandelay.compile_profile AS $_$
1303 DECLARE
1304     output              vandelay.compile_profile%ROWTYPE;
1305     profile             vandelay.merge_profile%ROWTYPE;
1306     profile_tmpl        TEXT;
1307     profile_tmpl_owner  TEXT;
1308     add_rule            TEXT := '';
1309     strip_rule          TEXT := '';
1310     replace_rule        TEXT := '';
1311     preserve_rule       TEXT := '';
1312
1313 BEGIN
1314
1315     profile_tmpl := (oils_xpath('//*[@tag="905"]/*[@code="t"]/text()',incoming_xml))[1];
1316     profile_tmpl_owner := (oils_xpath('//*[@tag="905"]/*[@code="o"]/text()',incoming_xml))[1];
1317
1318     IF profile_tmpl IS NOT NULL AND profile_tmpl <> '' AND profile_tmpl_owner IS NOT NULL AND profile_tmpl_owner <> '' THEN
1319         SELECT  p.* INTO profile
1320           FROM  vandelay.merge_profile p
1321                 JOIN actor.org_unit u ON (u.id = p.owner)
1322           WHERE p.name = profile_tmpl
1323                 AND u.shortname = profile_tmpl_owner;
1324
1325         IF profile.id IS NOT NULL THEN
1326             add_rule := COALESCE(profile.add_spec,'');
1327             strip_rule := COALESCE(profile.strip_spec,'');
1328             replace_rule := COALESCE(profile.replace_spec,'');
1329             preserve_rule := COALESCE(profile.preserve_spec,'');
1330         END IF;
1331     END IF;
1332
1333     add_rule := add_rule || ',' || COALESCE(ARRAY_TO_STRING(oils_xpath('//*[@tag="905"]/*[@code="a"]/text()',incoming_xml),','),'');
1334     strip_rule := strip_rule || ',' || COALESCE(ARRAY_TO_STRING(oils_xpath('//*[@tag="905"]/*[@code="d"]/text()',incoming_xml),','),'');
1335     replace_rule := replace_rule || ',' || COALESCE(ARRAY_TO_STRING(oils_xpath('//*[@tag="905"]/*[@code="r"]/text()',incoming_xml),','),'');
1336     preserve_rule := preserve_rule || ',' || COALESCE(ARRAY_TO_STRING(oils_xpath('//*[@tag="905"]/*[@code="p"]/text()',incoming_xml),','),'');
1337
1338     output.add_rule := BTRIM(add_rule,',');
1339     output.replace_rule := BTRIM(replace_rule,',');
1340     output.strip_rule := BTRIM(strip_rule,',');
1341     output.preserve_rule := BTRIM(preserve_rule,',');
1342
1343     RETURN output;
1344 END;
1345 $_$ LANGUAGE PLPGSQL;
1346
1347 CREATE OR REPLACE FUNCTION vandelay.template_overlay_bib_record ( v_marc TEXT, eg_id BIGINT, merge_profile_id INT ) RETURNS BOOL AS $$
1348 DECLARE
1349     merge_profile   vandelay.merge_profile%ROWTYPE;
1350     dyn_profile     vandelay.compile_profile%ROWTYPE;
1351     editor_string   TEXT;
1352     editor_id       INT;
1353     source_marc     TEXT;
1354     target_marc     TEXT;
1355     eg_marc         TEXT;
1356     replace_rule    TEXT;
1357     match_count     INT;
1358 BEGIN
1359
1360     SELECT  b.marc INTO eg_marc
1361       FROM  biblio.record_entry b
1362       WHERE b.id = eg_id
1363       LIMIT 1;
1364
1365     IF eg_marc IS NULL OR v_marc IS NULL THEN
1366         -- RAISE NOTICE 'no marc for template or bib record';
1367         RETURN FALSE;
1368     END IF;
1369
1370     dyn_profile := vandelay.compile_profile( v_marc );
1371
1372     IF merge_profile_id IS NOT NULL THEN
1373         SELECT * INTO merge_profile FROM vandelay.merge_profile WHERE id = merge_profile_id;
1374         IF FOUND THEN
1375             dyn_profile.add_rule := BTRIM( dyn_profile.add_rule || ',' || COALESCE(merge_profile.add_spec,''), ',');
1376             dyn_profile.strip_rule := BTRIM( dyn_profile.strip_rule || ',' || COALESCE(merge_profile.strip_spec,''), ',');
1377             dyn_profile.replace_rule := BTRIM( dyn_profile.replace_rule || ',' || COALESCE(merge_profile.replace_spec,''), ',');
1378             dyn_profile.preserve_rule := BTRIM( dyn_profile.preserve_rule || ',' || COALESCE(merge_profile.preserve_spec,''), ',');
1379         END IF;
1380     END IF;
1381
1382     IF dyn_profile.replace_rule <> '' AND dyn_profile.preserve_rule <> '' THEN
1383         -- RAISE NOTICE 'both replace [%] and preserve [%] specified', dyn_profile.replace_rule, dyn_profile.preserve_rule;
1384         RETURN FALSE;
1385     END IF;
1386
1387     IF dyn_profile.replace_rule = '' AND dyn_profile.preserve_rule = '' AND dyn_profile.add_rule = '' AND dyn_profile.strip_rule = '' THEN
1388         --Since we have nothing to do, just return a NOOP "we did it"
1389         RETURN TRUE;
1390     ELSIF dyn_profile.replace_rule <> '' THEN
1391         source_marc = v_marc;
1392         target_marc = eg_marc;
1393         replace_rule = dyn_profile.replace_rule;
1394     ELSE
1395         source_marc = eg_marc;
1396         target_marc = v_marc;
1397         replace_rule = dyn_profile.preserve_rule;
1398     END IF;
1399
1400     UPDATE  biblio.record_entry
1401       SET   marc = vandelay.merge_record_xml( target_marc, source_marc, dyn_profile.add_rule, replace_rule, dyn_profile.strip_rule )
1402       WHERE id = eg_id;
1403
1404     IF NOT FOUND THEN
1405         -- RAISE NOTICE 'update of biblio.record_entry failed';
1406         RETURN FALSE;
1407     END IF;
1408
1409     RETURN TRUE;
1410
1411 END;
1412 $$ LANGUAGE PLPGSQL;
1413
1414 CREATE OR REPLACE FUNCTION vandelay.merge_record_xml ( target_marc TEXT, template_marc TEXT ) RETURNS TEXT AS $$
1415 DECLARE
1416     dyn_profile     vandelay.compile_profile%ROWTYPE;
1417     replace_rule    TEXT;
1418     tmp_marc        TEXT;
1419     trgt_marc        TEXT;
1420     tmpl_marc        TEXT;
1421     match_count     INT;
1422 BEGIN
1423
1424     IF target_marc IS NULL OR template_marc IS NULL THEN
1425         -- RAISE NOTICE 'no marc for target or template record';
1426         RETURN NULL;
1427     END IF;
1428
1429     dyn_profile := vandelay.compile_profile( template_marc );
1430
1431     IF dyn_profile.replace_rule <> '' AND dyn_profile.preserve_rule <> '' THEN
1432         -- RAISE NOTICE 'both replace [%] and preserve [%] specified', dyn_profile.replace_rule, dyn_profile.preserve_rule;
1433         RETURN NULL;
1434     END IF;
1435
1436     IF dyn_profile.replace_rule = '' AND dyn_profile.preserve_rule = '' AND dyn_profile.add_rule = '' AND dyn_profile.strip_rule = '' THEN
1437         --Since we have nothing to do, just return what we were given.
1438         RETURN target_marc;
1439     ELSIF dyn_profile.replace_rule <> '' THEN
1440         trgt_marc = target_marc;
1441         tmpl_marc = template_marc;
1442         replace_rule = dyn_profile.replace_rule;
1443     ELSE
1444         tmp_marc = target_marc;
1445         trgt_marc = template_marc;
1446         tmpl_marc = tmp_marc;
1447         replace_rule = dyn_profile.preserve_rule;
1448     END IF;
1449
1450     RETURN vandelay.merge_record_xml( trgt_marc, tmpl_marc, dyn_profile.add_rule, replace_rule, dyn_profile.strip_rule );
1451
1452 END;
1453 $$ LANGUAGE PLPGSQL;
1454
1455 CREATE OR REPLACE FUNCTION vandelay.merge_record_xml_using_profile ( incoming_marc TEXT, existing_marc TEXT, merge_profile_id BIGINT ) RETURNS TEXT AS $$
1456 DECLARE
1457     merge_profile   vandelay.merge_profile%ROWTYPE;
1458     dyn_profile     vandelay.compile_profile%ROWTYPE;
1459     target_marc     TEXT;
1460     source_marc     TEXT;
1461     replace_rule    TEXT;
1462     match_count     INT;
1463 BEGIN
1464
1465     IF existing_marc IS NULL OR incoming_marc IS NULL THEN
1466         -- RAISE NOTICE 'no marc for source or target records';
1467         RETURN NULL;
1468     END IF;
1469
1470     IF merge_profile_id IS NOT NULL THEN
1471         SELECT * INTO merge_profile FROM vandelay.merge_profile WHERE id = merge_profile_id;
1472         IF FOUND THEN
1473             dyn_profile.add_rule := COALESCE(merge_profile.add_spec,'');
1474             dyn_profile.strip_rule := COALESCE(merge_profile.strip_spec,'');
1475             dyn_profile.replace_rule := COALESCE(merge_profile.replace_spec,'');
1476             dyn_profile.preserve_rule := COALESCE(merge_profile.preserve_spec,'');
1477         ELSE
1478             -- RAISE NOTICE 'merge profile not found';
1479             RETURN NULL;
1480         END IF;
1481     ELSE
1482         -- RAISE NOTICE 'no merge profile specified';
1483         RETURN NULL;
1484     END IF;
1485
1486     IF dyn_profile.replace_rule <> '' AND dyn_profile.preserve_rule <> '' THEN
1487         -- RAISE NOTICE 'both replace [%] and preserve [%] specified', dyn_profile.replace_rule, dyn_profile.preserve_rule;
1488         RETURN NULL;
1489     END IF;
1490
1491     IF dyn_profile.replace_rule = '' AND dyn_profile.preserve_rule = '' AND dyn_profile.add_rule = '' AND dyn_profile.strip_rule = '' THEN
1492         -- Since we have nothing to do, just return a target record as is
1493         RETURN existing_marc;
1494     ELSIF dyn_profile.preserve_rule <> '' THEN
1495         source_marc = existing_marc;
1496         target_marc = incoming_marc;
1497         replace_rule = dyn_profile.preserve_rule;
1498     ELSE
1499         source_marc = incoming_marc;
1500         target_marc = existing_marc;
1501         replace_rule = dyn_profile.replace_rule;
1502     END IF;
1503
1504     RETURN vandelay.merge_record_xml( target_marc, source_marc, dyn_profile.add_rule, replace_rule, dyn_profile.strip_rule );
1505
1506 END;
1507 $$ LANGUAGE PLPGSQL;
1508
1509 CREATE OR REPLACE FUNCTION vandelay.template_overlay_bib_record ( v_marc TEXT, eg_id BIGINT) RETURNS BOOL AS $$
1510     SELECT vandelay.template_overlay_bib_record( $1, $2, NULL);
1511 $$ LANGUAGE SQL;
1512
1513 CREATE OR REPLACE FUNCTION vandelay.overlay_bib_record ( import_id BIGINT, eg_id BIGINT, merge_profile_id INT ) RETURNS BOOL AS $$
1514 DECLARE
1515     editor_string   TEXT;
1516     editor_id       INT;
1517     v_marc          TEXT;
1518     v_bib_source    INT;
1519     update_fields   TEXT[];
1520     update_query    TEXT;
1521     update_bib      BOOL;
1522 BEGIN
1523
1524     SELECT  q.marc, q.bib_source INTO v_marc, v_bib_source
1525       FROM  vandelay.queued_bib_record q
1526             JOIN vandelay.bib_match m ON (m.queued_record = q.id AND q.id = import_id)
1527       LIMIT 1;
1528
1529     IF v_marc IS NULL THEN
1530         -- RAISE NOTICE 'no marc for vandelay or bib record';
1531         RETURN FALSE;
1532     END IF;
1533
1534     IF vandelay.template_overlay_bib_record( v_marc, eg_id, merge_profile_id) THEN
1535         UPDATE  vandelay.queued_bib_record
1536           SET   imported_as = eg_id,
1537                 import_time = NOW()
1538           WHERE id = import_id;
1539
1540           SELECT q.update_bib_source INTO update_bib FROM vandelay.merge_profile q where q.id = merge_profile_Id;
1541
1542           IF update_bib THEN
1543                 editor_string := (oils_xpath('//*[@tag="905"]/*[@code="u"]/text()',v_marc))[1];
1544
1545                 IF editor_string IS NOT NULL AND editor_string <> '' THEN
1546                     SELECT usr INTO editor_id FROM actor.card WHERE barcode = editor_string;
1547
1548                     IF editor_id IS NULL THEN
1549                         SELECT id INTO editor_id FROM actor.usr WHERE usrname = editor_string;
1550                     END IF;
1551
1552                     IF editor_id IS NOT NULL THEN
1553                         --only update the edit date if we have a valid editor
1554                         update_fields := ARRAY_APPEND(update_fields, 'editor = ' || editor_id || ', edit_date = NOW()');
1555                     END IF;
1556                 END IF;
1557
1558                 IF v_bib_source IS NOT NULL THEN
1559                     update_fields := ARRAY_APPEND(update_fields, 'source = ' || v_bib_source);
1560                 END IF;
1561
1562                 IF ARRAY_LENGTH(update_fields, 1) > 0 THEN
1563                     update_query := 'UPDATE biblio.record_entry SET ' || ARRAY_TO_STRING(update_fields, ',') || ' WHERE id = ' || eg_id || ';';
1564                     --RAISE NOTICE 'query: %', update_query;
1565                     EXECUTE update_query;
1566                 END IF;
1567         END IF;
1568
1569         RETURN TRUE;
1570     END IF;
1571
1572     -- RAISE NOTICE 'update of biblio.record_entry failed';
1573
1574     RETURN FALSE;
1575
1576 END;
1577 $$ LANGUAGE PLPGSQL;
1578
1579 CREATE OR REPLACE FUNCTION vandelay.auto_overlay_bib_record_with_best ( import_id BIGINT, merge_profile_id INT, lwm_ratio_value_p NUMERIC ) RETURNS BOOL AS $$
1580 DECLARE
1581     eg_id           BIGINT;
1582     lwm_ratio_value NUMERIC;
1583 BEGIN
1584
1585     lwm_ratio_value := COALESCE(lwm_ratio_value_p, 0.0);
1586
1587     PERFORM * FROM vandelay.queued_bib_record WHERE import_time IS NOT NULL AND id = import_id;
1588
1589     IF FOUND THEN
1590         -- RAISE NOTICE 'already imported, cannot auto-overlay'
1591         RETURN FALSE;
1592     END IF;
1593
1594     SELECT  m.eg_record INTO eg_id
1595       FROM  vandelay.bib_match m
1596             JOIN vandelay.queued_bib_record qr ON (m.queued_record = qr.id)
1597             JOIN vandelay.bib_queue q ON (qr.queue = q.id)
1598             JOIN biblio.record_entry r ON (r.id = m.eg_record)
1599       WHERE m.queued_record = import_id
1600             AND qr.quality::NUMERIC / COALESCE(NULLIF(m.quality,0),1)::NUMERIC >= lwm_ratio_value
1601       ORDER BY  m.match_score DESC, -- required match score
1602                 qr.quality::NUMERIC / COALESCE(NULLIF(m.quality,0),1)::NUMERIC DESC, -- quality tie breaker
1603                 m.id -- when in doubt, use the first match
1604       LIMIT 1;
1605
1606     IF eg_id IS NULL THEN
1607         -- RAISE NOTICE 'incoming record is not of high enough quality';
1608         RETURN FALSE;
1609     END IF;
1610
1611     RETURN vandelay.overlay_bib_record( import_id, eg_id, merge_profile_id );
1612 END;
1613 $$ LANGUAGE PLPGSQL;
1614
1615 CREATE OR REPLACE FUNCTION vandelay.auto_overlay_bib_record_with_best ( import_id BIGINT, merge_profile_id INT ) RETURNS BOOL AS $$
1616     SELECT vandelay.auto_overlay_bib_record_with_best( $1, $2, p.lwm_ratio ) FROM vandelay.merge_profile p WHERE id = $2;
1617 $$ LANGUAGE SQL;
1618
1619 CREATE OR REPLACE FUNCTION vandelay.auto_overlay_bib_record ( import_id BIGINT, merge_profile_id INT ) RETURNS BOOL AS $$
1620 DECLARE
1621     eg_id           BIGINT;
1622     match_count     INT;
1623 BEGIN
1624
1625     PERFORM * FROM vandelay.queued_bib_record WHERE import_time IS NOT NULL AND id = import_id;
1626
1627     IF FOUND THEN
1628         -- RAISE NOTICE 'already imported, cannot auto-overlay'
1629         RETURN FALSE;
1630     END IF;
1631
1632     SELECT COUNT(*) INTO match_count FROM vandelay.bib_match WHERE queued_record = import_id;
1633
1634     IF match_count <> 1 THEN
1635         -- RAISE NOTICE 'not an exact match';
1636         RETURN FALSE;
1637     END IF;
1638
1639     -- Check that the one match is on the first 901c
1640     SELECT  m.eg_record INTO eg_id
1641       FROM  vandelay.queued_bib_record q
1642             JOIN vandelay.bib_match m ON (m.queued_record = q.id)
1643       WHERE q.id = import_id
1644             AND m.eg_record = oils_xpath_string('//*[@tag="901"]/*[@code="c"][1]',marc)::BIGINT;
1645
1646     IF NOT FOUND THEN
1647         -- RAISE NOTICE 'not a 901c match';
1648         RETURN FALSE;
1649     END IF;
1650
1651     RETURN vandelay.overlay_bib_record( import_id, eg_id, merge_profile_id );
1652 END;
1653 $$ LANGUAGE PLPGSQL;
1654
1655 CREATE OR REPLACE FUNCTION vandelay.auto_overlay_bib_queue ( queue_id BIGINT, merge_profile_id INT ) RETURNS SETOF BIGINT AS $$
1656 DECLARE
1657     queued_record   vandelay.queued_bib_record%ROWTYPE;
1658 BEGIN
1659
1660     FOR queued_record IN SELECT * FROM vandelay.queued_bib_record WHERE queue = queue_id AND import_time IS NULL LOOP
1661
1662         IF vandelay.auto_overlay_bib_record( queued_record.id, merge_profile_id ) THEN
1663             RETURN NEXT queued_record.id;
1664         END IF;
1665
1666     END LOOP;
1667
1668     RETURN;
1669     
1670 END;
1671 $$ LANGUAGE PLPGSQL;
1672
1673 CREATE OR REPLACE FUNCTION vandelay.auto_overlay_bib_queue_with_best ( queue_id BIGINT, merge_profile_id INT, lwm_ratio_value NUMERIC ) RETURNS SETOF BIGINT AS $$
1674 DECLARE
1675     queued_record   vandelay.queued_bib_record%ROWTYPE;
1676 BEGIN
1677
1678     FOR queued_record IN SELECT * FROM vandelay.queued_bib_record WHERE queue = queue_id AND import_time IS NULL LOOP
1679
1680         IF vandelay.auto_overlay_bib_record_with_best( queued_record.id, merge_profile_id, lwm_ratio_value ) THEN
1681             RETURN NEXT queued_record.id;
1682         END IF;
1683
1684     END LOOP;
1685
1686     RETURN;
1687     
1688 END;
1689 $$ LANGUAGE PLPGSQL;
1690
1691 CREATE OR REPLACE FUNCTION vandelay.auto_overlay_bib_queue_with_best ( import_id BIGINT, merge_profile_id INT ) RETURNS SETOF BIGINT AS $$
1692     SELECT vandelay.auto_overlay_bib_queue_with_best( $1, $2, p.lwm_ratio ) FROM vandelay.merge_profile p WHERE id = $2;
1693 $$ LANGUAGE SQL;
1694
1695 CREATE OR REPLACE FUNCTION vandelay.auto_overlay_bib_queue ( queue_id BIGINT ) RETURNS SETOF BIGINT AS $$
1696     SELECT * FROM vandelay.auto_overlay_bib_queue( $1, NULL );
1697 $$ LANGUAGE SQL;
1698
1699 CREATE OR REPLACE FUNCTION vandelay.ingest_bib_marc ( ) RETURNS TRIGGER AS $$
1700 DECLARE
1701     value   TEXT;
1702     atype   TEXT;
1703     adef    RECORD;
1704 BEGIN
1705     IF TG_OP IN ('INSERT','UPDATE') AND NEW.imported_as IS NOT NULL THEN
1706         RETURN NEW;
1707     END IF;
1708
1709     FOR adef IN SELECT * FROM vandelay.bib_attr_definition LOOP
1710
1711         SELECT extract_marc_field('vandelay.queued_bib_record', id, adef.xpath, adef.remove) INTO value FROM vandelay.queued_bib_record WHERE id = NEW.id;
1712         IF (value IS NOT NULL AND value <> '') THEN
1713             INSERT INTO vandelay.queued_bib_record_attr (record, field, attr_value) VALUES (NEW.id, adef.id, value);
1714         END IF;
1715
1716     END LOOP;
1717
1718     RETURN NULL;
1719 END;
1720 $$ LANGUAGE PLPGSQL;
1721
1722 CREATE OR REPLACE FUNCTION vandelay.cleanup_bib_marc ( ) RETURNS TRIGGER AS $$
1723 BEGIN
1724     IF TG_OP IN ('INSERT','UPDATE') AND NEW.imported_as IS NOT NULL THEN
1725         RETURN NEW;
1726     END IF;
1727
1728     DELETE FROM vandelay.queued_bib_record_attr WHERE record = OLD.id;
1729     DELETE FROM vandelay.import_item WHERE record = OLD.id;
1730
1731     IF TG_OP = 'UPDATE' THEN
1732         RETURN NEW;
1733     END IF;
1734     RETURN OLD;
1735 END;
1736 $$ LANGUAGE PLPGSQL;
1737
1738 CREATE TRIGGER cleanup_bib_trigger
1739     BEFORE UPDATE OR DELETE ON vandelay.queued_bib_record
1740     FOR EACH ROW EXECUTE PROCEDURE vandelay.cleanup_bib_marc();
1741
1742 CREATE TRIGGER ingest_bib_trigger
1743     AFTER INSERT OR UPDATE ON vandelay.queued_bib_record
1744     FOR EACH ROW EXECUTE PROCEDURE vandelay.ingest_bib_marc();
1745
1746 CREATE TRIGGER zz_match_bibs_trigger
1747     BEFORE INSERT OR UPDATE ON vandelay.queued_bib_record
1748     FOR EACH ROW EXECUTE PROCEDURE vandelay.match_bib_record();
1749
1750
1751 /* Authority stuff down here */
1752 ---------------------------------------
1753 CREATE TABLE vandelay.authority_attr_definition (
1754         id                      SERIAL  PRIMARY KEY,
1755         code            TEXT    UNIQUE NOT NULL,
1756         description     TEXT,
1757         xpath           TEXT    NOT NULL,
1758         remove          TEXT    NOT NULL DEFAULT ''
1759 );
1760
1761 CREATE TYPE vandelay.authority_queue_queue_type AS ENUM ('authority');
1762 CREATE TABLE vandelay.authority_queue (
1763         queue_type      vandelay.authority_queue_queue_type NOT NULL DEFAULT 'authority',
1764         CONSTRAINT vand_authority_queue_name_once_per_owner_const UNIQUE (owner,name,queue_type)
1765 ) INHERITS (vandelay.queue);
1766 ALTER TABLE vandelay.authority_queue ADD PRIMARY KEY (id);
1767
1768 CREATE TABLE vandelay.queued_authority_record (
1769         queue           INT     NOT NULL REFERENCES vandelay.authority_queue (id) ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED,
1770         imported_as     INT     REFERENCES authority.record_entry (id) DEFERRABLE INITIALLY DEFERRED,
1771         import_error    TEXT    REFERENCES vandelay.import_error (code) ON DELETE SET NULL ON UPDATE CASCADE DEFERRABLE INITIALLY DEFERRED,
1772         error_detail    TEXT
1773 ) INHERITS (vandelay.queued_record);
1774 ALTER TABLE vandelay.queued_authority_record ADD PRIMARY KEY (id);
1775 CREATE INDEX queued_authority_record_queue_idx ON vandelay.queued_authority_record (queue);
1776
1777 CREATE TABLE vandelay.queued_authority_record_attr (
1778         id                      BIGSERIAL       PRIMARY KEY,
1779         record          BIGINT          NOT NULL REFERENCES vandelay.queued_authority_record (id) DEFERRABLE INITIALLY DEFERRED,
1780         field           INT                     NOT NULL REFERENCES vandelay.authority_attr_definition (id) DEFERRABLE INITIALLY DEFERRED,
1781         attr_value      TEXT            NOT NULL
1782 );
1783 CREATE INDEX queued_authority_record_attr_record_idx ON vandelay.queued_authority_record_attr (record);
1784
1785 CREATE TABLE vandelay.authority_match (
1786         id                              BIGSERIAL       PRIMARY KEY,
1787         queued_record   BIGINT          REFERENCES vandelay.queued_authority_record (id) ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED,
1788         eg_record               BIGINT          REFERENCES authority.record_entry (id) DEFERRABLE INITIALLY DEFERRED,
1789     quality         INT         NOT NULL DEFAULT 0,
1790     match_score     INT         NOT NULL DEFAULT 0
1791 );
1792
1793 CREATE OR REPLACE FUNCTION vandelay.ingest_authority_marc ( ) RETURNS TRIGGER AS $$
1794 DECLARE
1795     value   TEXT;
1796     atype   TEXT;
1797     adef    RECORD;
1798 BEGIN
1799     IF TG_OP IN ('INSERT','UPDATE') AND NEW.imported_as IS NOT NULL THEN
1800         RETURN NEW;
1801     END IF;
1802
1803     FOR adef IN SELECT * FROM vandelay.authority_attr_definition LOOP
1804
1805         SELECT extract_marc_field('vandelay.queued_authority_record', id, adef.xpath, adef.remove) INTO value FROM vandelay.queued_authority_record WHERE id = NEW.id;
1806         IF (value IS NOT NULL AND value <> '') THEN
1807             INSERT INTO vandelay.queued_authority_record_attr (record, field, attr_value) VALUES (NEW.id, adef.id, value);
1808         END IF;
1809
1810     END LOOP;
1811
1812     RETURN NULL;
1813 END;
1814 $$ LANGUAGE PLPGSQL;
1815
1816 CREATE OR REPLACE FUNCTION vandelay.cleanup_authority_marc ( ) RETURNS TRIGGER AS $$
1817 BEGIN
1818     IF TG_OP IN ('INSERT','UPDATE') AND NEW.imported_as IS NOT NULL THEN
1819         RETURN NEW;
1820     END IF;
1821
1822     DELETE FROM vandelay.queued_authority_record_attr WHERE record = OLD.id;
1823     IF TG_OP = 'UPDATE' THEN
1824         RETURN NEW;
1825     END IF;
1826     RETURN OLD;
1827 END;
1828 $$ LANGUAGE PLPGSQL;
1829
1830 CREATE TRIGGER cleanup_authority_trigger
1831     BEFORE UPDATE OR DELETE ON vandelay.queued_authority_record
1832     FOR EACH ROW EXECUTE PROCEDURE vandelay.cleanup_authority_marc();
1833
1834 CREATE TRIGGER ingest_authority_trigger
1835     AFTER INSERT OR UPDATE ON vandelay.queued_authority_record
1836     FOR EACH ROW EXECUTE PROCEDURE vandelay.ingest_authority_marc();
1837
1838 CREATE OR REPLACE FUNCTION vandelay.overlay_authority_record ( import_id BIGINT, eg_id BIGINT, merge_profile_id INT ) RETURNS BOOL AS $$
1839 DECLARE
1840     merge_profile   vandelay.merge_profile%ROWTYPE;
1841     dyn_profile     vandelay.compile_profile%ROWTYPE;
1842     editor_string   TEXT;
1843     new_editor      INT;
1844     new_edit_date   TIMESTAMPTZ;
1845     source_marc     TEXT;
1846     target_marc     TEXT;
1847     eg_marc_row     authority.record_entry%ROWTYPE;
1848     eg_marc         TEXT;
1849     v_marc          TEXT;
1850     replace_rule    TEXT;
1851     match_count     INT;
1852     update_query    TEXT;
1853 BEGIN
1854
1855     SELECT  * INTO eg_marc_row
1856       FROM  authority.record_entry b
1857             JOIN vandelay.authority_match m ON (m.eg_record = b.id AND m.queued_record = import_id)
1858       LIMIT 1;
1859
1860     SELECT  q.marc INTO v_marc
1861       FROM  vandelay.queued_record q
1862             JOIN vandelay.authority_match m ON (m.queued_record = q.id AND q.id = import_id)
1863       LIMIT 1;
1864
1865     eg_marc := eg_marc_row.marc;
1866
1867     IF eg_marc IS NULL OR v_marc IS NULL THEN
1868         -- RAISE NOTICE 'no marc for vandelay or authority record';
1869         RETURN FALSE;
1870     END IF;
1871
1872     -- Extract the editor string before any modification to the vandelay
1873     -- MARC occur.
1874     editor_string := 
1875         (oils_xpath('//*[@tag="905"]/*[@code="u"]/text()',v_marc))[1];
1876
1877     -- If an editor value can be found, update the authority record
1878     -- editor and edit_date values.
1879     IF editor_string IS NOT NULL AND editor_string <> '' THEN
1880
1881         -- Vandelay.pm sets the value to 'usrname' when needed.  
1882         SELECT id INTO new_editor
1883             FROM actor.usr WHERE usrname = editor_string;
1884
1885         IF new_editor IS NULL THEN
1886             SELECT usr INTO new_editor
1887                 FROM actor.card WHERE barcode = editor_string;
1888         END IF;
1889
1890         IF new_editor IS NOT NULL THEN
1891             new_edit_date := NOW();
1892         ELSE -- No valid editor, use current values
1893             new_editor = eg_marc_row.editor;
1894             new_edit_date = eg_marc_row.edit_date;
1895         END IF;
1896     ELSE
1897         new_editor = eg_marc_row.editor;
1898         new_edit_date = eg_marc_row.edit_date;
1899     END IF;
1900
1901     dyn_profile := vandelay.compile_profile( v_marc );
1902
1903     IF merge_profile_id IS NOT NULL THEN
1904         SELECT * INTO merge_profile FROM vandelay.merge_profile WHERE id = merge_profile_id;
1905         IF FOUND THEN
1906             dyn_profile.add_rule := BTRIM( dyn_profile.add_rule || ',' || COALESCE(merge_profile.add_spec,''), ',');
1907             dyn_profile.strip_rule := BTRIM( dyn_profile.strip_rule || ',' || COALESCE(merge_profile.strip_spec,''), ',');
1908             dyn_profile.replace_rule := BTRIM( dyn_profile.replace_rule || ',' || COALESCE(merge_profile.replace_spec,''), ',');
1909             dyn_profile.preserve_rule := BTRIM( dyn_profile.preserve_rule || ',' || COALESCE(merge_profile.preserve_spec,''), ',');
1910         END IF;
1911     END IF;
1912
1913     IF dyn_profile.replace_rule <> '' AND dyn_profile.preserve_rule <> '' THEN
1914         -- RAISE NOTICE 'both replace [%] and preserve [%] specified', dyn_profile.replace_rule, dyn_profile.preserve_rule;
1915         RETURN FALSE;
1916     END IF;
1917
1918     IF dyn_profile.replace_rule = '' AND dyn_profile.preserve_rule = '' AND dyn_profile.add_rule = '' AND dyn_profile.strip_rule = '' THEN
1919         --Since we have nothing to do, just return a NOOP "we did it"
1920         RETURN TRUE;
1921     ELSIF dyn_profile.replace_rule <> '' THEN
1922         source_marc = v_marc;
1923         target_marc = eg_marc;
1924         replace_rule = dyn_profile.replace_rule;
1925     ELSE
1926         source_marc = eg_marc;
1927         target_marc = v_marc;
1928         replace_rule = dyn_profile.preserve_rule;
1929     END IF;
1930
1931     UPDATE  authority.record_entry
1932       SET   marc = vandelay.merge_record_xml( target_marc, source_marc, dyn_profile.add_rule, replace_rule, dyn_profile.strip_rule ),
1933             editor = new_editor,
1934             edit_date = new_edit_date
1935       WHERE id = eg_id;
1936
1937     IF NOT FOUND THEN 
1938         -- Import/merge failed.  Nothing left to do.
1939         RETURN FALSE;
1940     END IF;
1941
1942     -- Authority record successfully merged / imported.
1943
1944     -- Update the vandelay record to show the successful import.
1945     UPDATE  vandelay.queued_authority_record
1946       SET   imported_as = eg_id,
1947             import_time = NOW()
1948       WHERE id = import_id;
1949
1950     RETURN TRUE;
1951
1952 END;
1953 $$ LANGUAGE PLPGSQL;
1954
1955 CREATE OR REPLACE FUNCTION vandelay.auto_overlay_authority_record ( import_id BIGINT, merge_profile_id INT ) RETURNS BOOL AS $$
1956 DECLARE
1957     eg_id           BIGINT;
1958     match_count     INT;
1959 BEGIN
1960     SELECT COUNT(*) INTO match_count FROM vandelay.authority_match WHERE queued_record = import_id;
1961
1962     IF match_count <> 1 THEN
1963         -- RAISE NOTICE 'not an exact match';
1964         RETURN FALSE;
1965     END IF;
1966
1967     SELECT  m.eg_record INTO eg_id
1968       FROM  vandelay.authority_match m
1969       WHERE m.queued_record = import_id
1970       LIMIT 1;
1971
1972     IF eg_id IS NULL THEN
1973         RETURN FALSE;
1974     END IF;
1975
1976     RETURN vandelay.overlay_authority_record( import_id, eg_id, merge_profile_id );
1977 END;
1978 $$ LANGUAGE PLPGSQL;
1979
1980 CREATE OR REPLACE FUNCTION vandelay.auto_overlay_authority_queue ( queue_id BIGINT, merge_profile_id INT ) RETURNS SETOF BIGINT AS $$
1981 DECLARE
1982     queued_record   vandelay.queued_authority_record%ROWTYPE;
1983 BEGIN
1984
1985     FOR queued_record IN SELECT * FROM vandelay.queued_authority_record WHERE queue = queue_id AND import_time IS NULL LOOP
1986
1987         IF vandelay.auto_overlay_authority_record( queued_record.id, merge_profile_id ) THEN
1988             RETURN NEXT queued_record.id;
1989         END IF;
1990
1991     END LOOP;
1992
1993     RETURN;
1994     
1995 END;
1996 $$ LANGUAGE PLPGSQL;
1997
1998 CREATE OR REPLACE FUNCTION vandelay.auto_overlay_authority_queue ( queue_id BIGINT ) RETURNS SETOF BIGINT AS $$
1999     SELECT * FROM vandelay.auto_overlay_authority_queue( $1, NULL );
2000 $$ LANGUAGE SQL;
2001
2002 CREATE OR REPLACE FUNCTION vandelay.match_set_test_authxml(
2003     match_set_id INTEGER, record_xml TEXT
2004 ) RETURNS SETOF vandelay.match_set_test_result AS $$
2005 DECLARE
2006     tags_rstore HSTORE;
2007     heading     TEXT;
2008     coal        TEXT;
2009     joins       TEXT;
2010     query_      TEXT;
2011     wq          TEXT;
2012     qvalue      INTEGER;
2013     rec         RECORD;
2014 BEGIN
2015     tags_rstore := vandelay.flatten_marc_hstore(record_xml);
2016
2017     SELECT normalize_heading INTO heading 
2018         FROM authority.normalize_heading(record_xml);
2019
2020     CREATE TEMPORARY TABLE _vandelay_tmp_qrows (q INTEGER);
2021     CREATE TEMPORARY TABLE _vandelay_tmp_jrows (j TEXT);
2022
2023     -- generate the where clause and return that directly (into wq), and as
2024     -- a side-effect, populate the _vandelay_tmp_[qj]rows tables.
2025     wq := vandelay.get_expr_from_match_set(
2026         match_set_id, tags_rstore, heading);
2027
2028     query_ := 'SELECT DISTINCT(record), ';
2029
2030     -- qrows table is for the quality bits we add to the SELECT clause
2031     SELECT STRING_AGG(
2032         'COALESCE(n' || q::TEXT || '.quality, 0)', ' + '
2033     ) INTO coal FROM _vandelay_tmp_qrows;
2034
2035     -- our query string so far is the SELECT clause and the inital FROM.
2036     -- no JOINs yet nor the WHERE clause
2037     query_ := query_ || coal || ' AS quality ' || E'\n';
2038
2039     -- jrows table is for the joins we must make (and the real text conditions)
2040     SELECT STRING_AGG(j, E'\n') INTO joins
2041         FROM _vandelay_tmp_jrows;
2042
2043     -- add those joins and the where clause to our query.
2044     query_ := query_ || joins || E'\n';
2045
2046     query_ := query_ || 'JOIN authority.record_entry are ON (are.id = record) ' 
2047         || 'WHERE ' || wq || ' AND not are.deleted';
2048
2049     -- this will return rows of record,quality
2050     FOR rec IN EXECUTE query_ USING tags_rstore LOOP
2051         RETURN NEXT rec;
2052     END LOOP;
2053
2054     DROP TABLE _vandelay_tmp_qrows;
2055     DROP TABLE _vandelay_tmp_jrows;
2056     RETURN;
2057 END;
2058 $$ LANGUAGE PLPGSQL;
2059
2060 CREATE OR REPLACE FUNCTION vandelay.measure_auth_record_quality 
2061     ( xml TEXT, match_set_id INT ) RETURNS INT AS $_$
2062 DECLARE
2063     out_q   INT := 0;
2064     rvalue  TEXT;
2065     test    vandelay.match_set_quality%ROWTYPE;
2066 BEGIN
2067
2068     FOR test IN SELECT * FROM vandelay.match_set_quality 
2069             WHERE match_set = match_set_id LOOP
2070         IF test.tag IS NOT NULL THEN
2071             FOR rvalue IN SELECT value FROM vandelay.flatten_marc( xml ) 
2072                 WHERE tag = test.tag AND subfield = test.subfield LOOP
2073                 IF test.value = rvalue THEN
2074                     out_q := out_q + test.quality;
2075                 END IF;
2076             END LOOP;
2077         END IF;
2078     END LOOP;
2079
2080     RETURN out_q;
2081 END;
2082 $_$ LANGUAGE PLPGSQL;
2083
2084
2085
2086 CREATE OR REPLACE FUNCTION vandelay.match_authority_record() RETURNS TRIGGER AS $func$
2087 DECLARE
2088     incoming_existing_id    TEXT;
2089     test_result             vandelay.match_set_test_result%ROWTYPE;
2090     tmp_rec                 BIGINT;
2091     match_set               INT;
2092 BEGIN
2093     IF TG_OP IN ('INSERT','UPDATE') AND NEW.imported_as IS NOT NULL THEN
2094         RETURN NEW;
2095     END IF;
2096
2097     DELETE FROM vandelay.authority_match WHERE queued_record = NEW.id;
2098
2099     SELECT q.match_set INTO match_set FROM vandelay.authority_queue q WHERE q.id = NEW.queue;
2100
2101     IF match_set IS NOT NULL THEN
2102         NEW.quality := vandelay.measure_auth_record_quality( NEW.marc, match_set );
2103     END IF;
2104
2105     -- Perfect matches on 901$c exit early with a match with high quality.
2106     incoming_existing_id :=
2107         oils_xpath_string('//*[@tag="901"]/*[@code="c"][1]', NEW.marc);
2108
2109     IF incoming_existing_id IS NOT NULL AND incoming_existing_id != '' THEN
2110         SELECT id INTO tmp_rec FROM authority.record_entry WHERE id = incoming_existing_id::bigint;
2111         IF tmp_rec IS NOT NULL THEN
2112             INSERT INTO vandelay.authority_match (queued_record, eg_record, match_score, quality) 
2113                 SELECT
2114                     NEW.id, 
2115                     b.id,
2116                     9999,
2117                     -- note: no match_set means quality==0
2118                     vandelay.measure_auth_record_quality( b.marc, match_set )
2119                 FROM authority.record_entry b
2120                 WHERE id = incoming_existing_id::bigint;
2121         END IF;
2122     END IF;
2123
2124     IF match_set IS NULL THEN
2125         RETURN NEW;
2126     END IF;
2127
2128     FOR test_result IN SELECT * FROM
2129         vandelay.match_set_test_authxml(match_set, NEW.marc) LOOP
2130
2131         INSERT INTO vandelay.authority_match ( queued_record, eg_record, match_score, quality )
2132             SELECT  
2133                 NEW.id,
2134                 test_result.record,
2135                 test_result.quality,
2136                 vandelay.measure_auth_record_quality( b.marc, match_set )
2137                 FROM  authority.record_entry b
2138                 WHERE id = test_result.record;
2139
2140     END LOOP;
2141
2142     RETURN NEW;
2143 END;
2144 $func$ LANGUAGE PLPGSQL;
2145
2146 CREATE TRIGGER zz_match_auths_trigger
2147     BEFORE INSERT OR UPDATE ON vandelay.queued_authority_record
2148     FOR EACH ROW EXECUTE PROCEDURE vandelay.match_authority_record();
2149
2150 CREATE OR REPLACE FUNCTION vandelay.auto_overlay_authority_record_with_best ( import_id BIGINT, merge_profile_id INT, lwm_ratio_value_p NUMERIC ) RETURNS BOOL AS $$
2151 DECLARE
2152     eg_id           BIGINT;
2153     lwm_ratio_value NUMERIC;
2154 BEGIN
2155
2156     lwm_ratio_value := COALESCE(lwm_ratio_value_p, 0.0);
2157
2158     PERFORM * FROM vandelay.queued_authority_record WHERE import_time IS NOT NULL AND id = import_id;
2159
2160     IF FOUND THEN
2161         -- RAISE NOTICE 'already imported, cannot auto-overlay'
2162         RETURN FALSE;
2163     END IF;
2164
2165     SELECT  m.eg_record INTO eg_id
2166       FROM  vandelay.authority_match m
2167             JOIN vandelay.queued_authority_record qr ON (m.queued_record = qr.id)
2168             JOIN vandelay.authority_queue q ON (qr.queue = q.id)
2169             JOIN authority.record_entry r ON (r.id = m.eg_record)
2170       WHERE m.queued_record = import_id
2171             AND qr.quality::NUMERIC / COALESCE(NULLIF(m.quality,0),1)::NUMERIC >= lwm_ratio_value
2172       ORDER BY  m.match_score DESC, -- required match score
2173                 qr.quality::NUMERIC / COALESCE(NULLIF(m.quality,0),1)::NUMERIC DESC, -- quality tie breaker
2174                 m.id -- when in doubt, use the first match
2175       LIMIT 1;
2176
2177     IF eg_id IS NULL THEN
2178         -- RAISE NOTICE 'incoming record is not of high enough quality';
2179         RETURN FALSE;
2180     END IF;
2181
2182     RETURN vandelay.overlay_authority_record( import_id, eg_id, merge_profile_id );
2183 END;
2184 $$ LANGUAGE PLPGSQL;
2185
2186
2187
2188
2189 -- Vandelay (for importing and exporting records) 012.schema.vandelay.sql 
2190 --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)]');
2191 --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)]');
2192 --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]');
2193 --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]');
2194 --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$);
2195 --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$);
2196 --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]');
2197 --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);
2198 --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);
2199 --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);
2200 --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);
2201 --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]');
2202 --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$);
2203 --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]');
2204 --
2205 --INSERT INTO vandelay.import_item_attr_definition (
2206 --    owner, name, tag, owning_lib, circ_lib, location,
2207 --    call_number, circ_modifier, barcode, price, copy_number,
2208 --    circulate, ref, holdable, opac_visible, status
2209 --) VALUES (
2210 --    1,
2211 --    'Evergreen 852 export format',
2212 --    '852',
2213 --    '[@code = "b"][1]',
2214 --    '[@code = "b"][2]',
2215 --    'c',
2216 --    'j',
2217 --    'g',
2218 --    'p',
2219 --    'y',
2220 --    't',
2221 --    '[@code = "x" and text() = "circulating"]',
2222 --    '[@code = "x" and text() = "reference"]',
2223 --    '[@code = "x" and text() = "holdable"]',
2224 --    '[@code = "x" and text() = "visible"]',
2225 --    'z'
2226 --);
2227 --
2228 --INSERT INTO vandelay.import_item_attr_definition (
2229 --    owner,
2230 --    name,
2231 --    tag,
2232 --    owning_lib,
2233 --    location,
2234 --    call_number,
2235 --    circ_modifier,
2236 --    barcode,
2237 --    price,
2238 --    status
2239 --) VALUES (
2240 --    1,
2241 --    'Unicorn Import format -- 999',
2242 --    '999',
2243 --    'm',
2244 --    'l',
2245 --    'a',
2246 --    't',
2247 --    'i',
2248 --    'p',
2249 --    'k'
2250 --);
2251 --
2252 --INSERT INTO vandelay.authority_attr_definition ( code, description, xpath, ident ) VALUES ('rec_identifier','Identifier','//*[@tag="001"]', TRUE);
2253
2254
2255 CREATE TABLE vandelay.session_tracker (
2256     id          BIGSERIAL PRIMARY KEY,
2257
2258     -- string of characters (e.g. md5) used for linking trackers
2259     -- of different actions into a series.  There can be multiple
2260     -- session_keys of each action type, creating the opportunity
2261     -- to link multiple action trackers into a single session.
2262     session_key TEXT NOT NULL,
2263
2264     -- optional user-supplied name
2265     name        TEXT NOT NULL, 
2266
2267     usr         INTEGER NOT NULL REFERENCES actor.usr(id)
2268                 DEFERRABLE INITIALLY DEFERRED,
2269
2270     -- org unit can be derived from WS
2271     workstation INTEGER NOT NULL REFERENCES actor.workstation(id)
2272                 ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED,
2273
2274     -- bib/auth
2275     record_type TEXT NOT NULL DEFAULT 'bib',
2276
2277     -- Queue defines the source of the data, it does not necessarily
2278     -- mean that an action is being performed against an entire queue.
2279     -- E.g. some imports are misc. lists of record IDs, but they always 
2280     -- come from one queue.
2281     -- No foreign key -- could be auth or bib queue.
2282     queue       BIGINT NOT NULL,
2283
2284     create_time TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
2285     update_time TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
2286
2287     state       TEXT NOT NULL DEFAULT 'active',
2288
2289     action_type TEXT NOT NULL DEFAULT 'enqueue', -- import
2290
2291     -- total number of tasks to perform / loosely defined
2292     -- could be # of recs to import or # of recs + # of copies 
2293     -- depending on the import context
2294     total_actions INTEGER NOT NULL DEFAULT 0,
2295
2296     -- total number of tasked performed so far
2297     actions_performed INTEGER NOT NULL DEFAULT 0,
2298
2299     CONSTRAINT vand_tracker_valid_state 
2300         CHECK (state IN ('active','error','complete')),
2301
2302     CONSTRAINT vand_tracker_valid_action_type
2303         CHECK (action_type IN ('upload', 'enqueue', 'import')),
2304
2305     CONSTRAINT vand_tracker_valid_record_type
2306         CHECK (record_type IN ('bib', 'authority'))
2307 );
2308
2309 COMMIT;
2310