[open-ils-commits] [GIT] Evergreen ILS branch master updated. 09740b3f81588c7aed11f9f67af37b0fa7102e38

Evergreen Git git at git.evergreen-ils.org
Tue Oct 23 16:33:00 EDT 2018


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  09740b3f81588c7aed11f9f67af37b0fa7102e38 (commit)
      from  f6d79995818756309ae6138b4b5e887150ca6df8 (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 09740b3f81588c7aed11f9f67af37b0fa7102e38
Author: Jason Stephenson <jason at sigio.com>
Date:   Tue Oct 23 16:32:17 2018 -0400

    Forward port 3.1.5-3.2.0-upgrade-db.sql.
    
    Signed-off-by: Jason Stephenson <jason at sigio.com>

diff --git a/Open-ILS/src/sql/Pg/version-upgrade/3.1.5-3.2.0-upgrade-db.sql b/Open-ILS/src/sql/Pg/version-upgrade/3.1.5-3.2.0-upgrade-db.sql
new file mode 100644
index 0000000..bc276b0
--- /dev/null
+++ b/Open-ILS/src/sql/Pg/version-upgrade/3.1.5-3.2.0-upgrade-db.sql
@@ -0,0 +1,4904 @@
+--Upgrade Script for 3.1.5 to 3.2.0
+\set eg_version '''3.2.0'''
+BEGIN;
+INSERT INTO config.upgrade_log (version, applied_to) VALUES ('3.2.0', :eg_version);
+
+SELECT evergreen.upgrade_deps_block_check('1115', :eg_version);
+
+INSERT INTO permission.perm_list (id,code,description) VALUES ( 607, 'EMERGENCY_CLOSING', 'Create and manage Emergency Closings');
+
+INSERT INTO action_trigger.hook (key,core_type,description) VALUES ('checkout.due.emergency_closing','aecc','Circulation due date was adjusted by the Emergency Closing handler');
+INSERT INTO action_trigger.hook (key,core_type,description) VALUES ('hold.shelf_expire.emergency_closing','aech','Hold shelf expire time was adjusted by the Emergency Closing handler');
+INSERT INTO action_trigger.hook (key,core_type,description) VALUES ('booking.due.emergency_closing','aecr','Booking reservation return date was adjusted by the Emergency Closing handler');
+
+CREATE TABLE action.emergency_closing (
+    id                  SERIAL      PRIMARY KEY,
+    creator             INT         NOT NULL REFERENCES actor.usr (id) ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED,
+    create_time         TIMESTAMPTZ NOT NULL DEFAULT NOW(),
+    process_start_time  TIMESTAMPTZ,
+    process_end_time    TIMESTAMPTZ,
+    last_update_time    TIMESTAMPTZ
+);
+
+ALTER TABLE actor.org_unit_closed
+    ADD COLUMN emergency_closing INT
+        REFERENCES action.emergency_closing (id) ON DELETE RESTRICT DEFERRABLE INITIALLY DEFERRED;
+
+CREATE TABLE action.emergency_closing_circulation (
+    id                  BIGSERIAL   PRIMARY KEY,
+    emergency_closing   INT         NOT NULL REFERENCES action.emergency_closing (id) ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED,
+    circulation         INT         NOT NULL REFERENCES action.circulation (id) ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED,
+    original_due_date   TIMESTAMPTZ,
+    process_time        TIMESTAMPTZ
+);
+CREATE INDEX emergency_closing_circulation_emergency_closing_idx ON action.emergency_closing_circulation (emergency_closing);
+CREATE INDEX emergency_closing_circulation_circulation_idx ON action.emergency_closing_circulation (circulation);
+
+CREATE TABLE action.emergency_closing_reservation (
+    id                  BIGSERIAL   PRIMARY KEY,
+    emergency_closing   INT         NOT NULL REFERENCES action.emergency_closing (id) ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED,
+    reservation         INT         NOT NULL REFERENCES booking.reservation (id) ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED,
+    original_end_time   TIMESTAMPTZ,
+    process_time        TIMESTAMPTZ
+);
+CREATE INDEX emergency_closing_reservation_emergency_closing_idx ON action.emergency_closing_reservation (emergency_closing);
+CREATE INDEX emergency_closing_reservation_reservation_idx ON action.emergency_closing_reservation (reservation);
+
+CREATE TABLE action.emergency_closing_hold (
+    id                  BIGSERIAL   PRIMARY KEY,
+    emergency_closing   INT         NOT NULL REFERENCES action.emergency_closing (id) ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED,
+    hold                INT         NOT NULL REFERENCES action.hold_request (id) ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED,
+    original_shelf_expire_time   TIMESTAMPTZ,
+    process_time        TIMESTAMPTZ
+);
+CREATE INDEX emergency_closing_hold_emergency_closing_idx ON action.emergency_closing_hold (emergency_closing);
+CREATE INDEX emergency_closing_hold_hold_idx ON action.emergency_closing_hold (hold);
+
+CREATE OR REPLACE VIEW action.emergency_closing_status AS
+    SELECT  e.*,
+            COALESCE(c.count, 0) AS circulations,
+            COALESCE(c.completed, 0) AS circulations_complete,
+            COALESCE(b.count, 0) AS reservations,
+            COALESCE(b.completed, 0) AS reservations_complete,
+            COALESCE(h.count, 0) AS holds,
+            COALESCE(h.completed, 0) AS holds_complete
+      FROM  action.emergency_closing e
+            LEFT JOIN (SELECT emergency_closing, count(*) count, SUM((process_time IS NOT NULL)::INT) completed FROM action.emergency_closing_circulation GROUP BY 1) c ON (c.emergency_closing = e.id)
+            LEFT JOIN (SELECT emergency_closing, count(*) count, SUM((process_time IS NOT NULL)::INT) completed FROM action.emergency_closing_reservation GROUP BY 1) b ON (b.emergency_closing = e.id)
+            LEFT JOIN (SELECT emergency_closing, count(*) count, SUM((process_time IS NOT NULL)::INT) completed FROM action.emergency_closing_hold GROUP BY 1) h ON (h.emergency_closing = e.id)
+;
+
+CREATE OR REPLACE FUNCTION evergreen.find_next_open_time ( circ_lib INT, initial TIMESTAMPTZ, hourly BOOL DEFAULT FALSE, initial_time TIME DEFAULT NULL, dow_count INT DEFAULT 0 )
+    RETURNS TIMESTAMPTZ AS $$
+DECLARE
+    day_number      INT;
+    plus_days       INT;
+    final_time      TEXT;
+    time_adjusted   BOOL;
+    hoo_open        TIME WITHOUT TIME ZONE;
+    hoo_close       TIME WITHOUT TIME ZONE;
+    adjacent        actor.org_unit_closed%ROWTYPE;
+    breakout        INT := 0;
+BEGIN
+
+    IF dow_count > 6 THEN
+        RETURN initial;
+    END IF;
+
+    IF initial_time IS NULL THEN
+        initial_time := initial::TIME;
+    END IF;
+
+    final_time := (initial + '1 second'::INTERVAL)::TEXT;
+    LOOP
+        breakout := breakout + 1;
+
+        time_adjusted := FALSE;
+
+        IF dow_count > 0 THEN -- we're recursing, so check for HOO closing
+            day_number := EXTRACT(ISODOW FROM final_time::TIMESTAMPTZ) - 1;
+            plus_days := 0;
+            FOR i IN 1..7 LOOP
+                EXECUTE 'SELECT dow_' || day_number || '_open, dow_' || day_number || '_close FROM actor.hours_of_operation WHERE id = $1'
+                    INTO hoo_open, hoo_close
+                    USING circ_lib;
+
+                -- RAISE NOTICE 'initial time: %; dow: %; close: %',initial_time,day_number,hoo_close;
+
+                IF hoo_close = '00:00:00' THEN -- bah ... I guess we'll check the next day
+                    day_number := (day_number + 1) % 7;
+                    plus_days := plus_days + 1;
+                    time_adjusted := TRUE;
+                    CONTINUE;
+                END IF;
+
+                IF hoo_close IS NULL THEN -- no hours of operation ... assume no closing?
+                    hoo_close := '23:59:59';
+                END IF;
+
+                EXIT;
+            END LOOP;
+
+            final_time := DATE(final_time::TIMESTAMPTZ + (plus_days || ' days')::INTERVAL)::TEXT;
+            IF hoo_close <> '00:00:00' AND hourly THEN -- Not a day-granular circ
+                final_time := final_time||' '|| hoo_close;
+            ELSE
+                final_time := final_time||' 23:59:59';
+            END IF;
+        END IF;
+
+        -- Loop through other closings
+        LOOP 
+            SELECT * INTO adjacent FROM actor.org_unit_closed WHERE org_unit = circ_lib AND final_time::TIMESTAMPTZ between close_start AND close_end;
+            EXIT WHEN adjacent.id IS NULL;
+            time_adjusted := TRUE;
+            -- RAISE NOTICE 'recursing for closings with final_time: %',final_time;
+            final_time := evergreen.find_next_open_time(circ_lib, adjacent.close_end::TIMESTAMPTZ, hourly, initial_time, dow_count + 1)::TEXT;
+        END LOOP;
+
+        EXIT WHEN breakout > 100;
+        EXIT WHEN NOT time_adjusted;
+
+    END LOOP;
+
+    RETURN final_time;
+END;
+$$ LANGUAGE PLPGSQL;
+
+CREATE TYPE action.emergency_closing_stage_1_count AS (circulations INT, reservations INT, holds INT);
+CREATE OR REPLACE FUNCTION action.emergency_closing_stage_1 ( e_closing INT )
+    RETURNS SETOF action.emergency_closing_stage_1_count AS $$
+DECLARE
+    tmp     INT;
+    touched action.emergency_closing_stage_1_count%ROWTYPE;
+BEGIN
+    -- First, gather circs
+    INSERT INTO action.emergency_closing_circulation (emergency_closing, circulation)
+        SELECT  e_closing,
+                circ.id
+          FROM  actor.org_unit_closed closing
+                JOIN action.emergency_closing ec ON (closing.emergency_closing = ec.id AND ec.id = e_closing)
+                JOIN action.circulation circ ON (
+                    circ.circ_lib = closing.org_unit
+                    AND circ.due_date BETWEEN closing.close_start AND (closing.close_end + '1s'::INTERVAL)
+                    AND circ.xact_finish IS NULL
+                )
+          WHERE NOT EXISTS (SELECT 1 FROM action.emergency_closing_circulation t WHERE t.emergency_closing = e_closing AND t.circulation = circ.id);
+
+    GET DIAGNOSTICS tmp = ROW_COUNT;
+    touched.circulations := tmp;
+
+    INSERT INTO action.emergency_closing_reservation (emergency_closing, reservation)
+        SELECT  e_closing,
+                res.id
+          FROM  actor.org_unit_closed closing
+                JOIN action.emergency_closing ec ON (closing.emergency_closing = ec.id AND ec.id = e_closing)
+                JOIN booking.reservation res ON (
+                    res.pickup_lib = closing.org_unit
+                    AND res.end_time BETWEEN closing.close_start AND (closing.close_end + '1s'::INTERVAL)
+                )
+          WHERE NOT EXISTS (SELECT 1 FROM action.emergency_closing_reservation t WHERE t.emergency_closing = e_closing AND t.reservation = res.id);
+
+    GET DIAGNOSTICS tmp = ROW_COUNT;
+    touched.reservations := tmp;
+
+    INSERT INTO action.emergency_closing_hold (emergency_closing, hold)
+        SELECT  e_closing,
+                hold.id
+          FROM  actor.org_unit_closed closing
+                JOIN action.emergency_closing ec ON (closing.emergency_closing = ec.id AND ec.id = e_closing)
+                JOIN action.hold_request hold ON (
+                    pickup_lib = closing.org_unit
+                    AND hold.shelf_expire_time BETWEEN closing.close_start AND (closing.close_end + '1s'::INTERVAL)
+                    AND hold.fulfillment_time IS NULL
+                    AND hold.cancel_time IS NULL
+                )
+          WHERE NOT EXISTS (SELECT 1 FROM action.emergency_closing_hold t WHERE t.emergency_closing = e_closing AND t.hold = hold.id);
+
+    GET DIAGNOSTICS tmp = ROW_COUNT;
+    touched.holds := tmp;
+
+    UPDATE  action.emergency_closing
+      SET   process_start_time = NOW(),
+            last_update_time = NOW()
+      WHERE id = e_closing;
+
+    RETURN NEXT touched;
+END;
+$$ LANGUAGE PLPGSQL;
+
+CREATE OR REPLACE FUNCTION action.emergency_closing_stage_2_hold ( hold_closing_entry INT )
+    RETURNS BOOL AS $$
+DECLARE
+    hold        action.hold_request%ROWTYPE;
+    e_closing   action.emergency_closing%ROWTYPE;
+    e_c_hold    action.emergency_closing_hold%ROWTYPE;
+    closing     actor.org_unit_closed%ROWTYPE;
+    day_number  INT;
+    hoo_close   TIME WITHOUT TIME ZONE;
+    plus_days   INT;
+BEGIN
+    -- Gather objects involved
+    SELECT  * INTO e_c_hold
+      FROM  action.emergency_closing_hold
+      WHERE id = hold_closing_entry;
+
+    IF e_c_hold.process_time IS NOT NULL THEN
+        -- Already processed ... moving on
+        RETURN FALSE;
+    END IF;
+
+    SELECT  * INTO e_closing
+      FROM  action.emergency_closing
+      WHERE id = e_c_hold.emergency_closing;
+
+    IF e_closing.process_start_time IS NULL THEN
+        -- Huh... that's odd. And wrong.
+        RETURN FALSE;
+    END IF;
+
+    SELECT  * INTO closing
+      FROM  actor.org_unit_closed
+      WHERE emergency_closing = e_closing.id;
+
+    SELECT  * INTO hold
+      FROM  action.hold_request h
+      WHERE id = e_c_hold.hold;
+
+    -- Record the processing
+    UPDATE  action.emergency_closing_hold
+      SET   original_shelf_expire_time = hold.shelf_expire_time,
+            process_time = NOW()
+      WHERE id = hold_closing_entry;
+
+    UPDATE  action.emergency_closing
+      SET   last_update_time = NOW()
+      WHERE id = e_closing.id;
+
+    UPDATE  action.hold_request
+      SET   shelf_expire_time = evergreen.find_next_open_time(closing.org_unit, hold.shelf_expire_time, TRUE)
+      WHERE id = hold.id;
+
+    RETURN TRUE;
+END;
+$$ LANGUAGE PLPGSQL;
+
+CREATE OR REPLACE FUNCTION action.emergency_closing_stage_2_circ ( circ_closing_entry INT )
+    RETURNS BOOL AS $$
+DECLARE
+    circ            action.circulation%ROWTYPE;
+    e_closing       action.emergency_closing%ROWTYPE;
+    e_c_circ        action.emergency_closing_circulation%ROWTYPE;
+    closing         actor.org_unit_closed%ROWTYPE;
+    adjacent        actor.org_unit_closed%ROWTYPE;
+    bill            money.billing%ROWTYPE;
+    last_bill       money.billing%ROWTYPE;
+    day_number      INT;
+    hoo_close       TIME WITHOUT TIME ZONE;
+    plus_days       INT;
+    avoid_negative  BOOL;
+    extend_grace    BOOL;
+    new_due_date    TEXT;
+BEGIN
+    -- Gather objects involved
+    SELECT  * INTO e_c_circ
+      FROM  action.emergency_closing_circulation
+      WHERE id = circ_closing_entry;
+
+    IF e_c_circ.process_time IS NOT NULL THEN
+        -- Already processed ... moving on
+        RETURN FALSE;
+    END IF;
+
+    SELECT  * INTO e_closing
+      FROM  action.emergency_closing
+      WHERE id = e_c_circ.emergency_closing;
+
+    IF e_closing.process_start_time IS NULL THEN
+        -- Huh... that's odd. And wrong.
+        RETURN FALSE;
+    END IF;
+
+    SELECT  * INTO closing
+      FROM  actor.org_unit_closed
+      WHERE emergency_closing = e_closing.id;
+
+    SELECT  * INTO circ
+      FROM  action.circulation
+      WHERE id = e_c_circ.circulation;
+
+    -- Record the processing
+    UPDATE  action.emergency_closing_circulation
+      SET   original_due_date = circ.due_date,
+            process_time = NOW()
+      WHERE id = circ_closing_entry;
+
+    UPDATE  action.emergency_closing
+      SET   last_update_time = NOW()
+      WHERE id = e_closing.id;
+
+    SELECT value::BOOL INTO avoid_negative FROM actor.org_unit_ancestor_setting('bill.prohibit_negative_balance_on_overdues', circ.circ_lib);
+    SELECT value::BOOL INTO extend_grace FROM actor.org_unit_ancestor_setting('circ.grace.extend', circ.circ_lib);
+
+    new_due_date := evergreen.find_next_open_time( closing.org_unit, circ.due_date, EXTRACT(EPOCH FROM circ.duration)::INT % 86400 > 0 )::TEXT;
+    UPDATE action.circulation SET due_date = new_due_date::TIMESTAMPTZ WHERE id = circ.id;
+
+    -- Now, see if we need to get rid of some fines
+    SELECT  * INTO last_bill
+      FROM  money.billing b
+      WHERE b.xact = circ.id
+            AND NOT b.voided
+            AND b.btype = 1
+      ORDER BY billing_ts DESC
+      LIMIT 1;
+
+    FOR bill IN
+        SELECT  *
+          FROM  money.billing b
+          WHERE b.xact = circ.id
+                AND b.btype = 1
+                AND NOT b.voided
+                AND (
+                    b.billing_ts BETWEEN closing.close_start AND new_due_date::TIMESTAMPTZ
+                    OR (extend_grace AND last_bill.billing_ts <= new_due_date::TIMESTAMPTZ + circ.grace_period)
+                )
+                AND NOT EXISTS (SELECT 1 FROM money.account_adjustment a WHERE a.billing = b.id)
+          ORDER BY billing_ts
+    LOOP
+        IF avoid_negative THEN
+            PERFORM FROM money.materialized_billable_xact_summary WHERE id = circ.id AND balanced_owd < bill.amount;
+            EXIT WHEN FOUND; -- We can't go negative, and voiding this bill would do that...
+        END IF;
+
+        UPDATE  money.billing
+          SET   voided = TRUE,
+                void_time = NOW(),
+                note = COALESCE(note,'') || ' :: Voided by emergency closing handler'
+          WHERE id = bill.id;
+    END LOOP;
+    
+    RETURN TRUE;
+END;
+$$ LANGUAGE PLPGSQL;
+
+CREATE OR REPLACE FUNCTION action.emergency_closing_stage_2_reservation ( res_closing_entry INT )
+    RETURNS BOOL AS $$
+DECLARE
+    res             booking.reservation%ROWTYPE;
+    e_closing       action.emergency_closing%ROWTYPE;
+    e_c_res         action.emergency_closing_reservation%ROWTYPE;
+    closing         actor.org_unit_closed%ROWTYPE;
+    adjacent        actor.org_unit_closed%ROWTYPE;
+    bill            money.billing%ROWTYPE;
+    day_number      INT;
+    hoo_close       TIME WITHOUT TIME ZONE;
+    plus_days       INT;
+    avoid_negative  BOOL;
+    new_due_date    TEXT;
+BEGIN
+    -- Gather objects involved
+    SELECT  * INTO e_c_res
+      FROM  action.emergency_closing_reservation
+      WHERE id = res_closing_entry;
+
+    IF e_c_res.process_time IS NOT NULL THEN
+        -- Already processed ... moving on
+        RETURN FALSE;
+    END IF;
+
+    SELECT  * INTO e_closing
+      FROM  action.emergency_closing
+      WHERE id = e_c_res.emergency_closing;
+
+    IF e_closing.process_start_time IS NULL THEN
+        -- Huh... that's odd. And wrong.
+        RETURN FALSE;
+    END IF;
+
+    SELECT  * INTO closing
+      FROM  actor.org_unit_closed
+      WHERE emergency_closing = e_closing.id;
+
+    SELECT  * INTO res
+      FROM  booking.reservation
+      WHERE id = e_c_res.reservation;
+
+    IF res.pickup_lib IS NULL THEN -- Need to be far enough along to have a pickup lib
+        RETURN FALSE;
+    END IF;
+
+    -- Record the processing
+    UPDATE  action.emergency_closing_reservation
+      SET   original_end_time = res.end_time,
+            process_time = NOW()
+      WHERE id = res_closing_entry;
+
+    UPDATE  action.emergency_closing
+      SET   last_update_time = NOW()
+      WHERE id = e_closing.id;
+
+    SELECT value::BOOL INTO avoid_negative FROM actor.org_unit_ancestor_setting('bill.prohibit_negative_balance_on_overdues', res.pickup_lib);
+
+    new_due_date := evergreen.find_next_open_time( closing.org_unit, res.end_time, EXTRACT(EPOCH FROM res.booking_interval)::INT % 86400 > 0 )::TEXT;
+    UPDATE booking.reservation SET end_time = new_due_date::TIMESTAMPTZ WHERE id = res.id;
+
+    -- Now, see if we need to get rid of some fines
+    FOR bill IN
+        SELECT  *
+          FROM  money.billing b
+          WHERE b.xact = res.id
+                AND b.btype = 1
+                AND NOT b.voided
+                AND b.billing_ts BETWEEN closing.close_start AND new_due_date::TIMESTAMPTZ
+                AND NOT EXISTS (SELECT 1 FROM money.account_adjustment a WHERE a.billing = b.id)
+    LOOP
+        IF avoid_negative THEN
+            PERFORM FROM money.materialized_billable_xact_summary WHERE id = res.id AND balanced_owd < bill.amount;
+            EXIT WHEN FOUND; -- We can't go negative, and voiding this bill would do that...
+        END IF;
+
+        UPDATE  money.billing
+          SET   voided = TRUE,
+                void_time = NOW(),
+                note = COALESCE(note,'') || ' :: Voided by emergency closing handler'
+          WHERE id = bill.id;
+    END LOOP;
+    
+    RETURN TRUE;
+END;
+$$ LANGUAGE PLPGSQL;
+
+
+
+CREATE TYPE actor.cascade_setting_summary AS (
+    name TEXT,
+    value JSON,
+    has_org_setting BOOLEAN,
+    has_user_setting BOOLEAN,
+    has_workstation_setting BOOLEAN
+);
+
+SELECT evergreen.upgrade_deps_block_check('1116', :eg_version);
+
+CREATE TABLE config.workstation_setting_type (
+    name            TEXT    PRIMARY KEY,
+    label           TEXT    UNIQUE NOT NULL,
+    grp             TEXT    REFERENCES config.settings_group (name),
+    description     TEXT,
+    datatype        TEXT    NOT NULL DEFAULT 'string',
+    fm_class        TEXT,
+    --
+    -- define valid datatypes
+    --
+    CONSTRAINT cwst_valid_datatype CHECK ( datatype IN
+    ( 'bool', 'integer', 'float', 'currency', 'interval',
+      'date', 'string', 'object', 'array', 'link' ) ),
+    --
+    -- fm_class is meaningful only for 'link' datatype
+    --
+    CONSTRAINT cwst_no_empty_link CHECK
+    ( ( datatype =  'link' AND fm_class IS NOT NULL ) OR
+      ( datatype <> 'link' AND fm_class IS NULL ) )
+);
+
+CREATE TABLE actor.workstation_setting (
+    id          SERIAL PRIMARY KEY,
+    workstation INT    NOT NULL REFERENCES actor.workstation (id) 
+                       ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED,
+    name        TEXT   NOT NULL REFERENCES config.workstation_setting_type (name) 
+                       ON DELETE CASCADE ON UPDATE CASCADE DEFERRABLE INITIALLY DEFERRED,
+    value       JSON   NOT NULL
+);
+
+
+CREATE INDEX actor_workstation_setting_workstation_idx 
+    ON actor.workstation_setting (workstation);
+
+CREATE OR REPLACE FUNCTION config.setting_is_user_or_ws()
+RETURNS TRIGGER AS $FUNC$
+BEGIN
+
+    IF TG_TABLE_NAME = 'usr_setting_type' THEN
+        PERFORM TRUE FROM config.workstation_setting_type cwst
+            WHERE cwst.name = NEW.name;
+        IF NOT FOUND THEN
+            RETURN NULL;
+        END IF;
+    END IF;
+
+    IF TG_TABLE_NAME = 'workstation_setting_type' THEN
+        PERFORM TRUE FROM config.usr_setting_type cust
+            WHERE cust.name = NEW.name;
+        IF NOT FOUND THEN
+            RETURN NULL;
+        END IF;
+    END IF;
+
+    RAISE EXCEPTION 
+        '% Cannot be used as both a user setting and a workstation setting.', 
+        NEW.name;
+END;
+$FUNC$ LANGUAGE PLPGSQL STABLE;
+
+CREATE CONSTRAINT TRIGGER check_setting_is_usr_or_ws
+  AFTER INSERT OR UPDATE ON config.usr_setting_type
+  FOR EACH ROW EXECUTE PROCEDURE config.setting_is_user_or_ws();
+
+CREATE CONSTRAINT TRIGGER check_setting_is_usr_or_ws
+  AFTER INSERT OR UPDATE ON config.workstation_setting_type
+  FOR EACH ROW EXECUTE PROCEDURE config.setting_is_user_or_ws();
+
+CREATE OR REPLACE FUNCTION actor.get_cascade_setting(
+    setting_name TEXT, org_id INT, user_id INT, workstation_id INT) 
+    RETURNS actor.cascade_setting_summary AS
+$FUNC$
+DECLARE
+    setting_value JSON;
+    summary actor.cascade_setting_summary;
+    org_setting_type config.org_unit_setting_type%ROWTYPE;
+BEGIN
+
+    summary.name := setting_name;
+
+    -- Collect the org setting type status first in case we exit early.
+    -- The existance of an org setting type is not considered
+    -- privileged information.
+    SELECT INTO org_setting_type * 
+        FROM config.org_unit_setting_type WHERE name = setting_name;
+    IF FOUND THEN
+        summary.has_org_setting := TRUE;
+    ELSE
+        summary.has_org_setting := FALSE;
+    END IF;
+
+    -- User and workstation settings have the same priority.
+    -- Start with user settings since that's the simplest code path.
+    -- The workstation_id is ignored if no user_id is provided.
+    IF user_id IS NOT NULL THEN
+
+        SELECT INTO summary.value value FROM actor.usr_setting
+            WHERE usr = user_id AND name = setting_name;
+
+        IF FOUND THEN
+            -- if we have a value, we have a setting type
+            summary.has_user_setting := TRUE;
+
+            IF workstation_id IS NOT NULL THEN
+                -- Only inform the caller about the workstation
+                -- setting type disposition when a workstation id is
+                -- provided.  Otherwise, it's NULL to indicate UNKNOWN.
+                summary.has_workstation_setting := FALSE;
+            END IF;
+
+            RETURN summary;
+        END IF;
+
+        -- no user setting value, but a setting type may exist
+        SELECT INTO summary.has_user_setting EXISTS (
+            SELECT TRUE FROM config.usr_setting_type 
+            WHERE name = setting_name
+        );
+
+        IF workstation_id IS NOT NULL THEN 
+
+            IF NOT summary.has_user_setting THEN
+                -- A workstation setting type may only exist when a user
+                -- setting type does not.
+
+                SELECT INTO summary.value value 
+                    FROM actor.workstation_setting         
+                    WHERE workstation = workstation_id AND name = setting_name;
+
+                IF FOUND THEN
+                    -- if we have a value, we have a setting type
+                    summary.has_workstation_setting := TRUE;
+                    RETURN summary;
+                END IF;
+
+                -- no value, but a setting type may exist
+                SELECT INTO summary.has_workstation_setting EXISTS (
+                    SELECT TRUE FROM config.workstation_setting_type 
+                    WHERE name = setting_name
+                );
+            END IF;
+
+            -- Finally make use of the workstation to determine the org
+            -- unit if none is provided.
+            IF org_id IS NULL AND summary.has_org_setting THEN
+                SELECT INTO org_id owning_lib 
+                    FROM actor.workstation WHERE id = workstation_id;
+            END IF;
+        END IF;
+    END IF;
+
+    -- Some org unit settings are protected by a view permission.
+    -- First see if we have any data that needs protecting, then 
+    -- check the permission if needed.
+
+    IF NOT summary.has_org_setting THEN
+        RETURN summary;
+    END IF;
+
+    -- avoid putting the value into the summary until we confirm
+    -- the value should be visible to the caller.
+    SELECT INTO setting_value value 
+        FROM actor.org_unit_ancestor_setting(setting_name, org_id);
+
+    IF NOT FOUND THEN
+        -- No value found -- perm check is irrelevant.
+        RETURN summary;
+    END IF;
+
+    IF org_setting_type.view_perm IS NOT NULL THEN
+
+        IF user_id IS NULL THEN
+            RAISE NOTICE 'Perm check required but no user_id provided';
+            RETURN summary;
+        END IF;
+
+        IF NOT permission.usr_has_perm(
+            user_id, (SELECT code FROM permission.perm_list 
+                WHERE id = org_setting_type.view_perm), org_id) 
+        THEN
+            RAISE NOTICE 'Perm check failed for user % on %',
+                user_id, org_setting_type.view_perm;
+            RETURN summary;
+        END IF;
+    END IF;
+
+    -- Perm check succeeded or was not necessary.
+    summary.value := setting_value;
+    RETURN summary;
+END;
+$FUNC$ LANGUAGE PLPGSQL;
+
+
+CREATE OR REPLACE FUNCTION actor.get_cascade_setting_batch(
+    setting_names TEXT[], org_id INT, user_id INT, workstation_id INT) 
+    RETURNS SETOF actor.cascade_setting_summary AS
+$FUNC$
+-- Returns a row per setting matching the setting name order.  If no 
+-- value is applied, NULL is returned to retain name-response ordering.
+DECLARE
+    setting_name TEXT;
+    summary actor.cascade_setting_summary;
+BEGIN
+    FOREACH setting_name IN ARRAY setting_names LOOP
+        SELECT INTO summary * FROM actor.get_cascade_setting(
+            setting_Name, org_id, user_id, workstation_id);
+        RETURN NEXT summary;
+    END LOOP;
+END;
+$FUNC$ LANGUAGE PLPGSQL;
+
+
+
+
+
+SELECT evergreen.upgrade_deps_block_check('1117', :eg_version);
+
+INSERT INTO permission.perm_list (id, code, description) VALUES
+ (608, 'APPLY_WORKSTATION_SETTING',
+   oils_i18n_gettext(608, 'APPLY_WORKSTATION_SETTING', 'ppl', 'description'));
+
+INSERT INTO config.workstation_setting_type (name, grp, datatype, label)
+VALUES (
+    'eg.circ.checkin.no_precat_alert', 'circ', 'bool',
+    oils_i18n_gettext(
+        'eg.circ.checkin.no_precat_alert',
+        'Checkin: Ignore Precataloged Items',
+        'cwst', 'label'
+    )
+), (
+    'eg.circ.checkin.noop', 'circ', 'bool',
+    oils_i18n_gettext(
+        'eg.circ.checkin.noop',
+        'Checkin: Suppress Holds and Transits',
+        'cwst', 'label'
+    )
+), (
+    'eg.circ.checkin.void_overdues', 'circ', 'bool',
+    oils_i18n_gettext(
+        'eg.circ.checkin.void_overdues',
+        'Checkin: Amnesty Mode',
+        'cwst', 'label'
+    )
+), (
+    'eg.circ.checkin.auto_print_holds_transits', 'circ', 'bool',
+    oils_i18n_gettext(
+        'eg.circ.checkin.auto_print_holds_transits',
+        'Checkin: Auto-Print Holds and Transits',
+        'cwst', 'label'
+    )
+), (
+    'eg.circ.checkin.clear_expired', 'circ', 'bool',
+    oils_i18n_gettext(
+        'eg.circ.checkin.clear_expired',
+        'Checkin: Clear Holds Shelf',
+        'cwst', 'label'
+    )
+), (
+    'eg.circ.checkin.retarget_holds', 'circ', 'bool',
+    oils_i18n_gettext(
+        'eg.circ.checkin.retarget_holds',
+        'Checkin: Retarget Local Holds',
+        'cwst', 'label'
+    )
+), (
+    'eg.circ.checkin.retarget_holds_all', 'circ', 'bool',
+    oils_i18n_gettext(
+        'eg.circ.checkin.retarget_holds_all',
+        'Checkin: Retarget All Statuses',
+        'cwst', 'label'
+    )
+), (
+    'eg.circ.checkin.hold_as_transit', 'circ', 'bool',
+    oils_i18n_gettext(
+        'eg.circ.checkin.hold_as_transit',
+        'Checkin: Capture Local Holds as Transits',
+        'cwst', 'label'
+    )
+), (
+    'eg.circ.checkin.manual_float', 'circ', 'bool',
+    oils_i18n_gettext(
+        'eg.circ.checkin.manual_float',
+        'Checkin: Manual Floating Active',
+        'cwst', 'label'
+    )
+), (
+    'eg.circ.patron.summary.collapse', 'circ', 'bool',
+    oils_i18n_gettext(
+        'eg.circ.patron.summary.collapse',
+        'Collaps Patron Summary Display',
+        'cwst', 'label'
+    )
+), (
+    'circ.bills.receiptonpay', 'circ', 'bool',
+    oils_i18n_gettext(
+        'circ.bills.receiptonpay',
+        'Print Receipt On Payment',
+        'cwst', 'label'
+    )
+), (
+    'circ.renew.strict_barcode', 'circ', 'bool',
+    oils_i18n_gettext(
+        'circ.renew.strict_barcode',
+        'Renew: Strict Barcode',
+        'cwst', 'label'
+    )
+), (
+    'circ.checkin.strict_barcode', 'circ', 'bool',
+    oils_i18n_gettext(
+        'circ.checkin.strict_barcode',
+        'Checkin: Strict Barcode',
+        'cwst', 'label'
+    )
+), (
+    'circ.checkout.strict_barcode', 'circ', 'bool',
+    oils_i18n_gettext(
+        'circ.checkout.strict_barcode',
+        'Checkout: Strict Barcode',
+        'cwst', 'label'
+    )
+), (
+    'cat.holdings_show_copies', 'cat', 'bool',
+    oils_i18n_gettext(
+        'cat.holdings_show_copies',
+        'Holdings View Show Copies',
+        'cwst', 'label'
+    )
+), (
+    'cat.holdings_show_empty', 'cat', 'bool',
+    oils_i18n_gettext(
+        'cat.holdings_show_empty',
+        'Holdings View Show Empty Volumes',
+        'cwst', 'label'
+    )
+), (
+    'cat.holdings_show_empty_org', 'cat', 'bool',
+    oils_i18n_gettext(
+        'cat.holdings_show_empty_org',
+        'Holdings View Show Empty Orgs',
+        'cwst', 'label'
+    )
+), (
+    'cat.holdings_show_vols', 'cat', 'bool',
+    oils_i18n_gettext(
+        'cat.holdings_show_vols',
+        'Holdings View Show Volumes',
+        'cwst', 'label'
+    )
+), (
+    'cat.copy.defaults', 'cat', 'object',
+    oils_i18n_gettext(
+        'cat.copy.defaults',
+        'Copy Edit Default Values',
+        'cwst', 'label'
+    )
+), (
+    'cat.printlabels.default_template', 'cat', 'string',
+    oils_i18n_gettext(
+        'cat.printlabels.default_template',
+        'Print Label Default Template',
+        'cwst', 'label'
+    )
+), (
+    'cat.printlabels.templates', 'cat', 'object',
+    oils_i18n_gettext(
+        'cat.printlabels.templates',
+        'Print Label Templates',
+        'cwst', 'label'
+    )
+), (
+    'eg.circ.patron.search.include_inactive', 'circ', 'bool',
+    oils_i18n_gettext(
+        'eg.circ.patron.search.include_inactive',
+        'Patron Search Include Inactive',
+        'cwst', 'label'
+    )
+), (
+    'eg.circ.patron.search.show_extras', 'circ', 'bool',
+    oils_i18n_gettext(
+        'eg.circ.patron.search.show_extras',
+        'Patron Search Show Extra Search Options',
+        'cwst', 'label'
+    )
+), (
+    'eg.grid.circ.checkin.checkin', 'gui', 'object',
+    oils_i18n_gettext(
+        'eg.grid.circ.checkin.checkin',
+        'Grid Config: circ.checkin.checkin',
+        'cwst', 'label'
+    )
+), (
+    'eg.grid.circ.checkin.capture', 'gui', 'object',
+    oils_i18n_gettext(
+        'eg.grid.circ.checkin.capture',
+        'Grid Config: circ.checkin.capture',
+        'cwst', 'label'
+    )
+), (
+    'eg.grid.admin.server.config.copy_tag_type', 'gui', 'object',
+    oils_i18n_gettext(
+        'eg.grid.admin.server.config.copy_tag_type',
+        'Grid Config: admin.server.config.copy_tag_type',
+        'cwst', 'label'
+    )
+), (
+    'eg.grid.admin.server.config.metabib_field_virtual_map.grid', 'gui', 'object',
+    oils_i18n_gettext(
+        'eg.grid.admin.server.config.metabib_field_virtual_map.grid',
+        'Grid Config: admin.server.config.metabib_field_virtual_map.grid',
+        'cwst', 'label'
+    )
+), (
+    'eg.grid.admin.server.config.metabib_field.grid', 'gui', 'object',
+    oils_i18n_gettext(
+        'eg.grid.admin.server.config.metabib_field.grid',
+        'Grid Config: admin.server.config.metabib_field.grid',
+        'cwst', 'label'
+    )
+), (
+    'eg.grid.admin.server.config.marc_field', 'gui', 'object',
+    oils_i18n_gettext(
+        'eg.grid.admin.server.config.marc_field',
+        'Grid Config: admin.server.config.marc_field',
+        'cwst', 'label'
+    )
+), (
+    'eg.grid.admin.server.asset.copy_tag', 'gui', 'object',
+    oils_i18n_gettext(
+        'eg.grid.admin.server.asset.copy_tag',
+        'Grid Config: admin.server.asset.copy_tag',
+        'cwst', 'label'
+    )
+), (
+    'eg.grid.admin.local.circ.neg_balance_users', 'gui', 'object',
+    oils_i18n_gettext(
+        'eg.grid.admin.local.circ.neg_balance_users',
+        'Grid Config: admin.local.circ.neg_balance_users',
+        'cwst', 'label'
+    )
+), (
+    'eg.grid.admin.local.rating.badge', 'gui', 'object',
+    oils_i18n_gettext(
+        'eg.grid.admin.local.rating.badge',
+        'Grid Config: admin.local.rating.badge',
+        'cwst', 'label'
+    )
+), (
+    'eg.grid.admin.workstation.work_log', 'gui', 'object',
+    oils_i18n_gettext(
+        'eg.grid.admin.workstation.work_log',
+        'Grid Config: admin.workstation.work_log',
+        'cwst', 'label'
+    )
+), (
+    'eg.grid.admin.workstation.patron_log', 'gui', 'object',
+    oils_i18n_gettext(
+        'eg.grid.admin.workstation.patron_log',
+        'Grid Config: admin.workstation.patron_log',
+        'cwst', 'label'
+    )
+), (
+    'eg.grid.admin.serials.pattern_template', 'gui', 'object',
+    oils_i18n_gettext(
+        'eg.grid.admin.serials.pattern_template',
+        'Grid Config: admin.serials.pattern_template',
+        'cwst', 'label'
+    )
+), (
+    'eg.grid.serials.copy_templates', 'gui', 'object',
+    oils_i18n_gettext(
+        'eg.grid.serials.copy_templates',
+        'Grid Config: serials.copy_templates',
+        'cwst', 'label'
+    )
+), (
+    'eg.grid.cat.record_overlay.holdings', 'gui', 'object',
+    oils_i18n_gettext(
+        'eg.grid.cat.record_overlay.holdings',
+        'Grid Config: cat.record_overlay.holdings',
+        'cwst', 'label'
+    )
+), (
+    'eg.grid.cat.bucket.record.search', 'gui', 'object',
+    oils_i18n_gettext(
+        'eg.grid.cat.bucket.record.search',
+        'Grid Config: cat.bucket.record.search',
+        'cwst', 'label'
+    )
+), (
+    'eg.grid.cat.bucket.record.view', 'gui', 'object',
+    oils_i18n_gettext(
+        'eg.grid.cat.bucket.record.view',
+        'Grid Config: cat.bucket.record.view',
+        'cwst', 'label'
+    )
+), (
+    'eg.grid.cat.bucket.record.pending', 'gui', 'object',
+    oils_i18n_gettext(
+        'eg.grid.cat.bucket.record.pending',
+        'Grid Config: cat.bucket.record.pending',
+        'cwst', 'label'
+    )
+), (
+    'eg.grid.cat.bucket.copy.view', 'gui', 'object',
+    oils_i18n_gettext(
+        'eg.grid.cat.bucket.copy.view',
+        'Grid Config: cat.bucket.copy.view',
+        'cwst', 'label'
+    )
+), (
+    'eg.grid.cat.bucket.copy.pending', 'gui', 'object',
+    oils_i18n_gettext(
+        'eg.grid.cat.bucket.copy.pending',
+        'Grid Config: cat.bucket.copy.pending',
+        'cwst', 'label'
+    )
+), (
+    'eg.grid.cat.items', 'gui', 'object',
+    oils_i18n_gettext(
+        'eg.grid.cat.items',
+        'Grid Config: cat.items',
+        'cwst', 'label'
+    )
+), (
+    'eg.grid.cat.volcopy.copies', 'gui', 'object',
+    oils_i18n_gettext(
+        'eg.grid.cat.volcopy.copies',
+        'Grid Config: cat.volcopy.copies',
+        'cwst', 'label'
+    )
+), (
+    'eg.grid.cat.volcopy.copies.complete', 'gui', 'object',
+    oils_i18n_gettext(
+        'eg.grid.cat.volcopy.copies.complete',
+        'Grid Config: cat.volcopy.copies.complete',
+        'cwst', 'label'
+    )
+), (
+    'eg.grid.cat.peer_bibs', 'gui', 'object',
+    oils_i18n_gettext(
+        'eg.grid.cat.peer_bibs',
+        'Grid Config: cat.peer_bibs',
+        'cwst', 'label'
+    )
+), (
+    'eg.grid.cat.catalog.holds', 'gui', 'object',
+    oils_i18n_gettext(
+        'eg.grid.cat.catalog.holds',
+        'Grid Config: cat.catalog.holds',
+        'cwst', 'label'
+    )
+), (
+    'eg.grid.cat.holdings', 'gui', 'object',
+    oils_i18n_gettext(
+        'eg.grid.cat.holdings',
+        'Grid Config: cat.holdings',
+        'cwst', 'label'
+    )
+), (
+    'eg.grid.cat.z3950_results', 'gui', 'object',
+    oils_i18n_gettext(
+        'eg.grid.cat.z3950_results',
+        'Grid Config: cat.z3950_results',
+        'cwst', 'label'
+    )
+), (
+    'eg.grid.circ.holds.shelf', 'gui', 'object',
+    oils_i18n_gettext(
+        'eg.grid.circ.holds.shelf',
+        'Grid Config: circ.holds.shelf',
+        'cwst', 'label'
+    )
+), (
+    'eg.grid.circ.holds.pull', 'gui', 'object',
+    oils_i18n_gettext(
+        'eg.grid.circ.holds.pull',
+        'Grid Config: circ.holds.pull',
+        'cwst', 'label'
+    )
+), (
+    'eg.grid.circ.in_house_use', 'gui', 'object',
+    oils_i18n_gettext(
+        'eg.grid.circ.in_house_use',
+        'Grid Config: circ.in_house_use',
+        'cwst', 'label'
+    )
+), (
+    'eg.grid.circ.renew', 'gui', 'object',
+    oils_i18n_gettext(
+        'eg.grid.circ.renew',
+        'Grid Config: circ.renew',
+        'cwst', 'label'
+    )
+), (
+    'eg.grid.circ.transits.list', 'gui', 'object',
+    oils_i18n_gettext(
+        'eg.grid.circ.transits.list',
+        'Grid Config: circ.transits.list',
+        'cwst', 'label'
+    )
+), (
+    'eg.grid.circ.patron.holds', 'gui', 'object',
+    oils_i18n_gettext(
+        'eg.grid.circ.patron.holds',
+        'Grid Config: circ.patron.holds',
+        'cwst', 'label'
+    )
+), (
+    'eg.grid.circ.pending_patrons.list', 'gui', 'object',
+    oils_i18n_gettext(
+        'eg.grid.circ.pending_patrons.list',
+        'Grid Config: circ.pending_patrons.list',
+        'cwst', 'label'
+    )
+), (
+    'eg.grid.circ.patron.items_out.noncat', 'gui', 'object',
+    oils_i18n_gettext(
+        'eg.grid.circ.patron.items_out.noncat',
+        'Grid Config: circ.patron.items_out.noncat',
+        'cwst', 'label'
+    )
+), (
+    'eg.grid.circ.patron.items_out', 'gui', 'object',
+    oils_i18n_gettext(
+        'eg.grid.circ.patron.items_out',
+        'Grid Config: circ.patron.items_out',
+        'cwst', 'label'
+    )
+), (
+    'eg.grid.circ.patron.billhistory_payments', 'gui', 'object',
+    oils_i18n_gettext(
+        'eg.grid.circ.patron.billhistory_payments',
+        'Grid Config: circ.patron.billhistory_payments',
+        'cwst', 'label'
+    )
+), (
+    'eg.grid.user.bucket.view', 'gui', 'object',
+    oils_i18n_gettext(
+        'eg.grid.user.bucket.view',
+        'Grid Config: user.bucket.view',
+        'cwst', 'label'
+    )
+), (
+    'eg.grid.user.bucket.pending', 'gui', 'object',
+    oils_i18n_gettext(
+        'eg.grid.user.bucket.pending',
+        'Grid Config: user.bucket.pending',
+        'cwst', 'label'
+    )
+), (
+    'eg.grid.circ.patron.staff_messages', 'gui', 'object',
+    oils_i18n_gettext(
+        'eg.grid.circ.patron.staff_messages',
+        'Grid Config: circ.patron.staff_messages',
+        'cwst', 'label'
+    )
+), (
+    'eg.grid.circ.patron.archived_messages', 'gui', 'object',
+    oils_i18n_gettext(
+        'eg.grid.circ.patron.archived_messages',
+        'Grid Config: circ.patron.archived_messages',
+        'cwst', 'label'
+    )
+), (
+    'eg.grid.circ.patron.bills', 'gui', 'object',
+    oils_i18n_gettext(
+        'eg.grid.circ.patron.bills',
+        'Grid Config: circ.patron.bills',
+        'cwst', 'label'
+    )
+), (
+    'eg.grid.circ.patron.checkout', 'gui', 'object',
+    oils_i18n_gettext(
+        'eg.grid.circ.patron.checkout',
+        'Grid Config: circ.patron.checkout',
+        'cwst', 'label'
+    )
+), (
+    'eg.grid.serials.mfhd_grid', 'gui', 'object',
+    oils_i18n_gettext(
+        'eg.grid.serials.mfhd_grid',
+        'Grid Config: serials.mfhd_grid',
+        'cwst', 'label'
+    )
+), (
+    'eg.grid.serials.view_item_grid', 'gui', 'object',
+    oils_i18n_gettext(
+        'eg.grid.serials.view_item_grid',
+        'Grid Config: serials.view_item_grid',
+        'cwst', 'label'
+    )
+), (
+    'eg.grid.serials.dist_stream_grid', 'gui', 'object',
+    oils_i18n_gettext(
+        'eg.grid.serials.dist_stream_grid',
+        'Grid Config: serials.dist_stream_grid',
+        'cwst', 'label'
+    )
+), (
+    'eg.grid.circ.patron.search', 'gui', 'object',
+    oils_i18n_gettext(
+        'eg.grid.circ.patron.search',
+        'Grid Config: circ.patron.search',
+        'cwst', 'label'
+    )
+), (
+    'eg.cat.record.summary.collapse', 'gui', 'bool',
+    oils_i18n_gettext(
+        'eg.cat.record.summary.collapse',
+        'Collapse Bib Record Summary',
+        'cwst', 'label'
+    )
+), (
+    'cat.marcedit.flateditor', 'gui', 'bool',
+    oils_i18n_gettext(
+        'cat.marcedit.flateditor',
+        'Use Flat MARC Editor',
+        'cwst', 'label'
+    )
+), (
+    'cat.marcedit.stack_subfields', 'gui', 'bool',
+    oils_i18n_gettext(
+        'cat.marcedit.stack_subfields',
+        'MARC Editor Stack Subfields',
+        'cwst', 'label'
+    )
+), (
+    'eg.offline.print_receipt', 'gui', 'bool',
+    oils_i18n_gettext(
+        'eg.offline.print_receipt',
+        'Offline Print Receipt',
+        'cwst', 'label'
+    )
+), (
+    'eg.offline.strict_barcode', 'gui', 'bool',
+    oils_i18n_gettext(
+        'eg.offline.strict_barcode',
+        'Offline Use Strict Barcode',
+        'cwst', 'label'
+    )
+), (
+    'cat.default_bib_marc_template', 'gui', 'string',
+    oils_i18n_gettext(
+        'cat.default_bib_marc_template',
+        'Default MARC Template',
+        'cwst', 'label'
+    )
+), (
+    'eg.audio.disable', 'gui', 'bool',
+    oils_i18n_gettext(
+        'eg.audio.disable',
+        'Disable Staff Client Notification Audio',
+        'cwst', 'label'
+    )
+), (
+    'eg.search.adv_pane', 'gui', 'string',
+    oils_i18n_gettext(
+        'eg.search.adv_pane',
+        'Catalog Advanced Search Default Pane',
+        'cwst', 'label'
+    )
+), (
+    'eg.print.template_context.bills_current', 'gui', 'string',
+    oils_i18n_gettext(
+        'eg.print.template_context.bills_current',
+        'Print Template Context: bills_current',
+        'cwst', 'label'
+    )
+), (
+    'eg.print.template.bills_current', 'gui', 'string',
+    oils_i18n_gettext(
+        'eg.print.template.bills_current',
+        'Print Template: bills_current',
+        'cwst', 'label'
+    )
+), (
+    'eg.print.template_context.bills_historical', 'gui', 'string',
+    oils_i18n_gettext(
+        'eg.print.template_context.bills_historical',
+        'Print Template Context: bills_historical',
+        'cwst', 'label'
+    )
+), (
+    'eg.print.template.bills_historical', 'gui', 'string',
+    oils_i18n_gettext(
+        'eg.print.template.bills_historical',
+        'Print Template: bills_historical',
+        'cwst', 'label'
+    )
+), (
+    'eg.print.template_context.bill_payment', 'gui', 'string',
+    oils_i18n_gettext(
+        'eg.print.template_context.bill_payment',
+        'Print Template Context: bill_payment',
+        'cwst', 'label'
+    )
+), (
+    'eg.print.template.bill_payment', 'gui', 'string',
+    oils_i18n_gettext(
+        'eg.print.template.bill_payment',
+        'Print Template: bill_payment',
+        'cwst', 'label'
+    )
+), (
+    'eg.print.template_context.checkin', 'gui', 'string',
+    oils_i18n_gettext(
+        'eg.print.template_context.checkin',
+        'Print Template Context: checkin',
+        'cwst', 'label'
+    )
+), (
+    'eg.print.template.checkin', 'gui', 'string',
+    oils_i18n_gettext(
+        'eg.print.template.checkin',
+        'Print Template: checkin',
+        'cwst', 'label'
+    )
+), (
+    'eg.print.template_context.checkout', 'gui', 'string',
+    oils_i18n_gettext(
+        'eg.print.template_context.checkout',
+        'Print Template Context: checkout',
+        'cwst', 'label'
+    )
+), (
+    'eg.print.template.checkout', 'gui', 'string',
+    oils_i18n_gettext(
+        'eg.print.template.checkout',
+        'Print Template: checkout',
+        'cwst', 'label'
+    )
+), (
+    'eg.print.template_context.hold_transit_slip', 'gui', 'string',
+    oils_i18n_gettext(
+        'eg.print.template_context.hold_transit_slip',
+        'Print Template Context: hold_transit_slip',
+        'cwst', 'label'
+    )
+), (
+    'eg.print.template.hold_transit_slip', 'gui', 'string',
+    oils_i18n_gettext(
+        'eg.print.template.hold_transit_slip',
+        'Print Template: hold_transit_slip',
+        'cwst', 'label'
+    )
+), (
+    'eg.print.template_context.hold_shelf_slip', 'gui', 'string',
+    oils_i18n_gettext(
+        'eg.print.template_context.hold_shelf_slip',
+        'Print Template Context: hold_shelf_slip',
+        'cwst', 'label'
+    )
+), (
+    'eg.print.template.hold_shelf_slip', 'gui', 'string',
+    oils_i18n_gettext(
+        'eg.print.template.hold_shelf_slip',
+        'Print Template: hold_shelf_slip',
+        'cwst', 'label'
+    )
+), (
+    'eg.print.template_context.holds_for_bib', 'gui', 'string',
+    oils_i18n_gettext(
+        'eg.print.template_context.holds_for_bib',
+        'Print Template Context: holds_for_bib',
+        'cwst', 'label'
+    )
+), (
+    'eg.print.template.holds_for_bib', 'gui', 'string',
+    oils_i18n_gettext(
+        'eg.print.template.holds_for_bib',
+        'Print Template: holds_for_bib',
+        'cwst', 'label'
+    )
+), (
+    'eg.print.template_context.holds_for_patron', 'gui', 'string',
+    oils_i18n_gettext(
+        'eg.print.template_context.holds_for_patron',
+        'Print Template Context: holds_for_patron',
+        'cwst', 'label'
+    )
+), (
+    'eg.print.template.holds_for_patron', 'gui', 'string',
+    oils_i18n_gettext(
+        'eg.print.template.holds_for_patron',
+        'Print Template: holds_for_patron',
+        'cwst', 'label'
+    )
+), (
+    'eg.print.template_context.hold_pull_list', 'gui', 'string',
+    oils_i18n_gettext(
+        'eg.print.template_context.hold_pull_list',
+        'Print Template Context: hold_pull_list',
+        'cwst', 'label'
+    )
+), (
+    'eg.print.template.hold_pull_list', 'gui', 'string',
+    oils_i18n_gettext(
+        'eg.print.template.hold_pull_list',
+        'Print Template: hold_pull_list',
+        'cwst', 'label'
+    )
+), (
+    'eg.print.template_context.hold_shelf_list', 'gui', 'string',
+    oils_i18n_gettext(
+        'eg.print.template_context.hold_shelf_list',
+        'Print Template Context: hold_shelf_list',
+        'cwst', 'label'
+    )
+), (
+    'eg.print.template.hold_shelf_list', 'gui', 'string',
+    oils_i18n_gettext(
+        'eg.print.template.hold_shelf_list',
+        'Print Template: hold_shelf_list',
+        'cwst', 'label'
+    )
+), (
+    'eg.print.template_context.in_house_use_list', 'gui', 'string',
+    oils_i18n_gettext(
+        'eg.print.template_context.in_house_use_list',
+        'Print Template Context: in_house_use_list',
+        'cwst', 'label'
+    )
+), (
+    'eg.print.template.in_house_use_list', 'gui', 'string',
+    oils_i18n_gettext(
+        'eg.print.template.in_house_use_list',
+        'Print Template: in_house_use_list',
+        'cwst', 'label'
+    )
+), (
+    'eg.print.template_context.item_status', 'gui', 'string',
+    oils_i18n_gettext(
+        'eg.print.template_context.item_status',
+        'Print Template Context: item_status',
+        'cwst', 'label'
+    )
+), (
+    'eg.print.template.item_status', 'gui', 'string',
+    oils_i18n_gettext(
+        'eg.print.template.item_status',
+        'Print Template: item_status',
+        'cwst', 'label'
+    )
+), (
+    'eg.print.template_context.items_out', 'gui', 'string',
+    oils_i18n_gettext(
+        'eg.print.template_context.items_out',
+        'Print Template Context: items_out',
+        'cwst', 'label'
+    )
+), (
+    'eg.print.template.items_out', 'gui', 'string',
+    oils_i18n_gettext(
+        'eg.print.template.items_out',
+        'Print Template: items_out',
+        'cwst', 'label'
+    )
+), (
+    'eg.print.template_context.patron_address', 'gui', 'string',
+    oils_i18n_gettext(
+        'eg.print.template_context.patron_address',
+        'Print Template Context: patron_address',
+        'cwst', 'label'
+    )
+), (
+    'eg.print.template.patron_address', 'gui', 'string',
+    oils_i18n_gettext(
+        'eg.print.template.patron_address',
+        'Print Template: patron_address',
+        'cwst', 'label'
+    )
+), (
+    'eg.print.template_context.patron_data', 'gui', 'string',
+    oils_i18n_gettext(
+        'eg.print.template_context.patron_data',
+        'Print Template Context: patron_data',
+        'cwst', 'label'
+    )
+), (
+    'eg.print.template.patron_data', 'gui', 'string',
+    oils_i18n_gettext(
+        'eg.print.template.patron_data',
+        'Print Template: patron_data',
+        'cwst', 'label'
+    )
+), (
+    'eg.print.template_context.patron_note', 'gui', 'string',
+    oils_i18n_gettext(
+        'eg.print.template_context.patron_note',
+        'Print Template Context: patron_note',
+        'cwst', 'label'
+    )
+), (
+    'eg.print.template.patron_note', 'gui', 'string',
+    oils_i18n_gettext(
+        'eg.print.template.patron_note',
+        'Print Template: patron_note',
+        'cwst', 'label'
+    )
+), (
+    'eg.print.template_context.renew', 'gui', 'string',
+    oils_i18n_gettext(
+        'eg.print.template_context.renew',
+        'Print Template Context: renew',
+        'cwst', 'label'
+    )
+), (
+    'eg.print.template.renew', 'gui', 'string',
+    oils_i18n_gettext(
+        'eg.print.template.renew',
+        'Print Template: renew',
+        'cwst', 'label'
+    )
+), (
+    'eg.print.template_context.transit_list', 'gui', 'string',
+    oils_i18n_gettext(
+        'eg.print.template_context.transit_list',
+        'Print Template Context: transit_list',
+        'cwst', 'label'
+    )
+), (
+    'eg.print.template.transit_list', 'gui', 'string',
+    oils_i18n_gettext(
+        'eg.print.template.transit_list',
+        'Print Template: transit_list',
+        'cwst', 'label'
+    )
+), (
+    'eg.print.template_context.transit_slip', 'gui', 'string',
+    oils_i18n_gettext(
+        'eg.print.template_context.transit_slip',
+        'Print Template Context: transit_slip',
+        'cwst', 'label'
+    )
+), (
+    'eg.print.template.transit_slip', 'gui', 'string',
+    oils_i18n_gettext(
+        'eg.print.template.transit_slip',
+        'Print Template: transit_slip',
+        'cwst', 'label'
+    )
+), (
+    'eg.print.template_context.offline_checkout', 'gui', 'string',
+    oils_i18n_gettext(
+        'eg.print.template_context.offline_checkout',
+        'Print Template Context: offline_checkout',
+        'cwst', 'label'
+    )
+), (
+    'eg.print.template.offline_checkout', 'gui', 'string',
+    oils_i18n_gettext(
+        'eg.print.template.offline_checkout',
+        'Print Template: offline_checkout',
+        'cwst', 'label'
+    )
+), (
+    'eg.print.template_context.offline_renew', 'gui', 'string',
+    oils_i18n_gettext(
+        'eg.print.template_context.offline_renew',
+        'Print Template Context: offline_renew',
+        'cwst', 'label'
+    )
+), (
+    'eg.print.template.offline_renew', 'gui', 'string',
+    oils_i18n_gettext(
+        'eg.print.template.offline_renew',
+        'Print Template: offline_renew',
+        'cwst', 'label'
+    )
+), (
+    'eg.print.template_context.offline_checkin', 'gui', 'string',
+    oils_i18n_gettext(
+        'eg.print.template_context.offline_checkin',
+        'Print Template Context: offline_checkin',
+        'cwst', 'label'
+    )
+), (
+    'eg.print.template.offline_checkin', 'gui', 'string',
+    oils_i18n_gettext(
+        'eg.print.template.offline_checkin',
+        'Print Template: offline_checkin',
+        'cwst', 'label'
+    )
+), (
+    'eg.print.template_context.offline_in_house_use', 'gui', 'string',
+    oils_i18n_gettext(
+        'eg.print.template_context.offline_in_house_use',
+        'Print Template Context: offline_in_house_use',
+        'cwst', 'label'
+    )
+), (
+    'eg.print.template.offline_in_house_use', 'gui', 'string',
+    oils_i18n_gettext(
+        'eg.print.template.offline_in_house_use',
+        'Print Template: offline_in_house_use',
+        'cwst', 'label'
+    )
+), (
+    'eg.serials.stream_names', 'gui', 'array',
+    oils_i18n_gettext(
+        'eg.serials.stream_names',
+        'Serials Local Stream Names',
+        'cwst', 'label'
+    )
+), (
+    'eg.serials.items.do_print_routing_lists', 'gui', 'bool',
+    oils_i18n_gettext(
+        'eg.serials.items.do_print_routing_lists',
+        'Serials Print Routing Lists',
+        'cwst', 'label'
+    )
+), (
+    'eg.serials.items.receive_and_barcode', 'gui', 'bool',
+    oils_i18n_gettext(
+        'eg.serials.items.receive_and_barcode',
+        'Serials Barcode On Receive',
+        'cwst', 'label'
+    )
+);
+
+
+-- More values with fm_class'es
+INSERT INTO config.workstation_setting_type (name, grp, datatype, fm_class, label)
+VALUES (
+    'eg.search.search_lib', 'gui', 'link', 'aou',
+    oils_i18n_gettext(
+        'eg.search.search_lib',
+        'Staff Catalog Default Search Library',
+        'cwst', 'label'
+    )
+), (
+    'eg.search.pref_lib', 'gui', 'link', 'aou',
+    oils_i18n_gettext(
+        'eg.search.pref_lib',
+        'Staff Catalog Preferred Library',
+        'cwst', 'label'
+    )
+);
+
+
+
+
+
+SELECT evergreen.upgrade_deps_block_check('1118', :eg_version);
+
+UPDATE action_trigger.event_definition
+SET template =
+$$
+[%- USE date -%]
+[%- SET user = target.0.owner -%]
+To: [%- params.recipient_email || user.email %]
+From: [%- params.sender_email || default_sender %]
+Date: [%- date.format(date.now, '%a, %d %b %Y %T -0000', gmt => 1) %]
+Subject: Bibliographic Records
+Auto-Submitted: auto-generated
+
+[% FOR cbreb IN target %]
+[% FOR item IN cbreb.items;
+    bre_id = item.target_biblio_record_entry;
+
+    bibxml = helpers.unapi_bre(bre_id, {flesh => '{mra}'});
+    title = '';
+    FOR part IN bibxml.findnodes('//*[@tag="245"]/*[@code="a" or @code="b"]');
+        title = title _ part.textContent;
+    END;
+
+    author = bibxml.findnodes('//*[@tag="100"]/*[@code="a"]').textContent;
+    item_type = bibxml.findnodes('//*[local-name()="attributes"]/*[local-name()="field"][@name="item_type"]').getAttribute('coded-value');
+    publisher = bibxml.findnodes('//*[@tag="260"]/*[@code="b"]').textContent;
+    pubdate = bibxml.findnodes('//*[@tag="260"]/*[@code="c"]').textContent;
+    isbn = bibxml.findnodes('//*[@tag="020"]/*[@code="a"]').textContent;
+    issn = bibxml.findnodes('//*[@tag="022"]/*[@code="a"]').textContent;
+    upc = bibxml.findnodes('//*[@tag="024"]/*[@code="a"]').textContent;
+%]
+
+[% loop.count %]/[% loop.size %].  Bib ID# [% bre_id %] 
+[% IF isbn %]ISBN: [% isbn _ "\n" %][% END -%]
+[% IF issn %]ISSN: [% issn _ "\n" %][% END -%]
+[% IF upc  %]UPC:  [% upc _ "\n" %] [% END -%]
+Title: [% title %]
+Author: [% author %]
+Publication Info: [% publisher %] [% pubdate %]
+Item Type: [% item_type %]
+
+[% END %]
+[% END %]
+$$
+WHERE hook = 'biblio.format.record_entry.email'
+-- from previous stock definition
+AND MD5(template) = 'ee4e6c1b3049086c570c7a77413d46c1';
+
+UPDATE action_trigger.event_definition
+SET template =
+$$
+<div>
+    <style> li { padding: 8px; margin 5px; }</style>
+    <ol>
+    [% FOR cbreb IN target %]
+    [% FOR item IN cbreb.items;
+        bre_id = item.target_biblio_record_entry;
+
+        bibxml = helpers.unapi_bre(bre_id, {flesh => '{mra}'});
+        title = '';
+        FOR part IN bibxml.findnodes('//*[@tag="245"]/*[@code="a" or @code="b"]');
+            title = title _ part.textContent;
+        END;
+
+        author = bibxml.findnodes('//*[@tag="100"]/*[@code="a"]').textContent;
+        item_type = bibxml.findnodes('//*[local-name()="attributes"]/*[local-name()="field"][@name="item_type"]').getAttribute('coded-value');
+        publisher = bibxml.findnodes('//*[@tag="260"]/*[@code="b"]').textContent;
+        pubdate = bibxml.findnodes('//*[@tag="260"]/*[@code="c"]').textContent;
+        isbn = bibxml.findnodes('//*[@tag="020"]/*[@code="a"]').textContent;
+        %]
+
+        <li>
+            Bib ID# [% bre_id %] ISBN: [% isbn %]<br />
+            Title: [% title %]<br />
+            Author: [% author %]<br />
+            Publication Info: [% publisher %] [% pubdate %]<br/>
+            Item Type: [% item_type %]
+        </li>
+    [% END %]
+    [% END %]
+    </ol>
+</div>
+$$
+WHERE hook = 'biblio.format.record_entry.print'
+-- from previous stock definition
+AND MD5(template) = '9ada7ea8417cb23f89d0dc8f15ec68d0';
+
+
+SELECT evergreen.upgrade_deps_block_check('1120', :eg_version);
+
+--Only insert if the attributes are not already present
+
+INSERT INTO config.z3950_attr (source, name, label, code, format, truncation)
+SELECT 'oclc','upc','UPC','1007','6','0'
+WHERE NOT EXISTS (SELECT name FROM config.z3950_attr WHERE source = 'oclc' AND name = 'upc');
+
+INSERT INTO config.z3950_attr (source, name, label, code, format, truncation)
+SELECT 'loc','upc','UPC','1007','1','1'
+WHERE NOT EXISTS (SELECT name FROM config.z3950_attr WHERE source = 'loc' AND name = 'upc');
+
+SELECT evergreen.upgrade_deps_block_check('1121', :eg_version);
+
+CREATE TABLE permission.grp_tree_display_entry (
+    id      SERIAL PRIMARY KEY,
+    position INTEGER NOT NULL,
+    org     INTEGER NOT NULL REFERENCES actor.org_unit (id)
+            DEFERRABLE INITIALLY DEFERRED,
+    grp     INTEGER NOT NULL REFERENCES permission.grp_tree (id)
+            DEFERRABLE INITIALLY DEFERRED,
+    CONSTRAINT pgtde_once_per_org UNIQUE (org, grp)
+);
+
+ALTER TABLE permission.grp_tree_display_entry
+    ADD COLUMN parent integer REFERENCES permission.grp_tree_display_entry (id)
+            DEFERRABLE INITIALLY DEFERRED;
+
+INSERT INTO permission.perm_list (id, code, description)
+VALUES (609, 'MANAGE_CUSTOM_PERM_GRP_TREE', oils_i18n_gettext( 609,
+    'Allows a user to manage custom permission group lists.', 'ppl', 'description' ));
+            
+
+SELECT evergreen.upgrade_deps_block_check('1122', :eg_version);
+
+ALTER TABLE actor.usr 
+    ADD COLUMN pref_prefix TEXT,
+    ADD COLUMN pref_first_given_name TEXT,
+    ADD COLUMN pref_second_given_name TEXT,
+    ADD COLUMN pref_family_name TEXT,
+    ADD COLUMN pref_suffix TEXT,
+    ADD COLUMN name_keywords TEXT,
+    ADD COLUMN name_kw_tsvector TSVECTOR;
+
+ALTER TABLE staging.user_stage
+    ADD COLUMN pref_first_given_name TEXT,
+    ADD COLUMN pref_second_given_name TEXT,
+    ADD COLUMN pref_family_name TEXT;
+
+CREATE INDEX actor_usr_pref_first_given_name_idx 
+    ON actor.usr (evergreen.lowercase(pref_first_given_name));
+CREATE INDEX actor_usr_pref_second_given_name_idx 
+    ON actor.usr (evergreen.lowercase(pref_second_given_name));
+CREATE INDEX actor_usr_pref_family_name_idx 
+    ON actor.usr (evergreen.lowercase(pref_family_name));
+CREATE INDEX actor_usr_pref_first_given_name_unaccent_idx 
+    ON actor.usr (evergreen.unaccent_and_squash(pref_first_given_name));
+CREATE INDEX actor_usr_pref_second_given_name_unaccent_idx 
+    ON actor.usr (evergreen.unaccent_and_squash(pref_second_given_name));
+CREATE INDEX actor_usr_pref_family_name_unaccent_idx 
+   ON actor.usr (evergreen.unaccent_and_squash(pref_family_name));
+
+-- Update keyword indexes for existing patrons
+
+UPDATE actor.usr SET name_kw_tsvector = 
+    TO_TSVECTOR(
+        COALESCE(prefix, '') || ' ' || 
+        COALESCE(first_given_name, '') || ' ' || 
+        COALESCE(evergreen.unaccent_and_squash(first_given_name), '') || ' ' || 
+        COALESCE(second_given_name, '') || ' ' || 
+        COALESCE(evergreen.unaccent_and_squash(second_given_name), '') || ' ' || 
+        COALESCE(family_name, '') || ' ' || 
+        COALESCE(evergreen.unaccent_and_squash(family_name), '') || ' ' || 
+        COALESCE(suffix, '')
+    );
+
+CREATE OR REPLACE FUNCTION actor.user_ingest_name_keywords() 
+    RETURNS TRIGGER AS $func$
+BEGIN
+    NEW.name_kw_tsvector := TO_TSVECTOR(
+        COALESCE(NEW.prefix, '')                || ' ' || 
+        COALESCE(NEW.first_given_name, '')      || ' ' || 
+        COALESCE(evergreen.unaccent_and_squash(NEW.first_given_name), '') || ' ' || 
+        COALESCE(NEW.second_given_name, '')     || ' ' || 
+        COALESCE(evergreen.unaccent_and_squash(NEW.second_given_name), '') || ' ' || 
+        COALESCE(NEW.family_name, '')           || ' ' || 
+        COALESCE(evergreen.unaccent_and_squash(NEW.family_name), '') || ' ' || 
+        COALESCE(NEW.suffix, '')                || ' ' || 
+        COALESCE(NEW.pref_prefix, '')            || ' ' || 
+        COALESCE(NEW.pref_first_given_name, '')  || ' ' || 
+        COALESCE(evergreen.unaccent_and_squash(NEW.pref_first_given_name), '') || ' ' || 
+        COALESCE(NEW.pref_second_given_name, '') || ' ' || 
+        COALESCE(evergreen.unaccent_and_squash(NEW.pref_second_given_name), '') || ' ' || 
+        COALESCE(NEW.pref_family_name, '')       || ' ' || 
+        COALESCE(evergreen.unaccent_and_squash(NEW.pref_family_name), '') || ' ' || 
+        COALESCE(NEW.pref_suffix, '')            || ' ' || 
+        COALESCE(NEW.name_keywords, '')
+    );
+    RETURN NEW;
+END;
+$func$ LANGUAGE PLPGSQL;
+
+-- Add after the batch upate above to avoid duplicate updates.
+CREATE TRIGGER user_ingest_name_keywords_tgr 
+    BEFORE INSERT OR UPDATE ON actor.usr 
+    FOR EACH ROW EXECUTE PROCEDURE actor.user_ingest_name_keywords();
+
+
+-- merge pref names from source user to target user, except when
+-- clobbering existing pref names.
+CREATE OR REPLACE FUNCTION actor.usr_merge(src_usr INT, dest_usr INT, 
+    del_addrs BOOLEAN, del_cards BOOLEAN, deactivate_cards BOOLEAN ) 
+    RETURNS VOID AS $$
+DECLARE
+	suffix TEXT;
+	bucket_row RECORD;
+	picklist_row RECORD;
+	queue_row RECORD;
+	folder_row RECORD;
+BEGIN
+
+    -- do some initial cleanup 
+    UPDATE actor.usr SET card = NULL WHERE id = src_usr;
+    UPDATE actor.usr SET mailing_address = NULL WHERE id = src_usr;
+    UPDATE actor.usr SET billing_address = NULL WHERE id = src_usr;
+
+    -- actor.*
+    IF del_cards THEN
+        DELETE FROM actor.card where usr = src_usr;
+    ELSE
+        IF deactivate_cards THEN
+            UPDATE actor.card SET active = 'f' WHERE usr = src_usr;
+        END IF;
+        UPDATE actor.card SET usr = dest_usr WHERE usr = src_usr;
+    END IF;
+
+
+    IF del_addrs THEN
+        DELETE FROM actor.usr_address WHERE usr = src_usr;
+    ELSE
+        UPDATE actor.usr_address SET usr = dest_usr WHERE usr = src_usr;
+    END IF;
+
+    UPDATE actor.usr_note SET usr = dest_usr WHERE usr = src_usr;
+    -- dupes are technically OK in actor.usr_standing_penalty, should manually delete them...
+    UPDATE actor.usr_standing_penalty SET usr = dest_usr WHERE usr = src_usr;
+    PERFORM actor.usr_merge_rows('actor.usr_org_unit_opt_in', 'usr', src_usr, dest_usr);
+    PERFORM actor.usr_merge_rows('actor.usr_setting', 'usr', src_usr, dest_usr);
+
+    -- permission.*
+    PERFORM actor.usr_merge_rows('permission.usr_perm_map', 'usr', src_usr, dest_usr);
+    PERFORM actor.usr_merge_rows('permission.usr_object_perm_map', 'usr', src_usr, dest_usr);
+    PERFORM actor.usr_merge_rows('permission.usr_grp_map', 'usr', src_usr, dest_usr);
+    PERFORM actor.usr_merge_rows('permission.usr_work_ou_map', 'usr', src_usr, dest_usr);
+
+
+    -- container.*
+	
+	-- For each *_bucket table: transfer every bucket belonging to src_usr
+	-- into the custody of dest_usr.
+	--
+	-- In order to avoid colliding with an existing bucket owned by
+	-- the destination user, append the source user's id (in parenthesese)
+	-- to the name.  If you still get a collision, add successive
+	-- spaces to the name and keep trying until you succeed.
+	--
+	FOR bucket_row in
+		SELECT id, name
+		FROM   container.biblio_record_entry_bucket
+		WHERE  owner = src_usr
+	LOOP
+		suffix := ' (' || src_usr || ')';
+		LOOP
+			BEGIN
+				UPDATE  container.biblio_record_entry_bucket
+				SET     owner = dest_usr, name = name || suffix
+				WHERE   id = bucket_row.id;
+			EXCEPTION WHEN unique_violation THEN
+				suffix := suffix || ' ';
+				CONTINUE;
+			END;
+			EXIT;
+		END LOOP;
+	END LOOP;
+
+	FOR bucket_row in
+		SELECT id, name
+		FROM   container.call_number_bucket
+		WHERE  owner = src_usr
+	LOOP
+		suffix := ' (' || src_usr || ')';
+		LOOP
+			BEGIN
+				UPDATE  container.call_number_bucket
+				SET     owner = dest_usr, name = name || suffix
+				WHERE   id = bucket_row.id;
+			EXCEPTION WHEN unique_violation THEN
+				suffix := suffix || ' ';
+				CONTINUE;
+			END;
+			EXIT;
+		END LOOP;
+	END LOOP;
+
+	FOR bucket_row in
+		SELECT id, name
+		FROM   container.copy_bucket
+		WHERE  owner = src_usr
+	LOOP
+		suffix := ' (' || src_usr || ')';
+		LOOP
+			BEGIN
+				UPDATE  container.copy_bucket
+				SET     owner = dest_usr, name = name || suffix
+				WHERE   id = bucket_row.id;
+			EXCEPTION WHEN unique_violation THEN
+				suffix := suffix || ' ';
+				CONTINUE;
+			END;
+			EXIT;
+		END LOOP;
+	END LOOP;
+
+	FOR bucket_row in
+		SELECT id, name
+		FROM   container.user_bucket
+		WHERE  owner = src_usr
+	LOOP
+		suffix := ' (' || src_usr || ')';
+		LOOP
+			BEGIN
+				UPDATE  container.user_bucket
+				SET     owner = dest_usr, name = name || suffix
+				WHERE   id = bucket_row.id;
+			EXCEPTION WHEN unique_violation THEN
+				suffix := suffix || ' ';
+				CONTINUE;
+			END;
+			EXIT;
+		END LOOP;
+	END LOOP;
+
+	UPDATE container.user_bucket_item SET target_user = dest_usr WHERE target_user = src_usr;
+
+    -- vandelay.*
+	-- transfer queues the same way we transfer buckets (see above)
+	FOR queue_row in
+		SELECT id, name
+		FROM   vandelay.queue
+		WHERE  owner = src_usr
+	LOOP
+		suffix := ' (' || src_usr || ')';
+		LOOP
+			BEGIN
+				UPDATE  vandelay.queue
+				SET     owner = dest_usr, name = name || suffix
+				WHERE   id = queue_row.id;
+			EXCEPTION WHEN unique_violation THEN
+				suffix := suffix || ' ';
+				CONTINUE;
+			END;
+			EXIT;
+		END LOOP;
+	END LOOP;
+
+    -- money.*
+    PERFORM actor.usr_merge_rows('money.collections_tracker', 'usr', src_usr, dest_usr);
+    PERFORM actor.usr_merge_rows('money.collections_tracker', 'collector', src_usr, dest_usr);
+    UPDATE money.billable_xact SET usr = dest_usr WHERE usr = src_usr;
+    UPDATE money.billing SET voider = dest_usr WHERE voider = src_usr;
+    UPDATE money.bnm_payment SET accepting_usr = dest_usr WHERE accepting_usr = src_usr;
+
+    -- action.*
+    UPDATE action.circulation SET usr = dest_usr WHERE usr = src_usr;
+    UPDATE action.circulation SET circ_staff = dest_usr WHERE circ_staff = src_usr;
+    UPDATE action.circulation SET checkin_staff = dest_usr WHERE checkin_staff = src_usr;
+    UPDATE action.usr_circ_history SET usr = dest_usr WHERE usr = src_usr;
+
+    UPDATE action.hold_request SET usr = dest_usr WHERE usr = src_usr;
+    UPDATE action.hold_request SET fulfillment_staff = dest_usr WHERE fulfillment_staff = src_usr;
+    UPDATE action.hold_request SET requestor = dest_usr WHERE requestor = src_usr;
+    UPDATE action.hold_notification SET notify_staff = dest_usr WHERE notify_staff = src_usr;
+
+    UPDATE action.in_house_use SET staff = dest_usr WHERE staff = src_usr;
+    UPDATE action.non_cataloged_circulation SET staff = dest_usr WHERE staff = src_usr;
+    UPDATE action.non_cataloged_circulation SET patron = dest_usr WHERE patron = src_usr;
+    UPDATE action.non_cat_in_house_use SET staff = dest_usr WHERE staff = src_usr;
+    UPDATE action.survey_response SET usr = dest_usr WHERE usr = src_usr;
+
+    -- acq.*
+    UPDATE acq.fund_allocation SET allocator = dest_usr WHERE allocator = src_usr;
+	UPDATE acq.fund_transfer SET transfer_user = dest_usr WHERE transfer_user = src_usr;
+
+	-- transfer picklists the same way we transfer buckets (see above)
+	FOR picklist_row in
+		SELECT id, name
+		FROM   acq.picklist
+		WHERE  owner = src_usr
+	LOOP
+		suffix := ' (' || src_usr || ')';
+		LOOP
+			BEGIN
+				UPDATE  acq.picklist
+				SET     owner = dest_usr, name = name || suffix
+				WHERE   id = picklist_row.id;
+			EXCEPTION WHEN unique_violation THEN
+				suffix := suffix || ' ';
+				CONTINUE;
+			END;
+			EXIT;
+		END LOOP;
+	END LOOP;
+
+    UPDATE acq.purchase_order SET owner = dest_usr WHERE owner = src_usr;
+    UPDATE acq.po_note SET creator = dest_usr WHERE creator = src_usr;
+    UPDATE acq.po_note SET editor = dest_usr WHERE editor = src_usr;
+    UPDATE acq.provider_note SET creator = dest_usr WHERE creator = src_usr;
+    UPDATE acq.provider_note SET editor = dest_usr WHERE editor = src_usr;
+    UPDATE acq.lineitem_note SET creator = dest_usr WHERE creator = src_usr;
+    UPDATE acq.lineitem_note SET editor = dest_usr WHERE editor = src_usr;
+    UPDATE acq.lineitem_usr_attr_definition SET usr = dest_usr WHERE usr = src_usr;
+
+    -- asset.*
+    UPDATE asset.copy SET creator = dest_usr WHERE creator = src_usr;
+    UPDATE asset.copy SET editor = dest_usr WHERE editor = src_usr;
+    UPDATE asset.copy_note SET creator = dest_usr WHERE creator = src_usr;
+    UPDATE asset.call_number SET creator = dest_usr WHERE creator = src_usr;
+    UPDATE asset.call_number SET editor = dest_usr WHERE editor = src_usr;
+    UPDATE asset.call_number_note SET creator = dest_usr WHERE creator = src_usr;
+
+    -- serial.*
+    UPDATE serial.record_entry SET creator = dest_usr WHERE creator = src_usr;
+    UPDATE serial.record_entry SET editor = dest_usr WHERE editor = src_usr;
+
+    -- reporter.*
+    -- It's not uncommon to define the reporter schema in a replica 
+    -- DB only, so don't assume these tables exist in the write DB.
+    BEGIN
+    	UPDATE reporter.template SET owner = dest_usr WHERE owner = src_usr;
+    EXCEPTION WHEN undefined_table THEN
+        -- do nothing
+    END;
+    BEGIN
+    	UPDATE reporter.report SET owner = dest_usr WHERE owner = src_usr;
+    EXCEPTION WHEN undefined_table THEN
+        -- do nothing
+    END;
+    BEGIN
+    	UPDATE reporter.schedule SET runner = dest_usr WHERE runner = src_usr;
+    EXCEPTION WHEN undefined_table THEN
+        -- do nothing
+    END;
+    BEGIN
+		-- transfer folders the same way we transfer buckets (see above)
+		FOR folder_row in
+			SELECT id, name
+			FROM   reporter.template_folder
+			WHERE  owner = src_usr
+		LOOP
+			suffix := ' (' || src_usr || ')';
+			LOOP
+				BEGIN
+					UPDATE  reporter.template_folder
+					SET     owner = dest_usr, name = name || suffix
+					WHERE   id = folder_row.id;
+				EXCEPTION WHEN unique_violation THEN
+					suffix := suffix || ' ';
+					CONTINUE;
+				END;
+				EXIT;
+			END LOOP;
+		END LOOP;
+    EXCEPTION WHEN undefined_table THEN
+        -- do nothing
+    END;
+    BEGIN
+		-- transfer folders the same way we transfer buckets (see above)
+		FOR folder_row in
+			SELECT id, name
+			FROM   reporter.report_folder
+			WHERE  owner = src_usr
+		LOOP
+			suffix := ' (' || src_usr || ')';
+			LOOP
+				BEGIN
+					UPDATE  reporter.report_folder
+					SET     owner = dest_usr, name = name || suffix
+					WHERE   id = folder_row.id;
+				EXCEPTION WHEN unique_violation THEN
+					suffix := suffix || ' ';
+					CONTINUE;
+				END;
+				EXIT;
+			END LOOP;
+		END LOOP;
+    EXCEPTION WHEN undefined_table THEN
+        -- do nothing
+    END;
+    BEGIN
+		-- transfer folders the same way we transfer buckets (see above)
+		FOR folder_row in
+			SELECT id, name
+			FROM   reporter.output_folder
+			WHERE  owner = src_usr
+		LOOP
+			suffix := ' (' || src_usr || ')';
+			LOOP
+				BEGIN
+					UPDATE  reporter.output_folder
+					SET     owner = dest_usr, name = name || suffix
+					WHERE   id = folder_row.id;
+				EXCEPTION WHEN unique_violation THEN
+					suffix := suffix || ' ';
+					CONTINUE;
+				END;
+				EXIT;
+			END LOOP;
+		END LOOP;
+    EXCEPTION WHEN undefined_table THEN
+        -- do nothing
+    END;
+
+    -- propagate preferred name values from the source user to the
+    -- destination user, but only when values are not being replaced.
+    WITH susr AS (SELECT * FROM actor.usr WHERE id = src_usr)
+    UPDATE actor.usr SET 
+        pref_prefix = 
+            COALESCE(pref_prefix, (SELECT pref_prefix FROM susr)),
+        pref_first_given_name = 
+            COALESCE(pref_first_given_name, (SELECT pref_first_given_name FROM susr)),
+        pref_second_given_name = 
+            COALESCE(pref_second_given_name, (SELECT pref_second_given_name FROM susr)),
+        pref_family_name = 
+            COALESCE(pref_family_name, (SELECT pref_family_name FROM susr)),
+        pref_suffix = 
+            COALESCE(pref_suffix, (SELECT pref_suffix FROM susr))
+    WHERE id = dest_usr;
+
+    -- Copy and deduplicate name keywords
+    -- String -> array -> rows -> DISTINCT -> array -> string
+    WITH susr AS (SELECT * FROM actor.usr WHERE id = src_usr),
+         dusr AS (SELECT * FROM actor.usr WHERE id = dest_usr)
+    UPDATE actor.usr SET name_keywords = (
+        WITH keywords AS (
+            SELECT DISTINCT UNNEST(
+                REGEXP_SPLIT_TO_ARRAY(
+                    COALESCE((SELECT name_keywords FROM susr), '') || ' ' ||
+                    COALESCE((SELECT name_keywords FROM dusr), ''),  E'\\s+'
+                )
+            ) AS parts
+        ) SELECT ARRAY_TO_STRING(ARRAY_AGG(kw.parts), ' ') FROM keywords kw
+    ) WHERE id = dest_usr;
+
+    -- Finally, delete the source user
+    DELETE FROM actor.usr WHERE id = src_usr;
+
+END;
+$$ LANGUAGE plpgsql;
+
+
+
+
+SELECT evergreen.upgrade_deps_block_check('1123', :eg_version);
+
+    ALTER TABLE config.rule_circ_duration
+    ADD column max_auto_renewals INTEGER;
+
+    ALTER TABLE action.circulation
+    ADD column auto_renewal BOOLEAN;
+
+    ALTER TABLE action.circulation
+    ADD column auto_renewal_remaining INTEGER;
+
+    ALTER TABLE action.aged_circulation
+    ADD column auto_renewal BOOLEAN;
+
+    ALTER TABLE action.aged_circulation
+    ADD column auto_renewal_remaining INTEGER;
+
+    INSERT INTO action_trigger.validator values('CircIsAutoRenewable', 'Checks whether the circulation is able to be autorenewed.');
+    INSERT INTO action_trigger.reactor values('Circ::AutoRenew', 'Auto-Renews a circulation.');
+    INSERT INTO action_trigger.hook(key, core_type, description) values('autorenewal', 'circ', 'Item was auto-renewed to patron.');
+
+    -- AutoRenewer A/T Def: 
+    INSERT INTO action_trigger.event_definition(active, owner, name, hook, validator, reactor, delay, max_delay, delay_field, group_field)
+        values (false, 1, 'Autorenew', 'checkout.due', 'CircIsOpen', 'Circ::AutoRenew', '-23 hours'::interval,'-1 minute'::interval, 'due_date', 'usr');
+
+    -- AutoRenewal outcome Email notifier A/T Def:
+    INSERT INTO action_trigger.event_definition(active, owner, name, hook, validator, reactor, group_field, template)
+        values (false, 1, 'AutorenewNotify', 'autorenewal', 'NOOP_True', 'SendEmail', 'usr', 
+$$
+[%- USE date -%]
+[%- user = target.0.usr -%]
+To: [%- params.recipient_email || user.email %]
+From: [%- params.sender_email || default_sender %]
+Date: [%- date.format(date.now, '%a, %d %b %Y %T -0000', gmt => 1) %]
+Subject: Items Out Auto-Renewal Notification 
+Auto-Submitted: auto-generated
+
+Dear [% user.family_name %], [% user.first_given_name %]
+An automatic renewal attempt was made for the following items:
+
+[% FOR circ IN target %]
+    [%- SET idx = loop.count - 1; SET udata =  user_data.$idx -%]
+    [%- SET cid = circ.target_copy || udata.copy -%]
+    [%- SET copy_details = helpers.get_copy_bib_basics(cid) -%]
+    Item# [% loop.count %]
+    Title: [% copy_details.title %]
+    Author: [% copy_details.author %]
+    [%- IF udata.is_renewed %]
+    Status: Loan Renewed
+    New Due Date: [% date.format(helpers.format_date(udata.new_due_date), '%Y-%m-%d') %]
+    [%- ELSE %]
+    Status: Not Renewed
+    Reason: [% udata.reason %]
+    Due Date: [% date.format(helpers.format_date(circ.due_date), '%Y-%m-%d') %]
+    [% END %]
+[% END %]
+$$
+    );
+
+    INSERT INTO action_trigger.environment (event_def, path ) VALUES
+    ( currval('action_trigger.event_definition_id_seq'), 'usr' ),
+    ( currval('action_trigger.event_definition_id_seq'), 'circ_lib' );
+
+
+DROP VIEW action.all_circulation;
+CREATE OR REPLACE VIEW action.all_circulation AS
+    SELECT  id,usr_post_code, usr_home_ou, usr_profile, usr_birth_year, copy_call_number, copy_location,
+        copy_owning_lib, copy_circ_lib, copy_bib_record, xact_start, xact_finish, target_copy,
+        circ_lib, circ_staff, checkin_staff, checkin_lib, renewal_remaining, grace_period, due_date,
+        stop_fines_time, checkin_time, create_time, duration, fine_interval, recurring_fine,
+        max_fine, phone_renewal, desk_renewal, opac_renewal, duration_rule, recurring_fine_rule,
+        max_fine_rule, stop_fines, workstation, checkin_workstation, checkin_scan_time, parent_circ,
+        auto_renewal, auto_renewal_remaining, NULL AS usr
+      FROM  action.aged_circulation
+            UNION ALL
+    SELECT  DISTINCT circ.id,COALESCE(a.post_code,b.post_code) AS usr_post_code, p.home_ou AS usr_home_ou, p.profile AS usr_profile, EXTRACT(YEAR FROM p.dob)::INT AS usr_birth_year,
+        cp.call_number AS copy_call_number, circ.copy_location, cn.owning_lib AS copy_owning_lib, cp.circ_lib AS copy_circ_lib,
+        cn.record AS copy_bib_record, circ.xact_start, circ.xact_finish, circ.target_copy, circ.circ_lib, circ.circ_staff, circ.checkin_staff,
+        circ.checkin_lib, circ.renewal_remaining, circ.grace_period, circ.due_date, circ.stop_fines_time, circ.checkin_time, circ.create_time, circ.duration,
+        circ.fine_interval, circ.recurring_fine, circ.max_fine, circ.phone_renewal, circ.desk_renewal, circ.opac_renewal, circ.duration_rule,
+        circ.recurring_fine_rule, circ.max_fine_rule, circ.stop_fines, circ.workstation, circ.checkin_workstation, circ.checkin_scan_time,
+        circ.parent_circ, circ.auto_renewal, circ.auto_renewal_remaining, circ.usr
+      FROM  action.circulation circ
+        JOIN asset.copy cp ON (circ.target_copy = cp.id)
+        JOIN asset.call_number cn ON (cp.call_number = cn.id)
+        JOIN actor.usr p ON (circ.usr = p.id)
+        LEFT JOIN actor.usr_address a ON (p.mailing_address = a.id)
+        LEFT JOIN actor.usr_address b ON (p.billing_address = b.id);
+
+
+DROP FUNCTION action.summarize_all_circ_chain (INTEGER);
+DROP FUNCTION action.all_circ_chain (INTEGER);
+
+-- rebuild slim circ view
+DROP VIEW action.all_circulation_slim;
+CREATE OR REPLACE VIEW action.all_circulation_slim AS
+    SELECT
+        id,
+        usr,
+        xact_start,
+        xact_finish,
+        unrecovered,
+        target_copy,
+        circ_lib,
+        circ_staff,
+        checkin_staff,
+        checkin_lib,
+        renewal_remaining,
+        grace_period,
+        due_date,
+        stop_fines_time,
+        checkin_time,
+        create_time,
+        duration,
+        fine_interval,
+        recurring_fine,
+        max_fine,
+        phone_renewal,
+        desk_renewal,
+        opac_renewal,
+        duration_rule,
+        recurring_fine_rule,
+        max_fine_rule,
+        stop_fines,
+        workstation,
+        checkin_workstation,
+        copy_location,
+        checkin_scan_time,
+        auto_renewal,
+        auto_renewal_remaining,
+        parent_circ
+    FROM action.circulation
+UNION ALL
+    SELECT
+        id,
+        NULL AS usr,
+        xact_start,
+        xact_finish,
+        unrecovered,
+        target_copy,
+        circ_lib,
+        circ_staff,
+        checkin_staff,
+        checkin_lib,
+        renewal_remaining,
+        grace_period,
+        due_date,
+        stop_fines_time,
+        checkin_time,
+        create_time,
+        duration,
+        fine_interval,
+        recurring_fine,
+        max_fine,
+        phone_renewal,
+        desk_renewal,
+        opac_renewal,
+        duration_rule,
+        recurring_fine_rule,
+        max_fine_rule,
+        stop_fines,
+        workstation,
+        checkin_workstation,
+        copy_location,
+        checkin_scan_time,
+        auto_renewal,
+        auto_renewal_remaining,
+        parent_circ
+    FROM action.aged_circulation
+;
+
+CREATE OR REPLACE FUNCTION action.all_circ_chain (ctx_circ_id INTEGER) 
+    RETURNS SETOF action.all_circulation_slim AS $$
+DECLARE
+    tmp_circ action.all_circulation_slim%ROWTYPE;
+    circ_0 action.all_circulation_slim%ROWTYPE;
+BEGIN
+
+    SELECT INTO tmp_circ * FROM action.all_circulation_slim WHERE id = ctx_circ_id;
+
+    IF tmp_circ IS NULL THEN
+        RETURN NEXT tmp_circ;
+    END IF;
+    circ_0 := tmp_circ;
+
+    -- find the front of the chain
+    WHILE TRUE LOOP
+        SELECT INTO tmp_circ * FROM action.all_circulation_slim 
+            WHERE id = tmp_circ.parent_circ;
+        IF tmp_circ IS NULL THEN
+            EXIT;
+        END IF;
+        circ_0 := tmp_circ;
+    END LOOP;
+
+    -- now send the circs to the caller, oldest to newest
+    tmp_circ := circ_0;
+    WHILE TRUE LOOP
+        IF tmp_circ IS NULL THEN
+            EXIT;
+        END IF;
+        RETURN NEXT tmp_circ;
+        SELECT INTO tmp_circ * FROM action.all_circulation_slim 
+            WHERE parent_circ = tmp_circ.id;
+    END LOOP;
+
+END;
+$$ LANGUAGE 'plpgsql';
+
+-- same as action.summarize_circ_chain, but returns data collected
+-- from action.all_circulation, which may include aged circulations.
+CREATE OR REPLACE FUNCTION action.summarize_all_circ_chain 
+    (ctx_circ_id INTEGER) RETURNS action.circ_chain_summary AS $$
+
+DECLARE
+
+    -- first circ in the chain
+    circ_0 action.all_circulation_slim%ROWTYPE;
+
+    -- last circ in the chain
+    circ_n action.all_circulation_slim%ROWTYPE;
+
+    -- circ chain under construction
+    chain action.circ_chain_summary;
+    tmp_circ action.all_circulation_slim%ROWTYPE;
+
+BEGIN
+    
+    chain.num_circs := 0;
+    FOR tmp_circ IN SELECT * FROM action.all_circ_chain(ctx_circ_id) LOOP
+
+        IF chain.num_circs = 0 THEN
+            circ_0 := tmp_circ;
+        END IF;
+
+        chain.num_circs := chain.num_circs + 1;
+        circ_n := tmp_circ;
+    END LOOP;
+
+    chain.start_time := circ_0.xact_start;
+    chain.last_stop_fines := circ_n.stop_fines;
+    chain.last_stop_fines_time := circ_n.stop_fines_time;
+    chain.last_checkin_time := circ_n.checkin_time;
+    chain.last_checkin_scan_time := circ_n.checkin_scan_time;
+    SELECT INTO chain.checkout_workstation name FROM actor.workstation WHERE id = circ_0.workstation;
+    SELECT INTO chain.last_checkin_workstation name FROM actor.workstation WHERE id = circ_n.checkin_workstation;
+
+    IF chain.num_circs > 1 THEN
+        chain.last_renewal_time := circ_n.xact_start;
+        SELECT INTO chain.last_renewal_workstation name FROM actor.workstation WHERE id = circ_n.workstation;
+    END IF;
+
+    RETURN chain;
+
+END;
+$$ LANGUAGE 'plpgsql';
+
+
+
+SELECT evergreen.upgrade_deps_block_check('1124', :eg_version);
+
+INSERT into config.workstation_setting_type (name, grp, datatype, label)
+VALUES (
+    'eg.grid.circ.wide_holds.shelf', 'gui', 'object',
+    oils_i18n_gettext (
+        'eg.grid.circ.wide_holds.shelf',
+        'Grid Config: circ.wide_holds.shelf',
+        'cwst', 'label'
+    )
+), (
+    'eg.grid.cat.catalog.wide_holds', 'gui', 'object',
+    oils_i18n_gettext(
+        'eg.grid.cat.catalog.wide_holds',
+        'Grid Config: cat.catalog.wide_holds',
+        'cwst', 'label'
+    )
+);
+
+DELETE from config.workstation_setting_type
+WHERE name = 'eg.grid.cat.catalog.holds' OR name = 'eg.grid.circ.holds.shelf';
+
+
+SELECT evergreen.upgrade_deps_block_check('1125', :eg_version);
+
+CREATE TABLE asset.latest_inventory (
+    id                          SERIAL                      PRIMARY KEY,
+    inventory_workstation       INTEGER                     REFERENCES actor.workstation (id) DEFERRABLE INITIALLY DEFERRED,
+    inventory_date              TIMESTAMP WITH TIME ZONE    DEFAULT NOW(),
+    copy                        BIGINT                      NOT NULL
+);
+CREATE INDEX latest_inventory_copy_idx ON asset.latest_inventory (copy);
+
+CREATE OR REPLACE FUNCTION evergreen.asset_latest_inventory_copy_inh_fkey() RETURNS TRIGGER AS $f$
+BEGIN
+        PERFORM 1 FROM asset.copy WHERE id = NEW.copy;
+        IF NOT FOUND THEN
+                RAISE foreign_key_violation USING MESSAGE = FORMAT(
+                        $$Referenced asset.copy id not found, copy:%s$$, NEW.copy
+                );
+        END IF;
+        RETURN NEW;
+END;
+$f$ LANGUAGE PLPGSQL VOLATILE COST 50;
+
+CREATE CONSTRAINT TRIGGER inherit_asset_latest_inventory_copy_fkey
+        AFTER UPDATE OR INSERT ON asset.latest_inventory
+        DEFERRABLE FOR EACH ROW EXECUTE PROCEDURE evergreen.asset_latest_inventory_copy_inh_fkey();
+
+INSERT into config.workstation_setting_type (name, grp, datatype, label)
+VALUES (
+    'eg.circ.checkin.do_inventory_update', 'circ', 'bool',
+    oils_i18n_gettext (
+             'eg.circ.checkin.do_inventory_update',
+             'Checkin: Update Inventory',
+             'cwst', 'label'
+    )
+);
+
+
+SELECT evergreen.upgrade_deps_block_check('1126', :eg_version);
+
+CREATE TABLE vandelay.session_tracker (
+    id          BIGSERIAL PRIMARY KEY,
+
+    -- string of characters (e.g. md5) used for linking trackers
+    -- of different actions into a series.  There can be multiple
+    -- session_keys of each action type, creating the opportunity
+    -- to link multiple action trackers into a single session.
+    session_key TEXT NOT NULL,
+
+    -- optional user-supplied name
+    name        TEXT NOT NULL, 
+
+    usr         INTEGER NOT NULL REFERENCES actor.usr(id)
+                DEFERRABLE INITIALLY DEFERRED,
+
+    -- org unit can be derived from WS
+    workstation INTEGER NOT NULL REFERENCES actor.workstation(id)
+                ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED,
+
+    -- bib/auth
+    record_type vandelay.bib_queue_queue_type NOT NULL DEFAULT 'bib',
+
+    -- Queue defines the source of the data, it does not necessarily
+    -- mean that an action is being performed against an entire queue.
+    -- E.g. some imports are misc. lists of record IDs, but they always 
+    -- come from one queue.
+    -- No foreign key -- could be auth or bib queue.
+    queue       BIGINT NOT NULL,
+
+    create_time TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
+    update_time TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
+
+    state       TEXT NOT NULL DEFAULT 'active',
+
+    action_type TEXT NOT NULL DEFAULT 'enqueue', -- import
+
+    -- total number of tasks to perform / loosely defined
+    -- could be # of recs to import or # of recs + # of copies 
+    -- depending on the import context
+    total_actions INTEGER NOT NULL DEFAULT 0,
+
+    -- total number of tasked performed so far
+    actions_performed INTEGER NOT NULL DEFAULT 0,
+
+    CONSTRAINT vand_tracker_valid_state 
+        CHECK (state IN ('active','error','complete')),
+
+    CONSTRAINT vand_tracker_valid_action_type
+        CHECK (action_type IN ('upload', 'enqueue', 'import'))
+);
+
+
+CREATE OR REPLACE FUNCTION actor.usr_merge( src_usr INT, dest_usr INT, del_addrs BOOLEAN, del_cards BOOLEAN, deactivate_cards BOOLEAN ) RETURNS VOID AS $$
+DECLARE
+	suffix TEXT;
+	bucket_row RECORD;
+	picklist_row RECORD;
+	queue_row RECORD;
+	folder_row RECORD;
+BEGIN
+
+    -- do some initial cleanup 
+    UPDATE actor.usr SET card = NULL WHERE id = src_usr;
+    UPDATE actor.usr SET mailing_address = NULL WHERE id = src_usr;
+    UPDATE actor.usr SET billing_address = NULL WHERE id = src_usr;
+
+    -- actor.*
+    IF del_cards THEN
+        DELETE FROM actor.card where usr = src_usr;
+    ELSE
+        IF deactivate_cards THEN
+            UPDATE actor.card SET active = 'f' WHERE usr = src_usr;
+        END IF;
+        UPDATE actor.card SET usr = dest_usr WHERE usr = src_usr;
+    END IF;
+
+
+    IF del_addrs THEN
+        DELETE FROM actor.usr_address WHERE usr = src_usr;
+    ELSE
+        UPDATE actor.usr_address SET usr = dest_usr WHERE usr = src_usr;
+    END IF;
+
+    UPDATE actor.usr_note SET usr = dest_usr WHERE usr = src_usr;
+    -- dupes are technically OK in actor.usr_standing_penalty, should manually delete them...
+    UPDATE actor.usr_standing_penalty SET usr = dest_usr WHERE usr = src_usr;
+    PERFORM actor.usr_merge_rows('actor.usr_org_unit_opt_in', 'usr', src_usr, dest_usr);
+    PERFORM actor.usr_merge_rows('actor.usr_setting', 'usr', src_usr, dest_usr);
+
+    -- permission.*
+    PERFORM actor.usr_merge_rows('permission.usr_perm_map', 'usr', src_usr, dest_usr);
+    PERFORM actor.usr_merge_rows('permission.usr_object_perm_map', 'usr', src_usr, dest_usr);
+    PERFORM actor.usr_merge_rows('permission.usr_grp_map', 'usr', src_usr, dest_usr);
+    PERFORM actor.usr_merge_rows('permission.usr_work_ou_map', 'usr', src_usr, dest_usr);
+
+
+    -- container.*
+	
+	-- For each *_bucket table: transfer every bucket belonging to src_usr
+	-- into the custody of dest_usr.
+	--
+	-- In order to avoid colliding with an existing bucket owned by
+	-- the destination user, append the source user's id (in parenthesese)
+	-- to the name.  If you still get a collision, add successive
+	-- spaces to the name and keep trying until you succeed.
+	--
+	FOR bucket_row in
+		SELECT id, name
+		FROM   container.biblio_record_entry_bucket
+		WHERE  owner = src_usr
+	LOOP
+		suffix := ' (' || src_usr || ')';
+		LOOP
+			BEGIN
+				UPDATE  container.biblio_record_entry_bucket
+				SET     owner = dest_usr, name = name || suffix
+				WHERE   id = bucket_row.id;
+			EXCEPTION WHEN unique_violation THEN
+				suffix := suffix || ' ';
+				CONTINUE;
+			END;
+			EXIT;
+		END LOOP;
+	END LOOP;
+
+	FOR bucket_row in
+		SELECT id, name
+		FROM   container.call_number_bucket
+		WHERE  owner = src_usr
+	LOOP
+		suffix := ' (' || src_usr || ')';
+		LOOP
+			BEGIN
+				UPDATE  container.call_number_bucket
+				SET     owner = dest_usr, name = name || suffix
+				WHERE   id = bucket_row.id;
+			EXCEPTION WHEN unique_violation THEN
+				suffix := suffix || ' ';
+				CONTINUE;
+			END;
+			EXIT;
+		END LOOP;
+	END LOOP;
+
+	FOR bucket_row in
+		SELECT id, name
+		FROM   container.copy_bucket
+		WHERE  owner = src_usr
+	LOOP
+		suffix := ' (' || src_usr || ')';
+		LOOP
+			BEGIN
+				UPDATE  container.copy_bucket
+				SET     owner = dest_usr, name = name || suffix
+				WHERE   id = bucket_row.id;
+			EXCEPTION WHEN unique_violation THEN
+				suffix := suffix || ' ';
+				CONTINUE;
+			END;
+			EXIT;
+		END LOOP;
+	END LOOP;
+
+	FOR bucket_row in
+		SELECT id, name
+		FROM   container.user_bucket
+		WHERE  owner = src_usr
+	LOOP
+		suffix := ' (' || src_usr || ')';
+		LOOP
+			BEGIN
+				UPDATE  container.user_bucket
+				SET     owner = dest_usr, name = name || suffix
+				WHERE   id = bucket_row.id;
+			EXCEPTION WHEN unique_violation THEN
+				suffix := suffix || ' ';
+				CONTINUE;
+			END;
+			EXIT;
+		END LOOP;
+	END LOOP;
+
+	UPDATE container.user_bucket_item SET target_user = dest_usr WHERE target_user = src_usr;
+
+    -- vandelay.*
+	-- transfer queues the same way we transfer buckets (see above)
+	FOR queue_row in
+		SELECT id, name
+		FROM   vandelay.queue
+		WHERE  owner = src_usr
+	LOOP
+		suffix := ' (' || src_usr || ')';
+		LOOP
+			BEGIN
+				UPDATE  vandelay.queue
+				SET     owner = dest_usr, name = name || suffix
+				WHERE   id = queue_row.id;
+			EXCEPTION WHEN unique_violation THEN
+				suffix := suffix || ' ';
+				CONTINUE;
+			END;
+			EXIT;
+		END LOOP;
+	END LOOP;
+
+    UPDATE vandelay.session_tracker SET usr = dest_usr WHERE usr = src_usr;
+
+    -- money.*
+    PERFORM actor.usr_merge_rows('money.collections_tracker', 'usr', src_usr, dest_usr);
+    PERFORM actor.usr_merge_rows('money.collections_tracker', 'collector', src_usr, dest_usr);
+    UPDATE money.billable_xact SET usr = dest_usr WHERE usr = src_usr;
+    UPDATE money.billing SET voider = dest_usr WHERE voider = src_usr;
+    UPDATE money.bnm_payment SET accepting_usr = dest_usr WHERE accepting_usr = src_usr;
+
+    -- action.*
+    UPDATE action.circulation SET usr = dest_usr WHERE usr = src_usr;
+    UPDATE action.circulation SET circ_staff = dest_usr WHERE circ_staff = src_usr;
+    UPDATE action.circulation SET checkin_staff = dest_usr WHERE checkin_staff = src_usr;
+    UPDATE action.usr_circ_history SET usr = dest_usr WHERE usr = src_usr;
+
+    UPDATE action.hold_request SET usr = dest_usr WHERE usr = src_usr;
+    UPDATE action.hold_request SET fulfillment_staff = dest_usr WHERE fulfillment_staff = src_usr;
+    UPDATE action.hold_request SET requestor = dest_usr WHERE requestor = src_usr;
+    UPDATE action.hold_notification SET notify_staff = dest_usr WHERE notify_staff = src_usr;
+
+    UPDATE action.in_house_use SET staff = dest_usr WHERE staff = src_usr;
+    UPDATE action.non_cataloged_circulation SET staff = dest_usr WHERE staff = src_usr;
+    UPDATE action.non_cataloged_circulation SET patron = dest_usr WHERE patron = src_usr;
+    UPDATE action.non_cat_in_house_use SET staff = dest_usr WHERE staff = src_usr;
+    UPDATE action.survey_response SET usr = dest_usr WHERE usr = src_usr;
+
+    -- acq.*
+    UPDATE acq.fund_allocation SET allocator = dest_usr WHERE allocator = src_usr;
+	UPDATE acq.fund_transfer SET transfer_user = dest_usr WHERE transfer_user = src_usr;
+
+	-- transfer picklists the same way we transfer buckets (see above)
+	FOR picklist_row in
+		SELECT id, name
+		FROM   acq.picklist
+		WHERE  owner = src_usr
+	LOOP
+		suffix := ' (' || src_usr || ')';
+		LOOP
+			BEGIN
+				UPDATE  acq.picklist
+				SET     owner = dest_usr, name = name || suffix
+				WHERE   id = picklist_row.id;
+			EXCEPTION WHEN unique_violation THEN
+				suffix := suffix || ' ';
+				CONTINUE;
+			END;
+			EXIT;
+		END LOOP;
+	END LOOP;
+
+    UPDATE acq.purchase_order SET owner = dest_usr WHERE owner = src_usr;
+    UPDATE acq.po_note SET creator = dest_usr WHERE creator = src_usr;
+    UPDATE acq.po_note SET editor = dest_usr WHERE editor = src_usr;
+    UPDATE acq.provider_note SET creator = dest_usr WHERE creator = src_usr;
+    UPDATE acq.provider_note SET editor = dest_usr WHERE editor = src_usr;
+    UPDATE acq.lineitem_note SET creator = dest_usr WHERE creator = src_usr;
+    UPDATE acq.lineitem_note SET editor = dest_usr WHERE editor = src_usr;
+    UPDATE acq.lineitem_usr_attr_definition SET usr = dest_usr WHERE usr = src_usr;
+
+    -- asset.*
+    UPDATE asset.copy SET creator = dest_usr WHERE creator = src_usr;
+    UPDATE asset.copy SET editor = dest_usr WHERE editor = src_usr;
+    UPDATE asset.copy_note SET creator = dest_usr WHERE creator = src_usr;
+    UPDATE asset.call_number SET creator = dest_usr WHERE creator = src_usr;
+    UPDATE asset.call_number SET editor = dest_usr WHERE editor = src_usr;
+    UPDATE asset.call_number_note SET creator = dest_usr WHERE creator = src_usr;
+
+    -- serial.*
+    UPDATE serial.record_entry SET creator = dest_usr WHERE creator = src_usr;
+    UPDATE serial.record_entry SET editor = dest_usr WHERE editor = src_usr;
+
+    -- reporter.*
+    -- It's not uncommon to define the reporter schema in a replica 
+    -- DB only, so don't assume these tables exist in the write DB.
+    BEGIN
+    	UPDATE reporter.template SET owner = dest_usr WHERE owner = src_usr;
+    EXCEPTION WHEN undefined_table THEN
+        -- do nothing
+    END;
+    BEGIN
+    	UPDATE reporter.report SET owner = dest_usr WHERE owner = src_usr;
+    EXCEPTION WHEN undefined_table THEN
+        -- do nothing
+    END;
+    BEGIN
+    	UPDATE reporter.schedule SET runner = dest_usr WHERE runner = src_usr;
+    EXCEPTION WHEN undefined_table THEN
+        -- do nothing
+    END;
+    BEGIN
+		-- transfer folders the same way we transfer buckets (see above)
+		FOR folder_row in
+			SELECT id, name
+			FROM   reporter.template_folder
+			WHERE  owner = src_usr
+		LOOP
+			suffix := ' (' || src_usr || ')';
+			LOOP
+				BEGIN
+					UPDATE  reporter.template_folder
+					SET     owner = dest_usr, name = name || suffix
+					WHERE   id = folder_row.id;
+				EXCEPTION WHEN unique_violation THEN
+					suffix := suffix || ' ';
+					CONTINUE;
+				END;
+				EXIT;
+			END LOOP;
+		END LOOP;
+    EXCEPTION WHEN undefined_table THEN
+        -- do nothing
+    END;
+    BEGIN
+		-- transfer folders the same way we transfer buckets (see above)
+		FOR folder_row in
+			SELECT id, name
+			FROM   reporter.report_folder
+			WHERE  owner = src_usr
+		LOOP
+			suffix := ' (' || src_usr || ')';
+			LOOP
+				BEGIN
+					UPDATE  reporter.report_folder
+					SET     owner = dest_usr, name = name || suffix
+					WHERE   id = folder_row.id;
+				EXCEPTION WHEN unique_violation THEN
+					suffix := suffix || ' ';
+					CONTINUE;
+				END;
+				EXIT;
+			END LOOP;
+		END LOOP;
+    EXCEPTION WHEN undefined_table THEN
+        -- do nothing
+    END;
+    BEGIN
+		-- transfer folders the same way we transfer buckets (see above)
+		FOR folder_row in
+			SELECT id, name
+			FROM   reporter.output_folder
+			WHERE  owner = src_usr
+		LOOP
+			suffix := ' (' || src_usr || ')';
+			LOOP
+				BEGIN
+					UPDATE  reporter.output_folder
+					SET     owner = dest_usr, name = name || suffix
+					WHERE   id = folder_row.id;
+				EXCEPTION WHEN unique_violation THEN
+					suffix := suffix || ' ';
+					CONTINUE;
+				END;
+				EXIT;
+			END LOOP;
+		END LOOP;
+    EXCEPTION WHEN undefined_table THEN
+        -- do nothing
+    END;
+
+    -- propagate preferred name values from the source user to the
+    -- destination user, but only when values are not being replaced.
+    WITH susr AS (SELECT * FROM actor.usr WHERE id = src_usr)
+    UPDATE actor.usr SET 
+        pref_prefix = 
+            COALESCE(pref_prefix, (SELECT pref_prefix FROM susr)),
+        pref_first_given_name = 
+            COALESCE(pref_first_given_name, (SELECT pref_first_given_name FROM susr)),
+        pref_second_given_name = 
+            COALESCE(pref_second_given_name, (SELECT pref_second_given_name FROM susr)),
+        pref_family_name = 
+            COALESCE(pref_family_name, (SELECT pref_family_name FROM susr)),
+        pref_suffix = 
+            COALESCE(pref_suffix, (SELECT pref_suffix FROM susr))
+    WHERE id = dest_usr;
+
+    -- Copy and deduplicate name keywords
+    -- String -> array -> rows -> DISTINCT -> array -> string
+    WITH susr AS (SELECT * FROM actor.usr WHERE id = src_usr),
+         dusr AS (SELECT * FROM actor.usr WHERE id = dest_usr)
+    UPDATE actor.usr SET name_keywords = (
+        WITH keywords AS (
+            SELECT DISTINCT UNNEST(
+                REGEXP_SPLIT_TO_ARRAY(
+                    COALESCE((SELECT name_keywords FROM susr), '') || ' ' ||
+                    COALESCE((SELECT name_keywords FROM dusr), ''),  E'\\s+'
+                )
+            ) AS parts
+        ) SELECT ARRAY_TO_STRING(ARRAY_AGG(kw.parts), ' ') FROM keywords kw
+    ) WHERE id = dest_usr;
+
+    -- Finally, delete the source user
+    DELETE FROM actor.usr WHERE id = src_usr;
+
+END;
+$$ LANGUAGE plpgsql;
+
+
+CREATE OR REPLACE FUNCTION actor.usr_purge_data(
+	src_usr  IN INTEGER,
+	specified_dest_usr IN INTEGER
+) RETURNS VOID AS $$
+DECLARE
+	suffix TEXT;
+	renamable_row RECORD;
+	dest_usr INTEGER;
+BEGIN
+
+	IF specified_dest_usr IS NULL THEN
+		dest_usr := 1; -- Admin user on stock installs
+	ELSE
+		dest_usr := specified_dest_usr;
+	END IF;
+
+	-- acq.*
+	UPDATE acq.fund_allocation SET allocator = dest_usr WHERE allocator = src_usr;
+	UPDATE acq.lineitem SET creator = dest_usr WHERE creator = src_usr;
+	UPDATE acq.lineitem SET editor = dest_usr WHERE editor = src_usr;
+	UPDATE acq.lineitem SET selector = dest_usr WHERE selector = src_usr;
+	UPDATE acq.lineitem_note SET creator = dest_usr WHERE creator = src_usr;
+	UPDATE acq.lineitem_note SET editor = dest_usr WHERE editor = src_usr;
+	DELETE FROM acq.lineitem_usr_attr_definition WHERE usr = src_usr;
+
+	-- Update with a rename to avoid collisions
+	FOR renamable_row in
+		SELECT id, name
+		FROM   acq.picklist
+		WHERE  owner = src_usr
+	LOOP
+		suffix := ' (' || src_usr || ')';
+		LOOP
+			BEGIN
+				UPDATE  acq.picklist
+				SET     owner = dest_usr, name = name || suffix
+				WHERE   id = renamable_row.id;
+			EXCEPTION WHEN unique_violation THEN
+				suffix := suffix || ' ';
+				CONTINUE;
+			END;
+			EXIT;
+		END LOOP;
+	END LOOP;
+
+	UPDATE acq.picklist SET creator = dest_usr WHERE creator = src_usr;
+	UPDATE acq.picklist SET editor = dest_usr WHERE editor = src_usr;
+	UPDATE acq.po_note SET creator = dest_usr WHERE creator = src_usr;
+	UPDATE acq.po_note SET editor = dest_usr WHERE editor = src_usr;
+	UPDATE acq.purchase_order SET owner = dest_usr WHERE owner = src_usr;
+	UPDATE acq.purchase_order SET creator = dest_usr WHERE creator = src_usr;
+	UPDATE acq.purchase_order SET editor = dest_usr WHERE editor = src_usr;
+	UPDATE acq.claim_event SET creator = dest_usr WHERE creator = src_usr;
+
+	-- action.*
+	DELETE FROM action.circulation WHERE usr = src_usr;
+	UPDATE action.circulation SET circ_staff = dest_usr WHERE circ_staff = src_usr;
+	UPDATE action.circulation SET checkin_staff = dest_usr WHERE checkin_staff = src_usr;
+	UPDATE action.hold_notification SET notify_staff = dest_usr WHERE notify_staff = src_usr;
+	UPDATE action.hold_request SET fulfillment_staff = dest_usr WHERE fulfillment_staff = src_usr;
+	UPDATE action.hold_request SET requestor = dest_usr WHERE requestor = src_usr;
+	DELETE FROM action.hold_request WHERE usr = src_usr;
+	UPDATE action.in_house_use SET staff = dest_usr WHERE staff = src_usr;
+	UPDATE action.non_cat_in_house_use SET staff = dest_usr WHERE staff = src_usr;
+	DELETE FROM action.non_cataloged_circulation WHERE patron = src_usr;
+	UPDATE action.non_cataloged_circulation SET staff = dest_usr WHERE staff = src_usr;
+	DELETE FROM action.survey_response WHERE usr = src_usr;
+	UPDATE action.fieldset SET owner = dest_usr WHERE owner = src_usr;
+	DELETE FROM action.usr_circ_history WHERE usr = src_usr;
+
+	-- actor.*
+	DELETE FROM actor.card WHERE usr = src_usr;
+	DELETE FROM actor.stat_cat_entry_usr_map WHERE target_usr = src_usr;
+
+	-- The following update is intended to avoid transient violations of a foreign
+	-- key constraint, whereby actor.usr_address references itself.  It may not be
+	-- necessary, but it does no harm.
+	UPDATE actor.usr_address SET replaces = NULL
+		WHERE usr = src_usr AND replaces IS NOT NULL;
+	DELETE FROM actor.usr_address WHERE usr = src_usr;
+	DELETE FROM actor.usr_note WHERE usr = src_usr;
+	UPDATE actor.usr_note SET creator = dest_usr WHERE creator = src_usr;
+	DELETE FROM actor.usr_org_unit_opt_in WHERE usr = src_usr;
+	UPDATE actor.usr_org_unit_opt_in SET staff = dest_usr WHERE staff = src_usr;
+	DELETE FROM actor.usr_setting WHERE usr = src_usr;
+	DELETE FROM actor.usr_standing_penalty WHERE usr = src_usr;
+	UPDATE actor.usr_standing_penalty SET staff = dest_usr WHERE staff = src_usr;
+
+	-- asset.*
+	UPDATE asset.call_number SET creator = dest_usr WHERE creator = src_usr;
+	UPDATE asset.call_number SET editor = dest_usr WHERE editor = src_usr;
+	UPDATE asset.call_number_note SET creator = dest_usr WHERE creator = src_usr;
+	UPDATE asset.copy SET creator = dest_usr WHERE creator = src_usr;
+	UPDATE asset.copy SET editor = dest_usr WHERE editor = src_usr;
+	UPDATE asset.copy_note SET creator = dest_usr WHERE creator = src_usr;
+
+	-- auditor.*
+	DELETE FROM auditor.actor_usr_address_history WHERE id = src_usr;
+	DELETE FROM auditor.actor_usr_history WHERE id = src_usr;
+	UPDATE auditor.asset_call_number_history SET creator = dest_usr WHERE creator = src_usr;
+	UPDATE auditor.asset_call_number_history SET editor  = dest_usr WHERE editor  = src_usr;
+	UPDATE auditor.asset_copy_history SET creator = dest_usr WHERE creator = src_usr;
+	UPDATE auditor.asset_copy_history SET editor  = dest_usr WHERE editor  = src_usr;
+	UPDATE auditor.biblio_record_entry_history SET creator = dest_usr WHERE creator = src_usr;
+	UPDATE auditor.biblio_record_entry_history SET editor  = dest_usr WHERE editor  = src_usr;
+
+	-- biblio.*
+	UPDATE biblio.record_entry SET creator = dest_usr WHERE creator = src_usr;
+	UPDATE biblio.record_entry SET editor = dest_usr WHERE editor = src_usr;
+	UPDATE biblio.record_note SET creator = dest_usr WHERE creator = src_usr;
+	UPDATE biblio.record_note SET editor = dest_usr WHERE editor = src_usr;
+
+	-- container.*
+	-- Update buckets with a rename to avoid collisions
+	FOR renamable_row in
+		SELECT id, name
+		FROM   container.biblio_record_entry_bucket
+		WHERE  owner = src_usr
+	LOOP
+		suffix := ' (' || src_usr || ')';
+		LOOP
+			BEGIN
+				UPDATE  container.biblio_record_entry_bucket
+				SET     owner = dest_usr, name = name || suffix
+				WHERE   id = renamable_row.id;
+			EXCEPTION WHEN unique_violation THEN
+				suffix := suffix || ' ';
+				CONTINUE;
+			END;
+			EXIT;
+		END LOOP;
+	END LOOP;
+
+	FOR renamable_row in
+		SELECT id, name
+		FROM   container.call_number_bucket
+		WHERE  owner = src_usr
+	LOOP
+		suffix := ' (' || src_usr || ')';
+		LOOP
+			BEGIN
+				UPDATE  container.call_number_bucket
+				SET     owner = dest_usr, name = name || suffix
+				WHERE   id = renamable_row.id;
+			EXCEPTION WHEN unique_violation THEN
+				suffix := suffix || ' ';
+				CONTINUE;
+			END;
+			EXIT;
+		END LOOP;
+	END LOOP;
+
+	FOR renamable_row in
+		SELECT id, name
+		FROM   container.copy_bucket
+		WHERE  owner = src_usr
+	LOOP
+		suffix := ' (' || src_usr || ')';
+		LOOP
+			BEGIN
+				UPDATE  container.copy_bucket
+				SET     owner = dest_usr, name = name || suffix
+				WHERE   id = renamable_row.id;
+			EXCEPTION WHEN unique_violation THEN
+				suffix := suffix || ' ';
+				CONTINUE;
+			END;
+			EXIT;
+		END LOOP;
+	END LOOP;
+
+	FOR renamable_row in
+		SELECT id, name
+		FROM   container.user_bucket
+		WHERE  owner = src_usr
+	LOOP
+		suffix := ' (' || src_usr || ')';
+		LOOP
+			BEGIN
+				UPDATE  container.user_bucket
+				SET     owner = dest_usr, name = name || suffix
+				WHERE   id = renamable_row.id;
+			EXCEPTION WHEN unique_violation THEN
+				suffix := suffix || ' ';
+				CONTINUE;
+			END;
+			EXIT;
+		END LOOP;
+	END LOOP;
+
+	DELETE FROM container.user_bucket_item WHERE target_user = src_usr;
+
+	-- money.*
+	DELETE FROM money.billable_xact WHERE usr = src_usr;
+	DELETE FROM money.collections_tracker WHERE usr = src_usr;
+	UPDATE money.collections_tracker SET collector = dest_usr WHERE collector = src_usr;
+
+	-- permission.*
+	DELETE FROM permission.usr_grp_map WHERE usr = src_usr;
+	DELETE FROM permission.usr_object_perm_map WHERE usr = src_usr;
+	DELETE FROM permission.usr_perm_map WHERE usr = src_usr;
+	DELETE FROM permission.usr_work_ou_map WHERE usr = src_usr;
+
+	-- reporter.*
+	-- Update with a rename to avoid collisions
+	BEGIN
+		FOR renamable_row in
+			SELECT id, name
+			FROM   reporter.output_folder
+			WHERE  owner = src_usr
+		LOOP
+			suffix := ' (' || src_usr || ')';
+			LOOP
+				BEGIN
+					UPDATE  reporter.output_folder
+					SET     owner = dest_usr, name = name || suffix
+					WHERE   id = renamable_row.id;
+				EXCEPTION WHEN unique_violation THEN
+					suffix := suffix || ' ';
+					CONTINUE;
+				END;
+				EXIT;
+			END LOOP;
+		END LOOP;
+	EXCEPTION WHEN undefined_table THEN
+		-- do nothing
+	END;
+
+	BEGIN
+		UPDATE reporter.report SET owner = dest_usr WHERE owner = src_usr;
+	EXCEPTION WHEN undefined_table THEN
+		-- do nothing
+	END;
+
+	-- Update with a rename to avoid collisions
+	BEGIN
+		FOR renamable_row in
+			SELECT id, name
+			FROM   reporter.report_folder
+			WHERE  owner = src_usr
+		LOOP
+			suffix := ' (' || src_usr || ')';
+			LOOP
+				BEGIN
+					UPDATE  reporter.report_folder
+					SET     owner = dest_usr, name = name || suffix
+					WHERE   id = renamable_row.id;
+				EXCEPTION WHEN unique_violation THEN
+					suffix := suffix || ' ';
+					CONTINUE;
+				END;
+				EXIT;
+			END LOOP;
+		END LOOP;
+	EXCEPTION WHEN undefined_table THEN
+		-- do nothing
+	END;
+
+	BEGIN
+		UPDATE reporter.schedule SET runner = dest_usr WHERE runner = src_usr;
+	EXCEPTION WHEN undefined_table THEN
+		-- do nothing
+	END;
+
+	BEGIN
+		UPDATE reporter.template SET owner = dest_usr WHERE owner = src_usr;
+	EXCEPTION WHEN undefined_table THEN
+		-- do nothing
+	END;
+
+	-- Update with a rename to avoid collisions
+	BEGIN
+		FOR renamable_row in
+			SELECT id, name
+			FROM   reporter.template_folder
+			WHERE  owner = src_usr
+		LOOP
+			suffix := ' (' || src_usr || ')';
+			LOOP
+				BEGIN
+					UPDATE  reporter.template_folder
+					SET     owner = dest_usr, name = name || suffix
+					WHERE   id = renamable_row.id;
+				EXCEPTION WHEN unique_violation THEN
+					suffix := suffix || ' ';
+					CONTINUE;
+				END;
+				EXIT;
+			END LOOP;
+		END LOOP;
+	EXCEPTION WHEN undefined_table THEN
+	-- do nothing
+	END;
+
+	-- vandelay.*
+	-- Update with a rename to avoid collisions
+	FOR renamable_row in
+		SELECT id, name
+		FROM   vandelay.queue
+		WHERE  owner = src_usr
+	LOOP
+		suffix := ' (' || src_usr || ')';
+		LOOP
+			BEGIN
+				UPDATE  vandelay.queue
+				SET     owner = dest_usr, name = name || suffix
+				WHERE   id = renamable_row.id;
+			EXCEPTION WHEN unique_violation THEN
+				suffix := suffix || ' ';
+				CONTINUE;
+			END;
+			EXIT;
+		END LOOP;
+	END LOOP;
+
+    UPDATE vandelay.session_tracker SET usr = dest_usr WHERE usr = src_usr;
+
+    -- NULL-ify addresses last so other cleanup (e.g. circ anonymization)
+    -- can access the information before deletion.
+	UPDATE actor.usr SET
+		active = FALSE,
+		card = NULL,
+		mailing_address = NULL,
+		billing_address = NULL
+	WHERE id = src_usr;
+
+END;
+$$ LANGUAGE plpgsql;
+
+
+
+SELECT evergreen.upgrade_deps_block_check('1127', :eg_version);
+
+ALTER TABLE acq.user_request ADD COLUMN cancel_time TIMESTAMPTZ;
+ALTER TABLE acq.user_request ADD COLUMN upc TEXT;
+ALTER TABLE action.hold_request ADD COLUMN acq_request INT REFERENCES acq.user_request (id);
+
+UPDATE
+    config.org_unit_setting_type
+SET
+    label = oils_i18n_gettext(
+        'circ.holds.canceled.display_age',
+        'Canceled holds/requests display age',
+        'coust', 'label'),
+    description = oils_i18n_gettext(
+        'circ.holds.canceled.display_age',
+        'Show all canceled entries in patron holds and patron acquisition requests interfaces that were canceled within this amount of time',
+        'coust', 'description')
+WHERE
+    name = 'circ.holds.canceled.display_age'
+;
+
+UPDATE
+    config.org_unit_setting_type
+SET
+    label = oils_i18n_gettext(
+        'circ.holds.canceled.display_count',
+        'Canceled holds/requests display count',
+        'coust', 'label'),
+    description = oils_i18n_gettext(
+        'circ.holds.canceled.display_count',
+        'How many canceled entries to show in patron holds and patron acquisition requests interfaces',
+        'coust', 'description')
+WHERE
+    name = 'circ.holds.canceled.display_count'
+;
+
+INSERT INTO acq.cancel_reason (org_unit, keep_debits, id, label, description)
+    VALUES (
+        1, 'f', 1015,
+        oils_i18n_gettext(1015, 'Canceled: Fulfilled', 'acqcr', 'label'),
+        oils_i18n_gettext(1015, 'This acquisition request has been fulfilled.', 'acqcr', 'description')
+    )
+;
+
+UPDATE
+    acq.user_request_type
+SET
+    label = oils_i18n_gettext('2', 'Articles', 'aurt', 'label')
+WHERE
+    id = 2
+;
+
+INSERT INTO acq.user_request_type (id,label)
+    SELECT 6, oils_i18n_gettext('6', 'Other', 'aurt', 'label');
+
+SELECT SETVAL('acq.user_request_type_id_seq'::TEXT, (SELECT MAX(id)+1 FROM acq.user_request_type));
+
+INSERT INTO permission.perm_list ( id, code, description ) VALUES
+ ( 610, 'CLEAR_PURCHASE_REQUEST', oils_i18n_gettext(610,
+    'Clear Completed User Purchase Requests', 'ppl', 'description'))
+;
+
+CREATE TABLE acq.user_request_status_type (
+     id  SERIAL  PRIMARY KEY
+    ,label TEXT
+);
+
+INSERT INTO acq.user_request_status_type (id,label) VALUES
+     (0,oils_i18n_gettext(0,'Error','aurst','label'))
+    ,(1,oils_i18n_gettext(1,'New','aurst','label'))
+    ,(2,oils_i18n_gettext(2,'Pending','aurst','label'))
+    ,(3,oils_i18n_gettext(3,'Ordered, Hold Not Placed','aurst','label'))
+    ,(4,oils_i18n_gettext(4,'Ordered, Hold Placed','aurst','label'))
+    ,(5,oils_i18n_gettext(5,'Received','aurst','label'))
+    ,(6,oils_i18n_gettext(6,'Fulfilled','aurst','label'))
+    ,(7,oils_i18n_gettext(7,'Canceled','aurst','label'))
+;
+
+SELECT SETVAL('acq.user_request_status_type_id_seq'::TEXT, 100);
+
+-- not used
+DELETE FROM actor.org_unit_setting WHERE name = 'acq.holds.allow_holds_from_purchase_request';
+DELETE FROM config.org_unit_setting_type_log WHERE field_name = 'acq.holds.allow_holds_from_purchase_request';
+DELETE FROM config.org_unit_setting_type WHERE name = 'acq.holds.allow_holds_from_purchase_request';
+
+
+SELECT evergreen.upgrade_deps_block_check('1128', :eg_version);
+
+DROP VIEW auditor.acq_invoice_lifecycle;
+
+ALTER TABLE acq.invoice
+    ADD COLUMN close_date TIMESTAMPTZ,
+    ADD COLUMN closed_by  INTEGER 
+        REFERENCES actor.usr (id) DEFERRABLE INITIALLY DEFERRED;
+
+-- duplicate steps for auditor table
+ALTER TABLE auditor.acq_invoice_history
+    ADD COLUMN close_date TIMESTAMPTZ,
+    ADD COLUMN closed_by  INTEGER;
+
+UPDATE acq.invoice SET close_date = NOW() WHERE complete;
+UPDATE auditor.acq_invoice_history SET close_date = NOW() WHERE complete;
+
+ALTER TABLE acq.invoice DROP COLUMN complete;
+ALTER TABLE auditor.acq_invoice_history DROP COLUMN complete;
+
+-- this recreates auditor.acq_invoice_lifecycle;
+SELECT auditor.update_auditors();
+
+CREATE OR REPLACE FUNCTION actor.usr_merge( src_usr INT, dest_usr INT, del_addrs BOOLEAN, del_cards BOOLEAN, deactivate_cards BOOLEAN ) RETURNS VOID AS $$
+DECLARE
+	suffix TEXT;
+	bucket_row RECORD;
+	picklist_row RECORD;
+	queue_row RECORD;
+	folder_row RECORD;
+BEGIN
+
+    -- do some initial cleanup 
+    UPDATE actor.usr SET card = NULL WHERE id = src_usr;
+    UPDATE actor.usr SET mailing_address = NULL WHERE id = src_usr;
+    UPDATE actor.usr SET billing_address = NULL WHERE id = src_usr;
+
+    -- actor.*
+    IF del_cards THEN
+        DELETE FROM actor.card where usr = src_usr;
+    ELSE
+        IF deactivate_cards THEN
+            UPDATE actor.card SET active = 'f' WHERE usr = src_usr;
+        END IF;
+        UPDATE actor.card SET usr = dest_usr WHERE usr = src_usr;
+    END IF;
+
+
+    IF del_addrs THEN
+        DELETE FROM actor.usr_address WHERE usr = src_usr;
+    ELSE
+        UPDATE actor.usr_address SET usr = dest_usr WHERE usr = src_usr;
+    END IF;
+
+    UPDATE actor.usr_note SET usr = dest_usr WHERE usr = src_usr;
+    -- dupes are technically OK in actor.usr_standing_penalty, should manually delete them...
+    UPDATE actor.usr_standing_penalty SET usr = dest_usr WHERE usr = src_usr;
+    PERFORM actor.usr_merge_rows('actor.usr_org_unit_opt_in', 'usr', src_usr, dest_usr);
+    PERFORM actor.usr_merge_rows('actor.usr_setting', 'usr', src_usr, dest_usr);
+
+    -- permission.*
+    PERFORM actor.usr_merge_rows('permission.usr_perm_map', 'usr', src_usr, dest_usr);
+    PERFORM actor.usr_merge_rows('permission.usr_object_perm_map', 'usr', src_usr, dest_usr);
+    PERFORM actor.usr_merge_rows('permission.usr_grp_map', 'usr', src_usr, dest_usr);
+    PERFORM actor.usr_merge_rows('permission.usr_work_ou_map', 'usr', src_usr, dest_usr);
+
+
+    -- container.*
+	
+	-- For each *_bucket table: transfer every bucket belonging to src_usr
+	-- into the custody of dest_usr.
+	--
+	-- In order to avoid colliding with an existing bucket owned by
+	-- the destination user, append the source user's id (in parenthesese)
+	-- to the name.  If you still get a collision, add successive
+	-- spaces to the name and keep trying until you succeed.
+	--
+	FOR bucket_row in
+		SELECT id, name
+		FROM   container.biblio_record_entry_bucket
+		WHERE  owner = src_usr
+	LOOP
+		suffix := ' (' || src_usr || ')';
+		LOOP
+			BEGIN
+				UPDATE  container.biblio_record_entry_bucket
+				SET     owner = dest_usr, name = name || suffix
+				WHERE   id = bucket_row.id;
+			EXCEPTION WHEN unique_violation THEN
+				suffix := suffix || ' ';
+				CONTINUE;
+			END;
+			EXIT;
+		END LOOP;
+	END LOOP;
+
+	FOR bucket_row in
+		SELECT id, name
+		FROM   container.call_number_bucket
+		WHERE  owner = src_usr
+	LOOP
+		suffix := ' (' || src_usr || ')';
+		LOOP
+			BEGIN
+				UPDATE  container.call_number_bucket
+				SET     owner = dest_usr, name = name || suffix
+				WHERE   id = bucket_row.id;
+			EXCEPTION WHEN unique_violation THEN
+				suffix := suffix || ' ';
+				CONTINUE;
+			END;
+			EXIT;
+		END LOOP;
+	END LOOP;
+
+	FOR bucket_row in
+		SELECT id, name
+		FROM   container.copy_bucket
+		WHERE  owner = src_usr
+	LOOP
+		suffix := ' (' || src_usr || ')';
+		LOOP
+			BEGIN
+				UPDATE  container.copy_bucket
+				SET     owner = dest_usr, name = name || suffix
+				WHERE   id = bucket_row.id;
+			EXCEPTION WHEN unique_violation THEN
+				suffix := suffix || ' ';
+				CONTINUE;
+			END;
+			EXIT;
+		END LOOP;
+	END LOOP;
+
+	FOR bucket_row in
+		SELECT id, name
+		FROM   container.user_bucket
+		WHERE  owner = src_usr
+	LOOP
+		suffix := ' (' || src_usr || ')';
+		LOOP
+			BEGIN
+				UPDATE  container.user_bucket
+				SET     owner = dest_usr, name = name || suffix
+				WHERE   id = bucket_row.id;
+			EXCEPTION WHEN unique_violation THEN
+				suffix := suffix || ' ';
+				CONTINUE;
+			END;
+			EXIT;
+		END LOOP;
+	END LOOP;
+
+	UPDATE container.user_bucket_item SET target_user = dest_usr WHERE target_user = src_usr;
+
+    -- vandelay.*
+	-- transfer queues the same way we transfer buckets (see above)
+	FOR queue_row in
+		SELECT id, name
+		FROM   vandelay.queue
+		WHERE  owner = src_usr
+	LOOP
+		suffix := ' (' || src_usr || ')';
+		LOOP
+			BEGIN
+				UPDATE  vandelay.queue
+				SET     owner = dest_usr, name = name || suffix
+				WHERE   id = queue_row.id;
+			EXCEPTION WHEN unique_violation THEN
+				suffix := suffix || ' ';
+				CONTINUE;
+			END;
+			EXIT;
+		END LOOP;
+	END LOOP;
+
+    -- money.*
+    PERFORM actor.usr_merge_rows('money.collections_tracker', 'usr', src_usr, dest_usr);
+    PERFORM actor.usr_merge_rows('money.collections_tracker', 'collector', src_usr, dest_usr);
+    UPDATE money.billable_xact SET usr = dest_usr WHERE usr = src_usr;
+    UPDATE money.billing SET voider = dest_usr WHERE voider = src_usr;
+    UPDATE money.bnm_payment SET accepting_usr = dest_usr WHERE accepting_usr = src_usr;
+
+    -- action.*
+    UPDATE action.circulation SET usr = dest_usr WHERE usr = src_usr;
+    UPDATE action.circulation SET circ_staff = dest_usr WHERE circ_staff = src_usr;
+    UPDATE action.circulation SET checkin_staff = dest_usr WHERE checkin_staff = src_usr;
+    UPDATE action.usr_circ_history SET usr = dest_usr WHERE usr = src_usr;
+
+    UPDATE action.hold_request SET usr = dest_usr WHERE usr = src_usr;
+    UPDATE action.hold_request SET fulfillment_staff = dest_usr WHERE fulfillment_staff = src_usr;
+    UPDATE action.hold_request SET requestor = dest_usr WHERE requestor = src_usr;
+    UPDATE action.hold_notification SET notify_staff = dest_usr WHERE notify_staff = src_usr;
+
+    UPDATE action.in_house_use SET staff = dest_usr WHERE staff = src_usr;
+    UPDATE action.non_cataloged_circulation SET staff = dest_usr WHERE staff = src_usr;
+    UPDATE action.non_cataloged_circulation SET patron = dest_usr WHERE patron = src_usr;
+    UPDATE action.non_cat_in_house_use SET staff = dest_usr WHERE staff = src_usr;
+    UPDATE action.survey_response SET usr = dest_usr WHERE usr = src_usr;
+
+    -- acq.*
+    UPDATE acq.fund_allocation SET allocator = dest_usr WHERE allocator = src_usr;
+	UPDATE acq.fund_transfer SET transfer_user = dest_usr WHERE transfer_user = src_usr;
+    UPDATE acq.invoice SET closed_by = dest_usr WHERE closed_by = src_usr;
+
+	-- transfer picklists the same way we transfer buckets (see above)
+	FOR picklist_row in
+		SELECT id, name
+		FROM   acq.picklist
+		WHERE  owner = src_usr
+	LOOP
+		suffix := ' (' || src_usr || ')';
+		LOOP
+			BEGIN
+				UPDATE  acq.picklist
+				SET     owner = dest_usr, name = name || suffix
+				WHERE   id = picklist_row.id;
+			EXCEPTION WHEN unique_violation THEN
+				suffix := suffix || ' ';
+				CONTINUE;
+			END;
+			EXIT;
+		END LOOP;
+	END LOOP;
+
+    UPDATE acq.purchase_order SET owner = dest_usr WHERE owner = src_usr;
+    UPDATE acq.po_note SET creator = dest_usr WHERE creator = src_usr;
+    UPDATE acq.po_note SET editor = dest_usr WHERE editor = src_usr;
+    UPDATE acq.provider_note SET creator = dest_usr WHERE creator = src_usr;
+    UPDATE acq.provider_note SET editor = dest_usr WHERE editor = src_usr;
+    UPDATE acq.lineitem_note SET creator = dest_usr WHERE creator = src_usr;
+    UPDATE acq.lineitem_note SET editor = dest_usr WHERE editor = src_usr;
+    UPDATE acq.lineitem_usr_attr_definition SET usr = dest_usr WHERE usr = src_usr;
+
+    -- asset.*
+    UPDATE asset.copy SET creator = dest_usr WHERE creator = src_usr;
+    UPDATE asset.copy SET editor = dest_usr WHERE editor = src_usr;
+    UPDATE asset.copy_note SET creator = dest_usr WHERE creator = src_usr;
+    UPDATE asset.call_number SET creator = dest_usr WHERE creator = src_usr;
+    UPDATE asset.call_number SET editor = dest_usr WHERE editor = src_usr;
+    UPDATE asset.call_number_note SET creator = dest_usr WHERE creator = src_usr;
+
+    -- serial.*
+    UPDATE serial.record_entry SET creator = dest_usr WHERE creator = src_usr;
+    UPDATE serial.record_entry SET editor = dest_usr WHERE editor = src_usr;
+
+    -- reporter.*
+    -- It's not uncommon to define the reporter schema in a replica 
+    -- DB only, so don't assume these tables exist in the write DB.
+    BEGIN
+    	UPDATE reporter.template SET owner = dest_usr WHERE owner = src_usr;
+    EXCEPTION WHEN undefined_table THEN
+        -- do nothing
+    END;
+    BEGIN
+    	UPDATE reporter.report SET owner = dest_usr WHERE owner = src_usr;
+    EXCEPTION WHEN undefined_table THEN
+        -- do nothing
+    END;
+    BEGIN
+    	UPDATE reporter.schedule SET runner = dest_usr WHERE runner = src_usr;
+    EXCEPTION WHEN undefined_table THEN
+        -- do nothing
+    END;
+    BEGIN
+		-- transfer folders the same way we transfer buckets (see above)
+		FOR folder_row in
+			SELECT id, name
+			FROM   reporter.template_folder
+			WHERE  owner = src_usr
+		LOOP
+			suffix := ' (' || src_usr || ')';
+			LOOP
+				BEGIN
+					UPDATE  reporter.template_folder
+					SET     owner = dest_usr, name = name || suffix
+					WHERE   id = folder_row.id;
+				EXCEPTION WHEN unique_violation THEN
+					suffix := suffix || ' ';
+					CONTINUE;
+				END;
+				EXIT;
+			END LOOP;
+		END LOOP;
+    EXCEPTION WHEN undefined_table THEN
+        -- do nothing
+    END;
+    BEGIN
+		-- transfer folders the same way we transfer buckets (see above)
+		FOR folder_row in
+			SELECT id, name
+			FROM   reporter.report_folder
+			WHERE  owner = src_usr
+		LOOP
+			suffix := ' (' || src_usr || ')';
+			LOOP
+				BEGIN
+					UPDATE  reporter.report_folder
+					SET     owner = dest_usr, name = name || suffix
+					WHERE   id = folder_row.id;
+				EXCEPTION WHEN unique_violation THEN
+					suffix := suffix || ' ';
+					CONTINUE;
+				END;
+				EXIT;
+			END LOOP;
+		END LOOP;
+    EXCEPTION WHEN undefined_table THEN
+        -- do nothing
+    END;
+    BEGIN
+		-- transfer folders the same way we transfer buckets (see above)
+		FOR folder_row in
+			SELECT id, name
+			FROM   reporter.output_folder
+			WHERE  owner = src_usr
+		LOOP
+			suffix := ' (' || src_usr || ')';
+			LOOP
+				BEGIN
+					UPDATE  reporter.output_folder
+					SET     owner = dest_usr, name = name || suffix
+					WHERE   id = folder_row.id;
+				EXCEPTION WHEN unique_violation THEN
+					suffix := suffix || ' ';
+					CONTINUE;
+				END;
+				EXIT;
+			END LOOP;
+		END LOOP;
+    EXCEPTION WHEN undefined_table THEN
+        -- do nothing
+    END;
+
+    -- Finally, delete the source user
+    DELETE FROM actor.usr WHERE id = src_usr;
+
+END;
+$$ LANGUAGE plpgsql;
+
+
+CREATE OR REPLACE FUNCTION actor.usr_purge_data(
+	src_usr  IN INTEGER,
+	specified_dest_usr IN INTEGER
+) RETURNS VOID AS $$
+DECLARE
+	suffix TEXT;
+	renamable_row RECORD;
+	dest_usr INTEGER;
+BEGIN
+
+	IF specified_dest_usr IS NULL THEN
+		dest_usr := 1; -- Admin user on stock installs
+	ELSE
+		dest_usr := specified_dest_usr;
+	END IF;
+
+	-- acq.*
+	UPDATE acq.fund_allocation SET allocator = dest_usr WHERE allocator = src_usr;
+	UPDATE acq.lineitem SET creator = dest_usr WHERE creator = src_usr;
+	UPDATE acq.lineitem SET editor = dest_usr WHERE editor = src_usr;
+	UPDATE acq.lineitem SET selector = dest_usr WHERE selector = src_usr;
+	UPDATE acq.lineitem_note SET creator = dest_usr WHERE creator = src_usr;
+	UPDATE acq.lineitem_note SET editor = dest_usr WHERE editor = src_usr;
+    UPDATE acq.invoice SET closed_by = dest_usr WHERE closed_by = src_usr;
+	DELETE FROM acq.lineitem_usr_attr_definition WHERE usr = src_usr;
+
+	-- Update with a rename to avoid collisions
+	FOR renamable_row in
+		SELECT id, name
+		FROM   acq.picklist
+		WHERE  owner = src_usr
+	LOOP
+		suffix := ' (' || src_usr || ')';
+		LOOP
+			BEGIN
+				UPDATE  acq.picklist
+				SET     owner = dest_usr, name = name || suffix
+				WHERE   id = renamable_row.id;
+			EXCEPTION WHEN unique_violation THEN
+				suffix := suffix || ' ';
+				CONTINUE;
+			END;
+			EXIT;
+		END LOOP;
+	END LOOP;
+
+	UPDATE acq.picklist SET creator = dest_usr WHERE creator = src_usr;
+	UPDATE acq.picklist SET editor = dest_usr WHERE editor = src_usr;
+	UPDATE acq.po_note SET creator = dest_usr WHERE creator = src_usr;
+	UPDATE acq.po_note SET editor = dest_usr WHERE editor = src_usr;
+	UPDATE acq.purchase_order SET owner = dest_usr WHERE owner = src_usr;
+	UPDATE acq.purchase_order SET creator = dest_usr WHERE creator = src_usr;
+	UPDATE acq.purchase_order SET editor = dest_usr WHERE editor = src_usr;
+	UPDATE acq.claim_event SET creator = dest_usr WHERE creator = src_usr;
+
+	-- action.*
+	DELETE FROM action.circulation WHERE usr = src_usr;
+	UPDATE action.circulation SET circ_staff = dest_usr WHERE circ_staff = src_usr;
+	UPDATE action.circulation SET checkin_staff = dest_usr WHERE checkin_staff = src_usr;
+	UPDATE action.hold_notification SET notify_staff = dest_usr WHERE notify_staff = src_usr;
+	UPDATE action.hold_request SET fulfillment_staff = dest_usr WHERE fulfillment_staff = src_usr;
+	UPDATE action.hold_request SET requestor = dest_usr WHERE requestor = src_usr;
+	DELETE FROM action.hold_request WHERE usr = src_usr;
+	UPDATE action.in_house_use SET staff = dest_usr WHERE staff = src_usr;
+	UPDATE action.non_cat_in_house_use SET staff = dest_usr WHERE staff = src_usr;
+	DELETE FROM action.non_cataloged_circulation WHERE patron = src_usr;
+	UPDATE action.non_cataloged_circulation SET staff = dest_usr WHERE staff = src_usr;
+	DELETE FROM action.survey_response WHERE usr = src_usr;
+	UPDATE action.fieldset SET owner = dest_usr WHERE owner = src_usr;
+	DELETE FROM action.usr_circ_history WHERE usr = src_usr;
+
+	-- actor.*
+	DELETE FROM actor.card WHERE usr = src_usr;
+	DELETE FROM actor.stat_cat_entry_usr_map WHERE target_usr = src_usr;
+
+	-- The following update is intended to avoid transient violations of a foreign
+	-- key constraint, whereby actor.usr_address references itself.  It may not be
+	-- necessary, but it does no harm.
+	UPDATE actor.usr_address SET replaces = NULL
+		WHERE usr = src_usr AND replaces IS NOT NULL;
+	DELETE FROM actor.usr_address WHERE usr = src_usr;
+	DELETE FROM actor.usr_note WHERE usr = src_usr;
+	UPDATE actor.usr_note SET creator = dest_usr WHERE creator = src_usr;
+	DELETE FROM actor.usr_org_unit_opt_in WHERE usr = src_usr;
+	UPDATE actor.usr_org_unit_opt_in SET staff = dest_usr WHERE staff = src_usr;
+	DELETE FROM actor.usr_setting WHERE usr = src_usr;
+	DELETE FROM actor.usr_standing_penalty WHERE usr = src_usr;
+	UPDATE actor.usr_standing_penalty SET staff = dest_usr WHERE staff = src_usr;
+
+	-- asset.*
+	UPDATE asset.call_number SET creator = dest_usr WHERE creator = src_usr;
+	UPDATE asset.call_number SET editor = dest_usr WHERE editor = src_usr;
+	UPDATE asset.call_number_note SET creator = dest_usr WHERE creator = src_usr;
+	UPDATE asset.copy SET creator = dest_usr WHERE creator = src_usr;
+	UPDATE asset.copy SET editor = dest_usr WHERE editor = src_usr;
+	UPDATE asset.copy_note SET creator = dest_usr WHERE creator = src_usr;
+
+	-- auditor.*
+	DELETE FROM auditor.actor_usr_address_history WHERE id = src_usr;
+	DELETE FROM auditor.actor_usr_history WHERE id = src_usr;
+	UPDATE auditor.asset_call_number_history SET creator = dest_usr WHERE creator = src_usr;
+	UPDATE auditor.asset_call_number_history SET editor  = dest_usr WHERE editor  = src_usr;
+	UPDATE auditor.asset_copy_history SET creator = dest_usr WHERE creator = src_usr;
+	UPDATE auditor.asset_copy_history SET editor  = dest_usr WHERE editor  = src_usr;
+	UPDATE auditor.biblio_record_entry_history SET creator = dest_usr WHERE creator = src_usr;
+	UPDATE auditor.biblio_record_entry_history SET editor  = dest_usr WHERE editor  = src_usr;
+
+	-- biblio.*
+	UPDATE biblio.record_entry SET creator = dest_usr WHERE creator = src_usr;
+	UPDATE biblio.record_entry SET editor = dest_usr WHERE editor = src_usr;
+	UPDATE biblio.record_note SET creator = dest_usr WHERE creator = src_usr;
+	UPDATE biblio.record_note SET editor = dest_usr WHERE editor = src_usr;
+
+	-- container.*
+	-- Update buckets with a rename to avoid collisions
+	FOR renamable_row in
+		SELECT id, name
+		FROM   container.biblio_record_entry_bucket
+		WHERE  owner = src_usr
+	LOOP
+		suffix := ' (' || src_usr || ')';
+		LOOP
+			BEGIN
+				UPDATE  container.biblio_record_entry_bucket
+				SET     owner = dest_usr, name = name || suffix
+				WHERE   id = renamable_row.id;
+			EXCEPTION WHEN unique_violation THEN
+				suffix := suffix || ' ';
+				CONTINUE;
+			END;
+			EXIT;
+		END LOOP;
+	END LOOP;
+
+	FOR renamable_row in
+		SELECT id, name
+		FROM   container.call_number_bucket
+		WHERE  owner = src_usr
+	LOOP
+		suffix := ' (' || src_usr || ')';
+		LOOP
+			BEGIN
+				UPDATE  container.call_number_bucket
+				SET     owner = dest_usr, name = name || suffix
+				WHERE   id = renamable_row.id;
+			EXCEPTION WHEN unique_violation THEN
+				suffix := suffix || ' ';
+				CONTINUE;
+			END;
+			EXIT;
+		END LOOP;
+	END LOOP;
+
+	FOR renamable_row in
+		SELECT id, name
+		FROM   container.copy_bucket
+		WHERE  owner = src_usr
+	LOOP
+		suffix := ' (' || src_usr || ')';
+		LOOP
+			BEGIN
+				UPDATE  container.copy_bucket
+				SET     owner = dest_usr, name = name || suffix
+				WHERE   id = renamable_row.id;
+			EXCEPTION WHEN unique_violation THEN
+				suffix := suffix || ' ';
+				CONTINUE;
+			END;
+			EXIT;
+		END LOOP;
+	END LOOP;
+
+	FOR renamable_row in
+		SELECT id, name
+		FROM   container.user_bucket
+		WHERE  owner = src_usr
+	LOOP
+		suffix := ' (' || src_usr || ')';
+		LOOP
+			BEGIN
+				UPDATE  container.user_bucket
+				SET     owner = dest_usr, name = name || suffix
+				WHERE   id = renamable_row.id;
+			EXCEPTION WHEN unique_violation THEN
+				suffix := suffix || ' ';
+				CONTINUE;
+			END;
+			EXIT;
+		END LOOP;
+	END LOOP;
+
+	DELETE FROM container.user_bucket_item WHERE target_user = src_usr;
+
+	-- money.*
+	DELETE FROM money.billable_xact WHERE usr = src_usr;
+	DELETE FROM money.collections_tracker WHERE usr = src_usr;
+	UPDATE money.collections_tracker SET collector = dest_usr WHERE collector = src_usr;
+
+	-- permission.*
+	DELETE FROM permission.usr_grp_map WHERE usr = src_usr;
+	DELETE FROM permission.usr_object_perm_map WHERE usr = src_usr;
+	DELETE FROM permission.usr_perm_map WHERE usr = src_usr;
+	DELETE FROM permission.usr_work_ou_map WHERE usr = src_usr;
+
+	-- reporter.*
+	-- Update with a rename to avoid collisions
+	BEGIN
+		FOR renamable_row in
+			SELECT id, name
+			FROM   reporter.output_folder
+			WHERE  owner = src_usr
+		LOOP
+			suffix := ' (' || src_usr || ')';
+			LOOP
+				BEGIN
+					UPDATE  reporter.output_folder
+					SET     owner = dest_usr, name = name || suffix
+					WHERE   id = renamable_row.id;
+				EXCEPTION WHEN unique_violation THEN
+					suffix := suffix || ' ';
+					CONTINUE;
+				END;
+				EXIT;
+			END LOOP;
+		END LOOP;
+	EXCEPTION WHEN undefined_table THEN
+		-- do nothing
+	END;
+
+	BEGIN
+		UPDATE reporter.report SET owner = dest_usr WHERE owner = src_usr;
+	EXCEPTION WHEN undefined_table THEN
+		-- do nothing
+	END;
+
+	-- Update with a rename to avoid collisions
+	BEGIN
+		FOR renamable_row in
+			SELECT id, name
+			FROM   reporter.report_folder
+			WHERE  owner = src_usr
+		LOOP
+			suffix := ' (' || src_usr || ')';
+			LOOP
+				BEGIN
+					UPDATE  reporter.report_folder
+					SET     owner = dest_usr, name = name || suffix
+					WHERE   id = renamable_row.id;
+				EXCEPTION WHEN unique_violation THEN
+					suffix := suffix || ' ';
+					CONTINUE;
+				END;
+				EXIT;
+			END LOOP;
+		END LOOP;
+	EXCEPTION WHEN undefined_table THEN
+		-- do nothing
+	END;
+
+	BEGIN
+		UPDATE reporter.schedule SET runner = dest_usr WHERE runner = src_usr;
+	EXCEPTION WHEN undefined_table THEN
+		-- do nothing
+	END;
+
+	BEGIN
+		UPDATE reporter.template SET owner = dest_usr WHERE owner = src_usr;
+	EXCEPTION WHEN undefined_table THEN
+		-- do nothing
+	END;
+
+	-- Update with a rename to avoid collisions
+	BEGIN
+		FOR renamable_row in
+			SELECT id, name
+			FROM   reporter.template_folder
+			WHERE  owner = src_usr
+		LOOP
+			suffix := ' (' || src_usr || ')';
+			LOOP
+				BEGIN
+					UPDATE  reporter.template_folder
+					SET     owner = dest_usr, name = name || suffix
+					WHERE   id = renamable_row.id;
+				EXCEPTION WHEN unique_violation THEN
+					suffix := suffix || ' ';
+					CONTINUE;
+				END;
+				EXIT;
+			END LOOP;
+		END LOOP;
+	EXCEPTION WHEN undefined_table THEN
+	-- do nothing
+	END;
+
+	-- vandelay.*
+	-- Update with a rename to avoid collisions
+	FOR renamable_row in
+		SELECT id, name
+		FROM   vandelay.queue
+		WHERE  owner = src_usr
+	LOOP
+		suffix := ' (' || src_usr || ')';
+		LOOP
+			BEGIN
+				UPDATE  vandelay.queue
+				SET     owner = dest_usr, name = name || suffix
+				WHERE   id = renamable_row.id;
+			EXCEPTION WHEN unique_violation THEN
+				suffix := suffix || ' ';
+				CONTINUE;
+			END;
+			EXIT;
+		END LOOP;
+	END LOOP;
+
+    -- NULL-ify addresses last so other cleanup (e.g. circ anonymization)
+    -- can access the information before deletion.
+	UPDATE actor.usr SET
+		active = FALSE,
+		card = NULL,
+		mailing_address = NULL,
+		billing_address = NULL
+	WHERE id = src_usr;
+
+END;
+$$ LANGUAGE plpgsql;
+
+
+
+
+
+-- UNDO (minus user purge/merge changes)
+/*
+
+DROP VIEW auditor.acq_invoice_lifecycle;
+ALTER TABLE acq.invoice ADD COLUMN complete BOOLEAN NOT NULL DEFAULT FALSE;
+ALTER TABLE auditor.acq_invoice_history 
+    ADD COLUMN complete BOOLEAN NOT NULL DEFAULT FALSE;
+UPDATE acq.invoice SET complete = TRUE where close_date IS NOT NULL;
+UPDATE auditor.acq_invoice_history 
+    SET complete = TRUE where close_date IS NOT NULL;
+SET CONSTRAINTS ALL IMMEDIATE; -- or get pending triggers error.
+ALTER TABLE acq.invoice DROP COLUMN close_date, DROP COLUMN closed_by;
+ALTER TABLE auditor.acq_invoice_history
+    DROP COLUMN close_date, DROP COLUMN closed_by;
+SELECT auditor.update_auditors();
+
+*/
+
+
+SELECT evergreen.upgrade_deps_block_check('1129', :eg_version);
+
+INSERT into config.workstation_setting_type (name, grp, datatype, label)
+VALUES (
+    'eg.grid.admin.acq.cancel_reason', 'gui', 'object',
+    oils_i18n_gettext (
+        'eg.grid.admin.acq.cancel_reason',
+        'Grid Config: admin.acq.cancel_reason',
+        'cwst', 'label'
+    )
+), (
+    'eg.grid.admin.acq.claim_event_type', 'gui', 'object',
+    oils_i18n_gettext (
+    'eg.grid.admin.acq.claim_event_type',
+        'Grid Config: admin.acq.claim_event_type',
+        'cwst', 'label'
+    )
+), (
+    'eg.grid.admin.acq.claim_policy', 'gui', 'object',
+    oils_i18n_gettext (
+    'eg.grid.admin.acq.claim_policy',
+        'Grid Config: admin.acq.claim_policy',
+        'cwst', 'label'
+    )
+), (
+    'eg.grid.admin.acq.claim_policy_action', 'gui', 'object',
+    oils_i18n_gettext (
+    'eg.grid.admin.acq.claim_policy_action',
+        'Grid Config: admin.acq.claim_policy_action',
+        'cwst', 'label'
+    )
+), (
+    'eg.grid.admin.acq.claim_type', 'gui', 'object',
+    oils_i18n_gettext (
+    'eg.grid.admin.acq.claim_type',
+        'Grid Config: admin.acq.claim_type',
+        'cwst', 'label'
+    )
+), (
+    'eg.grid.admin.acq.currency_type', 'gui', 'object',
+    oils_i18n_gettext (
+    'eg.grid.admin.acq.currency_type',
+        'Grid Config: admin.acq.currency_type',
+        'cwst', 'label'
+    )
+), (
+    'eg.grid.admin.acq.edi_account', 'gui', 'object',
+    oils_i18n_gettext (
+    'eg.grid.admin.acq.edi_account',
+        'Grid Config: admin.acq.edi_account',
+        'cwst', 'label'
+    )
+), (
+    'eg.grid.admin.acq.edi_message', 'gui', 'object',
+    oils_i18n_gettext (
+    'eg.grid.admin.acq.edi_message',
+        'Grid Config: admin.acq.edi_message',
+        'cwst', 'label'
+    )
+), (
+    'eg.grid.admin.acq.exchange_rate', 'gui', 'object',
+    oils_i18n_gettext (
+    'eg.grid.admin.acq.exchange_rate',
+        'Grid Config: admin.acq.exchange_rate',
+        'cwst', 'label'
+    )
+), (
+    'eg.grid.admin.acq.fund_tag', 'gui', 'object',
+    oils_i18n_gettext (
+    'eg.grid.admin.acq.fund_tag',
+        'Grid Config: admin.acq.fund_tag',
+        'cwst', 'label'
+    )
+), (
+    'eg.grid.admin.acq.invoice_item_type', 'gui', 'object',
+    oils_i18n_gettext (
+    'eg.grid.admin.acq.invoice_item_type',
+        'Grid Config: admin.acq.invoice_item_type',
+        'cwst', 'label'
+    )
+), (
+    'eg.grid.admin.acq.invoice_payment_method', 'gui', 'object',
+    oils_i18n_gettext (
+    'eg.grid.admin.acq.invoice_payment_method',
+        'Grid Config: admin.acq.invoice_payment_method',
+        'cwst', 'label'
+    )
+), (
+    'eg.grid.admin.acq.lineitem_alert_text', 'gui', 'object',
+    oils_i18n_gettext (
+    'eg.grid.admin.acq.lineitem_alert_text',
+        'Grid Config: admin.acq.lineitem_alert_text',
+        'cwst', 'label'
+    )
+), (
+    'eg.grid.admin.acq.lineitem_marc_attr_definition', 'gui', 'object',
+    oils_i18n_gettext (
+    'eg.grid.admin.acq.lineitem_marc_attr_definition',
+        'Grid Config: admin.acq.lineitem_marc_attr_definition',
+        'cwst', 'label'
+    )
+);
+
+
+SELECT evergreen.upgrade_deps_block_check('1130', :eg_version);
+
+CREATE OR REPLACE FUNCTION actor.usr_merge( src_usr INT, dest_usr INT, del_addrs BOOLEAN, del_cards BOOLEAN, deactivate_cards BOOLEAN ) RETURNS VOID AS $$
+DECLARE
+	suffix TEXT;
+	bucket_row RECORD;
+	picklist_row RECORD;
+	queue_row RECORD;
+	folder_row RECORD;
+BEGIN
+
+    -- Bail if src_usr equals dest_usr because the result of merging a
+    -- user with itself is not what you want.
+    IF src_usr = dest_usr THEN
+        RETURN;
+    END IF;
+
+    -- do some initial cleanup 
+    UPDATE actor.usr SET card = NULL WHERE id = src_usr;
+    UPDATE actor.usr SET mailing_address = NULL WHERE id = src_usr;
+    UPDATE actor.usr SET billing_address = NULL WHERE id = src_usr;
+
+    -- actor.*
+    IF del_cards THEN
+        DELETE FROM actor.card where usr = src_usr;
+    ELSE
+        IF deactivate_cards THEN
+            UPDATE actor.card SET active = 'f' WHERE usr = src_usr;
+        END IF;
+        UPDATE actor.card SET usr = dest_usr WHERE usr = src_usr;
+    END IF;
+
+
+    IF del_addrs THEN
+        DELETE FROM actor.usr_address WHERE usr = src_usr;
+    ELSE
+        UPDATE actor.usr_address SET usr = dest_usr WHERE usr = src_usr;
+    END IF;
+
+    UPDATE actor.usr_note SET usr = dest_usr WHERE usr = src_usr;
+    -- dupes are technically OK in actor.usr_standing_penalty, should manually delete them...
+    UPDATE actor.usr_standing_penalty SET usr = dest_usr WHERE usr = src_usr;
+    PERFORM actor.usr_merge_rows('actor.usr_org_unit_opt_in', 'usr', src_usr, dest_usr);
+    PERFORM actor.usr_merge_rows('actor.usr_setting', 'usr', src_usr, dest_usr);
+
+    -- permission.*
+    PERFORM actor.usr_merge_rows('permission.usr_perm_map', 'usr', src_usr, dest_usr);
+    PERFORM actor.usr_merge_rows('permission.usr_object_perm_map', 'usr', src_usr, dest_usr);
+    PERFORM actor.usr_merge_rows('permission.usr_grp_map', 'usr', src_usr, dest_usr);
+    PERFORM actor.usr_merge_rows('permission.usr_work_ou_map', 'usr', src_usr, dest_usr);
+
+
+    -- container.*
+	
+	-- For each *_bucket table: transfer every bucket belonging to src_usr
+	-- into the custody of dest_usr.
+	--
+	-- In order to avoid colliding with an existing bucket owned by
+	-- the destination user, append the source user's id (in parenthesese)
+	-- to the name.  If you still get a collision, add successive
+	-- spaces to the name and keep trying until you succeed.
+	--
+	FOR bucket_row in
+		SELECT id, name
+		FROM   container.biblio_record_entry_bucket
+		WHERE  owner = src_usr
+	LOOP
+		suffix := ' (' || src_usr || ')';
+		LOOP
+			BEGIN
+				UPDATE  container.biblio_record_entry_bucket
+				SET     owner = dest_usr, name = name || suffix
+				WHERE   id = bucket_row.id;
+			EXCEPTION WHEN unique_violation THEN
+				suffix := suffix || ' ';
+				CONTINUE;
+			END;
+			EXIT;
+		END LOOP;
+	END LOOP;
+
+	FOR bucket_row in
+		SELECT id, name
+		FROM   container.call_number_bucket
+		WHERE  owner = src_usr
+	LOOP
+		suffix := ' (' || src_usr || ')';
+		LOOP
+			BEGIN
+				UPDATE  container.call_number_bucket
+				SET     owner = dest_usr, name = name || suffix
+				WHERE   id = bucket_row.id;
+			EXCEPTION WHEN unique_violation THEN
+				suffix := suffix || ' ';
+				CONTINUE;
+			END;
+			EXIT;
+		END LOOP;
+	END LOOP;
+
+	FOR bucket_row in
+		SELECT id, name
+		FROM   container.copy_bucket
+		WHERE  owner = src_usr
+	LOOP
+		suffix := ' (' || src_usr || ')';
+		LOOP
+			BEGIN
+				UPDATE  container.copy_bucket
+				SET     owner = dest_usr, name = name || suffix
+				WHERE   id = bucket_row.id;
+			EXCEPTION WHEN unique_violation THEN
+				suffix := suffix || ' ';
+				CONTINUE;
+			END;
+			EXIT;
+		END LOOP;
+	END LOOP;
+
+	FOR bucket_row in
+		SELECT id, name
+		FROM   container.user_bucket
+		WHERE  owner = src_usr
+	LOOP
+		suffix := ' (' || src_usr || ')';
+		LOOP
+			BEGIN
+				UPDATE  container.user_bucket
+				SET     owner = dest_usr, name = name || suffix
+				WHERE   id = bucket_row.id;
+			EXCEPTION WHEN unique_violation THEN
+				suffix := suffix || ' ';
+				CONTINUE;
+			END;
+			EXIT;
+		END LOOP;
+	END LOOP;
+
+	UPDATE container.user_bucket_item SET target_user = dest_usr WHERE target_user = src_usr;
+
+    -- vandelay.*
+	-- transfer queues the same way we transfer buckets (see above)
+	FOR queue_row in
+		SELECT id, name
+		FROM   vandelay.queue
+		WHERE  owner = src_usr
+	LOOP
+		suffix := ' (' || src_usr || ')';
+		LOOP
+			BEGIN
+				UPDATE  vandelay.queue
+				SET     owner = dest_usr, name = name || suffix
+				WHERE   id = queue_row.id;
+			EXCEPTION WHEN unique_violation THEN
+				suffix := suffix || ' ';
+				CONTINUE;
+			END;
+			EXIT;
+		END LOOP;
+	END LOOP;
+
+    UPDATE vandelay.session_tracker SET usr = dest_usr WHERE usr = src_usr;
+
+    -- money.*
+    PERFORM actor.usr_merge_rows('money.collections_tracker', 'usr', src_usr, dest_usr);
+    PERFORM actor.usr_merge_rows('money.collections_tracker', 'collector', src_usr, dest_usr);
+    UPDATE money.billable_xact SET usr = dest_usr WHERE usr = src_usr;
+    UPDATE money.billing SET voider = dest_usr WHERE voider = src_usr;
+    UPDATE money.bnm_payment SET accepting_usr = dest_usr WHERE accepting_usr = src_usr;
+
+    -- action.*
+    UPDATE action.circulation SET usr = dest_usr WHERE usr = src_usr;
+    UPDATE action.circulation SET circ_staff = dest_usr WHERE circ_staff = src_usr;
+    UPDATE action.circulation SET checkin_staff = dest_usr WHERE checkin_staff = src_usr;
+    UPDATE action.usr_circ_history SET usr = dest_usr WHERE usr = src_usr;
+
+    UPDATE action.hold_request SET usr = dest_usr WHERE usr = src_usr;
+    UPDATE action.hold_request SET fulfillment_staff = dest_usr WHERE fulfillment_staff = src_usr;
+    UPDATE action.hold_request SET requestor = dest_usr WHERE requestor = src_usr;
+    UPDATE action.hold_notification SET notify_staff = dest_usr WHERE notify_staff = src_usr;
+
+    UPDATE action.in_house_use SET staff = dest_usr WHERE staff = src_usr;
+    UPDATE action.non_cataloged_circulation SET staff = dest_usr WHERE staff = src_usr;
+    UPDATE action.non_cataloged_circulation SET patron = dest_usr WHERE patron = src_usr;
+    UPDATE action.non_cat_in_house_use SET staff = dest_usr WHERE staff = src_usr;
+    UPDATE action.survey_response SET usr = dest_usr WHERE usr = src_usr;
+
+    -- acq.*
+    UPDATE acq.fund_allocation SET allocator = dest_usr WHERE allocator = src_usr;
+	UPDATE acq.fund_transfer SET transfer_user = dest_usr WHERE transfer_user = src_usr;
+    UPDATE acq.invoice SET closed_by = dest_usr WHERE closed_by = src_usr;
+
+	-- transfer picklists the same way we transfer buckets (see above)
+	FOR picklist_row in
+		SELECT id, name
+		FROM   acq.picklist
+		WHERE  owner = src_usr
+	LOOP
+		suffix := ' (' || src_usr || ')';
+		LOOP
+			BEGIN
+				UPDATE  acq.picklist
+				SET     owner = dest_usr, name = name || suffix
+				WHERE   id = picklist_row.id;
+			EXCEPTION WHEN unique_violation THEN
+				suffix := suffix || ' ';
+				CONTINUE;
+			END;
+			EXIT;
+		END LOOP;
+	END LOOP;
+
+    UPDATE acq.purchase_order SET owner = dest_usr WHERE owner = src_usr;
+    UPDATE acq.po_note SET creator = dest_usr WHERE creator = src_usr;
+    UPDATE acq.po_note SET editor = dest_usr WHERE editor = src_usr;
+    UPDATE acq.provider_note SET creator = dest_usr WHERE creator = src_usr;
+    UPDATE acq.provider_note SET editor = dest_usr WHERE editor = src_usr;
+    UPDATE acq.lineitem_note SET creator = dest_usr WHERE creator = src_usr;
+    UPDATE acq.lineitem_note SET editor = dest_usr WHERE editor = src_usr;
+    UPDATE acq.lineitem_usr_attr_definition SET usr = dest_usr WHERE usr = src_usr;
+
+    -- asset.*
+    UPDATE asset.copy SET creator = dest_usr WHERE creator = src_usr;
+    UPDATE asset.copy SET editor = dest_usr WHERE editor = src_usr;
+    UPDATE asset.copy_note SET creator = dest_usr WHERE creator = src_usr;
+    UPDATE asset.call_number SET creator = dest_usr WHERE creator = src_usr;
+    UPDATE asset.call_number SET editor = dest_usr WHERE editor = src_usr;
+    UPDATE asset.call_number_note SET creator = dest_usr WHERE creator = src_usr;
+
+    -- serial.*
+    UPDATE serial.record_entry SET creator = dest_usr WHERE creator = src_usr;
+    UPDATE serial.record_entry SET editor = dest_usr WHERE editor = src_usr;
+
+    -- reporter.*
+    -- It's not uncommon to define the reporter schema in a replica 
+    -- DB only, so don't assume these tables exist in the write DB.
+    BEGIN
+    	UPDATE reporter.template SET owner = dest_usr WHERE owner = src_usr;
+    EXCEPTION WHEN undefined_table THEN
+        -- do nothing
+    END;
+    BEGIN
+    	UPDATE reporter.report SET owner = dest_usr WHERE owner = src_usr;
+    EXCEPTION WHEN undefined_table THEN
+        -- do nothing
+    END;
+    BEGIN
+    	UPDATE reporter.schedule SET runner = dest_usr WHERE runner = src_usr;
+    EXCEPTION WHEN undefined_table THEN
+        -- do nothing
+    END;
+    BEGIN
+		-- transfer folders the same way we transfer buckets (see above)
+		FOR folder_row in
+			SELECT id, name
+			FROM   reporter.template_folder
+			WHERE  owner = src_usr
+		LOOP
+			suffix := ' (' || src_usr || ')';
+			LOOP
+				BEGIN
+					UPDATE  reporter.template_folder
+					SET     owner = dest_usr, name = name || suffix
+					WHERE   id = folder_row.id;
+				EXCEPTION WHEN unique_violation THEN
+					suffix := suffix || ' ';
+					CONTINUE;
+				END;
+				EXIT;
+			END LOOP;
+		END LOOP;
+    EXCEPTION WHEN undefined_table THEN
+        -- do nothing
+    END;
+    BEGIN
+		-- transfer folders the same way we transfer buckets (see above)
+		FOR folder_row in
+			SELECT id, name
+			FROM   reporter.report_folder
+			WHERE  owner = src_usr
+		LOOP
+			suffix := ' (' || src_usr || ')';
+			LOOP
+				BEGIN
+					UPDATE  reporter.report_folder
+					SET     owner = dest_usr, name = name || suffix
+					WHERE   id = folder_row.id;
+				EXCEPTION WHEN unique_violation THEN
+					suffix := suffix || ' ';
+					CONTINUE;
+				END;
+				EXIT;
+			END LOOP;
+		END LOOP;
+    EXCEPTION WHEN undefined_table THEN
+        -- do nothing
+    END;
+    BEGIN
+		-- transfer folders the same way we transfer buckets (see above)
+		FOR folder_row in
+			SELECT id, name
+			FROM   reporter.output_folder
+			WHERE  owner = src_usr
+		LOOP
+			suffix := ' (' || src_usr || ')';
+			LOOP
+				BEGIN
+					UPDATE  reporter.output_folder
+					SET     owner = dest_usr, name = name || suffix
+					WHERE   id = folder_row.id;
+				EXCEPTION WHEN unique_violation THEN
+					suffix := suffix || ' ';
+					CONTINUE;
+				END;
+				EXIT;
+			END LOOP;
+		END LOOP;
+    EXCEPTION WHEN undefined_table THEN
+        -- do nothing
+    END;
+
+    -- propagate preferred name values from the source user to the
+    -- destination user, but only when values are not being replaced.
+    WITH susr AS (SELECT * FROM actor.usr WHERE id = src_usr)
+    UPDATE actor.usr SET 
+        pref_prefix = 
+            COALESCE(pref_prefix, (SELECT pref_prefix FROM susr)),
+        pref_first_given_name = 
+            COALESCE(pref_first_given_name, (SELECT pref_first_given_name FROM susr)),
+        pref_second_given_name = 
+            COALESCE(pref_second_given_name, (SELECT pref_second_given_name FROM susr)),
+        pref_family_name = 
+            COALESCE(pref_family_name, (SELECT pref_family_name FROM susr)),
+        pref_suffix = 
+            COALESCE(pref_suffix, (SELECT pref_suffix FROM susr))
+    WHERE id = dest_usr;
+
+    -- Copy and deduplicate name keywords
+    -- String -> array -> rows -> DISTINCT -> array -> string
+    WITH susr AS (SELECT * FROM actor.usr WHERE id = src_usr),
+         dusr AS (SELECT * FROM actor.usr WHERE id = dest_usr)
+    UPDATE actor.usr SET name_keywords = (
+        WITH keywords AS (
+            SELECT DISTINCT UNNEST(
+                REGEXP_SPLIT_TO_ARRAY(
+                    COALESCE((SELECT name_keywords FROM susr), '') || ' ' ||
+                    COALESCE((SELECT name_keywords FROM dusr), ''),  E'\\s+'
+                )
+            ) AS parts
+        ) SELECT ARRAY_TO_STRING(ARRAY_AGG(kw.parts), ' ') FROM keywords kw
+    ) WHERE id = dest_usr;
+
+    -- Finally, delete the source user
+    DELETE FROM actor.usr WHERE id = src_usr;
+
+END;
+$$ LANGUAGE plpgsql;
+
+
+SELECT evergreen.upgrade_deps_block_check('1131', :eg_version);
+
+CREATE OR REPLACE FUNCTION actor.usr_merge( src_usr INT, dest_usr INT, del_addrs BOOLEAN, del_cards BOOLEAN, deactivate_cards BOOLEAN ) RETURNS VOID AS $$
+DECLARE
+	suffix TEXT;
+	bucket_row RECORD;
+	picklist_row RECORD;
+	queue_row RECORD;
+	folder_row RECORD;
+BEGIN
+
+    -- Bail if src_usr equals dest_usr because the result of merging a
+    -- user with itself is not what you want.
+    IF src_usr = dest_usr THEN
+        RETURN;
+    END IF;
+
+    -- do some initial cleanup 
+    UPDATE actor.usr SET card = NULL WHERE id = src_usr;
+    UPDATE actor.usr SET mailing_address = NULL WHERE id = src_usr;
+    UPDATE actor.usr SET billing_address = NULL WHERE id = src_usr;
+
+    -- actor.*
+    IF del_cards THEN
+        DELETE FROM actor.card where usr = src_usr;
+    ELSE
+        IF deactivate_cards THEN
+            UPDATE actor.card SET active = 'f' WHERE usr = src_usr;
+        END IF;
+        UPDATE actor.card SET usr = dest_usr WHERE usr = src_usr;
+    END IF;
+
+
+    IF del_addrs THEN
+        DELETE FROM actor.usr_address WHERE usr = src_usr;
+    ELSE
+        UPDATE actor.usr_address SET usr = dest_usr WHERE usr = src_usr;
+    END IF;
+
+    UPDATE actor.usr_note SET usr = dest_usr WHERE usr = src_usr;
+    -- dupes are technically OK in actor.usr_standing_penalty, should manually delete them...
+    UPDATE actor.usr_standing_penalty SET usr = dest_usr WHERE usr = src_usr;
+    PERFORM actor.usr_merge_rows('actor.usr_org_unit_opt_in', 'usr', src_usr, dest_usr);
+    PERFORM actor.usr_merge_rows('actor.usr_setting', 'usr', src_usr, dest_usr);
+
+    -- permission.*
+    PERFORM actor.usr_merge_rows('permission.usr_perm_map', 'usr', src_usr, dest_usr);
+    PERFORM actor.usr_merge_rows('permission.usr_object_perm_map', 'usr', src_usr, dest_usr);
+    PERFORM actor.usr_merge_rows('permission.usr_grp_map', 'usr', src_usr, dest_usr);
+    PERFORM actor.usr_merge_rows('permission.usr_work_ou_map', 'usr', src_usr, dest_usr);
+
+
+    -- container.*
+	
+	-- For each *_bucket table: transfer every bucket belonging to src_usr
+	-- into the custody of dest_usr.
+	--
+	-- In order to avoid colliding with an existing bucket owned by
+	-- the destination user, append the source user's id (in parenthesese)
+	-- to the name.  If you still get a collision, add successive
+	-- spaces to the name and keep trying until you succeed.
+	--
+	FOR bucket_row in
+		SELECT id, name
+		FROM   container.biblio_record_entry_bucket
+		WHERE  owner = src_usr
+	LOOP
+		suffix := ' (' || src_usr || ')';
+		LOOP
+			BEGIN
+				UPDATE  container.biblio_record_entry_bucket
+				SET     owner = dest_usr, name = name || suffix
+				WHERE   id = bucket_row.id;
+			EXCEPTION WHEN unique_violation THEN
+				suffix := suffix || ' ';
+				CONTINUE;
+			END;
+			EXIT;
+		END LOOP;
+	END LOOP;
+
+	FOR bucket_row in
+		SELECT id, name
+		FROM   container.call_number_bucket
+		WHERE  owner = src_usr
+	LOOP
+		suffix := ' (' || src_usr || ')';
+		LOOP
+			BEGIN
+				UPDATE  container.call_number_bucket
+				SET     owner = dest_usr, name = name || suffix
+				WHERE   id = bucket_row.id;
+			EXCEPTION WHEN unique_violation THEN
+				suffix := suffix || ' ';
+				CONTINUE;
+			END;
+			EXIT;
+		END LOOP;
+	END LOOP;
+
+	FOR bucket_row in
+		SELECT id, name
+		FROM   container.copy_bucket
+		WHERE  owner = src_usr
+	LOOP
+		suffix := ' (' || src_usr || ')';
+		LOOP
+			BEGIN
+				UPDATE  container.copy_bucket
+				SET     owner = dest_usr, name = name || suffix
+				WHERE   id = bucket_row.id;
+			EXCEPTION WHEN unique_violation THEN
+				suffix := suffix || ' ';
+				CONTINUE;
+			END;
+			EXIT;
+		END LOOP;
+	END LOOP;
+
+	FOR bucket_row in
+		SELECT id, name
+		FROM   container.user_bucket
+		WHERE  owner = src_usr
+	LOOP
+		suffix := ' (' || src_usr || ')';
+		LOOP
+			BEGIN
+				UPDATE  container.user_bucket
+				SET     owner = dest_usr, name = name || suffix
+				WHERE   id = bucket_row.id;
+			EXCEPTION WHEN unique_violation THEN
+				suffix := suffix || ' ';
+				CONTINUE;
+			END;
+			EXIT;
+		END LOOP;
+	END LOOP;
+
+	UPDATE container.user_bucket_item SET target_user = dest_usr WHERE target_user = src_usr;
+
+    -- vandelay.*
+	-- transfer queues the same way we transfer buckets (see above)
+	FOR queue_row in
+		SELECT id, name
+		FROM   vandelay.queue
+		WHERE  owner = src_usr
+	LOOP
+		suffix := ' (' || src_usr || ')';
+		LOOP
+			BEGIN
+				UPDATE  vandelay.queue
+				SET     owner = dest_usr, name = name || suffix
+				WHERE   id = queue_row.id;
+			EXCEPTION WHEN unique_violation THEN
+				suffix := suffix || ' ';
+				CONTINUE;
+			END;
+			EXIT;
+		END LOOP;
+	END LOOP;
+
+    UPDATE vandelay.session_tracker SET usr = dest_usr WHERE usr = src_usr;
+
+    -- money.*
+    PERFORM actor.usr_merge_rows('money.collections_tracker', 'usr', src_usr, dest_usr);
+    PERFORM actor.usr_merge_rows('money.collections_tracker', 'collector', src_usr, dest_usr);
+    UPDATE money.billable_xact SET usr = dest_usr WHERE usr = src_usr;
+    UPDATE money.billing SET voider = dest_usr WHERE voider = src_usr;
+    UPDATE money.bnm_payment SET accepting_usr = dest_usr WHERE accepting_usr = src_usr;
+
+    -- action.*
+    UPDATE action.circulation SET usr = dest_usr WHERE usr = src_usr;
+    UPDATE action.circulation SET circ_staff = dest_usr WHERE circ_staff = src_usr;
+    UPDATE action.circulation SET checkin_staff = dest_usr WHERE checkin_staff = src_usr;
+    UPDATE action.usr_circ_history SET usr = dest_usr WHERE usr = src_usr;
+
+    UPDATE action.hold_request SET usr = dest_usr WHERE usr = src_usr;
+    UPDATE action.hold_request SET fulfillment_staff = dest_usr WHERE fulfillment_staff = src_usr;
+    UPDATE action.hold_request SET requestor = dest_usr WHERE requestor = src_usr;
+    UPDATE action.hold_notification SET notify_staff = dest_usr WHERE notify_staff = src_usr;
+
+    UPDATE action.in_house_use SET staff = dest_usr WHERE staff = src_usr;
+    UPDATE action.non_cataloged_circulation SET staff = dest_usr WHERE staff = src_usr;
+    UPDATE action.non_cataloged_circulation SET patron = dest_usr WHERE patron = src_usr;
+    UPDATE action.non_cat_in_house_use SET staff = dest_usr WHERE staff = src_usr;
+    UPDATE action.survey_response SET usr = dest_usr WHERE usr = src_usr;
+
+    -- acq.*
+    UPDATE acq.fund_allocation SET allocator = dest_usr WHERE allocator = src_usr;
+	UPDATE acq.fund_transfer SET transfer_user = dest_usr WHERE transfer_user = src_usr;
+    UPDATE acq.invoice SET closed_by = dest_usr WHERE closed_by = src_usr;
+
+	-- transfer picklists the same way we transfer buckets (see above)
+	FOR picklist_row in
+		SELECT id, name
+		FROM   acq.picklist
+		WHERE  owner = src_usr
+	LOOP
+		suffix := ' (' || src_usr || ')';
+		LOOP
+			BEGIN
+				UPDATE  acq.picklist
+				SET     owner = dest_usr, name = name || suffix
+				WHERE   id = picklist_row.id;
+			EXCEPTION WHEN unique_violation THEN
+				suffix := suffix || ' ';
+				CONTINUE;
+			END;
+			EXIT;
+		END LOOP;
+	END LOOP;
+
+    UPDATE acq.purchase_order SET owner = dest_usr WHERE owner = src_usr;
+    UPDATE acq.po_note SET creator = dest_usr WHERE creator = src_usr;
+    UPDATE acq.po_note SET editor = dest_usr WHERE editor = src_usr;
+    UPDATE acq.provider_note SET creator = dest_usr WHERE creator = src_usr;
+    UPDATE acq.provider_note SET editor = dest_usr WHERE editor = src_usr;
+    UPDATE acq.lineitem_note SET creator = dest_usr WHERE creator = src_usr;
+    UPDATE acq.lineitem_note SET editor = dest_usr WHERE editor = src_usr;
+    UPDATE acq.lineitem_usr_attr_definition SET usr = dest_usr WHERE usr = src_usr;
+
+    -- asset.*
+    UPDATE asset.copy SET creator = dest_usr WHERE creator = src_usr;
+    UPDATE asset.copy SET editor = dest_usr WHERE editor = src_usr;
+    UPDATE asset.copy_note SET creator = dest_usr WHERE creator = src_usr;
+    UPDATE asset.call_number SET creator = dest_usr WHERE creator = src_usr;
+    UPDATE asset.call_number SET editor = dest_usr WHERE editor = src_usr;
+    UPDATE asset.call_number_note SET creator = dest_usr WHERE creator = src_usr;
+
+    -- serial.*
+    UPDATE serial.record_entry SET creator = dest_usr WHERE creator = src_usr;
+    UPDATE serial.record_entry SET editor = dest_usr WHERE editor = src_usr;
+
+    -- reporter.*
+    -- It's not uncommon to define the reporter schema in a replica 
+    -- DB only, so don't assume these tables exist in the write DB.
+    BEGIN
+    	UPDATE reporter.template SET owner = dest_usr WHERE owner = src_usr;
+    EXCEPTION WHEN undefined_table THEN
+        -- do nothing
+    END;
+    BEGIN
+    	UPDATE reporter.report SET owner = dest_usr WHERE owner = src_usr;
+    EXCEPTION WHEN undefined_table THEN
+        -- do nothing
+    END;
+    BEGIN
+    	UPDATE reporter.schedule SET runner = dest_usr WHERE runner = src_usr;
+    EXCEPTION WHEN undefined_table THEN
+        -- do nothing
+    END;
+    BEGIN
+		-- transfer folders the same way we transfer buckets (see above)
+		FOR folder_row in
+			SELECT id, name
+			FROM   reporter.template_folder
+			WHERE  owner = src_usr
+		LOOP
+			suffix := ' (' || src_usr || ')';
+			LOOP
+				BEGIN
+					UPDATE  reporter.template_folder
+					SET     owner = dest_usr, name = name || suffix
+					WHERE   id = folder_row.id;
+				EXCEPTION WHEN unique_violation THEN
+					suffix := suffix || ' ';
+					CONTINUE;
+				END;
+				EXIT;
+			END LOOP;
+		END LOOP;
+    EXCEPTION WHEN undefined_table THEN
+        -- do nothing
+    END;
+    BEGIN
+		-- transfer folders the same way we transfer buckets (see above)
+		FOR folder_row in
+			SELECT id, name
+			FROM   reporter.report_folder
+			WHERE  owner = src_usr
+		LOOP
+			suffix := ' (' || src_usr || ')';
+			LOOP
+				BEGIN
+					UPDATE  reporter.report_folder
+					SET     owner = dest_usr, name = name || suffix
+					WHERE   id = folder_row.id;
+				EXCEPTION WHEN unique_violation THEN
+					suffix := suffix || ' ';
+					CONTINUE;
+				END;
+				EXIT;
+			END LOOP;
+		END LOOP;
+    EXCEPTION WHEN undefined_table THEN
+        -- do nothing
+    END;
+    BEGIN
+		-- transfer folders the same way we transfer buckets (see above)
+		FOR folder_row in
+			SELECT id, name
+			FROM   reporter.output_folder
+			WHERE  owner = src_usr
+		LOOP
+			suffix := ' (' || src_usr || ')';
+			LOOP
+				BEGIN
+					UPDATE  reporter.output_folder
+					SET     owner = dest_usr, name = name || suffix
+					WHERE   id = folder_row.id;
+				EXCEPTION WHEN unique_violation THEN
+					suffix := suffix || ' ';
+					CONTINUE;
+				END;
+				EXIT;
+			END LOOP;
+		END LOOP;
+    EXCEPTION WHEN undefined_table THEN
+        -- do nothing
+    END;
+
+    -- propagate preferred name values from the source user to the
+    -- destination user, but only when values are not being replaced.
+    WITH susr AS (SELECT * FROM actor.usr WHERE id = src_usr)
+    UPDATE actor.usr SET 
+        pref_prefix = 
+            COALESCE(pref_prefix, (SELECT pref_prefix FROM susr)),
+        pref_first_given_name = 
+            COALESCE(pref_first_given_name, (SELECT pref_first_given_name FROM susr)),
+        pref_second_given_name = 
+            COALESCE(pref_second_given_name, (SELECT pref_second_given_name FROM susr)),
+        pref_family_name = 
+            COALESCE(pref_family_name, (SELECT pref_family_name FROM susr)),
+        pref_suffix = 
+            COALESCE(pref_suffix, (SELECT pref_suffix FROM susr))
+    WHERE id = dest_usr;
+
+    -- Copy and deduplicate name keywords
+    -- String -> array -> rows -> DISTINCT -> array -> string
+    WITH susr AS (SELECT * FROM actor.usr WHERE id = src_usr),
+         dusr AS (SELECT * FROM actor.usr WHERE id = dest_usr)
+    UPDATE actor.usr SET name_keywords = (
+        WITH keywords AS (
+            SELECT DISTINCT UNNEST(
+                REGEXP_SPLIT_TO_ARRAY(
+                    COALESCE((SELECT name_keywords FROM susr), '') || ' ' ||
+                    COALESCE((SELECT name_keywords FROM dusr), ''),  E'\\s+'
+                )
+            ) AS parts
+        ) SELECT ARRAY_TO_STRING(ARRAY_AGG(kw.parts), ' ') FROM keywords kw
+    ) WHERE id = dest_usr;
+
+    -- Finally, delete the source user
+    DELETE FROM actor.usr WHERE id = src_usr;
+
+END;
+$$ LANGUAGE plpgsql;
+
+
+-- check whether patch can be applied
+SELECT evergreen.upgrade_deps_block_check('1132', :eg_version); -- remingtron/csharp
+
+-- fix two typo/pasto's in setting descriptions
+UPDATE config.org_unit_setting_type
+SET description = oils_i18n_gettext(
+	'circ.copy_alerts.forgive_fines_on_long_overdue_checkin',
+	'Controls whether fines are automatically forgiven when checking out an '||
+	'item that has been marked as long-overdue, and the corresponding copy alert has been '||
+	'suppressed.',
+	'coust', 'description'
+)
+WHERE NAME = 'circ.copy_alerts.forgive_fines_on_long_overdue_checkin';
+
+UPDATE config.org_unit_setting_type
+SET description = oils_i18n_gettext(
+	'circ.longoverdue.xact_open_on_zero',
+	'Leave transaction open when long-overdue balance equals zero.  ' ||
+	'This leaves the long-overdue copy on the patron record when it is paid',
+	'coust', 'description'
+)
+WHERE NAME = 'circ.longoverdue.xact_open_on_zero';
+
+
+
+SELECT evergreen.upgrade_deps_block_check('1133', :eg_version);
+
+/* 
+Unique indexes are not inherited by child tables, so they will not prevent
+duplicate inserts on action.transit_copy and action.hold_transit_copy,
+for example.  Use check constraints instead to enforce unique-per-copy
+transits accross all transit types.
+*/
+
+-- Create an index for speedy check constraint lookups.
+CREATE INDEX active_transit_for_copy 
+    ON action.transit_copy (target_copy)
+    WHERE dest_recv_time IS NULL AND cancel_time IS NULL;
+
+-- Check for duplicate transits across all transit types
+CREATE OR REPLACE FUNCTION action.copy_transit_is_unique() 
+    RETURNS TRIGGER AS $func$
+BEGIN
+    PERFORM * FROM action.transit_copy 
+        WHERE target_copy = NEW.target_copy 
+              AND dest_recv_time IS NULL 
+              AND cancel_time IS NULL;
+    IF FOUND THEN
+        RAISE EXCEPTION 'Copy id=% is already in transit', NEW.target_copy;
+    END IF;
+    RETURN NULL;
+END;
+$func$ LANGUAGE PLPGSQL STABLE;
+
+-- Apply constraint to all transit tables
+CREATE CONSTRAINT TRIGGER transit_copy_is_unique_check
+    AFTER INSERT ON action.transit_copy
+    FOR EACH ROW EXECUTE PROCEDURE action.copy_transit_is_unique();
+
+CREATE CONSTRAINT TRIGGER hold_transit_copy_is_unique_check
+    AFTER INSERT ON action.hold_transit_copy
+    FOR EACH ROW EXECUTE PROCEDURE action.copy_transit_is_unique();
+
+CREATE CONSTRAINT TRIGGER reservation_transit_copy_is_unique_check
+    AFTER INSERT ON action.reservation_transit_copy
+    FOR EACH ROW EXECUTE PROCEDURE action.copy_transit_is_unique();
+
+/*
+-- UNDO
+DROP TRIGGER transit_copy_is_unique_check ON action.transit_copy;
+DROP TRIGGER hold_transit_copy_is_unique_check ON action.hold_transit_copy;
+DROP TRIGGER reservation_transit_copy_is_unique_check ON action.reservation_transit_copy;
+DROP INDEX action.active_transit_for_copy;
+*/
+
+
+COMMIT;
+
+\qecho A unique constraint was applied to action.transit_copy.  This will
+\qecho only effect newly created transits.  Admins are encouraged to manually 
+\qecho remove any existing duplicate transits by applying values for cancel_time
+\qecho or dest_recv_time, or by deleting the offending transits. Below is a
+\qecho query to locate duplicate transits.  Note dupes may exist accross
+\qecho parent (action.transit_copy) and child tables (action.hold_transit_copy,
+\qecho action.reservation_transit_copy)
+\qecho 
+\qecho    WITH dupe_transits AS (
+\qecho        SELECT COUNT(*), target_copy FROM action.transit_copy
+\qecho        WHERE dest_recv_time IS NULL AND cancel_time IS NULL
+\qecho        GROUP BY 2 HAVING COUNT(*) > 1
+\qecho    ) SELECT atc.* 
+\qecho        FROM dupe_transits
+\qecho        JOIN action.transit_copy atc USING (target_copy)
+\qecho        WHERE dest_recv_time IS NULL AND cancel_time IS NULL;
+\qecho

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

Summary of changes:
 .../Pg/version-upgrade/3.1.5-3.2.0-upgrade-db.sql  | 4904 ++++++++++++++++++++
 1 files changed, 4904 insertions(+), 0 deletions(-)
 create mode 100644 Open-ILS/src/sql/Pg/version-upgrade/3.1.5-3.2.0-upgrade-db.sql


hooks/post-receive
-- 
Evergreen ILS


More information about the open-ils-commits mailing list