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