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