[open-ils-commits] [GIT] Evergreen ILS branch master updated. 81d720fefe89c2a9a38b9658a44a3b0bf69cee4a

Evergreen Git git at git.evergreen-ils.org
Fri Feb 21 16:02:33 EST 2014


This is an automated email from the git hooks/post-receive script. It was
generated because a ref change was pushed to the repository containing
the project "Evergreen ILS".

The branch, master has been updated
       via  81d720fefe89c2a9a38b9658a44a3b0bf69cee4a (commit)
       via  c7503c77fab597029a53097fb8f414b72382a1c0 (commit)
       via  00c36d702f909079294eba5d0ef591fb1a85a1f8 (commit)
       via  0a6b913e0cbc025626bcf12d98e04f9d49448c9c (commit)
       via  3fd258fda0af6cf5cc53c7a87974027d694d6add (commit)
       via  4c8fc7fbb8733e27d480b838df2e86f1f1cc7ce7 (commit)
       via  f5bc354c5d6dffd1a89514bd159b2051bfb90271 (commit)
       via  3dfd7055bb024a385165ceca9e88b49014a8e515 (commit)
       via  f2e96e0ebf3f0f75e0d770ccc755406d5df84c09 (commit)
       via  c2c8b2796d14b8375ddca631a74d7ed75b882a20 (commit)
       via  d8e33af69f190503798823e979e9ea80307c50dd (commit)
       via  019f80a810c8d2e81e5602936b02d73b67a15d27 (commit)
       via  6d328afcc86a28fdd9cf43294b4c630b02807a95 (commit)
       via  16e8e0e59e913c551a429d1fb3f2dc1b126e2099 (commit)
       via  b0ff3c9e3e839dd16b4260763817c4a9078c8339 (commit)
       via  c62821619ce2a00128aa9cc4a7b19d6e94f1b4fa (commit)
       via  f77269997a72f188ca8a733f5125175bf2b83dc2 (commit)
       via  811325f638f9291752de1134f61fa073732ed3f7 (commit)
       via  d6f40df7f4f0b4d9210dc7aee5ab9771a63cbb5b (commit)
       via  b2e4783726756af658f0fee069c7780425955cd1 (commit)
       via  fc99f4f2230061922f8bea0e061f04d240a1ddfa (commit)
       via  e6421bcacd6e7c8cd13e871e1961f14c939151c6 (commit)
       via  36e3c0a8d4861eb8d877664623ef925331e45bb1 (commit)
       via  586acbd644362c823bd9865e61bf130fb0b4fc11 (commit)
       via  922a53a5b64f0d23eacba5eb1c834a6cd60e46fb (commit)
       via  5a7add8fe93ac1228bf4791401d109df90c3d380 (commit)
       via  e4ca479407e3dd17b16a4e53868b9e09881e6ce5 (commit)
       via  33961761022b2021b2428d1909f75b8aeb972af3 (commit)
       via  734d2302b72a0f7d55f23a2ed92e7313c10cee03 (commit)
       via  c33b7cf919db4eb087502322ad3b82572bb54f65 (commit)
       via  00ced69379ad8101beaf2cdfa410d72ece9bc6b0 (commit)
       via  abe6bdf09ea832296baf41daa5dadc378b2fef08 (commit)
       via  bcc5d4146b49b174be4b8511d791fcfdb6a448b9 (commit)
       via  d2b047058e99da0ce2c944643a03b432beb4dda5 (commit)
      from  0bb4915e5129c21a2600eb28205a8f111cba1f2b (commit)

Those revisions listed above that are new to this repository have
not appeared on any other notification email; so we list those
revisions in full, below.

- Log -----------------------------------------------------------------
commit 81d720fefe89c2a9a38b9658a44a3b0bf69cee4a
Author: Dan Wells <dbw2 at calvin.edu>
Date:   Fri Feb 21 15:58:50 2014 -0500

    Stamping 0864-0867 for MVF, CRA, and TPAC MRs
    
    That is, Multi-valued Fields, Composite Record Attibutes, and TPAC
    Metarecord support.
    
    Signed-off-by: Dan Wells <dbw2 at calvin.edu>

diff --git a/Open-ILS/src/sql/Pg/002.schema.config.sql b/Open-ILS/src/sql/Pg/002.schema.config.sql
index e165f06..6900d1b 100644
--- a/Open-ILS/src/sql/Pg/002.schema.config.sql
+++ b/Open-ILS/src/sql/Pg/002.schema.config.sql
@@ -91,7 +91,7 @@ CREATE TRIGGER no_overlapping_deps
     BEFORE INSERT OR UPDATE ON config.db_patch_dependencies
     FOR EACH ROW EXECUTE PROCEDURE evergreen.array_overlap_check ('deprecates');
 
-INSERT INTO config.upgrade_log (version, applied_to) VALUES ('0863', :eg_version); -- senator/dbwells
+INSERT INTO config.upgrade_log (version, applied_to) VALUES ('0867', :eg_version); -- miker/berick/dbwells
 
 CREATE TABLE config.bib_source (
 	id		SERIAL	PRIMARY KEY,
diff --git a/Open-ILS/src/sql/Pg/upgrade/QQQQ.MVF_CRA-upgrade.sql b/Open-ILS/src/sql/Pg/upgrade/0864.MVF_CRA-upgrade.sql
similarity index 99%
rename from Open-ILS/src/sql/Pg/upgrade/QQQQ.MVF_CRA-upgrade.sql
rename to Open-ILS/src/sql/Pg/upgrade/0864.MVF_CRA-upgrade.sql
index b0d1770..4adbde7 100644
--- a/Open-ILS/src/sql/Pg/upgrade/QQQQ.MVF_CRA-upgrade.sql
+++ b/Open-ILS/src/sql/Pg/upgrade/0864.MVF_CRA-upgrade.sql
@@ -1,5 +1,7 @@
 BEGIN;
 
+SELECT evergreen.upgrade_deps_block_check('0864', :eg_version);
+
 CREATE EXTENSION intarray;
 
 -- while we have this opportunity, and before we start collecting 
diff --git a/Open-ILS/src/sql/Pg/upgrade/ZZZZ.schema.convert-MR-holdable_formats.sql b/Open-ILS/src/sql/Pg/upgrade/0865.schema.convert-MR-holdable_formats.sql
similarity index 95%
rename from Open-ILS/src/sql/Pg/upgrade/ZZZZ.schema.convert-MR-holdable_formats.sql
rename to Open-ILS/src/sql/Pg/upgrade/0865.schema.convert-MR-holdable_formats.sql
index b2e4028..166460f 100644
--- a/Open-ILS/src/sql/Pg/upgrade/ZZZZ.schema.convert-MR-holdable_formats.sql
+++ b/Open-ILS/src/sql/Pg/upgrade/0865.schema.convert-MR-holdable_formats.sql
@@ -1,6 +1,8 @@
 
 BEGIN;
 
+SELECT evergreen.upgrade_deps_block_check('0865', :eg_version);
+
 -- First, explode the field into constituent parts
 WITH format_parts_array AS (
     SELECT  a.id,
diff --git a/Open-ILS/src/sql/Pg/upgrade/ZZZZ.schema.unapi-mmr.sql b/Open-ILS/src/sql/Pg/upgrade/0866.schema.unapi-mmr.sql
similarity index 99%
rename from Open-ILS/src/sql/Pg/upgrade/ZZZZ.schema.unapi-mmr.sql
rename to Open-ILS/src/sql/Pg/upgrade/0866.schema.unapi-mmr.sql
index 20889b1..9d707dd 100644
--- a/Open-ILS/src/sql/Pg/upgrade/ZZZZ.schema.unapi-mmr.sql
+++ b/Open-ILS/src/sql/Pg/upgrade/0866.schema.unapi-mmr.sql
@@ -1,5 +1,7 @@
 BEGIN;
 
+SELECT evergreen.upgrade_deps_block_check('0866', :eg_version);
+
 DROP FUNCTION asset.record_has_holdable_copy (BIGINT);
 CREATE FUNCTION asset.record_has_holdable_copy ( rid BIGINT, ou INT DEFAULT NULL) RETURNS BOOL AS $f$
 BEGIN
diff --git a/Open-ILS/src/sql/Pg/upgrade/ZZZZZ.data.mmr-holds-formats.sql b/Open-ILS/src/sql/Pg/upgrade/0867.data.mmr-holds-formats.sql
similarity index 99%
rename from Open-ILS/src/sql/Pg/upgrade/ZZZZZ.data.mmr-holds-formats.sql
rename to Open-ILS/src/sql/Pg/upgrade/0867.data.mmr-holds-formats.sql
index 5c11a87..48fa0ef 100644
--- a/Open-ILS/src/sql/Pg/upgrade/ZZZZZ.data.mmr-holds-formats.sql
+++ b/Open-ILS/src/sql/Pg/upgrade/0867.data.mmr-holds-formats.sql
@@ -1,5 +1,7 @@
 BEGIN;
 
+SELECT evergreen.upgrade_deps_block_check('0867', :eg_version);
+
 INSERT INTO config.global_flag (name, label, value, enabled) VALUES (
     'opac.metarecord.holds.format_attr', 
     oils_i18n_gettext(

commit c7503c77fab597029a53097fb8f414b72382a1c0
Author: Bill Erickson <berick at esilibrary.com>
Date:   Fri Feb 21 13:48:07 2014 -0500

    LP#1053397 Prevent display non-opac-visible icons / hold formats
    
    Avoid showing coded value maps for icons, hold formats, and hold
    languages where the coded value's opac_visible value is FALSE.
    
    Signed-off-by: Bill Erickson <berick at esilibrary.com>
    Signed-off-by: Dan Wells <dbw2 at calvin.edu>

diff --git a/Open-ILS/src/templates/opac/myopac/holds.tt2 b/Open-ILS/src/templates/opac/myopac/holds.tt2
index b08d230..0f5555a 100644
--- a/Open-ILS/src/templates/opac/myopac/holds.tt2
+++ b/Open-ILS/src/templates/opac/myopac/holds.tt2
@@ -156,9 +156,10 @@
                               # only show selected formats for metarecords
                               formats = [];
                               FOR ccvm IN hold.metarecord_selected_filters.icons;
+                                NEXT IF ccvm.opac_visible == 'f';
                                 format = {};
-                                format.icon = PROCESS get_ccvm_icon id=ccvm.id search_label=1;
-                                format.label = PROCESS get_ccvm_label id=ccvm.id search_label=1;
+                                format.label = ccvm.search_label || ccvm.value;
+                                format.icon = PROCESS get_ccvm_icon ccvm=ccvm;
                                 formats.push(format);
                               END;
                             END;
diff --git a/Open-ILS/src/templates/opac/parts/metarecord_hold_filters.tt2 b/Open-ILS/src/templates/opac/parts/metarecord_hold_filters.tt2
index 452feed..5478400 100644
--- a/Open-ILS/src/templates/opac/parts/metarecord_hold_filters.tt2
+++ b/Open-ILS/src/templates/opac/parts/metarecord_hold_filters.tt2
@@ -47,8 +47,9 @@ limiting the set of desired records for a given metarecord.
     </div>
     <select multiple='multiple' 
       name="metarecord_formats_[% target_id %]">
-      [% FOR ccvm IN 
-        hold_data.metarecord_filters.formats.sort('search_label') %]
+      [% FOR ccvm IN
+          hold_data.metarecord_filters.formats.sort('search_label');
+          NEXT IF ccvm.opac_visible == 'f' %]
         <option value="[% ccvm.code %]"[%- code = ccvm.code; 
             IF selected_formats.$code %] selected='selected'[% END -%]>
           [% ccvm.search_label | html %]
@@ -65,8 +66,9 @@ limiting the set of desired records for a given metarecord.
     </div>
     <select multiple='multiple' 
       name="metarecord_langs_[% target_id %]">
-      [% FOR lang_ccvm IN hold_data.metarecord_filters.langs.sort('value') %]
-        [%  selected = 0; 
+      [% FOR lang_ccvm IN hold_data.metarecord_filters.langs.sort('value');
+            NEXT IF lang_ccvm.opac_visible == 'f';
+            selected = 0; 
             code = lang_ccvm.code;
             IF selected_langs.size;
                 # user has already selected their preferred language(s)
diff --git a/Open-ILS/src/templates/opac/parts/misc_util.tt2 b/Open-ILS/src/templates/opac/parts/misc_util.tt2
index c003c15..63b82b4 100644
--- a/Open-ILS/src/templates/opac/parts/misc_util.tt2
+++ b/Open-ILS/src/templates/opac/parts/misc_util.tt2
@@ -79,22 +79,10 @@
         END;
     END;
 
-    BLOCK get_ccvm_label;
-        ccvm = ctx.get_ccvm(id); # caches internally
-        IF search_label and ccvm.search_label;
-            ccvm.search_label;
-        ELSE;
-            ccvm.$id.value;
-        END;
-    END;
-
     BLOCK get_ccvm_icon;
-        ccvm = ctx.get_ccvm(id); # caches internally
-        label = PROCESS get_ccvm_label id=id search_label=search_label;
         ctx.media_prefix _ '/images/format_icons/' _ ccvm.ctype _ '/' _ ccvm.code _ '.png';
     END;
 
-
     # Extract MARC fields from XML
     #   get_marc_attrs( { marc_xml => doc } )
     BLOCK get_marc_attrs;
@@ -446,12 +434,13 @@
         args.all_formats = [];
         FOR node IN xml.findnodes(formats_xpath);
             IF node AND node.textContent;
-                type = node.textContent;
+                ccvm = ctx.get_ccvm(node.getAttribute('cvmid'));
+                NEXT IF ccvm.opac_visible == 'f';
+
                 format = {};
-                format.icon = PROCESS get_ccvm_icon 
-                    id=node.getAttribute('cvmid') search_label=1;
-                format.label = PROCESS get_ccvm_label 
-                    id=node.getAttribute('cvmid') search_label=1;
+                type = node.textContent;
+                format.label = ccvm.search_label || ccvm.value;
+                format.icon = PROCESS get_ccvm_icon ccvm=ccvm;
                 format.itemtype = schema_typemap.$type || 'CreativeWork';
 
                 args.all_formats.push(format); # metarecords want all formats

commit 00c36d702f909079294eba5d0ef591fb1a85a1f8
Author: Bill Erickson <berick at esilibrary.com>
Date:   Fri Feb 21 12:19:14 2014 -0500

    LP#1053397 icon / metarcord format additions
    
     * Adds a new blu-ray format for icon and metarecord holds
    
     * Remove large-print items from the generic "book" attribute for icons
       and holds.
    
       For icons, this means we will no longer see two icons displayed for a
       single large-print book.
    
       For holds, this means that to receive a large-print book, one has to
       select the large-print book format.  "book" alone will not result in
       a large-print item being targted.
    
       Note that when building a data set for the format selector drop-down,
       it might make sense to keep large-print items in the generic "book"
       format.  That'll come later.
    
    Signed-off-by: Bill Erickson <berick at esilibrary.com>
    Signed-off-by: Mike Rylander <mrylander at gmail.com>
    Signed-off-by: Dan Wells <dbw2 at calvin.edu>

diff --git a/Open-ILS/src/sql/Pg/950.data.seed-values.sql b/Open-ILS/src/sql/Pg/950.data.seed-values.sql
index 98e5617..8625c1b 100644
--- a/Open-ILS/src/sql/Pg/950.data.seed-values.sql
+++ b/Open-ILS/src/sql/Pg/950.data.seed-values.sql
@@ -6818,6 +6818,21 @@ INSERT INTO config.coded_value_map
     oils_i18n_gettext(607, 'Musical Sound Recording (Unknown Format)', 'ccvm', 'value'),
     oils_i18n_gettext(607, 'Musical Sound Recording (Unknown Format)', 'ccvm', 'search_label'));
 
+-- icon for blu-ray
+INSERT INTO config.coded_value_map
+    (id, ctype, code, value, search_label) VALUES 
+(608, 'icon_format', 'blu-ray', 
+    oils_i18n_gettext(608, 'Blu-ray', 'ccvm', 'value'),
+    oils_i18n_gettext(608, 'Blu-ray', 'ccvm', 'search_label'));
+
+-- metarecord hold format for blu-ray
+INSERT INTO config.coded_value_map
+    (id, ctype, code, value, search_label) VALUES 
+(609, 'mr_hold_format', 'blu-ray', 
+    oils_i18n_gettext(609, 'Blu-ray', 'ccvm', 'value'),
+    oils_i18n_gettext(609, 'Blu-ray', 'ccvm', 'search_label'));
+
+
 -- carve out a slot of 10k IDs for stock CCVMs
 SELECT SETVAL('config.coded_value_map_id_seq'::TEXT, 10000);
 
@@ -6827,7 +6842,7 @@ SELECT SETVAL('config.coded_value_map_id_seq'::TEXT, 10000);
 INSERT INTO config.composite_attr_entry_definition 
     (coded_value, definition) VALUES
 --book
-(564, '{"0":[{"_attr":"item_type","_val":"a"},{"_attr":"item_type","_val":"t"}],"1":{"_not":[{"_attr":"item_form","_val":"a"},{"_attr":"item_form","_val":"b"},{"_attr":"item_form","_val":"c"},{"_attr":"item_form","_val":"f"},{"_attr":"item_form","_val":"o"},{"_attr":"item_form","_val":"q"},{"_attr":"item_form","_val":"r"},{"_attr":"item_form","_val":"s"}]},"2":[{"_attr":"bib_level","_val":"a"},{"_attr":"bib_level","_val":"c"},{"_attr":"bib_level","_val":"d"},{"_attr":"bib_level","_val":"m"}]}'),
+(564, '{"0":[{"_attr":"item_type","_val":"a"},{"_attr":"item_type","_val":"t"}],"1":{"_not":[{"_attr":"item_form","_val":"a"},{"_attr":"item_form","_val":"b"},{"_attr":"item_form","_val":"c"},{"_attr":"item_form","_val":"d"},{"_attr":"item_form","_val":"f"},{"_attr":"item_form","_val":"o"},{"_attr":"item_form","_val":"q"},{"_attr":"item_form","_val":"r"},{"_attr":"item_form","_val":"s"}]},"2":[{"_attr":"bib_level","_val":"a"},{"_attr":"bib_level","_val":"c"},{"_attr":"bib_level","_val":"d"},{"_attr":"bib_level","_val":"m"}]}'),
 
 -- braille
 (565, '{"0":{"_attr":"item_type","_val":"a"},"1":{"_attr":"item_form","_val":"f"}}'),
@@ -6897,12 +6912,16 @@ INSERT INTO config.composite_attr_entry_definition
     (coded_value, definition) VALUES
 (607, '{"0":{"_attr":"item_type","_val":"j"},"1":{"_not":[{"_attr":"sr_format","_val":"a"},{"_attr":"sr_format","_val":"b"},{"_attr":"sr_format","_val":"c"},{"_attr":"sr_format","_val":"d"},{"_attr":"sr_format","_val":"f"},{"_attr":"sr_format","_val":"e"},{"_attr":"sr_format","_val":"l"}]}}');
 
+-- blu-ray icon_format
+INSERT INTO config.composite_attr_entry_definition 
+    (coded_value, definition) VALUES (608, '{"_attr":"vr_format","_val":"s"}');
+
 -- use the definitions from the icon_format as the basis for the MR hold format definitions
 DO $$
     DECLARE format TEXT;
 BEGIN
     FOR format IN SELECT UNNEST(
-        '{book,braille,software,dvd,kit,map,microform,score,picture,equip,serial,vhs,cdaudiobook,cdmusic,casaudiobook,casmusic,phonospoken,phonomusic,lpbook}'::text[])
+        '{book,braille,software,dvd,kit,map,microform,score,picture,equip,serial,vhs,cdaudiobook,cdmusic,casaudiobook,casmusic,phonospoken,phonomusic,lpbook,blu-ray}'::text[])
     LOOP
         INSERT INTO config.composite_attr_entry_definition 
             (coded_value, definition) VALUES
@@ -6918,6 +6937,7 @@ BEGIN
     END LOOP; 
 END $$;
 
+
 -- Trigger Event Definitions -------------------------------------------------
 
 -- Sample Overdue Notice --
diff --git a/Open-ILS/src/sql/Pg/upgrade/QQQQ.MVF_CRA-upgrade.sql b/Open-ILS/src/sql/Pg/upgrade/QQQQ.MVF_CRA-upgrade.sql
index dd84a49..b0d1770 100644
--- a/Open-ILS/src/sql/Pg/upgrade/QQQQ.MVF_CRA-upgrade.sql
+++ b/Open-ILS/src/sql/Pg/upgrade/QQQQ.MVF_CRA-upgrade.sql
@@ -785,7 +785,7 @@ INSERT INTO config.record_attr_definition
 INSERT INTO config.composite_attr_entry_definition 
     (coded_value, definition) VALUES
 --book
-(564, '{"0":[{"_attr":"item_type","_val":"a"},{"_attr":"item_type","_val":"t"}],"1":{"_not":[{"_attr":"item_form","_val":"a"},{"_attr":"item_form","_val":"b"},{"_attr":"item_form","_val":"c"},{"_attr":"item_form","_val":"f"},{"_attr":"item_form","_val":"o"},{"_attr":"item_form","_val":"q"},{"_attr":"item_form","_val":"r"},{"_attr":"item_form","_val":"s"}]},"2":[{"_attr":"bib_level","_val":"a"},{"_attr":"bib_level","_val":"c"},{"_attr":"bib_level","_val":"d"},{"_attr":"bib_level","_val":"m"}]}'),
+(564, '{"0":[{"_attr":"item_type","_val":"a"},{"_attr":"item_type","_val":"t"}],"1":{"_not":[{"_attr":"item_form","_val":"a"},{"_attr":"item_form","_val":"b"},{"_attr":"item_form","_val":"c"},{"_attr":"item_form","_val":"d"},{"_attr":"item_form","_val":"f"},{"_attr":"item_form","_val":"o"},{"_attr":"item_form","_val":"q"},{"_attr":"item_form","_val":"r"},{"_attr":"item_form","_val":"s"}]},"2":[{"_attr":"bib_level","_val":"a"},{"_attr":"bib_level","_val":"c"},{"_attr":"bib_level","_val":"d"},{"_attr":"bib_level","_val":"m"}]}'),
 
 -- braille
 (565, '{"0":{"_attr":"item_type","_val":"a"},"1":{"_attr":"item_form","_val":"f"}}'),
diff --git a/Open-ILS/src/sql/Pg/upgrade/ZZZZZ.data.mmr-holds-formats.sql b/Open-ILS/src/sql/Pg/upgrade/ZZZZZ.data.mmr-holds-formats.sql
index 52b6cc9..5c11a87 100644
--- a/Open-ILS/src/sql/Pg/upgrade/ZZZZZ.data.mmr-holds-formats.sql
+++ b/Open-ILS/src/sql/Pg/upgrade/ZZZZZ.data.mmr-holds-formats.sql
@@ -137,6 +137,23 @@ INSERT INTO config.composite_attr_entry_definition
     (coded_value, definition) VALUES
 (607, '{"0":{"_attr":"item_type","_val":"j"},"1":{"_not":[{"_attr":"sr_format","_val":"a"},{"_attr":"sr_format","_val":"b"},{"_attr":"sr_format","_val":"c"},{"_attr":"sr_format","_val":"d"},{"_attr":"sr_format","_val":"f"},{"_attr":"sr_format","_val":"e"},{"_attr":"sr_format","_val":"l"}]}}');
 
+-- icon for blu-ray
+INSERT INTO config.coded_value_map
+    (id, ctype, code, value, search_label) VALUES 
+(608, 'icon_format', 'blu-ray', 
+    oils_i18n_gettext(608, 'Blu-ray', 'ccvm', 'value'),
+    oils_i18n_gettext(608, 'Blu-ray', 'ccvm', 'search_label'));
+INSERT INTO config.composite_attr_entry_definition 
+    (coded_value, definition) VALUES (608, '{"_attr":"vr_format","_val":"s"}');
+
+-- metarecord hold format for blu-ray
+INSERT INTO config.coded_value_map
+    (id, ctype, code, value, search_label) VALUES 
+(609, 'mr_hold_format', 'blu-ray', 
+    oils_i18n_gettext(609, 'Blu-ray', 'ccvm', 'value'),
+    oils_i18n_gettext(609, 'Blu-ray', 'ccvm', 'search_label'));
+INSERT INTO config.composite_attr_entry_definition 
+    (coded_value, definition) VALUES (609, '{"_attr":"vr_format","_val":"s"}');
 
 COMMIT;
 

commit 0a6b913e0cbc025626bcf12d98e04f9d49448c9c
Author: Bill Erickson <berick at esilibrary.com>
Date:   Fri Feb 21 10:19:00 2014 -0500

    LP#1053397 MR list return to grouped results
    
    Show a "Return to Grouped Search Results" link from the metarecord
    constituent records page.
    
    Signed-off-by: Bill Erickson <berick at esilibrary.com>
    Signed-off-by: Mike Rylander <mrylander at gmail.com>
    Signed-off-by: Dan Wells <dbw2 at calvin.edu>

diff --git a/Open-ILS/src/templates/opac/results.tt2 b/Open-ILS/src/templates/opac/results.tt2
index 6bd1c94..a99ac0d 100644
--- a/Open-ILS/src/templates/opac/results.tt2
+++ b/Open-ILS/src/templates/opac/results.tt2
@@ -32,6 +32,14 @@
             [% l('Viewing Results for Grouped Record: [_1]', 
                 mr_attrs.title) | html %]
           </div>
+          [% IF CGI.param('query') %]
+          <div>
+            <a href="[% mkurl(ctx.opac_root _ '/results', {}, ['metarecord']); %]">
+              [% l('&#9668; Return to Grouped Search Results') %]
+            </a>
+          </div>
+          <br/>
+          [% END %]
         [% END %]
         <div class="results_header_bar[%- IF ctx.metarecord %] hidden[% END -%]">
             <div id="results_header_inner">

commit 3fd258fda0af6cf5cc53c7a87974027d694d6add
Author: Bill Erickson <berick at esilibrary.com>
Date:   Thu Feb 20 15:05:11 2014 -0500

    LP#1053397 staff client MR results paging repair
    
    When selecting a record from the list of metarecord constituents, the
    paging controls (start/previous/next/end) within the staff client should
    page through the constiuents, not the original search.
    
    Signed-off-by: Bill Erickson <berick at esilibrary.com>
    Signed-off-by: Mike Rylander <mrylander at gmail.com>
    Signed-off-by: Dan Wells <dbw2 at calvin.edu>

diff --git a/Open-ILS/src/perlmods/lib/OpenILS/WWW/EGCatLoader/Search.pm b/Open-ILS/src/perlmods/lib/OpenILS/WWW/EGCatLoader/Search.pm
index b2098bd..57e0bfb 100644
--- a/Open-ILS/src/perlmods/lib/OpenILS/WWW/EGCatLoader/Search.pm
+++ b/Open-ILS/src/perlmods/lib/OpenILS/WWW/EGCatLoader/Search.pm
@@ -383,7 +383,7 @@ sub load_rresults {
         }
     }
 
-    if ($metarecord and !$internal) {
+    if ($metarecord) {
 
         # TODO: other limits, like SVF/format, etc.
         $self->timelog("Getting metarecords to records");

commit 4c8fc7fbb8733e27d480b838df2e86f1f1cc7ce7
Author: Bill Erickson <berick at esilibrary.com>
Date:   Thu Feb 20 14:48:53 2014 -0500

    LP#1053397 MR holds displays selected formats
    
    In a patron's holds list, only show formats in the format column
    matching the patron's selected hold formats.
    
    Signed-off-by: Bill Erickson <berick at esilibrary.com>
    Signed-off-by: Mike Rylander <mrylander at gmail.com>
    Signed-off-by: Dan Wells <dbw2 at calvin.edu>

diff --git a/Open-ILS/src/templates/opac/myopac/holds.tt2 b/Open-ILS/src/templates/opac/myopac/holds.tt2
index 0a7b04a..b08d230 100644
--- a/Open-ILS/src/templates/opac/myopac/holds.tt2
+++ b/Open-ILS/src/templates/opac/myopac/holds.tt2
@@ -150,10 +150,22 @@
                     </td>
                     <td>
                         <div class="format_icon">
-                            [% IF attrs.all_formats.size %]
-                                [% FOR format IN attrs.all_formats %]
-                                <img title="[% format.label | html %]" alt="[% format.label | html %]" src="[% format.icon %]" />
-                                [% END %]
+                          [% 
+                            formats = attrs.all_formats.size;
+                            IF ahr.hold_type == 'M';
+                              # only show selected formats for metarecords
+                              formats = [];
+                              FOR ccvm IN hold.metarecord_selected_filters.icons;
+                                format = {};
+                                format.icon = PROCESS get_ccvm_icon id=ccvm.id search_label=1;
+                                format.label = PROCESS get_ccvm_label id=ccvm.id search_label=1;
+                                formats.push(format);
+                              END;
+                            END;
+                            FOR format IN formats 
+                          %]
+                            <img title="[% format.label | html %]" 
+                              alt="[% format.label | html %]" src="[% format.icon %]" />
                             [% END %]
                         </div>
                     </td>
diff --git a/Open-ILS/src/templates/opac/parts/misc_util.tt2 b/Open-ILS/src/templates/opac/parts/misc_util.tt2
index 589a2c1..c003c15 100644
--- a/Open-ILS/src/templates/opac/parts/misc_util.tt2
+++ b/Open-ILS/src/templates/opac/parts/misc_util.tt2
@@ -88,6 +88,13 @@
         END;
     END;
 
+    BLOCK get_ccvm_icon;
+        ccvm = ctx.get_ccvm(id); # caches internally
+        label = PROCESS get_ccvm_label id=id search_label=search_label;
+        ctx.media_prefix _ '/images/format_icons/' _ ccvm.ctype _ '/' _ ccvm.code _ '.png';
+    END;
+
+
     # Extract MARC fields from XML
     #   get_marc_attrs( { marc_xml => doc } )
     BLOCK get_marc_attrs;
@@ -440,16 +447,20 @@
         FOR node IN xml.findnodes(formats_xpath);
             IF node AND node.textContent;
                 type = node.textContent;
-                label = PROCESS get_ccvm_label id=node.getAttribute('cvmid') search_label=1;
-                itemtype = schema_typemap.$type || 'CreativeWork';
-                icon = ctx.media_prefix _ '/images/format_icons/' _ icon_style _ '/' _ type _ '.png';
-                # collect all formats for metarecord support
-                args.all_formats.push({label => label, icon => icon, itemtype => itemtype});
+                format = {};
+                format.icon = PROCESS get_ccvm_icon 
+                    id=node.getAttribute('cvmid') search_label=1;
+                format.label = PROCESS get_ccvm_label 
+                    id=node.getAttribute('cvmid') search_label=1;
+                format.itemtype = schema_typemap.$type || 'CreativeWork';
+
+                args.all_formats.push(format); # metarecords want all formats
+
                 IF !args.format_label;
                     # use the first format as the default
-                    args.format_label = label; 
-                    args.schema.itemtype = itemtype;
-                    args.format_icon = icon;
+                    args.format_label = format.label; 
+                    args.schema.itemtype = format.itemtype;
+                    args.format_icon = format.icon;
                 END;
             END;
         END;

commit f5bc354c5d6dffd1a89514bd159b2051bfb90271
Author: Mike Rylander <mrylander at gmail.com>
Date:   Wed Feb 19 17:11:29 2014 -0500

    LP#1053397: Use the new data structures for looking up language indexing configuration
    
    Also, from Dan Wells:
    
    First, we don't want to fully exclude records which don't have a
    metabib.record_attr_vector_list entry, so we do a LEFT JOIN instead
    for that table.  Otherwise, some records error out when ingesting
    (see placeholder record -1 in the stock data set).
    
    Second, let's add a check for 'active' which appears to have been
    always missing.
    
    Signed-off-by: Mike Rylander <mrylander at gmail.com>
    Signed-off-by: Dan Wells <dbw2 at calvin.edu>

diff --git a/Open-ILS/src/sql/Pg/030.schema.metabib.sql b/Open-ILS/src/sql/Pg/030.schema.metabib.sql
index ba9818b..554deb4 100644
--- a/Open-ILS/src/sql/Pg/030.schema.metabib.sql
+++ b/Open-ILS/src/sql/Pg/030.schema.metabib.sql
@@ -2054,31 +2054,52 @@ BEGIN
    END IF;
 
     IF TG_TABLE_NAME::TEXT ~ 'browse_entry$' THEN
+
         value :=  ARRAY_TO_STRING(
             evergreen.regexp_split_to_array(value, E'\\W+'), ' '
         );
         value := public.search_normalize(value);
         NEW.index_vector = to_tsvector(TG_ARGV[0]::regconfig, value);
+
     ELSIF TG_TABLE_NAME::TEXT ~ 'field_entry$' THEN
         FOR ts_rec IN
-            SELECT ts_config, index_weight
-            FROM config.metabib_class_ts_map
-            WHERE field_class = TG_ARGV[0]
-                AND index_lang IS NULL OR EXISTS (SELECT 1 FROM metabib.record_attr WHERE id = NEW.source AND index_lang IN(attrs->'item_lang',attrs->'language'))
-                AND always OR NOT EXISTS (SELECT 1 FROM config.metabib_field_ts_map WHERE metabib_field = NEW.field)
-            UNION
-            SELECT ts_config, index_weight
-            FROM config.metabib_field_ts_map
-            WHERE metabib_field = NEW.field
-               AND index_lang IS NULL OR EXISTS (SELECT 1 FROM metabib.record_attr WHERE id = NEW.source AND index_lang IN(attrs->'item_lang',attrs->'language'))
+
+            SELECT DISTINCT m.ts_config, m.index_weight
+            FROM config.metabib_class_ts_map m
+                 LEFT JOIN metabib.record_attr_vector_list r ON (r.source = NEW.source)
+                 LEFT JOIN config.coded_value_map ccvm ON (
+                    ccvm.ctype IN ('item_lang', 'language') AND
+                    ccvm.code = m.index_lang AND
+                    r.vlist @> intset(ccvm.id)
+                )
+            WHERE m.field_class = TG_ARGV[0]
+                AND m.active
+                AND (m.always OR NOT EXISTS (SELECT 1 FROM config.metabib_field_ts_map WHERE metabib_field = NEW.field))
+                AND (m.index_lang IS NULL OR ccvm.id IS NOT NULL)
+                        UNION
+            SELECT DISTINCT m.ts_config, m.index_weight
+            FROM config.metabib_field_ts_map m
+                 LEFT JOIN metabib.record_attr_vector_list r ON (r.source = NEW.source)
+                 LEFT JOIN config.coded_value_map ccvm ON (
+                    ccvm.ctype IN ('item_lang', 'language') AND
+                    ccvm.code = m.index_lang AND
+                    r.vlist @> intset(ccvm.id)
+                )
+            WHERE m.metabib_field = NEW.field
+                AND m.active
+                AND (m.index_lang IS NULL OR ccvm.id IS NOT NULL)
             ORDER BY index_weight ASC
+
         LOOP
+
             IF cur_weight IS NOT NULL AND cur_weight != ts_rec.index_weight THEN
                 NEW.index_vector = NEW.index_vector || setweight(temp_vector::tsvector,cur_weight);
                 temp_vector = '';
             END IF;
+
             cur_weight = ts_rec.index_weight;
             SELECT INTO temp_vector temp_vector || ' ' || to_tsvector(ts_rec.ts_config::regconfig, value)::TEXT;
+
         END LOOP;
         NEW.index_vector = NEW.index_vector || setweight(temp_vector::tsvector,cur_weight);
     ELSE
diff --git a/Open-ILS/src/sql/Pg/upgrade/QQQQ.MVF_CRA-upgrade.sql b/Open-ILS/src/sql/Pg/upgrade/QQQQ.MVF_CRA-upgrade.sql
index 4148fa4..dd84a49 100644
--- a/Open-ILS/src/sql/Pg/upgrade/QQQQ.MVF_CRA-upgrade.sql
+++ b/Open-ILS/src/sql/Pg/upgrade/QQQQ.MVF_CRA-upgrade.sql
@@ -553,6 +553,117 @@ BEGIN
 END;
 $func$ LANGUAGE PLPGSQL;
 
+CREATE OR REPLACE FUNCTION public.oils_tsearch2 () RETURNS TRIGGER AS $$
+DECLARE
+    normalizer      RECORD;
+    value           TEXT := '';
+    temp_vector     TEXT := '';
+    ts_rec          RECORD;
+    cur_weight      "char";
+BEGIN
+
+    value := NEW.value;
+    NEW.index_vector = ''::tsvector;
+
+    IF TG_TABLE_NAME::TEXT ~ 'field_entry$' THEN
+        FOR normalizer IN
+            SELECT  n.func AS func,
+                    n.param_count AS param_count,
+                    m.params AS params
+              FROM  config.index_normalizer n
+                    JOIN config.metabib_field_index_norm_map m ON (m.norm = n.id)
+              WHERE field = NEW.field AND m.pos < 0
+              ORDER BY m.pos LOOP
+                EXECUTE 'SELECT ' || normalizer.func || '(' ||
+                    quote_literal( value ) ||
+                    CASE
+                        WHEN normalizer.param_count > 0
+                            THEN ',' || REPLACE(REPLACE(BTRIM(normalizer.params,'[]'),E'\'',E'\\\''),E'"',E'\'')
+                            ELSE ''
+                        END ||
+                    ')' INTO value;
+
+        END LOOP;
+
+        NEW.value = value;
+
+        FOR normalizer IN
+            SELECT  n.func AS func,
+                    n.param_count AS param_count,
+                    m.params AS params
+              FROM  config.index_normalizer n
+                    JOIN config.metabib_field_index_norm_map m ON (m.norm = n.id)
+              WHERE field = NEW.field AND m.pos >= 0
+              ORDER BY m.pos LOOP
+                EXECUTE 'SELECT ' || normalizer.func || '(' ||
+                    quote_literal( value ) ||
+                    CASE
+                        WHEN normalizer.param_count > 0
+                            THEN ',' || REPLACE(REPLACE(BTRIM(normalizer.params,'[]'),E'\'',E'\\\''),E'"',E'\'')
+                            ELSE ''
+                        END ||
+                    ')' INTO value;
+
+        END LOOP;
+   END IF;
+
+    IF TG_TABLE_NAME::TEXT ~ 'browse_entry$' THEN
+
+        value :=  ARRAY_TO_STRING(
+            evergreen.regexp_split_to_array(value, E'\\W+'), ' '
+        );
+        value := public.search_normalize(value);
+        NEW.index_vector = to_tsvector(TG_ARGV[0]::regconfig, value);
+
+    ELSIF TG_TABLE_NAME::TEXT ~ 'field_entry$' THEN
+        FOR ts_rec IN
+
+            SELECT DISTINCT m.ts_config, m.index_weight
+            FROM config.metabib_class_ts_map m
+                 LEFT JOIN metabib.record_attr_vector_list r ON (r.source = NEW.source)
+                 LEFT JOIN config.coded_value_map ccvm ON (
+                    ccvm.ctype IN ('item_lang', 'language') AND
+                    ccvm.code = m.index_lang AND
+                    r.vlist @> intset(ccvm.id)
+                )
+            WHERE m.field_class = TG_ARGV[0]
+                AND m.active
+                AND (m.always OR NOT EXISTS (SELECT 1 FROM config.metabib_field_ts_map WHERE metabib_field = NEW.field))
+                AND (m.index_lang IS NULL OR ccvm.id IS NOT NULL)
+                        UNION
+            SELECT DISTINCT m.ts_config, m.index_weight
+            FROM config.metabib_field_ts_map m
+                 LEFT JOIN metabib.record_attr_vector_list r ON (r.source = NEW.source)
+                 LEFT JOIN config.coded_value_map ccvm ON (
+                    ccvm.ctype IN ('item_lang', 'language') AND
+                    ccvm.code = m.index_lang AND
+                    r.vlist @> intset(ccvm.id)
+                )
+            WHERE m.metabib_field = NEW.field
+                AND m.active
+                AND (m.index_lang IS NULL OR ccvm.id IS NOT NULL)
+            ORDER BY index_weight ASC
+
+        LOOP
+
+            IF cur_weight IS NOT NULL AND cur_weight != ts_rec.index_weight THEN
+                NEW.index_vector = NEW.index_vector || setweight(temp_vector::tsvector,cur_weight);
+                temp_vector = '';
+            END IF;
+
+            cur_weight = ts_rec.index_weight;
+            SELECT INTO temp_vector temp_vector || ' ' || to_tsvector(ts_rec.ts_config::regconfig, value)::TEXT;
+
+        END LOOP;
+        NEW.index_vector = NEW.index_vector || setweight(temp_vector::tsvector,cur_weight);
+    ELSE
+        NEW.index_vector = to_tsvector(TG_ARGV[0]::regconfig, value);
+    END IF;
+
+    RETURN NEW;
+END;
+$$ LANGUAGE PLPGSQL;
+
 -- add new sr_format attribute definition
 
 INSERT INTO config.record_attr_definition (name, label, phys_char_sf)

commit 3dfd7055bb024a385165ceca9e88b49014a8e515
Author: Mike Rylander <mrylander at gmail.com>
Date:   Wed Feb 19 13:06:32 2014 -0500

    LP#1053397: Correct operator thinko on CRA sorters
    
    Signed-off-by: Mike Rylander <mrylander at gmail.com>
    Signed-off-by: Dan Wells <dbw2 at calvin.edu>

diff --git a/Open-ILS/src/sql/Pg/030.schema.metabib.sql b/Open-ILS/src/sql/Pg/030.schema.metabib.sql
index e5c9673..ba9818b 100644
--- a/Open-ILS/src/sql/Pg/030.schema.metabib.sql
+++ b/Open-ILS/src/sql/Pg/030.schema.metabib.sql
@@ -1613,7 +1613,7 @@ BEGIN
             END IF;
 
             IF attr_def.sorter THEN
-                IF attr_vector ~~ tmp_val THEN
+                IF attr_vector @@ tmp_val THEN
                     DELETE FROM metabib.record_sorter WHERE source = rid AND attr = attr_def.name;
                     INSERT INTO metabib.record_sorter (source, attr, value) VALUES (rid, attr_def.name, ccvm_row.code);
                 END IF;
diff --git a/Open-ILS/src/sql/Pg/upgrade/QQQQ.MVF_CRA-upgrade.sql b/Open-ILS/src/sql/Pg/upgrade/QQQQ.MVF_CRA-upgrade.sql
index c04d8f4..4148fa4 100644
--- a/Open-ILS/src/sql/Pg/upgrade/QQQQ.MVF_CRA-upgrade.sql
+++ b/Open-ILS/src/sql/Pg/upgrade/QQQQ.MVF_CRA-upgrade.sql
@@ -450,7 +450,7 @@ BEGIN
             END IF;
 
             IF attr_def.sorter THEN
-                IF attr_vector ~~ tmp_val THEN
+                IF attr_vector @@ tmp_val THEN
                     DELETE FROM metabib.record_sorter WHERE source = rid AND attr = attr_def.name;
                     INSERT INTO metabib.record_sorter (source, attr, value) VALUES (rid, attr_def.name, ccvm_row.code);
                 END IF;

commit f2e96e0ebf3f0f75e0d770ccc755406d5df84c09
Author: Mike Rylander <mrylander at gmail.com>
Date:   Tue Feb 18 16:53:53 2014 -0500

    LP1053397: Don't leak memory; Cache compiled ccraed values
    
    We were leaking memory in a PLPGSQL function that compiles the
    query_int from a ccraed value.  Stop doing that!
    
    Also, implement a self-invalidating (upon table update) cache
    for compiled composite attr defs.  This brings concerto reingest
    down from 50+ seconds to under 10.
    
    Signed-off-by: Mike Rylander <mrylander at gmail.com>
    Signed-off-by: Dan Wells <dbw2 at calvin.edu>

diff --git a/Open-ILS/src/sql/Pg/030.schema.metabib.sql b/Open-ILS/src/sql/Pg/030.schema.metabib.sql
index 7eb7dd9..e5c9673 100644
--- a/Open-ILS/src/sql/Pg/030.schema.metabib.sql
+++ b/Open-ILS/src/sql/Pg/030.schema.metabib.sql
@@ -296,13 +296,44 @@ CREATE VIEW metabib.full_attr_id_map AS
     SELECT id, attr, value FROM metabib.composite_attr_id_map;
 
 
+CREATE OR REPLACE FUNCTION metabib.compile_composite_attr_cache_init () RETURNS BOOL AS $f$
+    $_SHARED{metabib_compile_composite_attr_cache} = {}
+        if ! exists $_SHARED{metabib_compile_composite_attr_cache};
+    return exists $_SHARED{metabib_compile_composite_attr_cache};
+$f$ LANGUAGE PLPERLU;
+
+CREATE OR REPLACE FUNCTION metabib.compile_composite_attr_cache_disable () RETURNS BOOL AS $f$
+    delete $_SHARED{metabib_compile_composite_attr_cache};
+    return ! exists $_SHARED{metabib_compile_composite_attr_cache};
+$f$ LANGUAGE PLPERLU;
+
+CREATE OR REPLACE FUNCTION metabib.compile_composite_attr_cache_invalidate () RETURNS BOOL AS $f$
+    SELECT metabib.compile_composite_attr_cache_disable() AND metabib.compile_composite_attr_cache_init();
+$f$ LANGUAGE SQL;
+
+CREATE OR REPLACE FUNCTION metabib.composite_attr_def_cache_inval_tgr () RETURNS TRIGGER AS $f$
+BEGIN
+    PERFORM metabib.compile_composite_attr_cache_invalidate();
+    RETURN NULL;
+END;
+$f$ LANGUAGE PLPGSQL;
+
+CREATE TRIGGER ccraed_cache_inval_tgr AFTER INSERT OR UPDATE OR DELETE ON config.composite_attr_entry_definition FOR EACH STATEMENT EXECUTE PROCEDURE metabib.composite_attr_def_cache_inval_tgr();
+    
 CREATE OR REPLACE FUNCTION metabib.compile_composite_attr ( cattr_def TEXT ) RETURNS query_int AS $func$
 
     use JSON::XS;
-    my $def = decode_json(shift);
+
+    my $json = shift;
+    my $def = decode_json($json);
 
     die("Composite attribute definition not supplied") unless $def;
 
+    my $_cache = (exists $_SHARED{metabib_compile_composite_attr_cache}) ? 1 : 0;
+
+    return $_SHARED{metabib_compile_composite_attr_cache}{$json}
+        if ($_cache && $_SHARED{metabib_compile_composite_attr_cache}{$json});
+
     sub recurse {
         my $d = shift;
         my $j = '&';
@@ -311,10 +342,11 @@ CREATE OR REPLACE FUNCTION metabib.compile_composite_attr ( cattr_def TEXT ) RET
         if (ref $d eq 'HASH') { # node or AND
             if (exists $d->{_attr}) { # it is a node
                 my $plan = spi_prepare('SELECT * FROM metabib.full_attr_id_map WHERE attr = $1 AND value = $2', qw/TEXT TEXT/);
-                return spi_exec_prepared(
+                my $id = spi_exec_prepared(
                     $plan, {limit => 1}, $d->{_attr}, $d->{_val}
                 )->{rows}[0]{id};
                 spi_freeplan($plan);
+                return $id;
             } elsif (exists $d->{_not} && scalar(keys(%$d)) == 1) { # it is a NOT
                 return '!' . recurse($$d{_not});
             } else { # an AND list
@@ -331,7 +363,9 @@ CREATE OR REPLACE FUNCTION metabib.compile_composite_attr ( cattr_def TEXT ) RET
         return '';
     }
 
-    return recurse($def) || undef;
+    my $val = recurse($def) || undef;
+    $_SHARED{metabib_compile_composite_attr_cache}{$json} = $val if $_cache;
+    return $val;
 
 $func$ IMMUTABLE LANGUAGE plperlu;
 
@@ -1563,6 +1597,7 @@ BEGIN
 
     -- On to composite attributes, now that the record attrs have been pulled.  Processed in name order, so later composite
     -- attributes can depend on earlier ones.
+    PERFORM metabib.compile_composite_attr_cache_init();
     FOR attr_def IN SELECT * FROM config.record_attr_definition WHERE composite AND name = ANY( attr_list ) ORDER BY name LOOP
 
         FOR ccvm_row IN SELECT * FROM config.coded_value_map c WHERE c.ctype = attr_def.name ORDER BY value LOOP
diff --git a/Open-ILS/src/sql/Pg/upgrade/QQQQ.MVF_CRA-upgrade.sql b/Open-ILS/src/sql/Pg/upgrade/QQQQ.MVF_CRA-upgrade.sql
index 0fc6cd6..c04d8f4 100644
--- a/Open-ILS/src/sql/Pg/upgrade/QQQQ.MVF_CRA-upgrade.sql
+++ b/Open-ILS/src/sql/Pg/upgrade/QQQQ.MVF_CRA-upgrade.sql
@@ -185,13 +185,44 @@ CREATE VIEW metabib.rec_descriptor AS
             (populate_record(NULL::metabib.rec_desc_type, attrs)).*
       FROM  metabib.record_attr;
 
+CREATE OR REPLACE FUNCTION metabib.compile_composite_attr_cache_init () RETURNS BOOL AS $f$
+    $_SHARED{metabib_compile_composite_attr_cache} = {}
+        if ! exists $_SHARED{metabib_compile_composite_attr_cache};
+    return exists $_SHARED{metabib_compile_composite_attr_cache};
+$f$ LANGUAGE PLPERLU;
+
+CREATE OR REPLACE FUNCTION metabib.compile_composite_attr_cache_disable () RETURNS BOOL AS $f$
+    delete $_SHARED{metabib_compile_composite_attr_cache};
+    return ! exists $_SHARED{metabib_compile_composite_attr_cache};
+$f$ LANGUAGE PLPERLU;
+
+CREATE OR REPLACE FUNCTION metabib.compile_composite_attr_cache_invalidate () RETURNS BOOL AS $f$
+    SELECT metabib.compile_composite_attr_cache_disable() AND metabib.compile_composite_attr_cache_init();
+$f$ LANGUAGE SQL;
+
+CREATE OR REPLACE FUNCTION metabib.composite_attr_def_cache_inval_tgr () RETURNS TRIGGER AS $f$
+BEGIN
+    PERFORM metabib.compile_composite_attr_cache_invalidate();
+    RETURN NULL;
+END;
+$f$ LANGUAGE PLPGSQL;
+
+CREATE TRIGGER ccraed_cache_inval_tgr AFTER INSERT OR UPDATE OR DELETE ON config.composite_attr_entry_definition FOR EACH STATEMENT EXECUTE PROCEDURE metabib.composite_attr_def_cache_inval_tgr();
+    
 CREATE OR REPLACE FUNCTION metabib.compile_composite_attr ( cattr_def TEXT ) RETURNS query_int AS $func$
 
     use JSON::XS;
-    my $def = decode_json(shift);
+
+    my $json = shift;
+    my $def = decode_json($json);
 
     die("Composite attribute definition not supplied") unless $def;
 
+    my $_cache = (exists $_SHARED{metabib_compile_composite_attr_cache}) ? 1 : 0;
+
+    return $_SHARED{metabib_compile_composite_attr_cache}{$json}
+        if ($_cache && $_SHARED{metabib_compile_composite_attr_cache}{$json});
+
     sub recurse {
         my $d = shift;
         my $j = '&';
@@ -200,10 +231,11 @@ CREATE OR REPLACE FUNCTION metabib.compile_composite_attr ( cattr_def TEXT ) RET
         if (ref $d eq 'HASH') { # node or AND
             if (exists $d->{_attr}) { # it is a node
                 my $plan = spi_prepare('SELECT * FROM metabib.full_attr_id_map WHERE attr = $1 AND value = $2', qw/TEXT TEXT/);
-                return spi_exec_prepared(
+                my $id = spi_exec_prepared(
                     $plan, {limit => 1}, $d->{_attr}, $d->{_val}
                 )->{rows}[0]{id};
                 spi_freeplan($plan);
+                return $id;
             } elsif (exists $d->{_not} && scalar(keys(%$d)) == 1) { # it is a NOT
                 return '!' . recurse($$d{_not});
             } else { # an AND list
@@ -220,7 +252,9 @@ CREATE OR REPLACE FUNCTION metabib.compile_composite_attr ( cattr_def TEXT ) RET
         return '';
     }
 
-    return recurse($def) || undef;
+    my $val = recurse($def) || undef;
+    $_SHARED{metabib_compile_composite_attr_cache}{$json} = $val if $_cache;
+    return $val;
 
 $func$ IMMUTABLE LANGUAGE plperlu;
 
@@ -400,6 +434,7 @@ BEGIN
 
     -- On to composite attributes, now that the record attrs have been pulled.  Processed in name order, so later composite
     -- attributes can depend on earlier ones.
+    PERFORM metabib.compile_composite_attr_cache_init();
     FOR attr_def IN SELECT * FROM config.record_attr_definition WHERE composite AND name = ANY( attr_list ) ORDER BY name LOOP
 
         FOR ccvm_row IN SELECT * FROM config.coded_value_map c WHERE c.ctype = attr_def.name ORDER BY value LOOP

commit c2c8b2796d14b8375ddca631a74d7ed75b882a20
Author: Bill Erickson <berick at esilibrary.com>
Date:   Mon Feb 17 15:08:11 2014 -0500

    LP#1053397 catch-all 'music' icon_format attribute value
    
    Many records in the Evergreen sample data have item_type=j and no sound
    recording format.  This seems likely to happen in the wild, as well.
    The new coded value captures everying with item_type=j and no sr_format.
    
    Also included is a "music.png" icon, which looks like a pair of happy
    eigth notes.
    
    Signed-off-by: Bill Erickson <berick at esilibrary.com>
    Signed-off-by: Mike Rylander <mrylander at gmail.com>
    Signed-off-by: Dan Wells <dbw2 at calvin.edu>

diff --git a/Open-ILS/src/sql/Pg/950.data.seed-values.sql b/Open-ILS/src/sql/Pg/950.data.seed-values.sql
index 08e71ab..98e5617 100644
--- a/Open-ILS/src/sql/Pg/950.data.seed-values.sql
+++ b/Open-ILS/src/sql/Pg/950.data.seed-values.sql
@@ -6811,6 +6811,13 @@ INSERT INTO config.coded_value_map
     oils_i18n_gettext(606, 'Large Print Book', 'ccvm', 'value'),
     oils_i18n_gettext(606, 'Large Print Book', 'ccvm', 'search_label')) ;
 
+-- catch-all music of unkown format
+INSERT INTO config.coded_value_map
+    (id, ctype, code, value, search_label) VALUES 
+(607, 'icon_format', 'music', 
+    oils_i18n_gettext(607, 'Musical Sound Recording (Unknown Format)', 'ccvm', 'value'),
+    oils_i18n_gettext(607, 'Musical Sound Recording (Unknown Format)', 'ccvm', 'search_label'));
+
 -- carve out a slot of 10k IDs for stock CCVMs
 SELECT SETVAL('config.coded_value_map_id_seq'::TEXT, 10000);
 
@@ -6885,6 +6892,11 @@ INSERT INTO config.composite_attr_entry_definition
 -- lpbook
 (585, '{"0":[{"_attr":"item_type","_val":"a"},{"_attr":"item_type","_val":"t"}],"1":{"_attr":"item_form","_val":"d"},"2":[{"_attr":"bib_level","_val":"a"},{"_attr":"bib_level","_val":"c"},{"_attr":"bib_level","_val":"d"},{"_attr":"bib_level","_val":"m"}]}');
 
+-- music (catch-all)
+INSERT INTO config.composite_attr_entry_definition 
+    (coded_value, definition) VALUES
+(607, '{"0":{"_attr":"item_type","_val":"j"},"1":{"_not":[{"_attr":"sr_format","_val":"a"},{"_attr":"sr_format","_val":"b"},{"_attr":"sr_format","_val":"c"},{"_attr":"sr_format","_val":"d"},{"_attr":"sr_format","_val":"f"},{"_attr":"sr_format","_val":"e"},{"_attr":"sr_format","_val":"l"}]}}');
+
 -- use the definitions from the icon_format as the basis for the MR hold format definitions
 DO $$
     DECLARE format TEXT;
diff --git a/Open-ILS/src/sql/Pg/upgrade/ZZZZZ.data.mmr-holds-formats.sql b/Open-ILS/src/sql/Pg/upgrade/ZZZZZ.data.mmr-holds-formats.sql
index bab086e..52b6cc9 100644
--- a/Open-ILS/src/sql/Pg/upgrade/ZZZZZ.data.mmr-holds-formats.sql
+++ b/Open-ILS/src/sql/Pg/upgrade/ZZZZZ.data.mmr-holds-formats.sql
@@ -127,5 +127,16 @@ BEGIN
     END LOOP; 
 END $$;
 
+INSERT INTO config.coded_value_map
+    (id, ctype, code, value, search_label) VALUES 
+(607, 'icon_format', 'music', 
+    oils_i18n_gettext(607, 'Musical Sound Recording (Unknown Format)', 'ccvm', 'value'),
+    oils_i18n_gettext(607, 'Musical Sound Recording (Unknown Format)', 'ccvm', 'search_label'));
+
+INSERT INTO config.composite_attr_entry_definition 
+    (coded_value, definition) VALUES
+(607, '{"0":{"_attr":"item_type","_val":"j"},"1":{"_not":[{"_attr":"sr_format","_val":"a"},{"_attr":"sr_format","_val":"b"},{"_attr":"sr_format","_val":"c"},{"_attr":"sr_format","_val":"d"},{"_attr":"sr_format","_val":"f"},{"_attr":"sr_format","_val":"e"},{"_attr":"sr_format","_val":"l"}]}}');
+
+
 COMMIT;
 
diff --git a/Open-ILS/web/images/format_icons/icon_format/music.png b/Open-ILS/web/images/format_icons/icon_format/music.png
new file mode 100644
index 0000000..132ca40
Binary files /dev/null and b/Open-ILS/web/images/format_icons/icon_format/music.png differ

commit d8e33af69f190503798823e979e9ea80307c50dd
Author: Mike Rylander <mrylander at gmail.com>
Date:   Mon Feb 17 14:03:02 2014 -0500

    LP#1269911: Cast bigint to int
    
    Signed-off-by: Mike Rylander <mrylander at gmail.com>
    Signed-off-by: Dan Wells <dbw2 at calvin.edu>

diff --git a/Open-ILS/src/sql/Pg/030.schema.metabib.sql b/Open-ILS/src/sql/Pg/030.schema.metabib.sql
index c42b2df..7eb7dd9 100644
--- a/Open-ILS/src/sql/Pg/030.schema.metabib.sql
+++ b/Open-ILS/src/sql/Pg/030.schema.metabib.sql
@@ -1557,7 +1557,7 @@ BEGIN
 
     IF ARRAY_LENGTH(pattr_list, 1) > 0 THEN 
         SELECT vlist INTO attr_vector_tmp FROM metabib.record_attr_vector_list WHERE source = rid;
-        SELECT attr_vector_tmp - ARRAY_AGG(id) INTO attr_vector_tmp FROM metabib.full_attr_id_map WHERE attr = ANY (pattr_list);
+        SELECT attr_vector_tmp - ARRAY_AGG(id::INT) INTO attr_vector_tmp FROM metabib.full_attr_id_map WHERE attr = ANY (pattr_list);
         attr_vector := attr_vector || attr_vector_tmp;
     END IF;
 
diff --git a/Open-ILS/src/sql/Pg/upgrade/QQQQ.MVF_CRA-upgrade.sql b/Open-ILS/src/sql/Pg/upgrade/QQQQ.MVF_CRA-upgrade.sql
index 5c1524e..0fc6cd6 100644
--- a/Open-ILS/src/sql/Pg/upgrade/QQQQ.MVF_CRA-upgrade.sql
+++ b/Open-ILS/src/sql/Pg/upgrade/QQQQ.MVF_CRA-upgrade.sql
@@ -394,7 +394,7 @@ BEGIN
 
         IF ARRAY_LENGTH(pattr_list, 1) > 0 THEN 
             SELECT vlist INTO attr_vector_tmp FROM metabib.record_attr_vector_list WHERE source = rid;
-            SELECT attr_vector_tmp - ARRAY_AGG(id) INTO attr_vector_tmp FROM metabib.full_attr_id_map WHERE attr = ANY (pattr_list);
+            SELECT attr_vector_tmp - ARRAY_AGG(id::INT) INTO attr_vector_tmp FROM metabib.full_attr_id_map WHERE attr = ANY (pattr_list);
             attr_vector := attr_vector || attr_vector_tmp;
         END IF;
 

commit 019f80a810c8d2e81e5602936b02d73b67a15d27
Author: Bill Erickson <berick at esilibrary.com>
Date:   Mon Feb 17 09:24:40 2014 -0500

    LP#1053397 filter MR icons/formats repair
    
    Only limit the scope of the MR attributes in the presence of a hard hold
    boundary when searching for which attributes to display in the hold
    placement form.
    
    In other words, don't limit the attributes based on search parameters,
    since searching at Branch 1 does not necesssarily meany you only want to
    place holds on formats available at branch 1.
    
    Signed-off-by: Bill Erickson <berick at esilibrary.com>
    Signed-off-by: Mike Rylander <mrylander at gmail.com>
    Signed-off-by: Dan Wells <dbw2 at calvin.edu>

diff --git a/Open-ILS/src/perlmods/lib/OpenILS/Application/Circ/Holds.pm b/Open-ILS/src/perlmods/lib/OpenILS/Application/Circ/Holds.pm
index 3eaec7a..ac3ade2 100644
--- a/Open-ILS/src/perlmods/lib/OpenILS/Application/Circ/Holds.pm
+++ b/Open-ILS/src/perlmods/lib/OpenILS/Application/Circ/Holds.pm
@@ -4246,10 +4246,15 @@ sub mr_hold_filter_attrs {
     my ($self, $client, $mr_id, $org_id, $hold_ids) = @_;
     my $e = new_editor();
 
-    # providing a context org means we filter out records that
-    # cannot possibly be held.
-    my $org_depth = $U->ou_ancestor_setting_value(
-        $org_id, OILS_SETTING_HOLD_HARD_BOUNDARY) if $org_id;
+    # by default, return MR / hold attributes for all constituent
+    # records with holdable copies.  If there is a hard boundary,
+    # though, limit to records with copies within the boundary,
+    # since anything outside the boundary can never be held.
+    my $org_depth = 0;
+    if ($org_id) {
+        $org_depth = $U->ou_ancestor_setting_value(
+            $org_id, OILS_SETTING_HOLD_HARD_BOUNDARY) || 0;
+    }
 
     # get all org-scoped records w/ holdable copies for this metarecord
     my ($bre_ids) = $self->method_lookup(

commit 6d328afcc86a28fdd9cf43294b4c630b02807a95
Author: Bill Erickson <berick at esilibrary.com>
Date:   Fri Feb 14 17:25:44 2014 -0500

    LP#1053397 filter MR icons/formats by org scope
    
    Signed-off-by: Bill Erickson <berick at esilibrary.com>
    Signed-off-by: Mike Rylander <mrylander at gmail.com>
    Signed-off-by: Dan Wells <dbw2 at calvin.edu>

diff --git a/Open-ILS/src/perlmods/lib/OpenILS/Application/Circ/Holds.pm b/Open-ILS/src/perlmods/lib/OpenILS/Application/Circ/Holds.pm
index 66b6391..3eaec7a 100644
--- a/Open-ILS/src/perlmods/lib/OpenILS/Application/Circ/Holds.pm
+++ b/Open-ILS/src/perlmods/lib/OpenILS/Application/Circ/Holds.pm
@@ -2478,7 +2478,7 @@ sub do_possibility_checks {
     } elsif( $hold_type eq OILS_HOLD_TYPE_METARECORD ) {
 
 
-        my ($recs) = __PACKAGE__->method_lookup('open-ils.circ.holds.metarecord.filterd_records')->run($mrid, $holdable_formats, $selection_ou, $depth);
+        my ($recs) = __PACKAGE__->method_lookup('open-ils.circ.holds.metarecord.filtered_records')->run($mrid, $holdable_formats, $selection_ou, $depth);
         my @status = ();
         for my $rec (@$recs) {
             @status = _check_title_hold_is_possible(
@@ -2504,7 +2504,7 @@ sub MR_filter_records {
 }
 __PACKAGE__->register_method(
     method   => 'MR_filter_records',
-    api_name => 'open-ils.circ.holds.metarecord.filterd_records',
+    api_name => 'open-ils.circ.holds.metarecord.filtered_records',
 );
 
 
@@ -4227,6 +4227,7 @@ __PACKAGE__->register_method(
         /,
         params => [
             {desc => 'Metarecord ID', type => 'number'},
+            {desc => 'Context Org ID', type => 'number'},
             {desc => 'Hold ID List', type => 'array'},
         ],
         return => {
@@ -4241,18 +4242,19 @@ __PACKAGE__->register_method(
     }
 );
 
-sub mr_hold_filter_attrs {
-    my ($self, $client, $mr_id, $hold_ids) = @_;
+sub mr_hold_filter_attrs { 
+    my ($self, $client, $mr_id, $org_id, $hold_ids) = @_;
     my $e = new_editor();
 
+    # providing a context org means we filter out records that
+    # cannot possibly be held.
+    my $org_depth = $U->ou_ancestor_setting_value(
+        $org_id, OILS_SETTING_HOLD_HARD_BOUNDARY) if $org_id;
 
-    my $mr = $e->retrieve_metabib_metarecord($mr_id) or return $e->event;
-    my $bre_ids = $e->json_query({
-        select => {mmrsm => ['source']},
-        from => 'mmrsm',
-        where => {'+mmrsm' => {metarecord => $mr_id}}
-    });
-    $bre_ids = [map {$_->{source}} @$bre_ids];
+    # get all org-scoped records w/ holdable copies for this metarecord
+    my ($bre_ids) = $self->method_lookup(
+        'open-ils.circ.holds.metarecord.filtered_records')->run(
+            $mr_id, undef, $org_id, $org_depth);
 
     my $item_lang_attr = 'item_lang'; # configurable?
     my $format_attr = $e->retrieve_config_global_flag(
@@ -4302,10 +4304,10 @@ sub mr_hold_filter_attrs {
             }
         };
 
-        # collect the ccvm's for the selected formats / language (
+        # collect the ccvm's for the selected formats / language
         # (i.e. the holdable formats) on the MR.
         # this assumes a two-key structure for format / language,
-        # though assumption is made about the keys themselves.
+        # though no assumption is made about the keys themselves.
         my $hformats = OpenSRF::Utils::JSON->JSON2perl($hold->holdable_formats);
         my $lang_vals = [];
         my $format_vals = [];
@@ -4329,8 +4331,9 @@ sub mr_hold_filter_attrs {
         # find all of the bib records within this metarcord whose 
         # format / language match the holdable formats on the hold
         my ($bre_ids) = $self->method_lookup(
-            'open-ils.circ.holds.metarecord.filterd_records')->run(
-                $hold->target, $hold->holdable_formats);
+            'open-ils.circ.holds.metarecord.filtered_records')->run(
+                $hold->target, $hold->holdable_formats, 
+                $hold->selection_ou, $hold->selection_depth);
 
         # now find all of the 'icon' attributes for the records
         $resp->{hold}{icons} = get_batch_ccvms($e, $icon_attr, $bre_ids);
diff --git a/Open-ILS/src/perlmods/lib/OpenILS/WWW/EGCatLoader/Account.pm b/Open-ILS/src/perlmods/lib/OpenILS/WWW/EGCatLoader/Account.pm
index 3961984..d992fda 100644
--- a/Open-ILS/src/perlmods/lib/OpenILS/WWW/EGCatLoader/Account.pm
+++ b/Open-ILS/src/perlmods/lib/OpenILS/WWW/EGCatLoader/Account.pm
@@ -586,7 +586,7 @@ sub fetch_user_holds {
                     my $filter_data = $U->simplereq(
                         'open-ils.circ',
                         'open-ils.circ.mmr.holds.filters.authoritative.atomic', 
-                        $hold->target, [$hold->id]
+                        $hold->target, $hold->selection_ou, [$hold->id]
                     );
 
                     $blob->{metarecord_filters} = 
@@ -818,9 +818,10 @@ sub load_place_hold {
             for my $id (@targets) {
                 my ($mr) = grep {$_->id eq $id} @$mrecs;
 
+                my $ou_id = $cgi->param('pickup_lib') || $self->ctx->{search_ou};
                 my $filter_data = $U->simplereq(
                     'open-ils.circ',
-                    'open-ils.circ.mmr.holds.filters.authoritative', $mr->id);
+                    'open-ils.circ.mmr.holds.filters.authoritative', $mr->id, $ou_id);
 
                 my $holdable_formats = 
                     $self->compile_holdable_formats($mr->id);

commit 16e8e0e59e913c551a429d1fb3f2dc1b126e2099
Author: Mike Rylander <mrylander at gmail.com>
Date:   Fri Feb 14 16:06:59 2014 -0500

    LP#1053397: Add optional org filter to "has holdable copies", and use that in open-ils.circ.holds.metarecord.filterd_records
    
    Signed-off-by: Mike Rylander <mrylander at gmail.com>
    Signed-off-by: Bill Erickson <berick at esilibrary.com>
    Signed-off-by: Dan Wells <dbw2 at calvin.edu>

diff --git a/Open-ILS/src/perlmods/lib/OpenILS/Application/Circ/Holds.pm b/Open-ILS/src/perlmods/lib/OpenILS/Application/Circ/Holds.pm
index 4f56877..66b6391 100644
--- a/Open-ILS/src/perlmods/lib/OpenILS/Application/Circ/Holds.pm
+++ b/Open-ILS/src/perlmods/lib/OpenILS/Application/Circ/Holds.pm
@@ -2477,7 +2477,8 @@ sub do_possibility_checks {
 
     } elsif( $hold_type eq OILS_HOLD_TYPE_METARECORD ) {
 
-        my ($recs) = __PACKAGE__->method_lookup('open-ils.circ.holds.metarecord.filterd_records')->run($mrid, $holdable_formats);
+
+        my ($recs) = __PACKAGE__->method_lookup('open-ils.circ.holds.metarecord.filterd_records')->run($mrid, $holdable_formats, $selection_ou, $depth);
         my @status = ();
         for my $rec (@$recs) {
             @status = _check_title_hold_is_possible(
@@ -2491,7 +2492,15 @@ sub do_possibility_checks {
 }
 
 sub MR_filter_records {
-    return $U->storagereq('open-ils.storage.metarecord.filtered_records.atomic', $_[2], $_[3]);
+    my $self = shift;
+    my $client = shift;
+    my $m = shift;
+    my $f = shift;
+    my $o = shift;
+    my $d = shift;
+    
+    my $org_at_depth = defined($d) ? $U->org_unit_ancestor_at_depth($o, $d) : $o;
+    return $U->storagereq('open-ils.storage.metarecord.filtered_records.atomic', $m, $f, $org_at_depth);
 }
 __PACKAGE__->register_method(
     method   => 'MR_filter_records',
diff --git a/Open-ILS/src/perlmods/lib/OpenILS/Application/Storage/Publisher/action.pm b/Open-ILS/src/perlmods/lib/OpenILS/Application/Storage/Publisher/action.pm
index 2f98fa5..fe1448d 100644
--- a/Open-ILS/src/perlmods/lib/OpenILS/Application/Storage/Publisher/action.pm
+++ b/Open-ILS/src/perlmods/lib/OpenILS/Application/Storage/Publisher/action.pm
@@ -1288,6 +1288,7 @@ sub MR_records_matching_format {
     my $client = shift;
     my $MR = shift;
     my $filter = shift;
+    my $org = shift;
 
     # find filters for MR holds
     my $mr_filter;
@@ -1301,19 +1302,19 @@ sub MR_records_matching_format {
 
     my $records = [metabib::metarecord->retrieve($MR)->source_records];
 
+    my $q = 'SELECT source FROM metabib.record_attr_vector_list WHERE source = ? AND vlist @@ ? AND asset.record_has_holdable_copy(?,?)';
+    my @args = ( $mr_filter, $org );
     if (!$mr_filter) {
-        $client->respond( $_->id ) for @$records;
-    } else {
-        for my $r ( map { isTrue($_->deleted) ?  () : ($_->id) } @$records ) {
-            $client->respond($r) if
-                @{action::hold_request->db_Main->selectcol_arrayref(
-                    'SELECT source FROM metabib.record_attr_vector_list WHERE source = ? AND vlist @@ ?',
-                    {},
-                    $r,
-                    $mr_filter
-                )};
-        }
+        $q = 'SELECT true WHERE asset.record_has_holdable_copy(?,?)';
+        @args = ( $org );
     }
+
+    for my $r ( map { isTrue($_->deleted) ?  () : ($_->id) } @$records ) {
+        # the map{} below is tricky. it puts the record ID in front of each param. see $q above
+        $client->respond($r)
+            if @{action::hold_request->db_Main->selectcol_arrayref( $q, {}, map { ( $r => $_ ) } @args )};
+    }
+
     return; # discard final l-val
 }
 __PACKAGE__->register_method(
diff --git a/Open-ILS/src/sql/Pg/040.schema.asset.sql b/Open-ILS/src/sql/Pg/040.schema.asset.sql
index 20dbc95..dfe75bf 100644
--- a/Open-ILS/src/sql/Pg/040.schema.asset.sql
+++ b/Open-ILS/src/sql/Pg/040.schema.asset.sql
@@ -656,7 +656,7 @@ BEGIN
 END;
 $f$ LANGUAGE PLPGSQL;
 
-CREATE OR REPLACE FUNCTION asset.record_has_holdable_copy ( rid BIGINT ) RETURNS BOOL AS $f$
+CREATE OR REPLACE FUNCTION asset.record_has_holdable_copy ( rid BIGINT, ou INT DEFAULT NULL) RETURNS BOOL AS $f$
 BEGIN
     PERFORM 1
         FROM
@@ -670,6 +670,7 @@ BEGIN
             AND acpl.holdable = true
             AND ccs.holdable = true
             AND acp.deleted = false
+            AND acp.circ_lib IN (SELECT id FROM actor.org_unit_descendants(COALESCE($2,(SELECT id FROM evergreen.org_top()))))
         LIMIT 1;
     IF FOUND THEN
         RETURN true;
@@ -826,7 +827,7 @@ BEGIN
 END;
 $f$ LANGUAGE PLPGSQL;
 
-CREATE OR REPLACE FUNCTION asset.metarecord_has_holdable_copy ( rid BIGINT ) RETURNS BOOL AS $f$
+CREATE OR REPLACE FUNCTION asset.metarecord_has_holdable_copy ( rid BIGINT, ou INT DEFAULT NULL) RETURNS BOOL AS $f$
 BEGIN
     PERFORM 1
         FROM
@@ -841,6 +842,7 @@ BEGIN
             AND acpl.holdable = true
             AND ccs.holdable = true
             AND acp.deleted = false
+            AND acp.circ_lib IN (SELECT id FROM actor.org_unit_descendants(COALESCE($2,(SELECT id FROM evergreen.org_top()))))
         LIMIT 1;
     IF FOUND THEN
         RETURN true;
diff --git a/Open-ILS/src/sql/Pg/upgrade/ZZZZ.schema.unapi-mmr.sql b/Open-ILS/src/sql/Pg/upgrade/ZZZZ.schema.unapi-mmr.sql
index 89c938d..20889b1 100644
--- a/Open-ILS/src/sql/Pg/upgrade/ZZZZ.schema.unapi-mmr.sql
+++ b/Open-ILS/src/sql/Pg/upgrade/ZZZZ.schema.unapi-mmr.sql
@@ -1,5 +1,54 @@
 BEGIN;
 
+DROP FUNCTION asset.record_has_holdable_copy (BIGINT);
+CREATE FUNCTION asset.record_has_holdable_copy ( rid BIGINT, ou INT DEFAULT NULL) RETURNS BOOL AS $f$
+BEGIN
+    PERFORM 1
+        FROM
+            asset.copy acp
+            JOIN asset.call_number acn ON acp.call_number = acn.id
+            JOIN asset.copy_location acpl ON acp.location = acpl.id
+            JOIN config.copy_status ccs ON acp.status = ccs.id
+        WHERE
+            acn.record = rid
+            AND acp.holdable = true
+            AND acpl.holdable = true
+            AND ccs.holdable = true
+            AND acp.deleted = false
+            AND acp.circ_lib IN (SELECT id FROM actor.org_unit_descendants(COALESCE($2,(SELECT id FROM evergreen.org_top()))))
+        LIMIT 1;
+    IF FOUND THEN
+        RETURN true;
+    END IF;
+    RETURN FALSE;
+END;
+$f$ LANGUAGE PLPGSQL;
+
+DROP FUNCTION asset.metarecord_has_holdable_copy (BIGINT);
+CREATE FUNCTION asset.metarecord_has_holdable_copy ( rid BIGINT, ou INT DEFAULT NULL) RETURNS BOOL AS $f$
+BEGIN
+    PERFORM 1
+        FROM
+            asset.copy acp
+            JOIN asset.call_number acn ON acp.call_number = acn.id
+            JOIN asset.copy_location acpl ON acp.location = acpl.id
+            JOIN config.copy_status ccs ON acp.status = ccs.id
+            JOIN metabib.metarecord_source_map mmsm ON acn.record = mmsm.source
+        WHERE
+            mmsm.metarecord = rid
+            AND acp.holdable = true
+            AND acpl.holdable = true
+            AND ccs.holdable = true
+            AND acp.deleted = false
+            AND acp.circ_lib IN (SELECT id FROM actor.org_unit_descendants(COALESCE($2,(SELECT id FROM evergreen.org_top()))))
+        LIMIT 1;
+    IF FOUND THEN
+        RETURN true;
+    END IF;
+    RETURN FALSE;
+END;
+$f$ LANGUAGE PLPGSQL;
+
 CREATE OR REPLACE FUNCTION asset.opac_ou_metarecord_copy_count (org INT, rid BIGINT) RETURNS TABLE (depth INT, org_unit INT, visible BIGINT, available BIGINT, unshadow BIGINT, transcendant INT) AS $f$
 DECLARE
     ans RECORD;

commit b0ff3c9e3e839dd16b4260763817c4a9078c8339
Author: Bill Erickson <berick at esilibrary.com>
Date:   Fri Feb 14 14:34:02 2014 -0500

    LP#1053397 apply org scope to metarecord attributes
    
    When loading record attributes (cra, ccvm, etc.) for constituent
    records, limit the attributes to those contained by records which have
    holdings within the provided (search) scope.
    
    With this, for example, a search at Branch 1 for metarecords will only
    show format icons for constituent records that have holdings at BR1.
    
    Signed-off-by: Bill Erickson <berick at esilibrary.com>
    Signed-off-by: Mike Rylander <mrylander at gmail.com>
    Signed-off-by: Dan Wells <dbw2 at calvin.edu>

diff --git a/Open-ILS/src/sql/Pg/990.schema.unapi.sql b/Open-ILS/src/sql/Pg/990.schema.unapi.sql
index 0b10b39..34737d5 100644
--- a/Open-ILS/src/sql/Pg/990.schema.unapi.sql
+++ b/Open-ILS/src/sql/Pg/990.schema.unapi.sql
@@ -220,7 +220,7 @@ CREATE OR REPLACE FUNCTION unapi.mmr (
 RETURNS XML AS $F$ SELECT NULL::XML $F$ LANGUAGE SQL STABLE;
 CREATE OR REPLACE FUNCTION unapi.bmp    ( obj_id BIGINT, format TEXT, ename TEXT, includes TEXT[], org TEXT, depth INT DEFAULT NULL, slimit HSTORE DEFAULT NULL, soffset HSTORE DEFAULT NULL, include_xmlns BOOL DEFAULT TRUE ) RETURNS XML AS $F$ SELECT NULL::XML $F$ LANGUAGE SQL STABLE;
 CREATE OR REPLACE FUNCTION unapi.mra    ( obj_id BIGINT, format TEXT, ename TEXT, includes TEXT[], org TEXT, depth INT DEFAULT NULL, slimit HSTORE DEFAULT NULL, soffset HSTORE DEFAULT NULL, include_xmlns BOOL DEFAULT TRUE ) RETURNS XML AS $F$ SELECT NULL::XML $F$ LANGUAGE SQL STABLE;
-CREATE OR REPLACE FUNCTION unapi.mmr_mra    ( obj_id BIGINT, format TEXT, ename TEXT, includes TEXT[], org TEXT, depth INT DEFAULT NULL, slimit HSTORE DEFAULT NULL, soffset HSTORE DEFAULT NULL, include_xmlns BOOL DEFAULT TRUE ) RETURNS XML AS $F$ SELECT NULL::XML $F$ LANGUAGE SQL STABLE;
+CREATE OR REPLACE FUNCTION unapi.mmr_mra    ( obj_id BIGINT, format TEXT, ename TEXT, includes TEXT[], org TEXT, depth INT DEFAULT NULL, slimit HSTORE DEFAULT NULL, soffset HSTORE DEFAULT NULL, include_xmlns BOOL DEFAULT TRUE, pref_lib INT DEFAULT NULL) RETURNS XML AS $F$ SELECT NULL::XML $F$ LANGUAGE SQL STABLE;
 CREATE OR REPLACE FUNCTION unapi.circ   ( obj_id BIGINT, format TEXT, ename TEXT, includes TEXT[], org TEXT DEFAULT '-', depth INT DEFAULT NULL, slimit HSTORE DEFAULT NULL, soffset HSTORE DEFAULT NULL, include_xmlns BOOL DEFAULT TRUE ) RETURNS XML AS $F$ SELECT NULL::XML $F$ LANGUAGE SQL STABLE;
 
 CREATE OR REPLACE FUNCTION unapi.holdings_xml (
@@ -1226,7 +1226,8 @@ CREATE OR REPLACE FUNCTION unapi.mmr_mra (
     depth INT DEFAULT NULL,
     slimit HSTORE DEFAULT NULL,
     soffset HSTORE DEFAULT NULL,
-    include_xmlns BOOL DEFAULT TRUE
+    include_xmlns BOOL DEFAULT TRUE,
+    pref_lib INT DEFAULT NULL
 ) RETURNS XML AS $F$
     SELECT  XMLELEMENT(
         name attributes,
@@ -1255,7 +1256,21 @@ CREATE OR REPLACE FUNCTION unapi.mmr_mra (
                     JOIN config.record_attr_definition rad ON (mra.attr = rad.name)
                     LEFT JOIN config.coded_value_map cvm ON (cvm.ctype = mra.attr AND code = mra.value)
                     LEFT JOIN metabib.uncontrolled_record_attr_value uvm ON (uvm.attr = mra.attr AND uvm.value = mra.value)
-              WHERE mra.id IN (SELECT source FROM metabib.metarecord_source_map WHERE metarecord = $1)
+              WHERE mra.id IN (
+                    WITH aou AS (SELECT COALESCE(id, (evergreen.org_top()).id) AS id 
+                        FROM actor.org_unit WHERE shortname = $5 LIMIT 1)
+                    SELECT source 
+                    FROM metabib.metarecord_source_map, aou
+                    WHERE metarecord = $1 AND (
+                        EXISTS (
+                            SELECT 1 FROM asset.opac_visible_copies 
+                            WHERE record = source AND circ_lib IN (
+                                SELECT id FROM actor.org_unit_descendants(aou.id, $6)) 
+                            LIMIT 1
+                        )
+                        OR EXISTS (SELECT 1 FROM located_uris(source, aou.id, $10) LIMIT 1)
+                    )
+                )
               ORDER BY 1
             )foo(id,y)
         )
@@ -1405,7 +1420,7 @@ BEGIN
 
     -- Grab distinct MVF for all records if requested
     IF ('mra' = ANY (includes)) THEN 
-        axml := unapi.mmr_mra(obj_id,NULL,NULL,NULL,NULL,NULL,NULL,NULL,TRUE);
+        axml := unapi.mmr_mra(obj_id,NULL,NULL,NULL,org,depth,NULL,NULL,TRUE,pref_lib);
     ELSE
         axml := NULL::XML;
     END IF;
diff --git a/Open-ILS/src/sql/Pg/upgrade/ZZZZ.schema.unapi-mmr.sql b/Open-ILS/src/sql/Pg/upgrade/ZZZZ.schema.unapi-mmr.sql
index 2aaa075..89c938d 100644
--- a/Open-ILS/src/sql/Pg/upgrade/ZZZZ.schema.unapi-mmr.sql
+++ b/Open-ILS/src/sql/Pg/upgrade/ZZZZ.schema.unapi-mmr.sql
@@ -137,7 +137,8 @@ CREATE OR REPLACE FUNCTION unapi.mmr_mra (
     depth INT DEFAULT NULL,
     slimit HSTORE DEFAULT NULL,
     soffset HSTORE DEFAULT NULL,
-    include_xmlns BOOL DEFAULT TRUE
+    include_xmlns BOOL DEFAULT TRUE,
+    pref_lib INT DEFAULT NULL
 ) RETURNS XML AS $F$
     SELECT  XMLELEMENT(
         name attributes,
@@ -166,7 +167,21 @@ CREATE OR REPLACE FUNCTION unapi.mmr_mra (
                     JOIN config.record_attr_definition rad ON (mra.attr = rad.name)
                     LEFT JOIN config.coded_value_map cvm ON (cvm.ctype = mra.attr AND code = mra.value)
                     LEFT JOIN metabib.uncontrolled_record_attr_value uvm ON (uvm.attr = mra.attr AND uvm.value = mra.value)
-              WHERE mra.id IN (SELECT source FROM metabib.metarecord_source_map WHERE metarecord = $1)
+              WHERE mra.id IN (
+                    WITH aou AS (SELECT COALESCE(id, (evergreen.org_top()).id) AS id 
+                        FROM actor.org_unit WHERE shortname = $5 LIMIT 1)
+                    SELECT source 
+                    FROM metabib.metarecord_source_map, aou
+                    WHERE metarecord = $1 AND (
+                        EXISTS (
+                            SELECT 1 FROM asset.opac_visible_copies 
+                            WHERE record = source AND circ_lib IN (
+                                SELECT id FROM actor.org_unit_descendants(aou.id, $6)) 
+                            LIMIT 1
+                        )
+                        OR EXISTS (SELECT 1 FROM located_uris(source, aou.id, $10) LIMIT 1)
+                    )
+                )
               ORDER BY 1
             )foo(id,y)
         )
@@ -405,7 +420,7 @@ BEGIN
 
     -- Grab distinct MVF for all records if requested
     IF ('mra' = ANY (includes)) THEN 
-        axml := unapi.mmr_mra(obj_id,NULL,NULL,NULL,NULL,NULL,NULL,NULL,TRUE);
+        axml := unapi.mmr_mra(obj_id,NULL,NULL,NULL,org,depth,NULL,NULL,TRUE,pref_lib);
     ELSE
         axml := NULL::XML;
     END IF;
@@ -496,5 +511,6 @@ END;
 $F$ LANGUAGE PLPGSQL STABLE;
 
 
+
 COMMIT;
 

commit c62821619ce2a00128aa9cc4a7b19d6e94f1b4fa
Author: Bill Erickson <berick at esilibrary.com>
Date:   Fri Feb 14 12:23:02 2014 -0500

    LP#1053397 repair one-hit redirect logic for metarecords
    
    * Only perform the single-hit redirect if there is only one metarecord
      search result and the MR only has a single constituent record.
    
    * Repair the record id logic so the redirect jumps to the correct
      record.
    
    Signed-off-by: Bill Erickson <berick at esilibrary.com>
    Signed-off-by: Mike Rylander <mrylander at gmail.com>
    Signed-off-by: Dan Wells <dbw2 at calvin.edu>

diff --git a/Open-ILS/src/perlmods/lib/OpenILS/WWW/EGCatLoader/Search.pm b/Open-ILS/src/perlmods/lib/OpenILS/WWW/EGCatLoader/Search.pm
index 26df556..b2098bd 100644
--- a/Open-ILS/src/perlmods/lib/OpenILS/WWW/EGCatLoader/Search.pm
+++ b/Open-ILS/src/perlmods/lib/OpenILS/WWW/EGCatLoader/Search.pm
@@ -485,9 +485,17 @@ sub load_rresults {
     );
     $self->timelog("Returned from get_records_and_facets()");
 
-    if ($page == 0) {
-        # TODO: handle metarecords
-        my $stat = $self->check_1hit_redirect($rec_ids);
+    if ($page == 0 and @$rec_ids == 1) {
+        my $stat = 0;
+        if ($is_meta) {
+            # if the MR has a single constituent record, it will
+            # be in array position 2 of the result blob.
+            # otherwise, we don't want to redirect anyway.
+            my $bre_id = $results->{ids}->[0]->[2];
+            $stat = $self->check_1hit_redirect([$bre_id]) if $bre_id;
+        } else {
+            my $stat = $self->check_1hit_redirect($rec_ids);
+        }
         return $stat if $stat;
     }
 

commit f77269997a72f188ca8a733f5125175bf2b83dc2
Author: Bill Erickson <berick at esilibrary.com>
Date:   Thu Feb 13 16:28:01 2014 -0500

    LP#1269911 global flag for opac format selector attribute
    
    Adds a new config.global_flag for defining which record attribute the
    OPAC will use to find formats for the format selector.
    
    Signed-off-by: Bill Erickson <berick at esilibrary.com>
    Signed-off-by: Mike Rylander <mrylander at gmail.com>
    Signed-off-by: Dan Wells <dbw2 at calvin.edu>

diff --git a/Open-ILS/src/sql/Pg/950.data.seed-values.sql b/Open-ILS/src/sql/Pg/950.data.seed-values.sql
index 1605b05..08e71ab 100644
--- a/Open-ILS/src/sql/Pg/950.data.seed-values.sql
+++ b/Open-ILS/src/sql/Pg/950.data.seed-values.sql
@@ -13925,5 +13925,19 @@ INSERT INTO config.global_flag (name, label, value, enabled) VALUES (
     TRUE
 );
 
+-- until we have a custom attribute for the selector, 
+-- default to the icon_format attribute
+INSERT INTO config.global_flag (name, label, value, enabled) VALUES (
+    'opac.format_selector.attr', 
+    oils_i18n_gettext(
+        'opac.format_selector.attr', 
+        'OPAC Format Selector Attribute', 
+        'cgf',
+        'label'
+    ),
+    'icon_format', 
+    TRUE
+);
+
 
 
diff --git a/Open-ILS/src/sql/Pg/upgrade/ZZZZZ.data.mmr-holds-formats.sql b/Open-ILS/src/sql/Pg/upgrade/ZZZZZ.data.mmr-holds-formats.sql
index 690d919..bab086e 100644
--- a/Open-ILS/src/sql/Pg/upgrade/ZZZZZ.data.mmr-holds-formats.sql
+++ b/Open-ILS/src/sql/Pg/upgrade/ZZZZZ.data.mmr-holds-formats.sql
@@ -12,6 +12,21 @@ INSERT INTO config.global_flag (name, label, value, enabled) VALUES (
     TRUE
 );
 
+-- until we have a custom attribute for the selector, 
+-- default to the icon_format attribute
+INSERT INTO config.global_flag (name, label, value, enabled) VALUES (
+    'opac.format_selector.attr', 
+    oils_i18n_gettext(
+        'opac.format_selector.attr', 
+        'OPAC Format Selector Attribute', 
+        'cgf',
+        'label'
+    ),
+    'icon_format', 
+    TRUE
+);
+
+
 INSERT INTO config.record_attr_definition 
     (name, label, multi, filter, composite) 
 VALUES (
diff --git a/Open-ILS/src/templates/opac/parts/config.tt2 b/Open-ILS/src/templates/opac/parts/config.tt2
index 509d6a1..618d63a 100644
--- a/Open-ILS/src/templates/opac/parts/config.tt2
+++ b/Open-ILS/src/templates/opac/parts/config.tt2
@@ -147,7 +147,7 @@ search.default_qtypes = ['keyword','title','author'];
 
 search.basic_config = {
     type => 'attr',
-    group => [ctx.get_cgf('opac.icon_attr').value, 'item_type'],
+    group => [ctx.get_cgf('opac.format_selector.attr').value, 'item_type'],
     none_label => l("All Formats"),
 };
 

commit 811325f638f9291752de1134f61fa073732ed3f7
Author: Bill Erickson <berick at esilibrary.com>
Date:   Thu Feb 13 15:28:39 2014 -0500

    LP#1269911 seed data SQL repairs
    
    Products of merge conflict resolution with the mvf-cra branch.
    
    * repaired syntax error
    * removed some duplicate global flag entries
    
    Signed-off-by: Bill Erickson <berick at esilibrary.com>
    Signed-off-by: Mike Rylander <mrylander at gmail.com>
    Signed-off-by: Dan Wells <dbw2 at calvin.edu>

diff --git a/Open-ILS/src/sql/Pg/950.data.seed-values.sql b/Open-ILS/src/sql/Pg/950.data.seed-values.sql
index c53ef10..1605b05 100644
--- a/Open-ILS/src/sql/Pg/950.data.seed-values.sql
+++ b/Open-ILS/src/sql/Pg/950.data.seed-values.sql
@@ -6091,7 +6091,7 @@ INSERT INTO config.record_attr_definition (name, label, multi, filter, composite
     VALUES ('icon_format', oils_i18n_gettext('icon_format', 'OPAC Format Icons', 'crad', 'label'), TRUE, TRUE, TRUE);
 INSERT INTO config.record_attr_definition (name, label, multi, filter, composite) 
     VALUES ('mr_hold_format', oils_i18n_gettext('mr_hold_format', 'Metarecord Hold Formats', 'crad', 'label'),
-    TRUE, TRUE, TRUE
+    TRUE, TRUE, TRUE);
 
 -- TO-DO: Auto-generate these values from CLDR
 -- XXX These are the values used in MARC records ... does that match CLDR, including deprecated languages?
@@ -9679,18 +9679,6 @@ INSERT INTO config.global_flag (name, label, value, enabled) VALUES (
     TRUE
 );
 
-INSERT INTO config.record_attr_definition
-    (name, label, multi, filter, composite) VALUES (
-    'icon_format',
-    oils_i18n_gettext(
-        'icon_format',
-        'OPAC Format Icons',
-        'crad',
-        'label'
-    ),
-    TRUE, TRUE, TRUE
-);
-
 INSERT INTO config.usr_setting_type (name,opac_visible,label,description,datatype)
     VALUES (
         'history.circ.retention_age',
@@ -13926,19 +13914,6 @@ INSERT INTO config.floating_group(name) VALUES ('Everywhere');
 INSERT INTO config.floating_group_member(floating_group, org_unit) VALUES (1, 1);
 
 INSERT INTO config.global_flag (name, label, value, enabled) VALUES (
-    'opac.icon_attr',
-    oils_i18n_gettext(
-        'opac.icon_attr', 
-        'OPAC Format Icons Attribute',
-        'cgf',
-        'label'
-    ),
-    'icon_format', 
-    TRUE
-);
-
-
-INSERT INTO config.global_flag (name, label, value, enabled) VALUES (
     'opac.metarecord.holds.format_attr', 
     oils_i18n_gettext(
         'opac.metarecord.holds.format_attr',

commit d6f40df7f4f0b4d9210dc7aee5ab9771a63cbb5b
Author: Bill Erickson <berick at esilibrary.com>
Date:   Tue Feb 11 10:33:01 2014 -0500

    LP#1269911 'large print book' is also a 'book'
    
    Remove the item_form != d component of the "book" composite definition.
    I think this more closely matches how humans would interpret it.
    
    Signed-off-by: Bill Erickson <berick at esilibrary.com>
    Signed-off-by: Mike Rylander <mrylander at gmail.com>
    
    Conflicts:
    	Open-ILS/src/sql/Pg/950.data.seed-values.sql
    
    Signed-off-by: Dan Wells <dbw2 at calvin.edu>

diff --git a/Open-ILS/src/sql/Pg/950.data.seed-values.sql b/Open-ILS/src/sql/Pg/950.data.seed-values.sql
index 557dcff..c53ef10 100644
--- a/Open-ILS/src/sql/Pg/950.data.seed-values.sql
+++ b/Open-ILS/src/sql/Pg/950.data.seed-values.sql
@@ -6820,7 +6820,7 @@ SELECT SETVAL('config.coded_value_map_id_seq'::TEXT, 10000);
 INSERT INTO config.composite_attr_entry_definition 
     (coded_value, definition) VALUES
 --book
-(564, '{"0":[{"_attr":"item_type","_val":"a"},{"_attr":"item_type","_val":"t"}],"1":{"_not":[{"_attr":"item_form","_val":"a"},{"_attr":"item_form","_val":"b"},{"_attr":"item_form","_val":"c"},{"_attr":"item_form","_val":"d"},{"_attr":"item_form","_val":"f"},{"_attr":"item_form","_val":"o"},{"_attr":"item_form","_val":"q"},{"_attr":"item_form","_val":"r"},{"_attr":"item_form","_val":"s"}]},"2":[{"_attr":"bib_level","_val":"a"},{"_attr":"bib_level","_val":"c"},{"_attr":"bib_level","_val":"d"},{"_attr":"bib_level","_val":"m"}]}'),
+(564, '{"0":[{"_attr":"item_type","_val":"a"},{"_attr":"item_type","_val":"t"}],"1":{"_not":[{"_attr":"item_form","_val":"a"},{"_attr":"item_form","_val":"b"},{"_attr":"item_form","_val":"c"},{"_attr":"item_form","_val":"f"},{"_attr":"item_form","_val":"o"},{"_attr":"item_form","_val":"q"},{"_attr":"item_form","_val":"r"},{"_attr":"item_form","_val":"s"}]},"2":[{"_attr":"bib_level","_val":"a"},{"_attr":"bib_level","_val":"c"},{"_attr":"bib_level","_val":"d"},{"_attr":"bib_level","_val":"m"}]}'),
 
 -- braille
 (565, '{"0":{"_attr":"item_type","_val":"a"},"1":{"_attr":"item_form","_val":"f"}}'),
diff --git a/Open-ILS/src/sql/Pg/upgrade/QQQQ.MVF_CRA-upgrade.sql b/Open-ILS/src/sql/Pg/upgrade/QQQQ.MVF_CRA-upgrade.sql
index dc1d478..5c1524e 100644
--- a/Open-ILS/src/sql/Pg/upgrade/QQQQ.MVF_CRA-upgrade.sql
+++ b/Open-ILS/src/sql/Pg/upgrade/QQQQ.MVF_CRA-upgrade.sql
@@ -639,7 +639,7 @@ INSERT INTO config.record_attr_definition
 INSERT INTO config.composite_attr_entry_definition 
     (coded_value, definition) VALUES
 --book
-(564, '{"0":[{"_attr":"item_type","_val":"a"},{"_attr":"item_type","_val":"t"}],"1":{"_not":[{"_attr":"item_form","_val":"a"},{"_attr":"item_form","_val":"b"},{"_attr":"item_form","_val":"c"},{"_attr":"item_form","_val":"d"},{"_attr":"item_form","_val":"f"},{"_attr":"item_form","_val":"o"},{"_attr":"item_form","_val":"q"},{"_attr":"item_form","_val":"r"},{"_attr":"item_form","_val":"s"}]},"2":[{"_attr":"bib_level","_val":"a"},{"_attr":"bib_level","_val":"c"},{"_attr":"bib_level","_val":"d"},{"_attr":"bib_level","_val":"m"}]}'),
+(564, '{"0":[{"_attr":"item_type","_val":"a"},{"_attr":"item_type","_val":"t"}],"1":{"_not":[{"_attr":"item_form","_val":"a"},{"_attr":"item_form","_val":"b"},{"_attr":"item_form","_val":"c"},{"_attr":"item_form","_val":"f"},{"_attr":"item_form","_val":"o"},{"_attr":"item_form","_val":"q"},{"_attr":"item_form","_val":"r"},{"_attr":"item_form","_val":"s"}]},"2":[{"_attr":"bib_level","_val":"a"},{"_attr":"bib_level","_val":"c"},{"_attr":"bib_level","_val":"d"},{"_attr":"bib_level","_val":"m"}]}'),
 
 -- braille
 (565, '{"0":{"_attr":"item_type","_val":"a"},"1":{"_attr":"item_form","_val":"f"}}'),

commit b2e4783726756af658f0fee069c7780425955cd1
Author: Bill Erickson <berick at esilibrary.com>
Date:   Mon Feb 10 14:01:05 2014 -0500

    LP#1053397 TPAC Metarecords release notes
    
    Signed-off-by: Bill Erickson <berick at esilibrary.com>
    Signed-off-by: Mike Rylander <mrylander at gmail.com>
    Signed-off-by: Dan Wells <dbw2 at calvin.edu>

diff --git a/docs/RELEASE_NOTES_NEXT/OPAC/TPAC_metarecords.txt b/docs/RELEASE_NOTES_NEXT/OPAC/TPAC_metarecords.txt
new file mode 100644
index 0000000..be20a22
--- /dev/null
+++ b/docs/RELEASE_NOTES_NEXT/OPAC/TPAC_metarecords.txt
@@ -0,0 +1,38 @@
+= TPAC Metarecord Search and Holds =
+
+This feature adds support for searching and placing holds against 
+metarecords.
+
+== Metarecord Searching ==
+
+In the top search bar and in the advanced search page, there is a new
+search modifier labeled "Group Formats and Editions".  When selected,
+searches are performed against metarecords and metarecords are shown
+in the results list.
+
+For each metarecord, format icons for all constituent records are shown.
+When a use clicks on a metarecord, if the metarecord has multiple
+constituent records, the user is taken to the constituent records
+list.  Similarly, when a metarecord only has one constituent record,
+the user is directed to the record detail page for the constituent
+record.
+
+== Metarecord Holds ==
+
+Clicking the place hold link from the metarecord results page shows
+the available formats and languages for the metarecord, allowing
+the user to limit the scope of the hold.  Non-metarecord holds now
+get a new "Advanced Holds Options" link which allows user to promote
+a title hold to a metarecord hold, thus providing access 
+to the formats / editions selector, before the hold is placed.
+
+In the My Account holds list, icons for all selected formats are 
+displayed in the Format columns for the hold.  When editing a 
+metarecord hold, users may modify the desired formats and languages.
+
+
+== Configuration ==
+
+Admins may disable this feature my un-commenting the "metarecord.disabled"
+attribute in config.tt2
+

commit fc99f4f2230061922f8bea0e061f04d240a1ddfa
Author: Bill Erickson <berick at esilibrary.com>
Date:   Mon Feb 10 14:01:47 2014 -0500

    LP#1053397 TPAC metarecord search and holds UI
    
    API, TPAC backend, and UI bits for TPAC metarecord searching and holds.
    
    * Group Formats and Editions options in advanced search / searchbar
    * MR holds placement form, allowing selected formats and languages
    * MR holds targeting updated to work w/ new holdable formats composite
      definitions
    
    Signed-off-by: Bill Erickson <berick at esilibrary.com>
    Signed-off-by: Mike Rylander <mrylander at gmail.com>
    Signed-off-by: Dan Wells <dbw2 at calvin.edu>

diff --git a/Open-ILS/src/perlmods/lib/OpenILS/Application/Circ/Holds.pm b/Open-ILS/src/perlmods/lib/OpenILS/Application/Circ/Holds.pm
index 8ba48ab..4f56877 100644
--- a/Open-ILS/src/perlmods/lib/OpenILS/Application/Circ/Holds.pm
+++ b/Open-ILS/src/perlmods/lib/OpenILS/Application/Circ/Holds.pm
@@ -92,8 +92,15 @@ sub test_and_create_hold_batch {
     elsif ($$params{'hold_type'} eq 'P') { $target_field = 'partid'; }
     else { return undef; }
 
+    my $formats_map = delete $$params{holdable_formats_map};
+
     foreach (@$target_list) {
         $$params{$target_field} = $_;
+
+        # copy the requested formats from the target->formats map
+        # into the top-level formats attr for each hold
+        $$params{holdable_formats} = $formats_map->{$_};
+
         my $res;
         ($res) = $self->method_lookup(
             'open-ils.circ.title_hold.is_possible')->run($auth, $params, $override ? $oargs : {});
@@ -2470,10 +2477,9 @@ sub do_possibility_checks {
 
     } elsif( $hold_type eq OILS_HOLD_TYPE_METARECORD ) {
 
-        my $maps = $e->search_metabib_metarecord_source_map({metarecord=>$mrid});
-        my @recs = map { $_->source } @$maps;
+        my ($recs) = __PACKAGE__->method_lookup('open-ils.circ.holds.metarecord.filterd_records')->run($mrid, $holdable_formats);
         my @status = ();
-        for my $rec (@recs) {
+        for my $rec (@$recs) {
             @status = _check_title_hold_is_possible(
                 $rec, $depth, $request_lib, $patron, $e->requestor, $pickup_lib, $selection_ou, $holdable_formats, $oargs
             );
@@ -2484,6 +2490,15 @@ sub do_possibility_checks {
 #   else { Unrecognized hold_type ! }   # FIXME: return error? or 0?
 }
 
+sub MR_filter_records {
+    return $U->storagereq('open-ils.storage.metarecord.filtered_records.atomic', $_[2], $_[3]);
+}
+__PACKAGE__->register_method(
+    method   => 'MR_filter_records',
+    api_name => 'open-ils.circ.holds.metarecord.filterd_records',
+);
+
+
 my %prox_cache;
 sub create_ranged_org_filter {
     my($e, $selection_ou, $depth) = @_;
@@ -2511,11 +2526,7 @@ sub create_ranged_org_filter {
 
 sub _check_title_hold_is_possible {
     my( $titleid, $depth, $request_lib, $patron, $requestor, $pickup_lib, $selection_ou, $holdable_formats, $oargs ) = @_;
-
-    my ($types, $formats, $lang);
-    if (defined($holdable_formats)) {
-        ($types, $formats, $lang) = split '-', $holdable_formats;
-    }
+    # $holdable_formats is now unused. We pre-filter the MR's records.
 
     my $e = new_editor();
     my %org_filter = create_ranged_org_filter($e, $selection_ou, $depth);
@@ -2529,23 +2540,7 @@ sub _check_title_hold_is_possible {
                     acn => {
                         field  => 'id',
                         fkey   => 'call_number',
-                        'join' => {
-                            bre => {
-                                field  => 'id',
-                                filter => { id => $titleid },
-                                fkey   => 'record'
-                            },
-                            mrd => {
-                                field  => 'record',
-                                fkey   => 'record',
-                                filter => {
-                                    record => $titleid,
-                                    ( $types   ? (item_type => [split '', $types])   : () ),
-                                    ( $formats ? (item_form => [split '', $formats]) : () ),
-                                    ( $lang    ? (item_lang => $lang)                : () )
-                                }
-                            }
-                        }
+                        filter => { record => $titleid }
                     },
                     acpl => { field => 'id', filter => { holdable => 't'}, fkey => 'location' },
                     ccs  => { field => 'id', filter => { holdable => 't'}, fkey => 'status'   },
@@ -4205,4 +4200,135 @@ sub calculate_expire_time
     return undef;
 }
 
+
+__PACKAGE__->register_method(
+    method    => 'mr_hold_filter_attrs',
+    api_name  => 'open-ils.circ.mmr.holds.filters',
+    authoritative => 1,
+    stream => 1,
+    signature => {
+        desc => q/
+            Returns the set of available formats and languages for the
+            constituent records of the provided metarcord.
+            If an array of hold IDs is also provided, information about
+            each is returned as well.  This information includes:
+            1. a slightly easier to read version of holdable_formats
+            2. attributes describing the set of format icons included
+               in the set of desired, constituent records.
+        /,
+        params => [
+            {desc => 'Metarecord ID', type => 'number'},
+            {desc => 'Hold ID List', type => 'array'},
+        ],
+        return => {
+            desc => q/
+                Stream of objects.  The first will have a 'metarecord' key
+                containing non-hold-specific metarecord information, subsequent
+                responses will contain a 'hold' key containing hold-specific
+                information
+            /, 
+            type => 'object'
+        }
+    }
+);
+
+sub mr_hold_filter_attrs {
+    my ($self, $client, $mr_id, $hold_ids) = @_;
+    my $e = new_editor();
+
+
+    my $mr = $e->retrieve_metabib_metarecord($mr_id) or return $e->event;
+    my $bre_ids = $e->json_query({
+        select => {mmrsm => ['source']},
+        from => 'mmrsm',
+        where => {'+mmrsm' => {metarecord => $mr_id}}
+    });
+    $bre_ids = [map {$_->{source}} @$bre_ids];
+
+    my $item_lang_attr = 'item_lang'; # configurable?
+    my $format_attr = $e->retrieve_config_global_flag(
+        'opac.metarecord.holds.format_attr')->value;
+
+    # helper sub for fetching ccvms for a batch of record IDs
+    sub get_batch_ccvms {
+        my ($e, $attr, $bre_ids) = @_;
+        return [] unless $bre_ids and @$bre_ids;
+        my $vals = $e->search_metabib_record_attr_flat({
+            attr => $attr,
+            id => $bre_ids
+        });
+        return [] unless @$vals;
+        return $e->search_config_coded_value_map({
+            ctype => $attr,
+            code => [map {$_->value} @$vals]
+        });
+    }
+
+    my $langs = get_batch_ccvms($e, $item_lang_attr, $bre_ids);
+    my $formats = get_batch_ccvms($e, $format_attr, $bre_ids);
+
+    $client->respond({
+        metarecord => {
+            id => $mr_id,
+            formats => $formats,
+            langs => $langs
+        }
+    });
+
+    return unless $hold_ids;
+    my $icon_attr = $e->retrieve_config_global_flag('opac.icon_attr');
+    $icon_attr = $icon_attr ? $icon_attr->value : '';
+
+    for my $hold_id (@$hold_ids) {
+        my $hold = $e->retrieve_action_hold_request($hold_id) 
+            or return $e->event;
+
+        next unless $hold->hold_type eq 'M';
+
+        my $resp = {
+            hold => {
+                id => $hold_id,
+                formats => [],
+                langs => []
+            }
+        };
+
+        # collect the ccvm's for the selected formats / language (
+        # (i.e. the holdable formats) on the MR.
+        # this assumes a two-key structure for format / language,
+        # though assumption is made about the keys themselves.
+        my $hformats = OpenSRF::Utils::JSON->JSON2perl($hold->holdable_formats);
+        my $lang_vals = [];
+        my $format_vals = [];
+        for my $val (values %$hformats) {
+            # val is either a single ccvm or an array of them
+            $val = [$val] unless ref $val eq 'ARRAY';
+            for my $node (@$val) {
+                push (@$lang_vals, $node->{_val})   
+                    if $node->{_attr} eq $item_lang_attr; 
+                push (@$format_vals, $node->{_val})   
+                    if $node->{_attr} eq $format_attr;
+            }
+        }
+
+        # fetch the ccvm's for consistency with the {metarecord} blob
+        $resp->{hold}{formats} = $e->search_config_coded_value_map({
+            ctype => $format_attr, code => $format_vals});
+        $resp->{hold}{langs} = $e->search_config_coded_value_map({
+            ctype => $item_lang_attr, code => $lang_vals});
+
+        # find all of the bib records within this metarcord whose 
+        # format / language match the holdable formats on the hold
+        my ($bre_ids) = $self->method_lookup(
+            'open-ils.circ.holds.metarecord.filterd_records')->run(
+                $hold->target, $hold->holdable_formats);
+
+        # now find all of the 'icon' attributes for the records
+        $resp->{hold}{icons} = get_batch_ccvms($e, $icon_attr, $bre_ids);
+        $client->respond($resp);
+    }
+
+    return;
+}
+
 1;
diff --git a/Open-ILS/src/perlmods/lib/OpenILS/Application/Storage/Publisher/action.pm b/Open-ILS/src/perlmods/lib/OpenILS/Application/Storage/Publisher/action.pm
index b81e847..2f98fa5 100644
--- a/Open-ILS/src/perlmods/lib/OpenILS/Application/Storage/Publisher/action.pm
+++ b/Open-ILS/src/perlmods/lib/OpenILS/Application/Storage/Publisher/action.pm
@@ -1283,6 +1283,47 @@ __PACKAGE__->register_method(
 );
 
 
+sub MR_records_matching_format {
+    my $self = shift;
+    my $client = shift;
+    my $MR = shift;
+    my $filter = shift;
+
+    # find filters for MR holds
+    my $mr_filter;
+    if (defined($filter)) {
+        ($mr_filter) = @{action::hold_request->db_Main->selectcol_arrayref(
+            'SELECT metabib.compile_composite_attr(?)',
+            {},
+            $filter
+        )};
+    }
+
+    my $records = [metabib::metarecord->retrieve($MR)->source_records];
+
+    if (!$mr_filter) {
+        $client->respond( $_->id ) for @$records;
+    } else {
+        for my $r ( map { isTrue($_->deleted) ?  () : ($_->id) } @$records ) {
+            $client->respond($r) if
+                @{action::hold_request->db_Main->selectcol_arrayref(
+                    'SELECT source FROM metabib.record_attr_vector_list WHERE source = ? AND vlist @@ ?',
+                    {},
+                    $r,
+                    $mr_filter
+                )};
+        }
+    }
+    return; # discard final l-val
+}
+__PACKAGE__->register_method(
+    api_name        => 'open-ils.storage.metarecord.filtered_records',
+    api_level       => 1,
+    stream          => 1,
+    argc            => 2,
+    method          => 'MR_records_matching_format',
+);
+
 
 sub new_hold_copy_targeter {
     my $self = shift;
@@ -1432,41 +1473,23 @@ sub new_hold_copy_targeter {
 
             my $all_copies = [];
 
-            # find filters for MR holds
-            my ($types, $formats, $lang);
-            if (defined($hold->holdable_formats)) {
-                ($types, $formats, $lang) = split '-', $hold->holdable_formats;
-            }
-
             # find all the potential copies
             if ($hold->hold_type eq 'M') {
-                my $records = [
-                    map {
-                        isTrue($_->deleted) ?  () : ($_->id)
-                    } metabib::metarecord->retrieve($hold->target)->source_records
-                ];
-                if(@$records > 0) {
-                    for my $r ( map
-                            {$_->record}
-                            metabib::record_descriptor
-                                ->search(
-                                    record => $records,
-                                    ( $types   ? (item_type => [split '', $types])   : () ),
-                                    ( $formats ? (item_form => [split '', $formats]) : () ),
-                                    ( $lang    ? (item_lang => $lang)                : () ),
-                                )
-                    ) {
-                        my ($rtree) = $self
-                            ->method_lookup( 'open-ils.storage.biblio.record_entry.ranged_tree')
-                            ->run( $r->id, $hold->selection_ou, $hold->selection_depth );
-
-                        for my $cn ( @{ $rtree->call_numbers } ) {
-                            push @$all_copies,
-                                asset::copy->search_where(
-                                    { id => [map {$_->id} @{ $cn->copies }],
-                                      deleted => 'f' }
-                                ) if ($cn && @{ $cn->copies });
-                        }
+                for my $r_id (
+                    $self->method_lookup(
+                        'open-ils.storage.metarecord.filtered_records'
+                    )->run( $hold->target, $hold->holdable_formats )
+                ) {
+                    my ($rtree) = $self
+                        ->method_lookup( 'open-ils.storage.biblio.record_entry.ranged_tree')
+                        ->run( $r_id, $hold->selection_ou, $hold->selection_depth );
+
+                    for my $cn ( @{ $rtree->call_numbers } ) {
+                        push @$all_copies,
+                            asset::copy->search_where(
+                                { id => [map {$_->id} @{ $cn->copies }],
+                                  deleted => 'f' }
+                            ) if ($cn && @{ $cn->copies });
                     }
                 }
             } elsif ($hold->hold_type eq 'T') {
@@ -2323,32 +2346,5 @@ sub title_hold_capture {
     $self->volume_hold_capture($hold,$cn_list) if (ref $cn_list and @$cn_list);
 }
 
-sub metarecord_hold_capture {
-    my $self = shift;
-    my $hold = shift;
-
-    my $titles;
-    try {
-        $titles = [ metabib::metarecord_source_map->search( metarecord => $hold->target) ];
-    
-    } catch Error with {
-        my $e = shift;
-        die "Could not retrieve initial title list:\n\n$e\n";
-    };
-
-    try {
-        my @recs = map {$_->record} metabib::record_descriptor->search( record => $titles, item_type => [split '', $hold->holdable_formats] ); 
-
-        $titles = [ biblio::record_entry->search( id => \@recs ) ];
-    
-    } catch Error with {
-        my $e = shift;
-        die "Could not retrieve format-pruned title list:\n\n$e\n";
-    };
-
-
-    $cache{titles}{$_->id} = $_ for (@$titles);
-    $self->title_hold_capture($hold,$titles) if (ref $titles and @$titles);
-}
 
 1;
diff --git a/Open-ILS/src/perlmods/lib/OpenILS/WWW/EGCatLoader/Account.pm b/Open-ILS/src/perlmods/lib/OpenILS/WWW/EGCatLoader/Account.pm
index ac11d61..3961984 100644
--- a/Open-ILS/src/perlmods/lib/OpenILS/WWW/EGCatLoader/Account.pm
+++ b/Open-ILS/src/perlmods/lib/OpenILS/WWW/EGCatLoader/Account.pm
@@ -571,9 +571,35 @@ sub fetch_user_holds {
 
         if(@collected) {
             while(my $blob = pop(@collected)) {
-                my (undef, @data) = $self->get_records_and_facets(
-                    [$blob->{hold}->{bre_id}], undef, {flesh => '{mra}'}
-                );
+                my @data;
+
+                # in the holds edit UI, we need to know what formats and
+                # languages the user selected for this hold, plus what
+                # formats/langs are available on the MR as a whole.
+                if ($blob->{hold}{hold}->hold_type eq 'M') {
+                    my $hold = $blob->{hold}->{hold};
+
+                    # for MR, fetch the combined MR unapi blob
+                    (undef, @data) = $self->get_records_and_facets(
+                        [$hold->target], undef, {flesh => '{mra}', metarecord => 1});
+
+                    my $filter_data = $U->simplereq(
+                        'open-ils.circ',
+                        'open-ils.circ.mmr.holds.filters.authoritative.atomic', 
+                        $hold->target, [$hold->id]
+                    );
+
+                    $blob->{metarecord_filters} = 
+                        $filter_data->[0]->{metarecord};
+                    $blob->{metarecord_selected_filters} = 
+                        $filter_data->[1]->{hold};
+                } else {
+
+                    (undef, @data) = $self->get_records_and_facets(
+                        [$blob->{hold}->{bre_id}], undef, {flesh => '{mra}'}
+                    );
+                }
+
                 $blob->{marc_xml} = $data[0]->{marc_xml};
                 push(@holds, $blob);
             }
@@ -652,6 +678,10 @@ sub handle_hold_update {
                     m:^(\d{2})/(\d{2})/(\d{4})$:;
                 $val->{$field} = "$3-$1-$2";
             }
+
+            $val->{holdable_formats} = # no-op for non-MR holds
+                $self->compile_holdable_formats(undef, $_);
+
             $val;
         } @hold_ids;
 
@@ -777,8 +807,37 @@ sub load_place_hold {
     };
 
     my $type_dispatch = {
+        M => sub {
+            # target metarecords
+            my $mrecs = $e->batch_retrieve_metabib_metarecord([
+                \@targets, 
+                {flesh => 1, flesh_fields => {mmr => ['master_record']}}], 
+                {substream => 1}
+            );
+
+            for my $id (@targets) {
+                my ($mr) = grep {$_->id eq $id} @$mrecs;
+
+                my $filter_data = $U->simplereq(
+                    'open-ils.circ',
+                    'open-ils.circ.mmr.holds.filters.authoritative', $mr->id);
+
+                my $holdable_formats = 
+                    $self->compile_holdable_formats($mr->id);
+
+                push(@hold_data, $data_filler->({
+                    target => $mr, 
+                    record => $mr->master_record,
+                    holdable_formats => $holdable_formats,
+                    metarecord_filters => $filter_data->{metarecord}
+                }));
+            }
+        },
         T => sub {
-            my $recs = $e->batch_retrieve_biblio_record_entry(\@targets, {substream => 1});
+            my $recs = $e->batch_retrieve_biblio_record_entry(
+                [\@targets,  {flesh => 1, flesh_fields => {bre => ['metarecord']}}],
+                {substream => 1}
+            );
 
             for my $id (@targets) { # force back into the correct order
                 my ($rec) = grep {$_->id eq $id} @$recs;
@@ -984,13 +1043,23 @@ sub attempt_hold_placement {
 
     if(@create_targets) {
 
+        # holdable formats may be different for each MR hold.
+        # map each set to the ID of the target.
+        my $holdable_formats = {};
+        if ($hold_type eq 'M') {
+            $holdable_formats->{$_->{target_id}} = 
+                $_->{holdable_formats} for @hold_data;
+        }
+
         my $bses = OpenSRF::AppSession->create('open-ils.circ');
         my $breq = $bses->request( 
             $method, 
             $e->authtoken, 
-            $data_filler->({   patronid => $usr,
+            $data_filler->({   
+                patronid => $usr,
                 pickup_lib => $pickup_lib, 
-                hold_type => $hold_type
+                hold_type => $hold_type,
+                holdable_formats_map => $holdable_formats
             }),
             \@create_targets
         );
@@ -1053,6 +1122,63 @@ sub attempt_hold_placement {
     }
 }
 
+# pull the selected formats and languages for metarecord holds
+# from the CGI params and map them into the JSON holdable
+# formats...er, format.
+# if no metarecord is provided, we'll pull it from the target
+# of the provided hold.
+sub compile_holdable_formats {
+    my ($self, $mr_id, $hold_id) = @_;
+    my $e = $self->editor;
+    my $cgi = $self->cgi;
+
+    # exit early if not needed
+    return "" unless 
+        grep /metarecord_formats_|metarecord_langs_/, 
+        $cgi->param;
+
+    # CGI params are based on the MR id, since during hold placement
+    # we have no old ID.  During hold edit, map the hold ID back to 
+    # the metarecod target.
+    $mr_id = 
+        $e->retrieve_action_hold_request($hold_id)->target 
+        unless $mr_id;
+
+    my $format_attr = $self->ctx->{get_cgf}->(
+        'opac.metarecord.holds.format_attr');
+
+    if (!$format_attr) {
+        $logger->error("Missing config.global_flag: ".
+            "opac.metarecord.holds.format_attr!");
+        return "";
+    }
+
+    $format_attr = $format_attr->value;
+
+    # during hold placement or edit submission, the user selects
+    # which of the available formats/langs are acceptable.
+    # Capture those here as the holdable_formats for the MR hold.
+    my @selected_formats = $cgi->param("metarecord_formats_$mr_id");
+    my @selected_langs = $cgi->param("metarecord_langs_$mr_id");
+
+    # map the selected attrs into the JSON holdable_formats structure
+    my $blob = {};
+    if (@selected_formats) {
+        $blob->{0} = [
+            map { {_attr => $format_attr, _val => $_} } 
+            @selected_formats
+        ];
+    }
+    if (@selected_langs) {
+        $blob->{1} = [
+            map { {_attr => 'item_lang', _val => $_} } 
+            @selected_langs
+        ];
+    }
+
+    return OpenSRF::Utils::JSON->perl2JSON($blob);
+}
+
 sub fetch_user_circs {
     my $self = shift;
     my $flesh = shift; # flesh bib data, etc.
diff --git a/Open-ILS/src/perlmods/lib/OpenILS/WWW/EGCatLoader/Search.pm b/Open-ILS/src/perlmods/lib/OpenILS/WWW/EGCatLoader/Search.pm
index 00c4c49..26df556 100644
--- a/Open-ILS/src/perlmods/lib/OpenILS/WWW/EGCatLoader/Search.pm
+++ b/Open-ILS/src/perlmods/lib/OpenILS/WWW/EGCatLoader/Search.pm
@@ -67,7 +67,8 @@ sub _prepare_biblio_search {
 
     foreach ($cgi->param('modifier')) {
         # The unless bit is to avoid stacking modifiers.
-        $query = ('#' . $_ . ' ' . $query) unless $query =~ qr/\#\Q$_/;
+        $query = ('#' . $_ . ' ' . $query) unless 
+            $query =~ qr/\#\Q$_/ or $_ eq 'metabib';
     }
 
     # filters
@@ -307,6 +308,13 @@ sub load_rresults {
     my $ctx = $self->ctx;
     my $e = $self->editor;
 
+    # 1. param->metarecord : view constituent bib records for a metarecord
+    # 2. param->modifier=metabib : perform a metarecord search
+    my $metarecord = $ctx->{metarecord} = $cgi->param('metarecord');
+    my @mods = $cgi->param('modifier');
+    my $is_meta = (@mods and grep {$_ eq 'metabib'} @mods and !$metarecord);
+    my $id_key = $is_meta ? 'mmr_id' : 'bre_id';
+
     # find the last record in the set, then redirect
     my $find_last = $cgi->param('find_last');
 
@@ -343,7 +351,6 @@ sub load_rresults {
     $ctx->{search_ou} = $self->_get_search_lib();
     $ctx->{pref_ou} = $self->_get_pref_lib() || $ctx->{search_ou};
     my $offset = $page * $limit;
-    my $metarecord = $cgi->param('metarecord');
     my $results; 
     my $tag_circs = $self->tag_circed_items;
     $self->timelog("Got search parameters");
@@ -408,12 +415,14 @@ sub load_rresults {
 
         $query = "$_ $query" for @facets;
 
-        $logger->activity("EGWeb: [search] $query");
+        my $ltag = $is_meta ? '[mmr search]' : '[bre search]';
+        $logger->activity("EGWeb: $ltag $query");
 
         try {
 
             my $method = 'open-ils.search.biblio.multiclass.query';
             $method .= '.staff' if $ctx->{is_staff};
+            $method =~ s/biblio/metabib/ if $is_meta;
 
             my $ses = OpenSRF::AppSession->create('open-ils.search');
 
@@ -440,6 +449,7 @@ sub load_rresults {
         my $rec_id = pop @$rec_ids;
         $cgi->delete('find_last');
         my $url = $cgi->url(-full => 1, -path => 1, -query => 1);
+        # TODO: metarecord => /rresults?metarecord=$mmr_id
         $url =~ s|/results|/record/$rec_id|;
         return $self->generic_redirect($url);
     }
@@ -448,12 +458,27 @@ sub load_rresults {
 
     $self->load_rresults_bookbag_item_notes($rec_ids) if $ctx->{bookbag};
 
+    my $fetch_recs = $rec_ids;
+
+    my $metarecord_master;
+    if ($metarecord) {
+        # when listing the contents of a metarecord, be sure to fetch
+        # the lead record for summary display.  Adding the ID to
+        # $fetch_recs lets us grab the record (if necessary) w/o it
+        # unintentially becoming a member of the result set.
+        my $mr = $e->retrieve_metabib_metarecord($metarecord);
+        push(@$fetch_recs, $mr->master_record)
+            unless grep {$_ eq $mr->master_record} @$fetch_recs;
+        $metarecord_master = $mr->master_record;
+    }
+
     $self->timelog("Calling get_records_and_facets()");
     my ($facets, @data) = $self->get_records_and_facets(
-        $rec_ids, $results->{facet_key}, 
+        $fetch_recs, $results->{facet_key}, 
         {
             flesh => '{holdings_xml,mra,acp,acnp,acns,bmp}',
             site => $site,
+            metarecord => $is_meta,
             depth => $depth,
             pref_lib => $ctx->{pref_ou},
         }
@@ -461,6 +486,7 @@ sub load_rresults {
     $self->timelog("Returned from get_records_and_facets()");
 
     if ($page == 0) {
+        # TODO: handle metarecords
         my $stat = $self->check_1hit_redirect($rec_ids);
         return $stat if $stat;
     }
@@ -470,19 +496,26 @@ sub load_rresults {
 
     # shove recs into context in search results order
     for my $rec_id (@$rec_ids) {
-        push(
-            @{$ctx->{records}},
-            grep { $_->{id} == $rec_id } @data
-        );
+        my ($rec) = grep { $_->{$id_key} == $rec_id } @data;
+        push(@{$ctx->{records}}, $rec);
+
+        $ctx->{metarecord_master} = $rec
+            if $metarecord_master and $metarecord_master eq $rec_id;
+
+        # MR's with multiple constituent records will have a
+        # null value in position 2 of the result set.  
+        my ($res_rec) = grep { $_->[0] == $rec_id} @{$results->{ids}};
+        $rec->{mr_has_multi} = !$res_rec->[2];
     }
 
     if ($tag_circs) {
         for my $rec (@{$ctx->{records}}) {
-            my ($res_rec) = grep { $_->[0] == $rec->{id} } @{$results->{ids}};
+            my ($res_rec) = grep { $_->[0] == $rec->{$id_key} } @{$results->{ids}};
             # index 1 in the per-record result array is a boolean which
             # indicates whether the record in question is in the users
             # accessible circ history list
-            $rec->{user_circulated} = 1 if $res_rec->[1];
+            my $index = $is_meta ? 3 : 1;
+            $rec->{user_circulated} = 1 if $res_rec->[$index];
         }
     }
 
diff --git a/Open-ILS/src/perlmods/lib/OpenILS/WWW/EGCatLoader/Util.pm b/Open-ILS/src/perlmods/lib/OpenILS/WWW/EGCatLoader/Util.pm
index ce009dd..2bc2c16 100644
--- a/Open-ILS/src/perlmods/lib/OpenILS/WWW/EGCatLoader/Util.pm
+++ b/Open-ILS/src/perlmods/lib/OpenILS/WWW/EGCatLoader/Util.pm
@@ -277,9 +277,13 @@ sub get_records_and_facets {
     $unapi_args->{depth} ||= $self->ctx->{aou_tree}->()->ou_type->depth;
     $unapi_args->{flesh_depth} ||= 5;
 
+    my $is_meta = delete $unapi_args->{metarecord};
+    my $unapi_type = $is_meta ? 'unapi.mmr' : 'unapi.bre';
+
     $unapi_cache ||= OpenSRF::Utils::Cache->new('global');
     my $unapi_cache_key_suffix = join(
         '_',
+        $is_meta || 0,
         $unapi_args->{site},
         $unapi_args->{depth},
         $unapi_args->{flesh_depth},
@@ -299,36 +303,59 @@ sub get_records_and_facets {
             $outer_self->timelog("get_records_and_facets(): got response content");
 
             # Protect against requests for non-existent records
-            return unless $data->{'unapi.bre'};
+            return unless $data->{$unapi_type};
 
-            my $xml = XML::LibXML->new->parse_string($data->{'unapi.bre'})->documentElement;
+            my $xml = XML::LibXML->new->parse_string($data->{$unapi_type})->documentElement;
 
             $outer_self->timelog("get_records_and_facets(): parsed xml");
             # Protect against legacy invalid MARCXML that might not have a 901c
             my $bre_id;
+            my $mmr_id;
             my $bre_id_nodes =  $xml->find('*[@tag="901"]/*[@code="c"]');
             if ($bre_id_nodes) {
                 $bre_id =  $bre_id_nodes->[0]->textContent;
             } else {
                 $logger->warn("Missing 901 subfield 'c' in " . $xml->toString());
             }
-            $tmp_data{$bre_id} = {id => $bre_id, marc_xml => $xml};
 
-            if ($bre_id) {
+            if ($is_meta) {
+                # extract metarecord ID from mmr.unapi tag
+                for my $node ($xml->getElementsByTagName('abbr')) {
+                    my $title = $node->getAttribute('title');
+                    ($mmr_id = $title) =~ 
+                        s/tag:open-ils.org:U2\@mmr\/(\d+)\/.*/$1/g;
+                    last if $mmr_id;
+                }
+            }
+
+            my $rec_id = $mmr_id ? $mmr_id : $bre_id;
+            $tmp_data{$rec_id} = {
+                id => $rec_id, 
+                bre_id => $bre_id, 
+                mmr_id => $mmr_id,
+                marc_xml => $xml
+            };
+
+            if ($rec_id) {
                 # Let other backends grab our data now that we're done.
-                my $key = 'TPAC_unapi_cache_'.$bre_id.'_'.$unapi_cache_key_suffix;
+                my $key = 'TPAC_unapi_cache_'.$rec_id.'_'.$unapi_cache_key_suffix;
                 my $cache_data = $unapi_cache->get_cache($key);
                 if ($$cache_data{running}) {
-                    $unapi_cache->put_cache($key, { id => $bre_id, marc_xml => $data->{'unapi.bre'} }, 10);
+                    $unapi_cache->put_cache($key, {
+                        bre_id => $bre_id,
+                        mmr_id => $mmr_id,
+                        id => $rec_id, 
+                        marc_xml => $data->{$unapi_type} 
+                    }, 10);
                 }
             }
 
-
             $outer_self->timelog("get_records_and_facets(): end of success handler");
         }
     );
 
-    $self->timelog("get_records_and_facets(): about to call unapi.bre via json_query (rec_ids has " . scalar(@$rec_ids));
+    $self->timelog("get_records_and_facets(): about to call ".
+        "$unapi_type via json_query (rec_ids has " . scalar(@$rec_ids));
 
     my @loop_recs = @$rec_ids;
     my %rec_timeout;
@@ -359,19 +386,26 @@ sub get_records_and_facets {
             $tmp_data{$unapi_data->{id}} = $unapi_data;
         } else { # we're the first or we timed out. success_handler will populate the real value
             $unapi_cache->put_cache($unapi_cache_key, { running => $$ }, 10);
+
+            my $sdepth = $unapi_args->{flesh_depth};
+            my $slimit = "acn=>$sdepth,acp=>$sdepth";
+            $slimit .= ",bre=>$sdepth" if $is_meta;
+            my $flesh = $unapi_args->{flesh} || '';
+
+            # tag the record with the MR id
+            $flesh =~ s/}$/,mmr.unapi}/g if $is_meta;
+
             $ses->request(
                 'open-ils.cstore.json_query',
                  {from => [
-                    'unapi.bre', $bid, 'marcxml','record', 
-                    $unapi_args->{flesh}, 
+                    $unapi_type, $bid, 'marcxml','record', $flesh,
                     $unapi_args->{site}, 
                     $unapi_args->{depth}, 
-                    'acn=>' . $unapi_args->{flesh_depth} . ',acp=>' . $unapi_args->{flesh_depth}, 
+                    $slimit,
                     undef, undef, $unapi_args->{pref_lib}
                 ]}
             );
         }
-
     }
 
 
diff --git a/Open-ILS/src/perlmods/lib/OpenILS/WWW/EGWeb.pm b/Open-ILS/src/perlmods/lib/OpenILS/WWW/EGWeb.pm
index 57cb02b..8b72e53 100644
--- a/Open-ILS/src/perlmods/lib/OpenILS/WWW/EGWeb.pm
+++ b/Open-ILS/src/perlmods/lib/OpenILS/WWW/EGWeb.pm
@@ -186,7 +186,10 @@ sub load_context {
         parse_accept_lang($r->headers_in->get('Accept-Language'));
 
     # set the editor default locale for each page load
-    OpenSRF::AppSession->default_locale(parse_eg_locale($ctx->{locale}));
+    my $ses_locale = parse_eg_locale($ctx->{locale});
+    OpenSRF::AppSession->default_locale($ses_locale);
+    # give templates access to the en-US style locale
+    $ctx->{eg_locale} = $ses_locale;
 
     my $mprefix = $ctx->{media_prefix};
     if($mprefix and $mprefix !~ /^http/ and $mprefix !~ /^\//) {
diff --git a/Open-ILS/src/templates/opac/css/style.css.tt2 b/Open-ILS/src/templates/opac/css/style.css.tt2
index e57741f..0c5e71b 100644
--- a/Open-ILS/src/templates/opac/css/style.css.tt2
+++ b/Open-ILS/src/templates/opac/css/style.css.tt2
@@ -982,6 +982,7 @@ table.acct_notes th {
 div.adv_search_available {
     margin-top: 1em;
 }
+
 #myopac_loading {
     width:100%;
     text-align:center;
diff --git a/Open-ILS/src/templates/opac/myopac/holds.tt2 b/Open-ILS/src/templates/opac/myopac/holds.tt2
index 81ebc34..0a7b04a 100644
--- a/Open-ILS/src/templates/opac/myopac/holds.tt2
+++ b/Open-ILS/src/templates/opac/myopac/holds.tt2
@@ -150,8 +150,10 @@
                     </td>
                     <td>
                         <div class="format_icon">
-                            [% IF attrs.format_icon %]
-                            <img title="[% attrs.format_label | html %]" alt="[% attrs.format_label | html %]" src="[% attrs.format_icon %]" />
+                            [% IF attrs.all_formats.size %]
+                                [% FOR format IN attrs.all_formats %]
+                                <img title="[% format.label | html %]" alt="[% format.label | html %]" src="[% format.icon %]" />
+                                [% END %]
                             [% END %]
                         </div>
                     </td>
diff --git a/Open-ILS/src/templates/opac/myopac/holds/edit.tt2 b/Open-ILS/src/templates/opac/myopac/holds/edit.tt2
index 2fd2e92..e0981bc 100644
--- a/Open-ILS/src/templates/opac/myopac/holds/edit.tt2
+++ b/Open-ILS/src/templates/opac/myopac/holds/edit.tt2
@@ -2,6 +2,7 @@
     PROCESS "opac/parts/misc_util.tt2";
     PROCESS "opac/parts/hold_status.tt2";
     PROCESS "opac/parts/org_selector.tt2";
+    PROCESS "opac/parts/metarecord_hold_filters.tt2";
     WRAPPER "opac/parts/myopac/base.tt2";
     myopac_page = "holds"; # in this case, just for tab coloring.
 
@@ -100,6 +101,14 @@
                         </td>
                     </tr>
                     [% END %]
+                     
+                    <tr><td colspan='2'>
+                    [% IF hold.metarecord_filters.formats.size OR
+                        hold.metarecord_filters.langs.size > 1;
+                            PROCESS metarecord_hold_filters_selector 
+                                hold_data=hold; END %]
+                    </td></tr>
+
                     <tr>
                         <td colspan="2" class="hold-editor-controls">
                             <a href="[% ctx.opac_root %]/myopac/holds"><button 
diff --git a/Open-ILS/src/templates/opac/parts/advanced/search.tt2 b/Open-ILS/src/templates/opac/parts/advanced/search.tt2
index d5a663f..eb863a7 100644
--- a/Open-ILS/src/templates/opac/parts/advanced/search.tt2
+++ b/Open-ILS/src/templates/opac/parts/advanced/search.tt2
@@ -99,8 +99,22 @@
                 CASE "sort_selector";
                     INCLUDE "opac/parts/filtersort.tt2"
                         value=CGI.param('sort') class='results_header_sel';
+                    %]
 
-                CASE "copy_location" %]
+                    [% IF NOT metarecords.disabled %]
+                        <br/><!-- <br> may seem redundant, but it allows the
+                            <input> (below) to drop down inline w/ its label -->
+                        <div class="adv_search_available">
+                            <input type='checkbox' name="modifier" value="metabib"
+                              [%- CGI.param('modifier').grep('metabib').size ? 
+                                    ' checked="checked"' : '' %]
+                                id='opac.result.ismetabib' />
+                            <label for='opac.result.ismetabib'>
+                                [% l("Group Formats and Editions") %]</label>
+                        </div>
+                    [% END %]
+
+                [% CASE "copy_location" %]
                     <select id="adv_copy_location_selector" 
                         aria-label="[% l('Select Shelving Location') %]"
                         name="fi:locations" size="3" multiple="multiple">
diff --git a/Open-ILS/src/templates/opac/parts/config.tt2 b/Open-ILS/src/templates/opac/parts/config.tt2
index 77e1936..509d6a1 100644
--- a/Open-ILS/src/templates/opac/parts/config.tt2
+++ b/Open-ILS/src/templates/opac/parts/config.tt2
@@ -162,4 +162,11 @@ ctx.google_books_preview = 0;
 #
 # ctx.maintenance_message = "The system will not be available February 29, 2104.";
 
+
+##############################################################################
+# Metarecords configuration
+# metarecords.disabled = 1; # disable all metarecord access points
+##############################################################################
+
+
 %]
diff --git a/Open-ILS/src/templates/opac/parts/metarecord_hold_filters.tt2 b/Open-ILS/src/templates/opac/parts/metarecord_hold_filters.tt2
new file mode 100644
index 0000000..452feed
--- /dev/null
+++ b/Open-ILS/src/templates/opac/parts/metarecord_hold_filters.tt2
@@ -0,0 +1,89 @@
+[%#
+Draws the format multi-select and the language multi-select for
+limiting the set of desired records for a given metarecord.
+%]
+
+<style>
+  /* TODO: MOVE ME */
+  .metarecord_filters {
+      padding: 5px;
+      margin-top: 5px;
+      border-bottom: 1px solid #333;
+      border-top: 1px solid #333;
+  }
+  .metarecord_filter_container {
+    float : left;
+    margin-right: 10px;
+  }
+  .metarecord_filter_container select {
+    padding: 2px;
+    width: 13em; /* consistent w/ adv search selectors */
+  }
+  .metarecord_filter_header {
+    padding-bottom: 5px;
+  }
+</style>
+
+[% BLOCK metarecord_hold_filters_selector;
+    # in edit mode, pull the target from the existing hold
+    target_id = hold_data.target.id || hold_data.hold.hold.target;
+
+    selected_formats = {};
+    selected_langs = {};
+    FOR fmt IN hold_data.metarecord_selected_filters.formats;
+        code = fmt.code;
+        selected_formats.$code = fmt;
+    END;
+    FOR lang IN hold_data.metarecord_selected_filters.langs;
+        code = lang.code;
+        selected_langs.$code = lang;
+    END;
+%]
+
+<div class="metarecord_filters">
+  <div class="metarecord_filter_container">
+    <div class="metarecord_filter_header">
+      <div>[% l('Select your desired format(s).') %]</div>
+    </div>
+    <select multiple='multiple' 
+      name="metarecord_formats_[% target_id %]">
+      [% FOR ccvm IN 
+        hold_data.metarecord_filters.formats.sort('search_label') %]
+        <option value="[% ccvm.code %]"[%- code = ccvm.code; 
+            IF selected_formats.$code %] selected='selected'[% END -%]>
+          [% ccvm.search_label | html %]
+        </option>
+      [% END %]
+    </select>
+  </div>
+  [% IF hold_data.metarecord_filters.langs.size;
+        my_lang = ctx.get_i18n_l(ctx.eg_locale).marc_code;
+  %]
+  <div class="metarecord_filter_container">
+    <div class="metarecord_filter_header">
+      [% l('Select your desired language(s)') %]
+    </div>
+    <select multiple='multiple' 
+      name="metarecord_langs_[% target_id %]">
+      [% FOR lang_ccvm IN hold_data.metarecord_filters.langs.sort('value') %]
+        [%  selected = 0; 
+            code = lang_ccvm.code;
+            IF selected_langs.size;
+                # user has already selected their preferred language(s)
+                SET selected = 1 IF selected_langs.$code;
+            ELSE;
+                # no prefered language selected, default to current locale
+                SET selected = 1 IF code == my_lang;
+            END;
+        %]
+        <option value="[% lang_ccvm.code %]"[%- 
+            IF selected %] selected='selected'[%- END %]>
+          [% lang_ccvm.value | html %]
+        </option>
+      [% END %]
+    </select>
+  </div>
+  [% END %]
+  <div class="clear-both">&nbsp;</div>
+</div>
+[% END # metarecord_hold_filters_selector %]
diff --git a/Open-ILS/src/templates/opac/parts/misc_util.tt2 b/Open-ILS/src/templates/opac/parts/misc_util.tt2
index 78898fb..589a2c1 100644
--- a/Open-ILS/src/templates/opac/parts/misc_util.tt2
+++ b/Open-ILS/src/templates/opac/parts/misc_util.tt2
@@ -79,18 +79,12 @@
         END;
     END;
 
-    # Get CCVM labels
     BLOCK get_ccvm_label;
-        IF !ctx.ccvm_cache.$id;
-            fetch_ccvm = ctx.search_ccvm('id', id);
-            IF fetch_ccvm;
-                ctx.ccvm_cache.$id = fetch_ccvm.0;
-            END;
-        END;
-        IF search_label and ctx.ccvm_cache.$id.search_label;
-            ctx.ccvm_cache.$id.search_label;
+        ccvm = ctx.get_ccvm(id); # caches internally
+        IF search_label and ccvm.search_label;
+            ccvm.search_label;
         ELSE;
-            ctx.ccvm_cache.$id.value;
+            ccvm.$id.value;
         END;
     END;
 
@@ -440,17 +434,24 @@
 
         # "mattype" == "custom marc format specifier"
         icon_style = ctx.get_cgf('opac.icon_attr').value || 'item_type';
-        node = xml.findnodes(
-            '//*[local-name()="attributes"]/*[local-name()="field"][@name="' _ icon_style _ '"]');
-        IF node AND node.textContent;
-            type = node.textContent;
-            args.format_label = PROCESS get_ccvm_label id=node.getAttribute('cvmid') search_label=1;
-            IF !args.format_label;
-                args.format_label = node.getAttribute('coded-value');
+        formats_xpath = '//*[local-name()="attributes"]/*[local-name()="field"][@name="' _ icon_style _ '"]';
+
+        args.all_formats = [];
+        FOR node IN xml.findnodes(formats_xpath);
+            IF node AND node.textContent;
+                type = node.textContent;
+                label = PROCESS get_ccvm_label id=node.getAttribute('cvmid') search_label=1;
+                itemtype = schema_typemap.$type || 'CreativeWork';
+                icon = ctx.media_prefix _ '/images/format_icons/' _ icon_style _ '/' _ type _ '.png';
+                # collect all formats for metarecord support
+                args.all_formats.push({label => label, icon => icon, itemtype => itemtype});
+                IF !args.format_label;
+                    # use the first format as the default
+                    args.format_label = label; 
+                    args.schema.itemtype = itemtype;
+                    args.format_icon = icon;
+                END;
             END;
-            args.schema.itemtype = schema_typemap.$type || 'CreativeWork';
-            args.format_icon = ctx.media_prefix _ '/images/format_icons/' _ icon_style _ '/' _ type _ '.png';
-            LAST;
         END;
 	
         args.bibid = [];
diff --git a/Open-ILS/src/templates/opac/parts/place_hold.tt2 b/Open-ILS/src/templates/opac/parts/place_hold.tt2
index f12cd56..390b9ea 100644
--- a/Open-ILS/src/templates/opac/parts/place_hold.tt2
+++ b/Open-ILS/src/templates/opac/parts/place_hold.tt2
@@ -1,5 +1,6 @@
 [%  PROCESS "opac/parts/misc_util.tt2";
     PROCESS "opac/parts/hold_error_messages.tt2";
+    PROCESS "opac/parts/metarecord_hold_filters.tt2";
 %]
 
 <div id='holds_box' class='canvas' style='margin-top: 6px;'>
@@ -63,6 +64,16 @@
                         <input type='hidden' name='part' value=''/>
                         [% END %]
                     [% END %]
+                    [% IF NOT metarecords.disabled %]
+                        [% IF CGI.param('hold_type') == 'T' AND hdata.record.metarecord %]
+                            <a href="[% mkurl('', {hold_type => 'M', hold_target => hdata.record.metarecord.id}) %]">
+                                [% l('Advanced Hold Options') %]</a>
+                        [% END %]
+                        [% IF hdata.metarecord_filters.formats.size OR # should this be size > 1
+                            hdata.metarecord_filters.langs.size > 1;
+                            PROCESS metarecord_hold_filters_selector hold_data=hdata;
+                        END;
+                    END %]
                 </td>
             </tr>
         [% END %]
diff --git a/Open-ILS/src/templates/opac/parts/result/table.tt2 b/Open-ILS/src/templates/opac/parts/result/table.tt2
index 0922a70..b927de2 100644
--- a/Open-ILS/src/templates/opac/parts/result/table.tt2
+++ b/Open-ILS/src/templates/opac/parts/result/table.tt2
@@ -37,21 +37,46 @@
                             IF CGI.param('detail_record_view');
                                 attrs.title = attrs.title_extended;
                             END;
+                            # note: rec.id refers to the record identifier, regardless
+                            # of the type of record. i.e. rec.id = mmr_id ? mmr_id : bre_id
+                            IF rec.mmr_id;
+                                IF rec.mr_has_multi;
+                                    # metarecords link to record list page
+                                    record_url = mkurl(ctx.opac_root _ '/results', 
+                                        {metarecord => rec.mmr_id}, ['page']);
+                                ELSE;
+                                    # for MR, bre_id refers to the master and in
+                                    # this case, only, record
+                                    record_url = mkurl(ctx.opac_root _ '/record/' _ rec.bre_id);
+                                END;
+                                hold_type = 'M';
+                            ELSE;
+                                record_url = mkurl(ctx.opac_root _ '/record/' _ rec.bre_id);
+                                hold_type = 'T';
+                            END;
                     -%]
                         <tr class="result_table_row">
                                             <td class="results_row_count" name="results_row_count">[%
                                                     result_count; result_count = result_count + 1
                                                 %].</td>
                                             <td class='result_table_pic_header'>
-                                                <a href="[% mkurl(ctx.opac_root _ '/record/' _ rec.id) %]"><img alt="[% l('Image of item') %]"
+                                                <a href="[% record_url %]"><img alt="[% l('Image of item') %]"
                                                         name='item_jacket' class='result_table_pic' width="55"
-                                                        src='[% ctx.media_prefix %]/opac/extras/ac/jacket/small/r/[% rec.id | uri %]' /></a><br />
+                                                        src='[% ctx.media_prefix %]/opac/extras/ac/jacket/small/r/[% rec.bre_id | uri %]' /></a><br />
                                             </td>
                                             <td class='result_table_title_cell' name='result_table_title_cell'>
                                                <div class="result_metadata">
-                                                    <abbr class="unapi-id" title='tag:[% ctx.hostname %],[% date.format(date.now, '%Y') %]:biblio-record_entry/[% rec.id %]'></abbr>
+                                                    [% IF rec.mmr_id %]
+                                                    <abbr class="unapi-id" 
+                                                      title='tag:[% ctx.hostname %],[% date.format(date.now, '%Y') %]:metabib-metarecord/[% rec.mmr_id %]'>
+                                                    </abbr>
+                                                    [% ELSE %]
+                                                    <abbr class="unapi-id" 
+                                                      title='tag:[% ctx.hostname %],[% date.format(date.now, '%Y') %]:biblio-record_entry/[% rec.bre_id %]'>
+                                                    </abbr>
+                                                    [% END %]
                                                     <a class='record_title search_link' name='record_[% rec.id %]'
-                                                        href="[% mkurl(ctx.opac_root _ '/record/' _ rec.id) %]"
+                                                        href="[% record_url %]"
                                                         [% html_text_attr('title', l('Display record details for "[_1]"', attrs.title)) %]
                                                         >[% attrs.title | html %]</a>
 [%-
@@ -96,7 +121,12 @@ END;
                                                     </div>
                                                     <div class='result_table_title_cell'>
                                                     [%- IF attrs.format_label; %]
-                                                        <img title="[% attrs.format_label | html %]" alt="[% attrs.format_label | html %]" src="[% attrs.format_icon %]" /> [% attrs.format_label; %]
+                                                        [% FOR format IN attrs.all_formats %]
+                                                            <img title="[% format.label | html %]" 
+                                                                alt="[% format.label | html %]" 
+                                                                src="[% format.icon %]" /> 
+                                                            [% format.label | html %]
+                                                        [% END %]
                                                     [%- END %]
                                                     [%- UNLESS CGI.param('detail_record_view')
                                                             OR (show_more_details.default == 'true'
@@ -294,7 +324,8 @@ END;
 %]
                                                         <div class="results_aux_utils place_hold"><a
                                                                 href="[% mkurl(ctx.opac_root _ '/place_hold',
-                                                                    {hold_target => rec.id, hold_type => 'T', hold_source_page => mkurl()}, ['query']) %]"
+                                                                    {hold_target => rec.id, hold_type => hold_type, 
+                                                                      hold_source_page => mkurl()}, ['query']) %]"
                                                                 [% html_text_attr('title', l('Place Hold on [_1]', attrs.title)) %]
                                                                     class="no-dec"><img
                                                                 src="[% ctx.media_prefix %]/images/green_check.png"
diff --git a/Open-ILS/src/templates/opac/results.tt2 b/Open-ILS/src/templates/opac/results.tt2
index d1a3da2..6bd1c94 100644
--- a/Open-ILS/src/templates/opac/results.tt2
+++ b/Open-ILS/src/templates/opac/results.tt2
@@ -21,7 +21,19 @@
     [% INCLUDE "opac/parts/searchbar.tt2" took_care_of_form=1 %]
     <h3 class="sr-only">[% l('Additional search filters and navigation') %]</h3>
     <div class="almost-content-wrapper">
-        <div id="results_header_bar">
+
+        [%# hide the header bar when displaying metarecord constituents 
+          instead of skipping it altogether to allow the search form
+          variables to propagate %]
+        [% IF ctx.metarecord;
+          mr_attrs = {marc_xml => ctx.metarecord_master.marc_xml};
+          PROCESS get_marc_attrs args=mr_attrs %]
+          <div class="results_header_lbl">
+            [% l('Viewing Results for Grouped Record: [_1]', 
+                mr_attrs.title) | html %]
+          </div>
+        [% END %]
+        <div class="results_header_bar[%- IF ctx.metarecord %] hidden[% END -%]">
             <div id="results_header_inner">
                 <div class="results_header_btns">
                     <a href="[% mkurl(ctx.opac_root _ '/home', {$loc_name => loc_value}, 1) %]">[% l('Another Search') %]</a>
@@ -53,10 +65,18 @@
 
                     <label class="results_header_lbl" for="limit_to_available">
                         <input type="checkbox" id="limit_to_available" name="modifier" value="available"
-                            onchange="limit_to_avail_onchange(this, true)"
+                            onchange="search_modifier_onchange('available', this, true)"
                             [% CGI.param('modifier').grep('available').size ? ' checked="checked"' : '' %] />
                         [% l('Limit to available items') %]
                     </label>
+                    [% IF NOT metarecords.disabled %]
+                        <label class="results_header_lbl">
+                            <input type="checkbox" name="modifier" value="metabib"
+                                onchange="search_modifier_onchange('metabib', this, true)"
+                                [% CGI.param('modifier').grep('metabib').size ? ' checked="checked"' : '' %] />
+                            [% l('Group Formats and Editions') %]
+                        </label>
+                    [% END %]
                     [% IF CGI.param('detail_record_view') %]
                         <input type="hidden" name="detail_record_view" value="1" />
                     [% END %]
diff --git a/Open-ILS/web/js/ui/default/opac/simple.js b/Open-ILS/web/js/ui/default/opac/simple.js
index 6e3d29f..b491586 100644
--- a/Open-ILS/web/js/ui/default/opac/simple.js
+++ b/Open-ILS/web/js/ui/default/opac/simple.js
@@ -74,10 +74,11 @@ function select_all_checkboxes(name, checked) {
     }
 }
 
-function limit_to_avail_onchange(checkbox, submitOnChange) {
+function search_modifier_onchange(type, checkbox, submitOnChange) {
     if (checkbox.form._adv && !checkbox.checked) {
         var search_box = $('search_box');
-        search_box.value = search_box.value.replace(/#available ?/g, "");
+        var reg = new RegExp('#' + type + ' ?', 'g');
+        search_box.value = search_box.value.replace(reg, "");
     }
 
     if (submitOnChange) {  

commit e6421bcacd6e7c8cd13e871e1961f14c939151c6
Author: Bill Erickson <berick at esilibrary.com>
Date:   Mon Feb 10 14:00:21 2014 -0500

    LP#1053397 DB metarecord seed data
    
    Seed data for representing the holdable formats for metarecords.  This
    is a subset of the "icon_format" collection, excluding the electronic
    resources.
    
    Signed-off-by: Bill Erickson <berick at esilibrary.com>
    Signed-off-by: Mike Rylander <mrylander at gmail.com>
    Signed-off-by: Dan Wells <dbw2 at calvin.edu>

diff --git a/Open-ILS/src/sql/Pg/950.data.seed-values.sql b/Open-ILS/src/sql/Pg/950.data.seed-values.sql
index 32fd3ad..557dcff 100644
--- a/Open-ILS/src/sql/Pg/950.data.seed-values.sql
+++ b/Open-ILS/src/sql/Pg/950.data.seed-values.sql
@@ -6085,6 +6085,13 @@ INSERT INTO config.record_attr_definition (name,label,fixed_field,multi) values
 INSERT INTO config.record_attr_definition (name,label,phys_char_sf) values ('vr_format','Videorecording format',72);
 INSERT INTO config.record_attr_definition (name,label,sorter,filter,tag,multi) values ('titlesort','Title',TRUE,FALSE,'tnf',FALSE);
 INSERT INTO config.record_attr_definition (name,label,sorter,filter,tag,sf_list,multi) values ('authorsort','Author',TRUE,FALSE,'1%','abcdefgklmnopqrstvxyz',FALSE);
+INSERT INTO config.record_attr_definition (name, label, phys_char_sf)
+    VALUES ('sr_format', oils_i18n_gettext('sr_format', 'Sound recording format', 'crad', 'label'), '62');
+INSERT INTO config.record_attr_definition (name, label, multi, filter, composite) 
+    VALUES ('icon_format', oils_i18n_gettext('icon_format', 'OPAC Format Icons', 'crad', 'label'), TRUE, TRUE, TRUE);
+INSERT INTO config.record_attr_definition (name, label, multi, filter, composite) 
+    VALUES ('mr_hold_format', oils_i18n_gettext('mr_hold_format', 'Metarecord Hold Formats', 'crad', 'label'),
+    TRUE, TRUE, TRUE
 
 -- TO-DO: Auto-generate these values from CLDR
 -- XXX These are the values used in MARC records ... does that match CLDR, including deprecated languages?
@@ -6627,6 +6634,7 @@ INSERT INTO config.coded_value_map (id, ctype, code, value) VALUES
     (527, 'item_form', 'f', oils_i18n_gettext('527', 'Braille', 'ccvm', 'value')),
     (528, 'item_form', 'r', oils_i18n_gettext('528', 'Regular print reproduction', 'ccvm', 'value')),
     (529, 'item_form', 's', oils_i18n_gettext('529', 'Electronic', 'ccvm', 'value'));
+    -- see below for more item_form entries
 
 INSERT INTO config.coded_value_map (id, ctype, code, value) VALUES
     (530, 'bib_level', 'a', oils_i18n_gettext('530', 'Monographic component part', 'ccvm', 'value')),
@@ -6659,95 +6667,244 @@ INSERT INTO config.coded_value_map(id, ctype, code, value) VALUES
     (555, 'vr_format', 'z', oils_i18n_gettext('555', 'Other', 'ccvm', 'value')),
     (556, 'vr_format', ' ', oils_i18n_gettext('556', 'Unspecified', 'ccvm', 'value'));
 
-INSERT INTO config.record_attr_definition (name, label, phys_char_sf)
-VALUES (
-    'sr_format',
-    oils_i18n_gettext('sr_format', 'Sound recording format', 'crad', 'label'),
-    '62'
-);
-
 INSERT INTO config.coded_value_map (id, ctype, code, value) VALUES
-(557, 'sr_format', 'a', oils_i18n_gettext(557, '16 rpm', 'ccvm', 'value')),
-(558, 'sr_format', 'b', oils_i18n_gettext(558, '33 1/3 rpm', 'ccvm', 'value')),
-(559, 'sr_format', 'c', oils_i18n_gettext(559, '45 rpm', 'ccvm', 'value')),
-(560, 'sr_format', 'f', oils_i18n_gettext(560, '1.4 m. per second', 'ccvm', 'value')),
-(561, 'sr_format', 'd', oils_i18n_gettext(561, '78 rpm', 'ccvm', 'value')),
-(562, 'sr_format', 'e', oils_i18n_gettext(562, '8 rpm', 'ccvm', 'value')),
-(563, 'sr_format', 'l', oils_i18n_gettext(563, '1 7/8 ips', 'ccvm', 'value')),
-(586, 'item_form', 'o', oils_i18n_gettext('586', 'Online', 'ccvm', 'value')),
-(587, 'item_form', 'q', oils_i18n_gettext('587', 'Direct electronic', 'ccvm', 'value'));
+    (557, 'sr_format', 'a', oils_i18n_gettext(557, '16 rpm', 'ccvm', 'value')),
+    (558, 'sr_format', 'b', oils_i18n_gettext(558, '33 1/3 rpm', 'ccvm', 'value')),
+    (559, 'sr_format', 'c', oils_i18n_gettext(559, '45 rpm', 'ccvm', 'value')),
+    (560, 'sr_format', 'f', oils_i18n_gettext(560, '1.4 m. per second', 'ccvm', 'value')),
+    (561, 'sr_format', 'd', oils_i18n_gettext(561, '78 rpm', 'ccvm', 'value')),
+    (562, 'sr_format', 'e', oils_i18n_gettext(562, '8 rpm', 'ccvm', 'value')),
+    (563, 'sr_format', 'l', oils_i18n_gettext(563, '1 7/8 ips', 'ccvm', 'value'));
 
 INSERT INTO config.coded_value_map
-    (id, ctype, code, value, search_label) VALUES
-(564, 'icon_format', 'book',
+    (id, ctype, code, value, search_label) VALUES 
+(564, 'icon_format', 'book', 
     oils_i18n_gettext(564, 'Book', 'ccvm', 'value'),
     oils_i18n_gettext(564, 'Book', 'ccvm', 'search_label')),
-(565, 'icon_format', 'braille',
+(565, 'icon_format', 'braille', 
     oils_i18n_gettext(565, 'Braille', 'ccvm', 'value'),
     oils_i18n_gettext(565, 'Braille', 'ccvm', 'search_label')),
-(566, 'icon_format', 'software',
+(566, 'icon_format', 'software', 
     oils_i18n_gettext(566, 'Software and video games', 'ccvm', 'value'),
     oils_i18n_gettext(566, 'Software and video games', 'ccvm', 'search_label')),
-(567, 'icon_format', 'dvd',
+(567, 'icon_format', 'dvd', 
     oils_i18n_gettext(567, 'DVD', 'ccvm', 'value'),
     oils_i18n_gettext(567, 'DVD', 'ccvm', 'search_label')),
-(568, 'icon_format', 'ebook',
+(568, 'icon_format', 'ebook', 
     oils_i18n_gettext(568, 'E-book', 'ccvm', 'value'),
     oils_i18n_gettext(568, 'E-book', 'ccvm', 'search_label')),
-(569, 'icon_format', 'eaudio',
+(569, 'icon_format', 'eaudio', 
     oils_i18n_gettext(569, 'E-audio', 'ccvm', 'value'),
     oils_i18n_gettext(569, 'E-audio', 'ccvm', 'search_label')),
-(570, 'icon_format', 'kit',
+(570, 'icon_format', 'kit', 
     oils_i18n_gettext(570, 'Kit', 'ccvm', 'value'),
     oils_i18n_gettext(570, 'Kit', 'ccvm', 'search_label')),
-(571, 'icon_format', 'map',
+(571, 'icon_format', 'map', 
     oils_i18n_gettext(571, 'Map', 'ccvm', 'value'),
     oils_i18n_gettext(571, 'Map', 'ccvm', 'search_label')),
-(572, 'icon_format', 'microform',
+(572, 'icon_format', 'microform', 
     oils_i18n_gettext(572, 'Microform', 'ccvm', 'value'),
     oils_i18n_gettext(572, 'Microform', 'ccvm', 'search_label')),
-(573, 'icon_format', 'score',
+(573, 'icon_format', 'score', 
     oils_i18n_gettext(573, 'Music Score', 'ccvm', 'value'),
     oils_i18n_gettext(573, 'Music Score', 'ccvm', 'search_label')),
-(574, 'icon_format', 'picture',
+(574, 'icon_format', 'picture', 
     oils_i18n_gettext(574, 'Picture', 'ccvm', 'value'),
     oils_i18n_gettext(574, 'Picture', 'ccvm', 'search_label')),
-(575, 'icon_format', 'equip',
+(575, 'icon_format', 'equip', 
     oils_i18n_gettext(575, 'Equipment, games, toys', 'ccvm', 'value'),
     oils_i18n_gettext(575, 'Equipment, games, toys', 'ccvm', 'search_label')),
-(576, 'icon_format', 'serial',
+(576, 'icon_format', 'serial', 
     oils_i18n_gettext(576, 'Serials and magazines', 'ccvm', 'value'),
     oils_i18n_gettext(576, 'Serials and magazines', 'ccvm', 'search_label')),
-(577, 'icon_format', 'vhs',
+(577, 'icon_format', 'vhs', 
     oils_i18n_gettext(577, 'VHS', 'ccvm', 'value'),
     oils_i18n_gettext(577, 'VHS', 'ccvm', 'search_label')),
-(578, 'icon_format', 'evideo',
+(578, 'icon_format', 'evideo', 
     oils_i18n_gettext(578, 'E-video', 'ccvm', 'value'),
     oils_i18n_gettext(578, 'E-video', 'ccvm', 'search_label')),
-(579, 'icon_format', 'cdaudiobook',
+(579, 'icon_format', 'cdaudiobook', 
     oils_i18n_gettext(579, 'CD Audiobook', 'ccvm', 'value'),
     oils_i18n_gettext(579, 'CD Audiobook', 'ccvm', 'search_label')),
-(580, 'icon_format', 'cdmusic',
+(580, 'icon_format', 'cdmusic', 
     oils_i18n_gettext(580, 'CD Music recording', 'ccvm', 'value'),
     oils_i18n_gettext(580, 'CD Music recording', 'ccvm', 'search_label')),
-(581, 'icon_format', 'casaudiobook',
+(581, 'icon_format', 'casaudiobook', 
     oils_i18n_gettext(581, 'Cassette audiobook', 'ccvm', 'value'),
     oils_i18n_gettext(581, 'Cassette audiobook', 'ccvm', 'search_label')),
 (582, 'icon_format', 'casmusic',
     oils_i18n_gettext(582, 'Audiocassette music recording', 'ccvm', 'value'),
     oils_i18n_gettext(582, 'Audiocassette music recording', 'ccvm', 'search_label')),
-(583, 'icon_format', 'phonospoken',
+(583, 'icon_format', 'phonospoken', 
     oils_i18n_gettext(583, 'Phonograph spoken recording', 'ccvm', 'value'),
     oils_i18n_gettext(583, 'Phonograph spoken recording', 'ccvm', 'search_label')),
-(584, 'icon_format', 'phonomusic',
+(584, 'icon_format', 'phonomusic', 
     oils_i18n_gettext(584, 'Phonograph music recording', 'ccvm', 'value'),
     oils_i18n_gettext(584, 'Phonograph music recording', 'ccvm', 'search_label')),
-(585, 'icon_format', 'lpbook',
+(585, 'icon_format', 'lpbook', 
     oils_i18n_gettext(585, 'Large Print Book', 'ccvm', 'value'),
-    oils_i18n_gettext(585, 'Large Print Book', 'ccvm', 'search_label'))
-;
+    oils_i18n_gettext(585, 'Large Print Book', 'ccvm', 'search_label'));
+
+INSERT INTO config.coded_value_map (id, ctype, code, value) VALUES 
+(586, 'item_form', 'o', oils_i18n_gettext('586', 'Online', 'ccvm', 'value')),
+(587, 'item_form', 'q', oils_i18n_gettext('587', 'Direct electronic', 'ccvm', 'value'));
+
+-- these formats are a subset of the "icon_format" attribute,
+-- modified to exclude electronic resources, which are not holdable
+INSERT INTO config.coded_value_map
+    (id, ctype, code, value, search_label) VALUES 
+(588, 'mr_hold_format', 'book', 
+    oils_i18n_gettext(588, 'Book', 'ccvm', 'value'),
+    oils_i18n_gettext(588, 'Book', 'ccvm', 'search_label')),
+(589, 'mr_hold_format', 'braille', 
+    oils_i18n_gettext(589, 'Braille', 'ccvm', 'value'),
+    oils_i18n_gettext(589, 'Braille', 'ccvm', 'search_label')),
+(590, 'mr_hold_format', 'software', 
+    oils_i18n_gettext(590, 'Software and video games', 'ccvm', 'value'),
+    oils_i18n_gettext(590, 'Software and video games', 'ccvm', 'search_label')),
+(591, 'mr_hold_format', 'dvd', 
+    oils_i18n_gettext(591, 'DVD', 'ccvm', 'value'),
+    oils_i18n_gettext(591, 'DVD', 'ccvm', 'search_label')),
+(592, 'mr_hold_format', 'kit', 
+    oils_i18n_gettext(592, 'Kit', 'ccvm', 'value'),
+    oils_i18n_gettext(592, 'Kit', 'ccvm', 'search_label')),
+(593, 'mr_hold_format', 'map', 
+    oils_i18n_gettext(593, 'Map', 'ccvm', 'value'),
+    oils_i18n_gettext(593, 'Map', 'ccvm', 'search_label')),
+(594, 'mr_hold_format', 'microform', 
+    oils_i18n_gettext(594, 'Microform', 'ccvm', 'value'),
+    oils_i18n_gettext(594, 'Microform', 'ccvm', 'search_label')),
+(595, 'mr_hold_format', 'score', 
+    oils_i18n_gettext(595, 'Music Score', 'ccvm', 'value'),
+    oils_i18n_gettext(595, 'Music Score', 'ccvm', 'search_label')),
+(596, 'mr_hold_format', 'picture', 
+    oils_i18n_gettext(596, 'Picture', 'ccvm', 'value'),
+    oils_i18n_gettext(596, 'Picture', 'ccvm', 'search_label')),
+(597, 'mr_hold_format', 'equip', 
+    oils_i18n_gettext(597, 'Equipment, games, toys', 'ccvm', 'value'),
+    oils_i18n_gettext(597, 'Equipment, games, toys', 'ccvm', 'search_label')),
+(598, 'mr_hold_format', 'serial', 
+    oils_i18n_gettext(598, 'Serials and magazines', 'ccvm', 'value'),
+    oils_i18n_gettext(598, 'Serials and magazines', 'ccvm', 'search_label')),
+(599, 'mr_hold_format', 'vhs', 
+    oils_i18n_gettext(599, 'VHS', 'ccvm', 'value'),
+    oils_i18n_gettext(599, 'VHS', 'ccvm', 'search_label')),
+(600, 'mr_hold_format', 'cdaudiobook', 
+    oils_i18n_gettext(600, 'CD Audiobook', 'ccvm', 'value'),
+    oils_i18n_gettext(600, 'CD Audiobook', 'ccvm', 'search_label')),
+(601, 'mr_hold_format', 'cdmusic', 
+    oils_i18n_gettext(601, 'CD Music recording', 'ccvm', 'value'),
+    oils_i18n_gettext(601, 'CD Music recording', 'ccvm', 'search_label')),
+(602, 'mr_hold_format', 'casaudiobook', 
+    oils_i18n_gettext(602, 'Cassette audiobook', 'ccvm', 'value'),
+    oils_i18n_gettext(602, 'Cassette audiobook', 'ccvm', 'search_label')),
+(603, 'mr_hold_format', 'casmusic',
+    oils_i18n_gettext(603, 'Audiocassette music recording', 'ccvm', 'value'),
+    oils_i18n_gettext(603, 'Audiocassette music recording', 'ccvm', 'search_label')),
+(604, 'mr_hold_format', 'phonospoken', 
+    oils_i18n_gettext(604, 'Phonograph spoken recording', 'ccvm', 'value'),
+    oils_i18n_gettext(604, 'Phonograph spoken recording', 'ccvm', 'search_label')),
+(605, 'mr_hold_format', 'phonomusic', 
+    oils_i18n_gettext(605, 'Phonograph music recording', 'ccvm', 'value'),
+    oils_i18n_gettext(605, 'Phonograph music recording', 'ccvm', 'search_label')),
+(606, 'mr_hold_format', 'lpbook', 
+    oils_i18n_gettext(606, 'Large Print Book', 'ccvm', 'value'),
+    oils_i18n_gettext(606, 'Large Print Book', 'ccvm', 'search_label')) ;
+
+-- carve out a slot of 10k IDs for stock CCVMs
+SELECT SETVAL('config.coded_value_map_id_seq'::TEXT, 10000);
+
+
+-- composite definitions for record attr "icon_format"
+
+INSERT INTO config.composite_attr_entry_definition 
+    (coded_value, definition) VALUES
+--book
+(564, '{"0":[{"_attr":"item_type","_val":"a"},{"_attr":"item_type","_val":"t"}],"1":{"_not":[{"_attr":"item_form","_val":"a"},{"_attr":"item_form","_val":"b"},{"_attr":"item_form","_val":"c"},{"_attr":"item_form","_val":"d"},{"_attr":"item_form","_val":"f"},{"_attr":"item_form","_val":"o"},{"_attr":"item_form","_val":"q"},{"_attr":"item_form","_val":"r"},{"_attr":"item_form","_val":"s"}]},"2":[{"_attr":"bib_level","_val":"a"},{"_attr":"bib_level","_val":"c"},{"_attr":"bib_level","_val":"d"},{"_attr":"bib_level","_val":"m"}]}'),
 
-SELECT SETVAL('config.coded_value_map_id_seq'::TEXT, (SELECT max(id) FROM config.coded_value_map));
+-- braille
+(565, '{"0":{"_attr":"item_type","_val":"a"},"1":{"_attr":"item_form","_val":"f"}}'),
+
+-- software
+(566, '{"_attr":"item_type","_val":"m"}'),
+
+-- dvd
+(567, '{"_attr":"vr_format","_val":"v"}'),
+
+-- ebook
+(568, '{"0":[{"_attr":"item_type","_val":"a"},{"_attr":"item_type","_val":"t"}],"1":[{"_attr":"item_form","_val":"o"},{"_attr":"item_form","_val":"s"},{"_attr":"item_form","_val":"q"}],"2":[{"_attr":"bib_level","_val":"a"},{"_attr":"bib_level","_val":"c"},{"_attr":"bib_level","_val":"d"},{"_attr":"bib_level","_val":"m"}]}'),
+
+-- eaudio
+(569, '{"0":{"_attr":"item_type","_val":"i"},"1":[{"_attr":"item_form","_val":"o"},{"_attr":"item_form","_val":"q"},{"_attr":"item_form","_val":"s"}]}'),
+
+-- kit
+(570, '[{"_attr":"item_type","_val":"o"},{"_attr":"item_type","_val":"p"}]'),
+
+-- map
+(571, '[{"_attr":"item_type","_val":"e"},{"_attr":"item_type","_val":"f"}]'),
+
+-- microform
+(572, '[{"_attr":"item_form","_val":"a"},{"_attr":"item_form","_val":"b"},{"_attr":"item_form","_val":"c"}]'),
+
+-- score
+(573, '[{"_attr":"item_type","_val":"c"},{"_attr":"item_type","_val":"d"}]'),
+
+-- picture
+(574, '{"_attr":"item_type","_val":"k"}'),
+
+-- equip
+(575, '{"_attr":"item_type","_val":"r"}'),
+
+-- serial
+(576, '[{"_attr":"bib_level","_val":"b"},{"_attr":"bib_level","_val":"s"}]'),
+
+-- vhs
+(577, '{"_attr":"vr_format","_val":"b"}'),
+
+-- evideo
+(578, '{"0":{"_attr":"item_type","_val":"g"},"1":[{"_attr":"item_form","_val":"o"},{"_attr":"item_form","_val":"s"},{"_attr":"item_form","_val":"q"}]}'),
+
+-- cdaudiobook
+(579, '{"0":{"_attr":"item_type","_val":"i"},"1":{"_attr":"sr_format","_val":"f"}}'),
+
+-- cdmusic
+(580, '{"0":{"_attr":"item_type","_val":"j"},"1":{"_attr":"sr_format","_val":"f"}}'),
+
+-- casaudiobook
+(581, '{"0":{"_attr":"item_type","_val":"i"},"1":{"_attr":"sr_format","_val":"l"}}'),
+
+-- casmusic
+(582, '{"0":{"_attr":"item_type","_val":"j"},"1":{"_attr":"sr_format","_val":"l"}}'),
+
+-- phonospoken
+(583, '{"0":{"_attr":"item_type","_val":"i"},"1":[{"_attr":"sr_format","_val":"a"},{"_attr":"sr_format","_val":"b"},{"_attr":"sr_format","_val":"c"},{"_attr":"sr_format","_val":"d"},{"_attr":"sr_format","_val":"e"}]}'),
+
+-- phonomusic
+(584, '{"0":{"_attr":"item_type","_val":"j"},"1":[{"_attr":"sr_format","_val":"a"},{"_attr":"sr_format","_val":"b"},{"_attr":"sr_format","_val":"c"},{"_attr":"sr_format","_val":"d"},{"_attr":"sr_format","_val":"e"}]}'),
+
+-- lpbook
+(585, '{"0":[{"_attr":"item_type","_val":"a"},{"_attr":"item_type","_val":"t"}],"1":{"_attr":"item_form","_val":"d"},"2":[{"_attr":"bib_level","_val":"a"},{"_attr":"bib_level","_val":"c"},{"_attr":"bib_level","_val":"d"},{"_attr":"bib_level","_val":"m"}]}');
+
+-- use the definitions from the icon_format as the basis for the MR hold format definitions
+DO $$
+    DECLARE format TEXT;
+BEGIN
+    FOR format IN SELECT UNNEST(
+        '{book,braille,software,dvd,kit,map,microform,score,picture,equip,serial,vhs,cdaudiobook,cdmusic,casaudiobook,casmusic,phonospoken,phonomusic,lpbook}'::text[])
+    LOOP
+        INSERT INTO config.composite_attr_entry_definition 
+            (coded_value, definition) VALUES
+            (
+                -- get the ID from the new ccvm above
+                (SELECT id FROM config.coded_value_map 
+                    WHERE code = format AND ctype = 'mr_hold_format'),
+                -- get the def of the matching ccvm attached to the icon_format attr
+                (SELECT definition FROM config.composite_attr_entry_definition ccaed
+                    JOIN config.coded_value_map ccvm ON (ccaed.coded_value = ccvm.id)
+                    WHERE ccvm.ctype = 'icon_format' AND ccvm.code = format)
+            );
+    END LOOP; 
+END $$;
 
 -- Trigger Event Definitions -------------------------------------------------
 
@@ -9534,75 +9691,6 @@ INSERT INTO config.record_attr_definition
     TRUE, TRUE, TRUE
 );
 
-INSERT INTO config.composite_attr_entry_definition
-    (coded_value, definition) VALUES
---book
-(564, '{"0":[{"_attr":"item_type","_val":"a"},{"_attr":"item_type","_val":"t"}],"1":{"_not":[{"_attr":"item_form","_val":"a"},{"_attr":"item_form","_val":"b"},{"_attr":"item_form","_val":"c"},{"_attr":"item_form","_val":"d"},{"_attr":"item_form","_val":"f"},{"_attr":"item_form","_val":"o"},{"_attr":"item_form","_val":"q"},{"_attr":"item_form","_val":"r"},{"_attr":"item_form","_val":"s"}]},"2":[{"_attr":"bib_level","_val":"a"},{"_attr":"bib_level","_val":"c"},{"_attr":"bib_level","_val":"d"},{"_attr":"bib_level","_val":"m"}]}'),
-
--- braille
-(565, '{"0":{"_attr":"item_type","_val":"a"},"1":{"_attr":"item_form","_val":"f"}}'),
-
--- software
-(566, '{"_attr":"item_type","_val":"m"}'),
-
--- dvd
-(567, '{"_attr":"vr_format","_val":"v"}'),
-
--- ebook
-(568, '{"0":[{"_attr":"item_type","_val":"a"},{"_attr":"item_type","_val":"t"}],"1":[{"_attr":"item_form","_val":"o"},{"_attr":"item_form","_val":"s"},{"_attr":"item_form","_val":"q"}],"2":[{"_attr":"bib_level","_val":"a"},{"_attr":"bib_level","_val":"c"},{"_attr":"bib_level","_val":"d"},{"_attr":"bib_level","_val":"m"}]}'),
-
--- eaudio
-(569, '{"0":{"_attr":"item_type","_val":"i"},"1":[{"_attr":"item_form","_val":"o"},{"_attr":"item_form","_val":"q"},{"_attr":"item_form","_val":"s"}]}'),
-
--- kit
-(570, '[{"_attr":"item_type","_val":"o"},{"_attr":"item_type","_val":"p"}]'),
-
--- map
-(571, '[{"_attr":"item_type","_val":"e"},{"_attr":"item_type","_val":"f"}]'),
-
--- microform
-(572, '[{"_attr":"item_form","_val":"a"},{"_attr":"item_form","_val":"b"},{"_attr":"item_form","_val":"c"}]'),
-
--- score
-(573, '[{"_attr":"item_type","_val":"c"},{"_attr":"item_type","_val":"d"}]'),
-
--- picture
-(574, '{"_attr":"item_type","_val":"k"}'),
-
--- equip
-(575, '{"_attr":"item_type","_val":"r"}'),
-
--- serial
-(576, '[{"_attr":"bib_level","_val":"b"},{"_attr":"bib_level","_val":"s"}]'),
-
--- vhs
-(577, '{"_attr":"vr_format","_val":"b"}'),
-
--- evideo
-(578, '{"0":{"_attr":"item_type","_val":"g"},"1":[{"_attr":"item_form","_val":"o"},{"_attr":"item_form","_val":"s"},{"_attr":"item_form","_val":"q"}]}'),
-
--- cdaudiobook
-(579, '{"0":{"_attr":"item_type","_val":"i"},"1":{"_attr":"sr_format","_val":"f"}}'),
-
--- cdmusic
-(580, '{"0":{"_attr":"item_type","_val":"j"},"1":{"_attr":"sr_format","_val":"f"}}'),
-
--- casaudiobook
-(581, '{"0":{"_attr":"item_type","_val":"i"},"1":{"_attr":"sr_format","_val":"l"}}'),
-
--- casmusic
-(582, '{"0":{"_attr":"item_type","_val":"j"},"1":{"_attr":"sr_format","_val":"l"}}'),
-
--- phonospoken
-(583, '{"0":{"_attr":"item_type","_val":"i"},"1":[{"_attr":"sr_format","_val":"a"},{"_attr":"sr_format","_val":"b"},{"_attr":"sr_format","_val":"c"},{"_attr":"sr_format","_val":"d"},{"_attr":"sr_format","_val":"e"}]}'),
-
--- phonomusic
-(584, '{"0":{"_attr":"item_type","_val":"j"},"1":[{"_attr":"sr_format","_val":"a"},{"_attr":"sr_format","_val":"b"},{"_attr":"sr_format","_val":"c"},{"_attr":"sr_format","_val":"d"},{"_attr":"sr_format","_val":"e"}]}'),
-
--- lpbook
-(585, '{"0":[{"_attr":"item_type","_val":"a"},{"_attr":"item_type","_val":"t"}],"1":{"_attr":"item_form","_val":"d"},"2":[{"_attr":"bib_level","_val":"a"},{"_attr":"bib_level","_val":"c"},{"_attr":"bib_level","_val":"d"},{"_attr":"bib_level","_val":"m"}]}');
-
-
 INSERT INTO config.usr_setting_type (name,opac_visible,label,description,datatype)
     VALUES (
         'history.circ.retention_age',
@@ -13836,3 +13924,31 @@ VALUES (
 
 INSERT INTO config.floating_group(name) VALUES ('Everywhere');
 INSERT INTO config.floating_group_member(floating_group, org_unit) VALUES (1, 1);
+
+INSERT INTO config.global_flag (name, label, value, enabled) VALUES (
+    'opac.icon_attr',
+    oils_i18n_gettext(
+        'opac.icon_attr', 
+        'OPAC Format Icons Attribute',
+        'cgf',
+        'label'
+    ),
+    'icon_format', 
+    TRUE
+);
+
+
+INSERT INTO config.global_flag (name, label, value, enabled) VALUES (
+    'opac.metarecord.holds.format_attr', 
+    oils_i18n_gettext(
+        'opac.metarecord.holds.format_attr',
+        'OPAC Metarecord Hold Formats Attribute', 
+        'cgf',
+        'label'
+    ),
+    'mr_hold_format', 
+    TRUE
+);
+
+
+
diff --git a/Open-ILS/src/sql/Pg/upgrade/ZZZZZ.data.mmr-holds-formats.sql b/Open-ILS/src/sql/Pg/upgrade/ZZZZZ.data.mmr-holds-formats.sql
new file mode 100644
index 0000000..690d919
--- /dev/null
+++ b/Open-ILS/src/sql/Pg/upgrade/ZZZZZ.data.mmr-holds-formats.sql
@@ -0,0 +1,116 @@
+BEGIN;
+
+INSERT INTO config.global_flag (name, label, value, enabled) VALUES (
+    'opac.metarecord.holds.format_attr', 
+    oils_i18n_gettext(
+        'opac.metarecord.holds.format_attr',
+        'OPAC Metarecord Hold Formats Attribute', 
+        'cgf',
+        'label'
+    ),
+    'mr_hold_format', 
+    TRUE
+);
+
+INSERT INTO config.record_attr_definition 
+    (name, label, multi, filter, composite) 
+VALUES (
+    'mr_hold_format', 
+    oils_i18n_gettext(
+        'mr_hold_format',
+        'Metarecord Hold Formats', 
+        'crad',
+        'label'
+    ),
+    TRUE, TRUE, TRUE
+);
+
+-- these formats are a subset of the "icon_format" attribute,
+-- modified to exclude electronic resources, which are not holdable
+
+-- for i18n purposes, these have to be listed individually
+INSERT INTO config.coded_value_map
+    (id, ctype, code, value, search_label) VALUES 
+(588, 'mr_hold_format', 'book', 
+    oils_i18n_gettext(588, 'Book', 'ccvm', 'value'),
+    oils_i18n_gettext(588, 'Book', 'ccvm', 'search_label')),
+(589, 'mr_hold_format', 'braille', 
+    oils_i18n_gettext(589, 'Braille', 'ccvm', 'value'),
+    oils_i18n_gettext(589, 'Braille', 'ccvm', 'search_label')),
+(590, 'mr_hold_format', 'software', 
+    oils_i18n_gettext(590, 'Software and video games', 'ccvm', 'value'),
+    oils_i18n_gettext(590, 'Software and video games', 'ccvm', 'search_label')),
+(591, 'mr_hold_format', 'dvd', 
+    oils_i18n_gettext(591, 'DVD', 'ccvm', 'value'),
+    oils_i18n_gettext(591, 'DVD', 'ccvm', 'search_label')),
+(592, 'mr_hold_format', 'kit', 
+    oils_i18n_gettext(592, 'Kit', 'ccvm', 'value'),
+    oils_i18n_gettext(592, 'Kit', 'ccvm', 'search_label')),
+(593, 'mr_hold_format', 'map', 
+    oils_i18n_gettext(593, 'Map', 'ccvm', 'value'),
+    oils_i18n_gettext(593, 'Map', 'ccvm', 'search_label')),
+(594, 'mr_hold_format', 'microform', 
+    oils_i18n_gettext(594, 'Microform', 'ccvm', 'value'),
+    oils_i18n_gettext(594, 'Microform', 'ccvm', 'search_label')),
+(595, 'mr_hold_format', 'score', 
+    oils_i18n_gettext(595, 'Music Score', 'ccvm', 'value'),
+    oils_i18n_gettext(595, 'Music Score', 'ccvm', 'search_label')),
+(596, 'mr_hold_format', 'picture', 
+    oils_i18n_gettext(596, 'Picture', 'ccvm', 'value'),
+    oils_i18n_gettext(596, 'Picture', 'ccvm', 'search_label')),
+(597, 'mr_hold_format', 'equip', 
+    oils_i18n_gettext(597, 'Equipment, games, toys', 'ccvm', 'value'),
+    oils_i18n_gettext(597, 'Equipment, games, toys', 'ccvm', 'search_label')),
+(598, 'mr_hold_format', 'serial', 
+    oils_i18n_gettext(598, 'Serials and magazines', 'ccvm', 'value'),
+    oils_i18n_gettext(598, 'Serials and magazines', 'ccvm', 'search_label')),
+(599, 'mr_hold_format', 'vhs', 
+    oils_i18n_gettext(599, 'VHS', 'ccvm', 'value'),
+    oils_i18n_gettext(599, 'VHS', 'ccvm', 'search_label')),
+(600, 'mr_hold_format', 'cdaudiobook', 
+    oils_i18n_gettext(600, 'CD Audiobook', 'ccvm', 'value'),
+    oils_i18n_gettext(600, 'CD Audiobook', 'ccvm', 'search_label')),
+(601, 'mr_hold_format', 'cdmusic', 
+    oils_i18n_gettext(601, 'CD Music recording', 'ccvm', 'value'),
+    oils_i18n_gettext(601, 'CD Music recording', 'ccvm', 'search_label')),
+(602, 'mr_hold_format', 'casaudiobook', 
+    oils_i18n_gettext(602, 'Cassette audiobook', 'ccvm', 'value'),
+    oils_i18n_gettext(602, 'Cassette audiobook', 'ccvm', 'search_label')),
+(603, 'mr_hold_format', 'casmusic',
+    oils_i18n_gettext(603, 'Audiocassette music recording', 'ccvm', 'value'),
+    oils_i18n_gettext(603, 'Audiocassette music recording', 'ccvm', 'search_label')),
+(604, 'mr_hold_format', 'phonospoken', 
+    oils_i18n_gettext(604, 'Phonograph spoken recording', 'ccvm', 'value'),
+    oils_i18n_gettext(604, 'Phonograph spoken recording', 'ccvm', 'search_label')),
+(605, 'mr_hold_format', 'phonomusic', 
+    oils_i18n_gettext(605, 'Phonograph music recording', 'ccvm', 'value'),
+    oils_i18n_gettext(605, 'Phonograph music recording', 'ccvm', 'search_label')),
+(606, 'mr_hold_format', 'lpbook', 
+    oils_i18n_gettext(606, 'Large Print Book', 'ccvm', 'value'),
+    oils_i18n_gettext(606, 'Large Print Book', 'ccvm', 'search_label'))
+;
+
+-- but we can auto-generate the composite definitions
+
+DO $$
+    DECLARE format TEXT;
+BEGIN
+    FOR format IN SELECT UNNEST(
+        '{book,braille,software,dvd,kit,map,microform,score,picture,equip,serial,vhs,cdaudiobook,cdmusic,casaudiobook,casmusic,phonospoken,phonomusic,lpbook}'::text[]) LOOP
+
+        INSERT INTO config.composite_attr_entry_definition 
+            (coded_value, definition) VALUES
+            (
+                -- get the ID from the new ccvm above
+                (SELECT id FROM config.coded_value_map 
+                    WHERE code = format AND ctype = 'mr_hold_format'),
+                -- get the def of the matching ccvm attached to the icon_format attr
+                (SELECT definition FROM config.composite_attr_entry_definition ccaed
+                    JOIN config.coded_value_map ccvm ON (ccaed.coded_value = ccvm.id)
+                    WHERE ccvm.ctype = 'icon_format' AND ccvm.code = format)
+            );
+    END LOOP; 
+END $$;
+
+COMMIT;
+

commit 36e3c0a8d4861eb8d877664623ef925331e45bb1
Author: Mike Rylander <mrylander at gmail.com>
Date:   Mon Feb 10 13:59:23 2014 -0500

    LP#1053397 DB for metarecords unapi and holds
    
    Schema for unapi metarecord access (unap.mmr()) and associated
    components.
    
    Signed-off-by: Bill Erickson <berick at esilibrary.com>
    Signed-off-by: Mike Rylander <mrylander at gmail.com>
    Signed-off-by: Dan Wells <dbw2 at calvin.edu>

diff --git a/Open-ILS/src/sql/Pg/040.schema.asset.sql b/Open-ILS/src/sql/Pg/040.schema.asset.sql
index 34d829f..20dbc95 100644
--- a/Open-ILS/src/sql/Pg/040.schema.asset.sql
+++ b/Open-ILS/src/sql/Pg/040.schema.asset.sql
@@ -683,7 +683,7 @@ DECLARE
     ans RECORD;
     trans INT;
 BEGIN
-    SELECT 1 INTO trans FROM biblio.record_entry b JOIN config.bib_source src ON (b.source = src.id) WHERE src.transcendant AND b.id = rid;
+    SELECT 1 INTO trans FROM biblio.record_entry b JOIN config.bib_source src ON (b.source = src.id) JOIN metabib.metarecord_source_map m ON (m.source = b.id) WHERE src.transcendant AND m.metarecord = rid;
 
     FOR ans IN SELECT u.id, t.depth FROM actor.org_unit_ancestors(org) AS u JOIN actor.org_unit_type t ON (u.ou_type = t.id) LOOP
         RETURN QUERY
@@ -695,9 +695,9 @@ BEGIN
                 trans
           FROM  
                 actor.org_unit_descendants(ans.id) d
-                JOIN asset.opac_visible_copies av ON (av.record = rid AND av.circ_lib = d.id)
+                JOIN asset.opac_visible_copies av ON (av.circ_lib = d.id)
                 JOIN asset.copy cp ON (cp.id = av.copy_id)
-                JOIN metabib.metarecord_source_map m ON (m.source = av.record)
+                JOIN metabib.metarecord_source_map m ON (m.metarecord = rid AND m.source = av.record)
           GROUP BY 1,2,6;
 
         IF NOT FOUND THEN
@@ -715,7 +715,7 @@ DECLARE
     ans RECORD;
     trans INT;
 BEGIN
-    SELECT 1 INTO trans FROM biblio.record_entry b JOIN config.bib_source src ON (b.source = src.id) WHERE src.transcendant AND b.id = rid;
+    SELECT 1 INTO trans FROM biblio.record_entry b JOIN config.bib_source src ON (b.source = src.id) JOIN metabib.metarecord_source_map m ON (m.source = b.id) WHERE src.transcendant AND m.metarecord = rid;
 
     FOR ans IN SELECT u.org_unit AS id FROM actor.org_lasso_map AS u WHERE lasso = i_lasso LOOP
         RETURN QUERY
@@ -727,9 +727,9 @@ BEGIN
                 trans
           FROM
                 actor.org_unit_descendants(ans.id) d
-                JOIN asset.opac_visible_copies av ON (av.record = rid AND av.circ_lib = d.id)
+                JOIN asset.opac_visible_copies av ON (av.circ_lib = d.id)
                 JOIN asset.copy cp ON (cp.id = av.copy_id)
-                JOIN metabib.metarecord_source_map m ON (m.source = av.record)
+                JOIN metabib.metarecord_source_map m ON (m.metarecord = rid AND m.source = av.record)
           GROUP BY 1,2,6;
 
         IF NOT FOUND THEN
@@ -747,7 +747,7 @@ DECLARE
     ans RECORD; 
     trans INT;
 BEGIN
-    SELECT 1 INTO trans FROM biblio.record_entry b JOIN config.bib_source src ON (b.source = src.id) WHERE src.transcendant AND b.id = rid;
+    SELECT 1 INTO trans FROM biblio.record_entry b JOIN config.bib_source src ON (b.source = src.id) JOIN metabib.metarecord_source_map m ON (m.source = b.id) WHERE src.transcendant AND m.metarecord = rid;
 
     FOR ans IN SELECT u.id, t.depth FROM actor.org_unit_ancestors(org) AS u JOIN actor.org_unit_type t ON (u.ou_type = t.id) LOOP
         RETURN QUERY
@@ -760,8 +760,8 @@ BEGIN
           FROM
                 actor.org_unit_descendants(ans.id) d
                 JOIN asset.copy cp ON (cp.circ_lib = d.id AND NOT cp.deleted)
-                JOIN asset.call_number cn ON (cn.record = rid AND cn.id = cp.call_number AND NOT cn.deleted)
-                JOIN metabib.metarecord_source_map m ON (m.source = cn.record)
+                JOIN asset.call_number cn ON (cn.id = cp.call_number AND NOT cn.deleted)
+                JOIN metabib.metarecord_source_map m ON (m.metarecord = rid AND m.source = cn.record)
           GROUP BY 1,2,6;
 
         IF NOT FOUND THEN
@@ -779,7 +779,7 @@ DECLARE
     ans RECORD;
     trans INT;
 BEGIN
-    SELECT 1 INTO trans FROM biblio.record_entry b JOIN config.bib_source src ON (b.source = src.id) WHERE src.transcendant AND b.id = rid;
+    SELECT 1 INTO trans FROM biblio.record_entry b JOIN config.bib_source src ON (b.source = src.id) JOIN metabib.metarecord_source_map m ON (m.source = b.id) WHERE src.transcendant AND m.metarecord = rid;
 
     FOR ans IN SELECT u.org_unit AS id FROM actor.org_lasso_map AS u WHERE lasso = i_lasso LOOP
         RETURN QUERY
@@ -792,8 +792,8 @@ BEGIN
           FROM
                 actor.org_unit_descendants(ans.id) d
                 JOIN asset.copy cp ON (cp.circ_lib = d.id AND NOT cp.deleted)
-                JOIN asset.call_number cn ON (cn.record = rid AND cn.id = cp.call_number AND NOT cn.deleted)
-                JOIN metabib.metarecord_source_map m ON (m.source = cn.record)
+                JOIN asset.call_number cn ON (cn.id = cp.call_number AND NOT cn.deleted)
+                JOIN metabib.metarecord_source_map m ON (m.metarecord = rid AND m.source = cn.record)
           GROUP BY 1,2,6;
 
         IF NOT FOUND THEN
diff --git a/Open-ILS/src/sql/Pg/990.schema.unapi.sql b/Open-ILS/src/sql/Pg/990.schema.unapi.sql
index 6d1e070..0b10b39 100644
--- a/Open-ILS/src/sql/Pg/990.schema.unapi.sql
+++ b/Open-ILS/src/sql/Pg/990.schema.unapi.sql
@@ -65,7 +65,7 @@ RETURNS INTEGER AS $$
 $$ LANGUAGE SQL STABLE;
 
 CREATE OR REPLACE FUNCTION evergreen.ranked_volumes(
-    bibid BIGINT, 
+    bibid BIGINT[], 
     ouid INT,
     depth INT DEFAULT NULL,
     slimit HSTORE DEFAULT NULL,
@@ -87,7 +87,7 @@ CREATE OR REPLACE FUNCTION evergreen.ranked_volumes(
                     WHERE ou.id = $2
                 ), $6)
             ) AS aou ON (acp.circ_lib = aou.id)
-        WHERE acn.record = $1
+        WHERE acn.record = ANY ($1)
             AND acn.deleted IS FALSE
             AND acp.deleted IS FALSE
             AND CASE WHEN ('exclude_invisible_acn' = ANY($7)) THEN 
@@ -108,8 +108,14 @@ CREATE OR REPLACE FUNCTION evergreen.ranked_volumes(
 $$
 LANGUAGE SQL STABLE;
 
+CREATE OR REPLACE FUNCTION evergreen.ranked_volumes
+    ( bibid BIGINT, ouid INT, depth INT DEFAULT NULL, slimit HSTORE DEFAULT NULL, soffset HSTORE DEFAULT NULL, pref_lib INT DEFAULT NULL, includes TEXT[] DEFAULT NULL::TEXT[] )
+    RETURNS TABLE (id BIGINT, name TEXT, label_sortkey TEXT, rank BIGINT)
+    AS $$ SELECT * FROM evergreen.ranked_volumes(ARRAY[$1],$2,$3,$4,$5,$6,$7) $$ LANGUAGE SQL STABLE;
+
+
 CREATE OR REPLACE FUNCTION evergreen.located_uris (
-    bibid BIGINT,
+    bibid BIGINT[], 
     ouid INT,
     pref_lib INT DEFAULT NULL
 ) RETURNS TABLE (id BIGINT, name TEXT, label_sortkey TEXT, rank INT) AS $$
@@ -122,7 +128,7 @@ CREATE OR REPLACE FUNCTION evergreen.located_uris (
            LEFT JOIN actor.org_unit_ancestors( COALESCE($3, $2) ) aou ON (acn.owning_lib = aou.id)
            LEFT JOIN actor.org_unit_descendants( COALESCE($3, $2) ) aoud ON (acn.owning_lib = aoud.id),
            all_orgs
-      WHERE acn.record = $1
+      WHERE acn.record = ANY ($1)
           AND acn.deleted IS FALSE
           AND auri.active IS TRUE
           AND ((NOT all_orgs.flag AND aou.id IS NOT NULL) OR COALESCE(aou.id,aoud.id) IS NOT NULL)
@@ -134,7 +140,7 @@ CREATE OR REPLACE FUNCTION evergreen.located_uris (
            LEFT JOIN actor.org_unit_ancestors( $2 ) aou ON (acn.owning_lib = aou.id)
            LEFT JOIN actor.org_unit_descendants( $2 ) aoud ON (acn.owning_lib = aoud.id),
            all_orgs
-      WHERE acn.record = $1
+      WHERE acn.record = ANY ($1)
           AND acn.deleted IS FALSE
           AND auri.active IS TRUE
           AND ((NOT all_orgs.flag AND aou.id IS NOT NULL) OR COALESCE(aou.id,aoud.id) IS NOT NULL))x
@@ -142,6 +148,10 @@ CREATE OR REPLACE FUNCTION evergreen.located_uris (
 $$
 LANGUAGE SQL STABLE;
 
+CREATE OR REPLACE FUNCTION evergreen.located_uris ( bibid BIGINT, ouid INT, pref_lib INT DEFAULT NULL)
+    RETURNS TABLE (id BIGINT, name TEXT, label_sortkey TEXT, rank INT)
+    AS $$ SELECT * FROM evergreen.located_uris(ARRAY[$1],$2,$3) $$ LANGUAGE SQL STABLE;
+
 CREATE TABLE unapi.bre_output_layout (
     name                TEXT    PRIMARY KEY,
     transform           TEXT    REFERENCES config.xml_transform (name) ON DELETE CASCADE ON UPDATE CASCADE DEFERRABLE INITIALLY DEFERRED,
@@ -195,8 +205,22 @@ CREATE OR REPLACE FUNCTION unapi.bre (
     pref_lib INT DEFAULT NULL
 )
 RETURNS XML AS $F$ SELECT NULL::XML $F$ LANGUAGE SQL STABLE;
+CREATE OR REPLACE FUNCTION unapi.mmr (
+    obj_id BIGINT,
+    format TEXT,
+    ename TEXT,
+    includes TEXT[],
+    org TEXT,
+    depth INT DEFAULT NULL,
+    slimit HSTORE DEFAULT NULL,
+    soffset HSTORE DEFAULT NULL,
+    include_xmlns BOOL DEFAULT TRUE,
+    pref_lib INT DEFAULT NULL
+)
+RETURNS XML AS $F$ SELECT NULL::XML $F$ LANGUAGE SQL STABLE;
 CREATE OR REPLACE FUNCTION unapi.bmp    ( obj_id BIGINT, format TEXT, ename TEXT, includes TEXT[], org TEXT, depth INT DEFAULT NULL, slimit HSTORE DEFAULT NULL, soffset HSTORE DEFAULT NULL, include_xmlns BOOL DEFAULT TRUE ) RETURNS XML AS $F$ SELECT NULL::XML $F$ LANGUAGE SQL STABLE;
 CREATE OR REPLACE FUNCTION unapi.mra    ( obj_id BIGINT, format TEXT, ename TEXT, includes TEXT[], org TEXT, depth INT DEFAULT NULL, slimit HSTORE DEFAULT NULL, soffset HSTORE DEFAULT NULL, include_xmlns BOOL DEFAULT TRUE ) RETURNS XML AS $F$ SELECT NULL::XML $F$ LANGUAGE SQL STABLE;
+CREATE OR REPLACE FUNCTION unapi.mmr_mra    ( obj_id BIGINT, format TEXT, ename TEXT, includes TEXT[], org TEXT, depth INT DEFAULT NULL, slimit HSTORE DEFAULT NULL, soffset HSTORE DEFAULT NULL, include_xmlns BOOL DEFAULT TRUE ) RETURNS XML AS $F$ SELECT NULL::XML $F$ LANGUAGE SQL STABLE;
 CREATE OR REPLACE FUNCTION unapi.circ   ( obj_id BIGINT, format TEXT, ename TEXT, includes TEXT[], org TEXT DEFAULT '-', depth INT DEFAULT NULL, slimit HSTORE DEFAULT NULL, soffset HSTORE DEFAULT NULL, include_xmlns BOOL DEFAULT TRUE ) RETURNS XML AS $F$ SELECT NULL::XML $F$ LANGUAGE SQL STABLE;
 
 CREATE OR REPLACE FUNCTION unapi.holdings_xml (
@@ -212,6 +236,19 @@ CREATE OR REPLACE FUNCTION unapi.holdings_xml (
 )
 RETURNS XML AS $F$ SELECT NULL::XML $F$ LANGUAGE SQL STABLE;
 
+CREATE OR REPLACE FUNCTION unapi.mmr_holdings_xml (
+    mid BIGINT,
+    ouid INT,
+    org TEXT,
+    depth INT DEFAULT NULL,
+    includes TEXT[] DEFAULT NULL::TEXT[],
+    slimit HSTORE DEFAULT NULL,
+    soffset HSTORE DEFAULT NULL,
+    include_xmlns BOOL DEFAULT TRUE,
+    pref_lib INT DEFAULT NULL
+)
+RETURNS XML AS $F$ SELECT NULL::XML $F$ LANGUAGE SQL STABLE;
+
 CREATE OR REPLACE FUNCTION unapi.biblio_record_entry_feed ( id_list BIGINT[], format TEXT, includes TEXT[], org TEXT, depth INT DEFAULT NULL, slimit HSTORE DEFAULT NULL, soffset HSTORE DEFAULT NULL, include_xmlns BOOL DEFAULT TRUE, title TEXT DEFAULT NULL, description TEXT DEFAULT NULL, creator TEXT DEFAULT NULL, update_ts TEXT DEFAULT NULL, unapi_url TEXT DEFAULT NULL, header_xml XML DEFAULT NULL ) RETURNS XML AS $F$ SELECT NULL::XML $F$ LANGUAGE SQL STABLE;
 
 CREATE OR REPLACE FUNCTION unapi.memoize (classname TEXT, obj_id BIGINT, format TEXT,  ename TEXT, includes TEXT[], org TEXT, depth INT DEFAULT NULL, slimit HSTORE DEFAULT NULL, soffset HSTORE DEFAULT NULL, include_xmlns BOOL DEFAULT TRUE ) RETURNS XML AS $F$
@@ -1180,6 +1217,285 @@ CREATE OR REPLACE FUNCTION unapi.circ (obj_id BIGINT, format TEXT, ename TEXT, i
     WHERE id = $1;
 $F$ LANGUAGE SQL STABLE;
 
+CREATE OR REPLACE FUNCTION unapi.mmr_mra (
+    obj_id BIGINT,
+    format TEXT,
+    ename TEXT,
+    includes TEXT[],
+    org TEXT,
+    depth INT DEFAULT NULL,
+    slimit HSTORE DEFAULT NULL,
+    soffset HSTORE DEFAULT NULL,
+    include_xmlns BOOL DEFAULT TRUE
+) RETURNS XML AS $F$
+    SELECT  XMLELEMENT(
+        name attributes,
+        XMLATTRIBUTES(
+            CASE WHEN $9 THEN 'http://open-ils.org/spec/indexing/v1' ELSE NULL END AS xmlns,
+            'tag:open-ils.org:U2 at mmr/' || $1 AS metarecord
+        ),
+        (SELECT XMLAGG(foo.y)
+          FROM (
+            SELECT  DISTINCT ON (COALESCE(cvm.id,uvm.id))
+                    COALESCE(cvm.id,uvm.id),
+                    XMLELEMENT(
+                        name field,
+                        XMLATTRIBUTES(
+                            mra.attr AS name,
+                            cvm.value AS "coded-value",
+                            cvm.id AS "cvmid",
+                            rad.composite,
+                            rad.multi,
+                            rad.filter,
+                            rad.sorter
+                        ),
+                        mra.value
+                    )
+              FROM  metabib.record_attr_flat mra
+                    JOIN config.record_attr_definition rad ON (mra.attr = rad.name)
+                    LEFT JOIN config.coded_value_map cvm ON (cvm.ctype = mra.attr AND code = mra.value)
+                    LEFT JOIN metabib.uncontrolled_record_attr_value uvm ON (uvm.attr = mra.attr AND uvm.value = mra.value)
+              WHERE mra.id IN (SELECT source FROM metabib.metarecord_source_map WHERE metarecord = $1)
+              ORDER BY 1
+            )foo(id,y)
+        )
+    )
+$F$ LANGUAGE SQL STABLE;
+
+CREATE OR REPLACE FUNCTION unapi.mmr_holdings_xml (
+    mid BIGINT,
+    ouid INT,
+    org TEXT,
+    depth INT DEFAULT NULL,
+    includes TEXT[] DEFAULT NULL::TEXT[],
+    slimit HSTORE DEFAULT NULL,
+    soffset HSTORE DEFAULT NULL,
+    include_xmlns BOOL DEFAULT TRUE,
+    pref_lib INT DEFAULT NULL
+)
+RETURNS XML AS $F$
+     SELECT  XMLELEMENT(
+                 name holdings,
+                 XMLATTRIBUTES(
+                    CASE WHEN $8 THEN 'http://open-ils.org/spec/holdings/v1' ELSE NULL END AS xmlns,
+                    CASE WHEN ('mmr' = ANY ($5)) THEN 'tag:open-ils.org:U2 at mmr/' || $1 || '/' || $3 ELSE NULL END AS id,
+                    (SELECT metarecord_has_holdable_copy FROM asset.metarecord_has_holdable_copy($1)) AS has_holdable
+                 ),
+                 XMLELEMENT(
+                     name counts,
+                     (SELECT  XMLAGG(XMLELEMENT::XML) FROM (
+                         SELECT  XMLELEMENT(
+                                     name count,
+                                     XMLATTRIBUTES('public' as type, depth, org_unit, coalesce(transcendant,0) as transcendant, available, visible as count, unshadow)
+                                 )::text
+                           FROM  asset.opac_ou_metarecord_copy_count($2,  $1)
+                                     UNION
+                         SELECT  XMLELEMENT(
+                                     name count,
+                                     XMLATTRIBUTES('staff' as type, depth, org_unit, coalesce(transcendant,0) as transcendant, available, visible as count, unshadow)
+                                 )::text
+                           FROM  asset.staff_ou_metarecord_copy_count($2, $1)
+                                     UNION
+                         SELECT  XMLELEMENT(
+                                     name count,
+                                     XMLATTRIBUTES('pref_lib' as type, depth, org_unit, coalesce(transcendant,0) as transcendant, available, visible as count, unshadow)
+                                 )::text
+                           FROM  asset.opac_ou_metarecord_copy_count($9,  $1)
+                                     ORDER BY 1
+                     )x)
+                 ),
+                 -- XXX monograph_parts and foreign_copies are skipped in MRs ... put them back some day?
+                 XMLELEMENT(
+                     name volumes,
+                     (SELECT XMLAGG(acn ORDER BY rank, name, label_sortkey) FROM (
+                        -- Physical copies
+                        SELECT  unapi.acn(y.id,'xml','volume',evergreen.array_remove_item_by_value( evergreen.array_remove_item_by_value($5,'holdings_xml'),'bre'), $3, $4, $6, $7, FALSE), y.rank, name, label_sortkey
+                        FROM evergreen.ranked_volumes((SELECT ARRAY_AGG(source) FROM metabib.metarecord_source_map WHERE metarecord = $1), $2, $4, $6, $7, $9, $5) AS y
+                        UNION ALL
+                        -- Located URIs
+                        SELECT unapi.acn(uris.id,'xml','volume',evergreen.array_remove_item_by_value( evergreen.array_remove_item_by_value($5,'holdings_xml'),'bre'), $3, $4, $6, $7, FALSE), uris.rank, name, label_sortkey
+                        FROM evergreen.located_uris((SELECT ARRAY_AGG(source) FROM metabib.metarecord_source_map WHERE metarecord = $1), $2, $9) AS uris
+                     )x)
+                 ),
+                 CASE WHEN ('ssub' = ANY ($5)) THEN
+                     XMLELEMENT(
+                         name subscriptions,
+                         (SELECT XMLAGG(ssub) FROM (
+                            SELECT  unapi.ssub(id,'xml','subscription','{}'::TEXT[], $3, $4, $6, $7, FALSE)
+                              FROM  serial.subscription
+                              WHERE record_entry IN (SELECT source FROM metabib.metarecord_source_map WHERE metarecord = $1)
+                        )x)
+                     )
+                 ELSE NULL END
+             );
+$F$ LANGUAGE SQL STABLE;
+
+CREATE OR REPLACE FUNCTION unapi.mmr (
+    obj_id BIGINT,
+    format TEXT,
+    ename TEXT,
+    includes TEXT[],
+    org TEXT,
+    depth INT DEFAULT NULL,
+    slimit HSTORE DEFAULT NULL,
+    soffset HSTORE DEFAULT NULL,
+    include_xmlns BOOL DEFAULT TRUE,
+    pref_lib INT DEFAULT NULL
+)
+RETURNS XML AS $F$
+DECLARE
+    mmrec   metabib.metarecord%ROWTYPE;
+    leadrec biblio.record_entry%ROWTYPE;
+    subrec biblio.record_entry%ROWTYPE;
+    layout  unapi.bre_output_layout%ROWTYPE;
+    xfrm    config.xml_transform%ROWTYPE;
+    ouid    INT;
+    xml_buf TEXT; -- growing XML document
+    tmp_xml TEXT; -- single-use XML string
+    xml_frag TEXT; -- single-use XML fragment
+    top_el  TEXT;
+    output  XML;
+    hxml    XML;
+    axml    XML;
+    subxml  XML; -- subordinate records elements
+    sub_xpath TEXT; 
+    parts   TEXT[]; 
+BEGIN
+
+    -- xpath for extracting bre.marc values from subordinate records 
+    -- so they may be appended to the MARC of the master record prior
+    -- to XSLT processing.
+    -- subjects, isbn, issn, upc -- anything else?
+    sub_xpath := 
+      '//*[starts-with(@tag, "6") or @tag="020" or @tag="022" or @tag="024"]';
+
+    IF org = '-' OR org IS NULL THEN
+        SELECT shortname INTO org FROM evergreen.org_top();
+    END IF;
+
+    SELECT id INTO ouid FROM actor.org_unit WHERE shortname = org;
+
+    IF ouid IS NULL THEN
+        RETURN NULL::XML;
+    END IF;
+
+    SELECT INTO mmrec * FROM metabib.metarecord WHERE id = obj_id;
+    IF NOT FOUND THEN
+        RETURN NULL::XML;
+    END IF;
+
+    -- TODO: aggregate holdings from constituent records
+    IF format = 'holdings_xml' THEN -- the special case
+        output := unapi.mmr_holdings_xml(
+            obj_id, ouid, org, depth,
+            evergreen.array_remove_item_by_value(includes,'holdings_xml'),
+            slimit, soffset, include_xmlns, pref_lib);
+        RETURN output;
+    END IF;
+
+    SELECT * INTO layout FROM unapi.bre_output_layout WHERE name = format;
+
+    IF layout.name IS NULL THEN
+        RETURN NULL::XML;
+    END IF;
+
+    SELECT * INTO xfrm FROM config.xml_transform WHERE name = layout.transform;
+
+    SELECT INTO leadrec * FROM biblio.record_entry WHERE id = mmrec.master_record;
+
+    -- Grab distinct MVF for all records if requested
+    IF ('mra' = ANY (includes)) THEN 
+        axml := unapi.mmr_mra(obj_id,NULL,NULL,NULL,NULL,NULL,NULL,NULL,TRUE);
+    ELSE
+        axml := NULL::XML;
+    END IF;
+
+    xml_buf = leadrec.marc;
+
+    hxml := NULL::XML;
+    IF ('holdings_xml' = ANY (includes)) THEN
+        hxml := unapi.mmr_holdings_xml(
+                    obj_id, ouid, org, depth,
+                    evergreen.array_remove_item_by_value(includes,'holdings_xml'),
+                    slimit, soffset, include_xmlns, pref_lib);
+    END IF;
+
+    subxml := NULL::XML;
+    parts := '{}'::TEXT[];
+    FOR subrec IN SELECT bre.* FROM biblio.record_entry bre
+         JOIN metabib.metarecord_source_map mmsm ON (mmsm.source = bre.id)
+         JOIN metabib.metarecord mmr ON (mmr.id = mmsm.metarecord)
+         WHERE mmr.id = obj_id
+         ORDER BY CASE WHEN bre.id = mmr.master_record THEN 0 ELSE bre.id END
+         LIMIT COALESCE((slimit->'bre')::INT, 5) LOOP
+
+        IF subrec.id = leadrec.id THEN CONTINUE; END IF;
+        -- Append choice data from the the non-lead records to the 
+        -- the lead record document
+
+        parts := parts || xpath(sub_xpath, subrec.marc::XML)::TEXT[];
+    END LOOP;
+
+    SELECT ARRAY_TO_STRING( ARRAY_AGG( DISTINCT p ), '' )::XML INTO subxml FROM UNNEST(parts) p;
+
+    -- append data from the subordinate records to the 
+    -- main record document before applying the XSLT
+
+    IF subxml IS NOT NULL THEN 
+        xml_buf := REGEXP_REPLACE(xml_buf, 
+            '</record>(.*?)$', subxml || '</record>' || E'\\1');
+    END IF;
+
+    IF format = 'marcxml' THEN
+         -- If we're not using the prefixed namespace in 
+         -- this record, then remove all declarations of it
+        IF xml_buf !~ E'<marc:' THEN
+           xml_buf := REGEXP_REPLACE(xml_buf, 
+            ' xmlns:marc="http://www.loc.gov/MARC21/slim"', '', 'g');
+        END IF; 
+    ELSE
+        xml_buf := oils_xslt_process(xml_buf, xfrm.xslt)::XML;
+    END IF;
+
+    -- update top_el to reflect the change in xml_buf, which may
+    -- now be a different type of document (e.g. record -> mods)
+    top_el := REGEXP_REPLACE(xml_buf, E'^.*?<((?:\\S+:)?' || 
+        layout.holdings_element || ').*$', E'\\1');
+
+    IF axml IS NOT NULL THEN 
+        xml_buf := REGEXP_REPLACE(xml_buf, 
+            '</' || top_el || '>(.*?)$', axml || '</' || top_el || E'>\\1');
+    END IF;
+
+    IF hxml IS NOT NULL THEN
+        xml_buf := REGEXP_REPLACE(xml_buf, 
+            '</' || top_el || '>(.*?)$', hxml || '</' || top_el || E'>\\1');
+    END IF;
+
+    IF ('mmr.unapi' = ANY (includes)) THEN 
+        output := REGEXP_REPLACE(
+            xml_buf,
+            '</' || top_el || '>(.*?)',
+            XMLELEMENT(
+                name abbr,
+                XMLATTRIBUTES(
+                    'http://www.w3.org/1999/xhtml' AS xmlns,
+                    'unapi-id' AS class,
+                    'tag:open-ils.org:U2 at mmr/' || obj_id || '/' || org AS title
+                )
+            )::TEXT || '</' || top_el || E'>\\1'
+        );
+    ELSE
+        output := xml_buf;
+    END IF;
+
+    -- remove ignorable whitesace
+    output := REGEXP_REPLACE(output::TEXT,E'>\\s+<','><','gs')::XML;
+    RETURN output;
+END;
+$F$ LANGUAGE PLPGSQL STABLE;
+
+
 /*
 
  -- Some test queries
@@ -1207,3 +1523,4 @@ EXPLAIN ANALYZE SELECT unapi.bre(36,'marcxml','record','{holdings_xml,mra,acp,ac
 */
 
 COMMIT;
+
diff --git a/Open-ILS/src/sql/Pg/upgrade/ZZZZ.schema.convert-MR-holdable_formats.sql b/Open-ILS/src/sql/Pg/upgrade/ZZZZ.schema.convert-MR-holdable_formats.sql
new file mode 100644
index 0000000..b2e4028
--- /dev/null
+++ b/Open-ILS/src/sql/Pg/upgrade/ZZZZ.schema.convert-MR-holdable_formats.sql
@@ -0,0 +1,39 @@
+
+BEGIN;
+
+-- First, explode the field into constituent parts
+WITH format_parts_array AS (
+    SELECT  a.id,
+            STRING_TO_ARRAY(a.holdable_formats, '-') AS parts
+      FROM  action.hold_request a
+      WHERE a.hold_type = 'M'
+            AND a.fulfillment_time IS NULL
+), format_parts_wide AS (
+    SELECT  id,
+            regexp_split_to_array(parts[1], '') AS item_type,
+            regexp_split_to_array(parts[2], '') AS item_form,
+            parts[3] AS item_lang
+      FROM  format_parts_array
+), converted_formats_flat AS (
+    SELECT  id, 
+            CASE WHEN ARRAY_LENGTH(item_type,1) > 0
+                THEN '"0":[{"_attr":"item_type","_val":"' || ARRAY_TO_STRING(item_type,'"},{"_attr":"item_type","_val":"') || '"}]'
+                ELSE '"0":""'
+            END AS item_type,
+            CASE WHEN ARRAY_LENGTH(item_form,1) > 0
+                THEN '"1":[{"_attr":"item_form","_val":"' || ARRAY_TO_STRING(item_form,'"},{"_attr":"item_form","_val":"') || '"}]'
+                ELSE '"1":""'
+            END AS item_form,
+            CASE WHEN item_lang <> ''
+                THEN '"2":[{"_attr":"item_lang","_val":"' || item_lang ||'"}]'
+                ELSE '"2":""'
+            END AS item_lang
+      FROM  format_parts_wide
+) UPDATE action.hold_request SET holdable_formats = '{' ||
+        converted_formats_flat.item_type || ',' ||
+        converted_formats_flat.item_form || ',' ||
+        converted_formats_flat.item_lang || '}'
+    FROM converted_formats_flat WHERE converted_formats_flat.id = action.hold_request.id;
+
+COMMIT;
+
diff --git a/Open-ILS/src/sql/Pg/upgrade/ZZZZ.schema.unapi-mmr.sql b/Open-ILS/src/sql/Pg/upgrade/ZZZZ.schema.unapi-mmr.sql
new file mode 100644
index 0000000..2aaa075
--- /dev/null
+++ b/Open-ILS/src/sql/Pg/upgrade/ZZZZ.schema.unapi-mmr.sql
@@ -0,0 +1,500 @@
+BEGIN;
+
+CREATE OR REPLACE FUNCTION asset.opac_ou_metarecord_copy_count (org INT, rid BIGINT) RETURNS TABLE (depth INT, org_unit INT, visible BIGINT, available BIGINT, unshadow BIGINT, transcendant INT) AS $f$
+DECLARE
+    ans RECORD;
+    trans INT;
+BEGIN
+    SELECT 1 INTO trans FROM biblio.record_entry b JOIN config.bib_source src ON (b.source = src.id) JOIN metabib.metarecord_source_map m ON (m.source = b.id) WHERE src.transcendant AND m.metarecord = rid;
+
+    FOR ans IN SELECT u.id, t.depth FROM actor.org_unit_ancestors(org) AS u JOIN actor.org_unit_type t ON (u.ou_type = t.id) LOOP
+        RETURN QUERY
+        SELECT  ans.depth,
+                ans.id,
+                COUNT( av.id ),
+                SUM( CASE WHEN cp.status IN (0,7,12) THEN 1 ELSE 0 END ),
+                COUNT( av.id ),
+                trans
+          FROM  
+                actor.org_unit_descendants(ans.id) d
+                JOIN asset.opac_visible_copies av ON (av.circ_lib = d.id)
+                JOIN asset.copy cp ON (cp.id = av.copy_id)
+                JOIN metabib.metarecord_source_map m ON (m.metarecord = rid AND m.source = av.record)
+          GROUP BY 1,2,6;
+
+        IF NOT FOUND THEN
+            RETURN QUERY SELECT ans.depth, ans.id, 0::BIGINT, 0::BIGINT, 0::BIGINT, trans;
+        END IF;
+
+    END LOOP;
+
+    RETURN;
+END;
+$f$ LANGUAGE PLPGSQL;
+
+CREATE OR REPLACE FUNCTION asset.opac_lasso_metarecord_copy_count (i_lasso INT, rid BIGINT) RETURNS TABLE (depth INT, org_unit INT, visible BIGINT, available BIGINT, unshadow BIGINT, transcendant INT) AS $f$
+DECLARE
+    ans RECORD;
+    trans INT;
+BEGIN
+    SELECT 1 INTO trans FROM biblio.record_entry b JOIN config.bib_source src ON (b.source = src.id) JOIN metabib.metarecord_source_map m ON (m.source = b.id) WHERE src.transcendant AND m.metarecord = rid;
+
+    FOR ans IN SELECT u.org_unit AS id FROM actor.org_lasso_map AS u WHERE lasso = i_lasso LOOP
+        RETURN QUERY
+        SELECT  -1,
+                ans.id,
+                COUNT( av.id ),
+                SUM( CASE WHEN cp.status IN (0,7,12) THEN 1 ELSE 0 END ),
+                COUNT( av.id ),
+                trans
+          FROM
+                actor.org_unit_descendants(ans.id) d
+                JOIN asset.opac_visible_copies av ON (av.circ_lib = d.id)
+                JOIN asset.copy cp ON (cp.id = av.copy_id)
+                JOIN metabib.metarecord_source_map m ON (m.metarecord = rid AND m.source = av.record)
+          GROUP BY 1,2,6;
+
+        IF NOT FOUND THEN
+            RETURN QUERY SELECT ans.depth, ans.id, 0::BIGINT, 0::BIGINT, 0::BIGINT, trans;
+        END IF;
+
+    END LOOP;   
+                
+    RETURN;     
+END;            
+$f$ LANGUAGE PLPGSQL;
+
+CREATE OR REPLACE FUNCTION asset.staff_ou_metarecord_copy_count (org INT, rid BIGINT) RETURNS TABLE (depth INT, org_unit INT, visible BIGINT, available BIGINT, unshadow BIGINT, transcendant INT) AS $f$
+DECLARE         
+    ans RECORD; 
+    trans INT;
+BEGIN
+    SELECT 1 INTO trans FROM biblio.record_entry b JOIN config.bib_source src ON (b.source = src.id) JOIN metabib.metarecord_source_map m ON (m.source = b.id) WHERE src.transcendant AND m.metarecord = rid;
+
+    FOR ans IN SELECT u.id, t.depth FROM actor.org_unit_ancestors(org) AS u JOIN actor.org_unit_type t ON (u.ou_type = t.id) LOOP
+        RETURN QUERY
+        SELECT  ans.depth,
+                ans.id,
+                COUNT( cp.id ),
+                SUM( CASE WHEN cp.status IN (0,7,12) THEN 1 ELSE 0 END ),
+                COUNT( cp.id ),
+                trans
+          FROM
+                actor.org_unit_descendants(ans.id) d
+                JOIN asset.copy cp ON (cp.circ_lib = d.id AND NOT cp.deleted)
+                JOIN asset.call_number cn ON (cn.id = cp.call_number AND NOT cn.deleted)
+                JOIN metabib.metarecord_source_map m ON (m.metarecord = rid AND m.source = cn.record)
+          GROUP BY 1,2,6;
+
+        IF NOT FOUND THEN
+            RETURN QUERY SELECT ans.depth, ans.id, 0::BIGINT, 0::BIGINT, 0::BIGINT, trans;
+        END IF;
+
+    END LOOP;
+
+    RETURN;
+END;
+$f$ LANGUAGE PLPGSQL;
+
+CREATE OR REPLACE FUNCTION asset.staff_lasso_metarecord_copy_count (i_lasso INT, rid BIGINT) RETURNS TABLE (depth INT, org_unit INT, visible BIGINT, available BIGINT, unshadow BIGINT, transcendant INT) AS $f$
+DECLARE
+    ans RECORD;
+    trans INT;
+BEGIN
+    SELECT 1 INTO trans FROM biblio.record_entry b JOIN config.bib_source src ON (b.source = src.id) JOIN metabib.metarecord_source_map m ON (m.source = b.id) WHERE src.transcendant AND m.metarecord = rid;
+
+    FOR ans IN SELECT u.org_unit AS id FROM actor.org_lasso_map AS u WHERE lasso = i_lasso LOOP
+        RETURN QUERY
+        SELECT  -1,
+                ans.id,
+                COUNT( cp.id ),
+                SUM( CASE WHEN cp.status IN (0,7,12) THEN 1 ELSE 0 END ),
+                COUNT( cp.id ),
+                trans
+          FROM
+                actor.org_unit_descendants(ans.id) d
+                JOIN asset.copy cp ON (cp.circ_lib = d.id AND NOT cp.deleted)
+                JOIN asset.call_number cn ON (cn.id = cp.call_number AND NOT cn.deleted)
+                JOIN metabib.metarecord_source_map m ON (m.metarecord = rid AND m.source = cn.record)
+          GROUP BY 1,2,6;
+
+        IF NOT FOUND THEN
+            RETURN QUERY SELECT ans.depth, ans.id, 0::BIGINT, 0::BIGINT, 0::BIGINT, trans;
+        END IF;
+
+    END LOOP;
+
+    RETURN;
+END;
+$f$ LANGUAGE PLPGSQL;
+
+CREATE OR REPLACE FUNCTION unapi.mmr_mra (
+    obj_id BIGINT,
+    format TEXT,
+    ename TEXT,
+    includes TEXT[],
+    org TEXT,
+    depth INT DEFAULT NULL,
+    slimit HSTORE DEFAULT NULL,
+    soffset HSTORE DEFAULT NULL,
+    include_xmlns BOOL DEFAULT TRUE
+) RETURNS XML AS $F$
+    SELECT  XMLELEMENT(
+        name attributes,
+        XMLATTRIBUTES(
+            CASE WHEN $9 THEN 'http://open-ils.org/spec/indexing/v1' ELSE NULL END AS xmlns,
+            'tag:open-ils.org:U2 at mmr/' || $1 AS metarecord
+        ),
+        (SELECT XMLAGG(foo.y)
+          FROM (
+            SELECT  DISTINCT ON (COALESCE(cvm.id,uvm.id))
+                    COALESCE(cvm.id,uvm.id),
+                    XMLELEMENT(
+                        name field,
+                        XMLATTRIBUTES(
+                            mra.attr AS name,
+                            cvm.value AS "coded-value",
+                            cvm.id AS "cvmid",
+                            rad.composite,
+                            rad.multi,
+                            rad.filter,
+                            rad.sorter
+                        ),
+                        mra.value
+                    )
+              FROM  metabib.record_attr_flat mra
+                    JOIN config.record_attr_definition rad ON (mra.attr = rad.name)
+                    LEFT JOIN config.coded_value_map cvm ON (cvm.ctype = mra.attr AND code = mra.value)
+                    LEFT JOIN metabib.uncontrolled_record_attr_value uvm ON (uvm.attr = mra.attr AND uvm.value = mra.value)
+              WHERE mra.id IN (SELECT source FROM metabib.metarecord_source_map WHERE metarecord = $1)
+              ORDER BY 1
+            )foo(id,y)
+        )
+    )
+$F$ LANGUAGE SQL STABLE;
+
+CREATE OR REPLACE FUNCTION evergreen.ranked_volumes(
+    bibid BIGINT[],
+    ouid INT,
+    depth INT DEFAULT NULL,
+    slimit HSTORE DEFAULT NULL,
+    soffset HSTORE DEFAULT NULL,
+    pref_lib INT DEFAULT NULL,
+    includes TEXT[] DEFAULT NULL::TEXT[]
+) RETURNS TABLE (id BIGINT, name TEXT, label_sortkey TEXT, rank BIGINT) AS $$
+    SELECT ua.id, ua.name, ua.label_sortkey, MIN(ua.rank) AS rank FROM (
+        SELECT acn.id, aou.name, acn.label_sortkey,
+            evergreen.rank_ou(aou.id, $2, $6), evergreen.rank_cp_status(acp.status),
+            RANK() OVER w
+        FROM asset.call_number acn
+            JOIN asset.copy acp ON (acn.id = acp.call_number)
+            JOIN actor.org_unit_descendants( $2, COALESCE(
+                $3, (
+                    SELECT depth
+                    FROM actor.org_unit_type aout
+                        INNER JOIN actor.org_unit ou ON ou_type = aout.id
+                    WHERE ou.id = $2
+                ), $6)
+            ) AS aou ON (acp.circ_lib = aou.id)
+        WHERE acn.record = ANY ($1)
+            AND acn.deleted IS FALSE
+            AND acp.deleted IS FALSE
+            AND CASE WHEN ('exclude_invisible_acn' = ANY($7)) THEN
+                EXISTS (
+                    SELECT 1
+                    FROM asset.opac_visible_copies
+                    WHERE copy_id = acp.id AND record = acn.record
+                ) ELSE TRUE END
+        GROUP BY acn.id, acp.status, aou.name, acn.label_sortkey, aou.id
+        WINDOW w AS (
+            ORDER BY evergreen.rank_ou(aou.id, $2, $6), evergreen.rank_cp_status(acp.status)
+        )
+    ) AS ua
+    GROUP BY ua.id, ua.name, ua.label_sortkey
+    ORDER BY rank, ua.name, ua.label_sortkey
+    LIMIT ($4 -> 'acn')::INT
+    OFFSET ($5 -> 'acn')::INT;
+$$
+LANGUAGE SQL STABLE;
+
+CREATE OR REPLACE FUNCTION evergreen.ranked_volumes
+    ( bibid BIGINT, ouid INT, depth INT DEFAULT NULL, slimit HSTORE DEFAULT NULL, soffset HSTORE DEFAULT NULL, pref_lib INT DEFAULT NULL, includes TEXT[] DEFAULT NULL::TEXT[] )
+    RETURNS TABLE (id BIGINT, name TEXT, label_sortkey TEXT, rank BIGINT)
+    AS $$ SELECT * FROM evergreen.ranked_volumes(ARRAY[$1],$2,$3,$4,$5,$6,$7) $$ LANGUAGE SQL STABLE;
+
+
+CREATE OR REPLACE FUNCTION evergreen.located_uris (
+    bibid BIGINT[],
+    ouid INT,
+    pref_lib INT DEFAULT NULL
+) RETURNS TABLE (id BIGINT, name TEXT, label_sortkey TEXT, rank INT) AS $$
+    WITH all_orgs AS (SELECT COALESCE( enabled, FALSE ) AS flag FROM config.global_flag WHERE name = 'opac.located_uri.act_as_copy')
+    SELECT DISTINCT ON (id) * FROM (
+    SELECT acn.id, COALESCE(aou.name,aoud.name), acn.label_sortkey, evergreen.rank_ou(aou.id, $2, $3) AS pref_ou
+      FROM asset.call_number acn
+           INNER JOIN asset.uri_call_number_map auricnm ON acn.id = auricnm.call_number
+           INNER JOIN asset.uri auri ON auri.id = auricnm.uri
+           LEFT JOIN actor.org_unit_ancestors( COALESCE($3, $2) ) aou ON (acn.owning_lib = aou.id)
+           LEFT JOIN actor.org_unit_descendants( COALESCE($3, $2) ) aoud ON (acn.owning_lib = aoud.id),
+           all_orgs
+      WHERE acn.record = ANY ($1)
+          AND acn.deleted IS FALSE
+          AND auri.active IS TRUE
+          AND ((NOT all_orgs.flag AND aou.id IS NOT NULL) OR COALESCE(aou.id,aoud.id) IS NOT NULL)
+    UNION
+    SELECT acn.id, COALESCE(aou.name,aoud.name) AS name, acn.label_sortkey, evergreen.rank_ou(aou.id, $2, $3) AS pref_ou
+      FROM asset.call_number acn
+           INNER JOIN asset.uri_call_number_map auricnm ON acn.id = auricnm.call_number
+           INNER JOIN asset.uri auri ON auri.id = auricnm.uri
+           LEFT JOIN actor.org_unit_ancestors( $2 ) aou ON (acn.owning_lib = aou.id)
+           LEFT JOIN actor.org_unit_descendants( $2 ) aoud ON (acn.owning_lib = aoud.id),
+           all_orgs
+      WHERE acn.record = ANY ($1)
+          AND acn.deleted IS FALSE
+          AND auri.active IS TRUE
+          AND ((NOT all_orgs.flag AND aou.id IS NOT NULL) OR COALESCE(aou.id,aoud.id) IS NOT NULL))x
+    ORDER BY id, pref_ou DESC;
+$$
+LANGUAGE SQL STABLE;
+
+CREATE OR REPLACE FUNCTION evergreen.located_uris ( bibid BIGINT, ouid INT, pref_lib INT DEFAULT NULL)
+    RETURNS TABLE (id BIGINT, name TEXT, label_sortkey TEXT, rank INT)
+    AS $$ SELECT * FROM evergreen.located_uris(ARRAY[$1],$2,$3) $$ LANGUAGE SQL STABLE;
+
+
+CREATE OR REPLACE FUNCTION unapi.mmr_holdings_xml (
+    mid BIGINT,
+    ouid INT,
+    org TEXT,
+    depth INT DEFAULT NULL,
+    includes TEXT[] DEFAULT NULL::TEXT[],
+    slimit HSTORE DEFAULT NULL,
+    soffset HSTORE DEFAULT NULL,
+    include_xmlns BOOL DEFAULT TRUE,
+    pref_lib INT DEFAULT NULL
+)
+RETURNS XML AS $F$
+     SELECT  XMLELEMENT(
+                 name holdings,
+                 XMLATTRIBUTES(
+                    CASE WHEN $8 THEN 'http://open-ils.org/spec/holdings/v1' ELSE NULL END AS xmlns,
+                    CASE WHEN ('mmr' = ANY ($5)) THEN 'tag:open-ils.org:U2 at mmr/' || $1 || '/' || $3 ELSE NULL END AS id,
+                    (SELECT metarecord_has_holdable_copy FROM asset.metarecord_has_holdable_copy($1)) AS has_holdable
+                 ),
+                 XMLELEMENT(
+                     name counts,
+                     (SELECT  XMLAGG(XMLELEMENT::XML) FROM (
+                         SELECT  XMLELEMENT(
+                                     name count,
+                                     XMLATTRIBUTES('public' as type, depth, org_unit, coalesce(transcendant,0) as transcendant, available, visible as count, unshadow)
+                                 )::text
+                           FROM  asset.opac_ou_metarecord_copy_count($2,  $1)
+                                     UNION
+                         SELECT  XMLELEMENT(
+                                     name count,
+                                     XMLATTRIBUTES('staff' as type, depth, org_unit, coalesce(transcendant,0) as transcendant, available, visible as count, unshadow)
+                                 )::text
+                           FROM  asset.staff_ou_metarecord_copy_count($2, $1)
+                                     UNION
+                         SELECT  XMLELEMENT(
+                                     name count,
+                                     XMLATTRIBUTES('pref_lib' as type, depth, org_unit, coalesce(transcendant,0) as transcendant, available, visible as count, unshadow)
+                                 )::text
+                           FROM  asset.opac_ou_metarecord_copy_count($9,  $1)
+                                     ORDER BY 1
+                     )x)
+                 ),
+                 -- XXX monograph_parts and foreign_copies are skipped in MRs ... put them back some day?
+                 XMLELEMENT(
+                     name volumes,
+                     (SELECT XMLAGG(acn ORDER BY rank, name, label_sortkey) FROM (
+                        -- Physical copies
+                        SELECT  unapi.acn(y.id,'xml','volume',evergreen.array_remove_item_by_value( evergreen.array_remove_item_by_value($5,'holdings_xml'),'bre'), $3, $4, $6, $7, FALSE), y.rank, name, label_sortkey
+                        FROM evergreen.ranked_volumes((SELECT ARRAY_AGG(source) FROM metabib.metarecord_source_map WHERE metarecord = $1), $2, $4, $6, $7, $9, $5) AS y
+                        UNION ALL
+                        -- Located URIs
+                        SELECT unapi.acn(uris.id,'xml','volume',evergreen.array_remove_item_by_value( evergreen.array_remove_item_by_value($5,'holdings_xml'),'bre'), $3, $4, $6, $7, FALSE), uris.rank, name, label_sortkey
+                        FROM evergreen.located_uris((SELECT ARRAY_AGG(source) FROM metabib.metarecord_source_map WHERE metarecord = $1), $2, $9) AS uris
+                     )x)
+                 ),
+                 CASE WHEN ('ssub' = ANY ($5)) THEN
+                     XMLELEMENT(
+                         name subscriptions,
+                         (SELECT XMLAGG(ssub) FROM (
+                            SELECT  unapi.ssub(id,'xml','subscription','{}'::TEXT[], $3, $4, $6, $7, FALSE)
+                              FROM  serial.subscription
+                              WHERE record_entry IN (SELECT source FROM metabib.metarecord_source_map WHERE metarecord = $1)
+                        )x)
+                     )
+                 ELSE NULL END
+             );
+$F$ LANGUAGE SQL STABLE;
+
+CREATE OR REPLACE FUNCTION unapi.mmr (
+    obj_id BIGINT,
+    format TEXT,
+    ename TEXT,
+    includes TEXT[],
+    org TEXT,
+    depth INT DEFAULT NULL,
+    slimit HSTORE DEFAULT NULL,
+    soffset HSTORE DEFAULT NULL,
+    include_xmlns BOOL DEFAULT TRUE,
+    pref_lib INT DEFAULT NULL
+)
+RETURNS XML AS $F$
+DECLARE
+    mmrec   metabib.metarecord%ROWTYPE;
+    leadrec biblio.record_entry%ROWTYPE;
+    subrec biblio.record_entry%ROWTYPE;
+    layout  unapi.bre_output_layout%ROWTYPE;
+    xfrm    config.xml_transform%ROWTYPE;
+    ouid    INT;
+    xml_buf TEXT; -- growing XML document
+    tmp_xml TEXT; -- single-use XML string
+    xml_frag TEXT; -- single-use XML fragment
+    top_el  TEXT;
+    output  XML;
+    hxml    XML;
+    axml    XML;
+    subxml  XML; -- subordinate records elements
+    sub_xpath TEXT; 
+    parts   TEXT[]; 
+BEGIN
+
+    -- xpath for extracting bre.marc values from subordinate records 
+    -- so they may be appended to the MARC of the master record prior
+    -- to XSLT processing.
+    -- subjects, isbn, issn, upc -- anything else?
+    sub_xpath := 
+      '//*[starts-with(@tag, "6") or @tag="020" or @tag="022" or @tag="024"]';
+
+    IF org = '-' OR org IS NULL THEN
+        SELECT shortname INTO org FROM evergreen.org_top();
+    END IF;
+
+    SELECT id INTO ouid FROM actor.org_unit WHERE shortname = org;
+
+    IF ouid IS NULL THEN
+        RETURN NULL::XML;
+    END IF;
+
+    SELECT INTO mmrec * FROM metabib.metarecord WHERE id = obj_id;
+    IF NOT FOUND THEN
+        RETURN NULL::XML;
+    END IF;
+
+    -- TODO: aggregate holdings from constituent records
+    IF format = 'holdings_xml' THEN -- the special case
+        output := unapi.mmr_holdings_xml(
+            obj_id, ouid, org, depth,
+            evergreen.array_remove_item_by_value(includes,'holdings_xml'),
+            slimit, soffset, include_xmlns);
+        RETURN output;
+    END IF;
+
+    SELECT * INTO layout FROM unapi.bre_output_layout WHERE name = format;
+
+    IF layout.name IS NULL THEN
+        RETURN NULL::XML;
+    END IF;
+
+    SELECT * INTO xfrm FROM config.xml_transform WHERE name = layout.transform;
+
+    SELECT INTO leadrec * FROM biblio.record_entry WHERE id = mmrec.master_record;
+
+    -- Grab distinct MVF for all records if requested
+    IF ('mra' = ANY (includes)) THEN 
+        axml := unapi.mmr_mra(obj_id,NULL,NULL,NULL,NULL,NULL,NULL,NULL,TRUE);
+    ELSE
+        axml := NULL::XML;
+    END IF;
+
+    xml_buf = leadrec.marc;
+
+    hxml := NULL::XML;
+    IF ('holdings_xml' = ANY (includes)) THEN
+        hxml := unapi.mmr_holdings_xml(
+                    obj_id, ouid, org, depth,
+                    evergreen.array_remove_item_by_value(includes,'holdings_xml'),
+                    slimit, soffset, include_xmlns, pref_lib);
+    END IF;
+
+    subxml := NULL::XML;
+    parts := '{}'::TEXT[];
+    FOR subrec IN SELECT bre.* FROM biblio.record_entry bre
+         JOIN metabib.metarecord_source_map mmsm ON (mmsm.source = bre.id)
+         JOIN metabib.metarecord mmr ON (mmr.id = mmsm.metarecord)
+         WHERE mmr.id = obj_id
+         ORDER BY CASE WHEN bre.id = mmr.master_record THEN 0 ELSE bre.id END
+         LIMIT COALESCE((slimit->'bre')::INT, 5) LOOP
+
+        IF subrec.id = leadrec.id THEN CONTINUE; END IF;
+        -- Append choice data from the the non-lead records to the 
+        -- the lead record document
+
+        parts := parts || xpath(sub_xpath, subrec.marc::XML)::TEXT[];
+    END LOOP;
+
+    SELECT ARRAY_TO_STRING( ARRAY_AGG( DISTINCT p ), '' )::XML INTO subxml FROM UNNEST(parts) p;
+
+    -- append data from the subordinate records to the 
+    -- main record document before applying the XSLT
+
+    IF subxml IS NOT NULL THEN 
+        xml_buf := REGEXP_REPLACE(xml_buf, 
+            '</record>(.*?)$', subxml || '</record>' || E'\\1');
+    END IF;
+
+    IF format = 'marcxml' THEN
+         -- If we're not using the prefixed namespace in 
+         -- this record, then remove all declarations of it
+        IF xml_buf !~ E'<marc:' THEN
+           xml_buf := REGEXP_REPLACE(xml_buf, 
+            ' xmlns:marc="http://www.loc.gov/MARC21/slim"', '', 'g');
+        END IF; 
+    ELSE
+        xml_buf := oils_xslt_process(xml_buf, xfrm.xslt)::XML;
+    END IF;
+
+    -- update top_el to reflect the change in xml_buf, which may
+    -- now be a different type of document (e.g. record -> mods)
+    top_el := REGEXP_REPLACE(xml_buf, E'^.*?<((?:\\S+:)?' || 
+        layout.holdings_element || ').*$', E'\\1');
+
+    IF axml IS NOT NULL THEN 
+        xml_buf := REGEXP_REPLACE(xml_buf, 
+            '</' || top_el || '>(.*?)$', axml || '</' || top_el || E'>\\1');
+    END IF;
+
+    IF hxml IS NOT NULL THEN
+        xml_buf := REGEXP_REPLACE(xml_buf, 
+            '</' || top_el || '>(.*?)$', hxml || '</' || top_el || E'>\\1');
+    END IF;
+
+    IF ('mmr.unapi' = ANY (includes)) THEN 
+        output := REGEXP_REPLACE(
+            xml_buf,
+            '</' || top_el || '>(.*?)',
+            XMLELEMENT(
+                name abbr,
+                XMLATTRIBUTES(
+                    'http://www.w3.org/1999/xhtml' AS xmlns,
+                    'unapi-id' AS class,
+                    'tag:open-ils.org:U2 at mmr/' || obj_id || '/' || org AS title
+                )
+            )::TEXT || '</' || top_el || E'>\\1'
+        );
+    ELSE
+        output := xml_buf;
+    END IF;
+
+    -- remove ignorable whitesace
+    output := REGEXP_REPLACE(output::TEXT,E'>\\s+<','><','gs')::XML;
+    RETURN output;
+END;
+$F$ LANGUAGE PLPGSQL STABLE;
+
+
+COMMIT;
+

commit 586acbd644362c823bd9865e61bf130fb0b4fc11
Author: Bill Erickson <berick at esilibrary.com>
Date:   Mon Feb 10 13:54:43 2014 -0500

    LP#1053397 IDL class mraf for metabib::record_attr_flat
    
    Signed-off-by: Bill Erickson <berick at esilibrary.com>
    Signed-off-by: Mike Rylander <mrylander at gmail.com>
    Signed-off-by: Dan Wells <dbw2 at calvin.edu>

diff --git a/Open-ILS/examples/fm_IDL.xml b/Open-ILS/examples/fm_IDL.xml
index 7c8201d..e86ee9c 100644
--- a/Open-ILS/examples/fm_IDL.xml
+++ b/Open-ILS/examples/fm_IDL.xml
@@ -901,6 +901,21 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA
         </permacrud>
 	</class>
 
+	<class id="mraf" controller="open-ils.cstore open-ils.pcrud" oils_obj:fieldmapper="metabib::record_attr_flat" oils_persist:tablename="metabib.record_attr_flat" reporter:label="MVF Record Attribute Flat List" oils_persist:field_safe="true">
+		<fields>
+			<field reporter:label="Record ID" name="id" reporter:datatype="id" oils_obj:required="true"/>
+			<field reporter:label="Attribute" name="attr" reporter:datatype="text"  oils_obj:required="true"/>
+			<field reporter:label="Value" name="value" reporter:datatype="text"  oils_obj:required="true"/>
+		</fields>
+		<links>
+			<link field="source" reltype="has_a" key="id" map="" class="bre"/>
+		</links>
+        <permacrud xmlns="http://open-ils.org/spec/opensrf/IDL/permacrud/v1">
+            <actions>
+                <retrieve/>
+            </actions>
+        </permacrud>
+	</class>
 
 	<class id="mra" controller="open-ils.cstore open-ils.pcrud" oils_obj:fieldmapper="metabib::record_attr" oils_persist:tablename="metabib.record_attr" reporter:label="SVF Record Attribute" oils_persist:field_safe="true" oils_persist:readonly="true">
 		<fields oils_persist:primary="id">

commit 922a53a5b64f0d23eacba5eb1c834a6cd60e46fb
Author: Mike Rylander <mrylander at gmail.com>
Date:   Mon Feb 10 14:10:38 2014 -0500

    LP#1269911: Release notes for Composite Record Attributes
    
    Signed-off-by: Mike Rylander <mrylander at gmail.com>
    Signed-off-by: Dan Wells <dbw2 at calvin.edu>

diff --git a/docs/RELEASE_NOTES_NEXT/OPAC/Composite_Record_Attributes.txt b/docs/RELEASE_NOTES_NEXT/OPAC/Composite_Record_Attributes.txt
new file mode 100644
index 0000000..e256184
--- /dev/null
+++ b/docs/RELEASE_NOTES_NEXT/OPAC/Composite_Record_Attributes.txt
@@ -0,0 +1,31 @@
+Composite Record Attributes
+^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+With this feature we create an abstraction on top of the Record Attribute
+infrastructure to allow the aggregation of multiple, cross-Attribute values
+under a single search filter value, accessible through new, dynamic filters.
+
+Each QueryParser filter will be created by the addition of a Composite Record
+Attribute Definition. For instance, one may wish to create a Composite Record
+Attribute Definition for an abstract "Item Type" interface component that
+uses information from the item_type, vr_format, bib_level and item_form
+Record Attribute Definitions, with each Composite Record Attribute Value
+having a different combination of Record Attribute Values from some or all of
+these Record Attribute Definitions. In this way, as single interface
+component might be presented as a dropdown with options such as "All Books",
+"All video recordings", "DVDs", "VHS Tapes", "E-Books", "Audio Books" and
+"Large Print Books". Of particular note are the "DVDs" and "VHS Tapes"
+entries, which include information from Record Attribute Definitions
+completely separate from the others. Additionally, the Composite Record
+Attribute Values defined by this Composite Record Attribute Definition
+can be used to drive behavioral logic, such as alternate icon display or
+link generation, in upgrade-friendly template adjustments.
+
+Included in this development is a replacement for the single-attribute
+Format filter supplied for basic search.  Instead, a Composite Attribute
+is used to combine the values from Item Type, Item Form and Videorecording
+Format in various ways that provide a more patron-friendly set of choices.
+
+This new Format filter can be adjusted, or even replaced with a completely
+local one, through configuration and without template adjustment.
+

commit 5a7add8fe93ac1228bf4791401d109df90c3d380
Author: Mike Rylander <mrylander at gmail.com>
Date:   Mon Feb 10 14:02:22 2014 -0500

    LP#1269911: Release notes for Multi-valued Fields and Controlled Attributes
    
    Signed-off-by: Mike Rylander <mrylander at gmail.com>
    Signed-off-by: Dan Wells <dbw2 at calvin.edu>

diff --git a/docs/RELEASE_NOTES_NEXT/OPAC/Multi_Valued_fields.txt b/docs/RELEASE_NOTES_NEXT/OPAC/Multi_Valued_fields.txt
new file mode 100644
index 0000000..03ff170
--- /dev/null
+++ b/docs/RELEASE_NOTES_NEXT/OPAC/Multi_Valued_fields.txt
@@ -0,0 +1,45 @@
+Multi-valued Record Attributes and Controlled Record Attributes
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+Having identified common use cases and reasonable restrictions that can be
+placed on the feature set, we have extended the Record Attribute
+infrastructure to support both the extraction and storage of all instances
+of a defined Attribute found within a bibliographic record, as well as
+provide new and more powerful indexing of existing data, in several ways.
+
+Record Attributes can now be defined by configuration as either single-valued
+or multi-valued. For any Attribute configured as single-valued, only the
+first value extracted from a record will be stored. This configuration
+parameter and restriction is in place to support potential query
+optimizations based on foreknowledge of whether a given Attribute is multi-
+valued or not.
+
+Record Attributes will be defined by configuration as either controlled or
+uncontrolled. A controlled Record Attribute is one that has entries in the
+Coded Value Map infrastructure specifying the valid values the record may
+carry for this attribute. If defined as a controlled Attribute, any unknown
+values extracted from a record will be ignored. Uncontrolled Attributes,
+however, may contain any value. This configuration parameter and restriction
+also supports potential query optimization.
+
+We store uncontrolled attribute values in a new table with a monotonically
+decreasing ID sequence, separating it from controlled values, reducing storage
+requirements by retaining only unique values, and making lookup faster.
+
+Restrictions
+++++++++++++
+
+ * A Record Attribute's values must match Coded Value Map entries if it is to be a Controlled Attribute. Coded Value Map control is indicated by a new "controlled" boolean on the config.record_attr_definition table.
+ * Record Attributes must "opt in" to multi-valued-ness. Record Attributes will opt in via a new "multi" boolean on config.record_attr_definition; this restriction enforces site config requirements by being explicit about the definition of "multi" fields.
+ * If controlled but not opt'd in to multi-mode, only the first value will be recorded but the new search mechanism will be used.
+ * Only single-valued Record Attributes will be available for use by the system as Sort Axes.
+ * Only controlled Record Attributes will be available for use by the TPAC as dynamically generated filter UI components, such as filter dropdowns or multi-selects.
+
+New External Dependency
++++++++++++++++++++++++
+
+This new feature requires the addition of the intarray extension to Postgres.
+This is a stock extension available on most linux distributions via the same
+package as the already-required plperl extension.
+
+

commit e4ca479407e3dd17b16a4e53868b9e09881e6ce5
Author: Bill Erickson <berick at esilibrary.com>
Date:   Mon Feb 10 14:12:15 2014 -0500

    LP#1053397: removed extraneous MR formats global flag
    
    Signed-off-by: Bill Erickson <berick at esilibrary.com>
    Signed-off-by: Mike Rylander <mrylander at gmail.com>
    Signed-off-by: Dan Wells <dbw2 at calvin.edu>

diff --git a/Open-ILS/src/sql/Pg/upgrade/QQQQ.MVF_CRA-upgrade.sql b/Open-ILS/src/sql/Pg/upgrade/QQQQ.MVF_CRA-upgrade.sql
index 05b07d1..dc1d478 100644
--- a/Open-ILS/src/sql/Pg/upgrade/QQQQ.MVF_CRA-upgrade.sql
+++ b/Open-ILS/src/sql/Pg/upgrade/QQQQ.MVF_CRA-upgrade.sql
@@ -707,17 +707,6 @@ INSERT INTO config.composite_attr_entry_definition
 
 
 
--- SEED DATA ---------------------------------------------------------------
-
--- by default, use the same format record attribute as that used for icons
--- TODO: verify attr name still matches
-INSERT INTO config.global_flag (name, label, value, enabled) VALUES (
-    'opac.metarecord.holds.format_attr', 
-    'OPAC Metarecord Hold Formats Attribute', 
-    'local_format', 
-    TRUE
-);
-
 CREATE OR REPLACE FUNCTION unapi.mra (
     obj_id BIGINT,
     format TEXT,

commit 33961761022b2021b2428d1909f75b8aeb972af3
Author: Bill Erickson <berick at esilibrary.com>
Date:   Thu Feb 6 13:31:18 2014 -0500

    LP#1269911: TPAC generates format selector from icon_format attr
    
    Signed-off-by: Bill Erickson <berick at esilibrary.com>
    Signed-off-by: Mike Rylander <mrylander at gmail.com>
    Signed-off-by: Dan Wells <dbw2 at calvin.edu>

diff --git a/Open-ILS/src/templates/opac/parts/config.tt2 b/Open-ILS/src/templates/opac/parts/config.tt2
index bd4f77c..77e1936 100644
--- a/Open-ILS/src/templates/opac/parts/config.tt2
+++ b/Open-ILS/src/templates/opac/parts/config.tt2
@@ -147,7 +147,7 @@ search.default_qtypes = ['keyword','title','author'];
 
 search.basic_config = {
     type => 'attr',
-    group => ['mattype','item_type'],
+    group => [ctx.get_cgf('opac.icon_attr').value, 'item_type'],
     none_label => l("All Formats"),
 };
 

commit 734d2302b72a0f7d55f23a2ed92e7313c10cee03
Author: Bill Erickson <berick at esilibrary.com>
Date:   Wed Feb 5 16:18:38 2014 -0500

    LP#1269911: composite attributes: configurable "icons" attribute
    
    Honor the configurable icon attribute within the TPAC.
    
    Create a default set of icons, by copying from the existing icons,
    which roughly map to the new "icon_format" record attribute
    definition mappings.
    
    Signed-off-by: Bill Erickson <berick at esilibrary.com>
    Signed-off-by: Mike Rylander <mrylander at gmail.com>
    Signed-off-by: Dan Wells <dbw2 at calvin.edu>

diff --git a/Open-ILS/src/templates/opac/parts/misc_util.tt2 b/Open-ILS/src/templates/opac/parts/misc_util.tt2
index 41ac634..78898fb 100644
--- a/Open-ILS/src/templates/opac/parts/misc_util.tt2
+++ b/Open-ILS/src/templates/opac/parts/misc_util.tt2
@@ -439,19 +439,18 @@
         END;
 
         # "mattype" == "custom marc format specifier"
-        FOR icon_style IN ['mattype', 'item_type']; 
-            node = xml.findnodes(
-                '//*[local-name()="attributes"]/*[local-name()="field"][@name="' _ icon_style _ '"]');
-            IF node AND node.textContent;
-                type = node.textContent;
-                args.format_label = PROCESS get_ccvm_label id=node.getAttribute('cvmid') search_label=1;
-                IF !args.format_label;
-                    args.format_label = node.getAttribute('coded-value');
-                END;
-                args.schema.itemtype = schema_typemap.$type || 'CreativeWork';
-                args.format_icon = ctx.media_prefix _ '/images/format_icons/' _ icon_style _ '/' _ type _ '.png';
-                LAST;
+        icon_style = ctx.get_cgf('opac.icon_attr').value || 'item_type';
+        node = xml.findnodes(
+            '//*[local-name()="attributes"]/*[local-name()="field"][@name="' _ icon_style _ '"]');
+        IF node AND node.textContent;
+            type = node.textContent;
+            args.format_label = PROCESS get_ccvm_label id=node.getAttribute('cvmid') search_label=1;
+            IF !args.format_label;
+                args.format_label = node.getAttribute('coded-value');
             END;
+            args.schema.itemtype = schema_typemap.$type || 'CreativeWork';
+            args.format_icon = ctx.media_prefix _ '/images/format_icons/' _ icon_style _ '/' _ type _ '.png';
+            LAST;
         END;
 	
         args.bibid = [];
diff --git a/Open-ILS/web/images/format_icons/icon_format/book.png b/Open-ILS/web/images/format_icons/icon_format/book.png
new file mode 100644
index 0000000..2800684
Binary files /dev/null and b/Open-ILS/web/images/format_icons/icon_format/book.png differ
diff --git a/Open-ILS/web/images/format_icons/icon_format/braille.png b/Open-ILS/web/images/format_icons/icon_format/braille.png
new file mode 100644
index 0000000..2800684
Binary files /dev/null and b/Open-ILS/web/images/format_icons/icon_format/braille.png differ
diff --git a/Open-ILS/web/images/format_icons/icon_format/casaudiobook.png b/Open-ILS/web/images/format_icons/icon_format/casaudiobook.png
new file mode 100644
index 0000000..5ada234
Binary files /dev/null and b/Open-ILS/web/images/format_icons/icon_format/casaudiobook.png differ
diff --git a/Open-ILS/web/images/format_icons/icon_format/casmusic.png b/Open-ILS/web/images/format_icons/icon_format/casmusic.png
new file mode 100644
index 0000000..132ca40
Binary files /dev/null and b/Open-ILS/web/images/format_icons/icon_format/casmusic.png differ
diff --git a/Open-ILS/web/images/format_icons/icon_format/cdaudiobook.png b/Open-ILS/web/images/format_icons/icon_format/cdaudiobook.png
new file mode 100644
index 0000000..5ada234
Binary files /dev/null and b/Open-ILS/web/images/format_icons/icon_format/cdaudiobook.png differ
diff --git a/Open-ILS/web/images/format_icons/icon_format/cdmusic.png b/Open-ILS/web/images/format_icons/icon_format/cdmusic.png
new file mode 100644
index 0000000..132ca40
Binary files /dev/null and b/Open-ILS/web/images/format_icons/icon_format/cdmusic.png differ
diff --git a/Open-ILS/web/images/format_icons/icon_format/dvd.png b/Open-ILS/web/images/format_icons/icon_format/dvd.png
new file mode 100644
index 0000000..dd0a56f
Binary files /dev/null and b/Open-ILS/web/images/format_icons/icon_format/dvd.png differ
diff --git a/Open-ILS/web/images/format_icons/icon_format/eaudio.png b/Open-ILS/web/images/format_icons/icon_format/eaudio.png
new file mode 100644
index 0000000..b6ddf3e
Binary files /dev/null and b/Open-ILS/web/images/format_icons/icon_format/eaudio.png differ
diff --git a/Open-ILS/web/images/format_icons/icon_format/ebook.png b/Open-ILS/web/images/format_icons/icon_format/ebook.png
new file mode 100644
index 0000000..2800684
Binary files /dev/null and b/Open-ILS/web/images/format_icons/icon_format/ebook.png differ
diff --git a/Open-ILS/web/images/format_icons/icon_format/equip.png b/Open-ILS/web/images/format_icons/icon_format/equip.png
new file mode 100644
index 0000000..7b76d03
Binary files /dev/null and b/Open-ILS/web/images/format_icons/icon_format/equip.png differ
diff --git a/Open-ILS/web/images/format_icons/icon_format/evideo.png b/Open-ILS/web/images/format_icons/icon_format/evideo.png
new file mode 100644
index 0000000..dd0a56f
Binary files /dev/null and b/Open-ILS/web/images/format_icons/icon_format/evideo.png differ
diff --git a/Open-ILS/web/images/format_icons/icon_format/kit.png b/Open-ILS/web/images/format_icons/icon_format/kit.png
new file mode 100644
index 0000000..7b76d03
Binary files /dev/null and b/Open-ILS/web/images/format_icons/icon_format/kit.png differ
diff --git a/Open-ILS/web/images/format_icons/icon_format/lpbook.png b/Open-ILS/web/images/format_icons/icon_format/lpbook.png
new file mode 100644
index 0000000..2800684
Binary files /dev/null and b/Open-ILS/web/images/format_icons/icon_format/lpbook.png differ
diff --git a/Open-ILS/web/images/format_icons/icon_format/map.png b/Open-ILS/web/images/format_icons/icon_format/map.png
new file mode 100644
index 0000000..f9f8047
Binary files /dev/null and b/Open-ILS/web/images/format_icons/icon_format/map.png differ
diff --git a/Open-ILS/web/images/format_icons/icon_format/microform.png b/Open-ILS/web/images/format_icons/icon_format/microform.png
new file mode 100644
index 0000000..2800684
Binary files /dev/null and b/Open-ILS/web/images/format_icons/icon_format/microform.png differ
diff --git a/Open-ILS/web/images/format_icons/icon_format/phonomusic.png b/Open-ILS/web/images/format_icons/icon_format/phonomusic.png
new file mode 100644
index 0000000..132ca40
Binary files /dev/null and b/Open-ILS/web/images/format_icons/icon_format/phonomusic.png differ
diff --git a/Open-ILS/web/images/format_icons/icon_format/phonospoken.png b/Open-ILS/web/images/format_icons/icon_format/phonospoken.png
new file mode 100644
index 0000000..5ada234
Binary files /dev/null and b/Open-ILS/web/images/format_icons/icon_format/phonospoken.png differ
diff --git a/Open-ILS/web/images/format_icons/icon_format/picture.png b/Open-ILS/web/images/format_icons/icon_format/picture.png
new file mode 100644
index 0000000..e523300
Binary files /dev/null and b/Open-ILS/web/images/format_icons/icon_format/picture.png differ
diff --git a/Open-ILS/web/images/format_icons/icon_format/score.png b/Open-ILS/web/images/format_icons/icon_format/score.png
new file mode 100644
index 0000000..f7b5c7b
Binary files /dev/null and b/Open-ILS/web/images/format_icons/icon_format/score.png differ
diff --git a/Open-ILS/web/images/format_icons/icon_format/serial.png b/Open-ILS/web/images/format_icons/icon_format/serial.png
new file mode 100644
index 0000000..2800684
Binary files /dev/null and b/Open-ILS/web/images/format_icons/icon_format/serial.png differ
diff --git a/Open-ILS/web/images/format_icons/icon_format/software.png b/Open-ILS/web/images/format_icons/icon_format/software.png
new file mode 100644
index 0000000..a347513
Binary files /dev/null and b/Open-ILS/web/images/format_icons/icon_format/software.png differ
diff --git a/Open-ILS/web/images/format_icons/icon_format/vhs.png b/Open-ILS/web/images/format_icons/icon_format/vhs.png
new file mode 100644
index 0000000..dd0a56f
Binary files /dev/null and b/Open-ILS/web/images/format_icons/icon_format/vhs.png differ

commit c33b7cf919db4eb087502322ad3b82572bb54f65
Author: Bill Erickson <berick at esilibrary.com>
Date:   Tue Jan 21 17:47:41 2014 -0500

    LP#1269911 composite attributes admin UI
    
    New interface for managing composite record attribute definitions:
    
    /eg/conify/global/config/composite_attr_entry_definition/<id>
    
    The UI for a coded value map is accessed from an existing coded value
    via a new "Manage" link column in the CCVM table.  The UI allows staff
    to build tree-shaped boolean composite definitions for CCVMs in terms
    of existing CCVMs.
    
    Additionally, the record attribute definition UI now has a link from
    each definition to the coded value map page for the attribute.
    
    Signed-off-by: Bill Erickson <berick at esilibrary.com>
    Signed-off-by: Mike Rylander <mrylander at gmail.com>
    Signed-off-by: Dan Wells <dbw2 at calvin.edu>

diff --git a/Open-ILS/src/templates/conify/global/config/coded_value_map.tt2 b/Open-ILS/src/templates/conify/global/config/coded_value_map.tt2
index 3d30089..329a432 100644
--- a/Open-ILS/src/templates/conify/global/config/coded_value_map.tt2
+++ b/Open-ILS/src/templates/conify/global/config/coded_value_map.tt2
@@ -48,12 +48,14 @@
       if (id && isComposite) {
         return "<a href='" + oilsBasePath +
           "/conify/global/config/composite_attr_entry_definition/" 
-          + id + "'>Manage</a>";
+          + id + "'>[% l('Manage') %]</a>";
         } else {
           return "";
       }
     }
 
+    var cradName = '[% ctx.page_args.0 %]';
+
     openils.Util.addOnLoad(
         function() {
 
@@ -88,6 +90,9 @@
                             // ^-- why is this not working?
                         }
                     );
+
+                    // if a crad is already selected via URL, fetch the ccvm's
+                    if (cradName) w.attr('value', cradName);
                 }
             );
 
diff --git a/Open-ILS/src/templates/conify/global/config/composite_attr_entry_definition.tt2 b/Open-ILS/src/templates/conify/global/config/composite_attr_entry_definition.tt2
new file mode 100644
index 0000000..53be578
--- /dev/null
+++ b/Open-ILS/src/templates/conify/global/config/composite_attr_entry_definition.tt2
@@ -0,0 +1,100 @@
+[% WRAPPER base.tt2 %]
+<style type="text/css">
+  #tree-container { font-size: 120%; }
+  #tree-container td { padding: 10px; }
+  #tree-container tr:nth-child(odd) {background: #E7A555;}
+  #tree-expression {font-size: 110%; border: 1px solid #555; padding: 10px; }
+  .new-data-item  { padding: 10px; }
+  .new-data-item td { padding: 10px; }
+  .new-data-item-odd  { background: #E7A555;}
+  .exp-val { font-weight: bold; color: #833; }
+</style>
+
+<h1>[% l('Composite Attribute Entry Definitions') %]</h1>
+
+<h2>
+  <div>[% l('Record Attribute: ') %] <span id='attr-def-name'></span></div>
+  <div>[% l('Coded Value: ') %] <span id='coded-value-map-name'></span></div>
+</h2>
+
+<button dojoType='dijit.form.Button' id='return-to-ccvm' scrollOnFocus='false'>
+    [% l('&#x2196; Return To Coded Value Maps') %]
+</button>
+
+<h2>[% l('Composite Data Expression') %]</h2>
+<div id='tree-expression'></div>
+
+<h2>
+  [% l('Composite Data Tree') %]
+  <div dojoType='dijit.form.Button' onclick='addChild(null)'
+    jsId='newTreeBtn'>[% l('New Tree') %]</div>
+  <div dojoType='dijit.form.Button' onclick='delTree()'
+    jsId='delTreeBtn'>[% l('Delete Tree') %]</div>
+  <div dojoType='dijit.form.Button' onclick='saveTree()'
+    jsId='saveTreeBtn'>[% l('Save Changes') %]</div>
+</h2>
+<table>
+  <tbody id='tree-container'>
+    <tr id='node-template' class='node-template'>
+      <td class='node-column'>
+        <span name='attr'></span>
+        <span class='invisible'> => </span>
+        <span name='val'></span>
+      </td>
+      <td><a name='del-child' href='javascript:;'
+        onclick='delChild(this)'>[% l('Delete') %]</a></td>
+      <td><a name='add-child' href='javascript:;' 
+        onclick='addChild(this)'>[% l('Add Child') %]</a></span></td>
+    </tr>
+  </tbody>
+</table>
+
+<div class='hidden'>
+  <span id='tree-pad' style='padding: 0px 8px 0px 8px;'> - </span>
+</div>
+
+<div class='hidden'>
+  <div dojoType='dijit.Dialog' jsId='newDataDialog'>
+    <div class='new-data-item new-data-item-odd'>
+      <input type='radio' name='data-type' id='new-data-and' value='and'/>
+        [% l('Boolean: AND') %]
+    </div>
+    <div class='new-data-item'>
+      <input type='radio' name='data-type' id='new-data-or' value='or'/>
+        [% l('Boolean: OR') %]
+    </div>
+    <div class='new-data-item new-data-item-odd'>
+      <label>
+        <input type='radio' name='data-type' id='new-data-not' value='not'/>
+        [% l('Boolean: NOT') %]
+      </label>
+    </div>
+    <div class='new-data-item'>
+      <label>
+        <input type='radio' name='data-type' id='new-data-attr' value='attr' checked='checked'/>
+        [% l('Record Attribute') %]
+      </label>
+      <table>
+        <tr>
+          <td>[% l('Select Attribute Type: ') %]</td>
+          <td><div id='new-data-crad-selector'></td>
+        </tr>
+        <tr>
+          <td>[% l('Select Value: ') %]</td>
+          <td><div dojoType='dijit.form.FilteringSelect' 
+            jsId='ccvmSelector'></div></td>
+        </tr>
+      </table>
+    </div>
+    <div dojoType='dijit.form.Button' 
+      type='submit' jsId='newDataSubmit'>[% l('Submit') %]</div>
+  </div>
+</div>
+
+<script>var ccvmId = '[% ctx.page_args.0 %]'</script>
+<script type="text/javascript" 
+  src='[% ctx.media_prefix %]/js/ui/default/conify/global/config/composite_attr_entry_definition.js'>
+</script>
+
+[% END %]
+
diff --git a/Open-ILS/src/templates/conify/global/config/record_attr_definition.tt2 b/Open-ILS/src/templates/conify/global/config/record_attr_definition.tt2
index fbe1012..aa32f08 100644
--- a/Open-ILS/src/templates/conify/global/config/record_attr_definition.tt2
+++ b/Open-ILS/src/templates/conify/global/config/record_attr_definition.tt2
@@ -18,11 +18,34 @@
             query="{name: '*'}"
             fmClass='crad'
             showPaginator='true'
-            editOnEnter='true'/>
+            editOnEnter='true'>
+      <thead>
+        <tr><th field='coded_value_maps' 
+                get='getCcvms' 
+                formatter='formatCcvmsLink'>
+            [% l('Coded Value Maps') %]</th></tr>
+      </thead>
+    </table>
  </div>
 
 <script type ="text/javascript">
     dojo.require('openils.widget.AutoGrid');
+
+    function getCcvms(rowId, item) {
+      if (!item) return '';
+      return this.grid.store.getValue(item, 'name');
+    }
+
+    function formatCcvmsLink(name) {
+      if (name) {
+        return "<a href='" + oilsBasePath +
+          "/conify/global/config/coded_value_map/"
+          + name + "'>[% l('Manage') %]</a>";
+        } else {
+          return "";
+      }
+    }
+
     openils.Util.addOnLoad(
         function() {
             // avoid loading the entire config.xml_transform object
diff --git a/Open-ILS/web/js/ui/default/conify/global/config/composite_attr_entry_definition.js b/Open-ILS/web/js/ui/default/conify/global/config/composite_attr_entry_definition.js
new file mode 100644
index 0000000..daa34db
--- /dev/null
+++ b/Open-ILS/web/js/ui/default/conify/global/config/composite_attr_entry_definition.js
@@ -0,0 +1,385 @@
+dojo.require('dijit.Dialog');
+dojo.require('dijit.form.Button');
+dojo.require('dijit.form.FilteringSelect');
+dojo.require('openils.PermaCrud');
+dojo.require('openils.widget.AutoFieldWidget');
+
+var recordAttrDefs = {};// full name => crad map
+var codedValueMaps = {};// growing cache of id => ccvm
+var compositeDef;       // the thing what we're building / editing
+var nodeTree;           // internal composite attrs tree representation
+var treeIndex = 0;      // internal composit attrs node index
+
+var localeStrings = {}; // TODO: move to nls file
+localeStrings.OR = "OR";
+localeStrings.AND = "AND";
+localeStrings.NOT = "NOT";
+
+function drawPage() {
+    console.log('fetching ccvm ' + ccvmId);
+
+    var asyncReqs = 2;
+
+    new openils.PermaCrud().retrieve('ccvm', ccvmId, {
+        flesh : 1, 
+        flesh_fields : {ccvm : ['composite_def', 'ctype']},
+        oncomplete : function(r) {
+            map = openils.Util.readResponse(r);
+
+            // draw the names
+            dojo.byId('attr-def-name').innerHTML = 
+                map.ctype().label();
+            dojo.byId('coded-value-map-name').innerHTML = 
+                map.code() + ' / ' + map.value();
+
+            dojo.byId('return-to-ccvm').onclick = function() {
+                location.href = oilsBasePath + 
+                '/conify/global/config/coded_value_map/' + 
+                map.ctype().name();
+            };
+
+            // build a new def if needed
+            compositeDef = map.composite_def();
+            if (!compositeDef) {
+                compositeDef = new fieldmapper.ccraed();
+                compositeDef.isnew(true);
+                compositeDef.coded_value(map.id());
+            }
+            if (!--asyncReqs) drawCompositDef();
+        }
+    });
+
+    new openils.PermaCrud().retrieveAll('crad', {
+        order_by : {crad : ['name']},
+        oncomplete : function(r) {
+            var defs = openils.Util.readResponse(r); 
+            dojo.forEach(defs, function(def) {
+                recordAttrDefs[def.name()] = def;
+            });
+            if (!--asyncReqs) drawCompositDef();
+        }
+    });
+}
+
+var fetchAttrs = [];
+function drawCompositDef() {
+    var defBlob = JSON2js(compositeDef.definition());
+
+    importNodeTree(null, defBlob);
+
+    if (fetchAttrs.length) {
+        new openils.PermaCrud().search('ccvm', {'-or' : fetchAttrs}, {
+            oncomplete : function(r) {
+                var maps = openils.Util.readResponse(r);
+                dojo.forEach(maps, function(map) {
+                    codedValueMaps[map.id()] = map;
+                });
+                drawNodeTree();
+            }
+        });
+    } else {
+        drawNodeTree();
+    }
+}
+
+// translate the DB-stored tree into a local structure
+function importNodeTree(pnode, node) {
+    if (!node) return;
+
+    var newnode = {
+        index : treeIndex++,
+        pnode : pnode,
+        children : []
+    }
+
+    if (pnode) {
+        pnode.children.push(newnode);
+    } else {
+        fetchAttrs = [];
+        nodeTree = newnode;
+    }
+
+    if (dojo.isArray(node)) { 
+        newnode.or = true;
+        dojo.forEach(node, function(n) { importNodeTree(newnode, n) });
+
+    } else if (node._not) {
+        newnode.not = true;
+        importNodeTree(newnode, node._not);
+
+    } else if (node._attr) {
+        // list of attrs that we have to fetch for display
+        fetchAttrs.push({'-and' : {ctype : node._attr, code : node._val}});
+
+        newnode.attr = node._attr;
+        newnode.val = node._val;
+
+    } else {
+        newnode.and = true;
+        dojo.forEach(Object.keys(node).sort(), function(key) {
+            importNodeTree(newnode, node[key]);
+        });
+    }
+}
+
+function byname(elm, name) {
+    return dojo.query('[name=' + name + ']', elm)[0];
+}
+function findccvm(ctype, code) {
+    for (var id in codedValueMaps) {
+        var m = codedValueMaps[id];
+        if (m.code() == code && m.ctype() == ctype) {
+            return m;
+        }
+    }
+    console.error('cannot find ccvm ' + ctype + ' : ' + code);
+}
+
+// render the local structure tree in the DOM
+var nodeTemplate;
+var nodeTbody;
+function drawNodeTree(node) {
+
+    if (!nodeTbody) {
+        nodeTbody = dojo.byId('tree-container');
+        nodeTemplate = nodeTbody.removeChild(dojo.byId('node-template'));
+    } 
+
+    var root = false;
+    if (!node) {
+        dojo.empty(nodeTbody);
+        if (!nodeTree) {
+            newTreeBtn.attr('disabled', false);
+            delTreeBtn.attr('disabled', true);
+            return;
+        } else {
+            node = nodeTree;
+            root = true;
+        }
+    }
+
+    newTreeBtn.attr('disabled', true);
+    delTreeBtn.attr('disabled', false);
+
+    var depth = -1;
+    function d(node) {if (node) {depth++; d(node.pnode);}};
+    d(node);
+
+    node.element = nodeTemplate.cloneNode(true);
+    var expression = '';
+
+    var addLink = byname(node.element, 'add-child');
+    var delLink = byname(node.element, 'del-child');
+    addLink.setAttribute('index', node.index);
+    delLink.setAttribute('index', node.index);
+
+    if (node.or) {
+        byname(node.element, 'attr').innerHTML = localeStrings.OR;
+
+    } else if (node.and) {
+        byname(node.element, 'attr').innerHTML = localeStrings.AND;
+
+    } else if (node.not) {
+        byname(node.element, 'attr').innerHTML = localeStrings.NOT;
+
+    } else {
+        dojo.addClass(addLink, 'hidden');
+
+        byname(node.element, 'attr').innerHTML = 
+            recordAttrDefs[node.attr].label() + ' (' + node.attr + ')';
+
+        var map = findccvm(node.attr, node.val);
+        byname(node.element, 'val').innerHTML = 
+            map.value() + ' (' + map.code() + ')';
+
+        dojo.removeClass(
+            dojo.query('.invisible', node.element)[0], 'invisible');
+
+        expression = map.value();
+    }
+
+    nodeTbody.appendChild(node.element);
+
+    var nc = dojo.query('.node-column', node.element)[0];
+    for (var i = 0; i < depth; i++) {
+        nc.insertBefore(dojo.byId('tree-pad').cloneNode(true), nc.firstChild); 
+    }
+
+    if (node.attr) return expression;
+
+    if (node.not) {
+        if (node.children[0]) {
+            expression = localeStrings.NOT + 
+                ' ' + drawNodeTree(node.children[0]);
+        }
+
+    } else { // AND | OR
+
+        if (!root) expression = '( ';
+        for (var i = 0; i < node.children.length; i++) {
+            expression += drawNodeTree(node.children[i]);
+            if (i == node.children.length - 1) break;
+            expression += ' ' + (node.or ? localeStrings.OR : 
+                (node.and ? localeStrings.AND : localeStrings.NOT)) + ' ';
+        }
+        if (!root) expression += ' )';
+    }
+
+    if (root) {
+        dojo.byId('tree-expression').innerHTML = expression;
+    }
+
+    return expression;
+}
+
+function findNode(index, node) {
+    if (!node) node = nodeTree;
+    if (node.index == index) return node;
+    for (var i = 0; i < node.children.length; i++) {
+        var n = findNode(index, node.children[i]);
+        if (n) return n;
+    }
+}
+
+var cradSelector;
+function buildSelectors() {
+    if (cradSelector) return;
+    cradSelector = new openils.widget.AutoFieldWidget({
+        fmClass : 'crad',
+        selfReference : true,
+        parentNode : 'new-data-crad-selector'
+    });
+    cradSelector.build(function(w, ww) {
+        dojo.connect(w, 'onChange', function(val) { 
+            dojo.byId('new-data-attr').checked = true;
+            new openils.PermaCrud().search('ccvm', {ctype : val}, {
+                oncomplete : function(r) {
+                    var maps = openils.Util.readResponse(r);
+                    var items = [];
+                    dojo.forEach(maps, function(map) {
+                        codedValueMaps[map.id()] = map;
+                        items.push({name : map.value(), value : map.id()});
+                    });
+                    ccvmSelector.store = new dojo.data.ItemFileReadStore({
+                        data : {
+                            identifier : 'value',
+                            label : 'name',
+                            items : items
+                        }
+                    });
+                    ccvmSelector.startup();
+                }
+            });
+        });
+    });
+}
+
+function addChild(link) {
+    buildSelectors();
+    var ctxNode = link ? findNode(link.getAttribute('index')) : null;
+
+    newDataSubmit.onClick = function(args) {
+        var node = {
+            index : treeIndex++,
+            pnode : ctxNode,
+            children : []
+        };
+
+        if (dojo.byId('new-data-and').checked) {
+            node.and = true;
+        } else if (dojo.byId('new-data-or').checked) {
+            node.or = true;
+        } else if (dojo.byId('new-data-not').checked) {
+            node.not = true;
+        } else {
+            node.attr = cradSelector.widget.attr('value');
+            node.val = codedValueMaps[ccvmSelector.attr('value')].code();
+            if (!node.attr || !node.val) return;
+        }
+
+        newDataDialog.hide();
+
+        // for visual clarity, push the non-boolean children to the front
+        if (ctxNode) {
+            if (node.and || node.or || node.not) {
+                ctxNode.children.push(node);
+            } else {
+                ctxNode.children.unshift(node);
+            }
+        } else {
+            // starting a new tree from scratch
+            nodeTree = node; 
+        }
+        drawNodeTree();
+    }
+
+    dojo.byId('new-data-attr').checked = true;
+    newDataDialog.show(); 
+}
+
+function delChild(link) {
+    var node = findNode(link.getAttribute('index'));
+
+    if (node.pnode) {
+        for (var i = 0; i < node.pnode.children.length; i++) {
+            var child = node.pnode.children[i];
+            if (child.index == node.index) {
+                node.pnode.children.splice(i, 1);
+                break;
+            }
+        }
+    } else {
+        newTreeBtn.attr('disabled', false);
+        delTreeBtn.attr('disabled', true);
+        nodeTree = null;
+    }
+
+    drawNodeTree();
+}
+
+function delTree() {
+    nodeTree = null;
+    drawNodeTree(); // resets
+    new openils.PermaCrud().eliminate(compositeDef);
+    compositeDef.isnew(true);
+    compositeDef.definition(null);
+}
+
+function saveTree() {
+    var expression = exportTree();
+
+    compositeDef.definition(js2JSON(expression))
+    var pcrud = new openils.PermaCrud();
+    saveTreeBtn.attr('disabled', true);
+
+    var oncomplete = function(r) {
+        openils.Util.readResponse(r);  // pickup any alerts
+        saveTreeBtn.attr('disabled', false);
+        compositeDef.isnew(false);
+    }
+
+    var pfunc = compositeDef.isnew() ? 'create' : 'update';
+    pcrud[pfunc](compositeDef, {oncomplete : oncomplete});
+}
+
+function exportTree(node) {
+    if (!node) node = nodeTree;
+
+    if (node.attr) 
+        return {_attr : node.attr, _val : node.val};
+
+    if (node.not)
+        // _not nodes may only have one child
+        return {_not : exportTree(node.children[0])};
+
+    var compiled;
+    for (var i = 0; i < node.children.length; i++) {
+        var child = node.children[i];
+        if (!compiled) compiled = node.or ? [] : {};
+        compiled[i] = exportTree(child);
+    }
+
+    return compiled;
+}
+
+openils.Util.addOnLoad(drawPage);

commit 00ced69379ad8101beaf2cdfa410d72ece9bc6b0
Author: Mike Rylander <mrylander at gmail.com>
Date:   Wed Jan 15 16:48:13 2014 -0500

    LP#1269911: Upgrade script for MVF and CRA
    
    Signed-off-by: Mike Rylander <mrylander at gmail.com>
    Signed-off-by: Dan Wells <dbw2 at calvin.edu>

diff --git a/Open-ILS/src/sql/Pg/002.schema.config.sql b/Open-ILS/src/sql/Pg/002.schema.config.sql
index ef517e9..e165f06 100644
--- a/Open-ILS/src/sql/Pg/002.schema.config.sql
+++ b/Open-ILS/src/sql/Pg/002.schema.config.sql
@@ -852,8 +852,8 @@ END;
 $f$ LANGUAGE PLPGSQL;
 
 CREATE TABLE config.composite_attr_entry_definition(
-    coded_value PRIMARY KEY NOT NULL REFERENCES config.coded_value_map (id) ON UPDATE CASCADE ON DELETE CASCADE,
-    definition  TEXT        NOT NULL -- JSON
+    coded_value INT  PRIMARY KEY NOT NULL REFERENCES config.coded_value_map (id) ON UPDATE CASCADE ON DELETE CASCADE,
+    definition  TEXT    NOT NULL -- JSON
 );
 
 -- List applied db patches that are deprecated by (and block the application of) my_db_patch
diff --git a/Open-ILS/src/sql/Pg/030.schema.metabib.sql b/Open-ILS/src/sql/Pg/030.schema.metabib.sql
index fc0e454..c42b2df 100644
--- a/Open-ILS/src/sql/Pg/030.schema.metabib.sql
+++ b/Open-ILS/src/sql/Pg/030.schema.metabib.sql
@@ -296,19 +296,12 @@ CREATE VIEW metabib.full_attr_id_map AS
     SELECT id, attr, value FROM metabib.composite_attr_id_map;
 
 
-CREATE FUNCTION metabib.compile_composite_attr ( cattr_id INT ) RETURNS query_int AS $func$
+CREATE OR REPLACE FUNCTION metabib.compile_composite_attr ( cattr_def TEXT ) RETURNS query_int AS $func$
 
-    use JSON;
-    my $cid = shift;
+    use JSON::XS;
+    my $def = decode_json(shift);
 
-    my $cattr = spi_exec_query(
-        "SELECT * FROM config.composite_attr_entry_defintion WHERE id = $cid"
-    )->{rows}[0];
-
-    die("Composite attribute not found with an id of $cid") unless $cattr;
-
-    my $plan = spi_prepare('SELECT * FROM metabib.full_attr_id_map WHERE attr = $1 AND value = $2', qw/TEXT TEXT/);
-    my $def = from_json $cattr->{definition};
+    die("Composite attribute definition not supplied") unless $def;
 
     sub recurse {
         my $d = shift;
@@ -317,28 +310,37 @@ CREATE FUNCTION metabib.compile_composite_attr ( cattr_id INT ) RETURNS query_in
 
         if (ref $d eq 'HASH') { # node or AND
             if (exists $d->{_attr}) { # it is a node
-                return spi_query_prepared(
+                my $plan = spi_prepare('SELECT * FROM metabib.full_attr_id_map WHERE attr = $1 AND value = $2', qw/TEXT TEXT/);
+                return spi_exec_prepared(
                     $plan, {limit => 1}, $d->{_attr}, $d->{_val}
                 )->{rows}[0]{id};
+                spi_freeplan($plan);
             } elsif (exists $d->{_not} && scalar(keys(%$d)) == 1) { # it is a NOT
                 return '!' . recurse($$d{_not});
             } else { # an AND list
                 @list = map { recurse($$d{$_}) } sort keys %$d;
             }
-        } elsif (ref $d eq 'ARRAY')
-            $j = '|'
+        } elsif (ref $d eq 'ARRAY') {
+            $j = '|';
             @list = map { recurse($_) } @$d;
         }
+
+        @list = grep { defined && $_ ne '' } @list;
+
         return '(' . join($j, at list) . ')' if @list;
         return '';
     }
 
-    return recurse($def);
+    return recurse($def) || undef;
+
+$func$ IMMUTABLE LANGUAGE plperlu;
 
-$func$ STRICT STABLE IMMUTABLE LANGUAGE plperlu;
+CREATE OR REPLACE FUNCTION metabib.compile_composite_attr ( cattr_id INT ) RETURNS query_int AS $func$
+    SELECT metabib.compile_composite_attr(definition) FROM config.composite_attr_entry_definition WHERE coded_value = $1;
+$func$ STRICT IMMUTABLE LANGUAGE SQL;
 
 CREATE TABLE metabib.record_attr_vector_list (
-    source  BIGINT  PRIMARY KEY REFERNECES  biblio.record_entry (id),
+    source  BIGINT  PRIMARY KEY REFERENCES biblio.record_entry (id),
     vlist   INT[]   NOT NULL -- stores id from ccvm AND murav
 );
 CREATE INDEX mrca_vlist_idx ON metabib.record_attr_vector_list USING gin ( vlist gin__int_ops );
@@ -376,7 +378,7 @@ CREATE VIEW metabib.record_attr_flat AS
             m.attr,
             m.value
       FROM  metabib.full_attr_id_map m
-            JOIN  metabib.record_attr_vector_list v ( m.id = ANY( v.vlist ) );
+            JOIN  metabib.record_attr_vector_list v ON ( m.id = ANY( v.vlist ) );
 
 CREATE VIEW metabib.record_attr AS
     SELECT id, HSTORE( ARRAY_AGG( attr ), ARRAY_AGG( value ) ) AS attrs FROM metabib.record_attr_flat GROUP BY 1;
@@ -1404,7 +1406,7 @@ DECLARE
     norm_attr_value TEXT[];
     tmp_xml         XML;
     attr_def        config.record_attr_definition%ROWTYPE;
-    ccvm_row        config.code_value_map%ROWTYPE;
+    ccvm_row        config.coded_value_map%ROWTYPE;
 BEGIN
 
     IF attr_list IS NULL OR rdeleted THEN -- need to do the full dance on INSERT or undelete
@@ -1417,11 +1419,11 @@ BEGIN
 
     FOR attr_def IN SELECT * FROM config.record_attr_definition WHERE NOT composite AND name = ANY( attr_list ) ORDER BY format LOOP
 
-        attr_value := '{}'::TEXT[]
-        norm_attr_value := '{}'::TEXT[]
-        attr_vector_tmp := '{}'::INT[]
+        attr_value := '{}'::TEXT[];
+        norm_attr_value := '{}'::TEXT[];
+        attr_vector_tmp := '{}'::INT[];
 
-        SELECT * INTO ccvm_row FROM config.code_value_map c WHERE c.ctype = attr_def.name; 
+        SELECT * INTO ccvm_row FROM config.coded_value_map c WHERE c.ctype = attr_def.name LIMIT 1; 
 
         -- tag+sf attrs only support SVF
         IF attr_def.tag IS NOT NULL THEN -- tag (and optional subfield list) selection
@@ -1433,7 +1435,7 @@ BEGIN
                         WHEN attr_def.sf_list IS NOT NULL 
                             THEN POSITION(subfield IN attr_def.sf_list) > 0
                         ELSE TRUE
-                        END
+                    END
               GROUP BY tag
               ORDER BY tag
               LIMIT 1;
@@ -1448,7 +1450,7 @@ BEGIN
         ELSIF attr_def.xpath IS NOT NULL THEN -- and xpath expression
 
             SELECT INTO xfrm * FROM config.xml_transform WHERE name = attr_def.format;
-    
+        
             -- See if we can skip the XSLT ... it's expensive
             IF prev_xfrm IS NULL OR prev_xfrm <> xfrm.name THEN
                 -- Can't skip the transform
@@ -1468,22 +1470,24 @@ BEGIN
             END IF;
 
             FOR tmp_xml IN SELECT XPATH(attr_def.xpath, transformed_xml, ARRAY[ARRAY[xfrm.prefix, xfrm.namespace_uri]]) LOOP
-                attr_value := attr_value ||
-                                oils_xpath_string(
-                                    '//*',
-                                    tmp_xml::TEXT,
-                                    COALESCE(attr_def.joiner,' '),
-                                    ARRAY[ARRAY[xfrm.prefix, xfrm.namespace_uri]]
-                                );
-                EXIT WHEN NOT attr_def.multi;
+                tmp_val := oils_xpath_string(
+                                '//*',
+                                tmp_xml::TEXT,
+                                COALESCE(attr_def.joiner,' '),
+                                ARRAY[ARRAY[xfrm.prefix, xfrm.namespace_uri]]
+                            );
+                IF tmp_val IS NOT NULL AND BTRIM(tmp_val) <> '' THEN
+                    attr_value := attr_value || tmp_val;
+                    EXIT WHEN NOT attr_def.multi;
+                END IF;
             END LOOP;
 
         ELSIF attr_def.phys_char_sf IS NOT NULL THEN -- a named Physical Characteristic, see config.marc21_physical_characteristic_*_map
-            SELECT  ARRAY_AGG(m.value) INTO attr_vlue
+            SELECT  ARRAY_AGG(m.value) INTO attr_value
               FROM  vandelay.marc21_physical_characteristics(rmarc) v
                     LEFT JOIN config.marc21_physical_characteristic_value_map m ON (m.id = v.value)
-              WHERE v.subfield = attr_def.phys_char_sf
-                    AND ( ccvm.id IS NULL OR ( ccvm.id IS NOT NULL AND v.id IS NOT NULL) );
+              WHERE v.subfield = attr_def.phys_char_sf AND (m.value IS NOT NULL AND BTRIM(m.value) <> '')
+                    AND ( ccvm_row.id IS NULL OR ( ccvm_row.id IS NOT NULL AND v.id IS NOT NULL) );
 
             IF NOT attr_def.multi THEN
                 attr_value := ARRAY[attr_value[1]];
@@ -1491,8 +1495,8 @@ BEGIN
 
         END IF;
 
-        -- apply index normalizers to attr_value
-        FOR tmp_val IN SELECT value FROM UNNEST(attr_value) x(value);
+                -- apply index normalizers to attr_value
+        FOR tmp_val IN SELECT value FROM UNNEST(attr_value) x(value) LOOP
             FOR normalizer IN
                 SELECT  n.func AS func,
                         n.param_count AS param_count,
@@ -1502,37 +1506,41 @@ BEGIN
                   WHERE attr = attr_def.name
                   ORDER BY m.pos LOOP
                     EXECUTE 'SELECT ' || normalizer.func || '(' ||
-                        COALESCE( quote_literal( tmp_val ), 'NULL' ) ||
+                    COALESCE( quote_literal( tmp_val ), 'NULL' ) ||
                         CASE
                             WHEN normalizer.param_count > 0
                                 THEN ',' || REPLACE(REPLACE(BTRIM(normalizer.params,'[]'),E'\'',E'\\\''),E'"',E'\'')
                                 ELSE ''
                             END ||
-                        ')' INTO tmp_val;
+                    ')' INTO tmp_val;
 
             END LOOP;
-            norm_attr_value := norm_attr_value || tmp_val;
+            IF tmp_val IS NOT NULL AND BTRIM(tmp_val) <> '' THEN
+                norm_attr_value := norm_attr_value || tmp_val;
+            END IF;
         END LOOP;
-
+        
         IF attr_def.filter THEN
             -- Create unknown uncontrolled values and find the IDs of the values
-            IF ccvm.id IS NULL THEN
-                FOR tmp_val FROM SELECT value FROM UNNEST(norm_attr_value) x(value) LOOP
-                    BEGIN; -- use subtransaction to isolate unique constraint violations
-                        INSERT INTO metabib.uncontrolled_record_attr_value ( attr, value ) VALUES ( attr_def.name, tmp_val );
-                    EXCEPTION WHEN unique_violation THEN END;
+            IF ccvm_row.id IS NULL THEN
+                FOR tmp_val IN SELECT value FROM UNNEST(norm_attr_value) x(value) LOOP
+                    IF tmp_val IS NOT NULL AND BTRIM(tmp_val) <> '' THEN
+                        BEGIN -- use subtransaction to isolate unique constraint violations
+                            INSERT INTO metabib.uncontrolled_record_attr_value ( attr, value ) VALUES ( attr_def.name, tmp_val );
+                        EXCEPTION WHEN unique_violation THEN END;
+                    END IF;
                 END LOOP;
-    
+
                 SELECT ARRAY_AGG(id) INTO attr_vector_tmp FROM metabib.uncontrolled_record_attr_value WHERE attr = attr_def.name AND value = ANY( norm_attr_value );
             ELSE
-                SELECT ARRAY_AGG(id) INTO attr_vector_tmp FROM config.coded_value_map WHERE ctype = attr_def.name AND value = ANY( norm_attr_value );
+                SELECT ARRAY_AGG(id) INTO attr_vector_tmp FROM config.coded_value_map WHERE ctype = attr_def.name AND code = ANY( norm_attr_value );
             END IF;
 
             -- Add the new value to the vector
             attr_vector := attr_vector || attr_vector_tmp;
         END IF;
 
-        IF attr_def.sorter THEN
+        IF attr_def.sorter AND norm_attr_value[1] IS NOT NULL THEN
             DELETE FROM metabib.record_sorter WHERE source = rid AND attr = attr_def.name;
             INSERT INTO metabib.record_sorter (source, attr, value) VALUES (rid, attr_def.name, norm_attr_value[1]);
         END IF;
@@ -1560,7 +1568,7 @@ BEGIN
         FOR ccvm_row IN SELECT * FROM config.coded_value_map c WHERE c.ctype = attr_def.name ORDER BY value LOOP
 
             tmp_val := metabib.compile_composite_attr( ccvm_row.id );
-            NEXT WHEN tmp_val IS NULL OR tmp_val = ''; -- nothing to do
+            CONTINUE WHEN tmp_val IS NULL OR tmp_val = ''; -- nothing to do
 
             IF attr_def.filter THEN
                 IF attr_vector @@ tmp_val::query_int THEN
@@ -1591,14 +1599,14 @@ BEGIN
 
 END;
 
-$$ LANGUAGE PLPGSQL;
+$func$ LANGUAGE PLPGSQL;
 
 
 -- AFTER UPDATE OR INSERT trigger for biblio.record_entry
 CREATE OR REPLACE FUNCTION biblio.indexing_ingest_or_delete () RETURNS TRIGGER AS $func$
 BEGIN
 
-    IF NEW.deleted IS TRUE THEN -- If this bib is deleted
+    IF NEW.deleted THEN -- If this bib is deleted
         PERFORM * FROM config.internal_flag WHERE
             name = 'ingest.metarecord_mapping.preserve_on_delete' AND enabled;
         IF NOT FOUND THEN
@@ -2471,3 +2479,4 @@ END;
 $p$ LANGUAGE PLPGSQL;
 
 COMMIT;
+
diff --git a/Open-ILS/src/sql/Pg/990.schema.unapi.sql b/Open-ILS/src/sql/Pg/990.schema.unapi.sql
index 1c46b0b..6d1e070 100644
--- a/Open-ILS/src/sql/Pg/990.schema.unapi.sql
+++ b/Open-ILS/src/sql/Pg/990.schema.unapi.sql
@@ -1122,34 +1122,46 @@ CREATE OR REPLACE FUNCTION unapi.auri ( obj_id BIGINT, format TEXT,  ename TEXT,
           GROUP BY uri.id, use_restriction, href, label;
 $F$ LANGUAGE SQL STABLE;
 
-CREATE OR REPLACE FUNCTION unapi.mra ( obj_id BIGINT, format TEXT,  ename TEXT, includes TEXT[], org TEXT, depth INT DEFAULT NULL, slimit HSTORE DEFAULT NULL, soffset HSTORE DEFAULT NULL, include_xmlns BOOL DEFAULT TRUE ) RETURNS XML AS $F$
-        SELECT  XMLELEMENT(
-                    name attributes,
-                    XMLATTRIBUTES(
-                        CASE WHEN $9 THEN 'http://open-ils.org/spec/indexing/v1' ELSE NULL END AS xmlns,
-                        'tag:open-ils.org:U2 at mra/' || mra.id AS id,
-                        'tag:open-ils.org:U2 at bre/' || mra.id AS record
-                    ),
-                    (SELECT XMLAGG(foo.y)
-                      FROM (SELECT XMLELEMENT(
-                                name field,
-                                XMLATTRIBUTES(
-                                    key AS name,
-                                    cvm.value AS "coded-value",
-                                    cvm.id AS "cvmid",
-                                    rad.filter,
-                                    rad.sorter
-                                ),
-                                x.value
-                            )
-                           FROM EACH(mra.attrs) AS x
-                                JOIN config.record_attr_definition rad ON (x.key = rad.name)
-                                LEFT JOIN config.coded_value_map cvm ON (cvm.ctype = x.key AND code = x.value)
-                        )foo(y)
+CREATE OR REPLACE FUNCTION unapi.mra (
+    obj_id BIGINT,
+    format TEXT,
+    ename TEXT,
+    includes TEXT[],
+    org TEXT,
+    depth INT DEFAULT NULL,
+    slimit HSTORE DEFAULT NULL,
+    soffset HSTORE DEFAULT NULL,
+    include_xmlns BOOL DEFAULT TRUE
+) RETURNS XML AS $F$
+    SELECT  XMLELEMENT(
+        name attributes,
+        XMLATTRIBUTES(
+            CASE WHEN $9 THEN 'http://open-ils.org/spec/indexing/v1' ELSE NULL END AS xmlns,
+            'tag:open-ils.org:U2 at mra/' || $1 AS id, 
+            'tag:open-ils.org:U2 at bre/' || $1 AS record 
+        ),  
+        (SELECT XMLAGG(foo.y)
+          FROM (
+            SELECT  XMLELEMENT(
+                        name field,
+                        XMLATTRIBUTES(
+                            mra.attr AS name,
+                            cvm.value AS "coded-value",
+                            cvm.id AS "cvmid",
+                            rad.composite,
+                            rad.multi,
+                            rad.filter,
+                            rad.sorter
+                        ),
+                        mra.value
                     )
-                )
-          FROM  metabib.record_attr mra
-          WHERE mra.id = $1;
+              FROM  metabib.record_attr_flat mra
+                    JOIN config.record_attr_definition rad ON (mra.attr = rad.name)
+                    LEFT JOIN config.coded_value_map cvm ON (cvm.ctype = mra.attr AND code = mra.value)
+              WHERE mra.id = $1
+            )foo(y)
+        )   
+    )   
 $F$ LANGUAGE SQL STABLE;
 
 CREATE OR REPLACE FUNCTION unapi.circ (obj_id BIGINT, format TEXT, ename TEXT, includes TEXT[], org TEXT DEFAULT '-', depth INT DEFAULT NULL, slimit HSTORE DEFAULT NULL, soffset HSTORE DEFAULT NULL, include_xmlns BOOL DEFAULT TRUE ) RETURNS XML AS $F$
diff --git a/Open-ILS/src/sql/Pg/upgrade/QQQQ.MVF_CRA-upgrade.sql b/Open-ILS/src/sql/Pg/upgrade/QQQQ.MVF_CRA-upgrade.sql
new file mode 100644
index 0000000..05b07d1
--- /dev/null
+++ b/Open-ILS/src/sql/Pg/upgrade/QQQQ.MVF_CRA-upgrade.sql
@@ -0,0 +1,763 @@
+BEGIN;
+
+CREATE EXTENSION intarray;
+
+-- while we have this opportunity, and before we start collecting 
+-- CCVM IDs (below) carve out a nice space for stock ccvm values
+UPDATE config.coded_value_map SET id = id + 10000 WHERE id > 556;
+SELECT SETVAL('config.coded_value_map_id_seq'::TEXT, 
+    (SELECT GREATEST(max(id), 10000) FROM config.coded_value_map));
+
+ALTER TABLE config.record_attr_definition ADD COLUMN multi BOOL NOT NULL DEFAULT TRUE, ADD COLUMN composite BOOL NOT NULL DEFAULT FALSE;
+
+UPDATE  config.record_attr_definition
+  SET   multi = FALSE
+  WHERE name IN ('bib_level','control_type','pubdate','cat_form','enc_level','item_type','titlesort','authorsort');
+
+CREATE OR REPLACE FUNCTION vandelay.marc21_physical_characteristics( marc TEXT) RETURNS SETOF biblio.marc21_physical_characteristics AS $func$
+DECLARE
+    rowid   INT := 0;
+    _007    TEXT;
+    ptype   config.marc21_physical_characteristic_type_map%ROWTYPE;
+    psf     config.marc21_physical_characteristic_subfield_map%ROWTYPE;
+    pval    config.marc21_physical_characteristic_value_map%ROWTYPE;
+    retval  biblio.marc21_physical_characteristics%ROWTYPE;
+BEGIN
+
+    FOR _007 IN SELECT oils_xpath_string('//*', value) FROM UNNEST(oils_xpath('//*[@tag="007"]', marc)) x(value) LOOP
+        IF _007 IS NOT NULL AND _007 <> '' THEN
+            SELECT * INTO ptype FROM config.marc21_physical_characteristic_type_map WHERE ptype_key = SUBSTRING( _007, 1, 1 );
+
+            IF ptype.ptype_key IS NOT NULL THEN
+                FOR psf IN SELECT * FROM config.marc21_physical_characteristic_subfield_map WHERE ptype_key = ptype.ptype_key LOOP
+                    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 );
+
+                    IF pval.id IS NOT NULL THEN
+                        rowid := rowid + 1;
+                        retval.id := rowid;
+                        retval.ptype := ptype.ptype_key;
+                        retval.subfield := psf.id;
+                        retval.value := pval.id;
+                        RETURN NEXT retval;
+                    END IF;
+
+                END LOOP;
+            END IF;
+        END IF;
+    END LOOP;
+
+    RETURN;
+END;
+$func$ LANGUAGE PLPGSQL;
+
+CREATE OR REPLACE FUNCTION vandelay.marc21_extract_fixed_field_list( marc TEXT, ff TEXT ) RETURNS TEXT[] AS $func$
+DECLARE
+    rtype       TEXT;
+    ff_pos      RECORD;
+    tag_data    RECORD;
+    val         TEXT;
+    collection  TEXT[] := '{}'::TEXT[];
+BEGIN
+    rtype := (vandelay.marc21_record_type( marc )).code;
+    FOR ff_pos IN SELECT * FROM config.marc21_ff_pos_map WHERE fixed_field = ff AND rec_type = rtype ORDER BY tag DESC LOOP
+        IF ff_pos.tag = 'ldr' THEN
+            val := oils_xpath_string('//*[local-name()="leader"]', marc);
+            IF val IS NOT NULL THEN
+                val := SUBSTRING( val, ff_pos.start_pos + 1, ff_pos.length );
+                collection := collection || val;
+            END IF;
+        ELSE
+            FOR tag_data IN SELECT value FROM UNNEST( oils_xpath( '//*[@tag="' || UPPER(ff_pos.tag) || '"]/text()', marc ) ) x(value) LOOP
+                val := SUBSTRING( tag_data.value, ff_pos.start_pos + 1, ff_pos.length );
+                collection := collection || val;
+            END LOOP;
+        END IF;
+        val := REPEAT( ff_pos.default_val, ff_pos.length );
+        collection := collection || val;
+    END LOOP;
+
+    RETURN collection;
+END;
+$func$ LANGUAGE PLPGSQL;
+
+CREATE OR REPLACE FUNCTION biblio.marc21_extract_fixed_field_list( rid BIGINT, ff TEXT ) RETURNS TEXT[] AS $func$
+    SELECT * FROM vandelay.marc21_extract_fixed_field_list( (SELECT marc FROM biblio.record_entry WHERE id = $1), $2 );
+$func$ LANGUAGE SQL;
+
+-- DECREMENTING serial starts at -1
+CREATE SEQUENCE metabib.uncontrolled_record_attr_value_id_seq INCREMENT BY -1;
+
+CREATE TABLE metabib.uncontrolled_record_attr_value (
+    id      BIGINT  PRIMARY KEY DEFAULT nextval('metabib.uncontrolled_record_attr_value_id_seq'),
+    attr    TEXT    NOT NULL REFERENCES config.record_attr_definition (name),
+    value   TEXT    NOT NULL
+);
+CREATE UNIQUE INDEX muv_once_idx ON metabib.uncontrolled_record_attr_value (attr,value);
+
+CREATE TABLE metabib.record_attr_vector_list (
+    source  BIGINT  PRIMARY KEY REFERENCES  biblio.record_entry (id),
+    vlist   INT[]   NOT NULL -- stores id from ccvm AND murav
+);
+CREATE INDEX mrca_vlist_idx ON metabib.record_attr_vector_list USING gin ( vlist gin__int_ops );
+
+CREATE TABLE metabib.record_sorter (
+    id      BIGSERIAL   PRIMARY KEY,
+    source  BIGINT      NOT NULL REFERENCES biblio.record_entry (id) ON DELETE CASCADE,
+    attr    TEXT        NOT NULL REFERENCES config.record_attr_definition (name) ON DELETE CASCADE,
+    value   TEXT        NOT NULL
+);
+CREATE INDEX metabib_sorter_source_idx ON metabib.record_sorter (source); -- we may not need one of this or the next ... stats will tell
+CREATE INDEX metabib_sorter_s_a_idx ON metabib.record_sorter (source, attr);
+CREATE INDEX metabib_sorter_a_v_idx ON metabib.record_sorter (attr, value);
+
+CREATE TEMP TABLE attr_set ON COMMIT DROP AS SELECT  DISTINCT id AS source, (each(attrs)).key,(each(attrs)).value FROM metabib.record_attr;
+DELETE FROM attr_set WHERE BTRIM(value) = '';
+
+-- Grab sort values for the new sorting mechanism
+INSERT INTO metabib.record_sorter (source,attr,value)
+    SELECT  a.source, a.key, a.value
+      FROM  attr_set a
+            JOIN config.record_attr_definition d ON (d.name = a.key AND d.sorter AND a.value IS NOT NULL);
+
+-- Rewrite uncontrolled SVF record attrs as the seeds of an intarray vector
+INSERT INTO metabib.uncontrolled_record_attr_value (attr,value)
+    SELECT  DISTINCT a.key, a.value
+      FROM  attr_set a
+            JOIN config.record_attr_definition d ON (d.name = a.key AND d.filter AND a.value IS NOT NULL)
+            LEFT JOIN config.coded_value_map m ON (m.ctype = a.key)
+      WHERE m.id IS NULL;
+
+-- Now construct the record-specific vector from the SVF data
+INSERT INTO metabib.record_attr_vector_list (source,vlist)
+    SELECT  a.id, ARRAY_AGG(COALESCE(u.id, c.id))
+      FROM  metabib.record_attr a
+            JOIN attr_set ON (a.id = attr_set.source)
+            LEFT JOIN metabib.uncontrolled_record_attr_value u ON (u.attr = attr_set.key AND u.value = attr_set.value)
+            LEFT JOIN config.coded_value_map c ON (c.ctype = attr_set.key AND c.code = attr_set.value)
+      WHERE COALESCE(u.id,c.id) IS NOT NULL
+      GROUP BY 1;
+
+DROP VIEW metabib.rec_descriptor;
+DROP TABLE metabib.record_attr;
+
+CREATE TYPE metabib.record_attr_type AS (
+    id      BIGINT,
+    attrs   HSTORE
+);
+
+CREATE TABLE config.composite_attr_entry_definition(
+    coded_value INT  PRIMARY KEY NOT NULL REFERENCES config.coded_value_map (id) ON UPDATE CASCADE ON DELETE CASCADE,
+    definition  TEXT NOT NULL -- JSON
+);
+
+CREATE OR REPLACE VIEW metabib.record_attr_id_map AS
+    SELECT id, attr, value FROM metabib.uncontrolled_record_attr_value
+        UNION
+    SELECT  c.id, c.ctype AS attr, c.code AS value
+      FROM  config.coded_value_map c
+            JOIN config.record_attr_definition d ON (d.name = c.ctype AND NOT d.composite);
+
+CREATE VIEW metabib.composite_attr_id_map AS
+    SELECT  c.id, c.ctype AS attr, c.code AS value
+      FROM  config.coded_value_map c
+            JOIN config.record_attr_definition d ON (d.name = c.ctype AND d.composite);
+
+CREATE OR REPLACE VIEW metabib.full_attr_id_map AS
+    SELECT id, attr, value FROM metabib.record_attr_id_map
+        UNION
+    SELECT id, attr, value FROM metabib.composite_attr_id_map;
+
+
+-- Back-compat view ... we're moving to an INTARRAY world
+CREATE VIEW metabib.record_attr_flat AS
+    SELECT  v.source AS id,
+            m.attr,
+            m.value
+      FROM  metabib.full_attr_id_map m
+            JOIN  metabib.record_attr_vector_list v ON ( m.id = ANY( v.vlist ) );
+
+CREATE VIEW metabib.record_attr AS
+    SELECT id, HSTORE( ARRAY_AGG( attr ), ARRAY_AGG( value ) ) AS attrs FROM metabib.record_attr_flat GROUP BY 1;
+
+CREATE VIEW metabib.rec_descriptor AS
+    SELECT  id,
+            id AS record,
+            (populate_record(NULL::metabib.rec_desc_type, attrs)).*
+      FROM  metabib.record_attr;
+
+CREATE OR REPLACE FUNCTION metabib.compile_composite_attr ( cattr_def TEXT ) RETURNS query_int AS $func$
+
+    use JSON::XS;
+    my $def = decode_json(shift);
+
+    die("Composite attribute definition not supplied") unless $def;
+
+    sub recurse {
+        my $d = shift;
+        my $j = '&';
+        my @list;
+
+        if (ref $d eq 'HASH') { # node or AND
+            if (exists $d->{_attr}) { # it is a node
+                my $plan = spi_prepare('SELECT * FROM metabib.full_attr_id_map WHERE attr = $1 AND value = $2', qw/TEXT TEXT/);
+                return spi_exec_prepared(
+                    $plan, {limit => 1}, $d->{_attr}, $d->{_val}
+                )->{rows}[0]{id};
+                spi_freeplan($plan);
+            } elsif (exists $d->{_not} && scalar(keys(%$d)) == 1) { # it is a NOT
+                return '!' . recurse($$d{_not});
+            } else { # an AND list
+                @list = map { recurse($$d{$_}) } sort keys %$d;
+            }
+        } elsif (ref $d eq 'ARRAY') {
+            $j = '|';
+            @list = map { recurse($_) } @$d;
+        }
+
+        @list = grep { defined && $_ ne '' } @list;
+
+        return '(' . join($j, at list) . ')' if @list;
+        return '';
+    }
+
+    return recurse($def) || undef;
+
+$func$ IMMUTABLE LANGUAGE plperlu;
+
+CREATE OR REPLACE FUNCTION metabib.compile_composite_attr ( cattr_id INT ) RETURNS query_int AS $func$
+    SELECT metabib.compile_composite_attr(definition) FROM config.composite_attr_entry_definition WHERE coded_value = $1;
+$func$ STRICT IMMUTABLE LANGUAGE SQL;
+
+CREATE OR REPLACE FUNCTION metabib.reingest_record_attributes (rid BIGINT, pattr_list TEXT[] DEFAULT NULL, prmarc TEXT DEFAULT NULL, rdeleted BOOL DEFAULT TRUE) RETURNS VOID AS $func$
+DECLARE
+    transformed_xml TEXT;
+    rmarc           TEXT := prmarc;
+    tmp_val         TEXT;
+    prev_xfrm       TEXT;
+    normalizer      RECORD;
+    xfrm            config.xml_transform%ROWTYPE;
+    attr_vector     INT[] := '{}'::INT[];
+    attr_vector_tmp INT[];
+    attr_list       TEXT[] := pattr_list;
+    attr_value      TEXT[];
+    norm_attr_value TEXT[];
+    tmp_xml         XML;
+    attr_def        config.record_attr_definition%ROWTYPE;
+    ccvm_row        config.coded_value_map%ROWTYPE;
+BEGIN
+
+    IF attr_list IS NULL OR rdeleted THEN -- need to do the full dance on INSERT or undelete
+        SELECT ARRAY_AGG(name) INTO attr_list FROM config.record_attr_definition;
+    END IF;
+
+    IF rmarc IS NULL THEN
+        SELECT marc INTO rmarc FROM biblio.record_entry WHERE id = rid;
+    END IF;
+
+    FOR attr_def IN SELECT * FROM config.record_attr_definition WHERE NOT composite AND name = ANY( attr_list ) ORDER BY format LOOP
+
+        attr_value := '{}'::TEXT[];
+        norm_attr_value := '{}'::TEXT[];
+        attr_vector_tmp := '{}'::INT[];
+
+        SELECT * INTO ccvm_row FROM config.coded_value_map c WHERE c.ctype = attr_def.name LIMIT 1; 
+
+        -- tag+sf attrs only support SVF
+        IF attr_def.tag IS NOT NULL THEN -- tag (and optional subfield list) selection
+            SELECT  ARRAY[ARRAY_TO_STRING(ARRAY_AGG(value), COALESCE(attr_def.joiner,' '))] INTO attr_value
+              FROM  (SELECT * FROM metabib.full_rec ORDER BY tag, subfield) AS x
+              WHERE record = rid
+                    AND tag LIKE attr_def.tag
+                    AND CASE
+                        WHEN attr_def.sf_list IS NOT NULL 
+                            THEN POSITION(subfield IN attr_def.sf_list) > 0
+                        ELSE TRUE
+                    END
+              GROUP BY tag
+              ORDER BY tag
+              LIMIT 1;
+
+        ELSIF attr_def.fixed_field IS NOT NULL THEN -- a named fixed field, see config.marc21_ff_pos_map.fixed_field
+            attr_value := vandelay.marc21_extract_fixed_field_list(rmarc, attr_def.fixed_field);
+
+            IF NOT attr_def.multi THEN
+                attr_value := ARRAY[attr_value[1]];
+            END IF;
+
+        ELSIF attr_def.xpath IS NOT NULL THEN -- and xpath expression
+
+            SELECT INTO xfrm * FROM config.xml_transform WHERE name = attr_def.format;
+        
+            -- See if we can skip the XSLT ... it's expensive
+            IF prev_xfrm IS NULL OR prev_xfrm <> xfrm.name THEN
+                -- Can't skip the transform
+                IF xfrm.xslt <> '---' THEN
+                    transformed_xml := oils_xslt_process(rmarc,xfrm.xslt);
+                ELSE
+                    transformed_xml := rmarc;
+                END IF;
+    
+                prev_xfrm := xfrm.name;
+            END IF;
+
+            IF xfrm.name IS NULL THEN
+                -- just grab the marcxml (empty) transform
+                SELECT INTO xfrm * FROM config.xml_transform WHERE xslt = '---' LIMIT 1;
+                prev_xfrm := xfrm.name;
+            END IF;
+
+            FOR tmp_xml IN SELECT XPATH(attr_def.xpath, transformed_xml, ARRAY[ARRAY[xfrm.prefix, xfrm.namespace_uri]]) LOOP
+                tmp_val := oils_xpath_string(
+                                '//*',
+                                tmp_xml::TEXT,
+                                COALESCE(attr_def.joiner,' '),
+                                ARRAY[ARRAY[xfrm.prefix, xfrm.namespace_uri]]
+                            );
+                IF tmp_val IS NOT NULL AND BTRIM(tmp_val) <> '' THEN
+                    attr_value := attr_value || tmp_val;
+                    EXIT WHEN NOT attr_def.multi;
+                END IF;
+            END LOOP;
+
+        ELSIF attr_def.phys_char_sf IS NOT NULL THEN -- a named Physical Characteristic, see config.marc21_physical_characteristic_*_map
+            SELECT  ARRAY_AGG(m.value) INTO attr_value
+              FROM  vandelay.marc21_physical_characteristics(rmarc) v
+                    LEFT JOIN config.marc21_physical_characteristic_value_map m ON (m.id = v.value)
+              WHERE v.subfield = attr_def.phys_char_sf AND (m.value IS NOT NULL AND BTRIM(m.value) <> '')
+                    AND ( ccvm_row.id IS NULL OR ( ccvm_row.id IS NOT NULL AND v.id IS NOT NULL) );
+
+            IF NOT attr_def.multi THEN
+                attr_value := ARRAY[attr_value[1]];
+            END IF;
+
+        END IF;
+
+                -- apply index normalizers to attr_value
+        FOR tmp_val IN SELECT value FROM UNNEST(attr_value) x(value) LOOP
+            FOR normalizer IN
+                SELECT  n.func AS func,
+                        n.param_count AS param_count,
+                        m.params AS params
+                  FROM  config.index_normalizer n
+                        JOIN config.record_attr_index_norm_map m ON (m.norm = n.id)
+                  WHERE attr = attr_def.name
+                  ORDER BY m.pos LOOP
+                    EXECUTE 'SELECT ' || normalizer.func || '(' ||
+                    COALESCE( quote_literal( tmp_val ), 'NULL' ) ||
+                        CASE
+                            WHEN normalizer.param_count > 0
+                                THEN ',' || REPLACE(REPLACE(BTRIM(normalizer.params,'[]'),E'\'',E'\\\''),E'"',E'\'')
+                                ELSE ''
+                            END ||
+                    ')' INTO tmp_val;
+
+            END LOOP;
+            IF tmp_val IS NOT NULL AND BTRIM(tmp_val) <> '' THEN
+                norm_attr_value := norm_attr_value || tmp_val;
+            END IF;
+        END LOOP;
+        
+        IF attr_def.filter THEN
+            -- Create unknown uncontrolled values and find the IDs of the values
+            IF ccvm_row.id IS NULL THEN
+                FOR tmp_val IN SELECT value FROM UNNEST(norm_attr_value) x(value) LOOP
+                    IF tmp_val IS NOT NULL AND BTRIM(tmp_val) <> '' THEN
+                        BEGIN -- use subtransaction to isolate unique constraint violations
+                            INSERT INTO metabib.uncontrolled_record_attr_value ( attr, value ) VALUES ( attr_def.name, tmp_val );
+                        EXCEPTION WHEN unique_violation THEN END;
+                    END IF;
+                END LOOP;
+
+                SELECT ARRAY_AGG(id) INTO attr_vector_tmp FROM metabib.uncontrolled_record_attr_value WHERE attr = attr_def.name AND value = ANY( norm_attr_value );
+            ELSE
+                SELECT ARRAY_AGG(id) INTO attr_vector_tmp FROM config.coded_value_map WHERE ctype = attr_def.name AND code = ANY( norm_attr_value );
+            END IF;
+
+            -- Add the new value to the vector
+            attr_vector := attr_vector || attr_vector_tmp;
+        END IF;
+
+        IF attr_def.sorter AND norm_attr_value[1] IS NOT NULL THEN
+            DELETE FROM metabib.record_sorter WHERE source = rid AND attr = attr_def.name;
+            INSERT INTO metabib.record_sorter (source, attr, value) VALUES (rid, attr_def.name, norm_attr_value[1]);
+        END IF;
+
+    END LOOP;
+
+/* We may need to rewrite the vlist to contain
+   the intersection of new values for requested
+   attrs and old values for ignored attrs. To
+   do this, we take the old attr vlist and
+   subtract any values that are valid for the
+   requested attrs, and then add back the new
+   set of attr values. */
+
+        IF ARRAY_LENGTH(pattr_list, 1) > 0 THEN 
+            SELECT vlist INTO attr_vector_tmp FROM metabib.record_attr_vector_list WHERE source = rid;
+            SELECT attr_vector_tmp - ARRAY_AGG(id) INTO attr_vector_tmp FROM metabib.full_attr_id_map WHERE attr = ANY (pattr_list);
+            attr_vector := attr_vector || attr_vector_tmp;
+        END IF;
+
+    -- On to composite attributes, now that the record attrs have been pulled.  Processed in name order, so later composite
+    -- attributes can depend on earlier ones.
+    FOR attr_def IN SELECT * FROM config.record_attr_definition WHERE composite AND name = ANY( attr_list ) ORDER BY name LOOP
+
+        FOR ccvm_row IN SELECT * FROM config.coded_value_map c WHERE c.ctype = attr_def.name ORDER BY value LOOP
+
+            tmp_val := metabib.compile_composite_attr( ccvm_row.id );
+            CONTINUE WHEN tmp_val IS NULL OR tmp_val = ''; -- nothing to do
+
+            IF attr_def.filter THEN
+                IF attr_vector @@ tmp_val::query_int THEN
+                    attr_vector = attr_vector + intset(ccvm_row.id);
+                    EXIT WHEN NOT attr_def.multi;
+                END IF;
+            END IF;
+
+            IF attr_def.sorter THEN
+                IF attr_vector ~~ tmp_val THEN
+                    DELETE FROM metabib.record_sorter WHERE source = rid AND attr = attr_def.name;
+                    INSERT INTO metabib.record_sorter (source, attr, value) VALUES (rid, attr_def.name, ccvm_row.code);
+                END IF;
+            END IF;
+
+        END LOOP;
+
+    END LOOP;
+
+    IF ARRAY_LENGTH(attr_vector, 1) > 0 THEN
+        IF rdeleted THEN -- initial insert OR revivication
+            DELETE FROM metabib.record_attr_vector_list WHERE source = rid;
+            INSERT INTO metabib.record_attr_vector_list (source, vlist) VALUES (rid, attr_vector);
+        ELSE
+            UPDATE metabib.record_attr_vector_list SET vlist = attr_vector WHERE source = rid;
+        END IF;
+    END IF;
+
+END;
+
+$func$ LANGUAGE PLPGSQL;
+
+-- AFTER UPDATE OR INSERT trigger for biblio.record_entry
+CREATE OR REPLACE FUNCTION biblio.indexing_ingest_or_delete () RETURNS TRIGGER AS $func$
+BEGIN
+
+    IF NEW.deleted THEN -- If this bib is deleted
+        PERFORM * FROM config.internal_flag WHERE
+            name = 'ingest.metarecord_mapping.preserve_on_delete' AND enabled;
+        IF NOT FOUND THEN
+            -- One needs to keep these around to support searches
+            -- with the #deleted modifier, so one should turn on the named
+            -- internal flag for that functionality.
+            DELETE FROM metabib.metarecord_source_map WHERE source = NEW.id;
+            DELETE FROM metabib.record_attr_vector_list WHERE source = NEW.id;
+        END IF;
+
+        DELETE FROM authority.bib_linking WHERE bib = NEW.id; -- Avoid updating fields in bibs that are no longer visible
+        DELETE FROM biblio.peer_bib_copy_map WHERE peer_record = NEW.id; -- Separate any multi-homed items
+        DELETE FROM metabib.browse_entry_def_map WHERE source = NEW.id; -- Don't auto-suggest deleted bibs
+        RETURN NEW; -- and we're done
+            END IF;
+
+    IF TG_OP = 'UPDATE' THEN -- re-ingest?
+        PERFORM * FROM config.internal_flag WHERE name = 'ingest.reingest.force_on_same_marc' AND enabled;
+
+        IF NOT FOUND AND OLD.marc = NEW.marc THEN -- don't do anything if the MARC didn't change
+            RETURN NEW;
+        END IF;
+    END IF;
+
+    -- Record authority linking
+    PERFORM * FROM config.internal_flag WHERE name = 'ingest.disable_authority_linking' AND enabled;
+    IF NOT FOUND THEN
+        PERFORM biblio.map_authority_linking( NEW.id, NEW.marc );
+    END IF;
+
+    -- Flatten and insert the mfr data
+    PERFORM * FROM config.internal_flag WHERE name = 'ingest.disable_metabib_full_rec' AND enabled;
+    IF NOT FOUND THEN
+        PERFORM metabib.reingest_metabib_full_rec(NEW.id);
+
+        -- Now we pull out attribute data, which is dependent on the mfr for all but XPath-based fields
+        PERFORM * FROM config.internal_flag WHERE name = 'ingest.disable_metabib_rec_descriptor' AND enabled;
+        IF NOT FOUND THEN
+            PERFORM metabib.reingest_record_attributes(NEW.id, NULL, NEW.marc, TG_OP = 'INSERT' OR OLD.deleted);
+        END IF;
+    END IF;
+
+    -- Gather and insert the field entry data
+    PERFORM metabib.reingest_metabib_field_entries(NEW.id);
+
+    -- Located URI magic
+    IF TG_OP = 'INSERT' THEN
+        PERFORM * FROM config.internal_flag WHERE name = 'ingest.disable_located_uri' AND enabled;
+        IF NOT FOUND THEN
+            PERFORM biblio.extract_located_uris( NEW.id, NEW.marc, NEW.editor );
+        END IF;
+    ELSE
+        PERFORM * FROM config.internal_flag WHERE name = 'ingest.disable_located_uri' AND enabled;
+        IF NOT FOUND THEN
+            PERFORM biblio.extract_located_uris( NEW.id, NEW.marc, NEW.editor );
+        END IF;
+    END IF;
+
+    -- (re)map metarecord-bib linking
+    IF TG_OP = 'INSERT' THEN -- if not deleted and performing an insert, check for the flag
+        PERFORM * FROM config.internal_flag WHERE name = 'ingest.metarecord_mapping.skip_on_insert' AND enabled;
+        IF NOT FOUND THEN
+            PERFORM metabib.remap_metarecord_for_bib( NEW.id, NEW.fingerprint );
+        END IF;
+    ELSE -- we're doing an update, and we're not deleted, remap
+        PERFORM * FROM config.internal_flag WHERE name = 'ingest.metarecord_mapping.skip_on_update' AND enabled;
+        IF NOT FOUND THEN
+            PERFORM metabib.remap_metarecord_for_bib( NEW.id, NEW.fingerprint );
+        END IF;
+    END IF;
+
+    RETURN NEW;
+END;
+$func$ LANGUAGE PLPGSQL;
+
+-- add new sr_format attribute definition
+
+INSERT INTO config.record_attr_definition (name, label, phys_char_sf)
+VALUES (
+    'sr_format', 
+    oils_i18n_gettext('sr_format', 'Sound recording format', 'crad', 'label'),
+    '62'
+);
+
+INSERT INTO config.coded_value_map (id, ctype, code, value) VALUES
+(557, 'sr_format', 'a', oils_i18n_gettext(557, '16 rpm', 'ccvm', 'value')),
+(558, 'sr_format', 'b', oils_i18n_gettext(558, '33 1/3 rpm', 'ccvm', 'value')),
+(559, 'sr_format', 'c', oils_i18n_gettext(559, '45 rpm', 'ccvm', 'value')),
+(560, 'sr_format', 'f', oils_i18n_gettext(560, '1.4 m. per second', 'ccvm', 'value')),
+(561, 'sr_format', 'd', oils_i18n_gettext(561, '78 rpm', 'ccvm', 'value')),
+(562, 'sr_format', 'e', oils_i18n_gettext(562, '8 rpm', 'ccvm', 'value')),
+(563, 'sr_format', 'l', oils_i18n_gettext(563, '1 7/8 ips', 'ccvm', 'value')),
+(586, 'item_form', 'o', oils_i18n_gettext('586', 'Online', 'ccvm', 'value')),
+(587, 'item_form', 'q', oils_i18n_gettext('587', 'Direct electronic', 'ccvm', 'value'));
+
+INSERT INTO config.coded_value_map
+    (id, ctype, code, value, search_label) VALUES 
+(564, 'icon_format', 'book', 
+    oils_i18n_gettext(564, 'Book', 'ccvm', 'value'),
+    oils_i18n_gettext(564, 'Book', 'ccvm', 'search_label')),
+(565, 'icon_format', 'braille', 
+    oils_i18n_gettext(565, 'Braille', 'ccvm', 'value'),
+    oils_i18n_gettext(565, 'Braille', 'ccvm', 'search_label')),
+(566, 'icon_format', 'software', 
+    oils_i18n_gettext(566, 'Software and video games', 'ccvm', 'value'),
+    oils_i18n_gettext(566, 'Software and video games', 'ccvm', 'search_label')),
+(567, 'icon_format', 'dvd', 
+    oils_i18n_gettext(567, 'DVD', 'ccvm', 'value'),
+    oils_i18n_gettext(567, 'DVD', 'ccvm', 'search_label')),
+(568, 'icon_format', 'ebook', 
+    oils_i18n_gettext(568, 'E-book', 'ccvm', 'value'),
+    oils_i18n_gettext(568, 'E-book', 'ccvm', 'search_label')),
+(569, 'icon_format', 'eaudio', 
+    oils_i18n_gettext(569, 'E-audio', 'ccvm', 'value'),
+    oils_i18n_gettext(569, 'E-audio', 'ccvm', 'search_label')),
+(570, 'icon_format', 'kit', 
+    oils_i18n_gettext(570, 'Kit', 'ccvm', 'value'),
+    oils_i18n_gettext(570, 'Kit', 'ccvm', 'search_label')),
+(571, 'icon_format', 'map', 
+    oils_i18n_gettext(571, 'Map', 'ccvm', 'value'),
+    oils_i18n_gettext(571, 'Map', 'ccvm', 'search_label')),
+(572, 'icon_format', 'microform', 
+    oils_i18n_gettext(572, 'Microform', 'ccvm', 'value'),
+    oils_i18n_gettext(572, 'Microform', 'ccvm', 'search_label')),
+(573, 'icon_format', 'score', 
+    oils_i18n_gettext(573, 'Music Score', 'ccvm', 'value'),
+    oils_i18n_gettext(573, 'Music Score', 'ccvm', 'search_label')),
+(574, 'icon_format', 'picture', 
+    oils_i18n_gettext(574, 'Picture', 'ccvm', 'value'),
+    oils_i18n_gettext(574, 'Picture', 'ccvm', 'search_label')),
+(575, 'icon_format', 'equip', 
+    oils_i18n_gettext(575, 'Equipment, games, toys', 'ccvm', 'value'),
+    oils_i18n_gettext(575, 'Equipment, games, toys', 'ccvm', 'search_label')),
+(576, 'icon_format', 'serial', 
+    oils_i18n_gettext(576, 'Serials and magazines', 'ccvm', 'value'),
+    oils_i18n_gettext(576, 'Serials and magazines', 'ccvm', 'search_label')),
+(577, 'icon_format', 'vhs', 
+    oils_i18n_gettext(577, 'VHS', 'ccvm', 'value'),
+    oils_i18n_gettext(577, 'VHS', 'ccvm', 'search_label')),
+(578, 'icon_format', 'evideo', 
+    oils_i18n_gettext(578, 'E-video', 'ccvm', 'value'),
+    oils_i18n_gettext(578, 'E-video', 'ccvm', 'search_label')),
+(579, 'icon_format', 'cdaudiobook', 
+    oils_i18n_gettext(579, 'CD Audiobook', 'ccvm', 'value'),
+    oils_i18n_gettext(579, 'CD Audiobook', 'ccvm', 'search_label')),
+(580, 'icon_format', 'cdmusic', 
+    oils_i18n_gettext(580, 'CD Music recording', 'ccvm', 'value'),
+    oils_i18n_gettext(580, 'CD Music recording', 'ccvm', 'search_label')),
+(581, 'icon_format', 'casaudiobook', 
+    oils_i18n_gettext(581, 'Cassette audiobook', 'ccvm', 'value'),
+    oils_i18n_gettext(581, 'Cassette audiobook', 'ccvm', 'search_label')),
+(582, 'icon_format', 'casmusic',
+    oils_i18n_gettext(582, 'Audiocassette music recording', 'ccvm', 'value'),
+    oils_i18n_gettext(582, 'Audiocassette music recording', 'ccvm', 'search_label')),
+(583, 'icon_format', 'phonospoken', 
+    oils_i18n_gettext(583, 'Phonograph spoken recording', 'ccvm', 'value'),
+    oils_i18n_gettext(583, 'Phonograph spoken recording', 'ccvm', 'search_label')),
+(584, 'icon_format', 'phonomusic', 
+    oils_i18n_gettext(584, 'Phonograph music recording', 'ccvm', 'value'),
+    oils_i18n_gettext(584, 'Phonograph music recording', 'ccvm', 'search_label')),
+(585, 'icon_format', 'lpbook', 
+    oils_i18n_gettext(585, 'Large Print Book', 'ccvm', 'value'),
+    oils_i18n_gettext(585, 'Large Print Book', 'ccvm', 'search_label'))
+;
+
+-- add the new icon format attribute definition
+
+INSERT INTO config.global_flag (name, label, value, enabled) VALUES (
+    'opac.icon_attr',
+    oils_i18n_gettext(
+        'opac.icon_attr', 
+        'OPAC Format Icons Attribute',
+        'cgf',
+        'label'
+    ),
+    'icon_format', 
+    TRUE
+);
+
+INSERT INTO config.record_attr_definition 
+    (name, label, multi, filter, composite) VALUES (
+    'icon_format',
+    oils_i18n_gettext(
+        'icon_format',
+        'OPAC Format Icons',
+        'crad',
+        'label'
+    ),
+    TRUE, TRUE, TRUE
+);
+
+-- icon format composite definitions
+
+INSERT INTO config.composite_attr_entry_definition 
+    (coded_value, definition) VALUES
+--book
+(564, '{"0":[{"_attr":"item_type","_val":"a"},{"_attr":"item_type","_val":"t"}],"1":{"_not":[{"_attr":"item_form","_val":"a"},{"_attr":"item_form","_val":"b"},{"_attr":"item_form","_val":"c"},{"_attr":"item_form","_val":"d"},{"_attr":"item_form","_val":"f"},{"_attr":"item_form","_val":"o"},{"_attr":"item_form","_val":"q"},{"_attr":"item_form","_val":"r"},{"_attr":"item_form","_val":"s"}]},"2":[{"_attr":"bib_level","_val":"a"},{"_attr":"bib_level","_val":"c"},{"_attr":"bib_level","_val":"d"},{"_attr":"bib_level","_val":"m"}]}'),
+
+-- braille
+(565, '{"0":{"_attr":"item_type","_val":"a"},"1":{"_attr":"item_form","_val":"f"}}'),
+
+-- software
+(566, '{"_attr":"item_type","_val":"m"}'),
+
+-- dvd
+(567, '{"_attr":"vr_format","_val":"v"}'),
+
+-- ebook
+(568, '{"0":[{"_attr":"item_type","_val":"a"},{"_attr":"item_type","_val":"t"}],"1":[{"_attr":"item_form","_val":"o"},{"_attr":"item_form","_val":"s"},{"_attr":"item_form","_val":"q"}],"2":[{"_attr":"bib_level","_val":"a"},{"_attr":"bib_level","_val":"c"},{"_attr":"bib_level","_val":"d"},{"_attr":"bib_level","_val":"m"}]}'),
+
+-- eaudio
+(569, '{"0":{"_attr":"item_type","_val":"i"},"1":[{"_attr":"item_form","_val":"o"},{"_attr":"item_form","_val":"q"},{"_attr":"item_form","_val":"s"}]}'),
+
+-- kit
+(570, '[{"_attr":"item_type","_val":"o"},{"_attr":"item_type","_val":"p"}]'),
+
+-- map
+(571, '[{"_attr":"item_type","_val":"e"},{"_attr":"item_type","_val":"f"}]'),
+
+-- microform
+(572, '[{"_attr":"item_form","_val":"a"},{"_attr":"item_form","_val":"b"},{"_attr":"item_form","_val":"c"}]'),
+
+-- score
+(573, '[{"_attr":"item_type","_val":"c"},{"_attr":"item_type","_val":"d"}]'),
+
+-- picture
+(574, '{"_attr":"item_type","_val":"k"}'),
+
+-- equip
+(575, '{"_attr":"item_type","_val":"r"}'),
+
+-- serial
+(576, '[{"_attr":"bib_level","_val":"b"},{"_attr":"bib_level","_val":"s"}]'),
+
+-- vhs
+(577, '{"_attr":"vr_format","_val":"b"}'),
+
+-- evideo
+(578, '{"0":{"_attr":"item_type","_val":"g"},"1":[{"_attr":"item_form","_val":"o"},{"_attr":"item_form","_val":"s"},{"_attr":"item_form","_val":"q"}]}'),
+
+-- cdaudiobook
+(579, '{"0":{"_attr":"item_type","_val":"i"},"1":{"_attr":"sr_format","_val":"f"}}'),
+
+-- cdmusic
+(580, '{"0":{"_attr":"item_type","_val":"j"},"1":{"_attr":"sr_format","_val":"f"}}'),
+
+-- casaudiobook
+(581, '{"0":{"_attr":"item_type","_val":"i"},"1":{"_attr":"sr_format","_val":"l"}}'),
+
+-- casmusic
+(582, '{"0":{"_attr":"item_type","_val":"j"},"1":{"_attr":"sr_format","_val":"l"}}'),
+
+-- phonospoken
+(583, '{"0":{"_attr":"item_type","_val":"i"},"1":[{"_attr":"sr_format","_val":"a"},{"_attr":"sr_format","_val":"b"},{"_attr":"sr_format","_val":"c"},{"_attr":"sr_format","_val":"d"},{"_attr":"sr_format","_val":"e"}]}'),
+
+-- phonomusic
+(584, '{"0":{"_attr":"item_type","_val":"j"},"1":[{"_attr":"sr_format","_val":"a"},{"_attr":"sr_format","_val":"b"},{"_attr":"sr_format","_val":"c"},{"_attr":"sr_format","_val":"d"},{"_attr":"sr_format","_val":"e"}]}'),
+
+-- lpbook
+(585, '{"0":[{"_attr":"item_type","_val":"a"},{"_attr":"item_type","_val":"t"}],"1":{"_attr":"item_form","_val":"d"},"2":[{"_attr":"bib_level","_val":"a"},{"_attr":"bib_level","_val":"c"},{"_attr":"bib_level","_val":"d"},{"_attr":"bib_level","_val":"m"}]}');
+
+
+
+
+-- SEED DATA ---------------------------------------------------------------
+
+-- by default, use the same format record attribute as that used for icons
+-- TODO: verify attr name still matches
+INSERT INTO config.global_flag (name, label, value, enabled) VALUES (
+    'opac.metarecord.holds.format_attr', 
+    'OPAC Metarecord Hold Formats Attribute', 
+    'local_format', 
+    TRUE
+);
+
+CREATE OR REPLACE FUNCTION unapi.mra (
+    obj_id BIGINT,
+    format TEXT,
+    ename TEXT,
+    includes TEXT[],
+    org TEXT,
+    depth INT DEFAULT NULL,
+    slimit HSTORE DEFAULT NULL,
+    soffset HSTORE DEFAULT NULL,
+    include_xmlns BOOL DEFAULT TRUE
+) RETURNS XML AS $F$
+    SELECT  XMLELEMENT(
+        name attributes,
+        XMLATTRIBUTES(
+            CASE WHEN $9 THEN 'http://open-ils.org/spec/indexing/v1' ELSE NULL END AS xmlns,
+            'tag:open-ils.org:U2 at mra/' || $1 AS id, 
+            'tag:open-ils.org:U2 at bre/' || $1 AS record 
+        ),  
+        (SELECT XMLAGG(foo.y)
+          FROM (
+            SELECT  XMLELEMENT(
+                        name field,
+                        XMLATTRIBUTES(
+                            mra.attr AS name,
+                            cvm.value AS "coded-value",
+                            cvm.id AS "cvmid",
+                            rad.composite,
+                            rad.multi,
+                            rad.filter,
+                            rad.sorter
+                        ),
+                        mra.value
+                    )
+              FROM  metabib.record_attr_flat mra
+                    JOIN config.record_attr_definition rad ON (mra.attr = rad.name)
+                    LEFT JOIN config.coded_value_map cvm ON (cvm.ctype = mra.attr AND code = mra.value)
+              WHERE mra.id = $1
+            )foo(y)
+        )   
+    )   
+$F$ LANGUAGE SQL STABLE;
+
+COMMIT;

commit abe6bdf09ea832296baf41daa5dadc378b2fef08
Author: Mike Rylander <mrylander at gmail.com>
Date:   Wed Jan 15 16:00:52 2014 -0500

    LP#1269911: Teach QueryParser new tricks
    
    QP Needs to be made aware of several new structures in the database.
    
    First, we have added a new sort-supporting table called metabib.record_sorter
    which holds values extracted by crad.sorter=true attrs.  This is used instead
    of the mrd.attrs->"something" hstore composite.
    
    Next, we teach QP how to convert from a list of user-supplied values across
    many dynamic filters (based on crad) into an intarray query of ids extracted
    from config.coded_value_map (in the case of controlled attributes) or
    metabib.uncontrolled_record_attr_value (in the case of, you guessed it,
    uncontrolled attributes).  This query is applied against the vlist column
    of metabib.record_attr_vector_list, which is GIN indexed for speed.
    
    Finally, metabib.record_attr is now a view over metabib.record_attr_vector_list
    and is consequently going to be slow for general use.  We restrict
    its inclusion in the core query to only the case of a during() filter
    which requires access to the value of a bib's Date2 field.  For the
    other common case, requiring access to the Date1 field, we instead
    use the pubdate sort value now stored in metabib.record_sorter.  We
    might consider making the specific sorter attribute used configurable
    so that we can change the definition of pubdate down the road, but it
    starts out (and generally stays) defined as equivalent to Date1.
    
    Signed-off-by: Mike Rylander <mrylander at gmail.com>
    Signed-off-by: Dan Wells <dbw2 at calvin.edu>

diff --git a/Open-ILS/src/perlmods/lib/OpenILS/Application/Storage/Driver/Pg/QueryParser.pm b/Open-ILS/src/perlmods/lib/OpenILS/Application/Storage/Driver/Pg/QueryParser.pm
index 7d0aae0..147b165 100644
--- a/Open-ILS/src/perlmods/lib/OpenILS/Application/Storage/Driver/Pg/QueryParser.pm
+++ b/Open-ILS/src/perlmods/lib/OpenILS/Application/Storage/Driver/Pg/QueryParser.pm
@@ -695,6 +695,40 @@ use Data::Dumper;
 use OpenILS::Application::AppUtils;
 my $apputils = "OpenILS::Application::AppUtils";
 
+our %_dfilter_controlled_cache = ();
+
+sub dynamic_filter_compile {
+    my ($self, $filter, $params, $negate) = @_;
+    my $e = OpenILS::Utils::CStoreEditor->new;
+
+    $negate = $negate ? '!' : '';
+
+    if (!exists($_dfilter_controlled_cache{$filter})) {
+        my $crad = $e->retrieve_config_record_attr_definition($filter);
+        my $ccvm_list = $e->search_config_coded_value_map({ctype =>$filter});
+
+        $_dfilter_controlled_cache{$filter} = $crad->to_bare_hash;
+        $_dfilter_controlled_cache{$filter}{controlled} = scalar @$ccvm_list;
+    }
+
+    my $method = $_dfilter_controlled_cache{$filter}{controlled} ?
+        'search_config_coded_value_map' : 'search_metabib_uncontrolled_record_attr_value';
+    my $attr_field = $_dfilter_controlled_cache{$filter}{controlled} ?
+        'ctype' : 'attr';
+    my $value_field = $_dfilter_controlled_cache{$filter}{controlled} ?
+        'code' : 'value';
+
+    return sprintf('%s(%s)', $negate,
+        join(
+            '|', 
+            map {
+                $_->id
+            } @{
+                $e->$method({ $attr_field => $filter, $value_field => $params })
+            }
+        )
+    );
+}
 
 sub toSQL {
     my $self = shift;
@@ -736,14 +770,31 @@ sub toSQL {
         $sort_filter = 'rel';
     }
 
+    my $lang_join = '';
     if (($filters{preferred_language} || $self->QueryParser->default_preferred_language) && ($filters{preferred_language_multiplier} || $self->QueryParser->default_preferred_language_multiplier)) {
+    
         my $pl = $self->QueryParser->quote_value( $filters{preferred_language} ? $filters{preferred_language} : $self->QueryParser->default_preferred_language );
+        $$flat_plan{with} .= ',' if $$flat_plan{with};
+        $$flat_plan{with} .= "lang_with AS (SELECT id FROM config.coded_value_map WHERE ctype = 'item_lang' AND code = $pl)";
+        $lang_join = ",lang_with";
+
         my $plw = $filters{preferred_language_multiplier} ? $filters{preferred_language_multiplier} : $self->QueryParser->default_preferred_language_multiplier;
-        $rel = "($rel * COALESCE( NULLIF( FIRST(mrd.attrs \@> hstore('item_lang', $pl)), FALSE )::INT * $plw, 1))";
+        $rel = "($rel * COALESCE( NULLIF( FIRST(mrv.vlist \@> ARRAY[lang_with.id]), FALSE )::INT * $plw, 1))";
+        $$flat_plan{uses_mrv} = 1;
     }
     $rel = "1.0/($rel)::NUMERIC";
 
-    my $mra_join = 'INNER JOIN metabib.record_attr mrd ON m.source = mrd.id';
+    my $mrv_join = '';
+    if ($$flat_plan{uses_mrv}) {
+        $mrv_join = 'INNER JOIN metabib.record_attr_vector_list mrv ON m.source = mrv.source';
+    }
+
+    my $mra_join = '';
+    if ($$flat_plan{uses_mrd}) {
+        $mra_join = 'INNER JOIN metabib.record_attr mrd ON m.source = mrd.id';
+    }
+
+    my $pubdate_join = "LEFT JOIN metabib.record_sorter pubdate_t ON m.source = pubdate_t.source AND attr = 'pubdate'";
 
     my $bre_join = '';
     if ($self->find_modifier('deleted')) {
@@ -763,7 +814,7 @@ sub toSQL {
     $nullpos = 'NULLS FIRST' if ($self->find_modifier('nullsfirst'));
 
     if (grep {$_ eq $sort_filter} @{$self->QueryParser->dynamic_sorters}) {
-        $rank = "FIRST(mrd.attrs->'$sort_filter')"
+        $rank = "FIRST((SELECT value FROM metabib.record_sorter rbr WHERE rbr.source = m.source and attr = '$sort_filter'))"
     } elsif ($sort_filter eq 'create_date') {
         $rank = "FIRST((SELECT create_date FROM biblio.record_entry rbr WHERE rbr.id = m.source))";
     } elsif ($sort_filter eq 'edit_date') {
@@ -800,11 +851,14 @@ SELECT  $key AS id,
         $agg_records,
         $rel AS rel,
         $rank AS rank, 
-        FIRST(mrd.attrs->'date1') AS tie_break
+        FIRST(pubdate_t.value) AS tie_break
   FROM  metabib.metarecord_source_map m
+        $$flat_plan{from}
+        $pubdate_join
         $mra_join
+        $mrv_join
         $bre_join
-        $$flat_plan{from}
+        $lang_join
   WHERE 1=1
         $flat_where
   GROUP BY 1
@@ -826,6 +880,8 @@ sub flatten {
     my $where = shift || '';
     my $with = '';
     my $uses_bre = 0;
+    my $uses_mrd = 0;
+    my $uses_mrv = 0;
 
     my @rank_list;
     for my $node ( @{$self->query_nodes} ) {
@@ -986,6 +1042,8 @@ sub flatten {
                 }
 
                 $uses_bre = $$subnode{uses_bre};
+                $uses_mrd = $$subnode{uses_mrd};
+                $uses_mrv = $$subnode{uses_mrv};
             }
         } else {
 
@@ -999,40 +1057,37 @@ sub flatten {
     }
 
     my $joiner = "\n" . ${spc} x ( $self->plan_level + 5 ) . ($self->joiner eq '&' ? 'AND ' : 'OR ');
+
+    my @dlist = ();
     # for each dynamic filter, build more of the WHERE clause
     for my $filter (@{$self->filters}) {
         my $NOT = $filter->negate ? 'NOT ' : '';
         if (grep { $_ eq $filter->name } @{ $self->QueryParser->dynamic_filters }) {
 
-            warn "flatten(): processing dynamic filter ". $filter->name ."\n"
-                if $self->QueryParser->debug;
-
-            # bool joiner for intra-plan nodes/filters
-            $where .= $joiner if $where ne '';
-
-            my @fargs = @{$filter->args};
             my $fname = $filter->name;
             $fname = 'item_lang' if $fname eq 'language'; #XXX filter aliases 
 
-            $where .= sprintf(
-                "${NOT}COALESCE((mrd.attrs->'%s') IN (%s), false)", $fname, 
-                join(',', map { $self->QueryParser->quote_value($_) } @fargs)
-            );
-
-            warn "flatten(): filter where => $where\n"
+            warn "flatten(): processing dynamic filter ". $filter->name ."\n"
                 if $self->QueryParser->debug;
+
+            my $vlist_query = $self->dynamic_filter_compile( $fname, $filter->args, $filter->negate );
+
+            # bool joiner for intra-plan nodes/filters
+            push(@dlist, $self->joiner) if @dlist;
+            push(@dlist, $vlist_query);
+            $uses_mrv = 1;
         } else {
             if ($filter->name eq 'before') {
                 if (@{$filter->args} == 1) {
                     $where .= $joiner if $where ne '';
-                    $where .= "${NOT}COALESCE((mrd.attrs->'date1') <= "
+                    $where .= "${NOT}COALESCE(pubdate_t.value <= "
                            . $self->QueryParser->quote_value($filter->args->[0])
                            . ", false)";
                 }
             } elsif ($filter->name eq 'after') {
                 if (@{$filter->args} == 1) {
                     $where .= $joiner if $where ne '';
-                    $where .= "${NOT}COALESCE((mrd.attrs->'date1') >= "
+                    $where .= "${NOT}COALESCE(pubdate_t.value >= "
                            . $self->QueryParser->quote_value($filter->args->[0])
                            . ", false)";
                 }
@@ -1041,12 +1096,13 @@ sub flatten {
                     $where .= $joiner if $where ne '';
                     $where .= "${NOT}COALESCE("
                            . $self->QueryParser->quote_value($filter->args->[0])
-                           . " BETWEEN (mrd.attrs->'date1') AND (mrd.attrs->'date2'), false)";
+                           . " BETWEEN pubdate_t.value AND (mrd.attrs->'date2'), false)";
+                    $uses_mrd = 1;
                 }
             } elsif ($filter->name eq 'between') {
                 if (@{$filter->args} == 2) {
                     $where .= $joiner if $where ne '';
-                    $where .= "${NOT}COALESCE((mrd.attrs->'date1') BETWEEN "
+                    $where .= "${NOT}COALESCE(pubdate_t.value BETWEEN "
                            . $self->QueryParser->quote_value($filter->args->[0])
                            . " AND "
                            . $self->QueryParser->quote_value($filter->args->[1])
@@ -1170,9 +1226,27 @@ sub flatten {
             }
         }
     }
+
+    if (@dlist) {
+
+        $where .= $joiner if $where ne '';
+        $where .= sprintf(
+            'mrv.vlist @@ \'%s\'',
+            join('', @dlist)
+        );
+    }
+
     warn "flatten(): full filter where => $where\n" if $self->QueryParser->debug;
 
-    return { rank_list => \@rank_list, from => $from, where => $where,  with => $with, uses_bre => $uses_bre };
+    return {
+        rank_list => \@rank_list,
+        from => $from,
+        where => $where,
+        with => $with,
+        uses_bre => $uses_bre,
+        uses_mrv => $uses_mrv,
+        uses_mrd => $uses_mrd
+    };
 }
 
 

commit bcc5d4146b49b174be4b8511d791fcfdb6a448b9
Author: Mike Rylander <mrylander at gmail.com>
Date:   Wed Jan 15 13:17:39 2014 -0500

    LP#1269911: Teach the IDL about MVF- and CRA-related structures
    
    IDL link from ccvm => ccraed via composite_def field
    
    IDL selector attribute for ccvm class
    
    ccraed gets CUD actions
    
    In conify/global/config/coded_value_map, if the selected attr type is
    composite=true, show a link from each coded value to manage the
    composite definition.
    
    Signed-off-by: Bill Erickson <berick at esilibrary.com>
    Signed-off-by: Mike Rylander <mrylander at gmail.com>
    Signed-off-by: Dan Wells <dbw2 at calvin.edu>

diff --git a/Open-ILS/examples/fm_IDL.xml b/Open-ILS/examples/fm_IDL.xml
index 3e82953..7c8201d 100644
--- a/Open-ILS/examples/fm_IDL.xml
+++ b/Open-ILS/examples/fm_IDL.xml
@@ -784,8 +784,10 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA
 			<field reporter:label="Name" name="name" reporter:datatype="id" reporter:selector="label"  oils_obj:required="true"/>
 			<field reporter:label="Label" name="label" reporter:datatype="text"  oils_obj:required="true"/>
 			<field reporter:label="Description" name="description" reporter:datatype="text" />
+			<field reporter:label="Multi-valued?" name="multi" reporter:datatype="bool"/>
 			<field reporter:label="Filter?" name="filter" reporter:datatype="bool"/>
 			<field reporter:label="Sorter?" name="sorter" reporter:datatype="bool"/>
+			<field reporter:label="Composite attribute?" name="composite" reporter:datatype="bool"/>
 			<field reporter:label="MARC Tag" name="tag" reporter:datatype="text"/>
 			<field reporter:label="MARC Subfields" name="sf_list" reporter:datatype="text"/>
 			<field reporter:label="Joiner" name="joiner" reporter:datatype="text"/>
@@ -830,7 +832,77 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA
         </permacrud>
 	</class>
 
-	<class id="mra" controller="open-ils.cstore open-ils.pcrud" oils_obj:fieldmapper="metabib::record_attr" oils_persist:tablename="metabib.record_attr" reporter:label="SVF Record Attribute" oils_persist:field_safe="true">
+	<class id="ccraed" controller="open-ils.cstore open-ils.pcrud" oils_obj:fieldmapper="config::composite_attr_entry_definition" oils_persist:tablename="config.composite_attr_entry_definition" reporter:label="Composite Attribute Definitions" oils_persist:field_safe="true">
+		<fields oils_persist:primary="coded_value">
+			<field reporter:label="Coded Value" name="coded_value" reporter:datatype="id" oils_obj:required="true"/>
+			<field reporter:label="Defintion" name="definition" reporter:datatype="text"  oils_obj:required="true"/>
+		</fields>
+		<links>
+			<link field="coded_value" reltype="has_a" key="id" map="" class="ccvm"/>
+		</links>
+        <permacrud xmlns="http://open-ils.org/spec/opensrf/IDL/permacrud/v1">
+            <actions>
+                <create permission="ADMIN_CODED_VALUE" global_required="true"/>
+                <retrieve/>
+                <update permission="ADMIN_CODED_VALUE" global_required="true"/>
+                <delete permission="ADMIN_CODED_VALUE" global_required="true"/>
+            </actions>
+        </permacrud>
+	</class>
+
+	<class id="murav" controller="open-ils.cstore open-ils.pcrud" oils_obj:fieldmapper="metabib::uncontrolled_record_attr_value" oils_persist:tablename="metabib.uncontrolled_record_attr_value" reporter:label="Uncontrolled Record Attribute Values" oils_persist:field_safe="true">
+		<fields oils_persist:primary="id" oils_persist:sequence="metabib.uncontrolled_record_attr_value_id_seq">
+			<field reporter:label="ID" name="id" reporter:datatype="id" oils_obj:required="true"/>
+			<field reporter:label="Attribute" name="attr" reporter:datatype="text"  oils_obj:required="true"/>
+			<field reporter:label="Value" name="value" reporter:datatype="text"  oils_obj:required="true"/>
+		</fields>
+		<links>
+			<link field="attr" reltype="has_a" key="name" map="" class="crad"/>
+		</links>
+        <permacrud xmlns="http://open-ils.org/spec/opensrf/IDL/permacrud/v1">
+            <actions>
+                <retrieve/>
+            </actions>
+        </permacrud>
+	</class>
+
+
+	<class id="mrs" controller="open-ils.cstore open-ils.pcrud" oils_obj:fieldmapper="metabib::record_sorter" oils_persist:tablename="metabib.record_sorter" reporter:label="Record Sort Values" oils_persist:field_safe="true">
+		<fields oils_persist:primary="id" oils_persist:sequence="metabib.record_sorter_id_seq">
+			<field reporter:label="ID" name="id" reporter:datatype="id" oils_obj:required="true"/>
+			<field reporter:label="Bib Record ID" name="source" reporter:datatype="int" oils_obj:required="true"/>
+			<field reporter:label="Attribute" name="attr" reporter:datatype="text"  oils_obj:required="true"/>
+			<field reporter:label="Value" name="value" reporter:datatype="text"  oils_obj:required="true"/>
+		</fields>
+		<links>
+			<link field="source" reltype="has_a" key="id" map="" class="bre"/>
+			<link field="attr" reltype="has_a" key="name" map="" class="crad"/>
+		</links>
+        <permacrud xmlns="http://open-ils.org/spec/opensrf/IDL/permacrud/v1">
+            <actions>
+                <retrieve/>
+            </actions>
+        </permacrud>
+	</class>
+
+
+	<class id="mravl" controller="open-ils.cstore open-ils.pcrud" oils_obj:fieldmapper="metabib::record_attr_vector_list" oils_persist:tablename="metabib.record_attr_vector_list" reporter:label="MVF Record Attribute Vectors" oils_persist:field_safe="true">
+		<fields oils_persist:primary="source">
+			<field reporter:label="Record ID" name="source" reporter:datatype="id" oils_obj:required="true"/>
+			<field reporter:label="Vector" name="vlist" reporter:datatype="text"  oils_obj:required="true"/> <!-- Actually an int[], but this is the best we can do in fm_IDL.xml -->
+		</fields>
+		<links>
+			<link field="source" reltype="has_a" key="id" map="" class="bre"/>
+		</links>
+        <permacrud xmlns="http://open-ils.org/spec/opensrf/IDL/permacrud/v1">
+            <actions>
+                <retrieve/>
+            </actions>
+        </permacrud>
+	</class>
+
+
+	<class id="mra" controller="open-ils.cstore open-ils.pcrud" oils_obj:fieldmapper="metabib::record_attr" oils_persist:tablename="metabib.record_attr" reporter:label="SVF Record Attribute" oils_persist:field_safe="true" oils_persist:readonly="true">
 		<fields oils_persist:primary="id">
 			<field reporter:label="Record ID" name="id" reporter:datatype="id" oils_obj:required="true"/>
 			<field reporter:label="Attributes" name="attrs" reporter:datatype="text"  oils_obj:required="true"/>
@@ -869,7 +941,7 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA
 
 	<class id="ccvm" controller="open-ils.cstore open-ils.pcrud" oils_obj:fieldmapper="config::coded_value_map" oils_persist:tablename="config.coded_value_map" reporter:label="SVF Record Attribute Coded Value Map" oils_persist:field_safe="true">
 		<fields oils_persist:primary="id" oils_persist:sequence="config.coded_value_map_id_seq">
-			<field reporter:label="ID" name="id" reporter:datatype="id"  oils_obj:required="true"/>
+			<field reporter:label="ID" name="id" reporter:datatype="id"  oils_obj:required="true" reporter:selector="value"/>
 			<field reporter:label="SVF Attribute" name="ctype" reporter:datatype="link"  oils_obj:required="true"/>
 			<field reporter:label="Code" name="code" reporter:datatype="text"  oils_obj:required="true"/>
 			<field reporter:label="Value" name="value" reporter:datatype="text"  oils_obj:required="true" oils_persist:i18n="true"/>
@@ -877,9 +949,11 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA
 			<field reporter:label="OPAC Visible" name="opac_visible" reporter:datatype="bool"/>
 			<field reporter:label="Search Label" name="search_label" reporter:datatype="text" oils_persist:i18n="true"/>
             <field reporter:label="Is Simple Selector" name="is_simple" reporter:datatype="bool"/>
+            <field reporter:label="Composite Definition" name="composite_def" oils_persist:virtual="true" reporter:datatype="link"/>
 		</fields>
 		<links>
 			<link field="ctype" reltype="has_a" key="name" map="" class="crad"/>
+			<link field="composite_def" reltype="might_have" key="coded_value" map="" class="ccraed"/>
 		</links>
         <permacrud xmlns="http://open-ils.org/spec/opensrf/IDL/permacrud/v1">
             <actions>
diff --git a/Open-ILS/src/templates/conify/global/config/coded_value_map.tt2 b/Open-ILS/src/templates/conify/global/config/coded_value_map.tt2
index a31d2c8..3d30089 100644
--- a/Open-ILS/src/templates/conify/global/config/coded_value_map.tt2
+++ b/Open-ILS/src/templates/conify/global/config/coded_value_map.tt2
@@ -17,12 +17,19 @@
     <table  jsId="ccvmGrid"
             autoHeight='true'
             dojoType="openils.widget.AutoGrid"
-            fieldOrder="['ctype', 'code', 'value', 'description', 'opac_visible', 'search_label']"
+            fieldOrder="['ctype', 'code', 'value', 'description', 'opac_visible', 'search_label', 'is_simple', 'composite_def']"
             query="{code: '*'}"
             defaultCellWidth='"25%"'
             fmClass='ccvm'
             showPaginator='true'
-            editOnEnter='true'/>
+            editOnEnter='true'>
+      <thead>
+        <tr><th field='composite_def' 
+                get='getCompositeDefLink' 
+                formatter='formatCompositeDefLink'>
+            [% l('Composite Definition') %]</th></tr>
+      </thead>
+    </table>
  </div>
 
 <script type ="text/javascript">
@@ -31,6 +38,22 @@
     dojo.require('openils.widget.AutoGrid');
     dojo.require('openils.widget.AutoFieldWidget');
 
+    function getCompositeDefLink(rowId, item) {
+      if (!item) return '';
+      return this.grid.store.getValue(item, 'id');
+    }
+
+    var isComposite = false;
+    function formatCompositeDefLink(id) {
+      if (id && isComposite) {
+        return "<a href='" + oilsBasePath +
+          "/conify/global/config/composite_attr_entry_definition/" 
+          + id + "'>Manage</a>";
+        } else {
+          return "";
+      }
+    }
+
     openils.Util.addOnLoad(
         function() {
 
@@ -44,6 +67,21 @@
                 function(w, ww) {
                     dojo.connect(w, 'onChange', 
                         function(newVal) {
+
+                            // see if this attr def supports composite entries
+                            w.store.fetch({
+                              onComplete : function(list) {
+                                if (!list.length) return;
+                                var comp = w.store.getValue(list[0], 'composite');
+                                if (comp == 't') {
+                                  isComposite = true;
+                                } else {
+                                  isComposite = false;
+                                }
+                              },
+                              query : {name : ''+newVal}
+                            });
+                            
                             ccvmGrid.resetStore();
                             ccvmGrid.loadAll({order_by : {ccvm : 'code'}}, {ctype : newVal});
                             ccvmGrid.overrideWidgetArgs.ctype = {dijitArgs : {value : newVal}};

commit d2b047058e99da0ce2c944643a03b432beb4dda5
Author: Mike Rylander <mrylander at gmail.com>
Date:   Tue Jan 14 16:28:04 2014 -0500

    LP#1269911: Database elements of MVF and CRA
    
    * Teach vandelay.marc21_physical_characteristics() to see all 007's
    
    We use vandelay.marc21_physical_characteristics() to extract fixed
    field data that lives in the 007.  Before this change, it would
    only look at the first 007 in the record.  Now it will look at
    all of them in turn, supporting configurations such as DVD+BluRay.
    
    * Add intarray extension
    
    We need intarray for GIN index support of integer arrays, which is
    how we'll be storing the in-use record attribute value identifiers.
    
    * Hidy hole in which to stick "uncontrolled" values
    
    In order to make use of the massive speed increases provided by
    intarray indexing, we need to use (you guessed it) integers.  But
    uncontrolled record attributes are not necessarily (or even very
    often) numbers.  We will store them in a table of unique (per
    attribute) values, and use the id from that table in our intarray
    indexing.  That id comes from a DECREMENTING serial that starts
    at -1 and counts downward.  This avoids collision with the other
    set of integers (the id from config.coded_value_map) that we will
    use for controlled record attribute values.
    
    * Add a multi flag for record attrs
    
    We pre-coordinate which record attrs are allowed to be multi-valued
    with this new bool.  Most can be, we set the default to true and
    adjust the seed data for those that should be false (sorters and
    fields in the leader).
    
    * New intarray-focused attribute extraction
    
    We rewrite the record attribute extraction to capture all the
    record-supplied values for each attribute (where multi is true)
    and store that list in the new (fkey-corrected) metabib.record_attr_vector_list
    table.  Only filters make it into this table.
    
    We also insert a parameter after the record id to accept a list of
    record attributes we want to rewrite. This defaults to NULL to rewrite
    all of them.
    
    Sorters are stored in a new, separate table built specifically for them.
    
    metabib.record_attr becomes a vew atop metabib.record_attr_vector_list
    which expands the intarray stored therein into an hstore. For
    multi=true attributes, only one will be returned through this view,
    as is HSTORE's way, and which will be returned is undefined.  However
    this view is only provided for the purpose of backward compat with
    reports or other locally defined logic.
    
    And, finally, baseline seed data
    
    Signed-off-by: Mike Rylander <mrylander at gmail.com>
    Signed-off-by: Dan Wells <dbw2 at calvin.edu>

diff --git a/Open-ILS/src/sql/Pg/002.schema.config.sql b/Open-ILS/src/sql/Pg/002.schema.config.sql
index 8e196d3..ef517e9 100644
--- a/Open-ILS/src/sql/Pg/002.schema.config.sql
+++ b/Open-ILS/src/sql/Pg/002.schema.config.sql
@@ -770,8 +770,10 @@ CREATE TABLE config.record_attr_definition (
     name        TEXT    PRIMARY KEY,
     label       TEXT    NOT NULL, -- I18N
     description TEXT,
+    multi       BOOL    NOT NULL DEFAULT TRUE,  -- will store all values from a record
     filter      BOOL    NOT NULL DEFAULT TRUE,  -- becomes QP filter if true
     sorter      BOOL    NOT NULL DEFAULT FALSE, -- becomes QP sort() axis if true
+    composite   BOOL    NOT NULL DEFAULT FALSE, -- its values are derived from others
 
 -- For pre-extracted fields. Takes the first occurance, uses naive subfield ordering
     tag         TEXT, -- LIKE format
@@ -849,6 +851,11 @@ BEGIN
 END;
 $f$ LANGUAGE PLPGSQL;
 
+CREATE TABLE config.composite_attr_entry_definition(
+    coded_value PRIMARY KEY NOT NULL REFERENCES config.coded_value_map (id) ON UPDATE CASCADE ON DELETE CASCADE,
+    definition  TEXT        NOT NULL -- JSON
+);
+
 -- List applied db patches that are deprecated by (and block the application of) my_db_patch
 CREATE OR REPLACE FUNCTION evergreen.upgrade_list_applied_deprecates ( my_db_patch TEXT ) RETURNS SETOF evergreen.patch AS $$
     SELECT  DISTINCT l.version
diff --git a/Open-ILS/src/sql/Pg/012.schema.vandelay.sql b/Open-ILS/src/sql/Pg/012.schema.vandelay.sql
index a57c6f9..5076037 100644
--- a/Open-ILS/src/sql/Pg/012.schema.vandelay.sql
+++ b/Open-ILS/src/sql/Pg/012.schema.vandelay.sql
@@ -302,27 +302,27 @@ DECLARE
     retval  biblio.marc21_physical_characteristics%ROWTYPE;
 BEGIN
 
-    _007 := oils_xpath_string( '//*[@tag="007"]', marc );
-
-    IF _007 IS NOT NULL AND _007 <> '' THEN
-        SELECT * INTO ptype FROM config.marc21_physical_characteristic_type_map WHERE ptype_key = SUBSTRING( _007, 1, 1 );
-
-        IF ptype.ptype_key IS NOT NULL THEN
-            FOR psf IN SELECT * FROM config.marc21_physical_characteristic_subfield_map WHERE ptype_key = ptype.ptype_key LOOP
-                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 );
-
-                IF pval.id IS NOT NULL THEN
-                    rowid := rowid + 1;
-                    retval.id := rowid;
-                    retval.ptype := ptype.ptype_key;
-                    retval.subfield := psf.id;
-                    retval.value := pval.id;
-                    RETURN NEXT retval;
-                END IF;
-
-            END LOOP;
+    FOR _007 IN SELECT oils_xpath_string('//*', value) FROM UNNEST(oils_xpath('//*[@tag="007"]', marc)) x(value) LOOP
+        IF _007 IS NOT NULL AND _007 <> '' THEN
+            SELECT * INTO ptype FROM config.marc21_physical_characteristic_type_map WHERE ptype_key = SUBSTRING( _007, 1, 1 );
+
+            IF ptype.ptype_key IS NOT NULL THEN
+                FOR psf IN SELECT * FROM config.marc21_physical_characteristic_subfield_map WHERE ptype_key = ptype.ptype_key LOOP
+                    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 );
+
+                    IF pval.id IS NOT NULL THEN
+                        rowid := rowid + 1;
+                        retval.id := rowid;
+                        retval.ptype := ptype.ptype_key;
+                        retval.subfield := psf.id;
+                        retval.value := pval.id;
+                        RETURN NEXT retval;
+                    END IF;
+
+                END LOOP;
+            END IF;
         END IF;
-    END IF;
+    END LOOP;
 
     RETURN;
 END;
diff --git a/Open-ILS/src/sql/Pg/030.schema.metabib.sql b/Open-ILS/src/sql/Pg/030.schema.metabib.sql
index d81525e..fc0e454 100644
--- a/Open-ILS/src/sql/Pg/030.schema.metabib.sql
+++ b/Open-ILS/src/sql/Pg/030.schema.metabib.sql
@@ -268,6 +268,82 @@ CREATE TRIGGER facet_force_nfc_tgr
 	BEFORE UPDATE OR INSERT ON metabib.facet_entry
 	FOR EACH ROW EXECUTE PROCEDURE evergreen.facet_force_nfc();
 
+-- DECREMENTING serial starts at -1
+CREATE SEQUENCE metabib.uncontrolled_record_attr_value_id_seq INCREMENT BY -1;
+
+CREATE TABLE metabib.uncontrolled_record_attr_value (
+    id      BIGINT  PRIMARY KEY DEFAULT nextval('metabib.uncontrolled_record_attr_value_id_seq'),
+    attr    TEXT    NOT NULL REFERENCES config.record_attr_definition (name),
+    value   TEXT    NOT NULL
+);
+CREATE UNIQUE INDEX muv_once_idx ON metabib.uncontrolled_record_attr_value (attr,value);
+
+CREATE VIEW metabib.record_attr_id_map AS
+    SELECT id, attr, value FROM metabib.uncontrolled_record_attr_value
+        UNION
+    SELECT  c.id, c.ctype AS attr, c.code AS value
+      FROM  config.coded_value_map c
+            JOIN config.record_attr_definition d ON (d.name = c.ctype AND NOT d.composite);
+
+CREATE VIEW metabib.composite_attr_id_map AS
+    SELECT  c.id, c.ctype AS attr, c.code AS value
+      FROM  config.coded_value_map c
+            JOIN config.record_attr_definition d ON (d.name = c.ctype AND d.composite);
+
+CREATE VIEW metabib.full_attr_id_map AS
+    SELECT id, attr, value FROM metabib.record_attr_id_map
+        UNION
+    SELECT id, attr, value FROM metabib.composite_attr_id_map;
+
+
+CREATE FUNCTION metabib.compile_composite_attr ( cattr_id INT ) RETURNS query_int AS $func$
+
+    use JSON;
+    my $cid = shift;
+
+    my $cattr = spi_exec_query(
+        "SELECT * FROM config.composite_attr_entry_defintion WHERE id = $cid"
+    )->{rows}[0];
+
+    die("Composite attribute not found with an id of $cid") unless $cattr;
+
+    my $plan = spi_prepare('SELECT * FROM metabib.full_attr_id_map WHERE attr = $1 AND value = $2', qw/TEXT TEXT/);
+    my $def = from_json $cattr->{definition};
+
+    sub recurse {
+        my $d = shift;
+        my $j = '&';
+        my @list;
+
+        if (ref $d eq 'HASH') { # node or AND
+            if (exists $d->{_attr}) { # it is a node
+                return spi_query_prepared(
+                    $plan, {limit => 1}, $d->{_attr}, $d->{_val}
+                )->{rows}[0]{id};
+            } elsif (exists $d->{_not} && scalar(keys(%$d)) == 1) { # it is a NOT
+                return '!' . recurse($$d{_not});
+            } else { # an AND list
+                @list = map { recurse($$d{$_}) } sort keys %$d;
+            }
+        } elsif (ref $d eq 'ARRAY')
+            $j = '|'
+            @list = map { recurse($_) } @$d;
+        }
+        return '(' . join($j, at list) . ')' if @list;
+        return '';
+    }
+
+    return recurse($def);
+
+$func$ STRICT STABLE IMMUTABLE LANGUAGE plperlu;
+
+CREATE TABLE metabib.record_attr_vector_list (
+    source  BIGINT  PRIMARY KEY REFERNECES  biblio.record_entry (id),
+    vlist   INT[]   NOT NULL -- stores id from ccvm AND murav
+);
+CREATE INDEX mrca_vlist_idx ON metabib.record_attr_vector_list USING gin ( vlist gin__int_ops );
+
+/* This becomes a view, and we do sorters differently ...
 CREATE TABLE metabib.record_attr (
 	id		BIGINT	PRIMARY KEY REFERENCES biblio.record_entry (id) ON DELETE CASCADE,
 	attrs	HSTORE	NOT NULL DEFAULT ''::HSTORE
@@ -275,8 +351,37 @@ CREATE TABLE metabib.record_attr (
 CREATE INDEX metabib_svf_attrs_idx ON metabib.record_attr USING GIST (attrs);
 CREATE INDEX metabib_svf_date1_idx ON metabib.record_attr ((attrs->'date1'));
 CREATE INDEX metabib_svf_dates_idx ON metabib.record_attr ((attrs->'date1'),(attrs->'date2'));
+*/
+
+/* ... like this */
+CREATE TABLE metabib.record_sorter (
+    id      BIGSERIAL   PRIMARY KEY,
+    source  BIGINT      NOT NULL REFERENCES biblio.record_entry (id) ON DELETE CASCADE,
+    attr    TEXT        NOT NULL REFERENCES config.record_attr_definition (name) ON DELETE CASCADE,
+    value   TEXT        NOT NULL
+);
+CREATE INDEX metabib_sorter_source_idx ON metabib.record_sorter (source); -- we may not need one of this or the next ... stats will tell
+CREATE INDEX metabib_sorter_s_a_idx ON metabib.record_sorter (source, attr);
+CREATE INDEX metabib_sorter_a_v_idx ON metabib.record_sorter (attr, value);
+
+
+CREATE TYPE metabib.record_attr_type AS (
+    id      BIGINT,
+    attrs   HSTORE
+);
 
--- Back-compat view ... we're moving to an HSTORE world
+-- Back-compat view ... we're moving to an INTARRAY world
+CREATE VIEW metabib.record_attr_flat AS
+    SELECT  v.source AS id,
+            m.attr,
+            m.value
+      FROM  metabib.full_attr_id_map m
+            JOIN  metabib.record_attr_vector_list v ( m.id = ANY( v.vlist ) );
+
+CREATE VIEW metabib.record_attr AS
+    SELECT id, HSTORE( ARRAY_AGG( attr ), ARRAY_AGG( value ) ) AS attrs FROM metabib.record_attr_flat GROUP BY 1;
+
+-- Back-back-compat view ... we use to live in an HSTORE world
 CREATE TYPE metabib.rec_desc_type AS (
     item_type       TEXT,
     item_form       TEXT,
@@ -832,6 +937,36 @@ CREATE OR REPLACE FUNCTION biblio.marc21_record_type( rid BIGINT ) RETURNS confi
     SELECT * FROM vandelay.marc21_record_type( (SELECT marc FROM biblio.record_entry WHERE id = $1) );
 $func$ LANGUAGE SQL;
 
+CREATE OR REPLACE FUNCTION vandelay.marc21_extract_fixed_field_list( marc TEXT, ff TEXT ) RETURNS TEXT[] AS $func$
+DECLARE
+    rtype       TEXT;
+    ff_pos      RECORD;
+    tag_data    RECORD;
+    val         TEXT;
+    collection  TEXT[] := '{}'::TEXT[];
+BEGIN
+    rtype := (vandelay.marc21_record_type( marc )).code;
+    FOR ff_pos IN SELECT * FROM config.marc21_ff_pos_map WHERE fixed_field = ff AND rec_type = rtype ORDER BY tag DESC LOOP
+        IF ff_pos.tag = 'ldr' THEN
+            val := oils_xpath_string('//*[local-name()="leader"]', marc);
+            IF val IS NOT NULL THEN
+                val := SUBSTRING( val, ff_pos.start_pos + 1, ff_pos.length );
+                collection := collection || val;
+            END IF;
+        ELSE
+            FOR tag_data IN SELECT value FROM UNNEST( oils_xpath( '//*[@tag="' || UPPER(ff_pos.tag) || '"]/text()', marc ) ) x(value) LOOP
+                val := SUBSTRING( tag_data.value, ff_pos.start_pos + 1, ff_pos.length );
+                collection := collection || val;
+            END LOOP;
+        END IF;
+        val := REPEAT( ff_pos.default_val, ff_pos.length );
+        collection := collection || val;
+    END LOOP;
+
+    RETURN collection;
+END;
+$func$ LANGUAGE PLPGSQL;
+
 CREATE OR REPLACE FUNCTION vandelay.marc21_extract_fixed_field( marc TEXT, ff TEXT ) RETURNS TEXT AS $func$
 DECLARE
     rtype       TEXT;
@@ -861,6 +996,10 @@ BEGIN
 END;
 $func$ LANGUAGE PLPGSQL;
 
+CREATE OR REPLACE FUNCTION biblio.marc21_extract_fixed_field_list( rid BIGINT, ff TEXT ) RETURNS TEXT[] AS $func$
+    SELECT * FROM vandelay.marc21_extract_fixed_field_list( (SELECT marc FROM biblio.record_entry WHERE id = $1), $2 );
+$func$ LANGUAGE SQL;
+
 CREATE OR REPLACE FUNCTION biblio.marc21_extract_fixed_field( rid BIGINT, ff TEXT ) RETURNS TEXT AS $func$
     SELECT * FROM vandelay.marc21_extract_fixed_field( (SELECT marc FROM biblio.record_entry WHERE id = $1), $2 );
 $func$ LANGUAGE SQL;
@@ -1250,16 +1389,213 @@ CREATE OR REPLACE FUNCTION biblio.map_authority_linking (bibid BIGINT, marc TEXT
     SELECT $1;
 $func$ LANGUAGE SQL;
 
--- AFTER UPDATE OR INSERT trigger for biblio.record_entry
-CREATE OR REPLACE FUNCTION biblio.indexing_ingest_or_delete () RETURNS TRIGGER AS $func$
+CREATE OR REPLACE FUNCTION metabib.reingest_record_attributes (rid BIGINT, pattr_list TEXT[] DEFAULT NULL, prmarc TEXT DEFAULT NULL, rdeleted BOOL DEFAULT TRUE) RETURNS VOID AS $func$
 DECLARE
     transformed_xml TEXT;
+    rmarc           TEXT := prmarc;
+    tmp_val         TEXT;
     prev_xfrm       TEXT;
     normalizer      RECORD;
     xfrm            config.xml_transform%ROWTYPE;
-    attr_value      TEXT;
-    new_attrs       HSTORE := ''::HSTORE;
+    attr_vector     INT[] := '{}'::INT[];
+    attr_vector_tmp INT[];
+    attr_list       TEXT[] := pattr_list;
+    attr_value      TEXT[];
+    norm_attr_value TEXT[];
+    tmp_xml         XML;
     attr_def        config.record_attr_definition%ROWTYPE;
+    ccvm_row        config.code_value_map%ROWTYPE;
+BEGIN
+
+    IF attr_list IS NULL OR rdeleted THEN -- need to do the full dance on INSERT or undelete
+        SELECT ARRAY_AGG(name) INTO attr_list FROM config.record_attr_definition;
+    END IF;
+
+    IF rmarc IS NULL THEN
+        SELECT marc INTO rmarc FROM biblio.record_entry WHERE id = rid;
+    END IF;
+
+    FOR attr_def IN SELECT * FROM config.record_attr_definition WHERE NOT composite AND name = ANY( attr_list ) ORDER BY format LOOP
+
+        attr_value := '{}'::TEXT[]
+        norm_attr_value := '{}'::TEXT[]
+        attr_vector_tmp := '{}'::INT[]
+
+        SELECT * INTO ccvm_row FROM config.code_value_map c WHERE c.ctype = attr_def.name; 
+
+        -- tag+sf attrs only support SVF
+        IF attr_def.tag IS NOT NULL THEN -- tag (and optional subfield list) selection
+            SELECT  ARRAY[ARRAY_TO_STRING(ARRAY_AGG(value), COALESCE(attr_def.joiner,' '))] INTO attr_value
+              FROM  (SELECT * FROM metabib.full_rec ORDER BY tag, subfield) AS x
+              WHERE record = rid
+                    AND tag LIKE attr_def.tag
+                    AND CASE
+                        WHEN attr_def.sf_list IS NOT NULL 
+                            THEN POSITION(subfield IN attr_def.sf_list) > 0
+                        ELSE TRUE
+                        END
+              GROUP BY tag
+              ORDER BY tag
+              LIMIT 1;
+
+        ELSIF attr_def.fixed_field IS NOT NULL THEN -- a named fixed field, see config.marc21_ff_pos_map.fixed_field
+            attr_value := vandelay.marc21_extract_fixed_field_list(rmarc, attr_def.fixed_field);
+
+            IF NOT attr_def.multi THEN
+                attr_value := ARRAY[attr_value[1]];
+            END IF;
+
+        ELSIF attr_def.xpath IS NOT NULL THEN -- and xpath expression
+
+            SELECT INTO xfrm * FROM config.xml_transform WHERE name = attr_def.format;
+    
+            -- See if we can skip the XSLT ... it's expensive
+            IF prev_xfrm IS NULL OR prev_xfrm <> xfrm.name THEN
+                -- Can't skip the transform
+                IF xfrm.xslt <> '---' THEN
+                    transformed_xml := oils_xslt_process(rmarc,xfrm.xslt);
+                ELSE
+                    transformed_xml := rmarc;
+                END IF;
+    
+                prev_xfrm := xfrm.name;
+            END IF;
+
+            IF xfrm.name IS NULL THEN
+                -- just grab the marcxml (empty) transform
+                SELECT INTO xfrm * FROM config.xml_transform WHERE xslt = '---' LIMIT 1;
+                prev_xfrm := xfrm.name;
+            END IF;
+
+            FOR tmp_xml IN SELECT XPATH(attr_def.xpath, transformed_xml, ARRAY[ARRAY[xfrm.prefix, xfrm.namespace_uri]]) LOOP
+                attr_value := attr_value ||
+                                oils_xpath_string(
+                                    '//*',
+                                    tmp_xml::TEXT,
+                                    COALESCE(attr_def.joiner,' '),
+                                    ARRAY[ARRAY[xfrm.prefix, xfrm.namespace_uri]]
+                                );
+                EXIT WHEN NOT attr_def.multi;
+            END LOOP;
+
+        ELSIF attr_def.phys_char_sf IS NOT NULL THEN -- a named Physical Characteristic, see config.marc21_physical_characteristic_*_map
+            SELECT  ARRAY_AGG(m.value) INTO attr_vlue
+              FROM  vandelay.marc21_physical_characteristics(rmarc) v
+                    LEFT JOIN config.marc21_physical_characteristic_value_map m ON (m.id = v.value)
+              WHERE v.subfield = attr_def.phys_char_sf
+                    AND ( ccvm.id IS NULL OR ( ccvm.id IS NOT NULL AND v.id IS NOT NULL) );
+
+            IF NOT attr_def.multi THEN
+                attr_value := ARRAY[attr_value[1]];
+            END IF;
+
+        END IF;
+
+        -- apply index normalizers to attr_value
+        FOR tmp_val IN SELECT value FROM UNNEST(attr_value) x(value);
+            FOR normalizer IN
+                SELECT  n.func AS func,
+                        n.param_count AS param_count,
+                        m.params AS params
+                  FROM  config.index_normalizer n
+                        JOIN config.record_attr_index_norm_map m ON (m.norm = n.id)
+                  WHERE attr = attr_def.name
+                  ORDER BY m.pos LOOP
+                    EXECUTE 'SELECT ' || normalizer.func || '(' ||
+                        COALESCE( quote_literal( tmp_val ), 'NULL' ) ||
+                        CASE
+                            WHEN normalizer.param_count > 0
+                                THEN ',' || REPLACE(REPLACE(BTRIM(normalizer.params,'[]'),E'\'',E'\\\''),E'"',E'\'')
+                                ELSE ''
+                            END ||
+                        ')' INTO tmp_val;
+
+            END LOOP;
+            norm_attr_value := norm_attr_value || tmp_val;
+        END LOOP;
+
+        IF attr_def.filter THEN
+            -- Create unknown uncontrolled values and find the IDs of the values
+            IF ccvm.id IS NULL THEN
+                FOR tmp_val FROM SELECT value FROM UNNEST(norm_attr_value) x(value) LOOP
+                    BEGIN; -- use subtransaction to isolate unique constraint violations
+                        INSERT INTO metabib.uncontrolled_record_attr_value ( attr, value ) VALUES ( attr_def.name, tmp_val );
+                    EXCEPTION WHEN unique_violation THEN END;
+                END LOOP;
+    
+                SELECT ARRAY_AGG(id) INTO attr_vector_tmp FROM metabib.uncontrolled_record_attr_value WHERE attr = attr_def.name AND value = ANY( norm_attr_value );
+            ELSE
+                SELECT ARRAY_AGG(id) INTO attr_vector_tmp FROM config.coded_value_map WHERE ctype = attr_def.name AND value = ANY( norm_attr_value );
+            END IF;
+
+            -- Add the new value to the vector
+            attr_vector := attr_vector || attr_vector_tmp;
+        END IF;
+
+        IF attr_def.sorter THEN
+            DELETE FROM metabib.record_sorter WHERE source = rid AND attr = attr_def.name;
+            INSERT INTO metabib.record_sorter (source, attr, value) VALUES (rid, attr_def.name, norm_attr_value[1]);
+        END IF;
+
+    END LOOP;
+
+/* We may need to rewrite the vlist to contain
+   the intersection of new values for requested
+   attrs and old values for ignored attrs. To
+   do this, we take the old attr vlist and
+   subtract any values that are valid for the
+   requested attrs, and then add back the new
+   set of attr values. */
+
+    IF ARRAY_LENGTH(pattr_list, 1) > 0 THEN 
+        SELECT vlist INTO attr_vector_tmp FROM metabib.record_attr_vector_list WHERE source = rid;
+        SELECT attr_vector_tmp - ARRAY_AGG(id) INTO attr_vector_tmp FROM metabib.full_attr_id_map WHERE attr = ANY (pattr_list);
+        attr_vector := attr_vector || attr_vector_tmp;
+    END IF;
+
+    -- On to composite attributes, now that the record attrs have been pulled.  Processed in name order, so later composite
+    -- attributes can depend on earlier ones.
+    FOR attr_def IN SELECT * FROM config.record_attr_definition WHERE composite AND name = ANY( attr_list ) ORDER BY name LOOP
+
+        FOR ccvm_row IN SELECT * FROM config.coded_value_map c WHERE c.ctype = attr_def.name ORDER BY value LOOP
+
+            tmp_val := metabib.compile_composite_attr( ccvm_row.id );
+            NEXT WHEN tmp_val IS NULL OR tmp_val = ''; -- nothing to do
+
+            IF attr_def.filter THEN
+                IF attr_vector @@ tmp_val::query_int THEN
+                    attr_vector = attr_vector + intset(ccvm_row.id);
+                    EXIT WHEN NOT attr_def.multi;
+                END IF;
+            END IF;
+
+            IF attr_def.sorter THEN
+                IF attr_vector ~~ tmp_val THEN
+                    DELETE FROM metabib.record_sorter WHERE source = rid AND attr = attr_def.name;
+                    INSERT INTO metabib.record_sorter (source, attr, value) VALUES (rid, attr_def.name, ccvm_row.code);
+                END IF;
+            END IF;
+
+        END LOOP;
+
+    END LOOP;
+
+    IF ARRAY_LENGTH(attr_vector, 1) > 0 THEN
+        IF rdeleted THEN -- initial insert OR revivication
+            DELETE FROM metabib.record_attr_vector_list WHERE source = rid;
+            INSERT INTO metabib.record_attr_vector_list (source, vlist) VALUES (rid, attr_vector);
+        ELSE
+            UPDATE metabib.record_attr_vector_list SET vlist = attr_vector WHERE source = rid;
+        END IF;
+    END IF;
+
+END;
+
+$$ LANGUAGE PLPGSQL;
+
+
+-- AFTER UPDATE OR INSERT trigger for biblio.record_entry
+CREATE OR REPLACE FUNCTION biblio.indexing_ingest_or_delete () RETURNS TRIGGER AS $func$
 BEGIN
 
     IF NEW.deleted IS TRUE THEN -- If this bib is deleted
@@ -1270,7 +1606,7 @@ BEGIN
             -- with the #deleted modifier, so one should turn on the named
             -- internal flag for that functionality.
             DELETE FROM metabib.metarecord_source_map WHERE source = NEW.id;
-            DELETE FROM metabib.record_attr WHERE id = NEW.id;
+            DELETE FROM metabib.record_attr_vector_list WHERE source = NEW.id;
         END IF;
 
         DELETE FROM authority.bib_linking WHERE bib = NEW.id; -- Avoid updating fields in bibs that are no longer visible
@@ -1301,90 +1637,7 @@ BEGIN
         -- Now we pull out attribute data, which is dependent on the mfr for all but XPath-based fields
         PERFORM * FROM config.internal_flag WHERE name = 'ingest.disable_metabib_rec_descriptor' AND enabled;
         IF NOT FOUND THEN
-            FOR attr_def IN SELECT * FROM config.record_attr_definition ORDER BY format LOOP
-
-                IF attr_def.tag IS NOT NULL THEN -- tag (and optional subfield list) selection
-                    SELECT  STRING_AGG(value, COALESCE(attr_def.joiner,' ')) INTO attr_value
-                      FROM  (SELECT * FROM metabib.full_rec ORDER BY tag, subfield) AS x
-                      WHERE record = NEW.id
-                            AND tag LIKE attr_def.tag
-                            AND CASE
-                                WHEN attr_def.sf_list IS NOT NULL 
-                                    THEN POSITION(subfield IN attr_def.sf_list) > 0
-                                ELSE TRUE
-                                END
-                      GROUP BY tag
-                      ORDER BY tag
-                      LIMIT 1;
-
-                ELSIF attr_def.fixed_field IS NOT NULL THEN -- a named fixed field, see config.marc21_ff_pos_map.fixed_field
-                    attr_value := biblio.marc21_extract_fixed_field(NEW.id, attr_def.fixed_field);
-
-                ELSIF attr_def.xpath IS NOT NULL THEN -- and xpath expression
-
-                    SELECT INTO xfrm * FROM config.xml_transform WHERE name = attr_def.format;
-            
-                    -- See if we can skip the XSLT ... it's expensive
-                    IF prev_xfrm IS NULL OR prev_xfrm <> xfrm.name THEN
-                        -- Can't skip the transform
-                        IF xfrm.xslt <> '---' THEN
-                            transformed_xml := oils_xslt_process(NEW.marc,xfrm.xslt);
-                        ELSE
-                            transformed_xml := NEW.marc;
-                        END IF;
-            
-                        prev_xfrm := xfrm.name;
-                    END IF;
-
-                    IF xfrm.name IS NULL THEN
-                        -- just grab the marcxml (empty) transform
-                        SELECT INTO xfrm * FROM config.xml_transform WHERE xslt = '---' LIMIT 1;
-                        prev_xfrm := xfrm.name;
-                    END IF;
-
-                    attr_value := oils_xpath_string(attr_def.xpath, transformed_xml, COALESCE(attr_def.joiner,' '), ARRAY[ARRAY[xfrm.prefix, xfrm.namespace_uri]]);
-
-                ELSIF attr_def.phys_char_sf IS NOT NULL THEN -- a named Physical Characteristic, see config.marc21_physical_characteristic_*_map
-                    SELECT  m.value INTO attr_value
-                      FROM  biblio.marc21_physical_characteristics(NEW.id) v
-                            JOIN config.marc21_physical_characteristic_value_map m ON (m.id = v.value)
-                      WHERE v.subfield = attr_def.phys_char_sf
-                      LIMIT 1; -- Just in case ...
-
-                END IF;
-
-                -- apply index normalizers to attr_value
-                FOR normalizer IN
-                    SELECT  n.func AS func,
-                            n.param_count AS param_count,
-                            m.params AS params
-                      FROM  config.index_normalizer n
-                            JOIN config.record_attr_index_norm_map m ON (m.norm = n.id)
-                      WHERE attr = attr_def.name
-                      ORDER BY m.pos LOOP
-                        EXECUTE 'SELECT ' || normalizer.func || '(' ||
-                            COALESCE( quote_literal( attr_value ), 'NULL' ) ||
-                            CASE
-                                WHEN normalizer.param_count > 0
-                                    THEN ',' || REPLACE(REPLACE(BTRIM(normalizer.params,'[]'),E'\'',E'\\\''),E'"',E'\'')
-                                    ELSE ''
-                                END ||
-                            ')' INTO attr_value;
-        
-                END LOOP;
-
-                -- Add the new value to the hstore
-                new_attrs := new_attrs || hstore( attr_def.name, attr_value );
-
-            END LOOP;
-
-            IF TG_OP = 'INSERT' OR OLD.deleted THEN -- initial insert OR revivication
-                DELETE FROM metabib.record_attr WHERE id = NEW.id;
-                INSERT INTO metabib.record_attr (id, attrs) VALUES (NEW.id, new_attrs);
-            ELSE
-                UPDATE metabib.record_attr SET attrs = new_attrs WHERE id = NEW.id;
-            END IF;
-
+            PERFORM metabib.reingest_record_attributes(NEW.id, NULL, NEW.marc, TG_OP = 'INSERT' OR OLD.deleted);
         END IF;
     END IF;
 
diff --git a/Open-ILS/src/sql/Pg/950.data.seed-values.sql b/Open-ILS/src/sql/Pg/950.data.seed-values.sql
index 2e3d1af..32fd3ad 100644
--- a/Open-ILS/src/sql/Pg/950.data.seed-values.sql
+++ b/Open-ILS/src/sql/Pg/950.data.seed-values.sql
@@ -6059,17 +6059,17 @@ INSERT INTO config.marc21_ff_pos_map (fixed_field, tag, rec_type,start_pos, leng
 -- record attributes
 INSERT INTO config.record_attr_definition (name,label,fixed_field) values ('alph','Alph','Alph');
 INSERT INTO config.record_attr_definition (name,label,fixed_field) values ('audience','Audn','Audn');
-INSERT INTO config.record_attr_definition (name,label,fixed_field) values ('bib_level','BLvl','BLvl');
+INSERT INTO config.record_attr_definition (name,label,fixed_field,multi) values ('bib_level','BLvl','BLvl',FALSE);
 INSERT INTO config.record_attr_definition (name,label,fixed_field) values ('biog','Biog','Biog');
 INSERT INTO config.record_attr_definition (name,label,fixed_field) values ('conf','Conf','Conf');
-INSERT INTO config.record_attr_definition (name,label,fixed_field) values ('control_type','Ctrl','Ctrl');
+INSERT INTO config.record_attr_definition (name,label,fixed_field,multi) values ('control_type','Ctrl','Ctrl',FALSE);
 INSERT INTO config.record_attr_definition (name,label,fixed_field) values ('ctry','Ctry','Ctry');
 INSERT INTO config.record_attr_definition (name,label,fixed_field) values ('date1','Date1','Date1');
-INSERT INTO config.record_attr_definition (name,label,fixed_field,sorter,filter) values ('pubdate','Pub Date','Date1',TRUE,FALSE);
+INSERT INTO config.record_attr_definition (name,label,fixed_field,sorter,filter,multi) values ('pubdate','Pub Date','Date1',TRUE,FALSE,FALSE);
 INSERT INTO config.record_attr_definition (name,label,fixed_field) values ('date2','Date2','Date2');
-INSERT INTO config.record_attr_definition (name,label,fixed_field) values ('cat_form','Desc','Desc');
+INSERT INTO config.record_attr_definition (name,label,fixed_field,multi) values ('cat_form','Desc','Desc',FALSE);
 INSERT INTO config.record_attr_definition (name,label,fixed_field) values ('pub_status','DtSt','DtSt');
-INSERT INTO config.record_attr_definition (name,label,fixed_field) values ('enc_level','ELvl','ELvl');
+INSERT INTO config.record_attr_definition (name,label,fixed_field,multi) values ('enc_level','ELvl','ELvl',FALSE);
 INSERT INTO config.record_attr_definition (name,label,fixed_field) values ('fest','Fest','Fest');
 INSERT INTO config.record_attr_definition (name,label,fixed_field) values ('item_form','Form','Form');
 INSERT INTO config.record_attr_definition (name,label,fixed_field) values ('gpub','GPub','GPub');
@@ -6081,10 +6081,10 @@ INSERT INTO config.record_attr_definition (name,label,fixed_field) values ('lit_
 INSERT INTO config.record_attr_definition (name,label,fixed_field) values ('mrec','MRec','MRec');
 INSERT INTO config.record_attr_definition (name,label,fixed_field) values ('ff_sl','S/L','S/L');
 INSERT INTO config.record_attr_definition (name,label,fixed_field) values ('type_mat','TMat','TMat');
-INSERT INTO config.record_attr_definition (name,label,fixed_field) values ('item_type','Type','Type');
+INSERT INTO config.record_attr_definition (name,label,fixed_field,multi) values ('item_type','Type','Type',FALSE);
 INSERT INTO config.record_attr_definition (name,label,phys_char_sf) values ('vr_format','Videorecording format',72);
-INSERT INTO config.record_attr_definition (name,label,sorter,filter,tag) values ('titlesort','Title',TRUE,FALSE,'tnf');
-INSERT INTO config.record_attr_definition (name,label,sorter,filter,tag,sf_list) values ('authorsort','Author',TRUE,FALSE,'1%','abcdefgklmnopqrstvxyz');
+INSERT INTO config.record_attr_definition (name,label,sorter,filter,tag,multi) values ('titlesort','Title',TRUE,FALSE,'tnf',FALSE);
+INSERT INTO config.record_attr_definition (name,label,sorter,filter,tag,sf_list,multi) values ('authorsort','Author',TRUE,FALSE,'1%','abcdefgklmnopqrstvxyz',FALSE);
 
 -- TO-DO: Auto-generate these values from CLDR
 -- XXX These are the values used in MARC records ... does that match CLDR, including deprecated languages?
@@ -6659,6 +6659,94 @@ INSERT INTO config.coded_value_map(id, ctype, code, value) VALUES
     (555, 'vr_format', 'z', oils_i18n_gettext('555', 'Other', 'ccvm', 'value')),
     (556, 'vr_format', ' ', oils_i18n_gettext('556', 'Unspecified', 'ccvm', 'value'));
 
+INSERT INTO config.record_attr_definition (name, label, phys_char_sf)
+VALUES (
+    'sr_format',
+    oils_i18n_gettext('sr_format', 'Sound recording format', 'crad', 'label'),
+    '62'
+);
+
+INSERT INTO config.coded_value_map (id, ctype, code, value) VALUES
+(557, 'sr_format', 'a', oils_i18n_gettext(557, '16 rpm', 'ccvm', 'value')),
+(558, 'sr_format', 'b', oils_i18n_gettext(558, '33 1/3 rpm', 'ccvm', 'value')),
+(559, 'sr_format', 'c', oils_i18n_gettext(559, '45 rpm', 'ccvm', 'value')),
+(560, 'sr_format', 'f', oils_i18n_gettext(560, '1.4 m. per second', 'ccvm', 'value')),
+(561, 'sr_format', 'd', oils_i18n_gettext(561, '78 rpm', 'ccvm', 'value')),
+(562, 'sr_format', 'e', oils_i18n_gettext(562, '8 rpm', 'ccvm', 'value')),
+(563, 'sr_format', 'l', oils_i18n_gettext(563, '1 7/8 ips', 'ccvm', 'value')),
+(586, 'item_form', 'o', oils_i18n_gettext('586', 'Online', 'ccvm', 'value')),
+(587, 'item_form', 'q', oils_i18n_gettext('587', 'Direct electronic', 'ccvm', 'value'));
+
+INSERT INTO config.coded_value_map
+    (id, ctype, code, value, search_label) VALUES
+(564, 'icon_format', 'book',
+    oils_i18n_gettext(564, 'Book', 'ccvm', 'value'),
+    oils_i18n_gettext(564, 'Book', 'ccvm', 'search_label')),
+(565, 'icon_format', 'braille',
+    oils_i18n_gettext(565, 'Braille', 'ccvm', 'value'),
+    oils_i18n_gettext(565, 'Braille', 'ccvm', 'search_label')),
+(566, 'icon_format', 'software',
+    oils_i18n_gettext(566, 'Software and video games', 'ccvm', 'value'),
+    oils_i18n_gettext(566, 'Software and video games', 'ccvm', 'search_label')),
+(567, 'icon_format', 'dvd',
+    oils_i18n_gettext(567, 'DVD', 'ccvm', 'value'),
+    oils_i18n_gettext(567, 'DVD', 'ccvm', 'search_label')),
+(568, 'icon_format', 'ebook',
+    oils_i18n_gettext(568, 'E-book', 'ccvm', 'value'),
+    oils_i18n_gettext(568, 'E-book', 'ccvm', 'search_label')),
+(569, 'icon_format', 'eaudio',
+    oils_i18n_gettext(569, 'E-audio', 'ccvm', 'value'),
+    oils_i18n_gettext(569, 'E-audio', 'ccvm', 'search_label')),
+(570, 'icon_format', 'kit',
+    oils_i18n_gettext(570, 'Kit', 'ccvm', 'value'),
+    oils_i18n_gettext(570, 'Kit', 'ccvm', 'search_label')),
+(571, 'icon_format', 'map',
+    oils_i18n_gettext(571, 'Map', 'ccvm', 'value'),
+    oils_i18n_gettext(571, 'Map', 'ccvm', 'search_label')),
+(572, 'icon_format', 'microform',
+    oils_i18n_gettext(572, 'Microform', 'ccvm', 'value'),
+    oils_i18n_gettext(572, 'Microform', 'ccvm', 'search_label')),
+(573, 'icon_format', 'score',
+    oils_i18n_gettext(573, 'Music Score', 'ccvm', 'value'),
+    oils_i18n_gettext(573, 'Music Score', 'ccvm', 'search_label')),
+(574, 'icon_format', 'picture',
+    oils_i18n_gettext(574, 'Picture', 'ccvm', 'value'),
+    oils_i18n_gettext(574, 'Picture', 'ccvm', 'search_label')),
+(575, 'icon_format', 'equip',
+    oils_i18n_gettext(575, 'Equipment, games, toys', 'ccvm', 'value'),
+    oils_i18n_gettext(575, 'Equipment, games, toys', 'ccvm', 'search_label')),
+(576, 'icon_format', 'serial',
+    oils_i18n_gettext(576, 'Serials and magazines', 'ccvm', 'value'),
+    oils_i18n_gettext(576, 'Serials and magazines', 'ccvm', 'search_label')),
+(577, 'icon_format', 'vhs',
+    oils_i18n_gettext(577, 'VHS', 'ccvm', 'value'),
+    oils_i18n_gettext(577, 'VHS', 'ccvm', 'search_label')),
+(578, 'icon_format', 'evideo',
+    oils_i18n_gettext(578, 'E-video', 'ccvm', 'value'),
+    oils_i18n_gettext(578, 'E-video', 'ccvm', 'search_label')),
+(579, 'icon_format', 'cdaudiobook',
+    oils_i18n_gettext(579, 'CD Audiobook', 'ccvm', 'value'),
+    oils_i18n_gettext(579, 'CD Audiobook', 'ccvm', 'search_label')),
+(580, 'icon_format', 'cdmusic',
+    oils_i18n_gettext(580, 'CD Music recording', 'ccvm', 'value'),
+    oils_i18n_gettext(580, 'CD Music recording', 'ccvm', 'search_label')),
+(581, 'icon_format', 'casaudiobook',
+    oils_i18n_gettext(581, 'Cassette audiobook', 'ccvm', 'value'),
+    oils_i18n_gettext(581, 'Cassette audiobook', 'ccvm', 'search_label')),
+(582, 'icon_format', 'casmusic',
+    oils_i18n_gettext(582, 'Audiocassette music recording', 'ccvm', 'value'),
+    oils_i18n_gettext(582, 'Audiocassette music recording', 'ccvm', 'search_label')),
+(583, 'icon_format', 'phonospoken',
+    oils_i18n_gettext(583, 'Phonograph spoken recording', 'ccvm', 'value'),
+    oils_i18n_gettext(583, 'Phonograph spoken recording', 'ccvm', 'search_label')),
+(584, 'icon_format', 'phonomusic',
+    oils_i18n_gettext(584, 'Phonograph music recording', 'ccvm', 'value'),
+    oils_i18n_gettext(584, 'Phonograph music recording', 'ccvm', 'search_label')),
+(585, 'icon_format', 'lpbook',
+    oils_i18n_gettext(585, 'Large Print Book', 'ccvm', 'value'),
+    oils_i18n_gettext(585, 'Large Print Book', 'ccvm', 'search_label'))
+;
+
 SELECT SETVAL('config.coded_value_map_id_seq'::TEXT, (SELECT max(id) FROM config.coded_value_map));
 
 -- Trigger Event Definitions -------------------------------------------------
@@ -9422,6 +9510,99 @@ VALUES (
     )
 );
 
+INSERT INTO config.global_flag (name, label, value, enabled) VALUES (
+    'opac.icon_attr',
+    oils_i18n_gettext(
+        'opac.icon_attr',
+        'OPAC Format Icons Attribute',
+        'cgf',
+        'label'
+    ),
+    'icon_format',
+    TRUE
+);
+
+INSERT INTO config.record_attr_definition
+    (name, label, multi, filter, composite) VALUES (
+    'icon_format',
+    oils_i18n_gettext(
+        'icon_format',
+        'OPAC Format Icons',
+        'crad',
+        'label'
+    ),
+    TRUE, TRUE, TRUE
+);
+
+INSERT INTO config.composite_attr_entry_definition
+    (coded_value, definition) VALUES
+--book
+(564, '{"0":[{"_attr":"item_type","_val":"a"},{"_attr":"item_type","_val":"t"}],"1":{"_not":[{"_attr":"item_form","_val":"a"},{"_attr":"item_form","_val":"b"},{"_attr":"item_form","_val":"c"},{"_attr":"item_form","_val":"d"},{"_attr":"item_form","_val":"f"},{"_attr":"item_form","_val":"o"},{"_attr":"item_form","_val":"q"},{"_attr":"item_form","_val":"r"},{"_attr":"item_form","_val":"s"}]},"2":[{"_attr":"bib_level","_val":"a"},{"_attr":"bib_level","_val":"c"},{"_attr":"bib_level","_val":"d"},{"_attr":"bib_level","_val":"m"}]}'),
+
+-- braille
+(565, '{"0":{"_attr":"item_type","_val":"a"},"1":{"_attr":"item_form","_val":"f"}}'),
+
+-- software
+(566, '{"_attr":"item_type","_val":"m"}'),
+
+-- dvd
+(567, '{"_attr":"vr_format","_val":"v"}'),
+
+-- ebook
+(568, '{"0":[{"_attr":"item_type","_val":"a"},{"_attr":"item_type","_val":"t"}],"1":[{"_attr":"item_form","_val":"o"},{"_attr":"item_form","_val":"s"},{"_attr":"item_form","_val":"q"}],"2":[{"_attr":"bib_level","_val":"a"},{"_attr":"bib_level","_val":"c"},{"_attr":"bib_level","_val":"d"},{"_attr":"bib_level","_val":"m"}]}'),
+
+-- eaudio
+(569, '{"0":{"_attr":"item_type","_val":"i"},"1":[{"_attr":"item_form","_val":"o"},{"_attr":"item_form","_val":"q"},{"_attr":"item_form","_val":"s"}]}'),
+
+-- kit
+(570, '[{"_attr":"item_type","_val":"o"},{"_attr":"item_type","_val":"p"}]'),
+
+-- map
+(571, '[{"_attr":"item_type","_val":"e"},{"_attr":"item_type","_val":"f"}]'),
+
+-- microform
+(572, '[{"_attr":"item_form","_val":"a"},{"_attr":"item_form","_val":"b"},{"_attr":"item_form","_val":"c"}]'),
+
+-- score
+(573, '[{"_attr":"item_type","_val":"c"},{"_attr":"item_type","_val":"d"}]'),
+
+-- picture
+(574, '{"_attr":"item_type","_val":"k"}'),
+
+-- equip
+(575, '{"_attr":"item_type","_val":"r"}'),
+
+-- serial
+(576, '[{"_attr":"bib_level","_val":"b"},{"_attr":"bib_level","_val":"s"}]'),
+
+-- vhs
+(577, '{"_attr":"vr_format","_val":"b"}'),
+
+-- evideo
+(578, '{"0":{"_attr":"item_type","_val":"g"},"1":[{"_attr":"item_form","_val":"o"},{"_attr":"item_form","_val":"s"},{"_attr":"item_form","_val":"q"}]}'),
+
+-- cdaudiobook
+(579, '{"0":{"_attr":"item_type","_val":"i"},"1":{"_attr":"sr_format","_val":"f"}}'),
+
+-- cdmusic
+(580, '{"0":{"_attr":"item_type","_val":"j"},"1":{"_attr":"sr_format","_val":"f"}}'),
+
+-- casaudiobook
+(581, '{"0":{"_attr":"item_type","_val":"i"},"1":{"_attr":"sr_format","_val":"l"}}'),
+
+-- casmusic
+(582, '{"0":{"_attr":"item_type","_val":"j"},"1":{"_attr":"sr_format","_val":"l"}}'),
+
+-- phonospoken
+(583, '{"0":{"_attr":"item_type","_val":"i"},"1":[{"_attr":"sr_format","_val":"a"},{"_attr":"sr_format","_val":"b"},{"_attr":"sr_format","_val":"c"},{"_attr":"sr_format","_val":"d"},{"_attr":"sr_format","_val":"e"}]}'),
+
+-- phonomusic
+(584, '{"0":{"_attr":"item_type","_val":"j"},"1":[{"_attr":"sr_format","_val":"a"},{"_attr":"sr_format","_val":"b"},{"_attr":"sr_format","_val":"c"},{"_attr":"sr_format","_val":"d"},{"_attr":"sr_format","_val":"e"}]}'),
+
+-- lpbook
+(585, '{"0":[{"_attr":"item_type","_val":"a"},{"_attr":"item_type","_val":"t"}],"1":{"_attr":"item_form","_val":"d"},"2":[{"_attr":"bib_level","_val":"a"},{"_attr":"bib_level","_val":"c"},{"_attr":"bib_level","_val":"d"},{"_attr":"bib_level","_val":"m"}]}');
+
+
 INSERT INTO config.usr_setting_type (name,opac_visible,label,description,datatype)
     VALUES (
         'history.circ.retention_age',
diff --git a/Open-ILS/src/sql/Pg/create_database_extensions.sql b/Open-ILS/src/sql/Pg/create_database_extensions.sql
index e06ac59..b73a871 100644
--- a/Open-ILS/src/sql/Pg/create_database_extensions.sql
+++ b/Open-ILS/src/sql/Pg/create_database_extensions.sql
@@ -19,3 +19,4 @@ CREATE LANGUAGE plperlu;
 CREATE EXTENSION tablefunc;
 CREATE EXTENSION xml2;
 CREATE EXTENSION hstore;
+CREATE EXTENSION intarray;

-----------------------------------------------------------------------

Summary of changes:
 Open-ILS/examples/fm_IDL.xml                       |   93 ++-
 .../perlmods/lib/OpenILS/Application/Circ/Holds.pm |  193 ++++-
 .../Application/Storage/Driver/Pg/QueryParser.pm   |  120 +++-
 .../Application/Storage/Publisher/action.pm        |  117 ++--
 .../lib/OpenILS/WWW/EGCatLoader/Account.pm         |  139 +++-
 .../perlmods/lib/OpenILS/WWW/EGCatLoader/Search.pm |   67 ++-
 .../perlmods/lib/OpenILS/WWW/EGCatLoader/Util.pm   |   58 +-
 Open-ILS/src/perlmods/lib/OpenILS/WWW/EGWeb.pm     |    5 +-
 Open-ILS/src/sql/Pg/002.schema.config.sql          |    9 +-
 Open-ILS/src/sql/Pg/012.schema.vandelay.sql        |   40 +-
 Open-ILS/src/sql/Pg/030.schema.metabib.sql         |  520 +++++++++---
 Open-ILS/src/sql/Pg/040.schema.asset.sql           |   30 +-
 Open-ILS/src/sql/Pg/950.data.seed-values.sql       |  336 +++++++-
 Open-ILS/src/sql/Pg/990.schema.unapi.sql           |  408 ++++++++-
 Open-ILS/src/sql/Pg/create_database_extensions.sql |    1 +
 .../src/sql/Pg/upgrade/0864.MVF_CRA-upgrade.sql    |  900 ++++++++++++++++++++
 .../0865.schema.convert-MR-holdable_formats.sql    |   41 +
 .../src/sql/Pg/upgrade/0866.schema.unapi-mmr.sql   |  567 ++++++++++++
 .../sql/Pg/upgrade/0867.data.mmr-holds-formats.sql |  161 ++++
 .../conify/global/config/coded_value_map.tt2       |   47 +-
 .../config/composite_attr_entry_definition.tt2     |  100 +++
 .../global/config/record_attr_definition.tt2       |   25 +-
 Open-ILS/src/templates/opac/css/style.css.tt2      |    1 +
 Open-ILS/src/templates/opac/myopac/holds.tt2       |   19 +-
 Open-ILS/src/templates/opac/myopac/holds/edit.tt2  |    9 +
 .../src/templates/opac/parts/advanced/search.tt2   |   16 +-
 Open-ILS/src/templates/opac/parts/config.tt2       |    9 +-
 .../opac/parts/metarecord_hold_filters.tt2         |   91 ++
 Open-ILS/src/templates/opac/parts/misc_util.tt2    |   42 +-
 Open-ILS/src/templates/opac/parts/place_hold.tt2   |   11 +
 Open-ILS/src/templates/opac/parts/result/table.tt2 |   43 +-
 Open-ILS/src/templates/opac/results.tt2            |   32 +-
 .../{item_type/a.png => icon_format/book.png}      |  Bin 1538 -> 1538 bytes
 .../{item_type/a.png => icon_format/braille.png}   |  Bin 1538 -> 1538 bytes
 .../j.png => icon_format/casaudiobook.png}         |  Bin 1726 -> 1726 bytes
 .../{item_type/c.png => icon_format/casmusic.png}  |  Bin 1112 -> 1112 bytes
 .../j.png => icon_format/cdaudiobook.png}          |  Bin 1726 -> 1726 bytes
 .../{item_type/c.png => icon_format/cdmusic.png}   |  Bin 1112 -> 1112 bytes
 .../{item_type/g.png => icon_format/dvd.png}       |  Bin 1648 -> 1648 bytes
 .../{item_type/i.png => icon_format/eaudio.png}    |  Bin 1330 -> 1330 bytes
 .../{item_type/a.png => icon_format/ebook.png}     |  Bin 1538 -> 1538 bytes
 .../{item_type/r.png => icon_format/equip.png}     |  Bin 1392 -> 1392 bytes
 .../{item_type/g.png => icon_format/evideo.png}    |  Bin 1648 -> 1648 bytes
 .../{item_type/r.png => icon_format/kit.png}       |  Bin 1392 -> 1392 bytes
 .../{item_type/a.png => icon_format/lpbook.png}    |  Bin 1538 -> 1538 bytes
 .../{item_type/e.png => icon_format/map.png}       |  Bin 1324 -> 1324 bytes
 .../{item_type/a.png => icon_format/microform.png} |  Bin 1538 -> 1538 bytes
 .../{item_type/c.png => icon_format/music.png}     |  Bin 1112 -> 1112 bytes
 .../c.png => icon_format/phonomusic.png}           |  Bin 1112 -> 1112 bytes
 .../j.png => icon_format/phonospoken.png}          |  Bin 1726 -> 1726 bytes
 .../{item_type/k.png => icon_format/picture.png}   |  Bin 891 -> 891 bytes
 .../{item_type/o.png => icon_format/score.png}     |  Bin 1724 -> 1724 bytes
 .../{item_type/a.png => icon_format/serial.png}    |  Bin 1538 -> 1538 bytes
 .../{item_type/m.png => icon_format/software.png}  |  Bin 955 -> 955 bytes
 .../{item_type/g.png => icon_format/vhs.png}       |  Bin 1648 -> 1648 bytes
 .../config/composite_attr_entry_definition.js      |  385 +++++++++
 Open-ILS/web/js/ui/default/opac/simple.js          |    5 +-
 .../OPAC/Composite_Record_Attributes.txt           |   31 +
 .../OPAC/Multi_Valued_fields.txt                   |   45 +
 docs/RELEASE_NOTES_NEXT/OPAC/TPAC_metarecords.txt  |   38 +
 60 files changed, 4397 insertions(+), 357 deletions(-)
 create mode 100644 Open-ILS/src/sql/Pg/upgrade/0864.MVF_CRA-upgrade.sql
 create mode 100644 Open-ILS/src/sql/Pg/upgrade/0865.schema.convert-MR-holdable_formats.sql
 create mode 100644 Open-ILS/src/sql/Pg/upgrade/0866.schema.unapi-mmr.sql
 create mode 100644 Open-ILS/src/sql/Pg/upgrade/0867.data.mmr-holds-formats.sql
 create mode 100644 Open-ILS/src/templates/conify/global/config/composite_attr_entry_definition.tt2
 create mode 100644 Open-ILS/src/templates/opac/parts/metarecord_hold_filters.tt2
 copy Open-ILS/web/images/format_icons/{item_type/a.png => icon_format/book.png} (100%)
 copy Open-ILS/web/images/format_icons/{item_type/a.png => icon_format/braille.png} (100%)
 copy Open-ILS/web/images/format_icons/{item_type/j.png => icon_format/casaudiobook.png} (100%)
 copy Open-ILS/web/images/format_icons/{item_type/c.png => icon_format/casmusic.png} (100%)
 copy Open-ILS/web/images/format_icons/{item_type/j.png => icon_format/cdaudiobook.png} (100%)
 copy Open-ILS/web/images/format_icons/{item_type/c.png => icon_format/cdmusic.png} (100%)
 copy Open-ILS/web/images/format_icons/{item_type/g.png => icon_format/dvd.png} (100%)
 copy Open-ILS/web/images/format_icons/{item_type/i.png => icon_format/eaudio.png} (100%)
 copy Open-ILS/web/images/format_icons/{item_type/a.png => icon_format/ebook.png} (100%)
 copy Open-ILS/web/images/format_icons/{item_type/r.png => icon_format/equip.png} (100%)
 copy Open-ILS/web/images/format_icons/{item_type/g.png => icon_format/evideo.png} (100%)
 copy Open-ILS/web/images/format_icons/{item_type/r.png => icon_format/kit.png} (100%)
 copy Open-ILS/web/images/format_icons/{item_type/a.png => icon_format/lpbook.png} (100%)
 copy Open-ILS/web/images/format_icons/{item_type/e.png => icon_format/map.png} (100%)
 copy Open-ILS/web/images/format_icons/{item_type/a.png => icon_format/microform.png} (100%)
 copy Open-ILS/web/images/format_icons/{item_type/c.png => icon_format/music.png} (100%)
 copy Open-ILS/web/images/format_icons/{item_type/c.png => icon_format/phonomusic.png} (100%)
 copy Open-ILS/web/images/format_icons/{item_type/j.png => icon_format/phonospoken.png} (100%)
 copy Open-ILS/web/images/format_icons/{item_type/k.png => icon_format/picture.png} (100%)
 copy Open-ILS/web/images/format_icons/{item_type/o.png => icon_format/score.png} (100%)
 copy Open-ILS/web/images/format_icons/{item_type/a.png => icon_format/serial.png} (100%)
 copy Open-ILS/web/images/format_icons/{item_type/m.png => icon_format/software.png} (100%)
 copy Open-ILS/web/images/format_icons/{item_type/g.png => icon_format/vhs.png} (100%)
 create mode 100644 Open-ILS/web/js/ui/default/conify/global/config/composite_attr_entry_definition.js
 create mode 100644 docs/RELEASE_NOTES_NEXT/OPAC/Composite_Record_Attributes.txt
 create mode 100644 docs/RELEASE_NOTES_NEXT/OPAC/Multi_Valued_fields.txt
 create mode 100644 docs/RELEASE_NOTES_NEXT/OPAC/TPAC_metarecords.txt


hooks/post-receive
-- 
Evergreen ILS


More information about the open-ils-commits mailing list